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

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