Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7323b89
Change graphic
tastybento Apr 14, 2026
b4db0ab
Remove unnecessary public modifiers from JUnit 5 test methods
tastybento Apr 14, 2026
17b8073
Use assertDoesNotThrow for no-assertion tests in AcidIslandTest
tastybento Apr 14, 2026
eebc4cb
Reduce complexity of makeNetherRoof() by extracting helper methods
tastybento Apr 14, 2026
a453d2d
Reduce complexity of findEntities() by extracting helper methods
tastybento Apr 14, 2026
4d92cc6
Reduce complexity of onPlayerMove() and getWorld() by extracting helpers
tastybento Apr 14, 2026
4127ce0
Add since/forRemoval to @Deprecated annotations (S6355, S1123)
tastybento Apr 14, 2026
ab8faeb
Replace assertTrue(x == y) with assertEquals in AcidRainEventTest (S5…
tastybento Apr 14, 2026
7d7e603
Add explanation to @Disabled annotation in AISettingsTest (S1607)
tastybento Apr 14, 2026
d5ad06e
Rename local 'l' to 'loc' to avoid hiding field in AcidTaskTest (S1117)
tastybento Apr 14, 2026
029ea55
Modernise AcidEffect: putIfAbsent and Math.clamp (S3824, S6885)
tastybento Apr 14, 2026
33e1c0c
Replace assertTrue(x == y) with assertEquals in AcidEffectTest (S5785)
tastybento Apr 14, 2026
16864bc
Use pattern-matching instanceof in AcidTask; remove spurious throws (…
tastybento Apr 14, 2026
9ebed9a
Remove unused imports and eq() wrappers in test files (S1128, S6068)
tastybento Apr 14, 2026
967d639
Remove unused import and eq() wrappers in AcidEffectTest (S1128, S6068)
tastybento Apr 14, 2026
96b06e9
Guard playSound location against null in AcidEffect (S2637)
tastybento Apr 14, 2026
7c7259b
Update CLAUDE.md with testing rules and SonarCloud conventions
tastybento Apr 14, 2026
f6edbcd
Add purified water mechanic (issue #163)
tastybento Apr 15, 2026
8bbb523
Refine purified water mechanics
tastybento Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class MyTest {
private MockedStatic<Bukkit> mockedBukkit;

@BeforeEach
public void setUp() {
void setUp() {
server = MockBukkit.mock(); // always first
mockedBukkit = Mockito.mockStatic(Bukkit.class, Mockito.RETURNS_DEEP_STUBS);
mockedBukkit.when(Bukkit::getMinecraftVersion).thenReturn("1.21.11");
Expand All @@ -54,7 +54,7 @@ public class MyTest {
}

@AfterEach
public void tearDown() {
void tearDown() {
mockedBukkit.closeOnDemand();
MockBukkit.unmock();
}
Expand All @@ -68,6 +68,12 @@ Key rules:
- MockBukkit's JUnit transitive deps are excluded in `pom.xml` to avoid JUnit 6 version conflicts with surefire.
- Do not use `new ItemStack(Material.AIR)` in tests — use `null` for empty armor slots; Paper 1.21's ItemStack handles AIR differently.
- Do not reference `world.bentobox.bentobox.lists.Flags` static fields in tests — the class static initializer requires full BentoBox initialization. Use the string flag ID instead (e.g., `"ANIMAL_NATURAL_SPAWN"`).
- **No `public` modifier** on test methods (`@Test`, `@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`) — JUnit 5 does not require it and SonarCloud flags it.
- **No `throws Exception`** on `@BeforeEach`/`@AfterEach` unless the method body actually declares a checked exception.
- **`assertDoesNotThrow(() -> ...)`** for tests that verify no exception is thrown — bare method calls with no assertions are flagged by SonarCloud.
- **`assertEquals(expected, actual)`** for numeric equality — `assertTrue(x == y)` is flagged (S5785).
- **`@Disabled` must include a reason string** — e.g. `@Disabled("PotionEffectType cannot be mocked without full server initialisation")`.
- **Do not use `eq()` in `verify()` or `when()` for concrete values** — pass the value directly; `eq()` wrappers on non-matcher arguments are redundant and flagged (S6068).

### Locales

Expand All @@ -80,6 +86,27 @@ All 24 locale files use **MiniMessage format** (e.g., `<red>`, `<dark_blue>`). L
- `src/main/resources/locales/` — 24 language translation files (MiniMessage format)
- `src/main/resources/blueprints/` — Island templates (overworld, nether, end)

## Code Conventions

### Java 21 idioms (enforced by SonarCloud)

- **Pattern-matching instanceof** — use `instanceof Type varName` instead of `instanceof Type` + explicit cast (S6201).
- **`Math.clamp`** — use `Math.clamp(value, min, max)` instead of `Math.max(min, Math.min(max, value))` (S6885).
- **`Map.putIfAbsent`** — replace `if (!map.containsKey(k)) { map.put(k, v); ... }` with `if (map.putIfAbsent(k, v) == null) { ... }` (S3824).
- **`@Deprecated` annotation** — always include `since` and `forRemoval` arguments, e.g. `@Deprecated(since = "1.21", forRemoval = true)` (S6355). If `forRemoval` intent is unclear use `since` only (S1123).
- **Reduce cognitive complexity** by extracting private helper methods rather than nesting conditions.

### SonarCloud issue lookup

Query open issues via the public API (no auth needed for public projects):

```bash
curl -s "https://sonarcloud.io/api/issues/search?componentKeys=BentoBoxWorld_AcidIsland&statuses=OPEN&impactSeverities=MEDIUM&ps=100" \
| python3 -c "import json,sys; [print(f'{i[\"component\"].split(\":\",1)[-1]}:{i.get(\"line\",\"?\")} [{i[\"rule\"]}] {i[\"message\"]}') for i in json.load(sys.stdin)[\"issues\"]]"
```

Change `impactSeverities` to `HIGH`, `MEDIUM`, or `LOW` as needed.

## CI

GitHub Actions workflow on `develop` branch and PRs: builds with Java 21, runs JaCoCo coverage, reports to SonarCloud.
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
AcidIsland
===========
AcidIsland
==========
[![Build Status](https://ci.codemc.org/buildStatus/icon?job=BentoBoxWorld/AcidIsland)](https://ci.codemc.org/job/BentoBoxWorld/job/AcidIsland/)
[![Lines Of Code](https://sonarcloud.io/api/project_badges/measure?project=BentoBoxWorld_AcidIsland&metric=ncloc)](https://sonarcloud.io/component_measures?id=BentoBoxWorld_AcidIsland&metric=ncloc)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=BentoBoxWorld_AcidIsland&metric=sqale_rating)](https://sonarcloud.io/component_measures?id=BentoBoxWorld_AcidIslandd&metric=Maintainability)
Expand All @@ -8,16 +8,15 @@ AcidIsland™
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=BentoBoxWorld_AcidIsland&metric=bugs)](https://sonarcloud.io/project/issues?id=BentoBoxWorld_AcidIsland&resolved=false&types=BUG)

# Introduction
AcidIsland add-on for BentoBox, so to run an AcidIsland game, you must have BentoBox installed. Docs can be found at [https://docs.bentobox.world](https://docs.bentobox.world).
AcidIsland add-on for BentoBox, so to run an AcidIsland game, you must have BentoBox installed. Docs can be found at [https://docs.bentobox.world](https://docs.bentobox.world).

<img width="512" alt="AcidIsland" src="https://github.com/user-attachments/assets/a4fe5284-e20f-457b-8337-3bfe60ddf21d" />

## The Story
You're on an island, in a sea of acid! If you like Skyblock, try the AcidIsland™ game mode for a new challenge!

Instead of falling you must contend with acid water when expanding your island, and players can boat to each other's islands.

<img width="512" alt="acidislandart" src="https://github.com/BentoBoxWorld/AcidIsland/assets/4407265/60e97bba-2b7d-425e-9130-cffef73cf76e">

## Download

You can download from GitHub, or ready made plugin packs from [https://download.bentobox.world](https://download.bentobox.world).
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<junit.version>5.11.4</junit.version>
<!-- More visible way how to change dependency versions -->
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
<bentobox.version>3.12.0-SNAPSHOT</bentobox.version>
<bentobox.version>3.14.0-SNAPSHOT</bentobox.version>
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- Do not change unless you want different name for local builds. -->
Expand Down
73 changes: 73 additions & 0 deletions src/main/java/world/bentobox/acidisland/AISettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ public class AISettings implements WorldSettings {
@ConfigEntry(path = "acid.damage.protection.full-armor")
private boolean fullArmorProtection;

/* PURIFIED WATER */
@ConfigComment("Enable the purified water mechanic. Drinking acid water from a bottle damages")
@ConfigComment("the player; drinking purified water heals them. Purified water can be made by")
@ConfigComment("collecting rain in a cauldron (dripstone fills it as purified; rain/bucket fills")
@ConfigComment("it as acid), smelting a water bottle in a furnace, brewing water bottles with coal,")
@ConfigComment("or smelting a water bucket in a furnace (if bucket-furnace-enabled is true).")
@ConfigEntry(path = "acid.purified-water.enabled", since = "1.21")
private boolean purifiedWaterEnabled = true;

@ConfigComment("Damage dealt to a player who drinks an acid water bottle (in half-hearts)")
@ConfigEntry(path = "acid.purified-water.drink-damage", since = "1.21")
private double acidDrinkDamage = 4.0;

@ConfigComment("Health restored to a player who drinks a purified water bottle (in half-hearts)")
@ConfigEntry(path = "acid.purified-water.heal-amount", since = "1.21")
private double purifiedWaterHeal = 4.0;

@ConfigComment("Allow purifying a water bucket by smelting it in a furnace.")
@ConfigComment("The cook time is 2000 ticks (100 seconds) to simulate the effort of boiling.")
@ConfigComment("Disable if this feels too easy for your server's balance.")
@ConfigEntry(path = "acid.purified-water.bucket-furnace-enabled", since = "1.21")
private boolean purifiedBucketFurnaceEnabled = true;


/* WORLD */
@ConfigComment("Friendly name for this world. Used in admin commands. Must be a single word")
Expand Down Expand Up @@ -670,6 +693,12 @@ public int getAcidDamageMonster() {
public long getAcidDestroyItemTime() {
return acidDestroyItemTime;
}
/**
* @return damage dealt when drinking an acid water bottle (half-hearts)
*/
public double getAcidDrinkDamage() {
return acidDrinkDamage;
}
/**
* @return the acidEffects
*/
Expand All @@ -682,6 +711,24 @@ public List<PotionEffectType> getAcidEffects() {
public int getAcidRainDamage() {
return acidRainDamage;
}
/**
* @return health restored when drinking purified water (half-hearts)
*/
public double getPurifiedWaterHeal() {
return purifiedWaterHeal;
}
/**
* @return true if the purified water mechanic is enabled
*/
public boolean isPurifiedWaterEnabled() {
return purifiedWaterEnabled;
}
/**
* @return true if water buckets can be purified by smelting in a furnace
*/
public boolean isPurifiedBucketFurnaceEnabled() {
return purifiedBucketFurnaceEnabled;
}

@Override
public int getBanLimit() {
Expand Down Expand Up @@ -742,6 +789,7 @@ public Map<String, Integer> getDefaultIslandSettingNames()
* @return the defaultIslandProtection
* @deprecated since 1.21
*/
@Deprecated(since = "1.21")
@Override
public Map<Flag, Integer> getDefaultIslandFlags() {
return Collections.emptyMap();
Expand All @@ -752,6 +800,7 @@ public Map<Flag, Integer> getDefaultIslandFlags() {
* @return the defaultIslandSettings
* @deprecated since 1.21
*/
@Deprecated(since = "1.21")
@Override
public Map<Flag, Integer> getDefaultIslandSettings() {
return Collections.emptyMap();
Expand Down Expand Up @@ -1157,6 +1206,30 @@ public boolean isWaterUnsafe() {
public void setAcidDamage(int acidDamage) {
this.acidDamage = acidDamage;
}
/**
* @param acidDrinkDamage damage dealt when drinking an acid water bottle (half-hearts)
*/
public void setAcidDrinkDamage(double acidDrinkDamage) {
this.acidDrinkDamage = acidDrinkDamage;
}
/**
* @param purifiedWaterEnabled true to enable the purified water mechanic
*/
public void setPurifiedWaterEnabled(boolean purifiedWaterEnabled) {
this.purifiedWaterEnabled = purifiedWaterEnabled;
}
/**
* @param purifiedWaterHeal health restored when drinking purified water (half-hearts)
*/
public void setPurifiedWaterHeal(double purifiedWaterHeal) {
this.purifiedWaterHeal = purifiedWaterHeal;
}
/**
* @param purifiedBucketFurnaceEnabled true to allow furnace-purifying water buckets
*/
public void setPurifiedBucketFurnaceEnabled(boolean purifiedBucketFurnaceEnabled) {
this.purifiedBucketFurnaceEnabled = purifiedBucketFurnaceEnabled;
}
/**
* @param acidDamageAnimal the acidDamageAnimal to set
*/
Expand Down
43 changes: 20 additions & 23 deletions src/main/java/world/bentobox/acidisland/AcidIsland.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import world.bentobox.acidisland.commands.IslandAboutCommand;
import world.bentobox.acidisland.listeners.AcidEffect;
import world.bentobox.acidisland.listeners.LavaCheck;
import world.bentobox.acidisland.listeners.PurifiedWaterListener;
import world.bentobox.acidisland.world.AcidBiomeProvider;
import world.bentobox.acidisland.world.AcidTask;
import world.bentobox.acidisland.world.ChunkGeneratorWorld;
Expand Down Expand Up @@ -103,6 +104,7 @@ public void onEnable() {
// Acid Effects
registerListener(new AcidEffect(this));
registerListener(new LavaCheck(this));
registerListener(new PurifiedWaterListener(this));
// Burn everything
acidTask = new AcidTask(this);
}
Expand Down Expand Up @@ -153,34 +155,29 @@ public void createWorlds() {
* @return world loaded or generated
*/
private World getWorld(String worldName2, Environment env, @Nullable ChunkGenerator chunkGenerator2) {
// Set world name
worldName2 = env.equals(World.Environment.NETHER) ? worldName2 + NETHER : worldName2;
worldName2 = env.equals(World.Environment.THE_END) ? worldName2 + THE_END : worldName2;
WorldCreator wc = WorldCreator.name(worldName2).environment(env).type(WorldType.NORMAL);
String name = getWorldName(worldName2, env);
WorldCreator wc = WorldCreator.name(name).environment(env).type(WorldType.NORMAL);
World w = settings.isUseOwnGenerator() ? wc.createWorld() : wc.generator(chunkGenerator2).createWorld();
// Set spawn rates
if (w != null && getSettings() != null) {
if (getSettings().getSpawnLimitMonsters() > 0) {
w.setSpawnLimit(SpawnCategory.MONSTER, getSettings().getSpawnLimitMonsters());
}
if (getSettings().getSpawnLimitAmbient() > 0) {
w.setSpawnLimit(SpawnCategory.AMBIENT, getSettings().getSpawnLimitAmbient());
}
if (getSettings().getSpawnLimitAnimals() > 0) {
w.setSpawnLimit(SpawnCategory.ANIMAL, getSettings().getSpawnLimitAnimals());
}
if (getSettings().getSpawnLimitWaterAnimals() > 0) {
w.setSpawnLimit(SpawnCategory.WATER_ANIMAL, getSettings().getSpawnLimitWaterAnimals());
}
if (getSettings().getTicksPerAnimalSpawns() > 0) {
w.setTicksPerSpawns(SpawnCategory.ANIMAL, getSettings().getTicksPerAnimalSpawns());
}
if (getSettings().getTicksPerMonsterSpawns() > 0) {
w.setTicksPerSpawns(SpawnCategory.MONSTER, getSettings().getTicksPerMonsterSpawns());
}
configureSpawnRates(w);
}
return w;
}

private String getWorldName(String base, Environment env) {
if (env.equals(World.Environment.NETHER)) return base + NETHER;
if (env.equals(World.Environment.THE_END)) return base + THE_END;
return base;
}

private void configureSpawnRates(World w) {
AISettings s = getSettings();
if (s.getSpawnLimitMonsters() > 0) w.setSpawnLimit(SpawnCategory.MONSTER, s.getSpawnLimitMonsters());
if (s.getSpawnLimitAmbient() > 0) w.setSpawnLimit(SpawnCategory.AMBIENT, s.getSpawnLimitAmbient());
if (s.getSpawnLimitAnimals() > 0) w.setSpawnLimit(SpawnCategory.ANIMAL, s.getSpawnLimitAnimals());
if (s.getSpawnLimitWaterAnimals() > 0) w.setSpawnLimit(SpawnCategory.WATER_ANIMAL, s.getSpawnLimitWaterAnimals());
if (s.getTicksPerAnimalSpawns() > 0) w.setTicksPerSpawns(SpawnCategory.ANIMAL, s.getTicksPerAnimalSpawns());
if (s.getTicksPerMonsterSpawns() > 0) w.setTicksPerSpawns(SpawnCategory.MONSTER, s.getTicksPerMonsterSpawns());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
package world.bentobox.acidisland.events;

import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;

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

/**
* Fired when an ItemStack (water bottle or bucket) is filled with acid
* Fired when an ItemStack (water bottle or bucket) is filled with acid water.
* Cancel this event to prevent the acid bottle from being given to the player.
* @author Poslovitch
* @since 1.0
* @deprecated never used
*/
@Deprecated
public class ItemFillWithAcidEvent extends IslandBaseEvent {
public class ItemFillWithAcidEvent extends IslandBaseEvent implements Cancellable {

private final Player player;
private final ItemStack item;
private boolean cancelled;
private static final HandlerList handlers = new HandlerList();

@Override
Expand Down Expand Up @@ -50,4 +51,14 @@ public Player getPlayer() {
public ItemStack getItem() {
return item;
}

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

@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package world.bentobox.acidisland.events;

import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;

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

/**
* Fired when a player drinks acid and... DIES
* Fired when a player drinks acid water from a bottle.
* Cancel this event to prevent the damage from being applied.
* @author Poslovitch
* @since 1.0
* @deprecated - never fired
*/
@Deprecated
public class PlayerDrinkAcidEvent extends IslandBaseEvent {
public class PlayerDrinkAcidEvent extends IslandBaseEvent implements Cancellable {

private final Player player;
private double damage;
private boolean cancelled;
private static final HandlerList handlers = new HandlerList();

@Override
Expand All @@ -27,16 +29,43 @@ public static HandlerList getHandlerList() {
return handlers;
}

public PlayerDrinkAcidEvent(Island island, Player player) {
public PlayerDrinkAcidEvent(Island island, Player player, double damage) {
super(island);
this.player = player;
this.damage = damage;
}

/**
* Gets the player which is getting killed by its stupid thirsty
* @return the killed player
* Gets the player who drank acid water
* @return the player
*/
public Player getPlayer() {
return player;
}

/**
* Gets the damage that will be applied to the player
* @return damage amount
*/
public double getDamage() {
return damage;
}

/**
* Sets the damage that will be applied to the player
* @param damage new damage amount
*/
public void setDamage(double damage) {
this.damage = damage;
}

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

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