Track LocalMods as part of monolith
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
partial class BeaconConstMission : Mission
|
||||
{
|
||||
private readonly LocalizedString sonarLabel;
|
||||
private readonly int PriceUtility;
|
||||
private readonly int PriceStructure;
|
||||
private readonly int PriceElectric;
|
||||
public BeaconConstMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
sonarLabel = TextManager.Get("beaconconsonarlabel");
|
||||
var supplyCosts = prefab.ConfigElement.GetChildElement("supplycosts");
|
||||
PriceUtility = GetPrice("supply_utility");
|
||||
PriceStructure = GetPrice("supply_structural");
|
||||
PriceElectric = GetPrice("supply_electrical");
|
||||
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
|
||||
description = descriptionWithoutReward.Replace("[reward]", rewardText);
|
||||
|
||||
int GetPrice(string supplyType) => supplyCosts.GetChildElement(supplyType).GetAttributeInt("price", 0);
|
||||
}
|
||||
|
||||
// public override LocalizedString SonarLabel => base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
|
||||
public override int Reward => GetReward();
|
||||
public override float GetBaseReward(Submarine sub) => GetReward();
|
||||
|
||||
private int GetReward()
|
||||
{
|
||||
LocationConnection connection = Locations[0].Connections.Find(lc => lc.Locations.Contains(Locations[1]));
|
||||
var levelData = connection.LevelData.MLC();
|
||||
int reward = (int)(((PriceUtility * levelData.RequestedU) +
|
||||
(PriceStructure * levelData.RequestedS) +
|
||||
(PriceElectric * levelData.RequestedE)) * 2.5);
|
||||
return reward;
|
||||
}
|
||||
|
||||
public override void StartMissionSpecific(Level level) => description = description.Replace("[requestedsupplies]", level.LevelData.MLC().GetRequestedSupplies());
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (level.MLC().BeaconConstructionStation == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
Vector2 worldPos = level.MLC().BeaconConstructionStation.WorldPosition;
|
||||
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient) { return; }
|
||||
if (State == 0 && level.MLC().CheckSuppliesDelivered())
|
||||
{
|
||||
State = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
if (completed && level.LevelData != null)
|
||||
{
|
||||
level.LevelData.IsBeaconActive = true;
|
||||
level.LevelData.HasBeaconStation = true;
|
||||
level.LevelData.MLC().HasBeaconConstruction = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void AdjustLevelData(LevelData levelData) => levelData.MLC().HasBeaconConstruction = true;
|
||||
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => level.MLC().CheckSuppliesDelivered();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using MoreLevelContent.Shared.Generation;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Resources;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
internal partial class CablePuzzleMission : Mission
|
||||
{
|
||||
/// <summary>
|
||||
/// This sucks ass but we should /NEVER/ have more than one relay station in one level
|
||||
/// </summary>
|
||||
public static SubmarineFile SubmarineFile { get; set; }
|
||||
|
||||
const float INTERVAL = 2.5f;
|
||||
const int RANGE_MIN = 50;
|
||||
const int RANGE_MAX = 200;
|
||||
const int REQUIRED_CYCLES = 2;
|
||||
const float REQUIRED_CYCLE_TIME = 1;
|
||||
const double CYCLE_GRACE_PERIOD = Timing.Step * 2;
|
||||
|
||||
private readonly LocalizedString defaultSonarLabel;
|
||||
private readonly string terminalTag;
|
||||
private readonly XElement _SubmarineConfig;
|
||||
private Submarine _Station;
|
||||
private LevelData _LevelData;
|
||||
private float _Timer = INTERVAL;
|
||||
|
||||
public CablePuzzleMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
_SubmarineConfig = prefab.ConfigElement.GetChildElement("Submarine");
|
||||
defaultSonarLabel = TextManager.Get("relaysonarlabel");
|
||||
terminalTag = _SubmarineConfig.GetAttributeString("welcomemsg", "relayrepair.terminal");
|
||||
SubmarineFile = null;
|
||||
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
|
||||
if (levelData != null)
|
||||
{
|
||||
SetLevel(levelData);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Station == null) yield break;
|
||||
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? defaultSonarLabel : Prefab.SonarLabel, _Station.WorldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLevel(LevelData level)
|
||||
{
|
||||
if (_LevelData != null)
|
||||
{
|
||||
//level already set
|
||||
return;
|
||||
}
|
||||
_LevelData = level;
|
||||
SetSub(_SubmarineConfig, Prefab);
|
||||
}
|
||||
|
||||
public static void SetSub(XElement config, MissionPrefab prefab)
|
||||
{
|
||||
ContentPath subPath = config.GetAttributeContentPath("path", prefab.ContentPackage);
|
||||
|
||||
if (subPath.IsNullOrEmpty())
|
||||
{
|
||||
Log.Error($"No path used for submarine for the relay station mission \"{prefab.Identifier}\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
|
||||
if (file == null)
|
||||
{
|
||||
Log.Error($"Failed to find submarine at path {subPath}");
|
||||
return;
|
||||
}
|
||||
SubmarineFile = file;
|
||||
Log.Debug("Set relay station sub file");
|
||||
}
|
||||
|
||||
|
||||
private MemoryComponent _WpInput;
|
||||
private MemoryComponent _WpTarget;
|
||||
private LightComponent _WpLight;
|
||||
private Terminal _WpHint;
|
||||
|
||||
private readonly List<SignalOperation> _Operations = new();
|
||||
private readonly SignalOperation[] _Sequence = new SignalOperation[4];
|
||||
private enum OperationType
|
||||
{
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div
|
||||
}
|
||||
|
||||
private struct SignalOperation
|
||||
{
|
||||
public SignalOperation(OperationType type, int value)
|
||||
{
|
||||
_Value = value;
|
||||
_Operation = type;
|
||||
}
|
||||
private readonly OperationType _Operation;
|
||||
private readonly int _Value;
|
||||
|
||||
public override string ToString() => $"{_Operation} {_Value}";
|
||||
|
||||
public int Run(int input)
|
||||
{
|
||||
switch (_Operation)
|
||||
{
|
||||
case OperationType.Add:
|
||||
return input += _Value;
|
||||
case OperationType.Sub:
|
||||
return input -= _Value;
|
||||
case OperationType.Mul:
|
||||
return input *= _Value;
|
||||
case OperationType.Div:
|
||||
return input /= _Value;
|
||||
}
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void StartMissionSpecific(Level level)
|
||||
{
|
||||
_Station = level.MLC().RelayStation;
|
||||
|
||||
if (_Station == null)
|
||||
{
|
||||
Log.Error("Failed to spawn relay station!!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsClient) return;
|
||||
var items = _Station.GetItems(false);
|
||||
var rand = MLCUtils.GetRandomFromString(_LevelData.Seed);
|
||||
var memoryComps = new List<MemoryComponent>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.HasTag("wp_input")) _WpInput = item.GetComponent<MemoryComponent>();
|
||||
if (item.HasTag("wp_target")) _WpTarget = item.GetComponent<MemoryComponent>();
|
||||
if (item.HasTag("wp_light")) _WpLight = item.GetComponent<LightComponent>();
|
||||
if (item.HasTag("wp_hint")) _WpHint = item.GetComponent<Terminal>();
|
||||
|
||||
if (item.HasTag("wp_add")) AddOperation(OperationType.Add);
|
||||
if (item.HasTag("wp_sub")) AddOperation(OperationType.Sub);
|
||||
if (item.HasTag("wp_mul")) AddOperation(OperationType.Mul);
|
||||
|
||||
void AddOperation(OperationType type, int low = 5, int high = 25)
|
||||
{
|
||||
var comp = item.GetComponent<MemoryComponent>();
|
||||
if (comp == null)
|
||||
{
|
||||
Log.Error("Failed to find a memory component on tagged item");
|
||||
return;
|
||||
}
|
||||
int val = rand.Next(low, high);
|
||||
comp.Value = val.ToString();
|
||||
memoryComps.Add(comp);
|
||||
|
||||
_Operations.Add(new SignalOperation(type, val));
|
||||
}
|
||||
}
|
||||
|
||||
if (_Operations.Count == 0)
|
||||
{
|
||||
Log.Error("Failed to collect any operations!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Build random sequence
|
||||
_Operations.Shuffle(rand);
|
||||
|
||||
_Sequence[0] = _Operations[0];
|
||||
_Sequence[1] = _Operations[1];
|
||||
_Sequence[2] = _Operations[2];
|
||||
_Sequence[3] = _Operations[3];
|
||||
|
||||
int[] steps0 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
|
||||
int[] steps1 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
|
||||
int[] steps2 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
|
||||
|
||||
_WpHint.ShowMessage = TextManager.GetWithVariables("relayrepair.terminal",
|
||||
("[version]", $"${Main.Version}"),
|
||||
("[station]", $"{rand.Next(100, 999)}"),
|
||||
|
||||
("[input0]", $"{steps0[0].ToString().PadLeft(3, '0')}"),
|
||||
("[input1]", $"{steps1[0].ToString().PadLeft(3, '0')}"),
|
||||
("[input2]", $"{steps2[0].ToString().PadLeft(3, '0')}"),
|
||||
|
||||
("[step10]", $"{steps0[1].ToString().PadLeft(3, '0')}"),
|
||||
("[step20]", $"{steps0[2].ToString().PadLeft(3, '0')}"),
|
||||
("[step30]", $"{steps0[3].ToString().PadLeft(3, '0')}"),
|
||||
|
||||
("[step11]", $"{steps1[1].ToString().PadLeft(3, '0')}"),
|
||||
("[step21]", $"{steps1[2].ToString().PadLeft(3, '0')}"),
|
||||
("[step31]", $"{steps1[3].ToString().PadLeft(3, '0')}"),
|
||||
|
||||
("[step12]", $"{steps2[1].ToString().PadLeft(3, '0')}"),
|
||||
("[step22]", $"{steps2[2].ToString().PadLeft(3, '0')}"),
|
||||
("[step32]", $"{steps2[3].ToString().PadLeft(3, '0')}"),
|
||||
|
||||
("[output0]", $"{steps0[4]}"),
|
||||
("[output1]", $"{steps1[4]}"),
|
||||
("[output2]", $"{steps2[4]}")
|
||||
|
||||
).Value;
|
||||
|
||||
Log.Debug("Sub created");
|
||||
|
||||
#if SERVER
|
||||
// Have the server send these changes to the client
|
||||
_WpHint.SyncHistory();
|
||||
foreach (var comp in memoryComps)
|
||||
{
|
||||
comp.Item.CreateServerEvent(comp);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
private int GetStepForInput(int input, int stepCount)
|
||||
{
|
||||
if (stepCount > _Sequence.Length)
|
||||
{
|
||||
Log.Error($"Requested sequence step ({stepCount}) is bigger then the sequence length ({_Sequence.Length})");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < stepCount; i++)
|
||||
{
|
||||
var operation = _Sequence[i];
|
||||
input = operation.Run(input);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
private int[] GetStepsForInput(int input)
|
||||
{
|
||||
int[] output = new int[_Sequence.Length + 1];
|
||||
output[0] = input;
|
||||
for (int i = 0; i < _Sequence.Length; i++)
|
||||
{
|
||||
input = _Sequence[i].Run(input);
|
||||
output[i + 1] = input;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
double successTimer = 0;
|
||||
public override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient) return;
|
||||
if (_Station == null) return;
|
||||
|
||||
if (State == 0)
|
||||
{
|
||||
if (CrewInSub())
|
||||
{
|
||||
State = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Crew has entered the relay and is fixing it
|
||||
if (State >= 1)
|
||||
{
|
||||
// We have the correct value
|
||||
if (_WpLight.IsOn)
|
||||
{
|
||||
successTimer = 2f;
|
||||
} else if (successTimer > 0)
|
||||
{
|
||||
successTimer -= deltaTime;
|
||||
}
|
||||
|
||||
State = successTimer > 0 ? 2 : 1;
|
||||
}
|
||||
|
||||
// Return if timer is counting
|
||||
if (_Timer > 0)
|
||||
{
|
||||
_Timer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
var input = Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced);
|
||||
_WpInput.Value = input.ToString();
|
||||
_WpTarget.Value = GetStepForInput(input, 4).ToString();
|
||||
|
||||
#if SERVER
|
||||
// Have the server send the info to the client
|
||||
_WpInput.Item.CreateServerEvent(_WpInput);
|
||||
_WpTarget.Item.CreateServerEvent(_WpTarget);
|
||||
#endif
|
||||
|
||||
_Timer = INTERVAL;
|
||||
|
||||
|
||||
bool CrewInSub()
|
||||
{
|
||||
foreach (var crewMember in GameSession.GetSessionCrewCharacters(CharacterType.Player))
|
||||
{
|
||||
if (crewMember.Submarine == _Station)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
if (completed && level.LevelData != null)
|
||||
{
|
||||
level.LevelData.MLC().RelayStationStatus = RelayStationStatus.Active;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => State == 2;
|
||||
}
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
using Barotrauma;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using static Barotrauma.Level;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
partial class DistressEscortMission : DistressMission
|
||||
{
|
||||
private readonly XElement localCharacterConfig;
|
||||
private readonly LocalizedString sonarLabel;
|
||||
private readonly PositionType spawnPositionType;
|
||||
private readonly bool hostile;
|
||||
|
||||
private readonly MissionNPCCollection missionNPCs;
|
||||
|
||||
|
||||
private int calculatedReward;
|
||||
private Submarine missionSub;
|
||||
private Vector2 spawnPosition;
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (State != 1)
|
||||
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, spawnPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public DistressEscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
missionSub = sub;
|
||||
localCharacterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
sonarLabel = TextManager.Get("missionname.distressmission");
|
||||
spawnPositionType = localCharacterConfig.GetAttributeEnum("spawntype", PositionType.Cave);
|
||||
hostile = localCharacterConfig.GetAttributeBool("hostile", false);
|
||||
missionNPCs = new(this, localCharacterConfig);
|
||||
CalculateReward();
|
||||
}
|
||||
|
||||
private void CalculateReward()
|
||||
{
|
||||
if (missionSub == null)
|
||||
{
|
||||
calculatedReward = Prefab.Reward;
|
||||
return;
|
||||
}
|
||||
|
||||
// Disabled for now, because they make balancing the missions a pain.
|
||||
int multiplier = 1;//CalculateScalingEscortedCharacterCount();
|
||||
calculatedReward = Prefab.Reward * multiplier;
|
||||
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖";
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
|
||||
public override float GetBaseReward(Submarine sub)
|
||||
{
|
||||
if (sub != missionSub)
|
||||
{
|
||||
missionSub = sub;
|
||||
CalculateReward();
|
||||
}
|
||||
return calculatedReward;
|
||||
}
|
||||
|
||||
private void InitEscort()
|
||||
{
|
||||
missionNPCs.Clear();
|
||||
|
||||
if (!Loaded.TryGetInterestingPosition(false, spawnPositionType, 0, out InterestingPosition _spawnPos))
|
||||
{
|
||||
Log.Error($"Failed to find a spawn position of type {spawnPositionType}! Falling back to any position.");
|
||||
bool failed = Loaded.TryGetInterestingPosition(false, PositionType.MainPath | PositionType.SidePath | PositionType.Cave | PositionType.Wreck, 0, out _spawnPos);
|
||||
if (failed)
|
||||
{
|
||||
Log.Error("Could not find ANY position to spawn distress humans at!");
|
||||
spawnPosition = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
spawnPosition = _spawnPos.Position.ToVector2();
|
||||
|
||||
CharacterTeamType team = hostile ? CharacterTeamType.None : CharacterTeamType.FriendlyNPC;
|
||||
missionNPCs.CreateHumansAtPosition(team, spawnPosition, OnCharacterCreated);
|
||||
|
||||
void OnCharacterCreated(Character character, XElement characterMissionConfig)
|
||||
{
|
||||
character.MLC().NPCElement = characterMissionConfig;
|
||||
if (character.AIController is not HumanAIController humanAI) return;
|
||||
|
||||
// Only turn on the mental state for non-hostile divers
|
||||
// so that sometimes you'll get insane divers that attack you
|
||||
if (!hostile) humanAI.InitMentalStateManager();
|
||||
|
||||
// Force the AI to wait in the location so if they get spooked by damage they
|
||||
// don't try to swim cross map to your sub to find saftey
|
||||
var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
|
||||
humanAI.SetForcedOrder(waitOrder);
|
||||
|
||||
int minMoney = characterMissionConfig.GetAttributeInt("minmoney", 0);
|
||||
int maxMoney = characterMissionConfig.GetAttributeInt("maxmoney", 0);
|
||||
|
||||
if (maxMoney > 0)
|
||||
{
|
||||
int money = Rand.Range(minMoney, maxMoney, Rand.RandSync.Unsynced);
|
||||
character.Wallet.Give(money);
|
||||
Log.InternalDebug($"Gave {money} to {character.Name}");
|
||||
}
|
||||
|
||||
if (characterMissionConfig.GetAttributeBool("allowordering", false))
|
||||
{
|
||||
#if CLIENT
|
||||
GameMain.GameSession.CrewManager.AddCharacterToCrewList(character);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitCharacters()
|
||||
{
|
||||
spawnPosition = missionNPCs[0].WorldPosition;
|
||||
}
|
||||
|
||||
public override void StartMissionSpecific(Level level)
|
||||
{
|
||||
if (missionNPCs.characters.Count > 0)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new Exception($"characters.Count > 0 ({missionNPCs.characters.Count})");
|
||||
#else
|
||||
DebugConsole.AddWarning("Character list was not empty at the start of a escort mission. The mission instance may not have been ended correctly on previous rounds.");
|
||||
missionNPCs.characters.Clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (localCharacterConfig == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
|
||||
return;
|
||||
}
|
||||
|
||||
// to ensure single missions run without issues, default to mainsub
|
||||
if (missionSub == null)
|
||||
{
|
||||
missionSub = Submarine.MainSub;
|
||||
CalculateReward();
|
||||
}
|
||||
|
||||
if (!IsClient)
|
||||
{
|
||||
InitEscort();
|
||||
InitCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
const float MINDIST = 2000f;
|
||||
bool triggered = false;
|
||||
|
||||
public override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient) return;
|
||||
// Exit if we're client or if we're already active or if all of the characters are dead
|
||||
|
||||
if (missionNPCs.characters.Any(c => !MissionNPCCollection.IsAlive(c)))
|
||||
{
|
||||
State = 1;
|
||||
}
|
||||
|
||||
if (triggered || missionNPCs.characters.Any(c => !MissionNPCCollection.IsAlive(c))) return;
|
||||
|
||||
foreach (var aiCharacter in missionNPCs.characters.Where(c => MissionNPCCollection.IsAlive(c)))
|
||||
{
|
||||
if (!triggered && ShouldActivate())
|
||||
{
|
||||
Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
bool ShouldActivate()
|
||||
{
|
||||
bool shouldActivate = GameSession.GetSessionCrewCharacters(CharacterType.Player).Any(c => MissionNPCCollection.Close(c, aiCharacter, MINDIST));
|
||||
return shouldActivate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the forced wait order from the AI
|
||||
/// and forces them into combat with the closest player
|
||||
/// if they're hostile
|
||||
/// </summary>
|
||||
private void Activate()
|
||||
{
|
||||
triggered = true;
|
||||
Log.Debug("Activated NPC");
|
||||
foreach (var aiCharacter in missionNPCs.characters)
|
||||
{
|
||||
if (aiCharacter.AIController is HumanAIController humanAI)
|
||||
{
|
||||
humanAI.ClearForcedOrder();
|
||||
if (hostile)
|
||||
{
|
||||
Character target = MissionNPCCollection.GetClosest(aiCharacter);
|
||||
if (target != null) humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Offensive, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
|
||||
{
|
||||
if (hostile)
|
||||
{
|
||||
return triggered;
|
||||
}
|
||||
return missionNPCs.characters.All(c => MissionNPCCollection.Survived(c));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
if (!IsClient)
|
||||
{
|
||||
missionNPCs.End(completed);
|
||||
}
|
||||
missionNPCs.Clear();
|
||||
base.EndMissionSpecific(completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
using Barotrauma;
|
||||
using MoreLevelContent.Shared.Generation;
|
||||
using MoreLevelContent.Shared;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using HarmonyLib;
|
||||
using System;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
using System.Collections.Generic;
|
||||
using Steamworks.Data;
|
||||
using Barotrauma.Networking;
|
||||
using static MoreLevelContent.Shared.Generation.MissionGenerationDirector;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
partial class DistressGhostshipMission : DistressMission
|
||||
{
|
||||
private readonly LocalizedString defaultSonarLabel;
|
||||
private readonly XElement localCharacterConfig;
|
||||
private readonly XElement submarineConfig;
|
||||
private readonly XElement decalConfig;
|
||||
private readonly XElement damageDevices;
|
||||
private readonly XElement removeItems;
|
||||
private readonly XElement tagDevices;
|
||||
private readonly MissionNPCCollection missionNPCs;
|
||||
private readonly bool AllowStealing;
|
||||
|
||||
private readonly TravelTarget travelTarget;
|
||||
private readonly bool reactorActive;
|
||||
private readonly Level.PositionType spawnPosition;
|
||||
|
||||
private Submarine ghostship;
|
||||
private LevelData levelData;
|
||||
private TrackingSonarMarker trackingSonarMarker;
|
||||
private StructureDamageTracker damageTracker;
|
||||
private int salvagedReward = 0;
|
||||
|
||||
enum TravelTarget
|
||||
{
|
||||
Start,
|
||||
Maintain,
|
||||
End
|
||||
}
|
||||
|
||||
public DistressGhostshipMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
// Config
|
||||
submarineConfig = prefab.ConfigElement.GetChildElement("submarines");
|
||||
localCharacterConfig = prefab.ConfigElement.GetChildElement("characters");
|
||||
removeItems = prefab.ConfigElement.GetChildElement("removeitems");
|
||||
decalConfig = prefab.ConfigElement.GetChildElement("decals");
|
||||
damageDevices = prefab.ConfigElement.GetChildElement("damageDevices");
|
||||
tagDevices = prefab.ConfigElement.GetChildElement("tagDevices");
|
||||
|
||||
// Top level attributes
|
||||
travelTarget = submarineConfig.GetAttributeEnum("TravelTarget", TravelTarget.Maintain);
|
||||
spawnPosition = submarineConfig.GetAttributeEnum("SpawnPosition", Level.PositionType.MainPath);
|
||||
reactorActive = submarineConfig.GetAttributeBool("ReactorActive", true);
|
||||
AllowStealing = submarineConfig.GetAttributeBool("AllowStealing", true);
|
||||
|
||||
// General
|
||||
defaultSonarLabel = TextManager.Get("missionname.distressmission");
|
||||
missionNPCs = new(this, localCharacterConfig);
|
||||
|
||||
// for campaign missions, set level at construction
|
||||
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
|
||||
if (levelData != null)
|
||||
{
|
||||
SetLevel(levelData);
|
||||
}
|
||||
}
|
||||
|
||||
public override int Reward
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_SubWasSalvaged) return salvagedReward;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _SubWasSalvaged = false;
|
||||
private bool SubSalvaged => ghostship.AtEndExit || ghostship.AtStartExit;
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ghostship == null) yield break;
|
||||
yield return trackingSonarMarker.CurrentPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLevel(LevelData level)
|
||||
{
|
||||
if (levelData != null)
|
||||
{
|
||||
//level already set
|
||||
return;
|
||||
}
|
||||
|
||||
levelData = level;
|
||||
List<(float, ContentPath)> submarines = new List<(float, ContentPath)>();
|
||||
foreach (var sub in submarineConfig.GetChildElements("sub"))
|
||||
{
|
||||
ContentPath path = sub.GetAttributeContentPath("path", Prefab.ContentPackage);
|
||||
int commenness = sub.GetAttributeInt("commonness", 0);
|
||||
submarines.Add((commenness, path));
|
||||
}
|
||||
|
||||
ContentPath subPath = ToolBox.SelectWeightedRandom(submarines, s => s.Item1, Rand.RandSync.ServerAndClient).Item2;
|
||||
|
||||
if (subPath.IsNullOrEmpty())
|
||||
{
|
||||
Log.Error($"No path used for submarine for the shuttle rescue mission \"{Prefab.Identifier}\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
|
||||
if (file == null)
|
||||
{
|
||||
Log.Error($"Failed to find submarine at path {subPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
|
||||
{
|
||||
File = file,
|
||||
Callback = OnSubCreated,
|
||||
SpawnPosition = SubSpawnPosition.Path,
|
||||
AutoFill = true,
|
||||
Prefix = MissionGenerationDirector.SubmarineSpawnRequest.AutoFillPrefix.Abandoned,
|
||||
AllowStealing = AllowStealing,
|
||||
SkipItemChance = 0.75f
|
||||
});
|
||||
}
|
||||
|
||||
void OnSubCreated(Submarine submarine)
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
ghostship = submarine;
|
||||
ghostship.FlipX();
|
||||
submarine.ShowSonarMarker = false;
|
||||
submarine.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
ghostship.Info.Type = (SubmarineType)7;
|
||||
submarine.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic;
|
||||
|
||||
SubPlacementUtils.PositionSubmarine(submarine, Level.PositionType.MainPath);
|
||||
SubPlacementUtils.SetCrushDepth(submarine);
|
||||
|
||||
salvagedReward = (int)Math.Round(ghostship.Info.Price * 0.85f);
|
||||
|
||||
double minFlood = submarineConfig.GetAttributeDouble("minfloodpercentage", 0);
|
||||
double maxFlood = submarineConfig.GetAttributeDouble("maxfloodpercentage", 0);
|
||||
string[] floodHulls = submarineConfig.GetAttributeStringArray("floodtargets", new string[] { }, convertToLowerInvariant: true);
|
||||
if (floodHulls.Length > 0 && minFlood >= 0 && maxFlood > 0)
|
||||
{
|
||||
List<Hull> validHulls = ghostship.GetHulls(false).Where(h => floodHulls.Contains(h.RoomName.ToLowerInvariant())).ToList();
|
||||
foreach (var target in validHulls)
|
||||
{
|
||||
target.WaterVolume = (float)(target.Volume * ((rand.NextDouble() * (maxFlood - minFlood)) + minFlood));
|
||||
Log.Debug($"Flooded hull {target.RoomName}");
|
||||
}
|
||||
} else
|
||||
{
|
||||
Log.Debug("No hulls to flood");
|
||||
}
|
||||
|
||||
// tag all sub waypoints
|
||||
submarine.TagSubmarineWaypoints("distress_ghostship");
|
||||
|
||||
if (tagDevices != null)
|
||||
{
|
||||
foreach (var item in tagDevices.Elements())
|
||||
{
|
||||
Identifier targetItem = item.GetAttributeIdentifier("identifier", null);
|
||||
Identifier tag = item.GetAttributeIdentifier("tag", null);
|
||||
var items = submarine.GetItems(false).Where(i => i.Prefab.Identifier == targetItem);
|
||||
items.ForEach((i) =>
|
||||
{
|
||||
i.AddTag(tag);
|
||||
Log.Debug(i.Tags);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Init tracking sonar marker
|
||||
trackingSonarMarker = new TrackingSonarMarker(30, submarine, Prefab.SonarLabel.IsNullOrEmpty() ? defaultSonarLabel : Prefab.SonarLabel);
|
||||
}
|
||||
|
||||
private void SpawnCharacters()
|
||||
{
|
||||
missionNPCs.CreateHumansInSubmarine(ghostship, onCharacterCreated: (character, config) =>
|
||||
{
|
||||
if (character.AIController is not HumanAIController humanAI) return;
|
||||
|
||||
bool alive = config.GetAttributeBool("alive", true);
|
||||
if (!alive)
|
||||
{
|
||||
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
humanAI.InitMentalStateManager();
|
||||
}
|
||||
|
||||
int minMoney = config.GetAttributeInt("minmoney", 0);
|
||||
int maxMoney = config.GetAttributeInt("maxmoney", 0);
|
||||
|
||||
if (maxMoney > 0)
|
||||
{
|
||||
int money = Rand.Range(minMoney, maxMoney, Rand.RandSync.Unsynced);
|
||||
character.Wallet.Give(money);
|
||||
Log.InternalDebug($"Gave {money} to {character.Name}");
|
||||
}
|
||||
|
||||
foreach (var affliction in config.GetChildElements("affliction"))
|
||||
{
|
||||
string identifier = affliction.GetAttributeString("identifier", null);
|
||||
LimbType limb = affliction.GetAttributeEnum("limb", LimbType.None);
|
||||
float strength = affliction.GetAttributeFloat("strength", 1);
|
||||
bool targetRandomLimb = affliction.GetAttributeBool("randomLimb", false);
|
||||
bool randomStrength = affliction.GetAttributeBool("randomStrength", false);
|
||||
int count = affliction.GetAttributeInt("count", 1);
|
||||
|
||||
if (AfflictionHelper.TryGetAffliction(identifier, out AfflictionPrefab prefab))
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Limb targetLimb = character.AnimController.MainLimb;
|
||||
if (limb != LimbType.None) targetLimb = character.AnimController.GetLimb(limb);
|
||||
if (targetRandomLimb) targetLimb = character.AnimController.Limbs.GetRandomUnsynced();
|
||||
if (randomStrength) strength = Rand.Range(10f, 70f, Rand.RandSync.Unsynced);
|
||||
character.CharacterHealth.ApplyAffliction(targetLimb, new Affliction(prefab, strength));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Unable to get affliction with identifier {identifier}");
|
||||
}
|
||||
}
|
||||
character.CharacterHealth.ForceUpdateVisuals();
|
||||
});
|
||||
}
|
||||
|
||||
private void SpawnDecals()
|
||||
{
|
||||
if (decalConfig == null) return;
|
||||
foreach (XElement item in decalConfig.GetChildElements("item"))
|
||||
{
|
||||
string prefab = item.GetAttributeString("prefab", "");
|
||||
string preferedHullName = item.GetAttributeString("preferedhull", "");
|
||||
int count = item.GetAttributeInt("count", 0);
|
||||
if (count == 0 || string.IsNullOrWhiteSpace(prefab)) continue;
|
||||
|
||||
PlaceDecals(prefab, preferedHullName, count);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlaceDecals(string decalName, string preferedHull, int count)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool hasPreferedHull = !string.IsNullOrWhiteSpace(preferedHull);
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
List<Hull> filteredHulls = ghostship.GetHulls(false).Where(h => !h.RoomName.Contains("ballast") && !h.RoomName.Contains("airlock") && !h.IsWetRoom).ToList();
|
||||
var preferedHulls = filteredHulls.Where(h => h.RoomName.ToLowerInvariant() == preferedHull.ToLowerInvariant());
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Hull hull = filteredHulls.ToList().GetRandom(rand);
|
||||
if (hasPreferedHull && preferedHull.Any())
|
||||
{
|
||||
hull = preferedHulls.ToList().GetRandom(rand);
|
||||
Log.Debug($"Set prefered hull, roomname {hull.RoomName}");
|
||||
}
|
||||
|
||||
Vector2 pos = new Vector2(hull.WorldPosition.X + rand.Next(-hull.RectWidth / 2, hull.RectWidth / 2), hull.WorldPosition.Y + rand.Next(-hull.RectHeight / 2, hull.RectHeight / 2));
|
||||
Decal decal = hull.AddDecal(decalName, pos, 1.0f, false);
|
||||
}
|
||||
} catch(Exception e)
|
||||
{
|
||||
Log.Error(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void DamageDevices()
|
||||
{
|
||||
if (damageDevices == null) return;
|
||||
foreach (XElement device in damageDevices.GetChildElements("item"))
|
||||
{
|
||||
Identifier tag = device.GetAttributeIdentifier("tag", "");
|
||||
int condition = device.GetAttributeInt("condition", 0);
|
||||
int amount = device.GetAttributeInt("amount", 1);
|
||||
bool all = device.GetAttributeBool("all", false);
|
||||
if (tag.IsEmpty) continue;
|
||||
DamageDevice(tag, condition, amount, all);
|
||||
}
|
||||
}
|
||||
|
||||
private void DamageDevice(Identifier tag, int condition, int amount, bool all)
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
var validItems = ghostship.GetItems(false).Where(i => i.IsPlayerTeamInteractable && i.HasTag(tag)).ToList();
|
||||
if (!validItems.Any()) return;
|
||||
|
||||
if (all)
|
||||
{
|
||||
validItems.ForEach(i => Damage(i));
|
||||
return;
|
||||
}
|
||||
|
||||
while(amount > 0 && validItems.Any())
|
||||
{
|
||||
amount--;
|
||||
Item target = validItems.GetRandom(rand);
|
||||
_ = validItems.Remove(target);
|
||||
Damage(target);
|
||||
}
|
||||
|
||||
void Damage(Item item)
|
||||
{
|
||||
if (item.GetComponent<Repairable>() is Repairable repairable)
|
||||
{
|
||||
item.Condition = condition;
|
||||
}
|
||||
Log.Debug("Damaged Device");
|
||||
}
|
||||
}
|
||||
|
||||
public override void StartMissionSpecific(Level level)
|
||||
{
|
||||
if (!IsClient)
|
||||
{
|
||||
StartServer();
|
||||
InitShip();
|
||||
SpawnCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
const float MAX_REP_LOSS = 20f;
|
||||
float _LostRep = 0;
|
||||
void StartServer()
|
||||
{
|
||||
// Reputation stuff
|
||||
damageTracker = new StructureDamageTracker(ghostship, 2.0f, 1f, 2f);
|
||||
damageTracker.DamageAfterThreshold += DamageTracker_DamageAfterThreshold;
|
||||
damageTracker.ThresholdCrossed += DamageTracker_ThresholdCrossed;
|
||||
_LostRep = 0;
|
||||
}
|
||||
|
||||
private void DamageTracker_ThresholdCrossed()
|
||||
{
|
||||
#if SERVER
|
||||
GameMain.Server?.SendChatMessage(TextManager.GetServerMessage("distress.ghostship.damagenotification")?.Value, ChatMessageType.Default);
|
||||
#endif
|
||||
#if CLIENT
|
||||
if (GameMain.IsSingleplayer)
|
||||
{
|
||||
GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(
|
||||
TextManager.Get("mlc.info1")?.Value, TextManager.Get("distress.ghostship.damagenotification")?.Value,
|
||||
ChatMessageType.MessageBox, null);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void DamageTracker_DamageAfterThreshold(float amount)
|
||||
{
|
||||
if (_LostRep >= MAX_REP_LOSS) return;
|
||||
var reputationLoss = amount * Reputation.ReputationLossPerWallDamage;
|
||||
reputationLoss = Math.Min(reputationLoss, 10); // clamp rep loss to a value 0-10
|
||||
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
|
||||
_LostRep += reputationLoss;
|
||||
}
|
||||
|
||||
void InitShip()
|
||||
{
|
||||
ghostship.NeutralizeBallast();
|
||||
var ghostshipItems = ghostship.GetItems(alsoFromConnectedSubs: false);
|
||||
|
||||
if (ghostshipItems.Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
|
||||
{
|
||||
Item reactorItem = reactor.Item;
|
||||
ItemContainer container = reactorItem.GetComponent<ItemContainer>();
|
||||
if (reactorActive)
|
||||
{
|
||||
reactor.PowerUpImmediately();
|
||||
reactor.FuelConsumptionRate = 0;
|
||||
}
|
||||
|
||||
// ItemPrefab rod = ItemPrefab.Find(null, "fuelrod".ToIdentifier());
|
||||
|
||||
|
||||
// Make sure the reactor doesn't explode or irradiate the bots
|
||||
if (CompatabilityHelper.Instance.ReactorModInstalled) CompatabilityHelper.SetupHazReactor(reactor);
|
||||
Repairable repairable = reactor.Item.GetComponent<Repairable>();
|
||||
if (repairable != null)
|
||||
{
|
||||
repairable.DeteriorationSpeed = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure shit doesn't break by itself
|
||||
ghostshipItems.FindAll(i => i.HasTag("junctionbox") || i.HasTag("oxygengenerator")).ForEach(i =>
|
||||
{
|
||||
if (i.GetComponent<Repairable>() is Repairable repairable)
|
||||
{
|
||||
repairable.DeteriorationSpeed = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Item sonarItem = Item.ItemList.Find(it => it.Submarine == ghostship && it.GetComponent<Sonar>() != null);
|
||||
if (sonarItem == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"No sonar found in the beacon station \"{ghostship.Info.Name}\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
var steering = sonarItem.GetComponent<Steering>();
|
||||
steering.AutoPilot = true;
|
||||
switch (travelTarget)
|
||||
{
|
||||
case TravelTarget.Start:
|
||||
steering.SetDestinationLevelStart();
|
||||
break;
|
||||
case TravelTarget.Maintain:
|
||||
steering.MaintainPos = true;
|
||||
steering.PosToMaintain = ghostship.WorldPosition;
|
||||
break;
|
||||
case TravelTarget.End:
|
||||
steering.SetDestinationLevelEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readonly float detectDist = Sonar.DefaultSonarRange;
|
||||
private bool finalSubSetup = false;
|
||||
const float FINAL_SETUP_DELAY = 5;
|
||||
float setupDelay = FINAL_SETUP_DELAY;
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
public override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (ghostship == null) return;
|
||||
if (!finalSubSetup && !IsClient && setupDelay <= 0)
|
||||
{
|
||||
SpawnDecals();
|
||||
DamageDevices();
|
||||
if (removeItems != null)
|
||||
{
|
||||
foreach (XElement item in removeItems.Elements())
|
||||
{
|
||||
Identifier tagToRemove = item.GetAttributeIdentifier("tag", null);
|
||||
if (tagToRemove != null)
|
||||
{
|
||||
foreach (var itemToRemove in ghostship.GetItems(false).FindAll(i => i.HasTag(tagToRemove)))
|
||||
{
|
||||
Entity.Spawner.AddItemToRemoveQueue(itemToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.InternalDebug("Preformed final sub setup");
|
||||
finalSubSetup = true;
|
||||
}
|
||||
if (setupDelay > 0)
|
||||
{
|
||||
setupDelay -= deltaTime;
|
||||
}
|
||||
trackingSonarMarker.Update(deltaTime);
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
bool crewMemberInSub = CrewInSub();
|
||||
switch (State)
|
||||
{
|
||||
case 0:
|
||||
float dist = Vector2.DistanceSquared(ghostship.WorldPosition, Submarine.MainSub.WorldPosition);
|
||||
if (dist < detectDist * detectDist || crewMemberInSub)
|
||||
{
|
||||
State = 1;
|
||||
Log.InternalDebug("State -> 1");
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (crewMemberInSub)
|
||||
{
|
||||
State = 2;
|
||||
Log.InternalDebug("State -> 2");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (SubSalvaged) _SubWasSalvaged = true;
|
||||
|
||||
if (IsClient) return;
|
||||
damageTracker.Update();
|
||||
|
||||
bool CrewInSub()
|
||||
{
|
||||
foreach (var crewMember in GameSession.GetSessionCrewCharacters(CharacterType.Player))
|
||||
{
|
||||
if (crewMember.Submarine == ghostship)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => State == 2;
|
||||
|
||||
public override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
base.EndMissionSpecific(completed);
|
||||
missionNPCs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Barotrauma;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
abstract partial class DistressMission : Mission
|
||||
{
|
||||
protected bool DisplayReward;
|
||||
public DistressMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) => DisplayReward = prefab.ConfigElement.GetAttributeBool("displayreward", false);
|
||||
|
||||
public override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
failed = !completed;
|
||||
if (completed || Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit)
|
||||
{
|
||||
if (level?.LevelData != null && Prefab.Tags.Contains("distress"))
|
||||
{
|
||||
level.LevelData.MLC().HasDistress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using HarmonyLib;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared;
|
||||
using MoreLevelContent.Shared.Generation;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using static HarmonyLib.Code;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
partial class DistressSubmarineMission : DistressMission
|
||||
{
|
||||
private class MonsterSet
|
||||
{
|
||||
public readonly HashSet<(CharacterPrefab character, Point amountRange)> MonsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>();
|
||||
public float Commonness;
|
||||
|
||||
public MonsterSet(XElement element) => Commonness = element.GetAttributeFloat("commonness", 100.0f);
|
||||
}
|
||||
|
||||
private readonly XElement localCharacterConfig;
|
||||
private readonly List<MonsterSet> monsterSets = new List<MonsterSet>();
|
||||
private readonly XElement submarineTypeConfig;
|
||||
private readonly LocalizedString sonarLabel;
|
||||
private readonly LocalizedString successCrew;
|
||||
private readonly LocalizedString successSub;
|
||||
private readonly LocalizedString revealedFailure;
|
||||
private readonly MissionNPCCollection missionNPCs;
|
||||
|
||||
private readonly Dictionary<Character, int> rewardLookup = new();
|
||||
|
||||
private Submarine lostSubmarine;
|
||||
private LevelData levelData;
|
||||
private bool swarmSpawned;
|
||||
private bool outsideOfSonarRange;
|
||||
private bool playerSubClose;
|
||||
private TrackingSonarMarker trackingSonarMarker;
|
||||
|
||||
private bool SubSalvaged => lostSubmarine.AtEndExit || lostSubmarine.AtStartExit;
|
||||
private bool CrewResuced => missionNPCs.AnyHumanSurvived;
|
||||
|
||||
public DistressSubmarineMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
// Setup submarine
|
||||
localCharacterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
submarineTypeConfig = prefab.ConfigElement.GetChildElement("Submarine");
|
||||
|
||||
// Setup text
|
||||
sonarLabel = TextManager.Get("missionname.distressmission");
|
||||
successCrew = TextManager.Get(prefab.ConfigElement.GetAttributeString("crewsurviveidentifier", "distress.submarinesuccess.crew"));
|
||||
successSub = TextManager.Get(prefab.ConfigElement.GetAttributeString("subsalvagedidentifer", "distress.submarinesuccess.sub"));
|
||||
revealedFailure = TextManager.Get(prefab.ConfigElement.GetAttributeString("revealedFailureidentifer", "distress.submarinefail"));
|
||||
|
||||
// Setup monsters, copied from beacon station code
|
||||
swarmSpawned = false;
|
||||
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
|
||||
{
|
||||
if (!monsterSets.Any())
|
||||
{
|
||||
monsterSets.Add(new MonsterSet(monsterElement));
|
||||
}
|
||||
LoadMonsters(monsterElement, monsterSets[0]);
|
||||
}
|
||||
foreach (var monsterSetElement in prefab.ConfigElement.GetChildElements("monsters"))
|
||||
{
|
||||
monsterSets.Add(new MonsterSet(monsterSetElement));
|
||||
foreach (var monsterElement in monsterSetElement.GetChildElements("monster"))
|
||||
{
|
||||
LoadMonsters(monsterElement, monsterSets.Last());
|
||||
}
|
||||
}
|
||||
|
||||
missionNPCs = new(this, localCharacterConfig);
|
||||
|
||||
// for campaign missions, set level at construction
|
||||
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
|
||||
if (levelData != null)
|
||||
{
|
||||
SetLevel(levelData);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow override the default sonar label with a sonarlabel="" attribute
|
||||
// public override LocalizedString SonarLabel => base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
|
||||
|
||||
// Display a different failure message if the shuttle was located
|
||||
public override LocalizedString FailureMessage => state > 0 ? revealedFailure : base.FailureMessage;
|
||||
|
||||
// Display a different success message if the crew is dead or alive at the end of the level
|
||||
public override LocalizedString SuccessMessage => state < 2 ? FormatReward(successCrew) : FormatReward(successSub);
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (lostSubmarine == null) yield break;
|
||||
if (outsideOfSonarRange)
|
||||
{
|
||||
if (trackingSonarMarker == null) yield break;
|
||||
yield return trackingSonarMarker.CurrentPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Reward
|
||||
|
||||
LocalizedString FormatReward(LocalizedString input)
|
||||
{
|
||||
string msg = input.Value;
|
||||
msg = msg.Replace("[reward]", GetReward(null).ToString());
|
||||
return msg;
|
||||
}
|
||||
|
||||
public override float GetBaseReward(Submarine sub) => Completed ? GetRewardCompleted() : GetRewardInLevel();
|
||||
|
||||
int survivingCrewPayout = 0;
|
||||
|
||||
void CalculateSurvivingPayout(out int payout)
|
||||
{
|
||||
payout = 0;
|
||||
foreach (var character in missionNPCs.characters)
|
||||
{
|
||||
if (MissionNPCCollection.Survived(character))
|
||||
{
|
||||
payout += rewardLookup[character];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int GetRewardCompleted()
|
||||
{
|
||||
int reward = survivingCrewPayout;
|
||||
|
||||
if (SubSalvaged)
|
||||
{
|
||||
reward += Prefab.Reward;
|
||||
Log.Debug("Sub salvaged");
|
||||
}
|
||||
Log.Debug($"Get reward completed: {reward}");
|
||||
return reward;
|
||||
}
|
||||
|
||||
int GetRewardInLevel()
|
||||
{
|
||||
int reward = Prefab.Reward;
|
||||
if (missionNPCs?.characters?.Count > 0)
|
||||
{
|
||||
foreach (var character in missionNPCs.characters)
|
||||
{
|
||||
// Add payout for each living character
|
||||
if (MissionNPCCollection.IsAlive(character))
|
||||
{
|
||||
reward += rewardLookup[character];
|
||||
Log.Debug($"Added {rewardLookup[character]} to reward");
|
||||
}
|
||||
else Log.Debug("Character is dead");
|
||||
}
|
||||
}
|
||||
else Log.Debug("No Characters");
|
||||
|
||||
Log.Debug($"Reward: {reward}");
|
||||
return reward;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void LoadMonsters(XElement monsterElement, MonsterSet set)
|
||||
{
|
||||
Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty);
|
||||
int defaultCount = monsterElement.GetAttributeInt("count", -1);
|
||||
if (defaultCount < 0)
|
||||
{
|
||||
defaultCount = monsterElement.GetAttributeInt("amount", 1);
|
||||
}
|
||||
int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255);
|
||||
int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255);
|
||||
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (characterPrefab != null)
|
||||
{
|
||||
_ = set.MonsterPrefabs.Add((characterPrefab, new Point(min, max)));
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in distress submarine mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLevel(LevelData level)
|
||||
{
|
||||
if (levelData != null)
|
||||
{
|
||||
//level already set
|
||||
return;
|
||||
}
|
||||
|
||||
levelData = level;
|
||||
ContentPath subPath = submarineTypeConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
|
||||
if (subPath.IsNullOrEmpty())
|
||||
{
|
||||
Log.Error($"No path used for submarine for the shuttle rescue mission \"{Prefab.Identifier}\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
|
||||
if (file == null)
|
||||
{
|
||||
Log.Error($"Failed to find submarine at path {subPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
|
||||
{
|
||||
File = file,
|
||||
Callback = OnSubCreated,
|
||||
AllowStealing = true,
|
||||
AutoFill = true,
|
||||
IgnoreCrushDpeth = true,
|
||||
Prefix = MissionGenerationDirector.SubmarineSpawnRequest.AutoFillPrefix.Abandoned,
|
||||
SpawnPosition = MissionGenerationDirector.SubSpawnPosition.Path,
|
||||
SkipItemChance = 0.5f
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void OnSubCreated(Submarine submarine)
|
||||
{
|
||||
if (submarine == null)
|
||||
{
|
||||
State = -1;
|
||||
DebugConsole.ThrowError("Distress submarine failed to spawn! Mission will now fail.");
|
||||
return;
|
||||
}
|
||||
lostSubmarine = submarine;
|
||||
lostSubmarine.Info.Type = SubmarineType.Player;
|
||||
lostSubmarine.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
lostSubmarine.ShowSonarMarker = false;
|
||||
lostSubmarine.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic;
|
||||
|
||||
SubPlacementUtils.PositionSubmarine(lostSubmarine, Level.PositionType.SidePath | Level.PositionType.MainPath);
|
||||
|
||||
//make the shuttle resist at least it's spawn position + 1000m
|
||||
SubPlacementUtils.SetCrushDepth(lostSubmarine);
|
||||
|
||||
// tag all sub waypoints
|
||||
lostSubmarine.TagSubmarineWaypoints("distress_shuttle");
|
||||
|
||||
// Init sonar tracking
|
||||
trackingSonarMarker = new TrackingSonarMarker(30, lostSubmarine, Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel);
|
||||
// TriggerEvents(0);
|
||||
}
|
||||
|
||||
public override void StartMissionSpecific(Level level)
|
||||
{
|
||||
if (lostSubmarine == null) return;
|
||||
if (!IsClient) StartServer();
|
||||
}
|
||||
|
||||
void StartServer()
|
||||
{
|
||||
SinkSub();
|
||||
lostSubmarine.EnableMaintainPosition();
|
||||
|
||||
Item sonarItem = Item.ItemList.Find(it => it.Submarine == lostSubmarine && it.GetComponent<Sonar>() != null);
|
||||
if (sonarItem != null)
|
||||
{
|
||||
// Always allow the lost sub sonar to run so it attracts monsters :)
|
||||
Powered sonarPower = sonarItem.GetComponent<Powered>();
|
||||
sonarPower.MinVoltage = 0;
|
||||
|
||||
var sonar = sonarItem.GetComponent<Sonar>();
|
||||
var steering = sonarItem.GetComponent<Steering>();
|
||||
sonar.CurrentMode = Sonar.Mode.Active;
|
||||
// Notify clients of the sonar's state
|
||||
#if SERVER
|
||||
sonar.Item.CreateServerEvent(sonar);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
bool givenCharge = false;
|
||||
// Drain all of the batteries on the shuttle
|
||||
foreach (var item in lostSubmarine.GetItems(alsoFromConnectedSubs: false).Where(i => i.HasTag("battery") && !i.NonInteractable))
|
||||
{
|
||||
if (item.GetComponent<PowerContainer>() is PowerContainer powerContainer)
|
||||
{
|
||||
// Allow fast recharging
|
||||
powerContainer.MaxRechargeSpeed = powerContainer.Capacity;
|
||||
|
||||
// Drain batteries, give them a little bit of power
|
||||
powerContainer.Charge = givenCharge ? 0 : 10;
|
||||
givenCharge = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Drain any rods that are inside a reactor, if the shuttle has one
|
||||
|
||||
// Init NPCS
|
||||
missionNPCs.CreateHumansInSubmarine(lostSubmarine, onCharacterCreated: (character, config) =>
|
||||
{
|
||||
int payout = config.GetAttributeInt("payout", 0);
|
||||
rewardLookup.Add(character, payout);
|
||||
character.Info.Title = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", payout));
|
||||
|
||||
((HumanAIController)character.AIController).InitMentalStateManager();
|
||||
|
||||
if (config.GetAttributeBool("isCaptain", false))
|
||||
{
|
||||
// Set shuttle captain orders
|
||||
var fightIntruders = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
|
||||
var repairBrokenDevices = OrderPrefab.Prefabs["repairsystems"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
|
||||
var fixLeaks = OrderPrefab.Prefabs["fixleaks"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
|
||||
|
||||
character.SetOrder(fixLeaks, true, false);
|
||||
character.SetOrder(fightIntruders, true, false);
|
||||
character.SetOrder(repairBrokenDevices, true, false);
|
||||
|
||||
Log.InternalDebug("Updated captain orders");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
void SinkSub()
|
||||
{
|
||||
HashSet<Hull> ballastHulls = new HashSet<Hull>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != lostSubmarine) { continue; }
|
||||
var pump = item.GetComponent<Pump>();
|
||||
if (pump == null || item.CurrentHull == null) { continue; }
|
||||
if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
pump.FlowPercentage = 0.0f;
|
||||
_ = ballastHulls.Add(item.CurrentHull);
|
||||
}
|
||||
|
||||
foreach (Hull hull in ballastHulls)
|
||||
{
|
||||
hull.WaterVolume = hull.Volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// state 0 = init
|
||||
// state 1 = crew alive, escort to end
|
||||
// state 2 = crew dead, escort sub to end
|
||||
readonly float spawnDist = Sonar.DefaultSonarRange * 2;
|
||||
private bool _salvedState = false;
|
||||
private bool _migrate = false;
|
||||
public override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (State == -1 || lostSubmarine == null) return;
|
||||
UpdateLastPing(deltaTime);
|
||||
#if CLIENT
|
||||
if (SubSalvaged && _salvedState != SubSalvaged)
|
||||
{
|
||||
CoroutineManager.StartCoroutine(_showMessageBox(TextManager.Get("missionheader1.distress_shiprescue"), TextManager.Get("distress.lostshuttle.atend")));
|
||||
_salvedState = SubSalvaged;
|
||||
}
|
||||
IEnumerable<CoroutineStatus> _showMessageBox(LocalizedString header, LocalizedString message)
|
||||
{
|
||||
while (GUIMessageBox.VisibleBox?.UserData is RoundSummary)
|
||||
{
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
}
|
||||
CreateMessageBox(header, message);
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
#endif
|
||||
if (IsClient) return;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
// init
|
||||
case 0:
|
||||
float dist = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition);
|
||||
if (dist > spawnDist * spawnDist) return;
|
||||
if (!swarmSpawned) SpawnSwarm();
|
||||
if (playerSubClose && swarmSpawned)
|
||||
{
|
||||
State = missionNPCs.AnyHumanAlive ? 1 : 2;
|
||||
}
|
||||
break;
|
||||
|
||||
// crew alive
|
||||
case 1:
|
||||
if (!missionNPCs.AnyHumanAlive)
|
||||
{
|
||||
State = 2;
|
||||
}
|
||||
break;
|
||||
|
||||
// crew dead
|
||||
case 2:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
readonly float sonarClose = Sonar.DefaultSonarRange / 0.8f;
|
||||
void UpdateLastPing(float deltaTime)
|
||||
{
|
||||
if (Submarine.MainSub == null)
|
||||
{
|
||||
Log.Warn($"Skipped updating last ping as main sub was null");
|
||||
return;
|
||||
}
|
||||
outsideOfSonarRange = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition) > Sonar.DefaultSonarRange * Sonar.DefaultSonarRange;
|
||||
playerSubClose = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition) < sonarClose * sonarClose;
|
||||
|
||||
trackingSonarMarker.Update(deltaTime);
|
||||
}
|
||||
|
||||
void SpawnSwarm()
|
||||
{
|
||||
swarmSpawned = true;
|
||||
// Find spawn position for the monsters
|
||||
|
||||
if (monsterSets.Count == 0) return;
|
||||
Vector2 spawnPos = lostSubmarine.WorldPosition;
|
||||
spawnPos.Y += lostSubmarine.GetDockedBorders().Height * 1.5f;
|
||||
var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced);
|
||||
foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs)
|
||||
{
|
||||
int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1);
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
_ = CoroutineManager.Invoke(() =>
|
||||
{
|
||||
//round ended before the coroutine finished
|
||||
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
|
||||
Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos, (Character character) =>
|
||||
{
|
||||
if (character.AIController is EnemyAIController controller)
|
||||
{
|
||||
AITarget target = missionNPCs.characters.GetRandomUnsynced().AiTarget;
|
||||
if (target != null) controller.SelectTarget(target);
|
||||
}
|
||||
});
|
||||
}, Rand.Range(0f, amount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
CalculateSurvivingPayout(out survivingCrewPayout);
|
||||
return SubSalvaged || CrewResuced;
|
||||
}
|
||||
|
||||
public override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
if (!IsClient) missionNPCs.End(completed);
|
||||
missionNPCs.Clear();
|
||||
|
||||
base.EndMissionSpecific(completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Barotrauma;
|
||||
using System;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
partial class LostCargoMission
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.MoreLevelContent.Shared.Utils;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
partial class MissionNPCCollection
|
||||
{
|
||||
internal readonly List<Character> characters = new();
|
||||
internal readonly Dictionary<Character, List<Item>> characterItems = new();
|
||||
|
||||
internal bool AllHumansAlive => characters.All(c => IsAlive(c));
|
||||
internal bool AnyHumanAlive => characters.Any(c => IsAlive(c));
|
||||
internal bool AnyHumanSurvived => characters.Any(c => Survived(c));
|
||||
internal int AliveHumans => characters.Where(c => IsAlive(c)).Count();
|
||||
internal delegate void OnCharacterCreated(Character character, XElement missionCharacterConfig);
|
||||
internal Character this[int index] => characters[index];
|
||||
|
||||
internal MissionNPCCollection(Mission mission, XElement characterConfig)
|
||||
{
|
||||
this.mission = mission;
|
||||
this.characterConfig = characterConfig;
|
||||
}
|
||||
private readonly Mission mission;
|
||||
private readonly XElement characterConfig;
|
||||
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
}
|
||||
|
||||
public void End(bool completed)
|
||||
{
|
||||
foreach (Character character in characters)
|
||||
{
|
||||
if (character.Inventory == null) { continue; }
|
||||
foreach (Item item in character.Inventory.AllItemsMod)
|
||||
{
|
||||
//item didn't spawn with the characters -> drop it
|
||||
if (!characterItems.Any(c => c.Value.Contains(item)))
|
||||
{
|
||||
item.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// characters that survived will take their items with them, in case players tried to be crafty and steal them
|
||||
// this needs to run here in case players abort the mission by going back home
|
||||
foreach (var characterItem in characterItems)
|
||||
{
|
||||
if (Survived(characterItem.Key) || !completed)
|
||||
{
|
||||
foreach (Item item in characterItem.Value)
|
||||
{
|
||||
if (!item.Removed)
|
||||
{
|
||||
item.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal void CreateHumansInSubmarine(Submarine submarine, CharacterTeamType team = CharacterTeamType.FriendlyNPC, OnCharacterCreated onCharacterCreated = null)
|
||||
{
|
||||
if (characterConfig == null)
|
||||
{
|
||||
Log.Warn("No characters");
|
||||
return;
|
||||
}
|
||||
WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, submarine);
|
||||
Rand.RandSync randSync = Rand.RandSync.Unsynced;
|
||||
List<(HumanPrefab prefab, XElement config)> humanPrefabsToSpawn = new List<(HumanPrefab, XElement)>();
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
var humanPrefab = GetHumanPrefabFromElement(element);
|
||||
humanPrefabsToSpawn.Add((humanPrefab, element));
|
||||
}
|
||||
foreach (var (prefab, config) in humanPrefabsToSpawn)
|
||||
{
|
||||
var humanPrefab = prefab;
|
||||
XElement characterSpecificConfig = config;
|
||||
if (humanPrefab == null || humanPrefab.Job.IsEmpty || humanPrefab.Job == "any") { continue; }
|
||||
var jobPrefab = humanPrefab.GetJobPrefab(randSync);
|
||||
var stayPos = explicitStayInHullPos;
|
||||
if (jobPrefab != null)
|
||||
{
|
||||
stayPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, submarine) ?? explicitStayInHullPos;
|
||||
}
|
||||
XElement additionalItemsElement = config?.GetChildElement("additionalitems");
|
||||
ContentXElement additionalItems = new ContentXElement(null, additionalItemsElement);
|
||||
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, team, stayPos, additionalItems: additionalItemsElement != null ? additionalItems.Elements() : null);
|
||||
spawnedCharacter.EnableDespawn = false; // don't let mission npcs despawn
|
||||
spawnedCharacter.GiveIdCardTags(stayPos, false);
|
||||
onCharacterCreated?.Invoke(spawnedCharacter, characterSpecificConfig);
|
||||
spawnedCharacter.MLC().NPCElement = characterSpecificConfig;
|
||||
#if CLIENT
|
||||
if (GameMain.IsSingleplayer)
|
||||
{
|
||||
if (characterSpecificConfig.GetAttributeBool("allowordering", false))
|
||||
{
|
||||
_ = GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Log.Debug("end");
|
||||
InitCharacters();
|
||||
}
|
||||
|
||||
internal Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, bool giveTags = true, IEnumerable<ContentXElement> additionalItems = null)
|
||||
{
|
||||
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.Unsynced);
|
||||
characterInfo.TeamID = teamType;
|
||||
|
||||
if (positionToStayIn == null)
|
||||
{
|
||||
positionToStayIn =
|
||||
WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
|
||||
WayPoint.GetRandom(SpawnType.Human, null, submarine);
|
||||
}
|
||||
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
|
||||
spawnedCharacter.HumanPrefab = humanPrefab;
|
||||
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
|
||||
_ = humanPrefab.GiveItems(spawnedCharacter, submarine, null, createNetworkEvents: false);
|
||||
foreach (var item in spawnedCharacter.Inventory.AllItems)
|
||||
{
|
||||
IdCard card = item.GetComponent<IdCard>();
|
||||
if (card == null) continue;
|
||||
card.SubmarineSpecificID = submarine.SubmarineSpecificIDTag;
|
||||
}
|
||||
|
||||
if (additionalItems != null)
|
||||
{
|
||||
foreach (var additionalItem in additionalItems)
|
||||
{
|
||||
int amount = additionalItem.GetAttributeInt("amount", 1);
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
HumanPrefab.InitializeItem(spawnedCharacter, additionalItem, submarine, humanPrefab, createNetworkEvents: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
characters.Add(spawnedCharacter);
|
||||
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
|
||||
|
||||
return spawnedCharacter;
|
||||
}
|
||||
|
||||
internal void GiveCharacterItem(Character character, ContentXElement itemElement, bool createNetworkEvents = true)
|
||||
{
|
||||
HumanPrefab.InitializeItem(character, itemElement, null, character.HumanPrefab, createNetworkEvents: createNetworkEvents);
|
||||
characterItems[character] = character.Inventory.FindAllItems(recursive: true);
|
||||
}
|
||||
|
||||
internal void CreateHumansAtPosition(CharacterTeamType team, Vector2 position, OnCharacterCreated onCharacterCreated)
|
||||
{
|
||||
List<(HumanPrefab, XElement)> humanPrefabsToSpawn = new List<(HumanPrefab, XElement)>();
|
||||
foreach (XElement element in characterConfig?.Elements())
|
||||
{
|
||||
var humanPrefab = GetHumanPrefabFromElement(element);
|
||||
humanPrefabsToSpawn.Add((humanPrefab, element));
|
||||
}
|
||||
|
||||
foreach (var prefabToSpawn in humanPrefabsToSpawn)
|
||||
{
|
||||
var humanPrefab = prefabToSpawn.Item1;
|
||||
XElement characterMissionConfig = prefabToSpawn.Item2;
|
||||
Character character = CreateHumanAtPosition(humanPrefab, team, position);
|
||||
character.EnableDespawn = false; // don't let mission npcs despawn
|
||||
onCharacterCreated.Invoke(character, characterMissionConfig);
|
||||
character.MLC().NPCElement = characterMissionConfig;
|
||||
}
|
||||
InitCharacters();
|
||||
}
|
||||
|
||||
internal Character CreateHumanAtPosition(HumanPrefab humanPrefab, CharacterTeamType team, Vector2 spawnPosition)
|
||||
{
|
||||
Character character = CharacterUtils.CreateHuman(humanPrefab, characters, characterItems, team, spawnPosition, false);
|
||||
return character;
|
||||
}
|
||||
|
||||
private HumanPrefab GetHumanPrefabFromElement(XElement element)
|
||||
{
|
||||
if (element.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission \"" + mission.Prefab.Identifier + "\" - use character identifiers instead of names to configure the characters.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty);
|
||||
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
|
||||
if (humanPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
return humanPrefab;
|
||||
}
|
||||
|
||||
private void InitCharacters()
|
||||
{
|
||||
Log.Debug("characterCount");
|
||||
int i = 0;
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
characters[i].IsEscorted = false;
|
||||
Color col = element.GetAttributeColor("color", Color.LightGreen);
|
||||
characters[i].UniqueNameColor = col;
|
||||
Log.Debug($"Set color to {col} for character {characters[i].Name}");
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsAlive(Character character) => character != null && !character.Removed && !character.IsDead;
|
||||
|
||||
internal static bool IsCaptured(Character character) => character.LockHands;
|
||||
|
||||
internal static bool Survived(Character character)
|
||||
{
|
||||
return IsAlive(character) && character.CurrentHull?.Submarine != null &&
|
||||
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine));
|
||||
}
|
||||
|
||||
internal static bool Close(Character target, Character ai, float closeDistance)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(ai.WorldPosition, target.WorldPosition);
|
||||
return dist < closeDistance * closeDistance;
|
||||
}
|
||||
|
||||
internal static Character GetClosest(Character ai)
|
||||
{
|
||||
float closest = float.MaxValue;
|
||||
Character target = null;
|
||||
foreach (Character player in Character.CharacterList.Where(c => c.IsPlayer))
|
||||
{
|
||||
if (player.IsDead) continue; // skip dead players
|
||||
float dist = Vector2.DistanceSquared(player.WorldPosition, ai.WorldPosition);
|
||||
if (dist < closest)
|
||||
{
|
||||
closest = dist;
|
||||
target = player;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma.MoreLevelContent.Shared.Missions
|
||||
{
|
||||
class MissionTest
|
||||
{
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Barotrauma;
|
||||
using MoreLevelContent.Shared.Generation;
|
||||
using MoreLevelContent.Shared;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
// partial class PirateOutpostMission : Mission
|
||||
// {
|
||||
// public override bool DisplayAsCompleted => throw new NotImplementedException();
|
||||
//
|
||||
// public override bool DisplayAsFailed => throw new NotImplementedException();
|
||||
//
|
||||
// public override bool DetermineCompleted() => throw new NotImplementedException();
|
||||
// }
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
using Barotrauma;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MoreLevelContent.Missions
|
||||
{
|
||||
// Shared
|
||||
internal partial class TriangulationMission : Mission
|
||||
{
|
||||
private readonly LocalizedString sonarLabel0;
|
||||
private readonly LocalizedString sonarLabel1;
|
||||
private readonly LocalizedString sonarLabel2;
|
||||
private LevelData levelData;
|
||||
|
||||
public TriangulationMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
sonarLabel0 = TextManager.Get("tri-point-1");
|
||||
sonarLabel1 = TextManager.Get("tri-point-1");
|
||||
sonarLabel2 = TextManager.Get("tri-point-1");
|
||||
|
||||
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
|
||||
if (levelData != null)
|
||||
{
|
||||
SetLevel(levelData);
|
||||
}
|
||||
}
|
||||
|
||||
//public override bool DetermineCompleted() => false;
|
||||
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => false;
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels => base.SonarLabels;
|
||||
|
||||
public override void SetLevel(LevelData level)
|
||||
{
|
||||
if (levelData != null)
|
||||
{
|
||||
//level already set
|
||||
return;
|
||||
}
|
||||
|
||||
levelData = level;
|
||||
|
||||
switch (level.MLC().TriangulationTarget)
|
||||
{
|
||||
case TriangulationTarget.None:
|
||||
break;
|
||||
case TriangulationTarget.MapFeature:
|
||||
break;
|
||||
case TriangulationTarget.PirateBase:
|
||||
break;
|
||||
case TriangulationTarget.Treasure:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user