Files
crystall-punk-14/Content.Server/_CP14/Procedural/Demiplane/CP14DemiplaneSystem.cs
Red b60ceb38ca Demiplanes redesign again (#1616)
* global demi refactor

* rebalance, smaller demiplanes

* remove outdated connections

* audio design + room coords fix

* Update CP14Rooms.cs

* Update wastelands.yml

* hotfixes

* Update CP14SpellThrowFromUser.cs

* Update sky_lightning.yml
2025-08-05 12:15:24 +03:00

314 lines
9.7 KiB
C#

using System.Linq;
using Content.Server._CP14.Procedural.Demiplane.Components;
using Content.Shared._CP14.Procedural.Prototypes;
using Content.Shared.Interaction;
using Content.Shared.Teleportation.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server._CP14.Procedural.Demiplane;
public sealed class CP14DemiplaneSystem : EntitySystem
{
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly CP14LocationGenerationSystem _generation = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MetaDataSystem _meta = default!;
[Dependency] private readonly LinkedEntitySystem _link = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CP14DemiplaneRiftComponent, InteractHandEvent>(OnRiftInteracted);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<CP14DemiplaneRiftComponent>();
while (query.MoveNext(out var uid, out var demiplaneRift))
{
if (demiplaneRift.ScanningTargetMap is null)
continue;
if (_timing.CurTime < demiplaneRift.NextScanTime)
continue;
demiplaneRift.NextScanTime = _timing.CurTime + TimeSpan.FromSeconds(5);
var targetQuery = EntityQueryEnumerator<CP14DemiplaneEnterPointComponent>();
while (targetQuery.MoveNext(out var enterUid, out var enterComp))
{
if (Transform(enterUid).MapUid != demiplaneRift.ScanningTargetMap)
continue;
//Remove awaiting
QueueDel(demiplaneRift.AwaitingEntity);
//Start connection
var portal1 = SpawnAtPosition(demiplaneRift.PortalProto, Transform(enterUid).Coordinates);
var portal2 = SpawnAtPosition(demiplaneRift.PortalProto, Transform(uid).Coordinates);
_link.TryLink(portal1, portal2, true);
//Delete self
QueueDel(uid);
QueueDel(enterUid);
return;
}
}
}
private void OnRiftInteracted(Entity<CP14DemiplaneRiftComponent> ent, ref InteractHandEvent args)
{
if (HasComp<CP14DemiplaneBlockInteractionsComponent>(args.User))
return;
if (!ent.Comp.CanCreate)
return;
var nextLevel = 1;
var originMap = Transform(ent).MapUid;
if (TryComp<CP14DemiplaneMapComponent>(originMap, out var demiplane))
{
nextLevel = demiplane.Level + 1;
}
_map.CreateMap(out var mapId, runMapInit: false);
var mapUid = _map.GetMap(mapId);
EnsureComp<CP14DemiplaneMapComponent>(mapUid).Level = nextLevel;
var limits = new Dictionary<ProtoId<CP14ProceduralModifierCategoryPrototype>, float>
{
{ "Danger", Math.Max(nextLevel * 0.2f, 0.5f) },
{ "GhostRoleDanger", 1f },
{ "Reward", Math.Max(nextLevel * 0.3f, 0.5f) },
{ "Ore", Math.Max(nextLevel * 0.5f, 1f) },
{ "Fun", 1f },
{ "Weather", 1f },
{ "MapLight", 1f },
{ "Passage", 1f },
};
var nextLocation = SelectLocation(nextLevel);
var nextModifiers = SelectModifiers(nextLevel, nextLocation, limits);
nextModifiers.Add("CP14DemiplanEnterRoom"); //HARDCODE, BOO
_meta.SetEntityName(mapUid, $"Demi: [{nextLevel}] - {nextLocation.LocationConfig.Id}");
_generation.GenerateLocation(
mapUid,
mapId,
nextLocation,
nextModifiers);
var awaiting = SpawnAtPosition(ent.Comp.AwaitingProto, Transform(ent).Coordinates);
ent.Comp.AwaitingEntity = awaiting;
ent.Comp.ScanningTargetMap = mapUid;
ent.Comp.CanCreate = false;
}
/// <summary>
/// Returns a suitable location for the specified difficulty level.
/// </summary>
public CP14ProceduralLocationPrototype SelectLocation(int level)
{
CP14ProceduralLocationPrototype? selectedConfig = null;
HashSet<CP14ProceduralLocationPrototype> suitableConfigs = new();
foreach (var locationConfig in _proto.EnumeratePrototypes<CP14ProceduralLocationPrototype>())
{
suitableConfigs.Add(locationConfig);
}
while (suitableConfigs.Count > 0)
{
var randomConfig = _random.Pick(suitableConfigs);
var passed = true;
//Random prob filter
if (passed)
{
if (!_random.Prob(randomConfig.GenerationProb))
{
passed = false;
}
}
//Levels filter
if (passed)
{
if (level < randomConfig.Levels.Min || level > randomConfig.Levels.Max)
{
passed = false;
}
}
if (!passed)
{
suitableConfigs.Remove(randomConfig);
continue;
}
selectedConfig = randomConfig;
break;
}
if (selectedConfig is null)
throw new Exception($"No suitable procedural location config found for level {level}!");
return selectedConfig;
}
/// <summary>
/// Returns a set of modifiers under the specified difficulty level that are appropriate for the specified location
/// </summary>
public List<ProtoId<CP14ProceduralModifierPrototype>> SelectModifiers(
int level,
CP14ProceduralLocationPrototype location,
Dictionary<ProtoId<CP14ProceduralModifierCategoryPrototype>, float> modifierLimits)
{
List<ProtoId<CP14ProceduralModifierPrototype>> selectedModifiers = new();
//Modifier generation
Dictionary<CP14ProceduralModifierPrototype, float> suitableModifiersWeights = new();
foreach (var modifier in _proto.EnumeratePrototypes<CP14ProceduralModifierPrototype>())
{
var passed = true;
//Random prob filter
if (passed)
{
if (!_random.Prob(modifier.GenerationProb))
{
passed = false;
}
}
//Levels filter
if (passed)
{
if (level < modifier.Levels.Min || level > modifier.Levels.Max)
{
passed = false;
}
}
//Tag blacklist filter
foreach (var configTag in location.Tags)
{
if (modifier.BlacklistTags.Count != 0 && modifier.BlacklistTags.Contains(configTag))
{
passed = false;
break;
}
}
//Tag required filter
if (passed)
{
foreach (var reqTag in modifier.RequiredTags)
{
if (!location.Tags.Contains(reqTag))
{
passed = false;
break;
}
}
}
if (passed)
suitableModifiersWeights.Add(modifier, modifier.GenerationWeight);
}
//Limits calculation
Dictionary<ProtoId<CP14ProceduralModifierCategoryPrototype>, float> limits = new();
foreach (var limit in modifierLimits)
{
limits.Add(limit.Key, limit.Value);
}
while (suitableModifiersWeights.Count > 0)
{
var selectedModifier = ModifierPick(suitableModifiersWeights, _random);
//Fill location under limits
var passed = true;
foreach (var category in selectedModifier.Categories)
{
if (!limits.ContainsKey(category.Key))
{
suitableModifiersWeights.Remove(selectedModifier);
passed = false;
break;
}
if (limits[category.Key] - category.Value < 0)
{
suitableModifiersWeights.Remove(selectedModifier);
passed = false;
break;
}
}
if (!passed)
continue;
selectedModifiers.Add(selectedModifier);
foreach (var category in selectedModifier.Categories)
{
limits[category.Key] -= category.Value;
}
if (selectedModifier.Unique)
suitableModifiersWeights.Remove(selectedModifier);
}
return selectedModifiers;
}
/// <summary>
/// Optimization moment: avoid re-indexing for weight selection
/// </summary>
private static CP14ProceduralModifierPrototype ModifierPick(
Dictionary<CP14ProceduralModifierPrototype, float> weights,
IRobustRandom random)
{
var picks = weights;
var sum = picks.Values.Sum();
var accumulated = 0f;
var rand = random.NextFloat() * sum;
foreach (var (key, weight) in picks)
{
accumulated += weight;
if (accumulated >= rand)
{
return key;
}
}
// Shouldn't happen
throw new InvalidOperationException($"Invalid weighted pick in CP14DemiplanSystem.Generation!");
}
}
public sealed class CP14LocationGeneratedEvent : EntityEventArgs
{
}