public LevelData Bake(int?nonce, Random random) { var result = this.Static.MakeLevelData(new Vector2(this.Bounds.Left, this.Bounds.Top), nonce); bool ohgodwhat = random.Next(100) == 0; // :) string pickCrystalColor() { switch (random.Next(15)) { case 0: case 1: case 2: return("blue"); case 3: case 4: case 5: return("red"); case 6: case 7: case 8: return("purple"); case 9: return("rainbow"); default: return("dust"); } } string crystalcolor = pickCrystalColor(); string pickSpinnerColor() { return(random.Next(2) == 0 ? "dust" : "spike"); } string spinnercolor = pickSpinnerColor(); int maxID = 0; var toRemove = new List <EntityData>(); foreach (var e in result.Entities) { maxID = Math.Max(maxID, e.ID); switch (e.Name) { case "spinner": if (ohgodwhat) { crystalcolor = pickCrystalColor(); } if (e.Values == null) { e.Values = new Dictionary <string, object>(); } if (crystalcolor == "dust") { e.Values["dust"] = "true"; } else { e.Values["color"] = crystalcolor; } break; case "trackSpinner": case "rotateSpinner": if (ohgodwhat) { spinnercolor = pickSpinnerColor(); } if (e.Values == null) { e.Values = new Dictionary <string, object>(); } if (spinnercolor == "dust") { e.Values["dust"] = "true"; } break; case "lockBlock": if (!this.UsedKeyholes.Contains(e.ID)) { toRemove.Add(e); } break; } } foreach (var e in toRemove) { result.Entities.Remove(e); } void blockHole(Hole hole) { var topbottom = hole.Side == ScreenDirection.Up || hole.Side == ScreenDirection.Down; var farside = hole.Side == ScreenDirection.Down || hole.Side == ScreenDirection.Right; Vector2 corner; switch (hole.Side) { case ScreenDirection.Up: corner = new Vector2(0, -8); break; case ScreenDirection.Left: corner = new Vector2(-8, 0); break; case ScreenDirection.Down: corner = new Vector2(0, this.Bounds.Height); break; case ScreenDirection.Right: default: corner = new Vector2(this.Bounds.Width, 0); break; } corner += hole.AlongDir.Unit() * hole.LowBound * 8; var e = new EntityData { ID = ++maxID, Name = "invisibleBarrier", Width = !topbottom ? 8 : hole.Size * 8, Height = topbottom ? 8 : hole.Size * 8, Position = corner, Level = result, }; result.Entities.Add(e); } void partiallyBlockHole(Hole hole, Hole hole2) { int diff = hole.Compatible(hole2); var newHighBound = diff + hole2.LowBound - 1; var newHole = new Hole(hole.Side, hole.LowBound, newHighBound, false); //Logger.Log("DEBUG", $"{result.Name}: special-blocking {newHole}"); blockHole(newHole); } void beamHole(Hole hole) { Vector2 center; int rotation; switch (hole.Side) { case ScreenDirection.Up: center = new Vector2(0, 0); rotation = 0; break; case ScreenDirection.Left: center = new Vector2(0, 0); rotation = 270; break; case ScreenDirection.Down: center = new Vector2(0, this.Bounds.Height); rotation = 180; break; case ScreenDirection.Right: default: center = new Vector2(this.Bounds.Width, 0); rotation = 90; break; } center += hole.AlongDir.Unit() * (hole.LowBound * 8 + hole.Size * 4); var e = new EntityData { ID = ++maxID, Name = "lightbeam", Width = hole.Size * 8, Height = 24, Position = center, Level = result, Values = new Dictionary <string, object> { { "rotation", (object)rotation }, } }; result.Entities.Add(e); } void gateTopHole(Hole hole) { if (!hole.LowOpen) { var coord = (hole.LowBound - 1) * 8; var e = new EntityData { Name = "invisibleBarrier", Position = new Vector2(coord, -80), Width = 8, Height = 80, Level = result, ID = ++maxID, }; result.Entities.Add(e); } if (!hole.HighOpen) { var coord = (hole.HighBound + 1) * 8; var e = new EntityData { Name = "invisibleBarrier", Position = new Vector2(coord, -80), Width = 8, Height = 80, Level = result, ID = ++maxID, }; result.Entities.Add(e); } } bool disableDown = true; bool disableUp = true; var unusedHorizontalHoles = new HashSet <Hole>(); var unusedTopHoles = new HashSet <Hole>(); foreach (var hole in this.Static.Holes) { if (hole.Side == ScreenDirection.Left || hole.Side == ScreenDirection.Right) { unusedHorizontalHoles.Add(hole); } if (hole.Side == ScreenDirection.Up) { unusedTopHoles.Add(hole); } } foreach (var node in this.Nodes.Values) { foreach (var edge in node.Edges) { var hole = edge.CorrespondingEdge(node).HoleTarget; var hole2 = edge.OtherEdge(node).HoleTarget; if (hole != null && hole.Side == ScreenDirection.Down) { disableDown = false; } if (hole != null && hole.Side == ScreenDirection.Up) { disableUp = false; } if (hole != null && (hole.Side == ScreenDirection.Left || hole.Side == ScreenDirection.Right)) { unusedHorizontalHoles.Remove(hole); } if (hole != null && hole.Side == ScreenDirection.Up) { unusedTopHoles.Remove(hole); } // Block off holes connected to edges which should not be re-entered if (hole != null && hole2 != null && hole2.Kind == HoleKind.Out) { blockHole(hole); } // Block off pieces of holes which are partially unused if (hole != null && hole2 != null && hole.Size > hole2.Size) { partiallyBlockHole(hole, hole2); } // Add beams var shine = RandoModule.Instance.Settings.Lights; if ((shine == ShineLights.On || (shine == ShineLights.Hubs && this.Static.Hub)) && hole != null && hole2 != null && (hole.Kind == HoleKind.Out || hole.Kind == HoleKind.InOut) && (hole2.Kind == HoleKind.In || hole2.Kind == HoleKind.Unknown || hole2.Kind == HoleKind.InOut)) { beamHole(hole); } } foreach (var kv in node.Collectables) { string name = null; switch (kv.Value) { case LinkedNode.LinkedCollectable.Key: name = "key"; break; case LinkedNode.LinkedCollectable.Strawberry: case LinkedNode.LinkedCollectable.WingedStrawberry: name = "strawberry"; break; } var e = new EntityData { ID = ++maxID, Name = name, Level = result, Position = kv.Key.Position, }; if (kv.Value == LinkedNode.LinkedCollectable.WingedStrawberry) { e.Values["winged"] = "true"; } result.Entities.Add(e); } } result.DisableDownTransition = disableDown; if (disableUp) { new DynData <LevelData>(result).Set("DisableUpTransition", true); } foreach (var hole in unusedHorizontalHoles) { blockHole(hole); } foreach (var hole in unusedTopHoles) { gateTopHole(hole); } return(result); }
private void ProcessSubroom(StaticNode node, RandoConfigRoom config) { if (node.Name != "main" && config.Tweaks != null) { throw new Exception("Config error: you have a subroom with tweaks in it"); } foreach (RandoConfigHole holeConfig in config.Holes ?? new List <RandoConfigHole>()) { Hole matchedHole = null; int remainingMatches = holeConfig.Idx; foreach (Hole hole in this.Holes) { if (hole.Side == holeConfig.Side) { if (remainingMatches == 0) { matchedHole = hole; break; } else { remainingMatches--; } } } if (matchedHole == null) { throw new Exception($"Could not find the hole identified by area:{this.Area} room:{config.Room} side:{holeConfig.Side} idx:{holeConfig.Idx}"); } //Logger.Log("randomizer", $"Matching {roomConfig.Room} {holeConfig.Side} {holeConfig.Idx} to {matchedHole}"); matchedHole.Kind = holeConfig.Kind; matchedHole.Launch = holeConfig.Launch; if (holeConfig.LowBound != null) { matchedHole.LowBound = (int)holeConfig.LowBound; } if (holeConfig.HighBound != null) { matchedHole.HighBound = (int)holeConfig.HighBound; } if (holeConfig.HighOpen != null) { matchedHole.HighOpen = (bool)holeConfig.HighOpen; } if (holeConfig.Kind != HoleKind.None) { node.Edges.Add(new StaticEdge() { FromNode = node, HoleTarget = matchedHole, ReqIn = this.ProcessReqs(holeConfig.ReqIn, matchedHole, false), ReqOut = this.ProcessReqs(holeConfig.ReqOut, matchedHole, true) }); } } foreach (var edge in config.InternalEdges ?? new List <RandoConfigInternalEdge>()) { StaticNode toNode; if (edge.To != null) { toNode = this.Nodes[edge.To]; } else if (edge.Split != null) { if (node.Edges.Count != 2) { throw new Exception($"[{this.Name}.{node.Name}] Cannot split: must have exactly two edges"); } toNode = new StaticNode() { Name = node.Name + "_autosplit", ParentRoom = node.ParentRoom }; if (node.ParentRoom.Nodes.ContainsKey(toNode.Name)) { throw new Exception($"[{this.Name}.{node.Name}] You may only autosplit a room once"); } node.ParentRoom.Nodes[toNode.Name] = toNode; bool firstMain; var first = node.Edges[0].HoleTarget; var second = node.Edges[1].HoleTarget; switch (edge.Split) { case RandoConfigInternalEdge.SplitKind.BottomToTop: firstMain = first.Side == ScreenDirection.Down || second.Side == ScreenDirection.Up || (first.Side != ScreenDirection.Up && second.Side != ScreenDirection.Down && first.HighBound > second.HighBound); break; case RandoConfigInternalEdge.SplitKind.TopToBottom: firstMain = first.Side == ScreenDirection.Up || second.Side == ScreenDirection.Down || (first.Side != ScreenDirection.Down && second.Side != ScreenDirection.Up && first.LowBound < second.LowBound); break; case RandoConfigInternalEdge.SplitKind.RightToLeft: firstMain = first.Side == ScreenDirection.Right || second.Side == ScreenDirection.Left || (first.Side != ScreenDirection.Left && second.Side != ScreenDirection.Right && first.HighBound > second.HighBound); break; case RandoConfigInternalEdge.SplitKind.LeftToRight: default: firstMain = first.Side == ScreenDirection.Left || second.Side == ScreenDirection.Right || (first.Side != ScreenDirection.Right && second.Side != ScreenDirection.Left && first.LowBound < second.LowBound); break; } var secondary = firstMain ? node.Edges[1] : node.Edges[0]; node.Edges.Remove(secondary); toNode.Edges.Add(secondary); secondary.FromNode = toNode; } else if (edge.Collectable != null) { toNode = new StaticNode() { Name = node.Name + "_coll" + edge.Collectable.ToString(), ParentRoom = node.ParentRoom }; if (node.ParentRoom.Nodes.ContainsKey(toNode.Name)) { throw new Exception($"[{this.Name}.{node.Name}] You may only autosplit a room once"); } node.ParentRoom.Nodes[toNode.Name] = toNode; var thing = this.Collectables[edge.Collectable.Value]; if (thing.ParentNode != null) { throw new Exception($"[{this.Name}.{node.Name}] Can only assign a collectable to one owner"); } thing.ParentNode = toNode; toNode.Collectables.Add(thing); } else { throw new Exception($"[{this.Name}.{node.Name}] Internal edge must have either To or Split or Collectable"); } var reqIn = this.ProcessReqs(edge.ReqIn, null, false); var reqOut = this.ProcessReqs(edge.ReqOut, null, true); var forward = new StaticEdge() { FromNode = node, NodeTarget = toNode, ReqIn = reqIn, ReqOut = reqOut }; var reverse = new StaticEdgeReversed(forward, node); node.Edges.Add(forward); toNode.Edges.Add(reverse); } foreach (var col in config.Collectables) { if (col.Idx != null) { var thing = this.Collectables[col.Idx.Value]; if (thing.ParentNode != null) { throw new Exception($"[{this.Name}.{node.Name}] Can only assign a collectable to one owner"); } thing.ParentNode = node; thing.MustFly = col.MustFly; node.Collectables.Add(thing); } else if (col.X != null && col.Y != null) { node.Collectables.Add(new StaticCollectable { ParentNode = node, Position = new Vector2((float)col.X.Value, (float)col.Y.Value), MustFly = col.MustFly }); } else { throw new Exception($"[{this.Name}.{node.Name}] Collectable must specify Idx or X/Y"); } } }
private Requirement ProcessReqs(RandoConfigReq config, Hole matchHole = null, bool isOut = false) { if (matchHole != null) { if (matchHole.Kind == HoleKind.None) { return(new Impossible()); } if (isOut && (matchHole.Kind == HoleKind.In || matchHole.Kind == HoleKind.Unknown)) { return(new Impossible()); } if (!isOut && matchHole.Kind == HoleKind.Out) { return(new Impossible()); } if (config == null) { return(new Possible()); } } if (config == null) { return(new Impossible()); } var conjunction = new List <Requirement>(); if (config.And != null) { var children = new List <Requirement>(); foreach (var childconfig in config.And) { children.Add(this.ProcessReqs(childconfig)); } conjunction.Add(new Conjunction(children)); } if (config.Or != null) { var children = new List <Requirement>(); foreach (var childconfig in config.Or) { children.Add(this.ProcessReqs(childconfig)); } conjunction.Add(new Disjunction(children)); } if (config.Dashes != null) { conjunction.Add(new DashRequirement(config.Dashes.Value)); } // not nullable conjunction.Add(new SkillRequirement(config.Difficulty)); if (config.Key) { if (config.KeyholeID == null) { throw new Exception("Config error: Key: true without KeyholeID"); } conjunction.Add(new KeyRequirement(config.KeyholeID.Value)); } if (conjunction.Count == 0) { throw new Exception("this should be unreachable"); } else if (conjunction.Count == 1) { return(conjunction[0]); } return(new Conjunction(conjunction)); }
public int Compatible(Hole other) { int alignLow() { return(this.LowBound - other.LowBound); } int alignHigh() { return(this.HighBound - other.HighBound); } if (other.Side == ScreenDirection.Up && this.Side != ScreenDirection.Up && this.Side != ScreenDirection.Left) { return(-other.Compatible(this)); } if (other.Side == ScreenDirection.Left && this.Side != ScreenDirection.Left && this.Side != ScreenDirection.Up) { return(-other.Compatible(this)); } if (this.Side == ScreenDirection.Up && other.Side == ScreenDirection.Down) { // Vertical transitions if (this.Launch != null) { if (other.Launch != null) { if (other.Launch == -1) { return(INCOMPATIBLE); } return(this.Launch.Value - other.Launch.Value); } else if (other.Closed) { return(this.Launch.Value - (other.LowBound + other.HighBound) / 2); } else if (other.HighOpen) { return(this.Launch.Value - (other.LowBound + 2)); } else if (other.LowOpen) { return(this.Launch.Value - (other.HighBound - 2)); } } else if (this.BothOpen || other.BothOpen) { // if either is open on both ends, they must line up perfectly if (this.BothOpen && other.BothOpen && this.Size == other.Size) { return(0); } } else if (this.HalfOpen || other.HalfOpen) { // if either is half-open, they must be the same half open if (this.LowOpen == other.LowOpen) { return(this.LowOpen ? alignHigh() : alignLow()); } } else { // Only remaining option is both closed. they must be the same size if (this.Size == other.Size) { return(alignLow()); } } } if (this.Side == ScreenDirection.Left && other.Side == ScreenDirection.Right) { // Horizontal transitions if (this.HighOpen || other.HighOpen) { // if either is open on the bottom, they both must be open on the bottom and we align the death planes // this is kind of a questionable choice all around tbh // maybe additionally restrict that sizes must be the same? if (this.HighOpen && other.HighOpen) { return(alignHigh()); } } else if (this.LowOpen || other.LowOpen) { // if either is open on the top, the other must also be open on the top OR it must be sufficiently tall if (this.LowOpen && other.LowOpen) { return(alignHigh()); } else if (this.Closed && this.Size > 3) { return(alignHigh()); } else if (other.Closed && other.Size > 3) { return(alignHigh()); } } else { // only remaining option is both closed. they must be the same size OR sufficiently tall if (this.Size == other.Size) { return(alignHigh()); } else if (this.Size > 3 && other.Size > 3) { return(alignHigh()); } } } return(INCOMPATIBLE); }