diff --git a/gradle.properties b/gradle.properties index a367cb3..367a988 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.20.1 yarn_mappings=1.20.1+build.10 loader_version=0.18.3 # Mod Properties -mod_version=26.3.19 +mod_version=26.3.20 maven_group=dev.tggamesyt archives_base_name=szar # Dependencies diff --git a/src/client/java/dev/tggamesyt/szar/client/BackroomsLightBlockEntityRenderer.java b/src/client/java/dev/tggamesyt/szar/client/BackroomsLightBlockEntityRenderer.java index 1f9a1f1..14d2314 100644 --- a/src/client/java/dev/tggamesyt/szar/client/BackroomsLightBlockEntityRenderer.java +++ b/src/client/java/dev/tggamesyt/szar/client/BackroomsLightBlockEntityRenderer.java @@ -1,6 +1,9 @@ package dev.tggamesyt.szar.client; +import dev.tggamesyt.szar.BackroomsLightBlock; 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.VertexConsumer; import net.minecraft.client.render.VertexConsumerProvider; @@ -14,7 +17,6 @@ import org.joml.Matrix4f; public class BackroomsLightBlockEntityRenderer implements BlockEntityRenderer { - // Your light texture private static final Identifier TEXTURE = new Identifier("szar", "textures/block/white.png"); @@ -25,23 +27,38 @@ public class BackroomsLightBlockEntityRenderer implements BlockEntityRenderer 0 + ? WorldRenderer.getLightmapCoordinates(entity.getWorld(), pos) + : 0; VertexConsumer consumer = vertexConsumers.getBuffer( RenderLayer.getEntityCutoutNoCull(TEXTURE)); 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)); Matrix4f matrix = matrices.peek().getPositionMatrix(); - // Apply brightness as color multiplier int r = (int)(255 * brightness); int g = (int)(255 * brightness); int b = (int)(255 * brightness); @@ -62,4 +79,22 @@ public class BackroomsLightBlockEntityRenderer implements BlockEntityRenderer { + 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; + } + } + } +} \ No newline at end of file diff --git a/src/client/java/dev/tggamesyt/szar/client/SmilerRenderer.java b/src/client/java/dev/tggamesyt/szar/client/SmilerRenderer.java new file mode 100644 index 0000000..3882711 --- /dev/null +++ b/src/client/java/dev/tggamesyt/szar/client/SmilerRenderer.java @@ -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 { + + 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); + } +} \ No newline at end of file diff --git a/src/client/java/dev/tggamesyt/szar/client/SzarClient.java b/src/client/java/dev/tggamesyt/szar/client/SzarClient.java index f928cfc..e78fa84 100644 --- a/src/client/java/dev/tggamesyt/szar/client/SzarClient.java +++ b/src/client/java/dev/tggamesyt/szar/client/SzarClient.java @@ -93,6 +93,8 @@ public class SzarClient implements ClientModInitializer { ); @Override public void onInitializeClient() { + SmilerEffectRenderer.register(); + EntityRendererRegistry.register(Szar.SMILER_ENTITY_TYPE, SmilerRenderer::new); ClientPlayNetworking.registerGlobalReceiver(OPEN_DETONATOR_SCREEN, (client, handler, buf, responseSender) -> { client.execute(() -> client.setScreen(new CoordInputScreen())); }); diff --git a/src/main/java/dev/tggamesyt/szar/BackroomsChunkGenerator.java b/src/main/java/dev/tggamesyt/szar/BackroomsChunkGenerator.java index d5e3735..2ed6dbe 100644 --- a/src/main/java/dev/tggamesyt/szar/BackroomsChunkGenerator.java +++ b/src/main/java/dev/tggamesyt/szar/BackroomsChunkGenerator.java @@ -9,6 +9,7 @@ import net.minecraft.block.Blocks; import net.minecraft.registry.RegistryCodecs; import net.minecraft.registry.RegistryKeys; import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.server.world.ServerWorld; import net.minecraft.util.math.BlockPos; import net.minecraft.world.ChunkRegion; 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.FixedBiomeSource; import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.WorldChunk; import net.minecraft.world.gen.GenerationStep; import net.minecraft.world.gen.StructureAccessor; 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); int roll = (int)(Math.abs(lightRoll) % 100); if (roll < 98) { - // 98% ON (flickering determined by block entity in generateFeatures) ceilingBlock = Szar.BACKROOMS_LIGHT.getDefaultState() .with(BackroomsLightBlock.LIGHT_STATE, BackroomsLightBlock.LightState.ON); } else { - // 2% missing โ€” ceiling block, no light ceilingBlock = Szar.CEILING.getDefaultState(); } } 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); BlockState lightState = world.getBlockState(mutable); if (lightState.getBlock() instanceof BackroomsLightBlock) { - // Always re-set to force block entity creation โ€” no null check needed world.setBlockState(mutable, lightState, Block.NOTIFY_ALL); BlockPos immutable = mutable.toImmutable(); if (world.getBlockEntity(immutable) instanceof BackroomsLightBlockEntity light) { long typeRoll = hash(chunkX + lx, chunkZ + lz); light.isFlickering = (Math.abs(typeRoll) % 10) >= 8; - light.flickerOffset = (int)(Math.abs(hash(chunkX + lx, chunkZ + lz)) % 100); - light.flickerTimer = light.flickerOffset; + light.flickerOffset = (int)(Math.abs(hash(chunkX + lx, chunkZ + lz)) % 1000); 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(); } } @@ -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); + } } } \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/BackroomsLightBlock.java b/src/main/java/dev/tggamesyt/szar/BackroomsLightBlock.java index 6f71a31..9015561 100644 --- a/src/main/java/dev/tggamesyt/szar/BackroomsLightBlock.java +++ b/src/main/java/dev/tggamesyt/szar/BackroomsLightBlock.java @@ -8,19 +8,18 @@ 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; +import java.util.Objects; + public class BackroomsLightBlock extends BlockWithEntity { public enum LightState implements StringIdentifiable { - ON, OFF; + ON, OFF, FLICKERING; @Override - public String asString() { - return name().toLowerCase(); - } + public String asString() { return name().toLowerCase(); } } public static final EnumProperty LIGHT_STATE = @@ -52,13 +51,8 @@ public class BackroomsLightBlock extends BlockWithEntity { World world, BlockState state, BlockEntityType type) { if (world.isClient) return null; return type == Szar.BACKROOMS_LIGHT_ENTITY - ? (w, pos, s, be) -> BackroomsLightBlockEntity.tick( - w, pos, s, (BackroomsLightBlockEntity) be) + ? (w, pos, s, be) -> + BackroomsLightBlockEntity.tick(w, pos, s, (BackroomsLightBlockEntity) be) : null; } - - // Light level based on state - public static int getLightLevel(BlockState state) { - return state.get(LIGHT_STATE) == LightState.ON ? 15 : 0; - } } \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/BackroomsLightBlockEntity.java b/src/main/java/dev/tggamesyt/szar/BackroomsLightBlockEntity.java index 9c09c5a..6a8ce1d 100644 --- a/src/main/java/dev/tggamesyt/szar/BackroomsLightBlockEntity.java +++ b/src/main/java/dev/tggamesyt/szar/BackroomsLightBlockEntity.java @@ -10,13 +10,12 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; public class BackroomsLightBlockEntity extends BlockEntity { - public float brightness = 1.0f; // 0.0 = black, 1.0 = full bright - // Random offset so each light flickers at different times + public boolean isFlickering = false; + // Used by renderer โ€” offset into the global flicker timer public int flickerOffset = 0; - // How many ticks until next state toggle during flicker - public int flickerTimer = 0; + // Visual brightness โ€” only changed during blackout, not during flicker + public float brightness = 1.0f; private boolean initialized = false; - public boolean isFlickering = false; // true for lights generated as flickering type public BackroomsLightBlockEntity(BlockPos pos, BlockState 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, BackroomsLightBlockEntity entity) { if (!entity.initialized) { - entity.flickerOffset = world.random.nextInt(100); + entity.flickerOffset = world.random.nextInt(1000); 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); nbt.putFloat("Brightness", brightness); + nbt.putBoolean("Initialized", initialized); + nbt.putBoolean("IsFlickering", isFlickering); } @Override public void readNbt(NbtCompound nbt) { super.readNbt(nbt); flickerOffset = nbt.getInt("FlickerOffset"); - flickerTimer = nbt.getInt("FlickerTimer"); - initialized = nbt.getBoolean("Initialized"); brightness = nbt.getFloat("Brightness"); + initialized = nbt.getBoolean("Initialized"); + isFlickering = nbt.getBoolean("IsFlickering"); } @Override - public NbtCompound toInitialChunkDataNbt() { - return createNbt(); - } + public NbtCompound toInitialChunkDataNbt() { return createNbt(); } @Override public Packet toUpdatePacket() { diff --git a/src/main/java/dev/tggamesyt/szar/BackroomsLightManager.java b/src/main/java/dev/tggamesyt/szar/BackroomsLightManager.java index cbd482e..6b9c2b4 100644 --- a/src/main/java/dev/tggamesyt/szar/BackroomsLightManager.java +++ b/src/main/java/dev/tggamesyt/szar/BackroomsLightManager.java @@ -1,16 +1,15 @@ package dev.tggamesyt.szar; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.minecraft.block.Block; 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; import net.minecraft.world.chunk.WorldChunk; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; public class BackroomsLightManager { @@ -19,6 +18,7 @@ public class BackroomsLightManager { public static GlobalEvent currentEvent = GlobalEvent.NONE; public static int eventTimer = 0; public static int cooldownTimer = 3600; + public static int globalFlickerTimer = 0; private static final int FLICKER_DURATION_MIN = 60; private static final int FLICKER_DURATION_MAX = 160; @@ -34,26 +34,46 @@ public class BackroomsLightManager { ServerWorld backrooms = server.getWorld(Szar.BACKROOMS_KEY); 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) { eventTimer--; - if (eventTimer <= 0) { - endEvent(backrooms); - } + if (eventTimer <= 0) endEvent(backrooms); } else { cooldownTimer--; if (cooldownTimer <= 0) { int roll = backrooms.random.nextInt(100); - if (roll < 30) { - startBlackout(backrooms); - } else if (roll < 63) { - startFlicker(backrooms); - } else { - cooldownTimer = EVENT_COOLDOWN; - } + if (roll < 30) startBlackout(backrooms); + else if (roll < 63) startFlicker(backrooms); + else cooldownTimer = EVENT_COOLDOWN; } } } + private static void startFlicker(ServerWorld world) { currentEvent = GlobalEvent.FLICKER; eventTimer = FLICKER_DURATION_MIN + world.random.nextInt( @@ -65,65 +85,66 @@ public class BackroomsLightManager { currentEvent = GlobalEvent.BLACKOUT; eventTimer = BLACKOUT_MIN + world.random.nextInt(BLACKOUT_MAX - BLACKOUT_MIN); cooldownTimer = EVENT_COOLDOWN; - // Set all lights to brightness 0 - forEachLightEntity(world, entity -> { - entity.brightness = 0.0f; - entity.markDirty(); + forEachLightEntity(world, (entity, state, pos) -> { + if (state.get(BackroomsLightBlock.LIGHT_STATE) != BackroomsLightBlock.LightState.OFF) { + world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE, + BackroomsLightBlock.LightState.OFF), Block.NOTIFY_ALL); + } }); } private static void endEvent(ServerWorld world) { - // Restore all lights to full brightness - forEachLightEntity(world, entity -> { - entity.brightness = 1.0f; - entity.markDirty(); - }); currentEvent = GlobalEvent.NONE; eventTimer = 0; cooldownTimer = EVENT_COOLDOWN; - } - - // Called per-light from BackroomsLightBlockEntity.tick - public static void tickLight(World world, BlockPos pos, BlockState state, - BackroomsLightBlockEntity entity) { - if (currentEvent == GlobalEvent.BLACKOUT) return; - - BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE); - if (ls == BackroomsLightBlock.LightState.OFF) return; - - 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(); + forEachLightEntity(world, (entity, state, pos) -> { + BackroomsLightBlock.LightState ls = state.get(BackroomsLightBlock.LIGHT_STATE); + // Restore any light that was turned off by an event + if (ls == BackroomsLightBlock.LightState.OFF) { + BackroomsLightBlock.LightState restore = entity.isFlickering + ? BackroomsLightBlock.LightState.FLICKERING + : BackroomsLightBlock.LightState.ON; + world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE, + restore), Block.NOTIFY_ALL); } - 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, - Consumer consumer) { + // Called from generateFeatures when a new chunk loads โ€” apply current event state + 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)) { int cx = chunk.getPos().getStartX(); int cz = chunk.getPos().getStartZ(); @@ -131,9 +152,12 @@ public class BackroomsLightManager { for (int lx = 0; lx < 16; lx++) { for (int lz = 0; lz < 16; 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) { - consumer.accept(entity); + consumer.accept(entity, state, immutable); } } } @@ -159,21 +183,23 @@ public class BackroomsLightManager { } public static void forceRestoreAllLights(ServerWorld world) { - forEachLightEntity(world, entity -> { - entity.brightness = 1.0f; - entity.markDirty(); - }); currentEvent = GlobalEvent.NONE; eventTimer = 0; 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) { - forEachLightEntity(world, entity -> { - entity.brightness = 0.0f; - entity.markDirty(); - }); currentEvent = GlobalEvent.BLACKOUT; eventTimer = 3600; + forEachLightEntity(world, (entity, state, pos) -> + world.setBlockState(pos, state.with(BackroomsLightBlock.LIGHT_STATE, + BackroomsLightBlock.LightState.OFF), Block.NOTIFY_ALL)); } } \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/BeerItem.java b/src/main/java/dev/tggamesyt/szar/BeerItem.java new file mode 100644 index 0000000..5e6992f --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/BeerItem.java @@ -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 use(World world, PlayerEntity user, Hand hand) { + return ItemUsage.consumeHeldItem(world, user, hand); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/DrunkEffect.java b/src/main/java/dev/tggamesyt/szar/DrunkEffect.java new file mode 100644 index 0000000..d726f57 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/DrunkEffect.java @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/SmilerEffectManager.java b/src/main/java/dev/tggamesyt/szar/SmilerEffectManager.java new file mode 100644 index 0000000..6b25e96 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/SmilerEffectManager.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/SmilerEntity.java b/src/main/java/dev/tggamesyt/szar/SmilerEntity.java new file mode 100644 index 0000000..e037684 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/SmilerEntity.java @@ -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 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; } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/SmilerSpawnManager.java b/src/main/java/dev/tggamesyt/szar/SmilerSpawnManager.java new file mode 100644 index 0000000..76aedcf --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/SmilerSpawnManager.java @@ -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> cooldowns = new HashMap<>(); + // Per player, per type: is there currently a live smiler of this type + private static final Map> 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 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 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 active = activeSmilers.get(uuid); + if (active != null) active.remove(type); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/SmilerType.java b/src/main/java/dev/tggamesyt/szar/SmilerType.java new file mode 100644 index 0000000..4a17a77 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/SmilerType.java @@ -0,0 +1,5 @@ +package dev.tggamesyt.szar; + +public enum SmilerType { + EYES, SCARY, REAL +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/Szar.java b/src/main/java/dev/tggamesyt/szar/Szar.java index 713f365..42484f1 100644 --- a/src/main/java/dev/tggamesyt/szar/Szar.java +++ b/src/main/java/dev/tggamesyt/szar/Szar.java @@ -6,6 +6,8 @@ import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; 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.ServerTickEvents; 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.CEILING_ITEM); entries.add(Szar.PLASTIC_ITEM); + entries.add(Szar.BACKROOMS_LIGHT_ITEM); entries.add(Szar.BEAN); entries.add(Szar.CAN_OF_BEANS); entries.add(Szar.ALMOND_WATER); @@ -389,6 +392,7 @@ public class Szar implements ModInitializer { entries.add(Szar.WEED_ITEM); entries.add(Szar.WEED_JOINT_ITEM); entries.add(Szar.CHEMICAL_WORKBENCH_ITEM); + entries.add(Szar.BEER); // war guys entries.add(Szar.HITTER_SPAWNEGG); entries.add(Szar.NAZI_SPAWNEGG); @@ -793,6 +797,10 @@ public class Szar implements ModInitializer { TERRORIST_ENTITY_TYPE, IslamTerrorist.createAttributes() ); + FabricDefaultAttributeRegistry.register( + SMILER_ENTITY_TYPE, + SmilerEntity.createAttributes() + ); SpawnRestriction.register( Szar.PoliceEntityType, SpawnRestriction.Location.ON_GROUND, // spawn on solid blocks @@ -1175,6 +1183,30 @@ public class Szar implements ModInitializer { ); BackroomsBarrelManager.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 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( 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 WALL_BLOCK_ENTITY = Registry.register( 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( 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( 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( 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( Registries.ITEM, @@ -1805,14 +1837,18 @@ public class Szar implements ModInitializer { public static final Item ALMOND_WATER = Registry.register( Registries.ITEM, 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( Registries.BLOCK, new Identifier(MOD_ID, "backrooms_light"), new BackroomsLightBlock(FabricBlockSettings.create() - .nonOpaque() - .luminance(state -> BackroomsLightBlock.getLightLevel(state)) - .strength(0.3f)) + .luminance(state -> { + return switch (state.get(BackroomsLightBlock.LIGHT_STATE)) { + case ON, FLICKERING -> 15; + case OFF -> 0; + }; + }) + .strength(-1.0f, 3600000f)) ); public static final BlockItem BACKROOMS_LIGHT_ITEM = Registry.register( @@ -1827,6 +1863,25 @@ public class Szar implements ModInitializer { FabricBlockEntityTypeBuilder.create(BackroomsLightBlockEntity::new, BACKROOMS_LIGHT).build() ); + + public static final EntityType 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 = SoundEvent.of(new Identifier(MOD_ID, "baiter")); public static final Item BAITER_DISC = Registry.register( @@ -2176,5 +2231,41 @@ public class Szar implements ModInitializer { player.getWorld().playSound(null, player.getBlockPos(), 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"); + } } diff --git a/src/main/resources/assets/szar/blockstates/backrooms_light.json b/src/main/resources/assets/szar/blockstates/backrooms_light.json index fa5149c..3babcca 100644 --- a/src/main/resources/assets/szar/blockstates/backrooms_light.json +++ b/src/main/resources/assets/szar/blockstates/backrooms_light.json @@ -1,8 +1,7 @@ { "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" } + "light_state=on": { "model": "szar:block/backrooms_light_on" }, + "light_state=off": { "model": "szar:block/backrooms_light_off" }, + "light_state=flickering": { "model": "szar:block/backrooms_light_on" } } } \ No newline at end of file diff --git a/src/main/resources/assets/szar/lang/en_us.json b/src/main/resources/assets/szar/lang/en_us.json index cc442e8..9d50082 100644 --- a/src/main/resources/assets/szar/lang/en_us.json +++ b/src/main/resources/assets/szar/lang/en_us.json @@ -149,5 +149,8 @@ "item.szar.bean": "Bean", "item.szar.can_of_beans": "Can of Beans", "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" } diff --git a/src/main/resources/assets/szar/models/item/beer.json b/src/main/resources/assets/szar/models/item/beer.json new file mode 100644 index 0000000..a6f93cd --- /dev/null +++ b/src/main/resources/assets/szar/models/item/beer.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "szar:item/beer" + } +} diff --git a/src/main/resources/assets/szar/sounds.json b/src/main/resources/assets/szar/sounds.json index 7a4d05d..b6dde61 100644 --- a/src/main/resources/assets/szar/sounds.json +++ b/src/main/resources/assets/szar/sounds.json @@ -202,5 +202,29 @@ "stream": true } ] + }, + "real": { + "sounds": [ + { + "name": "szar:real", + "stream": true + } + ] + }, + "scary": { + "sounds": [ + { + "name": "szar:scary", + "stream": true + } + ] + }, + "flashbang": { + "sounds": [ + { + "name": "szar:flashbang", + "stream": true + } + ] } } diff --git a/src/main/resources/assets/szar/sounds/flashbang.ogg b/src/main/resources/assets/szar/sounds/flashbang.ogg new file mode 100644 index 0000000..ede1631 Binary files /dev/null and b/src/main/resources/assets/szar/sounds/flashbang.ogg differ diff --git a/src/main/resources/assets/szar/sounds/real.ogg b/src/main/resources/assets/szar/sounds/real.ogg new file mode 100644 index 0000000..5d67d25 Binary files /dev/null and b/src/main/resources/assets/szar/sounds/real.ogg differ diff --git a/src/main/resources/assets/szar/sounds/scary.ogg b/src/main/resources/assets/szar/sounds/scary.ogg new file mode 100644 index 0000000..baa934a Binary files /dev/null and b/src/main/resources/assets/szar/sounds/scary.ogg differ diff --git a/src/main/resources/assets/szar/textures/entity/eyes.png b/src/main/resources/assets/szar/textures/entity/eyes.png new file mode 100644 index 0000000..0a3585d Binary files /dev/null and b/src/main/resources/assets/szar/textures/entity/eyes.png differ diff --git a/src/main/resources/assets/szar/textures/entity/real.png b/src/main/resources/assets/szar/textures/entity/real.png new file mode 100644 index 0000000..23ccdbd Binary files /dev/null and b/src/main/resources/assets/szar/textures/entity/real.png differ diff --git a/src/main/resources/assets/szar/textures/entity/scary.png b/src/main/resources/assets/szar/textures/entity/scary.png new file mode 100644 index 0000000..61e742f Binary files /dev/null and b/src/main/resources/assets/szar/textures/entity/scary.png differ diff --git a/src/main/resources/assets/szar/textures/item/beer.png b/src/main/resources/assets/szar/textures/item/beer.png new file mode 100644 index 0000000..73e51f6 Binary files /dev/null and b/src/main/resources/assets/szar/textures/item/beer.png differ diff --git a/src/main/resources/assets/szar/textures/mob_effect/drunk.png b/src/main/resources/assets/szar/textures/mob_effect/drunk.png new file mode 100644 index 0000000..73e51f6 Binary files /dev/null and b/src/main/resources/assets/szar/textures/mob_effect/drunk.png differ diff --git a/src/main/resources/data/szar/recipes/beer.json b/src/main/resources/data/szar/recipes/beer.json new file mode 100644 index 0000000..da13a28 --- /dev/null +++ b/src/main/resources/data/szar/recipes/beer.json @@ -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 + } +} \ No newline at end of file