backrooms entity, lights fix and beer

This commit is contained in:
2026-03-20 18:06:53 +01:00
parent af21279b00
commit 4d802285ef
29 changed files with 1049 additions and 125 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.3.19 mod_version=26.3.20
maven_group=dev.tggamesyt maven_group=dev.tggamesyt
archives_base_name=szar archives_base_name=szar
# Dependencies # Dependencies

View File

@@ -1,6 +1,9 @@
package dev.tggamesyt.szar.client; package dev.tggamesyt.szar.client;
import dev.tggamesyt.szar.BackroomsLightBlock;
import dev.tggamesyt.szar.BackroomsLightBlockEntity; import dev.tggamesyt.szar.BackroomsLightBlockEntity;
import dev.tggamesyt.szar.BackroomsLightManager;
import net.minecraft.block.BlockState;
import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer; import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.VertexConsumerProvider;
@@ -14,7 +17,6 @@ import org.joml.Matrix4f;
public class BackroomsLightBlockEntityRenderer implements BlockEntityRenderer<BackroomsLightBlockEntity> { public class BackroomsLightBlockEntityRenderer implements BlockEntityRenderer<BackroomsLightBlockEntity> {
// Your light texture
private static final Identifier TEXTURE = private static final Identifier TEXTURE =
new Identifier("szar", "textures/block/white.png"); new Identifier("szar", "textures/block/white.png");
@@ -25,23 +27,38 @@ public class BackroomsLightBlockEntityRenderer implements BlockEntityRenderer<Ba
MatrixStack matrices, VertexConsumerProvider vertexConsumers, MatrixStack matrices, VertexConsumerProvider vertexConsumers,
int light, int overlay) { int light, int overlay) {
float brightness = entity.brightness; BlockState state = entity.getCachedState();
if (brightness <= 0.0f) return; // fully dark, don't render BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE);
float brightness;
if (ls == BackroomsLightBlock.LightState.OFF) {
brightness = 0.0f;
} else if (ls == BackroomsLightBlock.LightState.ON) {
// Check if there's a global flicker event — if so flicker this light too
if (BackroomsLightManager.currentEvent == BackroomsLightManager.GlobalEvent.FLICKER) {
brightness = computeFlicker(entity, tickDelta);
} else {
brightness = 1.0f;
}
} else {
// FLICKERING — always flicker regardless of event
brightness = computeFlicker(entity, tickDelta);
}
BlockPos pos = entity.getPos(); BlockPos pos = entity.getPos();
int lightLevel = WorldRenderer.getLightmapCoordinates(entity.getWorld(), pos); int lightLevel = brightness > 0
? WorldRenderer.getLightmapCoordinates(entity.getWorld(), pos)
: 0;
VertexConsumer consumer = vertexConsumers.getBuffer( VertexConsumer consumer = vertexConsumers.getBuffer(
RenderLayer.getEntityCutoutNoCull(TEXTURE)); RenderLayer.getEntityCutoutNoCull(TEXTURE));
matrices.push(); matrices.push();
// Center on block, render on bottom face (light faces downward into room) matrices.translate(0.5, -0.001, 0.5);
matrices.translate(0.5, 0.001, 0.5);
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(90)); matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(90));
Matrix4f matrix = matrices.peek().getPositionMatrix(); Matrix4f matrix = matrices.peek().getPositionMatrix();
// Apply brightness as color multiplier
int r = (int)(255 * brightness); int r = (int)(255 * brightness);
int g = (int)(255 * brightness); int g = (int)(255 * brightness);
int b = (int)(255 * brightness); int b = (int)(255 * brightness);
@@ -62,4 +79,22 @@ public class BackroomsLightBlockEntityRenderer implements BlockEntityRenderer<Ba
matrices.pop(); matrices.pop();
} }
private float computeFlicker(BackroomsLightBlockEntity entity, float tickDelta) {
// Sample a pseudo-random noise function using global timer + per-block offset
// This produces independent flicker per block with no server communication
int t = BackroomsLightManager.globalFlickerTimer + entity.flickerOffset;
// Multi-frequency noise for natural-looking flicker
double n = Math.sin(t * 0.3) * 0.3
+ Math.sin(t * 0.7 + entity.flickerOffset) * 0.2
+ Math.sin(t * 1.3 + entity.flickerOffset * 0.5) * 0.1;
// Occasionally snap to near-zero for a sharp flicker
boolean snap = ((t + entity.flickerOffset) % 23) < 2;
if (snap) return 0.05f + (float)(Math.random() * 0.1);
float base = 0.75f + (float)(n * 0.8);
return Math.max(0.05f, Math.min(1.0f, base));
}
} }

View File

@@ -0,0 +1,111 @@
package dev.tggamesyt.szar.client;
import dev.tggamesyt.szar.SmilerEffectManager;
import dev.tggamesyt.szar.SmilerType;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.sound.PositionedSoundInstance;
import net.minecraft.sound.SoundEvent;
import net.minecraft.util.Identifier;
public class SmilerEffectRenderer {
private static float flashbangAlpha = 0f;
private static float jumpscareAlpha = 0f;
private static SmilerType jumpscareType = null;
private static int jumpscareTimer = 0;
private static final Identifier EYES_FACE =
new Identifier("szar", "textures/entity/eyes.png");
private static final Identifier SCARY_FACE =
new Identifier("szar", "textures/entity/scary.png");
private static final Identifier REAL_FACE =
new Identifier("szar", "textures/entity/real.png");
public static void register() {
// Flashbang packet (kept as-is, but mainly used by EYES now)
ClientPlayNetworking.registerGlobalReceiver(
SmilerEffectManager.FLASHBANG_PACKET, (client, handler, buf, responseSender) -> {
client.execute(() -> flashbangAlpha = 1.0f);
});
// Jumpscare packet
ClientPlayNetworking.registerGlobalReceiver(
SmilerEffectManager.JUMPSCARE_PACKET, (client, handler, buf, responseSender) -> {
String typeName = buf.readString();
SmilerType type = SmilerType.valueOf(typeName);
client.execute(() -> {
jumpscareType = type;
jumpscareTimer = 40;
// 🔊 Play correct sound per type
Identifier soundId = switch (type) {
case EYES -> new Identifier("szar", "flashbang");
case SCARY -> new Identifier("szar", "scary");
case REAL -> new Identifier("szar", "real");
};
client.getSoundManager().play(
PositionedSoundInstance.master(
SoundEvent.of(soundId), 1.0f));
// 💥 Special handling for EYES
if (type == SmilerType.EYES) {
flashbangAlpha = 1.0f;
jumpscareAlpha = 0f; // no face rendering
} else {
jumpscareAlpha = 1.0f;
}
});
});
// HUD renderer
HudRenderCallback.EVENT.register(SmilerEffectRenderer::renderHud);
}
private static void renderHud(DrawContext context, float tickDelta) {
MinecraftClient client = MinecraftClient.getInstance();
if (client.player == null) return;
int screenW = client.getWindow().getScaledWidth();
int screenH = client.getWindow().getScaledHeight();
// 💥 Flashbang
if (flashbangAlpha > 0) {
int alpha = (int)(flashbangAlpha * 255);
context.fill(0, 0, screenW, screenH,
(alpha << 24) | 0x00FFFFFF);
flashbangAlpha = Math.max(0, flashbangAlpha - 0.02f);
}
// 👁️ Jumpscare (ONLY if not EYES)
if (jumpscareAlpha > 0 && jumpscareType != null && jumpscareType != SmilerType.EYES) {
jumpscareTimer--;
Identifier faceTexture = switch (jumpscareType) {
case SCARY -> SCARY_FACE;
case REAL -> REAL_FACE;
default -> SCARY_FACE;
};
int alpha = (int)(jumpscareAlpha * 255);
context.drawTexture(faceTexture,
0, 0, screenW, screenH,
0, 0, 768, 768,
1536, 1536);
context.fill(0, 0, screenW, screenH,
(alpha << 24) & 0xFF000000);
if (jumpscareTimer <= 0) {
jumpscareAlpha = Math.max(0, jumpscareAlpha - 0.05f);
if (jumpscareAlpha <= 0) jumpscareType = null;
}
}
}
}

