Merge branch 'master' of https://github.com/Regalis11/Barotrauma.git
This commit is contained in:
@@ -220,10 +220,10 @@ namespace Barotrauma
|
||||
|
||||
private static readonly (int quality, float commonness)[] qualityCommonnesses = new (int quality, float commonness)[Quality.MaxQuality + 1]
|
||||
{
|
||||
(0, 0.85f),
|
||||
(1, 0.125f),
|
||||
(2, 0.0225f),
|
||||
(3, 0.0025f),
|
||||
(0, 1.0f),
|
||||
(1, 0.0f),
|
||||
(2, 0.0f),
|
||||
(3, 0.0f),
|
||||
};
|
||||
|
||||
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer, float difficultyModifier)
|
||||
|
||||
@@ -16,32 +16,98 @@ namespace Barotrauma
|
||||
{
|
||||
public ItemPrefab ItemPrefab { get; }
|
||||
public int Quantity { get; set; }
|
||||
public bool? IsStoreComponentEnabled { get; set; }
|
||||
|
||||
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
|
||||
{
|
||||
ItemPrefab = itemPrefab;
|
||||
Quantity = quantity;
|
||||
IsStoreComponentEnabled = null;
|
||||
}
|
||||
}
|
||||
|
||||
class SoldItem
|
||||
{
|
||||
public ItemPrefab ItemPrefab { get; }
|
||||
public ushort ID { get; }
|
||||
public ushort ID { get; private set; }
|
||||
public bool Removed { get; set; }
|
||||
public byte SellerID { get; }
|
||||
public SellOrigin Origin { get; }
|
||||
|
||||
public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId)
|
||||
public enum SellOrigin
|
||||
{
|
||||
Character,
|
||||
Submarine
|
||||
}
|
||||
|
||||
public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId, SellOrigin origin)
|
||||
{
|
||||
ItemPrefab = itemPrefab;
|
||||
ID = id;
|
||||
Removed = removed;
|
||||
SellerID = sellerId;
|
||||
Origin = origin;
|
||||
}
|
||||
|
||||
public void SetItemId(ushort id)
|
||||
{
|
||||
if (ID != Entity.NullEntityID)
|
||||
{
|
||||
DebugConsole.ShowError("Error setting SoldItem.ID: ID has already been set and should not be changed.");
|
||||
return;
|
||||
}
|
||||
ID = id;
|
||||
}
|
||||
}
|
||||
|
||||
partial class CargoManager
|
||||
{
|
||||
private class SoldEntity
|
||||
{
|
||||
public enum SellStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity sold in SP. Or, entity sold by client and confirmed by server in MP.
|
||||
/// </summary>
|
||||
Confirmed,
|
||||
/// <summary>
|
||||
/// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed.
|
||||
/// </summary>
|
||||
Unconfirmed,
|
||||
/// <summary>
|
||||
/// Entity sold by client in MP. Client hasn't yet received an update from server after selling.
|
||||
/// </summary>
|
||||
Local
|
||||
}
|
||||
|
||||
public Item Item { get; private set; }
|
||||
public ItemPrefab ItemPrefab { get; }
|
||||
public SellStatus Status { get; set; }
|
||||
|
||||
public SoldEntity(Item item, SellStatus status)
|
||||
{
|
||||
Item = item;
|
||||
ItemPrefab = item?.Prefab;
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public SoldEntity(ItemPrefab itemPrefab, SellStatus status)
|
||||
{
|
||||
ItemPrefab = itemPrefab;
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public void SetItem(Item item)
|
||||
{
|
||||
if (Item != null)
|
||||
{
|
||||
DebugConsole.ShowError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
return;
|
||||
}
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
public const int MaxQuantity = 100;
|
||||
|
||||
public List<PurchasedItem> ItemsInBuyCrate { get; } = new List<PurchasedItem>();
|
||||
@@ -92,7 +158,7 @@ namespace Barotrauma
|
||||
|
||||
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity)
|
||||
{
|
||||
PurchasedItem itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
|
||||
var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
|
||||
if (itemInCrate != null)
|
||||
{
|
||||
itemInCrate.Quantity += changeInQuantity;
|
||||
@@ -109,6 +175,25 @@ namespace Barotrauma
|
||||
OnItemsInBuyCrateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity)
|
||||
{
|
||||
var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab);
|
||||
if (itemInCrate != null)
|
||||
{
|
||||
itemInCrate.Quantity += changeInQuantity;
|
||||
if (itemInCrate.Quantity < 1)
|
||||
{
|
||||
ItemsInSellFromSubCrate.Remove(itemInCrate);
|
||||
}
|
||||
}
|
||||
else if (changeInQuantity > 0)
|
||||
{
|
||||
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
|
||||
ItemsInSellFromSubCrate.Add(itemInCrate);
|
||||
}
|
||||
OnItemsInSellFromSubCrateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate)
|
||||
{
|
||||
// Check all the prices before starting the transaction
|
||||
@@ -132,6 +217,7 @@ namespace Barotrauma
|
||||
// Exchange money
|
||||
var itemValue = item.Quantity * buyValues[item.ItemPrefab];
|
||||
campaign.Money -= itemValue;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier);
|
||||
Location.StoreCurrentBalance += itemValue;
|
||||
|
||||
if (removeFromCrate)
|
||||
@@ -184,6 +270,82 @@ namespace Barotrauma
|
||||
OnPurchasedItemsChanged?.Invoke();
|
||||
}
|
||||
|
||||
private Dictionary<ItemPrefab, int> UndeterminedSoldEntities { get; } = new Dictionary<ItemPrefab, int>();
|
||||
|
||||
public IEnumerable<Item> GetSellableItemsFromSub()
|
||||
{
|
||||
if (Submarine.MainSub == null) { return new List<Item>(); }
|
||||
var confirmedSoldEntities = Enumerable.Empty<SoldEntity>();
|
||||
UndeterminedSoldEntities.Clear();
|
||||
#if CLIENT
|
||||
confirmedSoldEntities = GetConfirmedSoldEntities();
|
||||
foreach (var soldEntity in SoldEntities)
|
||||
{
|
||||
if (soldEntity.Item != null) { continue; }
|
||||
if (UndeterminedSoldEntities.TryGetValue(soldEntity.ItemPrefab, out int count))
|
||||
{
|
||||
UndeterminedSoldEntities[soldEntity.ItemPrefab] = count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
UndeterminedSoldEntities.Add(soldEntity.ItemPrefab, 1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return Submarine.MainSub.GetItems(true).FindAll(item =>
|
||||
{
|
||||
if (!IsItemSellable(item, confirmedSoldEntities)) { return false; }
|
||||
if (item.GetRootInventoryOwner() is Character) { return false; }
|
||||
if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; }
|
||||
if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; }
|
||||
if (!ItemAndAllContainersInteractable(item)) { return false; }
|
||||
if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; }
|
||||
return true;
|
||||
}).Distinct();
|
||||
|
||||
static bool ItemAndAllContainersInteractable(Item item)
|
||||
{
|
||||
do
|
||||
{
|
||||
if (!item.IsPlayerTeamInteractable) { return false; }
|
||||
item = item.Container;
|
||||
} while (item != null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsItemSellable(Item item, IEnumerable<SoldEntity> confirmedItems)
|
||||
{
|
||||
if (item.Removed) { return false; }
|
||||
if (!item.Prefab.CanBeSold) { return false; }
|
||||
if (item.SpawnedInCurrentOutpost) { return false; }
|
||||
if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; }
|
||||
if (confirmedItems.Any(ci => ci.Item == item)) { return false; }
|
||||
if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count))
|
||||
{
|
||||
int newCount = count - 1;
|
||||
if (newCount > 0)
|
||||
{
|
||||
UndeterminedSoldEntities[item.Prefab] = newCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
UndeterminedSoldEntities.Remove(item.Prefab);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (item.OwnInventory?.Container is ItemContainer itemContainer)
|
||||
{
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems.None()) { return true; }
|
||||
// Allow selling the item if contained items are unsellable and set to be removed on deconstruct
|
||||
if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; }
|
||||
// Otherwise there must be no contained items or the contained items must be confirmed as sold
|
||||
if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub)
|
||||
{
|
||||
if (itemsToSpawn.Count == 0) { return; }
|
||||
@@ -265,11 +427,13 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
itemContainer?.Inventory.TryPutItem(item, null);
|
||||
itemSpawned(item);
|
||||
itemContainer?.Inventory.TryPutItem(item, null);
|
||||
|
||||
itemSpawned(item);
|
||||
#if SERVER
|
||||
Entity.Spawner?.CreateNetworkEvent(item, false);
|
||||
#endif
|
||||
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
|
||||
static void itemSpawned(Item item)
|
||||
{
|
||||
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
|
||||
@@ -291,7 +455,7 @@ namespace Barotrauma
|
||||
float floorPos = hull.Rect.Y - hull.Rect.Height;
|
||||
|
||||
Vector2 position = new Vector2(
|
||||
hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20, hull.Rect.Right - 20) : hull.Rect.Center.X,
|
||||
hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20f, hull.Rect.Right - 20f) : hull.Rect.Center.X,
|
||||
floorPos);
|
||||
|
||||
//check where the actual floor structure is in case the bottom of the hull extends below it
|
||||
|
||||
@@ -37,7 +37,6 @@ namespace Barotrauma
|
||||
{
|
||||
IsSinglePlayer = isSinglePlayer;
|
||||
conversationTimer = 5.0f;
|
||||
|
||||
InitProjectSpecific();
|
||||
}
|
||||
|
||||
@@ -47,7 +46,9 @@ namespace Barotrauma
|
||||
{
|
||||
if (order.TargetEntity == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
string message = $"Attempted to add a \"{order.Name}\" order with no target entity to CrewManager!\n{Environment.StackTrace.CleanupStackTrace()}";
|
||||
DebugConsole.AddWarning(message);
|
||||
GameAnalyticsManager.AddErrorEventOnce("CrewManager.AddOrder:OrderTargetEntityNull", GameAnalyticsManager.ErrorSeverity.Error, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -98,10 +99,10 @@ namespace Barotrauma
|
||||
foreach (XElement characterElement in element.Elements())
|
||||
{
|
||||
if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
|
||||
CharacterInfo characterInfo = new CharacterInfo(characterElement);
|
||||
#if CLIENT
|
||||
if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; }
|
||||
characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1);
|
||||
#endif
|
||||
characterInfos.Add(characterInfo);
|
||||
foreach (XElement subElement in characterElement.Elements())
|
||||
@@ -131,7 +132,7 @@ namespace Barotrauma
|
||||
characterInfos.Remove(characterInfo);
|
||||
}
|
||||
|
||||
public void AddCharacter(Character character)
|
||||
public void AddCharacter(Character character, bool sortCrewList = true)
|
||||
{
|
||||
if (character.Removed)
|
||||
{
|
||||
@@ -153,7 +154,11 @@ namespace Barotrauma
|
||||
characterInfos.Add(character.Info);
|
||||
}
|
||||
#if CLIENT
|
||||
AddCharacterToCrewList(character);
|
||||
var characterComponent = AddCharacterToCrewList(character);
|
||||
if (sortCrewList)
|
||||
{
|
||||
SortCrewList();
|
||||
}
|
||||
if (character.CurrentOrders != null)
|
||||
{
|
||||
foreach (var order in character.CurrentOrders)
|
||||
@@ -185,6 +190,10 @@ namespace Barotrauma
|
||||
|
||||
public void InitRound()
|
||||
{
|
||||
#if CLIENT
|
||||
GUIContextMenu.CurrentContextMenu = null;
|
||||
#endif
|
||||
|
||||
characters.Clear();
|
||||
|
||||
List<WayPoint> spawnWaypoints = null;
|
||||
@@ -248,12 +257,16 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
AddCharacter(character);
|
||||
AddCharacter(character, sortCrewList: false);
|
||||
#if CLIENT
|
||||
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
|
||||
#endif
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (IsSinglePlayer) { SortCrewList(); }
|
||||
#endif
|
||||
|
||||
//longer delay in multiplayer to prevent the server from triggering NPC conversations while the players are still loading the round
|
||||
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
|
||||
}
|
||||
@@ -435,19 +448,21 @@ namespace Barotrauma
|
||||
filteredCharacters = filteredCharacters.Union(extraCharacters);
|
||||
}
|
||||
return filteredCharacters
|
||||
// 1. Prioritize those who are on the same submarine than the controlled character
|
||||
// Prioritize those who are on the same submarine as the controlled character
|
||||
.OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine)
|
||||
// 2. Prioritize those who have been given the same maintenance or operate order as now issued
|
||||
.ThenByDescending(c => c.CurrentOrders.Any(o =>
|
||||
o.Order != null && o.Order.Identifier == order.Identifier &&
|
||||
(order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate)))
|
||||
// 3. Prioritize those with the appropriate job for the order
|
||||
// Prioritize those who are already ordered to operate the device
|
||||
.ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity))
|
||||
// Prioritize those with the appropriate job for the order
|
||||
.ThenByDescending(c => order.HasAppropriateJob(c))
|
||||
// 4. Prioritize bots over player controlled characters
|
||||
// Prioritize those who don't yet have the same order (which allows quick-assigning the order to different characters)
|
||||
.ThenByDescending(c => c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier))
|
||||
// Prioritize those with the preferred job for the order
|
||||
.ThenByDescending(c => order.HasPreferredJob(c))
|
||||
// Prioritize bots over player-controlled characters
|
||||
.ThenByDescending(c => c.IsBot)
|
||||
// 5. Use the priority value of the current objective
|
||||
// Prioritize those with a lower current objective priority
|
||||
.ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0)
|
||||
// 6. Prioritize those with the best skill for the order
|
||||
// Prioritize those with a higher order skill level
|
||||
.ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Barotrauma
|
||||
public Faction(CampaignMetadata metadata, FactionPrefab prefab)
|
||||
{
|
||||
Prefab = prefab;
|
||||
Reputation = new Reputation(metadata, $"faction.{prefab.Identifier}", prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
|
||||
Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,22 @@ namespace Barotrauma
|
||||
private set
|
||||
{
|
||||
if (MathUtils.NearlyEqual(Value, value)) { return; }
|
||||
|
||||
float prevValue = Value;
|
||||
|
||||
Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
|
||||
OnReputationValueChanged?.Invoke();
|
||||
OnAnyReputationValueChanged?.Invoke();
|
||||
#if CLIENT
|
||||
int increase = (int)Value - (int)prevValue;
|
||||
if (increase != 0 && Character.Controlled != null)
|
||||
{
|
||||
Character.Controlled.AddMessage(
|
||||
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name),
|
||||
increase > 0 ? GUI.Style.Green : GUI.Style.Red,
|
||||
playSound: true, Identifier, increase, lifetime: 5.0f);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +76,32 @@ namespace Barotrauma
|
||||
public Action OnReputationValueChanged;
|
||||
public static Action OnAnyReputationValueChanged;
|
||||
|
||||
public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation)
|
||||
public readonly Faction Faction;
|
||||
public readonly Location Location;
|
||||
|
||||
|
||||
public Reputation(CampaignMetadata metadata, Location location, string identifier, int minReputation, int maxReputation, int initialReputation)
|
||||
: this(metadata, null, location, identifier, minReputation, maxReputation, initialReputation)
|
||||
{
|
||||
}
|
||||
|
||||
public Reputation(CampaignMetadata metadata, Faction faction, int minReputation, int maxReputation, int initialReputation)
|
||||
: this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}", minReputation, maxReputation, initialReputation)
|
||||
{
|
||||
}
|
||||
|
||||
private Reputation(CampaignMetadata metadata, Faction faction, Location location, string identifier, int minReputation, int maxReputation, int initialReputation)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(metadata != null);
|
||||
System.Diagnostics.Debug.Assert(faction != null || location != null);
|
||||
Metadata = metadata;
|
||||
Identifier = identifier.ToLowerInvariant();
|
||||
metaDataIdentifier = $"reputation.{Identifier}";
|
||||
MinReputation = minReputation;
|
||||
MaxReputation = maxReputation;
|
||||
InitialReputation = initialReputation;
|
||||
Faction = faction;
|
||||
Location = location;
|
||||
}
|
||||
|
||||
public string GetReputationName()
|
||||
|
||||
@@ -78,10 +78,19 @@ namespace Barotrauma
|
||||
//there can be no events before this time has passed during the 1st campaign round
|
||||
const float FirstRoundEventDelay = 0.0f;
|
||||
|
||||
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
|
||||
public double TotalPlayTime;
|
||||
public int TotalPassedLevels;
|
||||
|
||||
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub, MedicalClinic, Cargo }
|
||||
|
||||
public static bool BlocksInteraction(InteractionType interactionType)
|
||||
{
|
||||
return interactionType != InteractionType.None && interactionType != InteractionType.Cargo;
|
||||
}
|
||||
|
||||
public readonly CargoManager CargoManager;
|
||||
public UpgradeManager UpgradeManager;
|
||||
public MedicalClinic MedicalClinic;
|
||||
|
||||
public List<Faction> Factions;
|
||||
|
||||
@@ -91,7 +100,7 @@ namespace Barotrauma
|
||||
|
||||
public CampaignSettings Settings;
|
||||
|
||||
private List<Mission> extraMissions = new List<Mission>();
|
||||
private readonly List<Mission> extraMissions = new List<Mission>();
|
||||
|
||||
public enum TransitionType
|
||||
{
|
||||
@@ -176,6 +185,7 @@ namespace Barotrauma
|
||||
{
|
||||
Money = InitialMoney;
|
||||
CargoManager = new CargoManager(this);
|
||||
MedicalClinic = new MedicalClinic(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -688,11 +698,13 @@ namespace Barotrauma
|
||||
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
GameAnalyticsManager.ProgressionStatus.Complete,
|
||||
Name ?? "none");
|
||||
Preset?.Identifier ?? "none");
|
||||
string eventId = "FinishCampaign:";
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels);
|
||||
}
|
||||
|
||||
protected virtual void EndCampaignProjSpecific() { }
|
||||
@@ -705,12 +717,14 @@ namespace Barotrauma
|
||||
location.RemoveHireableCharacter(characterInfo);
|
||||
CrewManager.AddCharacterInfo(characterInfo);
|
||||
Money -= characterInfo.Salary;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier ?? "unknown");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void NPCInteract(Character npc, Character interactor)
|
||||
{
|
||||
if (!npc.AllowCustomInteract) { return; }
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignInteraction:" + Preset.Identifier + ":" + npc.CampaignInteractionType);
|
||||
NPCInteractProjSpecific(npc, interactor);
|
||||
string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID);
|
||||
if (!CoroutineManager.IsCoroutineRunning(coroutineName))
|
||||
@@ -874,6 +888,19 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public abstract void Save(XElement element);
|
||||
|
||||
protected void LoadStats(XElement element)
|
||||
{
|
||||
TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0);
|
||||
TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0);
|
||||
}
|
||||
|
||||
protected XElement SaveStats()
|
||||
{
|
||||
return new XElement("stats",
|
||||
new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime),
|
||||
new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels));
|
||||
}
|
||||
|
||||
public void LogState()
|
||||
{
|
||||
|
||||
@@ -126,6 +126,10 @@ namespace Barotrauma
|
||||
{
|
||||
case "campaignsettings":
|
||||
Settings = new CampaignSettings(subElement);
|
||||
#if CLIENT
|
||||
GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount;
|
||||
GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled;
|
||||
#endif
|
||||
break;
|
||||
case "map":
|
||||
if (map == null)
|
||||
@@ -159,6 +163,9 @@ namespace Barotrauma
|
||||
case "pets":
|
||||
petsElement = subElement;
|
||||
break;
|
||||
case "stats":
|
||||
LoadStats(subElement);
|
||||
break;
|
||||
#if SERVER
|
||||
case "savedexperiencepoints":
|
||||
foreach (XElement savedExp in subElement.Elements())
|
||||
@@ -192,6 +199,37 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
public static List<SubmarineInfo> GetCampaignSubs()
|
||||
{
|
||||
bool isSubmarineVisible(SubmarineInfo s)
|
||||
=> !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h
|
||||
=> s.Name.Equals(h, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
List<SubmarineInfo> availableSubs =
|
||||
SubmarineInfo.SavedSubmarines
|
||||
.Where(s =>
|
||||
s.IsCampaignCompatible
|
||||
&& isSubmarineVisible(s))
|
||||
.ToList();
|
||||
|
||||
if (!availableSubs.Any())
|
||||
{
|
||||
//None of the available subs were marked as campaign-compatible, just include all visible subs
|
||||
availableSubs.AddRange(
|
||||
SubmarineInfo.SavedSubmarines
|
||||
.Where(isSubmarineVisible));
|
||||
}
|
||||
|
||||
if (!availableSubs.Any())
|
||||
{
|
||||
//No subs are visible at all! Just make the selected one available
|
||||
availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
|
||||
}
|
||||
|
||||
return availableSubs;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace Barotrauma
|
||||
|
||||
public double RoundStartTime;
|
||||
|
||||
public double TimeSpentCleaning, TimeSpentPainting;
|
||||
|
||||
private readonly List<Mission> missions = new List<Mission>();
|
||||
public IEnumerable<Mission> Missions { get { return missions; } }
|
||||
|
||||
@@ -276,6 +278,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Campaign.Money -= cost;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name);
|
||||
|
||||
((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine;
|
||||
return newSubmarine;
|
||||
@@ -288,6 +291,7 @@ namespace Barotrauma
|
||||
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
|
||||
{
|
||||
Campaign.Money -= newSubmarine.Price;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
|
||||
OwnedSubmarines.Add(newSubmarine);
|
||||
}
|
||||
}
|
||||
@@ -409,18 +413,52 @@ namespace Barotrauma
|
||||
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
GameAnalyticsManager.ProgressionStatus.Start,
|
||||
GameMode?.Name ?? "none");
|
||||
GameMode?.Preset?.Identifier ?? "none");
|
||||
|
||||
string eventId = "StartRound:GameMode:" + (GameMode?.Name ?? "none") + ":";
|
||||
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":";
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"));
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ?
|
||||
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
|
||||
Level.Loaded.GenerationParams?.Identifier;
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + (levelId ?? "null"));
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"));
|
||||
#if CLIENT
|
||||
if (GameMode is TutorialMode tutorialMode)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
|
||||
if (GameMain.IsFirstLaunch)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
|
||||
}
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
|
||||
#endif
|
||||
if (GameMode is CampaignMode campaignMode)
|
||||
{
|
||||
if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled");
|
||||
}
|
||||
bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData.Biome);
|
||||
if (firstTimeInBiome)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
|
||||
}
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
|
||||
@@ -457,6 +495,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
ReadyCheck.ReadyCheckCooldown = DateTime.MinValue;
|
||||
|
||||
GUI.PreventPauseMenuToggle = false;
|
||||
|
||||
HintManager.OnRoundStarted();
|
||||
@@ -752,38 +792,29 @@ namespace Barotrauma
|
||||
GameMode?.End(transitionType);
|
||||
EventManager?.EndRound();
|
||||
StatusEffect.StopAll();
|
||||
missions.Clear();
|
||||
IsRunning = false;
|
||||
|
||||
|
||||
bool success = false;
|
||||
#if CLIENT
|
||||
success = CrewManager.GetCharacters().Any(c => !c.IsDead);
|
||||
bool success = CrewManager.GetCharacters().Any(c => !c.IsDead);
|
||||
#else
|
||||
success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
||||
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
||||
#endif
|
||||
double roundDuration = Timing.TotalTime - RoundStartTime;
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
|
||||
GameMode?.Name ?? "none",
|
||||
roundDuration);
|
||||
string eventId = "EndRound:GameMode:" + (GameMode?.Name ?? "none") + ":";
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration);
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration);
|
||||
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":";
|
||||
LogEndRoundStats(eventId);
|
||||
if (GameMode is CampaignMode campaignMode)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney);
|
||||
campaignMode.TotalPlayTime += roundDuration;
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnRoundEnded();
|
||||
#endif
|
||||
missions.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -791,6 +822,82 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void LogEndRoundStats(string eventId)
|
||||
{
|
||||
double roundDuration = Timing.TotalTime - RoundStartTime;
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration);
|
||||
}
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ?
|
||||
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
|
||||
Level.Loaded.GenerationParams?.Identifier;
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + (levelId ?? "null")), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration);
|
||||
}
|
||||
|
||||
if (Submarine.MainSub != null)
|
||||
{
|
||||
Dictionary<ItemPrefab, int> submarineInventory = new Dictionary<ItemPrefab, int>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
var rootContainer = item.GetRootContainer() ?? item;
|
||||
if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; }
|
||||
|
||||
var holdable = item.GetComponent<Holdable>();
|
||||
if (holdable == null || holdable.Attached) { continue; }
|
||||
var wire = item.GetComponent<Wire>();
|
||||
if (wire != null && wire.Connections.Any(c => c != null)) { continue; }
|
||||
|
||||
if (!submarineInventory.ContainsKey(item.Prefab))
|
||||
{
|
||||
submarineInventory.Add(item.Prefab, 0);
|
||||
}
|
||||
submarineInventory[item.Prefab]++;
|
||||
}
|
||||
foreach (var subItem in submarineInventory)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Character c in GetSessionCrewCharacters())
|
||||
{
|
||||
foreach (var itemSelectedDuration in c.ItemSelectedDurations)
|
||||
{
|
||||
string characterType = "Unknown";
|
||||
if (c.IsBot)
|
||||
{
|
||||
characterType = "Bot";
|
||||
}
|
||||
else if (c.IsPlayer)
|
||||
{
|
||||
characterType = "Player";
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
if (GameMode is TutorialMode tutorialMode)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
|
||||
if (GameMain.IsFirstLaunch)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
|
||||
}
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting);
|
||||
TimeSpentCleaning = TimeSpentPainting = 0.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void KillCharacter(Character character)
|
||||
{
|
||||
#if CLIENT
|
||||
@@ -901,14 +1008,7 @@ namespace Barotrauma
|
||||
|
||||
((CampaignMode)GameMode).Save(doc.Root);
|
||||
|
||||
try
|
||||
{
|
||||
doc.SaveSafe(filePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Saving gamesession to \"" + filePath + "\" failed!", e);
|
||||
}
|
||||
doc.SaveSafe(filePath, throwExceptions: true);
|
||||
}
|
||||
|
||||
/*public void Load(XElement saveElement)
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal partial class MedicalClinic
|
||||
{
|
||||
public enum NetworkHeader
|
||||
{
|
||||
REQUEST_AFFLICTIONS,
|
||||
REQUEST_PENDING,
|
||||
ADD_PENDING,
|
||||
REMOVE_PENDING,
|
||||
CLEAR_PENDING,
|
||||
HEAL_PENDING
|
||||
}
|
||||
|
||||
public enum AfflictionSeverity
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High
|
||||
}
|
||||
|
||||
public enum MessageFlag
|
||||
{
|
||||
Response, // responding to your request
|
||||
Announce // responding to someone else's request
|
||||
}
|
||||
|
||||
public enum HealRequestResult
|
||||
{
|
||||
Unknown, // everything is not ok
|
||||
Success, // everything ok
|
||||
InsufficientFunds, // not enough money
|
||||
Refused // the outpost has refused to provide medical assistance
|
||||
}
|
||||
|
||||
[NetworkSerialize]
|
||||
public struct NetHealRequest : INetSerializableStruct
|
||||
{
|
||||
public HealRequestResult Result;
|
||||
}
|
||||
|
||||
[NetworkSerialize]
|
||||
public struct NetRemovedAffliction : INetSerializableStruct
|
||||
{
|
||||
public NetCrewMember CrewMember;
|
||||
public NetAffliction Affliction;
|
||||
}
|
||||
|
||||
public struct NetPendingCrew : INetSerializableStruct
|
||||
{
|
||||
[NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)]
|
||||
public NetCrewMember[] CrewMembers;
|
||||
}
|
||||
|
||||
public struct NetAffliction : INetSerializableStruct
|
||||
{
|
||||
[NetworkSerialize]
|
||||
public string Identifier;
|
||||
|
||||
[NetworkSerialize]
|
||||
public ushort Strength;
|
||||
|
||||
[NetworkSerialize]
|
||||
public ushort Price;
|
||||
|
||||
public AfflictionSeverity AfflictionSeverity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Prefab is null) { return AfflictionSeverity.Low; }
|
||||
|
||||
float normalizedStrength = Strength / Prefab.MaxStrength;
|
||||
|
||||
// lesser than 0.1
|
||||
if (normalizedStrength <= 0.1)
|
||||
{
|
||||
return AfflictionSeverity.Low;
|
||||
}
|
||||
|
||||
// between 0.1 and 0.5
|
||||
if (normalizedStrength > 0.1f && normalizedStrength < 0.5f)
|
||||
{
|
||||
return AfflictionSeverity.Medium;
|
||||
}
|
||||
|
||||
// greater than 0.5
|
||||
return AfflictionSeverity.High;
|
||||
}
|
||||
}
|
||||
|
||||
public Affliction Affliction
|
||||
{
|
||||
set
|
||||
{
|
||||
Identifier = value.Identifier;
|
||||
Strength = (ushort)Math.Ceiling(value.Strength);
|
||||
Price = (ushort)(value.Prefab.BaseHealCost + Strength * value.Prefab.HealCostMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
private AfflictionPrefab? cachedPrefab;
|
||||
|
||||
public AfflictionPrefab? Prefab
|
||||
{
|
||||
get
|
||||
{
|
||||
if (cachedPrefab is { } cached) { return cached; }
|
||||
|
||||
foreach (AfflictionPrefab prefab in AfflictionPrefab.List)
|
||||
{
|
||||
if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cachedPrefab = prefab;
|
||||
return prefab;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
cachedPrefab = value;
|
||||
Identifier = value?.Identifier ?? string.Empty;
|
||||
Strength = 0;
|
||||
Price = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly bool AfflictionEquals(AfflictionPrefab prefab)
|
||||
{
|
||||
return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public readonly bool AfflictionEquals(NetAffliction affliction)
|
||||
{
|
||||
return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public struct NetCrewMember : INetSerializableStruct
|
||||
{
|
||||
[NetworkSerialize]
|
||||
public int CharacterInfoID;
|
||||
|
||||
[NetworkSerialize]
|
||||
public NetAffliction[] Afflictions;
|
||||
|
||||
public CharacterInfo CharacterInfo
|
||||
{
|
||||
set => CharacterInfoID = value.GetIdentifierUsingOriginalName();
|
||||
}
|
||||
|
||||
public readonly CharacterInfo? FindCharacterInfo(ImmutableArray<CharacterInfo> crew)
|
||||
{
|
||||
foreach (CharacterInfo info in crew)
|
||||
{
|
||||
if (info.GetIdentifierUsingOriginalName() == CharacterInfoID)
|
||||
{
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public readonly bool CharacterEquals(NetCrewMember crewMember)
|
||||
{
|
||||
return crewMember.CharacterInfoID == CharacterInfoID;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly CampaignMode? campaign;
|
||||
|
||||
public MedicalClinic(CampaignMode campaign)
|
||||
{
|
||||
this.campaign = campaign;
|
||||
}
|
||||
|
||||
public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();
|
||||
|
||||
public Action? OnUpdate;
|
||||
|
||||
private static bool IsOutpostInCombat()
|
||||
{
|
||||
if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; }
|
||||
|
||||
IEnumerable<Character> crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet();
|
||||
|
||||
foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC))
|
||||
{
|
||||
bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy);
|
||||
if (isInCombatWithCrew) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private HealRequestResult HealAllPending(bool force = false)
|
||||
{
|
||||
int totalCost = GetTotalCost();
|
||||
if (!force)
|
||||
{
|
||||
if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; }
|
||||
|
||||
if (IsOutpostInCombat()) { return HealRequestResult.Refused; }
|
||||
}
|
||||
|
||||
ImmutableArray<CharacterInfo> crew = GetCrewCharacters();
|
||||
foreach (NetCrewMember crewMember in PendingHeals)
|
||||
{
|
||||
CharacterInfo? targetCharacter = crewMember.FindCharacterInfo(crew);
|
||||
if (!(targetCharacter?.Character is { CharacterHealth: { } health })) { continue; }
|
||||
|
||||
foreach (NetAffliction affliction in crewMember.Afflictions)
|
||||
{
|
||||
health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength);
|
||||
}
|
||||
}
|
||||
|
||||
if (campaign != null)
|
||||
{
|
||||
campaign.Money -= totalCost;
|
||||
}
|
||||
|
||||
ClearPendingHeals();
|
||||
|
||||
return HealRequestResult.Success;
|
||||
}
|
||||
|
||||
private void ClearPendingHeals()
|
||||
{
|
||||
PendingHeals.Clear();
|
||||
}
|
||||
|
||||
private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction)
|
||||
{
|
||||
foreach (NetCrewMember listMember in PendingHeals.ToList())
|
||||
{
|
||||
PendingHeals.Remove(listMember);
|
||||
NetCrewMember pendingMember = listMember;
|
||||
|
||||
if (pendingMember.CharacterEquals(crewMember))
|
||||
{
|
||||
List<NetAffliction> newAfflictions = new List<NetAffliction>();
|
||||
foreach (NetAffliction pendingAffliction in pendingMember.Afflictions)
|
||||
{
|
||||
if (pendingAffliction.AfflictionEquals(affliction)) { continue; }
|
||||
|
||||
newAfflictions.Add(pendingAffliction);
|
||||
}
|
||||
|
||||
pendingMember.Afflictions = newAfflictions.ToArray();
|
||||
}
|
||||
|
||||
if (!pendingMember.Afflictions.Any()) { continue; }
|
||||
|
||||
PendingHeals.Add(pendingMember);
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertPendingCrewMember(NetCrewMember crewMember)
|
||||
{
|
||||
if (PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal)
|
||||
{
|
||||
PendingHeals.Remove(foundHeal);
|
||||
}
|
||||
|
||||
PendingHeals.Add(crewMember);
|
||||
}
|
||||
|
||||
public static bool IsHealable(Affliction affliction)
|
||||
{
|
||||
return affliction.Prefab.HealableInMedicalClinic && affliction.Strength > GetShowTreshold(affliction);
|
||||
static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold));
|
||||
}
|
||||
|
||||
private NetAffliction[] GetAllAfflictions(CharacterHealth health)
|
||||
{
|
||||
IEnumerable<Affliction> rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a));
|
||||
|
||||
List<NetAffliction> afflictions = new List<NetAffliction>();
|
||||
|
||||
foreach (Affliction affliction in rawAfflictions)
|
||||
{
|
||||
NetAffliction newAffliction;
|
||||
if (afflictions.FirstOrNull(netAffliction => netAffliction.AfflictionEquals(affliction.Prefab)) is { } foundAffliction)
|
||||
{
|
||||
afflictions.Remove(foundAffliction);
|
||||
foundAffliction.Strength += (ushort)affliction.Strength;
|
||||
foundAffliction.Price += (ushort)GetAdjustedPrice(GetHealPrice(affliction));
|
||||
newAffliction = foundAffliction;
|
||||
}
|
||||
else
|
||||
{
|
||||
newAffliction = new NetAffliction { Affliction = affliction };
|
||||
newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price);
|
||||
}
|
||||
|
||||
afflictions.Add(newAffliction);
|
||||
}
|
||||
|
||||
return afflictions.ToArray();
|
||||
|
||||
static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength));
|
||||
}
|
||||
|
||||
public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price);
|
||||
|
||||
private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue;
|
||||
|
||||
public int GetMoney() => campaign?.Money ?? 0;
|
||||
|
||||
public static ImmutableArray<CharacterInfo> GetCrewCharacters()
|
||||
{
|
||||
#if DEBUG && CLIENT
|
||||
if (Screen.Selected is TestScreen)
|
||||
{
|
||||
return TestInfos.ToImmutableArray();
|
||||
}
|
||||
#endif
|
||||
|
||||
return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray();
|
||||
}
|
||||
|
||||
#if DEBUG && CLIENT
|
||||
private static readonly CharacterInfo[] TestInfos =
|
||||
{
|
||||
new CharacterInfo("human"),
|
||||
new CharacterInfo("human"),
|
||||
new CharacterInfo("human"),
|
||||
new CharacterInfo("human"),
|
||||
new CharacterInfo("human"),
|
||||
new CharacterInfo("human"),
|
||||
new CharacterInfo("human")
|
||||
};
|
||||
|
||||
private static readonly NetAffliction[] TestAfflictions =
|
||||
{
|
||||
new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 },
|
||||
new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 },
|
||||
new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 },
|
||||
new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 }
|
||||
};
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Campaign.Money -= price;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier);
|
||||
|
||||
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
|
||||
|
||||
@@ -323,6 +324,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Campaign.Money -= price;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier);
|
||||
|
||||
foreach (Item itemToSwap in linkedItems)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user