Add upper-road stairwell generation

This commit is contained in:
2026-09-27 06:55:39 -07:00
parent 04a0c1d959
commit fd5bf785d3
9 changed files with 465 additions and 17 deletions

View File

@@ -31,6 +31,7 @@ Use this checklist for behavior that depends on a real Paper world, player inter
### Generated blocks and geometry
- Inspect a newly allocated home and adjoining roads. Check road width, level, material, clearance, plot floor, support blocks, and optional sign against the active config.
- Enable avenue stairs with a stair width narrower than the avenue. Generate a plot on a level below `plot.maxLevels`; verify stairs are added while the upper road is built and the lower road stays unchanged. Confirm the first tread starts one block above and forward from the road-level landing, connected following treads use inverted stairs underneath, and the top tread reaches the upper road Y. Check that exactly three road blocks before the top tread remain open only across the stair width, with regular road beside the opening. Repeat with a higher-level plot to confirm the lower stair flight remains open.
- Test generation over varied terrain, including water, caves, uneven ground, and existing structures. Confirm generated floors and roads do not leave unsafe gaps or erase blocks outside their intended area.
- Enable and disable optional floor, support, sign, spawn, world-spawn, and perimeter-wall settings one at a time on a disposable world. Confirm each setting changes only its intended behavior.
- Allocate plots in several directions and near the world origin. Check road joins, plot dimensions, City region expansion, and boundary coordinates for symmetry and off-by-one errors.
@@ -72,8 +73,10 @@ Use this checklist for behavior that depends on a real Paper world, player inter
## Existing Automated Coverage
Run `./gradlew test` for the server-free JUnit suite. It has 28 tests covering:
Run `./gradlew test` for the server-free JUnit suite. It has 34 tests covering:
- `AvenueStairwayLayoutTest`: centered flight geometry, one-block offset from the lower road, top-tread alignment with the upper road, inverted backing plan, exact-width upper-road opening, and invalid-fit rejection.
- `AvenueStairwayBuilderTest`: configured stair material and facing/half data, upside-down backing stairs, exact upper-road AIR placements, and no block writes at lower-road Y.
- `CuboidTest`: WorldEdit bound conversion, vector ordering/null handling, touching intersections, and point containment.
- `CurrentHomesStoreTest`: UUID-based persistence and resolving a legacy player-name key.
- `PlayerJoinListenerTest`: the welcome bounds message when a home is supplied by a mock.

View File

@@ -60,6 +60,8 @@ Copy `.env.example` to `.env` and configure the Paper version/build and server p
`plugins/Metropolis/config.yml` controls plot dimensions and limits, floor and support generation, signs, road width/level/material/clearance, spawn behavior, wall generation, world name, and per-player overrides. The per-level stack is configured with `plot.sizeY` and `plot.maxLevels`: `sizeY` is the full height of one plot level, and `maxLevels` is the number of stacked levels that can be allocated without exceeding the world build height. `plot.multiplier` sets the default plot-size multiplier; `plot.maxPerPlayer` sets the default home limit; `plot.initial` sets how many homes are auto-generated on join, and `0` requires `/metropolis-home-acquire` for the player's first home.
Enable `road.avenues.stairs.generate` to add a centered stair run as part of the upper-level road. Set `material`, `width`, and `everyNBlocks`; the stair width must be narrower than `road.avenues.width`. The lower road is left unchanged and provides the normal-material landing; stairs begin one block above and forward from it. Connected following treads use upside-down stairs facing back down the run as backing, and the final tread reaches the upper road Y. The first implementation ascends along positive Z on the east-side avenue and leaves the three blocks before the top tread open in the upper road, only across the stair width.
The breaking multi-level schema is documented in the example config at `src/main/resources/examples/multi-level.yml`. It uses split road sections (`road.streets.*` and `road.avenues.*`), a level-aware `plot.maxLevels` field, and the valid stair material `COBBLESTONE_STAIRS`.
Use `permissionOverrides` to assign plot size and home limits through any permission manager. Each permission node maps to a `priority`, `plotMultiplier`, and `maxPlots`; the matching node with the highest priority wins, and the last matching entry in YAML order wins ties. An explicit username entry in `userOverrides` takes precedence over permission rules. If no override matches, the global `plot.*` defaults apply. Permission rules are checked for online players; offline home generation uses username overrides or global defaults. For example:

