|
|
|
|
@@ -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<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<>();
|
|
|
|
|
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<ItemStack> 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<StatusEffect, Integer> collectCombinedEffects() {
|
|
|
|
|
Map<StatusEffect, Integer> 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<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;
|
|
|
|
|
Map<StatusEffect, Integer> combined = collectCombinedEffects();
|
|
|
|
|
if (combined.isEmpty()) return;
|
|
|
|
|
double myRadius = getEffectRadius();
|
|
|
|
|
if (myRadius <= 0) return;
|
|
|
|
|
|
|
|
|
|
double radius = getEffectRadius();
|
|
|
|
|
Box box = new Box(pos).expand(radius);
|
|
|
|
|
List<PlayerEntity> players = world.getEntitiesByClass(PlayerEntity.class, box, p -> true);
|
|
|
|
|
List<SuperBeaconBlockEntity> nearbyBeacons = findNearbyActiveBeacons();
|
|
|
|
|
|
|
|
|
|
// 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()) {
|
|
|
|
|
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<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<>());
|
|
|
|
|
|
|
|
|
|
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<StatusEffect, Integer> combined = collectCombinedEffects();
|
|
|
|
|
List<SuperBeaconBlockEntity> nearbyBeacons = findNearbyActiveBeacons();
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<UUID, Map<Integer, Set<StatusEffect>>> entry : persistentTracking.entrySet()) {
|
|
|
|
|
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());
|
|
|
|
|
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();
|
|
|
|
|
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<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 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);
|
|
|
|
|
}
|
|
|
|
|
}
|