
ZombieApocalypseSSS
ZombieApocalypseSSS is a comprehensive Minecraft plugin that transforms your server into a thrilling zombie survival apocalypse. Featuring advanced zombie AI, evolving variants, player infection mechanics, psychological horror elements, and dynamic events
Список изменений
[4.1] — 2026-03-14 🔧 The Bug Purge Update
23 bugs fixed across all severity levels. This release focuses entirely on correctness, stability, and developer experience — no new gameplay features, just a rock-solid foundation.
🔴 Critical Fixes
🏰 Turrets lost on every server restart (BUG-02)
loadTurrets() and saveTurrets() were both empty // TODO stubs.
Every turret placed by players vanished after a restart.
Fix: Full persistence implemented via turrets_data.yml using atomic
write (.tmp → rename) to prevent data corruption on crashes.
📦 Supply Drop loot table was never loaded (BUG-03)
spawnSupplyDrop() read supply-drop.loot from config.yml, but this section
is defined in events.yml. The config section was always null, so every
Supply Drop fell back to a hardcoded 5-item list, ignoring all admin configuration.
Fix: spawnSupplyDrop() and all related methods now read exclusively from
eventsConfig (events.yml). Admin-configured loot tables are now respected.
🌕 blood-moon.chance config had no effect (BUG-04)
The bloodMoonChance field was loaded from config (default 5%) but never
referenced in checkBloodMoon(). Blood Moon triggered at 100% probability
every N days instead of the configured 5% chance.
Fix: Math.random() < bloodMoonChance check added in checkBloodMoon().
Blood Moon now correctly triggers by chance each eligible night.
💥 NullPointerException when Molotov reached max range (BUG-05)
ProjectileHitEvent fires when a projectile expires at max range, at which point
both getHitBlock() and getHitEntity() return null. The handler assumed at
least one was non-null, causing a server-crashing NPE.
Fix: Three-way null check: hit block → hit entity → projectile location (fallback).
🟠 High Severity Fixes
🩸 Bleeding damage triggered every 40 seconds instead of 2 (BUG-06)
PlayerStatusTask runs every 20 ticks (1 second). The bleeding check used
tickCount % 40 == 0, which fires every 40 runs = every 40 seconds.
Players with bleeding could regenerate faster than they took damage.
Fix: Changed to tickCount % 2 == 0 → bleeds every 2 seconds as intended.
⚠️ No combined cap on zombie difficulty multipliers (BUG-07)
Three independent scaling systems stacked without a total ceiling:
- Global difficulty multiplier (capped at ×3.0 ✓)
- Evolution tier multiplier (Aberrant ×2.5 ✓)
- Per-player
daysSurvivedscaling (no cap ✗)
On a 100-day server, the combined multiplier could reach ×26×, making zombies practically unkillable.
Fix: daysSurvived scaling is now capped — health ≤ ×3.0, speed ≤ ×2.0,
spawn count ≤ 5 per spawn event.
🔫 Gun reload lock persisted through player relog (BUG-08)
RELOAD_COOLDOWN_KEY and FIRE_COOLDOWN_KEY were stored in
player.getPersistentDataContainer() as Unix timestamps. If a player disconnected
mid-reload and reconnected, the stale future timestamp would lock them out of
firing/reloading until the original timer expired.
Fix: Both cooldowns moved to ConcurrentHashMap<UUID, Long> in-memory maps.
Cleared automatically when the player disconnects.
👻 Hallucination Villager stood completely still (BUG-09)
updateHallucinations() called mob.setTarget(player) to make hallucinations
chase their target. While Zombie entities respond to setTarget() by pathfinding
toward the player, Villager ignores it (Villagers don't attack).
Fix: Villager hallucinations now use getPathfinder().moveTo(player, 1.15)
with periodic re-pathfind if the current path becomes null.
👁️ Players who joined late could see hallucination entities (BUG-10)
spawnHallucination() called hideEntity() only for players online at spawn time.
Any player joining after a hallucination spawned would see a phantom zombie/villager
wandering around with no explanation.
Fix: The existing onPlayerJoin() handler correctly hides all active
hallucinations for new joiners. Spawn-time hiding loop now also reliably covers
all current online players.
🟡 Medium Severity Fixes
🪓 Weapon upgrade chain used hardcoded CustomModelData values (BUG-11)
FeaturesListener hardcoded CMDs like 3004, 3011, 3005 directly in Java.
Changing any custom-model-data in items.yml would silently break upgrade
chains with no error or warning.
Fix: CMDs are now read from items.yml and mechanics.yml at runtime.
Upgrade chains survive config customization.
🔥 Campfire data corrupted if world name contained _ (BUG-12)
Save keys used worldName_x_y_z format. Loading split on _ and took parts[0]
as the world name. A world named my_world would produce 5 parts instead of 4,
causing parts[0] = "my" → Bukkit.getWorld("my") = null → all campfires lost.
Fix: Keys now use worldUUID_x_y_z (UUID with hyphens stripped = always 32
hex chars, no ambiguous underscores). Legacy world-name keys are still parsed as
a migration fallback.
🌙 blood-moon.enabled / supply-drop.enabled read from wrong config (BUG-14)
Both keys are defined in events.yml, but WorldEventTask.run() read them from
config.yml. Since config.yml doesn't contain these keys, the fallback true
was always used — neither event could be disabled.
Fix: All WorldEventTask config reads now use eventsConfig consistently.
☠️ Corpse reanimation could not be disabled (BUG-15)
When a player died, items were cleared and a zombie "corpse" was spawned wearing their gear. There was no config toggle — servers running minigames or PvP arenas had no way to opt out of this mechanic.
Fix: Gated behind corpse-reanimation.enabled in mechanics.yml (default: true).
💬 Misleading comment in ZombieBehaviorTask (BUG-16)
Comment said // Every 20 ticks = 1s next to tickCounter % 2 == 0. The actual
math (task runs every 10 ticks; %2 = every 2 runs = every 20 ticks = 1s) was
correct but the comment was confusing enough to invite accidental breakage.
Fix: Comment updated to explain the full calculation chain.
🎆 Default Screamer particle "PORTAL" is invalid in Paper 1.21 (BUG-17)
Particle.valueOf("PORTAL") throws IllegalArgumentException in Paper 1.21+.
The exception was caught and logged, but the screamer ability silently failed every
time on unmodified configs.
Fix: Default changed to "ENTITY_EFFECT" in both code and zombies.yml.
🟢 Code Quality
| # | Change |
|---|---|
| 🗑️ BUG-18 | Deleted manager/ZombieManager.java — 532 lines of dead code never instantiated. Contained a severe anti-pattern (one BukkitTask per zombie). |
| 🗑️ BUG-19 | Deleted manager/EventManager.java — 739 lines of boilerplate never registered as a Listener in onEnable(). |
| 📐 BUG-20 | Added missing constants to ZombieConstants: AI ranges, noise radii, bleed values, day-scaling caps. Magic numbers reduced across codebase. |
| 🏷️ BUG-21 | Renamed trapKey / is_trap PDC key to engineeringItemKey / is_engineering_item in EngineeringTableManager. Ammo is not a trap. |
| 🏷️ BUG-22 | Renamed ZombieUtils.applyAI() → applyFollowRange(). The method only sets follow range to 40 blocks — the old name implied far more than it did. |
| 🧹 BUG-23 | ZombieCleanupListener now removes zombies from the tracking map on chunk unload, freeing strong references to de-activated entities and reducing memory pressure on long-running servers. |
⚙️ Config Changes
mechanics.yml — new keys:
corpse-reanimation:
enabled: true # Set false to disable zombie corpse on player death
infection:
natural-decay:
enabled: true
chance-per-second: 0.033 # ~1 point per 30s; 3× faster in Campfire safe zones
events.yml — new key:
blood-moon:
cycle-days: 7 # (moved from config.yml) eligible cycle in days
zombies.yml — changed default:
screamer:
particle: ENTITY_EFFECT # was: PORTAL (invalid in Paper 1.21)
🧩 API Changes
// ✅ Fixed
api.isInfected(player) // now correctly returns true when infected
// ✅ New
api.getInfectionLevel(player) // int 0–5
api.getInfectionPoints(player) // int 0–100
api.setInfectionPoints(player, n)
api.getThreatLevel() // double
api.getZombieTier(zombie) // ZombieTier enum
api.isInSafeZone(location) // boolean
api.spawnZombie(world, loc, type)
// ⚠️ Deprecated
api.infectPlayer(player, ticks) // use setInfectionPoints() instead