View File

@@ -0,0 +1,63 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Flight;
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Step;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Stairs;
public final class AvenueStairwayBuilder {
private AvenueStairwayBuilder() {}
public static void build(World world, Flight flight, Material stairMaterial) {
for (Step step : flight.stairBlocks()) {
Block stairBlock = world.getBlockAt(step.x(), step.y(), step.z());
stairBlock.setType(stairMaterial, false);
BlockData blockData = stairBlock.getBlockData();
if (blockData instanceof Stairs stairs) {
stairs.setFacing(BlockFace.SOUTH);
stairs.setHalf(Bisected.Half.BOTTOM);
stairs.setShape(Stairs.Shape.STRAIGHT);
stairBlock.setBlockData(stairs, false);
}
}
for (Step backing : flight.invertedBackingBlocks()) {
Block supportBlock = world.getBlockAt(backing.x(), backing.y(), backing.z());
supportBlock.setType(stairMaterial, false);
BlockData supportData = supportBlock.getBlockData();
if (supportData instanceof Stairs stairs) {
stairs.setFacing(BlockFace.NORTH);
stairs.setHalf(Bisected.Half.TOP);
stairs.setShape(Stairs.Shape.STRAIGHT);
supportBlock.setBlockData(stairs, false);
}
}
for (Step opening : flight.roadOpenings()) {
world.getBlockAt(opening.x(), opening.y(), opening.z()).setType(Material.AIR, false);
}
}
}

View File

@@ -0,0 +1,97 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import java.util.ArrayList;
import java.util.List;
public final class AvenueStairwayLayout {
private AvenueStairwayLayout() {}
public static List<Step> centeredEastAvenue(
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise) {
int plotLength = plot.maxZ - plot.minZ + 1;
if (roadWidth < 1
|| stairWidth < 1
|| stairWidth >= roadWidth
|| rise < 1
|| rise + 2 > plotLength) {
return List.of();
}
int firstX = plot.maxX + 1 + (roadWidth - stairWidth) / 2;
int firstZ = plot.minZ + (plotLength - rise) / 2 + 1;
List<Step> steps = new ArrayList<>(stairWidth * rise);
for (int riseIndex = 0; riseIndex < rise; riseIndex++) {
for (int widthIndex = 0; widthIndex < stairWidth; widthIndex++) {
steps.add(new Step(firstX + widthIndex, lowerY + riseIndex + 1, firstZ + riseIndex));
}
}
return List.copyOf(steps);
}
public static List<Step> upperRoadOpening(
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) {
List<Step> stairs = centeredEastAvenue(plot, roadWidth, stairWidth, lowerY, rise);
if (stairs.isEmpty() || openingLength < 1) {
return List.of();
}
Step topStair = stairs.get(stairs.size() - 1);
int firstX = stairs.getFirst().x();
int upperRoadY = lowerY + rise;
List<Step> opening = new ArrayList<>(stairWidth * openingLength);
for (int widthIndex = 0; widthIndex < stairWidth; widthIndex++) {
for (int lengthIndex = openingLength; lengthIndex > 0; lengthIndex--) {
opening.add(new Step(firstX + widthIndex, upperRoadY, topStair.z() - lengthIndex));
}
}
return List.copyOf(opening);
}
public static Flight forUpperRoad(
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) {
List<Step> stairs = centeredEastAvenue(plot, roadWidth, stairWidth, lowerY, rise);
if (stairs.isEmpty()) {
return new Flight(List.of(), List.of(), List.of());
}
List<Step> invertedBackings = new ArrayList<>();
for (Step stair : stairs) {
if (stair.y() > lowerY + 1) {
invertedBackings.add(new Step(stair.x(), stair.y() - 1, stair.z()));
}
}
return new Flight(
stairs,
invertedBackings,
upperRoadOpening(plot, roadWidth, stairWidth, lowerY, rise, openingLength));
}
public record Step(int x, int y, int z) {}
public record Flight(
List<Step> stairBlocks, List<Step> invertedBackingBlocks, List<Step> roadOpenings) {
public Flight {
stairBlocks = List.copyOf(stairBlocks);
invertedBackingBlocks = List.copyOf(invertedBackingBlocks);
roadOpenings = List.copyOf(roadOpenings);
}
}
}

