diff --git a/gradle.properties b/gradle.properties index 88f5d53..b928391 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.4.14.1 +mod_version=26.4.15 maven_group=dev.tggamesyt archives_base_name=szar # Dependencies diff --git a/src/client/java/dev/tggamesyt/szar/client/SuperBeaconBlockEntityRenderer.java b/src/client/java/dev/tggamesyt/szar/client/SuperBeaconBlockEntityRenderer.java new file mode 100644 index 0000000..3c85fdc --- /dev/null +++ b/src/client/java/dev/tggamesyt/szar/client/SuperBeaconBlockEntityRenderer.java @@ -0,0 +1,65 @@ +package dev.tggamesyt.szar.client; + +import dev.tggamesyt.szar.Szar; +import dev.tggamesyt.szar.SuperBeaconBlockEntity; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.block.entity.BeaconBlockEntityRenderer; +import net.minecraft.client.render.block.entity.BlockEntityRenderer; +import net.minecraft.client.render.block.entity.BlockEntityRendererFactory; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; + +public class SuperBeaconBlockEntityRenderer implements BlockEntityRenderer { + + // Path: assets/szar/textures/entity/beacon_beam.png + public static final Identifier BEAM_TEXTURE = new Identifier(Szar.MOD_ID, "textures/entity/beacon_beam.png"); + + private static boolean loggedOnce = false; + + public SuperBeaconBlockEntityRenderer(BlockEntityRendererFactory.Context ctx) { + Szar.LOGGER.info("[Szar] SuperBeaconBlockEntityRenderer constructed"); + } + + @Override + public void render(SuperBeaconBlockEntity be, float tickDelta, MatrixStack matrices, + VertexConsumerProvider vertexConsumers, int light, int overlay) { + + if (!loggedOnce) { + Szar.LOGGER.info("[Szar] render() called once. level={}", be.getBeaconLevel()); + loggedOnce = true; + } + + if (be.getBeaconLevel() <= 0) return; + if (be.getWorld() == null) return; + + long worldTime = be.getWorld().getTime(); + + // Vanilla beacon beam, 1.20.1 signature: + // public static void renderBeam(MatrixStack, VertexConsumerProvider, Identifier, + // float tickDelta, float heightScale, long worldTime, int yOffset, int maxY, + // float[] color, float innerRadius, float outerRadius) + BeaconBlockEntityRenderer.renderBeam( + matrices, + vertexConsumers, + BEAM_TEXTURE, + tickDelta, + 1.0f, + worldTime, + 0, + 1024, + new float[]{1.0f, 1.0f, 1.0f}, + 0.2f, + 0.25f + ); + } + + @Override + public boolean rendersOutsideBoundingBox(SuperBeaconBlockEntity blockEntity) { + return true; + } + + @Override + public int getRenderDistance() { + return 256; + } +} \ No newline at end of file diff --git a/src/client/java/dev/tggamesyt/szar/client/SuperBeaconScreen.java b/src/client/java/dev/tggamesyt/szar/client/SuperBeaconScreen.java new file mode 100644 index 0000000..0ceb70f --- /dev/null +++ b/src/client/java/dev/tggamesyt/szar/client/SuperBeaconScreen.java @@ -0,0 +1,113 @@ +package dev.tggamesyt.szar.client; + +import com.mojang.blaze3d.systems.RenderSystem; +import dev.tggamesyt.szar.Szar; +import dev.tggamesyt.szar.SuperBeaconBlockEntity; +import dev.tggamesyt.szar.SuperBeaconScreenHandler; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public class SuperBeaconScreen extends HandledScreen { + + private static final Identifier TEXTURE = new Identifier(Szar.MOD_ID, "textures/gui/super_beacon.png"); + + private static final int GUI_WIDTH = 176; + private static final int GUI_HEIGHT = 222; + + private final ButtonWidget[] activateButtons = new ButtonWidget[4]; + + public SuperBeaconScreen(SuperBeaconScreenHandler handler, PlayerInventory inventory, Text title) { + super(handler, inventory, title); + this.backgroundWidth = GUI_WIDTH; + this.backgroundHeight = GUI_HEIGHT; + } + + @Override + protected void init() { + super.init(); + this.titleX = (this.backgroundWidth - this.textRenderer.getWidth(this.title)) / 2; + this.titleY = 5; + this.playerInventoryTitleY = this.backgroundHeight - 94; + + for (int row = 0; row < 4; row++) { + final int r = row; + int buttonX = this.x + 108; + int buttonY = this.y + 14 + row * 28; + + activateButtons[row] = ButtonWidget.builder( + getButtonText(row), + button -> onActivateClicked(r) + ).dimensions(buttonX, buttonY, 56, 20).build(); + + addDrawableChild(activateButtons[row]); + } + } + + private Text getButtonText(int row) { + int level = handler.getBeaconLevel(); + if (level < row + 1) return Text.literal("Locked"); + return handler.isRowActive(row) ? Text.literal("Deactivate") : Text.literal("Activate"); + } + + private void onActivateClicked(int row) { + Szar.LOGGER.info("[Szar][CLIENT] activate button clicked: row={}, pos={}", row, handler.getPos()); + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeBlockPos(handler.getPos()); + buf.writeInt(row); + ClientPlayNetworking.send(Szar.ACTIVATE_ROW_PACKET, buf); + Szar.LOGGER.info("[Szar][CLIENT] packet sent"); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + renderBackground(context); + super.render(context, mouseX, mouseY, delta); + drawMouseoverTooltip(context, mouseX, mouseY); + + // Live updates + for (int row = 0; row < 4; row++) { + int level = handler.getBeaconLevel(); + // Always clickable so we can debug — actual lock check is server-side + activateButtons[row].active = (level >= row + 1); + activateButtons[row].setMessage(getButtonText(row)); + } + + // Indicators + timer + for (int row = 0; row < 4; row++) { + int indicatorX = this.x + 26; + int indicatorY = this.y + 20 + row * 28; + + int level = handler.getBeaconLevel(); + int color; + if (level < row + 1) color = 0xFF555555; + else if (handler.isRowActive(row)) color = 0xFF00FF00; + else color = 0xFFFF0000; + + context.fill(indicatorX, indicatorY, indicatorX + 12, indicatorY + 12, color); + + if (handler.isRowActive(row)) { + int timer = handler.getFuelTimer(row); + int remainingTicks = SuperBeaconBlockEntity.FUEL_INTERVAL - timer; + int remainingSec = Math.max(0, remainingTicks / 20); + int min = remainingSec / 60; + int sec = remainingSec % 60; + String timeStr = String.format("%d:%02d", min, sec); + context.drawText(this.textRenderer, timeStr, + indicatorX - 2, indicatorY + 14, 0xFFFFFF, true); + } + } + } + + @Override + protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) { + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + context.drawTexture(TEXTURE, this.x, this.y, 0, 0, backgroundWidth, backgroundHeight, GUI_WIDTH, GUI_HEIGHT); + } +} \ 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 f3915a9..ea5150a 100644 --- a/src/client/java/dev/tggamesyt/szar/client/SzarClient.java +++ b/src/client/java/dev/tggamesyt/szar/client/SzarClient.java @@ -12,6 +12,7 @@ import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry; import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry; import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; @@ -616,6 +617,10 @@ public class SzarClient implements ClientModInitializer { Szar.CANNABIS_BLOCK, RenderLayer.getCutout() ); + BlockRenderLayerMap.INSTANCE.putBlock( + SUPER_BEACON_BLOCK, + RenderLayer.getCutout() + ); BlockRenderLayerMap.INSTANCE.putBlock( CONNECT_FOUR_BLOCK, RenderLayer.getCutout() @@ -738,6 +743,8 @@ public class SzarClient implements ClientModInitializer { (dispatcher, registryAccess) -> PanoramaClientCommand.register(dispatcher) ); } + HandledScreens.register(Szar.SUPER_BEACON_SCREEN_HANDLER, SuperBeaconScreen::new); + BlockEntityRendererFactories.register(Szar.SUPER_BEACON_BLOCK_ENTITY, SuperBeaconBlockEntityRenderer::new); } private boolean isDebugEnabled() { diff --git a/src/main/java/dev/tggamesyt/szar/SuperBeaconBlock.java b/src/main/java/dev/tggamesyt/szar/SuperBeaconBlock.java new file mode 100644 index 0000000..32f7ab1 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/SuperBeaconBlock.java @@ -0,0 +1,50 @@ +package dev.tggamesyt.szar; + +import net.minecraft.block.*; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.ItemScatterer; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public class SuperBeaconBlock extends BlockWithEntity implements BlockEntityProvider { + + public SuperBeaconBlock(Settings settings) { super(settings); } + + @Override public BlockRenderType getRenderType(BlockState state) { return BlockRenderType.MODEL; } + + @Nullable @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return new SuperBeaconBlockEntity(pos, state); + } + + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { + if (!world.isClient()) { + NamedScreenHandlerFactory factory = state.createScreenHandlerFactory(world, pos); + if (factory != null) player.openHandledScreen(factory); + } + return ActionResult.SUCCESS; + } + + @Override + public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { + if (!state.isOf(newState.getBlock())) { + BlockEntity be = world.getBlockEntity(pos); + if (be instanceof SuperBeaconBlockEntity sbe) ItemScatterer.spawn(world, pos, sbe); + super.onStateReplaced(state, world, pos, newState, moved); + } + } + + @Nullable @Override + public BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) { + return checkType(type, Szar.SUPER_BEACON_BLOCK_ENTITY, SuperBeaconBlockEntity::tick); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/SuperBeaconBlockEntity.java b/src/main/java/dev/tggamesyt/szar/SuperBeaconBlockEntity.java new file mode 100644 index 0000000..976707e --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/SuperBeaconBlockEntity.java @@ -0,0 +1,383 @@ +package dev.tggamesyt.szar; + +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventories; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.potion.PotionUtil; +import net.minecraft.registry.tag.BlockTags; +import net.minecraft.screen.PropertyDelegate; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import java.util.*; + +public class SuperBeaconBlockEntity extends BlockEntity implements Inventory, ExtendedScreenHandlerFactory { + + public static final int FUEL_INTERVAL = 27000; + + private static final Set PERSISTENT_EFFECTS = new HashSet<>(); + static { + PERSISTENT_EFFECTS.add(StatusEffects.NAUSEA); + PERSISTENT_EFFECTS.add(StatusEffects.HEALTH_BOOST); + } + + private final DefaultedList inventory = DefaultedList.ofSize(8, ItemStack.EMPTY); + private final boolean[] rowActive = new boolean[4]; + private final int[] fuelTimers = new int[4]; + private int beaconLevel = 0; + + // 4 unlock thresholds. Row N unlocks at level >= N+1. + // After lvl >= 4 every additional layer just expands range. + // Range formula: 2^(level + 5), capped at level 4 = 512 for unlock check, but range keeps growing. + + private final Map>> persistentTracking = new HashMap<>(); + + private final PropertyDelegate propertyDelegate = new PropertyDelegate() { + @Override public int get(int i) { + return switch (i) { + case 0 -> beaconLevel; + case 1, 2, 3, 4 -> rowActive[i - 1] ? 1 : 0; + case 5, 6, 7, 8 -> fuelTimers[i - 5]; + default -> 0; + }; + } + @Override public void set(int i, int v) { + switch (i) { + case 0 -> beaconLevel = v; + case 1, 2, 3, 4 -> rowActive[i - 1] = v != 0; + case 5, 6, 7, 8 -> fuelTimers[i - 5] = v; + } + } + @Override public int size() { return 9; } + }; + + public PropertyDelegate getPropertyDelegate() { return propertyDelegate; } + + public SuperBeaconBlockEntity(BlockPos pos, BlockState state) { + super(Szar.SUPER_BEACON_BLOCK_ENTITY, pos, state); + } + + // No level cap - check from 1 up to limit (until invalid layer found) + public int computeBeaconLevel() { + if (world == null) return 0; + int levels = 0; + // Hard upper cap of 320 just to prevent infinite loops; world height ~384 max + for (int layer = 1; layer <= 320; layer++) { + int y = pos.getY() - layer; + // Stop if outside world + if (y < world.getBottomY()) break; + + boolean valid = true; + for (int x = -layer; x <= layer && valid; x++) { + for (int z = -layer; z <= layer; z++) { + BlockState bs = world.getBlockState(new BlockPos(pos.getX() + x, y, pos.getZ() + z)); + if (!bs.isIn(BlockTags.BEACON_BASE_BLOCKS)) { valid = false; break; } + } + } + if (valid) levels = layer; else break; + } + return levels; + } + + public int getBeaconLevel() { return beaconLevel; } + public boolean isRowActive(int row) { return row >= 0 && row < 4 && rowActive[row]; } + public int getFuelTimer(int row) { return row >= 0 && row < 4 ? fuelTimers[row] : 0; } + + // 2^(level+5): lvl1=64, lvl2=128, lvl3=256, lvl4=512, lvl5=1024 ... no cap + public double getEffectRadius() { + if (beaconLevel <= 0) return 0.0; + // Use long math then cast to double to avoid int overflow at high levels + return (double)(1L << (beaconLevel + 5)); + } + + public static void tick(World world, BlockPos pos, BlockState state, SuperBeaconBlockEntity be) { + if (world.isClient()) return; + + if (world.getTime() % 20 == 0) { + int newLevel = be.computeBeaconLevel(); + if (newLevel != be.beaconLevel) { + be.beaconLevel = newLevel; + be.markDirty(); + } + } + + for (int row = 0; row < 4; row++) { + if (!be.rowActive[row]) continue; + + // Row N requires level >= N+1 + if (be.beaconLevel < row + 1 || be.getStack(row * 2).isEmpty()) { + be.deactivateRow(row); + continue; + } + + be.fuelTimers[row]++; + if (be.fuelTimers[row] >= FUEL_INTERVAL) { + ItemStack fuelItem = be.getStack(row * 2 + 1); + if (fuelItem.isEmpty()) { be.deactivateRow(row); continue; } + fuelItem.decrement(1); + be.fuelTimers[row] = 0; + if (fuelItem.isEmpty()) be.setStack(row * 2 + 1, ItemStack.EMPTY); + be.markDirty(); + } + + } + + if (world.getTime() % 80 == 0) be.applyAllEffects(); + if (world.getTime() % 100 == 0) be.checkPersistentRangeAll(); + if (world.getTime() % 100 == 0) be.cleanupPersistentTracking(); + } + + // Collects effects from all active rows, summing amplifiers when same effect appears + // in multiple rows. Stacking: lvl1 + lvl1 = lvl2 → amp_sum = amp1 + amp2 + 1. + private Map collectCombinedEffects() { + Map combined = new HashMap<>(); + for (int row = 0; row < 4; row++) { + if (!rowActive[row]) continue; + ItemStack effectItem = getStack(row * 2); + List effects = getEffectsFromItem(effectItem); + for (StatusEffectInstance e : effects) { + StatusEffect type = e.getEffectType(); + int amp = e.getAmplifier(); + if (combined.containsKey(type)) { + // Stack: each row contributes (amp+1) levels + combined.put(type, combined.get(type) + amp + 1); + } else { + combined.put(type, amp); + } + } + } + return combined; + } + + private void applyAllEffects() { + if (world == null) return; + Map combined = collectCombinedEffects(); + if (combined.isEmpty()) return; + + double radius = getEffectRadius(); + Box box = new Box(pos).expand(radius); + List players = world.getEntitiesByClass(PlayerEntity.class, box, p -> true); + + for (PlayerEntity player : players) { + for (Map.Entry e : combined.entrySet()) { + StatusEffect type = e.getKey(); + int amp = Math.min(e.getValue(), 255); // amp cap + + if (PERSISTENT_EFFECTS.contains(type)) { + applyPersistent(player, type, amp); + } else { + player.addStatusEffect(new StatusEffectInstance( + type, 120, amp, true, true, true)); + } + } + } + } + + // Persistent tracking: key by effect type, store current applied amplifier + // so we re-apply when amplifier changes (row added/removed) + private void applyPersistent(PlayerEntity player, StatusEffect type, int amp) { + UUID uuid = player.getUuid(); + Map> bucket = persistentTracking.computeIfAbsent(uuid, k -> new HashMap<>()); + // Use single bucket key 0 to track all persistent effects per player + Set tracked = bucket.computeIfAbsent(0, k -> new HashSet<>()); + + StatusEffectInstance current = player.getStatusEffect(type); + if (current == null || current.getAmplifier() != amp) { + player.removeStatusEffect(type); + player.addStatusEffect(new StatusEffectInstance( + type, Integer.MAX_VALUE, amp, true, true, true)); + tracked.add(type); + } + } + + private void checkPersistentRangeAll() { + if (world == null) return; + double radius = getEffectRadius(); + Vec3d center = new Vec3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + Map combined = collectCombinedEffects(); + + for (Map.Entry>> entry : persistentTracking.entrySet()) { + Set tracked = entry.getValue().get(0); + if (tracked == null || tracked.isEmpty()) continue; + + PlayerEntity player = world.getPlayerByUuid(entry.getKey()); + if (player == null) continue; + + boolean outOfRange = player.getPos().distanceTo(center) > radius; + + // Remove effects that are out of range OR no longer provided by any active row + Iterator it = tracked.iterator(); + while (it.hasNext()) { + StatusEffect e = it.next(); + if (outOfRange || !combined.containsKey(e)) { + player.removeStatusEffect(e); + it.remove(); + } + } + } + } + + private void cleanupPersistentTracking() { + persistentTracking.entrySet().removeIf(entry -> { + entry.getValue().entrySet().removeIf(e -> e.getValue().isEmpty()); + return entry.getValue().isEmpty(); + }); + } + + private void removePersistentForRow(int row) { + // Recompute combined effects (without this row, since it's already deactivated by caller) + // and remove any tracked effects no longer provided. + if (world == null) return; + Map combined = collectCombinedEffects(); + + for (Map.Entry>> entry : persistentTracking.entrySet()) { + Set tracked = entry.getValue().get(0); + if (tracked == null) continue; + PlayerEntity player = world.getPlayerByUuid(entry.getKey()); + if (player == null) { tracked.clear(); continue; } + + Iterator it = tracked.iterator(); + while (it.hasNext()) { + StatusEffect e = it.next(); + if (!combined.containsKey(e)) { + player.removeStatusEffect(e); + it.remove(); + } + } + } + } + + private List getEffectsFromItem(ItemStack stack) { + List potionEffects = PotionUtil.getPotionEffects(stack); + if (!potionEffects.isEmpty()) return potionEffects; + if (stack.getItem().getFoodComponent() != null) { + return stack.getItem().getFoodComponent().getStatusEffects().stream() + .map(pair -> pair.getFirst()).toList(); + } + return List.of(); + } + + public static boolean isValidFuel(ItemStack stack) { + return stack.isOf(Items.IRON_INGOT) || stack.isOf(Items.GOLD_INGOT) || + stack.isOf(Items.DIAMOND) || stack.isOf(Items.EMERALD) || + stack.isOf(Items.NETHERITE_INGOT); + } + + public static boolean isValidEffectItem(ItemStack stack) { + if (!PotionUtil.getPotionEffects(stack).isEmpty()) return true; + if (stack.getItem().getFoodComponent() != null) { + return !stack.getItem().getFoodComponent().getStatusEffects().isEmpty(); + } + return false; + } + + public void toggleRow(int row) { + Szar.LOGGER.info("[Szar] toggleRow called: row={}, level={}, active={}", row, beaconLevel, rowActive[row]); + if (row < 0 || row >= 4) { Szar.LOGGER.warn("[Szar] row out of bounds"); return; } + if (beaconLevel < row + 1) { Szar.LOGGER.warn("[Szar] row locked, need level {}", row + 1); return; } + + if (rowActive[row]) { + deactivateRow(row); + Szar.LOGGER.info("[Szar] deactivated row {}", row); + } else { + ItemStack effectItem = getStack(row * 2); + ItemStack fuelItem = getStack(row * 2 + 1); + if (effectItem.isEmpty()) { Szar.LOGGER.warn("[Szar] effect slot empty"); return; } + if (fuelItem.isEmpty()) { Szar.LOGGER.warn("[Szar] fuel slot empty"); return; } + if (!isValidEffectItem(effectItem)) { Szar.LOGGER.warn("[Szar] effect item invalid: {}", effectItem); return; } + if (!isValidFuel(fuelItem)) { Szar.LOGGER.warn("[Szar] fuel item invalid: {}", fuelItem); return; } + + fuelItem.decrement(1); + if (fuelItem.isEmpty()) setStack(row * 2 + 1, ItemStack.EMPTY); + rowActive[row] = true; + fuelTimers[row] = 0; + markDirty(); + Szar.LOGGER.info("[Szar] activated row {}", row); + } + } + + private void deactivateRow(int row) { + rowActive[row] = false; + fuelTimers[row] = 0; + removePersistentForRow(row); + markDirty(); + } + + @Override public int size() { return 8; } + @Override public boolean isEmpty() { return inventory.stream().allMatch(ItemStack::isEmpty); } + @Override public ItemStack getStack(int slot) { return inventory.get(slot); } + @Override public ItemStack removeStack(int slot, int amount) { + ItemStack r = Inventories.splitStack(inventory, slot, amount); markDirty(); return r; + } + @Override public ItemStack removeStack(int slot) { return Inventories.removeStack(inventory, slot); } + @Override public void setStack(int slot, ItemStack stack) { + inventory.set(slot, stack); + if (stack.getCount() > getMaxCountPerStack()) stack.setCount(getMaxCountPerStack()); + markDirty(); + } + @Override public boolean canPlayerUse(PlayerEntity player) { + return player.squaredDistanceTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5) <= 64.0; + } + @Override public void clear() { inventory.clear(); } + + @Override public boolean isValid(int slot, ItemStack stack) { + if (slot % 2 == 0) return isValidEffectItem(stack); + return isValidFuel(stack); + } + + @Override + public void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + Inventories.writeNbt(nbt, inventory); + nbt.putIntArray("FuelTimers", fuelTimers); + byte[] a = new byte[4]; + for (int i = 0; i < 4; i++) a[i] = (byte) (rowActive[i] ? 1 : 0); + nbt.putByteArray("RowActive", a); + nbt.putInt("BeaconLevel", beaconLevel); + } + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + inventory.clear(); + Inventories.readNbt(nbt, inventory); + if (nbt.contains("FuelTimers")) { + int[] t = nbt.getIntArray("FuelTimers"); + System.arraycopy(t, 0, fuelTimers, 0, Math.min(t.length, 4)); + } + if (nbt.contains("RowActive")) { + byte[] a = nbt.getByteArray("RowActive"); + for (int i = 0; i < Math.min(a.length, 4); i++) rowActive[i] = a[i] != 0; + } + beaconLevel = nbt.getInt("BeaconLevel"); + } + + @Override public Text getDisplayName() { return Text.translatable("block.szar.super_beacon"); } + + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory inv, PlayerEntity player) { + return new SuperBeaconScreenHandler(syncId, inv, this, propertyDelegate, pos); + } + + @Override + public void writeScreenOpeningData(ServerPlayerEntity player, PacketByteBuf buf) { + buf.writeBlockPos(pos); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/SuperBeaconScreenHandler.java b/src/main/java/dev/tggamesyt/szar/SuperBeaconScreenHandler.java new file mode 100644 index 0000000..796ee10 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/SuperBeaconScreenHandler.java @@ -0,0 +1,89 @@ +package dev.tggamesyt.szar; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.screen.ArrayPropertyDelegate; +import net.minecraft.screen.PropertyDelegate; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.util.math.BlockPos; + +public class SuperBeaconScreenHandler extends ScreenHandler { + + private final Inventory inventory; + private final PropertyDelegate propertyDelegate; + private final BlockPos pos; + + // Client ctor + public SuperBeaconScreenHandler(int syncId, PlayerInventory playerInventory, PacketByteBuf buf) { + this(syncId, playerInventory, new SimpleInventory(8), + new ArrayPropertyDelegate(9), buf.readBlockPos()); + } + + // Server ctor + public SuperBeaconScreenHandler(int syncId, PlayerInventory playerInventory, Inventory inventory, + PropertyDelegate propertyDelegate, BlockPos pos) { + super(Szar.SUPER_BEACON_SCREEN_HANDLER, syncId); + this.inventory = inventory; + this.propertyDelegate = propertyDelegate; + this.pos = pos; + + checkSize(inventory, 8); + inventory.onOpen(playerInventory.player); + + for (int row = 0; row < 4; row++) { + addSlot(new EffectSlot(inventory, row * 2, 44, 18 + row * 28)); + addSlot(new FuelSlot(inventory, row * 2 + 1, 80, 18 + row * 28)); + } + + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, 140 + row * 18)); + } + } + for (int col = 0; col < 9; col++) { + addSlot(new Slot(playerInventory, col, 8 + col * 18, 198)); + } + + addProperties(propertyDelegate); + } + + public int getBeaconLevel() { return propertyDelegate.get(0); } + public boolean isRowActive(int row) { return propertyDelegate.get(1 + row) != 0; } + public int getFuelTimer(int row) { return propertyDelegate.get(5 + row); } + public BlockPos getPos() { return pos; } + + @Override + public ItemStack quickMove(PlayerEntity player, int slotIndex) { + ItemStack newStack = ItemStack.EMPTY; + Slot slot = this.slots.get(slotIndex); + if (slot.hasStack()) { + ItemStack original = slot.getStack(); + newStack = original.copy(); + if (slotIndex < 8) { + if (!insertItem(original, 8, 44, true)) return ItemStack.EMPTY; + } else { + if (!insertItem(original, 0, 8, false)) return ItemStack.EMPTY; + } + if (original.isEmpty()) slot.setStack(ItemStack.EMPTY); else slot.markDirty(); + } + return newStack; + } + + @Override public boolean canUse(PlayerEntity player) { return inventory.canPlayerUse(player); } + + private static class EffectSlot extends Slot { + public EffectSlot(Inventory inv, int i, int x, int y) { super(inv, i, x, y); } + @Override public boolean canInsert(ItemStack stack) { return SuperBeaconBlockEntity.isValidEffectItem(stack); } + @Override public int getMaxItemCount() { return 1; } + } + + private static class FuelSlot extends Slot { + public FuelSlot(Inventory inv, int i, int x, int y) { super(inv, i, x, y); } + @Override public boolean canInsert(ItemStack stack) { return SuperBeaconBlockEntity.isValidFuel(stack); } + } +} \ 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 44af583..c23c995 100644 --- a/src/main/java/dev/tggamesyt/szar/Szar.java +++ b/src/main/java/dev/tggamesyt/szar/Szar.java @@ -26,6 +26,7 @@ import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRe import net.fabricmc.fabric.api.object.builder.v1.entity.FabricEntityTypeBuilder; import net.fabricmc.fabric.api.object.builder.v1.trade.TradeOfferHelper; import net.fabricmc.fabric.api.object.builder.v1.world.poi.PointOfInterestHelper; +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType; import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry; import net.fabricmc.fabric.impl.biome.TheEndBiomeData; import net.minecraft.advancement.Advancement; @@ -421,6 +422,7 @@ public class Szar implements ModInitializer { entries.add(Szar.ENDER_BOOTS); entries.add(Szar.SUPER_DIAMOND); entries.add(Szar.SUPER_APPLE); + entries.add(Szar.SUPER_BEACON_ITEM); // blueprint stuff entries.add(BlueprintBlocks.BLUEPRINT); entries.add(BlueprintBlocks.BLUEPRINT_DOOR_ITEM); @@ -1485,7 +1487,54 @@ public class Szar implements ModInitializer { return true; }); + Registry.register(Registries.BLOCK, new Identifier(MOD_ID, "super_beacon"), SUPER_BEACON_BLOCK); + Registry.register(Registries.ITEM, new Identifier(MOD_ID, "super_beacon"), SUPER_BEACON_ITEM); + + SUPER_BEACON_BLOCK_ENTITY = Registry.register( + Registries.BLOCK_ENTITY_TYPE, + new Identifier(MOD_ID, "super_beacon"), + FabricBlockEntityTypeBuilder.create(SuperBeaconBlockEntity::new, SUPER_BEACON_BLOCK).build() + ); + + SUPER_BEACON_SCREEN_HANDLER = Registry.register( + Registries.SCREEN_HANDLER, + new Identifier(MOD_ID, "super_beacon"), + new ExtendedScreenHandlerType<>(SuperBeaconScreenHandler::new) + ); + + ServerPlayNetworking.registerGlobalReceiver(ACTIVATE_ROW_PACKET, (server, player, handler, buf, responseSender) -> { + BlockPos pos = buf.readBlockPos(); + int row = buf.readInt(); + server.execute(() -> { + LOGGER.info("[Szar] activate_row: pos={}, row={}", pos, row); + if (player.getServerWorld().getBlockEntity(pos) instanceof SuperBeaconBlockEntity be) { + if (be.canPlayerUse(player)) { + boolean wasActive = be.isRowActive(row); + be.toggleRow(row); + LOGGER.info("[Szar] row {} {} -> {} (level={}, effect={}, fuel={})", + row, wasActive, be.isRowActive(row), be.getBeaconLevel(), + be.getStack(row * 2), be.getStack(row * 2 + 1)); + } else { + LOGGER.warn("[Szar] player too far"); + } + } else { + LOGGER.warn("[Szar] no block entity at {}", pos); + } + }); + }); + + LOGGER.info("[Szar] Initialized"); } + public static final Block SUPER_BEACON_BLOCK = new SuperBeaconBlock( + FabricBlockSettings.copyOf(Blocks.BEACON).luminance(15) + ); + + public static final Item SUPER_BEACON_ITEM = new BlockItem(SUPER_BEACON_BLOCK, new FabricItemSettings()); + + public static BlockEntityType SUPER_BEACON_BLOCK_ENTITY; + public static ScreenHandlerType SUPER_BEACON_SCREEN_HANDLER; + + public static final Identifier ACTIVATE_ROW_PACKET = new Identifier(MOD_ID, "activate_row"); private static void teleportRandomly(PlayerEntity player, ServerWorld world) { Random RANDOM = new Random(); diff --git a/src/main/resources/assets/szar/blockstates/super_beacon.json b/src/main/resources/assets/szar/blockstates/super_beacon.json new file mode 100644 index 0000000..05434d6 --- /dev/null +++ b/src/main/resources/assets/szar/blockstates/super_beacon.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "szar:block/super_beacon" + } + } +} diff --git a/src/main/resources/assets/szar/lang/en_us.json b/src/main/resources/assets/szar/lang/en_us.json index 7f6100d..b4764f8 100644 --- a/src/main/resources/assets/szar/lang/en_us.json +++ b/src/main/resources/assets/szar/lang/en_us.json @@ -223,5 +223,6 @@ "item.szar.ender_chestplate": "Ender Chestplate", "item.szar.ender_leggings": "Ender Leggings", "item.szar.ender_boots": "Ender Boots", - "item.szar.ender_helmet": "Ender Helmet" + "item.szar.ender_helmet": "Ender Helmet", + "block.szar.super_beacon": "Super Beacon" } diff --git a/src/main/resources/assets/szar/models/block/super_beacon.json b/src/main/resources/assets/szar/models/block/super_beacon.json new file mode 100644 index 0000000..5111f4f --- /dev/null +++ b/src/main/resources/assets/szar/models/block/super_beacon.json @@ -0,0 +1,67 @@ +{ + "format_version": "1.21.11", + "credit": "Made with Blockbench", + "parent": "block/block", + "textures": { + "3": "szar:block/enchanting_table_top", + "4": "szar:block/enchanting_table_side", + "particle": "szar:block/glass", + "glass": "szar:block/glass", + "obsidian": "szar:block/obsidian", + "beacon": "szar:block/beacon" + }, + "elements": [ + { + "name": "Glass shell", + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#glass"}, + "east": {"uv": [0, 0, 16, 16], "texture": "#glass"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#glass"}, + "west": {"uv": [0, 0, 16, 16], "texture": "#glass"}, + "up": {"uv": [0, 0, 16, 16], "texture": "#glass"}, + "down": {"uv": [0, 0, 16, 16], "texture": "#glass"} + } + }, + { + "name": "Obsidian base", + "from": [2, 0.1, 2], + "to": [14, 3, 14], + "faces": { + "north": {"uv": [2, 13, 14, 16], "texture": "#obsidian"}, + "east": {"uv": [2, 13, 14, 16], "texture": "#obsidian"}, + "south": {"uv": [2, 13, 14, 16], "texture": "#obsidian"}, + "west": {"uv": [2, 13, 14, 16], "texture": "#obsidian"}, + "up": {"uv": [2, 2, 14, 14], "texture": "#obsidian"}, + "down": {"uv": [2, 2, 14, 14], "texture": "#obsidian"} + } + }, + { + "name": "Enchanting Table", + "from": [3, 3, 3], + "to": [13, 11, 13], + "faces": { + "north": {"uv": [0, 4, 16, 16], "texture": "#4"}, + "east": {"uv": [0, 4, 16, 16], "texture": "#4"}, + "south": {"uv": [0, 4, 16, 16], "texture": "#4"}, + "west": {"uv": [0, 4, 16, 16], "texture": "#4"}, + "up": {"uv": [0, 0, 16, 16], "texture": "#3"}, + "down": {"uv": [3, 3, 13, 13], "texture": "#3"} + } + }, + { + "name": "Inner beacon texture", + "from": [6, 11, 6], + "to": [10, 15, 10], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#beacon"}, + "east": {"uv": [0, 0, 16, 16], "texture": "#beacon"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#beacon"}, + "west": {"uv": [0, 0, 16, 16], "texture": "#beacon"}, + "up": {"uv": [0, 0, 16, 16], "texture": "#beacon"}, + "down": {"uv": [0, 0, 16, 16], "texture": "#beacon"} + } + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/szar/models/item/super_beacon.json b/src/main/resources/assets/szar/models/item/super_beacon.json new file mode 100644 index 0000000..2e92481 --- /dev/null +++ b/src/main/resources/assets/szar/models/item/super_beacon.json @@ -0,0 +1,3 @@ +{ + "parent": "szar:block/super_beacon" +} diff --git a/src/main/resources/assets/szar/textures/block/beacon.png b/src/main/resources/assets/szar/textures/block/beacon.png new file mode 100644 index 0000000..8089d91 Binary files /dev/null and b/src/main/resources/assets/szar/textures/block/beacon.png differ diff --git a/src/main/resources/assets/szar/textures/block/enchanting_table_side.png b/src/main/resources/assets/szar/textures/block/enchanting_table_side.png new file mode 100644 index 0000000..a111759 Binary files /dev/null and b/src/main/resources/assets/szar/textures/block/enchanting_table_side.png differ diff --git a/src/main/resources/assets/szar/textures/block/enchanting_table_top.png b/src/main/resources/assets/szar/textures/block/enchanting_table_top.png new file mode 100644 index 0000000..ef5eea5 Binary files /dev/null and b/src/main/resources/assets/szar/textures/block/enchanting_table_top.png differ diff --git a/src/main/resources/assets/szar/textures/block/glass.png b/src/main/resources/assets/szar/textures/block/glass.png new file mode 100644 index 0000000..7270ac1 Binary files /dev/null and b/src/main/resources/assets/szar/textures/block/glass.png differ diff --git a/src/main/resources/assets/szar/textures/block/obsidian.png b/src/main/resources/assets/szar/textures/block/obsidian.png new file mode 100644 index 0000000..b9e0906 Binary files /dev/null and b/src/main/resources/assets/szar/textures/block/obsidian.png differ diff --git a/src/main/resources/assets/szar/textures/entity/beacon_beam.png b/src/main/resources/assets/szar/textures/entity/beacon_beam.png new file mode 100644 index 0000000..a2da8c6 Binary files /dev/null and b/src/main/resources/assets/szar/textures/entity/beacon_beam.png differ diff --git a/src/main/resources/assets/szar/textures/gui/super_beacon.png b/src/main/resources/assets/szar/textures/gui/super_beacon.png new file mode 100644 index 0000000..51e73b5 Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/super_beacon.png differ diff --git a/src/main/resources/data/szar/loot_tables/blocks/super_beacon.json b/src/main/resources/data/szar/loot_tables/blocks/super_beacon.json new file mode 100644 index 0000000..4940057 --- /dev/null +++ b/src/main/resources/data/szar/loot_tables/blocks/super_beacon.json @@ -0,0 +1,20 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "szar:super_beacon" + } + ], + "rolls": 1.0 + } + ] +} \ No newline at end of file diff --git a/src/main/resources/data/szar/recipes/super_beacon.json b/src/main/resources/data/szar/recipes/super_beacon.json index d10f7cd..06129b4 100644 --- a/src/main/resources/data/szar/recipes/super_beacon.json +++ b/src/main/resources/data/szar/recipes/super_beacon.json @@ -20,7 +20,7 @@ } }, "result": { - "item": "minecraft:bedrock", + "item": "szar:super_beacon", "count": 1 } } \ No newline at end of file