View File

@@ -0,0 +1,86 @@
package dev.tggamesyt.szar.client;
import dev.tggamesyt.szar.SmilerEntity;
import dev.tggamesyt.szar.SmilerType;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.EntityRendererFactory;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.RotationAxis;
import org.joml.Matrix4f;
public class SmilerRenderer extends EntityRenderer<SmilerEntity> {
private static final Identifier EYES_TEX =
new Identifier("szar", "textures/entity/eyes.png");
private static final Identifier SCARY_TEX =
new Identifier("szar", "textures/entity/scary.png");
private static final Identifier REAL_TEX =
new Identifier("szar", "textures/entity/real.png");
public SmilerRenderer(EntityRendererFactory.Context ctx) {
super(ctx);
}
@Override
public Identifier getTexture(SmilerEntity entity) {
return switch (entity.smilerType) {
case EYES -> EYES_TEX;
case SCARY -> SCARY_TEX;
case REAL -> REAL_TEX;
};
}
@Override
public void render(SmilerEntity entity, float yaw, float tickDelta,
MatrixStack matrices, VertexConsumerProvider vertexConsumers,
int light) {
matrices.push();
// Billboard — face camera
matrices.multiply(this.dispatcher.getRotation());
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f));
// Scale to roughly 1.5 blocks tall
matrices.scale(1.5f, 1.5f, 1.5f);
Matrix4f matrix = matrices.peek().getPositionMatrix();
Identifier texture = getTexture(entity);
VertexConsumer consumer = vertexConsumers.getBuffer(
RenderLayer.getEntityTranslucent(texture));
float s = 0.5f;
// Top-left quarter UVs
float u0 = 0.0f;
float v0 = 0.0f;
float u1 = 0.5f;
float v1 = 0.5f;
int fullBright = 0xF000F0;
consumer.vertex(matrix, -s, -s, 0).color(255, 255, 255, 255)
.texture(u0, v1).overlay(OverlayTexture.DEFAULT_UV).light(fullBright)
.normal(0, 1, 0).next();
consumer.vertex(matrix, s, -s, 0).color(255, 255, 255, 255)
.texture(u1, v1).overlay(OverlayTexture.DEFAULT_UV).light(fullBright)
.normal(0, 1, 0).next();
consumer.vertex(matrix, s, s, 0).color(255, 255, 255, 255)
.texture(u1, v0).overlay(OverlayTexture.DEFAULT_UV).light(fullBright)
.normal(0, 1, 0).next();
consumer.vertex(matrix, -s, s, 0).color(255, 255, 255, 255)
.texture(u0, v0).overlay(OverlayTexture.DEFAULT_UV).light(fullBright)
.normal(0, 1, 0).next();
matrices.pop();
super.render(entity, yaw, tickDelta, matrices, vertexConsumers, light);
}
}

View File

@@ -93,6 +93,8 @@ public class SzarClient implements ClientModInitializer {
); );
@Override @Override
public void onInitializeClient() { public void onInitializeClient() {
SmilerEffectRenderer.register();
EntityRendererRegistry.register(Szar.SMILER_ENTITY_TYPE, SmilerRenderer::new);
ClientPlayNetworking.registerGlobalReceiver(OPEN_DETONATOR_SCREEN, (client, handler, buf, responseSender) -> { ClientPlayNetworking.registerGlobalReceiver(OPEN_DETONATOR_SCREEN, (client, handler, buf, responseSender) -> {
client.execute(() -> client.setScreen(new CoordInputScreen())); client.execute(() -> client.setScreen(new CoordInputScreen()));
}); });

View File

@@ -9,6 +9,7 @@ import net.minecraft.block.Blocks;
import net.minecraft.registry.RegistryCodecs; import net.minecraft.registry.RegistryCodecs;
import net.minecraft.registry.RegistryKeys; import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.entry.RegistryEntry; import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.world.ChunkRegion; import net.minecraft.world.ChunkRegion;
import net.minecraft.world.HeightLimitView; import net.minecraft.world.HeightLimitView;
@@ -19,6 +20,7 @@ import net.minecraft.world.biome.BiomeKeys;
import net.minecraft.world.biome.source.BiomeAccess; import net.minecraft.world.biome.source.BiomeAccess;
import net.minecraft.world.biome.source.FixedBiomeSource; import net.minecraft.world.biome.source.FixedBiomeSource;
import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.WorldChunk;
import net.minecraft.world.gen.GenerationStep; import net.minecraft.world.gen.GenerationStep;
import net.minecraft.world.gen.StructureAccessor; import net.minecraft.world.gen.StructureAccessor;
import net.minecraft.world.gen.chunk.Blender; import net.minecraft.world.gen.chunk.Blender;
@@ -93,12 +95,10 @@ public class BackroomsChunkGenerator extends ChunkGenerator {
long lightRoll = hash(worldX * 53 + 7, worldZ * 47 + 13); long lightRoll = hash(worldX * 53 + 7, worldZ * 47 + 13);
int roll = (int)(Math.abs(lightRoll) % 100); int roll = (int)(Math.abs(lightRoll) % 100);
if (roll < 98) { if (roll < 98) {
// 98% ON (flickering determined by block entity in generateFeatures)
ceilingBlock = Szar.BACKROOMS_LIGHT.getDefaultState() ceilingBlock = Szar.BACKROOMS_LIGHT.getDefaultState()
.with(BackroomsLightBlock.LIGHT_STATE, .with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.ON); BackroomsLightBlock.LightState.ON);
} else { } else {
// 2% missing — ceiling block, no light
ceilingBlock = Szar.CEILING.getDefaultState(); ceilingBlock = Szar.CEILING.getDefaultState();
} }
} else { } else {
@@ -283,19 +283,23 @@ public class BackroomsChunkGenerator extends ChunkGenerator {
} }
} }
// Initialize light block entities // Initialize light block entities — set mutable explicitly to Y=9
mutable.set(chunkX + lx, 9, chunkZ + lz); mutable.set(chunkX + lx, 9, chunkZ + lz);
BlockState lightState = world.getBlockState(mutable); BlockState lightState = world.getBlockState(mutable);
if (lightState.getBlock() instanceof BackroomsLightBlock) { if (lightState.getBlock() instanceof BackroomsLightBlock) {
// Always re-set to force block entity creation — no null check needed
world.setBlockState(mutable, lightState, Block.NOTIFY_ALL); world.setBlockState(mutable, lightState, Block.NOTIFY_ALL);
BlockPos immutable = mutable.toImmutable(); BlockPos immutable = mutable.toImmutable();
if (world.getBlockEntity(immutable) instanceof BackroomsLightBlockEntity light) { if (world.getBlockEntity(immutable) instanceof BackroomsLightBlockEntity light) {
long typeRoll = hash(chunkX + lx, chunkZ + lz); long typeRoll = hash(chunkX + lx, chunkZ + lz);
light.isFlickering = (Math.abs(typeRoll) % 10) >= 8; light.isFlickering = (Math.abs(typeRoll) % 10) >= 8;
light.flickerOffset = (int)(Math.abs(hash(chunkX + lx, chunkZ + lz)) % 100); light.flickerOffset = (int)(Math.abs(hash(chunkX + lx, chunkZ + lz)) % 1000);
light.flickerTimer = light.flickerOffset;
light.brightness = 1.0f; light.brightness = 1.0f;
if (light.isFlickering) {
world.setBlockState(immutable,
world.getBlockState(immutable).with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.FLICKERING),
Block.NOTIFY_ALL);
}
light.markDirty(); light.markDirty();
} }
} }
@@ -324,5 +328,11 @@ public class BackroomsChunkGenerator extends ChunkGenerator {
} }
} }
} }
// At the end of generateFeatures, after all block entity init:
if (world instanceof ServerWorld sw) {
WorldChunk wc = sw.getChunk(chunk.getPos().x, chunk.getPos().z);
BackroomsLightManager.applyCurrentEventToChunk(sw, wc);
}
} }
} }

