adjust obsidian cube spawning and super beacon

This commit is contained in:
2026-04-16 10:32:02 +02:00
parent a8769a844d
commit 7e79bee3b9
4 changed files with 228 additions and 51 deletions

View File

@@ -6,7 +6,7 @@ minecraft_version=1.20.1
yarn_mappings=1.20.1+build.10 yarn_mappings=1.20.1+build.10
loader_version=0.18.3 loader_version=0.18.3
# Mod Properties # Mod Properties
mod_version=26.4.15 mod_version=26.4.16
maven_group=dev.tggamesyt maven_group=dev.tggamesyt
archives_base_name=szar archives_base_name=szar
# Dependencies # Dependencies

View File

@@ -32,12 +32,27 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
public static final int FUEL_INTERVAL = 27000; public static final int FUEL_INTERVAL = 27000;
// Static registry of all loaded super beacons for cross-beacon stacking
public static final Set<SuperBeaconBlockEntity> 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<UUID, Set<StatusEffect>> GLOBAL_PERSISTENT_TRACKING =
new java.util.concurrent.ConcurrentHashMap<>();
private static final Set<StatusEffect> PERSISTENT_EFFECTS = new HashSet<>(); private static final Set<StatusEffect> PERSISTENT_EFFECTS = new HashSet<>();
static { static {
PERSISTENT_EFFECTS.add(StatusEffects.NAUSEA); PERSISTENT_EFFECTS.add(StatusEffects.NAUSEA);
PERSISTENT_EFFECTS.add(StatusEffects.HEALTH_BOOST); PERSISTENT_EFFECTS.add(StatusEffects.HEALTH_BOOST);
} }
public static boolean isPersistentEffect(StatusEffect e) {
return PERSISTENT_EFFECTS.contains(e);
}
private final DefaultedList<ItemStack> inventory = DefaultedList.ofSize(8, ItemStack.EMPTY); private final DefaultedList<ItemStack> inventory = DefaultedList.ofSize(8, ItemStack.EMPTY);
private final boolean[] rowActive = new boolean[4]; private final boolean[] rowActive = new boolean[4];
private final int[] fuelTimers = new int[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) { public static void tick(World world, BlockPos pos, BlockState state, SuperBeaconBlockEntity be) {
if (world.isClient()) return; if (world.isClient()) return;
// Register self in static set for cross-beacon stacking
ACTIVE_BEACONS.add(be);
if (world.getTime() % 20 == 0) { if (world.getTime() % 20 == 0) {
int newLevel = be.computeBeaconLevel(); int newLevel = be.computeBeaconLevel();
if (newLevel != be.beaconLevel) { if (newLevel != be.beaconLevel) {
@@ -121,7 +139,6 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
for (int row = 0; row < 4; row++) { for (int row = 0; row < 4; row++) {
if (!be.rowActive[row]) continue; if (!be.rowActive[row]) continue;
// Row N requires level >= N+1
if (be.beaconLevel < row + 1 || be.getStack(row * 2).isEmpty()) { if (be.beaconLevel < row + 1 || be.getStack(row * 2).isEmpty()) {
be.deactivateRow(row); be.deactivateRow(row);
continue; continue;
@@ -136,16 +153,17 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
if (fuelItem.isEmpty()) be.setStack(row * 2 + 1, ItemStack.EMPTY); if (fuelItem.isEmpty()) be.setStack(row * 2 + 1, ItemStack.EMPTY);
be.markDirty(); 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.checkPersistentRangeAll();
if (world.getTime() % 100 == 0) be.cleanupPersistentTracking(); if (world.getTime() % 100 == 0) be.cleanupPersistentTracking();
} }
// Collects effects from all active rows, summing amplifiers when same effect appears // Collects effects from this beacon's active rows, summing amplifiers for duplicates.
// in multiple rows. Stacking: lvl1 + lvl1 = lvl2 → amp_sum = amp1 + amp2 + 1. // Stacking: lvl1 + lvl1 = lvl2 → amp_sum = amp1 + amp2 + 1.
private Map<StatusEffect, Integer> collectCombinedEffects() { private Map<StatusEffect, Integer> collectCombinedEffects() {
Map<StatusEffect, Integer> combined = new HashMap<>(); Map<StatusEffect, Integer> combined = new HashMap<>();
for (int row = 0; row < 4; row++) { for (int row = 0; row < 4; row++) {
@@ -156,7 +174,6 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
StatusEffect type = e.getEffectType(); StatusEffect type = e.getEffectType();
int amp = e.getAmplifier(); int amp = e.getAmplifier();
if (combined.containsKey(type)) { if (combined.containsKey(type)) {
// Stack: each row contributes (amp+1) levels
combined.put(type, combined.get(type) + amp + 1); combined.put(type, combined.get(type) + amp + 1);
} else { } else {
combined.put(type, amp); combined.put(type, amp);
@@ -166,19 +183,112 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
return combined; return combined;
} }
private void applyAllEffects() { // Merges another beacon's effects into this map. Same stacking rule.
private static void mergeInto(Map<StatusEffect, Integer> target, Map<StatusEffect, Integer> source) {
for (Map.Entry<StatusEffect, Integer> 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<SuperBeaconBlockEntity> findNearbyActiveBeacons() {
List<SuperBeaconBlockEntity> 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<StatusEffect, Integer> computeCoveringEffects(PlayerEntity player) {
Map<StatusEffect, Integer> 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<SuperBeaconBlockEntity> 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; if (world == null) return;
Map<StatusEffect, Integer> combined = collectCombinedEffects(); double myRadius = getEffectRadius();
if (combined.isEmpty()) return; if (myRadius <= 0) return;
double radius = getEffectRadius(); List<SuperBeaconBlockEntity> nearbyBeacons = findNearbyActiveBeacons();
Box box = new Box(pos).expand(radius);
List<PlayerEntity> players = world.getEntitiesByClass(PlayerEntity.class, box, p -> true); // We can only be coordinator for players WE cover.
Box searchBox = new Box(pos).expand(myRadius);
List<PlayerEntity> candidatePlayers = world.getEntitiesByClass(PlayerEntity.class, searchBox, p -> true);
for (PlayerEntity player : candidatePlayers) {
if (!isCoordinatorFor(player, nearbyBeacons)) continue;
Map<StatusEffect, Integer> 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<StatusEffect, Integer> e : combined.entrySet()) { for (Map.Entry<StatusEffect, Integer> e : combined.entrySet()) {
StatusEffect type = e.getKey(); 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)) { if (PERSISTENT_EFFECTS.contains(type)) {
applyPersistent(player, type, amp); 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 // Persistent tracking: re-apply when amplifier changes.
// so we re-apply when amplifier changes (row added/removed) // 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) { private void applyPersistent(PlayerEntity player, StatusEffect type, int amp) {
UUID uuid = player.getUuid(); UUID uuid = player.getUuid();
Map<Integer, Set<StatusEffect>> bucket = persistentTracking.computeIfAbsent(uuid, k -> new HashMap<>()); Map<Integer, Set<StatusEffect>> bucket = persistentTracking.computeIfAbsent(uuid, k -> new HashMap<>());
// Use single bucket key 0 to track all persistent effects per player
Set<StatusEffect> tracked = bucket.computeIfAbsent(0, k -> new HashSet<>()); Set<StatusEffect> tracked = bucket.computeIfAbsent(0, k -> new HashSet<>());
StatusEffectInstance current = player.getStatusEffect(type); 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.removeStatusEffect(type);
player.addStatusEffect(new StatusEffectInstance( player.addStatusEffect(new StatusEffectInstance(
type, Integer.MAX_VALUE, amp, true, true, true)); type, 200, amp, true, true, true));
tracked.add(type);
} }
tracked.add(type);
GLOBAL_PERSISTENT_TRACKING.computeIfAbsent(uuid, k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(type);
} }
private void checkPersistentRangeAll() { private void checkPersistentRangeAll() {
if (world == null) return; if (world == null) return;
double radius = getEffectRadius(); List<SuperBeaconBlockEntity> nearbyBeacons = findNearbyActiveBeacons();
Vec3d center = new Vec3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
Map<StatusEffect, Integer> combined = collectCombinedEffects();
for (Map.Entry<UUID, Map<Integer, Set<StatusEffect>>> entry : persistentTracking.entrySet()) { for (Map.Entry<UUID, Map<Integer, Set<StatusEffect>>> entry : persistentTracking.entrySet()) {
Set<StatusEffect> tracked = entry.getValue().get(0); Set<StatusEffect> tracked = entry.getValue().get(0);
@@ -219,14 +334,21 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
PlayerEntity player = world.getPlayerByUuid(entry.getKey()); PlayerEntity player = world.getPlayerByUuid(entry.getKey());
if (player == null) continue; 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<StatusEffect, Integer> 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<StatusEffect> it = tracked.iterator(); Iterator<StatusEffect> it = tracked.iterator();
while (it.hasNext()) { while (it.hasNext()) {
StatusEffect e = it.next(); StatusEffect e = it.next();
if (outOfRange || !combined.containsKey(e)) { if (!combined.containsKey(e)) {
player.removeStatusEffect(e); player.removeStatusEffect(e);
it.remove(); it.remove();
} }
@@ -289,27 +411,24 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
} }
public void toggleRow(int row) { public void toggleRow(int row) {
Szar.LOGGER.info("[Szar] toggleRow called: row={}, level={}, active={}", row, beaconLevel, rowActive[row]); if (row < 0 || row >= 4) {return; }
if (row < 0 || row >= 4) { Szar.LOGGER.warn("[Szar] row out of bounds"); return; } if (beaconLevel < row + 1) { return; }
if (beaconLevel < row + 1) { Szar.LOGGER.warn("[Szar] row locked, need level {}", row + 1); return; }
if (rowActive[row]) { if (rowActive[row]) {
deactivateRow(row); deactivateRow(row);
Szar.LOGGER.info("[Szar] deactivated row {}", row);
} else { } else {
ItemStack effectItem = getStack(row * 2); ItemStack effectItem = getStack(row * 2);
ItemStack fuelItem = getStack(row * 2 + 1); ItemStack fuelItem = getStack(row * 2 + 1);
if (effectItem.isEmpty()) { Szar.LOGGER.warn("[Szar] effect slot empty"); return; } if (effectItem.isEmpty()) { return; }
if (fuelItem.isEmpty()) { Szar.LOGGER.warn("[Szar] fuel slot empty"); return; } if (fuelItem.isEmpty()) { return; }
if (!isValidEffectItem(effectItem)) { Szar.LOGGER.warn("[Szar] effect item invalid: {}", effectItem); return; } if (!isValidEffectItem(effectItem)) { return; }
if (!isValidFuel(fuelItem)) { Szar.LOGGER.warn("[Szar] fuel item invalid: {}", fuelItem); return; } if (!isValidFuel(fuelItem)) { return; }
fuelItem.decrement(1); fuelItem.decrement(1);
if (fuelItem.isEmpty()) setStack(row * 2 + 1, ItemStack.EMPTY); if (fuelItem.isEmpty()) setStack(row * 2 + 1, ItemStack.EMPTY);
rowActive[row] = true; rowActive[row] = true;
fuelTimers[row] = 0; fuelTimers[row] = 0;
markDirty(); markDirty();
Szar.LOGGER.info("[Szar] activated row {}", row);
} }
} }
@@ -320,6 +439,28 @@ public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, Ex
markDirty(); 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<UUID, Map<Integer, Set<StatusEffect>>> entry : persistentTracking.entrySet()) {
Set<StatusEffect> 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 int size() { return 8; }
@Override public boolean isEmpty() { return inventory.stream().allMatch(ItemStack::isEmpty); } @Override public boolean isEmpty() { return inventory.stream().allMatch(ItemStack::isEmpty); }
@Override public ItemStack getStack(int slot) { return inventory.get(slot); } @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) { public void writeScreenOpeningData(ServerPlayerEntity player, PacketByteBuf buf) {
buf.writeBlockPos(pos); 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);
}
} }

View File

@@ -1506,24 +1506,48 @@ public class Szar implements ModInitializer {
BlockPos pos = buf.readBlockPos(); BlockPos pos = buf.readBlockPos();
int row = buf.readInt(); int row = buf.readInt();
server.execute(() -> { server.execute(() -> {
LOGGER.info("[Szar] activate_row: pos={}, row={}", pos, row);
if (player.getServerWorld().getBlockEntity(pos) instanceof SuperBeaconBlockEntity be) { if (player.getServerWorld().getBlockEntity(pos) instanceof SuperBeaconBlockEntity be) {
if (be.canPlayerUse(player)) { if (be.canPlayerUse(player)) {
boolean wasActive = be.isRowActive(row);
be.toggleRow(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<Map.Entry<UUID, Set<StatusEffect>>> it =
SuperBeaconBlockEntity.GLOBAL_PERSISTENT_TRACKING.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<UUID, Set<StatusEffect>> entry = it.next();
UUID uuid = entry.getKey();
Set<StatusEffect> 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<StatusEffect, Integer> covering = SuperBeaconBlockEntity.computeCoveringEffects(player);
Iterator<StatusEffect> 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( public static final Block SUPER_BEACON_BLOCK = new SuperBeaconBlock(
FabricBlockSettings.copyOf(Blocks.BEACON).luminance(15) FabricBlockSettings.copyOf(Blocks.BEACON).luminance(15)
@@ -1725,11 +1749,10 @@ public class Szar implements ModInitializer {
.hunger(20) .hunger(20)
.alwaysEdible() .alwaysEdible()
.saturationModifier(20F) .saturationModifier(20F)
.statusEffect(new StatusEffectInstance(StatusEffects.REGENERATION,60*20, 255 ), 1F) .statusEffect(new StatusEffectInstance(StatusEffects.REGENERATION,60*20, 2 ), 1F)
.statusEffect(new StatusEffectInstance(StatusEffects.HEALTH_BOOST,2*60*20, 4 ), 1F)
.statusEffect(new StatusEffectInstance(StatusEffects.RESISTANCE,5*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.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) .statusEffect(new StatusEffectInstance(StatusEffects.STRENGTH,5*60*20, 2), 1F)
.build() .build()
).rarity(Rarity.EPIC)) ).rarity(Rarity.EPIC))

View File

@@ -7,8 +7,8 @@
], ],
"placement": { "placement": {
"type": "minecraft:random_spread", "type": "minecraft:random_spread",
"spacing": 10, "spacing": 3,
"separation": 1, "separation": 0,
"salt": 398826349 "salt": 398826349
} }
} }