diff --git a/src/main/java/StevenDimDoors/experimental/DirectedGraph.java b/src/main/java/StevenDimDoors/experimental/DirectedGraph.java index cd56c75..df2aa6c 100644 --- a/src/main/java/StevenDimDoors/experimental/DirectedGraph.java +++ b/src/main/java/StevenDimDoors/experimental/DirectedGraph.java @@ -165,8 +165,8 @@ public class DirectedGraph { Edge innerEdge = (Edge) edge; - // Check that this node actually belongs to this graph instance. - // Accepting foreign nodes could corrupt the graph's internal state. + // Check that this edge actually belongs to this graph instance. + // Accepting foreign edges could corrupt the graph's internal state. if (innerEdge.graphEntry.owner() != edges) { throw new IllegalArgumentException("The specified edge does not belong to this graph."); diff --git a/src/main/java/StevenDimDoors/experimental/LinkPlan.java b/src/main/java/StevenDimDoors/experimental/LinkPlan.java new file mode 100644 index 0000000..fbd90c1 --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/LinkPlan.java @@ -0,0 +1,120 @@ +package StevenDimDoors.experimental; + +import StevenDimDoors.mod_pocketDim.Point3D; + +public class LinkPlan +{ + private RoomData source; + private RoomData destination; + private Point3D sourcePoint; + private Point3D destinationPoint; + private final boolean entrance; + private final boolean internal; + + private LinkPlan(RoomData source, boolean entrance, boolean internal) + { + if (source == null) + { + throw new IllegalArgumentException("source cannot be null."); + } + this.source = source; + this.destination = null; + this.sourcePoint = null; + this.destinationPoint = null; + this.entrance = entrance; + this.internal = internal; + } + + public static LinkPlan createInternalLink(RoomData source) + { + LinkPlan plan = new LinkPlan(source, false, true); + source.getOutboundLinks().add(plan); + return plan; + } + + public static LinkPlan createEntranceLink(RoomData source) + { + LinkPlan plan = new LinkPlan(source, true, false); + source.getOutboundLinks().add(plan); + return plan; + } + + public static LinkPlan createDungeonLink(RoomData source) + { + LinkPlan plan = new LinkPlan(source, false, false); + source.getOutboundLinks().add(plan); + return plan; + } + + public RoomData source() + { + return this.source; + } + + public RoomData destination() + { + return this.destination; + } + + public boolean isEntrance() + { + return entrance; + } + + public boolean isInternal() + { + return internal; + } + + public void remove() + { + if (source != null) + { + source.getOutboundLinks().remove(this); + source = null; + } + if (destination != null) + { + destination.getInboundLinks().remove(this); + destination = null; + } + } + + public void setDestination(RoomData destination) + { + if (!internal) + { + throw new IllegalStateException("LinkPlan.setDestination() is only applicable to internal links."); + } + if (this.destination != null) + { + throw new IllegalStateException("destination can only be set once."); + } + if (destination == null) + { + throw new IllegalArgumentException("destination cannot be null."); + } + this.destination = destination; + destination.getInboundLinks().add(this); + } + + public Point3D sourcePoint() + { + return this.sourcePoint; + } + + public Point3D destinationPoint() + { + return this.destinationPoint; + } + + public void setSourcePoint(Point3D value) + { + this.sourcePoint = value; + } + + public void setDestinationPoint(Point3D value) + { + this.destinationPoint = value; + } +} diff --git a/src/main/java/StevenDimDoors/experimental/MazeBuilder.java b/src/main/java/StevenDimDoors/experimental/MazeBuilder.java index 9e31e9a..f7a434c 100644 --- a/src/main/java/StevenDimDoors/experimental/MazeBuilder.java +++ b/src/main/java/StevenDimDoors/experimental/MazeBuilder.java @@ -1,63 +1,215 @@ package StevenDimDoors.experimental; +import java.util.ArrayList; import java.util.Random; +import java.util.Stack; import net.minecraft.block.Block; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import StevenDimDoors.experimental.decorators.BaseDecorator; +import StevenDimDoors.experimental.decorators.DecoratorFinder; import StevenDimDoors.mod_pocketDim.Point3D; +import StevenDimDoors.mod_pocketDim.config.DDProperties; +import StevenDimDoors.mod_pocketDim.core.DimLink; +import StevenDimDoors.mod_pocketDim.core.LinkTypes; +import StevenDimDoors.mod_pocketDim.core.NewDimData; +import StevenDimDoors.mod_pocketDim.core.PocketManager; public class MazeBuilder { + private static final int POCKET_WALL_GAP = 4; + private static final int DECORATION_CHANCE = 1; + private static final int MAX_DECORATION_CHANCE = 3; + private MazeBuilder() { } - public static void generate(World world, int x, int y, int z, Random random) + public static void generate(World world, int x, int y, int z, Random random, DDProperties properties) { + // ISSUE FOR LATER: The room needs to be shifted so as to be centered on its entrance + MazeDesign design = MazeDesigner.generate(random); Point3D offset = new Point3D(x - design.width() / 2, y - design.height() - 1, z - design.length() / 2); SphereDecayOperation decay = new SphereDecayOperation(random, 0, 0, Block.stoneBrick.blockID, 2); - buildRooms(design.getRoomGraph(), world, offset); - carveDoorways(design.getRoomGraph(), world, offset, decay, random); - - //placeDoors(design, world, offset); - + buildRooms(design.getLayout(), world, offset); + carveDoorways(design.getLayout(), world, offset, decay, random); applyRandomDestruction(design, world, offset, decay, random); + decorateRooms(design.getLayout(), world, offset, random, properties); + buildPocketWalls(design, world, offset, properties); } private static void applyRandomDestruction(MazeDesign design, World world, Point3D offset, SphereDecayOperation decay, Random random) { - //final int DECAY_BOX_SIZE = 8 + final int DECAY_BOX_SIZE = 7; + final int DECAY_OPERATIONS = 5 + random.nextInt(5); + final int DECAY_ATTEMPTS = 20; + + int x, y, z; + int successes = 0; + int attempts = 0; + PartitionNode root = design.getRootPartition(); + + for (; successes < DECAY_OPERATIONS && attempts < DECAY_ATTEMPTS; attempts++) + { + // Select the coordinates at which to apply the decay operation + x = random.nextInt(design.width()) - DECAY_BOX_SIZE / 2; + y = random.nextInt(design.height()) - DECAY_BOX_SIZE / 2; + z = random.nextInt(design.length()) - DECAY_BOX_SIZE / 2; + + // Check that the decay operation would not impact any protected areas + // and mark the affected areas as decayed + if (markDecayArea(x, y, z, DECAY_BOX_SIZE, root)) + { + // Apply decay + decay.apply(world, offset.getX() + x, offset.getY() + y, offset.getZ() + z, + DECAY_BOX_SIZE, DECAY_BOX_SIZE, DECAY_BOX_SIZE); + successes++; + } + } + } + + private static boolean markDecayArea(int x, int y, int z, int DECAY_BOX_SIZE, PartitionNode root) + { + // Check if a given PartitionNode intersects the decay area. If it's a leaf, then check + // if it's protected or not. Otherwise, check its children. The specific area is valid + // if and only if there are no protected rooms and at least one (unprotected) room in it. + // Also list the unprotected rooms to mark them if the decay operation will proceed. + + RoomData room; + PartitionNode partition; + ArrayList targets = new ArrayList(); + Stack> nodes = new Stack>(); + BoundingBox decayBounds = new BoundingBox(x, y, z, DECAY_BOX_SIZE, DECAY_BOX_SIZE, DECAY_BOX_SIZE); + + // Use depth-first search to explore all intersecting partitions + nodes.push(root); + while (!nodes.isEmpty()) + { + partition = nodes.pop(); + if (decayBounds.intersects(partition)) + { + if (partition.isLeaf()) + { + room = partition.getData(); + if (room.isProtected()) + return false; + targets.add(room); + } + else + { + if (partition.leftChild() != null) + nodes.push(partition.leftChild()); + if (partition.rightChild() != null) + nodes.push(partition.rightChild()); + } + } + } + // If execution has reached this point, then there were no protected rooms. + // Mark all intersecting rooms as decayed. + for (RoomData target : targets) + { + target.setDecayed(true); + } + return !targets.isEmpty(); } - private static void buildRooms(DirectedGraph roomGraph, World world, Point3D offset) + private static void buildRooms(DirectedGraph layout, World world, Point3D offset) { - for (IGraphNode node : roomGraph.nodes()) + for (IGraphNode node : layout.nodes()) { - PartitionNode room = node.data(); + PartitionNode room = node.data().getPartitionNode(); buildBox(world, offset, room.minCorner(), room.maxCorner(), Block.stoneBrick.blockID, 0); } } - private static void carveDoorways(DirectedGraph roomGraph, World world, + private static void decorateRooms(DirectedGraph layout, + World world, Point3D offset, Random random, DDProperties properties) + { + RoomData room; + BaseDecorator decorator; + PartitionNode partition; + ArrayList links = new ArrayList(); + + // Iterate over all rooms and apply decorators + for (IGraphNode node : layout.nodes()) + { + room = node.data(); + partition = room.getPartitionNode(); + links.addAll(room.getOutboundLinks()); + // Protected rooms must be decorated because they have links. + // Otherwise, choose randomly whether to decorate. + if (room.isProtected() || random.nextInt(MAX_DECORATION_CHANCE) < DECORATION_CHANCE) + { + decorator = DecoratorFinder.find(room, random); + if (decorator != null) + { + decorator.decorate(room, world, offset, random, properties); + } + } + } + + // Iterate over all link plans and place links in the world + NewDimData dimension = PocketManager.getDimensionData(world); + for (LinkPlan plan : links) + { + createLinkFromPlan(plan, dimension, world); + } + } + + private static void createLinkFromPlan(LinkPlan plan, NewDimData dimension, World world) + { + // TODO: Support entrances! Right now we'll treat them as dungeon doors for testing + + DimLink link; + Point3D source; + Point3D destination; + int orientation; + + source = plan.sourcePoint(); + orientation = world.getBlockMetadata(source.getX(), source.getY(), source.getZ()) & 3; + + // Check the link type and set the destination accordingly + if (plan.isInternal()) + { + // Create a link between sections + destination = plan.destinationPoint(); + link = dimension.createLink(source.getX(), source.getY(), source.getZ(), LinkTypes.DUNGEON, orientation); + dimension.setDestination(link, destination.getX(), destination.getY(), destination.getZ()); + } + else + { + // Create a dungeon link + dimension.createLink(source.getX(), source.getY(), source.getZ(), LinkTypes.DUNGEON, orientation); + } + } + + private static void carveDoorways(DirectedGraph layout, World world, Point3D offset, SphereDecayOperation decay, Random random) { char axis; Point3D lower; DoorwayData doorway; - for (IGraphNode node : roomGraph.nodes()) + for (IGraphNode node : layout.nodes()) { - for (IEdge passage : node.outbound()) + for (IEdge passage : node.outbound()) { + // Carve out the passage doorway = passage.data(); axis = doorway.axis(); lower = doorway.minCorner(); carveDoorway(world, axis, offset.getX() + lower.getX(), offset.getY() + lower.getY(), offset.getZ() + lower.getZ(), doorway.width(), doorway.height(), doorway.length(), decay, random); + + // If this is a vertical passage, then mark the upper room as decayed + if (axis == DoorwayData.Y_AXIS) + { + passage.tail().data().setDecayed(true); + } } } } @@ -68,6 +220,8 @@ public class MazeBuilder final int MIN_DOUBLE_DOOR_SPAN = 10; int gap; + int rx; + int rz; switch (axis) { case DoorwayData.X_AXIS: @@ -132,9 +286,10 @@ public class MazeBuilder { gap = 6; } - decay.apply(world, - x + random.nextInt(width - gap - 1) + 1, y - 1, - z + random.nextInt(length - gap - 1) + 1, gap, 4, gap); + rx = x + random.nextInt(width - gap - 1) + 1; + rz = z + random.nextInt(length - gap - 1) + 1; + carveHole(world, rx + gap / 2, y, rz + gap / 2); + decay.apply(world, rx, y - 1, rz, gap, 4, gap); } else { @@ -165,7 +320,19 @@ public class MazeBuilder setBlockDirectly(world, x, y, z, 0, 0); setBlockDirectly(world, x, y + 1, z, 0, 0); } - + + private static void buildPocketWalls(MazeDesign design, World world, Point3D offset, DDProperties properties) + { + // Build the inner Fabric of Reality box + Point3D minCorner = new Point3D(-POCKET_WALL_GAP - 1, -POCKET_WALL_GAP - 1, -POCKET_WALL_GAP - 1); + Point3D maxCorner = new Point3D(design.width() + POCKET_WALL_GAP, design.height() + POCKET_WALL_GAP, design.length() + POCKET_WALL_GAP); + buildBox(world, offset, minCorner, maxCorner, properties.FabricBlockID, 0); + + // Build the outer Eternal Fabric box + minCorner.add(-1, -1, -1); + maxCorner.add(1, 1, 1); + buildBox(world, offset, minCorner, maxCorner, properties.PermaFabricBlockID, 0); + } private static void buildBox(World world, Point3D offset, Point3D minCorner, Point3D maxCorner, int blockID, int metadata) { @@ -189,15 +356,15 @@ public class MazeBuilder } for (x = minX; x <= maxX; x++) { - for (y = minY; y <= maxY; y++) + for (y = minY + 1; y < maxY; y++) { setBlockDirectly(world, x, y, minZ, blockID, metadata); setBlockDirectly(world, x, y, maxZ, blockID, metadata); } } - for (z = minZ; z <= maxZ; z++) + for (z = minZ + 1; z < maxZ; z++) { - for (y = minY; y <= maxY; y++) + for (y = minY + 1; y < maxY; y++) { setBlockDirectly(world, minX, y, z, blockID, metadata); setBlockDirectly(world, maxX, y, z, blockID, metadata); diff --git a/src/main/java/StevenDimDoors/experimental/MazeDesign.java b/src/main/java/StevenDimDoors/experimental/MazeDesign.java index e7d4b4c..16c190b 100644 --- a/src/main/java/StevenDimDoors/experimental/MazeDesign.java +++ b/src/main/java/StevenDimDoors/experimental/MazeDesign.java @@ -4,37 +4,23 @@ import java.util.ArrayList; public class MazeDesign { - private PartitionNode root; - private DirectedGraph rooms; - private ArrayList> cores; - private ArrayList protectedAreas; + private PartitionNode root; + private DirectedGraph layout; - public MazeDesign(PartitionNode root, DirectedGraph rooms, - ArrayList> cores) + public MazeDesign(PartitionNode root, DirectedGraph layout) { this.root = root; - this.rooms = rooms; - this.cores = cores; + this.layout = layout; } - public PartitionNode getRootPartition() + public PartitionNode getRootPartition() { return root; } - public DirectedGraph getRoomGraph() + public DirectedGraph getLayout() { - return rooms; - } - - public ArrayList> getCoreNodes() - { - return cores; - } - - public ArrayList getProtectedAreas() - { - return protectedAreas; + return layout; } public int width() diff --git a/src/main/java/StevenDimDoors/experimental/MazeDesigner.java b/src/main/java/StevenDimDoors/experimental/MazeDesigner.java index 289931a..53113cb 100644 --- a/src/main/java/StevenDimDoors/experimental/MazeDesigner.java +++ b/src/main/java/StevenDimDoors/experimental/MazeDesigner.java @@ -1,8 +1,9 @@ package StevenDimDoors.experimental; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.Queue; import java.util.Random; @@ -25,54 +26,46 @@ public class MazeDesigner public static MazeDesign generate(Random random) { // Construct a random binary space partitioning of our maze volume - PartitionNode root = partitionRooms(MAZE_WIDTH, MAZE_HEIGHT, MAZE_LENGTH, SPLIT_COUNT, random); + PartitionNode root = partitionRooms(MAZE_WIDTH, MAZE_HEIGHT, MAZE_LENGTH, SPLIT_COUNT, random); - // List all the leaf nodes of the partition tree, which denote individual rooms - ArrayList partitions = new ArrayList(1 << SPLIT_COUNT); - listRoomPartitions(root, partitions); + // Attach rooms to all the leaf nodes of the partition tree + ArrayList rooms = new ArrayList(1 << SPLIT_COUNT); + attachRooms(root, rooms); + + // Shuffle the list of rooms so that they're not listed in any ordered way in the room graph + // This is the only convenient way of randomizing the maze sections generated later + Collections.shuffle(rooms, random); // Construct an adjacency graph of the rooms we've carved out. Two rooms are // considered adjacent if and only if a doorway could connect them. Their // common boundary must be large enough for a doorway. - DirectedGraph rooms = createRoomGraph(root, partitions, random); + DirectedGraph layout = createRoomGraph(root, rooms, random); // Cut out random subgraphs from the adjacency graph - ArrayList> cores = createMazeSections(rooms, random); + ArrayList cores = createMazeSections(layout, random); // Remove unnecessary passages through floors/ceilings and some from the walls - for (IGraphNode core : cores) + for (RoomData core : cores) { - pruneDoorways(core, rooms, random); + pruneDoorways(core.getLayoutNode(), layout, random); } - return new MazeDesign(root, rooms, cores); + // Set up the placement of dimensional doors within the maze + createMazeLinks(layout, cores, random); + + return new MazeDesign(root, layout); } - private static void listRoomPartitions(PartitionNode node, ArrayList partitions) + private static void attachRooms(PartitionNode node, ArrayList partitions) { if (node.isLeaf()) { - partitions.add(node); + partitions.add(new RoomData(node)); } else { - listRoomPartitions(node.leftChild(), partitions); - listRoomPartitions(node.rightChild(), partitions); - } - } - - private static void removeRoomPartitions(PartitionNode node) - { - // Remove a node and any of its ancestors that become leaf nodes - PartitionNode parent; - PartitionNode current; - - current = node; - while (current != null && current.isLeaf()) - { - parent = current.parent(); - current.remove(); - current = parent; + attachRooms(node.leftChild(), partitions); + attachRooms(node.rightChild(), partitions); } } @@ -140,35 +133,25 @@ public class MazeDesigner } } - private static DirectedGraph createRoomGraph(PartitionNode root, ArrayList partitions, Random random) + private static DirectedGraph createRoomGraph(PartitionNode root, ArrayList rooms, Random random) { - DirectedGraph roomGraph = new DirectedGraph(); - HashMap> roomsToGraph = new HashMap>(2 * partitions.size()); - - // Shuffle the list of rooms so that they're not listed in any ordered way in the room graph - // This is the only convenient way of randomizing the maze sections generated later - Collections.shuffle(partitions, random); + DirectedGraph layout = new DirectedGraph(); // Add all rooms to a graph - // Also add them to a map so we can associate rooms with their graph nodes - // The map is needed for linking graph nodes based on adjacent partitions - for (PartitionNode partition : partitions) + for (RoomData room : rooms) { - roomsToGraph.put(partition, roomGraph.addNode(partition)); + room.addToLayout(layout); } - // Add edges for each room - for (IGraphNode node : roomGraph.nodes()) + for (IGraphNode node : layout.nodes()) { - findDoorways(node, root, roomsToGraph, roomGraph); + findDoorways(node.data(), root, layout); } - - return roomGraph; + return layout; } - private static void findDoorways(IGraphNode roomNode, PartitionNode root, - HashMap> roomsToGraph, - DirectedGraph roomGraph) + private static void findDoorways(RoomData room, PartitionNode root, + DirectedGraph layout) { // This function finds rooms adjacent to a specified room that could be connected // to it through a doorway. Edges are added to the room graph to denote rooms that @@ -186,7 +169,7 @@ public class MazeDesigner // there will always be a way to walk from any room to any other room. boolean[][] detected; - PartitionNode adjacent; + PartitionNode adjacent; int a, b, c; int p, q, r; @@ -195,11 +178,10 @@ public class MazeDesigner Point3D otherMin; Point3D otherMax; DoorwayData doorway; - IGraphNode adjacentNode; - PartitionNode room = roomNode.data(); - Point3D minCorner = room.minCorner(); - Point3D maxCorner = room.maxCorner(); + PartitionNode partition = room.getPartitionNode(); + Point3D minCorner = partition.minCorner(); + Point3D maxCorner = partition.maxCorner(); int minX = minCorner.getX(); int minY = minCorner.getY(); @@ -209,9 +191,9 @@ public class MazeDesigner int maxY = maxCorner.getY(); int maxZ = maxCorner.getZ(); - int width = room.width(); - int height = room.height(); - int length = room.length(); + int width = partition.width(); + int height = partition.height(); + int length = partition.length(); if (maxZ < root.maxCorner().getZ()) { @@ -247,8 +229,7 @@ public class MazeDesigner otherMin = new Point3D(minXI, minYI, maxZ); otherMax = new Point3D(maxXI, maxYI, maxZ + 1); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Z_AXIS); - adjacentNode = roomsToGraph.get(adjacent); - roomGraph.addEdge(roomNode, adjacentNode, doorway); + layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway); } } else @@ -295,8 +276,7 @@ public class MazeDesigner otherMin = new Point3D(maxX, minYI, minZI); otherMax = new Point3D(maxX + 1, maxYI, maxZI); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.X_AXIS); - adjacentNode = roomsToGraph.get(adjacent); - roomGraph.addEdge(roomNode, adjacentNode, doorway); + layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway); } } else @@ -343,8 +323,7 @@ public class MazeDesigner otherMin = new Point3D(minXI, maxY, minZI); otherMax = new Point3D(maxXI, maxY + 1, maxZI); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Y_AXIS); - adjacentNode = roomsToGraph.get(adjacent); - roomGraph.addEdge(roomNode, adjacentNode, doorway); + layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway); } } else @@ -359,108 +338,130 @@ public class MazeDesigner //Done! } - private static ArrayList> createMazeSections(DirectedGraph roomGraph, Random random) + private static ArrayList createMazeSections(DirectedGraph layout, Random random) { // The randomness of the sections generated here hinges on // the nodes in the graph being in a random order. We assume // that was handled in a previous step! - final int MAX_DISTANCE = 2; + // We split the maze into sections by choosing core rooms and removing + // rooms that are a certain number of doorways away. However, for a section + // to be valid, it must also have enough space for at least two doors in + // rooms without floor holes. If a section can't fit two doors, more + // neighboring rooms are added until the necessary space is found or the + // search space is exhausted. + + final int MAX_DISTANCE = 2 + random.nextInt(2); final int MIN_SECTION_ROOMS = 5; + final int MIN_SECTION_CAPACITY = 2; int distance; - IGraphNode current; - IGraphNode neighbor; + int capacity; + RoomData room; + RoomData neighbor; + boolean hasHoles; + IGraphNode roomNode; - ArrayList> cores = new ArrayList>(); - ArrayList> removals = new ArrayList>(); - ArrayList> section = new ArrayList>(); + ArrayList cores = new ArrayList(); + ArrayList section = new ArrayList(); + ArrayList> nodes = new ArrayList>(layout.nodeCount()); - Queue> ordering = new LinkedList>(); - HashMap, Integer> distances = new HashMap, Integer>(); + Queue ordering = new LinkedList(); + + // List all graph nodes so that we can iterate over this list instead + // of using the graph's iterator. That avoids the risk of breaking + // the graph's iterator during removals. + for (IGraphNode node : layout.nodes()) + { + nodes.add(node); + } // Repeatedly generate sections until all nodes have been visited - for (IGraphNode node : roomGraph.nodes()) + for (IGraphNode node : nodes) { - // If this node hasn't been visited, then use it as the core of a new section - // Otherwise, ignore it, since it was already processed - if (!distances.containsKey(node)) + // If this room hasn't been visited (distance = -1), then use it as the core of a new section + // Otherwise, ignore it, since it was already processed. Also make sure to check that room + // isn't null, which happens if the room was removed previously. + room = node.data(); + if (room != null && room.getDistance() < 0) { // Perform a breadth-first search to tag surrounding nodes with distances - distances.put(node, 0); - ordering.add(node); + ordering.add(room); + room.setDistance(0); section.clear(); + capacity = 0; - while (!ordering.isEmpty()) + while (room != null && (room.getDistance() <= MAX_DISTANCE || capacity < 2)) { - current = ordering.remove(); - distance = distances.get(current) + 1; + ordering.remove(); + section.add(room); + roomNode = room.getLayoutNode(); + distance = room.getDistance() + 1; + hasHoles = false; - if (distance <= MAX_DISTANCE + 1) + // Visit neighboring rooms and assign them distances, + // if they don't have a proper distance assigned already. + // Also check for floor holes. + for (IEdge edge : roomNode.inbound()) { - section.add(current); - - // Visit neighboring nodes and assign them distances, if they don't - // have a distance assigned already - for (IEdge edge : current.inbound()) + neighbor = edge.head().data(); + if (neighbor.getDistance() < 0) { - neighbor = edge.head(); - if (!distances.containsKey(neighbor)) - { - distances.put(neighbor, distance); - ordering.add(neighbor); - } + neighbor.setDistance(distance); + ordering.add(neighbor); } - for (IEdge edge : current.outbound()) + if (edge.data().axis() == DoorwayData.Y_AXIS) { - neighbor = edge.tail(); - if (!distances.containsKey(neighbor)) - { - distances.put(neighbor, distance); - ordering.add(neighbor); - } + hasHoles = true; } } - else + for (IEdge edge : roomNode.outbound()) { - removals.add(current); - break; + neighbor = edge.tail().data(); + if (neighbor.getDistance() < 0) + { + neighbor.setDistance(distance); + ordering.add(neighbor); + } } + + // Count this room's door capacity if it has no floor holes + if (!hasHoles) + { + capacity += room.estimateDoorCapacity(); + } + + room = ordering.peek(); } - // List nodes that have a distance of exactly MAX_DISTANCE + 1 - // Those are precisely the nodes that remain in the queue - // We can't remove them immediately because that could break - // the iterator for the graph. + // The remaining rooms in the ordering are those that are at the + // frontier of structure. They must be removed to create a gap + // between this section and other sections. while (!ordering.isEmpty()) { - removals.add(ordering.remove()); + ordering.remove().remove(); } - // Check if this section contains enough rooms - if (section.size() >= MIN_SECTION_ROOMS) + // Check if this section contains enough rooms and capacity for doors + if (section.size() >= MIN_SECTION_ROOMS && capacity >= MIN_SECTION_CAPACITY) { - cores.add(node); + cores.add(node.data()); } else { - removals.addAll(section); + // Discard the whole section + for (RoomData target : section) + { + target.remove(); + } } } } - - // Remove all the nodes that were listed for removal - // Also remove unused partitions from the partition tree - for (IGraphNode node : removals) - { - removeRoomPartitions(node.data()); - roomGraph.removeNode(node); - } return cores; } - private static void pruneDoorways(IGraphNode core, - DirectedGraph rooms, Random random) + private static void pruneDoorways(IGraphNode core, + DirectedGraph layout, Random random) { // We receive a node for one of the rooms in a section of the maze // and we need to remove as many floor doorways as possible while @@ -478,12 +479,12 @@ public class MazeDesigner // idea applies for the other doorways, plus some randomness. // First, list all nodes in the subgraph - IGraphNode current; - IGraphNode neighbor; + IGraphNode current; + IGraphNode neighbor; - Stack> ordering = new Stack>(); - ArrayList> subgraph = new ArrayList>(64); - DisjointSet> components = new DisjointSet>(128); + Stack> ordering = new Stack>(); + ArrayList> subgraph = new ArrayList>(64); + DisjointSet> components = new DisjointSet>(128); ordering.add(core); components.makeSet(core); @@ -492,7 +493,7 @@ public class MazeDesigner current = ordering.pop(); subgraph.add(current); - for (IEdge edge : current.inbound()) + for (IEdge edge : current.inbound()) { neighbor = edge.head(); if (components.makeSet(neighbor)) @@ -500,7 +501,7 @@ public class MazeDesigner ordering.add(neighbor); } } - for (IEdge edge : current.outbound()) + for (IEdge edge : current.outbound()) { neighbor = edge.tail(); if (components.makeSet(neighbor)) @@ -510,15 +511,17 @@ public class MazeDesigner } } - // Now iterate over the list of nodes and merge their sets - // We only have to look at outbound edges since inbound edges mirror them - // Also list any Y_AXIS doorways we come across - ArrayList> targets = - new ArrayList>(); + // Now iterate over the list of nodes and merge their sets based on + // being connected by X_AXIS or Z_AXIS doorways. We only have to look + // at outbound edges since inbound edges mirror them. List any Y_AXIS + // doorways we come across to consider removing them later, depending + // on their impact on connectedness. + ArrayList> targets = + new ArrayList>(); - for (IGraphNode room : subgraph) + for (IGraphNode room : subgraph) { - for (IEdge passage : room.outbound()) + for (IEdge passage : room.outbound()) { if (passage.data().axis() != DoorwayData.Y_AXIS) { @@ -535,11 +538,12 @@ public class MazeDesigner Collections.shuffle(targets, random); // Merge sets together and remove unnecessary doorways - for (IEdge passage : targets) + for (IEdge passage : targets) { if (!components.mergeSets(passage.head(), passage.tail())) { - rooms.removeEdge(passage); + layout.removeEdge(passage); + } } @@ -548,13 +552,13 @@ public class MazeDesigner components.clear(); targets.clear(); - for (IGraphNode room : subgraph) + for (IGraphNode room : subgraph) { components.makeSet(room); } - for (IGraphNode room : subgraph) + for (IGraphNode room : subgraph) { - for (IEdge passage : room.outbound()) + for (IEdge passage : room.outbound()) { if (passage.data().axis() == DoorwayData.Y_AXIS) { @@ -567,13 +571,198 @@ public class MazeDesigner } } Collections.shuffle(targets, random); - for (IEdge passage : targets) + for (IEdge passage : targets) { if (!components.mergeSets(passage.head(), passage.tail()) && random.nextBoolean()) { - rooms.removeEdge(passage); + layout.removeEdge(passage); } } } + private static void createMazeLinks(DirectedGraph layout, + ArrayList cores, Random random) + { + // We have 4 objectives here... + // 1. Place the entrance to the maze + // 2. Place links to other dungeons + // 3. Place internal links connecting the different sections of the maze + // 4. Place more internal links to confuse people + + // We need to start by building up data for each section, such as their + // door capacities and the rooms available for placing doors. + int index; + int count; + SectionData selection; + SectionData destination; + ArrayList allSections; + ArrayList usableSections; + + // Check if there is only one section. Our concerns differ depending + // on whether there is one or more than one. + if (cores.size() > 1) + { + // More than 1 section + allSections = new ArrayList(cores.size()); + for (RoomData core : cores) + { + allSections.add( SectionData.createFromCore(core.getLayoutNode()) ); + } + usableSections = (ArrayList) allSections.clone(); + + // Select the room in which to place the entrance. + // We can safely consider all sections because createMazeSections() + // guarantees that each one has at least the capacity for 2 doors. + // Remove the selected section if it falls below a capacity of 2 + // since we need to leave at least 1 capacity for section linking. + index = random.nextInt(usableSections.size()); + selection = usableSections.get(index); + selection.createEntranceLink(random); + if (selection.capacity() <= 1) + { + usableSections.remove(index); + } + + // Place 3 to 4 dungeon doors in random sections + // Remove any sections that fall under a capacity of 2. + count = 3 + random.nextInt(2); + for (; count > 0 && !usableSections.isEmpty(); count--) + { + index = random.nextInt(usableSections.size()); + selection = usableSections.get(index); + selection.createDungeonLink(random); + if (selection.capacity() <= 1) + { + usableSections.remove(index); + } + } + + // The next task is to place internal links. These links must connect + // the different maze sections to create a strongly connected graph. + linkMazeSections(allSections, random); + + // Add 1 to 3 extra internal links to confuse people + usableSections.clear(); + for (SectionData section : allSections) + { + if (section.capacity() > 0) + { + usableSections.add(section); + } + } + count = 1 + random.nextInt(3); + for (; count > 0 && !usableSections.isEmpty(); count--) + { + index = random.nextInt(usableSections.size()); + selection = usableSections.get(index); + destination = allSections.get( random.nextInt(allSections.size()) ); + selection.reserveSectionLink(destination, random); + if (selection.capacity() == 0) + { + usableSections.remove(index); + } + } + + // Finally, make sure to process all reservations for section links. + for (SectionData section : allSections) + { + section.processReservedLinks(random); + } + } + else + { + // Only 1 section + selection = SectionData.createFromCore(cores.get(0).getLayoutNode()); + // Place entrance door in a random room + selection.createEntranceLink(random); + // Place 3 to 4 dungeon doors or fewer, based on capacity + count = Math.min(3 + random.nextInt(2), selection.capacity()); + for (; count > 0; count--) + { + selection.createDungeonLink(random); + } + } + } + + private static void linkMazeSections(ArrayList sections, Random random) + { + // This algorithm constructs links sections together using Dimensional Doors + // to create a random strongly connected graph. It takes into account capacity + // constraints as well. We assume that all sections have at least 1 door capacity. + final int EXTENSION_CHANCE = 2; + final int MAX_EXTENSION_CHANCE = 3; + + int index; + SectionData start; + SectionData current; + SectionData next; + + // Total spare capacity of the sections not in "remaining" + int capacity; + // Sections not in the graph + ArrayList remaining = (ArrayList) sections.clone(); + // Sections that are part of an incomplete cycle + ArrayList attached = new ArrayList(sections.size()); + // Sections that are part of a cycle and thus strongly connected + ArrayList connected = new ArrayList(sections.size()); + // Sections that are part of a cycle and have spare capacity - used to start new cycles + ArrayList starters = new ArrayList(sections.size()); + + // Shuffle remaining to achieve randomness + Collections.shuffle(remaining, random); + // Remove the starting node to serve as the base of our strongly connected graph + start = remaining.remove(remaining.size() - 1); + starters.add(start); + connected.add(start); + capacity = start.capacity(); + + // Repeat until all sections are connected + while (!remaining.isEmpty()) + { + // Select a section from which to start a new cycle + index = random.nextInt(starters.size()); + start = starters.get(index); + // Select the first new section in the cycle and link to it + current = remaining.remove(remaining.size() - 1); + attached.add(current); + start.reserveSectionLink(current, random); + // Add the current section's capacity to the total, but subtract two to account + // for the link just created and for the future link from this section to another + capacity += current.capacity() - 2; + // Remove the starting section from starters if it has exhausted its capacity + if (start.capacity() == 0) + { + starters.remove(index); + } + + // Continue attaching sections to the partial cycle while there are are still sections + // left to be added and we have no spare capacity. Or we could randomly decide to + // continue even with spare capacity. Spare capacity means we could start a new cycle + // safely and still achieve strong connectivity. Randomness here influences the kinds + // of graphs we can get. + while (!remaining.isEmpty() && (capacity == 0 || + random.nextInt(MAX_EXTENSION_CHANCE) < EXTENSION_CHANCE)) + { + next = remaining.remove(remaining.size() - 1); + attached.add(next); + current.reserveSectionLink(next, random); + // Account for this section's capacity, but subtract one for + // the future link that will connect this section to another + capacity += next.capacity() - 1; + current = next; + } + next = connected.get(random.nextInt(connected.size())); + current.reserveSectionLink(next, random); + for (SectionData section : attached) + { + connected.add(section); + if (section.capacity() > 0) + { + starters.add(section); + } + } + } + + // Done! At this point, all sections are connected. + } } diff --git a/src/main/java/StevenDimDoors/experimental/PartitionNode.java b/src/main/java/StevenDimDoors/experimental/PartitionNode.java index df53854..5226378 100644 --- a/src/main/java/StevenDimDoors/experimental/PartitionNode.java +++ b/src/main/java/StevenDimDoors/experimental/PartitionNode.java @@ -2,11 +2,12 @@ package StevenDimDoors.experimental; import StevenDimDoors.mod_pocketDim.Point3D; -public class PartitionNode extends BoundingBox +public class PartitionNode extends BoundingBox { private PartitionNode parent; private PartitionNode leftChild = null; private PartitionNode rightChild = null; + private T data = null; public PartitionNode(int width, int height, int length) { @@ -122,4 +123,14 @@ public class PartitionNode extends BoundingBox return this; } } + + public void setData(T value) + { + this.data = value; + } + + public T getData() + { + return data; + } } diff --git a/src/main/java/StevenDimDoors/experimental/RoomData.java b/src/main/java/StevenDimDoors/experimental/RoomData.java new file mode 100644 index 0000000..c87c406 --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/RoomData.java @@ -0,0 +1,133 @@ +package StevenDimDoors.experimental; + +import java.util.ArrayList; + +public class RoomData +{ + /* Implementation Note: + * Plans for links between rooms are stored in lists rather than a graph, + * even though they duplicate some graph functionality, because there are + * relatively few of them compared to the number of rooms. Moreover, some + * links don't even have destinations because they're intended to lead to + * other dungeons. + */ + + private int capacity; + private int distance; + private boolean decayed; + private PartitionNode partitionNode; + private ArrayList inboundLinks; + private ArrayList outboundLinks; + private DirectedGraph layout; + private IGraphNode layoutNode; + + public RoomData(PartitionNode partitionNode) + { + this.partitionNode = partitionNode; + this.inboundLinks = new ArrayList(); + this.outboundLinks = new ArrayList(); + this.layoutNode = null; + this.layout = null; + this.distance = -1; + this.capacity = -1; + this.decayed = false; + partitionNode.setData(this); + } + + public PartitionNode getPartitionNode() + { + return this.partitionNode; + } + + public IGraphNode getLayoutNode() + { + return this.layoutNode; + } + + public void addToLayout(DirectedGraph layout) + { + this.layout = layout; + this.layoutNode = layout.addNode(this); + } + + public boolean isDecayed() + { + return decayed; + } + + public void setDecayed(boolean value) + { + this.decayed = value; + } + + public ArrayList getInboundLinks() + { + return this.inboundLinks; + } + + public ArrayList getOutboundLinks() + { + return this.outboundLinks; + } + + public int getDistance() + { + return distance; + } + + public void setDistance(int value) + { + distance = value; + } + + public void remove() + { + // Remove the room from the partition tree and from the layout graph. + // Also remove any ancestors that become leaf nodes. + PartitionNode parent; + PartitionNode current = partitionNode; + while (current != null && current.isLeaf()) + { + parent = current.parent(); + current.remove(); + current = parent; + } + + // Remove the room from the layout graph + layout.removeNode(layoutNode); + + // Remove any links + while (!inboundLinks.isEmpty()) + inboundLinks.get(inboundLinks.size() - 1).remove(); + + while (!outboundLinks.isEmpty()) + outboundLinks.get(outboundLinks.size() - 1).remove(); + + // Wipe the room's data, as a precaution + layout = null; + partitionNode = null; + inboundLinks = null; + outboundLinks = null; + } + + public int estimateDoorCapacity() + { + if (capacity >= 0) + return capacity; + + int cellsX = (partitionNode.width() - 3) / 2; + int cellsZ = (partitionNode.length() - 3) / 2; + capacity = Math.min(cellsX * cellsZ, 3); + return capacity; + } + + public int getRemainingDoorCapacity() + { + return (estimateDoorCapacity() - outboundLinks.size()); + } + + public boolean isProtected() + { + return !inboundLinks.isEmpty() || !outboundLinks.isEmpty(); + } +} diff --git a/src/main/java/StevenDimDoors/experimental/SectionData.java b/src/main/java/StevenDimDoors/experimental/SectionData.java new file mode 100644 index 0000000..22d240a --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/SectionData.java @@ -0,0 +1,192 @@ +package StevenDimDoors.experimental; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Random; +import java.util.Stack; + +public class SectionData +{ + // Specifies the chance of selecting a destination from protectedRooms + // rather than from destinationRooms (which will then become protected) + private static final int PROTECTED_DESTINATION_CHANCE = 4; + private static final int MAX_PROTECTED_DESTINATION_CHANCE = 5; + + private int capacity; + private ArrayList sourceRooms; + private ArrayList protectedRooms; + private ArrayList destinationRooms; + private ArrayList reservations; + + private SectionData(ArrayList sourceRooms, ArrayList destinationRooms, int capacity) + { + this.capacity = capacity; + this.sourceRooms = sourceRooms; + this.destinationRooms = destinationRooms; + this.protectedRooms = new ArrayList(); + this.reservations = new ArrayList(); + } + + public static SectionData createFromCore(IGraphNode core) + { + int capacity = 0; + ArrayList sourceRooms = new ArrayList(); + ArrayList destinationRooms = new ArrayList(); + + boolean hasHoles; + RoomData currentRoom; + IGraphNode current; + IGraphNode neighbor; + Stack> ordering = new Stack>(); + HashSet> visited = new HashSet>(); + + visited.add(core); + ordering.add(core); + while (!ordering.isEmpty()) + { + current = ordering.pop(); + hasHoles = false; + + for (IEdge edge : current.outbound()) + { + neighbor = edge.tail(); + if (visited.add(neighbor)) + { + ordering.add(neighbor); + } + } + for (IEdge edge : current.inbound()) + { + neighbor = edge.head(); + if (visited.add(neighbor)) + { + ordering.add(neighbor); + } + if (edge.data().axis() == DoorwayData.Y_AXIS) + { + hasHoles = true; + } + } + + if (!hasHoles) + { + currentRoom = current.data(); + destinationRooms.add(currentRoom); + if (currentRoom.estimateDoorCapacity() > 0) + { + capacity += currentRoom.estimateDoorCapacity(); + sourceRooms.add(currentRoom); + } + } + } + return new SectionData(sourceRooms, destinationRooms, capacity); + } + + public int capacity() + { + return capacity; + } + + public void createEntranceLink(Random random) + { + int index = random.nextInt(sourceRooms.size()); + RoomData room = sourceRooms.get(index); + LinkPlan.createEntranceLink(room); + if (room.getRemainingDoorCapacity() == 0) + { + sourceRooms.remove(index); + } + // It's okay to check containment in this list because + // the number of protected rooms is expected to be small + if (!protectedRooms.contains(room)) + { + protectedRooms.add(room); + } + capacity--; + } + + public void createDungeonLink(Random random) + { + int index = random.nextInt(sourceRooms.size()); + RoomData room = sourceRooms.get(index); + LinkPlan.createDungeonLink(room); + if (room.getRemainingDoorCapacity() == 0) + { + sourceRooms.remove(index); + } + // It's okay to check containment in this list because + // the number of protected rooms is expected to be small + if (!protectedRooms.contains(room)) + { + protectedRooms.add(room); + } + capacity--; + } + + public void reserveSectionLink(SectionData destination, Random random) + { + // This method "reserves" a link by decrementing the capacity of this + // section and assigning a source room to the link. However, assigning + // its destination in a particular section is deferred. Why? + + // We favor using source rooms as destinations to cut down the number + // of rooms that have to be marked as protected against decay effects. + // We defer assigning a destination until after all source rooms are + // known so that we have that information available. Otherwise, + // destination selection would be biased toward non-source rooms and + // rooms with dungeon doors, which are placed before section links. + + int index = random.nextInt(sourceRooms.size()); + RoomData room = sourceRooms.get(index); + destination.reserveDestination(LinkPlan.createInternalLink(room)); + if (room.getRemainingDoorCapacity() == 0) + { + sourceRooms.remove(index); + } + // It's okay to check containment in this list because + // the number of protected rooms is expected to be small + if (!protectedRooms.contains(room)) + { + protectedRooms.add(room); + } + capacity--; + } + + public void processReservedLinks(Random random) + { + for (LinkPlan link : reservations) + { + link.setDestination( getLinkDestination(random) ); + } + reservations.clear(); + } + + private void reserveDestination(LinkPlan link) + { + reservations.add(link); + } + + private RoomData getLinkDestination(Random random) + { + RoomData destination; + + // Choose whether to select a room that is already protected or select + // from all possible destination rooms. Note that some destination rooms + // may also be protected rooms already. + if (random.nextInt(MAX_PROTECTED_DESTINATION_CHANCE) < PROTECTED_DESTINATION_CHANCE) + { + destination = protectedRooms.get( random.nextInt(protectedRooms.size()) ); + } + else + { + destination = destinationRooms.get( random.nextInt(destinationRooms.size()) ); + // It's okay to check containment in this list because + // the number of protected rooms is expected to be small + if (!protectedRooms.contains(destination)) + { + protectedRooms.add(destination); + } + } + return destination; + } +} diff --git a/src/main/java/StevenDimDoors/experimental/decorators/BaseDecorator.java b/src/main/java/StevenDimDoors/experimental/decorators/BaseDecorator.java new file mode 100644 index 0000000..2ea4e8a --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/decorators/BaseDecorator.java @@ -0,0 +1,17 @@ +package StevenDimDoors.experimental.decorators; + +import java.util.Random; + +import net.minecraft.world.World; +import StevenDimDoors.experimental.RoomData; +import StevenDimDoors.mod_pocketDim.Point3D; +import StevenDimDoors.mod_pocketDim.config.DDProperties; + +public abstract class BaseDecorator +{ + public BaseDecorator() { } + + public abstract boolean canDecorate(RoomData room); + + public abstract void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties); +} diff --git a/src/main/java/StevenDimDoors/experimental/decorators/DecoratorFinder.java b/src/main/java/StevenDimDoors/experimental/decorators/DecoratorFinder.java new file mode 100644 index 0000000..ce925fe --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/decorators/DecoratorFinder.java @@ -0,0 +1,51 @@ +package StevenDimDoors.experimental.decorators; + +import java.util.ArrayList; +import java.util.Random; + +import StevenDimDoors.experimental.RoomData; + +public class DecoratorFinder +{ + private static ArrayList decorators = null; + + private DecoratorFinder() { } + + public static BaseDecorator find(RoomData room, Random random) + { + if (decorators == null) + { + load(); + } + + // Since there are only a few decorators right now, we just iterate + // over the list and check them all. If we add a lot, we'll need to + // switch to a more efficient approach. + ArrayList matches = new ArrayList(); + for (BaseDecorator decorator : decorators) + { + if (decorator.canDecorate(room)) + { + matches.add(decorator); + } + } + + if (matches.isEmpty()) + { + return null; + } + else + { + return matches.get( random.nextInt(matches.size()) ); + } + } + + private static void load() + { + // List all the decorators we have + decorators = new ArrayList(); + decorators.add(new LinkDestinationDecorator()); + decorators.add(new DefaultDoorDecorator()); + decorators.add(new TorchDecorator()); + } +} diff --git a/src/main/java/StevenDimDoors/experimental/decorators/DefaultDoorDecorator.java b/src/main/java/StevenDimDoors/experimental/decorators/DefaultDoorDecorator.java new file mode 100644 index 0000000..fd653ab --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/decorators/DefaultDoorDecorator.java @@ -0,0 +1,49 @@ +package StevenDimDoors.experimental.decorators; + +import java.util.Random; + +import net.minecraft.item.ItemDoor; +import net.minecraft.world.World; +import StevenDimDoors.experimental.LinkPlan; +import StevenDimDoors.experimental.RoomData; +import StevenDimDoors.mod_pocketDim.Point3D; +import StevenDimDoors.mod_pocketDim.mod_pocketDim; +import StevenDimDoors.mod_pocketDim.config.DDProperties; + +public class DefaultDoorDecorator extends BaseDecorator +{ + @Override + public boolean canDecorate(RoomData room) + { + return !room.getOutboundLinks().isEmpty(); + } + + @Override + public void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties) + { + // TODO: This is just an improvised implementation for testing + Point3D corner = room.getPartitionNode().minCorner().clone(); + corner.add(offset); + + int count = 0; + Point3D source = null; + for (LinkPlan plan : room.getOutboundLinks()) + { + source = new Point3D(corner.getX() + 2, corner.getY() + 2, corner.getZ() + count + 1); + ItemDoor.placeDoorBlock(world, source.getX(), source.getY() - 1, source.getZ(), 0, mod_pocketDim.dimensionalDoor); + plan.setSourcePoint(source); + count++; + } + + if (source == null) + { + throw new IllegalStateException("This should never happen because this decorator only applies if outbound links exist!"); + } + for (LinkPlan plan : room.getInboundLinks()) + { + plan.setDestinationPoint(source); + } + + } + +} diff --git a/src/main/java/StevenDimDoors/experimental/decorators/LinkDestinationDecorator.java b/src/main/java/StevenDimDoors/experimental/decorators/LinkDestinationDecorator.java new file mode 100644 index 0000000..0b5d21f --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/decorators/LinkDestinationDecorator.java @@ -0,0 +1,37 @@ +package StevenDimDoors.experimental.decorators; + +import java.util.Random; + +import net.minecraft.world.World; + +import StevenDimDoors.experimental.LinkPlan; +import StevenDimDoors.experimental.PartitionNode; +import StevenDimDoors.experimental.RoomData; +import StevenDimDoors.mod_pocketDim.Point3D; +import StevenDimDoors.mod_pocketDim.config.DDProperties; + +public class LinkDestinationDecorator extends BaseDecorator +{ + @Override + public boolean canDecorate(RoomData room) + { + return room.getOutboundLinks().isEmpty() && !room.getInboundLinks().isEmpty(); + } + + @Override + public void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties) + { + // Set the center of the room as the destination for all inbound links + PartitionNode partition = room.getPartitionNode(); + Point3D destination = partition.minCorner().clone(); + destination.add( + offset.getX() + partition.width() / 2, + offset.getY() + 2, + offset.getZ() + partition.length() / 2); + + for (LinkPlan plan : room.getInboundLinks()) + { + plan.setDestinationPoint(destination); + } + } +} diff --git a/src/main/java/StevenDimDoors/experimental/decorators/TorchDecorator.java b/src/main/java/StevenDimDoors/experimental/decorators/TorchDecorator.java new file mode 100644 index 0000000..c200465 --- /dev/null +++ b/src/main/java/StevenDimDoors/experimental/decorators/TorchDecorator.java @@ -0,0 +1,86 @@ +package StevenDimDoors.experimental.decorators; + +import java.util.Random; + +import net.minecraft.block.Block; +import net.minecraft.util.MathHelper; +import net.minecraft.world.World; + +import StevenDimDoors.experimental.PartitionNode; +import StevenDimDoors.experimental.RoomData; +import StevenDimDoors.mod_pocketDim.Point3D; +import StevenDimDoors.mod_pocketDim.config.DDProperties; + +public class TorchDecorator extends BaseDecorator +{ + @Override + public boolean canDecorate(RoomData room) + { + return !room.isProtected(); + } + + @Override + public void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties) + { + // SenseiKiwi: Place a single random torch along the walls. + // We could do more complex arrangements but I feel that a single + // torches here and there will be a little unsettling. + // The walls might be broken by passages or decay, so this will + // require trial and error. + + final int MAX_ATTEMPTS = 5; + + int x; + int z; + int attempts = 0; + PartitionNode partition = room.getPartitionNode(); + int minX = partition.minCorner().getX() + offset.getX(); + int minZ = partition.minCorner().getZ() + offset.getZ(); + int maxX = partition.maxCorner().getX() + offset.getX(); + int maxZ = partition.maxCorner().getZ() + offset.getZ(); + int torchLevel = partition.minCorner().getY() + offset.getY() + 2; + + for (; attempts < MAX_ATTEMPTS; attempts++) + { + // Choose a random side of the room to place the torch. The sides are numbered arbitrarily here. + // Then choose a random position along the wall and check if there is a block there to place the + // torch against. We assume that all blocks are bricks and thus valid. + switch (random.nextInt(4)) + { + case 0: // Positive X side + z = MathHelper.getRandomIntegerInRange(random, minZ + 1, maxZ - 1); + if (!world.isAirBlock(maxX, torchLevel, z)) + { + world.setBlock(maxX - 1, torchLevel, z, Block.torchWood.blockID, 2, 0); + return; + } + break; + case 1: // Negative X side + z = MathHelper.getRandomIntegerInRange(random, minZ + 1, maxZ - 1); + if (!world.isAirBlock(minX, torchLevel, z)) + { + world.setBlock(minX + 1, torchLevel, z, Block.torchWood.blockID, 1, 0); + return; + } + break; + case 2: // Positive Z side + x = MathHelper.getRandomIntegerInRange(random, minX + 1, maxX - 1); + if (!world.isAirBlock(x, torchLevel, maxZ)) + { + world.setBlock(x, torchLevel, maxZ - 1, Block.torchWood.blockID, 4, 0); + return; + } + break; + case 3: // Negative Z side + x = MathHelper.getRandomIntegerInRange(random, minX + 1, maxX - 1); + if (!world.isAirBlock(x, torchLevel, minZ)) + { + world.setBlock(x, torchLevel, minZ + 1, Block.torchWood.blockID, 3, 0); + return; + } + break; + } + } + } + +} diff --git a/src/main/java/StevenDimDoors/mod_pocketDim/Point3D.java b/src/main/java/StevenDimDoors/mod_pocketDim/Point3D.java index f639ce1..f03469a 100644 --- a/src/main/java/StevenDimDoors/mod_pocketDim/Point3D.java +++ b/src/main/java/StevenDimDoors/mod_pocketDim/Point3D.java @@ -55,6 +55,20 @@ public class Point3D implements Serializable { { return this.z = z; } + + public void add(int x, int y, int z) + { + this.x += x; + this.y += y; + this.z += z; + } + + public void add(Point3D other) + { + this.x += other.x; + this.y += other.y; + this.z += other.z; + } @Override public Point3D clone() diff --git a/src/main/java/StevenDimDoors/mod_pocketDim/world/PocketBuilder.java b/src/main/java/StevenDimDoors/mod_pocketDim/world/PocketBuilder.java index f26939b..462033a 100644 --- a/src/main/java/StevenDimDoors/mod_pocketDim/world/PocketBuilder.java +++ b/src/main/java/StevenDimDoors/mod_pocketDim/world/PocketBuilder.java @@ -465,6 +465,7 @@ public class PocketBuilder Point3D door = new Point3D(x, y, z); BlockRotator.transformPoint(center, door, orientation - BlockRotator.EAST_DOOR_METADATA, door); + /* //Build the outer layer of Eternal Fabric buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2), properties.PermaFabricBlockID, false, 0); @@ -474,8 +475,9 @@ public class PocketBuilder buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2) - layer, properties.FabricBlockID, layer < (wallThickness - 1) && properties.TNFREAKINGT_Enabled, properties.NonTntWeight); } + */ - //MazeBuilder.generate(world, x, y, z, random); + MazeBuilder.generate(world, x, y, z, random, properties); //Build the door int doorOrientation = BlockRotator.transformMetadata(BlockRotator.EAST_DOOR_METADATA, orientation - BlockRotator.EAST_DOOR_METADATA + 2, properties.DimensionalDoorID);