13 Commits

Author SHA1 Message Date
StevenRS11
f2c585b568 Fixed door render 2014-05-05 13:55:17 -04:00
StevenRS11
363feac783 Merge pull request #155 from SenseiKiwi/mazes
Mazes
2014-04-15 07:46:00 -04:00
SenseiKiwi
fa629db4fe Progress on Maze Generation
* Implemented link creation based on link plans
* Improvised an implementation of door placement in DefaultDoorDecorator
for testing purposes. I'll provide a better version later.

Known issues:
1. Doors with one-way links to other rooms will generate a return door
at the destination. The return door doesn't actually lead back, it leads
to a new pocket. Need to disable pair generation for doors in mazes.
2. Consider weighing sections by the door capacity to avoid putting a
lot of doors into a small section. Also double-check that room selection
within sections is unbiased.
2014-04-15 07:40:44 -04:00
SenseiKiwi
77abcbb148 Progress on Maze Generation
* Increased the chance of decorating unprotected rooms from 1/4 to 1/3.
* Made minor optimizations to the box building function.
* Added some important arguments to BaseDecorator.decorate(). Also
updated all decorators as a result of this change.
* Implemented TorchDecorator so that mazes have light sources in them.
2014-04-15 05:14:38 -04:00
SenseiKiwi
f867f16d1d Fixed Maze Bug
Fixed a minor bug that affected maze decorators.
2014-04-15 01:46:39 -04:00
SenseiKiwi
bce329c8fb Added Maze Decorators
Added Decorators for use in mazes. I've added several decorators but all
of them are stubs for now.
2014-04-15 01:35:14 -04:00
SenseiKiwi
81b48158bd Progress on Maze Generation
* Finished implementing link planning for mazes. Doors aren't placed yet
because that's up to Decorators and those haven't been implemented yet.
* Added bounding walls to mazes.
* Added decay effects to mazes.
2014-04-14 22:24:59 -04:00
SenseiKiwi
906faf44eb Tweaked Maze Section Generation
Tweaked maze section generation to use a random MAX_DISTANCE for
including rooms in a section. Also changed the code to perform room
removals as sections are processed rather than deferring them to the
end. Deferring removals would cause the algorithm to detect holes from
rooms that were going to be removed. This made section generation much
stricter than necessary.
2014-04-13 16:17:42 -04:00
SenseiKiwi
53b5591149 Partial Implementation of Doors in Mazes
Made progress on implementing the placement of doors in mazes. Still
incomplete.
2014-04-11 19:28:05 -04:00
SenseiKiwi
f92020323f Partial Implementation of Doors in Mazes
Started implementing the placement of Dimensional Doors in mazes.
Currently, a design is guaranteed to have enough space for some doors.
MazeDesigner still needs more code to plan out which rooms will have
doors and where those doors will lead.
2014-04-08 06:40:45 -04:00
SenseiKiwi
935070e436 Minor Change
Minor correction to a comment in DirectedGraph
2014-04-08 06:34:45 -04:00
SenseiKiwi
5210de2e71 Hacked PocketBuilder to Generate Mazes
Made a minor change to PocketBuilder so that mazes generate instead of
regular pocket dimensions. I'm only doing this to test dungeon
generation - it'll get switched back once mazes are ready.
2014-04-07 09:39:10 -04:00
SenseiKiwi
d5e5e12cf9 Refactored Maze Generation to use RoomData
Rewrote portions of our maze generation code to use RoomData. This
provides an object that unifies all room data instead of having it
spread across various data structures and linked loosely by hash maps.
We'll need this to implement the remaining generation features.
2014-04-07 09:25:20 -04:00
17 changed files with 1259 additions and 204 deletions

View File

