Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
87b38dd
chore(setup): add new targets
theEvilReaper Aug 24, 2026
68adac2
chore(setup): add weight option
theEvilReaper Aug 24, 2026
0471339
feat(setup): add area inventory
theEvilReaper Aug 24, 2026
3e20140
chore(setup): improve annotation handling
theEvilReaper Aug 24, 2026
afd669b
chore(setup): add new target
theEvilReaper Aug 24, 2026
de5309c
chore(dialog): add new dialog types
theEvilReaper Aug 24, 2026
076a615
chore(slot): add shuffle interval overview slot
theEvilReaper Aug 24, 2026
4a3cf81
chore(registry): register new dialogs
theEvilReaper Aug 24, 2026
8711c4c
chore(dialog): add new request targets
theEvilReaper Aug 24, 2026
34c673c
feat(area): add area related code
theEvilReaper Aug 24, 2026
9f3ce7c
chore(map): wire area object
theEvilReaper Aug 24, 2026
617b28d
test(inventory): update slots
theEvilReaper Aug 24, 2026
57e5fc9
chore(push): add weight logic
theEvilReaper Aug 24, 2026
aa93be4
chore(area): improve block lookup
theEvilReaper Aug 24, 2026
5a8c20e
test(value): add new weight test
theEvilReaper Aug 24, 2026
c923e6e
chore(lore): add weight item method
theEvilReaper Aug 24, 2026
d3aceb0
chore(map): add area usage
theEvilReaper Aug 24, 2026
e1fb71e
chore(data): add view inventory usage
theEvilReaper Aug 24, 2026
dcf9bc0
chore(map): update item handling
theEvilReaper Aug 24, 2026
ff4e7da
feat(setup): add custom player
theEvilReaper Aug 24, 2026
139c08a
feat(map): custom Instance implementation
theEvilReaper Aug 25, 2026
ad7998f
chore(map): add shuffle percentage value
theEvilReaper Aug 25, 2026
5f7df06
chore(map): outsource area data code
theEvilReaper Aug 25, 2026
814b729
chore(type): remove static enumeration cache
theEvilReaper Aug 25, 2026
c631b3b
feat(setup): add dedicated area package
theEvilReaper Aug 25, 2026
d46e309
chore(game): update used instance
theEvilReaper Aug 25, 2026
8c33058
chore(setup): update value access
theEvilReaper Aug 25, 2026
ad30cbd
chore(dialog): update dialog handling
theEvilReaper Aug 25, 2026
a114687
chore(setup): update import
theEvilReaper Aug 25, 2026
74a0b82
feat(slot): add are slot implementations
theEvilReaper Aug 25, 2026
dddaec6
chore(map): add new getter
theEvilReaper Aug 25, 2026
ba5d3d2
chore(setup): improve inventory handling
theEvilReaper Aug 25, 2026
56d1450
feat(dialog): add shuffle dialog
theEvilReaper Aug 25, 2026
88990b1
feat(setup): add generic layer over the data types
theEvilReaper Aug 25, 2026
199d5f7
chore(slot): simplify slot implementation
theEvilReaper Aug 25, 2026
235e259
chore(map): add missing fields getter
theEvilReaper Aug 25, 2026
1ee1568
chore(slot): remove doubled slot
theEvilReaper Aug 25, 2026
d7c30ce
test: add new test cases
theEvilReaper Aug 25, 2026
3946a86
feat(setup): improve validation
theEvilReaper Aug 25, 2026
60debed
chore: cleanup unused imports
theEvilReaper Aug 25, 2026
f60d41f
fix(setup): improve setup finish / leave
theEvilReaper Aug 25, 2026
6eb58f5
chore(setup): improve validation and setup cleanup
theEvilReaper Aug 25, 2026
b0b4e37
chore: cleanup code
theEvilReaper Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
* Serializer and Deserializer implementation for {@link PushData} object.
*
* @author theEvilReaper
* @version 1.0.0
* @version 1.1.0
* @since 1.0.0
*/
public class PushDataAdapter implements JsonDeserializer<PushData>, JsonSerializer<PushData> {

private static final double DEFAULT_GROUND_WEIGHT = 1.0;
private static final double DEFAULT_PUSH_WEIGHT = 0.05;

@Override
public PushData deserialize(JsonElement element, Type type, JsonDeserializationContext context) {
JsonArray jsonArray = element.getAsJsonArray();
Expand All @@ -33,15 +36,19 @@ public PushData deserialize(JsonElement element, Type type, JsonDeserializationC
}

for (JsonElement jsonElement : jsonArray.asList()) {
Key blockKey = context.deserialize(jsonElement.getAsJsonObject().get("block"), Key.class);
int value = jsonElement.getAsJsonObject().get("value").getAsInt();
boolean ground = jsonElement.getAsJsonObject().get("ground").getAsBoolean();
JsonObject jsonObject = jsonElement.getAsJsonObject();
Key blockKey = context.deserialize(jsonObject.get("block"), Key.class);
int value = jsonObject.get("value").getAsInt();
boolean ground = jsonObject.get("ground").getAsBoolean();
double weight = jsonObject.has("weight")
? jsonObject.get("weight").getAsDouble()
: (ground ? DEFAULT_GROUND_WEIGHT : DEFAULT_PUSH_WEIGHT);
Block block = Block.fromKey(blockKey);

if (ground) {
builder.add(0, PushEntry.groundEntry(block, value));
builder.add(0, PushEntry.groundEntry(block, value, weight));
} else {
builder.add(PushEntry.pushEntry(block, value));
builder.add(PushEntry.pushEntry(block, value, weight));
}
}

Expand All @@ -58,6 +65,7 @@ public JsonElement serialize(PushData data, Type type, JsonSerializationContext
jsonObject.add("block", context.serialize(blockKey, Key.class));
jsonObject.addProperty("value", pushEntry.getValue());
jsonObject.addProperty("ground", pushEntry.isGround());
jsonObject.addProperty("weight", pushEntry.getWeight());
jsonArray.add(jsonObject);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package net.theevilreaper.bounce.common.config;

import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

/**
* The {@link GameConfig} interface represents the structure for a configuration which is used by the game.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package net.theevilreaper.bounce.common.ground;

import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.theevilreaper.bounce.common.push.PushData;

import java.util.List;

/**
* The {@link Area} interface represents an area in the game.
*
* @author theEvilReaper
* @version 1.0.0
* @version 1.1.0
* @since 0.1.0
*/
public interface Area {

void calculatePositions();
/**
* Scans the volume between {@link #min()} and {@link #max()} in the given instance and records every position
* whose block matches {@link #groundBlock()}. A no-op if positions were already calculated, see {@link #hasPositions()}.
*
* @param instance the instance to scan
*/
void calculatePositions(Instance instance);

/**
* Returns a boolean indicator if the are includes an amount of positions.
Expand All @@ -22,6 +31,14 @@ public interface Area {
*/
boolean hasPositions();

/**
* Returns the positions calculated by {@link #calculatePositions(Instance)}, or an empty list if it hasn't
* been called yet.
*
* @return an unmodifiable view of the calculated positions
*/
List<Vec> positions();

/**
* Returns the minimum point of the area.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package net.theevilreaper.bounce.common.ground;

import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.theevilreaper.bounce.common.push.PushEntry;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

/**
* Fills an {@link Area}'s scanned positions with a weighted-random mix of its {@link PushEntry} blocks, and
* partially reshuffles that fill at runtime.
*
* @author theEvilReaper
* @version 1.0.0
* @since 1.0.0
*/
public final class AreaFiller {

private AreaFiller() {
// Prevent instantiation
}

/**
* Scans the area (if not already scanned) and fills every found position with a weighted-random block.
*
* @param instance the instance to place blocks in
* @param area the area to fill
*/
public static void fill(Instance instance, Area area) {
area.calculatePositions(instance);

List<PushEntry> entries = area.data().push();
for (Vec position : area.positions()) {
instance.setBlock(position, pickWeightedBlock(entries, area.groundBlock()));
}
}

/**
* Re-rolls {@code percentage} of the area's already scanned positions, skipping the position directly under
* any of the given players so nobody's ground changes under their feet.
*
* @param instance the instance to place blocks in
* @param area the area to reshuffle, must already have positions calculated (see {@link #fill})
* @param percentage the fraction (0.0-1.0) of positions to re-roll
* @param players players whose current standing position must not be touched
*/
public static void reshuffle(Instance instance, Area area, double percentage, Collection<Player> players) {
List<Vec> positions = area.positions();
if (positions.isEmpty()) return;

Set<Vec> excluded = new HashSet<>();
for (Player player : players) {
Pos playerPosition = player.getPosition();
excluded.add(new Vec(Math.floor(playerPosition.x()), Math.floor(playerPosition.y() - 1), Math.floor(playerPosition.z())));
}

List<Vec> candidates = new ArrayList<>();
for (Vec position : positions) {
if (!excluded.contains(position)) candidates.add(position);
}
if (candidates.isEmpty()) return;

Collections.shuffle(candidates, ThreadLocalRandom.current());
int amount = Math.min(candidates.size(), (int) Math.round(positions.size() * percentage));

List<PushEntry> entries = area.data().push();
for (int i = 0; i < amount; i++) {
Vec position = candidates.get(i);
instance.setBlock(position, pickWeightedBlock(entries, area.groundBlock()));
}
}

private static Block pickWeightedBlock(List<PushEntry> entries, Block fallback) {
double roll = ThreadLocalRandom.current().nextDouble(); // 0.0 to 1.0
double cumulative = 0.0;
for (PushEntry entry : entries) {
if (entry.isGround()) continue;
double p = Math.clamp(entry.getWeight(), 0.0, 1.0);
cumulative += p;
if (roll < cumulative) {
return entry.getBlock();
}
}
return fallback;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package net.theevilreaper.bounce.common.ground;

import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.theevilreaper.bounce.common.push.PushData;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public final class GroundArea implements Area {

Expand All @@ -32,24 +35,68 @@ public GroundArea(Vec min, Vec max, Block groundBlock, PushData pushData) {
* {@inheritDoc}
*/
@Override
public void calculatePositions() {
public void calculatePositions(Instance instance) {
// Avoid double calculations
if (!this.positions.isEmpty()) return;

int minX = (int) Math.floor(Math.min(min.x(), max.x()));
int maxX = (int) Math.floor(Math.max(min.x(), max.x()));
int minZ = (int) Math.floor(Math.min(min.z(), max.z()));
int maxZ = (int) Math.floor(Math.max(min.z(), max.z()));
int targetY = (int) Math.floor(min.y());

int minChunkX = minX >> 4;
int maxChunkX = maxX >> 4;
int minChunkZ = minZ >> 4;
int maxChunkZ = maxZ >> 4;

List<CompletableFuture<Chunk>> chunkFutures = new ArrayList<>();
for (int cx = minChunkX; cx <= maxChunkX; cx++) {
for (int cz = minChunkZ; cz <= maxChunkZ; cz++) {
chunkFutures.add(instance.loadChunk(cx, cz));
}
}
CompletableFuture.allOf(chunkFutures.toArray(new CompletableFuture[0])).join();

// Scan the single 2D plane at targetY
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
positions.add(new Vec(x, min.y(), z));
if (isAreaBlock(instance.getBlock(x, targetY, z))) {
positions.add(new Vec(x, targetY, z));
}
}
}

// If no positions were found at targetY, try scanning targetY - 1
// in case coordinates were captured while standing on top of the ground platform
if (positions.isEmpty()) {
int scanY = targetY - 1;
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
if (isAreaBlock(instance.getBlock(x, scanY, z))) {
positions.add(new Vec(x, scanY, z));
}
}
}
}

LOGGER.info("Calculated positions for area: {} to {} with {} positions", min, max, positions.size());
}

private boolean isAreaBlock(Block block) {
if (block.compare(groundBlock) || block.compare(Block.REDSTONE_BLOCK)) {
return true;
}
if (data != null && data.push() != null) {
for (var entry : data.push()) {
if (block.compare(entry.getBlock())) {
return true;
}
}
}
return false;
}

/**
* {@inheritDoc}
*/
Expand All @@ -58,6 +105,14 @@ public boolean hasPositions() {
return !this.positions.isEmpty();
}

/**
* {@inheritDoc}
*/
@Override
public List<Vec> positions() {
return Collections.unmodifiableList(this.positions);
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import net.minestom.server.coordinate.Pos;
import net.theevilreaper.aves.map.BaseMap;
import net.theevilreaper.bounce.common.ground.Area;
import net.theevilreaper.bounce.common.push.PushData;
import org.jetbrains.annotations.Nullable;

import java.util.List;

Expand All @@ -11,26 +13,36 @@
* It holds data about the used positions and other things.
*
* @author theEvilReaper
* @version 1.1.0
* @version 1.2.0
* @since 0.1.0
*/
public final class GameMap extends BaseMap {

private final Pos gameSpawn;
private final PushData pushData;
private final @Nullable Area area;
private final int shuffleIntervalTicks;
private final double reshufflePercentage;

/**
* Creates a new reference from the map class.
*
* @param name the name of the map
* @param spawn the spawn position
* @param gameSpawn the spawn position during the game
* @param pushData the {@link PushData} which includes information about push values
* @param name the name of the map
* @param spawn the spawn position
* @param gameSpawn the spawn position during the game
* @param pushData the {@link PushData} which includes information about push values
* @param builders the list of builders who worked on the map
* @param area the ground area which gets dynamically filled, or {@code null} for a fully manual map
* @param shuffleIntervalTicks the amount of ticks between two runtime reshuffles of the area
* @param reshufflePercentage the fraction (0.0-1.0) of the area's positions to re-roll on each reshuffle
*/
public GameMap(String name, Pos spawn, Pos gameSpawn, PushData pushData, List<String> builders) {
public GameMap(String name, Pos spawn, Pos gameSpawn, PushData pushData, List<String> builders, @Nullable Area area, int shuffleIntervalTicks, double reshufflePercentage) {
super(name, spawn, builders);
this.gameSpawn = gameSpawn;
this.pushData = pushData;
this.area = area;
this.shuffleIntervalTicks = shuffleIntervalTicks;
this.reshufflePercentage = reshufflePercentage;
}

/**
Expand All @@ -50,4 +62,31 @@ public PushData getPushData() {
public Pos getGameSpawn() {
return gameSpawn;
}
}

/**
* Returns the dynamically filled ground area of this map, or {@code null} for a fully manual map.
*
* @return the area, or {@code null}
*/
public @Nullable Area getArea() {
return area;
}

/**
* Returns the amount of ticks between two runtime reshuffles of the area.
*
* @return the interval in ticks
*/
public int getShuffleIntervalTicks() {
return shuffleIntervalTicks;
}

/**
* Returns the fraction of the area's positions which get re-rolled on each runtime reshuffle.
*
* @return the percentage as a fraction between 0.0 and 1.0
*/
public double getReshufflePercentage() {
return reshufflePercentage;
}
}
Loading
Loading