Merge remote-tracking branch 'upstream/master' into develop
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Steam;
|
||||
using FarseerPhysics;
|
||||
@@ -15,6 +16,84 @@ namespace Barotrauma
|
||||
|
||||
static class AchievementManager
|
||||
{
|
||||
private static readonly ImmutableHashSet<Identifier> SupportedAchievements = ImmutableHashSet.Create(
|
||||
"killmoloch".ToIdentifier(),
|
||||
"killhammerhead".ToIdentifier(),
|
||||
"killendworm".ToIdentifier(),
|
||||
"artifactmission".ToIdentifier(),
|
||||
"combatmission1".ToIdentifier(),
|
||||
"combatmission2".ToIdentifier(),
|
||||
"healcrit".ToIdentifier(),
|
||||
"repairdevice".ToIdentifier(),
|
||||
"traitorwin".ToIdentifier(),
|
||||
"killtraitor".ToIdentifier(),
|
||||
"killclown".ToIdentifier(),
|
||||
"healopiateaddiction".ToIdentifier(),
|
||||
"survivecrushdepth".ToIdentifier(),
|
||||
"survivereactormeltdown".ToIdentifier(),
|
||||
"healhusk".ToIdentifier(),
|
||||
"killpoison".ToIdentifier(),
|
||||
"killnuke".ToIdentifier(),
|
||||
"killtool".ToIdentifier(),
|
||||
"clowncostume".ToIdentifier(),
|
||||
"lastmanstanding".ToIdentifier(),
|
||||
"lonesailor".ToIdentifier(),
|
||||
"subhighvelocity".ToIdentifier(),
|
||||
"nodamagerun".ToIdentifier(),
|
||||
"subdeep".ToIdentifier(),
|
||||
"maxintensity".ToIdentifier(),
|
||||
"discovercoldcaverns".ToIdentifier(),
|
||||
"discovereuropanridge".ToIdentifier(),
|
||||
"discoverhydrothermalwastes".ToIdentifier(),
|
||||
"discovertheaphoticplateau".ToIdentifier(),
|
||||
"discoverthegreatsea".ToIdentifier(),
|
||||
"travel10".ToIdentifier(),
|
||||
"travel100".ToIdentifier(),
|
||||
"xenocide".ToIdentifier(),
|
||||
"genocide".ToIdentifier(),
|
||||
"cargomission".ToIdentifier(),
|
||||
"subeditor24h".ToIdentifier(),
|
||||
"crewaway".ToIdentifier(),
|
||||
"captainround".ToIdentifier(),
|
||||
"securityofficerround".ToIdentifier(),
|
||||
"engineerround".ToIdentifier(),
|
||||
"mechanicround".ToIdentifier(),
|
||||
"medicaldoctorround".ToIdentifier(),
|
||||
"assistantround".ToIdentifier(),
|
||||
"campaigncompleted".ToIdentifier(),
|
||||
"salvagewreckmission".ToIdentifier(),
|
||||
"escortmission".ToIdentifier(),
|
||||
"killcharybdis".ToIdentifier(),
|
||||
"killlatcher".ToIdentifier(),
|
||||
"killspineling_giant".ToIdentifier(),
|
||||
"killcrawlerbroodmother".ToIdentifier(),
|
||||
"ascension".ToIdentifier(),
|
||||
"campaignmetadata_pathofthebikehorn_7".ToIdentifier(),
|
||||
"campaignmetadata_coalitionspecialhire1_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_coalitionspecialhire2_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_separatistspecialhire1_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_separatistspecialhire2_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_huskcultspecialhire1_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_clownspecialhire1_hired_true".ToIdentifier(),
|
||||
"scanruin".ToIdentifier(),
|
||||
"clearruin".ToIdentifier(),
|
||||
"beaconmission".ToIdentifier(),
|
||||
"abandonedoutpostrescue".ToIdentifier(),
|
||||
"abandonedoutpostassassinate".ToIdentifier(),
|
||||
"abandonedoutpostdestroyhumans".ToIdentifier(),
|
||||
"abandonedoutpostdestroymonsters".ToIdentifier(),
|
||||
"nestmission".ToIdentifier(),
|
||||
"miningmission".ToIdentifier(),
|
||||
"combatmissionseparatistsvscoalition".ToIdentifier(),
|
||||
"combatmissioncoalitionvsseparatists".ToIdentifier(),
|
||||
"getoutalive".ToIdentifier(),
|
||||
"abyssbeckons".ToIdentifier(),
|
||||
"europasfinest".ToIdentifier(),
|
||||
"kingofthehull".ToIdentifier(),
|
||||
"killmantis".ToIdentifier(),
|
||||
"ancientnovelty".ToIdentifier(),
|
||||
"whatsmirksbelow".ToIdentifier());
|
||||
|
||||
private const float UpdateInterval = 1.0f;
|
||||
|
||||
private static readonly HashSet<Identifier> unlockedAchievements = new HashSet<Identifier>();
|
||||
@@ -42,6 +121,29 @@ namespace Barotrauma
|
||||
private static PathFinder pathFinder;
|
||||
private static readonly Dictionary<Character, CachedDistance> cachedDistances = new Dictionary<Character, CachedDistance>();
|
||||
|
||||
static AchievementManager()
|
||||
{
|
||||
#if DEBUG
|
||||
if (SteamManager.IsInitialized && SteamManager.TryGetAllAvailableAchievements(out var achievements) && achievements.Any())
|
||||
{
|
||||
foreach (var achievement in achievements)
|
||||
{
|
||||
if (!SupportedAchievements.Contains(achievement.Identifier.ToIdentifier()))
|
||||
{
|
||||
DebugConsole.ThrowError($"Achievement \"{achievement.Identifier}\" is present on Steam's backend but not in achievements supported by {nameof(AchievementManager)}.");
|
||||
}
|
||||
}
|
||||
foreach (Identifier achievementId in SupportedAchievements)
|
||||
{
|
||||
if (achievements.None(a => a.Identifier.ToIdentifier() == achievementId))
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find achievement \"{achievementId}\" on Steam's backend.");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void OnStartRound(Biome biome = null)
|
||||
{
|
||||
roundData = new RoundData();
|
||||
@@ -584,6 +686,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (CheatsEnabled) { return; }
|
||||
if (Screen.Selected is { IsEditor: true }) { return; }
|
||||
if (!SupportedAchievements.Contains(identifier)) { return; }
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
|
||||
#endif
|
||||
|
||||
@@ -2628,7 +2628,7 @@ namespace Barotrauma
|
||||
float margin = MathHelper.PiOver4 * distanceFactor;
|
||||
if (angle < margin || dist < minDistance)
|
||||
{
|
||||
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
|
||||
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking;
|
||||
var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true);
|
||||
if (pickedBody != null)
|
||||
{
|
||||
@@ -2643,7 +2643,6 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Character t = null;
|
||||
if (pickedBody.UserData is Character c)
|
||||
{
|
||||
@@ -2657,6 +2656,16 @@ namespace Barotrauma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (pickedBody.UserData is Item item && item.Prefab.DamagedByProjectiles)
|
||||
{
|
||||
// Target behind an item -> allow shooting.
|
||||
return true;
|
||||
}
|
||||
if (pickedBody.UserData is Holdable holdable && holdable.Item.Prefab.DamagedByProjectiles)
|
||||
{
|
||||
// Target behind a blocking but destructible item -> allow shooting.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -2889,7 +2898,7 @@ namespace Barotrauma
|
||||
if (aiTarget.ShouldBeIgnored()) { continue; }
|
||||
if (ignoredTargets.Contains(aiTarget)) { continue; }
|
||||
if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
|
||||
if (!TargetOutposts && GameMain.GameSession.GameMode is not TestGameMode)
|
||||
if (!TargetOutposts && GameMain.GameSession?.GameMode is not TestGameMode)
|
||||
{
|
||||
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; }
|
||||
}
|
||||
@@ -3918,7 +3927,13 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters originally defined in the AI params and modified temporarily.
|
||||
/// </summary>
|
||||
private readonly Dictionary<Identifier, IEnumerable<CharacterParams.TargetParams>> modifiedParams = new Dictionary<Identifier, IEnumerable<CharacterParams.TargetParams>>();
|
||||
/// <summary>
|
||||
/// Parameters created temporarily. Not originally defined in the AI params at all.
|
||||
/// </summary>
|
||||
private readonly Dictionary<Identifier, CharacterParams.TargetParams> tempParams = new Dictionary<Identifier, CharacterParams.TargetParams>();
|
||||
private readonly List<CharacterParams.TargetParams> tempParamsList = new List<CharacterParams.TargetParams>();
|
||||
|
||||
@@ -3952,11 +3967,6 @@ namespace Barotrauma
|
||||
{
|
||||
if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out CharacterParams.TargetParams targetParams))
|
||||
{
|
||||
if (state == AIState.Attack)
|
||||
{
|
||||
// Only applies to new temp target params. Shouldn't affect any existing definitions (handled below).
|
||||
targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
|
||||
}
|
||||
tempParams.Add(tag, targetParams);
|
||||
}
|
||||
}
|
||||
@@ -3970,6 +3980,15 @@ namespace Barotrauma
|
||||
targetParams.Priority = Math.Max(targetParams.Priority, priority.Value);
|
||||
}
|
||||
targetParams.State = state;
|
||||
if (state == AIState.Attack)
|
||||
{
|
||||
targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
|
||||
targetParams.IgnoreInside = false;
|
||||
targetParams.IgnoreOutside = false;
|
||||
targetParams.IgnoreTargetInside = false;
|
||||
targetParams.IgnoreTargetOutside = false;
|
||||
targetParams.IgnoreIncapacitated = false;
|
||||
}
|
||||
}
|
||||
modifiedParams.TryAdd(tag, existingTargetParams);
|
||||
}
|
||||
|
||||
@@ -574,7 +574,7 @@ namespace Barotrauma
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Submarine != Character.Submarine) { continue; }
|
||||
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
|
||||
if (c.Removed || c.IsDead || c.IsIncapacitated || c.InDetectable) { continue; }
|
||||
if (IsFriendly(c)) { continue; }
|
||||
Vector2 toTarget = c.WorldPosition - WorldPosition;
|
||||
float dist = toTarget.LengthSquared();
|
||||
@@ -1045,7 +1045,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Character target in Character.CharacterList)
|
||||
{
|
||||
if (target.CurrentHull != hull || !target.Enabled) { continue; }
|
||||
if (target.CurrentHull != hull || !target.Enabled || target.InDetectable) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
|
||||
{
|
||||
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
@@ -1696,16 +1696,25 @@ namespace Barotrauma
|
||||
|
||||
public bool AllowCampaignInteraction()
|
||||
{
|
||||
if (Character == null || Character.Removed || Character.IsIncapacitated) { return false; }
|
||||
if (Character == null || Character.Removed) { return false; }
|
||||
|
||||
switch (ObjectiveManager.CurrentObjective)
|
||||
//some events might want to allow talking/examining characters that are incapacitated or in some "emergency" ai state,
|
||||
//so let's ignore those here
|
||||
var type = Character.CampaignInteractionType;
|
||||
if (type != CampaignMode.InteractionType.None &&
|
||||
type != CampaignMode.InteractionType.Talk &&
|
||||
type != CampaignMode.InteractionType.Examine)
|
||||
{
|
||||
case AIObjectiveCombat _:
|
||||
case AIObjectiveFindSafety _:
|
||||
case AIObjectiveExtinguishFires _:
|
||||
case AIObjectiveFightIntruders _:
|
||||
case AIObjectiveFixLeaks _:
|
||||
return false;
|
||||
if (Character.IsIncapacitated) { return false; }
|
||||
switch (ObjectiveManager.CurrentObjective)
|
||||
{
|
||||
case AIObjectiveCombat _:
|
||||
case AIObjectiveFindSafety _:
|
||||
case AIObjectiveExtinguishFires _:
|
||||
case AIObjectiveFightIntruders _:
|
||||
case AIObjectiveFixLeaks _:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -2272,7 +2281,7 @@ namespace Barotrauma
|
||||
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
|
||||
{
|
||||
if (other.IsHusk)
|
||||
if (other.IsHusk && !onlySameTeam)
|
||||
{
|
||||
// Disguised as husk
|
||||
return me.IsDisguisedAsHusk;
|
||||
@@ -2305,16 +2314,15 @@ namespace Barotrauma
|
||||
{
|
||||
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
|
||||
}
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode)
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode &&
|
||||
//ignore hostile faction if offering services that don't get disabled by faction hostility
|
||||
(me.CampaignInteractionType == CampaignMode.InteractionType.None || CampaignMode.HostileFactionDisablesInteraction(me.CampaignInteractionType)))
|
||||
{
|
||||
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
|
||||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
|
||||
{
|
||||
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
|
||||
|
||||
//NPCs that allow some campaign interaction are not turned hostile by low reputation
|
||||
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
|
||||
|
||||
if (npc.AIController is HumanAIController npcAI)
|
||||
{
|
||||
return !npcAI.IsInHostileFaction();
|
||||
@@ -2347,7 +2355,7 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
|
||||
public static bool IsActive(Character c) => c is { Enabled: true, IsUnconscious: false };
|
||||
|
||||
public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
|
||||
{
|
||||
@@ -2359,7 +2367,7 @@ namespace Barotrauma
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2468,11 +2476,11 @@ namespace Barotrauma
|
||||
operatingCharacter = c;
|
||||
return true;
|
||||
}
|
||||
if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager is AIObjectiveManager objectiveManager)
|
||||
if (c.AIController is HumanAIController { ObjectiveManager: AIObjectiveManager objectiveManager })
|
||||
{
|
||||
foreach (var objective in objectiveManager.Objectives)
|
||||
{
|
||||
if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; }
|
||||
if (objective is not AIObjectiveOperateItem operateObjective) { continue; }
|
||||
if (operateObjective.Component?.Item != target.Item) { continue; }
|
||||
if (operateObjective.Priority < highestPriority) { continue; }
|
||||
if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; }
|
||||
@@ -2485,136 +2493,6 @@ namespace Barotrauma
|
||||
return operatingCharacter != null;
|
||||
}
|
||||
|
||||
// There's some duplicate logic in the two methods below, but making them use the same code would require some changes in the target classes so that we could use exactly the same checks.
|
||||
// And even then there would be some differences that could end up being confusing (like the exception for steering).
|
||||
public bool IsItemOperatedByAnother(ItemComponent target, out Character other)
|
||||
{
|
||||
other = null;
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrder = IsOrderedToOperateTarget(this);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
if (c == Character) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (c.SelectedItem == target.Item)
|
||||
{
|
||||
// If the other character is player, don't try to operate
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToOperateTarget(otherAI);
|
||||
if (!isOrder && isTargetOrdered)
|
||||
{
|
||||
// If the other bot is ordered to operate the item, let him do it, unless we are ordered too
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOrder && !isTargetOrdered)
|
||||
{
|
||||
// We are ordered and the target is not -> allow to operate
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!IsOperatingTarget(otherAI))
|
||||
{
|
||||
// The other bot is doing something else -> stick to the target.
|
||||
continue;
|
||||
}
|
||||
if (target is Steering)
|
||||
{
|
||||
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
|
||||
if (Character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
|
||||
{
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c))
|
||||
{
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return other != null;
|
||||
bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
|
||||
bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
|
||||
}
|
||||
|
||||
public bool IsItemRepairedByAnother(Item target, out Character other)
|
||||
{
|
||||
other = null;
|
||||
if (Character == null) { return false; }
|
||||
if (target == null) { return false; }
|
||||
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
if (c == Character) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
other = c;
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (target.Repairables.Any(r => r.CurrentFixer == c))
|
||||
{
|
||||
// If the other character is player, don't try to repair
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController operatingAI)
|
||||
{
|
||||
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
|
||||
if (repairItemsObjective == null) { continue; }
|
||||
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToRepairThis(operatingAI);
|
||||
if (!isOrder && isTargetOrdered)
|
||||
{
|
||||
// If the other bot is ordered to repair the item, let him do it, unless we are ordered too
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOrder && !isTargetOrdered)
|
||||
{
|
||||
// We are ordered and the target is not -> allow to repair
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
|
||||
{
|
||||
// The other bot is ordered to do something else
|
||||
continue;
|
||||
}
|
||||
return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
|
||||
}
|
||||
|
||||
#region Wrappers
|
||||
public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
|
||||
public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
|
||||
|
||||
@@ -562,6 +562,7 @@ namespace Barotrauma
|
||||
bool buttonsFound = false;
|
||||
// Check wired controllers (e.g. buttons)
|
||||
// Always run the buttonFilter delegate (inside CanAccessButton method), if defined, because it's used for find a valid controller component that can be used for closing the door, when needed.
|
||||
// TODO: connectionFilter is ignored in the recursive searches, so it does nothing here.
|
||||
foreach (Controller button in door.Item.GetConnectedComponents<Controller>(recursive: true, connectionFilter: c => c.Name is "toggle" or "set_state"))
|
||||
{
|
||||
buttonsFound = true;
|
||||
@@ -727,12 +728,15 @@ namespace Barotrauma
|
||||
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
|
||||
//heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them
|
||||
if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; }
|
||||
if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
|
||||
if (closestButton == null || distance < closestDist)
|
||||
{
|
||||
closestButton = button;
|
||||
closestDist = distance;
|
||||
if (distance < MathUtils.Pow2(button.Item.InteractDistance + GetColliderLength()) && character.CanSeeTarget(button.Item))
|
||||
{
|
||||
closestButton = button;
|
||||
closestDist = distance;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return closestButton != null;
|
||||
});
|
||||
if (canAccess)
|
||||
{
|
||||
@@ -755,41 +759,19 @@ namespace Barotrauma
|
||||
}
|
||||
else if (closestButton != null)
|
||||
{
|
||||
if (closestDist < MathUtils.Pow2(closestButton.Item.InteractDistance + GetColliderLength()))
|
||||
if (pressButton)
|
||||
{
|
||||
if (pressButton)
|
||||
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
|
||||
{
|
||||
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
|
||||
{
|
||||
lastDoor = (door, shouldBeOpen);
|
||||
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonPressTimer = 0;
|
||||
}
|
||||
lastDoor = (door, shouldBeOpen);
|
||||
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can't reach the button closest to the character.
|
||||
// It's possible that we could reach another buttons.
|
||||
// If this becomes an issue, we could go through them here and check if any of them are reachable
|
||||
// (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
|
||||
var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
|
||||
if (body != null)
|
||||
else
|
||||
{
|
||||
if (body.UserData is Item item)
|
||||
{
|
||||
var d = item.GetComponent<Door>();
|
||||
if (d == null || d.IsOpen) { return; }
|
||||
}
|
||||
// The button is on the wrong side of the door or a wall
|
||||
currentPath.Unreachable = true;
|
||||
buttonPressTimer = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (shouldBeOpen)
|
||||
@@ -871,6 +853,16 @@ namespace Barotrauma
|
||||
{
|
||||
if (!CanAccessDoor(door, button =>
|
||||
{
|
||||
if (Vector2.DistanceSquared(door.Item.WorldPosition, button.Item.WorldPosition) > MathUtils.Pow2(button.Item.InteractDistance + GetColliderLength()))
|
||||
{
|
||||
// Too far from the door.
|
||||
return false;
|
||||
}
|
||||
if (!ISpatialEntity.IsTargetVisible(button.Item, door.Item))
|
||||
{
|
||||
// Obstructed.
|
||||
return false;
|
||||
}
|
||||
// Ignore buttons that are on the wrong side of the door, unless there's a motion sensor connected to the door, which can be triggered by the character.
|
||||
if (door.IsHorizontal)
|
||||
{
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (target.IsDead) { return false; }
|
||||
if (target.IsDead || target.InDetectable) { return false; }
|
||||
if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; }
|
||||
if (target == character) { return false; }
|
||||
if (target.Submarine == null) { return false; }
|
||||
|
||||
+5
-5
@@ -387,14 +387,14 @@ namespace Barotrauma
|
||||
chairCheckTimer -= deltaTime;
|
||||
if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null)
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
foreach (Item chair in Item.ChairItems)
|
||||
{
|
||||
if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; }
|
||||
if (chair.CurrentHull != currentHull) { continue; }
|
||||
//not possible in vanilla game, but a mod might have holdable/attachable chairs
|
||||
if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; }
|
||||
var controller = item.GetComponent<Controller>();
|
||||
if (chair.ParentInventory != null || chair.body is { Enabled: true }) { continue; }
|
||||
var controller = chair.GetComponent<Controller>();
|
||||
if (controller == null || controller.User != null) { continue; }
|
||||
item.TryInteract(character, forceSelectKey: true);
|
||||
chair.TryInteract(character, forceSelectKey: true);
|
||||
}
|
||||
chairCheckTimer = chairCheckInterval;
|
||||
}
|
||||
|
||||
+75
-11
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -15,7 +16,7 @@ namespace Barotrauma
|
||||
public override bool AllowMultipleInstances => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
|
||||
public override bool PrioritizeIfSubObjectivesActive => component is Reactor or Turret;
|
||||
|
||||
private readonly ItemComponent component, controller;
|
||||
private readonly Entity operateTarget;
|
||||
@@ -88,12 +89,12 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
var reactor = component?.Item.GetComponent<Reactor>();
|
||||
var reactor = component.Item.GetComponent<Reactor>();
|
||||
if (reactor != null)
|
||||
{
|
||||
if (!isOrder)
|
||||
{
|
||||
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
if (reactor.LastUserWasPlayer && character.IsOnPlayerTeam)
|
||||
{
|
||||
// The reactor was previously operated by a player -> ignore.
|
||||
Priority = 0;
|
||||
@@ -126,7 +127,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (!isOrder)
|
||||
{
|
||||
var steering = component?.Item.GetComponent<Steering>();
|
||||
var steering = component.Item.GetComponent<Steering>();
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
|
||||
{
|
||||
// Ignore if already set to autopilot or if there's a captain onboard
|
||||
@@ -137,7 +138,7 @@ namespace Barotrauma
|
||||
if (targetItem.CurrentHull == null ||
|
||||
targetItem.Submarine != character.Submarine && !isOrder ||
|
||||
targetItem.CurrentHull.FireSources.Any() ||
|
||||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
|
||||
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))
|
||||
{
|
||||
@@ -154,8 +155,8 @@ namespace Barotrauma
|
||||
else if (!OverridePriority.HasValue)
|
||||
{
|
||||
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
|
||||
float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup")
|
||||
const float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor is { PowerOn: true, FissionRate: > 1, AutoTemp: true } && Option == "powerup")
|
||||
{
|
||||
// Already on, no need to operate.
|
||||
value = 0;
|
||||
@@ -171,12 +172,12 @@ namespace Barotrauma
|
||||
Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier, option)
|
||||
{
|
||||
component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
component = item ?? throw new ArgumentNullException(nameof(item), "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
this.requireEquip = requireEquip;
|
||||
this.operateTarget = operateTarget;
|
||||
this.useController = useController;
|
||||
if (useController) { this.controller = controller ?? component?.Item?.FindController(); }
|
||||
var target = GetTarget();
|
||||
if (useController) { this.controller = controller ?? component.Item?.FindController(); }
|
||||
ItemComponent target = GetTarget();
|
||||
if (target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
@@ -320,5 +321,68 @@ namespace Barotrauma
|
||||
goToObjective = null;
|
||||
getItemObjective = null;
|
||||
}
|
||||
|
||||
private bool IsItemOperatedByAnother(ItemComponent target)
|
||||
{
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrdered = IsOrderedToOperateTarget(HumanAIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(c)) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.TeamID != character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (c.SelectedItem == target.Item)
|
||||
{
|
||||
// If the other character is player, don't try to operate
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isOtherCharacterOrdered = IsOrderedToOperateTarget(otherAI);
|
||||
switch (isOrdered)
|
||||
{
|
||||
case false when isOtherCharacterOrdered:
|
||||
// We are not ordered and the target is ordered -> let the other character operate the target item.
|
||||
return true;
|
||||
case true when !isOtherCharacterOrdered:
|
||||
// We are ordered and the other character is not -> allow to us to operate the target item.
|
||||
continue;
|
||||
default:
|
||||
{
|
||||
// Neither or both are ordered to operate this item.
|
||||
if (!IsOperatingTarget(otherAI))
|
||||
{
|
||||
// The other bot is doing something else -> stick to the target.
|
||||
continue;
|
||||
}
|
||||
if (target is Steering)
|
||||
{
|
||||
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
|
||||
if (character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
|
||||
bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ namespace Barotrauma
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
if (HumanAIController.IsItemRepairedByAnother(Item, out _))
|
||||
if (AIObjectiveRepairItems.IsItemRepairedByAnother(character, Item))
|
||||
{
|
||||
Priority = 0;
|
||||
IsCompleted = true;
|
||||
|
||||
+53
-1
@@ -76,7 +76,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
|
||||
}
|
||||
return !HumanAIController.IsItemRepairedByAnother(item, out _);
|
||||
return !IsItemRepairedByAnother(character, item);
|
||||
}
|
||||
|
||||
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
|
||||
@@ -161,5 +161,57 @@ namespace Barotrauma
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsItemRepairedByAnother(Character character, Item target)
|
||||
{
|
||||
if (target == null) { return false; }
|
||||
bool isOrder = IsOrderedToPrioritizeTarget(character.AIController as HumanAIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(c)) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.TeamID != character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (target.Repairables.Any(r => r.CurrentFixer == c))
|
||||
{
|
||||
// If the other character is player, don't try to repair
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
var repairItemsObjective = otherAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
|
||||
if (repairItemsObjective == null) { continue; }
|
||||
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToPrioritizeTarget(otherAI);
|
||||
switch (isOrder)
|
||||
{
|
||||
case false when isTargetOrdered:
|
||||
// We are not ordered and the target is ordered -> let the other character repair the target.
|
||||
return true;
|
||||
case true when !isTargetOrdered:
|
||||
// We are ordered and the target is not -> allow us to repair the target.
|
||||
continue;
|
||||
default:
|
||||
{
|
||||
// Neither or both are ordered to repair this item.
|
||||
if (otherAI.ObjectiveManager.CurrentObjective is not AIObjectiveRepairItems)
|
||||
{
|
||||
// The other bot is doing something else -> stick to the target.
|
||||
continue;
|
||||
}
|
||||
return target.Repairables.Max(r => r.DegreeOfSuccess(character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToPrioritizeTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -149,7 +149,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
|
||||
null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
@@ -239,7 +239,7 @@ namespace Barotrauma
|
||||
if (Target.CurrentHull?.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
|
||||
null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
@@ -287,6 +287,8 @@ namespace Barotrauma
|
||||
currentTreatmentSuitabilities,
|
||||
limb: Target.CharacterHealth.GetAfflictionLimb(affliction),
|
||||
user: character,
|
||||
checkTreatmentThreshold: true,
|
||||
checkTreatmentSuggestionThreshold: false,
|
||||
predictFutureDuration: 10.0f);
|
||||
|
||||
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
|
||||
@@ -330,7 +332,10 @@ namespace Barotrauma
|
||||
{
|
||||
//get "overall" suitability for no specific limb at this point
|
||||
Target.CharacterHealth.GetSuitableTreatments(
|
||||
currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f);
|
||||
currentTreatmentSuitabilities, user: character,
|
||||
checkTreatmentThreshold: true,
|
||||
checkTreatmentSuggestionThreshold: false,
|
||||
predictFutureDuration: 10.0f);
|
||||
//didn't have any suitable treatments available, try to find some medical items
|
||||
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
|
||||
{
|
||||
@@ -387,7 +392,7 @@ namespace Barotrauma
|
||||
if (Target != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
|
||||
null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
@@ -483,7 +488,7 @@ namespace Barotrauma
|
||||
if (IsCompleted && Target != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
|
||||
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value;
|
||||
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.DisplayName)?.Value;
|
||||
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
return IsCompleted;
|
||||
|
||||
+2
-2
@@ -47,9 +47,9 @@ namespace Barotrauma
|
||||
if (objectiveManager.GetFirstActiveObjective<AIObjectiveRescue>() == null)
|
||||
{
|
||||
charactersWithMinorInjuries.Add(target);
|
||||
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
|
||||
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.DisplayName).Value,
|
||||
delay: 1.0f,
|
||||
identifier: $"notreatableafflictions{target.Name}".ToIdentifier(),
|
||||
identifier: $"notreatableafflictions{target.DisplayName}".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -50,7 +50,8 @@ namespace Barotrauma
|
||||
if (OrderedCharacter.AIController is HumanAIController humanAI &&
|
||||
humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem))
|
||||
{
|
||||
if (orderedCharacter != CommandingCharacter)
|
||||
bool orderGivenByDifferentCharacter = orderedCharacter != CommandingCharacter;
|
||||
if (orderGivenByDifferentCharacter)
|
||||
{
|
||||
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false),
|
||||
minDurationBetweenSimilar: 5,
|
||||
@@ -62,9 +63,12 @@ namespace Barotrauma
|
||||
.WithOrderGiver(CommandingCharacter)
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter);
|
||||
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f,
|
||||
minDurationBetweenSimilar: 5,
|
||||
identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
|
||||
if (orderGivenByDifferentCharacter)
|
||||
{
|
||||
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f,
|
||||
minDurationBetweenSimilar: 5,
|
||||
identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
|
||||
}
|
||||
}
|
||||
TimeSinceLastAttempt = 0f;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using Barotrauma.Networking;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -89,12 +90,12 @@ namespace Barotrauma
|
||||
{
|
||||
public bool IsAlive { get; private set; }
|
||||
|
||||
private readonly List<Item> allItems;
|
||||
private readonly List<Item> thalamusItems;
|
||||
private readonly List<Structure> thalamusStructures;
|
||||
private readonly List<WayPoint> wayPoints = new List<WayPoint>();
|
||||
private readonly List<Hull> hulls = new List<Hull>();
|
||||
private readonly List<Item> spawnOrgans = new List<Item>();
|
||||
private readonly List<Door> jammedDoors = new List<Door>();
|
||||
private readonly Item brain;
|
||||
|
||||
private bool initialCellsSpawned;
|
||||
@@ -105,7 +106,7 @@ namespace Barotrauma
|
||||
|
||||
private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity);
|
||||
|
||||
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
|
||||
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).OfType<T>();
|
||||
|
||||
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
|
||||
|
||||
@@ -122,93 +123,52 @@ namespace Barotrauma
|
||||
{
|
||||
GetConfig();
|
||||
if (Config == null) { return; }
|
||||
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
|
||||
var thalamusPrefabs = ItemPrefab.Prefabs.Where(IsThalamus);
|
||||
var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient);
|
||||
if (brainPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
|
||||
DebugConsole.ThrowError($"WreckAI {wreck.Info.Name}: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.", contentPackage: Config.ContentPackage);
|
||||
return;
|
||||
}
|
||||
allItems = wreck.GetItems(false);
|
||||
thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab));
|
||||
hulls.AddRange(wreck.GetHulls(false));
|
||||
var potentialBrainHulls = new List<(Hull hull, float weight)>();
|
||||
thalamusItems = GetThalamusEntities<Item>(wreck, Config.Entity).ToList();
|
||||
hulls.AddRange(wreck.GetHulls(alsoFromConnectedSubs: false));
|
||||
brain = new Item(brainPrefab, Vector2.Zero, wreck);
|
||||
thalamusItems.Add(brain);
|
||||
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
|
||||
// Bigger hulls are allowed, but not preferred more than what's sufficent.
|
||||
Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
|
||||
// Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
|
||||
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height));
|
||||
foreach (Hull hull in hulls)
|
||||
{
|
||||
float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
|
||||
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
|
||||
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
|
||||
float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
|
||||
if (hull.GetLinkedEntities<Hull>().Any())
|
||||
{
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
continue;
|
||||
}
|
||||
else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
|
||||
{
|
||||
// Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
|
||||
continue;
|
||||
}
|
||||
else if (thalamusItems.Any(i => i.CurrentHull == hull))
|
||||
{
|
||||
// Don't create the brain in a room that already has thalamus items inside it.
|
||||
continue;
|
||||
}
|
||||
else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
|
||||
{
|
||||
// Don't select too small rooms.
|
||||
continue;
|
||||
}
|
||||
if (weight > 0)
|
||||
{
|
||||
potentialBrainHulls.Add((hull, weight));
|
||||
}
|
||||
}
|
||||
var potentialBrainHulls = GetPotentialBrainRooms(wreck, Config, minSize, thalamusItems);
|
||||
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.ServerAndClient);
|
||||
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(IsThalamus);
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room.");
|
||||
DebugConsole.ThrowError($"Wreck AI {wreck.Info.Name}: Cannot find a suitable room for the Thalamus brain. Using a random room. " +
|
||||
$"The wreck should be fixed so that there's at least one room where the following conditions are met: No linked hulls, no open gaps in the floor or to outside the sub, and no other Thalamus items present in the hull.",
|
||||
contentPackage: Config.ContentPackage);
|
||||
|
||||
brainHull = hulls.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus.");
|
||||
DebugConsole.ThrowError($"Wreck AI {wreck.Info.Name}: Cannot find any room for the brain! Failed to create the Thalamus.", contentPackage: Config.ContentPackage);
|
||||
return;
|
||||
}
|
||||
Debug.WriteLine($"Wreck AI {wreck.Info.Name}: Selected brain room: {brainHull.DisplayName}");
|
||||
brainHull.WaterVolume = brainHull.Volume;
|
||||
brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
|
||||
brain.CurrentHull = brainHull;
|
||||
|
||||
// Jam the doors, mainly to prevent any mechanisms from opening them. Also makes it a little bit more difficult for the player to breach into the brain room, because they now have to break the door.
|
||||
foreach (Door door in brainHull.ConnectedGaps.Select(g => g.ConnectedDoor))
|
||||
{
|
||||
if (door == null) { continue; }
|
||||
door.IsJammed = true;
|
||||
jammedDoors.Add(door);
|
||||
}
|
||||
|
||||
var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient);
|
||||
if (backgroundPrefab != null)
|
||||
{
|
||||
new Structure(brainHull.Rect, backgroundPrefab, wreck);
|
||||
}
|
||||
var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient);
|
||||
if (horizontalWallPrefab != null)
|
||||
{
|
||||
int height = (int)horizontalWallPrefab.Size.Y;
|
||||
int halfHeight = height / 2;
|
||||
int quarterHeight = halfHeight / 2;
|
||||
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
|
||||
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
|
||||
}
|
||||
var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient);
|
||||
if (verticalWallPrefab != null)
|
||||
{
|
||||
int width = (int)verticalWallPrefab.Size.X;
|
||||
int halfWidth = width / 2;
|
||||
int quarterWidth = halfWidth / 2;
|
||||
new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
|
||||
new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
|
||||
var background = new Structure(brainHull.Rect, backgroundPrefab, wreck);
|
||||
background.SpriteDepth -= 0.01f;
|
||||
}
|
||||
foreach (Item item in thalamusItems)
|
||||
{
|
||||
@@ -360,6 +320,7 @@ namespace Barotrauma
|
||||
|
||||
public void Kill()
|
||||
{
|
||||
jammedDoors.ForEach(d => d.IsJammed = false);
|
||||
thalamusItems.ForEach(i => i.Condition = 0);
|
||||
foreach (var turret in turrets)
|
||||
{
|
||||
@@ -376,27 +337,24 @@ namespace Barotrauma
|
||||
protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath);
|
||||
if (!IsClient)
|
||||
{
|
||||
if (Config != null)
|
||||
if (Config is { KillAgentsWhenEntityDies: true })
|
||||
{
|
||||
if (Config.KillAgentsWhenEntityDies)
|
||||
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
|
||||
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
|
||||
{
|
||||
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
|
||||
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
|
||||
foreach (var character in Character.CharacterList)
|
||||
{
|
||||
foreach (var character in Character.CharacterList)
|
||||
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
|
||||
// but as long as spawning is handled via status effects, I don't know if there is any better way.
|
||||
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
|
||||
// And if there was, the distance check should prevent killing the agents of a different organism.
|
||||
if (character.SpeciesName == Config.OffensiveAgent)
|
||||
{
|
||||
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
|
||||
// but as long as spawning is handled via status effects, I don't know if there is any better way.
|
||||
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
|
||||
// And if there was, the distance check should prevent killing the agents of a different organism.
|
||||
if (character.SpeciesName == Config.OffensiveAgent)
|
||||
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
|
||||
float maxDistance = Sonar.DefaultSonarRange;
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance)
|
||||
{
|
||||
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
|
||||
float maxDistance = Sonar.DefaultSonarRange;
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance)
|
||||
{
|
||||
character.Kill(CauseOfDeathType.Unknown, null);
|
||||
}
|
||||
character.Kill(CauseOfDeathType.Unknown, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,5 +473,62 @@ namespace Barotrauma
|
||||
msg.WriteBoolean(IsAlive);
|
||||
}
|
||||
#endif
|
||||
|
||||
public static List<(Hull hull, float weight)> GetPotentialBrainRooms(Submarine wreck, WreckAIConfig wreckAI, Point minSize, IEnumerable<Item> thalamusItems = null)
|
||||
{
|
||||
var potentialBrainHulls = new List<(Hull hull, float weight)>();
|
||||
// Bigger hulls are allowed, but not preferred more than what's sufficient.
|
||||
Vector2 sufficientSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
|
||||
Rectangle worldBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width, wreck.Borders.Height));
|
||||
thalamusItems ??= GetThalamusEntities<Item>(wreck, wreckAI.Entity);
|
||||
foreach (Hull hull in wreck.GetHulls(alsoFromConnectedSubs: false))
|
||||
{
|
||||
if (hull.GetLinkedEntities<Hull>().Any())
|
||||
{
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
continue;
|
||||
}
|
||||
else if (hull.ConnectedGaps.Any(g => (g.Open > 0 || g.ConnectedDoor?.Item.Condition <= 0) && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
|
||||
{
|
||||
// Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
|
||||
// Gaps in the broken doors are not yet open at this stage. Also Door.IsBroken is not yet up-to-date, so we'll have to check the item condition.
|
||||
continue;
|
||||
}
|
||||
else if (thalamusItems.Any(i => i.CurrentHull == hull && !i.HasTag(Tags.WireItem)))
|
||||
{
|
||||
// Don't create the brain in a room that already has thalamus items inside it.
|
||||
continue;
|
||||
}
|
||||
else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
|
||||
{
|
||||
// Don't select too small rooms.
|
||||
continue;
|
||||
}
|
||||
float weight = 0;
|
||||
if (hull.IsAirlock)
|
||||
{
|
||||
// Prefer something else than airlocks
|
||||
weight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(worldBounds.Width, worldBounds.Height) / 2f, distanceFromCenter));
|
||||
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficientSize.X, hull.Rect.Width));
|
||||
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficientSize.Y, hull.Rect.Height));
|
||||
weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
|
||||
}
|
||||
if (weight > 0 || potentialBrainHulls.None())
|
||||
{
|
||||
potentialBrainHulls.Add((hull, weight));
|
||||
}
|
||||
}
|
||||
Debug.WriteLine($"Wreck AI {wreck.Info.Name}: Potential brain rooms: {potentialBrainHulls.Count}");
|
||||
foreach ((Hull hull, float weight) in potentialBrainHulls)
|
||||
{
|
||||
Debug.WriteLine($"Wreck AI: Potential brain room: {hull.DisplayName}, {weight.FormatSingleDecimal()}");
|
||||
}
|
||||
return potentialBrainHulls;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -921,20 +921,16 @@ namespace Barotrauma
|
||||
{
|
||||
isRemote = character.IsRemotelyControlled;
|
||||
}
|
||||
if (isRemote)
|
||||
//if the character is remotely controlled,
|
||||
//let the server decide when to deselect the ladder and stop climbing
|
||||
if (!isRemote)
|
||||
{
|
||||
if (Math.Abs(targetMovement.X) > 0.05f ||
|
||||
(TargetMovement.Y < 0.0f && ConvertUnits.ToSimUnits(trigger.Height) + handPos.Y < HeadPosition) ||
|
||||
(TargetMovement.Y > 0.0f && handPos.Y > 0.1f))
|
||||
if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) &&
|
||||
(!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down)))
|
||||
{
|
||||
isClimbing = false;
|
||||
}
|
||||
}
|
||||
else if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) &&
|
||||
(!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down)))
|
||||
{
|
||||
isClimbing = false;
|
||||
}
|
||||
|
||||
if (!isClimbing)
|
||||
{
|
||||
|
||||
+4
-17
@@ -147,20 +147,7 @@ namespace Barotrauma
|
||||
|
||||
if (!character.CanMove)
|
||||
{
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
Collider.Enabled = false;
|
||||
Collider.LinearVelocity = mainLimb.LinearVelocity;
|
||||
Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
UpdateRagdollControlsMovement();
|
||||
if (character.IsDead && deathAnimTimer < deathAnimDuration)
|
||||
{
|
||||
deathAnimTimer += deltaTime;
|
||||
@@ -186,11 +173,11 @@ namespace Barotrauma
|
||||
|
||||
if (InWater)
|
||||
{
|
||||
Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f);
|
||||
Collider.SetTransformIgnoreContacts(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.SetTransform(new Vector2(
|
||||
Collider.SetTransformIgnoreContacts(new Vector2(
|
||||
Collider.SimPosition.X,
|
||||
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
|
||||
0.0f);
|
||||
@@ -995,7 +982,7 @@ namespace Barotrauma
|
||||
if (RagdollParams.IsSpritesheetOrientationHorizontal)
|
||||
{
|
||||
//horizontally aligned limbs need to be flipped 180 degrees
|
||||
l.body.SetTransform(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir);
|
||||
l.body.SetTransformIgnoreContacts(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir);
|
||||
}
|
||||
//no need to do anything when flipping vertically oriented limbs
|
||||
//the sprite gets flipped horizontally, which does the job
|
||||
|
||||
+4
-24
@@ -296,25 +296,7 @@ namespace Barotrauma
|
||||
fallingProneAnimTimer += deltaTime;
|
||||
UpdateFallingProne(1.0f);
|
||||
}
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
if (Collider.Enabled)
|
||||
{
|
||||
//deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now
|
||||
MainLimb.body.LinearVelocity = Collider.LinearVelocity;
|
||||
Collider.Enabled = false;
|
||||
}
|
||||
Collider.LinearVelocity = MainLimb.LinearVelocity;
|
||||
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
UpdateRagdollControlsMovement();
|
||||
return;
|
||||
}
|
||||
fallingProneAnimTimer = 0.0f;
|
||||
@@ -324,7 +306,7 @@ namespace Barotrauma
|
||||
{
|
||||
var lowestLimb = FindLowestLimb();
|
||||
|
||||
Collider.SetTransform(new Vector2(
|
||||
Collider.SetTransformIgnoreContacts(new Vector2(
|
||||
Collider.SimPosition.X,
|
||||
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
|
||||
Collider.Rotation);
|
||||
@@ -356,7 +338,7 @@ namespace Barotrauma
|
||||
float angleDiff = MathUtils.GetShortestAngle(Collider.Rotation, 0.0f);
|
||||
if (Math.Abs(angleDiff) > 0.001f)
|
||||
{
|
||||
Collider.SetTransform(Collider.SimPosition, Collider.Rotation + angleDiff);
|
||||
Collider.SetTransformIgnoreContacts(Collider.SimPosition, Collider.Rotation + angleDiff);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,9 +563,7 @@ namespace Barotrauma
|
||||
footMid += (Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f)) * Dir;
|
||||
}
|
||||
|
||||
movement = overrideTargetMovement == Vector2.Zero ?
|
||||
MathUtils.SmoothStep(movement, TargetMovement, movementLerp) :
|
||||
overrideTargetMovement;
|
||||
movement = overrideTargetMovement ?? MathUtils.SmoothStep(movement, TargetMovement, movementLerp);
|
||||
|
||||
if (Math.Abs(movement.X) < 0.005f)
|
||||
{
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace Barotrauma
|
||||
|
||||
//a movement vector that overrides targetmovement if trying to steer
|
||||
//a Character to the position sent by server in multiplayer mode
|
||||
protected Vector2 overrideTargetMovement;
|
||||
protected Vector2? overrideTargetMovement;
|
||||
|
||||
protected float floorY, standOnFloorY;
|
||||
protected Fixture floorFixture;
|
||||
@@ -142,6 +142,12 @@ namespace Barotrauma
|
||||
|
||||
private Category prevCollisionCategory = Category.None;
|
||||
|
||||
/// <summary>
|
||||
/// When the character is alive/conscious, the collider drives the character's movement and is used to sync the character's position in MP.
|
||||
/// When unconscious, the ragdoll controls the movement and the collider just sticks to the main limb.
|
||||
/// </summary>
|
||||
public bool ColliderControlsMovement => character.CanMove;
|
||||
|
||||
public bool IsStuck => Limbs.Any(l => l.IsStuck);
|
||||
|
||||
public PhysicsBody Collider
|
||||
@@ -189,7 +195,7 @@ namespace Barotrauma
|
||||
Vector2 pos = collider[colliderIndex].SimPosition;
|
||||
pos.Y -= collider[colliderIndex].Height * 0.5f;
|
||||
pos.Y += collider[value].Height * 0.5f;
|
||||
collider[value].SetTransform(pos, collider[colliderIndex].Rotation);
|
||||
collider[value].SetTransformIgnoreContacts(pos, collider[colliderIndex].Rotation);
|
||||
|
||||
collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity;
|
||||
collider[value].AngularVelocity = collider[colliderIndex].AngularVelocity;
|
||||
@@ -286,7 +292,7 @@ namespace Barotrauma
|
||||
foreach (Limb limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered || !limb.body.PhysEnabled) { continue; }
|
||||
limb.body.SetTransform(Collider.SimPosition, Collider.Rotation);
|
||||
limb.body.SetTransformIgnoreContacts(Collider.SimPosition, Collider.Rotation);
|
||||
//reset pull joints (they may be somewhere far away if the character has moved from the position where animations were last updated)
|
||||
limb.PullJointEnabled = false;
|
||||
limb.PullJointWorldAnchorB = limb.SimPosition;
|
||||
@@ -301,11 +307,11 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
return (overrideTargetMovement == Vector2.Zero) ? targetMovement : overrideTargetMovement;
|
||||
return overrideTargetMovement ?? targetMovement;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
if (!MathUtils.IsValid(value)) { return; }
|
||||
targetMovement.X = MathHelper.Clamp(value.X, -MAX_SPEED, MAX_SPEED);
|
||||
targetMovement.Y = MathHelper.Clamp(value.Y, -MAX_SPEED, MAX_SPEED);
|
||||
}
|
||||
@@ -1307,6 +1313,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
|
||||
Collider.LinearVelocity = new Vector2(
|
||||
NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12),
|
||||
NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12));
|
||||
|
||||
if (forceStanding)
|
||||
{
|
||||
inWater = false;
|
||||
@@ -1451,7 +1462,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck.
|
||||
if (Collider.LinearVelocity == Vector2.Zero)
|
||||
if (Collider.LinearVelocity == Vector2.Zero && !character.IsRemotePlayer)
|
||||
{
|
||||
character.IsRagdolled = true;
|
||||
if (character.IsBot)
|
||||
@@ -1466,6 +1477,30 @@ namespace Barotrauma
|
||||
forceNotStanding = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the logic that needs to run when the ragdoll is what controls the character's movement instead of the collider <see cref="ColliderControlsMovement"/>
|
||||
/// (making the collider stick to the ragdoll's main limb).
|
||||
/// </summary>
|
||||
protected void UpdateRagdollControlsMovement()
|
||||
{
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (Collider.Enabled)
|
||||
{
|
||||
//deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now
|
||||
MainLimb.body.LinearVelocity = Collider.LinearVelocity;
|
||||
Collider.Enabled = false;
|
||||
}
|
||||
Collider.LinearVelocity = MainLimb.LinearVelocity;
|
||||
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckBodyInRest(float deltaTime)
|
||||
{
|
||||
if (SimplePhysicsEnabled) { return; }
|
||||
@@ -2102,7 +2137,7 @@ namespace Barotrauma
|
||||
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
|
||||
private void UpdateNetPlayerPosition(float deltaTime)
|
||||
{
|
||||
if (GameMain.NetworkMember == null) return;
|
||||
if (GameMain.NetworkMember == null) { return; }
|
||||
|
||||
float lowestSubPos = float.MaxValue;
|
||||
if (Submarine.Loaded.Any())
|
||||
|
||||
@@ -197,7 +197,22 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Used for multiplying all the damage.
|
||||
/// </summary>
|
||||
public float DamageMultiplier { get; set; } = 1;
|
||||
public float DamageMultiplier
|
||||
{
|
||||
get => _damageMultiplier ?? initialDamageMultiplier;
|
||||
set
|
||||
{
|
||||
if (!_damageMultiplier.HasValue)
|
||||
{
|
||||
SetInitialDamageMultiplier(value);
|
||||
}
|
||||
_damageMultiplier = value;
|
||||
}
|
||||
}
|
||||
private float? _damageMultiplier;
|
||||
private float initialDamageMultiplier = 1.0f;
|
||||
public void ResetDamageMultiplier() => _damageMultiplier = initialDamageMultiplier;
|
||||
public void SetInitialDamageMultiplier(float value) => initialDamageMultiplier = value;
|
||||
|
||||
/// <summary>
|
||||
/// Used for multiplying all the ranges.
|
||||
@@ -275,6 +290,8 @@ namespace Barotrauma
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
|
||||
public Vector2 RootForceWorldEnd { get; private set; }
|
||||
|
||||
public bool HasRootForce => RootForceWorldStart != Vector2.Zero || RootForceWorldMiddle != Vector2.Zero || RootForceWorldEnd != Vector2.Zero;
|
||||
|
||||
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable]
|
||||
public TransitionMode RootTransitionEasing { get; private set; }
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ namespace Barotrauma
|
||||
public const float MaxHighlightDistance = 150.0f;
|
||||
public const float MaxDragDistance = 200.0f;
|
||||
|
||||
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
|
||||
|
||||
partial void UpdateLimbLightSource(Limb limb);
|
||||
|
||||
private bool enabled = true;
|
||||
@@ -666,9 +668,23 @@ namespace Barotrauma
|
||||
public bool RequireConsciousnessForCustomInteract = true;
|
||||
public bool AllowCustomInteract
|
||||
{
|
||||
get { return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; }
|
||||
get
|
||||
{
|
||||
if (CampaignMode.HostileFactionDisablesInteraction(CampaignInteractionType) &&
|
||||
AIController is HumanAIController humanAi && humanAi.IsInHostileFaction())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldShowCustomInteractText =>
|
||||
!CustomInteractHUDText.IsNullOrEmpty() &&
|
||||
AllowCustomInteract &&
|
||||
(AIController is not HumanAIController humanAi || humanAi.AllowCampaignInteraction());
|
||||
|
||||
private float lockHandsTimer;
|
||||
public bool LockHands
|
||||
{
|
||||
@@ -1212,6 +1228,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public CampaignMode.InteractionType CampaignInteractionType;
|
||||
|
||||
public Identifier MerchantIdentifier;
|
||||
|
||||
private bool accessRemovedCharacterErrorShown;
|
||||
@@ -1265,6 +1282,10 @@ namespace Barotrauma
|
||||
|
||||
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
|
||||
public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer;
|
||||
/// <summary>
|
||||
/// Alias for <see cref="IsInPlayerSub"/>, so the same property name works on both items and characters.
|
||||
/// </summary>
|
||||
public bool InPlayerSubmarine => IsInPlayerSub;
|
||||
|
||||
public float AITurretPriority
|
||||
{
|
||||
@@ -1412,7 +1433,7 @@ namespace Barotrauma
|
||||
if (characterInfo?.HumanPrefabIds is { } prefabIds &&
|
||||
prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default)
|
||||
{
|
||||
humanPrefab = NPCSet.Get(
|
||||
HumanPrefab = NPCSet.Get(
|
||||
characterInfo.HumanPrefabIds.NpcSetIdentifier,
|
||||
characterInfo.HumanPrefabIds.NpcIdentifier);
|
||||
}
|
||||
@@ -1765,6 +1786,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (this == Controlled && inputType == InputType.Run && ToggleRun)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return keys[(int)inputType].Held;
|
||||
}
|
||||
|
||||
@@ -1964,6 +1990,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool ToggleRun;
|
||||
|
||||
public bool CanRunWhileDragging()
|
||||
{
|
||||
if (selectedCharacter is not { IsDraggable: true }) { return true; }
|
||||
@@ -2169,8 +2197,9 @@ namespace Barotrauma
|
||||
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
|
||||
}
|
||||
|
||||
bool aiControlled = this is AICharacter && Controlled != this && !IsRemotelyControlled;
|
||||
if (!aiControlled)
|
||||
bool aiControlled = this is AICharacter && Controlled != this && !IsRemotePlayer;
|
||||
bool controlledByServer = GameMain.NetworkMember is { IsClient: true } && IsRemotelyControlled;
|
||||
if (!aiControlled && !controlledByServer)
|
||||
{
|
||||
Vector2 targetMovement = GetTargetMovement();
|
||||
AnimController.TargetMovement = targetMovement;
|
||||
@@ -2199,7 +2228,8 @@ namespace Barotrauma
|
||||
{
|
||||
AnimController.TargetDir = Direction.Right;
|
||||
}
|
||||
else
|
||||
//only humanoids' flipping is controlled by the cursor, monster flipping is driven by their movement in FishAnimController
|
||||
else if (AnimController is HumanoidAnimController)
|
||||
{
|
||||
if (CursorPosition.X < AnimController.Collider.Position.X - cursorFollowMargin)
|
||||
{
|
||||
@@ -2262,15 +2292,9 @@ namespace Barotrauma
|
||||
}
|
||||
else if (IsKeyDown(InputType.Attack))
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this)
|
||||
{
|
||||
if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false)
|
||||
{
|
||||
currentAttackTarget = default;
|
||||
}
|
||||
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
|
||||
}
|
||||
else if (IsPlayer)
|
||||
//normally the attack target, where to aim the attack and such is handled by EnemyAIController,
|
||||
//but in the case of player-controlled monsters, we handle it here
|
||||
if (IsPlayer)
|
||||
{
|
||||
float dist = -1;
|
||||
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
|
||||
@@ -2315,13 +2339,16 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
var currentContexts = GetAttackContexts();
|
||||
var validLimbs = AnimController.Limbs.Where(l =>
|
||||
var attackLimbs = AnimController.Limbs.Where(static l => l.attack != null);
|
||||
bool hasAttacksWithoutRootForce = attackLimbs.Any(static l=> !l.attack.HasRootForce);
|
||||
var validLimbs = attackLimbs.Where(l =>
|
||||
{
|
||||
if (l.IsSevered || l.IsStuck) { return false; }
|
||||
if (l.Disabled) { return false; }
|
||||
var attack = l.attack;
|
||||
if (attack == null) { return false; }
|
||||
if (attack.CoolDownTimer > 0) { return false; }
|
||||
//disallow attacks with root force if there's any other attacks available
|
||||
if (hasAttacksWithoutRootForce && attack.HasRootForce) { return false; }
|
||||
if (!attack.IsValidContext(currentContexts)) { return false; }
|
||||
if (attackTarget != null)
|
||||
{
|
||||
@@ -2359,6 +2386,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (GameMain.NetworkMember is { IsClient: true } && Controlled != this)
|
||||
{
|
||||
if (currentAttackTarget.DamageTarget is Entity { Removed: true })
|
||||
{
|
||||
currentAttackTarget = default;
|
||||
}
|
||||
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
|
||||
}
|
||||
}
|
||||
|
||||
if (Inventory != null)
|
||||
@@ -2479,119 +2514,11 @@ namespace Barotrauma
|
||||
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
|
||||
if (target is Character targetCharacter)
|
||||
{
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
|
||||
return ISpatialEntity.IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
if (seeingEntity is Character seeingCharacter)
|
||||
{
|
||||
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
|
||||
}
|
||||
if (target is Character targetCharacter)
|
||||
{
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (seeingEntity == null) { return false; }
|
||||
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (!target.AnimController.SimplePhysicsEnabled)
|
||||
{
|
||||
//find the limbs that are furthest from the target's position (from the viewer's point of view)
|
||||
Limb leftExtremity = null, rightExtremity = null;
|
||||
float leftMostDot = 0.0f, rightMostDot = 0.0f;
|
||||
Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition;
|
||||
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
|
||||
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
|
||||
foreach (Limb limb in target.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition;
|
||||
float leftDot = Vector2.Dot(limbDir, leftDir);
|
||||
if (leftDot > leftMostDot)
|
||||
{
|
||||
leftMostDot = leftDot;
|
||||
leftExtremity = limb;
|
||||
continue;
|
||||
}
|
||||
float rightDot = Vector2.Dot(limbDir, rightDir);
|
||||
if (rightDot > rightMostDot)
|
||||
{
|
||||
rightMostDot = rightDot;
|
||||
rightExtremity = limb;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null) { return false; }
|
||||
if (seeingEntity == null) { return false; }
|
||||
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
|
||||
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
|
||||
if (checkFacing && seeingEntity is Character seeingCharacter)
|
||||
{
|
||||
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
|
||||
}
|
||||
//both inside the same sub (or both outside)
|
||||
//OR the we're inside, the other character outside
|
||||
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
|
||||
{
|
||||
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//we're outside, the other character inside
|
||||
else if (seeingEntity.Submarine == null)
|
||||
{
|
||||
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//both inside different subs
|
||||
else
|
||||
{
|
||||
return
|
||||
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
|
||||
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
|
||||
bool IsBlocking(Fixture f)
|
||||
{
|
||||
var body = f.Body;
|
||||
if (body == null) { return false; }
|
||||
if (body.UserData is Structure wall)
|
||||
{
|
||||
if (!wall.CastShadow && seeThroughWindows) { return false; }
|
||||
return wall != target;
|
||||
}
|
||||
else if (body.UserData is Item item)
|
||||
{
|
||||
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
|
||||
{
|
||||
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
|
||||
}
|
||||
|
||||
return item != target;
|
||||
}
|
||||
return true;
|
||||
return ISpatialEntity.CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2740,7 +2667,7 @@ namespace Barotrauma
|
||||
public bool CanBeDraggedBy(Character character)
|
||||
{
|
||||
if (!IsDraggable) { return false; }
|
||||
return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID);
|
||||
return IsKnockedDown || LockHands || (IsPet && character.IsFriendly(this)) || (IsBot && character.TeamID == TeamID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3669,7 +3596,12 @@ namespace Barotrauma
|
||||
{
|
||||
humanAnimController.Crouching = false;
|
||||
}
|
||||
if (IsRagdolled) { AnimController.IgnorePlatforms = true; }
|
||||
//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 })
|
||||
{
|
||||
AnimController.IgnorePlatforms = true;
|
||||
}
|
||||
AnimController.ResetPullJoints();
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
return;
|
||||
@@ -4121,6 +4053,7 @@ namespace Barotrauma
|
||||
if (character.TeamID != TeamID) { continue; }
|
||||
if (character.AIController is not HumanAIController) { continue; }
|
||||
if (!HumanAIController.IsActive(character)) { continue; }
|
||||
if (character.Info == null) { continue; }
|
||||
foreach (var currentOrder in character.CurrentOrders)
|
||||
{
|
||||
if (currentOrder == null) { continue; }
|
||||
@@ -4136,12 +4069,15 @@ namespace Barotrauma
|
||||
case OrderCategory.Movement:
|
||||
// If there character has another movement order, dismiss that order
|
||||
Order orderToReplace = null;
|
||||
foreach (var currentOrder in CurrentOrders)
|
||||
if (CurrentOrders != null)
|
||||
{
|
||||
if (currentOrder == null) { continue; }
|
||||
if (currentOrder.Category != OrderCategory.Movement) { continue; }
|
||||
orderToReplace = currentOrder;
|
||||
break;
|
||||
foreach (var currentOrder in CurrentOrders)
|
||||
{
|
||||
if (currentOrder == null) { continue; }
|
||||
if (currentOrder.Category != OrderCategory.Movement) { continue; }
|
||||
orderToReplace = currentOrder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (orderToReplace is { AutoDismiss: true })
|
||||
{
|
||||
@@ -4177,6 +4113,7 @@ namespace Barotrauma
|
||||
|
||||
private void AddCurrentOrder(Order newOrder)
|
||||
{
|
||||
if (CurrentOrders == null) { return; }
|
||||
if (newOrder == null || newOrder.Identifier == "dismissed")
|
||||
{
|
||||
if (newOrder.Option != Identifier.Empty)
|
||||
@@ -4218,9 +4155,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemoveDuplicateOrders(Order order)
|
||||
private void RemoveDuplicateOrders(Order order)
|
||||
{
|
||||
bool removed = false;
|
||||
if (CurrentOrders == null) { return; }
|
||||
int? priorityOfRemoved = null;
|
||||
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
|
||||
{
|
||||
@@ -4229,12 +4166,11 @@ namespace Barotrauma
|
||||
{
|
||||
priorityOfRemoved = orderInfo.ManualPriority;
|
||||
CurrentOrders.RemoveAt(i);
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!priorityOfRemoved.HasValue) { return removed; }
|
||||
if (!priorityOfRemoved.HasValue) { return; }
|
||||
|
||||
for (int i = 0; i < CurrentOrders.Count; i++)
|
||||
{
|
||||
@@ -4245,11 +4181,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
|
||||
CurrentOrders.RemoveAll(o => o.ManualPriority <= 0);
|
||||
// Sort the current orders so the one with the highest priority comes first
|
||||
CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
public Order GetCurrentOrderWithTopPriority()
|
||||
@@ -4334,6 +4268,30 @@ namespace Barotrauma
|
||||
aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay));
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public void SendSinglePlayerMessage(AIChatMessage message, bool canUseRadio, WifiComponent radio)
|
||||
{
|
||||
if (message.MessageType == null)
|
||||
{
|
||||
message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
|
||||
}
|
||||
if (GameMain.GameSession?.CrewManager is { IsSinglePlayer: true } crewManager)
|
||||
{
|
||||
string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled);
|
||||
if (!string.IsNullOrEmpty(modifiedMessage))
|
||||
{
|
||||
crewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
|
||||
}
|
||||
if (canUseRadio)
|
||||
{
|
||||
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
|
||||
radio.TransmitSignal(s, sentFromChat: true);
|
||||
}
|
||||
}
|
||||
ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message);
|
||||
}
|
||||
#endif
|
||||
|
||||
private void UpdateAIChatMessages(float deltaTime)
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
@@ -4350,28 +4308,13 @@ namespace Barotrauma
|
||||
message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
|
||||
}
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
|
||||
{
|
||||
string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled);
|
||||
if (!string.IsNullOrEmpty(modifiedMessage))
|
||||
{
|
||||
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
|
||||
}
|
||||
if (canUseRadio)
|
||||
{
|
||||
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
|
||||
radio.TransmitSignal(s, sentFromChat: true);
|
||||
}
|
||||
}
|
||||
SendSinglePlayerMessage(message, canUseRadio, radio);
|
||||
#endif
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && message.MessageType != ChatMessageType.Order)
|
||||
{
|
||||
GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this);
|
||||
}
|
||||
#endif
|
||||
#if CLIENT
|
||||
ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message);
|
||||
#endif
|
||||
sentMessages.Add(message);
|
||||
}
|
||||
@@ -4451,10 +4394,12 @@ namespace Barotrauma
|
||||
{
|
||||
attackAfflictions = attack.Afflictions.Keys;
|
||||
}
|
||||
|
||||
|
||||
float damageMultiplier = attack.DamageMultiplier * attackData.DamageMultiplier;
|
||||
|
||||
var attackResult = targetLimb == null ?
|
||||
AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier * attackData.DamageMultiplier) :
|
||||
DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier * attackData.DamageMultiplier, penetration: penetration + attackData.AddedPenetration, shouldImplode: attackData.ShouldImplode);
|
||||
AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, damageMultiplier) :
|
||||
DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, damageMultiplier, penetration: penetration + attackData.AddedPenetration, shouldImplode: attackData.ShouldImplode);
|
||||
|
||||
if (attacker != null)
|
||||
{
|
||||
@@ -5337,7 +5282,7 @@ namespace Barotrauma
|
||||
{
|
||||
SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
|
||||
}
|
||||
|
||||
|
||||
private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List<Item> extraDuffelBags)
|
||||
{
|
||||
foreach (var itemElement in element.Elements())
|
||||
@@ -5352,8 +5297,8 @@ 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
|
||||
int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
|
||||
if (!slotIndices.Any())
|
||||
@@ -5576,7 +5521,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Removes the talents the character has unlocked in their talent tree.
|
||||
/// </summary>
|
||||
public void ResetTalents(bool applyXpPenalty)
|
||||
public void ResetTalents(int talentPointReduction)
|
||||
{
|
||||
characterTalents.Clear();
|
||||
abilityResistances.Clear();
|
||||
@@ -5584,13 +5529,17 @@ namespace Barotrauma
|
||||
CharacterHealth.RemoveAfflictions(affliction => affliction.Prefab.AfflictionType == Tags.AfflictionTypeTalentBuff);
|
||||
statValues.Clear();
|
||||
|
||||
if (applyXpPenalty)
|
||||
for (int i = 0; i < talentPointReduction; i++)
|
||||
{
|
||||
int currentLevel = info.GetCurrentLevel();
|
||||
if (currentLevel > 0)
|
||||
{
|
||||
info.SetExperience(info.ExperiencePoints - CharacterInfo.ExperienceRequiredPerLevel(currentLevel));
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5945,7 +5894,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
// NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
|
||||
return hadResistance ? resistance : 1f;
|
||||
return hadResistance ? Math.Max(0, resistance) : 1f;
|
||||
}
|
||||
|
||||
public float GetAbilityResistance(AfflictionPrefab affliction)
|
||||
@@ -5964,7 +5913,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
// NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
|
||||
return hadResistance ? resistance : 1f;
|
||||
return hadResistance ? Math.Max(0, resistance) : 1f;
|
||||
}
|
||||
|
||||
public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value)
|
||||
@@ -6001,7 +5950,7 @@ namespace Barotrauma
|
||||
// NPCs are friendly to the same team and the friendly NPCs
|
||||
CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
|
||||
// Friendly NPCs are friendly to both player teams
|
||||
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
|
||||
CharacterTeamType.FriendlyNPC => otherTeam is CharacterTeamType.Team1 or CharacterTeamType.Team2,
|
||||
// None (bandits and such) consider friendly NPCs friendly, not attacking them unless they attack first
|
||||
// Otherwise bandits would for example attach the hostages.
|
||||
CharacterTeamType.None => otherTeam == CharacterTeamType.FriendlyNPC,
|
||||
|
||||
@@ -1282,7 +1282,7 @@ namespace Barotrauma
|
||||
|
||||
partial void LoadAttachmentSprites();
|
||||
|
||||
public int CalculateSalary()
|
||||
public int CalculateSalary(int baseSalary = 0, float salaryMultiplier = 1.0f)
|
||||
{
|
||||
if (Name == null || Job == null) { return 0; }
|
||||
|
||||
@@ -1292,7 +1292,7 @@ namespace Barotrauma
|
||||
salary += (int)(skill.Level * skill.PriceMultiplier);
|
||||
}
|
||||
|
||||
return (int)(salary * Job.Prefab.PriceMultiplier);
|
||||
return (int)(baseSalary + (salary * Job.Prefab.PriceMultiplier * salaryMultiplier));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1485,11 +1485,9 @@ namespace Barotrauma
|
||||
//e.g. talents from endocrine booster or extra talents some special NPC has
|
||||
var talentsFromOutsideTree = GetUnlockedTalentsOutsideTree().ToList();
|
||||
|
||||
bool applyXpPenalty = talentResetCount > 0;
|
||||
|
||||
UnlockedTalents.Clear();
|
||||
SavedStatValues.Clear();
|
||||
Character?.ResetTalents(applyXpPenalty);
|
||||
Character?.ResetTalents(talentPointReduction: talentResetCount);
|
||||
TalentRefundPoints--;
|
||||
talentResetCount++;
|
||||
|
||||
|
||||
@@ -14,24 +14,29 @@ namespace Barotrauma
|
||||
|
||||
public readonly AnimController.Animation Animation;
|
||||
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None)
|
||||
: this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation)
|
||||
public bool IgnorePlatforms;
|
||||
|
||||
public readonly Vector2 TargetMovement;
|
||||
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false)
|
||||
: this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms)
|
||||
{
|
||||
}
|
||||
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None)
|
||||
: this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation)
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false)
|
||||
: this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms)
|
||||
{
|
||||
}
|
||||
|
||||
protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None)
|
||||
protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false)
|
||||
: base(pos, rotation, velocity, angularVelocity, ID, time)
|
||||
{
|
||||
Direction = dir;
|
||||
SelectedCharacter = selectedCharacter;
|
||||
SelectedItem = selectedItem;
|
||||
SelectedSecondaryItem = selectedSecondaryItem;
|
||||
|
||||
IgnorePlatforms = ignorePlatforms;
|
||||
TargetMovement = targetMovement;
|
||||
Animation = animation;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -398,7 +398,7 @@ namespace Barotrauma
|
||||
public readonly float MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum value to apply
|
||||
/// Maximum value to apply
|
||||
/// </summary>
|
||||
public readonly float MaxValue;
|
||||
|
||||
@@ -764,6 +764,12 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly float TreatmentThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// How strong the affliction needs to be for treatment suggestions to be shown in the health interface.
|
||||
/// Defaults to <see cref="TreatmentThreshold"/>.
|
||||
/// </summary>
|
||||
public readonly float TreatmentSuggestionThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Bots will not try to treat the affliction if the character has any of these afflictions
|
||||
/// </summary>
|
||||
@@ -941,6 +947,7 @@ namespace Barotrauma
|
||||
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
|
||||
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
|
||||
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
|
||||
TreatmentSuggestionThreshold = element.GetAttributeFloat(nameof(TreatmentSuggestionThreshold), TreatmentThreshold);
|
||||
|
||||
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
|
||||
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
|
||||
|
||||
@@ -1198,7 +1198,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
|
||||
/// <param name="predictFutureDuration">If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds.</param>
|
||||
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
|
||||
/// <param name="checkTreatmentThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentThreshold"/> (whether they're severe enough for AI to treat)?</param>
|
||||
/// <param name="checkTreatmentSuggestionThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentSuggestionThreshold"/> (whether treatment suggestions are shown in the health interface)?</param>
|
||||
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false,
|
||||
bool checkTreatmentThreshold = true, bool checkTreatmentSuggestionThreshold = true,
|
||||
float predictFutureDuration = 0.0f)
|
||||
{
|
||||
//key = item identifier
|
||||
//float = suitability
|
||||
@@ -1249,7 +1253,14 @@ namespace Barotrauma
|
||||
//if this a suitable treatment, ignore it if the affliction isn't severe enough to treat
|
||||
//if the suitability is negative though, we need to take it into account!
|
||||
//otherwise we may end up e.g. giving too much opiates to someone already close to overdosing
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
if (checkTreatmentThreshold)
|
||||
{
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
}
|
||||
if (checkTreatmentSuggestionThreshold)
|
||||
{
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentSuggestionThreshold) { continue; }
|
||||
}
|
||||
}
|
||||
if (treatment.Value > strength)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,12 @@ namespace Barotrauma
|
||||
[Serialize(0, IsPropertySaveable.No)]
|
||||
public int ExperiencePoints { get; private set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.No)]
|
||||
public int BaseSalary { get; private set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.No)]
|
||||
public float SalaryMultiplier { get; private set; }
|
||||
|
||||
private readonly HashSet<Identifier> tags = new HashSet<Identifier>();
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
@@ -247,8 +253,8 @@ namespace Barotrauma
|
||||
float newSkill = skill.Level * SkillMultiplier;
|
||||
skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false);
|
||||
}
|
||||
characterInfo.Salary = characterInfo.CalculateSalary();
|
||||
}
|
||||
characterInfo.Salary = characterInfo.CalculateSalary(BaseSalary, SalaryMultiplier);
|
||||
characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier);
|
||||
characterInfo.GiveExperience(ExperiencePoints);
|
||||
return characterInfo;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Barotrauma
|
||||
public SkillPrefab(ContentXElement element)
|
||||
{
|
||||
Identifier = element.GetAttributeIdentifier("identifier", "");
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f);
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 15.0f);
|
||||
levelRange = GetSkillRange("level", element, defaultValue: new Range<float>(0, 0));
|
||||
levelRangePvP = GetSkillRange("pvplevel", element, defaultValue: levelRange);
|
||||
IsPrimarySkill = element.GetAttributeBool("primary", false);
|
||||
|
||||
@@ -213,7 +213,9 @@ namespace Barotrauma
|
||||
public readonly Ragdoll ragdoll;
|
||||
public readonly LimbParams Params;
|
||||
|
||||
//the physics body of the limb
|
||||
/// <summary>
|
||||
/// The physics body of the limb
|
||||
/// </summary>
|
||||
public PhysicsBody body;
|
||||
|
||||
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
|
||||
@@ -528,6 +530,9 @@ namespace Barotrauma
|
||||
|
||||
public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
|
||||
|
||||
/// <summary>
|
||||
/// Other wearables attached to the head. I.e. husk sprite, hair, beard, moustache, and face attachments.
|
||||
/// </summary>
|
||||
public readonly List<WearableSprite> OtherWearables = new List<WearableSprite>();
|
||||
|
||||
public bool PullJointEnabled
|
||||
@@ -721,7 +726,7 @@ namespace Barotrauma
|
||||
var attackElement = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("attack");
|
||||
if (attackElement != null)
|
||||
{
|
||||
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);
|
||||
attack.SetInitialDamageMultiplier(attackElement.GetAttributeFloat("damagemultiplier", 1f));
|
||||
attack.RangeMultiplier = attackElement.GetAttributeFloat("rangemultiplier", 1f);
|
||||
attack.ImpactMultiplier = attackElement.GetAttributeFloat("impactmultiplier", 1f);
|
||||
}
|
||||
@@ -1014,7 +1019,6 @@ namespace Barotrauma
|
||||
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
|
||||
bool wasRunning = attack.IsRunning;
|
||||
attack.UpdateAttackTimer(deltaTime, character);
|
||||
attack.DamageMultiplier = 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier);
|
||||
|
||||
if (attack.Blink)
|
||||
{
|
||||
@@ -1164,7 +1168,7 @@ namespace Barotrauma
|
||||
// Set the main collider where the body lands after the attack
|
||||
if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f)
|
||||
{
|
||||
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
|
||||
character.AnimController.Collider.SetTransformIgnoreContacts(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
|
||||
}
|
||||
}
|
||||
return wasHit;
|
||||
@@ -1180,9 +1184,11 @@ namespace Barotrauma
|
||||
LastAttackSoundTime = SoundInterval;
|
||||
}
|
||||
#endif
|
||||
if (damageTarget is Character targetCharacter && targetLimb != null)
|
||||
{
|
||||
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body, this);
|
||||
attack.ResetDamageMultiplier();
|
||||
attack.DamageMultiplier *= 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier);
|
||||
if (damageTarget is Character && targetLimb != null)
|
||||
{
|
||||
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1192,7 +1198,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this);
|
||||
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this);
|
||||
}
|
||||
}
|
||||
/*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient))
|
||||
|
||||
+3
-3
@@ -189,7 +189,7 @@ namespace Barotrauma
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (prefab?.ConfigElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: prefab?.ContentPackage);
|
||||
return string.Empty;
|
||||
}
|
||||
return GetFolder(prefab.ConfigElement, prefab.FilePath.Value);
|
||||
@@ -414,7 +414,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (animationType == AnimationType.NotDefined)
|
||||
{
|
||||
throw new Exception("Cannot create an animation file of type " + animationType.ToString());
|
||||
throw new Exception("Cannot create an animation file of type " + animationType);
|
||||
}
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
|
||||
{
|
||||
@@ -543,7 +543,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (doc == null)
|
||||
{
|
||||
DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!");
|
||||
DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!", contentPackage: Path.ContentPackage);
|
||||
return;
|
||||
}
|
||||
Serialize();
|
||||
|
||||
@@ -193,25 +193,32 @@ namespace Barotrauma
|
||||
{
|
||||
return newXml;
|
||||
}
|
||||
// CreateVariantXML seems to merge the ai targets so that in the new xml we have both the old and the new target definitions.
|
||||
|
||||
// CreateVariantXML does not understand anything about targeting tags, it just replaces the <target> elements in the order they're defined in.
|
||||
// We can do better here by replacing the target with a matching tag, so let's clear the element and do that.
|
||||
var finalAiElement = newXml.GetChildElement("ai");
|
||||
var processedTags = new HashSet<string>();
|
||||
foreach (var aiTarget in finalAiElement.Elements().ToArray())
|
||||
finalAiElement.Elements().Remove();
|
||||
|
||||
//add all the targets from the base character
|
||||
baseAi.Elements().ForEach(e => finalAiElement.Add(e));
|
||||
|
||||
var processedTags = new List<Identifier>();
|
||||
foreach (var variantTargetElement in variantAi.Elements())
|
||||
{
|
||||
string tag = aiTarget.GetAttributeString("tag", null);
|
||||
if (tag == null) { continue; }
|
||||
if (processedTags.Contains(tag))
|
||||
Identifier tag = variantTargetElement.GetAttributeIdentifier("tag", Identifier.Empty);
|
||||
var matchingElements = finalAiElement.Elements().Where(e => e.GetAttributeIdentifier("tag", Identifier.Empty) == tag);
|
||||
int alreadyProcessed = processedTags.Count(t => t == tag);
|
||||
if (matchingElements.Count() > alreadyProcessed)
|
||||
{
|
||||
aiTarget.Remove();
|
||||
continue;
|
||||
//more matching elements found, replace the first one that hasn't been processed yet
|
||||
matchingElements.Skip(alreadyProcessed).First().ReplaceWith(variantTargetElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
//no more matching elements in the base XML, this must be a new target
|
||||
finalAiElement.Add(variantTargetElement);
|
||||
}
|
||||
processedTags.Add(tag);
|
||||
var matchInSelf = variantAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
|
||||
var matchInParent = baseAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
|
||||
if (matchInSelf != null && matchInParent != null)
|
||||
{
|
||||
aiTarget.ReplaceWith(new XElement(matchInSelf));
|
||||
}
|
||||
}
|
||||
return newXml;
|
||||
}
|
||||
|
||||
+36
-12
@@ -137,15 +137,14 @@ namespace Barotrauma
|
||||
.Concat(Joints);
|
||||
|
||||
public static string GetDefaultFileName(Identifier speciesName) => $"{speciesName.Value.CapitaliseFirstInvariant()}DefaultRagdoll";
|
||||
public static string GetDefaultFile(Identifier speciesName, ContentPackage contentPackage = null)
|
||||
=> IO.Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml");
|
||||
|
||||
public static string GetFolder(Identifier speciesName, ContentPackage contentPackage = null)
|
||||
public static string GetDefaultFile(Identifier speciesName) => IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName)}.xml");
|
||||
|
||||
public static string GetFolder(Identifier speciesName)
|
||||
{
|
||||
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage));
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (prefab?.ConfigElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: contentPackage);
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
|
||||
return string.Empty;
|
||||
}
|
||||
return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value);
|
||||
@@ -199,10 +198,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab)
|
||||
else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab parentPrefab)
|
||||
{
|
||||
// Ragdoll element not defined -> use the ragdoll defined in the base definition file.
|
||||
ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf);
|
||||
//get the params from the parent prefab if this one doesn't re-define them
|
||||
return GetDefaultRagdollParams<T>(variantOf, parentPrefab.ConfigElement, parentPrefab.ContentPackage);
|
||||
}
|
||||
// Using a null file definition means we use the default animations found in the Ragdolls folder.
|
||||
return GetRagdollParams<T>(speciesName, ragdollSpecies, file: null, contentPackage);
|
||||
@@ -245,7 +244,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage);
|
||||
DebugConsole.ThrowError($"[RagdollParams] Failed to load a ragdoll {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage);
|
||||
}
|
||||
}
|
||||
// Seek the default ragdoll from the character's ragdoll folder.
|
||||
@@ -294,8 +293,30 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harder to debug. It's better to fail early.
|
||||
throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.");
|
||||
string error = $"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.";
|
||||
if (contentPackage == GameMain.VanillaContent)
|
||||
{
|
||||
// Check if the base character content package is vanilla too.
|
||||
CharacterPrefab characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (characterPrefab?.ParentPrefab == null || characterPrefab.ParentPrefab.ContentPackage == GameMain.VanillaContent)
|
||||
{
|
||||
// If the error is in the vanilla content, it's just better to crash early.
|
||||
// If dodging with the solution below fails, we'll also get here.
|
||||
throw new Exception(error);
|
||||
}
|
||||
}
|
||||
// Try to dodge crashing on modded content.
|
||||
DebugConsole.ThrowError(error, contentPackage: contentPackage);
|
||||
if (typeof(T) == typeof(HumanRagdollParams))
|
||||
{
|
||||
Identifier fallbackSpecies = CharacterPrefab.HumanSpeciesName;
|
||||
r = GetRagdollParams<T>(fallbackSpecies, fallbackSpecies, file: ContentPath.FromRaw(contentPackage, "Content/Characters/Human/Ragdolls/HumanDefaultRagdoll.xml"), contentPackage: GameMain.VanillaContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Identifier fallbackSpecies = "crawler".ToIdentifier();
|
||||
r = GetRagdollParams<T>(fallbackSpecies, fallbackSpecies, file: ContentPath.FromRaw(contentPackage, "Content/Characters/Crawler/Ragdolls/CrawlerDefaultRagdoll.xml"), contentPackage: GameMain.VanillaContent);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -869,6 +890,9 @@ namespace Barotrauma
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Can the limb enter submarines? Only valid if the ragdoll's CanEnterSubmarine is set to Partial, otherwise the limb can enter if the ragdoll can."), Editable]
|
||||
public bool CanEnterSubmarine { get; private set; }
|
||||
|
||||
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "When set to something else than None, this limb will be hidden if the limb of the specified type is hidden."), Editable]
|
||||
public LimbType InheritHiding { get; set; }
|
||||
|
||||
public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
|
||||
{
|
||||
var spriteElement = element.GetChildElement("sprite");
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ namespace Barotrauma.Abilities
|
||||
string type = abilityElement.Name.ToString().ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true);
|
||||
abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.Abilities", type, false, true);
|
||||
if (abilityType == null)
|
||||
{
|
||||
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")",
|
||||
|
||||
+13
-5
@@ -19,6 +19,13 @@ namespace Barotrauma.Abilities
|
||||
|
||||
private bool effectBeingApplied;
|
||||
|
||||
/// <summary>
|
||||
/// Should the character who has the ability be marked as the "user" of the status effect?
|
||||
/// Means that e.g. enemies will consider damage from the effect to be coming from the character with the ability, and that the character will gain skills if the effect e.g. heals someone.
|
||||
/// </summary>
|
||||
|
||||
private readonly bool setUser;
|
||||
|
||||
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
|
||||
@@ -27,6 +34,7 @@ namespace Barotrauma.Abilities
|
||||
nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true);
|
||||
nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true);
|
||||
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
|
||||
setUser = abilityElement.GetAttributeBool("setuser", true);
|
||||
}
|
||||
|
||||
protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null)
|
||||
@@ -44,7 +52,7 @@ namespace Barotrauma.Abilities
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
// currently used to spawn items on the targeted character
|
||||
statusEffect.SetUser(targetCharacter);
|
||||
if (setUser) { statusEffect.SetUser(targetCharacter); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
@@ -63,22 +71,22 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
|
||||
}
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null)
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
|
||||
}
|
||||
else
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-3
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -19,10 +19,30 @@ namespace Barotrauma.Abilities
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect()
|
||||
{
|
||||
ApplyEffectToCharacter(Character);
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
if (abilityObject is not IAbilityCharacter character) { return; }
|
||||
character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character);
|
||||
if (abilityObject is IAbilityCharacter characterData)
|
||||
{
|
||||
ApplyEffectToCharacter(characterData.Character);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffectToCharacter(Character character)
|
||||
{
|
||||
character?.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character);
|
||||
}
|
||||
|
||||
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
|
||||
{
|
||||
if (conditionsMatched)
|
||||
{
|
||||
ApplyEffect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -140,7 +140,7 @@ namespace Barotrauma.Abilities
|
||||
string type = conditionElement.Name.ToString().ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true);
|
||||
conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.Abilities", type, false, true);
|
||||
if (conditionType == null)
|
||||
{
|
||||
if (errorMessages)
|
||||
|
||||
@@ -22,6 +22,11 @@ namespace Barotrauma
|
||||
|
||||
public readonly Sprite Icon;
|
||||
|
||||
/// <summary>
|
||||
/// When set to true, this talent will not be visible in the "Extra Talents" panel if it is not part of the character's job talent tree.
|
||||
/// </summary>
|
||||
public readonly bool IsHiddenExtraTalent;
|
||||
|
||||
/// <summary>
|
||||
/// When set to a value the talent tooltip will display a text showing the current value of the stat and the max value.
|
||||
/// For example "Progress: 37/100".
|
||||
@@ -62,6 +67,8 @@ namespace Barotrauma
|
||||
DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
|
||||
}
|
||||
|
||||
IsHiddenExtraTalent = element.GetAttributeBool("ishiddenextratalent", false);
|
||||
|
||||
Description = string.Empty;
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -739,7 +739,14 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type] [use relative strength]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) =>
|
||||
{
|
||||
if (args.Length < 2) { return; }
|
||||
if (args.Length < 2)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
ThrowError("Must give a strength value!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
string affliction = args[0];
|
||||
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction);
|
||||
if (afflictionPrefab == null)
|
||||
@@ -780,9 +787,9 @@ namespace Barotrauma
|
||||
{
|
||||
return new string[][]
|
||||
{
|
||||
AfflictionPrefab.Prefabs.Select(a => a.Name.Value).ToArray(),
|
||||
AfflictionPrefab.Prefabs.Select(a => a.Name.Value).ToArray().Concat(AfflictionPrefab.Prefabs.Select(a => a.Identifier.Value)).ToArray(),
|
||||
new string[] { "1" },
|
||||
Character.CharacterList.Select(c => c.Name).ToArray(),
|
||||
ListCharacterNames(),
|
||||
Enum.GetNames(typeof(LimbType)).ToArray()
|
||||
};
|
||||
}, isCheat: true));
|
||||
@@ -828,7 +835,9 @@ namespace Barotrauma
|
||||
if (character != null)
|
||||
{
|
||||
Dictionary<Identifier, float> treatments = new Dictionary<Identifier, float>();
|
||||
character.CharacterHealth.GetSuitableTreatments(treatments, user: null);
|
||||
character.CharacterHealth.GetSuitableTreatments(treatments, user: null,
|
||||
checkTreatmentThreshold: true,
|
||||
checkTreatmentSuggestionThreshold: false);
|
||||
foreach (var treatment in treatments.OrderByDescending(t => t.Value))
|
||||
{
|
||||
Color color = Color.White;
|
||||
@@ -2783,6 +2792,7 @@ namespace Barotrauma
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
targetCharacter.TeleportTo(worldPosition);
|
||||
targetCharacter.AnimController.BodyInRest = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Barotrauma.PerkBehaviors
|
||||
|
||||
public static bool TryLoadFromXml(ContentXElement element, DisembarkPerkPrefab prefab, [NotNullWhen(true)] out PerkBase? perk)
|
||||
{
|
||||
Type? type = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.PerkBehaviors", element.Name.ToString(), throwOnError: false, ignoreCase: true);
|
||||
Type? type = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.PerkBehaviors", element.Name.ToString(), throwOnError: false, ignoreCase: true);
|
||||
if (type is null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find a perk behavior of the type \"{element.Name}\".", contentPackage: element.ContentPackage);
|
||||
|
||||
@@ -311,6 +311,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
RangedAttackSpeed,
|
||||
|
||||
/// <summary>
|
||||
/// Increases the damage dealt by ranged weapons held by the character by a percentage.
|
||||
/// </summary>
|
||||
RangedAttackMultiplier,
|
||||
|
||||
/// <summary>
|
||||
/// Decreases the reload time of submarine turrets operated by the character by a percentage.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Modifies the win score of a team in the PvP mode.
|
||||
/// </summary>
|
||||
class AddScoreAction : EventAction
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of a target (character) whose team the score should be given to.")]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
[Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: $"Which team's score to add to? Ignored if {nameof(TargetTag)} is set.")]
|
||||
public CharacterTeamType Team { get; set; }
|
||||
|
||||
[Serialize(1, IsPropertySaveable.Yes, description: "How much to add to the score? Can also be negative.")]
|
||||
public int Amount { get; set; }
|
||||
|
||||
public AddScoreAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (Amount == 0)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in {nameof(AddScoreAction)}, event {parentEvent.Prefab.Identifier}: score set to 0, the action will do nothing.", contentPackage: element.ContentPackage);
|
||||
}
|
||||
if (TargetTag.IsEmpty && Team == CharacterTeamType.None)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in {nameof(AddScoreAction)}, event {parentEvent.Prefab.Identifier}: neither {nameof(Team)} or {nameof(TargetTag)} is set.", contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
{
|
||||
return isFinished;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
CharacterTeamType targetTeam = CharacterTeamType.None;
|
||||
if (TargetTag.IsEmpty)
|
||||
{
|
||||
targetTeam = Team;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
if (target is Character character)
|
||||
{
|
||||
targetTeam = character.TeamID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetTeam == CharacterTeamType.None) { return; }
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.GameSession?.Missions is { } missions)
|
||||
{
|
||||
foreach (var mission in missions)
|
||||
{
|
||||
if (mission is CombatMission combatMission)
|
||||
{
|
||||
combatMission.AddToScore(targetTeam, Amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
string target = TargetTag.IsEmpty ? $"team: {Team.ColorizeObject()}" : $"target: {TargetTag}";
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(AddScoreAction)} -> ({target}, amount: {Amount.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -50,7 +50,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!AllowSameEntity && entity == target) { continue; }
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; }
|
||||
if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing))
|
||||
if (ISpatialEntity.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing))
|
||||
{
|
||||
if (!ApplyTagToEntity.IsEmpty)
|
||||
{
|
||||
|
||||
@@ -240,7 +240,7 @@ namespace Barotrauma
|
||||
{
|
||||
humanAI.ClearForcedOrder();
|
||||
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
|
||||
if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
|
||||
if (prevGotoObjective != null && !prevGotoObjective.Abandon) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
|
||||
humanAI.ObjectiveManager.SortObjectives();
|
||||
}
|
||||
}
|
||||
@@ -402,7 +402,7 @@ namespace Barotrauma
|
||||
if (!targets.Any() || IsBlockedByAnotherConversation(targets, BlockOtherConversationsDuration)) { return; }
|
||||
}
|
||||
|
||||
if (targetCharacter != null && IsBlockedByAnotherConversation(targetCharacter.ToEnumerable(), 0.1f)) { return; }
|
||||
if (IsBlockedByAnotherConversation(targetCharacter?.ToEnumerable(), BlockOtherConversationsDuration)) { return; }
|
||||
|
||||
if (speaker?.AIController is HumanAIController humanAI)
|
||||
{
|
||||
|
||||
+8
-2
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
@@ -12,6 +12,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
class WaitForItemUsedAction : EventAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Counter used to ensure we have a unique identifier to use for the ItemComponent.OnUsed event
|
||||
/// </summary>
|
||||
private static int IdCounter;
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must be used. Note that the item needs to have been tagged by the event - this does not refer to the tags that can be set per-item in the sub editor.")]
|
||||
public Identifier ItemTag { get; set; }
|
||||
|
||||
@@ -50,7 +55,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (onUseEventIdentifier.IsEmpty)
|
||||
{
|
||||
onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString()).ToIdentifier();
|
||||
onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString() + IdCounter).ToIdentifier();
|
||||
IdCounter++;
|
||||
}
|
||||
return onUseEventIdentifier;
|
||||
}
|
||||
|
||||
@@ -1018,8 +1018,8 @@ namespace Barotrauma
|
||||
}
|
||||
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
|
||||
{
|
||||
if (character.Submarine != null &&
|
||||
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
|
||||
if (character.Submarine != null && Submarine.MainSub != null &&
|
||||
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
|
||||
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
|
||||
{
|
||||
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
|
||||
|
||||
@@ -160,7 +160,9 @@ namespace Barotrauma
|
||||
#if DEBUG || UNSTABLE
|
||||
if (State == 1 && !level.CheckBeaconActive())
|
||||
{
|
||||
DebugConsole.ThrowError("Beacon became inactive!");
|
||||
DebugConsole.ThrowError(
|
||||
"Debug/unstable only error message: beacon became inactive mid-mission after it had been activated! If this happened unexpectedly while you were away from the beacon, it may be a sign of a bug."+
|
||||
" If possible, please try to check what caused the beacon to go inactive.");
|
||||
State = 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace Barotrauma
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
|
||||
public override int GetBaseReward(Submarine sub)
|
||||
public override float GetBaseReward(Submarine sub)
|
||||
{
|
||||
// If we are not at the location of the mission, skip the calculation of the reward
|
||||
if (GameMain.GameSession?.StartLocation != Locations[0])
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Barotrauma
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
|
||||
public override int GetBaseReward(Submarine sub)
|
||||
public override float GetBaseReward(Submarine sub)
|
||||
{
|
||||
if (sub != missionSub)
|
||||
{
|
||||
|
||||
@@ -244,25 +244,23 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Calculates the base reward, can be overridden for different mission types
|
||||
/// </summary>
|
||||
public virtual int GetBaseReward(Submarine sub)
|
||||
public virtual float GetBaseReward(Submarine sub)
|
||||
{
|
||||
return Prefab.Reward;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the available reward, taking into account universal modifiers such as campaign settings
|
||||
/// Calculates the available monetary reward, taking into account universal modifiers such as campaign settings.
|
||||
/// </summary>
|
||||
public int GetReward(Submarine sub)
|
||||
{
|
||||
int reward = GetBaseReward(sub);
|
||||
|
||||
float reward = GetBaseReward(sub);
|
||||
// Some modifiers should apply universally to all implementations of GetBaseReward
|
||||
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
|
||||
{
|
||||
reward = (int)Math.Round(reward * campaign.Settings.MissionRewardMultiplier);
|
||||
reward *= campaign.Settings.MissionRewardMultiplier;
|
||||
}
|
||||
|
||||
return reward;
|
||||
return (int)Math.Round(reward);
|
||||
}
|
||||
|
||||
public void Start(Level level)
|
||||
@@ -428,15 +426,23 @@ namespace Barotrauma
|
||||
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
|
||||
}
|
||||
|
||||
private float CalculateDifficultyXPMultiplier()
|
||||
{
|
||||
const float minMissionDifficulty = 1;
|
||||
const float maxMissionDifficulty = 4;
|
||||
const float maxXpBonus = 1.3f;
|
||||
float selectedMissionDifficulty = MathUtils.InverseLerp(minMissionDifficulty, maxMissionDifficulty, Prefab.Difficulty.GetValueOrDefault());
|
||||
float xpBonusMultiplier = MathHelper.Lerp(1.0f, maxXpBonus, selectedMissionDifficulty);
|
||||
|
||||
return xpBonusMultiplier;
|
||||
}
|
||||
|
||||
private void GiveReward()
|
||||
{
|
||||
if (GameMain.GameSession.GameMode is not CampaignMode campaign) { return; }
|
||||
int reward = GetReward(Submarine.MainSub);
|
||||
|
||||
float baseExperienceGain = reward * 0.09f;
|
||||
|
||||
float difficultyMultiplier = 1 + level.Difficulty / 100f;
|
||||
baseExperienceGain *= difficultyMultiplier;
|
||||
float xpReward = GetBaseReward(Submarine.MainSub) * Prefab.ExperienceMultiplier * campaign.Settings.ExperienceRewardMultiplier;
|
||||
float xpGain = xpReward * level.LevelData.Biome.ExperienceFromMissionRewards * CalculateDifficultyXPMultiplier();
|
||||
|
||||
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
@@ -444,7 +450,7 @@ namespace Barotrauma
|
||||
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null);
|
||||
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
|
||||
|
||||
DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value));
|
||||
DistributeExperienceToCrew(crewCharacters, (int)(xpGain * experienceGainMultiplier.Value));
|
||||
|
||||
CalculateFinalReward(Submarine.MainSub);
|
||||
#if SERVER
|
||||
|
||||
@@ -101,6 +101,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly int Reward;
|
||||
|
||||
public readonly float ExperienceMultiplier;
|
||||
|
||||
// The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters.
|
||||
public readonly ImmutableArray<LocalizedString> Headers;
|
||||
public readonly ImmutableArray<LocalizedString> Messages;
|
||||
@@ -218,6 +220,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Reward = element.GetAttributeInt("reward", 1);
|
||||
ExperienceMultiplier = element.GetAttributeFloat("experiencemultiplier", 1.0f);
|
||||
AllowRetry = element.GetAttributeBool("allowretry", false);
|
||||
ShowInMenus = element.GetAttributeBool("showinmenus", true);
|
||||
ShowStartMessage = element.GetAttributeBool("showstartmessage", true);
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetBaseReward(Submarine sub)
|
||||
public override float GetBaseReward(Submarine sub)
|
||||
{
|
||||
return alternateReward;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.RuinGeneration;
|
||||
@@ -20,23 +21,14 @@ namespace Barotrauma
|
||||
private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
|
||||
private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
|
||||
private readonly float minTargetDistance;
|
||||
|
||||
|
||||
|
||||
private Ruin TargetRuin { get; set; }
|
||||
|
||||
private bool AllTargetsScanned
|
||||
{
|
||||
get
|
||||
{
|
||||
return scanTargets.Any() && scanTargets.All(kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (State > 0 || scanTargets.None())
|
||||
if (AllTargetsScanned())
|
||||
{
|
||||
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
|
||||
}
|
||||
@@ -234,24 +226,19 @@ namespace Barotrauma
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient) { return; }
|
||||
switch (State)
|
||||
{
|
||||
case 0:
|
||||
if (AllTargetsScanned)
|
||||
{
|
||||
State = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Allow the state to be set higher with MissionStateAction, but not lower.
|
||||
State = Math.Max(State, scanTargets.Count(kvp => kvp.Value));
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted() => State > 0;
|
||||
|
||||
private bool AllTargetsScanned() => State >= targetsToScan;
|
||||
|
||||
protected override bool DetermineCompleted() => AllTargetsScanned();
|
||||
|
||||
protected override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
foreach (var scanner in scanners)
|
||||
{
|
||||
if (scanner.Item != null && !scanner.Item.Removed)
|
||||
if (scanner.Item is { Removed: false })
|
||||
{
|
||||
scanner.OnScanStarted -= OnScanStarted;
|
||||
scanner.OnScanCompleted -= OnScanCompleted;
|
||||
@@ -259,7 +246,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
Reset();
|
||||
failed = !completed && state > 0;
|
||||
failed = !completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,6 +510,17 @@ namespace Barotrauma
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//can't sell items in hidden inventories
|
||||
Item rootContainer = item.Container;
|
||||
while (rootContainer != null)
|
||||
{
|
||||
if (rootContainer.OwnInventory?.Container is { } containerComponent)
|
||||
{
|
||||
if (!containerComponent.DrawInventory) { return false; }
|
||||
if (!containerComponent.IsAccessible()) { return false; }
|
||||
}
|
||||
rootContainer = rootContainer.Container;
|
||||
}
|
||||
if (item.OwnInventory?.Container is ItemContainer itemContainer)
|
||||
{
|
||||
var containedItems = item.ContainedItems;
|
||||
@@ -672,7 +683,8 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Item containedItem in character.Inventory.AllItemsMod)
|
||||
{
|
||||
if (containedItem.OwnInventory != null &&
|
||||
//only put into containers that draw the inventory (not ones with a hidden inventory like circuit boxes!)
|
||||
if (containedItem.OwnInventory?.Container is { DrawInventory: true } &&
|
||||
containedItem.OwnInventory.TryPutItem(item, user: null, item.AllowedSlots))
|
||||
{
|
||||
break;
|
||||
@@ -703,14 +715,23 @@ namespace Barotrauma
|
||||
|
||||
public static void ItemSpawned(Item item)
|
||||
{
|
||||
Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
|
||||
if (sub != null)
|
||||
CharacterTeamType teamID = CharacterTeamType.Team1;
|
||||
if (item.ParentInventory?.Owner is Character character)
|
||||
{
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
teamID = character.TeamID;
|
||||
}
|
||||
else
|
||||
{
|
||||
Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
|
||||
if (sub != null)
|
||||
{
|
||||
wifiComponent.TeamID = sub.TeamID;
|
||||
teamID = sub.TeamID;
|
||||
}
|
||||
}
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
{
|
||||
wifiComponent.TeamID = teamID;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>();
|
||||
|
||||
@@ -81,6 +81,7 @@ namespace Barotrauma
|
||||
|
||||
// Ignore orders work a bit differently since the "unignore" order counters the "ignore" order
|
||||
var isUnignoreOrder = order.Identifier == Tags.UnignoreThis;
|
||||
var isIgnoreOrder = order.Identifier == Tags.IgnoreThis;
|
||||
var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs[Tags.IgnoreThis];
|
||||
ActiveOrder existingOrder = ActiveOrders.Find(o =>
|
||||
o.Order.Prefab == orderPrefab && MatchesTarget(o.Order.TargetEntity, order.TargetEntity) &&
|
||||
@@ -96,6 +97,14 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
ActiveOrders.Remove(existingOrder);
|
||||
if (isIgnoreOrder && order.TargetEntity is Item targetItem)
|
||||
{
|
||||
foreach (var stackedItem in targetItem.GetStackedItems())
|
||||
{
|
||||
ActiveOrders.RemoveAll(o => o.Order.Prefab == orderPrefab && o.Order.TargetEntity == stackedItem);
|
||||
stackedItem.OrderedToBeIgnored = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +133,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
ActiveOrders.Add(new ActiveOrder(order, fadeOutTime));
|
||||
if (isIgnoreOrder && order.TargetEntity is Item targetItem)
|
||||
{
|
||||
foreach (var stackedItem in targetItem.GetStackedItems())
|
||||
{
|
||||
ActiveOrders.Add(new ActiveOrder(order.WithTargetEntity(stackedItem), fadeOutTime));
|
||||
stackedItem.OrderedToBeIgnored = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveOrders.Add(new ActiveOrder(order, fadeOutTime));
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnActiveOrderAdded(order);
|
||||
#endif
|
||||
@@ -554,7 +574,7 @@ namespace Barotrauma
|
||||
|
||||
public static IEnumerable<Character> GetCharactersSortedForOrder(Order order, IEnumerable<Character> characters, Character controlledCharacter, bool includeSelf, IEnumerable<Character> extraCharacters = null)
|
||||
{
|
||||
var filteredCharacters = characters.Where(c => controlledCharacter == null || ((includeSelf || c != controlledCharacter) && c.TeamID == controlledCharacter.TeamID));
|
||||
var filteredCharacters = characters.Where(c => c.Info != null && (controlledCharacter == null || ((includeSelf || c != controlledCharacter) && c.TeamID == controlledCharacter.TeamID)));
|
||||
if (extraCharacters != null)
|
||||
{
|
||||
filteredCharacters = filteredCharacters.Union(extraCharacters);
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Barotrauma
|
||||
string FilePath,
|
||||
Option<SerializableDateTime> SaveTime,
|
||||
string SubmarineName,
|
||||
RespawnMode RespawnMode,
|
||||
ImmutableArray<string> EnabledContentPackageNames) : INetSerializableStruct;
|
||||
|
||||
public const int MaxMoney = int.MaxValue / 2; //about 1 billion
|
||||
@@ -33,6 +34,20 @@ namespace Barotrauma
|
||||
|
||||
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Upgrade, PurchaseSub, MedicalClinic, Cargo }
|
||||
|
||||
/// <summary>
|
||||
/// Should the interaction be disabled if the character's faction is hostile towards the players?
|
||||
/// </summary>
|
||||
public static bool HostileFactionDisablesInteraction(InteractionType interactionType)
|
||||
{
|
||||
return
|
||||
interactionType != InteractionType.None &&
|
||||
//allow interacting with stores, otherwise you could get softlocked
|
||||
//(no way to get enough resources from a hostile outpost to make it to the next one?)
|
||||
interactionType != InteractionType.Store &&
|
||||
//examining is triggered by events, and there may be events that are intended to allow interaction with a hostile NPC.
|
||||
interactionType != InteractionType.Examine;
|
||||
}
|
||||
|
||||
public static bool BlocksInteraction(InteractionType interactionType)
|
||||
{
|
||||
return interactionType != InteractionType.None && interactionType != InteractionType.Cargo;
|
||||
@@ -1099,6 +1114,7 @@ namespace Barotrauma
|
||||
private void NPCInteract(Character npc, Character interactor)
|
||||
{
|
||||
if (!npc.AllowCustomInteract) { return; }
|
||||
if (npc.AIController is HumanAIController humanAi && !humanAi.AllowCampaignInteraction()) { return; }
|
||||
NPCInteractProjSpecific(npc, interactor);
|
||||
string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID);
|
||||
if (!CoroutineManager.IsCoroutineRunning(coroutineName))
|
||||
|
||||
@@ -140,13 +140,14 @@ namespace Barotrauma
|
||||
|
||||
private static readonly Dictionary<string, MultiplierSettings> _multiplierSettings = new Dictionary<string, MultiplierSettings>
|
||||
{
|
||||
{ "default", new MultiplierSettings { Min = 0.2f, Max = 2.0f, Step = 0.1f } },
|
||||
{ nameof(CrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } },
|
||||
{ nameof(NonCrewVitalityMultiplier),new MultiplierSettings { Min = 0.5f, Max = 3.0f, Step = 0.1f } },
|
||||
{ nameof(MissionRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } },
|
||||
{ nameof(RepairFailMultiplier), new MultiplierSettings { Min = 0.5f, Max = 5.0f, Step = 0.5f } },
|
||||
{ nameof(ShopPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } },
|
||||
{ nameof(ShipyardPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } }
|
||||
{ "default", new MultiplierSettings { Min = 0.2f, Max = 2.0f, Step = 0.1f } },
|
||||
{ nameof(CrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } },
|
||||
{ nameof(NonCrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 3.0f, Step = 0.1f } },
|
||||
{ nameof(MissionRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } },
|
||||
{ nameof(ExperienceRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } },
|
||||
{ nameof(RepairFailMultiplier), new MultiplierSettings { Min = 0.5f, Max = 5.0f, Step = 0.5f } },
|
||||
{ nameof(ShopPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } },
|
||||
{ nameof(ShipyardPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } }
|
||||
// Add overrides for default values here
|
||||
};
|
||||
|
||||
@@ -165,6 +166,9 @@ namespace Barotrauma
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize]
|
||||
public float MissionRewardMultiplier { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize]
|
||||
public float ExperienceRewardMultiplier { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize]
|
||||
public float ShopPriceMultiplier { get; set; }
|
||||
|
||||
|
||||
@@ -482,6 +482,7 @@ namespace Barotrauma
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelSeed));
|
||||
LocationType locationType = LocationType.Prefabs
|
||||
.OrderBy(lt => lt.UintIdentifier)
|
||||
.Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier))
|
||||
.GetRandom(rand)!;
|
||||
dummyLocations = CreateDummyLocations(levelSeed, locationType);
|
||||
@@ -1573,13 +1574,15 @@ namespace Barotrauma
|
||||
if (kvp.Key.TryUnwrap(out AccountId? accountId))
|
||||
{
|
||||
permadeathsElement.Add(
|
||||
new XElement("account"),
|
||||
new XElement("account",
|
||||
new XAttribute("id", accountId.StringRepresentation),
|
||||
new XAttribute("permadeathcount", kvp.Value));
|
||||
new XAttribute("permadeathcount", kvp.Value)));
|
||||
}
|
||||
}
|
||||
rootElement.Add(permadeathsElement);
|
||||
|
||||
rootElement.Add(new XAttribute("respawnmode", GameMain.NetworkMember?.ServerSettings?.RespawnMode ?? RespawnMode.None));
|
||||
|
||||
((CampaignMode)GameMode).Save(doc.Root, isSavingOnLoading);
|
||||
|
||||
doc.SaveSafe(filePath, throwExceptions: true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Barotrauma
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum InputType
|
||||
{
|
||||
@@ -7,7 +7,7 @@ namespace Barotrauma
|
||||
Aim,
|
||||
Up, Down, Left, Right,
|
||||
Attack,
|
||||
Run, Crouch,
|
||||
Run, ToggleRun, Crouch,
|
||||
InfoTab, Chat, RadioChat, CrewOrders,
|
||||
Ragdoll, Health, Grab,
|
||||
DropItem,
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components
|
||||
private Gap linkedGap;
|
||||
private bool isOpen;
|
||||
|
||||
private float openState;
|
||||
private float openState, lastOpenState;
|
||||
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
|
||||
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
|
||||
private readonly bool autoOrientGap;
|
||||
@@ -218,6 +218,7 @@ namespace Barotrauma.Items.Components
|
||||
get { return openState; }
|
||||
set
|
||||
{
|
||||
lastOpenState = openState;
|
||||
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
#if CLIENT
|
||||
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
|
||||
@@ -329,13 +330,24 @@ namespace Barotrauma.Items.Components
|
||||
private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
|
||||
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
|
||||
{
|
||||
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
|
||||
if (IsBroken)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (isOpen)
|
||||
{
|
||||
Msg = HasAccess(character) ? "ItemMsgClose" : "ItemMsgForceCloseCrowbar";
|
||||
}
|
||||
else
|
||||
{
|
||||
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
|
||||
}
|
||||
ParseMsg();
|
||||
if (addMessage)
|
||||
{
|
||||
msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
|
||||
msg ??= (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
|
||||
}
|
||||
return isBroken || base.HasRequiredItems(character, addMessage, msg);
|
||||
return base.HasRequiredItems(character, addMessage, msg);
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
@@ -461,12 +473,12 @@ namespace Barotrauma.Items.Components
|
||||
if (PredictedState == null)
|
||||
{
|
||||
OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed);
|
||||
isClosing = openState > 0.0f && openState < 1.0f && !isOpen;
|
||||
isClosing = openState is > 0.0f and < 1.0f && !isOpen;
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenState += deltaTime * ((bool)PredictedState ? OpeningSpeed : -ClosingSpeed);
|
||||
isClosing = openState > 0.0f && openState < 1.0f && !(bool)PredictedState;
|
||||
OpenState += deltaTime * (PredictedState.Value ? OpeningSpeed : -ClosingSpeed);
|
||||
isClosing = openState is > 0.0f and < 1.0f && !PredictedState.Value;
|
||||
|
||||
resetPredictionTimer -= deltaTime;
|
||||
if (resetPredictionTimer <= 0.0f)
|
||||
@@ -479,7 +491,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (isClosing)
|
||||
{
|
||||
if (OpenState < 0.9f) { PushCharactersAway(); }
|
||||
//server gives the clients more leeway on moving through closing doors
|
||||
//latency can often otherwise make a client get blocked by a closing door server-side even if it seemed like they made it through client-side
|
||||
float pushCharactersAwayThreshold = GameMain.NetworkMember is { IsServer: true } ? 0.1f : 0.9f;
|
||||
|
||||
if (OpenState < pushCharactersAwayThreshold) { PushCharactersAway(); }
|
||||
if (CheckSubmarinesInDoorWay())
|
||||
{
|
||||
PredictedState = null;
|
||||
@@ -771,11 +787,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (IsHorizontal)
|
||||
{
|
||||
body.SetTransform(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
|
||||
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
body.SetTransform(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
|
||||
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Holdable : Pickable, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private readonly struct EventData : IEventData
|
||||
private readonly struct AttachEventData : IEventData
|
||||
{
|
||||
public readonly Vector2 AttachPos;
|
||||
|
||||
public EventData(Vector2 attachPos)
|
||||
public readonly Character Attacher;
|
||||
|
||||
public AttachEventData(Vector2 attachPos, Character attacher)
|
||||
{
|
||||
AttachPos = attachPos;
|
||||
Attacher = attacher;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +227,44 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For setting the handle positions using status effects
|
||||
/// </summary>
|
||||
public Vector2 Handle1
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(handlePos[0]); }
|
||||
set
|
||||
{
|
||||
handlePos[0] = ConvertUnits.ToSimUnits(value);
|
||||
if (item.FlippedX)
|
||||
{
|
||||
handlePos[0].X = -handlePos[0].X;
|
||||
}
|
||||
if (!secondHandlePosDefined)
|
||||
{
|
||||
Handle2 = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For setting the handle positions using status effects
|
||||
/// </summary>
|
||||
public Vector2 Handle2
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(handlePos[1]); }
|
||||
set
|
||||
{
|
||||
handlePos[1] = ConvertUnits.ToSimUnits(value);
|
||||
if (item.FlippedX)
|
||||
{
|
||||
handlePos[1].X = -handlePos[1].X;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool secondHandlePosDefined;
|
||||
|
||||
public Holdable(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -254,9 +294,14 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
int index = i - 1;
|
||||
string attributeName = "handle" + i;
|
||||
var attribute = element.GetAttribute(attributeName);
|
||||
// If no value is defind for handle2, use the value of handle1.
|
||||
var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
|
||||
Vector2 value = previousValue;
|
||||
var attribute = element.GetAttribute(attributeName);
|
||||
if (attribute != null)
|
||||
{
|
||||
secondHandlePosDefined = i > 1;
|
||||
value = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value));
|
||||
}
|
||||
handlePos[index] = value;
|
||||
previousValue = value;
|
||||
}
|
||||
@@ -755,21 +800,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
if (character != Character.Controlled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if CLIENT
|
||||
if (character == Character.Controlled)
|
||||
{
|
||||
Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character));
|
||||
item.CreateClientEvent(this, new EventData(attachPos));
|
||||
#endif
|
||||
item.CreateClientEvent(this, new AttachEventData(attachPos, character));
|
||||
}
|
||||
#endif
|
||||
//don't attach at this point in MP: instead rely on the network events created above
|
||||
return false;
|
||||
}
|
||||
else
|
||||
@@ -824,9 +862,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (user.Submarine != null)
|
||||
{
|
||||
//we must add some "padding" to the raycast to ensure it reaches all the way to a wall
|
||||
//otherwise the cursor might be outside a wall, but the grid cell it's in might be partially inside
|
||||
Vector2 padding = Submarine.GridSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
|
||||
|
||||
if (Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(user.Position),
|
||||
ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null)
|
||||
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall) != null)
|
||||
{
|
||||
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ namespace Barotrauma.Items.Components
|
||||
Limb targetLimb = target.UserData as Limb;
|
||||
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
|
||||
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
|
||||
Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item;
|
||||
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
|
||||
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
|
||||
GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target);
|
||||
if (Attack != null)
|
||||
@@ -461,10 +461,9 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (target.UserData is Holdable holdable && holdable.CanPush)
|
||||
else if (target.UserData is Holdable { CanPush: true } holdable)
|
||||
{
|
||||
if (holdable.Item.Removed) { return; }
|
||||
Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f);
|
||||
RestoreCollision();
|
||||
hitting = false;
|
||||
User = null;
|
||||
|
||||
@@ -202,12 +202,26 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
if (requiredTime < float.MaxValue && picker == Character.Controlled)
|
||||
{
|
||||
string text = string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(PickingMsg))
|
||||
{
|
||||
text = PickingMsg;
|
||||
}
|
||||
else if (this is Door door)
|
||||
{
|
||||
text = door.IsClosed ? "progressbar.opening" : "progressbar.closing";
|
||||
}
|
||||
else
|
||||
{
|
||||
text = "progressbar.deattaching";
|
||||
}
|
||||
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
item.WorldPosition,
|
||||
pickTimer / requiredTime,
|
||||
GUIStyle.Red, GUIStyle.Green,
|
||||
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
|
||||
text);
|
||||
}
|
||||
#endif
|
||||
picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
|
||||
|
||||
@@ -296,7 +296,9 @@ namespace Barotrauma.Items.Components
|
||||
//which doesn't support multiple attached ropes (see Holdable.GetRope and the references to it)
|
||||
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
|
||||
}
|
||||
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
|
||||
|
||||
float rangedAttackMultiplier = character?.GetStatValue(StatTypes.RangedAttackMultiplier) ?? 0;
|
||||
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier) + rangedAttackMultiplier) * WeaponDamageModifier;
|
||||
projectile.Launcher = item;
|
||||
|
||||
ignoredBodies.Clear();
|
||||
@@ -306,6 +308,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (l.IsSevered) { continue; }
|
||||
ignoredBodies.Add(l.body.FarseerBody);
|
||||
#if SERVER
|
||||
ignoredBodies.Add(l.LagCompensatedBody.FarseerBody);
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (Item heldItem in character.HeldItems)
|
||||
|
||||
@@ -320,7 +320,7 @@ namespace Barotrauma.Items.Components
|
||||
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
|
||||
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
|
||||
{
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall;
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall | Physics.CollisionItemBlocking;
|
||||
if (!IgnoreCharacters)
|
||||
{
|
||||
collisionCategories |= Physics.CollisionCharacter;
|
||||
@@ -654,8 +654,9 @@ namespace Barotrauma.Items.Components
|
||||
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
|
||||
return true;
|
||||
}
|
||||
else if (targetBody.UserData is Item targetItem)
|
||||
else if (targetBody.UserData is Barotrauma.Item or Holdable)
|
||||
{
|
||||
Item targetItem = targetBody.UserData is Holdable holdable ? holdable.Item : (Item)targetBody.UserData;
|
||||
if (!HitItems || !targetItem.IsInteractable(user)) { return false; }
|
||||
|
||||
var levelResource = targetItem.GetComponent<LevelResource>();
|
||||
|
||||
@@ -77,6 +77,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public readonly ContentXElement originalElement;
|
||||
|
||||
/// <summary>
|
||||
/// The default delay for delayed client-side corrections (see <see cref="StartDelayedCorrection"/>.
|
||||
/// </summary>
|
||||
protected const float CorrectionDelay = 1.0f;
|
||||
protected CoroutineHandle delayedCorrectionCoroutine;
|
||||
|
||||
@@ -669,8 +672,7 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
}
|
||||
|
||||
protected string GetTextureDirectory(ContentXElement subElement)
|
||||
=> subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty;
|
||||
protected string GetTextureDirectory(ContentXElement subElement) => item.Prefab.GetTexturePath(subElement, item.Prefab.ParentPrefab);
|
||||
|
||||
public bool HasRequiredSkills(Character character)
|
||||
{
|
||||
|
||||
@@ -108,7 +108,7 @@ 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(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
|
||||
[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
|
||||
{
|
||||
get;
|
||||
@@ -923,6 +923,8 @@ namespace Barotrauma.Items.Components
|
||||
#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods.
|
||||
public void SetContainedItemPositions()
|
||||
{
|
||||
if (containedItems.Count == 0) { return; }
|
||||
|
||||
var rootBody = item.RootContainer?.body ?? item.body;
|
||||
|
||||
Vector2 transformedItemPos = GetContainedPosition(
|
||||
@@ -989,8 +991,7 @@ namespace Barotrauma.Items.Components
|
||||
rotation += -item.RotationRad;
|
||||
}
|
||||
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
|
||||
contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation);
|
||||
contained.Item.body.UpdateDrawPosition();
|
||||
contained.Item.body.UpdateDrawPosition(interpolate: false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -220,6 +220,10 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
private bool forceSelectNextFrame;
|
||||
|
||||
private float userCanInteractCheckTimer;
|
||||
|
||||
private const float UserCanInteractCheckInterval = 1.0f;
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
this.cam = cam;
|
||||
@@ -238,13 +242,15 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
forceSelectNextFrame = false;
|
||||
|
||||
userCanInteractCheckTimer -= deltaTime;
|
||||
|
||||
if (user == null
|
||||
|| user.Removed
|
||||
|| !user.IsAnySelectedItem(item)
|
||||
|| (item.ParentInventory != null && !IsAttachedUser(user))
|
||||
|| !user.CanInteractWith(item)
|
||||
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|
||||
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
|
||||
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater)
|
||||
|| !CheckUserCanInteract())
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
@@ -368,6 +374,22 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckUserCanInteract()
|
||||
{
|
||||
//optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame
|
||||
if (user != null)
|
||||
{
|
||||
if (userCanInteractCheckTimer <= 0.0f)
|
||||
{
|
||||
userCanInteractCheckTimer = UserCanInteractCheckInterval;
|
||||
return user.CanInteractWith(item);
|
||||
}
|
||||
}
|
||||
//we only do the actual check every UserCanInteractCheckInterval seconds
|
||||
//can mean the component can stay selected for <1s after the user no longer has access to it
|
||||
return true;
|
||||
}
|
||||
|
||||
private double lastUsed;
|
||||
|
||||
public override bool Use(float deltaTime, Character activator = null)
|
||||
|
||||
@@ -499,12 +499,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
|
||||
}
|
||||
InvSlotType invSlot = fabricatedItem.MoveToSlot;
|
||||
if (i < amountFittingContainer)
|
||||
{
|
||||
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality,
|
||||
onSpawned: (Item spawnedItem) =>
|
||||
{
|
||||
onItemSpawned(spawnedItem, tempUser);
|
||||
onItemSpawned(spawnedItem, tempUser, invSlot);
|
||||
spawnedItem.Quality = quality;
|
||||
spawnedItem.StolenDuringRound = ingredientsStolen;
|
||||
spawnedItem.AllowStealing = ingredientsAllowStealing;
|
||||
@@ -517,7 +518,7 @@ namespace Barotrauma.Items.Components
|
||||
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality,
|
||||
onSpawned: (Item spawnedItem) =>
|
||||
{
|
||||
onItemSpawned(spawnedItem, tempUser);
|
||||
onItemSpawned(spawnedItem, tempUser, invSlot);
|
||||
spawnedItem.Quality = quality;
|
||||
spawnedItem.StolenDuringRound = ingredientsStolen;
|
||||
spawnedItem.AllowStealing = ingredientsAllowStealing;
|
||||
@@ -527,15 +528,28 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
void onItemSpawned(Item spawnedItem, Character user)
|
||||
void onItemSpawned(Item spawnedItem, Character user, InvSlotType slot)
|
||||
{
|
||||
if (user != null && user.TeamID != CharacterTeamType.None)
|
||||
CharacterTeamType teamID = CharacterTeamType.None;
|
||||
if (user != null)
|
||||
{
|
||||
teamID = user.TeamID;
|
||||
}
|
||||
else if (item.Submarine != null)
|
||||
{
|
||||
teamID = item.Submarine.TeamID;
|
||||
}
|
||||
if (teamID != CharacterTeamType.None)
|
||||
{
|
||||
foreach (WifiComponent wifiComponent in spawnedItem.GetComponents<WifiComponent>())
|
||||
{
|
||||
wifiComponent.TeamID = user.TeamID;
|
||||
wifiComponent.TeamID = teamID;
|
||||
}
|
||||
}
|
||||
if (slot != InvSlotType.None)
|
||||
{
|
||||
user?.Inventory.TryPutItem(spawnedItem, user, slot.ToEnumerable());
|
||||
}
|
||||
OnItemFabricated?.Invoke(spawnedItem, user);
|
||||
}
|
||||
if (user?.Info != null && !user.Removed)
|
||||
@@ -562,7 +576,6 @@ namespace Barotrauma.Items.Components
|
||||
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components
|
||||
private set
|
||||
{
|
||||
if (lastUser == value) { return; }
|
||||
if (Screen.Selected.IsEditor) { return; }
|
||||
lastUser = value;
|
||||
if (lastUser == null)
|
||||
{
|
||||
@@ -246,6 +247,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
//rapidly adjust the reactor in the first few seconds of the round to prevent overvoltages if the load changed between rounds
|
||||
//(unless the reactor is being operated by a player)
|
||||
if (GameMain.GameSession is { RoundDuration: <5 } && lastUser is not { IsPlayer: true })
|
||||
{
|
||||
UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (PowerOn && AvailableFuel < 1)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Sonar : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
public static List<Sonar> SonarList = new List<Sonar>();
|
||||
|
||||
public enum Mode
|
||||
{
|
||||
Active,
|
||||
@@ -167,6 +169,7 @@ namespace Barotrauma.Items.Components
|
||||
IsActive = true;
|
||||
InitProjSpecific(element);
|
||||
CurrentMode = Mode.Passive;
|
||||
SonarList.Add(this);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
@@ -379,6 +382,29 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
#if CLIENT
|
||||
sonarBlip?.Remove();
|
||||
pingCircle?.Remove();
|
||||
directionalPingCircle?.Remove();
|
||||
screenOverlay?.Remove();
|
||||
screenBackground?.Remove();
|
||||
lineSprite?.Remove();
|
||||
|
||||
foreach (var t in targetIcons.Values)
|
||||
{
|
||||
t.Item1.Remove();
|
||||
}
|
||||
targetIcons.Clear();
|
||||
|
||||
MineralClusters = null;
|
||||
#endif
|
||||
SonarList.Remove(this);
|
||||
}
|
||||
|
||||
|
||||
public void ServerEventRead(IReadMessage msg, Client c)
|
||||
{
|
||||
bool isActive = msg.ReadBoolean();
|
||||
|
||||
@@ -232,7 +232,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
|
||||
|
||||
Overload = Voltage > maxOverVoltage;
|
||||
Overload = Voltage > maxOverVoltage && GameMain.GameSession is not { RoundDuration: < 5 };
|
||||
|
||||
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
|
||||
@@ -157,6 +157,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; }
|
||||
}
|
||||
|
||||
if (this is PowerTransfer && item.Condition <= 0.0f)
|
||||
{
|
||||
//if the junction box or other power transfer device is broken,
|
||||
//it cannot be supplying any power (voltage = 0)
|
||||
return 0.0f;
|
||||
}
|
||||
return PowerConsumption <= 0.0f ? 1.0f : voltage;
|
||||
}
|
||||
set
|
||||
|
||||
@@ -440,6 +440,14 @@ namespace Barotrauma.Items.Components
|
||||
//can't launch if already launched
|
||||
if (StickTarget != null || IsActive) { return false; }
|
||||
|
||||
#if SERVER
|
||||
var owner = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == User);
|
||||
if (owner != null)
|
||||
{
|
||||
Limb.SetLagCompensatedBodyPositions(owner);
|
||||
}
|
||||
#endif
|
||||
|
||||
float initialRotation = item.body.Rotation;
|
||||
//if the item is being launched from an inventory, assume it's being fired by a gun that handles setting the rotation correctly
|
||||
//but if the item is e.g. being thrown by a character, we need to take the direction into account
|
||||
@@ -461,10 +469,10 @@ namespace Barotrauma.Items.Components
|
||||
spreadIndex++;
|
||||
|
||||
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
|
||||
Vector2 prevSimpos = item.SimPosition;
|
||||
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
|
||||
if (Hitscan)
|
||||
{
|
||||
Vector2 prevSimpos = item.SimPosition;
|
||||
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
|
||||
DoHitscan(launchDir);
|
||||
if (i < HitScanCount - 1)
|
||||
{
|
||||
@@ -473,7 +481,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
item.body.SetTransform(item.body.SimPosition, launchAngle);
|
||||
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
|
||||
DoLaunch(launchDir * modifiedLaunchImpulse);
|
||||
}
|
||||
@@ -670,8 +677,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (fixture.Body.UserData is VineTile) { return true; }
|
||||
if (fixture.CollidesWith == Category.None) { return true; }
|
||||
//only collides with characters = probably an "outsideCollisionBlocker" created by a gap
|
||||
if (fixture.CollidesWith == Physics.CollisionCharacter) { return true; }
|
||||
|
||||
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; }
|
||||
|
||||
@@ -690,6 +695,11 @@ namespace Barotrauma.Items.Components
|
||||
if (item.Condition <= 0) { return true; }
|
||||
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return true; }
|
||||
}
|
||||
else if (fixture.Body.UserData is Gap)
|
||||
{
|
||||
//an "outsideCollisionBlocker" created by a gap, should never collide
|
||||
return true;
|
||||
}
|
||||
else if (fixture.Body.UserData is Holdable { CanPush: false })
|
||||
{
|
||||
// Ignore holdables that can't push -> shouldn't block
|
||||
@@ -724,14 +734,17 @@ namespace Barotrauma.Items.Components
|
||||
return -1;
|
||||
}
|
||||
if (fixture.Body.UserData is VineTile) { return -1; }
|
||||
if (fixture.CollidesWith == Category.None) { return -1; }
|
||||
//only collides with characters = probably an "outsideCollisionBlocker" created by a gap
|
||||
if (fixture.CollidesWith == Physics.CollisionCharacter) { return -1; }
|
||||
if (fixture.CollidesWith == Category.None && fixture.CollisionCategories != Physics.CollisionLagCompensationBody) { return -1; }
|
||||
if (fixture.Body.UserData is Item item)
|
||||
{
|
||||
if (item.Condition <= 0) { return -1; }
|
||||
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return -1; }
|
||||
}
|
||||
else if (fixture.Body.UserData is Gap)
|
||||
{
|
||||
//an "outsideCollisionBlocker" created by a gap, should never collide
|
||||
return -1;
|
||||
}
|
||||
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
|
||||
|
||||
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
|
||||
@@ -779,7 +792,7 @@ namespace Barotrauma.Items.Components
|
||||
hits.Add(new HitscanResult(fixture, point, normal, fraction));
|
||||
|
||||
return 1;
|
||||
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
|
||||
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile | Physics.CollisionLagCompensationBody);
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace Barotrauma.Items.Components
|
||||
private Vector2 detectOffset;
|
||||
|
||||
private float updateTimer;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
|
||||
public bool MotionDetected { get; set; }
|
||||
|
||||
[Flags]
|
||||
public enum TargetType
|
||||
@@ -26,14 +29,25 @@ namespace Barotrauma.Items.Components
|
||||
Any = Human | Monster | Wall | Pet,
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
|
||||
public bool MotionDetected { get; set; }
|
||||
|
||||
private bool triggerFromHumans = true;
|
||||
private bool triggerFromPets = true;
|
||||
private bool triggerFromMonsters = true;
|
||||
private TargetType _target;
|
||||
|
||||
[InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)]
|
||||
public TargetType Target
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get => _target;
|
||||
set
|
||||
{
|
||||
if (_target != value)
|
||||
{
|
||||
_target = value;
|
||||
triggerFromHumans = Target.HasFlag(TargetType.Human);
|
||||
triggerFromPets = Target.HasFlag(TargetType.Pet);
|
||||
triggerFromMonsters = Target.HasFlag(TargetType.Monster);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize("", IsPropertySaveable.Yes, description: "Does the sensor react only to certain characters (species names, groups or tags)? Doesn't have an effect, if the Target Type is incorrect.", alwaysUseInstanceValues: true)]
|
||||
@@ -263,10 +277,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool triggerFromHumans = Target.HasFlag(TargetType.Human);
|
||||
bool triggerFromPets = Target.HasFlag(TargetType.Pet);
|
||||
bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
|
||||
|
||||
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
|
||||
if (!hasTriggers) { return; }
|
||||
foreach (Character character in Character.CharacterList)
|
||||
@@ -299,9 +310,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public bool TriggersOn(Character character)
|
||||
{
|
||||
bool triggerFromHumans = Target.HasFlag(TargetType.Human);
|
||||
bool triggerFromPets = Target.HasFlag(TargetType.Pet);
|
||||
bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
|
||||
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
|
||||
if (!hasTriggers) { return false; }
|
||||
return TriggersOn(character, triggerFromHumans, triggerFromPets, triggerFromMonsters);
|
||||
|
||||
@@ -80,7 +80,6 @@ namespace Barotrauma.Items.Components
|
||||
set
|
||||
{
|
||||
isOn = value;
|
||||
CanTransfer = value;
|
||||
if (!isOn)
|
||||
{
|
||||
currPowerConsumption = 0.0f;
|
||||
|
||||
@@ -96,8 +96,20 @@ namespace Barotrauma.Items.Components
|
||||
[Editable, Serialize("> ", IsPropertySaveable.Yes)]
|
||||
public string LineStartSymbol { get; set; }
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.No)]
|
||||
public bool Readonly { get; set; }
|
||||
private bool _readonly;
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes)]
|
||||
public bool Readonly
|
||||
{
|
||||
get => _readonly;
|
||||
set
|
||||
{
|
||||
_readonly = value;
|
||||
#if CLIENT
|
||||
RefreshInputElements();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
public bool AutoScrollToBottom { get; set; }
|
||||
|
||||
@@ -231,17 +231,22 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("wifiSignalTransmitted", this, signal, sentFromChat);
|
||||
|
||||
if (should != null && should.Value)
|
||||
return;
|
||||
|
||||
if (sentFromChat)
|
||||
{
|
||||
item.LastSentSignalRecipients.Clear();
|
||||
}
|
||||
if (should != null && should.Value) { return; }
|
||||
|
||||
bool chatMsgSent = false;
|
||||
|
||||
var receivers = GetReceiversInRange();
|
||||
if (sentFromChat)
|
||||
{
|
||||
//if sent from chat, we need to reset the "signal chain" at this point
|
||||
//so we can correctly detect which components the signal has already passed through to avoid infinite loops
|
||||
//only relevant for signals originating from the chat - normally this is handled in Item.SendSignal
|
||||
item.LastSentSignalRecipients.Clear();
|
||||
foreach (WifiComponent receiver in receivers)
|
||||
{
|
||||
receiver.item.LastSentSignalRecipients.Clear();
|
||||
}
|
||||
}
|
||||
foreach (WifiComponent wifiComp in receivers)
|
||||
{
|
||||
if (sentFromChat && !wifiComp.LinkToChat) { continue; }
|
||||
|
||||
@@ -1051,7 +1051,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (TargetItems)
|
||||
{
|
||||
foreach (Item targetItem in Item.ItemList)
|
||||
foreach (Item targetItem in Item.TurretTargetItems)
|
||||
{
|
||||
if (!IsValidTarget(targetItem)) { continue; }
|
||||
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
|
||||
@@ -1395,7 +1395,7 @@ namespace Barotrauma.Items.Components
|
||||
closestDistance = dist / priority;
|
||||
currentTarget = closestEnemy;
|
||||
}
|
||||
foreach (Item targetItem in Item.ItemList)
|
||||
foreach (Item targetItem in Item.TurretTargetItems)
|
||||
{
|
||||
if (!IsValidTarget(targetItem)) { continue; }
|
||||
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
|
||||
@@ -1767,8 +1767,15 @@ namespace Barotrauma.Items.Components
|
||||
Submarine sub = e.Submarine ?? e as Submarine;
|
||||
if (sub == null) { return true; }
|
||||
if (sub == Item.Submarine) { return false; }
|
||||
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; }
|
||||
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
|
||||
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; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
|
||||
}
|
||||
}
|
||||
else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true })
|
||||
{
|
||||
@@ -1786,6 +1793,8 @@ namespace Barotrauma.Items.Components
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
|
||||
if (f.CollidesWith == Physics.CollisionNone) { return false; }
|
||||
if (f.Body.UserData == item) { return false; }
|
||||
if (f.UserData is Hull) { return false; }
|
||||
return !item.StaticFixtures.Contains(f);
|
||||
});
|
||||
|
||||
@@ -165,10 +165,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (element.DoesAttributeReferenceFileNameAlone("texture"))
|
||||
{
|
||||
var basePrefab = WearableComponent.Item.Prefab.ParentPrefab ?? WearableComponent.Item.Prefab;
|
||||
string textureName = element.GetAttributeString("texture", "");
|
||||
return ContentPath.FromRaw(
|
||||
element.ContentPackage,
|
||||
$"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{textureName}");
|
||||
$"{Path.GetDirectoryName(basePrefab.FilePath)}/{textureName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -331,13 +331,16 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<Item> GetAllItems(bool checkForDuplicates)
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
foreach (var item in slots[i].Items)
|
||||
var items = slots[i].Items;
|
||||
// ReSharper disable once ForCanBeConvertedToForeach, because this is performance-sensitive code.
|
||||
for (int j = 0; j < items.Count; j++)
|
||||
{
|
||||
var item = items[j];
|
||||
if (item == null)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -349,9 +352,9 @@ namespace Barotrauma
|
||||
if (checkForDuplicates)
|
||||
{
|
||||
bool duplicateFound = false;
|
||||
for (int j = 0; j < i; j++)
|
||||
for (int s = 0; s < i; s++)
|
||||
{
|
||||
if (slots[j].Items.Contains(item))
|
||||
if (slots[s].Items.Contains(item))
|
||||
{
|
||||
duplicateFound = true;
|
||||
break;
|
||||
@@ -364,7 +367,7 @@ namespace Barotrauma
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyItemComponentsOfChange()
|
||||
@@ -420,12 +423,17 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsIndexInRange(int index)
|
||||
{
|
||||
return index >= 0 && index < slots.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the item stored in the specified inventory slot. If the slot contains a stack of items, returns the first item in the stack.
|
||||
/// </summary>
|
||||
public Item GetItemAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= slots.Length) { return null; }
|
||||
if (!IsIndexInRange(index)) { return null; }
|
||||
return slots[index].FirstOrDefault();
|
||||
}
|
||||
|
||||
@@ -434,14 +442,13 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public IEnumerable<Item> GetItemsAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= slots.Length) { return Enumerable.Empty<Item>(); }
|
||||
if (!IsIndexInRange(index)) { return Enumerable.Empty<Item>(); }
|
||||
return slots[index].Items;
|
||||
}
|
||||
|
||||
public int GetItemStackSlotIndex(Item item, int index)
|
||||
{
|
||||
if (index < 0 || index >= slots.Length) { return -1; }
|
||||
|
||||
if (!IsIndexInRange(index)) { return -1; }
|
||||
return slots[index].Items.IndexOf(item);
|
||||
}
|
||||
|
||||
@@ -476,7 +483,7 @@ namespace Barotrauma
|
||||
public virtual bool ItemOwnsSelf(Item item)
|
||||
{
|
||||
if (Owner == null) { return false; }
|
||||
if (!(Owner is Item)) { return false; }
|
||||
if (Owner is not Item) { return false; }
|
||||
Item ownerItem = Owner as Item;
|
||||
if (ownerItem == item) { return true; }
|
||||
if (ownerItem.ParentInventory == null) { return false; }
|
||||
@@ -519,7 +526,7 @@ namespace Barotrauma
|
||||
public virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
|
||||
{
|
||||
if (ItemOwnsSelf(item)) { return false; }
|
||||
if (i < 0 || i >= slots.Length) { return false; }
|
||||
if (!IsIndexInRange(i)) { return false; }
|
||||
return slots[i].CanBePut(item, ignoreCondition);
|
||||
}
|
||||
|
||||
@@ -539,7 +546,7 @@ namespace Barotrauma
|
||||
|
||||
public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null, int? quality = null)
|
||||
{
|
||||
if (i < 0 || i >= slots.Length) { return false; }
|
||||
if (!IsIndexInRange(i)) { return false; }
|
||||
return slots[i].CanProbablyBePut(itemPrefab, condition, quality);
|
||||
}
|
||||
|
||||
@@ -555,7 +562,7 @@ namespace Barotrauma
|
||||
|
||||
public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false)
|
||||
{
|
||||
if (i < 0 || i >= slots.Length) { return 0; }
|
||||
if (!IsIndexInRange(i)) { return 0; }
|
||||
return slots[i].HowManyCanBePut(itemPrefab, condition: condition, ignoreItemsInSlot: ignoreItemsInSlot);
|
||||
}
|
||||
|
||||
@@ -573,7 +580,7 @@ namespace Barotrauma
|
||||
|
||||
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
|
||||
{
|
||||
if (i < 0 || i >= slots.Length)
|
||||
if (!IsIndexInRange(i))
|
||||
{
|
||||
string thisItemStr = item?.Prefab.Identifier.Value ?? "null";
|
||||
string ownerStr = "null";
|
||||
@@ -637,7 +644,7 @@ namespace Barotrauma
|
||||
|
||||
protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true)
|
||||
{
|
||||
if (i < 0 || i >= slots.Length)
|
||||
if (!IsIndexInRange(i))
|
||||
{
|
||||
string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
@@ -1097,10 +1104,16 @@ namespace Barotrauma
|
||||
|
||||
public bool IsInSlot(Item item, int index)
|
||||
{
|
||||
if (index < 0 || index >= slots.Length) { return false; }
|
||||
if (!IsIndexInRange(index)) { return false; }
|
||||
return slots[index].Contains(item);
|
||||
}
|
||||
|
||||
public bool IsSlotEmpty(int index)
|
||||
{
|
||||
if (!IsIndexInRange(index)) { return false; }
|
||||
return slots[index].Empty();
|
||||
}
|
||||
|
||||
public void SharedRead(IReadMessage msg, List<ushort>[] receivedItemIds, out bool readyToApply)
|
||||
{
|
||||
byte start = msg.ReadByte();
|
||||
|
||||
@@ -24,42 +24,66 @@ namespace Barotrauma
|
||||
{
|
||||
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable
|
||||
{
|
||||
#region Lists
|
||||
|
||||
/// <summary>
|
||||
/// A list of every item that exists somewhere in the world. Note that there can be a huge number of items in the list,
|
||||
/// and you probably shouldn't be enumerating it to find some that match some specific criteria (unless that's done very, very sparsely or during initialization).
|
||||
/// </summary>
|
||||
public static readonly List<Item> ItemList = new List<Item>();
|
||||
|
||||
private static readonly HashSet<Item> dangerousItems = new HashSet<Item>();
|
||||
private static readonly HashSet<Item> _dangerousItems = new HashSet<Item>();
|
||||
|
||||
public static IReadOnlyCollection<Item> DangerousItems { get { return dangerousItems; } }
|
||||
public static IReadOnlyCollection<Item> DangerousItems => _dangerousItems;
|
||||
|
||||
private static readonly List<Item> repairableItems = new List<Item>();
|
||||
private static readonly List<Item> _repairableItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that have one more more Repairable component
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> RepairableItems => repairableItems;
|
||||
public static IReadOnlyCollection<Item> RepairableItems => _repairableItems;
|
||||
|
||||
private static readonly List<Item> cleanableItems = new List<Item>();
|
||||
private static readonly List<Item> _cleanableItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container)
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> CleanableItems => cleanableItems;
|
||||
public static IReadOnlyCollection<Item> CleanableItems => _cleanableItems;
|
||||
|
||||
private static readonly HashSet<Item> deconstructItems = new HashSet<Item>();
|
||||
private static readonly HashSet<Item> _deconstructItems = new HashSet<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that have been marked for deconstruction
|
||||
/// </summary>
|
||||
public static HashSet<Item> DeconstructItems => deconstructItems;
|
||||
public static HashSet<Item> DeconstructItems => _deconstructItems;
|
||||
|
||||
private static readonly List<Item> sonarVisibleItems = new List<Item>();
|
||||
private static readonly List<Item> _sonarVisibleItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items whose <see cref="ItemPrefab.SonarSize"/> is larger than 0
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> SonarVisibleItems => sonarVisibleItems;
|
||||
public static IReadOnlyCollection<Item> SonarVisibleItems => _sonarVisibleItems;
|
||||
|
||||
private static readonly List<Item> _turretTargetItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items whose <see cref="ItemPrefab.IsAITurretTarget"/> is true.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> TurretTargetItems => _turretTargetItems;
|
||||
|
||||
private static readonly List<Item> _chairItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that have the tag <see cref="Tags.ChairItem"/>. Which is an oddly specific thing, but useful as an optimization for NPC AI.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> ChairItems => _chairItems;
|
||||
|
||||
#endregion
|
||||
|
||||
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
|
||||
|
||||
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
|
||||
|
||||
public static bool ShowLinks = true;
|
||||
|
||||
private HashSet<Identifier> tags;
|
||||
@@ -104,8 +128,9 @@ namespace Barotrauma
|
||||
#endif
|
||||
|
||||
//components that determine the functionality of the item
|
||||
private readonly Dictionary<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
|
||||
private readonly Dictionary<Type, List<ItemComponent>> componentsByType = new Dictionary<Type, List<ItemComponent>>();
|
||||
private readonly List<ItemComponent> components;
|
||||
|
||||
/// <summary>
|
||||
/// Components that are Active or need to be updated for some other reason (status effects, sounds)
|
||||
/// </summary>
|
||||
@@ -496,6 +521,22 @@ namespace Barotrauma
|
||||
Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight);
|
||||
}
|
||||
|
||||
//need to update to get the position of the physics body to match the new center of the item
|
||||
if (body != null)
|
||||
{
|
||||
if (FullyInitialized)
|
||||
{
|
||||
//fully intialized = scaling after the item has been created
|
||||
//if this happens in the editor, refresh the transform to get the rect to match the position of the physics body
|
||||
if (Screen.Selected is { IsEditor: true }) { UpdateTransform(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
//scaling during loading -> move the body to the new center of the rect
|
||||
body.SetTransformIgnoreContacts(ConvertUnits.ToSimUnits(base.Position), body.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
if (components != null)
|
||||
{
|
||||
foreach (ItemComponent component in components)
|
||||
@@ -1303,9 +1344,11 @@ namespace Barotrauma
|
||||
|
||||
InsertToList();
|
||||
ItemList.Add(this);
|
||||
if (Prefab.IsDangerous) { dangerousItems.Add(this); }
|
||||
if (Repairables.Any()) { repairableItems.Add(this); }
|
||||
if (Prefab.SonarSize > 0.0f) { sonarVisibleItems.Add(this); }
|
||||
if (Prefab.IsDangerous) { _dangerousItems.Add(this); }
|
||||
if (Repairables.Any()) { _repairableItems.Add(this); }
|
||||
if (Prefab.SonarSize > 0.0f) { _sonarVisibleItems.Add(this); }
|
||||
if (Prefab.IsAITurretTarget) { _turretTargetItems.Add(this); }
|
||||
if (Prefab.Tags.Contains(Barotrauma.Tags.ChairItem)) { _chairItems.Add(this); }
|
||||
CheckCleanable();
|
||||
|
||||
DebugConsole.Log("Created " + Name + " (" + ID + ")");
|
||||
@@ -1487,17 +1530,24 @@ namespace Barotrauma
|
||||
};
|
||||
|
||||
Type type = component.GetType();
|
||||
if (!componentsByType.ContainsKey(type))
|
||||
CacheComponent(type);
|
||||
Type baseType = type.BaseType;
|
||||
while (baseType != null)
|
||||
{
|
||||
componentsByType.Add(type, component);
|
||||
Type baseType = type.BaseType;
|
||||
while (baseType != null && baseType != typeof(ItemComponent))
|
||||
CacheComponent(baseType);
|
||||
baseType = baseType.BaseType;
|
||||
}
|
||||
|
||||
void CacheComponent(Type t)
|
||||
{
|
||||
if (!componentsByType.TryGetValue(t, out List<ItemComponent> cachedComponents))
|
||||
{
|
||||
if (!componentsByType.ContainsKey(baseType))
|
||||
{
|
||||
componentsByType.Add(baseType, component);
|
||||
}
|
||||
baseType = baseType.BaseType;
|
||||
cachedComponents = new List<ItemComponent>();
|
||||
componentsByType.Add(t, cachedComponents);
|
||||
}
|
||||
if (!cachedComponents.Contains(component))
|
||||
{
|
||||
cachedComponents.Add(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1534,15 +1584,11 @@ namespace Barotrauma
|
||||
|
||||
public T GetComponent<T>() where T : ItemComponent
|
||||
{
|
||||
if (componentsByType.TryGetValue(typeof(T), out ItemComponent component))
|
||||
if (componentsByType.TryGetValue(typeof(T), out List<ItemComponent> matchingComponents))
|
||||
{
|
||||
return (T)component;
|
||||
return (T)matchingComponents.First();
|
||||
}
|
||||
if (typeof(T) == typeof(ItemComponent))
|
||||
{
|
||||
return (T)components.FirstOrDefault();
|
||||
}
|
||||
return default;
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetComponents<T>()
|
||||
@@ -1551,8 +1597,11 @@ namespace Barotrauma
|
||||
{
|
||||
return components.Cast<T>();
|
||||
}
|
||||
if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty<T>(); }
|
||||
return components.Where(c => c is T).Cast<T>();
|
||||
if (componentsByType.TryGetValue(typeof(T), out List<ItemComponent> matchingComponents))
|
||||
{
|
||||
return matchingComponents.Cast<T>();
|
||||
}
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
public float GetQualityModifier(Quality.StatType statType)
|
||||
@@ -1563,7 +1612,7 @@ namespace Barotrauma
|
||||
public void RemoveContained(Item contained)
|
||||
{
|
||||
ownInventory?.RemoveItem(contained);
|
||||
contained.Container = null;
|
||||
contained.Container = null;
|
||||
}
|
||||
|
||||
public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true)
|
||||
@@ -1588,14 +1637,7 @@ namespace Barotrauma
|
||||
try
|
||||
{
|
||||
#endif
|
||||
if (!body.PhysEnabled || Submarine.Unloading)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
|
||||
}
|
||||
else
|
||||
{
|
||||
body.SetTransform(simPosition, rotation, setPrevTransform);
|
||||
}
|
||||
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
|
||||
#if DEBUG
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -1651,14 +1693,14 @@ namespace Barotrauma
|
||||
Prefab.PreferredContainers.Any() &&
|
||||
(container == null || container.HasTag(Barotrauma.Tags.AllowCleanup)))
|
||||
{
|
||||
if (!cleanableItems.Contains(this))
|
||||
if (!_cleanableItems.Contains(this))
|
||||
{
|
||||
cleanableItems.Add(this);
|
||||
_cleanableItems.Add(this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cleanableItems.Remove(this);
|
||||
_cleanableItems.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1847,9 +1889,9 @@ namespace Barotrauma
|
||||
|
||||
public void SetContainedItemPositions()
|
||||
{
|
||||
foreach (ItemComponent component in components)
|
||||
foreach (var ownInventory in OwnInventories)
|
||||
{
|
||||
(component as ItemContainer)?.SetContainedItemPositions();
|
||||
ownInventory.Container.SetContainedItemPositions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2508,15 +2550,15 @@ namespace Barotrauma
|
||||
|
||||
if (Submarine == null && prevSub != null)
|
||||
{
|
||||
body.SetTransform(body.SimPosition + prevSub.SimPosition, body.Rotation);
|
||||
body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition, body.Rotation);
|
||||
}
|
||||
else if (Submarine != null && prevSub == null)
|
||||
{
|
||||
body.SetTransform(body.SimPosition - Submarine.SimPosition, body.Rotation);
|
||||
body.SetTransformIgnoreContacts(body.SimPosition - Submarine.SimPosition, body.Rotation);
|
||||
}
|
||||
else if (Submarine != null && prevSub != null && Submarine != prevSub)
|
||||
{
|
||||
body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation);
|
||||
body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation);
|
||||
}
|
||||
|
||||
if (Submarine != prevSub)
|
||||
@@ -3416,7 +3458,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (setTransform)
|
||||
{
|
||||
body.SetTransform(dropper.SimPosition, 0.0f);
|
||||
body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4091,7 +4133,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (element.GetAttributeBool("markedfordeconstruction", false)) { deconstructItems.Add(item); }
|
||||
if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.Add(item); }
|
||||
|
||||
float prevRotation = item.Rotation;
|
||||
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
|
||||
@@ -4379,7 +4421,7 @@ namespace Barotrauma
|
||||
new XAttribute("name", Prefab.OriginalName),
|
||||
new XAttribute("identifier", Prefab.Identifier),
|
||||
new XAttribute("ID", ID),
|
||||
new XAttribute("markedfordeconstruction", deconstructItems.Contains(this)));
|
||||
new XAttribute("markedfordeconstruction", _deconstructItems.Contains(this)));
|
||||
|
||||
if (PendingItemSwap != null)
|
||||
{
|
||||
@@ -4583,11 +4625,13 @@ namespace Barotrauma
|
||||
private void RemoveFromLists()
|
||||
{
|
||||
ItemList.Remove(this);
|
||||
dangerousItems.Remove(this);
|
||||
repairableItems.Remove(this);
|
||||
sonarVisibleItems.Remove(this);
|
||||
cleanableItems.Remove(this);
|
||||
deconstructItems.Remove(this);
|
||||
_dangerousItems.Remove(this);
|
||||
_repairableItems.Remove(this);
|
||||
_sonarVisibleItems.Remove(this);
|
||||
_cleanableItems.Remove(this);
|
||||
_deconstructItems.Remove(this);
|
||||
_turretTargetItems.Remove(this);
|
||||
_chairItems.Remove(this);
|
||||
RemoveFromDroppedStack(allowClientExecute: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,7 @@ namespace Barotrauma
|
||||
public readonly int Amount;
|
||||
public readonly int? Quality;
|
||||
public readonly bool HideForNonTraitors;
|
||||
public readonly InvSlotType MoveToSlot;
|
||||
|
||||
/// <summary>
|
||||
/// How many of this item the fabricator can create (< 0 = unlimited)
|
||||
@@ -257,6 +258,7 @@ namespace Barotrauma
|
||||
FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault);
|
||||
|
||||
HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false);
|
||||
MoveToSlot = element.GetAttributeEnum(nameof(MoveToSlot), InvSlotType.None);
|
||||
|
||||
if (element.GetAttribute(nameof(Quality)) != null)
|
||||
{
|
||||
@@ -1000,7 +1002,7 @@ namespace Barotrauma
|
||||
ParseConfigElement(variantOf: null);
|
||||
}
|
||||
|
||||
private string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf)
|
||||
public string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf)
|
||||
=> subElement.DoesAttributeReferenceFileNameAlone("texture")
|
||||
? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path)
|
||||
: "";
|
||||
|
||||
@@ -86,7 +86,12 @@ namespace Barotrauma
|
||||
public readonly UInt64 CreationIndex;
|
||||
public string ErrorLine
|
||||
=> $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Which content package is this entity from (if it's something like an item or a character that's loaded from a package, otherwise we assume it's the vanilla package).
|
||||
/// </summary>
|
||||
public virtual ContentPackage ContentPackage => GameMain.VanillaContent;
|
||||
|
||||
public Entity(Submarine submarine, ushort id)
|
||||
{
|
||||
this.Submarine = submarine;
|
||||
|
||||
@@ -317,9 +317,9 @@ namespace Barotrauma
|
||||
Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f));
|
||||
Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration);
|
||||
}
|
||||
foreach (Item item in Item.ItemList)
|
||||
foreach (Sonar sonar in Sonar.SonarList)
|
||||
{
|
||||
item.GetComponent<Sonar>()?.RegisterExplosion(this, worldPosition);
|
||||
sonar.RegisterExplosion(this, worldPosition);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -35,6 +35,18 @@ namespace Barotrauma
|
||||
|
||||
public readonly float GlowEffectT;
|
||||
|
||||
private readonly List<Gap> overlappingGaps = new List<Gap>();
|
||||
|
||||
/// <summary>
|
||||
/// Do we need to recheck which gaps are overlapping with this one, and how much they should reduce this gap's flow?
|
||||
/// </summary>
|
||||
private bool overlappingGapsDirty;
|
||||
|
||||
/// <summary>
|
||||
/// How much overlapping gaps reduce the flow rate of this one?
|
||||
/// </summary>
|
||||
private float overlappingGapFlowRateReduction;
|
||||
|
||||
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
|
||||
private float open;
|
||||
|
||||
@@ -68,22 +80,29 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
if (float.IsNaN(value)) { return; }
|
||||
float prevValue = open;
|
||||
if (value > open)
|
||||
{
|
||||
openedTimer = 1.0f;
|
||||
}
|
||||
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
|
||||
|
||||
open = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
if (!MathUtils.NearlyEqual(open, prevValue))
|
||||
{
|
||||
if (value > open && value >= 1.0f)
|
||||
overlappingGapsDirty = true;
|
||||
FlagOverlappingGapsDirty();
|
||||
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: true);
|
||||
}
|
||||
else if (value < open && open >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: false);
|
||||
if (open > prevValue && open >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: true);
|
||||
}
|
||||
else if (open < prevValue && prevValue >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
open = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
|
||||
static void InformWaypointsAboutGapState(Gap gap, bool open)
|
||||
{
|
||||
@@ -206,7 +225,7 @@ namespace Barotrauma
|
||||
Physics.CollisionWall,
|
||||
Physics.CollisionCharacter,
|
||||
findNewContacts: false);
|
||||
outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})";
|
||||
outsideCollisionBlocker.UserData = this;
|
||||
outsideCollisionBlocker.Enabled = false;
|
||||
#if CLIENT
|
||||
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
|
||||
@@ -339,7 +358,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (hulls[i] == null) { continue; }
|
||||
linkedTo.Add(hulls[i]);
|
||||
if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this);
|
||||
if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); }
|
||||
foreach (var gap in hulls[i].ConnectedGaps)
|
||||
{
|
||||
gap.overlappingGapsDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +388,12 @@ namespace Barotrauma
|
||||
deltaTime *= updateCount;
|
||||
updateCount = 0;
|
||||
|
||||
if (overlappingGapsDirty)
|
||||
{
|
||||
RefreshOverlappingGaps();
|
||||
overlappingGapsDirty = false;
|
||||
}
|
||||
|
||||
flowForce = Vector2.Zero;
|
||||
outsideColliderRaycastTimer -= deltaTime;
|
||||
|
||||
@@ -432,7 +461,7 @@ namespace Barotrauma
|
||||
|
||||
//a variable affecting the water flow through the gap
|
||||
//the larger the gap is, the faster the water flows
|
||||
float sizeModifier = Size / 100.0f * open;
|
||||
float sizeModifier = Size / 100.0f * open * (1.0f - overlappingGapFlowRateReduction);
|
||||
|
||||
//horizontal gap (such as a regular door)
|
||||
if (IsHorizontal)
|
||||
@@ -598,7 +627,7 @@ namespace Barotrauma
|
||||
{
|
||||
//a variable affecting the water flow through the gap
|
||||
//the larger the gap is, the faster the water flows
|
||||
float sizeModifier = Size * open * open;
|
||||
float sizeModifier = Size * open * open * (1.0f - overlappingGapFlowRateReduction);
|
||||
|
||||
float delta = 500.0f * sizeModifier * deltaTime;
|
||||
|
||||
@@ -795,6 +824,52 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RefreshOverlappingGaps()
|
||||
{
|
||||
overlappingGapFlowRateReduction = 0.0f;
|
||||
overlappingGaps.Clear();
|
||||
foreach (var linked in linkedTo)
|
||||
{
|
||||
if (linked is not Hull hull) { continue; }
|
||||
foreach (var connectedGap in hull.ConnectedGaps)
|
||||
{
|
||||
if (connectedGap == this) { continue; }
|
||||
//let the "more open" gap reduce this gap's flow rate
|
||||
//or if they're both equally open, let the one that was created first handle it
|
||||
//(note that we can't use Entity.ID here because gaps on walls don't have IDs)
|
||||
if (connectedGap.open > open ||
|
||||
(connectedGap.open == open && connectedGap.CreationIndex < CreationIndex))
|
||||
{
|
||||
Rectangle intersection = Rectangle.Intersect(rect, connectedGap.rect);
|
||||
if (intersection.Width > 0 && intersection.Height > 0)
|
||||
{
|
||||
//reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is
|
||||
float relativeOverlap = IsHorizontal ?
|
||||
intersection.Height / (float)rect.Height :
|
||||
intersection.Width / (float)rect.Width;
|
||||
overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open;
|
||||
}
|
||||
}
|
||||
if (overlappingGapFlowRateReduction >= 1.0f)
|
||||
{
|
||||
overlappingGapFlowRateReduction = 1.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark all gaps that are currently known to overlap with this one as needing a refresh of overlapping gaps
|
||||
/// </summary>
|
||||
private void FlagOverlappingGapsDirty()
|
||||
{
|
||||
foreach (var overlappingGap in overlappingGaps)
|
||||
{
|
||||
overlappingGap.overlappingGapsDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ShallowRemove()
|
||||
{
|
||||
base.ShallowRemove();
|
||||
|
||||
@@ -303,7 +303,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) { return; }
|
||||
waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress);
|
||||
if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; }
|
||||
if (waterVolume <= Volume)
|
||||
{
|
||||
//recalculate pressure, but only if there's less water than the volume, above that point the "overpressure" logic kicks in
|
||||
Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
|
||||
}
|
||||
if (waterVolume > 0.0f)
|
||||
{
|
||||
update = true;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -8,6 +12,112 @@ namespace Barotrauma
|
||||
Vector2 WorldPosition { get; }
|
||||
Vector2 SimPosition { get; }
|
||||
Submarine Submarine { get; }
|
||||
|
||||
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
if (seeingEntity is Character seeingCharacter)
|
||||
{
|
||||
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
|
||||
}
|
||||
if (target is Character targetCharacter)
|
||||
{
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (seeingEntity == null) { return false; }
|
||||
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (!target.AnimController.SimplePhysicsEnabled)
|
||||
{
|
||||
//find the limbs that are furthest from the target's position (from the viewer's point of view)
|
||||
Limb leftExtremity = null, rightExtremity = null;
|
||||
float leftMostDot = 0.0f, rightMostDot = 0.0f;
|
||||
Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition;
|
||||
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
|
||||
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
|
||||
foreach (Limb limb in target.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition;
|
||||
float leftDot = Vector2.Dot(limbDir, leftDir);
|
||||
if (leftDot > leftMostDot)
|
||||
{
|
||||
leftMostDot = leftDot;
|
||||
leftExtremity = limb;
|
||||
continue;
|
||||
}
|
||||
float rightDot = Vector2.Dot(limbDir, rightDir);
|
||||
if (rightDot > rightMostDot)
|
||||
{
|
||||
rightMostDot = rightDot;
|
||||
rightExtremity = limb;
|
||||
}
|
||||
}
|
||||
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null) { return false; }
|
||||
if (seeingEntity == null) { return false; }
|
||||
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
|
||||
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
|
||||
if (checkFacing && seeingEntity is Character seeingCharacter)
|
||||
{
|
||||
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
|
||||
}
|
||||
//both inside the same sub (or both outside)
|
||||
//OR the we're inside, the other character outside
|
||||
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
|
||||
{
|
||||
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//we're outside, the other character inside
|
||||
else if (seeingEntity.Submarine == null)
|
||||
{
|
||||
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//both inside different subs
|
||||
else
|
||||
{
|
||||
return
|
||||
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
|
||||
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
|
||||
bool IsBlocking(Fixture f)
|
||||
{
|
||||
var body = f.Body;
|
||||
if (body == null) { return false; }
|
||||
if (body.UserData is Structure wall)
|
||||
{
|
||||
if (!wall.CastShadow && seeThroughWindows) { return false; }
|
||||
return wall != target;
|
||||
}
|
||||
else if (body.UserData is Item item)
|
||||
{
|
||||
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
|
||||
{
|
||||
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
|
||||
}
|
||||
return item != target;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IIgnorable : ISpatialEntity
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -21,6 +21,8 @@ namespace Barotrauma
|
||||
public float ActualMaxDifficulty => maxDifficulty;
|
||||
public float AdjustedMaxDifficulty => maxDifficulty - 0.1f;
|
||||
|
||||
public readonly float ExperienceFromMissionRewards;
|
||||
|
||||
|
||||
public readonly ImmutableHashSet<int> AllowedZones;
|
||||
|
||||
@@ -50,6 +52,10 @@ namespace Barotrauma
|
||||
AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet();
|
||||
MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0);
|
||||
maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100);
|
||||
float baseExperience = 0.09f;
|
||||
float difficultyRewardMultiplier = 0.25f;
|
||||
float calculateDefaultExperience = baseExperience + MinDifficulty * difficultyRewardMultiplier / 100;
|
||||
ExperienceFromMissionRewards = element.GetAttributeFloat("ExperienceFromMissionRewards", calculateDefaultExperience);
|
||||
|
||||
var submarineAvailabilityOverrides = new HashSet<SubmarineAvailability>();
|
||||
if (element.GetChildElement("submarines") is ContentXElement availabilityElement)
|
||||
|
||||
@@ -217,8 +217,13 @@ namespace Barotrauma
|
||||
/// Makes the cell rounder by subdividing the edges and offsetting them at the middle
|
||||
/// </summary>
|
||||
/// <param name="minEdgeLength">How small the individual subdivided edges can be (smaller values produce rounder shapes, but require more geometry)</param>
|
||||
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f)
|
||||
/// <param name="minThickness">How "thin" irregularity is allowed to make parts of the cell. Very high irregularity values can lead to thin "spikes" or even parts where the "spike's" thickness becomes negative and wall segments intersect each other.</param>
|
||||
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f, float minThickness = 0.0f)
|
||||
{
|
||||
//we need to make sure the vertices of the wall are still ordered counter-clockwise -
|
||||
//if we deform some vertices so much the cell becomes concave, rendering the triangles will break (parts of the inside of the wall will render outside the edges)
|
||||
var compareCCW = new CompareCCW(cell.Center);
|
||||
|
||||
List<GraphEdge> tempEdges = new List<GraphEdge>();
|
||||
foreach (GraphEdge edge in cell.Edges)
|
||||
{
|
||||
@@ -231,8 +236,10 @@ namespace Barotrauma
|
||||
Vector2 edgeDiff = edge.Point2 - edge.Point1;
|
||||
Vector2 edgeDir = Vector2.Normalize(edgeDiff);
|
||||
|
||||
float maxExtrusion = float.PositiveInfinity;
|
||||
const float minPassageWidth = 200.0f;
|
||||
//If the edge is next to an empty cell and there's another solid cell at the other side of the empty one,
|
||||
//don't touch this edge. Otherwise we may end up closing off small passages between cells.
|
||||
//we need to calculate how far we can extrude the edge so it doesn't end up closing off small passages between cells.
|
||||
var adjacentEmptyCell = edge.AdjacentCell(cell);
|
||||
if (adjacentEmptyCell?.CellType == CellType.Solid) { adjacentEmptyCell = null; }
|
||||
if (adjacentEmptyCell != null)
|
||||
@@ -252,8 +259,15 @@ namespace Barotrauma
|
||||
}
|
||||
if (adjacentEdge != null)
|
||||
{
|
||||
tempEdges.Add(edge);
|
||||
continue;
|
||||
maxExtrusion =
|
||||
new[]
|
||||
{
|
||||
Vector2.Distance(edge.Point1, adjacentEdge.Point1),
|
||||
Vector2.Distance(edge.Point1, adjacentEdge.Point2),
|
||||
Vector2.Distance(edge.Point1, adjacentEdge.Point2),
|
||||
Vector2.Distance(edge.Point2, adjacentEdge.Point1),
|
||||
}.Min();
|
||||
maxExtrusion = Math.Max(0, maxExtrusion - minPassageWidth);
|
||||
}
|
||||
}
|
||||
List<Vector2> edgePoints = new List<Vector2>();
|
||||
@@ -274,12 +288,53 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
|
||||
//value that's 0 at edges, 0.5 at center
|
||||
float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
|
||||
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient);
|
||||
Vector2 extrudedPoint =
|
||||
//make the value "curve" from 0 to 1 at the center, instead of going linearly from 0 to 1
|
||||
centerF = MathF.Sin(centerF * MathHelper.Pi);
|
||||
|
||||
//magic number intended to make old rounding values behave roughly the same with the new formula
|
||||
//previously the extrusion increased linearly towards the center, forming a "spike" like "/\"
|
||||
//now it follows a sine curve, which makes lower values produce rounder results
|
||||
const float RoundingScale = 0.25f;
|
||||
|
||||
//magic number intended to make old variance values behave roughly the same with the new formula
|
||||
//previously a value of 1 would allow a maximum extrusion of 50% of the edge's length at the center,
|
||||
//now we extrude by the variance at any point on the edge (not more in the center)
|
||||
const float RandomVarianceScale = 0.25f;
|
||||
float randomVariance = irregularity * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
|
||||
|
||||
float extrusionAmount = edgeLength * ((roundingAmount * RoundingScale * centerF) + randomVariance * RandomVarianceScale);
|
||||
extrusionAmount = Math.Min(extrusionAmount, maxExtrusion);
|
||||
|
||||
Vector2 nonExtrudedPoint =
|
||||
edge.Point1 +
|
||||
edgeDiff * (i / (float)pointCount) +
|
||||
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF;
|
||||
edgeDiff * (i / (float)pointCount);
|
||||
|
||||
Vector2 nextPoint =
|
||||
edge.Point1 +
|
||||
edgeDiff * ((i + 1) / (float)pointCount);
|
||||
|
||||
//"extruding" inwards, need to make sure we don't make the edge poke through the cell from the other side
|
||||
if (extrusionAmount < 0 && minThickness > 0.0f)
|
||||
{
|
||||
foreach (GraphEdge otherEdge in cell.Edges)
|
||||
{
|
||||
if (otherEdge == edge) { continue; }
|
||||
float margin = minThickness * Math.Sign(extrusionAmount);
|
||||
if (MathUtils.GetLineIntersection(
|
||||
nonExtrudedPoint, nonExtrudedPoint + edgeNormal * (extrusionAmount + margin),
|
||||
otherEdge.Point1, otherEdge.Point2, areLinesInfinite: false, out Vector2 intersection))
|
||||
{
|
||||
extrusionAmount = Math.Min(extrusionAmount, Vector2.Distance(edge.Point1, intersection)) - margin;
|
||||
//make sure we don't "overshoot", fix the inwards extrusion by instead extruding too much outwards
|
||||
//(can happen on small cells in caves for example)
|
||||
extrusionAmount = Math.Min(extrusionAmount, edge.Length / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 extrudedPoint = nonExtrudedPoint + edgeNormal * extrusionAmount;
|
||||
|
||||
var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2);
|
||||
bool isInside = false;
|
||||
@@ -306,14 +361,29 @@ namespace Barotrauma
|
||||
}
|
||||
if (isInside) { break; }
|
||||
}
|
||||
if (isInside) { continue; }
|
||||
|
||||
if (!isInside)
|
||||
{
|
||||
edgePoints.Add(extrudedPoint);
|
||||
}
|
||||
//if adding the point would deform the edge so much that the normal of the new edge would point
|
||||
//in the opposite direction from the undeformed edge's normal, don't allow adding the point
|
||||
//(that would lead to the edge being "inside out", the wall texture and objects on the wall pointing inwards)
|
||||
bool isNormalInverted =
|
||||
Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, edgePoints.Last(), extrudedPoint)) < 0 ||
|
||||
//check that the edge at the other side of the new point doesn't get inverted either
|
||||
Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, extrudedPoint, edge.Point2)) < 0;
|
||||
if (isNormalInverted) { continue; }
|
||||
|
||||
//make sure extruding the point doesn't change the vertex order
|
||||
//(they're assumed to be sorted counter-clockwise, and if they're not, the triangles will generate incorrectly)
|
||||
bool vertexOrderChanged =
|
||||
compareCCW.Compare(edgePoints.Last(), nonExtrudedPoint) != compareCCW.Compare(edgePoints.Last(), extrudedPoint) ||
|
||||
compareCCW.Compare(nonExtrudedPoint, nextPoint) != compareCCW.Compare(extrudedPoint, nextPoint);
|
||||
if (vertexOrderChanged) { continue; }
|
||||
|
||||
edgePoints.Add(extrudedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < edgePoints.Count - 1; i++)
|
||||
{
|
||||
tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1])
|
||||
@@ -376,19 +446,7 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 minVert = tempVertices[0];
|
||||
Vector2 maxVert = tempVertices[0];
|
||||
foreach (var vert in tempVertices)
|
||||
{
|
||||
minVert = new Vector2(
|
||||
Math.Min(minVert.X, vert.X),
|
||||
Math.Min(minVert.Y, vert.Y));
|
||||
maxVert = new Vector2(
|
||||
Math.Max(maxVert.X, vert.X),
|
||||
Math.Max(maxVert.Y, vert.Y));
|
||||
}
|
||||
Vector2 center = (minVert + maxVert) / 2;
|
||||
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center));
|
||||
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center));
|
||||
|
||||
if (bodyPoints.Count < 2) { continue; }
|
||||
|
||||
@@ -411,7 +469,7 @@ namespace Barotrauma
|
||||
if (cell.CellType == CellType.Empty) { continue; }
|
||||
|
||||
cellBody.UserData = cell;
|
||||
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center));
|
||||
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center));
|
||||
|
||||
for (int i = 0; i < triangles.Count; i++)
|
||||
{
|
||||
|
||||
@@ -1205,7 +1205,8 @@ namespace Barotrauma
|
||||
CaveGenerator.RoundCell(cell,
|
||||
minEdgeLength: GenerationParams.CellSubdivisionLength,
|
||||
roundingAmount: GenerationParams.CellRoundingAmount,
|
||||
irregularity: GenerationParams.CellIrregularity);
|
||||
irregularity: GenerationParams.CellIrregularity,
|
||||
minThickness: GenerationParams.WallTextureExpandInwardsAmount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,12 +1279,22 @@ namespace Barotrauma
|
||||
Debug.Assert(triangleLists.Count == cellBatches.Count);
|
||||
for (int i = 0; i < triangleLists.Count; i++)
|
||||
{
|
||||
//the solid black inner part of the wall
|
||||
var wallVerts = CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells,
|
||||
expandOutwards: 0.0f, expandInwards: GenerationParams.WallTextureExpandInwardsAmount,
|
||||
outerColor: GenerationParams.WallColor, innerColor: Color.Black,
|
||||
this, zCoord: 0.9f, preventExpandThroughCell: true).ToArray();
|
||||
CaveGenerator.GenerateTextureCoordinates(wallVerts, GenerationParams.WallTextureSize);
|
||||
renderer.SetVertices(
|
||||
CaveGenerator.GenerateWallVertices(triangleLists[i], GenerationParams, zCoord: 0.9f).ToArray(),
|
||||
CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, this, zCoord: 0.9f).ToArray(),
|
||||
wallVerts,
|
||||
CaveGenerator.GenerateWallEdgeVertices(
|
||||
cellBatches[i].cells,
|
||||
GenerationParams.WallEdgeExpandOutwardsAmount, GenerationParams.WallEdgeExpandInwardsAmount,
|
||||
outerColor: GenerationParams.WallColor, innerColor: GenerationParams.WallColor,
|
||||
this, zCoord: 0.9f).ToArray(),
|
||||
CaveGenerator.GenerateWallVertices(triangleLists[i], Color.Black, zCoord: 0.9f).ToArray(),
|
||||
cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture,
|
||||
cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture,
|
||||
GenerationParams.WallColor);
|
||||
cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2808,6 +2819,7 @@ namespace Barotrauma
|
||||
if (l.Cell == null || l.Edge == null) { return false; }
|
||||
if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; }
|
||||
if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; }
|
||||
if (l.Edge.Length < itemPrefab.Size.X) { return false; }
|
||||
if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; }
|
||||
return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
|
||||
|
||||
@@ -2839,6 +2851,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (l.Cell == null || l.Edge == null) { return false; }
|
||||
if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; }
|
||||
if (l.Edge.Length < selectedPrefab.Size.X) { return false; }
|
||||
l.InitializeResources();
|
||||
return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _);
|
||||
}, randSync: Rand.RandSync.ServerAndClient);
|
||||
@@ -3375,37 +3388,41 @@ namespace Barotrauma
|
||||
private void PlaceResources(ItemPrefab resourcePrefab, int resourceCount, ClusterLocation location, out List<Item> placedResources,
|
||||
float? edgeLength = null, float maxResourceOverlap = 0.4f)
|
||||
{
|
||||
edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
|
||||
edgeLength ??= location.Edge.Length;
|
||||
Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value;
|
||||
if (!MathUtils.IsValid(edgeDir))
|
||||
{
|
||||
edgeDir = Vector2.Zero;
|
||||
}
|
||||
var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
|
||||
float minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
|
||||
minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap);
|
||||
var lerpAmounts = new float[resourceCount];
|
||||
float[] lerpAmounts = new float[resourceCount];
|
||||
lerpAmounts[0] = 0.0f;
|
||||
var lerpAmount = 0.0f;
|
||||
float lerpAmount = 0.0f;
|
||||
for (int i = 1; i < resourceCount; i++)
|
||||
{
|
||||
var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
|
||||
lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value;
|
||||
lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
|
||||
float overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
|
||||
lerpAmount = Math.Clamp(lerpAmount + (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value, 0.0f, 1.0f);
|
||||
lerpAmounts[i] = lerpAmount;
|
||||
}
|
||||
|
||||
var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient);
|
||||
placedResources = new List<Item>();
|
||||
for (int i = 0; i < resourceCount; i++)
|
||||
{
|
||||
Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]);
|
||||
Vector2 selectedPos =
|
||||
location.Edge.Length < resourcePrefab.Size.X ?
|
||||
location.Edge.Center :
|
||||
Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]);
|
||||
var item = new Item(resourcePrefab, selectedPos, submarine: null);
|
||||
Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
|
||||
float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f));
|
||||
moveAmount += (item.GetComponent<LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
|
||||
item.Move(edgeNormal * moveAmount);
|
||||
item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
|
||||
if (item.GetComponent<Holdable>() is Holdable h)
|
||||
{
|
||||
h.AttachToWall();
|
||||
item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
|
||||
}
|
||||
else if (item.body != null)
|
||||
{
|
||||
@@ -3914,7 +3931,7 @@ namespace Barotrauma
|
||||
attempt++;
|
||||
spawnPoint = wayPoint.WorldPosition;
|
||||
success = TryPositionSub(subBorders, subName, placement, ref spawnPoint);
|
||||
positionHistory.Add($"{info.Name}: {attempt}", positions.ToList());
|
||||
positionHistory.TryAdd($"{info.Name}: {attempt}", positions.ToList());
|
||||
positions.Clear();
|
||||
if (success)
|
||||
{
|
||||
@@ -3940,6 +3957,11 @@ namespace Barotrauma
|
||||
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub));
|
||||
foreach (Hull hull in sub.GetHulls(false))
|
||||
{
|
||||
if (hull.WaterPercentage > 0)
|
||||
{
|
||||
// Don't override the water level set by the sub designer
|
||||
continue;
|
||||
}
|
||||
if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance)
|
||||
{
|
||||
hull.WaterVolume =
|
||||
@@ -4296,7 +4318,7 @@ namespace Barotrauma
|
||||
if (LevelData.ForceWreck != null)
|
||||
{
|
||||
//force the desired wreck to be chosen first
|
||||
var matchingFile = placeableWrecks.FirstOrDefault(wreck => wreck.WreckFile.Path == LevelData.ForceWreck.FilePath);
|
||||
PlaceableWreck matchingFile = placeableWrecks.FirstOrDefault(wreck => wreck.WreckFile.Path == LevelData.ForceWreck.FilePath);
|
||||
if (matchingFile.WreckFile != null)
|
||||
{
|
||||
placeableWrecks.Remove(matchingFile);
|
||||
@@ -4337,7 +4359,12 @@ namespace Barotrauma
|
||||
{
|
||||
var placeableWreck = placeableWrecks.First();
|
||||
var wreckFile = placeableWreck.WreckFile;
|
||||
placeableWrecks.RemoveAt(0);
|
||||
if (LevelData.ForceWreck == null)
|
||||
{
|
||||
// If a wreck is forced, don't remove it -> only spawns those wrecks (makes testing them in the editor easier).
|
||||
// Normally we don't want two instances of the same wreck to spawn in the same level, but when we test or debug certain wrecks, we want only them.
|
||||
placeableWrecks.RemoveAt(0);
|
||||
}
|
||||
LevelData.ThalamusSpawn thalamusSpawn = requireThalamus ? LevelData.ThalamusSpawn.Forced : LevelData.ThalamusSpawn.Random;
|
||||
if (LevelData.ForceWreck != null) { thalamusSpawn = LevelData.ForceThalamus; }
|
||||
|
||||
@@ -4783,40 +4810,13 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
bool allowDisconnectedWires = true;
|
||||
bool allowDamagedDevices = true;
|
||||
bool allowDamagedWalls = true;
|
||||
if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info)
|
||||
{
|
||||
allowDisconnectedWires = info.AllowDisconnectedWires;
|
||||
allowDamagedWalls = info.AllowDamagedWalls;
|
||||
allowDamagedDevices = info.AllowDamagedDevices;
|
||||
}
|
||||
|
||||
//remove wires
|
||||
float disconnectWireMinDifficulty = 20.0f;
|
||||
float disconnectWireProbability = MathUtils.InverseLerp(disconnectWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f;
|
||||
if (disconnectWireProbability > 0.0f && allowDisconnectedWires)
|
||||
{
|
||||
DisconnectBeaconStationWires(disconnectWireProbability);
|
||||
}
|
||||
|
||||
if (allowDamagedDevices)
|
||||
{
|
||||
DamageBeaconStationDevices(breakDeviceProbability: 0.5f);
|
||||
}
|
||||
if (allowDamagedWalls)
|
||||
{
|
||||
DamageBeaconStationWalls(damageWallProbability: 0.25f);
|
||||
}
|
||||
}
|
||||
SetLinkedSubCrushDepth(BeaconStation);
|
||||
}
|
||||
|
||||
public void DisconnectBeaconStationWires(float disconnectWireProbability)
|
||||
{
|
||||
if (BeaconStation?.Info?.BeaconStationInfo is { AllowDisconnectedWires: false }) { return; }
|
||||
|
||||
if (disconnectWireProbability <= 0.0f) { return; }
|
||||
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
|
||||
foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
|
||||
@@ -4852,6 +4852,8 @@ namespace Barotrauma
|
||||
|
||||
public void DamageBeaconStationDevices(float breakDeviceProbability)
|
||||
{
|
||||
if (BeaconStation?.Info?.BeaconStationInfo is { AllowDamagedDevices: false }) { return; }
|
||||
|
||||
if (breakDeviceProbability <= 0.0f) { return; }
|
||||
//break powered items
|
||||
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
|
||||
@@ -4867,6 +4869,8 @@ namespace Barotrauma
|
||||
|
||||
public void DamageBeaconStationWalls(float damageWallProbability)
|
||||
{
|
||||
if (BeaconStation?.Info?.BeaconStationInfo is { AllowDamagedWalls: false }) { return; }
|
||||
|
||||
if (damageWallProbability <= 0.0f) { return; }
|
||||
//poke holes in the walls
|
||||
foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
|
||||
|
||||
@@ -236,7 +236,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. "
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. "
|
||||
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
|
||||
public float CellRoundingAmount
|
||||
{
|
||||
@@ -247,7 +247,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. "
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. "
|
||||
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
|
||||
public float CellIrregularity
|
||||
{
|
||||
@@ -525,19 +525,19 @@ namespace Barotrauma
|
||||
[Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int MaxCorpseCount { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float HuskProbability { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float ThalamusProbability { get; set; }
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float WreckHullFloodingChance { get; set; }
|
||||
|
||||
[Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float WreckFloodingHullMinWaterPercentage { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float WreckFloodingHullMaxWaterPercentage { get; set; }
|
||||
#endregion
|
||||
|
||||
@@ -602,6 +602,13 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(1000.0f, IsPropertySaveable.Yes, description: "How deep inside the walls the wall texture extends to before fading to black."), Editable(minValue: 0.0f, maxValue: 10000.0f)]
|
||||
public float WallTextureExpandInwardsAmount
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Header("Colors")]
|
||||
[Serialize("27,30,36", IsPropertySaveable.Yes), Editable]
|
||||
public Color AmbientLightColor
|
||||
|
||||
+4
-4
@@ -339,11 +339,11 @@ namespace Barotrauma
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
//use the maximum width of the sprite as the minimum surface width if no value is given
|
||||
if (element != null && !element.Attributes("minsurfacewidth").Any())
|
||||
//use (a bit less than) the maximum width of the sprite as the minimum surface width if no value is given
|
||||
if (element != null && element.GetAttribute("minsurfacewidth") == null)
|
||||
{
|
||||
if (Sprites.Any()) MinSurfaceWidth = Sprites[0].size.X * MaxSize;
|
||||
if (DeformableSprite != null) MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize);
|
||||
if (Sprites.Any()) { MinSurfaceWidth = Sprites[0].size.X * MaxSize * 0.8f; }
|
||||
if (DeformableSprite != null) { MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize * 0.8f); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -609,6 +609,21 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (PhysicsBody != null)
|
||||
{
|
||||
if (currentForceFluctuation <= 0.0f && statusEffects.None() && attacks.None())
|
||||
{
|
||||
//no force atm, and no status effects or attacks the trigger could apply
|
||||
// -> we can disable the collider and get a minor physics performance improvement
|
||||
PhysicsBody.Enabled = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
PhysicsBody.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Entity triggerer in triggerers)
|
||||
{
|
||||
if (triggerer.Removed) { continue; }
|
||||
|
||||
@@ -124,8 +124,17 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public int PriceModifier { get; set; }
|
||||
public Location Location { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum effect positive reputation can have on store prices (e.g. 0.5 = 50% discount with max reputation).
|
||||
/// </summary>
|
||||
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum effect negative reputation can have on store prices (e.g. 0.5 = 50% price increase with minimum reputation).
|
||||
/// </summary>
|
||||
private float MinReputationModifier => Location.StoreMinReputationModifier;
|
||||
|
||||
private StoreInfo(Location location)
|
||||
{
|
||||
Location = location;
|
||||
@@ -343,7 +352,7 @@ namespace Barotrauma
|
||||
if (characters.Any())
|
||||
{
|
||||
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
|
||||
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
|
||||
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValueWithAll(StatTypes.StoreSellMultiplier, tag)));
|
||||
}
|
||||
|
||||
// Price should never go below 1 mk
|
||||
@@ -373,7 +382,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
return MathHelper.Lerp(1.0f, 1.0f + MinReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -384,7 +393,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
return MathHelper.Lerp(1.0f, 1.0f - MinReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,6 +407,7 @@ namespace Barotrauma
|
||||
public Dictionary<Identifier, StoreInfo> Stores { get; set; }
|
||||
|
||||
private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier;
|
||||
private float StoreMinReputationModifier => Type.StoreMinReputationModifier;
|
||||
private float StoreSellPriceModifier => Type.StoreSellPriceModifier;
|
||||
private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier;
|
||||
private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier;
|
||||
|
||||
@@ -118,6 +118,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public float StoreMaxReputationModifier { get; } = 0.1f;
|
||||
public float StoreMinReputationModifier { get; } = 1.0f;
|
||||
public float StoreSellPriceModifier { get; } = 0.3f;
|
||||
public float DailySpecialPriceModifier { get; } = 0.5f;
|
||||
public float RequestGoodPriceModifier { get; } = 2f;
|
||||
@@ -264,6 +265,7 @@ namespace Barotrauma
|
||||
break;
|
||||
case "store":
|
||||
StoreMaxReputationModifier = subElement.GetAttributeFloat("maxreputationmodifier", StoreMaxReputationModifier);
|
||||
StoreMinReputationModifier = subElement.GetAttributeFloat("minreputationmodifier", StoreMaxReputationModifier);
|
||||
StoreSellPriceModifier = subElement.GetAttributeFloat("sellpricemodifier", StoreSellPriceModifier);
|
||||
DailySpecialPriceModifier = subElement.GetAttributeFloat("dailyspecialpricemodifier", DailySpecialPriceModifier);
|
||||
RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -301,12 +302,13 @@ namespace Barotrauma
|
||||
|
||||
protected void LoadDescription(ContentXElement element)
|
||||
{
|
||||
Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
|
||||
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
|
||||
string originalDescription = Description.Value;
|
||||
if (descriptionIdentifier != Identifier.Empty)
|
||||
const string descriptionIdentifierAttributeName = "descriptionidentifier";
|
||||
XAttribute descriptionIdenfifierAttribute = element.GetAttribute(descriptionIdentifierAttributeName);
|
||||
if (descriptionIdenfifierAttribute != null)
|
||||
{
|
||||
Identifier descriptionIdentifier = element.GetAttributeIdentifier(descriptionIdentifierAttributeName, Identifier.Empty);
|
||||
Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}");
|
||||
}
|
||||
else if (nameIdentifier == Identifier.Empty)
|
||||
|
||||
@@ -115,7 +115,11 @@ namespace Barotrauma
|
||||
{
|
||||
outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
|
||||
}
|
||||
if (!generationParams.OutpostTag.IsEmpty)
|
||||
if (generationParams.OutpostTag.IsEmpty)
|
||||
{
|
||||
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.None());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (outpostInfos.Any(o => o.OutpostTags.Contains(generationParams.OutpostTag)))
|
||||
{
|
||||
@@ -448,6 +452,8 @@ namespace Barotrauma
|
||||
entities[selectedModule] = moduleEntities;
|
||||
}
|
||||
|
||||
int maxMoveAmount = Math.Max(2000, selectedModules.Max(m => Math.Max(m.Bounds.Width, m.Bounds.Height)));
|
||||
|
||||
bool overlapsFound = true;
|
||||
int iteration = 0;
|
||||
while (overlapsFound)
|
||||
@@ -465,7 +471,7 @@ namespace Barotrauma
|
||||
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingTries > 0)
|
||||
{
|
||||
overlapsFound = true;
|
||||
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, out Dictionary<PlacedModule, Vector2> solution))
|
||||
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
|
||||
{
|
||||
foreach (KeyValuePair<PlacedModule, Vector2> kvp in solution)
|
||||
{
|
||||
@@ -909,7 +915,12 @@ namespace Barotrauma
|
||||
/// <param name="allmodules">All generated modules</param>
|
||||
/// <param name="solution">The solution to the overlap (if any). Key = placed module, value = distance to move the module</param>
|
||||
/// <returns>Was a solution found for resolving the overlap.</returns>
|
||||
private static bool FindOverlapSolution(IEnumerable<PlacedModule> movableModules, PlacedModule module1, PlacedModule module2, IEnumerable<PlacedModule> allmodules, out Dictionary<PlacedModule, Vector2> solution)
|
||||
private static bool FindOverlapSolution(
|
||||
IEnumerable<PlacedModule> movableModules,
|
||||
PlacedModule module1, PlacedModule module2,
|
||||
IEnumerable<PlacedModule> allmodules,
|
||||
int maxMoveAmount,
|
||||
out Dictionary<PlacedModule, Vector2> solution)
|
||||
{
|
||||
solution = new Dictionary<PlacedModule, Vector2>();
|
||||
foreach (PlacedModule module in movableModules)
|
||||
@@ -925,7 +936,6 @@ namespace Barotrauma
|
||||
Vector2 moveDir = GetMoveDir(module.ThisGapPosition);
|
||||
Vector2 moveStep = moveDir * 50.0f;
|
||||
Vector2 currentMove = Vector2.Zero;
|
||||
float maxMoveAmount = 2000.0f;
|
||||
|
||||
List<PlacedModule> subsequentModules2 = new List<PlacedModule>();
|
||||
GetSubsequentModules(module, movableModules, ref subsequentModules2);
|
||||
|
||||
@@ -53,6 +53,8 @@ namespace Barotrauma
|
||||
const float LeakThreshold = 0.1f;
|
||||
const float BigGapThreshold = 0.7f;
|
||||
|
||||
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
|
||||
|
||||
#if CLIENT
|
||||
public SpriteEffects SpriteEffects = SpriteEffects.None;
|
||||
#endif
|
||||
|
||||
@@ -1522,7 +1522,14 @@ namespace Barotrauma
|
||||
if (entity.Submarine == null) { return false; }
|
||||
if (includingConnectedSubs)
|
||||
{
|
||||
return GetConnectedSubs().Any(s => s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type));
|
||||
// Performance-sensitive code -> implemented without Linq.
|
||||
foreach (Submarine s in connectedSubs)
|
||||
{
|
||||
if (s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1938,8 +1945,8 @@ namespace Barotrauma
|
||||
{
|
||||
bool hasThalamus = false;
|
||||
|
||||
var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity).ToImmutableHashSet();
|
||||
var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct().ToImmutableHashSet();
|
||||
var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity);
|
||||
var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct();
|
||||
|
||||
foreach (ItemPrefab prefab in prefabsOnSub)
|
||||
{
|
||||
@@ -2077,7 +2084,6 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
RoundSound.RemoveAllRoundSounds();
|
||||
GameMain.LightManager?.ClearLights();
|
||||
depthSortedDamageable.Clear();
|
||||
#endif
|
||||
var _loaded = new List<Submarine>(loaded);
|
||||
foreach (Submarine sub in _loaded)
|
||||
|
||||
@@ -504,10 +504,8 @@ namespace Barotrauma
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine != CanEnterSubmarine.True)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//character inside some sub, no need to displace
|
||||
if (c.Submarine != null) { continue; }
|
||||
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
@@ -525,13 +523,11 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
//"+ translatedir" in order to move the character slightly away from the wall
|
||||
c.AnimController.SetPosition(ConvertUnits.ToSimUnits(c.WorldPosition + (intersection - limb.WorldPosition)) + translateDir);
|
||||
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@ namespace Barotrauma.Networking
|
||||
return cursorPositionError *= 0.7f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quantizes the value so it's "as accurate as it can be" when the value is represented using the specified number of bits.
|
||||
/// Relevant e.g. when writing float values into network messages using some specific number of bits.
|
||||
/// </summary>
|
||||
public static Vector2 Quantize(Vector2 value, float min, float max, int numberOfBits)
|
||||
{
|
||||
return new Vector2(
|
||||
@@ -100,15 +104,21 @@ namespace Barotrauma.Networking
|
||||
Quantize(value.Y, min, max, numberOfBits));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quantizes the value so it's "as accurate as it can be" when the value is represented using the specified number of bits.
|
||||
/// Relevant e.g. when writing float values into network messages using some specific number of bits.
|
||||
/// </summary>
|
||||
public static float Quantize(float value, float min, float max, int numberOfBits)
|
||||
{
|
||||
float step = (max - min) / (1 << (numberOfBits + 1));
|
||||
value = MathHelper.Clamp(value, min, max);
|
||||
|
||||
float step = (max - min) / ((1 << numberOfBits) - 1);
|
||||
if (Math.Abs(value) < step + 0.00001f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return MathUtils.RoundTowardsClosest(MathHelper.Clamp(value, min, max), step);
|
||||
return MathUtils.RoundTowardsClosest(value - min, step) + min;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -1,9 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
class EntityEventException : Exception
|
||||
{
|
||||
public readonly Entity Entity;
|
||||
|
||||
public EntityEventException(string errorMessage, Entity causingEntity, Exception innerException = null) : base(errorMessage, innerException)
|
||||
{
|
||||
Entity = causingEntity;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NetEntityEventManager
|
||||
{
|
||||
public const int MaxEventBufferLength = 1024;
|
||||
@@ -29,7 +38,7 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to write an event for the entity \"" + e.Entity + "\"", exception);
|
||||
DebugConsole.ThrowError($"Failed to write an event (ID: {e.ID}) for the entity \"{e.Entity}\"", exception, contentPackage: e.Entity?.ContentPackage);
|
||||
GameAnalyticsManager.AddErrorEventOnce("NetEntityEventManager.Write:WriteFailed" + e.Entity.ToString(),
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Failed to write an event for the entity \"" + e.Entity + "\"\n" + exception.StackTrace.CleanupStackTrace());
|
||||
@@ -37,7 +46,8 @@ namespace Barotrauma.Networking
|
||||
//write an empty event to avoid messing up IDs
|
||||
//(otherwise the clients might read the next event in the message and think its ID
|
||||
//is consecutive to the previous one, even though we skipped over this broken event)
|
||||
tempBuffer.WriteUInt16(Entity.NullEntityID);
|
||||
tempBuffer.WriteUInt16(Entity.NullEntityID);
|
||||
tempBuffer.WriteVariableUInt32(0); //size of the event
|
||||
eventCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Barotrauma.Networking
|
||||
/// </summary>
|
||||
public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true)
|
||||
: this(order,
|
||||
order?.GetChatMessage(targetCharacter?.Name,
|
||||
order?.GetChatMessage(targetCharacter?.DisplayName,
|
||||
(order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value,
|
||||
givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder),
|
||||
targetCharacter, sender, isNewOrder)
|
||||
@@ -51,7 +51,7 @@ namespace Barotrauma.Networking
|
||||
=> entity switch
|
||||
{
|
||||
null => null,
|
||||
Character character => character.Name,
|
||||
Character character => character.DisplayName,
|
||||
Item it => it.Name,
|
||||
_ => throw new ArgumentException("Entity is not a character or item", nameof(entity))
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user