Improve multi-level support and stairs

This commit is contained in:
2026-09-27 07:52:48 -07:00
parent 30c31c185b
commit 917dac9c8e
7 changed files with 276 additions and 46 deletions

View File

@@ -23,7 +23,7 @@ Use this checklist for behavior that depends on a real Paper world, player inter
- Join as a new player. Confirm one home is allocated and the welcome message reports its actual bounds. Check the corresponding `h_1_<UUID>` region has the player as owner and its bounds match the configured plot size. - Join as a new player. Confirm one home is allocated and the welcome message reports its actual bounds. Check the corresponding `h_1_<UUID>` region has the player as owner and its bounds match the configured plot size.
- Set `plot.initial: 0` on a disposable server, restart, and join as a fresh player. Confirm no home is created until the player runs `/metropolis-home-acquire`; then verify the command creates the initial home and makes it active. - Set `plot.initial: 0` on a disposable server, restart, and join as a fresh player. Confirm no home is created until the player runs `/metropolis-home-acquire`; then verify the command creates the initial home and makes it active.
- Join with a second new player. Confirm the new home is distinct, does not overlap the first home or spawn, and the City region grows to include it. - Join with a second new player. Confirm the new home is distinct, does not overlap the first home or spawn, and the City region grows to include it.
- Start from a disposable world configured with `plot.sizeY` and `plot.maxLevels` from the multi-level example. Confirm the level count is honored, the city does not exceed the world's build height, and stacked plots use the same X/Z allocation pattern at each level. - Start from a disposable world configured with `plot.sizeY` and `plot.maxLevels` from the multi-level example. Confirm the level count is honored, the city does not exceed the world's build height, and upper plots are only allocated above footprints fully supported at every lower level. With a 1×1 spawn and a 2×2 home size, confirm a new home is allocated at base level rather than above the partially supported spawn.
- Join again as an existing player. Confirm the same home is selected and no duplicate region or plot is created. - Join again as an existing player. Confirm the same home is selected and no duplicate region or plot is created.
- Reserve an area before allocating another home. Confirm future allocations skip it. Repeat with reservations near the city edge and near existing roads to look for overlaps or gaps. - Reserve an area before allocating another home. Confirm future allocations skip it. Repeat with reservations near the city edge and near existing roads to look for overlaps or gaps.
- Test a user override that grants a different plot multiplier or home limit. Confirm the generated bounds and allowed number of homes match the override, and that the configured global defaults still apply to other players. - Test a user override that grants a different plot multiplier or home limit. Confirm the generated bounds and allowed number of homes match the override, and that the configured global defaults still apply to other players.
@@ -32,7 +32,7 @@ Use this checklist for behavior that depends on a real Paper world, player inter
- 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. - 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.
- Generate one plot in a disposable world and confirm each adjoining road is fully paved to the configured width. Generate a plot directly beside it: confirm already-correct shared road blocks are untouched, any missing half-width legacy section is filled, and stair treads/openings remain intact. - Generate one plot in a disposable world and confirm each adjoining road is fully paved to the configured width. Generate a plot directly beside it: confirm already-correct shared road blocks are untouched, any missing half-width legacy section is filled, and stair treads/openings remain intact.
- 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. - 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 on both bordering avenues 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 each top tread remain open only across the stair width, with regular road beside each opening. For a 2-plot-long home with `everyNBlocks: 1`, confirm there are two centered runs on each bordering avenue. Repeat with a higher-level plot to confirm lower stair flights remain 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. - 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. - 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. - 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.
@@ -74,9 +74,10 @@ Use this checklist for behavior that depends on a real Paper world, player inter
## Existing Automated Coverage ## Existing Automated Coverage
Run `./gradlew test` for the server-free JUnit suite. It has 40 tests covering: Run `./gradlew test` for the server-free JUnit suite. It has 45 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. - `AvenueStairwayLayoutTest`: centered per-segment flights on both avenue sides, per-segment cadence, top-tread alignment, inverted backing plan, exact-width upper-road openings, and invalid-fit rejection.
- `PlotLevelSupportTest`: rejection of upper plots with missing logical support cells and complete coverage requirements across all lower levels.
- `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. - `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.
- `RoadLayoutTest`: full-width surrounding road bands, non-overlapping corners, and disabled roads at zero width. - `RoadLayoutTest`: full-width surrounding road bands, non-overlapping corners, and disabled roads at zero width.
- `RoadBlockWriterTest`: no-op writes for existing roads, filling missing road blocks, and preserving stair treads. - `RoadBlockWriterTest`: no-op writes for existing roads, filling missing road blocks, and preserving stair treads.

