backrooms 3

This commit is contained in:
2026-03-19 18:11:32 +01:00
parent 4f455ead5f
commit 301d87bb5e
19 changed files with 876 additions and 68 deletions

View File

@@ -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.3.18.3
mod_version=26.3.19
maven_group=dev.tggamesyt
archives_base_name=szar
# Dependencies

View File

@@ -0,0 +1,108 @@
package dev.tggamesyt.szar;
import net.minecraft.advancement.criterion.Criteria;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.effect.StatusEffectCategory;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemUsage;
import net.minecraft.item.Items;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.sound.SoundEvent;
import net.minecraft.sound.SoundEvents;
import net.minecraft.stat.Stats;
import net.minecraft.util.Hand;
import net.minecraft.util.TypedActionResult;
import net.minecraft.util.UseAction;
import net.minecraft.world.World;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class AlmondWaterItem extends Item {
public AlmondWaterItem(Settings settings) {
super(settings);
}
@Override
public ItemStack finishUsing(ItemStack stack, World world, LivingEntity user) {
PlayerEntity player = user instanceof PlayerEntity ? (PlayerEntity) user : null;
// Advancement trigger
if (player instanceof ServerPlayerEntity serverPlayer) {
Criteria.CONSUME_ITEM.trigger(serverPlayer, stack);
}
if (!world.isClient) {
// Collect harmful effects first
List<StatusEffectInstance> toRemove = new ArrayList<>();
for (StatusEffectInstance effect : user.getStatusEffects()) {
if (effect.getEffectType().getCategory() == StatusEffectCategory.HARMFUL) {
toRemove.add(effect);
}
}
// Then remove them
for (StatusEffectInstance effect : toRemove) {
user.removeStatusEffect(effect.getEffectType());
}
// Hunger + saturation
if (player != null) {
player.getHungerManager().add(4, 0.6F);
}
}
// Stats + stack handling
if (player != null) {
player.incrementStat(Stats.USED.getOrCreateStat(this));
if (!player.getAbilities().creativeMode) {
stack.decrement(1);
}
}
// Return glass bottle
if (player == null || !player.getAbilities().creativeMode) {
if (stack.isEmpty()) {
return new ItemStack(Items.GLASS_BOTTLE);
}
if (player != null) {
player.getInventory().insertStack(new ItemStack(Items.GLASS_BOTTLE));
}
}
user.emitGameEvent(net.minecraft.world.event.GameEvent.DRINK);
return stack;
}
@Override
public int getMaxUseTime(ItemStack stack) {
return 32;
}
@Override
public UseAction getUseAction(ItemStack stack) {
return UseAction.DRINK;
}
@Override
public SoundEvent getDrinkSound() {
return SoundEvents.ENTITY_GENERIC_DRINK;
}
@Override
public SoundEvent getEatSound() {
return SoundEvents.ENTITY_GENERIC_DRINK;
}
@Override
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
return ItemUsage.consumeHeldItem(world, user, hand);
}
}

View File

