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(OnRiftInteracted); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); 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(); 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 ent, ref InteractHandEvent args) { if (HasComp(args.User)) return; if (!ent.Comp.CanCreate) return; var nextLevel = 1; var originMap = Transform(ent).MapUid; if (TryComp(originMap, out var demiplane)) { nextLevel = demiplane.Level + 1; } _map.CreateMap(out var mapId, runMapInit: false); var mapUid = _map.GetMap(mapId); EnsureComp(mapUid).Level = nextLevel; var limits = new Dictionary, 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; } /// /// Returns a suitable location for the specified difficulty level. /// public CP14ProceduralLocationPrototype SelectLocation(int level) { CP14ProceduralLocationPrototype? selectedConfig = null; HashSet suitableConfigs = new(); foreach (var locationConfig in _proto.EnumeratePrototypes()) { 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; } /// /// Returns a set of modifiers under the specified difficulty level that are appropriate for the specified location /// public List> SelectModifiers( int level, CP14ProceduralLocationPrototype location, Dictionary, float> modifierLimits) { List> selectedModifiers = new(); //Modifier generation Dictionary suitableModifiersWeights = new(); foreach (var modifier in _proto.EnumeratePrototypes()) { 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, 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; } /// /// Optimization moment: avoid re-indexing for weight selection /// private static CP14ProceduralModifierPrototype ModifierPick( Dictionary 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 { }