View File

@@ -60,7 +60,9 @@ 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. `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. Enable `road.avenues.stairs.generate` to add centered stair runs as part of upper-level roads. Set `material`, `width`, and `everyNBlocks`; the stair width must be narrower than `road.avenues.width`. Runs are centered within each eligible logical plot-sized avenue segment on both bordering avenues. For a 2-plot-long home, `everyNBlocks: 1` creates two runs on each border avenue. The lower road remains 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 implementation ascends along positive Z and leaves the three blocks before each top tread open only across the stair width.
Upper-level plots require complete generated support below: every logical plot cell in their footprint must be occupied by a generated home or spawn at every lower level. If a footprint has gaps below, allocation skips that upper candidate and keeps searching for a supported lower-level location.
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`. 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`.

View File

@@ -25,29 +25,64 @@ public final class AvenueStairwayLayout {
public static List<Step> centeredEastAvenue( public static List<Step> centeredEastAvenue(
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise) { Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise) {
int plotLength = plot.maxZ - plot.minZ + 1; return stairBlocks(plot, roadWidth, stairWidth, lowerY, rise, Side.EAST);
if (roadWidth < 1 }
|| stairWidth < 1
|| stairWidth >= roadWidth public static List<Flight> forUpperRoadSegments(
|| rise < 1 Cuboid plot,
|| rise + 2 > plotLength) { int plotSizeZ,
int gridSizeZ,
int firstBlockIndex,
int everyNBlocks,
int avenueWidth,
int stairWidth,
int lowerY,
int rise,
int openingLength) {
if (plotSizeZ < 1 || gridSizeZ < plotSizeZ || everyNBlocks < 1) {
return List.of(); return List.of();
} }
int firstX = plot.maxX + 1 + (roadWidth - stairWidth) / 2; List<Flight> flights = new ArrayList<>();
int firstZ = plot.minZ + (plotLength - rise) / 2 + 1; int segmentIndex = 0;
List<Step> steps = new ArrayList<>(stairWidth * rise); for (int segmentStart = plot.minZ;
for (int riseIndex = 0; riseIndex < rise; riseIndex++) { segmentStart <= plot.maxZ;
for (int widthIndex = 0; widthIndex < stairWidth; widthIndex++) { segmentStart += gridSizeZ, segmentIndex++) {
steps.add(new Step(firstX + widthIndex, lowerY + riseIndex + 1, firstZ + riseIndex)); int avenueBlockIndex = firstBlockIndex + segmentIndex;
if (Math.floorMod(avenueBlockIndex + 1, everyNBlocks) != 0) {
continue;
}
Cuboid segment =
new Cuboid(
plot.minX,
plot.minY,
segmentStart,
plot.maxX,
plot.maxY,
Math.min(segmentStart + plotSizeZ - 1, plot.maxZ));
for (Side side : Side.values()) {
List<Step> stairs = stairBlocks(segment, avenueWidth, stairWidth, lowerY, rise, side);
if (stairs.isEmpty()) {
continue;
}
List<Step> backings = invertedBackings(stairs, lowerY);
List<Step> openings =
upperRoadOpening(stairs, avenueWidth, stairWidth, lowerY, rise, openingLength);
flights.add(new Flight(stairs, backings, openings));
} }
} }
return List.copyOf(steps); return List.copyOf(flights);
} }
public static List<Step> upperRoadOpening( public static List<Step> upperRoadOpening(
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) { Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) {
List<Step> stairs = centeredEastAvenue(plot, roadWidth, stairWidth, lowerY, rise); List<Step> stairs = centeredEastAvenue(plot, roadWidth, stairWidth, lowerY, rise);
return upperRoadOpening(stairs, roadWidth, stairWidth, lowerY, rise, openingLength);
}
private static List<Step> upperRoadOpening(
List<Step> stairs, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) {
if (stairs.isEmpty() || openingLength < 1) { if (stairs.isEmpty() || openingLength < 1) {
return List.of(); return List.of();
} }
@@ -71,17 +106,50 @@ public final class AvenueStairwayLayout {
return new Flight(List.of(), List.of(), List.of()); return new Flight(List.of(), List.of(), List.of());
} }
return new Flight(
stairs,
invertedBackings(stairs, lowerY),
upperRoadOpening(stairs, roadWidth, stairWidth, lowerY, rise, openingLength));
}
private static List<Step> invertedBackings(List<Step> stairs, int lowerY) {
List<Step> invertedBackings = new ArrayList<>(); List<Step> invertedBackings = new ArrayList<>();
for (Step stair : stairs) { for (Step stair : stairs) {
if (stair.y() > lowerY + 1) { if (stair.y() > lowerY + 1) {
invertedBackings.add(new Step(stair.x(), stair.y() - 1, stair.z())); invertedBackings.add(new Step(stair.x(), stair.y() - 1, stair.z()));
} }
} }
return List.copyOf(invertedBackings);
}
return new Flight( private static List<Step> stairBlocks(
stairs, Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, Side side) {
invertedBackings, int plotLength = plot.maxZ - plot.minZ + 1;
upperRoadOpening(plot, roadWidth, stairWidth, lowerY, rise, openingLength)); if (roadWidth < 1
|| stairWidth < 1
|| stairWidth >= roadWidth
|| rise < 1
|| rise + 2 > plotLength) {
return List.of();
}
int firstX =
side == Side.EAST
? plot.maxX + 1 + (roadWidth - stairWidth) / 2
: plot.minX - roadWidth + (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 enum Side {
EAST,
WEST
} }
public record Step(int x, int y, int z) {} public record Step(int x, int y, int z) {}

View File

@@ -828,7 +828,7 @@ public class MetropolisPlugin extends JavaPlugin {
for (int level = 0; level < levelCount; level++) { for (int level = 0; level < levelCount; level++) {
// Top // Top
for (col = min; col <= max; col++) { for (col = min; col <= max; col++) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) { if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (DEBUG) { if (DEBUG) {
getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level));
} }
@@ -841,7 +841,7 @@ public class MetropolisPlugin extends JavaPlugin {
// Right side // Right side
col = max; col = max;
for (row = min + 1; row < max; row++) { for (row = min + 1; row < max; row++) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) { if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (DEBUG) { if (DEBUG) {
getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level));
} }
@@ -854,7 +854,7 @@ public class MetropolisPlugin extends JavaPlugin {
// Bottom // Bottom
row = max; row = max;
for (col = max; col >= min; col--) { for (col = max; col >= min; col--) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) { if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (DEBUG) { if (DEBUG) {
getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level));
} }
@@ -867,7 +867,7 @@ public class MetropolisPlugin extends JavaPlugin {
// Left // Left
col = min; col = min;
for (row = max; row > min; row--) { for (row = max; row > min; row--) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) { if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (row != 0 || col != 0) { if (row != 0 || col != 0) {
if (DEBUG) { if (DEBUG) {
getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level));
@@ -895,6 +895,24 @@ public class MetropolisPlugin extends JavaPlugin {
getPlotMin(row, col, plotMultiplier, 0), getPlotMax(row, col, plotMultiplier, 0)); getPlotMin(row, col, plotMultiplier, 0), getPlotMax(row, col, plotMultiplier, 0));
} }
private boolean isAvailableSupportedPlot(int row, int col, int plotMultiplier, int level) {
return !areBlocksOccupied(row, col, plotMultiplier, level)
&& PlotLevelSupport.hasCompleteSupport(
row, col, plotMultiplier, level, this::isGeneratedLogicalBlock);
}
private boolean isGeneratedLogicalBlock(int row, int col, int level) {
Cuboid gridCell = new Cuboid(getGridMin(row, col, 1, level), getGridMax(row, col, 1, level));
if (_occupiedPlots != null) {
for (Plot plot : _occupiedPlots) {
if (plot instanceof PlayerHome && plot.getCuboid().intersects(gridCell)) {
return true;
}
}
}
return _spawnCuboid != null && _spawnCuboid.intersects(gridCell);
}
private void resizeCityRegion() { private void resizeCityRegion() {
size = calculateCitySize(); size = calculateCitySize();
ProtectedRegion cityRegion = regionManager.getRegion("City"); ProtectedRegion cityRegion = regionManager.getRegion("City");
@@ -1149,16 +1167,9 @@ public class MetropolisPlugin extends JavaPlugin {
return; return;
} }
int avenueBlockIndex = getPlotZFromMin(plotCuboid);
if (Math.floorMod(avenueBlockIndex + 1, avenueStairsEveryNBlocks) != 0) {
return;
}
int lowerY = getLevelStartY(lowerLevel); int lowerY = getLevelStartY(lowerLevel);
AvenueStairwayLayout.Flight flight = List<AvenueStairwayLayout.Flight> flights = getAvenueStairFlights(plotCuboid, lowerLevel);
AvenueStairwayLayout.forUpperRoad( if (flights.isEmpty()) {
plotCuboid, avenueWidth, avenueStairWidth, lowerY, getLevelPitchY(), 3);
if (flight.stairBlocks().isEmpty()) {
getLogger() getLogger()
.warning( .warning(
"Avenue stair run does not fit beside plot at " "Avenue stair run does not fit beside plot at "
@@ -1170,8 +1181,10 @@ public class MetropolisPlugin extends JavaPlugin {
return; return;
} }
for (AvenueStairwayLayout.Flight flight : flights) {
AvenueStairwayBuilder.build(world, flight, avenueStairMaterial); AvenueStairwayBuilder.build(world, flight, avenueStairMaterial);
} }
}
private Set<AvenueStairwayLayout.Step> getUpperRoadOpenings(Cuboid plotCuboid, int roadY) { private Set<AvenueStairwayLayout.Step> getUpperRoadOpenings(Cuboid plotCuboid, int roadY) {
Set<AvenueStairwayLayout.Step> openings = new HashSet<>(); Set<AvenueStairwayLayout.Step> openings = new HashSet<>();
@@ -1198,19 +1211,32 @@ public class MetropolisPlugin extends JavaPlugin {
} }
int lowerLevel = upperLevel - 1; int lowerLevel = upperLevel - 1;
int avenueBlockIndex = getPlotZFromMin(plotCuboid); if (lowerLevel < 0) {
if (lowerLevel < 0 || Math.floorMod(avenueBlockIndex + 1, avenueStairsEveryNBlocks) != 0) {
return List.of(); return List.of();
} }
return AvenueStairwayLayout.forUpperRoad( return getAvenueStairFlights(plotCuboid, lowerLevel).stream()
.flatMap(flight -> flight.roadOpenings().stream())
.toList();
}
private List<AvenueStairwayLayout.Flight> getAvenueStairFlights(
Cuboid plotCuboid, int lowerLevel) {
if (!generateAvenueStairs || lowerLevel < 0 || lowerLevel >= getEffectiveMaxLevels() - 1) {
return List.of();
}
return AvenueStairwayLayout.forUpperRoadSegments(
plotCuboid, plotCuboid,
plotSizeZ,
gridSizeZ,
getPlotZFromMin(plotCuboid),
avenueStairsEveryNBlocks,
avenueWidth, avenueWidth,
avenueStairWidth, avenueStairWidth,
getLevelStartY(lowerLevel), getLevelStartY(lowerLevel),
getLevelPitchY(), getLevelPitchY(),
3) 3);
.roadOpenings();
} }
public List<Plot> getCityBlocks() { public List<Plot> getCityBlocks() {

View File

@@ -0,0 +1,45 @@
/*
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;
public final class PlotLevelSupport {
private PlotLevelSupport() {}
public static boolean hasCompleteSupport(
int row, int col, int multiplier, int targetLevel, GeneratedCellLookup generatedCellLookup) {
if (multiplier < 1 || targetLevel < 0) {
return false;
}
for (int supportLevel = 0; supportLevel < targetLevel; supportLevel++) {
for (int rowOffset = 0; rowOffset < multiplier; rowOffset++) {
for (int colOffset = 0; colOffset < multiplier; colOffset++) {
if (!generatedCellLookup.isGenerated(row + rowOffset, col + colOffset, supportLevel)) {
return false;
}
}
}
}
return true;
}
@FunctionalInterface
public interface GeneratedCellLookup {
boolean isGenerated(int row, int col, int level);
}
}

View File

@@ -71,6 +71,39 @@ class AvenueStairwayLayoutTest {
assertTrue(flight.roadOpenings().stream().allMatch(step -> step.y() == 76)); assertTrue(flight.roadOpenings().stream().allMatch(step -> step.y() == 76));
} }
@Test
void createsOneCenteredStairFlightPerLogicalPlotOnBothAvenueBorders() {
Cuboid twoByTwoPlot = new Cuboid(2, 62, 2, 69, 71, 69);
List<AvenueStairwayLayout.Flight> flights =
AvenueStairwayLayout.forUpperRoadSegments(twoByTwoPlot, 32, 36, 0, 1, 4, 2, 62, 14, 3);
assertEquals(4, flights.size());
assertEquals(
new AvenueStairwayLayout.Step(71, 63, 12), flights.get(0).stairBlocks().getFirst());
assertEquals(
new AvenueStairwayLayout.Step(-1, 63, 12), flights.get(1).stairBlocks().getFirst());
assertEquals(
new AvenueStairwayLayout.Step(71, 63, 48), flights.get(2).stairBlocks().getFirst());
assertEquals(
new AvenueStairwayLayout.Step(-1, 63, 48), flights.get(3).stairBlocks().getFirst());
assertTrue(flights.stream().allMatch(flight -> flight.stairBlocks().getLast().y() == 76));
}
@Test
void appliesStairCadenceToEachLogicalPlotSegment() {
Cuboid twoByTwoPlot = new Cuboid(2, 62, 2, 69, 71, 69);
List<AvenueStairwayLayout.Flight> flights =
AvenueStairwayLayout.forUpperRoadSegments(twoByTwoPlot, 32, 36, 0, 2, 4, 2, 62, 14, 3);
assertEquals(2, flights.size());
assertEquals(
new AvenueStairwayLayout.Step(71, 63, 48), flights.getFirst().stairBlocks().getFirst());
assertEquals(
new AvenueStairwayLayout.Step(-1, 63, 48), flights.getLast().stairBlocks().getFirst());
}
@Test @Test
void rejectsAStairRunThatCannotFitWithinThePlotLength() { void rejectsAStairRunThatCannotFitWithinThePlotLength() {
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 13); Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 13);

View File

@@ -0,0 +1,55 @@
/*
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Set;
import org.junit.jupiter.api.Test;
class PlotLevelSupportTest {
@Test
void rejectsUpperPlotWhenAnyLogicalCellBelowIsMissing() {
Set<String> generatedCells = Set.of("0:0:0", "0:1:0", "1:0:0");
assertFalse(
PlotLevelSupport.hasCompleteSupport(
0, 0, 2, 1, (row, col, level) -> generatedCells.contains(key(row, col, level))));
}
@Test
void requiresEveryLogicalCellAtEveryLowerLevel() {
Set<String> generatedCells =
Set.of("0:0:0", "0:1:0", "1:0:0", "1:1:0", "0:0:1", "0:1:1", "1:0:1", "1:1:1");
assertTrue(
PlotLevelSupport.hasCompleteSupport(
0, 0, 2, 2, (row, col, level) -> generatedCells.contains(key(row, col, level))));
}
@Test
void baseLevelDoesNotRequireLowerSupport() {
assertTrue(PlotLevelSupport.hasCompleteSupport(3, 4, 2, 0, (row, col, level) -> false));
}
private static String key(int row, int col, int level) {
return row + ":" + col + ":" + level;
}
}