View File

@@ -8,19 +8,18 @@ import net.minecraft.state.StateManager;
import net.minecraft.state.property.EnumProperty; import net.minecraft.state.property.EnumProperty;
import net.minecraft.util.StringIdentifiable; import net.minecraft.util.StringIdentifiable;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.world.BlockView;
import net.minecraft.world.World; import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public class BackroomsLightBlock extends BlockWithEntity { public class BackroomsLightBlock extends BlockWithEntity {
public enum LightState implements StringIdentifiable { public enum LightState implements StringIdentifiable {
ON, OFF; ON, OFF, FLICKERING;
@Override @Override
public String asString() { public String asString() { return name().toLowerCase(); }
return name().toLowerCase();
}
} }
public static final EnumProperty<LightState> LIGHT_STATE = public static final EnumProperty<LightState> LIGHT_STATE =
@@ -52,13 +51,8 @@ public class BackroomsLightBlock extends BlockWithEntity {
World world, BlockState state, BlockEntityType<T> type) { World world, BlockState state, BlockEntityType<T> type) {
if (world.isClient) return null; if (world.isClient) return null;
return type == Szar.BACKROOMS_LIGHT_ENTITY return type == Szar.BACKROOMS_LIGHT_ENTITY
? (w, pos, s, be) -> BackroomsLightBlockEntity.tick( ? (w, pos, s, be) ->
w, pos, s, (BackroomsLightBlockEntity) be) BackroomsLightBlockEntity.tick(w, pos, s, (BackroomsLightBlockEntity) be)
: null; : null;
} }
// Light level based on state
public static int getLightLevel(BlockState state) {
return state.get(LIGHT_STATE) == LightState.ON ? 15 : 0;
}
} }

View File