View File

@@ -56,6 +56,7 @@ import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.block.data.type.Stairs;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -100,11 +101,16 @@ public class MetropolisPlugin extends JavaPlugin {
private int maxLevels = 1;
private int gridSizeZ = 28;
private int roadWidth = 4;
private int avenueWidth = 4;
private int roadLevel = 62;
private int spaceAboveRoad = 2;
private Material roadMaterial = Material.COBBLESTONE;
private boolean generateRoadSupports = true;
private Material roadSupportMaterial = Material.STONE;
private boolean generateAvenueStairs;
private Material avenueStairMaterial = Material.COBBLESTONE_STAIRS;
private int avenueStairWidth;
private int avenueStairsEveryNBlocks;
private String worldName = "world";
private boolean generateFloor = false;
private Material floorMaterial = Material.GRASS_BLOCK;
@@ -190,6 +196,7 @@ public class MetropolisPlugin extends JavaPlugin {
generateSign = safeGetBooleanFromConfig(config, "plot.sign.generate");
roadWidth =
safeGetIntFromConfig(config, "road.streets.width", "road.avenues.width", "road.width");
avenueWidth = safeGetIntFromConfig(config, "road.avenues.width");
spaceAboveRoad =
safeGetIntFromConfig(
config,
@@ -213,6 +220,32 @@ public class MetropolisPlugin extends JavaPlugin {
"road.streets.supports.material",
"road.avenues.supports.material",
"road.supports.material");
generateAvenueStairs = safeGetBooleanFromConfig(config, "road.avenues.stairs.generate");
if (generateAvenueStairs) {
avenueStairMaterial = safeGetMaterialFromConfig(config, "road.avenues.stairs.material");
avenueStairWidth = safeGetIntFromConfig(config, "road.avenues.stairs.width");
avenueStairsEveryNBlocks = safeGetIntFromConfig(config, "road.avenues.stairs.everyNBlocks");
if (avenueStairWidth < 1 || avenueStairWidth >= avenueWidth) {
getLogger()
.severe(
"road.avenues.stairs.width must be positive and narrower than road.avenues.width;"
+ " avenue stairs are disabled.");
generateAvenueStairs = false;
}
if (avenueStairsEveryNBlocks < 1) {
getLogger()
.severe(
"road.avenues.stairs.everyNBlocks must be positive; avenue stairs are disabled.");
generateAvenueStairs = false;
}
if (!(avenueStairMaterial.createBlockData() instanceof Stairs)) {
getLogger()
.severe(
"road.avenues.stairs.material must be a stair block; avenue stairs are disabled.");
generateAvenueStairs = false;
}
}
generateSpawn = safeGetBooleanFromConfig(config, "spawn.generate");
setWorldSpawn = safeGetBooleanFromConfig(config, "spawn.setAsWorldSpawn");
spawnFloorMaterial = safeGetMaterialFromConfig(config, "spawn.material");
@@ -695,18 +728,19 @@ public class MetropolisPlugin extends JavaPlugin {
}
private void createRoads(Cuboid plotCuboid, int roadMask) {
if (plotCuboid == null) {
if (DEBUG) {
getLogger().warning("plotCuboid is null");
}
return;
}
if (roadWidth > 0) {
int x = 0;
int y = plotCuboid.minY;
int z = 0;
if (plotCuboid == null) {
if (DEBUG) {
getLogger().warning("plotCuboid is null");
}
return;
}
List<AvenueStairwayLayout.Step> stairOpenings = getUpperRoadOpenings(plotCuboid, y);
int roadWidth1 = roadWidth / 2;
int roadWidth2 = roadWidth - roadWidth1;
@@ -714,7 +748,7 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & (ROAD_NORTH | ROAD_WEST)) != 0) {
for (x = plotCuboid.minX - roadWidth1; x < plotCuboid.minX; x++) {
for (z = plotCuboid.minZ - roadWidth1; z < plotCuboid.minZ; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
@@ -723,7 +757,7 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & ROAD_NORTH) != 0) {
for (x = plotCuboid.minX; x <= plotCuboid.maxX; x++) {
for (z = plotCuboid.minZ - roadWidth1; z < plotCuboid.minZ; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
@@ -732,7 +766,7 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & (ROAD_NORTH | ROAD_EAST)) != 0) {
for (x = plotCuboid.maxX + 1; x <= plotCuboid.maxX + roadWidth2; x++) {
for (z = plotCuboid.minZ - roadWidth1; z < plotCuboid.minZ; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
@@ -741,7 +775,7 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & ROAD_EAST) != 0) {
for (x = plotCuboid.maxX + 1; x <= plotCuboid.maxX + roadWidth2; x++) {
for (z = plotCuboid.minZ; z <= plotCuboid.maxZ; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
@@ -750,7 +784,7 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & (ROAD_SOUTH | ROAD_EAST)) != 0) {
for (x = plotCuboid.maxX + 1; x <= plotCuboid.maxX + roadWidth2; x++) {
for (z = plotCuboid.maxZ + 1; z <= plotCuboid.maxZ + roadWidth2; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
@@ -759,7 +793,7 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & ROAD_SOUTH) != 0) {
for (x = plotCuboid.minX; x <= plotCuboid.maxX; x++) {
for (z = plotCuboid.maxZ + 1; z <= plotCuboid.maxZ + roadWidth2; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
@@ -768,7 +802,7 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & (ROAD_SOUTH | ROAD_WEST)) != 0) {
for (x = plotCuboid.minX - roadWidth1; x < plotCuboid.minX; x++) {
for (z = plotCuboid.maxZ + 1; z <= plotCuboid.maxZ + roadWidth2; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
@@ -777,10 +811,15 @@ public class MetropolisPlugin extends JavaPlugin {
if ((roadMask & ROAD_WEST) != 0) {
for (x = plotCuboid.minX - roadWidth1; x < plotCuboid.minX; x++) {
for (z = plotCuboid.minZ; z <= plotCuboid.maxZ; z++) {
setRoad(x, y, z);
setRoad(x, y, z, stairOpenings);
}
}
}
int upperLevel = Math.floorDiv(y - roadLevel, getLevelPitchY());
if (upperLevel > 0 && getLevelStartY(upperLevel) == y) {
generateAvenueStairs(plotCuboid, upperLevel - 1);
}
}
}
@@ -788,7 +827,11 @@ public class MetropolisPlugin extends JavaPlugin {
createRoads(plotCuboid, ROAD_NORTH | ROAD_SOUTH | ROAD_EAST | ROAD_WEST);
}
private void setRoad(int x, int y, int z) {
private void setRoad(int x, int y, int z, List<AvenueStairwayLayout.Step> stairOpenings) {
if (stairOpenings.contains(new AvenueStairwayLayout.Step(x, y, z))) {
return;
}
// if(DEBUG){getLogger().info(String.format("setting road at (%d, %d, %d)", x, y, z));}
Block block = world.getBlockAt(x, y, z);
@@ -1193,6 +1236,61 @@ public class MetropolisPlugin extends JavaPlugin {
sign.update(true);
}
private void generateAvenueStairs(Cuboid plotCuboid, int lowerLevel) {
if (!generateAvenueStairs || lowerLevel < 0 || lowerLevel >= getEffectiveMaxLevels() - 1) {
return;
}
int avenueBlockIndex = getPlotZFromMin(plotCuboid);
if (Math.floorMod(avenueBlockIndex + 1, avenueStairsEveryNBlocks) != 0) {
return;
}
int lowerY = getLevelStartY(lowerLevel);
AvenueStairwayLayout.Flight flight =
AvenueStairwayLayout.forUpperRoad(
plotCuboid, avenueWidth, avenueStairWidth, lowerY, getLevelPitchY(), 3);
if (flight.stairBlocks().isEmpty()) {
getLogger()
.warning(
"Avenue stair run does not fit beside plot at "
+ plotCuboid.minX
+ ","
+ plotCuboid.minY
+ ","
+ plotCuboid.minZ);
return;
}
AvenueStairwayBuilder.build(world, flight, avenueStairMaterial);
}
private List<AvenueStairwayLayout.Step> getUpperRoadOpenings(Cuboid plotCuboid, int roadY) {
if (!generateAvenueStairs || roadY <= roadLevel) {
return List.of();
}
int upperLevel = Math.floorDiv(roadY - roadLevel, getLevelPitchY());
if (getLevelStartY(upperLevel) != roadY) {
return List.of();
}
int lowerLevel = upperLevel - 1;
int avenueBlockIndex = getPlotZFromMin(plotCuboid);
if (lowerLevel < 0 || Math.floorMod(avenueBlockIndex + 1, avenueStairsEveryNBlocks) != 0) {
return List.of();
}
return AvenueStairwayLayout.forUpperRoad(
plotCuboid,
avenueWidth,
avenueStairWidth,
getLevelStartY(lowerLevel),
getLevelPitchY(),
3)
.roadOpenings();
}
public List<Plot> getCityBlocks() {
return Collections.unmodifiableList(_occupiedPlots);
}

View File

@@ -26,6 +26,8 @@ road:
supports:
generate: true
material: STONE
stairs:
generate: false
streets:
width: 4
clearSpaceAbove: 66

View File

@@ -31,6 +31,7 @@ road:
generate: true
material: COBBLESTONE_STAIRS
everyNBlocks: 4
width: 2
streets:
width: 4
clearSpaceAbove: 9

View File

@@ -0,0 +1,95 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Flight;
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Step;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.type.Stairs;
import org.junit.jupiter.api.Test;
class AvenueStairwayBuilderTest {
@Test
void buildsAscendingAndInvertedStairsAndClearsOnlyThePlannedOpening() {
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
Flight flight = AvenueStairwayLayout.forUpperRoad(plot, 4, 2, 62, 14, 3);
World world = mock(World.class);
Map<Step, Block> blocks = new HashMap<>();
Map<Step, Stairs> blockData = new HashMap<>();
when(world.getBlockAt(anyInt(), anyInt(), anyInt()))
.thenAnswer(
invocation -> {
Step position =
new Step(
invocation.getArgument(0),
invocation.getArgument(1),
invocation.getArgument(2));
return blocks.computeIfAbsent(
position,
ignored -> {
Block block = mock(Block.class);
Stairs stairs = mock(Stairs.class);
when(block.getBlockData()).thenReturn(stairs);
blockData.put(position, stairs);
return block;
});
});
AvenueStairwayBuilder.build(world, flight, Material.COBBLESTONE_STAIRS);
for (Step stair : flight.stairBlocks()) {
verify(blocks.get(stair)).setType(Material.COBBLESTONE_STAIRS, false);
}
for (Step backing : flight.invertedBackingBlocks()) {
verify(blocks.get(backing)).setType(Material.COBBLESTONE_STAIRS, false);
}
for (Step opening : flight.roadOpenings()) {
verify(blocks.get(opening)).setType(Material.AIR, false);
}
Step firstStair = flight.stairBlocks().getFirst();
Stairs firstStairData = blockData.get(firstStair);
assertNotNull(firstStairData);
verify(firstStairData).setFacing(BlockFace.SOUTH);
verify(firstStairData).setHalf(Bisected.Half.BOTTOM);
Step firstInvertedBacking = flight.invertedBackingBlocks().getFirst();
Stairs invertedBackingData = blockData.get(firstInvertedBacking);
assertNotNull(invertedBackingData);
verify(invertedBackingData).setFacing(BlockFace.NORTH);
verify(invertedBackingData).setHalf(Bisected.Half.TOP);
verify(world, never()).getBlockAt(anyInt(), eq(62), anyInt());
}
}

View File

@@ -0,0 +1,87 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
class AvenueStairwayLayoutTest {
@Test
void centersStairsAcrossTheAvenueAndAlongThePlotLength() {
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
List<AvenueStairwayLayout.Step> steps =
AvenueStairwayLayout.centeredEastAvenue(plot, 4, 2, 62, 14);
assertEquals(28, steps.size());
assertEquals(new AvenueStairwayLayout.Step(35, 63, 12), steps.get(0));
assertEquals(new AvenueStairwayLayout.Step(36, 63, 12), steps.get(1));
assertEquals(new AvenueStairwayLayout.Step(35, 64, 13), steps.get(2));
assertEquals(new AvenueStairwayLayout.Step(36, 76, 25), steps.get(27));
}
@Test
void opensOnlyTheThreeUpperRoadBlocksBeforeTheTopStair() {
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
List<AvenueStairwayLayout.Step> openings =
AvenueStairwayLayout.upperRoadOpening(plot, 4, 2, 62, 14, 3);
assertEquals(6, openings.size());
assertEquals(new AvenueStairwayLayout.Step(35, 76, 22), openings.get(0));
assertEquals(new AvenueStairwayLayout.Step(35, 76, 23), openings.get(1));
assertEquals(new AvenueStairwayLayout.Step(35, 76, 24), openings.get(2));
assertEquals(new AvenueStairwayLayout.Step(36, 76, 24), openings.get(5));
}
@Test
void upperRoadFlightPreservesTheLowerRoadAndPlansInvertedBacking() {
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
AvenueStairwayLayout.Flight flight = AvenueStairwayLayout.forUpperRoad(plot, 4, 2, 62, 14, 3);
assertEquals(28, flight.stairBlocks().size());
assertEquals(new AvenueStairwayLayout.Step(35, 63, 12), flight.stairBlocks().getFirst());
assertEquals(new AvenueStairwayLayout.Step(36, 76, 25), flight.stairBlocks().getLast());
assertEquals(26, flight.invertedBackingBlocks().size());
assertEquals(
new AvenueStairwayLayout.Step(35, 63, 13), flight.invertedBackingBlocks().getFirst());
assertTrue(flight.stairBlocks().stream().allMatch(step -> step.y() > 62));
assertTrue(flight.invertedBackingBlocks().stream().allMatch(step -> step.y() > 62));
assertEquals(6, flight.roadOpenings().size());
assertTrue(flight.roadOpenings().stream().allMatch(step -> step.y() == 76));
}
@Test
void rejectsAStairRunThatCannotFitWithinThePlotLength() {
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 13);
assertTrue(AvenueStairwayLayout.centeredEastAvenue(plot, 4, 2, 62, 14).isEmpty());
}
@Test
void rejectsStairsThatAreNotNarrowerThanTheAvenue() {
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
assertTrue(AvenueStairwayLayout.centeredEastAvenue(plot, 4, 4, 62, 14).isEmpty());
}
}