Skip to content
Open
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changelog

All notable changes to Sudden Death are documented in this file.

## [2.3.3] - 2026-08-31

### Fixed

- Restricted the extended target search used by Zombie Break Block to standard zombies holding a supported axe, pickaxe, or shovel.
- Prevented husks, drowned, zombie villagers, zombified piglins, and unequipped zombies from retaining player targets at the feature's configured maximum distance.
- Prevented Zombie Break Block target persistence from running in worlds where the feature is disabled.

### Changed

- Moved Zombie Break Block entity inspection back to the server thread instead of reading Bukkit entity state asynchronously.

### Known Issues

- Automated compilation validates API compatibility, but target acquisition behavior still requires an in-server Paper playtest with each zombie variant.
62 changes: 62 additions & 0 deletions README_STATE_SNAPSHOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Sudden Death State Snapshot

## Current Status

- Current version: `2.3.3`.
- Current development branch: `fix/zombie-target-range`.
- Platform: Paper/Spigot API 1.21 with Java 21.
- Build system: Gradle Wrapper 9.5.1.
- Build status: `gradlew.bat clean build --no-daemon` passed on 2026-08-31 using the local JDK 25 host and Java 21 release target.
- The project is a survival difficulty plugin with configurable player, world, event, monster ability, and custom mob systems.
- The current correction limits Zombie Break Block's custom target acquisition to eligible standard zombies.

## Implemented Features

- Survival mechanics including bleeding, infection, fall stun, hunger nausea, realistic pickup, dangerous coal, electricity shock, snow slow, stone stiffness, and advanced player drops.
- World events including Blood Moon, Meteor Rain, and enhanced thunderstorms.
- Monster abilities for guardians, spiders, skeletons, breezes, creepers, End mobs, blazes, evokers, phantoms, pillagers, slimes, drowned, zombies, witches, wither skeletons, and other hostile mobs.
- Configurable monster attack, movement speed, health, armor-piercing, and damage-resistance modifiers.
- Custom mob creation and editing with equipment, attributes, potion effects, names, and spawn probabilities.
- Zombie Break Block for standard zombies holding axes, pickaxes, or shovels, including configurable block lists, drops, and maximum player search distance.
- Command and GUI flows for status, administration, recipes, feature management, events, and custom mobs.
- Optional integrations for WorldGuard and supported claim plugins.

## Pending Features

- No new gameplay feature is currently scheduled in this repository snapshot.
- Complete an in-server regression test for the Zombie Break Block target-range correction.

## Technical Debt

- The project does not currently include source-level automated tests for Bukkit event and entity behavior.
- Several feature loops scan all entities of a class and may need profiling on high-population servers.
- Some older classes use broad exception handling and should be reviewed incrementally rather than refactored during unrelated fixes.
- The build declares Java 21 but may be invoked from newer local JDKs through Gradle toolchain behavior.

## Known Issues

- Runtime behavior cannot be fully validated without launching a Paper 1.21 test server.
- Existing servers upgrading from 2.3.2 should be restarted so entities do not retain transient targets assigned by the old plugin instance.

## Architecture Notes

- `FeatureManager` creates and registers feature classes derived from `AbstractFeature`.
- `Feature` defines each feature's default modifiers and owns its generated configuration file access.
- `ZombieBreakBlockFeature` owns block selection, block-breaking tasks, custom target search, and target persistence.
- Bukkit entity state must be read and modified on the primary server thread. The target search correction follows this rule.
- Paper's `Zombie` interface also covers `Drowned`, `Husk`, `PigZombie`, and `ZombieVillager`; code intended only for a standard zombie must explicitly check `EntityType.ZOMBIE`.
- `max-target-distance` remains a Zombie Break Block setting and is applied only to standard zombies holding a supported tool.
- ProtocolLib is a required dependency and is used for block break animation packets.

## Recent Changes

- Version 2.3.3 prevents Zombie Break Block from extending target acquisition for zombie variants and unequipped zombies.
- Target persistence now stops when the feature is disabled or the zombie becomes ineligible.
- Zombie entity inspection for this feature now runs on the server thread.