@@ -10,13 +10,12 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World; import net.minecraft.world.World;
public class BackroomsLightBlockEntity extends BlockEntity { public class BackroomsLightBlockEntity extends BlockEntity {
public float brightness = 1.0f; // 0.0 = black, 1.0 = full bright public boolean isFlickering = false;
// Random offset so each light flickers at different times // Used by renderer — offset into the global flicker timer
public int flickerOffset = 0; public int flickerOffset = 0;
// How many ticks until next state toggle during flicker // Visual brightness — only changed during blackout, not during flicker
public int flickerTimer = 0; public float brightness = 1.0f;
private boolean initialized = false; private boolean initialized = false;
public boolean isFlickering = false; // true for lights generated as flickering type
public BackroomsLightBlockEntity(BlockPos pos, BlockState state) { public BackroomsLightBlockEntity(BlockPos pos, BlockState state) {
super(Szar.BACKROOMS_LIGHT_ENTITY, pos, state); super(Szar.BACKROOMS_LIGHT_ENTITY, pos, state);
@@ -25,36 +24,32 @@ public class BackroomsLightBlockEntity extends BlockEntity {
public static void tick(World world, BlockPos pos, BlockState state, public static void tick(World world, BlockPos pos, BlockState state,
BackroomsLightBlockEntity entity) { BackroomsLightBlockEntity entity) {
if (!entity.initialized) { if (!entity.initialized) {
entity.flickerOffset = world.random.nextInt(100); entity.flickerOffset = world.random.nextInt(1000);
entity.initialized = true; entity.initialized = true;
entity.markDirty(); entity.markDirty();
} }
BackroomsLightManager.tickLight(world, pos, state, entity);
} }
@Override @Override
public void writeNbt(NbtCompound nbt) { public void writeNbt(NbtCompound nbt) {
super.writeNbt(nbt); super.writeNbt(nbt);
nbt.putInt("FlickerOffset", flickerOffset); nbt.putInt("FlickerOffset", flickerOffset);
nbt.putInt("FlickerTimer", flickerTimer);
nbt.putBoolean("Initialized", initialized);
nbt.putFloat("Brightness", brightness); nbt.putFloat("Brightness", brightness);
nbt.putBoolean("Initialized", initialized);
nbt.putBoolean("IsFlickering", isFlickering);
} }
@Override @Override
public void readNbt(NbtCompound nbt) { public void readNbt(NbtCompound nbt) {
super.readNbt(nbt); super.readNbt(nbt);
flickerOffset = nbt.getInt("FlickerOffset"); flickerOffset = nbt.getInt("FlickerOffset");
flickerTimer = nbt.getInt("FlickerTimer");
initialized = nbt.getBoolean("Initialized");
brightness = nbt.getFloat("Brightness"); brightness = nbt.getFloat("Brightness");
initialized = nbt.getBoolean("Initialized");
isFlickering = nbt.getBoolean("IsFlickering");
} }
@Override @Override
public NbtCompound toInitialChunkDataNbt() { public NbtCompound toInitialChunkDataNbt() { return createNbt(); }
return createNbt();
}
@Override @Override
public Packet<ClientPlayPacketListener> toUpdatePacket() { public Packet<ClientPlayPacketListener> toUpdatePacket() {

View File

@@ -1,16 +1,15 @@
package dev.tggamesyt.szar; package dev.tggamesyt.szar;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState; import net.minecraft.block.BlockState;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraft.world.chunk.WorldChunk; import net.minecraft.world.chunk.WorldChunk;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
public class BackroomsLightManager { public class BackroomsLightManager {
@@ -19,6 +18,7 @@ public class BackroomsLightManager {
public static GlobalEvent currentEvent = GlobalEvent.NONE; public static GlobalEvent currentEvent = GlobalEvent.NONE;
public static int eventTimer = 0; public static int eventTimer = 0;
public static int cooldownTimer = 3600; public static int cooldownTimer = 3600;
public static int globalFlickerTimer = 0;
private static final int FLICKER_DURATION_MIN = 60; private static final int FLICKER_DURATION_MIN = 60;
private static final int FLICKER_DURATION_MAX = 160; private static final int FLICKER_DURATION_MAX = 160;
@@ -34,26 +34,46 @@ public class BackroomsLightManager {
ServerWorld backrooms = server.getWorld(Szar.BACKROOMS_KEY); ServerWorld backrooms = server.getWorld(Szar.BACKROOMS_KEY);
if (backrooms == null) return; if (backrooms == null) return;
globalFlickerTimer++;
// Every 20 ticks, fix any lights in wrong state for current event
if (globalFlickerTimer % 20 == 0) {
if (currentEvent == GlobalEvent.BLACKOUT) {
forEachLightEntity(backrooms, (entity, state, pos) -> {
if (state.get(BackroomsLightBlock.LIGHT_STATE)
!= BackroomsLightBlock.LightState.OFF) {
backrooms.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.OFF), Block.NOTIFY_ALL);
}
});
} else if (currentEvent == GlobalEvent.NONE) {
forEachLightEntity(backrooms, (entity, state, pos) -> {
if (state.get(BackroomsLightBlock.LIGHT_STATE) == BackroomsLightBlock.LightState.OFF) {
BackroomsLightBlock.LightState restore = entity.isFlickering
? BackroomsLightBlock.LightState.FLICKERING
: BackroomsLightBlock.LightState.ON;
backrooms.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
restore), Block.NOTIFY_ALL);
}
});
}
}
if (currentEvent != GlobalEvent.NONE) { if (currentEvent != GlobalEvent.NONE) {
eventTimer--; eventTimer--;
if (eventTimer <= 0) { if (eventTimer <= 0) endEvent(backrooms);
endEvent(backrooms);
}
} else { } else {
cooldownTimer--; cooldownTimer--;
if (cooldownTimer <= 0) { if (cooldownTimer <= 0) {
int roll = backrooms.random.nextInt(100); int roll = backrooms.random.nextInt(100);
if (roll < 30) { if (roll < 30) startBlackout(backrooms);
startBlackout(backrooms); else if (roll < 63) startFlicker(backrooms);
} else if (roll < 63) { else cooldownTimer = EVENT_COOLDOWN;
startFlicker(backrooms);
} else {
cooldownTimer = EVENT_COOLDOWN;
}
} }
} }
} }
private static void startFlicker(ServerWorld world) { private static void startFlicker(ServerWorld world) {
currentEvent = GlobalEvent.FLICKER; currentEvent = GlobalEvent.FLICKER;
eventTimer = FLICKER_DURATION_MIN + world.random.nextInt( eventTimer = FLICKER_DURATION_MIN + world.random.nextInt(
@@ -65,65 +85,66 @@ public class BackroomsLightManager {
currentEvent = GlobalEvent.BLACKOUT; currentEvent = GlobalEvent.BLACKOUT;
eventTimer = BLACKOUT_MIN + world.random.nextInt(BLACKOUT_MAX - BLACKOUT_MIN); eventTimer = BLACKOUT_MIN + world.random.nextInt(BLACKOUT_MAX - BLACKOUT_MIN);
cooldownTimer = EVENT_COOLDOWN; cooldownTimer = EVENT_COOLDOWN;
// Set all lights to brightness 0 forEachLightEntity(world, (entity, state, pos) -> {
forEachLightEntity(world, entity -> { if (state.get(BackroomsLightBlock.LIGHT_STATE) != BackroomsLightBlock.LightState.OFF) {
entity.brightness = 0.0f; world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
entity.markDirty(); BackroomsLightBlock.LightState.OFF), Block.NOTIFY_ALL);
}
}); });
} }
private static void endEvent(ServerWorld world) { private static void endEvent(ServerWorld world) {
// Restore all lights to full brightness
forEachLightEntity(world, entity -> {
entity.brightness = 1.0f;
entity.markDirty();
});
currentEvent = GlobalEvent.NONE; currentEvent = GlobalEvent.NONE;
eventTimer = 0; eventTimer = 0;
cooldownTimer = EVENT_COOLDOWN; cooldownTimer = EVENT_COOLDOWN;
} forEachLightEntity(world, (entity, state, pos) -> {
BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE);
// Called per-light from BackroomsLightBlockEntity.tick // Restore any light that was turned off by an event
public static void tickLight(World world, BlockPos pos, BlockState state, if (ls == BackroomsLightBlock.LightState.OFF) {
BackroomsLightBlockEntity entity) { BackroomsLightBlock.LightState restore = entity.isFlickering
if (currentEvent == GlobalEvent.BLACKOUT) return; ? BackroomsLightBlock.LightState.FLICKERING
: BackroomsLightBlock.LightState.ON;
BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE); world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
if (ls == BackroomsLightBlock.LightState.OFF) return; restore), Block.NOTIFY_ALL);
boolean inFlickerEvent = currentEvent == GlobalEvent.FLICKER;
// Always-flickering lights tick regardless of event
// During flicker event, all ON lights also flicker
if (!entity.isFlickering && !inFlickerEvent) {
// Normal ON light, not in event — ensure full brightness
if (entity.brightness != 1.0f) {
entity.brightness = 1.0f;
entity.markDirty();
} }
return; });
}
entity.flickerTimer--;
if (entity.flickerTimer > 0) return;
// Random new brightness and timer
float newBrightness;
if (world.random.nextFloat() < 0.3f) {
// 30% chance of a dim flicker
newBrightness = 0.1f + world.random.nextFloat() * 0.4f;
} else {
// 70% chance of full or near-full
newBrightness = 0.7f + world.random.nextFloat() * 0.3f;
}
entity.brightness = newBrightness;
entity.flickerTimer = 2 + world.random.nextInt(8 + (entity.flickerOffset % 5));
entity.markDirty();
} }
private static void forEachLightEntity(ServerWorld world, // Called from generateFeatures when a new chunk loads — apply current event state
Consumer<BackroomsLightBlockEntity> consumer) { public static void applyCurrentEventToChunk(ServerWorld world, WorldChunk chunk) {
if (currentEvent == GlobalEvent.NONE) return;
int cx = chunk.getPos().getStartX();
int cz = chunk.getPos().getStartZ();
BlockPos.Mutable mutable = new BlockPos.Mutable();
for (int lx = 0; lx < 16; lx++) {
for (int lz = 0; lz < 16; lz++) {
mutable.set(cx + lx, 9, cz + lz);
BlockPos immutable = mutable.toImmutable();
BlockState state = world.getBlockState(immutable);
if (!(state.getBlock() instanceof BackroomsLightBlock)) continue;
if (!(world.getBlockEntity(immutable) instanceof BackroomsLightBlockEntity entity)) continue;
if (currentEvent == GlobalEvent.BLACKOUT) {
if (state.get(BackroomsLightBlock.LIGHT_STATE)
!= BackroomsLightBlock.LightState.OFF) {
world.setBlockState(immutable, state.with(
BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.OFF), Block.NOTIFY_ALL);
}
}
// FLICKER is handled per-tick so no chunk-load action needed
}
}
}
@FunctionalInterface
interface LightConsumer {
void accept(BackroomsLightBlockEntity entity, BlockState state, BlockPos pos);
}
private static void forEachLightEntity(ServerWorld world, LightConsumer consumer) {
for (WorldChunk chunk : getLoadedChunks(world)) { for (WorldChunk chunk : getLoadedChunks(world)) {
int cx = chunk.getPos().getStartX(); int cx = chunk.getPos().getStartX();
int cz = chunk.getPos().getStartZ(); int cz = chunk.getPos().getStartZ();
@@ -131,9 +152,12 @@ public class BackroomsLightManager {
for (int lx = 0; lx < 16; lx++) { for (int lx = 0; lx < 16; lx++) {
for (int lz = 0; lz < 16; lz++) { for (int lz = 0; lz < 16; lz++) {
mutable.set(cx + lx, 9, cz + lz); mutable.set(cx + lx, 9, cz + lz);
if (world.getBlockEntity(mutable.toImmutable()) BlockPos immutable = mutable.toImmutable();
BlockState state = world.getBlockState(immutable);
if (state.getBlock() instanceof BackroomsLightBlock
&& world.getBlockEntity(immutable)
instanceof BackroomsLightBlockEntity entity) { instanceof BackroomsLightBlockEntity entity) {
consumer.accept(entity); consumer.accept(entity, state, immutable);
} }
} }
} }
@@ -159,21 +183,23 @@ public class BackroomsLightManager {
} }
public static void forceRestoreAllLights(ServerWorld world) { public static void forceRestoreAllLights(ServerWorld world) {
forEachLightEntity(world, entity -> {
entity.brightness = 1.0f;
entity.markDirty();
});
currentEvent = GlobalEvent.NONE; currentEvent = GlobalEvent.NONE;
eventTimer = 0; eventTimer = 0;
cooldownTimer = EVENT_COOLDOWN; cooldownTimer = EVENT_COOLDOWN;
forEachLightEntity(world, (entity, state, pos) -> {
BackroomsLightBlock.LightState restore = entity.isFlickering
? BackroomsLightBlock.LightState.FLICKERING
: BackroomsLightBlock.LightState.ON;
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
restore), Block.NOTIFY_ALL);
});
} }
public static void forceBlackout(ServerWorld world) { public static void forceBlackout(ServerWorld world) {
forEachLightEntity(world, entity -> {
entity.brightness = 0.0f;
entity.markDirty();
});
currentEvent = GlobalEvent.BLACKOUT; currentEvent = GlobalEvent.BLACKOUT;
eventTimer = 3600; eventTimer = 3600;
forEachLightEntity(world, (entity, state, pos) ->
world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE,
BackroomsLightBlock.LightState.OFF), Block.NOTIFY_ALL));
} }
} }

