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

@@ -25,29 +25,64 @@ public final class 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 stairBlocks(plot, roadWidth, stairWidth, lowerY, rise, Side.EAST);
}
public static List<Flight> forUpperRoadSegments(
Cuboid plot,
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();
}
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));
List<Flight> flights = new ArrayList<>();
int segmentIndex = 0;
for (int segmentStart = plot.minZ;
segmentStart <= plot.maxZ;
segmentStart += gridSizeZ, segmentIndex++) {
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(
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) {
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) {
return List.of();
}
@@ -71,17 +106,50 @@ public final class AvenueStairwayLayout {
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<>();
for (Step stair : stairs) {
if (stair.y() > lowerY + 1) {
invertedBackings.add(new Step(stair.x(), stair.y() - 1, stair.z()));
}
}
return List.copyOf(invertedBackings);
}
return new Flight(
stairs,
invertedBackings,
upperRoadOpening(plot, roadWidth, stairWidth, lowerY, rise, openingLength));
private static List<Step> stairBlocks(
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, Side side) {
int plotLength = plot.maxZ - plot.minZ + 1;
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) {}

View File

@@ -828,7 +828,7 @@ public class MetropolisPlugin extends JavaPlugin {
for (int level = 0; level < levelCount; level++) {
// Top
for (col = min; col <= max; col++) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) {
if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (DEBUG) {
getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level));
}
@@ -841,7 +841,7 @@ public class MetropolisPlugin extends JavaPlugin {
// Right side
col = max;
for (row = min + 1; row < max; row++) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) {
if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (DEBUG) {
getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level));
}
@@ -854,7 +854,7 @@ public class MetropolisPlugin extends JavaPlugin {
// Bottom
row = max;
for (col = max; col >= min; col--) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) {
if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (DEBUG) {
getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level));
}
@@ -867,7 +867,7 @@ public class MetropolisPlugin extends JavaPlugin {
// Left
col = min;
for (row = max; row > min; row--) {
if (!areBlocksOccupied(row, col, plotMultiplier, level)) {
if (isAvailableSupportedPlot(row, col, plotMultiplier, level)) {
if (row != 0 || col != 0) {
if (DEBUG) {
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));
}
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() {
size = calculateCitySize();
ProtectedRegion cityRegion = regionManager.getRegion("City");
@@ -1149,16 +1167,9 @@ public class MetropolisPlugin extends JavaPlugin {
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()) {
List<AvenueStairwayLayout.Flight> flights = getAvenueStairFlights(plotCuboid, lowerLevel);
if (flights.isEmpty()) {
getLogger()
.warning(
"Avenue stair run does not fit beside plot at "
@@ -1170,7 +1181,9 @@ public class MetropolisPlugin extends JavaPlugin {
return;
}
AvenueStairwayBuilder.build(world, flight, avenueStairMaterial);
for (AvenueStairwayLayout.Flight flight : flights) {
AvenueStairwayBuilder.build(world, flight, avenueStairMaterial);
}
}
private Set<AvenueStairwayLayout.Step> getUpperRoadOpenings(Cuboid plotCuboid, int roadY) {
@@ -1198,19 +1211,32 @@ public class MetropolisPlugin extends JavaPlugin {
}
int lowerLevel = upperLevel - 1;
int avenueBlockIndex = getPlotZFromMin(plotCuboid);
if (lowerLevel < 0 || Math.floorMod(avenueBlockIndex + 1, avenueStairsEveryNBlocks) != 0) {
if (lowerLevel < 0) {
return List.of();
}
return AvenueStairwayLayout.forUpperRoad(
plotCuboid,
avenueWidth,
avenueStairWidth,
getLevelStartY(lowerLevel),
getLevelPitchY(),
3)
.roadOpenings();
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,
plotSizeZ,
gridSizeZ,
getPlotZFromMin(plotCuboid),
avenueStairsEveryNBlocks,
avenueWidth,
avenueStairWidth,
getLevelStartY(lowerLevel),
getLevelPitchY(),
3);
}
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));
}
@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
void rejectsAStairRunThatCannotFitWithinThePlotLength() {
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;
}
}