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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand DownExpand Up@@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand DownExpand Up@@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand DownExpand Up@@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand DownExpand Up@@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All@@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand DownExpand Up@@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

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

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# and they stay with the island even if the player later leaves the team.
# The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,
# and deaths are only recorded if deaths counted is enabled there.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading