Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -67,6 +67,7 @@ public ChunkBlockPlaceholders(ChunkBlock addon,
placeholdersManager.registerPlaceholder(addon, "island_next_chunk_level", this::getIslandNextChunkLevel);
placeholdersManager.registerPlaceholder(addon, "island_chunk_credit", this::getIslandChunkCredit);
placeholdersManager.registerPlaceholder(addon, "island_ring", this::getIslandRing);
placeholdersManager.registerPlaceholder(addon, "island_rings_complete", this::getIslandRingsComplete);
}

/**
Expand Down Expand Up @@ -128,6 +129,17 @@ public String getIslandRing(User user) {
return getUsersIsland(user).map(i -> String.valueOf(addon.getChunkManager().currentRing(i))).orElse("");
}

/**
* @param user user
* @return how many whole rings the user's island has closed around its center
*/
public String getIslandRingsComplete(User user) {
if (user == null || user.getUniqueId() == null) {
return "";
}
return getUsersIsland(user).map(i -> String.valueOf(addon.getChunkManager().completedRings(i))).orElse("");
}

/**
* Get the user's owned island. Returns the island owned by the user, not a team
* island they may be visiting as a member. If the user owns more than one island,
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/world/bentobox/chunkblock/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@ public class Settings implements WorldSettings {
@ConfigEntry(path = "chunkblock.claim.confirmation-timeout")
private int claimConfirmationTimeout = 15;

@ConfigComment("Announce ring milestones to the whole server, not just the island's members.")
@ConfigComment("A ring is the square of chunks at a fixed distance from the center chunk:")
@ConfigComment("ring 1 is the eight chunks around the center, ring 2 the sixteen around those.")
@ConfigEntry(path = "chunkblock.rings.broadcast")
private boolean ringBroadcast = false;

@ConfigComment("Console commands run once each time an island completes a whole ring.")
@ConfigComment("Placeholders: [ring] the completed ring, [chunks] the island's chunk count,")
@ConfigComment("[owner] the island owner's name.")
@ConfigComment("Rings are rewarded once per island — re-locking and re-claiming a ring pays")
@ConfigComment("nothing. Rewarding island levels here is not advised: levels buy chunks, so")
@ConfigComment("that makes each ring pay for the next one.")
@ConfigComment("Example: 'eco give [owner] 500'")
@ConfigEntry(path = "chunkblock.rings.commands")
private List<String> ringCommands = new ArrayList<>();

@ConfigComment("Console commands run once for every member of the island, including the")
@ConfigComment("owner and offline members. Placeholders: [player], [ring], [chunks].")
@ConfigComment("Example: 'give [player] diamond 1'")
@ConfigEntry(path = "chunkblock.rings.player-commands")
private List<String> ringPlayerCommands = new ArrayList<>();

@ConfigComment("If true, losing island levels below what has been spent re-locks chunks in")
@ConfigComment("reverse claim order (the most recently claimed chunks are lost first). Builds")
@ConfigComment("inside re-locked chunks are untouched but cannot be reached until the levels")
Expand Down Expand Up @@ -2607,6 +2629,48 @@ public void setMaxChunks(int maxChunks) {
this.maxChunks = maxChunks;
}

/**
* @return true if ring milestones are announced to the whole server
*/
public boolean isRingBroadcast() {
return ringBroadcast;
}

/**
* @param ringBroadcast the ringBroadcast to set
*/
public void setRingBroadcast(boolean ringBroadcast) {
this.ringBroadcast = ringBroadcast;
}

/**
* @return the console commands run once per completed ring, never null
*/
public List<String> getRingCommands() {
return ringCommands == null ? Collections.emptyList() : ringCommands;
}

/**
* @param ringCommands the ringCommands to set
*/
public void setRingCommands(List<String> ringCommands) {
this.ringCommands = ringCommands;
}

/**
* @return the console commands run for each island member per completed ring, never null
*/
public List<String> getRingPlayerCommands() {
return ringPlayerCommands == null ? Collections.emptyList() : ringPlayerCommands;
}

/**
* @param ringPlayerCommands the ringPlayerCommands to set
*/
public void setRingPlayerCommands(List<String> ringPlayerCommands) {
this.ringPlayerCommands = ringPlayerCommands;
}

/**
* @return true if a chunk must be previewed and confirmed before credit is spent
*/
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/world/bentobox/chunkblock/chunks/ChunkManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,48 @@ public int currentRing(Island island) {
return ring;
}

/**
* A ring is the square of chunks at a fixed Chebyshev distance from the center chunk:
* ring 1 is the eight chunks surrounding the center, ring 2 the sixteen around those.
* Ring 0 is the center chunk, which is always unlocked.
*
* @param island the island
* @param ring the ring radius in chunks
* @return true if every chunk in the ring is unlocked; false for rings that do not fit
* inside the island's protection range
*/
public boolean isRingComplete(Island island, int ring) {
if (ring <= 0) {
return true;
}
if (ring > maxRingRadius(island)) {
return false;
}
OneBlockIslands data = addon.getOneBlocksIsland(island);
for (int d = -ring; d <= ring; d++) {
// North and south edges cover the corners, so the east and west edges only
// need the same sweep to close the square
if (!data.isChunkUnlocked(d, -ring) || !data.isChunkUnlocked(d, ring)
|| !data.isChunkUnlocked(-ring, d) || !data.isChunkUnlocked(ring, d)) {
return false;
}
}
return true;
}