@@ -2,6 +2,7 @@ package dev.tggamesyt.szar;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.block.entity.BarrelBlockEntity;
import net.minecraft.entity.ItemEntity;
import net.minecraft.entity.player.HungerManager;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
@@ -22,6 +23,16 @@ public class BackroomsBarrelManager {
private static final Map<UUID, Set<BlockPos>> trackerBarrels = new HashMap<>();
private static final Map<UUID, Set<BlockPos>> foodBarrels = new HashMap<>();
// Cooldown: world time when food was last taken from a barrel per player
private static final Map<UUID, Long> foodTakenCooldown = new HashMap<>();
// How far from ANY player a barrel must be to get cleared
private static final int CLEAR_RANGE = 32;
// Cooldown in ticks (20 ticks/sec * 60 sec = 1200)
private static final long FOOD_COOLDOWN_TICKS = 1200;
// Range to check for dropped food items on ground
private static final int DROPPED_FOOD_RANGE = 8;
public static void register() {
ServerTickEvents.END_SERVER_TICK.register(BackroomsBarrelManager::tick);
}
@@ -30,7 +41,13 @@ public class BackroomsBarrelManager {
ServerWorld backrooms = server.getWorld(Szar.BACKROOMS_KEY);
if (backrooms == null) return;
for (ServerPlayerEntity player : backrooms.getPlayers()) {
List<ServerPlayerEntity> players = backrooms.getPlayers();
// --- Clear barrels too far from any player ---
clearDistantBarrels(backrooms, players, trackerBarrels, Szar.TRACKER_BLOCK_ITEM.asItem());
clearDistantBarrels(backrooms, players, foodBarrels, null);
for (ServerPlayerEntity player : players) {
UUID uuid = player.getUuid();
// --- Walk tracking ---
@@ -52,28 +69,71 @@ public class BackroomsBarrelManager {
// --- Check if tracker barrels need clearing ---
if (trackerBarrels.containsKey(uuid)) {
checkAndClearTrackerBarrels(backrooms, uuid);
checkAndClearBarrels(backrooms, uuid, trackerBarrels,
Szar.TRACKER_BLOCK_ITEM.asItem());
}
// --- Check if food barrels need clearing ---
if (foodBarrels.containsKey(uuid)) {
checkAndClearFoodBarrels(backrooms, uuid);
boolean anyTaken = checkFoodBarrelsTaken(backrooms, uuid);
if (anyTaken) {
// Clear all food from tracked barrels and start cooldown
clearAllFoodBarrels(backrooms, uuid);
foodTakenCooldown.put(uuid, backrooms.getTime());
foodBarrels.remove(uuid);
}
}
// --- Hunger check (every 20 ticks) ---
if (backrooms.getTime() % 20 == 0) {
HungerManager hunger = player.getHungerManager();
if (hunger.getFoodLevel() <= 10 && !hasAnyFood(player)) {
if (!foodBarrels.containsKey(uuid)) {
List<BarrelBlockEntity> nearby = getNearbyBarrels(backrooms, player, 16);
if (!nearby.isEmpty()) {
Set<BlockPos> positions = new HashSet<>();
for (BarrelBlockEntity barrel : nearby) {
placeItemInBarrel(barrel, new ItemStack(Szar.CAN_OF_BEANS));
positions.add(barrel.getPos().toImmutable());
}
foodBarrels.put(uuid, positions);
boolean isHungry = hunger.getFoodLevel() <= 10;
boolean hasFood = hasAnyFood(player);
boolean hasFoodOnGround = hasFoodDroppedNearby(backrooms, player);
long lastTaken = foodTakenCooldown.getOrDefault(uuid, -FOOD_COOLDOWN_TICKS);
boolean onCooldown = (backrooms.getTime() - lastTaken) < FOOD_COOLDOWN_TICKS;
if (isHungry && !hasFood && !hasFoodOnGround && !onCooldown) {
// Ensure ALL nearby barrels have food, not just new ones
List<BarrelBlockEntity> nearby = getNearbyBarrels(backrooms, player, 16);
Set<BlockPos> positions = foodBarrels.getOrDefault(uuid, new HashSet<>());
for (BarrelBlockEntity barrel : nearby) {
BlockPos bpos = barrel.getPos().toImmutable();
// If this barrel doesn't have food yet, add it
if (!barrelHasItem(barrel, Szar.CAN_OF_BEANS.asItem())
&& !barrelHasItem(barrel, Szar.ALMOND_WATER.asItem())) {
Item foodItem = backrooms.random.nextBoolean()
? Szar.CAN_OF_BEANS.asItem()
: Szar.ALMOND_WATER.asItem();
placeItemInBarrel(barrel, new ItemStack(foodItem));
}
positions.add(bpos);
}
// Also clear positions that are now out of range
positions.removeIf(bpos -> {
double d = player.squaredDistanceTo(
bpos.getX(), bpos.getY(), bpos.getZ());
if (d > 16 * 16) {
if (backrooms.getBlockEntity(bpos)
instanceof BarrelBlockEntity b) {
removeItemFromBarrel(b, Szar.CAN_OF_BEANS.asItem());
removeItemFromBarrel(b, Szar.ALMOND_WATER.asItem());
}
return true;
}
return false;
});
if (!positions.isEmpty()) {
foodBarrels.put(uuid, positions);
}
} else if (onCooldown || hasFood || hasFoodOnGround) {
// If player now has food or is on cooldown, clear all food barrels
if (foodBarrels.containsKey(uuid)) {
clearAllFoodBarrels(backrooms, uuid);
foodBarrels.remove(uuid);
}
}
}
@@ -97,47 +157,98 @@ public class BackroomsBarrelManager {
// Clean up data for players no longer in backrooms
Set<UUID> activePlayers = new HashSet<>();
for (ServerPlayerEntity p : backrooms.getPlayers()) activePlayers.add(p.getUuid());
for (ServerPlayerEntity p : players) activePlayers.add(p.getUuid());
lastX.keySet().retainAll(activePlayers);
lastZ.keySet().retainAll(activePlayers);
walkAccumulator.keySet().retainAll(activePlayers);
walkThreshold.keySet().retainAll(activePlayers);
foodBarrels.keySet().retainAll(activePlayers);
trackerBarrels.keySet().retainAll(activePlayers);
foodTakenCooldown.keySet().retainAll(activePlayers);
}
private static void checkAndClearTrackerBarrels(ServerWorld world, UUID uuid) {
Set<BlockPos> positions = trackerBarrels.get(uuid);
if (positions == null) return;
boolean anyTaken = false;
private static boolean checkFoodBarrelsTaken(ServerWorld world, UUID uuid) {
Set<BlockPos> positions = foodBarrels.get(uuid);
if (positions == null) return false;
for (BlockPos pos : positions) {
if (world.getBlockEntity(pos) instanceof BarrelBlockEntity barrel) {
if (!barrelHasItem(barrel, Szar.TRACKER_BLOCK_ITEM.asItem())) {
anyTaken = true;
break;
if (!barrelHasItem(barrel, Szar.CAN_OF_BEANS.asItem())
&& !barrelHasItem(barrel, Szar.ALMOND_WATER.asItem())) {
return true;
}
}
}
if (anyTaken) {
for (BlockPos pos : positions) {
if (world.getBlockEntity(pos) instanceof BarrelBlockEntity barrel) {
removeItemFromBarrel(barrel, Szar.TRACKER_BLOCK_ITEM.asItem());
}
}
trackerBarrels.remove(uuid);
}
return false;
}
private static void checkAndClearFoodBarrels(ServerWorld world, UUID uuid) {
private static void clearAllFoodBarrels(ServerWorld world, UUID uuid) {
Set<BlockPos> positions = foodBarrels.get(uuid);
if (positions == null) return;
for (BlockPos pos : positions) {
if (world.getBlockEntity(pos) instanceof BarrelBlockEntity barrel) {
removeItemFromBarrel(barrel, Szar.CAN_OF_BEANS.asItem());
removeItemFromBarrel(barrel, Szar.ALMOND_WATER.asItem());
}
}
}
private static boolean hasFoodDroppedNearby(ServerWorld world, ServerPlayerEntity player) {
Box box = player.getBoundingBox().expand(DROPPED_FOOD_RANGE);
List<ItemEntity> items = world.getEntitiesByClass(ItemEntity.class, box, e -> {
ItemStack stack = e.getStack();
return stack.isFood()
|| stack.isOf(Szar.CAN_OF_BEANS)
|| stack.isOf(Szar.ALMOND_WATER);
});
return !items.isEmpty();
}
private static void clearDistantBarrels(ServerWorld world,
List<ServerPlayerEntity> players,
Map<UUID, Set<BlockPos>> barrelMap,
Item item) {
Iterator<Map.Entry<UUID, Set<BlockPos>>> iter = barrelMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<UUID, Set<BlockPos>> entry = iter.next();
Set<BlockPos> positions = entry.getValue();
boolean allDistant = true;
for (BlockPos pos : positions) {
for (ServerPlayerEntity player : players) {
if (player.squaredDistanceTo(pos.getX(), pos.getY(), pos.getZ())
<= CLEAR_RANGE * CLEAR_RANGE) {
allDistant = false;
break;
}
}
if (!allDistant) break;
}
if (allDistant) {
for (BlockPos pos : positions) {
if (world.getBlockEntity(pos) instanceof BarrelBlockEntity barrel) {
if (item != null) {
removeItemFromBarrel(barrel, item);
} else {
removeItemFromBarrel(barrel, Szar.CAN_OF_BEANS.asItem());
removeItemFromBarrel(barrel, Szar.ALMOND_WATER.asItem());
}
}
}
iter.remove();
}
}
}
private static void checkAndClearBarrels(ServerWorld world, UUID uuid,
Map<UUID, Set<BlockPos>> barrelMap, Item item) {
Set<BlockPos> positions = barrelMap.get(uuid);
if (positions == null) return;
boolean anyTaken = false;
for (BlockPos pos : positions) {
if (world.getBlockEntity(pos) instanceof BarrelBlockEntity barrel) {
if (!barrelHasItem(barrel, Szar.CAN_OF_BEANS.asItem())) {
if (!barrelHasItem(barrel, item)) {
anyTaken = true;
break;
}
@@ -147,10 +258,10 @@ public class BackroomsBarrelManager {
if (anyTaken) {
for (BlockPos pos : positions) {
if (world.getBlockEntity(pos) instanceof BarrelBlockEntity barrel) {
removeItemFromBarrel(barrel, Szar.CAN_OF_BEANS.asItem());
removeItemFromBarrel(barrel, item);
}
}
foodBarrels.remove(uuid);
barrelMap.remove(uuid);
}
}
@@ -175,7 +286,8 @@ public class BackroomsBarrelManager {
for (int i = 0; i < player.getInventory().size(); i++) {
ItemStack stack = player.getInventory().getStack(i);
if (!stack.isEmpty() && (stack.isFood()
|| stack.isOf(Szar.CAN_OF_BEANS))) {
|| stack.isOf(Szar.CAN_OF_BEANS)
|| stack.isOf(Szar.ALMOND_WATER))) {
return true;
}
}
@@ -183,13 +295,14 @@ public class BackroomsBarrelManager {
}
private static void placeItemInBarrel(BarrelBlockEntity barrel, ItemStack item) {
List<Integer> emptySlots = new ArrayList<>();
for (int i = 0; i < barrel.size(); i++) {
if (barrel.getStack(i).isEmpty()) {
barrel.setStack(i, item.copy());
barrel.markDirty();
return;
}
if (barrel.getStack(i).isEmpty()) emptySlots.add(i);
}
if (emptySlots.isEmpty()) return;
int slot = emptySlots.get((int)(Math.random() * emptySlots.size()));
barrel.setStack(slot, item.copy());
barrel.markDirty();
}
private static List<BarrelBlockEntity> getNearbyBarrels(ServerWorld world,
@@ -197,10 +310,8 @@ public class BackroomsBarrelManager {
int radius) {
List<BarrelBlockEntity> result = new ArrayList<>();
Box box = player.getBoundingBox().expand(radius);
BlockPos min = BlockPos.ofFloored(box.minX, box.minY, box.minZ);
BlockPos max = BlockPos.ofFloored(box.maxX, box.maxY, box.maxZ);
for (BlockPos pos : BlockPos.iterate(min, max)) {
if (world.getBlockEntity(pos) instanceof BarrelBlockEntity barrel) {
result.add(barrel);

View File

@@ -87,11 +87,28 @@ public class BackroomsChunkGenerator extends ChunkGenerator {
Szar.PLASTIC.getDefaultState(), false);
}
// Ceiling
boolean isGlowstone = isGlowstonePos(worldX, worldZ);
BlockState ceilingBlock = isGlowstone
? Blocks.GLOWSTONE.getDefaultState()
: Szar.CEILING.getDefaultState();
BlockState ceilingBlock;
if (isGlowstone) {
long lightRoll = hash(worldX * 53 + 7, worldZ * 47 + 13);
int roll = (int)(Math.abs(lightRoll) % 100);
if (roll < 95) {
// 95% ON
ceilingBlock = Szar.BACKROOMS_LIGHT.getDefaultState()
.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.ON);
} else if (roll < 98) {
// 3% FLICKERING_ON
ceilingBlock = Szar.BACKROOMS_LIGHT.getDefaultState()
.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.FLICKERING_ON);
} else {
// 2% missing — ceiling block, no light
ceilingBlock = Szar.CEILING.getDefaultState();
}
} else {
ceilingBlock = Szar.CEILING.getDefaultState();
}
chunk.setBlockState(new BlockPos(lx, CEILING_Y, lz), ceilingBlock, false);
// Above ceiling — solid wall block fill so there's no void above
@@ -106,10 +123,17 @@ public class BackroomsChunkGenerator extends ChunkGenerator {
Blocks.AIR.getDefaultState(), false);
}
// 1 in 40 chance of a barrel on the floor
// With this — 1 per ~40x40 block area:
long barrelHash = hash(worldX * 31 + 17, worldZ * 29 + 11);
if ((barrelHash % 1600) == 0) {
int cellX = Math.floorDiv(worldX, 40);
int cellZ = Math.floorDiv(worldZ, 40);
long cellHash = hash(cellX * 31 + 17, cellZ * 29 + 11);
int barrelLocalX = (int)(cellHash & 0x1F) % 40; // 0-39
int barrelLocalZ = (int)((cellHash >> 8) & 0x1F) % 40; // 0-39
int cellStartX = cellX * 40;
int cellStartZ = cellZ * 40;
if (worldX == cellStartX + barrelLocalX && worldZ == cellStartZ + barrelLocalZ
&& isOpenSpace(worldX, worldZ)) {
chunk.setBlockState(new BlockPos(lx, FLOOR_Y + 1, lz),
Blocks.BARREL.getDefaultState(), false);
}
@@ -262,6 +286,19 @@ public class BackroomsChunkGenerator extends ChunkGenerator {
}
}
}
// Inside the lx/lz loop in generateFeatures, after the wall block section:
mutable.set(chunkX + lx, 9, chunkZ + lz); // CEILING_Y = 9
BlockState lightState = world.getBlockState(mutable);
if (lightState.getBlock() instanceof BackroomsLightBlock) {
if (world.getBlockEntity(mutable) == null) {
world.setBlockState(mutable, lightState, Block.NOTIFY_ALL);
}
if (world.getBlockEntity(mutable) instanceof BackroomsLightBlockEntity light) {
light.flickerOffset = (int)(Math.abs(hash(chunkX + lx, chunkZ + lz)) % 100);
light.flickerTimer = light.flickerOffset; // stagger initial timers
light.markDirty();
}
}
}
}

View File

@@ -0,0 +1,67 @@
package dev.tggamesyt.szar;
import net.minecraft.block.*;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.state.StateManager;
import net.minecraft.state.property.EnumProperty;
import net.minecraft.util.StringIdentifiable;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.BlockView;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
public class BackroomsLightBlock extends BlockWithEntity {
public enum LightState implements StringIdentifiable {
ON, OFF, FLICKERING_ON, FLICKERING_OFF;
@Override
public String asString() {
return name().toLowerCase();
}
}
public static final EnumProperty<LightState> LIGHT_STATE =
EnumProperty.of("light_state", LightState.class);
public BackroomsLightBlock(Settings settings) {
super(settings);
setDefaultState(getStateManager().getDefaultState()
.with(LIGHT_STATE, LightState.ON));
}
@Override
protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
builder.add(LIGHT_STATE);
}
@Override
public BlockRenderType getRenderType(BlockState state) {
return BlockRenderType.MODEL;
}
@Override
public @Nullable BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new BackroomsLightBlockEntity(pos, state);
}
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(
World world, BlockState state, BlockEntityType<T> type) {
if (world.isClient) return null;
return type == Szar.BACKROOMS_LIGHT_ENTITY
? (w, pos, s, be) -> BackroomsLightBlockEntity.tick(
w, pos, s, (BackroomsLightBlockEntity) be)
: null;
}
// Light level based on state
public static int getLightLevel(BlockState state) {
return switch (state.get(LIGHT_STATE)) {
case ON, FLICKERING_ON -> 15;
case OFF, FLICKERING_OFF -> 0;
};
}
}

View File

@@ -0,0 +1,60 @@
package dev.tggamesyt.szar;
import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.listener.ClientPlayPacketListener;
import net.minecraft.network.packet.Packet;
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
public class BackroomsLightBlockEntity extends BlockEntity {
// Random offset so each light flickers at different times
public int flickerOffset = 0;
// How many ticks until next state toggle during flicker
public int flickerTimer = 0;
private boolean initialized = false;
public BackroomsLightBlockEntity(BlockPos pos, BlockState state) {
super(Szar.BACKROOMS_LIGHT_ENTITY, pos, state);
}
public static void tick(World world, BlockPos pos, BlockState state,
BackroomsLightBlockEntity entity) {
if (!entity.initialized) {
entity.flickerOffset = world.random.nextInt(100);
entity.initialized = true;
entity.markDirty();
}
BackroomsLightManager.tickLight(world, pos, state, entity);
}
@Override
public void writeNbt(NbtCompound nbt) {
super.writeNbt(nbt);
nbt.putInt("FlickerOffset", flickerOffset);
nbt.putInt("FlickerTimer", flickerTimer);
nbt.putBoolean("Initialized", initialized);
}
@Override
public void readNbt(NbtCompound nbt) {
super.readNbt(nbt);
flickerOffset = nbt.getInt("FlickerOffset");
flickerTimer = nbt.getInt("FlickerTimer");
initialized = nbt.getBoolean("Initialized");
}
@Override
public NbtCompound toInitialChunkDataNbt() {
return createNbt();
}
@Override
public Packet<ClientPlayPacketListener> toUpdatePacket() {
return BlockEntityUpdateS2CPacket.create(this);
}
}

View File

@@ -0,0 +1,213 @@
package dev.tggamesyt.szar;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.block.BlockState;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
public class BackroomsLightManager {
// Global event state
public enum GlobalEvent { NONE, FLICKER, BLACKOUT }
public static GlobalEvent currentEvent = GlobalEvent.NONE;
public static int eventTimer = 0; // ticks remaining in current event
public static int cooldownTimer = 0; // ticks until next event check
// Flicker event duration: 3-8 seconds
private static final int FLICKER_DURATION_MIN = 60;
private static final int FLICKER_DURATION_MAX = 160;
// Blackout duration: 50-100 seconds
private static final int BLACKOUT_MIN = 1000;
private static final int BLACKOUT_MAX = 2000;
// Check for new event every ~3 minutes
private static final int EVENT_COOLDOWN = 3600;
public static void register() {
ServerTickEvents.END_SERVER_TICK.register(BackroomsLightManager::tick);
}
private static void tick(MinecraftServer server) {
ServerWorld backrooms = server.getWorld(Szar.BACKROOMS_KEY);
if (backrooms == null) return;
// Handle event timers
if (currentEvent != GlobalEvent.NONE) {
eventTimer--;
if (eventTimer <= 0) {
endEvent(backrooms);
}
} else {
cooldownTimer--;
if (cooldownTimer <= 0) {
// Roll for new event
int roll = backrooms.random.nextInt(100);
if (roll < 30) {
startBlackout(backrooms);
} else if (roll < 63) { // 30% blackout + 33% flicker
startFlicker(backrooms);
} else {
// No event — reset cooldown
cooldownTimer = EVENT_COOLDOWN;
}
}
}
}
private static void startFlicker(ServerWorld world) {
currentEvent = GlobalEvent.FLICKER;
eventTimer = FLICKER_DURATION_MIN + world.random.nextInt(
FLICKER_DURATION_MAX - FLICKER_DURATION_MIN);
cooldownTimer = EVENT_COOLDOWN;
}
private static void startBlackout(ServerWorld world) {
currentEvent = GlobalEvent.BLACKOUT;
eventTimer = BLACKOUT_MIN + world.random.nextInt(BLACKOUT_MAX - BLACKOUT_MIN);
cooldownTimer = EVENT_COOLDOWN;
// Immediately turn off all ON lights in loaded chunks
setAllLightsOff(world);
}
private static void endEvent(ServerWorld world) {
if (currentEvent == GlobalEvent.BLACKOUT) {
// Restore all lights
setAllLightsOn(world);
}
currentEvent = GlobalEvent.NONE;
eventTimer = 0;
cooldownTimer = EVENT_COOLDOWN;
}
private static void setAllLightsOff(ServerWorld world) {
forEachLight(world, (pos, state) -> {
BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE);
if (ls == BackroomsLightBlock.LightState.ON) {
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.OFF));
} else if (ls == BackroomsLightBlock.LightState.FLICKERING_ON) {
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.FLICKERING_OFF));
}
});
}
private static void setAllLightsOn(ServerWorld world) {
forEachLight(world, (pos, state) -> {
BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE);
if (ls == BackroomsLightBlock.LightState.OFF) {
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.ON));
} else if (ls == BackroomsLightBlock.LightState.FLICKERING_OFF) {
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.FLICKERING_ON));
}
});
}
private static void forEachLight(ServerWorld world, java.util.function.BiConsumer<BlockPos, BlockState> consumer) {
for (net.minecraft.world.chunk.WorldChunk chunk : getLoadedChunks(world)) {
BlockPos.Mutable mutable = new BlockPos.Mutable();
int cx = chunk.getPos().getStartX();
int cz = chunk.getPos().getStartZ();
for (int lx = 0; lx < 16; lx++) {
for (int lz = 0; lz < 16; lz++) {
// Ceiling Y in backrooms is 9
mutable.set(cx + lx, 9, cz + lz);
BlockState state = world.getBlockState(mutable);
if (state.getBlock() instanceof BackroomsLightBlock) {
consumer.accept(mutable.toImmutable(), state);
}
}
}
}
}
private static java.util.List<net.minecraft.world.chunk.WorldChunk> getLoadedChunks(ServerWorld world) {
java.util.List<net.minecraft.world.chunk.WorldChunk> chunks = new java.util.ArrayList<>();
// Iterate over all players and collect chunks around them
for (net.minecraft.server.network.ServerPlayerEntity player : world.getPlayers()) {
int playerChunkX = (int) player.getX() >> 4;
int playerChunkZ = (int) player.getZ() >> 4;
int viewDistance = world.getServer().getPlayerManager().getViewDistance();
for (int cx = playerChunkX - viewDistance; cx <= playerChunkX + viewDistance; cx++) {
for (int cz = playerChunkZ - viewDistance; cz <= playerChunkZ + viewDistance; cz++) {
if (world.getChunkManager().isChunkLoaded(cx, cz)) {
net.minecraft.world.chunk.WorldChunk chunk = world.getChunk(cx, cz);
if (!chunks.contains(chunk)) {
chunks.add(chunk);
}
}
}
}
}
return chunks;
}
// Called per-light from BackroomsLightBlockEntity.tick
public static void tickLight(World world, BlockPos pos, BlockState state,
BackroomsLightBlockEntity entity) {
BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE);
// During blackout, lights are already set off — don't touch them
if (currentEvent == GlobalEvent.BLACKOUT) return;
// Only flickering lights and lights during flicker events need ticking
boolean isFlickering = ls == BackroomsLightBlock.LightState.FLICKERING_ON
|| ls == BackroomsLightBlock.LightState.FLICKERING_OFF;
boolean inFlickerEvent = currentEvent == GlobalEvent.FLICKER;
if (!isFlickering && !inFlickerEvent) return;
// Decrement timer
entity.flickerTimer--;
if (entity.flickerTimer > 0) return;
// Toggle state and set new random timer
// Flickering lights: 2-8 ticks per toggle
// Event flicker: same but offset by entity's flickerOffset
int baseTime = 2 + world.random.nextInt(7);
if (inFlickerEvent && !isFlickering) {
// Normal ON light during flicker event — toggle it
if (ls == BackroomsLightBlock.LightState.ON) {
// Apply offset so not all lights flicker simultaneously
if (entity.flickerTimer == 0 && world.getTime() % 3 == entity.flickerOffset % 3) {
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.OFF));
entity.flickerTimer = baseTime;
entity.markDirty();
}
return;
} else if (ls == BackroomsLightBlock.LightState.OFF
&& currentEvent == GlobalEvent.FLICKER) {
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.ON));
entity.flickerTimer = baseTime;
entity.markDirty();
return;
}
}
if (isFlickering) {
BackroomsLightBlock.LightState next =
ls == BackroomsLightBlock.LightState.FLICKERING_ON
? BackroomsLightBlock.LightState.FLICKERING_OFF
: BackroomsLightBlock.LightState.FLICKERING_ON;
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE, next));
entity.flickerTimer = baseTime;
entity.markDirty();
}
}
public static void forceRestoreAllLights(ServerWorld world) {
setAllLightsOn(world);
}
public static void forceBlackout(ServerWorld world) {
setAllLightsOff(world);
}
}

