Track LocalMods as part of monolith
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using Barotrauma;
|
||||
using MoreLevelContent.Missions;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.MoreLevelContent.Config;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
internal partial class CablePuzzleMapModule : MapModule
|
||||
{
|
||||
protected override void InitProjSpecific() { }
|
||||
|
||||
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
|
||||
{
|
||||
CablePuzzleMission.SubmarineFile = null; // Clear the sub file at the start of every level
|
||||
if (levelData.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
Log.Debug("Ignored level due to being an outpost");
|
||||
return; // Ignore outpost levels
|
||||
}
|
||||
LevelData_MLCData data = levelData.MLC();
|
||||
if (data.RelayStationStatus == RelayStationStatus.None || !ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableRelayStations)
|
||||
{
|
||||
Log.Debug("No relay station");
|
||||
return; // Do nothing if we don't have a relay station
|
||||
}
|
||||
|
||||
var missions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("relayrepair")).OrderBy(m => m.UintIdentifier);
|
||||
if (!missions.Any())
|
||||
{
|
||||
Log.Error("Failed to find any cable puzzle missions!");
|
||||
return;
|
||||
}
|
||||
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
var cablePuzzleMissionPrefab = ToolBox.SelectWeightedRandom(missions, p => p.Commonness, rand);
|
||||
|
||||
// Add the mission if the station is inactive
|
||||
if (!__instance.Missions.Any(m => m.Prefab.Tags.Contains("relayrepair")) && data.RelayStationStatus == RelayStationStatus.Inactive)
|
||||
{
|
||||
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(__instance);
|
||||
Mission inst = cablePuzzleMissionPrefab.Instantiate(__instance.Map.SelectedConnection.Locations, Submarine.MainSub);
|
||||
_extraMissions.Add(inst);
|
||||
Instance.extraMissions.SetValue(__instance, _extraMissions);
|
||||
Log.Debug("Added relay staion mission to extra missions!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise the station is active, so we need to assign the sub file
|
||||
var configElement = cablePuzzleMissionPrefab.ConfigElement.GetChildElement("Submarine");
|
||||
CablePuzzleMission.SetSub(configElement, cablePuzzleMissionPrefab);
|
||||
Log.Debug("Set the relay station sub for a completed relay station");
|
||||
}
|
||||
|
||||
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection)
|
||||
{
|
||||
LevelData_MLCData levelData = __instance.MLC();
|
||||
if (levelData.HasBeaconConstruction) return; // Ignore levels with a construction site
|
||||
RollForRelay(__instance, levelData, locationConnection);
|
||||
}
|
||||
|
||||
// Map Migration
|
||||
public override void OnMapLoad(Map __instance)
|
||||
{
|
||||
if (!__instance.Connections.Any(c => c.LevelData.MLC().HasRelayStation))
|
||||
{
|
||||
Log.Debug("Map has no relay stations, adding some...");
|
||||
for (int i = 0; i < __instance.Connections.Count; i++)
|
||||
{
|
||||
var connection = __instance.Connections[i];
|
||||
|
||||
// See if we should generate a construction site
|
||||
LevelData_MLCData extraData = connection.LevelData.MLC();
|
||||
RollForRelay(connection.LevelData, extraData, connection);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Map has relay stations");
|
||||
}
|
||||
}
|
||||
|
||||
private void RollForRelay(LevelData levelData, LevelData_MLCData extraData, LocationConnection locationConnection)
|
||||
{
|
||||
var rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
if (!levelData.HasBeaconStation && !levelData.MLC().HasBeaconConstruction)
|
||||
{
|
||||
double roll = rand.NextDouble();
|
||||
// Relay stations have a 10% chance to spawn on any connection
|
||||
extraData.RelayStationStatus = roll < 0.10f ? RelayStationStatus.Inactive : RelayStationStatus.None;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using Barotrauma;
|
||||
using MoreLevelContent.Missions;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.MoreLevelContent.Config;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
internal partial class ConstructionMapModule : MapModule
|
||||
{
|
||||
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
|
||||
{
|
||||
if (levelData.Type == LevelData.LevelType.Outpost) return; // Ignore outpost levels
|
||||
LevelData_MLCData data = levelData.MLC();
|
||||
if (data.HasBeaconConstruction && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableConstructionSites)
|
||||
{
|
||||
var constructionMissions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("beaconconstruction")).OrderBy(m => m.UintIdentifier);
|
||||
if (constructionMissions.Any())
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
var beaconMissionPrefab = ToolBox.SelectWeightedRandom(constructionMissions, p => (float)p.Commonness, rand);
|
||||
if (!__instance.Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
|
||||
{
|
||||
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(__instance);
|
||||
Mission inst = beaconMissionPrefab.Instantiate(__instance.Map.SelectedConnection.Locations, Submarine.MainSub);
|
||||
_extraMissions.Add(inst);
|
||||
Instance.extraMissions.SetValue(__instance, _extraMissions);
|
||||
Log.Debug("Added beacon construction mission to extra missions!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Failed to find any beacon construction missions!");
|
||||
}
|
||||
}
|
||||
}
|
||||
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection)
|
||||
{
|
||||
LevelData_MLCData levelData = __instance.MLC();
|
||||
if (levelData.HasRelayStation) return;
|
||||
TrySpawnBeaconConstruction(__instance, levelData, locationConnection);
|
||||
}
|
||||
public override void OnMapLoad(Map __instance)
|
||||
{
|
||||
if (!__instance.Connections.Any(c => c.LevelData.MLC().HasBeaconConstruction))
|
||||
{
|
||||
Log.Debug("Map has no construction sites, adding some...");
|
||||
for (int i = 0; i < __instance.Connections.Count; i++)
|
||||
{
|
||||
var connection = __instance.Connections[i];
|
||||
|
||||
// See if we should generate a construction site
|
||||
LevelData_MLCData extraData = connection.LevelData.MLC();
|
||||
TrySpawnBeaconConstruction(connection.LevelData, extraData, connection);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Map has construction sites");
|
||||
}
|
||||
}
|
||||
|
||||
private void TrySpawnBeaconConstruction(LevelData levelData, LevelData_MLCData extraData, LocationConnection locationConnection)
|
||||
{
|
||||
var rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
// Place some beacon stations
|
||||
if (!levelData.IsBeaconActive)
|
||||
{
|
||||
double roll = rand.NextDouble();
|
||||
double chance = locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Min();
|
||||
extraData.HasBeaconConstruction = roll < (chance / 1.2); // construction sites have half the chance to spawn as regular beacon stations
|
||||
if (extraData.HasBeaconConstruction)
|
||||
{
|
||||
CreateBeaconConstruction(levelData, rand, extraData);
|
||||
levelData.HasBeaconStation = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
private void CreateBeaconConstruction(LevelData __instance, MTRandom rand, LevelData_MLCData levelData)
|
||||
{
|
||||
List<SupplyType> possibleSupplies = new();
|
||||
AddSupply(SupplyType.Electric, 4);
|
||||
AddSupply(SupplyType.Structure, 4);
|
||||
AddSupply(SupplyType.Utility, 4);
|
||||
|
||||
int diffClamped = (int)(__instance.Difficulty / 10);
|
||||
// Always request at least one
|
||||
int totalRequested = 1 + rand.Next(diffClamped + 1);
|
||||
|
||||
for (int i = 0; i < totalRequested; i++)
|
||||
{
|
||||
int index = rand.Next(possibleSupplies.Count);
|
||||
SupplyType requestedSupply = possibleSupplies[index];
|
||||
possibleSupplies.RemoveAt(index);
|
||||
switch (requestedSupply)
|
||||
{
|
||||
case SupplyType.Electric:
|
||||
levelData.RequestedE++;
|
||||
break;
|
||||
case SupplyType.Structure:
|
||||
levelData.RequestedS++;
|
||||
break;
|
||||
case SupplyType.Utility:
|
||||
levelData.RequestedU++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Debug("Created a beacon construction mission");
|
||||
|
||||
void AddSupply(SupplyType type, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
possibleSupplies.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
if (levelData.HasBeaconConstruction) __instance.HasBeaconStation = false;
|
||||
}
|
||||
|
||||
protected override void InitProjSpecific() { }
|
||||
|
||||
enum SupplyType
|
||||
{
|
||||
Electric,
|
||||
Structure,
|
||||
Utility
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using Barotrauma.MoreLevelContent.Config;
|
||||
using MoreLevelContent.Networking;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
// Shared
|
||||
internal partial class OldDistressMapModule : MapModule
|
||||
{
|
||||
public static bool ForceSpawnDistress = false;
|
||||
public static string ForcedMissionIdentifier = "";
|
||||
|
||||
private readonly List<Mission> _internalMissionStore = new();
|
||||
private static OldDistressMapModule _instance;
|
||||
private static bool _spawnStartingBeacon = false;
|
||||
const int MAX_DISTRESS_CREATE_ATTEMPTS = 5;
|
||||
const int DISTRESS_MIN_DIST = 1;
|
||||
const int DISTRESS_MAX_DIST = 3;
|
||||
|
||||
public OldDistressMapModule()
|
||||
{
|
||||
_instance = this;
|
||||
InitProjSpecific();
|
||||
}
|
||||
|
||||
internal void UpdateDistressBeacons(Map __instance)
|
||||
{
|
||||
foreach (LocationConnection connection in __instance.Connections.Where(c => c.LevelData.MLC().HasDistress))
|
||||
{
|
||||
// skip locations that are close
|
||||
if (GameMain.GameSession.Campaign.Map.CurrentLocation.Connections.Contains(connection)) continue;
|
||||
|
||||
// TODO: When multiple world steps happen at once in a long mission
|
||||
// this cause a distress to skip from active -> faint -> off before
|
||||
// the player has seen any notification of it. There could be a way
|
||||
// of avoiding this by counting the world steps before doing this
|
||||
// instead of doing it every world step
|
||||
var levelData = connection.LevelData.MLC();
|
||||
levelData.DistressStepsLeft--;
|
||||
if (levelData.DistressStepsLeft <= 0)
|
||||
{
|
||||
levelData.HasDistress = false;
|
||||
SendDistressUpdate("mlc.distress.lost", connection);
|
||||
}
|
||||
if (levelData.DistressStepsLeft == 3) SendDistressUpdate("mlc.distress.faint", connection);
|
||||
}
|
||||
}
|
||||
private void SendDistressUpdate(string updateType, LocationConnection connection)
|
||||
{
|
||||
#if CLIENT
|
||||
string msg = TextManager.GetWithVariables(updateType, ("[location1]", $"‖color:gui.orange‖{connection.Locations[0].DisplayName}‖end‖"), ("[location2]", $"‖color:gui.orange‖{connection.Locations[1].DisplayName}‖end‖")).Value;
|
||||
SendChatUpdate(msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void OnPreRoundStart(LevelData levelData)
|
||||
{
|
||||
_internalMissionStore.Clear();
|
||||
|
||||
if (levelData == null) return;
|
||||
if (!Main.IsCampaign) return;
|
||||
|
||||
TrySpawnDistress(GameMain.GameSession.Map, _spawnStartingBeacon);
|
||||
_spawnStartingBeacon = false;
|
||||
|
||||
if (!levelData.MLC().HasDistress && !ForceSpawnDistress)
|
||||
{
|
||||
Log.Debug("Level has no distress mission");
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetMissionByTag("distress", levelData, out MissionPrefab prefab, ForcedMissionIdentifier))
|
||||
{
|
||||
Log.Debug("Adding distress mission");
|
||||
Mission inst = prefab.Instantiate(GameMain.GameSession.Map.SelectedConnection.Locations, Submarine.MainSub);
|
||||
AddExtraMission(inst); // weird
|
||||
_internalMissionStore.Add(inst);
|
||||
Log.Debug("Added distress mission to extra missions!");
|
||||
} else
|
||||
{
|
||||
Log.Error("Failed to find any distress missions!");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
|
||||
{
|
||||
if (!_internalMissionStore.Any()) return;
|
||||
foreach (Mission mission in _internalMissionStore)
|
||||
{
|
||||
AddExtraMission(mission);
|
||||
}
|
||||
_internalMissionStore.Clear();
|
||||
}
|
||||
|
||||
private void AddExtraMission(Mission mission)
|
||||
{
|
||||
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(GameMain.GameSession.GameMode);
|
||||
_extraMissions.Add(mission);
|
||||
Instance.extraMissions.SetValue(GameMain.GameSession.GameMode, _extraMissions);
|
||||
}
|
||||
|
||||
public override void OnProgressWorld(Map __instance) => UpdateDistressBeacons(__instance);
|
||||
|
||||
private void TrySpawnDistress(Map __instance, bool force = false)
|
||||
{
|
||||
if (Main.IsClient) return;
|
||||
|
||||
if (__instance == null || __instance.Connections.Count == 0)
|
||||
{
|
||||
Log.Debug("Skipped trying to create a distress beacon as there was no map connections");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're at the max
|
||||
int activeDistressCalls = __instance.Connections.Where(c => c.LevelData.MLC().HasDistress).Count();
|
||||
if (activeDistressCalls > ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons)
|
||||
{
|
||||
if (force)
|
||||
{
|
||||
Log.Debug("Ignoring max distress cap due to force creation");
|
||||
} else
|
||||
{
|
||||
Log.Debug($"Skipped creating new distress due to being at the limit ({ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons})");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not, lets roll to see if we should make a new distress signal
|
||||
float chance = Rand.Value(Rand.RandSync.Unsynced);
|
||||
Log.InternalDebug($"{chance} >= {ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage} ({chance >= ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage})");
|
||||
if (chance >= ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage && !force) return;
|
||||
|
||||
// Lets get a random instance to use
|
||||
int seed = Rand.GetRNG(Rand.RandSync.Unsynced).Next();
|
||||
Random rand = new MTRandom(seed);
|
||||
|
||||
// Find a location connection to spawn a distress beacon at
|
||||
int dist = Rand.Range(DISTRESS_MIN_DIST, DISTRESS_MAX_DIST, Rand.RandSync.Unsynced);
|
||||
LocationConnection targetConnection = WalkConnection(__instance.CurrentLocation, rand, dist);
|
||||
int stepsLeft = rand.Next(4, 8);
|
||||
if (!MapDirector.ConnectionIdLookup.ContainsKey(targetConnection)) return; // how does this happen?
|
||||
|
||||
CreateDistress(targetConnection, stepsLeft);
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.IsMultiplayer)
|
||||
{
|
||||
// inform clients of the new distress beacon
|
||||
IWriteMessage msg = NetUtil.CreateNetMsg(NetEvent.MAP_SEND_NEWDISTRESS);
|
||||
msg.WriteUInt32((uint)MapDirector.ConnectionIdLookup[targetConnection]);
|
||||
msg.WriteByte((byte)stepsLeft);
|
||||
NetUtil.SendAll(msg);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void CreateDistress(LocationConnection connection, int stepsLeft)
|
||||
{
|
||||
connection.LevelData.MLC().HasDistress = true;
|
||||
connection.LevelData.MLC().DistressStepsLeft = stepsLeft;
|
||||
SendDistressUpdate("mlc.distress.new", connection);
|
||||
}
|
||||
|
||||
internal static void ForceDistress()
|
||||
{
|
||||
Log.Debug("Force creating distress beacon");
|
||||
_instance.TrySpawnDistress(GameMain.GameSession.Map, true);
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class DistressMapModule : TimedEventMapModule
|
||||
{
|
||||
protected override NetEvent EventCreated => NetEvent.MAP_SEND_NEWDISTRESS;
|
||||
|
||||
protected override string NewEventText => "distress.new";
|
||||
|
||||
protected override string EventTag => "distress";
|
||||
|
||||
protected override int MaxActiveEvents => ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons;
|
||||
|
||||
protected override float EventSpawnChance => ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage;
|
||||
|
||||
protected override int MinDistance => 1;
|
||||
|
||||
protected override int MaxDistance => 3;
|
||||
|
||||
protected override int MinEventDuration => 4;
|
||||
|
||||
protected override int MaxEventDuration => 8;
|
||||
|
||||
protected override bool ShouldSpawnEventAtStart => true;
|
||||
|
||||
protected override void HandleEventCreation(LevelData_MLCData data, int eventDuration)
|
||||
{
|
||||
data.HasDistress = true;
|
||||
data.DistressStepsLeft = eventDuration;
|
||||
}
|
||||
|
||||
protected override bool TryGetMissionPrefab(LevelData levelData, out MissionPrefab prefab)
|
||||
{
|
||||
prefab = null;
|
||||
if (!ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableDistressMissions) return false;
|
||||
if (!ForcedMissionIdentifier.IsNullOrEmpty()) return base.TryGetMissionPrefab(levelData, out prefab);
|
||||
var orderedMissions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(EventTag) && m.IsAllowedDifficulty(levelData.Difficulty)).OrderBy(m => m.UintIdentifier);
|
||||
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
prefab = ToolBox.SelectWeightedRandom(orderedMissions, p => p.Commonness, rand);
|
||||
return prefab != null;
|
||||
|
||||
}
|
||||
|
||||
protected override void HandleUpdate(LevelData_MLCData data, LocationConnection connection)
|
||||
{
|
||||
if (!data.HasDistress) return;
|
||||
data.DistressStepsLeft--;
|
||||
if (data.DistressStepsLeft == 3) AddNewsStory("distress.faint", connection);
|
||||
|
||||
if (data.DistressStepsLeft <= 0)
|
||||
{
|
||||
data.HasDistress = false;
|
||||
AddNewsStory("distress.lost", connection);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool LevelHasEvent(LevelData_MLCData data) => data.HasDistress && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableDistressMissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Barotrauma;
|
||||
using MoreLevelContent.Missions;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MoreLevelContent.Networking;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
internal partial class LostCargoMapModule : TimedEventMapModule
|
||||
{
|
||||
protected override NetEvent EventCreated => NetEvent.MAP_SEND_NEWCARGO;
|
||||
|
||||
//protected override NetEvent EventUpdated => throw new NotImplementedException();
|
||||
|
||||
protected override string NewEventText => "mlc.lostcargo.new";
|
||||
|
||||
protected override string EventTag => "lostcargo";
|
||||
|
||||
protected override int MaxActiveEvents => 5;
|
||||
|
||||
protected override float EventSpawnChance => 1;
|
||||
|
||||
protected override int MinDistance => 1;
|
||||
|
||||
protected override int MaxDistance => 2;
|
||||
|
||||
protected override int MinEventDuration => 4;
|
||||
|
||||
protected override int MaxEventDuration => 6;
|
||||
|
||||
protected override bool ShouldSpawnEventAtStart => true;
|
||||
|
||||
protected override void HandleEventCreation(LevelData_MLCData data, int eventDuration)
|
||||
{
|
||||
data.HasLostCargo = true;
|
||||
data.CargoStepsLeft = eventDuration;
|
||||
}
|
||||
|
||||
protected override void HandleUpdate(LevelData_MLCData data, LocationConnection connection)
|
||||
{
|
||||
data.CargoStepsLeft--;
|
||||
if (data.CargoStepsLeft <= 0)
|
||||
{
|
||||
data.HasLostCargo = false;
|
||||
string textTag = MLCUtils.GetRandomTag("mlc.lostcargo.tooslow", connection.LevelData);
|
||||
AddNewsStory(textTag, connection);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override bool LevelHasEvent(LevelData_MLCData data) => data.HasLostCargo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
using Barotrauma;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
using static Barotrauma.Level;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System.Globalization;
|
||||
using static MoreLevelContent.Shared.Generation.MissionGenerationDirector;
|
||||
using Barotrauma.Items.Components;
|
||||
using Steamworks.Ugc;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using Barotrauma.MoreLevelContent.Config;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
internal partial class MapFeatureModule : MapModule
|
||||
{
|
||||
private static List<MapFeature> _Features = new();
|
||||
private static Dictionary<Identifier, MapFeature> _IdentifierToFeature = new();
|
||||
private List<Location> _DisallowedLocations;
|
||||
public static Submarine MapFeatureSub { get; private set; }
|
||||
public static Identifier CurrentMapFeature { get; private set; }
|
||||
public static MapFeature Feature { get; private set; }
|
||||
|
||||
protected override void InitProjSpecific()
|
||||
{
|
||||
// Build table of map features
|
||||
_Features.Clear();
|
||||
_DisallowedLocations = new();
|
||||
var features = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("mapfeatureset"));
|
||||
var featureEvents = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("mapfeatureeventset"));
|
||||
|
||||
// Parse map features
|
||||
var featureDict = new Dictionary<Identifier, MapFeature>();
|
||||
foreach (var item in features)
|
||||
{
|
||||
var config = item.ConfigElement;
|
||||
foreach (var elm in config.GetChildElements("MapFeature"))
|
||||
{
|
||||
var feature = new MapFeature(elm, item.ContentPackage);
|
||||
if (featureDict.ContainsKey(feature.Name))
|
||||
{
|
||||
DebugConsole.ThrowError($"ContentPackage {item.ContentPackage.Name} contains a duplicate map feature with identifier {feature.Name}, skipping...");
|
||||
continue;
|
||||
}
|
||||
featureDict.Add(feature.Name, feature);
|
||||
}
|
||||
}
|
||||
|
||||
_IdentifierToFeature = featureDict;
|
||||
_Features = featureDict.Values.OrderBy(f => f.Name).ToList();
|
||||
|
||||
foreach (var featureEvent in featureEvents)
|
||||
{
|
||||
var config = featureEvent.ConfigElement;
|
||||
foreach (var eventElement in config.GetChildElements("Events"))
|
||||
{
|
||||
var targets = eventElement.GetAttributeIdentifierArray("features", Array.Empty<Identifier>(), true);
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (!_IdentifierToFeature.TryGetValue(target, out MapFeature feature))
|
||||
{
|
||||
DebugConsole.ThrowError($"MLC: Tried to add a event set to unknown map feature {target}", contentPackage: featureEvent.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
feature.AddEventSet(eventElement, featureEvent.ContentPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Hooks.Instance.AddUpdateAction(Update);
|
||||
Log.Debug($"Collected {_Features.Count} map features");
|
||||
}
|
||||
|
||||
void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (Loaded == null) return;
|
||||
if (MapFeatureSub == null) return;
|
||||
if (Loaded.LevelData.MLC().MapFeatureData.Revealed) return;
|
||||
if (GameSession.GetSessionCrewCharacters(CharacterType.Player).Any(c => c.Submarine == MapFeatureSub))
|
||||
{
|
||||
Loaded.LevelData.MLC().MapFeatureData.Revealed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetFeature(Identifier name, out MapFeature feature)
|
||||
{
|
||||
feature = null;
|
||||
if (name.IsEmpty) return false;
|
||||
if (!_IdentifierToFeature.ContainsKey(name))
|
||||
{
|
||||
DebugConsole.ThrowError($"No map feature found with identifier '{name}'");
|
||||
return false;
|
||||
}
|
||||
feature = _IdentifierToFeature[name];
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnLevelGenerate(LevelData levelData, bool mirror)
|
||||
{
|
||||
Feature = null;
|
||||
MapFeatureSub = null;
|
||||
var data = levelData.MLC();
|
||||
if (!ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableMapFeatures) return;
|
||||
if (data.MapFeatureData.Name.IsEmpty) return;
|
||||
if (!TryGetFeature(data.MapFeatureData.Name, out MapFeature feature))
|
||||
{
|
||||
Log.Error($"Tried to spawn non-existant map feature with identifier {data.MapFeatureData.Name}");
|
||||
return;
|
||||
}
|
||||
Feature = feature;
|
||||
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == feature.SubFile).FirstOrDefault();
|
||||
if (file == null)
|
||||
{
|
||||
Log.Error($"Failed to find submarine at path {feature.SubFile}");
|
||||
return;
|
||||
}
|
||||
// We need a custom placement thing for this
|
||||
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
|
||||
{
|
||||
AutoFill = true,
|
||||
File = file,
|
||||
IgnoreCrushDpeth = true,
|
||||
PlacementType = feature.PlacementType,
|
||||
AllowStealing = false,
|
||||
SpawnPosition = feature.SpawnLocation,
|
||||
Callback = OnSubSpawned
|
||||
});
|
||||
|
||||
void OnSubSpawned(Submarine sub)
|
||||
{
|
||||
Log.Debug("Spawned map feature sub");
|
||||
MapFeatureSub = sub;
|
||||
CurrentMapFeature = feature.Name;
|
||||
SubPlacementUtils.SetCrushDepth(sub, true);
|
||||
sub.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Static;
|
||||
sub.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
sub.Info.Type = SubmarineType.Outpost;
|
||||
sub.GodMode = true;
|
||||
sub.ShowSonarMarker = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection)
|
||||
{
|
||||
RollForFeature(__instance, locationConnection);
|
||||
}
|
||||
|
||||
public override void OnMapLoad(Map __instance)
|
||||
{
|
||||
if (!__instance.Connections.Any(c => !c.LevelData.MLC().MapFeatureData.Name.IsEmpty))
|
||||
{
|
||||
Log.Debug("Map has no map features, adding some...");
|
||||
for (int i = 0; i < __instance.Connections.Count; i++)
|
||||
{
|
||||
var connection = __instance.Connections[i];
|
||||
RollForFeature(connection.LevelData, connection);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Map has map features");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPostRoundStart(LevelData levelData)
|
||||
{
|
||||
if (levelData == null) return;
|
||||
if (levelData.Type == LevelData.LevelType.Outpost) return;
|
||||
var data = levelData.MLC();
|
||||
if (data == null) return;
|
||||
|
||||
if (!TryGetFeature(data.MapFeatureData.Name, out MapFeature feature))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (MapFeatureSub == null)
|
||||
{
|
||||
DebugConsole.ThrowError("MLC: This level calls for a map feature but no map feature sub was spawned!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set allow stealing
|
||||
if (!feature.AllowStealing)
|
||||
{
|
||||
foreach (var item in MapFeatureSub.GetItems(true))
|
||||
{
|
||||
if (item.Container?.Prefab.AllowStealingContainedItems ?? false) continue;
|
||||
item.AllowStealing = false;
|
||||
item.SpawnedInCurrentOutpost = true;
|
||||
}
|
||||
}
|
||||
|
||||
// No damaging map features
|
||||
MapFeatureSub.GodMode = true;
|
||||
|
||||
if (GameMain.GameSession?.EventManager == null)
|
||||
{
|
||||
Log.Error("Event manager was null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (feature.PossibleEvents.Count == 0) return;
|
||||
var rand = new MTRandom(GameMain.GameSession.EventManager.RandomSeed);
|
||||
var mapEvent = ToolBox.SelectWeightedRandom(feature.PossibleEvents, e => e.Commonness, rand);
|
||||
if (rand.NextDouble() > mapEvent.Probability) return;
|
||||
|
||||
EventPrefab eventPrefab = EventSet.GetAllEventPrefabs().Where(p => p.Identifier == mapEvent.EventIdentifier).Distinct().OrderBy(p => p.Identifier).FirstOrDefault();
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Map Feature \"{feature.Name}\" failed to trigger an event (couldn't find an event with the identifier \"{mapEvent.EventIdentifier}\").",
|
||||
contentPackage: feature.Package);
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.EventManager != null)
|
||||
{
|
||||
_ = CoroutineManager.StartCoroutine(SpawnMapFeatureEvent(eventPrefab));
|
||||
}
|
||||
}
|
||||
const float WAIT_TIME = 5;
|
||||
private IEnumerable<CoroutineStatus> SpawnMapFeatureEvent(EventPrefab prefab)
|
||||
{
|
||||
float timer = 0;
|
||||
|
||||
while(timer < WAIT_TIME)
|
||||
{
|
||||
timer += CoroutineManager.DeltaTime;
|
||||
yield return CoroutineStatus.Running;
|
||||
}
|
||||
|
||||
|
||||
var newEvent = prefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
|
||||
GameMain.GameSession.EventManager.ActivateEvent(newEvent);
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
void RollForFeature(LevelData data, LocationConnection connection)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if there's already a map featue nearby
|
||||
if (connection.Locations.Any(l => _DisallowedLocations.Contains(l)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rand = MLCUtils.GetRandomFromString(data.Seed);
|
||||
|
||||
int zoneIndex = connection.Locations[0].GetZoneIndex(GameMain.GameSession.Map);
|
||||
|
||||
var validFeatures = _Features.Where(f => f.CommonnessPerZone.ContainsKey(zoneIndex));
|
||||
if (!validFeatures.Any()) return;
|
||||
// Select feature to try and spawn
|
||||
MapFeature feature = ToolBox.SelectWeightedRandom(validFeatures, f => f.CommonnessPerZone[zoneIndex], rand);
|
||||
|
||||
// Roll for spawn
|
||||
if (feature.Chance > rand.NextDouble())
|
||||
{
|
||||
data.MLC().MapFeatureData.Name = feature.Name;
|
||||
data.MLC().MapFeatureData.Revealed = !feature.Display.HideUntilRevealed;
|
||||
_DisallowedLocations.AddRange(connection.Locations);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
internal class MapFeature
|
||||
{
|
||||
public MapFeature(XElement element, ContentPackage package)
|
||||
{
|
||||
Package = package;
|
||||
SubFile = element.GetAttributeContentPath("path", package);
|
||||
Name = element.GetAttributeIdentifier("identifier", "");
|
||||
SpawnLocation = element.GetAttributeEnum("spawnPosition", SubSpawnPosition.PathWall);
|
||||
PlacementType = element.GetAttributeEnum("placement", PlacementType.Bottom);
|
||||
Chance = element.GetAttributeFloat("chance", 0);
|
||||
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
|
||||
ParseCommonnessPerZone(commonnessPerZoneStrs);
|
||||
|
||||
AllowStealing = element.GetAttributeBool("allowstealing", true);
|
||||
Display = new MapFeatureDisplay(element.GetChildElement("Display"), Name);
|
||||
PossibleEvents = new();
|
||||
}
|
||||
|
||||
public ContentPackage Package { get; private set; }
|
||||
public ContentPath SubFile { get; private set; }
|
||||
public Identifier Name { get; private set; }
|
||||
public SubSpawnPosition SpawnLocation { get; private set; }
|
||||
public PlacementType PlacementType { get; private set; }
|
||||
public float Chance { get; private set; }
|
||||
public Dictionary<int, float> CommonnessPerZone { get; private set; }
|
||||
public bool AllowStealing { get; private set; }
|
||||
public MapFeatureDisplay Display { get; private set; }
|
||||
public List<MapFeatureEvent> PossibleEvents { get; private set; }
|
||||
|
||||
public struct MapFeatureDisplay
|
||||
{
|
||||
public MapFeatureDisplay(XElement element, Identifier name)
|
||||
{
|
||||
Icon = element.GetAttributeString("icon", "");
|
||||
Tooltip = element.GetAttributeString("tooltip", "");
|
||||
HideUntilRevealed = element.GetAttributeBool("hideuntilrevealed", false);
|
||||
DisplayName = TextManager.Get($"mapfeature.{name}.name");
|
||||
}
|
||||
public string Icon { get; private set; }
|
||||
public string Tooltip { get; private set; }
|
||||
public bool HideUntilRevealed { get; private set; }
|
||||
public LocalizedString DisplayName { get; private set; }
|
||||
}
|
||||
|
||||
public struct MapFeatureEvent
|
||||
{
|
||||
public MapFeatureEvent(XElement element, ContentPackage package)
|
||||
{
|
||||
Probability = element.GetAttributeFloat("probability", 0);
|
||||
Commonness = element.GetAttributeFloat("commonness", 0);
|
||||
EventIdentifier = element.GetAttributeIdentifier("identifier", "");
|
||||
if (EventIdentifier.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError("Map feature EventSet missing identifier!", contentPackage: package);
|
||||
}
|
||||
}
|
||||
public float Probability { get; private set; }
|
||||
public float Commonness { get; private set; }
|
||||
public Identifier EventIdentifier { get; private set; }
|
||||
}
|
||||
|
||||
void ParseCommonnessPerZone(string[] array)
|
||||
{
|
||||
CommonnessPerZone = new();
|
||||
foreach (string commonnessPerZoneStr in array)
|
||||
{
|
||||
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
|
||||
if (splitCommonnessPerZone.Length != 2 ||
|
||||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
|
||||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read commonness values for map feature \"" + Name + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"");
|
||||
break;
|
||||
}
|
||||
CommonnessPerZone[zoneIndex] = zoneCommonness;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEventSet(XElement element, ContentPackage package)
|
||||
{
|
||||
foreach (var item in element.GetChildElements("ScriptedEvent"))
|
||||
{
|
||||
PossibleEvents.Add(new MapFeatureEvent(item, package));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Flags]
|
||||
public enum SpawnLocation
|
||||
{
|
||||
Wreck = 1,
|
||||
Cave = 2,
|
||||
Abyss = 4,
|
||||
AbyssIsland = 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using Barotrauma;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Shared.Utils;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
internal abstract class MapModule
|
||||
{
|
||||
public MapModule() => InitProjSpecific();
|
||||
protected MapDirector Instance => MapDirector.Instance;
|
||||
|
||||
protected abstract void InitProjSpecific();
|
||||
|
||||
protected static bool TryGetMissionByTag(string tag, LevelData data, out MissionPrefab missionPrefab, string forceMission = "")
|
||||
{
|
||||
var orderedMissions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(tag)).OrderBy(m => m.UintIdentifier);
|
||||
if (!string.IsNullOrEmpty(forceMission))
|
||||
{
|
||||
orderedMissions = orderedMissions.Where(m => m.Identifier == forceMission).OrderBy(m => m.UintIdentifier);
|
||||
}
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(data.Seed));
|
||||
missionPrefab = ToolBox.SelectWeightedRandom(orderedMissions, p => p.Commonness, rand);
|
||||
return missionPrefab != null;
|
||||
}
|
||||
|
||||
protected LocationConnection WalkConnection(Location start, Random rand, int preferedWalkDistance)
|
||||
{
|
||||
// Since we do a connection step at the end of the process, there's one step implict in every walk
|
||||
// so we subtract a step here
|
||||
int actualWalkDist = preferedWalkDistance - 1;
|
||||
if (actualWalkDist <= 0)
|
||||
{
|
||||
return GetConnectionWeighted(start, rand);
|
||||
}
|
||||
Location location = WalkLocation(start, rand, actualWalkDist);
|
||||
return GetConnectionWeighted(location, rand);
|
||||
}
|
||||
|
||||
protected Location WalkLocation(Location start, Random rand, int preferedWalkDistance, LocationConnection from = null)
|
||||
{
|
||||
var filteredConnections = start.Connections.Where(c => c != from);
|
||||
if (!filteredConnections.Any())
|
||||
{
|
||||
return start;
|
||||
}
|
||||
|
||||
LocationConnection connectionToTravel = ToolBox.SelectWeightedRandom(
|
||||
filteredConnections.ToList(),
|
||||
filteredConnections.Select(c => GetConnectionWeight(start, c)).ToList(),
|
||||
rand);
|
||||
|
||||
Location walkedLocation = connectionToTravel.OtherLocation(start);
|
||||
preferedWalkDistance--;
|
||||
|
||||
// if we haven't walked our wanted dist or
|
||||
if (preferedWalkDistance > 0) walkedLocation = WalkLocation(walkedLocation, rand, preferedWalkDistance);
|
||||
return walkedLocation;
|
||||
}
|
||||
|
||||
static LocationConnection GetConnectionWeighted(Location location, Random rand)
|
||||
{
|
||||
LocationConnection connectionToTravel = ToolBox.SelectWeightedRandom(
|
||||
location.Connections,
|
||||
location.Connections.Select(c => GetConnectionWeight(location, c)).ToList(),
|
||||
rand);
|
||||
|
||||
return connectionToTravel;
|
||||
}
|
||||
|
||||
static float GetConnectionWeight(Location location, LocationConnection c)
|
||||
{
|
||||
|
||||
// get the destination of this connection
|
||||
Location destination = c.OtherLocation(location);
|
||||
if (destination == null) { return 0; }
|
||||
float minWeight = 0.0001f;
|
||||
float lowWeight = 0.2f;
|
||||
float normalWeight = 1.0f;
|
||||
float maxWeight = 2.0f;
|
||||
|
||||
// prefer connections we haven't passed through
|
||||
float weight = c.Passed ? lowWeight : normalWeight;
|
||||
|
||||
if (location.Biome.AllowedZones.Contains(1))
|
||||
{
|
||||
// In the first biome, give a stronger preference for locations that are farther to the right)
|
||||
float diff = destination.MapPosition.X - location.MapPosition.X;
|
||||
if (diff < 0)
|
||||
{
|
||||
weight *= 0.1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
float maxRelevantDiff = 300;
|
||||
weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
|
||||
}
|
||||
}
|
||||
else if (destination.MapPosition.X > location.MapPosition.X)
|
||||
{
|
||||
weight *= 2.0f;
|
||||
}
|
||||
|
||||
if (destination.IsRadiated())
|
||||
{
|
||||
weight *= 0.001f;
|
||||
}
|
||||
|
||||
// Prefer locations that have been revealed
|
||||
if (!destination.Discovered)
|
||||
{
|
||||
weight *= 0.5f;
|
||||
}
|
||||
|
||||
return MathHelper.Clamp(weight, minWeight, maxWeight);
|
||||
}
|
||||
|
||||
|
||||
protected void SendChatUpdate(string msg)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
GameMain.Client.AddChatMessage(msg, Barotrauma.Networking.ChatMessageType.Default, TextManager.Get("mlc.navigationannouce").Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage(
|
||||
TextManager.Get("mlc.navigationannouce").Value,
|
||||
msg,
|
||||
Barotrauma.Networking.ChatMessageType.Default,
|
||||
sender: null);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
protected void AddNewsStory(string tag, LocationConnection connection)
|
||||
{
|
||||
#if CLIENT
|
||||
string randomTag = MLCUtils.GetRandomTag(tag, connection.LevelData);
|
||||
string msg = TextManager.GetWithVariables(randomTag, ("[location1]", $"‖color:gui.orange‖{connection.Locations[0].DisplayName}‖end‖"), ("[location2]", $"‖color:gui.orange‖{connection.Locations[1].DisplayName}‖end‖")).Value;
|
||||
Log.Debug($"Added text tag {randomTag} : {msg} to news ticket");
|
||||
MapDirector.Instance.AddNewsStory(msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
public virtual void OnAddExtraMissions(CampaignMode __instance, LevelData levelData) { }
|
||||
public virtual void OnPreRoundStart(LevelData levelData) { }
|
||||
public virtual void OnPostRoundStart(LevelData levelData) { }
|
||||
public virtual void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection) { }
|
||||
public virtual void OnProgressWorld(Map __instance) { }
|
||||
public virtual void OnLevelDataLoad(LevelData __instance, XElement element) { }
|
||||
public virtual void OnLevelDataSave(LevelData __instance, XElement parentElement) { }
|
||||
public virtual void OnMapLoad(Map __instance) { }
|
||||
public virtual void OnLevelGenerate(LevelData levelData, bool mirror) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using Barotrauma;
|
||||
using MoreLevelContent.Missions;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MoreLevelContent.Shared.Generation.Pirate;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
internal partial class PirateOutpostMapModule : MapModule
|
||||
{
|
||||
List<Location> _DisallowedLocations;
|
||||
protected override void InitProjSpecific()
|
||||
{
|
||||
_DisallowedLocations = new();
|
||||
}
|
||||
|
||||
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection) => SetPirateData(__instance, __instance.MLC(), locationConnection);
|
||||
|
||||
public override void OnMapLoad(Map __instance)
|
||||
{
|
||||
// Map has no pirate outposts, lets generate some
|
||||
if (!__instance.Connections.Any(c => c.LevelData.MLC().PirateData.HasPirateBase))
|
||||
{
|
||||
Log.Debug("Map has no pirate bases, adding some...");
|
||||
for (int i = 0; i < __instance.Connections.Count; i++)
|
||||
{
|
||||
var connection = __instance.Connections[i];
|
||||
SetPirateData(connection.LevelData, connection.LevelData.MLC(), connection);
|
||||
}
|
||||
} else
|
||||
{
|
||||
Log.Debug("Map has pirate bases");
|
||||
}
|
||||
}
|
||||
|
||||
void SetPirateData(LevelData levelData, LevelData_MLCData additionalData, LocationConnection locationConnection)
|
||||
{
|
||||
PirateSpawnData spawnData = new PirateSpawnData(levelData, locationConnection);
|
||||
// Prevent pirate outposts from spawning too clustered together
|
||||
if (spawnData.WillSpawn && locationConnection.Locations.Any(l => _DisallowedLocations.Contains(l)))
|
||||
{
|
||||
// Unless they're husked, then that's fine
|
||||
if (!spawnData.Husked)
|
||||
{
|
||||
spawnData.WillSpawn = false;
|
||||
spawnData.Husked = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add nearby locations to disallowed list
|
||||
if (spawnData.WillSpawn)
|
||||
{
|
||||
_DisallowedLocations.AddRange(locationConnection.Locations);
|
||||
}
|
||||
additionalData.PirateData = new PirateData(spawnData);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal class PirateSpawnData
|
||||
{
|
||||
public PirateSpawnData(LevelData levelData, LocationConnection connection)
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
UpdatePirateSpawnData(rand, levelData, connection);
|
||||
|
||||
int spawnInt = rand.Next(100);
|
||||
int huskInt = rand.Next(100);
|
||||
|
||||
WillSpawn = _ModifiedSpawnChance > spawnInt;
|
||||
Husked = _ModifiedHuskChance > huskInt;
|
||||
}
|
||||
|
||||
public bool WillSpawn { get; set; }
|
||||
public bool Husked { get; set; }
|
||||
public float PirateDifficulty { get; private set; }
|
||||
|
||||
public override string ToString() => $"Will Spawn: {WillSpawn}, Is Husked: {Husked}";
|
||||
|
||||
private float _ModifiedSpawnChance;
|
||||
private float _ModifiedHuskChance;
|
||||
|
||||
private void UpdatePirateSpawnData(Random rand, LevelData levelData, LocationConnection connection)
|
||||
{
|
||||
var levelDiff = levelData.Difficulty;
|
||||
float a = PirateOutpostDirector.Config.PeakSpawnChance;
|
||||
float b = a / 2500;
|
||||
float c = MathF.Pow(levelDiff - 50.0f, 2);
|
||||
var spawnChance = (-b * c) + a;
|
||||
var huskChance = MathF.Max(PirateOutpostDirector.Config.BaseHuskChance, levelDiff / 10);
|
||||
ModifyChances();
|
||||
|
||||
_ModifiedSpawnChance = spawnChance;
|
||||
_ModifiedHuskChance = huskChance;
|
||||
|
||||
float difficultyNoise = Math.Abs(MathHelper.Lerp(-PirateOutpostDirector.Config.DifficultyNoise, PirateOutpostDirector.Config.DifficultyNoise, (float)rand.NextDouble()));
|
||||
PirateDifficulty = levelDiff + difficultyNoise;
|
||||
|
||||
void ModifyChances()
|
||||
{
|
||||
// Don't spawn bases on routes with an abyss creature
|
||||
if (levelData.HasHuntingGrounds)
|
||||
{
|
||||
spawnChance = 0;
|
||||
Log.Debug("Set spawn chance to 0 due to hunting grounds");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var location in connection.Locations)
|
||||
{
|
||||
var identifier = location.Type.Identifier;
|
||||
if (CompatabilityHelper.Instance.DynamicEuropaInstalled)
|
||||
{
|
||||
// Double spawn chance on routes leading to pirate outposts
|
||||
ModifySpawn("PirateOutpost", 2);
|
||||
|
||||
// Don't spawn on areas leading to military
|
||||
ModifySpawn("Camp", 0);
|
||||
|
||||
ModifySpawn("Base", 0);
|
||||
|
||||
ModifySpawn("Blockade", 0);
|
||||
|
||||
ModifyHusk("HuskgroundsDE", 10f);
|
||||
|
||||
ModifyHusk("OuterHuskLair", 5f);
|
||||
}
|
||||
|
||||
|
||||
// Increased chance to spawn next to natural formations
|
||||
ModifySpawn("None", 1.5f);
|
||||
|
||||
// Increased chance to spawn next to abandoned outposts
|
||||
ModifySpawn("Abandoned", 1.3f);
|
||||
|
||||
// Never spawn if one of the connections is a military outpost
|
||||
ModifySpawn("Military", 0);
|
||||
|
||||
// No chance if city
|
||||
ModifySpawn("City", 0f);
|
||||
|
||||
// Slightly reduced chance if leading to a outpost
|
||||
ModifySpawn("Outpost", 0.25f);
|
||||
|
||||
// Slightly reduced chance if leading to a research outpost
|
||||
ModifySpawn("Research", 0.25f);
|
||||
|
||||
// Slightly reduced chance if leading to a research outpost
|
||||
ModifySpawn("Mine", 0.25f);
|
||||
|
||||
// Never spawn leading to the end
|
||||
ModifySpawn("EndLocation", 0);
|
||||
|
||||
|
||||
void ModifySpawn(string input, float multi)
|
||||
{
|
||||
if (identifier == input) spawnChance *= multi;
|
||||
//Log.Debug($"M SC {input}: {spawnChance}");
|
||||
}
|
||||
|
||||
void ModifyHusk(string input, float multi)
|
||||
{
|
||||
if (identifier == input) spawnChance *= multi;
|
||||
//Log.Debug($"M HC {input}: {spawnChance}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.MoreLevelContent.Config;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics.Collision;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoreLevelContent.Networking;
|
||||
using MoreLevelContent.Shared.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MoreLevelContent.Shared.Generation
|
||||
{
|
||||
abstract partial class TimedEventMapModule : MapModule
|
||||
{
|
||||
public TimedEventMapModule()
|
||||
{
|
||||
InitProjSpecific();
|
||||
}
|
||||
|
||||
public string ForcedMissionIdentifier = "";
|
||||
public bool ForceSpawnMission = false;
|
||||
|
||||
// Networking
|
||||
protected abstract NetEvent EventCreated { get; }
|
||||
//protected abstract NetEvent EventUpdated { get; }
|
||||
|
||||
// Text
|
||||
protected abstract string NewEventText { get; }
|
||||
//protected abstract string UpdatedEventText { get; }
|
||||
protected abstract string EventTag { get; }
|
||||
|
||||
// Config
|
||||
protected abstract int MaxActiveEvents { get; }
|
||||
protected abstract float EventSpawnChance { get; }
|
||||
protected abstract int MinDistance { get; }
|
||||
protected abstract int MaxDistance { get; }
|
||||
protected abstract int MinEventDuration { get; }
|
||||
protected abstract int MaxEventDuration { get; }
|
||||
protected abstract bool ShouldSpawnEventAtStart { get; }
|
||||
protected bool SpawnedEventAtStart
|
||||
{
|
||||
get => GameMain.GameSession.Campaign.CampaignMetadata.GetBoolean($"{EventTag}SpawnedStart", false);
|
||||
set => GameMain.GameSession.Campaign.CampaignMetadata.SetValue($"{EventTag}SpawnedStart", value);
|
||||
}
|
||||
|
||||
private readonly List<Mission> _internalMissionStore = new();
|
||||
|
||||
public override void OnProgressWorld(Map __instance)
|
||||
{
|
||||
foreach (LocationConnection connection in __instance.Connections)
|
||||
{
|
||||
// skip locations that are close
|
||||
if (GameMain.GameSession.Campaign.Map.CurrentLocation.Connections.Contains(connection)) continue;
|
||||
HandleUpdate(connection.LevelData.MLC(), connection);
|
||||
}
|
||||
|
||||
if (ShouldSpawnEventAtStart && !SpawnedEventAtStart)
|
||||
{
|
||||
TrySpawnEvent(GameMain.GameSession.Map, true);
|
||||
SpawnedEventAtStart = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TrySpawnEvent(GameMain.GameSession.Map, false);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPreRoundStart(LevelData levelData)
|
||||
{
|
||||
if (levelData == null) return;
|
||||
if (!Main.IsCampaign) return;
|
||||
|
||||
if (!LevelHasEvent(levelData.MLC()) && !ForceSpawnMission)
|
||||
{
|
||||
Log.Debug($"Level has no {EventTag}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Never try to spawn a timed event on an outpost level
|
||||
if (levelData.Type == LevelData.LevelType.Outpost) return;
|
||||
|
||||
if (TryGetMissionPrefab(levelData, out MissionPrefab prefab))
|
||||
{
|
||||
Log.Debug($"Adding {EventTag} mission");
|
||||
Mission inst = prefab.Instantiate(GameMain.GameSession.Map.SelectedConnection.Locations, Submarine.MainSub);
|
||||
AddExtraMission(inst); // we have to double add missions to make them work correctly
|
||||
_internalMissionStore.Add(inst);
|
||||
Log.Debug($"Added {EventTag} mission to extra missions!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Failed to find any {EventTag} missions!");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool TryGetMissionPrefab(LevelData levelData, out MissionPrefab prefab)
|
||||
{
|
||||
return TryGetMissionByTag(EventTag, levelData, out prefab, ForcedMissionIdentifier);
|
||||
}
|
||||
|
||||
protected void AddExtraMission(Mission mission)
|
||||
{
|
||||
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(GameMain.GameSession.GameMode);
|
||||
_extraMissions.Add(mission);
|
||||
Instance.extraMissions.SetValue(GameMain.GameSession.GameMode, _extraMissions);
|
||||
}
|
||||
|
||||
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
|
||||
{
|
||||
if (!_internalMissionStore.Any()) return;
|
||||
foreach (Mission mission in _internalMissionStore)
|
||||
{
|
||||
AddExtraMission(mission);
|
||||
}
|
||||
_internalMissionStore.Clear();
|
||||
}
|
||||
|
||||
protected abstract void HandleUpdate(LevelData_MLCData data, LocationConnection connection);
|
||||
|
||||
public void TrySpawnEvent(Map __instance, bool force = false)
|
||||
{
|
||||
if (Main.IsClient) return;
|
||||
// Check if we're at the max
|
||||
int activeEvents = __instance.Connections.Where(c => LevelHasEvent(c.LevelData.MLC())).Count();
|
||||
if (activeEvents > MaxActiveEvents)
|
||||
{
|
||||
if (force)
|
||||
{
|
||||
Log.Debug($"Ignoring max {EventTag} cap due to force creation");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"Skipped creating new {EventTag} due to being at the limit ({MaxActiveEvents})");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not, lets roll to see if we should make a new distress signal
|
||||
float chance = Rand.Value(Rand.RandSync.Unsynced);
|
||||
Log.InternalDebug($"{chance} <= {EventSpawnChance} ({chance <= EventSpawnChance}) for {EventTag}");
|
||||
if (chance >= EventSpawnChance && !force) return;
|
||||
|
||||
// Lets get a random instance to use
|
||||
int seed = Rand.GetRNG(Rand.RandSync.Unsynced).Next();
|
||||
Random rand = new MTRandom(seed);
|
||||
|
||||
// Find a location connection to spawn a distress beacon at
|
||||
int wantedEventSpawnDistance = Rand.Range(MinDistance, MaxDistance, Rand.RandSync.Unsynced);
|
||||
LocationConnection targetConnection = WalkConnection(__instance.CurrentLocation, rand, wantedEventSpawnDistance);
|
||||
if (targetConnection == null)
|
||||
{
|
||||
Log.Warn($"Failed to spawn new {EventTag} due to target connection being null.");
|
||||
return;
|
||||
}
|
||||
int duration = rand.Next(MinEventDuration, MaxEventDuration);
|
||||
|
||||
if (!MapDirector.ConnectionIdLookup.ContainsKey(targetConnection)) return; // how does this happen?
|
||||
|
||||
CreateEvent(targetConnection, duration);
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.IsMultiplayer)
|
||||
{
|
||||
// inform clients of the new distress beacon
|
||||
IWriteMessage msg = NetUtil.CreateNetMsg(NetEvent.MAP_SEND_NEWDISTRESS);
|
||||
msg.WriteUInt32((uint)MapDirector.ConnectionIdLookup[targetConnection]);
|
||||
msg.WriteByte((byte)duration);
|
||||
NetUtil.SendAll(msg);
|
||||
}
|
||||
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
|
||||
{
|
||||
campaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
protected abstract bool LevelHasEvent(LevelData_MLCData data);
|
||||
protected void CreateEvent(LocationConnection connection, int eventDuration)
|
||||
{
|
||||
HandleEventCreation(connection.LevelData.MLC(), eventDuration);
|
||||
AddNewsStory(NewEventText, connection);
|
||||
}
|
||||
protected abstract void HandleEventCreation(LevelData_MLCData data, int eventDuration);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user