/**
* @param island the island
* @return the number of whole rings completed outwards from the center without a gap.
* A claimed chunk two rings out does not count while ring 1 has a hole in it.
*/
public int completedRings(Island island) {
int ring = 0;
while (isRingComplete(island, ring + 1)) {
ring++;
}
return ring;
}

/**
* @param island the island
* @return the island's unlocked chunk offsets in unlock order (x and z are chunk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@
int unlocked = cm.getUnlockedChunkCount(island);
int max = cm.getMaxChunks(island);
long credit = Math.max(0, cm.getCredit(island));
user.sendMessage("chunkblock.chunks.info", "[unlocked]", String.valueOf(unlocked), "[max]",

Check failure on line 61 in src/main/java/world/bentobox/chunkblock/commands/island/IslandChunksCommand.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "[max]" 3 times.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_ChunkBlock&issues=AZ_oARgY86MzHGZiFISw&open=AZ_oARgY86MzHGZiFISw&pullRequest=22
String.valueOf(max), "[credit]", String.valueOf(credit), "[cost]",
String.valueOf(cm.getChunkCost()));
user.sendMessage("chunkblock.chunks.rings", "[rings]", String.valueOf(cm.completedRings(island)), "[max]",
String.valueOf(cm.maxRingRadius(island)));
showMap(user, island, unlocked, max);
return true;
}
Expand All @@ -83,7 +85,11 @@
StringBuilder row = new StringBuilder();
for (int dx = -radius; dx <= radius; dx++) {
boolean here = dx == playerDx && dz == playerDz;
if (addon.getOneBlocksIsland(island).isChunkUnlocked(dx, dz)) {
if (dx == 0 && dz == 0) {
// The center chunk holds the magic block and can never lock, so it is
// marked in its own right — without it the grid has nothing to orient by
row.append(here ? "&b◉" : "&6◎");
} else if (addon.getOneBlocksIsland(island).isChunkUnlocked(dx, dz)) {
row.append(here ? "&b◆" : "&a■");
} else if (cm.checkGeometry(island, centerChunkX + dx, centerChunkZ + dz) == ClaimResult.OK) {
row.append(here ? "&b◆" : "&e▣");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public class OneBlockIslands implements DataObject {
@Expose
private long lastKnownLevel = 0;

/**
* The highest ring this island has already been rewarded for completing. Milestones
* are earned once and stay earned: re-locking a ring and claiming it back does not pay
* out again. Only an island create or reset clears it.
*/
@Expose
private int highestRingRewarded = 0;

/** Fast membership view of {@link #unlockedChunks}; rebuilt lazily after loads/edits */
private transient Set<Long> unlockedSet;

Expand Down Expand Up @@ -179,6 +187,20 @@ public void setLastKnownLevel(long lastKnownLevel) {
this.lastKnownLevel = lastKnownLevel;
}

/**
* @return the highest ring this island has already been rewarded for
*/
public int getHighestRingRewarded() {
return highestRingRewarded;
}

/**
* @param highestRingRewarded the highest rewarded ring
*/
public void setHighestRingRewarded(int highestRingRewarded) {
this.highestRingRewarded = highestRingRewarded;
}

/**
* @return the phaseName
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package world.bentobox.chunkblock.events;

import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.eclipse.jdt.annotation.NonNull;

import world.bentobox.bentobox.api.events.BentoBoxEvent;
import world.bentobox.bentobox.database.objects.Island;

/**
* Fired once when an island completes a whole ring of chunks around its center — every
* chunk at Chebyshev distance {@code ring} from the center chunk is unlocked. Rings are
* only ever rewarded once per island: re-locking and re-claiming the same chunks does not
* fire this again.
* <p>
* Cancelling suppresses the addon's own milestone handling (reward commands, messages and
* the celebration); the ring itself stays complete either way.
*
* @author tastybento
*/
public class RingCompleteEvent extends BentoBoxEvent implements Cancellable {

private static final HandlerList handlers = new HandlerList();

private final Island island;
private final int ring;
private final int unlockedChunkCount;
private boolean cancelled;

/**
* @param island the island that completed the ring
* @param ring the ring's radius in chunks, always &gt;= 1
* @param unlockedChunkCount how many chunks the island has unlocked in total
*/
public RingCompleteEvent(@NonNull Island island, int ring, int unlockedChunkCount) {
this.island = island;
this.ring = ring;
this.unlockedChunkCount = unlockedChunkCount;
}

@Override
public HandlerList getHandlers() {
return getHandlerList();
}

public static HandlerList getHandlerList() {
return handlers;
}

/**
* @return the island that completed the ring
*/
@NonNull
public Island getIsland() {
return island;
}

/**
* @return the completed ring's radius in chunks (ring 1 is the eight chunks around the
* center chunk)
*/
public int getRing() {
return ring;
}

/**
* @return the island's total unlocked chunk count including the center chunk
*/
public int getUnlockedChunkCount() {
return unlockedChunkCount;
}

@Override
public boolean isCancelled() {
return cancelled;
}

@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
Loading
Loading