View File

@@ -0,0 +1,94 @@
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.StatusEffects;
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;
public class BeerItem extends Item {
public BeerItem(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) {
// 🍺 Give nausea (10 seconds = 200 ticks)
user.addStatusEffect(new StatusEffectInstance(StatusEffects.NAUSEA, 60 * 20, 0));
user.addStatusEffect(new StatusEffectInstance(Szar.DRUNK_EFFECT, 60 * 20, 0));
// Optional: tiny hunger boost like drinks
if (player != null) {
player.getHungerManager().add(2, 0.2F);
}
}
// 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

@@ -0,0 +1,43 @@
package dev.tggamesyt.szar;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectCategory;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.world.RaycastContext;
public class DrunkEffect extends StatusEffect {
public DrunkEffect() {
super(StatusEffectCategory.HARMFUL, 0xFF9900);
}
static void tick(MinecraftServer server) {
for (var world : server.getWorlds()) {
// Every 20 ticks = 1 second
if (world.getTime() % 20 != 0) continue;
for (ServerPlayerEntity player : world.getPlayers()) {
if (!player.hasStatusEffect(Szar.DRUNK_EFFECT)) continue;
// 1 in 5 chance
if (world.random.nextInt(5) != 0) continue;
// Raycast to find what the player is looking at
HitResult hit = player.raycast(4.5, 0, false);
if (hit.getType() != HitResult.Type.ENTITY) continue;
if (!(hit instanceof EntityHitResult entityHit)) continue;
if (!(entityHit.getEntity() instanceof LivingEntity target)) continue;
// Hit the entity as if the player attacked it
player.attack(target);
}
}
}
}

View File

@@ -0,0 +1,24 @@
package dev.tggamesyt.szar;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
public class SmilerEffectManager {
public static final Identifier FLASHBANG_PACKET = new Identifier("szar", "flashbang");
public static final Identifier JUMPSCARE_PACKET = new Identifier("szar", "jumpscare");
public static void triggerFlashbang(ServerPlayerEntity player) {
PacketByteBuf buf = PacketByteBufs.create();
ServerPlayNetworking.send(player, FLASHBANG_PACKET, buf);
}
public static void triggerJumpscare(ServerPlayerEntity player, SmilerType type) {
PacketByteBuf buf = PacketByteBufs.create();
buf.writeString(type.name());
ServerPlayNetworking.send(player, JUMPSCARE_PACKET, buf);
}
}

View File

@@ -0,0 +1,225 @@
package dev.tggamesyt.szar;
import net.minecraft.entity.*;
import net.minecraft.entity.ai.goal.*;
import net.minecraft.entity.attribute.DefaultAttributeContainer;
import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.mob.PathAwareEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.LocalDifficulty;
import net.minecraft.world.ServerWorldAccess;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class SmilerEntity extends PathAwareEntity {
public SmilerType smilerType = SmilerType.EYES;
// Targeting
@Nullable private UUID targetPlayerUUID = null;
@Nullable private PlayerEntity cachedTarget = null;
// State tracking
private int stateTimer = 0;
private boolean hasActed = false; // has this entity done its main action?
// SCARY specific
private boolean scaryTeleported = false;
private boolean playerWasLooking = false;
public SmilerEntity(EntityType<? extends PathAwareEntity> type, World world) {
super(type, world);
this.setInvisible(false);
this.setNoGravity(true);
}
public static DefaultAttributeContainer.Builder createAttributes() {
return MobEntity.createMobAttributes()
.add(EntityAttributes.GENERIC_MAX_HEALTH, 1.0)
.add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.15)
.add(EntityAttributes.GENERIC_FOLLOW_RANGE, 64.0)
.add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 0.0);
}
@Override
protected void initGoals() {
// No standard goals — all behavior handled in tick
}
public void setTargetPlayer(PlayerEntity player) {
if (player.isCreative() || player.isSpectator()) return;
this.targetPlayerUUID = player.getUuid();
this.cachedTarget = player;
}
@Nullable
public PlayerEntity getTargetPlayer() {
if (cachedTarget != null && cachedTarget.isAlive()) return cachedTarget;
if (targetPlayerUUID != null && getWorld() instanceof ServerWorld sw) {
cachedTarget = sw.getPlayerByUuid(targetPlayerUUID);
return cachedTarget;
}
return null;
}
@Override
public void tick() {
super.tick();
if (getWorld().isClient) return;
PlayerEntity player = getTargetPlayer();
if (player == null || !player.isAlive() || player.isCreative() || player.isSpectator()) {
this.discard();
return;
}
// Despawn if blackout ended
if (BackroomsLightManager.currentEvent != BackroomsLightManager.GlobalEvent.BLACKOUT) {
this.discard();
return;
}
stateTimer++;
switch (smilerType) {
case EYES -> tickEyes(player);
case SCARY -> tickScary(player);
case REAL -> tickReal(player);
}
}
private void tickEyes(PlayerEntity player) {
if (hasActed) return;
// Slowly approach player
Vec3d toPlayer = player.getPos().subtract(this.getPos()).normalize();
this.setVelocity(toPlayer.multiply(0.05));
this.velocityModified = true;
// Look at player
this.getLookControl().lookAt(player, 30f, 30f);
// When close enough, flashbang
double dist = this.squaredDistanceTo(player);
if (dist < 4.0) {
hasActed = true;
if (player instanceof ServerPlayerEntity sp) {
SmilerEffectManager.triggerFlashbang(sp);
}
// Notify spawn manager
SmilerSpawnManager.onSmilerActed(player.getUuid(), SmilerType.EYES);
this.discard();
}
}
private void tickScary(PlayerEntity player) {
if (hasActed) return;
boolean playerLooking = isPlayerLookingAt(player);
if (!scaryTeleported) {
// Stay still while player is looking
if (playerLooking) {
playerWasLooking = true;
// Don't move
this.setVelocity(Vec3d.ZERO);
} else if (playerWasLooking) {
// Player looked away — teleport close
Vec3d behindPlayer = player.getPos()
.add(player.getRotationVector().multiply(-1.5));
this.teleport(behindPlayer.x, behindPlayer.y, behindPlayer.z);
scaryTeleported = true;
}
} else {
// Teleported — wait for player to look at us again
if (playerLooking) {
hasActed = true;
if (player instanceof ServerPlayerEntity sp) {
SmilerEffectManager.triggerJumpscare(sp, SmilerType.SCARY);
float damage = 2.0f + getWorld().random.nextFloat() * 4.0f; // 2-6 hearts = 4-12 hp
player.damage(getWorld().getDamageSources().mobAttack(this), damage * 2);
}
SmilerSpawnManager.onSmilerActed(player.getUuid(), SmilerType.SCARY);
this.discard();
}
}
}
private void tickReal(PlayerEntity player) {
if (hasActed) return;
// Run directly at player
Vec3d toPlayer = player.getPos().subtract(this.getPos()).normalize();
this.setVelocity(toPlayer.multiply(0.4));
this.velocityModified = true;
this.getLookControl().lookAt(player, 30f, 30f);
double dist = this.squaredDistanceTo(player);
if (dist < 4.0) {
hasActed = true;
if (player instanceof ServerPlayerEntity sp) {
SmilerEffectManager.triggerJumpscare(sp, SmilerType.REAL);
float damage = 4.0f + getWorld().random.nextFloat() * 4.0f; // 4-8 hearts = 8-16 hp
player.damage(getWorld().getDamageSources().mobAttack(this), damage * 2);
}
SmilerSpawnManager.onSmilerActed(player.getUuid(), SmilerType.REAL);
this.discard();
}
}
private boolean isPlayerLookingAt(PlayerEntity player) {
Vec3d eyePos = player.getEyePos();
Vec3d lookVec = player.getRotationVector();
Vec3d toEntity = this.getPos().subtract(eyePos).normalize();
double dot = lookVec.dotProduct(toEntity);
return dot > 0.97; // ~14 degree cone
}
@Override
public void writeCustomDataToNbt(NbtCompound nbt) {
super.writeCustomDataToNbt(nbt);
nbt.putString("SmilerType", smilerType.name());
if (targetPlayerUUID != null) {
nbt.putUuid("TargetPlayer", targetPlayerUUID);
}
nbt.putBoolean("HasActed", hasActed);
nbt.putBoolean("ScaryTeleported", scaryTeleported);
nbt.putBoolean("PlayerWasLooking", playerWasLooking);
}
@Override
public void readCustomDataFromNbt(NbtCompound nbt) {
super.readCustomDataFromNbt(nbt);
if (nbt.contains("SmilerType")) {
smilerType = SmilerType.valueOf(nbt.getString("SmilerType"));
}
if (nbt.containsUuid("TargetPlayer")) {
targetPlayerUUID = nbt.getUuid("TargetPlayer");
}
hasActed = nbt.getBoolean("HasActed");
scaryTeleported = nbt.getBoolean("ScaryTeleported");
playerWasLooking = nbt.getBoolean("PlayerWasLooking");
}
@Override
public boolean isInvisible() { return false; }
@Override
public boolean canTarget(EntityType<?> type) { return false; }
@Override
protected boolean canStartRiding(Entity entity) { return false; }
@Override
public boolean isPushable() { return false; }
}