## Next Recommended Tasks

1. Start a private Paper 1.21 test server with ProtocolLib and Sudden Death 2.3.3.
2. Spawn a zombie, husk, drowned, zombie villager, and zombified piglin more than the vanilla targeting distance from a Survival player.
3. Verify that only a standard zombie holding a supported tool uses the configured Zombie Break Block distance.
4. Verify feature disable/reload, entity death, world changes, and block-breaking cleanup before release.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
}

group = 'org.NguyenDevs'
version = '2.3.2'
version = '2.3.3'

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ public void run() {
continue;
List<Zombie> zombies = new ArrayList<>(world.getEntitiesByClass(Zombie.class));
for (Zombie zombie : zombies) {
if (!isEligibleBlockBreakingZombie(zombie))
continue;
long randomDelay = ThreadLocalRandom.current().nextLong(0, 5);
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
try {
if (zombie.isValid() &&
(zombie.getTarget() instanceof Player
Expand Down Expand Up @@ -103,14 +105,12 @@ public void run() {
public void run() {
try {
for (World world : Bukkit.getWorlds()) {
if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(world))
continue;
for (Zombie zombie : new ArrayList<>(world.getEntitiesByClass(Zombie.class))) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
searchForNearbyPlayers(zombie);
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING, "Error searching for players", e);
}
});
if (!isEligibleBlockBreakingZombie(zombie))
continue;
searchForNearbyPlayers(zombie);
}
}
} catch (Exception e) {
Expand Down Expand Up @@ -146,6 +146,12 @@ public void onEntityTarget(EntityTargetEvent event) {
return;
UUID zombieUUID = zombie.getUniqueId();

if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie) || !isEligibleBlockBreakingZombie(zombie)) {
persistentTargets.remove(zombieUUID);
lastTargetSearchTime.remove(zombieUUID);
return;
}

if (event.getTarget() instanceof Player player) {
if (!isValidGameMode(player.getGameMode())) {
event.setCancelled(true);
Expand Down Expand Up @@ -177,7 +183,8 @@ public void onEntityTarget(EntityTargetEvent event) {
}

private void searchForNearbyPlayers(Zombie zombie) {
if (!zombie.isValid())
if (!zombie.isValid() || !Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie)
|| !isEligibleBlockBreakingZombie(zombie))
return;

UUID zombieUUID = zombie.getUniqueId();
Expand Down Expand Up @@ -228,6 +235,11 @@ private void maintainPersistentTargets() {
if (!(zombieEntity instanceof Zombie zombie) || !zombie.isValid())
return true;

if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie) || !isEligibleBlockBreakingZombie(zombie)) {
zombie.setTarget(null);
return true;
}

Player target = Bukkit.getPlayer(entry.getValue());
if (target == null || !target.isOnline())
return true;
Expand Down Expand Up @@ -257,6 +269,8 @@ private void processZombieBreakBlock(Zombie zombie) {
private void applyZombieBreakBlock(Zombie zombie) {
if (zombie == null || zombie.getHealth() <= 0)
return;
if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie) || !isEligibleBlockBreakingZombie(zombie))
return;
if (activeBreakingTasks.containsKey(zombie.getUniqueId()))
return;

Expand Down Expand Up @@ -521,6 +535,12 @@ private boolean isValidTool(Material material) {
return isPickaxe(material) || isShovel(material) || isAxe(material);
}

private boolean isEligibleBlockBreakingZombie(Zombie zombie) {
return zombie.getType() == EntityType.ZOMBIE
&& zombie.getEquipment() != null
&& isValidTool(zombie.getEquipment().getItemInMainHand().getType());
}

private boolean isPickaxe(Material material) {
return material.name().contains("PICKAXE");
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: SuddenDeath
version: 2.3.2
version: 2.3.3
main: org.nguyendevs.suddendeath.SuddenDeath
depend: [ProtocolLib]
softdepend: [WorldGuard, SkinsRestorer, Lands, SuperiorSkyblock2, GriefPrevention, SimpleClaimSystem, xclaim]
Expand Down