@@ -165,8 +165,8 @@ public class DirectedGraph<U, V>
{ {
Edge<U, V> innerEdge = (Edge<U, V>) edge; Edge<U, V> innerEdge = (Edge<U, V>) edge;
// Check that this node actually belongs to this graph instance. // Check that this edge actually belongs to this graph instance.
// Accepting foreign nodes could corrupt the graph's internal state. // Accepting foreign edges could corrupt the graph's internal state.
if (innerEdge.graphEntry.owner() != edges) if (innerEdge.graphEntry.owner() != edges)
{ {
throw new IllegalArgumentException("The specified edge does not belong to this graph."); throw new IllegalArgumentException("The specified edge does not belong to this graph.");

View File

@@ -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;
}
}

View File

@@ -1,63 +1,215 @@
package StevenDimDoors.experimental; package StevenDimDoors.experimental;
import java.util.ArrayList;
import java.util.Random; import java.util.Random;
import java.util.Stack;
import net.minecraft.block.Block; import net.minecraft.block.Block;
import net.minecraft.world.World; import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage; 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.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 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() { } 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); MazeDesign design = MazeDesigner.generate(random);
Point3D offset = new Point3D(x - design.width() / 2, y - design.height() - 1, z - design.length() / 2); 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); SphereDecayOperation decay = new SphereDecayOperation(random, 0, 0, Block.stoneBrick.blockID, 2);
buildRooms(design.getRoomGraph(), world, offset); buildRooms(design.getLayout(), world, offset);
carveDoorways(design.getRoomGraph(), world, offset, decay, random); carveDoorways(design.getLayout(), world, offset, decay, random);
//placeDoors(design, world, offset);
applyRandomDestruction(design, 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, private static void applyRandomDestruction(MazeDesign design, World world,
Point3D offset, SphereDecayOperation decay, Random random) 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 void buildRooms(DirectedGraph<PartitionNode, DoorwayData> roomGraph, World world, Point3D offset) private static boolean markDecayArea(int x, int y, int z, int DECAY_BOX_SIZE, PartitionNode<RoomData> root)
{ {
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) // 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<RoomData> partition;
ArrayList<RoomData> targets = new ArrayList<RoomData>();
Stack<PartitionNode<RoomData>> nodes = new Stack<PartitionNode<RoomData>>();
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())
{ {
PartitionNode room = node.data(); 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<RoomData, DoorwayData> layout, World world, Point3D offset)
{
for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{
PartitionNode room = node.data().getPartitionNode();
buildBox(world, offset, room.minCorner(), room.maxCorner(), Block.stoneBrick.blockID, 0); buildBox(world, offset, room.minCorner(), room.maxCorner(), Block.stoneBrick.blockID, 0);
} }
} }
private static void carveDoorways(DirectedGraph<PartitionNode, DoorwayData> roomGraph, World world, private static void decorateRooms(DirectedGraph<RoomData, DoorwayData> layout,
World world, Point3D offset, Random random, DDProperties properties)
{
RoomData room;
BaseDecorator decorator;
PartitionNode<RoomData> partition;
ArrayList<LinkPlan> links = new ArrayList<LinkPlan>();
// Iterate over all rooms and apply decorators
for (IGraphNode<RoomData, DoorwayData> 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<RoomData, DoorwayData> layout, World world,
Point3D offset, SphereDecayOperation decay, Random random) Point3D offset, SphereDecayOperation decay, Random random)
{ {
char axis; char axis;
Point3D lower; Point3D lower;
DoorwayData doorway; DoorwayData doorway;
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{ {
for (IEdge<PartitionNode, DoorwayData> passage : node.outbound()) for (IEdge<RoomData, DoorwayData> passage : node.outbound())
{ {
// Carve out the passage
doorway = passage.data(); doorway = passage.data();
axis = doorway.axis(); axis = doorway.axis();
lower = doorway.minCorner(); lower = doorway.minCorner();
carveDoorway(world, axis, offset.getX() + lower.getX(), offset.getY() + lower.getY(), carveDoorway(world, axis, offset.getX() + lower.getX(), offset.getY() + lower.getY(),
offset.getZ() + lower.getZ(), doorway.width(), doorway.height(), doorway.length(), offset.getZ() + lower.getZ(), doorway.width(), doorway.height(), doorway.length(),
decay, random); 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; final int MIN_DOUBLE_DOOR_SPAN = 10;
int gap; int gap;
int rx;
int rz;
switch (axis) switch (axis)
{ {
case DoorwayData.X_AXIS: case DoorwayData.X_AXIS:
@@ -132,9 +286,10 @@ public class MazeBuilder
{ {
gap = 6; gap = 6;
} }
decay.apply(world, rx = x + random.nextInt(width - gap - 1) + 1;
x + random.nextInt(width - gap - 1) + 1, y - 1, rz = z + random.nextInt(length - gap - 1) + 1;
z + random.nextInt(length - gap - 1) + 1, gap, 4, gap); carveHole(world, rx + gap / 2, y, rz + gap / 2);
decay.apply(world, rx, y - 1, rz, gap, 4, gap);
} }
else else
{ {
@@ -166,6 +321,18 @@ public class MazeBuilder
setBlockDirectly(world, x, y + 1, 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) 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 (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, minZ, blockID, metadata);
setBlockDirectly(world, x, y, maxZ, 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, minX, y, z, blockID, metadata);
setBlockDirectly(world, maxX, y, z, blockID, metadata); setBlockDirectly(world, maxX, y, z, blockID, metadata);

View File

@@ -4,37 +4,23 @@ import java.util.ArrayList;
public class MazeDesign public class MazeDesign
{ {
private PartitionNode root; private PartitionNode<RoomData> root;
private DirectedGraph<PartitionNode, DoorwayData> rooms; private DirectedGraph<RoomData, DoorwayData> layout;
private ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores;
private ArrayList<BoundingBox> protectedAreas;
public MazeDesign(PartitionNode root, DirectedGraph<PartitionNode, DoorwayData> rooms, public MazeDesign(PartitionNode<RoomData> root, DirectedGraph<RoomData, DoorwayData> layout)
ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores)
{ {
this.root = root; this.root = root;
this.rooms = rooms; this.layout = layout;
this.cores = cores;
} }
public PartitionNode getRootPartition() public PartitionNode<RoomData> getRootPartition()
{ {
return root; return root;
} }
public DirectedGraph<PartitionNode, DoorwayData> getRoomGraph() public DirectedGraph<RoomData, DoorwayData> getLayout()
{ {
return rooms; return layout;
}
public ArrayList<IGraphNode<PartitionNode, DoorwayData>> getCoreNodes()
{
return cores;
}
public ArrayList<BoundingBox> getProtectedAreas()
{
return protectedAreas;
} }
public int width() public int width()

View File

@@ -1,8 +1,9 @@
package StevenDimDoors.experimental; package StevenDimDoors.experimental;
import java.lang.reflect.Array;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Queue; import java.util.Queue;
import java.util.Random; import java.util.Random;
@@ -25,54 +26,46 @@ public class MazeDesigner
public static MazeDesign generate(Random random) public static MazeDesign generate(Random random)
{ {
// Construct a random binary space partitioning of our maze volume // Construct a random binary space partitioning of our maze volume
PartitionNode root = partitionRooms(MAZE_WIDTH, MAZE_HEIGHT, MAZE_LENGTH, SPLIT_COUNT, random); PartitionNode<RoomData> root = partitionRooms(MAZE_WIDTH, MAZE_HEIGHT, MAZE_LENGTH, SPLIT_COUNT, random);
// List all the leaf nodes of the partition tree, which denote individual rooms // Attach rooms to all the leaf nodes of the partition tree
ArrayList<PartitionNode> partitions = new ArrayList<PartitionNode>(1 << SPLIT_COUNT); ArrayList<RoomData> rooms = new ArrayList<RoomData>(1 << SPLIT_COUNT);
listRoomPartitions(root, partitions); 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 // 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 // considered adjacent if and only if a doorway could connect them. Their
// common boundary must be large enough for a doorway. // common boundary must be large enough for a doorway.
DirectedGraph<PartitionNode, DoorwayData> rooms = createRoomGraph(root, partitions, random); DirectedGraph<RoomData, DoorwayData> layout = createRoomGraph(root, rooms, random);
// Cut out random subgraphs from the adjacency graph // Cut out random subgraphs from the adjacency graph
ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores = createMazeSections(rooms, random); ArrayList<RoomData> cores = createMazeSections(layout, random);
// Remove unnecessary passages through floors/ceilings and some from the walls // Remove unnecessary passages through floors/ceilings and some from the walls
for (IGraphNode<PartitionNode, DoorwayData> 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<PartitionNode> partitions) private static void attachRooms(PartitionNode<RoomData> node, ArrayList<RoomData> partitions)
{ {
if (node.isLeaf()) if (node.isLeaf())
{ {
partitions.add(node); partitions.add(new RoomData(node));
} }
else else
{ {
listRoomPartitions(node.leftChild(), partitions); attachRooms(node.leftChild(), partitions);
listRoomPartitions(node.rightChild(), partitions); attachRooms(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;
} }
} }
@@ -140,35 +133,25 @@ public class MazeDesigner
} }
} }
private static DirectedGraph<PartitionNode, DoorwayData> createRoomGraph(PartitionNode root, ArrayList<PartitionNode> partitions, Random random) private static DirectedGraph<RoomData, DoorwayData> createRoomGraph(PartitionNode<RoomData> root, ArrayList<RoomData> rooms, Random random)
{ {
DirectedGraph<PartitionNode, DoorwayData> roomGraph = new DirectedGraph<PartitionNode, DoorwayData>(); DirectedGraph<RoomData, DoorwayData> layout = new DirectedGraph<RoomData, DoorwayData>();
HashMap<PartitionNode, IGraphNode<PartitionNode, DoorwayData>> roomsToGraph = new HashMap<PartitionNode, IGraphNode<PartitionNode, DoorwayData>>(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);
// Add all rooms to a graph // Add all rooms to a graph
// Also add them to a map so we can associate rooms with their graph nodes for (RoomData room : rooms)
// The map is needed for linking graph nodes based on adjacent partitions
for (PartitionNode partition : partitions)
{ {
roomsToGraph.put(partition, roomGraph.addNode(partition)); room.addToLayout(layout);
} }
// Add edges for each room // Add edges for each room
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{ {
findDoorways(node, root, roomsToGraph, roomGraph); findDoorways(node.data(), root, layout);
}
return layout;
} }
return roomGraph; private static void findDoorways(RoomData room, PartitionNode<RoomData> root,
} DirectedGraph<RoomData, DoorwayData> layout)
private static void findDoorways(IGraphNode<PartitionNode, DoorwayData> roomNode, PartitionNode root,
HashMap<PartitionNode, IGraphNode<PartitionNode, DoorwayData>> roomsToGraph,
DirectedGraph<PartitionNode, DoorwayData> roomGraph)
{ {
// This function finds rooms adjacent to a specified room that could be connected // 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 // 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. // there will always be a way to walk from any room to any other room.
boolean[][] detected; boolean[][] detected;
PartitionNode adjacent; PartitionNode<RoomData> adjacent;
int a, b, c; int a, b, c;
int p, q, r; int p, q, r;
@@ -195,11 +178,10 @@ public class MazeDesigner
Point3D otherMin; Point3D otherMin;
Point3D otherMax; Point3D otherMax;
DoorwayData doorway; DoorwayData doorway;
IGraphNode<PartitionNode, DoorwayData> adjacentNode;
PartitionNode room = roomNode.data(); PartitionNode partition = room.getPartitionNode();
Point3D minCorner = room.minCorner(); Point3D minCorner = partition.minCorner();
Point3D maxCorner = room.maxCorner(); Point3D maxCorner = partition.maxCorner();
int minX = minCorner.getX(); int minX = minCorner.getX();
int minY = minCorner.getY(); int minY = minCorner.getY();
@@ -209,9 +191,9 @@ public class MazeDesigner
int maxY = maxCorner.getY(); int maxY = maxCorner.getY();
int maxZ = maxCorner.getZ(); int maxZ = maxCorner.getZ();
int width = room.width(); int width = partition.width();
int height = room.height(); int height = partition.height();
int length = room.length(); int length = partition.length();
if (maxZ < root.maxCorner().getZ()) if (maxZ < root.maxCorner().getZ())
{ {
@@ -247,8 +229,7 @@ public class MazeDesigner
otherMin = new Point3D(minXI, minYI, maxZ); otherMin = new Point3D(minXI, minYI, maxZ);
otherMax = new Point3D(maxXI, maxYI, maxZ + 1); otherMax = new Point3D(maxXI, maxYI, maxZ + 1);
doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Z_AXIS); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Z_AXIS);
adjacentNode = roomsToGraph.get(adjacent); layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway);
roomGraph.addEdge(roomNode, adjacentNode, doorway);
} }
} }
else else
@@ -295,8 +276,7 @@ public class MazeDesigner
otherMin = new Point3D(maxX, minYI, minZI); otherMin = new Point3D(maxX, minYI, minZI);
otherMax = new Point3D(maxX + 1, maxYI, maxZI); otherMax = new Point3D(maxX + 1, maxYI, maxZI);
doorway = new DoorwayData(otherMin, otherMax, DoorwayData.X_AXIS); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.X_AXIS);
adjacentNode = roomsToGraph.get(adjacent); layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway);
roomGraph.addEdge(roomNode, adjacentNode, doorway);
} }
} }
else else
@@ -343,8 +323,7 @@ public class MazeDesigner
otherMin = new Point3D(minXI, maxY, minZI); otherMin = new Point3D(minXI, maxY, minZI);
otherMax = new Point3D(maxXI, maxY + 1, maxZI); otherMax = new Point3D(maxXI, maxY + 1, maxZI);
doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Y_AXIS); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Y_AXIS);
adjacentNode = roomsToGraph.get(adjacent); layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway);
roomGraph.addEdge(roomNode, adjacentNode, doorway);
} }
} }
else else
@@ -359,108 +338,130 @@ public class MazeDesigner
//Done! //Done!
} }
private static ArrayList<IGraphNode<PartitionNode, DoorwayData>> createMazeSections(DirectedGraph<PartitionNode, DoorwayData> roomGraph, Random random) private static ArrayList<RoomData> createMazeSections(DirectedGraph<RoomData, DoorwayData> layout, Random random)
{ {
// The randomness of the sections generated here hinges on // The randomness of the sections generated here hinges on
// the nodes in the graph being in a random order. We assume // the nodes in the graph being in a random order. We assume
// that was handled in a previous step! // 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_ROOMS = 5;
final int MIN_SECTION_CAPACITY = 2;
int distance; int distance;
IGraphNode<PartitionNode, DoorwayData> current; int capacity;
IGraphNode<PartitionNode, DoorwayData> neighbor; RoomData room;
RoomData neighbor;
boolean hasHoles;
IGraphNode<RoomData, DoorwayData> roomNode;
ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(); ArrayList<RoomData> cores = new ArrayList<RoomData>();
ArrayList<IGraphNode<PartitionNode, DoorwayData>> removals = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(); ArrayList<RoomData> section = new ArrayList<RoomData>();
ArrayList<IGraphNode<PartitionNode, DoorwayData>> section = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(); ArrayList<IGraphNode<RoomData, DoorwayData>> nodes = new ArrayList<IGraphNode<RoomData, DoorwayData>>(layout.nodeCount());
Queue<IGraphNode<PartitionNode, DoorwayData>> ordering = new LinkedList<IGraphNode<PartitionNode, DoorwayData>>(); Queue<RoomData> ordering = new LinkedList<RoomData>();
HashMap<IGraphNode<PartitionNode, DoorwayData>, Integer> distances = new HashMap<IGraphNode<PartitionNode, DoorwayData>, Integer>();
// 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<RoomData, DoorwayData> node : layout.nodes())
{
nodes.add(node);
}
// Repeatedly generate sections until all nodes have been visited // Repeatedly generate sections until all nodes have been visited
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) for (IGraphNode<RoomData, DoorwayData> node : nodes)
{ {
// If this node hasn't been visited, then use it as the core of a new section // 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 // Otherwise, ignore it, since it was already processed. Also make sure to check that room
if (!distances.containsKey(node)) // 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 // Perform a breadth-first search to tag surrounding nodes with distances
distances.put(node, 0); ordering.add(room);
ordering.add(node); room.setDistance(0);
section.clear(); section.clear();
capacity = 0;
while (!ordering.isEmpty()) while (room != null && (room.getDistance() <= MAX_DISTANCE || capacity < 2))
{ {
current = ordering.remove(); ordering.remove();
distance = distances.get(current) + 1; 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<RoomData, DoorwayData> edge : roomNode.inbound())
{ {
section.add(current); neighbor = edge.head().data();
if (neighbor.getDistance() < 0)
// Visit neighboring nodes and assign them distances, if they don't
// have a distance assigned already
for (IEdge<PartitionNode, DoorwayData> edge : current.inbound())
{ {
neighbor = edge.head(); neighbor.setDistance(distance);
if (!distances.containsKey(neighbor)) ordering.add(neighbor);
}
if (edge.data().axis() == DoorwayData.Y_AXIS)
{ {
distances.put(neighbor, distance); hasHoles = true;
}
}
for (IEdge<RoomData, DoorwayData> edge : roomNode.outbound())
{
neighbor = edge.tail().data();
if (neighbor.getDistance() < 0)
{
neighbor.setDistance(distance);
ordering.add(neighbor); ordering.add(neighbor);
} }
} }
for (IEdge<PartitionNode, DoorwayData> edge : current.outbound())
// Count this room's door capacity if it has no floor holes
if (!hasHoles)
{ {
neighbor = edge.tail(); capacity += room.estimateDoorCapacity();
if (!distances.containsKey(neighbor))
{
distances.put(neighbor, distance);
ordering.add(neighbor);
}
}
}
else
{
removals.add(current);
break;
}
} }
// List nodes that have a distance of exactly MAX_DISTANCE + 1 room = ordering.peek();
// 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()) while (!ordering.isEmpty())
{ {
removals.add(ordering.remove()); ordering.remove().remove();
} }
// Check if this section contains enough rooms // Check if this section contains enough rooms and capacity for doors
if (section.size() >= MIN_SECTION_ROOMS) if (section.size() >= MIN_SECTION_ROOMS && capacity >= MIN_SECTION_CAPACITY)
{ {
cores.add(node); cores.add(node.data());
} }
else else
{ {
removals.addAll(section); // Discard the whole section
} for (RoomData target : section)
}
}
// Remove all the nodes that were listed for removal
// Also remove unused partitions from the partition tree
for (IGraphNode<PartitionNode, DoorwayData> node : removals)
{ {
removeRoomPartitions(node.data()); target.remove();
roomGraph.removeNode(node); }
}
}
} }
return cores; return cores;
} }
private static void pruneDoorways(IGraphNode<PartitionNode, DoorwayData> core, private static void pruneDoorways(IGraphNode<RoomData, DoorwayData> core,
DirectedGraph<PartitionNode, DoorwayData> rooms, Random random) DirectedGraph<RoomData, DoorwayData> layout, Random random)
{ {
// We receive a node for one of the rooms in a section of the maze // 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 // 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. // idea applies for the other doorways, plus some randomness.
// First, list all nodes in the subgraph // First, list all nodes in the subgraph
IGraphNode<PartitionNode, DoorwayData> current; IGraphNode<RoomData, DoorwayData> current;
IGraphNode<PartitionNode, DoorwayData> neighbor; IGraphNode<RoomData, DoorwayData> neighbor;
Stack<IGraphNode<PartitionNode, DoorwayData>> ordering = new Stack<IGraphNode<PartitionNode, DoorwayData>>(); Stack<IGraphNode<RoomData, DoorwayData>> ordering = new Stack<IGraphNode<RoomData, DoorwayData>>();
ArrayList<IGraphNode<PartitionNode, DoorwayData>> subgraph = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(64); ArrayList<IGraphNode<RoomData, DoorwayData>> subgraph = new ArrayList<IGraphNode<RoomData, DoorwayData>>(64);
DisjointSet<IGraphNode<PartitionNode, DoorwayData>> components = new DisjointSet<IGraphNode<PartitionNode, DoorwayData>>(128); DisjointSet<IGraphNode<RoomData, DoorwayData>> components = new DisjointSet<IGraphNode<RoomData, DoorwayData>>(128);
ordering.add(core); ordering.add(core);
components.makeSet(core); components.makeSet(core);
@@ -492,7 +493,7 @@ public class MazeDesigner
current = ordering.pop(); current = ordering.pop();
subgraph.add(current); subgraph.add(current);
for (IEdge<PartitionNode, DoorwayData> edge : current.inbound()) for (IEdge<RoomData, DoorwayData> edge : current.inbound())
{ {
neighbor = edge.head(); neighbor = edge.head();
if (components.makeSet(neighbor)) if (components.makeSet(neighbor))
@@ -500,7 +501,7 @@ public class MazeDesigner
ordering.add(neighbor); ordering.add(neighbor);
} }
} }
for (IEdge<PartitionNode, DoorwayData> edge : current.outbound()) for (IEdge<RoomData, DoorwayData> edge : current.outbound())
{ {
neighbor = edge.tail(); neighbor = edge.tail();
if (components.makeSet(neighbor)) if (components.makeSet(neighbor))
@@ -510,15 +511,17 @@ public class MazeDesigner
} }
} }
// Now iterate over the list of nodes and merge their sets // Now iterate over the list of nodes and merge their sets based on
// We only have to look at outbound edges since inbound edges mirror them // being connected by X_AXIS or Z_AXIS doorways. We only have to look
// Also list any Y_AXIS doorways we come across // at outbound edges since inbound edges mirror them. List any Y_AXIS
ArrayList<IEdge<PartitionNode, DoorwayData>> targets = // doorways we come across to consider removing them later, depending
new ArrayList<IEdge<PartitionNode, DoorwayData>>(); // on their impact on connectedness.
ArrayList<IEdge<RoomData, DoorwayData>> targets =
new ArrayList<IEdge<RoomData, DoorwayData>>();
for (IGraphNode<PartitionNode, DoorwayData> room : subgraph) for (IGraphNode<RoomData, DoorwayData> room : subgraph)
{ {
for (IEdge<PartitionNode, DoorwayData> passage : room.outbound()) for (IEdge<RoomData, DoorwayData> passage : room.outbound())
{ {
if (passage.data().axis() != DoorwayData.Y_AXIS) if (passage.data().axis() != DoorwayData.Y_AXIS)
{ {
@@ -535,11 +538,12 @@ public class MazeDesigner
Collections.shuffle(targets, random); Collections.shuffle(targets, random);
// Merge sets together and remove unnecessary doorways // Merge sets together and remove unnecessary doorways
for (IEdge<PartitionNode, DoorwayData> passage : targets) for (IEdge<RoomData, DoorwayData> passage : targets)
{ {
if (!components.mergeSets(passage.head(), passage.tail())) if (!components.mergeSets(passage.head(), passage.tail()))
{ {
rooms.removeEdge(passage); layout.removeEdge(passage);
} }
} }
@@ -548,13 +552,13 @@ public class MazeDesigner
components.clear(); components.clear();
targets.clear(); targets.clear();
for (IGraphNode<PartitionNode, DoorwayData> room : subgraph) for (IGraphNode<RoomData, DoorwayData> room : subgraph)
{ {
components.makeSet(room); components.makeSet(room);
} }
for (IGraphNode<PartitionNode, DoorwayData> room : subgraph) for (IGraphNode<RoomData, DoorwayData> room : subgraph)
{ {
for (IEdge<PartitionNode, DoorwayData> passage : room.outbound()) for (IEdge<RoomData, DoorwayData> passage : room.outbound())
{ {
if (passage.data().axis() == DoorwayData.Y_AXIS) if (passage.data().axis() == DoorwayData.Y_AXIS)
{ {
@@ -567,13 +571,198 @@ public class MazeDesigner
} }
} }
Collections.shuffle(targets, random); Collections.shuffle(targets, random);
for (IEdge<PartitionNode, DoorwayData> passage : targets) for (IEdge<RoomData, DoorwayData> passage : targets)
{ {
if (!components.mergeSets(passage.head(), passage.tail()) && random.nextBoolean()) if (!components.mergeSets(passage.head(), passage.tail()) && random.nextBoolean())
{ {
rooms.removeEdge(passage); layout.removeEdge(passage);
} }
} }
} }
private static void createMazeLinks(DirectedGraph<RoomData, DoorwayData> layout,
ArrayList<RoomData> 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<SectionData> allSections;
ArrayList<SectionData> 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<SectionData>(cores.size());
for (RoomData core : cores)
{
allSections.add( SectionData.createFromCore(core.getLayoutNode()) );
}
usableSections = (ArrayList<SectionData>) 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<SectionData> 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<SectionData> remaining = (ArrayList<SectionData>) sections.clone();
// Sections that are part of an incomplete cycle
ArrayList<SectionData> attached = new ArrayList<SectionData>(sections.size());
// Sections that are part of a cycle and thus strongly connected
ArrayList<SectionData> connected = new ArrayList<SectionData>(sections.size());
// Sections that are part of a cycle and have spare capacity - used to start new cycles
ArrayList<SectionData> starters = new ArrayList<SectionData>(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.
}
} }

View File

@@ -2,11 +2,12 @@ package StevenDimDoors.experimental;
import StevenDimDoors.mod_pocketDim.Point3D; import StevenDimDoors.mod_pocketDim.Point3D;
public class PartitionNode extends BoundingBox public class PartitionNode<T> extends BoundingBox
{ {
private PartitionNode parent; private PartitionNode parent;
private PartitionNode leftChild = null; private PartitionNode leftChild = null;
private PartitionNode rightChild = null; private PartitionNode rightChild = null;
private T data = null;
public PartitionNode(int width, int height, int length) public PartitionNode(int width, int height, int length)
{ {
@@ -122,4 +123,14 @@ public class PartitionNode extends BoundingBox
return this; return this;
} }
} }
public void setData(T value)
{
this.data = value;
}
public T getData()
{
return data;
}
} }

View File

@@ -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<LinkPlan> inboundLinks;
private ArrayList<LinkPlan> outboundLinks;
private DirectedGraph<RoomData, DoorwayData> layout;
private IGraphNode<RoomData, DoorwayData> layoutNode;
public RoomData(PartitionNode partitionNode)
{
this.partitionNode = partitionNode;
this.inboundLinks = new ArrayList<LinkPlan>();
this.outboundLinks = new ArrayList<LinkPlan>();
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<RoomData, DoorwayData> getLayoutNode()
{
return this.layoutNode;
}
public void addToLayout(DirectedGraph<RoomData, DoorwayData> layout)
{
this.layout = layout;
this.layoutNode = layout.addNode(this);
}
public boolean isDecayed()
{
return decayed;
}
public void setDecayed(boolean value)
{
this.decayed = value;
}
public ArrayList<LinkPlan> getInboundLinks()
{
return this.inboundLinks;
}
public ArrayList<LinkPlan> 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();
}
}

View File

@@ -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<RoomData> sourceRooms;
private ArrayList<RoomData> protectedRooms;
private ArrayList<RoomData> destinationRooms;
private ArrayList<LinkPlan> reservations;
private SectionData(ArrayList<RoomData> sourceRooms, ArrayList<RoomData> destinationRooms, int capacity)
{
this.capacity = capacity;
this.sourceRooms = sourceRooms;
this.destinationRooms = destinationRooms;
this.protectedRooms = new ArrayList<RoomData>();
this.reservations = new ArrayList<LinkPlan>();
}
public static SectionData createFromCore(IGraphNode<RoomData, DoorwayData> core)
{
int capacity = 0;
ArrayList<RoomData> sourceRooms = new ArrayList<RoomData>();
ArrayList<RoomData> destinationRooms = new ArrayList<RoomData>();
boolean hasHoles;
RoomData currentRoom;
IGraphNode<RoomData, DoorwayData> current;
IGraphNode<RoomData, DoorwayData> neighbor;
Stack<IGraphNode<RoomData, DoorwayData>> ordering = new Stack<IGraphNode<RoomData, DoorwayData>>();
HashSet<IGraphNode<RoomData, DoorwayData>> visited = new HashSet<IGraphNode<RoomData, DoorwayData>>();
visited.add(core);
ordering.add(core);
while (!ordering.isEmpty())
{
current = ordering.pop();
hasHoles = false;
for (IEdge<RoomData, DoorwayData> edge : current.outbound())
{
neighbor = edge.tail();
if (visited.add(neighbor))
{
ordering.add(neighbor);
}
}
for (IEdge<RoomData, DoorwayData> 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;
}
}

View File

@@ -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);
}

View File

@@ -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<BaseDecorator> 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<BaseDecorator> matches = new ArrayList<BaseDecorator>();
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<BaseDecorator>();
decorators.add(new LinkDestinationDecorator());
decorators.add(new DefaultDoorDecorator());
decorators.add(new TorchDecorator());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<RoomData> 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);
}
}
}

