From 7e79bee3b951cb748182dd65e58b8acdb2eb1902 Mon Sep 17 00:00:00 2001 From: TGdoesCode Date: Thu, 16 Apr 2026 10:32:02 +0200 Subject: [PATCH] adjust obsidian cube spawning and super beacon --- gradle.properties | 2 +- .../szar/SuperBeaconBlockEntity.java | 224 +++++++++++++++--- src/main/java/dev/tggamesyt/szar/Szar.java | 49 +++- .../worldgen/structure_set/obsidian_cube.json | 4 +- 4 files changed, 228 insertions(+), 51 deletions(-) diff --git a/gradle.properties b/gradle.properties index b928391..9e82ef1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.20.1 yarn_mappings=1.20.1+build.10 loader_version=0.18.3 # Mod Properties -mod_version=26.4.15 +mod_version=26.4.16 maven_group=dev.tggamesyt archives_base_name=szar # Dependencies diff --git a/src/main/java/dev/tggamesyt/szar/SuperBeaconBlockEntity.java b/src/main/java/dev/tggamesyt/szar/SuperBeaconBlockEntity.java index 976707e..a74e1c7 100644 --- a/src/main/java/dev/tggamesyt/szar/SuperBeaconBlockEntity.java +++ b/src/main/java/dev/tggamesyt/szar/SuperBeaconBlockEntity.java @@ -32,12 +32,27 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex public static final int FUEL_INTERVAL = 27000; + // Static registry of all loaded super beacons for cross-beacon stacking + public static final Set ACTIVE_BEACONS = + java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + + // GLOBAL tracking of which players have beacon-granted persistent effects. + // Survives beacon unload — the server tick handler in Szar.java checks this + // against ACTIVE_BEACONS to strip effects when no beacon covers the player. + // Key: player UUID. Value: set of effect types currently granted. + public static final Map> GLOBAL_PERSISTENT_TRACKING = + new java.util.concurrent.ConcurrentHashMap<>(); + private static final Set PERSISTENT_EFFECTS = new HashSet<>(); static { PERSISTENT_EFFECTS.add(StatusEffects.NAUSEA); PERSISTENT_EFFECTS.add(StatusEffects.HEALTH_BOOST); } + public static boolean isPersistentEffect(StatusEffect e) { + return PERSISTENT_EFFECTS.contains(e); + } + private final DefaultedList inventory = DefaultedList.ofSize(8, ItemStack.EMPTY); private final boolean[] rowActive = new boolean[4]; private final int[] fuelTimers = new int[4]; @@ -110,6 +125,9 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex public static void tick(World world, BlockPos pos, BlockState state, SuperBeaconBlockEntity be) { if (world.isClient()) return; + // Register self in static set for cross-beacon stacking + ACTIVE_BEACONS.add(be); + if (world.getTime() % 20 == 0) { int newLevel = be.computeBeaconLevel(); if (newLevel != be.beaconLevel) { @@ -121,7 +139,6 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex for (int row = 0; row < 4; row++) { if (!be.rowActive[row]) continue; - // Row N requires level >= N+1 if (be.beaconLevel < row + 1 || be.getStack(row * 2).isEmpty()) { be.deactivateRow(row); continue; @@ -136,16 +153,17 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex if (fuelItem.isEmpty()) be.setStack(row * 2 + 1, ItemStack.EMPTY); be.markDirty(); } - } - if (world.getTime() % 80 == 0) be.applyAllEffects(); + // Only the "coordinator" beacon for each player applies effects. + // Coordinator = closest active super beacon covering that player. + if (world.getTime() % 80 == 0) be.applyEffectsAsCoordinator(); if (world.getTime() % 100 == 0) be.checkPersistentRangeAll(); if (world.getTime() % 100 == 0) be.cleanupPersistentTracking(); } - // Collects effects from all active rows, summing amplifiers when same effect appears - // in multiple rows. Stacking: lvl1 + lvl1 = lvl2 → amp_sum = amp1 + amp2 + 1. + // Collects effects from this beacon's active rows, summing amplifiers for duplicates. + // Stacking: lvl1 + lvl1 = lvl2 → amp_sum = amp1 + amp2 + 1. private Map collectCombinedEffects() { Map combined = new HashMap<>(); for (int row = 0; row < 4; row++) { @@ -156,7 +174,6 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex StatusEffect type = e.getEffectType(); int amp = e.getAmplifier(); if (combined.containsKey(type)) { - // Stack: each row contributes (amp+1) levels combined.put(type, combined.get(type) + amp + 1); } else { combined.put(type, amp); @@ -166,19 +183,112 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex return combined; } - private void applyAllEffects() { + // Merges another beacon's effects into this map. Same stacking rule. + private static void mergeInto(Map target, Map source) { + for (Map.Entry e : source.entrySet()) { + if (target.containsKey(e.getKey())) { + target.put(e.getKey(), target.get(e.getKey()) + e.getValue() + 1); + } else { + target.put(e.getKey(), e.getValue()); + } + } + } + + // Search radius for finding other super beacons. A beacon only matters if its + // coverage could overlap ours, so we check distance against (our radius + their radius) + // when iterating the registry. + // Find all registered active super beacons whose radius overlaps ours. + // Includes self. + private List findNearbyActiveBeacons() { + List result = new ArrayList<>(); + if (world == null) return result; + + double myRadius = getEffectRadius(); + + for (SuperBeaconBlockEntity other : ACTIVE_BEACONS) { + if (other.world != this.world) continue; + if (!other.hasAnyActiveRow()) continue; + if (other == this) { result.add(other); continue; } + + double dist = other.centerVec().distanceTo(centerVec()); + // Only relevant if coverage regions could overlap for some player + if (dist <= myRadius + other.getEffectRadius()) { + result.add(other); + } + } + return result; + } + + public boolean hasAnyActiveRow() { + for (boolean b : rowActive) if (b) return true; + return false; + } + + // Compute all beacon effects currently covering a given player. + // Iterates the static ACTIVE_BEACONS registry (loaded beacons only). + public static Map computeCoveringEffects(PlayerEntity player) { + Map combined = new HashMap<>(); + Vec3d playerPos = player.getPos(); + for (SuperBeaconBlockEntity be : ACTIVE_BEACONS) { + if (be.world != player.getWorld()) continue; + if (!be.hasAnyActiveRow()) continue; + if (playerPos.distanceTo(be.centerVec()) > be.getEffectRadius()) continue; + mergeInto(combined, be.collectCombinedEffects()); + } + return combined; + } + + public Vec3d centerVec() { + return new Vec3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + } + + // Returns true if this beacon is the "coordinator" (closest active beacon covering player). + private boolean isCoordinatorFor(PlayerEntity player, List allBeacons) { + Vec3d playerPos = player.getPos(); + double myDist = playerPos.distanceTo(centerVec()); + if (myDist > getEffectRadius()) return false; + + SuperBeaconBlockEntity closest = this; + double closestDist = myDist; + + for (SuperBeaconBlockEntity other : allBeacons) { + if (other == this) continue; + double d = playerPos.distanceTo(other.centerVec()); + if (d > other.getEffectRadius()) continue; + if (d < closestDist) { + closest = other; + closestDist = d; + } + } + return closest == this; + } + + private void applyEffectsAsCoordinator() { if (world == null) return; - Map combined = collectCombinedEffects(); - if (combined.isEmpty()) return; + double myRadius = getEffectRadius(); + if (myRadius <= 0) return; - double radius = getEffectRadius(); - Box box = new Box(pos).expand(radius); - List players = world.getEntitiesByClass(PlayerEntity.class, box, p -> true); + List nearbyBeacons = findNearbyActiveBeacons(); + + // We can only be coordinator for players WE cover. + Box searchBox = new Box(pos).expand(myRadius); + List candidatePlayers = world.getEntitiesByClass(PlayerEntity.class, searchBox, p -> true); + + for (PlayerEntity player : candidatePlayers) { + if (!isCoordinatorFor(player, nearbyBeacons)) continue; + + Map combined = new HashMap<>(collectCombinedEffects()); + Vec3d playerPos = player.getPos(); + + for (SuperBeaconBlockEntity other : nearbyBeacons) { + if (other == this) continue; + if (playerPos.distanceTo(other.centerVec()) > other.getEffectRadius()) continue; + mergeInto(combined, other.collectCombinedEffects()); + } - for (PlayerEntity player : players) { for (Map.Entry e : combined.entrySet()) { StatusEffect type = e.getKey(); - int amp = Math.min(e.getValue(), 255); // amp cap + int amp = Math.min(e.getValue(), 255); if (PERSISTENT_EFFECTS.contains(type)) { applyPersistent(player, type, amp); @@ -190,28 +300,33 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex } } - // Persistent tracking: key by effect type, store current applied amplifier - // so we re-apply when amplifier changes (row added/removed) + // Persistent tracking: re-apply when amplifier changes. + // Duration is finite (200t = 10s) so effect wears off if beacon stops ticking. + // Also writes to GLOBAL_PERSISTENT_TRACKING so the server-wide cleanup in Szar.java + // can strip effects when beacon is unloaded. private void applyPersistent(PlayerEntity player, StatusEffect type, int amp) { UUID uuid = player.getUuid(); Map> bucket = persistentTracking.computeIfAbsent(uuid, k -> new HashMap<>()); - // Use single bucket key 0 to track all persistent effects per player Set tracked = bucket.computeIfAbsent(0, k -> new HashSet<>()); StatusEffectInstance current = player.getStatusEffect(type); - if (current == null || current.getAmplifier() != amp) { + if (current != null && current.getAmplifier() == amp) { + if (current.getDuration() < 160) { + player.addStatusEffect(new StatusEffectInstance( + type, 200, amp, true, true, true)); + } + } else { player.removeStatusEffect(type); player.addStatusEffect(new StatusEffectInstance( - type, Integer.MAX_VALUE, amp, true, true, true)); - tracked.add(type); + type, 200, amp, true, true, true)); } + tracked.add(type); + GLOBAL_PERSISTENT_TRACKING.computeIfAbsent(uuid, k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(type); } private void checkPersistentRangeAll() { if (world == null) return; - double radius = getEffectRadius(); - Vec3d center = new Vec3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); - Map combined = collectCombinedEffects(); + List nearbyBeacons = findNearbyActiveBeacons(); for (Map.Entry>> entry : persistentTracking.entrySet()) { Set tracked = entry.getValue().get(0); @@ -219,14 +334,21 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex PlayerEntity player = world.getPlayerByUuid(entry.getKey()); if (player == null) continue; + if (!isCoordinatorFor(player, nearbyBeacons)) continue; - boolean outOfRange = player.getPos().distanceTo(center) > radius; + // Compute what combined effects *should* exist for this player + Map combined = new HashMap<>(collectCombinedEffects()); + Vec3d playerPos = player.getPos(); + for (SuperBeaconBlockEntity other : nearbyBeacons) { + if (other == this) continue; + if (playerPos.distanceTo(other.centerVec()) > other.getEffectRadius()) continue; + mergeInto(combined, other.collectCombinedEffects()); + } - // Remove effects that are out of range OR no longer provided by any active row Iterator it = tracked.iterator(); while (it.hasNext()) { StatusEffect e = it.next(); - if (outOfRange || !combined.containsKey(e)) { + if (!combined.containsKey(e)) { player.removeStatusEffect(e); it.remove(); } @@ -289,27 +411,24 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex } public void toggleRow(int row) { - Szar.LOGGER.info("[Szar] toggleRow called: row={}, level={}, active={}", row, beaconLevel, rowActive[row]); - if (row < 0 || row >= 4) { Szar.LOGGER.warn("[Szar] row out of bounds"); return; } - if (beaconLevel < row + 1) { Szar.LOGGER.warn("[Szar] row locked, need level {}", row + 1); return; } + if (row < 0 || row >= 4) {return; } + if (beaconLevel < row + 1) { return; } if (rowActive[row]) { deactivateRow(row); - Szar.LOGGER.info("[Szar] deactivated row {}", row); } else { ItemStack effectItem = getStack(row * 2); ItemStack fuelItem = getStack(row * 2 + 1); - if (effectItem.isEmpty()) { Szar.LOGGER.warn("[Szar] effect slot empty"); return; } - if (fuelItem.isEmpty()) { Szar.LOGGER.warn("[Szar] fuel slot empty"); return; } - if (!isValidEffectItem(effectItem)) { Szar.LOGGER.warn("[Szar] effect item invalid: {}", effectItem); return; } - if (!isValidFuel(fuelItem)) { Szar.LOGGER.warn("[Szar] fuel item invalid: {}", fuelItem); return; } + if (effectItem.isEmpty()) { return; } + if (fuelItem.isEmpty()) { return; } + if (!isValidEffectItem(effectItem)) { return; } + if (!isValidFuel(fuelItem)) { return; } fuelItem.decrement(1); if (fuelItem.isEmpty()) setStack(row * 2 + 1, ItemStack.EMPTY); rowActive[row] = true; fuelTimers[row] = 0; markDirty(); - Szar.LOGGER.info("[Szar] activated row {}", row); } } @@ -320,6 +439,28 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex markDirty(); } + @Override + public void markRemoved() { + super.markRemoved(); + ACTIVE_BEACONS.remove(this); + // Strip any persistent effects we granted, since we won't tick anymore + clearAllPersistentEffects(); + } + + private void clearAllPersistentEffects() { + if (world == null) return; + for (Map.Entry>> entry : persistentTracking.entrySet()) { + Set tracked = entry.getValue().get(0); + if (tracked == null) continue; + PlayerEntity player = world.getPlayerByUuid(entry.getKey()); + if (player != null) { + for (StatusEffect e : tracked) player.removeStatusEffect(e); + } + tracked.clear(); + } + persistentTracking.clear(); + } + @Override public int size() { return 8; } @Override public boolean isEmpty() { return inventory.stream().allMatch(ItemStack::isEmpty); } @Override public ItemStack getStack(int slot) { return inventory.get(slot); } @@ -380,4 +521,17 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex public void writeScreenOpeningData(ServerPlayerEntity player, PacketByteBuf buf) { buf.writeBlockPos(pos); } + + // --- Registry lifecycle --- + @Override + public void setWorld(World world) { + super.setWorld(world); + if (!world.isClient()) ACTIVE_BEACONS.add(this); + } + + @Override + public void cancelRemoval() { + super.cancelRemoval(); + if (world != null && !world.isClient()) ACTIVE_BEACONS.add(this); + } } \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/Szar.java b/src/main/java/dev/tggamesyt/szar/Szar.java index c23c995..6994809 100644 --- a/src/main/java/dev/tggamesyt/szar/Szar.java +++ b/src/main/java/dev/tggamesyt/szar/Szar.java @@ -1506,24 +1506,48 @@ public class Szar implements ModInitializer { BlockPos pos = buf.readBlockPos(); int row = buf.readInt(); server.execute(() -> { - LOGGER.info("[Szar] activate_row: pos={}, row={}", pos, row); if (player.getServerWorld().getBlockEntity(pos) instanceof SuperBeaconBlockEntity be) { if (be.canPlayerUse(player)) { - boolean wasActive = be.isRowActive(row); be.toggleRow(row); - LOGGER.info("[Szar] row {} {} -> {} (level={}, effect={}, fuel={})", - row, wasActive, be.isRowActive(row), be.getBeaconLevel(), - be.getStack(row * 2), be.getStack(row * 2 + 1)); - } else { - LOGGER.warn("[Szar] player too far"); } - } else { - LOGGER.warn("[Szar] no block entity at {}", pos); } }); }); - LOGGER.info("[Szar] Initialized"); + // Global cleanup: every 20 ticks, iterate all players with tracked persistent effects + // and strip effects not currently produced by any LOADED active beacon covering them. + // Handles the case where the granting beacon unloaded (and thus can't tick anymore). + ServerTickEvents.END_SERVER_TICK.register(server -> { + if (server.getTicks() % 20 != 0) return; + + Iterator>> it = + SuperBeaconBlockEntity.GLOBAL_PERSISTENT_TRACKING.entrySet().iterator(); + + while (it.hasNext()) { + Map.Entry> entry = it.next(); + UUID uuid = entry.getKey(); + Set tracked = entry.getValue(); + + ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); + if (player == null) { + // Player offline - keep tracked set until they reconnect (effect will refresh or expire) + continue; + } + + Map covering = SuperBeaconBlockEntity.computeCoveringEffects(player); + + Iterator eit = tracked.iterator(); + while (eit.hasNext()) { + StatusEffect e = eit.next(); + if (!covering.containsKey(e)) { + player.removeStatusEffect(e); + eit.remove(); + } + } + + if (tracked.isEmpty()) it.remove(); + } + }); } public static final Block SUPER_BEACON_BLOCK = new SuperBeaconBlock( FabricBlockSettings.copyOf(Blocks.BEACON).luminance(15) @@ -1725,11 +1749,10 @@ public class Szar implements ModInitializer { .hunger(20) .alwaysEdible() .saturationModifier(20F) - .statusEffect(new StatusEffectInstance(StatusEffects.REGENERATION,60*20, 255 ), 1F) - .statusEffect(new StatusEffectInstance(StatusEffects.HEALTH_BOOST,2*60*20, 4 ), 1F) + .statusEffect(new StatusEffectInstance(StatusEffects.REGENERATION,60*20, 2 ), 1F) .statusEffect(new StatusEffectInstance(StatusEffects.RESISTANCE,5*60*20, 2), 1F) .statusEffect(new StatusEffectInstance(StatusEffects.FIRE_RESISTANCE,5*60*20, 2), 1F) - .statusEffect(new StatusEffectInstance(StatusEffects.ABSORPTION,5*60*20, 4), 1F) + .statusEffect(new StatusEffectInstance(StatusEffects.ABSORPTION,5*60*20, 10), 1F) .statusEffect(new StatusEffectInstance(StatusEffects.STRENGTH,5*60*20, 2), 1F) .build() ).rarity(Rarity.EPIC)) diff --git a/src/main/resources/data/szar/worldgen/structure_set/obsidian_cube.json b/src/main/resources/data/szar/worldgen/structure_set/obsidian_cube.json index c02cbb1..f3548eb 100644 --- a/src/main/resources/data/szar/worldgen/structure_set/obsidian_cube.json +++ b/src/main/resources/data/szar/worldgen/structure_set/obsidian_cube.json @@ -7,8 +7,8 @@ ], "placement": { "type": "minecraft:random_spread", - "spacing": 10, - "separation": 1, + "spacing": 3, + "separation": 0, "salt": 398826349 } } \ No newline at end of file