View File

@@ -0,0 +1,131 @@
package dev.tggamesyt.szar;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import java.util.*;
public class SmilerSpawnManager {
// Per player, per type: world time when cooldown started
private static final Map<UUID, Map<SmilerType, Long>> cooldowns = new HashMap<>();
// Per player, per type: is there currently a live smiler of this type
private static final Map<UUID, Set<SmilerType>> activeSmilers = new HashMap<>();
private static final long COOLDOWN_TICKS = 1200; // 1 minute
// Random spawn check every 3-8 seconds per player
private static int spawnCheckTimer = 0;
public static void register() {
ServerTickEvents.END_SERVER_TICK.register(SmilerSpawnManager::tick);
}
private static void tick(MinecraftServer server) {
ServerWorld backrooms = server.getWorld(Szar.BACKROOMS_KEY);
if (backrooms == null) return;
// Only spawn during blackout
if (BackroomsLightManager.currentEvent != BackroomsLightManager.GlobalEvent.BLACKOUT) {
// Clean up any lingering smilers if blackout ended
return;
}
spawnCheckTimer--;
if (spawnCheckTimer > 0) return;
spawnCheckTimer = 60 + backrooms.random.nextInt(100); // 3-8 seconds
for (ServerPlayerEntity player : backrooms.getPlayers()) {
UUID uuid = player.getUuid();
long now = backrooms.getTime();
for (SmilerType type : SmilerType.values()) {
// Skip if already active
if (isActive(uuid, type)) continue;
// Skip if on cooldown
if (isOnCooldown(uuid, type, now)) continue;
// 40% chance to actually spawn this type this check
if (backrooms.random.nextFloat() > 0.4f) continue;
spawnSmiler(backrooms, player, type);
}
}
}
private static void spawnSmiler(ServerWorld world, ServerPlayerEntity player, SmilerType type) {
// Find a spawn position in the backrooms — in a corridor, 15-30 blocks from player
Vec3d spawnPos = findSpawnPos(world, player);
if (spawnPos == null) return;
SmilerEntity entity = new SmilerEntity(Szar.SMILER_ENTITY_TYPE, world);
entity.smilerType = type;
entity.setTargetPlayer(player);
entity.refreshPositionAndAngles(spawnPos.x, spawnPos.y, spawnPos.z, 0f, 0f);
world.spawnEntity(entity);
markActive(player.getUuid(), type);
}
private static Vec3d findSpawnPos(ServerWorld world, PlayerEntity player) {
// Try to find an open spot 15-30 blocks away from player
for (int attempt = 0; attempt < 20; attempt++) {
double angle = world.random.nextDouble() * Math.PI * 2;
double dist = 15 + world.random.nextDouble() * 15;
double x = player.getX() + Math.cos(angle) * dist;
double z = player.getZ() + Math.sin(angle) * dist;
double y = 6; // backrooms floor level + 1
BlockPos pos = new BlockPos((int) x, (int) y, (int) z);
// Check it's open space
if (world.getBlockState(pos).isAir()
&& world.getBlockState(pos.up()).isAir()) {
return new Vec3d(x, y, z);
}
}
return null;
}
public static void onSmilerActed(UUID playerUUID, SmilerType type) {
markInactive(playerUUID, type);
// Start cooldown
// We don't have world time here so use system time approximation
// Actually store it via a flag and check in tick
cooldowns.computeIfAbsent(playerUUID, k -> new HashMap<>())
.put(type, System.currentTimeMillis());
}
public static void onSmilerDied(UUID playerUUID, SmilerType type) {
markInactive(playerUUID, type);
cooldowns.computeIfAbsent(playerUUID, k -> new HashMap<>())
.put(type, System.currentTimeMillis());
}
private static boolean isOnCooldown(UUID uuid, SmilerType type, long worldTime) {
Map<SmilerType, Long> playerCooldowns = cooldowns.get(uuid);
if (playerCooldowns == null) return false;
Long cooldownStart = playerCooldowns.get(type);
if (cooldownStart == null) return false;
// Use milliseconds: 1 minute = 60000ms
return (System.currentTimeMillis() - cooldownStart) < 60000;
}
private static boolean isActive(UUID uuid, SmilerType type) {
Set<SmilerType> active = activeSmilers.get(uuid);
return active != null && active.contains(type);
}
private static void markActive(UUID uuid, SmilerType type) {
activeSmilers.computeIfAbsent(uuid, k -> new HashSet<>()).add(type);
}
private static void markInactive(UUID uuid, SmilerType type) {
Set<SmilerType> active = activeSmilers.get(uuid);
if (active != null) active.remove(type);
}
}