View File

@@ -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<RoomData> 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;
}
}
}
}

View File

@@ -56,6 +56,20 @@ public class Point3D implements Serializable {
return this.z = z; 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 @Override
public Point3D clone() public Point3D clone()
{ {

View File

@@ -178,8 +178,9 @@ public abstract class BaseDimDoor extends BlockDoor implements IDimDoor, ITileEn
TileEntity tile = world.getBlockTileEntity(x, y, z); TileEntity tile = world.getBlockTileEntity(x, y, z);
if (tile instanceof TileEntityDimDoor) if (tile instanceof TileEntityDimDoor)
{ {
int metadata = world.getBlockMetadata(x, y, z);
TileEntityDimDoor dimTile = (TileEntityDimDoor) tile; TileEntityDimDoor dimTile = (TileEntityDimDoor) tile;
dimTile.openOrClosed = this.isDoorOnRift(world, x, y, z); dimTile.openOrClosed = this.isDoorOnRift(world, x, y, z)&&this.isUpperDoorBlock(metadata);
dimTile.orientation = this.getFullMetadata(world, x, y, z) & 7; dimTile.orientation = this.getFullMetadata(world, x, y, z) & 7;
} }
return this; return this;

View File

@@ -465,6 +465,7 @@ public class PocketBuilder
Point3D door = new Point3D(x, y, z); Point3D door = new Point3D(x, y, z);
BlockRotator.transformPoint(center, door, orientation - BlockRotator.EAST_DOOR_METADATA, door); BlockRotator.transformPoint(center, door, orientation - BlockRotator.EAST_DOOR_METADATA, door);
/*
//Build the outer layer of Eternal Fabric //Build the outer layer of Eternal Fabric
buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2), properties.PermaFabricBlockID, false, 0); 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, buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2) - layer, properties.FabricBlockID,
layer < (wallThickness - 1) && properties.TNFREAKINGT_Enabled, properties.NonTntWeight); 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 //Build the door
int doorOrientation = BlockRotator.transformMetadata(BlockRotator.EAST_DOOR_METADATA, orientation - BlockRotator.EAST_DOOR_METADATA + 2, properties.DimensionalDoorID); int doorOrientation = BlockRotator.transformMetadata(BlockRotator.EAST_DOOR_METADATA, orientation - BlockRotator.EAST_DOOR_METADATA + 2, properties.DimensionalDoorID);

View File

@@ -213,50 +213,50 @@ public class RenderDimDoor extends TileEntitySpecialRenderer
{ {
case 0: case 0:
GL11.glVertex3d(x + .01F, y, z); GL11.glVertex3d(x + .01F, y - 1, z);
GL11.glVertex3d(x + .01, y , z + 1.0D); GL11.glVertex3d(x + .01, y - 1, z + 1.0D);
GL11.glVertex3d(x + .01, y + 1, z + 1.0D); GL11.glVertex3d(x + .01, y + 1, z + 1.0D);
GL11.glVertex3d(x + .01, y + 1, z); GL11.glVertex3d(x + .01, y + 1, z);
break; break;
case 1: case 1:
GL11.glVertex3d(x, y + 1, z + .01); GL11.glVertex3d(x, y + 1, z + .01);
GL11.glVertex3d(x + 1, y + 1, z + .01); GL11.glVertex3d(x + 1, y + 1, z + .01);
GL11.glVertex3d(x + 1, y , z + .01); GL11.glVertex3d(x + 1, y -1, z + .01);
GL11.glVertex3d(x, y , z + .01); GL11.glVertex3d(x, y -1, z + .01);
break; break;
case 2: case 2:
GL11.glVertex3d(x + .99, y + 1, z); GL11.glVertex3d(x + .99, y + 1, z);
GL11.glVertex3d(x + .99, y + 1, z + 1.0D); GL11.glVertex3d(x + .99, y + 1, z + 1.0D);
GL11.glVertex3d(x + .99, y , z + 1.0D); GL11.glVertex3d(x + .99, y - 1, z + 1.0D);
GL11.glVertex3d(x + .99, y , z); GL11.glVertex3d(x + .99, y - 1, z);
break; break;
case 3: case 3:
GL11.glVertex3d(x, y , z + .99); GL11.glVertex3d(x, y -1, z + .99);
GL11.glVertex3d(x + 1, y , z + .99); GL11.glVertex3d(x + 1, y -1, z + .99);
GL11.glVertex3d(x + 1, y + 1, z + .99); GL11.glVertex3d(x + 1, y + 1, z + .99);
GL11.glVertex3d(x, y + 1, z + .99); GL11.glVertex3d(x, y + 1, z + .99);
break; break;
case 4: case 4:
GL11.glVertex3d(x + .15F, y , z); GL11.glVertex3d(x + .15F, y - 1 , z);
GL11.glVertex3d(x + .15, y , z + 1.0D); GL11.glVertex3d(x + .15, y - 1, z + 1.0D);
GL11.glVertex3d(x + .15, y + 1, z + 1.0D); GL11.glVertex3d(x + .15, y + 1, z + 1.0D);
GL11.glVertex3d(x + .15, y + 1, z); GL11.glVertex3d(x + .15, y + 1, z);
break; break;
case 5: case 5:
GL11.glVertex3d(x, y + 1, z + .15); GL11.glVertex3d(x, y + 1, z + .15);
GL11.glVertex3d(x + 1, y + 1, z + .15); GL11.glVertex3d(x + 1, y + 1, z + .15);
GL11.glVertex3d(x + 1, y , z + .15); GL11.glVertex3d(x + 1, y - 1, z + .15);
GL11.glVertex3d(x, y , z + .15); GL11.glVertex3d(x, y - 1, z + .15);
break; break;
case 6: case 6:
GL11.glVertex3d(x + .85, y + 1, z); GL11.glVertex3d(x + .85, y + 1, z);
GL11.glVertex3d(x + .85, y + 1, z + 1.0D); GL11.glVertex3d(x + .85, y + 1, z + 1.0D);
GL11.glVertex3d(x + .85, y , z + 1.0D); GL11.glVertex3d(x + .85, y - 1, z + 1.0D);
GL11.glVertex3d(x + .85, y , z); GL11.glVertex3d(x + .85, y - 1, z);
break; break;
case 7: case 7:
GL11.glVertex3d(x, y , z + .85); GL11.glVertex3d(x, y - 1, z + .85);
GL11.glVertex3d(x + 1, y , z + .85); GL11.glVertex3d(x + 1, y - 1, z + .85);
GL11.glVertex3d(x + 1, y + 1, z + .85); GL11.glVertex3d(x + 1, y + 1, z + .85);
GL11.glVertex3d(x, y + 1, z + .85); GL11.glVertex3d(x, y + 1, z + .85);
break; break;