View File

@@ -183,21 +183,17 @@ public class PortalBlock extends Block {
if (owTrackerPos != null && overworld != null) {
if (overworld.getBlockEntity(owTrackerPos) instanceof TrackerBlockEntity owTracker) {
// Resolve to root of the overworld group
TrackerBlockEntity root = owTracker.getRoot(overworld);
root.removePlayer(player.getUuid());
if (!root.hasPlayers()) {
// Collect and clean up all connected trackers
List<BlockPos> allTrackers = new ArrayList<>();
allTrackers.add(root.getPos());
for (BlockPos childPortal : root.getControlledPortals()) {
allTrackers.add(childPortal.up(4));
}
for (BlockPos trackerPos : allTrackers) {
if (overworld.getBlockEntity(trackerPos)
instanceof TrackerBlockEntity te) {
if (overworld.getBlockEntity(trackerPos) instanceof TrackerBlockEntity te) {
TrackerBlock.restoreAndCleanup(overworld, trackerPos, te, server);
}
}
@@ -205,17 +201,95 @@ public class PortalBlock extends Block {
}
}
// Clean up backrooms tracker too
if (!netherTracker.hasPlayers() && backrooms != null) {
TrackerBlock.restoreAndCleanup(backrooms,
netherTracker.getPos(), netherTracker, server);
}
if (overworld == null) return;
player.teleport(overworld, returnX, returnY, returnZ,
// Find a safe landing spot — try entry coords first, then spiral, then spawn
BlockPos safePos = findSafeOverworldSpot(overworld, returnX, returnY, returnZ, player);
player.teleport(overworld,
safePos.getX() + 0.5, safePos.getY(), safePos.getZ() + 0.5,
player.getYaw(), player.getPitch());
}
private BlockPos findSafeOverworldSpot(ServerWorld world, double baseX, double baseY,
double baseZ, ServerPlayerEntity player) {
int bx = (int) baseX;
int by = (int) baseY;
int bz = (int) baseZ;
// Try exact entry position first
BlockPos exact = new BlockPos(bx, by, bz);
if (isSafeOverworld(world, exact)) return exact;
// Try scanning Y up and down from entry Y at exact XZ
for (int dy = 0; dy <= 10; dy++) {
BlockPos up = new BlockPos(bx, by + dy, bz);
if (isSafeOverworld(world, up)) return up;
BlockPos down = new BlockPos(bx, by - dy, bz);
if (isSafeOverworld(world, down)) return down;
}
// Spiral outward in XZ, scan Y at each spot
for (int radius = 1; radius <= 10; radius++) {
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
if (Math.abs(dx) != radius && Math.abs(dz) != radius) continue;
int cx = bx + dx;
int cz = bz + dz;
// Scan Y range at this XZ
for (int dy = 0; dy <= 10; dy++) {
BlockPos up = new BlockPos(cx, by + dy, cz);
if (isSafeOverworld(world, up)) return up;
BlockPos down = new BlockPos(cx, by - dy, cz);
if (isSafeOverworld(world, down)) return down;
}
}
}
}
// Last resort — use player's spawn point
BlockPos spawnPos = player.getSpawnPointPosition();
if (spawnPos != null) {
// Scan Y near spawn to make sure it's safe
for (int dy = 0; dy <= 10; dy++) {
BlockPos sp = new BlockPos(spawnPos.getX(), spawnPos.getY() + dy, spawnPos.getZ());
if (isSafeOverworld(world, sp)) return sp;
}
return spawnPos;
}
// Absolute fallback — world spawn
BlockPos worldSpawn = world.getSpawnPos();
return new BlockPos(worldSpawn.getX(),
world.getTopY(net.minecraft.world.Heightmap.Type.MOTION_BLOCKING_NO_LEAVES,
worldSpawn.getX(), worldSpawn.getZ()),
worldSpawn.getZ());
}
private boolean isSafeOverworld(ServerWorld world, BlockPos feet) {
BlockPos head = feet.up();
BlockPos ground = feet.down();
// Ground must be solid, feet and head must be non-solid and not dangerous
if (world.getBlockState(ground).isSolidBlock(world, ground)
&& !world.getBlockState(feet).isSolidBlock(world, feet)
&& !world.getBlockState(head).isSolidBlock(world, head)) {
// Also check not standing in fire, lava, or cactus
net.minecraft.block.Block feetBlock = world.getBlockState(feet).getBlock();
net.minecraft.block.Block groundBlock = world.getBlockState(ground).getBlock();
if (feetBlock == net.minecraft.block.Blocks.LAVA) return false;
if (feetBlock == net.minecraft.block.Blocks.FIRE) return false;
if (feetBlock == net.minecraft.block.Blocks.CACTUS) return false;
if (groundBlock == net.minecraft.block.Blocks.LAVA) return false;
if (groundBlock instanceof net.minecraft.block.CactusBlock) return false;
return true;
}
return false;
}
// --- Helpers ---
private TrackerBlockEntity findTrackerAbove(World world, BlockPos portalPos) {

View File

@@ -55,6 +55,7 @@ import net.minecraft.registry.tag.BiomeTags;
import net.minecraft.registry.tag.BlockTags;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
@@ -370,6 +371,7 @@ public class Szar implements ModInitializer {
entries.add(Szar.PLASTIC_ITEM);
entries.add(Szar.BEAN);
entries.add(Szar.CAN_OF_BEANS);
entries.add(Szar.ALMOND_WATER);
// crazy weponary
entries.add(Szar.BULLET_ITEM);
entries.add(Szar.AK47);
@@ -970,6 +972,90 @@ public class Szar implements ModInitializer {
return 0;
})
);
dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("backroomlights")
.requires(context -> context.hasPermissionLevel(2))
.then(CommandManager.literal("get")
.executes(context -> {
ServerCommandSource source = context.getSource();
BackroomsLightManager.GlobalEvent event = BackroomsLightManager.currentEvent;
int timer = BackroomsLightManager.eventTimer;
String mode = switch (event) {
case NONE -> "normal";
case FLICKER -> "flickering";
case BLACKOUT -> "blackout";
};
if (event == BackroomsLightManager.GlobalEvent.NONE) {
int cooldown = BackroomsLightManager.cooldownTimer;
source.sendMessage(Text.literal(
"Current mode: §anormal§r — next event check in §e"
+ cooldown + "§r ticks ("
+ (cooldown / 20) + "s)"
));
} else {
source.sendMessage(Text.literal(
"Current mode: §e" + mode + "§r — ends in §c"
+ timer + "§r ticks ("
+ (timer / 20) + "s)"
));
}
return 1;
})
)
.then(CommandManager.literal("set")
.then(CommandManager.literal("normal")
.executes(context -> {
ServerCommandSource source = context.getSource();
ServerWorld backrooms = source.getServer().getWorld(Szar.BACKROOMS_KEY);
if (backrooms == null) {
source.sendError(Text.literal("Backrooms dimension not found"));
return 0;
}
// End current event cleanly
BackroomsLightManager.currentEvent = BackroomsLightManager.GlobalEvent.NONE;
BackroomsLightManager.eventTimer = 0;
BackroomsLightManager.cooldownTimer = 3600;
// Restore all lights
BackroomsLightManager.forceRestoreAllLights(backrooms);
source.sendMessage(Text.literal("§aBackrooms lights set to normal"));
return 1;
})
)
.then(CommandManager.literal("flickering")
.executes(context -> {
ServerCommandSource source = context.getSource();
ServerWorld backrooms = source.getServer().getWorld(Szar.BACKROOMS_KEY);
if (backrooms == null) {
source.sendError(Text.literal("Backrooms dimension not found"));
return 0;
}
BackroomsLightManager.currentEvent = BackroomsLightManager.GlobalEvent.FLICKER;
BackroomsLightManager.eventTimer = 3600; // 3 minutes default
BackroomsLightManager.cooldownTimer = 3600;
source.sendMessage(Text.literal("§eBackrooms lights set to flickering"));
return 1;
})
)
.then(CommandManager.literal("blackout")
.executes(context -> {
ServerCommandSource source = context.getSource();
ServerWorld backrooms = source.getServer().getWorld(Szar.BACKROOMS_KEY);
if (backrooms == null) {
source.sendError(Text.literal("Backrooms dimension not found"));
return 0;
}
BackroomsLightManager.currentEvent = BackroomsLightManager.GlobalEvent.BLACKOUT;
BackroomsLightManager.eventTimer = 3600;
BackroomsLightManager.cooldownTimer = 3600;
BackroomsLightManager.forceBlackout(backrooms);
source.sendMessage(Text.literal("§4Backrooms lights set to blackout"));
return 1;
})
)
)
);
});
Registry.register(
Registries.PAINTING_VARIANT,
@@ -1088,6 +1174,7 @@ public class Szar implements ModInitializer {
new Identifier(MOD_ID, "overworld_portal"))
);
BackroomsBarrelManager.register();
BackroomsLightManager.register();
}
// Blocks
public static final TrackerBlock TRACKER_BLOCK = Registry.register(
@@ -1715,6 +1802,31 @@ public class Szar implements ModInitializer {
new Identifier(MOD_ID, "can_of_beans"),
new CanOfBeansItem(new Item.Settings())
);
public static final Item ALMOND_WATER = Registry.register(
Registries.ITEM,
new Identifier(MOD_ID, "almond_water"),
new AlmondWaterItem(new Item.Settings())
);
public static final BackroomsLightBlock BACKROOMS_LIGHT = Registry.register(
Registries.BLOCK, new Identifier(MOD_ID, "backrooms_light"),
new BackroomsLightBlock(FabricBlockSettings.create()
.nonOpaque()
.luminance(state -> BackroomsLightBlock.getLightLevel(state))
.strength(0.3f))
);
public static final BlockItem BACKROOMS_LIGHT_ITEM = Registry.register(
Registries.ITEM, new Identifier(MOD_ID, "backrooms_light"),
new BlockItem(BACKROOMS_LIGHT, new FabricItemSettings())
);
public static final BlockEntityType<BackroomsLightBlockEntity> BACKROOMS_LIGHT_ENTITY =
Registry.register(
Registries.BLOCK_ENTITY_TYPE,
new Identifier(MOD_ID, "backrooms_light"),
FabricBlockEntityTypeBuilder.create(BackroomsLightBlockEntity::new,
BACKROOMS_LIGHT).build()
);
public static final SoundEvent BAITER =
SoundEvent.of(new Identifier(MOD_ID, "baiter"));
public static final Item BAITER_DISC = Registry.register(

View File

@@ -0,0 +1,8 @@
{
"variants": {
"light_state=on": { "model": "szar:block/backrooms_light_on" },
"light_state=off": { "model": "szar:block/backrooms_light_off" },
"light_state=flickering_on": { "model": "szar:block/backrooms_light_on" },
"light_state=flickering_off": { "model": "szar:block/backrooms_light_off" }
}
}

View File

@@ -147,5 +147,7 @@
"block.szar.wall_bottom": "Wall Bottom",
"item.szar.bean": "Bean",
"item.szar.can_of_beans": "Can of Beans"
"item.szar.can_of_beans": "Can of Beans",
"item.szar.almond_water": "Almond Water",
"block.szar.backrooms_light": "Light"
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "szar:block/black"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "szar:block/white"
}
}

View File

@@ -1,6 +1,7 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"parent": "block/block",
"texture_size": [64, 64],
"textures": {
"2": "szar:block/wallpaper_bottom_block_texture",
@@ -32,12 +33,6 @@
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [80.5, 45, 0],
"scale": [0.3, 0.3, 0.3]
}
},
"groups": [
{
"name": "block",

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "szar:item/almond_water"
}
}

View File

@@ -0,0 +1,3 @@
{
"parent": "szar:block/backrooms_light_on"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

View File

@@ -4,7 +4,7 @@
"coordinate_scale": 1.0,
"has_skylight": false,
"has_ceiling": true,
"ambient_light": 0.5,
"ambient_light": 0.0,
"fixed_time": 18000,
"monster_spawn_light_level": 0,
"monster_spawn_block_light_limit": 0,
@@ -16,5 +16,5 @@
"min_y": 0,
"height": 64,
"infiniburn": "#minecraft:infiniburn_overworld",
"effects": "minecraft:the_end"
"effects": "minecraft:overworld"
}