(965c31410a) Unstable v0.10.4.0
This commit is contained in:
@@ -12,22 +12,19 @@ namespace Barotrauma
|
||||
|
||||
public static bool OutputDebugInfo = false;
|
||||
|
||||
public static void PlaceIfNeeded(GameMode gameMode)
|
||||
public static void PlaceIfNeeded()
|
||||
{
|
||||
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
|
||||
|
||||
CampaignMode campaign = gameMode as CampaignMode;
|
||||
if (campaign == null || !campaign.InitialSuppliesSpawned)
|
||||
for (int i = 0; i < Submarine.MainSubs.Length; i++)
|
||||
{
|
||||
for (int i = 0; i < Submarine.MainSubs.Length; i++)
|
||||
{
|
||||
if (Submarine.MainSubs[i] == null) { continue; }
|
||||
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
|
||||
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
|
||||
Place(subs);
|
||||
}
|
||||
if (campaign != null) { campaign.InitialSuppliesSpawned = true; }
|
||||
if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; }
|
||||
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
|
||||
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
|
||||
Place(subs);
|
||||
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
|
||||
}
|
||||
|
||||
foreach (var wreck in Submarine.Loaded)
|
||||
{
|
||||
if (wreck.Info.IsWreck)
|
||||
@@ -35,6 +32,12 @@ namespace Barotrauma
|
||||
Place(wreck.ToEnumerable());
|
||||
}
|
||||
}
|
||||
|
||||
if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(Level.Loaded.StartOutpost.Info.Name));
|
||||
Place(Level.Loaded.StartOutpost.ToEnumerable());
|
||||
}
|
||||
}
|
||||
|
||||
private static void Place(IEnumerable<Submarine> subs)
|
||||
@@ -54,9 +57,10 @@ namespace Barotrauma
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (!subs.Contains(item.Submarine)) { continue; }
|
||||
if (item.GetRootInventoryOwner() is Character) { continue; }
|
||||
containers.AddRange(item.GetComponents<ItemContainer>());
|
||||
}
|
||||
containers.Shuffle();
|
||||
containers.Shuffle(Rand.RandSync.Server);
|
||||
|
||||
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
|
||||
{
|
||||
@@ -74,7 +78,7 @@ namespace Barotrauma
|
||||
|
||||
spawnedItems.Clear();
|
||||
var validContainers = new Dictionary<ItemContainer, PreferredContainer>();
|
||||
prefabsWithContainer.Shuffle();
|
||||
prefabsWithContainer.Shuffle(Rand.RandSync.Server);
|
||||
// Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc)
|
||||
for (int i = 0; i < prefabsWithContainer.Count; i++)
|
||||
{
|
||||
@@ -90,7 +94,7 @@ namespace Barotrauma
|
||||
// Another pass for items with containers because also they can spawn inside other items (like smg magazine)
|
||||
prefabsWithContainer.ForEach(i => SpawnItems(i));
|
||||
// Spawn items that don't have containers last
|
||||
prefabsWithoutContainer.Shuffle();
|
||||
prefabsWithoutContainer.Shuffle(Rand.RandSync.Server);
|
||||
prefabsWithoutContainer.ForEach(i => SpawnItems(i));
|
||||
|
||||
if (OutputDebugInfo)
|
||||
@@ -103,6 +107,30 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.Level != null &&
|
||||
GameMain.GameSession.Level.Type == LevelData.LevelType.Outpost &&
|
||||
GameMain.GameSession.StartLocation?.TakenItems != null)
|
||||
{
|
||||
foreach (Location.TakenItem takenItem in GameMain.GameSession.StartLocation.TakenItems)
|
||||
{
|
||||
var matchingItem = spawnedItems.Find(it => takenItem.Matches(it));
|
||||
if (matchingItem == null) { continue; }
|
||||
var containedItems = spawnedItems.FindAll(it => it.ParentInventory?.Owner == matchingItem);
|
||||
matchingItem.Remove();
|
||||
spawnedItems.Remove(matchingItem);
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
containedItem.Remove();
|
||||
spawnedItems.Remove(containedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
foreach (Item spawnedItem in spawnedItems)
|
||||
{
|
||||
Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false);
|
||||
}
|
||||
#endif
|
||||
bool SpawnItems(ItemPrefab itemPrefab)
|
||||
{
|
||||
if (itemPrefab == null)
|
||||
@@ -158,13 +186,13 @@ namespace Barotrauma
|
||||
private static bool SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
|
||||
{
|
||||
bool success = false;
|
||||
if (Rand.Value() > validContainer.Value.SpawnProbability) { return false; }
|
||||
if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability) { return false; }
|
||||
// Don't add dangerously reactive materials in thalamus wrecks
|
||||
if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1);
|
||||
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server);
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
if (validContainer.Key.Inventory.IsFull())
|
||||
@@ -172,17 +200,18 @@ namespace Barotrauma
|
||||
containers.Remove(validContainer.Key);
|
||||
break;
|
||||
}
|
||||
|
||||
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine);
|
||||
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
|
||||
{
|
||||
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
|
||||
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
|
||||
OriginalContainerID = validContainer.Key.Item.OriginalID
|
||||
};
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
{
|
||||
wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID;
|
||||
}
|
||||
spawnedItems.Add(item);
|
||||
#if SERVER
|
||||
Entity.Spawner.CreateNetworkEvent(item, remove: false);
|
||||
#endif
|
||||
validContainer.Key.Inventory.TryPutItem(item, null);
|
||||
validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false);
|
||||
containers.AddRange(item.GetComponents<ItemContainer>());
|
||||
success = true;
|
||||
}
|
||||
|
||||
@@ -1,93 +1,151 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
#if SERVER
|
||||
using Barotrauma.Networking;
|
||||
#endif
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class PurchasedItem
|
||||
{
|
||||
public readonly ItemPrefab ItemPrefab;
|
||||
public int Quantity;
|
||||
public ItemPrefab ItemPrefab { get; }
|
||||
public int Quantity { get; set; }
|
||||
|
||||
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
|
||||
{
|
||||
this.ItemPrefab = itemPrefab;
|
||||
this.Quantity = quantity;
|
||||
ItemPrefab = itemPrefab;
|
||||
Quantity = quantity;
|
||||
}
|
||||
}
|
||||
|
||||
class CargoManager
|
||||
class SoldItem
|
||||
{
|
||||
public ItemPrefab ItemPrefab { get; }
|
||||
public ushort ID { get; }
|
||||
public bool Removed { get; set; }
|
||||
public byte SellerID { get; }
|
||||
|
||||
public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId)
|
||||
{
|
||||
ItemPrefab = itemPrefab;
|
||||
ID = id;
|
||||
Removed = removed;
|
||||
SellerID = sellerId;
|
||||
}
|
||||
}
|
||||
|
||||
partial class CargoManager
|
||||
{
|
||||
public const int MaxQuantity = 100;
|
||||
|
||||
private readonly List<PurchasedItem> purchasedItems;
|
||||
public List<PurchasedItem> ItemsInBuyCrate { get; } = new List<PurchasedItem>();
|
||||
public List<PurchasedItem> ItemsInSellCrate { get; } = new List<PurchasedItem>();
|
||||
public List<PurchasedItem> PurchasedItems { get; } = new List<PurchasedItem>();
|
||||
public List<SoldItem> SoldItems { get; } = new List<SoldItem>();
|
||||
|
||||
private readonly CampaignMode campaign;
|
||||
|
||||
public Action OnItemsChanged;
|
||||
private Location location => campaign.Map.CurrentLocation;
|
||||
|
||||
public List<PurchasedItem> PurchasedItems
|
||||
{
|
||||
get { return purchasedItems; }
|
||||
}
|
||||
public Action OnItemsInBuyCrateChanged;
|
||||
public Action OnItemsInSellCrateChanged;
|
||||
public Action OnPurchasedItemsChanged;
|
||||
public Action OnSoldItemsChanged;
|
||||
|
||||
public CargoManager(CampaignMode campaign)
|
||||
{
|
||||
purchasedItems = new List<PurchasedItem>();
|
||||
this.campaign = campaign;
|
||||
}
|
||||
|
||||
public void ClearItemsInBuyCrate()
|
||||
{
|
||||
ItemsInBuyCrate.Clear();
|
||||
OnItemsInBuyCrateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void ClearItemsInSellCrate()
|
||||
{
|
||||
ItemsInSellCrate.Clear();
|
||||
OnItemsInSellCrateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SetPurchasedItems(List<PurchasedItem> items)
|
||||
{
|
||||
purchasedItems.Clear();
|
||||
purchasedItems.AddRange(items);
|
||||
|
||||
OnItemsChanged?.Invoke();
|
||||
PurchasedItems.Clear();
|
||||
PurchasedItems.AddRange(items);
|
||||
OnPurchasedItemsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void PurchaseItem(ItemPrefab item, int quantity = 1)
|
||||
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity)
|
||||
{
|
||||
PurchasedItem purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item);
|
||||
|
||||
campaign.Money -= item.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
|
||||
if (purchasedItem != null)
|
||||
PurchasedItem itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
|
||||
if (itemInCrate != null)
|
||||
{
|
||||
purchasedItem.Quantity += quantity;
|
||||
itemInCrate.Quantity += changeInQuantity;
|
||||
if (itemInCrate.Quantity < 1)
|
||||
{
|
||||
ItemsInBuyCrate.Remove(itemInCrate);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if(changeInQuantity > 0)
|
||||
{
|
||||
purchasedItem = new PurchasedItem(item, quantity);
|
||||
purchasedItems.Add(purchasedItem);
|
||||
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
|
||||
ItemsInBuyCrate.Add(itemInCrate);
|
||||
}
|
||||
|
||||
OnItemsChanged?.Invoke();
|
||||
OnItemsInBuyCrateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SellItem(PurchasedItem purchasedItem, int quantity = 1)
|
||||
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate)
|
||||
{
|
||||
quantity = Math.Min(purchasedItem.Quantity, quantity);
|
||||
campaign.Money += purchasedItem.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
|
||||
purchasedItem.Quantity -= quantity;
|
||||
if (purchasedItem != null && purchasedItem.Quantity <= 0)
|
||||
foreach (PurchasedItem item in itemsToPurchase)
|
||||
{
|
||||
PurchasedItems.Remove(purchasedItem);
|
||||
// Add to the purchased items
|
||||
var purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item.ItemPrefab);
|
||||
if (purchasedItem != null)
|
||||
{
|
||||
purchasedItem.Quantity += item.Quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity);
|
||||
PurchasedItems.Add(purchasedItem);
|
||||
}
|
||||
|
||||
// Exchange money
|
||||
var itemValue = GetBuyValueAtCurrentLocation(item);
|
||||
campaign.Money -= itemValue;
|
||||
campaign.Map.CurrentLocation.StoreCurrentBalance += itemValue;
|
||||
|
||||
if (removeFromCrate)
|
||||
{
|
||||
// Remove from the shopping crate
|
||||
var crateItem = ItemsInBuyCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab);
|
||||
if (crateItem != null)
|
||||
{
|
||||
crateItem.Quantity -= item.Quantity;
|
||||
if (crateItem.Quantity < 1) { ItemsInBuyCrate.Remove(crateItem); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnItemsChanged?.Invoke();
|
||||
OnPurchasedItemsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public int GetTotalItemCost()
|
||||
{
|
||||
if (purchasedItems == null) return 0;
|
||||
return purchasedItems.Sum(i => i.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * i.Quantity);
|
||||
}
|
||||
public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && campaign?.Map?.CurrentLocation != null ?
|
||||
item.Quantity* campaign.Map.CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0;
|
||||
|
||||
public void CreateItems()
|
||||
public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && campaign?.Map?.CurrentLocation != null ?
|
||||
quantity * campaign.Map.CurrentLocation.GetAdjustedItemSellPrice(itemPrefab) : 0;
|
||||
|
||||
public void CreatePurchasedItems()
|
||||
{
|
||||
CreateItems(purchasedItems);
|
||||
OnItemsChanged?.Invoke();
|
||||
CreateItems(PurchasedItems);
|
||||
OnPurchasedItemsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public static void CreateItems(List<PurchasedItem> itemsToSpawn)
|
||||
@@ -110,7 +168,14 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true));
|
||||
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon");
|
||||
#else
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
ChatMessage msg = ChatMessage.Create("", $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}", ChatMessageType.ServerMessageBoxInGame, null);
|
||||
msg.IconStyle = "StoreShoppingCrateIcon";
|
||||
GameMain.Server.SendDirectChatMessage(msg, client);
|
||||
}
|
||||
#endif
|
||||
|
||||
Dictionary<ItemContainer, int> availableContainers = new Dictionary<ItemContainer, int>();
|
||||
@@ -179,11 +244,12 @@ namespace Barotrauma
|
||||
//no container, place at the waypoint
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine);
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine, onSpawned: itemSpawned);
|
||||
}
|
||||
else
|
||||
{
|
||||
new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
itemSpawned(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -205,12 +271,25 @@ namespace Barotrauma
|
||||
//place in the container
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory);
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory, onSpawned: itemSpawned);
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
itemContainer.Inventory.TryPutItem(item, null);
|
||||
itemSpawned(item);
|
||||
}
|
||||
|
||||
static void itemSpawned(Item item)
|
||||
{
|
||||
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
|
||||
if (sub != null)
|
||||
{
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
{
|
||||
wifiComponent.TeamID = sub.TeamID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//reduce the number of available slots in the container
|
||||
@@ -227,5 +306,36 @@ namespace Barotrauma
|
||||
}
|
||||
itemsToSpawn.Clear();
|
||||
}
|
||||
|
||||
public void SavePurchasedItems(XElement parentElement)
|
||||
{
|
||||
var itemsElement = new XElement("cargo");
|
||||
foreach (PurchasedItem item in PurchasedItems)
|
||||
{
|
||||
if (item?.ItemPrefab == null) { continue; }
|
||||
itemsElement.Add(new XElement("item",
|
||||
new XAttribute("id", item.ItemPrefab.Identifier),
|
||||
new XAttribute("qty", item.Quantity)));
|
||||
}
|
||||
parentElement.Add(itemsElement);
|
||||
}
|
||||
|
||||
public void LoadPurchasedItems(XElement element)
|
||||
{
|
||||
var purchasedItems = new List<PurchasedItem>();
|
||||
if (element != null)
|
||||
{
|
||||
foreach (XElement itemElement in element.GetChildElements("item"))
|
||||
{
|
||||
var id = itemElement.GetAttributeString("id", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
|
||||
if (prefab == null) { continue; }
|
||||
var qty = itemElement.GetAttributeInt("qty", 0);
|
||||
purchasedItems.Add(new PurchasedItem(prefab, qty));
|
||||
}
|
||||
}
|
||||
SetPurchasedItems(purchasedItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -10,7 +12,16 @@ namespace Barotrauma
|
||||
const float ConversationIntervalMax = 180.0f;
|
||||
const float ConversationIntervalMultiplierMultiplayer = 5.0f;
|
||||
private float conversationTimer, conversationLineTimer;
|
||||
private List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
|
||||
private readonly List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
|
||||
|
||||
private readonly List<CharacterInfo> characterInfos = new List<CharacterInfo>();
|
||||
private readonly List<Character> characters = new List<Character>();
|
||||
|
||||
private Character welcomeMessageNPC;
|
||||
|
||||
public List<CharacterInfo> CharacterInfos => characterInfos;
|
||||
|
||||
public bool HasBots { get; set; }
|
||||
|
||||
public List<Pair<Order, float>> ActiveOrders { get; } = new List<Pair<Order, float>>();
|
||||
public bool IsSinglePlayer { get; private set; }
|
||||
@@ -51,6 +62,145 @@ namespace Barotrauma
|
||||
ActiveOrders.RemoveAll(o => o.First == order);
|
||||
}
|
||||
|
||||
public void AddCharacterElements(XElement element)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (!subElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
|
||||
CharacterInfo characterInfo = new CharacterInfo(subElement);
|
||||
#if CLIENT
|
||||
if (subElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; }
|
||||
#endif
|
||||
characterInfos.Add(characterInfo);
|
||||
foreach (XElement invElement in subElement.Elements())
|
||||
{
|
||||
if (!invElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
characterInfo.InventoryData = invElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove info of a selected character. The character will not be visible in any menus or the round summary.
|
||||
/// </summary>
|
||||
/// <param name="characterInfo"></param>
|
||||
public void RemoveCharacterInfo(CharacterInfo characterInfo)
|
||||
{
|
||||
characterInfos.Remove(characterInfo);
|
||||
}
|
||||
|
||||
public void AddCharacter(Character character)
|
||||
{
|
||||
if (character.Removed)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
if (character.IsDead)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!characters.Contains(character))
|
||||
{
|
||||
characters.Add(character);
|
||||
}
|
||||
if (!characterInfos.Contains(character.Info))
|
||||
{
|
||||
characterInfos.Add(character.Info);
|
||||
}
|
||||
#if CLIENT
|
||||
AddCharacterToCrewList(character);
|
||||
DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void AddCharacterInfo(CharacterInfo characterInfo)
|
||||
{
|
||||
if (characterInfos.Contains(characterInfo))
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
characterInfos.Add(characterInfo);
|
||||
}
|
||||
|
||||
public void InitRound()
|
||||
{
|
||||
characters.Clear();
|
||||
|
||||
List<WayPoint> spawnWaypoints = null;
|
||||
List<WayPoint> mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList();
|
||||
|
||||
if (Level.IsLoadedOutpost)
|
||||
{
|
||||
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
|
||||
wp.SpawnType == SpawnType.Human &&
|
||||
wp.Submarine == Level.Loaded.StartOutpost &&
|
||||
wp.CurrentHull?.OutpostModuleTags != null &&
|
||||
wp.CurrentHull.OutpostModuleTags.Contains("airlock"));
|
||||
while (spawnWaypoints.Count > characterInfos.Count)
|
||||
{
|
||||
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
|
||||
}
|
||||
while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count)
|
||||
{
|
||||
spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]);
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnWaypoints == null || !spawnWaypoints.Any())
|
||||
{
|
||||
spawnWaypoints = mainSubWaypoints;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(spawnWaypoints.Count == mainSubWaypoints.Count);
|
||||
|
||||
for (int i = 0; i < spawnWaypoints.Count; i++)
|
||||
{
|
||||
var info = characterInfos[i];
|
||||
info.TeamID = Character.TeamType.Team1;
|
||||
Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name);
|
||||
if (character.Info != null)
|
||||
{
|
||||
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
|
||||
}
|
||||
if (character.Info.InventoryData != null)
|
||||
{
|
||||
character.Info.SpawnInventoryItems(character.Inventory, character.Info.InventoryData);
|
||||
}
|
||||
else if (!character.Info.StartItemsGiven)
|
||||
{
|
||||
character.GiveJobItems(mainSubWaypoints[i]);
|
||||
}
|
||||
if (character.Info.HealthData != null)
|
||||
{
|
||||
character.Info.ApplyHealthData(character, character.Info.HealthData);
|
||||
}
|
||||
character.GiveIdCardTags(spawnWaypoints[i]);
|
||||
character.Info.StartItemsGiven = true;
|
||||
}
|
||||
|
||||
AddCharacter(character);
|
||||
#if CLIENT
|
||||
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
|
||||
#endif
|
||||
}
|
||||
|
||||
conversationTimer = Rand.Range(5.0f, 10.0f);
|
||||
}
|
||||
|
||||
public void FireCharacter(CharacterInfo characterInfo)
|
||||
{
|
||||
RemoveCharacterInfo(characterInfo);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
foreach (Pair<Order, float> order in ActiveOrders)
|
||||
@@ -88,6 +238,49 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (welcomeMessageNPC == null)
|
||||
{
|
||||
foreach (Character npc in Character.CharacterList)
|
||||
{
|
||||
if (npc.TeamID != Character.TeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; }
|
||||
if (npc.AIController?.ObjectiveManager != null && (npc.AIController.ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() || npc.AIController.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (Character player in Character.CharacterList)
|
||||
{
|
||||
if (player.TeamID != npc.TeamID && !player.IsIncapacitated && player.CurrentHull == npc.CurrentHull)
|
||||
{
|
||||
List<Character> availableSpeakers = new List<Character>() { npc, player };
|
||||
List<string> dialogFlags = new List<string>() { "OutpostNPC", "EnterOutpost" };
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && campaignMode.Map?.CurrentLocation?.Reputation != null)
|
||||
{
|
||||
float normalizedReputation = MathUtils.InverseLerp(
|
||||
campaignMode.Map.CurrentLocation.Reputation.MinReputation,
|
||||
campaignMode.Map.CurrentLocation.Reputation.MaxReputation,
|
||||
campaignMode.Map.CurrentLocation.Reputation.Value);
|
||||
if (normalizedReputation < 0.2f)
|
||||
{
|
||||
dialogFlags.Add("LowReputation");
|
||||
}
|
||||
else if (normalizedReputation > 0.8f)
|
||||
{
|
||||
dialogFlags.Add("HighReputation");
|
||||
}
|
||||
}
|
||||
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers, dialogFlags));
|
||||
welcomeMessageNPC = npc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (welcomeMessageNPC != null) { break; }
|
||||
}
|
||||
}
|
||||
else if (welcomeMessageNPC.Removed)
|
||||
{
|
||||
welcomeMessageNPC = null;
|
||||
}
|
||||
|
||||
if (pendingConversationLines.Count > 0)
|
||||
{
|
||||
conversationLineTimer -= deltaTime;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal partial class CampaignMetadata
|
||||
{
|
||||
public CampaignMode Campaign { get; }
|
||||
|
||||
private readonly Dictionary<string, object> data = new Dictionary<string, object>();
|
||||
|
||||
public CampaignMetadata(CampaignMode campaign)
|
||||
{
|
||||
Campaign = campaign;
|
||||
}
|
||||
|
||||
public CampaignMetadata(CampaignMode campaign, XElement element)
|
||||
{
|
||||
Campaign = campaign;
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
string identifier = subElement.GetAttributeString("key", string.Empty).ToLowerInvariant();
|
||||
string value = subElement.GetAttributeString("value", string.Empty);
|
||||
string valueType = subElement.GetAttributeString("type", string.Empty);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(identifier) || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType))
|
||||
{
|
||||
DebugConsole.ThrowError("Unable to load value because one or more of the required attributes are empty.\n" +
|
||||
$"key: \"{identifier}\", value: \"{value}\", type: \"{valueType}\"");
|
||||
continue;
|
||||
}
|
||||
|
||||
Type? type = Type.GetType(valueType);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Type for {identifier} not found ({valueType}).");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == typeof(float))
|
||||
{
|
||||
if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatValue))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in campaign metadata: could not parse \"{value}\" as a float.");
|
||||
continue;
|
||||
}
|
||||
data.Add(identifier, floatValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Add(identifier, Convert.ChangeType(value, type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetValue(string identifier, object value)
|
||||
{
|
||||
identifier = identifier.ToLowerInvariant();
|
||||
|
||||
DebugConsole.Log($"Set the value \"{identifier}\" to {value}");
|
||||
|
||||
if (!data.ContainsKey(identifier))
|
||||
{
|
||||
data.Add(identifier, value);
|
||||
return;
|
||||
}
|
||||
|
||||
data[identifier] = value;
|
||||
}
|
||||
|
||||
public float GetFloat(string identifier, float? defaultValue = null)
|
||||
{
|
||||
return (float)GetTypeOrDefault(identifier, typeof(float), defaultValue ?? 0f);
|
||||
}
|
||||
|
||||
public int GetInt(string identifier, int? defaultValue = null)
|
||||
{
|
||||
return (int)GetTypeOrDefault(identifier, typeof(int), defaultValue ?? 0);
|
||||
}
|
||||
|
||||
public bool GetBoolean(string identifier, bool? defaultValue = null)
|
||||
{
|
||||
return (bool)GetTypeOrDefault(identifier, typeof(bool), defaultValue ?? false);
|
||||
}
|
||||
|
||||
public string GetString(string identifier, string? defaultValue = null)
|
||||
{
|
||||
return (string)GetTypeOrDefault(identifier, typeof(string), defaultValue ?? string.Empty);
|
||||
}
|
||||
|
||||
public bool HasKey(string identifier)
|
||||
{
|
||||
identifier = identifier.ToLowerInvariant();
|
||||
return data.ContainsKey(identifier);
|
||||
}
|
||||
|
||||
private object GetTypeOrDefault(string identifier, Type type, object defaultValue)
|
||||
{
|
||||
object? value = GetValue(identifier);
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
SetValue(identifier, defaultValue);
|
||||
}
|
||||
else if (value.GetType() == type)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Attempted to get value \"{identifier}\" as a {type} but the value is {value.GetType()}.");
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public object? GetValue(string identifier)
|
||||
{
|
||||
return data.ContainsKey(identifier) ? data[identifier] : null;
|
||||
}
|
||||
|
||||
public void Save(XElement modeElement)
|
||||
{
|
||||
XElement element = new XElement("Metadata");
|
||||
|
||||
foreach (var (key, value) in data)
|
||||
{
|
||||
string valueStr = value?.ToString() ?? "";
|
||||
if (value?.GetType() == typeof(float))
|
||||
{
|
||||
valueStr = ((float)value).ToString("G", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
element.Add(new XElement("Data",
|
||||
new XAttribute("key", key),
|
||||
new XAttribute("value", valueStr),
|
||||
new XAttribute("type", value?.GetType())));
|
||||
}
|
||||
#if DEBUG || UNSTABLE
|
||||
DebugConsole.Log(element.ToString());
|
||||
#endif
|
||||
modeElement.Add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class Faction
|
||||
{
|
||||
public Reputation Reputation { get; }
|
||||
public FactionPrefab Prefab { get; }
|
||||
|
||||
public Faction(CampaignMetadata metadata, FactionPrefab prefab)
|
||||
{
|
||||
Prefab = prefab;
|
||||
Reputation = new Reputation(metadata, $"faction.{prefab.Identifier}", prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
|
||||
}
|
||||
}
|
||||
|
||||
internal class FactionPrefab : IDisposable
|
||||
{
|
||||
public static List<FactionPrefab> Prefabs { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string Description { get; }
|
||||
public string ShortDescription { get; }
|
||||
|
||||
public string Identifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How low the reputation can drop on this faction
|
||||
/// </summary>
|
||||
public int MinReputation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum reputation level you can gain on this faction
|
||||
/// </summary>
|
||||
public int MaxReputation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// What reputation does this faction start with
|
||||
/// </summary>
|
||||
public int InitialReputation { get; }
|
||||
|
||||
#if CLIENT
|
||||
public Sprite? Icon { get; private set; }
|
||||
|
||||
public Sprite? BackgroundPortrait { get; private set; }
|
||||
|
||||
public Color IconColor { get; }
|
||||
#endif
|
||||
|
||||
private FactionPrefab(XElement element)
|
||||
{
|
||||
Identifier = element.GetAttributeString("identifier", string.Empty);
|
||||
MinReputation = element.GetAttributeInt("minreputation", -100);
|
||||
MaxReputation = element.GetAttributeInt("maxreputation", 100);
|
||||
InitialReputation = element.GetAttributeInt("initialreputation", 0);
|
||||
Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}", returnNull: true) ?? "Unnamed";
|
||||
Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description", returnNull: true) ?? "";
|
||||
ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription", returnNull: true) ?? "";
|
||||
#if CLIENT
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
|
||||
if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
IconColor = subElement.GetAttributeColor("color", Color.White);
|
||||
Icon = new Sprite(subElement);
|
||||
}
|
||||
else if (subElement.Name.ToString().Equals("portrait", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
BackgroundPortrait = new Sprite(subElement);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void LoadFactions()
|
||||
{
|
||||
Prefabs?.ForEach(set => set.Dispose());
|
||||
Prefabs = new List<FactionPrefab>();
|
||||
IEnumerable<ContentFile> files = GameMain.Instance.GetFilesOfType(ContentType.Factions);
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
|
||||
XElement? rootElement = doc?.Root;
|
||||
|
||||
if (doc == null || rootElement == null) { continue; }
|
||||
|
||||
if (doc.Root.IsOverride())
|
||||
{
|
||||
Prefabs.Clear();
|
||||
DebugConsole.NewMessage($"Overriding all factions with '{file.Path}'", Color.Yellow);
|
||||
}
|
||||
|
||||
foreach (XElement element in rootElement.Elements())
|
||||
{
|
||||
bool isOverride = element.IsOverride();
|
||||
XElement sourceElement = isOverride ? element.FirstElement() : element;
|
||||
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
|
||||
string identifier = sourceElement.GetAttributeString("identifier", null);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
DebugConsole.ThrowError($"No identifier defined for the faction config '{elementName}' in file '{file.Path}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
var existingParams = Prefabs.Find(set => set.Identifier == identifier);
|
||||
if (existingParams != null)
|
||||
{
|
||||
if (isOverride)
|
||||
{
|
||||
DebugConsole.NewMessage($"Overriding faction config '{identifier}' using the file '{file.Path}'", Color.Yellow);
|
||||
Prefabs.Remove(existingParams);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Duplicate faction config: '{identifier}' defined in {elementName} of '{file.Path}'");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Prefabs.Add(new FactionPrefab(element));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
#if CLIENT
|
||||
Icon?.Remove();
|
||||
Icon = null;
|
||||
#endif
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class Reputation
|
||||
{
|
||||
public const float HostileThreshold = 0.1f;
|
||||
public const float ReputationLossPerNPCDamage = 0.1f;
|
||||
public const float ReputationLossPerStolenItemPrice = 0.01f;
|
||||
public const float MinReputationLossPerStolenItem = 0.5f;
|
||||
public const float MaxReputationLossPerStolenItem = 10.0f;
|
||||
|
||||
public string Identifier { get; }
|
||||
public int MinReputation { get; }
|
||||
public int MaxReputation { get; }
|
||||
public int InitialReputation { get; }
|
||||
public CampaignMetadata Metadata { get; }
|
||||
|
||||
private readonly string metaDataIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Reputation value normalized to the range of 0-1
|
||||
/// </summary>
|
||||
public float NormalizedValue
|
||||
{
|
||||
get { return MathUtils.InverseLerp(MinReputation, MaxReputation, Value); }
|
||||
}
|
||||
|
||||
public float Value
|
||||
{
|
||||
get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation));
|
||||
set => Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
|
||||
}
|
||||
|
||||
public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(metadata != null);
|
||||
Metadata = metadata;
|
||||
Identifier = identifier.ToLowerInvariant();
|
||||
metaDataIdentifier = $"reputation.{Identifier}";
|
||||
MinReputation = minReputation;
|
||||
MaxReputation = maxReputation;
|
||||
InitialReputation = initialReputation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -8,22 +10,62 @@ namespace Barotrauma
|
||||
{
|
||||
abstract partial class CampaignMode : GameMode
|
||||
{
|
||||
const int MaxMoney = int.MaxValue / 2; //about 1 billion
|
||||
const int InitialMoney = 2500;
|
||||
public const int MaxInitialSubmarinePrice = 6000;
|
||||
|
||||
//duration of the cinematic + credits at the end of the campaign
|
||||
protected const float EndCinematicDuration = 240.0f;
|
||||
//duration of the camera transition at the end of a round
|
||||
protected const float EndTransitionDuration = 5.0f;
|
||||
//there can be no events before this time has passed during the 1st campaign round
|
||||
const float FirstRoundEventDelay = 30.0f;
|
||||
|
||||
public enum InteractionType { None, Talk, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
|
||||
|
||||
public readonly CargoManager CargoManager;
|
||||
public UpgradeManager UpgradeManager;
|
||||
|
||||
public List<Faction> Factions;
|
||||
|
||||
public CampaignMetadata CampaignMetadata;
|
||||
|
||||
public enum TransitionType
|
||||
{
|
||||
None,
|
||||
//leaving a location level
|
||||
LeaveLocation,
|
||||
//progressing to next location level
|
||||
ProgressToNextLocation,
|
||||
//returning to previous location level
|
||||
ReturnToPreviousLocation,
|
||||
//returning to previous location (one with no level/outpost, the player is taken to the map screen and must choose their next destination)
|
||||
ReturnToPreviousEmptyLocation,
|
||||
//progressing to an empty location (one with no level/outpost, the player is taken to the map screen and must choose their next destination)
|
||||
ProgressToNextEmptyLocation,
|
||||
//end of campaign (reached end location)
|
||||
End
|
||||
}
|
||||
|
||||
public bool IsFirstRound { get; protected set; } = true;
|
||||
|
||||
public bool DisableEvents
|
||||
{
|
||||
get { return IsFirstRound && Timing.TotalTime < GameMain.GameSession.RoundStartTime + FirstRoundEventDelay; }
|
||||
}
|
||||
|
||||
public bool CheatsEnabled;
|
||||
|
||||
const int InitialMoney = 8700;
|
||||
public const int HullRepairCost = 500, ItemRepairCost = 500, ShuttleReplaceCost = 1000;
|
||||
|
||||
protected bool watchmenSpawned;
|
||||
protected Character startWatchman, endWatchman;
|
||||
protected bool wasDocked;
|
||||
|
||||
//key = dialog flag, double = Timing.TotalTime when the line was last said
|
||||
private Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
|
||||
private readonly Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
|
||||
|
||||
public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs;
|
||||
|
||||
public bool InitialSuppliesSpawned;
|
||||
public SubmarineInfo PendingSubmarineSwitch;
|
||||
|
||||
protected Map map;
|
||||
public Map Map
|
||||
@@ -43,28 +85,47 @@ namespace Barotrauma
|
||||
public int Money
|
||||
{
|
||||
get { return money; }
|
||||
set { money = Math.Max(value, 0); }
|
||||
set { money = MathHelper.Clamp(value, 0, MaxMoney); }
|
||||
}
|
||||
|
||||
public CampaignMode(GameModePreset preset, object param)
|
||||
: base(preset, param)
|
||||
public LevelData NextLevel
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
protected CampaignMode(GameModePreset preset)
|
||||
: base(preset)
|
||||
{
|
||||
Money = InitialMoney;
|
||||
CargoManager = new CargoManager(this);
|
||||
CargoManager = new CargoManager(this);
|
||||
}
|
||||
|
||||
public void GenerateMap(string seed)
|
||||
/// <summary>
|
||||
/// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level,
|
||||
/// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end
|
||||
/// </summary>
|
||||
public Location CurrentDisplayLocation
|
||||
{
|
||||
map = new Map(seed);
|
||||
get
|
||||
{
|
||||
if (Level.Loaded != null && !Level.Loaded.Generating &&
|
||||
Level.Loaded.Type == LevelData.LevelType.LocationConnection &&
|
||||
GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation)
|
||||
{
|
||||
return Level.Loaded.EndLocation;
|
||||
}
|
||||
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
|
||||
public List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
|
||||
{
|
||||
//leave subs behind if they're not docked to the leaving sub and not at the same exit
|
||||
return Submarine.Loaded.FindAll(s =>
|
||||
s != leavingSub &&
|
||||
!leavingSub.DockedTo.Contains(s) &&
|
||||
s.Info.Type == SubmarineInfo.SubmarineType.Player &&
|
||||
s.Info.Type == SubmarineType.Player &&
|
||||
(s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition));
|
||||
}
|
||||
|
||||
@@ -72,20 +133,18 @@ namespace Barotrauma
|
||||
{
|
||||
base.Start();
|
||||
dialogLastSpoken.Clear();
|
||||
watchmenSpawned = false;
|
||||
startWatchman = null;
|
||||
endWatchman = null;
|
||||
characterOutOfBoundsTimer.Clear();
|
||||
|
||||
if (PurchasedHullRepairs)
|
||||
{
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
{
|
||||
if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
|
||||
if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine))
|
||||
{
|
||||
for (int i = 0; i < wall.SectionCount; i++)
|
||||
{
|
||||
wall.AddDamage(i, -wall.Prefab.Health);
|
||||
wall.AddDamage(i, -wall.MaxHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,125 +154,539 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine == null || item.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
|
||||
if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine))
|
||||
{
|
||||
if (item.GetComponent<Items.Components.Repairable>() != null)
|
||||
{
|
||||
item.Condition = item.Prefab.Health;
|
||||
item.Condition = item.MaxCondition;
|
||||
}
|
||||
}
|
||||
}
|
||||
PurchasedItemRepairs = false;
|
||||
}
|
||||
PurchasedLostShuttles = false;
|
||||
var connectedSubs = Submarine.MainSub.GetConnectedSubs();
|
||||
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
public void InitCampaignData()
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
if (!IsRunning) { return; }
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
if (!watchmenSpawned)
|
||||
Factions = new List<Faction>();
|
||||
foreach (FactionPrefab factionPrefab in FactionPrefab.Prefabs)
|
||||
{
|
||||
if (Level.Loaded.StartOutpost != null) { startWatchman = SpawnWatchman(Level.Loaded.StartOutpost); }
|
||||
if (Level.Loaded.EndOutpost != null) { endWatchman = SpawnWatchman(Level.Loaded.EndOutpost); }
|
||||
watchmenSpawned = true;
|
||||
#if SERVER
|
||||
(this as MultiPlayerCampaign).LastUpdateID++;
|
||||
Factions.Add(new Faction(CampaignMetadata, factionPrefab));
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadNewLevel()
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (CoroutineManager.IsCoroutineRunning("LevelTransition"))
|
||||
{
|
||||
DebugConsole.ThrowError("Level transition already running.\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Level.Loaded == null || Submarine.MainSub == null)
|
||||
{
|
||||
LoadInitialLevel();
|
||||
return;
|
||||
}
|
||||
|
||||
var availableTransition = GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub);
|
||||
|
||||
if (availableTransition == TransitionType.None)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
|
||||
"(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
|
||||
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
|
||||
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
|
||||
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
|
||||
Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
if (nextLevel == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
|
||||
"(transition type: " + availableTransition + ", " +
|
||||
"current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
|
||||
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
|
||||
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
|
||||
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
|
||||
Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
#if CLIENT
|
||||
ShowCampaignUI = ForceMapUI = false;
|
||||
#endif
|
||||
DebugConsole.NewMessage("Transitioning to " + (nextLevel?.Seed ?? "null") +
|
||||
" (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
|
||||
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
|
||||
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
|
||||
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ", " +
|
||||
"transition type: " + availableTransition + ")");
|
||||
|
||||
IsFirstRound = false;
|
||||
bool mirror = map.SelectedConnection != null && map.CurrentLocation != map.SelectedConnection.Locations[0];
|
||||
CoroutineManager.StartCoroutine(DoLevelTransition(availableTransition, nextLevel, leavingSub, mirror), "LevelTransition");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the first level and start the round after loading a save file
|
||||
/// </summary>
|
||||
protected abstract void LoadInitialLevel();
|
||||
|
||||
protected abstract IEnumerable<object> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults = null);
|
||||
|
||||
/// <summary>
|
||||
/// Which type of transition between levels is currently possible (if any)
|
||||
/// </summary>
|
||||
public TransitionType GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub)
|
||||
{
|
||||
if (Level.Loaded == null || Submarine.MainSub == null)
|
||||
{
|
||||
nextLevel = null;
|
||||
leavingSub = null;
|
||||
return TransitionType.None;
|
||||
}
|
||||
|
||||
leavingSub = GetLeavingSub();
|
||||
if (leavingSub == null)
|
||||
{
|
||||
nextLevel = null;
|
||||
return TransitionType.None;
|
||||
}
|
||||
|
||||
//currently travelling from location to another
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
|
||||
{
|
||||
if (leavingSub.AtEndPosition)
|
||||
{
|
||||
if (Map.EndLocation != null && map.SelectedLocation == Map.EndLocation)
|
||||
{
|
||||
nextLevel = map.StartLocation.LevelData;
|
||||
return TransitionType.End;
|
||||
}
|
||||
if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null)
|
||||
{
|
||||
nextLevel = Level.Loaded.EndLocation.LevelData;
|
||||
return TransitionType.ProgressToNextLocation;
|
||||
}
|
||||
else if (map.SelectedConnection != null)
|
||||
{
|
||||
nextLevel = Level.Loaded.LevelData != map.SelectedConnection?.LevelData || (map.SelectedConnection.Locations[0] == Level.Loaded.EndLocation == Level.Loaded.Mirrored) ?
|
||||
map.SelectedConnection.LevelData : null;
|
||||
return TransitionType.ProgressToNextEmptyLocation;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextLevel = null;
|
||||
return TransitionType.ProgressToNextEmptyLocation;
|
||||
}
|
||||
}
|
||||
else if (leavingSub.AtStartPosition)
|
||||
{
|
||||
if (map.CurrentLocation.Type.HasOutpost && Level.Loaded.StartOutpost != null)
|
||||
{
|
||||
nextLevel = map.CurrentLocation.LevelData;
|
||||
return TransitionType.ReturnToPreviousLocation;
|
||||
}
|
||||
else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost &&
|
||||
(Level.Loaded.LevelData != map.SelectedConnection.LevelData))
|
||||
{
|
||||
nextLevel = map.SelectedConnection.LevelData;
|
||||
return TransitionType.LeaveLocation;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextLevel = map.SelectedConnection?.LevelData;
|
||||
return TransitionType.ReturnToPreviousEmptyLocation;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nextLevel = null;
|
||||
return TransitionType.None;
|
||||
}
|
||||
}
|
||||
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData;
|
||||
return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Character character in Character.CharacterList)
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which submarine is at a position where it can leave the level and enter another one (if any).
|
||||
/// </summary>
|
||||
private Submarine GetLeavingSub()
|
||||
{
|
||||
//in single player, only the sub the controlled character is inside can transition between levels
|
||||
//in multiplayer, if there's subs at both ends of the level, only the one with more players inside can transition
|
||||
//TODO: ignore players who don't have the permission to trigger a transition between levels?
|
||||
var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer));
|
||||
|
||||
//allow leaving if inside an outpost, and the submarine is either docked to it or close enough
|
||||
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers);
|
||||
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers);
|
||||
|
||||
if (Level.IsLoadedOutpost)
|
||||
{
|
||||
leavingSubAtStart ??= Submarine.MainSub;
|
||||
leavingSubAtEnd ??= Submarine.MainSub;
|
||||
}
|
||||
int playersInSubAtStart = leavingSubAtStart == null ? 0 :
|
||||
leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
|
||||
int playersInSubAtEnd = leavingSubAtEnd == null ? 0 :
|
||||
leavingPlayers.Count(c => c.Submarine == leavingSubAtEnd || leavingSubAtEnd.DockedTo.Contains(c.Submarine) || (Level.Loaded.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost));
|
||||
|
||||
if (playersInSubAtStart == 0 && playersInSubAtEnd == 0) { return null; }
|
||||
|
||||
return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
|
||||
|
||||
static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers)
|
||||
{
|
||||
if (Level.Loaded.StartOutpost == null)
|
||||
{
|
||||
#if SERVER
|
||||
if (string.IsNullOrEmpty(character.OwnerClientEndPoint)) { continue; }
|
||||
#else
|
||||
if (!CrewManager.GetCharacters().Contains(character)) { continue; }
|
||||
#endif
|
||||
if (character.Submarine == Level.Loaded.StartOutpost &&
|
||||
Vector2.DistanceSquared(character.WorldPosition, startWatchman.WorldPosition) < 500.0f * 500.0f)
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartPosition, ignoreOutposts: true);
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if there's a sub docked to the outpost, we can leave the level
|
||||
if (Level.Loaded.StartOutpost.DockedTo.Any())
|
||||
{
|
||||
CreateDialog(new List<Character> { startWatchman }, "EnterStartOutpost", 5 * 60.0f);
|
||||
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
|
||||
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
|
||||
}
|
||||
else if (character.Submarine == Level.Loaded.EndOutpost &&
|
||||
Vector2.DistanceSquared(character.WorldPosition, endWatchman.WorldPosition) < 500.0f * 500.0f)
|
||||
|
||||
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; }
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true);
|
||||
if (closestSub == null || !closestSub.AtStartPosition) { return null; }
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
}
|
||||
|
||||
static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers)
|
||||
{
|
||||
//no "end" in outpost levels
|
||||
if (Level.Loaded.Type == LevelData.LevelType.Outpost) { return null; }
|
||||
|
||||
if (Level.Loaded.EndOutpost == null)
|
||||
{
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndPosition, ignoreOutposts: true);
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if there's a sub docked to the outpost, we can leave the level
|
||||
if (Level.Loaded.EndOutpost.DockedTo.Any())
|
||||
{
|
||||
CreateDialog(new List<Character> { endWatchman }, "EnterEndOutpost", 5 * 60.0f);
|
||||
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
|
||||
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
|
||||
}
|
||||
|
||||
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; }
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true);
|
||||
if (closestSub == null || !closestSub.AtEndPosition) { return null; }
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void CreateDialog(List<Character> speakers, string conversationTag, float minInterval)
|
||||
public override void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
|
||||
{
|
||||
if (dialogLastSpoken.TryGetValue(conversationTag, out double lastTime))
|
||||
List<Item> takenItems = new List<Item>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (Timing.TotalTime - lastTime < minInterval) { return; }
|
||||
}
|
||||
|
||||
CrewManager.AddConversation(
|
||||
NPCConversation.CreateRandom(speakers, new List<string>() { conversationTag }));
|
||||
dialogLastSpoken[conversationTag] = Timing.TotalTime;
|
||||
}
|
||||
|
||||
private Character SpawnWatchman(Submarine outpost)
|
||||
{
|
||||
WayPoint watchmanSpawnpoint = WayPoint.WayPointList.Find(wp => wp.Submarine == outpost);
|
||||
if (watchmanSpawnpoint == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to spawn a watchman at the outpost. No spawnpoints found inside the outpost.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string seed = outpost == Level.Loaded.StartOutpost ? map.SelectedLocation.Name : map.CurrentLocation.Name;
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
JobPrefab watchmanJob = JobPrefab.Get("watchman");
|
||||
var variant = Rand.Range(0, watchmanJob.Variants, Rand.RandSync.Server);
|
||||
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: watchmanJob, variant: variant);
|
||||
var spawnedCharacter = Character.Create(characterInfo, watchmanSpawnpoint.WorldPosition,
|
||||
Level.Loaded.Seed + (outpost == Level.Loaded.StartOutpost ? "start" : "end"));
|
||||
InitializeWatchman(spawnedCharacter);
|
||||
var objectiveManager = (spawnedCharacter.AIController as HumanAIController)?.ObjectiveManager;
|
||||
if (objectiveManager != null)
|
||||
{
|
||||
var moveOrder = new AIObjectiveGoTo(watchmanSpawnpoint, spawnedCharacter, objectiveManager, repeat: true, getDivingGearIfNeeded: false);
|
||||
moveOrder.Completed += () =>
|
||||
if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; }
|
||||
if ((!(item.GetRootInventoryOwner()?.Submarine?.Info?.IsOutpost ?? false)) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
|
||||
{
|
||||
// Turn towards the center of the sub. Doesn't work in all possible cases, but this is the simplest solution for now.
|
||||
spawnedCharacter.AnimController.TargetDir = spawnedCharacter.Submarine.WorldPosition.X > spawnedCharacter.WorldPosition.X ? Direction.Right : Direction.Left;
|
||||
};
|
||||
objectiveManager.SetOrder(moveOrder);
|
||||
takenItems.Add(item);
|
||||
}
|
||||
}
|
||||
if (watchmanJob != null)
|
||||
map.CurrentLocation.RegisterTakenItems(takenItems);
|
||||
|
||||
map.CurrentLocation.AddToStock(CargoManager.SoldItems);
|
||||
CargoManager.ClearSoldItemsProjSpecific();
|
||||
map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems);
|
||||
if (GameMain.NetworkMember == null)
|
||||
{
|
||||
spawnedCharacter.GiveJobItems();
|
||||
CargoManager.ClearItemsInBuyCrate();
|
||||
CargoManager.ClearItemsInSellCrate();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
CargoManager.ClearItemsInBuyCrate();
|
||||
}
|
||||
else if (GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
CargoManager.ClearItemsInSellCrate();
|
||||
}
|
||||
}
|
||||
|
||||
if (Level.Loaded?.StartOutpost != null)
|
||||
{
|
||||
List<Character> killedCharacters = new List<Character>();
|
||||
foreach (Character c in Level.Loaded.StartOutpost.Info.OutpostNPCs.SelectMany(kpv => kpv.Value))
|
||||
{
|
||||
if (!c.IsDead && !c.Removed) { continue; }
|
||||
killedCharacters.Add(c);
|
||||
}
|
||||
map.CurrentLocation.RegisterKilledCharacters(killedCharacters);
|
||||
Level.Loaded.StartOutpost.Info.OutpostNPCs.Clear();
|
||||
}
|
||||
|
||||
List<Character> deadCharacters = Character.CharacterList.FindAll(c => c.IsDead);
|
||||
foreach (Character c in deadCharacters)
|
||||
{
|
||||
if (c.IsDead)
|
||||
{
|
||||
CrewManager.RemoveCharacterInfo(c.Info);
|
||||
c.DespawnNow();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (CharacterInfo ci in CrewManager.CharacterInfos)
|
||||
{
|
||||
ci?.ResetCurrentOrder();
|
||||
}
|
||||
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
{
|
||||
if (port.Door != null &
|
||||
port.Item.Submarine.Info.Type == SubmarineType.Player &&
|
||||
port.DockingTarget?.Item?.Submarine != null &&
|
||||
port.DockingTarget.Item.Submarine.Info.IsOutpost)
|
||||
{
|
||||
port.Door.IsOpen = false;
|
||||
}
|
||||
}
|
||||
return spawnedCharacter;
|
||||
}
|
||||
|
||||
protected void InitializeWatchman(Character character)
|
||||
|
||||
public void EndCampaign()
|
||||
{
|
||||
foreach (LocationConnection connection in Map.Connections)
|
||||
{
|
||||
connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f);
|
||||
connection.LevelData.Difficulty = connection.Difficulty;
|
||||
}
|
||||
foreach (Location location in Map.Locations)
|
||||
{
|
||||
location.CreateStore(force: true);
|
||||
location.ClearMissions();
|
||||
}
|
||||
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
|
||||
Map.SelectLocation(-1);
|
||||
EndCampaignProjSpecific();
|
||||
}
|
||||
|
||||
protected virtual void EndCampaignProjSpecific() { }
|
||||
|
||||
public bool TryHireCharacter(Location location, CharacterInfo characterInfo)
|
||||
{
|
||||
if (Money < characterInfo.Salary) { return false; }
|
||||
|
||||
characterInfo.IsNewHire = true;
|
||||
|
||||
location.RemoveHireableCharacter(characterInfo);
|
||||
CrewManager.AddCharacterInfo(characterInfo);
|
||||
Money -= characterInfo.Salary;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void NPCInteract(Character npc, Character interactor)
|
||||
{
|
||||
if (!npc.AllowCustomInteract) { return; }
|
||||
NPCInteractProjSpecific(npc, interactor);
|
||||
string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID);
|
||||
if (!CoroutineManager.IsCoroutineRunning(coroutineName))
|
||||
{
|
||||
CoroutineManager.StartCoroutine(DoCharacterWait(npc, interactor), coroutineName);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<object> DoCharacterWait(Character npc, Character interactor)
|
||||
{
|
||||
if (npc == null || interactor == null) { yield return CoroutineStatus.Failure; }
|
||||
|
||||
HumanAIController humanAI = npc.AIController as HumanAIController;
|
||||
if (humanAI == null) { yield return CoroutineStatus.Failure; }
|
||||
|
||||
OrderInfo? prevSpeakerOrder = null;
|
||||
if (humanAI.CurrentOrder != null)
|
||||
{
|
||||
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
|
||||
}
|
||||
var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase));
|
||||
humanAI.SetOrder(waitOrder, option: string.Empty, orderGiver: null, speak: false);
|
||||
humanAI.FaceTarget(interactor);
|
||||
|
||||
while (!npc.Removed && !interactor.Removed &&
|
||||
Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f &&
|
||||
humanAI.CurrentOrder == waitOrder &&
|
||||
humanAI.AllowCampaignInteraction() &&
|
||||
!interactor.IsIncapacitated)
|
||||
{
|
||||
yield return CoroutineStatus.Running;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
ShowCampaignUI = false;
|
||||
#endif
|
||||
|
||||
if (humanAI.CurrentOrder == waitOrder)
|
||||
{
|
||||
if (prevSpeakerOrder != null)
|
||||
{
|
||||
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
|
||||
}
|
||||
}
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
partial void NPCInteractProjSpecific(Character npc, Character interactor);
|
||||
|
||||
public void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
|
||||
{
|
||||
character.CampaignInteractionType = interactionType;
|
||||
if (interactionType == InteractionType.None)
|
||||
{
|
||||
character.SetCustomInteract(null, null);
|
||||
return;
|
||||
}
|
||||
character.CharacterHealth.UseHealthWindow = false;
|
||||
character.CharacterHealth.Unkillable = true;
|
||||
character.CanInventoryBeAccessed = false;
|
||||
character.CanBeDragged = false;
|
||||
character.TeamID = Character.TeamType.FriendlyNPC;
|
||||
//character.CanInventoryBeAccessed = false;
|
||||
character.SetCustomInteract(
|
||||
WatchmanInteract,
|
||||
#if CLIENT
|
||||
hudText: TextManager.GetWithVariable("TalkHint", "[key]", GameMain.Config.KeyBindText(InputType.Select)));
|
||||
NPCInteract,
|
||||
#if CLIENT
|
||||
hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameMain.Config.KeyBindText(InputType.Use)));
|
||||
#else
|
||||
hudText: TextManager.Get("TalkHint"));
|
||||
hudText: TextManager.Get("CampaignInteraction." + interactionType));
|
||||
#endif
|
||||
}
|
||||
|
||||
protected abstract void WatchmanInteract(Character watchman, Character interactor);
|
||||
|
||||
private readonly Dictionary<Character, float> characterOutOfBoundsTimer = new Dictionary<Character, float>();
|
||||
|
||||
protected void KeepCharactersCloseToOutpost(float deltaTime)
|
||||
{
|
||||
const float MaxDist = 3000.0f;
|
||||
const float MinDist = 2500.0f;
|
||||
|
||||
if (!Level.IsLoadedOutpost) { return; }
|
||||
|
||||
Rectangle worldBorders = Submarine.MainSub.GetDockedBorders();
|
||||
worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint();
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if ((c != Character.Controlled && !c.IsRemotePlayer) ||
|
||||
c.Removed || c.IsDead || c.IsIncapacitated || c.Submarine != null)
|
||||
{
|
||||
if (characterOutOfBoundsTimer.ContainsKey(c))
|
||||
{
|
||||
c.OverrideMovement = null;
|
||||
characterOutOfBoundsTimer.Remove(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c.WorldPosition.Y < worldBorders.Y - worldBorders.Height - MaxDist)
|
||||
{
|
||||
if (!characterOutOfBoundsTimer.ContainsKey(c))
|
||||
{
|
||||
characterOutOfBoundsTimer.Add(c, 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
characterOutOfBoundsTimer[c] += deltaTime;
|
||||
}
|
||||
}
|
||||
else if (c.WorldPosition.Y > worldBorders.Y - worldBorders.Height - MinDist)
|
||||
{
|
||||
if (characterOutOfBoundsTimer.ContainsKey(c))
|
||||
{
|
||||
c.OverrideMovement = null;
|
||||
characterOutOfBoundsTimer.Remove(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<Character, float> character in characterOutOfBoundsTimer)
|
||||
{
|
||||
if (character.Value <= 0.0f)
|
||||
{
|
||||
if (IsSinglePlayer)
|
||||
{
|
||||
#if CLIENT
|
||||
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(
|
||||
TextManager.Get("RadioAnnouncerName"),
|
||||
TextManager.Get("TooFarFromOutpostWarning"),
|
||||
Networking.ChatMessageType.Default,
|
||||
sender: null);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
#if SERVER
|
||||
foreach (Networking.Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
|
||||
GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create(
|
||||
TextManager.Get("RadioAnnouncerName"),
|
||||
TextManager.Get("TooFarFromOutpostWarning"), Networking.ChatMessageType.Default, null), c);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
character.Key.OverrideMovement = Vector2.UnitY * 10.0f;
|
||||
#if CLIENT
|
||||
Character.DisableControls = true;
|
||||
#endif
|
||||
//if the character doesn't get back up in 10 seconds (something blocking the way?), teleport it closer
|
||||
if (character.Value > 10.0f)
|
||||
{
|
||||
Vector2 teleportPos = character.Key.WorldPosition;
|
||||
teleportPos += Vector2.Normalize(Submarine.MainSub.WorldPosition - character.Key.WorldPosition) * 100.0f;
|
||||
character.Key.AnimController.SetPosition(ConvertUnits.ToSimUnits(teleportPos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
|
||||
{
|
||||
if (npc == null || attacker == null || npc.IsDead || npc.TurnedHostileByEvent) { return; }
|
||||
if (npc.TeamID != Character.TeamType.FriendlyNPC) { return; }
|
||||
if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; }
|
||||
Location location = Map?.CurrentLocation;
|
||||
if (location != null)
|
||||
{
|
||||
location.Reputation.Value -= attackResult.Damage * Reputation.ReputationLossPerNPCDamage;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void Save(XElement element);
|
||||
|
||||
public void LogState()
|
||||
|
||||
+19
-5
@@ -25,6 +25,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private XElement itemData;
|
||||
private XElement healthData;
|
||||
|
||||
partial void InitProjSpecific(Client client);
|
||||
public CharacterCampaignData(Client client)
|
||||
@@ -32,6 +33,8 @@ namespace Barotrauma
|
||||
Name = client.Name;
|
||||
InitProjSpecific(client);
|
||||
|
||||
healthData = new XElement("health");
|
||||
client.Character.CharacterHealth.Save(healthData);
|
||||
if (client.Character.Inventory != null)
|
||||
{
|
||||
itemData = new XElement("inventory");
|
||||
@@ -61,10 +64,24 @@ namespace Barotrauma
|
||||
case "inventory":
|
||||
itemData = subElement;
|
||||
break;
|
||||
case "health":
|
||||
healthData = subElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh(Character character)
|
||||
{
|
||||
healthData = new XElement("health");
|
||||
character.CharacterHealth.Save(healthData);
|
||||
if (character.Inventory != null)
|
||||
{
|
||||
itemData = new XElement("inventory");
|
||||
character.SaveInventory(character.Inventory, itemData);
|
||||
}
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
{
|
||||
XElement element = new XElement("CharacterCampaignData",
|
||||
@@ -73,11 +90,8 @@ namespace Barotrauma
|
||||
new XAttribute("steamid", SteamID));
|
||||
|
||||
CharacterInfo?.Save(element);
|
||||
|
||||
if (itemData != null)
|
||||
{
|
||||
element.Add(itemData);
|
||||
}
|
||||
if (itemData != null) { element.Add(itemData); }
|
||||
if (healthData != null) { element.Add(healthData); }
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,10 @@ namespace Barotrauma
|
||||
public static List<GameModePreset> PresetList = new List<GameModePreset>();
|
||||
|
||||
protected DateTime startTime;
|
||||
|
||||
protected bool isRunning;
|
||||
|
||||
|
||||
protected GameModePreset preset;
|
||||
|
||||
private string endMessage;
|
||||
|
||||
protected CrewManager CrewManager
|
||||
|
||||
public CrewManager CrewManager
|
||||
{
|
||||
get { return GameMain.GameSession?.CrewManager; }
|
||||
}
|
||||
@@ -25,11 +21,6 @@ namespace Barotrauma
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public bool IsRunning
|
||||
{
|
||||
get { return isRunning; }
|
||||
}
|
||||
|
||||
public bool IsSinglePlayer
|
||||
{
|
||||
get { return preset.IsSinglePlayer; }
|
||||
@@ -40,9 +31,9 @@ namespace Barotrauma
|
||||
get { return preset.Name; }
|
||||
}
|
||||
|
||||
public string EndMessage
|
||||
public virtual bool Paused
|
||||
{
|
||||
get { return endMessage; }
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public GameModePreset Preset
|
||||
@@ -50,7 +41,7 @@ namespace Barotrauma
|
||||
get { return preset; }
|
||||
}
|
||||
|
||||
public GameMode(GameModePreset preset, object param)
|
||||
public GameMode(GameModePreset preset)
|
||||
{
|
||||
this.preset = preset;
|
||||
}
|
||||
@@ -58,10 +49,6 @@ namespace Barotrauma
|
||||
public virtual void Start()
|
||||
{
|
||||
startTime = DateTime.Now;
|
||||
|
||||
endMessage = "The round has ended!";
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
public virtual void ShowStartMessage() { }
|
||||
@@ -69,8 +56,6 @@ namespace Barotrauma
|
||||
public virtual void AddToGUIUpdateList()
|
||||
{
|
||||
#if CLIENT
|
||||
if (!isRunning) return;
|
||||
|
||||
GameMain.GameSession?.CrewManager.AddToGUIUpdateList();
|
||||
#endif
|
||||
}
|
||||
@@ -80,15 +65,10 @@ namespace Barotrauma
|
||||
CrewManager?.Update(deltaTime);
|
||||
}
|
||||
|
||||
public virtual void End(string endMessage = "")
|
||||
public virtual void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
|
||||
{
|
||||
isRunning = false;
|
||||
|
||||
if (endMessage != "" || this.endMessage == null) this.endMessage = endMessage;
|
||||
|
||||
GameMain.GameSession.EndRound(endMessage);
|
||||
}
|
||||
|
||||
|
||||
public virtual void Remove() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,15 @@ namespace Barotrauma
|
||||
{
|
||||
public static List<GameModePreset> List = new List<GameModePreset>();
|
||||
|
||||
public readonly ConstructorInfo Constructor;
|
||||
public static GameModePreset SinglePlayerCampaign;
|
||||
public static GameModePreset MultiPlayerCampaign;
|
||||
public static GameModePreset Tutorial;
|
||||
public static GameModePreset Mission;
|
||||
public static GameModePreset TestMode;
|
||||
public static GameModePreset Sandbox;
|
||||
public static GameModePreset DevSandbox;
|
||||
|
||||
public readonly Type GameModeType;
|
||||
|
||||
public readonly string Name;
|
||||
public readonly string Description;
|
||||
@@ -26,7 +34,7 @@ namespace Barotrauma
|
||||
Description = TextManager.Get("GameModeDescription." + identifier, returnNull: true) ?? "";
|
||||
Identifier = identifier;
|
||||
|
||||
Constructor = type.GetConstructor(new Type[] { typeof(GameModePreset), typeof(object) });
|
||||
GameModeType = type;
|
||||
|
||||
IsSinglePlayer = isSinglePlayer;
|
||||
Votable = votable;
|
||||
@@ -34,23 +42,17 @@ namespace Barotrauma
|
||||
List.Add(this);
|
||||
}
|
||||
|
||||
public GameMode Instantiate(object param)
|
||||
{
|
||||
object[] lobject = new object[] { this, param };
|
||||
return (GameMode)Constructor.Invoke(lobject);
|
||||
}
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
#if CLIENT
|
||||
new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true);
|
||||
new GameModePreset("subtest", typeof(SubTestMode), true);
|
||||
new GameModePreset("tutorial", typeof(TutorialMode), true);
|
||||
new GameModePreset("devsandbox", typeof(GameMode), true);
|
||||
Tutorial = new GameModePreset("tutorial", typeof(TutorialMode), true);
|
||||
DevSandbox = new GameModePreset("devsandbox", typeof(GameMode), true);
|
||||
SinglePlayerCampaign = new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true);
|
||||
TestMode = new GameModePreset("testmode", typeof(TestGameMode), true);
|
||||
#endif
|
||||
new GameModePreset("sandbox", typeof(GameMode), false);
|
||||
new GameModePreset("mission", typeof(MissionMode), false);
|
||||
new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false);
|
||||
Sandbox = new GameModePreset("sandbox", typeof(GameMode), false);
|
||||
Mission = new GameModePreset("mission", typeof(MissionMode), false);
|
||||
MultiPlayerCampaign = new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
partial class MissionMode : GameMode
|
||||
{
|
||||
private Mission mission;
|
||||
private readonly Mission mission;
|
||||
|
||||
public override Mission Mission
|
||||
{
|
||||
@@ -12,26 +12,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
public MissionMode(GameModePreset preset, object param)
|
||||
: base(preset, param)
|
||||
public MissionMode(GameModePreset preset, MissionPrefab missionPrefab)
|
||||
: base(preset)
|
||||
{
|
||||
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
|
||||
if (param is MissionType missionType)
|
||||
{
|
||||
mission = Mission.LoadRandom(locations, GameMain.NetLobbyScreen.LevelSeed, false, missionType);
|
||||
}
|
||||
else if (param is MissionPrefab missionPrefab)
|
||||
{
|
||||
mission = missionPrefab.Instantiate(locations);
|
||||
}
|
||||
else if (param is Mission)
|
||||
{
|
||||
mission = (Mission)param;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new System.ArgumentException("Unrecognized MissionMode parameter \"" + param + "\"");
|
||||
}
|
||||
mission = missionPrefab.Instantiate(locations);
|
||||
}
|
||||
|
||||
public MissionMode(GameModePreset preset, MissionType missionType, string seed)
|
||||
: base(preset)
|
||||
{
|
||||
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
|
||||
mission = Mission.LoadRandom(locations, seed, false, missionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+84
-124
@@ -1,10 +1,8 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.IO;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -16,7 +14,7 @@ namespace Barotrauma
|
||||
get
|
||||
{
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && lastUpdateID < 1) lastUpdateID++;
|
||||
if (GameMain.Server != null && lastUpdateID < 1) { lastUpdateID++; }
|
||||
#endif
|
||||
return lastUpdateID;
|
||||
}
|
||||
@@ -29,141 +27,59 @@ namespace Barotrauma
|
||||
get
|
||||
{
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && lastSaveID < 1) lastSaveID++;
|
||||
if (GameMain.Server != null && lastSaveID < 1) { lastSaveID++; }
|
||||
#endif
|
||||
return lastSaveID;
|
||||
}
|
||||
set { lastSaveID = value; }
|
||||
set
|
||||
{
|
||||
#if SERVER
|
||||
//trigger a campaign update to notify the clients of the changed save ID
|
||||
lastUpdateID++;
|
||||
#endif
|
||||
lastSaveID = value;
|
||||
}
|
||||
}
|
||||
|
||||
public UInt16 PendingSaveID
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private static byte currentCampaignID;
|
||||
|
||||
public byte CampaignID
|
||||
{
|
||||
get; private set;
|
||||
get; set;
|
||||
}
|
||||
|
||||
public MultiPlayerCampaign(GameModePreset preset, object param) :
|
||||
base(preset, param)
|
||||
private MultiPlayerCampaign() : base(GameModePreset.MultiPlayerCampaign)
|
||||
{
|
||||
currentCampaignID++;
|
||||
CampaignID = currentCampaignID;
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
base.Start();
|
||||
if (GameMain.NetworkMember.IsServer) lastUpdateID++;
|
||||
}
|
||||
|
||||
public override void End(string endMessage = "")
|
||||
{
|
||||
isRunning = false;
|
||||
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
bool success =
|
||||
GameMain.Client.ConnectedClients.Any(c => c.Character != null && !c.Character.IsDead);
|
||||
|
||||
GameMain.GameSession.EndRound("");
|
||||
GameMain.GameSession.CrewManager.EndRound();
|
||||
|
||||
if (success)
|
||||
{
|
||||
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SERVER
|
||||
lastUpdateID++;
|
||||
|
||||
bool success =
|
||||
GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
||||
|
||||
success = success || (GameMain.Server.Character != null && !GameMain.Server.Character.IsDead);
|
||||
|
||||
/*if (success)
|
||||
{
|
||||
if (subsToLeaveBehind == null || leavingSub == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Leaving submarine not selected -> selecting the closest one");
|
||||
|
||||
leavingSub = GetLeavingSub();
|
||||
|
||||
subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub);
|
||||
}
|
||||
}*/
|
||||
|
||||
GameMain.GameSession.EndRound("");
|
||||
|
||||
//client character has spawned this round -> remove old data (and replace with an up-to-date one if the client still has an alive character)
|
||||
characterData.RemoveAll(cd => cd.HasSpawned);
|
||||
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (c.Character?.Info != null && !c.Character.IsDead)
|
||||
{
|
||||
c.Character.ResetCurrentOrder();
|
||||
c.CharacterInfo = c.Character.Info;
|
||||
characterData.Add(new CharacterCampaignData(c));
|
||||
}
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
bool atEndPosition = Submarine.MainSub.AtEndPosition;
|
||||
|
||||
/*if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
|
||||
{
|
||||
Submarine.MainSub = leavingSub;
|
||||
|
||||
GameMain.GameSession.Submarine = leavingSub;
|
||||
|
||||
foreach (Submarine sub in subsToLeaveBehind)
|
||||
{
|
||||
MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine);
|
||||
LinkedSubmarine.CreateDummy(leavingSub, sub);
|
||||
}
|
||||
}*/
|
||||
|
||||
if (atEndPosition)
|
||||
{
|
||||
map.MoveToNextLocation();
|
||||
|
||||
//select a random location to make sure we've got some destination
|
||||
//to head towards even if the host/clients don't select anything
|
||||
map.SelectRandomLocation(true);
|
||||
}
|
||||
map.ProgressWorld();
|
||||
|
||||
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
|
||||
|
||||
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
|
||||
}
|
||||
#endif
|
||||
CampaignMetadata = new CampaignMetadata(this);
|
||||
UpgradeManager = new UpgradeManager(this);
|
||||
InitCampaignData();
|
||||
}
|
||||
|
||||
partial void SetDelegates();
|
||||
|
||||
public static MultiPlayerCampaign LoadNew(XElement element)
|
||||
public static MultiPlayerCampaign StartNew(string mapSeed)
|
||||
{
|
||||
MultiPlayerCampaign campaign = new MultiPlayerCampaign(GameModePreset.List.Find(gm => gm.Identifier == "multiplayercampaign"), null);
|
||||
campaign.Load(element);
|
||||
campaign.SetDelegates();
|
||||
|
||||
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
|
||||
//only the server generates the map, the clients load it from a save file
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
campaign.map = new Map(campaign, mapSeed);
|
||||
}
|
||||
campaign.InitProjSpecific();
|
||||
return campaign;
|
||||
}
|
||||
|
||||
public static MultiPlayerCampaign LoadNew(XElement element)
|
||||
{
|
||||
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
|
||||
campaign.Load(element);
|
||||
campaign.InitProjSpecific();
|
||||
campaign.IsFirstRound = false;
|
||||
return campaign;
|
||||
}
|
||||
|
||||
partial void InitProjSpecific();
|
||||
|
||||
public static string GetCharacterDataSavePath(string savePath)
|
||||
{
|
||||
return Path.Combine(SaveUtil.MultiplayerSaveFolder, Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml");
|
||||
@@ -174,10 +90,12 @@ namespace Barotrauma
|
||||
return GetCharacterDataSavePath(GameMain.GameSession.SavePath);
|
||||
}
|
||||
|
||||
public void Load(XElement element)
|
||||
/// <summary>
|
||||
/// Loads the campaign from an XML element. Creates the map if it hasn't been created yet, otherwise updates the state of the map.
|
||||
/// </summary>
|
||||
private void Load(XElement element)
|
||||
{
|
||||
Money = element.GetAttributeInt("money", 0);
|
||||
InitialSuppliesSpawned = element.GetAttributeBool("initialsuppliesspawned", false);
|
||||
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
|
||||
if (CheatsEnabled)
|
||||
{
|
||||
@@ -195,6 +113,12 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
List<SubmarineInfo> availableSubs = new List<SubmarineInfo>();
|
||||
List<SubmarineInfo> sourceList = new List<SubmarineInfo>();
|
||||
sourceList.AddRange(SubmarineInfo.SavedSubmarines);
|
||||
#endif
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -203,19 +127,55 @@ namespace Barotrauma
|
||||
if (map == null)
|
||||
{
|
||||
//map not created yet, loading this campaign for the first time
|
||||
map = Map.LoadNew(subElement);
|
||||
map = Map.Load(this, subElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
//map already created, update it
|
||||
//if we're not downloading the initial save file (LastSaveID > 0),
|
||||
//show notifications about location type changes
|
||||
map.Load(subElement, LastSaveID > 0);
|
||||
map.LoadState(subElement, LastSaveID > 0);
|
||||
}
|
||||
break;
|
||||
case "metadata":
|
||||
CampaignMetadata = new CampaignMetadata(this, subElement);
|
||||
break;
|
||||
case "pendingupgrades":
|
||||
UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: false);
|
||||
break;
|
||||
case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer:
|
||||
CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false);
|
||||
CrewManager.AddCharacterElements(subElement);
|
||||
break;
|
||||
case "cargo":
|
||||
CargoManager?.LoadPurchasedItems(subElement);
|
||||
break;
|
||||
#if SERVER
|
||||
case "availablesubs":
|
||||
foreach (XElement availableSub in subElement.Elements())
|
||||
{
|
||||
string subName = availableSub.GetAttributeString("name", "");
|
||||
SubmarineInfo matchingSub = sourceList.Find(s => s.Name == subName);
|
||||
if (matchingSub != null) { availableSubs.Add(matchingSub); }
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
CampaignMetadata ??= new CampaignMetadata(this);
|
||||
UpgradeManager ??= new UpgradeManager(this);
|
||||
|
||||
InitCampaignData();
|
||||
#if SERVER
|
||||
// Fallback if using a save with no available subs assigned, use vanilla submarines
|
||||
if (availableSubs.Count == 0)
|
||||
{
|
||||
GameMain.NetLobbyScreen.CampaignSubmarines.AddRange(sourceList.FindAll(s => s.IsCampaignCompatible && s.IsVanillaSubmarine()));
|
||||
}
|
||||
|
||||
GameMain.NetLobbyScreen.CampaignSubmarines = availableSubs;
|
||||
|
||||
characterData.Clear();
|
||||
string characterDataPath = GetCharacterDataSavePath();
|
||||
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
|
||||
|
||||
@@ -26,7 +26,10 @@ namespace Barotrauma
|
||||
|
||||
public Character.TeamType? WinningTeam;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public Level Level { get; private set; }
|
||||
public LevelData LevelData { get; private set; }
|
||||
|
||||
public Map Map
|
||||
{
|
||||
@@ -36,17 +39,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public CampaignMode Campaign
|
||||
{
|
||||
get
|
||||
{
|
||||
return GameMode as CampaignMode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Location StartLocation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Map != null) return Map.CurrentLocation;
|
||||
|
||||
if (dummyLocations == null)
|
||||
{
|
||||
CreateDummyLocations();
|
||||
}
|
||||
|
||||
if (Map != null) { return Map.CurrentLocation; }
|
||||
if (dummyLocations == null) { CreateDummyLocations(); }
|
||||
return dummyLocations[0];
|
||||
}
|
||||
}
|
||||
@@ -55,18 +62,15 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Map != null) return Map.SelectedLocation;
|
||||
|
||||
if (dummyLocations == null)
|
||||
{
|
||||
CreateDummyLocations();
|
||||
}
|
||||
|
||||
if (Map != null) { return Map.SelectedLocation; }
|
||||
if (dummyLocations == null) { CreateDummyLocations(); }
|
||||
return dummyLocations[1];
|
||||
}
|
||||
}
|
||||
|
||||
public SubmarineInfo SubmarineInfo { get; set; }
|
||||
|
||||
public List<SubmarineInfo> OwnedSubmarines = new List<SubmarineInfo>();
|
||||
|
||||
public Submarine Submarine { get; set; }
|
||||
|
||||
@@ -74,41 +78,55 @@ namespace Barotrauma
|
||||
|
||||
partial void InitProjSpecific();
|
||||
|
||||
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None)
|
||||
: this(submarineInfo, savePath)
|
||||
{
|
||||
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
|
||||
GameMode = gameModePreset.Instantiate(missionType);
|
||||
}
|
||||
|
||||
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab)
|
||||
: this(submarineInfo, savePath)
|
||||
{
|
||||
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
|
||||
GameMode = gameModePreset.Instantiate(missionPrefab);
|
||||
|
||||
#if CLIENT
|
||||
if (GameMode is SubTestMode) { EventManager = null; }
|
||||
#endif
|
||||
}
|
||||
|
||||
private GameSession(SubmarineInfo submarineInfo, string savePath)
|
||||
private GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines = null)
|
||||
{
|
||||
InitProjSpecific();
|
||||
SubmarineInfo = submarineInfo;
|
||||
/*Submarine = new Submarine(submarineInfo);
|
||||
Submarine.MainSub = Submarine;*/
|
||||
|
||||
#if CLIENT
|
||||
if (ownedSubmarines == null && GameMode is MultiPlayerCampaign && GameMain.NetLobbyScreen.ServerOwnedSubmarines != null)
|
||||
{
|
||||
ownedSubmarines = GameMain.NetLobbyScreen.ServerOwnedSubmarines;
|
||||
}
|
||||
#endif
|
||||
|
||||
OwnedSubmarines = ownedSubmarines ?? new List<SubmarineInfo>();
|
||||
if (!OwnedSubmarines.Any(s => s.Name == submarineInfo.Name))
|
||||
{
|
||||
OwnedSubmarines.Add(submarineInfo);
|
||||
}
|
||||
GameMain.GameSession = this;
|
||||
EventManager = new EventManager();
|
||||
this.SavePath = savePath;
|
||||
}
|
||||
|
||||
|
||||
public GameSession(SubmarineInfo selectedSubInfo, string saveFile, XDocument doc)
|
||||
: this(selectedSubInfo, saveFile)
|
||||
/// <summary>
|
||||
/// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved).
|
||||
/// </summary>
|
||||
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, string seed = null, MissionType missionType = MissionType.None)
|
||||
: this(submarineInfo)
|
||||
{
|
||||
Submarine.MainSub = Submarine;
|
||||
this.SavePath = savePath;
|
||||
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
|
||||
GameMode = InstantiateGameMode(gameModePreset, seed, missionType: missionType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a new GameSession with a specific pre-selected mission.
|
||||
/// </summary>
|
||||
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, MissionPrefab missionPrefab = null)
|
||||
: this(submarineInfo)
|
||||
{
|
||||
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
|
||||
GameMode = InstantiateGameMode(gameModePreset, seed, missionPrefab: missionPrefab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a game session from the specified XML document. The session will be saved to the specified path.
|
||||
/// </summary>
|
||||
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile)
|
||||
: this(submarineInfo, ownedSubmarines)
|
||||
{
|
||||
this.SavePath = saveFile;
|
||||
GameMain.GameSession = this;
|
||||
//selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name);
|
||||
|
||||
@@ -120,17 +138,62 @@ namespace Barotrauma
|
||||
case "gamemode": //legacy support
|
||||
case "singleplayercampaign":
|
||||
CrewManager = new CrewManager(true);
|
||||
GameMode = SinglePlayerCampaign.Load(subElement);
|
||||
var campaign = SinglePlayerCampaign.Load(subElement);
|
||||
campaign.LoadNewLevel();
|
||||
GameMode = campaign;
|
||||
break;
|
||||
#endif
|
||||
case "multiplayercampaign":
|
||||
CrewManager = new CrewManager(false);
|
||||
GameMode = MultiPlayerCampaign.LoadNew(subElement);
|
||||
var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
|
||||
GameMode = mpCampaign;
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
//save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance
|
||||
SaveUtil.SaveGame(saveFile);
|
||||
mpCampaign.LoadNewLevel();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None)
|
||||
{
|
||||
if (gameModePreset.GameModeType == typeof(MissionMode))
|
||||
{
|
||||
return missionPrefab != null ?
|
||||
new MissionMode(gameModePreset, missionPrefab) :
|
||||
new MissionMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
|
||||
}
|
||||
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
|
||||
{
|
||||
return MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8));
|
||||
}
|
||||
#if CLIENT
|
||||
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
|
||||
{
|
||||
return SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8));
|
||||
}
|
||||
else if (gameModePreset.GameModeType == typeof(TutorialMode))
|
||||
{
|
||||
return new TutorialMode(gameModePreset);
|
||||
}
|
||||
else if (gameModePreset.GameModeType == typeof(TestGameMode))
|
||||
{
|
||||
return new TestGameMode(gameModePreset);
|
||||
}
|
||||
#endif
|
||||
else if (gameModePreset.GameModeType == typeof(GameMode))
|
||||
{
|
||||
return new GameMode(gameModePreset);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Could not find a game mode of the type \"{gameModePreset.GameModeType}\"");
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateDummyLocations()
|
||||
{
|
||||
dummyLocations = new Location[2];
|
||||
@@ -148,24 +211,146 @@ namespace Barotrauma
|
||||
MTRandom rand = new MTRandom(ToolBox.StringToInt(seed));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand);
|
||||
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadPrevious()
|
||||
public void LoadPreviousSave()
|
||||
{
|
||||
Submarine.Unload();
|
||||
SaveUtil.LoadGame(SavePath);
|
||||
}
|
||||
|
||||
public void StartRound(string levelSeed, float? difficulty = null)
|
||||
/// <summary>
|
||||
/// Switch to another submarine. The sub is loaded when the next round starts.
|
||||
/// </summary>
|
||||
public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost)
|
||||
{
|
||||
Level randomLevel = Level.CreateRandom(levelSeed, difficulty);
|
||||
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
|
||||
{
|
||||
OwnedSubmarines.Add(newSubmarine);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fetch owned submarine data as the newSubmarine is just the base submarine
|
||||
for (int i = 0; i < OwnedSubmarines.Count; i++)
|
||||
{
|
||||
if (OwnedSubmarines[i].Name == newSubmarine.Name)
|
||||
{
|
||||
newSubmarine = OwnedSubmarines[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StartRound(randomLevel);
|
||||
Campaign.Money -= cost;
|
||||
|
||||
((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine;
|
||||
}
|
||||
|
||||
public void StartRound(Level level, bool mirrorLevel = false)
|
||||
public void PurchaseSubmarine(SubmarineInfo newSubmarine)
|
||||
{
|
||||
if (Campaign == null) return;
|
||||
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
|
||||
{
|
||||
Campaign.Money -= newSubmarine.Price;
|
||||
OwnedSubmarines.Add(newSubmarine);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSubmarineOwned(SubmarineInfo query)
|
||||
{
|
||||
return
|
||||
Submarine.MainSub.Info.Name == query.Name ||
|
||||
(OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name));
|
||||
}
|
||||
|
||||
public void StartRound(string levelSeed, float? difficulty = null)
|
||||
{
|
||||
StartRound(LevelData.CreateRandom(levelSeed, difficulty));
|
||||
}
|
||||
|
||||
public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
|
||||
{
|
||||
if (SubmarineInfo == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
|
||||
return;
|
||||
}
|
||||
if (SubmarineInfo.IsFileCorrupted)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
|
||||
return;
|
||||
}
|
||||
|
||||
LevelData = levelData;
|
||||
|
||||
if (GameMode is CampaignMode campaignMode && GameMode.Mission != null &&
|
||||
LevelData != null && LevelData.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
campaignMode.Map.CurrentLocation.SelectedMission = null;
|
||||
}
|
||||
|
||||
Submarine.Unload();
|
||||
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
|
||||
foreach (Submarine sub in Submarine.GetConnectedSubs())
|
||||
{
|
||||
sub.TeamID = Character.TeamType.Team1;
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != sub) { continue; }
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
{
|
||||
wifiComponent.TeamID = sub.TeamID;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null)
|
||||
{
|
||||
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
|
||||
}
|
||||
|
||||
Level level = null;
|
||||
if (levelData != null)
|
||||
{
|
||||
level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost);
|
||||
}
|
||||
|
||||
InitializeLevel(level);
|
||||
|
||||
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name);
|
||||
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(levelData?.Seed ?? "[NO_LEVEL]"));
|
||||
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
|
||||
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
|
||||
|
||||
#if CLIENT
|
||||
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
|
||||
|
||||
var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
|
||||
if (existingRoundSummary?.ContinueButton != null)
|
||||
{
|
||||
existingRoundSummary.ContinueButton.Visible = true;
|
||||
}
|
||||
|
||||
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Mission, StartLocation, EndLocation);
|
||||
|
||||
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
|
||||
{
|
||||
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
|
||||
if (EndLocation != null)
|
||||
{
|
||||
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
private void InitializeLevel(Level level)
|
||||
{
|
||||
//make sure no status effects have been carried on from the next round
|
||||
//(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully)
|
||||
@@ -175,83 +360,10 @@ namespace Barotrauma
|
||||
GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null;
|
||||
if (GameMain.Client == null) GameMain.LightManager.LosMode = GameMain.Config.LosMode;
|
||||
#endif
|
||||
this.Level = level;
|
||||
LevelData = level?.LevelData;
|
||||
Level = level;
|
||||
|
||||
if (SubmarineInfo == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SubmarineInfo.IsFileCorrupted)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
|
||||
return;
|
||||
}
|
||||
|
||||
Submarine.Unload();
|
||||
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
|
||||
Submarine.MainSub = Submarine;
|
||||
if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null)
|
||||
{
|
||||
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
|
||||
}
|
||||
|
||||
if (level != null)
|
||||
{
|
||||
level.Generate(mirrorLevel);
|
||||
if (level.StartOutpost != null)
|
||||
{
|
||||
//start by placing the sub below the outpost
|
||||
Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders();
|
||||
Rectangle subBorders = Submarine.GetDockedBorders();
|
||||
|
||||
Vector2 startOutpostSize = Vector2.Zero;
|
||||
if (Level.Loaded.StartOutpost != null)
|
||||
{
|
||||
startOutpostSize = Level.Loaded.StartOutpost.Borders.Size.ToVector2();
|
||||
}
|
||||
Submarine.SetPosition(
|
||||
Level.Loaded.StartOutpost.WorldPosition -
|
||||
new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
|
||||
|
||||
//find the port that's the nearest to the outpost and dock if one is found
|
||||
float closestDistance = 0.0f;
|
||||
DockingPort myPort = null, outPostPort = null;
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
{
|
||||
if (port.IsHorizontal || port.Docked) { continue; }
|
||||
if (port.Item.Submarine == level.StartOutpost)
|
||||
{
|
||||
outPostPort = port;
|
||||
continue;
|
||||
}
|
||||
if (port.Item.Submarine != Submarine) { continue; }
|
||||
|
||||
//the submarine port has to be at the top of the sub
|
||||
if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; }
|
||||
|
||||
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
|
||||
if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
|
||||
{
|
||||
myPort = port;
|
||||
closestDistance = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (myPort != null && outPostPort != null)
|
||||
{
|
||||
Vector2 portDiff = myPort.Item.WorldPosition - Submarine.WorldPosition;
|
||||
Submarine.SetPosition((outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance);
|
||||
myPort.Dock(outPostPort);
|
||||
myPort.Lock(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition));
|
||||
}
|
||||
}
|
||||
PlaceSubAtStart(Level);
|
||||
|
||||
foreach (var sub in Submarine.Loaded)
|
||||
{
|
||||
@@ -267,9 +379,9 @@ namespace Barotrauma
|
||||
if (GameMode != null) { GameMode.Start(); }
|
||||
if (GameMode.Mission != null)
|
||||
{
|
||||
int prevEntityCount = Entity.GetEntityList().Count;
|
||||
int prevEntityCount = Entity.GetEntities().Count();
|
||||
Mission.Start(Level.Loaded);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntityList().Count != prevEntityCount)
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
"Entity count has changed after starting a mission as a client. " +
|
||||
@@ -278,7 +390,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
EventManager?.StartRound(level);
|
||||
EventManager?.StartRound(Level.Loaded);
|
||||
SteamAchievementManager.OnStartRound();
|
||||
|
||||
if (GameMode != null)
|
||||
@@ -289,37 +401,109 @@ namespace Barotrauma
|
||||
{
|
||||
//only place items and corpses here in single player
|
||||
//the server does this after loading the respawn shuttle
|
||||
Level?.SpawnNPCs();
|
||||
Level?.SpawnCorpses();
|
||||
AutoItemPlacer.PlaceIfNeeded(GameMode);
|
||||
AutoItemPlacer.PlaceIfNeeded();
|
||||
}
|
||||
if (GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
if (GameMode is MultiPlayerCampaign mpCampaign)
|
||||
{
|
||||
mpCampaign.CargoManager.CreateItems();
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
mpCampaign.CargoManager.CreatePurchasedItems();
|
||||
#if SERVER
|
||||
mpCampaign.SendCrewState(false, null);
|
||||
#endif
|
||||
}
|
||||
mpCampaign.UpgradeManager.ApplyUpgrades();
|
||||
mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine);
|
||||
}
|
||||
if (GameMode is CampaignMode)
|
||||
{
|
||||
Submarine.WarmStartPower();
|
||||
}
|
||||
}
|
||||
|
||||
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name);
|
||||
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level?.Seed ?? "[NO_LEVEL]"));
|
||||
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
|
||||
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
|
||||
|
||||
#if CLIENT
|
||||
if (GameMode is SinglePlayerCampaign) { SteamAchievementManager.OnBiomeDiscovered(level.Biome); }
|
||||
if (!(GameMode is SubTestMode)) { RoundSummary = new RoundSummary(this); }
|
||||
|
||||
GameMain.GameScreen.ColorFade(Color.Black, Color.TransparentBlack, 5.0f);
|
||||
|
||||
if (!(GameMode is TutorialMode) && !(GameMode is SubTestMode))
|
||||
{
|
||||
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
|
||||
GUI.AddMessage(level.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, level.Difficulty / 100.0f), 5.0f, playSound: false);
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false);
|
||||
}
|
||||
#endif
|
||||
|
||||
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
|
||||
RoundStartTime = Timing.TotalTime;
|
||||
GameMain.ResetFrameTime();
|
||||
IsRunning = true;
|
||||
}
|
||||
|
||||
public void PlaceSubAtStart(Level level)
|
||||
{
|
||||
if (level == null)
|
||||
{
|
||||
Submarine.MainSub.SetPosition(Vector2.Zero);
|
||||
return;
|
||||
}
|
||||
if (level.StartOutpost != null)
|
||||
{
|
||||
//start by placing the sub below the outpost
|
||||
Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders();
|
||||
Rectangle subBorders = Submarine.GetDockedBorders();
|
||||
|
||||
Submarine.SetPosition(
|
||||
Level.Loaded.StartOutpost.WorldPosition -
|
||||
new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
|
||||
|
||||
//find the port that's the nearest to the outpost and dock if one is found
|
||||
float closestDistance = 0.0f;
|
||||
DockingPort myPort = null, outPostPort = null;
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
{
|
||||
if (port.IsHorizontal || port.Docked) { continue; }
|
||||
if (port.Item.Submarine == level.StartOutpost)
|
||||
{
|
||||
outPostPort = port;
|
||||
continue;
|
||||
}
|
||||
if (port.Item.Submarine != Submarine) { continue; }
|
||||
|
||||
//the submarine port has to be at the top of the sub
|
||||
if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; }
|
||||
|
||||
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
|
||||
if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
|
||||
{
|
||||
myPort = port;
|
||||
closestDistance = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (myPort != null && outPostPort != null)
|
||||
{
|
||||
Vector2 portDiff = myPort.Item.WorldPosition - Submarine.WorldPosition;
|
||||
Vector2 spawnPos = (outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance;
|
||||
|
||||
bool startDocked = level.Type == LevelData.LevelType.Outpost;
|
||||
#if CLIENT
|
||||
startDocked |= GameMode is TutorialMode;
|
||||
#endif
|
||||
if (startDocked)
|
||||
{
|
||||
Submarine.SetPosition(spawnPos);
|
||||
myPort.Dock(outPostPort);
|
||||
myPort.Lock(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Submarine.SetPosition(spawnPos - Vector2.UnitY * 100.0f);
|
||||
Submarine.NeutralizeBallast();
|
||||
Submarine.EnableMaintainPosition();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Submarine.NeutralizeBallast();
|
||||
Submarine.EnableMaintainPosition();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition, verticalMoveDir: 1));
|
||||
Submarine.NeutralizeBallast();
|
||||
Submarine.EnableMaintainPosition();
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
@@ -333,35 +517,35 @@ namespace Barotrauma
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
public void EndRound(string endMessage)
|
||||
public void EndRound(string endMessage, List<TraitorMissionResult> traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
|
||||
{
|
||||
if (Mission != null) Mission.End();
|
||||
if (Mission != null) { Mission.End(); }
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
(Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail,
|
||||
(Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail,
|
||||
GameMode.Preset.Identifier,
|
||||
(Mission == null ? "None" : Mission.GetType().ToString()));
|
||||
Mission == null ? "None" : Mission.GetType().ToString());
|
||||
|
||||
#if CLIENT
|
||||
if (RoundSummary != null)
|
||||
if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null)
|
||||
{
|
||||
GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(endMessage);
|
||||
GUI.ClearMessages();
|
||||
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary);
|
||||
GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType);
|
||||
GUIMessageBox.MessageBoxes.Add(summaryFrame);
|
||||
var okButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), summaryFrame.Children.First().Children.First().FindChild("buttonarea").RectTransform),
|
||||
TextManager.Get("OK"))
|
||||
{
|
||||
OnClicked = (GUIButton button, object obj) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }
|
||||
};
|
||||
RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; };
|
||||
}
|
||||
|
||||
if (GameMain.NetLobbyScreen != null) GameMain.NetLobbyScreen.OnRoundEnded();
|
||||
TabMenu.OnRoundEnded();
|
||||
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction");
|
||||
#endif
|
||||
|
||||
EventManager?.EndRound();
|
||||
SteamAchievementManager.OnRoundEnded(this);
|
||||
|
||||
Mission = null;
|
||||
|
||||
GameMode?.End(transitionType);
|
||||
EventManager?.EndRound();
|
||||
StatusEffect.StopAll();
|
||||
Mission = null;
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
public void KillCharacter(Character character)
|
||||
@@ -455,7 +639,18 @@ namespace Barotrauma
|
||||
XDocument doc = new XDocument(new XElement("Gamesession"));
|
||||
|
||||
doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
|
||||
doc.Root.Add(new XAttribute("version", GameMain.Version));
|
||||
doc.Root.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
|
||||
if (OwnedSubmarines != null)
|
||||
{
|
||||
List<string> ownedSubmarineNames = new List<string>();
|
||||
var ownedSubsElement = new XElement("ownedsubmarines");
|
||||
doc.Root.Add(ownedSubsElement);
|
||||
foreach (var ownedSub in OwnedSubmarines)
|
||||
{
|
||||
ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name)));
|
||||
}
|
||||
}
|
||||
doc.Root.Add(new XAttribute("mapseed", Map.Seed));
|
||||
doc.Root.Add(new XAttribute("selectedcontentpackages",
|
||||
string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
|
||||
@@ -472,7 +667,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void Load(XElement saveElement)
|
||||
/*public void Load(XElement saveElement)
|
||||
{
|
||||
foreach (XElement subElement in saveElement.Elements())
|
||||
{
|
||||
@@ -495,7 +690,7 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class HireManager
|
||||
{
|
||||
private List<CharacterInfo> availableCharacters;
|
||||
public IEnumerable<CharacterInfo> AvailableCharacters
|
||||
{
|
||||
get { return availableCharacters; }
|
||||
}
|
||||
public List<CharacterInfo> AvailableCharacters { get; set; }
|
||||
public List<CharacterInfo> PendingHires = new List<CharacterInfo>();
|
||||
|
||||
public const int MaxAvailableCharacters = 10;
|
||||
|
||||
public HireManager()
|
||||
{
|
||||
availableCharacters = new List<CharacterInfo>();
|
||||
AvailableCharacters = new List<CharacterInfo>();
|
||||
}
|
||||
|
||||
public void RemoveCharacter(CharacterInfo character)
|
||||
{
|
||||
availableCharacters.Remove(character);
|
||||
AvailableCharacters.Remove(character);
|
||||
}
|
||||
|
||||
public void GenerateCharacters(Location location, int amount)
|
||||
{
|
||||
availableCharacters.ForEach(c => c.Remove());
|
||||
availableCharacters.Clear();
|
||||
AvailableCharacters.ForEach(c => c.Remove());
|
||||
AvailableCharacters.Clear();
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
JobPrefab job = location.Type.GetRandomHireable();
|
||||
if (job == null) { return; }
|
||||
|
||||
var variant = Rand.Range(0, job.Variants, Rand.RandSync.Server);
|
||||
availableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant));
|
||||
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant));
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
availableCharacters.ForEach(c => c.Remove());
|
||||
availableCharacters.Clear();
|
||||
AvailableCharacters.ForEach(c => c.Remove());
|
||||
AvailableCharacters.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,747 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal class PurchasedUpgrade
|
||||
{
|
||||
public readonly UpgradeCategory Category;
|
||||
public readonly UpgradePrefab Prefab;
|
||||
public int Level;
|
||||
|
||||
public PurchasedUpgrade(UpgradePrefab upgradePrefab, UpgradeCategory category, int level = 1)
|
||||
{
|
||||
Category = category;
|
||||
Prefab = upgradePrefab;
|
||||
Level = level;
|
||||
}
|
||||
|
||||
public void Deconstruct(out UpgradePrefab prefab, out UpgradeCategory category, out int level)
|
||||
{
|
||||
prefab = Prefab;
|
||||
category = Category;
|
||||
level = Level;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class handles all upgrade logic.
|
||||
/// Storing, applying, checking and validation of upgrades.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Upgrades are applied per item basis meaning each item has their own set of slots for upgrades.
|
||||
/// The store applies upgrades globally to categories of items so the purpose of this class is to keep those individual "upgrade slots" in sync.
|
||||
/// The target level of an upgrade is stored in the metadata and is what the store displays and modifies while this class will make sure that
|
||||
/// the upgrades on the items match the values stored in the metadata.
|
||||
/// </remarks>
|
||||
partial class UpgradeManager
|
||||
{
|
||||
/// <summary>
|
||||
/// This one toggles whether or not connected submarines get upgraded too.
|
||||
/// Could probably be removed, I just didn't like magic numbers.
|
||||
/// </summary>
|
||||
public const bool UpgradeAlsoConnectedSubs = true;
|
||||
|
||||
/// <summary>
|
||||
/// Prevents the player from upgrading the submarine when we are switching to a new one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In singleplayer we check if CampaignMode.PendingSubmarineSwitch is not null indicating we are switching submarines
|
||||
/// but in multiplayer that value is not synced so we use this variable instead by setting it to false in <see cref="UpgradeManager.ClientRead"/>
|
||||
/// and then set it back to true when the round ends in <see cref="MultiPlayerCampaign.End"/>
|
||||
/// </remarks>
|
||||
public bool CanUpgrade = true;
|
||||
|
||||
/// <summary>
|
||||
/// This is used by the client in multiplayer, acts like a secondary PendingUpgrades list
|
||||
/// but is not affected by server messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not used in singleplayer.
|
||||
/// </remarks>
|
||||
private List<PurchasedUpgrade>? loadedUpgrades;
|
||||
|
||||
/// <summary>
|
||||
/// This is used by the client to notify the server which upgrades are yet to be paid for.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In singleplayer this does nothing.
|
||||
/// </remarks>
|
||||
public readonly List<PurchasedUpgrade> PurchasedUpgrades = new List<PurchasedUpgrade>();
|
||||
|
||||
public readonly List<PurchasedUpgrade> PendingUpgrades = new List<PurchasedUpgrade>();
|
||||
|
||||
private CampaignMetadata Metadata => Campaign.CampaignMetadata;
|
||||
private readonly CampaignMode Campaign;
|
||||
private int spentMoney;
|
||||
|
||||
public event Action? OnUpgradesChanged;
|
||||
|
||||
public UpgradeManager(CampaignMode campaign)
|
||||
{
|
||||
DebugConsole.Log("Created brand new upgrade manager.");
|
||||
Campaign = campaign;
|
||||
}
|
||||
|
||||
public UpgradeManager(CampaignMode campaign, XElement element, bool isSingleplayer) : this(campaign)
|
||||
{
|
||||
DebugConsole.Log($"Restored upgrade manager from save file, ({element.Elements().Count()} pending upgrades).");
|
||||
LoadPendingUpgrades(element, isSingleplayer);
|
||||
}
|
||||
|
||||
private DateTime lastUpgradeSpeak, lastErrorSpeak;
|
||||
|
||||
/// <summary>
|
||||
/// Purchases an upgrade and handles logic for deducting the credit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Purchased upgrades are temporarily stored in <see cref="PendingUpgrades"/> and they are applied
|
||||
/// after the next round starts similarly how items are spawned in the stowage room after the round starts.
|
||||
/// </remarks>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category)
|
||||
{
|
||||
if (!CanUpgradeSub())
|
||||
{
|
||||
DebugConsole.ThrowError("Cannot upgrade when switching to another submarine.");
|
||||
return;
|
||||
}
|
||||
|
||||
int price = prefab.Price.GetBuyprice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
|
||||
int currentLevel = GetUpgradeLevel(prefab, category);
|
||||
|
||||
if (currentLevel + 1 > prefab.MaxLevel)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {prefab.MaxLevel}). The transaction has been cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (price < 0)
|
||||
{
|
||||
Location? location = Campaign.Map?.CurrentLocation;
|
||||
LogError($"Upgrade price is less than 0! ({price})",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
{ "Level", currentLevel },
|
||||
{ "Saved Level", GetRealUpgradeLevel(prefab, category) },
|
||||
{ "Upgrade", $"{category.Identifier}.{prefab.Identifier}" },
|
||||
{ "Location", location?.Type },
|
||||
{ "Reputation", $"{location?.Reputation?.Value} / {location?.Reputation?.MaxReputation}" },
|
||||
{ "Base Price", prefab.Price.BasePrice }
|
||||
});
|
||||
}
|
||||
|
||||
if (Campaign.Money > price)
|
||||
{
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
|
||||
if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
|
||||
{
|
||||
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
|
||||
lastUpgradeSpeak = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
Campaign.Money -= price;
|
||||
spentMoney += price;
|
||||
|
||||
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
|
||||
|
||||
#if CLIENT
|
||||
DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for ${price}", GUI.Style.Orange);
|
||||
#endif
|
||||
|
||||
if (upgrade == null)
|
||||
{
|
||||
PendingUpgrades.Add(new PurchasedUpgrade(prefab, category));
|
||||
}
|
||||
else
|
||||
{
|
||||
upgrade.Level++;
|
||||
}
|
||||
#if CLIENT
|
||||
// tell the server that this item is yet to be paid for server side
|
||||
PurchasedUpgrades.Add(new PurchasedUpgrade(prefab, category));
|
||||
#endif
|
||||
OnUpgradesChanged?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" +
|
||||
$"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.Money}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies all our pending upgrades to the submarine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Upgrades are applied similarly to how items on the submarine are spawned at the start of the round.
|
||||
/// Upgrades should be applied at the start of the round and after the round ends they are written into
|
||||
/// the submarine save and saved there.
|
||||
/// Because of the difficulty of accessing the actual Submarine object from and outpost or when the campaign UI is created
|
||||
/// we modify levels that are shown on the store interface using campaign metadata.
|
||||
///
|
||||
/// This method should be called by both the client and the server during level generation.
|
||||
/// <see cref="SetUpgradeLevel"/>
|
||||
/// <seealso cref="GetUpgradeLevel"/>
|
||||
/// </remarks>
|
||||
public void ApplyUpgrades()
|
||||
{
|
||||
PurchasedUpgrades.Clear();
|
||||
if (Submarine.MainSub == null) { return; }
|
||||
|
||||
List<PurchasedUpgrade> pendingUpgrades = PendingUpgrades;
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
if (Level.Loaded?.Type != LevelData.LevelType.Outpost)
|
||||
{
|
||||
if (loadedUpgrades != null)
|
||||
{
|
||||
// client receives pending upgrades from the save file
|
||||
pendingUpgrades = loadedUpgrades;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// prevent the client from applying pending upgrades at an outpost when joining mid round
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DebugConsole.Log("Applying upgrades...");
|
||||
foreach (var (prefab, category, level) in pendingUpgrades)
|
||||
{
|
||||
int newLevel = BuyUpgrade(prefab, category, Submarine.MainSub, level);
|
||||
DebugConsole.Log($" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})");
|
||||
if (newLevel > 0)
|
||||
{
|
||||
SetUpgradeLevel(prefab, category, Math.Clamp(newLevel, 0, prefab.MaxLevel));
|
||||
}
|
||||
}
|
||||
|
||||
PendingUpgrades.Clear();
|
||||
loadedUpgrades?.Clear();
|
||||
loadedUpgrades = null;
|
||||
spentMoney = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the pending upgrades and refunds the money spent
|
||||
/// </summary>
|
||||
private void RefundUpgrades()
|
||||
{
|
||||
DebugConsole.Log($"Refunded {spentMoney} marks in pending upgrades.");
|
||||
if (spentMoney > 0)
|
||||
{
|
||||
#if CLIENT
|
||||
GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("UpgradeRefundTitle"), TextManager.Get("UpgradeRefundBody"), new[] { TextManager.Get("Ok") });
|
||||
msgBox.Buttons[0].OnClicked += msgBox.Close;
|
||||
#endif
|
||||
}
|
||||
|
||||
Campaign.Money += spentMoney;
|
||||
spentMoney = 0;
|
||||
PendingUpgrades.Clear();
|
||||
PurchasedUpgrades.Clear();
|
||||
}
|
||||
|
||||
public void CreateUpgradeErrorMessage(string text, bool isSinglePlayer, Character character)
|
||||
{
|
||||
// 10 second cooldown on the error message but not the UI sound
|
||||
if (lastErrorSpeak == DateTime.MinValue || lastErrorSpeak.AddSeconds(10) < DateTime.Now)
|
||||
{
|
||||
UpgradeNPCSpeak(text, isSinglePlayer, character);
|
||||
lastErrorSpeak = DateTime.Now;
|
||||
}
|
||||
#if CLIENT
|
||||
GUI.PlayUISound(GUISoundType.PickItemFail);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the NPC talk or if no NPC has been specified find the upgrade NPC and make it talk.
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <param name="isSinglePlayer"></param>
|
||||
/// <param name="character">Optional NPC to make talk, if null tries to find one at the outpost.</param>
|
||||
/// <remarks>
|
||||
/// This might seem a bit spaghetti but it's the only way I could figure out how to do this and make it work
|
||||
/// in both multiplayer and singleplayer because in multiplayer the client doesn't have access to SubmarineInfo.OutpostNPCs list
|
||||
/// so we cannot find the upgrade NPC using that and the client cannot use Character.Speak anyways in multiplayer so the alternative
|
||||
/// is to send network packages when interacting with the NPC.
|
||||
/// </remarks>
|
||||
partial void UpgradeNPCSpeak(string text, bool isSinglePlayer, Character? character = null);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that upgrade values stored in CampaignMetadata matches the values on the submarine and fixes any inconsistencies.
|
||||
/// Should be called after every round start right after <see cref="ApplyUpgrades"/>
|
||||
/// </summary>
|
||||
/// <param name="submarine"></param>
|
||||
public void SanityCheckUpgrades(Submarine submarine)
|
||||
{
|
||||
// check walls
|
||||
foreach (Structure wall in submarine.GetWalls(UpgradeAlsoConnectedSubs))
|
||||
{
|
||||
foreach (UpgradeCategory category in UpgradeCategory.Categories)
|
||||
{
|
||||
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
|
||||
{
|
||||
int level = GetRealUpgradeLevel(prefab, category);
|
||||
if (level == 0 || !prefab.IsWallUpgrade) { continue; }
|
||||
|
||||
Upgrade? upgrade = wall.GetUpgrade(prefab.Identifier);
|
||||
|
||||
bool isOverMax = IsOverMaxLevel(level, prefab);
|
||||
if (isOverMax)
|
||||
{
|
||||
SetUpgradeLevel(prefab, category, prefab.MaxLevel);
|
||||
level = prefab.MaxLevel;
|
||||
}
|
||||
|
||||
if (upgrade == null || upgrade.Level != level || isOverMax)
|
||||
{
|
||||
DebugConsole.AddWarning($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
|
||||
FixUpgradeOnItem(wall, prefab, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check items
|
||||
foreach (Item item in submarine.GetItems(UpgradeAlsoConnectedSubs))
|
||||
{
|
||||
foreach (UpgradeCategory category in UpgradeCategory.Categories)
|
||||
{
|
||||
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
|
||||
{
|
||||
if (!category.CanBeApplied(item, prefab)) { continue; }
|
||||
|
||||
int level = GetRealUpgradeLevel(prefab, category);
|
||||
if (level == 0) { continue; }
|
||||
|
||||
Upgrade? upgrade = item.GetUpgrade(prefab.Identifier);
|
||||
bool isOverMax = IsOverMaxLevel(level, prefab);
|
||||
if (isOverMax)
|
||||
{
|
||||
SetUpgradeLevel(prefab, category, prefab.MaxLevel);
|
||||
level = prefab.MaxLevel;
|
||||
}
|
||||
|
||||
if (upgrade == null || upgrade.Level != level || isOverMax)
|
||||
{
|
||||
DebugConsole.AddWarning($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing...");
|
||||
FixUpgradeOnItem(item, prefab, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsOverMaxLevel(int level, UpgradePrefab prefab) => level > prefab.MaxLevel;
|
||||
}
|
||||
|
||||
private static void FixUpgradeOnItem(ISerializableEntity target, UpgradePrefab prefab, int level)
|
||||
{
|
||||
if (target is MapEntity mapEntity)
|
||||
{
|
||||
mapEntity.SetUpgrade(new Upgrade(target, prefab, level), false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an upgrade on the submarine, should be called by <see cref="ApplyUpgrades"/> when the round starts.
|
||||
/// </summary>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
/// <param name="submarine"></param>
|
||||
/// <param name="level"></param>
|
||||
/// <returns>New level that was applied, -1 if no upgrades were applied.</returns>
|
||||
private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1)
|
||||
{
|
||||
int? newLevel = null;
|
||||
if (category.IsWallUpgrade)
|
||||
{
|
||||
foreach (Structure structure in submarine.GetWalls(UpgradeAlsoConnectedSubs))
|
||||
{
|
||||
Upgrade upgrade = new Upgrade(structure, prefab, level);
|
||||
structure.AddUpgrade(upgrade, createNetworkEvent: false);
|
||||
|
||||
Upgrade? newUpgrade = structure.GetUpgrade(prefab.Identifier);
|
||||
if (newUpgrade != null)
|
||||
{
|
||||
SanityCheck(newUpgrade, structure);
|
||||
newLevel ??= newUpgrade.Level;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Item item in submarine.GetItems(UpgradeAlsoConnectedSubs))
|
||||
{
|
||||
if (category.CanBeApplied(item, prefab))
|
||||
{
|
||||
Upgrade upgrade = new Upgrade(item, prefab, level);
|
||||
item.AddUpgrade(upgrade, createNetworkEvent: false);
|
||||
|
||||
Upgrade? newUpgrade = item.GetUpgrade(prefab.Identifier);
|
||||
if (newUpgrade != null)
|
||||
{
|
||||
SanityCheck(newUpgrade, item);
|
||||
newLevel ??= newUpgrade.Level;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Submarine loadedSub in Submarine.Loaded.Where(sub => sub != submarine))
|
||||
{
|
||||
XElement? root = loadedSub.Info?.SubmarineElement;
|
||||
if (root == null) { continue; }
|
||||
|
||||
if (root.Name.ToString().Equals("LinkedSubmarine", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (root.Attribute("location") == null) { continue; }
|
||||
|
||||
// Check if this is our linked submarine
|
||||
ushort dockingPortID = (ushort) root.GetAttributeInt("originallinkedto", 0);
|
||||
if (dockingPortID > 0 && submarine.GetItems(true).Any(item => item.ID == dockingPortID))
|
||||
{
|
||||
BuyUpgrade(prefab, category, loadedSub, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newLevel ?? -1;
|
||||
|
||||
void SanityCheck(Upgrade newUpgrade, MapEntity target)
|
||||
{
|
||||
if (newLevel != null && newLevel != newUpgrade.Level)
|
||||
{
|
||||
// automatically fix this if it ever happens?
|
||||
DebugConsole.AddWarning($"The upgrade {newUpgrade.Prefab.Name} in {target.Name} has a different level compared to other items! \n" +
|
||||
$"Expected level was ${newLevel} but got {newUpgrade.Level} instead.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the progress that is shown on the store interface.
|
||||
/// Includes values stored in the metadata and <see cref="PendingUpgrades"/>
|
||||
/// </summary>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
|
||||
{
|
||||
if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); }
|
||||
|
||||
return GetRealUpgradeLevel(prefab, category) + GetPendingLevel();
|
||||
|
||||
int GetPendingLevel()
|
||||
{
|
||||
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
|
||||
return upgrade?.Level ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the level of the upgrade that is stored in the metadata.
|
||||
/// </summary>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
public int GetRealUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
|
||||
{
|
||||
return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the target upgrade level in the campaign metadata.
|
||||
/// </summary>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
/// <param name="level"></param>
|
||||
private void SetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, int level)
|
||||
{
|
||||
Metadata.SetValue(FormatIdentifier(prefab, category), level);
|
||||
}
|
||||
|
||||
public bool CanUpgradeSub()
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return CanUpgrade; }
|
||||
|
||||
return Campaign.PendingSubmarineSwitch == null;
|
||||
}
|
||||
|
||||
public void RefundResetAndReload(SubmarineInfo newSubmarine, bool notifyClients = false)
|
||||
{
|
||||
RefundUpgrades();
|
||||
ResetUpgrades();
|
||||
Dictionary<string, int> newUpgrades = ReloadUpgradeValues(newSubmarine);
|
||||
#if SERVER
|
||||
if (notifyClients)
|
||||
{
|
||||
SendUpgradeResetMessage(newUpgrades);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a SubmarineInfo and sets the store values accordingly.
|
||||
/// Used when reloading a previously saved submarine.
|
||||
/// </summary>
|
||||
/// <param name="info"></param>
|
||||
private Dictionary<string, int> ReloadUpgradeValues(SubmarineInfo info)
|
||||
{
|
||||
Dictionary<string, int> newValues = new Dictionary<string, int>();
|
||||
IEnumerable<XElement> linkedSubElements = info.SubmarineElement.Elements().Where(element => element.Name.ToString().Equals("LinkedSubmarine", StringComparison.OrdinalIgnoreCase)).SelectMany(element => element.Elements());
|
||||
IEnumerable<XElement> mainSubElements = info.SubmarineElement.Elements().Where(Predicate);
|
||||
List<XElement> elements = mainSubElements.Concat(linkedSubElements.Where(Predicate)).ToList();
|
||||
foreach (UpgradeCategory category in UpgradeCategory.Categories)
|
||||
{
|
||||
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
|
||||
{
|
||||
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
|
||||
|
||||
List<int> levels = GetUpgradeFromXML(elements, category, prefab);
|
||||
if (levels.Any())
|
||||
{
|
||||
int level = (int) levels.Average(i => i);
|
||||
newValues.Add(FormatIdentifier(prefab, category), level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (dataIdentifier, level) in newValues)
|
||||
{
|
||||
Campaign.CampaignMetadata.SetValue(dataIdentifier, level);
|
||||
}
|
||||
|
||||
return newValues;
|
||||
|
||||
static List<int> GetUpgradeFromXML(List<XElement> elements, UpgradeCategory category, UpgradePrefab prefab)
|
||||
{
|
||||
List<int> levels = new List<int>();
|
||||
foreach (XElement subElement in elements)
|
||||
{
|
||||
if (!category.CanBeApplied(subElement)) { continue; }
|
||||
|
||||
foreach (XElement component in subElement.Elements())
|
||||
{
|
||||
if (string.Equals(component.Name.ToString(), "upgrade", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string identifier = component.GetAttributeString("identifier", string.Empty);
|
||||
int level = component.GetAttributeInt("level", -1);
|
||||
if (string.IsNullOrWhiteSpace(identifier) || level <= 0) { continue; }
|
||||
|
||||
UpgradePrefab? matchingPrefab = UpgradePrefab.Find(identifier);
|
||||
if (matchingPrefab == null || matchingPrefab != prefab) { continue; }
|
||||
|
||||
if (matchingPrefab.UpgradeCategories.Contains(category)) { levels.Add(level); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
static bool Predicate(XElement element) => element.HasElements && element.Elements().Any(e => e.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Resets our upgrade progress and prices.
|
||||
/// This does not actually remove the upgrades from the submarine but resets the store interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method works by iterating thru all upgrade categories and prefabs and checking if they have a
|
||||
/// valid key stored in the metadata, if they do set it to 0, upgrades without a key stored are always
|
||||
/// assumed to be 0 so they don't need to be reset.
|
||||
///
|
||||
/// Should initially be called server side as we can't trust clients with such a simple notification.
|
||||
/// </remarks>
|
||||
private void ResetUpgrades()
|
||||
{
|
||||
foreach (UpgradeCategory category in UpgradeCategory.Categories)
|
||||
{
|
||||
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
|
||||
{
|
||||
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
|
||||
|
||||
string dataIdentifier = FormatIdentifier(prefab, category);
|
||||
if (Metadata.HasKey(dataIdentifier))
|
||||
{
|
||||
Metadata.SetValue(dataIdentifier, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnUpgradesChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SavePendingUpgrades(XElement? parent, List<PurchasedUpgrade> upgrades)
|
||||
{
|
||||
if (parent == null) { return; }
|
||||
|
||||
DebugConsole.Log("Saving pending upgrades to save file...");
|
||||
XElement upgradeElement = new XElement("PendingUpgrades");
|
||||
foreach (var (prefab, category, level) in upgrades)
|
||||
{
|
||||
upgradeElement.Add(new XElement("PendingUpgrade",
|
||||
new XAttribute("category", category.Identifier),
|
||||
new XAttribute("prefab", prefab.Identifier),
|
||||
new XAttribute("level", level)));
|
||||
}
|
||||
|
||||
DebugConsole.Log($"Saved {upgradeElement.Elements().Count()} pending upgrades.");
|
||||
parent.Add(upgradeElement);
|
||||
}
|
||||
|
||||
private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true)
|
||||
{
|
||||
if (element == null || !element.HasElements) { return; }
|
||||
|
||||
List<PurchasedUpgrade> pendingUpgrades = new List<PurchasedUpgrade>();
|
||||
|
||||
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||
foreach (XElement upgrade in element.Elements())
|
||||
{
|
||||
string? categoryIdentifier = upgrade.GetAttributeString("category", null);
|
||||
UpgradeCategory? category = UpgradeCategory.Find(categoryIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(categoryIdentifier) || category == null) { continue; }
|
||||
|
||||
string? prefabIdentifier = upgrade.GetAttributeString("prefab", null);
|
||||
UpgradePrefab? prefab = UpgradePrefab.Find(prefabIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(prefabIdentifier) || prefab == null) { continue; }
|
||||
|
||||
int level = upgrade.GetAttributeInt("level", -1);
|
||||
if (level < 0) { continue; }
|
||||
|
||||
pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level));
|
||||
}
|
||||
|
||||
if (isSingleplayer)
|
||||
{
|
||||
SetPendingUpgrades(pendingUpgrades);
|
||||
}
|
||||
else
|
||||
{
|
||||
loadedUpgrades = pendingUpgrades;
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogError(string text, Dictionary<string, object?> data, Exception e = null)
|
||||
{
|
||||
string error = $"{text}\n";
|
||||
foreach (var (label, value) in data)
|
||||
{
|
||||
error += $" - {label}: {value ?? "NULL"}\n";
|
||||
}
|
||||
|
||||
DebugConsole.ThrowError(error.TrimEnd('\n'), e);
|
||||
}
|
||||
|
||||
public static Dictionary<string, int> GetMetadataLevels(CampaignMetadata? metadata)
|
||||
{
|
||||
Dictionary<string, int> values = new Dictionary<string, int>();
|
||||
|
||||
if (metadata == null) { return values; }
|
||||
|
||||
foreach (UpgradeCategory category in UpgradeCategory.Categories)
|
||||
{
|
||||
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
|
||||
{
|
||||
string identifier = FormatIdentifier(prefab, category);
|
||||
if (metadata.HasKey(identifier) && !values.ContainsKey(identifier))
|
||||
{
|
||||
values.Add(identifier, metadata.GetInt(identifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the client and the server are agreeing on the upgrade levels, if not something has gone wrong.
|
||||
/// </summary>
|
||||
/// <param name="clientUpgrades"></param>
|
||||
/// <param name="serverUpgrades"></param>
|
||||
public static void CompareUpgrades(Dictionary<string, int> clientUpgrades, Dictionary<string, int> serverUpgrades)
|
||||
{
|
||||
int mismatches = 0;
|
||||
DebugLog("Comparing client upgrades to server upgrades...", Color.Orange);
|
||||
foreach (var (key, value) in clientUpgrades)
|
||||
{
|
||||
if (!serverUpgrades.ContainsKey(key))
|
||||
{
|
||||
DebugLog($"Client has an upgrade the server doesn't! {key} lvl. {value}.", Color.Red);
|
||||
mismatches++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value != serverUpgrades[key])
|
||||
{
|
||||
DebugLog($"Client's upgrade level doesn't match the server's! Client: {key} {value}, Server: {key} {serverUpgrades[key]}.", Color.Red);
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
DebugLog("...comparing server upgrades to client upgrades...", Color.Orange);
|
||||
foreach (var (key, value) in serverUpgrades)
|
||||
{
|
||||
if (!clientUpgrades.ContainsKey(key))
|
||||
{
|
||||
DebugLog($"Server has an upgrade the client doesn't! {key} lvl. {value}.", Color.Red);
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mismatches == 0)
|
||||
{
|
||||
DebugLog("Everything ok!");
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugLog($"{mismatches} mismatches found! This means that the client and the server are disagreeing on upgrade levels and might cause desync.\n", Color.Red);
|
||||
#if CLIENT
|
||||
DebugConsole.IsOpen = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to sync the pending upgrades list in multiplayer.
|
||||
/// </summary>
|
||||
/// <param name="upgrades"></param>
|
||||
/// <remarks>
|
||||
/// In singleplayer this is not used and should not be.
|
||||
/// </remarks>
|
||||
public void SetPendingUpgrades(List<PurchasedUpgrade> upgrades)
|
||||
{
|
||||
PendingUpgrades.Clear();
|
||||
PendingUpgrades.AddRange(upgrades);
|
||||
OnUpgradesChanged?.Invoke();
|
||||
}
|
||||
|
||||
public static void DebugLog(string msg, Color? color = null)
|
||||
{
|
||||
#if UNSTABLE || DEBUG
|
||||
DebugConsole.NewMessage(msg, color ?? Color.GreenYellow);
|
||||
#else
|
||||
DebugConsole.Log(msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
private PurchasedUpgrade? FindMatchingUpgrade(UpgradePrefab prefab, UpgradeCategory category) => PendingUpgrades.Find(u => u.Prefab == prefab && u.Category == category);
|
||||
|
||||
private static string FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user