View File

@@ -0,0 +1,5 @@
package dev.tggamesyt.szar;
public enum SmilerType {
EYES, SCARY, REAL
}

View File

@@ -6,6 +6,8 @@ import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.AttackEntityCallback; import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
@@ -369,6 +371,7 @@ public class Szar implements ModInitializer {
entries.add(Szar.WALL_BOTTOM_ITEM); entries.add(Szar.WALL_BOTTOM_ITEM);
entries.add(Szar.CEILING_ITEM); entries.add(Szar.CEILING_ITEM);
entries.add(Szar.PLASTIC_ITEM); entries.add(Szar.PLASTIC_ITEM);
entries.add(Szar.BACKROOMS_LIGHT_ITEM);
entries.add(Szar.BEAN); entries.add(Szar.BEAN);
entries.add(Szar.CAN_OF_BEANS); entries.add(Szar.CAN_OF_BEANS);
entries.add(Szar.ALMOND_WATER); entries.add(Szar.ALMOND_WATER);
@@ -389,6 +392,7 @@ public class Szar implements ModInitializer {
entries.add(Szar.WEED_ITEM); entries.add(Szar.WEED_ITEM);
entries.add(Szar.WEED_JOINT_ITEM); entries.add(Szar.WEED_JOINT_ITEM);
entries.add(Szar.CHEMICAL_WORKBENCH_ITEM); entries.add(Szar.CHEMICAL_WORKBENCH_ITEM);
entries.add(Szar.BEER);
// war guys // war guys
entries.add(Szar.HITTER_SPAWNEGG); entries.add(Szar.HITTER_SPAWNEGG);
entries.add(Szar.NAZI_SPAWNEGG); entries.add(Szar.NAZI_SPAWNEGG);
@@ -793,6 +797,10 @@ public class Szar implements ModInitializer {
TERRORIST_ENTITY_TYPE, TERRORIST_ENTITY_TYPE,
IslamTerrorist.createAttributes() IslamTerrorist.createAttributes()
); );
FabricDefaultAttributeRegistry.register(
SMILER_ENTITY_TYPE,
SmilerEntity.createAttributes()
);
SpawnRestriction.register( SpawnRestriction.register(
Szar.PoliceEntityType, Szar.PoliceEntityType,
SpawnRestriction.Location.ON_GROUND, // spawn on solid blocks SpawnRestriction.Location.ON_GROUND, // spawn on solid blocks
@@ -1175,6 +1183,30 @@ public class Szar implements ModInitializer {
); );
BackroomsBarrelManager.register(); BackroomsBarrelManager.register();
BackroomsLightManager.register(); BackroomsLightManager.register();
SmilerSpawnManager.register();
// 🔄 Dimension change
ServerEntityWorldChangeEvents.AFTER_PLAYER_CHANGE_WORLD.register((player, origin, destination) -> {
if (origin.getRegistryKey() == Szar.BACKROOMS_KEY) {
restoreIfNeeded(player);
}
});
// 💀 Death (handled on respawn copy)
ServerPlayerEvents.COPY_FROM.register((oldPlayer, newPlayer, alive) -> {
if (!alive) {
restoreIfNeeded(newPlayer);
}
});
// 🔌 Join (failsafe)
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
ServerPlayerEntity player = handler.getPlayer();
if (player.getWorld().getRegistryKey() != Szar.BACKROOMS_KEY) {
restoreIfNeeded(player);
}
});
ServerTickEvents.END_SERVER_TICK.register(DrunkEffect::tick);
} }
// Blocks // Blocks
public static final TrackerBlock TRACKER_BLOCK = Registry.register( public static final TrackerBlock TRACKER_BLOCK = Registry.register(
@@ -1213,7 +1245,7 @@ public class Szar implements ModInitializer {
); );
public static final WallBlock WALL_BLOCK = Registry.register( public static final WallBlock WALL_BLOCK = Registry.register(
Registries.BLOCK, new Identifier(MOD_ID, "wall"), Registries.BLOCK, new Identifier(MOD_ID, "wall"),
new WallBlock(AbstractBlock.Settings.create()) new WallBlock(AbstractBlock.Settings.create().strength(-1.0f, 3600000f))
); );
public static final BlockEntityType<WallBlockEntity> WALL_BLOCK_ENTITY = Registry.register( public static final BlockEntityType<WallBlockEntity> WALL_BLOCK_ENTITY = Registry.register(
Registries.BLOCK_ENTITY_TYPE, new Identifier(MOD_ID, "wall"), Registries.BLOCK_ENTITY_TYPE, new Identifier(MOD_ID, "wall"),
@@ -1221,15 +1253,15 @@ public class Szar implements ModInitializer {
); );
public static final Block WALL_BOTTOM_BLOCK = Registry.register( public static final Block WALL_BOTTOM_BLOCK = Registry.register(
Registries.BLOCK, new Identifier(MOD_ID, "wall_bottom"), Registries.BLOCK, new Identifier(MOD_ID, "wall_bottom"),
new Block(AbstractBlock.Settings.create()) new Block(AbstractBlock.Settings.create().strength(-1.0f, 3600000f))
); );
public static final Block CEILING = Registry.register( public static final Block CEILING = Registry.register(
Registries.BLOCK, new Identifier(MOD_ID, "ceiling"), Registries.BLOCK, new Identifier(MOD_ID, "ceiling"),
new Block(AbstractBlock.Settings.create()) new Block(AbstractBlock.Settings.create().strength(-1.0f, 3600000f))
); );
public static final Block PLASTIC = Registry.register( public static final Block PLASTIC = Registry.register(
Registries.BLOCK, new Identifier(MOD_ID, "plastic"), Registries.BLOCK, new Identifier(MOD_ID, "plastic"),
new Block(AbstractBlock.Settings.create()) new Block(AbstractBlock.Settings.create().strength(-1.0f, 3600000f))
); );
public static final Item WALL_ITEM = Registry.register( public static final Item WALL_ITEM = Registry.register(
Registries.ITEM, Registries.ITEM,
@@ -1805,14 +1837,18 @@ public class Szar implements ModInitializer {
public static final Item ALMOND_WATER = Registry.register( public static final Item ALMOND_WATER = Registry.register(
Registries.ITEM, Registries.ITEM,
new Identifier(MOD_ID, "almond_water"), new Identifier(MOD_ID, "almond_water"),
new AlmondWaterItem(new Item.Settings()) new AlmondWaterItem(new Item.Settings().maxCount(16))
); );
public static final BackroomsLightBlock BACKROOMS_LIGHT = Registry.register( public static final BackroomsLightBlock BACKROOMS_LIGHT = Registry.register(
Registries.BLOCK, new Identifier(MOD_ID, "backrooms_light"), Registries.BLOCK, new Identifier(MOD_ID, "backrooms_light"),
new BackroomsLightBlock(FabricBlockSettings.create() new BackroomsLightBlock(FabricBlockSettings.create()
.nonOpaque() .luminance(state -> {
.luminance(state -> BackroomsLightBlock.getLightLevel(state)) return switch (state.get(BackroomsLightBlock.LIGHT_STATE)) {
.strength(0.3f)) case ON, FLICKERING -> 15;
case OFF -> 0;
};
})
.strength(-1.0f, 3600000f))
); );
public static final BlockItem BACKROOMS_LIGHT_ITEM = Registry.register( public static final BlockItem BACKROOMS_LIGHT_ITEM = Registry.register(
@@ -1827,6 +1863,25 @@ public class Szar implements ModInitializer {
FabricBlockEntityTypeBuilder.create(BackroomsLightBlockEntity::new, FabricBlockEntityTypeBuilder.create(BackroomsLightBlockEntity::new,
BACKROOMS_LIGHT).build() BACKROOMS_LIGHT).build()
); );
public static final EntityType<SmilerEntity> SMILER_ENTITY_TYPE = Registry.register(
Registries.ENTITY_TYPE,
new Identifier(MOD_ID, "smiler"),
FabricEntityTypeBuilder.create(SpawnGroup.MONSTER, SmilerEntity::new)
.dimensions(EntityDimensions.fixed(1.0f, 1.5f))
.build()
);
public static final DrunkEffect DRUNK_EFFECT = Registry.register(
Registries.STATUS_EFFECT,
new Identifier(MOD_ID, "drunk"),
new DrunkEffect()
);
public static final Item BEER = Registry.register(
Registries.ITEM,
new Identifier(MOD_ID, "beer"),
new BeerItem(new Item.Settings().maxCount(16))
);
public static final SoundEvent BAITER = public static final SoundEvent BAITER =
SoundEvent.of(new Identifier(MOD_ID, "baiter")); SoundEvent.of(new Identifier(MOD_ID, "baiter"));
public static final Item BAITER_DISC = Registry.register( public static final Item BAITER_DISC = Registry.register(
@@ -2176,5 +2231,41 @@ public class Szar implements ModInitializer {
player.getWorld().playSound(null, player.getBlockPos(), player.getWorld().playSound(null, player.getBlockPos(),
Szar.REVOLVER_CLICK2_SOUND, SoundCategory.PLAYERS, 1f, pitch); Szar.REVOLVER_CLICK2_SOUND, SoundCategory.PLAYERS, 1f, pitch);
} }
private static void restoreIfNeeded(ServerPlayerEntity player) {
MinecraftServer server = player.getServer();
if (server == null) return;
ServerWorld overworld = server.getWorld(World.OVERWORLD);
if (overworld == null) return;
PortalDataState state = PortalDataState.getOrCreate(overworld);
NbtCompound tag = state.getOrCreatePlayerData(player.getUuid());
// 🔑 THIS is the only condition that matters
if (!tag.contains("SzarSavedInventory")) return;
// ✅ Restore inventory
PortalBlock.restoreInventory(player, server);
// 🧹 CLEANUP tracker using YOUR existing data
if (tag.contains("OwnerTrackerX")) {
BlockPos pos = new BlockPos(
tag.getInt("OwnerTrackerX"),
tag.getInt("OwnerTrackerY"),
tag.getInt("OwnerTrackerZ")
);
if (overworld.getBlockEntity(pos) instanceof TrackerBlockEntity tracker) {
TrackerBlockEntity root = tracker.getRoot(overworld);
root.removePlayer(player.getUuid());
}
}
// 🧽 Clean up leftover data
tag.remove("OwnerTrackerX");
tag.remove("OwnerTrackerY");
tag.remove("OwnerTrackerZ");
}
} }

View File

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

View File

@@ -149,5 +149,8 @@
"item.szar.bean": "Bean", "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", "item.szar.almond_water": "Almond Water",
"block.szar.backrooms_light": "Light" "block.szar.backrooms_light": "Light",
"entity.szar.smiler": "Smiler",
"item.szar.beer": "Beer",
"effect.szar.drunk": "Drunk"
} }

View File

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

View File

@@ -202,5 +202,29 @@
"stream": true "stream": true
} }
] ]
},
"real": {
"sounds": [
{
"name": "szar:real",
"stream": true
}
]
},
"scary": {
"sounds": [
{
"name": "szar:scary",
"stream": true
}
]
},
"flashbang": {
"sounds": [
{
"name": "szar:flashbang",
"stream": true
}
]
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,20 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
" W ",
"WPW",
" W "
],
"key": {
"W": {
"item": "minecraft:wheat"
},
"P": {
"item": "minecraft:potion"
}
},
"result": {
"item": "szar:beer",
"count": 1
}
}