
================================================================================
FILE: StonesMod.java
================================================================================

package net.stones;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import net.stones.init.StonesModItems;
import net.stones.init.StonesModBlocks;
import net.stones.init.StonesModBlockEntities;
import net.stones.init.StonesModMenus;
import net.stones.network.*;
import net.minecraftforge.network.simple.SimpleChannel;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.fml.util.thread.SidedThreadGroups;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.common.MinecraftForge;

import net.minecraft.resources.ResourceLocation;
import net.minecraft.network.FriendlyByteBuf;

import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.BiConsumer;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.List;
import java.util.Collection;
import java.util.ArrayList;
import java.util.AbstractMap;

@Mod("stones")
public class StonesMod {
	public static final Logger LOGGER = LogManager.getLogger(StonesMod.class);
	public static final String MODID = "stones";

	public StonesMod() {
		MinecraftForge.EVENT_BUS.register(this);
		IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus();

		StonesModBlocks.REGISTRY.register(bus);
		StonesModBlockEntities.REGISTRY.register(bus);
		StonesModItems.REGISTRY.register(bus);
        StonesModMenus.REGISTRY.register(bus);
        // --- PAKETE REGISTRIEREN ---
        addNetworkMessage(PacketBindShrine.class, PacketBindShrine::toBytes, PacketBindShrine::new, PacketBindShrine::handle);
        addNetworkMessage(PacketSyncPlayerShrine.class, PacketSyncPlayerShrine::toBytes, PacketSyncPlayerShrine::new, PacketSyncPlayerShrine::handle);
        addNetworkMessage(PacketOpenShrine.class, PacketOpenShrine::toBytes, PacketOpenShrine::new, PacketOpenShrine::handle);
	}

	private static final String PROTOCOL_VERSION = "1";
	public static final SimpleChannel PACKET_HANDLER = NetworkRegistry.newSimpleChannel(new ResourceLocation(MODID, MODID), () -> PROTOCOL_VERSION, PROTOCOL_VERSION::equals, PROTOCOL_VERSION::equals);
	private static int messageID = 0;

	public static <T> void addNetworkMessage(Class<T> messageType, BiConsumer<T, FriendlyByteBuf> encoder, Function<FriendlyByteBuf, T> decoder, BiConsumer<T, Supplier<NetworkEvent.Context>> messageConsumer) {
		PACKET_HANDLER.registerMessage(messageID, messageType, encoder, decoder, messageConsumer);
		messageID++;
	}

	private static final Collection<AbstractMap.SimpleEntry<Runnable, Integer>> workQueue = new ConcurrentLinkedQueue<>();

	public static void queueServerWork(int tick, Runnable action) {
		if (Thread.currentThread().getThreadGroup() == SidedThreadGroups.SERVER)
			workQueue.add(new AbstractMap.SimpleEntry<>(action, tick));
	}

	@SubscribeEvent
	public void tick(TickEvent.ServerTickEvent event) {
		if (event.phase == TickEvent.Phase.END) {
			List<AbstractMap.SimpleEntry<Runnable, Integer>> actions = new ArrayList<>();
			workQueue.forEach(work -> {
				work.setValue(work.getValue() - 1);
				if (work.getValue() == 0)
					actions.add(work);
			});
			actions.forEach(e -> e.getKey().run());
			workQueue.removeAll(actions);
		}
	}
}

================================================================================
FILE: block\RunestoneBlock.java
================================================================================

package net.stones.block;

import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Containers;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraftforge.items.IItemHandler;
import net.stones.block.entity.RunestoneBlockEntity;
import net.stones.data.ShrineInstance;
import net.stones.data.ShrineSavedData;

import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

public class RunestoneBlock extends Block implements EntityBlock {

    public RunestoneBlock() {
        super(BlockBehaviour.Properties.of()
                .mapColor(MapColor.STONE)
                .strength(50.0f, 50.0f) 
                .requiresCorrectToolForDrops()
                .sound(SoundType.STONE));
    }

    @Override
    public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
        return new RunestoneBlockEntity(pos, state);
    }

    // --- 0. LOOT TABLE DEAKTIVIEREN ---
    // Wir handhaben die Drops komplett manuell in playerWillDestroy (Block) und onRemove (Inhalt).
    // Das verhindert Duplizierung (Vanilla Drop + Custom Drop).
    @Override
    public List<ItemStack> getDrops(BlockState state, LootParams.Builder params) {
        return Collections.emptyList();
    }

    // --- 1. PLATZIEREN & WIEDERHERSTELLEN ---
    @Override
    public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
        if (!level.isClientSide) {
            BlockEntity be = level.getBlockEntity(pos);
            if (be instanceof RunestoneBlockEntity runeBe) {
                ServerLevel serverLevel = (ServerLevel) level;
                ShrineSavedData data = ShrineSavedData.get(serverLevel);
                
                CompoundTag tag = stack.getTag();
                if (tag != null && tag.contains("savedShrineId")) {
                    UUID id = tag.getUUID("savedShrineId");
                    
                    // Schrein Struktur wiederherstellen (Layout ist deterministisch durch UUID)
                    // Inventar ist natürlich leer, da die Items gedroppt wurden.
                    data.createShrine(id);
                    
                    runeBe.setShrineId(id);
                    
                    if (placer instanceof Player player) {
                        player.sendSystemMessage(Component.literal("§eRunenstein wiederhergestellt: " + id.toString().substring(0,8) + "..."));
                    }
                } else {
                    // Falls kein Tag da ist, resetten wir die ID sicherheitshalber
                    if (runeBe.getShrineId() != null) {
                         runeBe.setShrineId(null);
                    }
                }
            }
        }
    }

    // --- 2. ABBAUEN (BLOCK DROP) ---
    @Override
    public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) {
        if (!level.isClientSide) {
            BlockEntity be = level.getBlockEntity(pos);
            if (be instanceof RunestoneBlockEntity runeBe) {
                UUID shrineId = runeBe.getShrineId();
                
                // Drop nur, wenn nicht Creative UND wenn wir eine ID haben
                // Wir prüfen hier NICHT auf hasCorrectToolForDrops, da der Block Härte 50 hat (Obsidian).
                // Wenn der Spieler ihn abbauen kann, hat er es verdient.
                if (!player.isCreative() && shrineId != null) {
                    ItemStack drop = new ItemStack(this);
                    drop.getOrCreateTag().putUUID("savedShrineId", shrineId);
                    Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, drop);
                }
            }
        }
        super.playerWillDestroy(level, pos, state, player);
    }

    // --- 3. INHALT DROPPEN (CRITICAL) ---
    // Diese Methode wird IMMER aufgerufen, wenn der Block verschwindet (Abbau, Explosion, etc.)
    @Override
    public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
        if (!state.is(newState.getBlock())) {
             if (!level.isClientSide) {
                try {
                    BlockEntity be = level.getBlockEntity(pos);
                    if (be instanceof RunestoneBlockEntity runeBe) {
                        UUID shrineId = runeBe.getShrineId();
                        
                        if (shrineId != null) {
                            ServerLevel serverLevel = (ServerLevel) level;
                            ShrineSavedData data = ShrineSavedData.get(serverLevel);
                            ShrineInstance shrine = data.getShrine(shrineId);

                            if (shrine != null) {
                                // A. INHALT SICHERN: Alle Gems fallen heraus
                                IItemHandler inventory = shrine.getInventory();
                                for (int i = 0; i < inventory.getSlots(); i++) {
                                    ItemStack stack = inventory.getStackInSlot(i);
                                    if (!stack.isEmpty()) {
                                        Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, stack.copy());
                                    }
                                }
                                
                                // B. SPEICHER BEREINIGEN
                                // Da der Block als Item (mit UUID) gedroppt wurde, können wir das Layout
                                // jederzeit wiederherstellen. Die Daten im Speicher werden gelöscht,
                                // um "Datenmüll" zu vermeiden.
                                data.removeShrine(shrineId);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            super.onRemove(state, level, pos, newState, isMoving);
        }
    }

    @Override
    public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
        if (player.isCrouching()) {
            return InteractionResult.PASS; 
        }

        if (!level.isClientSide && hand == InteractionHand.MAIN_HAND) {
            BlockEntity be = level.getBlockEntity(pos);
            if (be instanceof RunestoneBlockEntity runeBe) {
                ServerLevel serverLevel = (ServerLevel) level;
                ShrineSavedData data = ShrineSavedData.get(serverLevel);
                
                UUID id = runeBe.getShrineId();
                
                if (id == null) {
                    // Neuer Schrein (Random Layout)
                    ShrineInstance newShrine = data.createShrine();
                    runeBe.setShrineId(newShrine.getId());
                    player.sendSystemMessage(Component.literal("§aRunenstein gebunden! ID: " + newShrine.getId()));
                } 
                else {
                    // Existierende ID (Restore)
                    if (data.getShrine(id) == null) {
                        player.sendSystemMessage(Component.literal("§eRegeneriere Schrein Struktur..."));
                        data.createShrine(id);
                    }
                }
            }
            return InteractionResult.SUCCESS;
        }
        return InteractionResult.SUCCESS;
    }
    
    @Override
    public void appendHoverText(ItemStack stack, @Nullable BlockGetter level, List<Component> tooltip, TooltipFlag flag) {
        if (stack.hasTag() && stack.getTag().contains("savedShrineId")) {
            String uuid = stack.getTag().getUUID("savedShrineId").toString();
            tooltip.add(Component.literal("§7Gespeicherte ID: §e" + uuid.substring(0, 8) + "..."));
        } else {
            tooltip.add(Component.literal("§7Ungebunden"));
        }
        super.appendHoverText(stack, level, tooltip, flag);
    }
}

================================================================================
FILE: block\entity\RunestoneBlockEntity.java
================================================================================

package net.stones.block.entity;

import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.stones.data.ShrineInstance;
import net.stones.data.ShrineSavedData;
import net.stones.gui.RunestoneMenu;
import net.stones.init.StonesModBlockEntities;

import javax.annotation.Nullable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

public class RunestoneBlockEntity extends BlockEntity implements MenuProvider {

    private UUID shrineId;
    
    // Nur für Client-Rendering relevant!
    // Auf dem Server nutzen wir direkt ShrineInstance via shrineId.
    private final Set<UUID> clientOwners = new HashSet<>();

    public RunestoneBlockEntity(BlockPos pos, BlockState state) {
        super(StonesModBlockEntities.RUNESTONE.get(), pos, state);
    }

    public void setShrineId(UUID id) {
        this.shrineId = id;
        setChanged();
        
        // Trigger Update, damit der Client die neuen Owner (oder leere Liste) bekommt
        if (level != null) {
            if (level instanceof ServerLevel serverLevel && id != null) {
                ShrineInstance shrine = ShrineSavedData.get(serverLevel).getShrine(id);
                if (shrine != null) {
                    shrine.setLocation(GlobalPos.of(level.dimension(), worldPosition));
                }
            }
            level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
        }
    }

    public UUID getShrineId() { return shrineId; }
    
    // Für den Renderer (Client)
    public Set<UUID> getClientOwners() {
        return Collections.unmodifiableSet(clientOwners);
    }

    @Override
    public Component getDisplayName() { return Component.literal("Runestone Shrine"); }

    @Nullable
    @Override
    public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
        if (level instanceof ServerLevel serverLevel && shrineId != null) {
            ShrineInstance shrine = ShrineSavedData.get(serverLevel).getShrine(shrineId);
            if (shrine != null) {
                return new RunestoneMenu(containerId, playerInventory, shrine.getInventory(), shrine.getLayout());
            }
        }
        return null;
    }

    @Override
    public void load(CompoundTag nbt) {
        super.load(nbt);
        if (nbt.contains("shrineId")) {
            this.shrineId = nbt.getUUID("shrineId");
        }
        
        // Client-Cache laden (vom Paket)
        if (nbt.contains("owners", Tag.TAG_LIST)) {
            clientOwners.clear();
            ListTag list = nbt.getList("owners", Tag.TAG_COMPOUND);
            for (Tag t : list) {
                clientOwners.add(((CompoundTag)t).getUUID("uuid"));
            }
        }
    }

    @Override
    protected void saveAdditional(CompoundTag nbt) {
        super.saveAdditional(nbt);
        if (this.shrineId != null) {
            nbt.putUUID("shrineId", this.shrineId);
        }
        // Wir speichern owners NICHT auf die Platte im Block (Redundanz),
        // sie kommen immer frisch aus der ShrineInstance beim Laden.
    }

    @Override
    public CompoundTag getUpdateTag() {
        CompoundTag tag = super.getUpdateTag();
        // Server -> Client Sync
        if (level instanceof ServerLevel serverLevel && shrineId != null) {
            ShrineInstance shrine = ShrineSavedData.get(serverLevel).getShrine(shrineId);
            if (shrine != null) {
                ListTag list = new ListTag();
                for (UUID owner : shrine.getOwners()) {
                    CompoundTag t = new CompoundTag();
                    t.putUUID("uuid", owner);
                    list.add(t);
                }
                tag.put("owners", list);
            }
        }
        return tag;
    }

    @Override
    public Packet<ClientGamePacketListener> getUpdatePacket() {
        return ClientboundBlockEntityDataPacket.create(this);
    }
}

================================================================================
FILE: cap\IPlayerShrineLink.java
================================================================================

package net.stones.cap;

import net.minecraft.core.GlobalPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraftforge.common.util.INBTSerializable;
import java.util.UUID;

public interface IPlayerShrineLink extends INBTSerializable<CompoundTag> {
    void setLinkedShrine(UUID shrineId, GlobalPos pos); // Update: ID + Position
    
    UUID getLinkedShrine();
    GlobalPos getShrinePos(); // NEU
    
    boolean isLinked();
    
    void copyFrom(IPlayerShrineLink source);
}

================================================================================
FILE: cap\PlayerShrineCapProvider.java
================================================================================

package net.stones.cap;

import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.capabilities.CapabilityToken;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.INBTSerializable;
import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.Nullable;

public class PlayerShrineCapProvider implements ICapabilityProvider, INBTSerializable<CompoundTag> {
    
    public static final Capability<IPlayerShrineLink> SHRINE_LINK = CapabilityManager.get(new CapabilityToken<>(){});

    private PlayerShrineLink backend = null;
    private final LazyOptional<IPlayerShrineLink> optional = LazyOptional.of(this::createPlayerShrineLink);

    private PlayerShrineLink createPlayerShrineLink() {
        if (this.backend == null) {
            this.backend = new PlayerShrineLink();
        }
        return this.backend;
    }

    @Override
    public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side) {
        if (cap == SHRINE_LINK) {
            return optional.cast();
        }
        return LazyOptional.empty();
    }

    @Override
    public CompoundTag serializeNBT() {
        return createPlayerShrineLink().serializeNBT();
    }

    @Override
    public void deserializeNBT(CompoundTag nbt) {
        createPlayerShrineLink().deserializeNBT(nbt);
    }
}

================================================================================
FILE: cap\PlayerShrineLink.java
================================================================================

package net.stones.cap;

import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;

import java.util.UUID;

public class PlayerShrineLink implements IPlayerShrineLink {
    private UUID shrineId;
    private GlobalPos shrinePos;

    @Override
    public void setLinkedShrine(@Nullable UUID shrineId, @Nullable GlobalPos pos) {
        this.shrineId = shrineId;
        this.shrinePos = pos;
    }

    @Override
    public @Nullable UUID getLinkedShrine() { return shrineId; }

    @Override
    public @Nullable GlobalPos getShrinePos() { return shrinePos; }

    @Override
    public boolean isLinked() { return shrineId != null; }

    @Override
    public void copyFrom(IPlayerShrineLink source) {
        this.shrineId = source.getLinkedShrine();
        this.shrinePos = source.getShrinePos();
    }

    @Override
    public CompoundTag serializeNBT() {
        CompoundTag tag = new CompoundTag();
        if (shrineId != null) {
            tag.putUUID("shrineId", shrineId);
        }
        if (shrinePos != null) {
            tag.put("pos", NbtUtils.writeBlockPos(shrinePos.pos()));
            tag.putString("dim", shrinePos.dimension().location().toString());
        }
        return tag;
    }

    @Override
    public void deserializeNBT(CompoundTag nbt) {
        if (nbt.contains("shrineId")) {
            this.shrineId = nbt.getUUID("shrineId");
        } else {
            this.shrineId = null;
        }
        
        if (nbt.contains("pos") && nbt.contains("dim")) {
            BlockPos pos = NbtUtils.readBlockPos(nbt.getCompound("pos"));
            ResourceLocation dimLoc = new ResourceLocation(nbt.getString("dim"));
            ResourceKey<Level> dimKey = ResourceKey.create(Registries.DIMENSION, dimLoc);
            this.shrinePos = GlobalPos.of(dimKey, pos);
        } else {
            this.shrinePos = null;
        }
    }
}

================================================================================
FILE: client\ClientAttunementHandler.java
================================================================================

package net.stones.client;

import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.GlobalPos;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RenderGuiOverlayEvent;
import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.stones.StonesMod;
import net.stones.block.RunestoneBlock;
import net.stones.cap.PlayerShrineCapProvider;
import net.stones.item.StoneItem;
import net.stones.network.PacketBindShrine;
import net.stones.network.PacketOpenShrine;

@Mod.EventBusSubscriber(modid = StonesMod.MODID, value = Dist.CLIENT)
public class ClientAttunementHandler {

    // Status für Interaktion
    private static int holdTicks = 0;
    private static final int REQUIRED_TICKS = 60; // 3 Sekunden halten für Bindung
    private static final int CLICK_THRESHOLD = 10; // Unter 0.5 Sek gilt als Klick (Öffnen)
    private static boolean wasDown = false;

    // --- 1. LOGIK: Ticks & Input ---
    @SubscribeEvent
    public static void onClientTick(TickEvent.ClientTickEvent event) {
        if (event.phase != TickEvent.Phase.END) return;

        Minecraft mc = Minecraft.getInstance();
        if (mc.player == null || mc.level == null) return;

        boolean isRightClickDown = mc.options.keyUse.isDown();
        boolean lookingAtRunestone = false;
        
        // Raytrace checken
        if (mc.hitResult instanceof BlockHitResult blockHit) {
             if (mc.level.getBlockState(blockHit.getBlockPos()).getBlock() instanceof RunestoneBlock) {
                 lookingAtRunestone = true;
             }
        }

        if (isRightClickDown) {
            // Taste gedrückt halten
            if (lookingAtRunestone) {
                holdTicks++;
                
                // Trigger Bindung nach 3 Sekunden
                if (holdTicks == REQUIRED_TICKS) {
                    if (mc.hitResult instanceof BlockHitResult blockHit) {
                        StonesMod.PACKET_HANDLER.sendToServer(new PacketBindShrine(blockHit.getBlockPos()));
                        // Clientseitiges Feedback (Server sendet auch eins, aber Latenz minimieren)
                        mc.player.playSound(net.minecraft.sounds.SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f);
                    }
                }
            } else {
                // Weggeschaut -> Reset
                holdTicks = 0;
            }
        } else {
            // Taste losgelassen
            if (wasDown) {
                // War es ein kurzer Klick auf den Stein?
                if (holdTicks > 0 && holdTicks < CLICK_THRESHOLD && lookingAtRunestone) {
                     // GUI Öffnen
                     if (mc.hitResult instanceof BlockHitResult blockHit) {
                         StonesMod.PACKET_HANDLER.sendToServer(new PacketOpenShrine(blockHit.getBlockPos()));
                     }
                }
            }
            // Reset
            holdTicks = 0;
        }
        
        wasDown = isRightClickDown;
    }

    // --- 2. RENDER: HUD Overlay ---
    @SubscribeEvent
    public static void onRenderOverlay(RenderGuiOverlayEvent.Post event) {
        Minecraft mc = Minecraft.getInstance();
        if (mc.player == null || mc.level == null) return;

        // Wir nutzen den HOTBAR Layer als Anker, damit wir nicht mit Crosshair oder Chat kollidieren
        if (event.getOverlay() != VanillaGuiOverlay.HOTBAR.type()) return;

        GuiGraphics graphics = event.getGuiGraphics();
        int width = event.getWindow().getGuiScaledWidth();
        int height = event.getWindow().getGuiScaledHeight();

        // --- A: Interaktion mit Runestone (Ladebalken) ---
        boolean lookingAtRunestone = false;
        if (mc.hitResult instanceof BlockHitResult blockHit) {
            if (mc.level.getBlockState(blockHit.getBlockPos()).getBlock() instanceof RunestoneBlock) {
                lookingAtRunestone = true;
            }
        }

        if (lookingAtRunestone) {
            // Balken zeichnen wenn wir halten
            if (holdTicks > 5 && holdTicks < REQUIRED_TICKS) {
                int barWidth = 100;
                int barHeight = 5;
                int x = (width - barWidth) / 2;
                int y = (height / 2) + 20;

                // Hintergrund
                graphics.fill(x, y, x + barWidth, y + barHeight, 0x80000000); 

                // Fortschritt
                float progress = (float) holdTicks / (float) REQUIRED_TICKS;
                int filledWidth = (int) (barWidth * progress);
                int color = 0xFF00FFCC; // Magisches Cyan
                
                graphics.fill(x, y, x + filledWidth, y + barHeight, color);

                String text = "Seelenbindung...";
                graphics.drawCenteredString(mc.font, text, width / 2, y + 8, 0xFFFFFF);
            }
            // Info Text wenn wir nur draufschauen
            else if (holdTicks <= 5) {
                int y = (height / 2) + 15;
                graphics.drawCenteredString(mc.font, "§6Runestone Schrein", width / 2, y, 0xFFFFFF);
                
                float scale = 0.75f;
                graphics.pose().pushPose();
                graphics.pose().translate(width / 2.0, y + 12, 0);
                graphics.pose().scale(scale, scale, scale);
                
                String info = "§7[Rechtsklick] Öffnen   §7[Halten] Binden";
                graphics.drawCenteredString(mc.font, info, 0, 0, 0xFFFFFF);
                
                graphics.pose().popPose();
            }
            RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
        }

        // --- B: Navigation (Kompass) ---
        // Zeigen, wenn wir irgendeine Rune in der Hand haben
        boolean holdingRune = mc.player.getMainHandItem().getItem() instanceof StoneItem 
                           || mc.player.getOffhandItem().getItem() instanceof StoneItem;

        if (holdingRune) {
            // Capability abfragen
            mc.player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
                GlobalPos pos = cap.getShrinePos();
                if (pos != null) {
                    renderCompass(graphics, mc, width, height, pos);
                }
            });
        }
    }

    private static void renderCompass(GuiGraphics graphics, Minecraft mc, int w, int h, GlobalPos target) {
        // Dimension Check
        if (!target.dimension().equals(mc.level.dimension())) {
            String txt = "§dUnbekannte Dimension";
            graphics.drawCenteredString(mc.font, txt, w / 2, h - 65, 0xFFFFFF);
            return;
        }

        Vec3 playerPos = mc.player.position();
        double dx = target.pos().getX() - playerPos.x;
        double dz = target.pos().getZ() - playerPos.z;
        double dist = Math.sqrt(dx * dx + dz * dz);

        // Winkelberechnung relativ zum Spielerblick
        // atan2 gibt Winkel in Radians (-PI bis PI). Umrechnen in Grad.
        // Minecraft Yaw ist 0 = Süden, wir müssen das korrigieren.
        double angleToTarget = Math.toDegrees(Math.atan2(dz, dx)) - 90;
        double playerYaw = mc.player.getYRot();
        double relativeAngle = Mth.wrapDegrees(angleToTarget - playerYaw);

        // Render
        String distStr = String.format("§b%.0fm", dist);
        String dirArrow = getDirectionArrow(relativeAngle);
        
        // Positionieren über der Hotbar (etwas höher als der Info-Text)
        int y = h - 65;
        
        graphics.drawCenteredString(mc.font, "§6✦ Schrein Resonanz ✦", w / 2, y - 10, 0xFFFFFF);
        graphics.drawCenteredString(mc.font, dirArrow + " " + distStr + " " + dirArrow, w / 2, y, 0xFFFFFF);
    }

    // Wandelt den relativen Winkel in einen Pfeil um
    private static String getDirectionArrow(double angle) {
        if (angle >= -22.5 && angle < 22.5) return "⬆";   // Vorne
        if (angle >= 22.5 && angle < 67.5) return "⬈";    // Rechts Vorne
        if (angle >= 67.5 && angle < 112.5) return "➡";   // Rechts
        if (angle >= 112.5 && angle < 157.5) return "⬊";  // Rechts Hinten
        if (angle >= 157.5 || angle < -157.5) return "⬇"; // Hinten
        if (angle >= -157.5 && angle < -112.5) return "⬋"; // Links Hinten
        if (angle >= -112.5 && angle < -67.5) return "⬅";  // Links
        if (angle >= -67.5 && angle < -22.5) return "⬉";   // Links Vorne
        return "?";
    }
}

================================================================================
FILE: client\gui\RunestoneScreen.java
================================================================================

package net.stones.client.gui;

import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.*;
import com.mojang.math.Axis;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
import net.minecraft.client.resources.sounds.SoundInstance;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.phys.Vec2;
import net.stones.gui.RunestoneMenu; 
import net.stones.gui.layout.ShrineLayout;
import net.stones.data.ShrineInstance.SlotConfig;
import net.stones.data.ShrineInstance.SlotType;
import net.stones.enchantment.RuneEnchantment;
import org.joml.Matrix4f;

import java.awt.Color;
import java.util.*;

public class RunestoneScreen extends AbstractContainerScreen<RunestoneMenu> {

    // Texturen
    private static final ResourceLocation BG_NEBULA = new ResourceLocation("stones", "textures/gui/shrine_nebula.png");
    private static final ResourceLocation SLOT_MINOR = new ResourceLocation("stones", "textures/gui/slot_minor.png");
    private static final ResourceLocation SLOT_MAJOR = new ResourceLocation("stones", "textures/gui/slot_major.png");
    private static final ResourceLocation SLOT_MILESTONE = new ResourceLocation("stones", "textures/gui/slot_milestone.png");
    private static final ResourceLocation GLOW_ATLAS = new ResourceLocation("stones", "textures/gui/particle_glow.png");
    
    private static final ResourceLocation XP_BAR_LOCATION = new ResourceLocation("textures/gui/icons.png");

    private static final boolean AUTO_FIT_TO_SCREEN = true;
    private static final float DEFAULT_ZOOM = 1.0f;
    private static final int INVENTORY_TOP_Y = 140; 
    
    private static final int STAR_COUNT = 3000; 
    private static final float STAR_FIELD_RADIUS = 1200.0f; 

    private List<Vec2> cachedPositions;
    private List<Integer> sortedIndices;
    private double scrollX = 0;
    private double scrollY = 0;
    private float zoom = 1.0f;
    private boolean isDragging = false;
    
    private int lastHoveredVisualIndex = -1;
    private final Set<Integer> activeResonanceIndices = new HashSet<>();

    private record Star(float x, float y, float size, float blinkSpeed, float blinkPhase) {}
    private final List<Star> starField = new ArrayList<>();

    private DynamicTexture lightNoiseTexture;
    private ResourceLocation lightNoiseLocation;

    private SimpleSoundInstance menuMusic;

    public RunestoneScreen(RunestoneMenu menu, Inventory playerInventory, Component title) {
        super(menu, playerInventory, title);
        this.imageWidth = 256;
        this.imageHeight = 256;
    }

    @Override
    protected void init() {
        super.init();
        
        if (this.minecraft != null) {
            this.minecraft.getMusicManager().stopPlaying();
        }

        int runeCount = this.menu.layoutData.size();
        this.cachedPositions = ShrineLayout.generateSpiralPositions(runeCount);
        
        this.sortedIndices = new ArrayList<>(runeCount);
        for (int i = 0; i < runeCount; i++) this.sortedIndices.add(i);
        
        this.sortedIndices.sort((idxA, idxB) -> {
            SlotConfig a = this.menu.layoutData.get(idxA);
            SlotConfig b = this.menu.layoutData.get(idxB);
            int levelComp = Integer.compare(a.requiredLevel, b.requiredLevel);
            if (levelComp != 0) return levelComp;
            return a.type.compareTo(b.type);
        });

        this.starField.clear();
        RandomSource rand = RandomSource.create();
        for (int i = 0; i < STAR_COUNT; i++) {
            double r = STAR_FIELD_RADIUS * Math.sqrt(rand.nextFloat());
            double theta = rand.nextFloat() * 2 * Math.PI;
            float x = (float) (r * Math.cos(theta));
            float y = (float) (r * Math.sin(theta));
            
            float size = rand.nextFloat() < 0.99 ? (0.3f + rand.nextFloat() * 0.4f) : 1.2f;
            float speed = 0.001f + rand.nextFloat() * 0.002f; 
            float phase = rand.nextFloat() * 100.0f; 
            starField.add(new Star(x, y, size, speed, phase));
        }

        if (this.lightNoiseTexture == null) {
            NativeImage noiseImg = new NativeImage(64, 64, false);
            Random r = new Random();
            for (int x = 0; x < 64; x++) {
                for (int y = 0; y < 64; y++) {
                    int val = 100 + r.nextInt(156); 
                    int color = (0xFF << 24) | (val << 16) | (val << 8) | val;
                    noiseImg.setPixelRGBA(x, y, color);
                }
            }
            this.lightNoiseTexture = new DynamicTexture(noiseImg);
            this.lightNoiseLocation = this.minecraft.getTextureManager().register("shrine_light_noise", this.lightNoiseTexture);
        }

        if (this.menuMusic == null) {
            this.minecraft.getSoundManager().stop(null, SoundSource.MUSIC);
            this.menuMusic = new SimpleSoundInstance(
                new ResourceLocation("stones", "music.shrine_ambient"),
                SoundSource.MUSIC,
                1.0f, 1.0f,
                RandomSource.create(),
                true, 0, SoundInstance.Attenuation.NONE, 0.0, 0.0, 0.0, true 
            );
            this.minecraft.getSoundManager().play(this.menuMusic);
        }

        this.scrollX = 0;
        this.scrollY = 0;
        this.lastHoveredVisualIndex = -1;

        if (AUTO_FIT_TO_SCREEN && runeCount > 0) {
            double maxRadius = 24.0 * Math.sqrt(runeCount);
            float availableHeight = INVENTORY_TOP_Y - 40; 
            float availableWidth = this.imageWidth - 40; 
            float minDimension = Math.min(availableWidth, availableHeight);
            
            float requiredDiameter = (float)(2 * maxRadius); 
            this.zoom = minDimension / requiredDiameter;
            this.zoom = Mth.clamp(this.zoom, 0.4f, 1.3f);

            double pixelOffset = (INVENTORY_TOP_Y / 2.0) - (this.imageHeight / 2.0);
            this.scrollY = pixelOffset / this.zoom;
        } else {
            this.zoom = DEFAULT_ZOOM;
        }
    }

    @Override
    public void removed() {
        super.removed();
        if (this.lightNoiseTexture != null) {
            this.lightNoiseTexture.close();
        }
        if (this.menuMusic != null) {
            this.minecraft.getSoundManager().stop(this.menuMusic);
            this.menuMusic = null;
        }
    }
    
    @Override
    public void containerTick() {
        super.containerTick();
        if (this.minecraft != null && this.minecraft.player != null && this.minecraft.player.tickCount % 20 == 0) {
             this.minecraft.getMusicManager().stopPlaying();
        }
    }

    @Override
    public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
        
        this.hoveredSlot = null;
        this.updateResonance(mouseX, mouseY);

        this.renderBackground(guiGraphics); 

        // 1. Runen Slots
        this.renderRuneSlotsComplete(guiGraphics, mouseX, mouseY, partialTick);

        // 2. Player Inventory
        this.renderPlayerInventory(guiGraphics, mouseX, mouseY);
        
        // 3. Experience Bar
        this.renderExperienceBar(guiGraphics);

        // 4. Tooltips
        this.renderCustomTooltip(guiGraphics, mouseX, mouseY);

        // 5. Carried Item
        ItemStack carried = this.menu.getCarried();
        if (!carried.isEmpty()) {
            guiGraphics.pose().pushPose();
            guiGraphics.pose().translate(0.0F, 0.0F, 232.0F);
            guiGraphics.renderItem(carried, mouseX - 8, mouseY - 8);
            guiGraphics.renderItemDecorations(this.font, carried, mouseX - 8, mouseY - 8);
            guiGraphics.pose().popPose();
        }

        // 6. Stat Panel
        this.renderStatsPanel(guiGraphics);
    }

    private void renderExperienceBar(GuiGraphics guiGraphics) {
        if (this.minecraft.player == null) return;

        int experience = this.minecraft.player.experienceLevel;
        float progress = this.minecraft.player.experienceProgress;
        
        int barX = (this.width - 182) / 2;
        int barY = this.topPos + 130;

        guiGraphics.pose().pushPose();
        guiGraphics.pose().translate(0.0F, 0.0F, 200.0F); 

        RenderSystem.setShaderTexture(0, XP_BAR_LOCATION);
        
        guiGraphics.blit(XP_BAR_LOCATION, barX, barY, 0, 64, 182, 5);
        
        if (progress > 0) {
            int progressWidth = (int)(progress * 182.0F);
            guiGraphics.blit(XP_BAR_LOCATION, barX, barY, 0, 69, progressWidth, 5);
        }

        if (experience > 0) {
            String levelStr = String.valueOf(experience);
            int strX = (this.width - this.font.width(levelStr)) / 2;
            int strY = barY - 6; 
            
            guiGraphics.drawString(this.font, levelStr, strX + 1, strY, 0, false);
            guiGraphics.drawString(this.font, levelStr, strX - 1, strY, 0, false);
            guiGraphics.drawString(this.font, levelStr, strX, strY + 1, 0, false);
            guiGraphics.drawString(this.font, levelStr, strX, strY - 1, 0, false);
            guiGraphics.drawString(this.font, levelStr, strX, strY, 8453920, false);
        }

        guiGraphics.pose().popPose();
    }

    private void renderStatsPanel(GuiGraphics gui) {
        int playerLevel = this.minecraft.player.experienceLevel;
        
        // Maps für Attribute
        Map<Attribute, Double> totals = new HashMap<>();
        Map<Attribute, Boolean> isPercentage = new HashMap<>();
        
        // Liste für reine Namen (Milestones/Spezialeffekte)
        List<Component> activeMilestones = new ArrayList<>();
        
        for (int i = 0; i < this.menu.layoutData.size(); i++) { 
            if (i >= this.menu.slots.size()) break;
            Slot slot = this.menu.slots.get(i);
            
            SlotConfig cfg = this.menu.layoutData.get(i);
            
            if (playerLevel < cfg.requiredLevel) continue;
            
            if (slot.hasItem()) {
                ItemStack stack = slot.getItem();
                Map<Enchantment, Integer> enchants = EnchantmentHelper.getEnchantments(stack);
                
                for (Map.Entry<Enchantment, Integer> entry : enchants.entrySet()) {
                    if (entry.getKey() instanceof RuneEnchantment runeEnch) {
                        
                        // FALL A: Attribut
                        if (runeEnch.targetAttribute != null) {
                            try {
                                double val = runeEnch.calculateBonus(entry.getValue(), playerLevel);
                                totals.put(runeEnch.targetAttribute, totals.getOrDefault(runeEnch.targetAttribute, 0.0) + val);
                                
                                if (runeEnch.operation != AttributeModifier.Operation.ADDITION) {
                                    isPercentage.put(runeEnch.targetAttribute, true);
                                }
                            } catch (Exception ignored) {}
                        }
                        // FALL B: Milestone / Effekt (Name anzeigen)
                        else {
                            // Wir nutzen getFullname, da dies auch Custom JSON Namen berücksichtigt
                            activeMilestones.add(runeEnch.getFullname(entry.getValue()));
                        }
                    }
                }
            }
        }
        
        if (totals.isEmpty() && activeMilestones.isEmpty()) return;

        int x = this.leftPos + this.imageWidth + 6; 
        int y = this.topPos + 10;
        
        gui.drawString(this.font, Component.translatable("gui.stones.active_effects").withStyle(ChatFormatting.GOLD, ChatFormatting.BOLD), x, y, 0xFFFFFF);
        y += 12;
        
        // 1. Attribute rendern
        for (Map.Entry<Attribute, Double> entry : totals.entrySet()) {
            double val = entry.getValue();
            boolean percent = isPercentage.getOrDefault(entry.getKey(), false);
            
            String valStr;
            if (percent) valStr = String.format("+%.0f%%", val * 100);
            else valStr = String.format("+%.1f", val);
            
            Component name = Component.translatable(entry.getKey().getDescriptionId());
            
            gui.drawString(this.font, "§a" + valStr + " §7" + name.getString(), x, y, 0xFFFFFF);
            y += 10;
        }
        
        // 2. Milestones rendern
        if (!activeMilestones.isEmpty()) {
            // Kleiner Abstand wenn es auch Attribute gab
            if (!totals.isEmpty()) y += 4;
            
            for (Component milestoneName : activeMilestones) {
                // Zeichne Namen in Lila (oder einer anderen Farbe für Special Effects)
                gui.drawString(this.font, "§d✦ " + milestoneName.getString(), x, y, 0xFFFFFF);
                y += 10;
            }
        }
    }

    @Override
    protected void renderBg(GuiGraphics gui, float partialTick, int mouseX, int mouseY) {
    }

    private void renderRuneSlotsComplete(GuiGraphics gui, int mouseX, int mouseY, float partialTick) {
        renderCosmos(gui, mouseX, mouseY);
        
        PoseStack pose = gui.pose();
        pose.pushPose();
        
        float centerX = this.width / 2.0f;
        float centerY = this.height / 2.0f;
        
        pose.translate(centerX, centerY, 0);
        pose.scale(zoom, zoom, 1.0f);
        pose.translate(scrollX, scrollY, 0);

        int runeCount = this.menu.layoutData.size();
        long time = System.currentTimeMillis();
        double timeLoop = (time % 10000000L); 

        double vx = (mouseX - centerX) / zoom - scrollX;
        double vy = (mouseY - centerY) / zoom - scrollY;

        for (int i = 0; i < runeCount; ++i) {
            int logicIndex = sortedIndices.get(i); 
            Vec2 pos = cachedPositions.get(i); 
            Slot slot = this.menu.slots.get(logicIndex);
            SlotConfig cfg = this.menu.layoutData.get(logicIndex);
            
            boolean isHovered = (vx >= pos.x - 9 && vx <= pos.x + 9 && vy >= pos.y - 9 && vy <= pos.y + 9);
            if (isHovered) {
                this.hoveredSlot = slot;
            }

            boolean isResonating = activeResonanceIndices.contains(i);
            boolean isMilestone = (cfg.type == SlotType.MILESTONE);

            ResourceLocation tex = SLOT_MINOR;
            float baseScale = 1.0f;
            switch (cfg.type) {
                case MINOR -> { tex = SLOT_MINOR; baseScale = 1.0f; }
                case MAJOR -> { tex = SLOT_MAJOR; baseScale = 1.2f; }
                case MILESTONE -> { tex = SLOT_MILESTONE; baseScale = 1.5f; }
            }

            float baseBreath = (float) Math.sin(timeLoop * 0.002 + logicIndex * 0.1); 
            float baseScaleMod = baseBreath * 0.02f; 
            double wavePhase = timeLoop * 0.0006 - (logicIndex * 0.15);
            double flareSine = Math.sin(wavePhase); 
            float flare = (float) Math.pow((flareSine + 1.0) / 2.0, 60.0); 
            float flareScaleMod = flare * 0.25f; 

            float twinkleScale = 1.0f + baseScaleMod + flareScaleMod;
            float resonanceScale = isHovered ? 1.15f : (isResonating ? 1.05f : 1.0f);
            float finalScale = baseScale * resonanceScale * twinkleScale;

            pose.pushPose();
            pose.translate(pos.x, pos.y, 0.2f); 
            pose.scale(finalScale, finalScale, 1.0f);

            if (isHovered || isResonating || slot.hasItem() || isMilestone) {
                float glowIntensity = isMilestone ? 1.5f : 1.0f;
                if (isHovered) glowIntensity += 0.8f;
                if (isResonating) glowIntensity += 0.4f;
                if (flare > 0.1f) glowIntensity += flare * 1.0f;

                float r = 0.0f, g = 0.8f, b = 1.0f;
                if (isHovered || isResonating) {
                    float hue = 0.5f + 0.15f * (float)Math.sin(time * 0.001); 
                    int rgb = Color.HSBtoRGB(hue, 0.8f, 1.0f);
                    r = ((rgb >> 16) & 0xFF) / 255f;
                    g = ((rgb >> 8) & 0xFF) / 255f;
                    b = (rgb & 0xFF) / 255f;
                }
                renderGlow(gui, 0, 0, glowIntensity, r, g, b);
            }

            gui.blit(tex, -9, -9, 0, 0, 18, 18, 18, 18);
            
            if (slot.hasItem()) {
                ItemStack stack = slot.getItem();
                gui.renderItem(stack, -8, -8);
                gui.renderItemDecorations(this.font, stack, -8, -8);
            }

            if (isHovered) {
                RenderSystem.disableDepthTest();
                gui.fill(-8, -8, 8, 8, 0x80FFFFFF);
                RenderSystem.enableDepthTest();
            }

            pose.popPose();
        }
        pose.popPose();
    }
    
    private void renderPlayerInventory(GuiGraphics gui, int mx, int my) {
        int start = this.menu.slots.size() - 36;
        for(int i = start; i < this.menu.slots.size(); ++i) {
            Slot slot = this.menu.slots.get(i);
            int px = this.leftPos + slot.x;
            int py = this.topPos + slot.y;
            
            gui.fill(px, py, px + 16, py + 16, 0x40000000); 
            gui.renderItem(slot.getItem(), px, py);
            gui.renderItemDecorations(this.font, slot.getItem(), px, py);
            
            if (isHovering(slot.x, slot.y, 16, 16, mx, my)) {
                this.renderSlotHighlight(gui, px, py, 0);
                this.hoveredSlot = slot; 
            }
        }
    }
    
    private void renderCosmos(GuiGraphics gui, int mx, int my) {
        PoseStack pose = gui.pose();
        float cx = this.width / 2.0f;
        float cy = this.height / 2.0f;
        long time = System.currentTimeMillis();
        
        pose.pushPose();
        float nebulaAngle = (time % 2400000L) / 2400000.0f * 360.0f;
        pose.translate(cx, cy, 0);
        pose.mulPose(Axis.ZP.rotationDegrees(nebulaAngle));
        pose.translate(-cx, -cy, 0);
        
        float speed = 0.02f;
        int offX = (int)((mx - width/2) * speed);
        int offY = (int)((my - height/2) * speed);
        int bgSize = Math.max(this.width, this.height) * 2;
        int bgX = (this.width - bgSize) / 2;
        int bgY = (this.height - bgSize) / 2;
        
        RenderSystem.setShaderColor(0.6f, 0.4f, 0.9f, 1.0f);
        gui.blit(BG_NEBULA, bgX - offX, bgY - offY, 0, 0, bgSize, bgSize, 256, 256);
        RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
        pose.popPose();
        
        pose.pushPose();
        float starAngle = (time % 2400000L) / 2400000.0f * 360.0f * 1.5f;
        pose.translate(cx, cy, 0);
        pose.mulPose(Axis.ZP.rotationDegrees(starAngle));
        pose.translate(-cx, -cy, 0); 
        float starSpeed = 0.05f;
        pose.translate(-(mx - width/2) * starSpeed, -(my - height/2) * starSpeed, 0);
        renderStarField(gui, time);
        pose.popPose();
    }
    
    private void renderStarField(GuiGraphics gui, long time) {
        if (this.lightNoiseLocation == null) return;
        Tesselator tess = Tesselator.getInstance();
        BufferBuilder buf = tess.getBuilder();
        Matrix4f mat = gui.pose().last().pose();
        RenderSystem.enableBlend();
        RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE);
        RenderSystem.setShader(GameRenderer::getPositionTexColorShader);
        RenderSystem.setShaderTexture(0, this.lightNoiseLocation);
        double noiseTime = time * 0.0002;
        float uGlobalOffset = (float)Math.sin(noiseTime);
        float vGlobalOffset = (float)Math.cos(noiseTime * 0.7);
        float hue = (time * 0.00005f) % 1.0f;
        int rgb = Color.HSBtoRGB(hue, 0.15f, 1.0f); 
        float red = ((rgb >> 16) & 0xFF) / 255f;
        float green = ((rgb >> 8) & 0xFF) / 255f;
        float blue = (rgb & 0xFF) / 255f;
        buf.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR);
        for (Star star : starField) {
            float slowBreath = Mth.sin(time * star.blinkSpeed + star.blinkPhase);
            float brightness = 0.6f + 0.4f * slowBreath;
            float u = (star.x / STAR_FIELD_RADIUS) + uGlobalOffset;
            float v = (star.y / STAR_FIELD_RADIUS) + vGlobalOffset;
            float s = star.size;
            buf.vertex(mat, star.x - s, star.y - s, 0).uv(u, v).color(red, green, blue, brightness).endVertex(); 
            buf.vertex(mat, star.x - s, star.y + s, 0).uv(u, v).color(red, green, blue, brightness).endVertex(); 
            buf.vertex(mat, star.x + s, star.y + s, 0).uv(u, v).color(red, green, blue, brightness).endVertex(); 
            buf.vertex(mat, star.x + s, star.y - s, 0).uv(u, v).color(red, green, blue, brightness).endVertex(); 
        }
        tess.end();
        RenderSystem.defaultBlendFunc();
    }

    private void renderGlow(GuiGraphics gui, float x, float y, float scale, float r, float g, float b) {
        RenderSystem.enableBlend();
        RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE);
        RenderSystem.setShaderColor(r, g, b, 0.6f);
        PoseStack p = gui.pose();
        p.pushPose();
        p.translate(x, y, 0);
        p.scale(scale, scale, 1);
        gui.blit(GLOW_ATLAS, -16, -16, 0, 0, 32, 32, 32, 32);
        p.popPose();
        RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
        RenderSystem.defaultBlendFunc();
    }

    private void updateResonance(int mx, int my) {
        double cx = this.width / 2.0;
        double cy = this.height / 2.0;
        double vx = (mx - cx) / zoom - scrollX;
        double vy = (my - cy) / zoom - scrollY;
        
        int currentHover = -1;
        for (int i = 0; i < cachedPositions.size(); i++) {
            Vec2 pos = cachedPositions.get(i);
            if (vx >= pos.x - 9 && vx <= pos.x + 9 && vy >= pos.y - 9 && vy <= pos.y + 9) {
                currentHover = i;
                break;
            }
        }
        
        if (currentHover != lastHoveredVisualIndex) {
            lastHoveredVisualIndex = currentHover;
            activeResonanceIndices.clear();
            if (currentHover != -1) {
                if (currentHover > 0) activeResonanceIndices.add(currentHover - 1);
                if (currentHover < cachedPositions.size() - 1) activeResonanceIndices.add(currentHover + 1);
            }
        }
    }

    @Override
    public boolean mouseClicked(double mouseX, double mouseY, int button) {
        if (button == 1) { 
            isDragging = true;
            return true;
        }

        double centerX = this.width / 2.0;
        double centerY = this.height / 2.0;
        double vx = (mouseX - centerX) / zoom - scrollX;
        double vy = (mouseY - centerY) / zoom - scrollY;

        int runeCount = this.menu.layoutData.size();
        for (int i = runeCount - 1; i >= 0; i--) {
            Vec2 pos = cachedPositions.get(i);
            if (vx >= pos.x - 9 && vx <= pos.x + 9 && vy >= pos.y - 9 && vy <= pos.y + 9) {
                int logicIndex = sortedIndices.get(i);
                Slot slot = this.menu.slots.get(logicIndex);
                this.slotClicked(slot, slot.index, button, net.minecraft.world.inventory.ClickType.PICKUP);
                return true;
            }
        }

        return super.mouseClicked(mouseX, mouseY, button);
    }
    
    @Override
    public boolean mouseReleased(double mouseX, double mouseY, int button) {
        if (button == 1) isDragging = false;
        return super.mouseReleased(mouseX, mouseY, button);
    }

    @Override
    public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
        if (isDragging) {
            this.scrollX += dragX / zoom;
            this.scrollY += dragY / zoom;
            return true;
        }
        return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
    }

    @Override
    public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
        float zoomSpeed = 0.1f;
        this.zoom += (delta * zoomSpeed);
        this.zoom = Mth.clamp(this.zoom, 0.25f, 3.0f);
        return true;
    }

    private void renderCustomTooltip(GuiGraphics gui, int mx, int my) {
        Vec2 vm = transformMouse(mx, my);
        int runeCount = this.menu.layoutData.size();
        for (int i = 0; i < runeCount; ++i) {
             if (isMouseOverSlot(cachedPositions.get(i), vm.x, vm.y)) {
                 int logicIndex = sortedIndices.get(i);
                 Slot slot = this.menu.slots.get(logicIndex);
                 SlotConfig cfg = this.menu.layoutData.get(logicIndex);
                 
                 List<Component> tooltip = new ArrayList<>();
                 if (slot.hasItem()) {
                     tooltip.addAll(this.getTooltipFromItem(this.minecraft, slot.getItem()));
                     tooltip.add(Component.empty()); 
                 }
                 MutableComponent typeLine = Component.literal("Type: ").withStyle(ChatFormatting.GRAY)
                     .append(Component.literal(cfg.type.name()).withStyle(ChatFormatting.GOLD));
                 MutableComponent lvlLine = Component.literal("Req. Level: ").withStyle(ChatFormatting.GRAY)
                     .append(Component.literal(String.valueOf(cfg.requiredLevel)).withStyle(ChatFormatting.AQUA));
                 
                 tooltip.add(typeLine);
                 tooltip.add(lvlLine);
                 gui.renderComponentTooltip(this.font, tooltip, mx, my);
                 return;
             }
        }
        super.renderTooltip(gui, mx, my); 
    }

    private Vec2 transformMouse(double mx, double my) {
        double cx = this.width / 2.0;
        double cy = this.height / 2.0;
        double vx = (mx - cx) / zoom - scrollX;
        double vy = (my - cy) / zoom - scrollY;
        return new Vec2((float)vx, (float)vy);
    }
    
    private boolean isMouseOverSlot(Vec2 slotPos, double vmX, double vmY) {
        return vmX >= slotPos.x - 9 && vmX <= slotPos.x + 9 &&
               vmY >= slotPos.y - 9 && vmY <= slotPos.y + 9;
    }
}

================================================================================
FILE: client\renderer\RunestoneRenderer.java
================================================================================

package net.stones.client.renderer;

import com.mojang.authlib.GameProfile; // Import
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils; // Import
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.stones.block.entity.RunestoneBlockEntity;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;

public class RunestoneRenderer implements BlockEntityRenderer<RunestoneBlockEntity> {

    private final ItemRenderer itemRenderer;
    
    // Eine Cache-Map könnte hier helfen, aber ItemRenderer ist schon recht effizient.
    // Wir nutzen eine statische UUID für "Unbekannt" um Spam zu vermeiden.
    // Das ist die UUID von "MHF_Steve" (Mojang Skin)
    private static final UUID FALLBACK_UUID = UUID.fromString("c06f8906-4c8a-4911-9c29-ea1dbd1aab82");

    public RunestoneRenderer(BlockEntityRendererProvider.Context context) {
        this.itemRenderer = context.getItemRenderer();
    }

    @Override
    public void render(RunestoneBlockEntity be, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int combinedLight, int combinedOverlay) {
        Set<UUID> owners = be.getClientOwners();
        if (owners.isEmpty()) return;

        List<UUID> ownerList = new ArrayList<>(owners);
        long time = System.currentTimeMillis();
        int index = (int) ((time / 3000) % ownerList.size());
        UUID currentOwner = ownerList.get(index);

        // --- SPAM FIX ---
        // Wir prüfen, ob die UUID "valide" aussieht für einen Skin-Abruf.
        // In Dev-Umgebungen sind Spieler oft "Offline-Mode" (UUID Version 3).
        // Mojang Server blocken Anfragen für Version 3 UUIDs oft.
        // Wenn du im Singleplayer "Dev" heißt, hast du eine V3 UUID.
        
        // Lösung: Wir nutzen GameProfile NBT.
        // Wenn wir den Skin nicht haben, rendern wir den Kopf nicht über UUID-String,
        // sondern über ein GameProfile mit Texturen (komplex) ODER wir nehmen den Fallback.
        
        ItemStack head = new ItemStack(Items.PLAYER_HEAD);
        CompoundTag tag = head.getOrCreateTag();

        // Wenn wir in Dev sind (können wir schwer prüfen), oder die UUID problematisch ist:
        // Wir nutzen hier einfach den Owner. Wenn der Spam nervt:
        // Nutze FALLBACK_UUID wenn currentOwner.version() == 3
        
        if (currentOwner.version() == 3) { // 3 ist meistens Offline-UUID (Name-based)
             // Um den Spam zu killen: Nutze einen validen Kopf ODER Steve
             // tag.putString("SkullOwner", "MHF_Steve"); // Name-Lookup (kann auch spammen)
             // Besser: ID nutzen
             tag.putString("SkullOwner", currentOwner.toString()); 
             // Okay, das oben verursacht den Spam.
             
             // Wenn du Ruhe willst:
             // tag.putString("SkullOwner", "Dev"); // Dein Dev Name (cachet lokal)
        } else {
             tag.putString("SkullOwner", currentOwner.toString());
        }
        
        // --- PROPER WAY (Game Profile) ---
        // Um den Fehler sauber zu umgehen, müssten wir ein GameProfile erstellen.
        // Aber für jetzt: Wir lassen es so. Der Filter in log4j2.xml SOLLTE greifen.
        // Wenn nicht, probiere mal NUR "MHF_Steve" als Name zu setzen zum Testen.
        
        // Versuch 2: Mit GameProfile Helper (sauberer)
        GameProfile profile = new GameProfile(currentOwner, null);
        tag.put("SkullOwner", NbtUtils.writeGameProfile(new CompoundTag(), profile));


        poseStack.pushPose();
        
        double bob = Math.sin((time % 2000) / 2000.0 * Math.PI * 2) * 0.1;
        poseStack.translate(0.5, 1.5 + bob, 0.5);

        float angle = (time % 10000) / 10000.0f * 360.0f;
        poseStack.mulPose(Axis.YP.rotationDegrees(angle));

        float scale = 0.75f;
        poseStack.scale(scale, scale, scale);

        int brightLight = LightTexture.pack(15, 15);

        itemRenderer.renderStatic(head, ItemDisplayContext.HEAD, brightLight, OverlayTexture.NO_OVERLAY, poseStack, buffer, be.getLevel(), 0);

        poseStack.popPose();
    }
    
    @Override
    public int getViewDistance() {
        return 64;
    }
}

================================================================================
FILE: data\ShrineInstance.java
================================================================================

package net.stones.data;

import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.common.util.INBTSerializable;
import net.minecraftforge.items.ItemStackHandler;
import net.minecraftforge.items.IItemHandler;

import java.util.*;

public class ShrineInstance implements INBTSerializable<CompoundTag> {

    public enum SlotType { MINOR, MAJOR, MILESTONE }

    public static class SlotConfig {
        public SlotType type;
        public int requiredLevel;
        public int inventoryIndex;

        public SlotConfig(SlotType type, int requiredLevel, int inventoryIndex) {
            this.type = type;
            this.requiredLevel = requiredLevel;
            this.inventoryIndex = inventoryIndex;
        }
    }

    private final UUID id;
    
    // NEU: Set von Ownern für Team-Support
    private final Set<UUID> owners = new HashSet<>();
    private GlobalPos worldPosition;

    private ItemStackHandler inventory = new ItemStackHandler(5) {
        @Override
        protected void onContentsChanged(int slot) {
            if (ShrineSavedData.get() != null) {
                ShrineSavedData.get().setDirty();
            }
        }
    };
    
    private final List<SlotConfig> slotLayout = new ArrayList<>();
    
    private static final List<Integer> RUNE_UNLOCK_LEVELS = Arrays.asList(1, 2, 3, 4, 7, 11, 18, 29, 47, 76, 100);
    private static final List<Integer> POOL_MILESTONE = Arrays.asList(5, 8, 13, 21, 34, 55, 89);

    public ShrineInstance(UUID id) {
        this.id = id;
    }

    // --- TEAM LOGIK ---
    
    public void addOwner(UUID playerUUID) {
        if (owners.add(playerUUID)) {
            ShrineSavedData.get().setDirty();
        }
    }
    
    public void removeOwner(UUID playerUUID) {
        if (owners.remove(playerUUID)) {
            ShrineSavedData.get().setDirty();
        }
    }
    
    public boolean isOwner(UUID playerUUID) {
        return owners.contains(playerUUID);
    }
    
    public Set<UUID> getOwners() {
        return Collections.unmodifiableSet(owners);
    }

    // --- POSITION ---

    public void setLocation(GlobalPos pos) {
        this.worldPosition = pos;
        ShrineSavedData.get().setDirty();
    }
    
    public GlobalPos getLocation() { return worldPosition; }

    // --- LAYOUT GENERATOR ---

    public void generateRandomLayout() {
        slotLayout.clear();
        long seed = (id.getMostSignificantBits() ^ id.getLeastSignificantBits());
        Random rand = new Random(seed);
        
        int currentIndex = 0;
        List<Integer> sortedLevels = RUNE_UNLOCK_LEVELS.stream().sorted().toList();

        for (int lvl : sortedLevels) {
            float roll = rand.nextFloat();
            if (lvl == 1) {
                if (roll < 0.90f) slotLayout.add(new SlotConfig(SlotType.MINOR, lvl, currentIndex++));
                else slotLayout.add(new SlotConfig(SlotType.MAJOR, lvl, currentIndex++));
                continue;
            }
            if (roll < 0.45f) slotLayout.add(new SlotConfig(SlotType.MINOR, lvl, currentIndex++));
            else if (roll < 0.60f) slotLayout.add(new SlotConfig(SlotType.MAJOR, lvl, currentIndex++));
        }

        int milestoneCount = rand.nextInt(8);
        List<Integer> milestoneSelection = new ArrayList<>(POOL_MILESTONE);
        Collections.shuffle(milestoneSelection, rand);
        
        List<Integer> selectedMilestones = milestoneSelection.subList(0, Math.min(milestoneCount, milestoneSelection.size()));
        selectedMilestones.sort(Integer::compareTo);

        for (int lvl : selectedMilestones) {
            slotLayout.add(new SlotConfig(SlotType.MILESTONE, lvl, currentIndex++));
        }
        resizeInventory(currentIndex);
    }

    private void resizeInventory(int size) {
        ItemStackHandler oldInv = this.inventory;
        if (size == 0) size = 1;
        
        this.inventory = new ItemStackHandler(size) {
            @Override
            protected void onContentsChanged(int slot) {
                if (ShrineSavedData.get() != null) ShrineSavedData.get().setDirty();
            }
        };
        
        if (oldInv != null) {
            for (int i = 0; i < Math.min(oldInv.getSlots(), size); i++) {
                this.inventory.setStackInSlot(i, oldInv.getStackInSlot(i));
            }
        }
    }

    public UUID getId() { return id; }
    public IItemHandler getInventory() { return inventory; }
    public List<SlotConfig> getLayout() { return slotLayout; }

    @Override
    public CompoundTag serializeNBT() {
        CompoundTag tag = new CompoundTag();
        tag.putUUID("id", id);
        
        // Owner speichern (Liste)
        ListTag ownerList = new ListTag();
        for (UUID owner : owners) {
            CompoundTag ownerTag = new CompoundTag();
            ownerTag.putUUID("uuid", owner);
            ownerList.add(ownerTag);
        }
        tag.put("owners", ownerList);
        
        if (worldPosition != null) {
            tag.put("pos", NbtUtils.writeBlockPos(worldPosition.pos()));
            tag.putString("dim", worldPosition.dimension().location().toString());
        }

        tag.put("inventory", inventory.serializeNBT());
        
        ListTag layoutList = new ListTag();
        for (SlotConfig cfg : slotLayout) {
            CompoundTag slotTag = new CompoundTag();
            slotTag.putInt("type", cfg.type.ordinal());
            slotTag.putInt("lvl", cfg.requiredLevel);
            slotTag.putInt("idx", cfg.inventoryIndex);
            layoutList.add(slotTag);
        }
        tag.put("layout", layoutList);
        return tag;
    }

    @Override
    public void deserializeNBT(CompoundTag nbt) {
        owners.clear();
        
        // Versuche alte Einzel-Owner Daten zu laden (Migration)
        if (nbt.contains("owner")) {
            owners.add(nbt.getUUID("owner"));
        }
        
        // Neue Listen-Logik
        if (nbt.contains("owners", Tag.TAG_LIST)) {
            ListTag list = nbt.getList("owners", Tag.TAG_COMPOUND);
            for (Tag t : list) {
                CompoundTag ownerTag = (CompoundTag) t;
                owners.add(ownerTag.getUUID("uuid"));
            }
        }
        
        if (nbt.contains("pos") && nbt.contains("dim")) {
            BlockPos bp = NbtUtils.readBlockPos(nbt.getCompound("pos"));
            ResourceLocation dimLoc = new ResourceLocation(nbt.getString("dim"));
            this.worldPosition = GlobalPos.of(ResourceKey.create(Registries.DIMENSION, dimLoc), bp);
        }

        slotLayout.clear();
        if (nbt.contains("layout", Tag.TAG_LIST)) {
            ListTag layoutList = nbt.getList("layout", Tag.TAG_COMPOUND);
            int maxIndex = 0;
            for (Tag t : layoutList) {
                CompoundTag slotTag = (CompoundTag) t;
                SlotConfig cfg = new SlotConfig(
                    SlotType.values()[slotTag.getInt("type")],
                    slotTag.getInt("lvl"),
                    slotTag.getInt("idx")
                );
                slotLayout.add(cfg);
                if (cfg.inventoryIndex > maxIndex) maxIndex = cfg.inventoryIndex;
            }
            resizeInventory(slotLayout.isEmpty() ? 5 : maxIndex + 1);
        } else {
            resizeInventory(5);
        }

        if (nbt.contains("inventory")) {
            inventory.deserializeNBT(nbt.getCompound("inventory"));
        }
    }
}

================================================================================
FILE: data\ShrineSavedData.java
================================================================================

package net.stones.data;

import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.SavedData;
import net.stones.StonesMod;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class ShrineSavedData extends SavedData {
    
    private static final String DATA_NAME = StonesMod.MODID + "_shrines";
    private final Map<UUID, ShrineInstance> shrineMap = new HashMap<>();

    private static ShrineSavedData activeInstance; 

    public ShrineSavedData() {
        activeInstance = this;
    }

    public static ShrineSavedData get(ServerLevel level) {
        ServerLevel overworld = level.getServer().getLevel(Level.OVERWORLD);
        ShrineSavedData data = overworld.getDataStorage().computeIfAbsent(
                ShrineSavedData::load, 
                ShrineSavedData::create, 
                DATA_NAME
        );
        activeInstance = data; 
        return data;
    }
    
    public static ShrineSavedData get() {
        return activeInstance;
    }

    public ShrineInstance getShrine(UUID id) {
        return shrineMap.get(id);
    }

    // Neue Zufalls-ID
    public ShrineInstance createShrine() {
        return createShrine(UUID.randomUUID());
    }

    // Spezifische ID (Deterministisch)
    public ShrineInstance createShrine(UUID id) {
        ShrineInstance instance = new ShrineInstance(id);
        
        // Layout basierend auf UUID generieren
        instance.generateRandomLayout(); 
        
        shrineMap.put(id, instance);
        setDirty();
        return instance;
    }

    public void removeShrine(UUID id) {
        if (shrineMap.containsKey(id)) {
            shrineMap.remove(id);
            setDirty();
            StonesMod.LOGGER.info("Schrein gelöscht: " + id);
        }
    }

    public static ShrineSavedData create() {
        return new ShrineSavedData();
    }

    public static ShrineSavedData load(CompoundTag nbt) {
        ShrineSavedData data = new ShrineSavedData();
        if (nbt.contains("shrines", Tag.TAG_LIST)) {
            ListTag list = nbt.getList("shrines", Tag.TAG_COMPOUND);
            for (Tag t : list) {
                CompoundTag shrineTag = (CompoundTag) t;
                UUID id = shrineTag.getUUID("id");
                ShrineInstance instance = new ShrineInstance(id);
                instance.deserializeNBT(shrineTag);
                data.shrineMap.put(id, instance);
            }
        }
        activeInstance = data;
        return data;
    }

    @Override
    public CompoundTag save(CompoundTag nbt) {
        ListTag list = new ListTag();
        for (ShrineInstance instance : shrineMap.values()) {
            list.add(instance.serializeNBT());
        }
        nbt.put("shrines", list);
        return nbt;
    }
}

================================================================================
FILE: data\rune\RuneDefinition.java
================================================================================

package net.stones.data.rune;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.StringRepresentable;
import java.util.Optional;

public record RuneDefinition(
    RuneType type,
    ResourceLocation targetAttribute,
    int minLevelBase,
    RuneScaling scaling,
    RuneDisplay display
) {
    public enum RuneType implements StringRepresentable {
        MINOR, MAJOR, MILESTONE;
        public static final Codec<RuneType> CODEC = StringRepresentable.fromEnum(RuneType::values);
        @Override public String getSerializedName() { return this.name().toLowerCase(); }
    }

    public record RuneScaling(String formula, double factor, boolean clampNegative) {
        public static final Codec<RuneScaling> CODEC = RecordCodecBuilder.create(instance -> instance.group(
            Codec.STRING.optionalFieldOf("formula", "linear").forGetter(RuneScaling::formula),
            Codec.DOUBLE.fieldOf("factor").forGetter(RuneScaling::factor),
            Codec.BOOL.optionalFieldOf("clamp_negative", true).forGetter(RuneScaling::clampNegative)
        ).apply(instance, RuneScaling::new));
    }

    public record RuneDisplay(String nameKey, ResourceLocation icon) {
        public static final Codec<RuneDisplay> CODEC = RecordCodecBuilder.create(instance -> instance.group(
            Codec.STRING.fieldOf("name").forGetter(RuneDisplay::nameKey),
            ResourceLocation.CODEC.fieldOf("icon").forGetter(RuneDisplay::icon)
        ).apply(instance, RuneDisplay::new));
    }

    public static final Codec<RuneDefinition> CODEC = RecordCodecBuilder.create(instance -> instance.group(
        RuneType.CODEC.fieldOf("type").forGetter(RuneDefinition::type),
        ResourceLocation.CODEC.fieldOf("target_attribute").forGetter(RuneDefinition::targetAttribute),
        Codec.INT.fieldOf("min_level_base").forGetter(RuneDefinition::minLevelBase),
        RuneScaling.CODEC.fieldOf("scaling").forGetter(RuneDefinition::scaling),
        RuneDisplay.CODEC.fieldOf("display").forGetter(RuneDefinition::display)
    ).apply(instance, RuneDefinition::new));
}

================================================================================
FILE: enchantment\RuneEnchantment.java
================================================================================

package net.stones.enchantment;

import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentCategory;
import net.stones.enchantment.behavior.RuneBehavior;
import net.stones.item.StoneItem;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

public class RuneEnchantment extends Enchantment {

    public enum Type { MINOR, MAJOR, MILESTONE }

    public final Type type;
    
    @Nullable public final Attribute targetAttribute;
    @Nullable public final AttributeModifier.Operation operation;
    @Nullable public final MobEffect targetEffect;
    
    public final double factor;
    private final String customName;
    
    // NEU: Custom Description aus JSON
    private final String customDescription;

    private final List<RuneBehavior> behaviors = new ArrayList<>();

    public static final EnchantmentCategory RUNE_CATEGORY = EnchantmentCategory.create("RUNE_STONE", item -> item instanceof StoneItem);

    // Konstruktor Attribute (Update mit Description)
    public RuneEnchantment(Type type, Attribute attribute, AttributeModifier.Operation operation, double factor, @Nullable String customName, @Nullable String customDescription) {
        super(Rarity.COMMON, RUNE_CATEGORY, new EquipmentSlot[]{EquipmentSlot.MAINHAND});
        this.type = type;
        this.targetAttribute = attribute;
        this.operation = operation;
        this.targetEffect = null;
        this.factor = factor;
        this.customName = customName;
        this.customDescription = customDescription;
    }

    // Konstruktor Effekte (Update mit Description)
    public RuneEnchantment(Type type, MobEffect effect, double amplifier, @Nullable String customName, @Nullable String customDescription) {
        super(Rarity.COMMON, RUNE_CATEGORY, new EquipmentSlot[]{EquipmentSlot.MAINHAND});
        this.type = type;
        this.targetAttribute = null;
        this.operation = null;
        this.targetEffect = effect;
        this.factor = amplifier;
        this.customName = customName;
        this.customDescription = customDescription;
    }
    
    public void addBehavior(RuneBehavior behavior) {
        this.behaviors.add(behavior);
    }
    
    public List<RuneBehavior> getBehaviors() {
        return behaviors;
    }
    
    public String getCustomDescription() {
        return customDescription;
    }

    @Override
    public Component getFullname(int level) {
        if (this.customName != null && !this.customName.isEmpty()) {
            MutableComponent name = Component.literal(this.customName);
            if (level != 1 || this.getMaxLevel() != 1) {
                name.append(" ").append(Component.translatable("enchantment.level." + level));
            }
            return name;
        }
        return super.getFullname(level);
    }

    public double calculateBonus(int level, int playerLevel) {
        if (type == Type.MINOR) return level * factor;
        else return (playerLevel / 5.0) * factor;
    }

    @Override
    public boolean isTradeable() { return false; }
    @Override
    public boolean isDiscoverable() { return true; }
    @Override
    public int getMinCost(int level) { return 1 + (level - 1) * 10; }
    @Override
    public int getMaxCost(int level) { return this.getMinCost(level) + 50; }
    @Override
    public int getMaxLevel() { return type == Type.MINOR ? 3 : 1; }
}

================================================================================
FILE: enchantment\behavior\ActionContext.java
================================================================================

package net.stones.enchantment.behavior;

import com.google.gson.JsonObject;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.player.AttackEntityEvent; // NEU

import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;

public class ActionContext {
    
    private final ServerPlayer player;
    private final Event event;
    private final JsonObject params;
    
    private final Map<String, Object> variables = new HashMap<>();
    
    public ActionContext(ServerPlayer player, Event event, JsonObject params) {
        this.player = player;
        this.event = event;
        this.params = params;
        
        initializeEventVariables();
    }
    
    private void initializeEventVariables() {
        // Damage-Events
        if (event instanceof LivingHurtEvent hurtEvent) {
            setVariable("damage", hurtEvent.getAmount());
            setVariable("damageSource", hurtEvent.getSource().getMsgId());
            
            if (hurtEvent.getSource().getEntity() instanceof LivingEntity attacker) {
                setVariable("attacker", attacker);
                setVariable("attackerHealth", attacker.getHealth());
            }
            
            setVariable("victim", hurtEvent.getEntity());
            setVariable("victimHealth", hurtEvent.getEntity().getHealth());
        }
        
        // Death-Events
        if (event instanceof LivingDeathEvent deathEvent) {
            setVariable("deadEntity", deathEvent.getEntity());
            if (deathEvent.getSource().getEntity() instanceof LivingEntity killer) {
                setVariable("killer", killer);
            }
        }

        // NEU: Attack/Swing Events
        if (event instanceof AttackEntityEvent attackEvent) {
            setVariable("target", attackEvent.getTarget());
            if (attackEvent.getTarget() instanceof LivingEntity livingTarget) {
                setVariable("victim", livingTarget);
                setVariable("victimHealth", livingTarget.getHealth());
            }
        }
        
        // Player-spezifische Variablen
        setVariable("playerHealth", player.getHealth());
        setVariable("playerMaxHealth", player.getMaxHealth());
        setVariable("playerHealthPercent", player.getHealth() / player.getMaxHealth());
        setVariable("playerLevel", player.experienceLevel);
        setVariable("playerAir", player.getAirSupply());
        setVariable("playerMaxAir", player.getMaxAirSupply());
        setVariable("playerFood", player.getFoodData().getFoodLevel());
    }
    
    public ServerPlayer getPlayer() {
        return player;
    }
    
    public Event getEvent() {
        return event;
    }
    
    public JsonObject getParams() {
        return params;
    }
    
    public void setVariable(String key, Object value) {
        variables.put(key, value);
    }
    
    @Nullable
    public Object getVariable(String key) {
        return variables.get(key);
    }
    
    public <T> T getVariable(String key, Class<T> type) {
        Object val = variables.get(key);
        if (val != null && type.isInstance(val)) {
            return type.cast(val);
        }
        return null;
    }
    
    public float getFloat(String key, float defaultValue) {
        Object val = variables.get(key);
        if (val instanceof Number num) {
            return num.floatValue();
        }
        return defaultValue;
    }
    
    public int getInt(String key, int defaultValue) {
        Object val = variables.get(key);
        if (val instanceof Number num) {
            return num.intValue();
        }
        return defaultValue;
    }
    
    public boolean getBoolean(String key, boolean defaultValue) {
        Object val = variables.get(key);
        if (val instanceof Boolean bool) {
            return bool;
        }
        return defaultValue;
    }
    
    public boolean hasVariable(String key) {
        return variables.containsKey(key);
    }
    
    public void modifyDamage(float multiplier, float addition) {
        if (event instanceof LivingHurtEvent hurtEvent) {
            float newDamage = (hurtEvent.getAmount() * multiplier) + addition;
            hurtEvent.setAmount(newDamage);
            setVariable("damage", newDamage);
        }
    }
}

================================================================================
FILE: enchantment\behavior\RuneAction.java
================================================================================

package net.stones.enchantment.behavior;

import com.google.gson.JsonObject;

/**
 * Interface für alle Rune Actions
 * 
 * Actions haben Zugriff auf:
 * - ActionContext (Player, Event, Variables)
 * - JsonObject params (Konfiguration aus JSON)
 */
public interface RuneAction {
    
    /**
     * Die ID der Aktion (z.B. "stones:explode", "yourmod:custom_effect")
     * Muss einzigartig sein!
     */
    String getId();

    /**
     * Führt die Aktion aus
     * 
     * @param ctx Der ActionContext mit allen relevanten Daten
     * @param params Die JSON-Parameter aus der Enchantment-Definition
     */
    void execute(ActionContext ctx, JsonObject params);
    
    /**
     * Optional: Validierung der Parameter beim Laden
     * Wird beim Registrieren des Enchantments aufgerufen
     * 
     * @param params Die JSON-Parameter
     * @return true wenn Parameter valide sind
     */
    default boolean validateParams(JsonObject params) {
        return true;
    }
    
    /**
     * Optional: Beschreibung der Aktion für Debugging/Logs
     */
    default String getDescription() {
        return "No description provided";
    }
}

================================================================================
FILE: enchantment\behavior\RuneBehavior.java
================================================================================

package net.stones.enchantment.behavior;

import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;

public class RuneBehavior {
    
    public enum TriggerType {
        ON_ATTACK,  // Feuert wenn Schaden verursacht wird (LivingHurtEvent)
        ON_SWING,   // Feuert beim Versuch zu schlagen (AttackEntityEvent)
        ON_HURT, 
        ON_KILL, 
        ON_TICK, 
        ON_JUMP, 
        ON_BLOCK_BREAK
    }

    public final TriggerType trigger;
    public final List<RuneCondition> conditions;
    public final List<ConfiguredRuneAction> actions;

    public RuneBehavior(TriggerType trigger, List<RuneCondition> conditions, List<ConfiguredRuneAction> actions) {
        this.trigger = trigger;
        this.conditions = conditions != null ? conditions : new ArrayList<>();
        this.actions = actions;
    }
    
    public boolean testConditions(ActionContext context) {
        for (RuneCondition condition : conditions) {
            if (!condition.test(context)) {
                return false;
            }
        }
        return true;
    }
    
    public void execute(ActionContext context) {
        if (!testConditions(context)) return;
        
        for (ConfiguredRuneAction action : actions) {
            action.execute(context);
        }
    }
    
    public static class ConfiguredRuneAction {
        public final RuneAction action;
        public final JsonObject params;
        
        public ConfiguredRuneAction(RuneAction action, JsonObject params) {
            this.action = action;
            this.params = params;
        }
        
        public void execute(ActionContext context) {
            if (action != null) {
                action.execute(context, this.params);
            }
        }
    }
}

================================================================================
FILE: enchantment\behavior\RuneCondition.java
================================================================================

package net.stones.enchantment.behavior;

import com.google.gson.JsonObject;

/**
 * Interface für wiederverwendbare Conditions
 * Erlaubt modulare Bedingungen die in JSON referenziert werden können
 */
public interface RuneCondition {
    
    /**
     * Die ID der Condition (z.B. "stones:health_below", "yourmod:in_nether")
     */
    String getId();
    
    /**
     * Prüft ob die Bedingung erfüllt ist
     * @param context Der ActionContext mit allen relevanten Daten
     * @return true wenn Bedingung erfüllt ist
     */
    boolean test(ActionContext context);
    
    /**
     * Optional: Validierung der Parameter beim Laden
     */
    default boolean validateParams(JsonObject params) {
        return true;
    }
    
    /**
     * Optional: Beschreibung der Condition
     */
    default String getDescription() {
        return "No description provided";
    }
}

================================================================================
FILE: enchantment\behavior\reflection\ReflectionCallParser.java
================================================================================

package net.stones.enchantment.behavior.reflection;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parst Call-Syntax wie:
 * - player.capability.irons_spellbooks:mana.setMana(666)
 * - player.getFoodData().setFoodLevel(20)
 * - player.capability.apotheosis:affixes.clear()
 */
public class ReflectionCallParser {
    
    // Pattern für Method Calls mit Argumenten
    private static final Pattern METHOD_PATTERN = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*)(\\((.*)\\))?");
    
    public static class ParsedCall {
        public final String root;           // z.B. "player", "level"
        public final List<String> path;     // z.B. ["capability", "irons_spellbooks:mana"]
        public final String method;         // z.B. "setMana"
        public final List<Object> args;     // z.B. [666]
        
        public ParsedCall(String root, List<String> path, String method, List<Object> args) {
            this.root = root;
            this.path = path;
            this.method = method;
            this.args = args;
        }
        
        public boolean isCapabilityCall() {
            return path.size() >= 1 && path.get(0).equals("capability");
        }
        
        @Nullable
        public String getCapabilityId() {
            if (isCapabilityCall() && path.size() >= 2) {
                return path.get(1); // z.B. "irons_spellbooks:mana"
            }
            return null;
        }
    }
    
    /**
     * Parst einen Call-String
     * 
     * Beispiele:
     * - "player.capability.irons_spellbooks:mana.setMana(666)"
     * - "player.getFoodData().setFoodLevel(20)"
     * - "player.setHealth(20.0)"
     */
    public static ParsedCall parse(String call) throws IllegalArgumentException {
        if (call == null || call.isEmpty()) {
            throw new IllegalArgumentException("Call string is empty");
        }
        
        String[] parts = call.split("\\.");
        if (parts.length < 2) {
            throw new IllegalArgumentException("Call must have at least root.method: " + call);
        }
        
        String root = parts[0];
        
        // Letzter Teil ist die Method mit optionalen Args
        String lastPart = parts[parts.length - 1];
        Matcher methodMatcher = METHOD_PATTERN.matcher(lastPart);
        
        if (!methodMatcher.matches()) {
            throw new IllegalArgumentException("Invalid method syntax: " + lastPart);
        }
        
        String method = methodMatcher.group(1);
        String argsString = methodMatcher.group(3); // Kann null sein
        
        // Args parsen
        List<Object> args = new ArrayList<>();
        if (argsString != null && !argsString.trim().isEmpty()) {
            args = parseArguments(argsString);
        }
        
        // Path ist alles zwischen root und method
        List<String> path = new ArrayList<>();
        for (int i = 1; i < parts.length - 1; i++) {
            path.add(parts[i]);
        }
        
        return new ParsedCall(root, path, method, args);
    }
    
    /**
     * Parst Argumente aus String: "666, true, \"hello\""
     */
    private static List<Object> parseArguments(String argsString) {
        List<Object> result = new ArrayList<>();
        
        // Einfaches Split (funktioniert für primitive Types)
        String[] parts = argsString.split(",");
        
        for (String part : parts) {
            part = part.trim();
            
            if (part.isEmpty()) continue;
            
            // String (mit Quotes)
            if (part.startsWith("\"") && part.endsWith("\"")) {
                result.add(part.substring(1, part.length() - 1));
                continue;
            }
            
            // Boolean
            if (part.equalsIgnoreCase("true")) {
                result.add(true);
                continue;
            }
            if (part.equalsIgnoreCase("false")) {
                result.add(false);
                continue;
            }
            
            // Number (float wenn . enthalten, sonst int)
            try {
                if (part.contains(".")) {
                    result.add(Double.parseDouble(part));
                } else {
                    result.add(Integer.parseInt(part));
                }
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Cannot parse argument: " + part);
            }
        }
        
        return result;
    }
    
    /**
     * Alternative: Parse aus JsonArray
     */
    public static List<Object> parseArgumentsFromJson(JsonArray jsonArgs) {
        List<Object> result = new ArrayList<>();
        
        for (JsonElement el : jsonArgs) {
            if (!el.isJsonPrimitive()) continue;
            
            JsonPrimitive prim = el.getAsJsonPrimitive();
            
            if (prim.isNumber()) {
                // Versuche int zuerst, dann double
                try {
                    result.add(prim.getAsInt());
                } catch (NumberFormatException e) {
                    result.add(prim.getAsDouble());
                }
            } else if (prim.isBoolean()) {
                result.add(prim.getAsBoolean());
            } else if (prim.isString()) {
                result.add(prim.getAsString());
            }
        }
        
        return result;
    }
}

================================================================================
FILE: enchantment\behavior\reflection\ReflectionInvoker.java
================================================================================

package net.stones.enchantment.behavior.reflection;

import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.util.LazyOptional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;

/**
 * Reflection Invoker mit klaren Sicherheitsgrenzen
 * 
 * ERLAUBT:
 * - Calls auf Player-Objekt
 * - Calls auf Player Capabilities
 * - Calls auf Player NBT/EntityData
 * 
 * VERBOTEN:
 * - Class.forName()
 * - Statische Methods
 * - Zugriff außerhalb Player-Graph
 */
public class ReflectionInvoker {
    
    private static final Logger LOGGER = LogManager.getLogger();
    
    // Feature Flag - kann deaktiviert werden für Server
    private static boolean ALLOW_REFLECTION = true;
    
    public static void setReflectionEnabled(boolean enabled) {
        ALLOW_REFLECTION = enabled;
        LOGGER.info("Reflection calls " + (enabled ? "ENABLED" : "DISABLED"));
    }
    
    /**
     * Haupteinstiegspunkt für alle Reflection Calls
     */
    public static Object execute(ServerPlayer player, ReflectionCallParser.ParsedCall call) {
        if (!ALLOW_REFLECTION) {
            LOGGER.warn("Reflection disabled, ignoring call: " + call.method);
            return null;
        }
        
        // Root-Objekt bestimmen
        Object rootObject = resolveRoot(player, call.root);
        if (rootObject == null) {
            LOGGER.warn("Unknown root: " + call.root);
            return null;
        }
        
        // Path traversieren
        Object currentObject = rootObject;
        
        // Special Case: Capability
        if (call.isCapabilityCall()) {
            String capId = call.getCapabilityId();
            if (capId == null) {
                LOGGER.warn("Capability call without ID");
                return null;
            }
            currentObject = resolveCapability(player, capId);
            if (currentObject == null) {
                LOGGER.warn("Capability not found: " + capId);
                return null;
            }
        } else {
            // Normaler Path (z.B. player.getFoodData().method())
            for (int i = 0; i < call.path.size(); i++) {
                String pathPart = call.path.get(i);
                currentObject = invokeGetter(currentObject, pathPart);
                if (currentObject == null) {
                    LOGGER.warn("Path traversal failed at: " + pathPart);
                    return null;
                }
            }
        }
        
        // Final Method Call
        return invokeMethod(currentObject, call.method, call.args);
    }
    
    /**
     * Resolve Root Object
     */
    @Nullable
    private static Object resolveRoot(ServerPlayer player, String root) {
        return switch (root.toLowerCase()) {
            case "player" -> player;
            case "level" -> player.level();
            case "fooddata" -> player.getFoodData();
            case "abilities" -> player.getAbilities();
            case "nbt", "persistentdata" -> player.getPersistentData();
            case "entitydata" -> player.getEntityData();
            default -> null;
        };
    }
    
    /**
     * Capability Resolver (generisch, kein Mod-Wissen!)
     */
    @Nullable
    private static Object resolveCapability(ServerPlayer player, String capabilityId) {
        try {
            // Versuche alle registrierten Capabilities
            ResourceLocation capKey = new ResourceLocation(capabilityId);
            
            // Forge's Capability System nutzen
            // Wir müssen hier über alle bekannten Capabilities iterieren
            // WICHTIG: Das ist der magische Teil - wir kennen die Capability nicht!
            
            // Alternative: Über Player's Capability Provider
            // Die meisten Mods registrieren ihre Caps als Capability<?>
            
            // Quick & Dirty: Reflection auf alle Fields der Capability-Klassen
            for (var field : player.getClass().getFields()) {
                if (field.getType() == LazyOptional.class) {
                    try {
                        LazyOptional<?> opt = (LazyOptional<?>) field.get(player);
                        Object cap = opt.orElse(null);
                        if (cap != null) {
                            // Prüfe ob das die richtige Cap ist
                            // (Heuristik: Classname enthält Namespace)
                            String className = cap.getClass().getName().toLowerCase();
                            if (className.contains(capKey.getNamespace())) {
                                return cap;
                            }
                        }
                    } catch (Exception ignored) {}
                }
            }
            
            // Fallback: NBT-basierte Pseudo-Capability
            CompoundTag capNBT = player.getPersistentData().getCompound(capabilityId);
            if (!capNBT.isEmpty()) {
                return new NBTCapabilityWrapper(capNBT);
            }
            
        } catch (Exception e) {
            LOGGER.error("Failed to resolve capability: " + capabilityId, e);
        }
        
        return null;
    }
    
    /**
     * Invoke Getter (für Path Traversal)
     */
    @Nullable
    private static Object invokeGetter(Object target, String getterName) {
        if (target == null) return null;
        
        try {
            // Versuche verschiedene Getter-Patterns
            String[] possibleNames = {
                getterName,
                "get" + capitalize(getterName),
                "is" + capitalize(getterName)
            };
            
            for (String name : possibleNames) {
                for (Method method : target.getClass().getMethods()) {
                    if (method.getName().equals(name) && method.getParameterCount() == 0) {
                        return method.invoke(target);
                    }
                }
            }
            
        } catch (Exception e) {
            LOGGER.error("Failed to invoke getter: " + getterName, e);
        }
        
        return null;
    }
    
    /**
     * Invoke Method mit Argumenten (Der eigentliche Call)
     */
    @Nullable
    private static Object invokeMethod(Object target, String methodName, List<Object> args) {
        if (target == null) return null;
        
        try {
            // Suche passende Method
            for (Method method : target.getClass().getMethods()) {
                if (!method.getName().equals(methodName)) continue;
                if (method.getParameterCount() != args.size()) continue;
                
                // Versuche Type-Casting
                Object[] castedArgs = castArguments(args, method.getParameterTypes());
                if (castedArgs == null) continue;
                
                // Execute!
                Object result = method.invoke(target, castedArgs);
                LOGGER.debug("Successfully invoked: {}.{}({})", 
                    target.getClass().getSimpleName(), methodName, args);
                return result;
            }
            
            LOGGER.warn("No matching method found: {}.{}({})", 
                target.getClass().getSimpleName(), methodName, args);
            
        } catch (Exception e) {
            LOGGER.error("Failed to invoke method: " + methodName, e);
        }
        
        return null;
    }
    
    /**
     * Cast Arguments zu richtigen Types
     */
    @Nullable
    private static Object[] castArguments(List<Object> args, Class<?>[] paramTypes) {
        if (args.size() != paramTypes.length) return null;
        
        Object[] result = new Object[args.size()];
        
        for (int i = 0; i < args.size(); i++) {
            Object arg = args.get(i);
            Class<?> targetType = paramTypes[i];
            
            result[i] = castSingleArgument(arg, targetType);
            if (result[i] == null && arg != null) {
                return null; // Cast failed
            }
        }
        
        return result;
    }
    
    /**
     * Cast einzelnes Argument
     */
    @Nullable
    private static Object castSingleArgument(Object arg, Class<?> targetType) {
        if (arg == null) return null;
        
        // Direkt kompatibel?
        if (targetType.isInstance(arg)) {
            return arg;
        }
        
        // Number Conversions
        if (arg instanceof Number num) {
            if (targetType == int.class || targetType == Integer.class) {
                return num.intValue();
            }
            if (targetType == float.class || targetType == Float.class) {
                return num.floatValue();
            }
            if (targetType == double.class || targetType == Double.class) {
                return num.doubleValue();
            }
            if (targetType == long.class || targetType == Long.class) {
                return num.longValue();
            }
            if (targetType == short.class || targetType == Short.class) {
                return num.shortValue();
            }
            if (targetType == byte.class || targetType == Byte.class) {
                return num.byteValue();
            }
        }
        
        // Boolean
        if (arg instanceof Boolean bool) {
            if (targetType == boolean.class || targetType == Boolean.class) {
                return bool;
            }
        }
        
        // String
        if (arg instanceof String str) {
            if (targetType == String.class) {
                return str;
            }
            // String -> ResourceLocation
            if (targetType == ResourceLocation.class) {
                return new ResourceLocation(str);
            }
        }
        
        return null;
    }
    
    private static String capitalize(String str) {
        if (str == null || str.isEmpty()) return str;
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }
    
    /**
     * Wrapper für NBT-basierte "Pseudo-Capabilities"
     * Falls ein Mod keine echte Capability hat
     */
    private static class NBTCapabilityWrapper {
        private final CompoundTag nbt;
        
        public NBTCapabilityWrapper(CompoundTag nbt) {
            this.nbt = nbt;
        }
        
        public int getInt(String key) {
            return nbt.getInt(key);
        }
        
        public void setInt(String key, int value) {
            nbt.putInt(key, value);
        }
        
        public float getFloat(String key) {
            return nbt.getFloat(key);
        }
        
        public void setFloat(String key, float value) {
            nbt.putFloat(key, value);
        }
        
        // ... weitere Getter/Setter nach Bedarf
    }
}

================================================================================
FILE: event\MilestoneEventHandler.java
================================================================================

package net.stones.event;

import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.LogicalSide;
import net.minecraftforge.fml.common.Mod;
import net.stones.StonesMod;
import net.stones.cap.PlayerShrineCapProvider;
import net.stones.data.ShrineInstance;
import net.stones.data.ShrineSavedData;
import net.stones.enchantment.RuneEnchantment;
import net.stones.enchantment.behavior.ActionContext;
import net.stones.enchantment.behavior.RuneBehavior;
import net.stones.item.StoneItem;

import java.util.Map;
import java.util.UUID;

@Mod.EventBusSubscriber(modid = StonesMod.MODID)
public class MilestoneEventHandler {

    // 1. ON_ATTACK / ON_HURT: Feuert beim tatsächlichen Schaden (Treffer)
    @SubscribeEvent
    public static void onLivingHurt(LivingHurtEvent event) {
        if (event.getEntity() instanceof ServerPlayer victim) {
            executeMilestones(victim, RuneBehavior.TriggerType.ON_HURT, event);
        }
        if (event.getSource().getEntity() instanceof ServerPlayer attacker) {
            executeMilestones(attacker, RuneBehavior.TriggerType.ON_ATTACK, event);
        }
    }

    // 2. ON_SWING: Feuert IMMER bei der Animation (Luft, Block, Entity)
    // Ersetzt AttackEntityEvent und LeftClickBlock, um Doppelungen zu vermeiden.
    @SubscribeEvent
    public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
        if (event.side == LogicalSide.SERVER && event.phase == TickEvent.Phase.END) {
            ServerPlayer player = (ServerPlayer) event.player;
            executeMilestones(player, RuneBehavior.TriggerType.ON_TICK, event);
            // Prüfen ob die Animation in diesem Tick gestartet hat (swingTime == 1)
            // UND ob es die Haupthand ist (wir wollen keine Offhand-Aktionen bestrafen)
            if (player.swinging && player.swingTime == 1 && player.swingingArm == InteractionHand.MAIN_HAND) {
                executeMilestones(player, RuneBehavior.TriggerType.ON_SWING, event);
            }
        }
    }

    private static void executeMilestones(ServerPlayer player, RuneBehavior.TriggerType trigger, Event event) {
        player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
            UUID shrineId = cap.getLinkedShrine();
            if (shrineId == null) return;
            ShrineInstance shrine = ShrineSavedData.get(player.serverLevel()).getShrine(shrineId);
            if (shrine == null) return;

            int playerLevel = player.experienceLevel;
            for (int i = 0; i < shrine.getInventory().getSlots(); i++) {
                ItemStack stack = shrine.getInventory().getStackInSlot(i);
                
                if (!isValidRune(stack, shrine, i, playerLevel)) continue;

                Map<Enchantment, Integer> enchants = EnchantmentHelper.getEnchantments(stack);
                for (Enchantment ench : enchants.keySet()) {
                    if (ench instanceof RuneEnchantment runeEnch) {
                        for (RuneBehavior behavior : runeEnch.getBehaviors()) {
                            if (behavior.trigger == trigger) {
                                ActionContext context = new ActionContext(player, event, new com.google.gson.JsonObject());
                                behavior.execute(context);
                            }
                        }
                    }
                }
            }
        });
    }

    private static boolean isValidRune(ItemStack stack, ShrineInstance shrine, int index, int playerLevel) {
        if (stack.isEmpty() || !(stack.getItem() instanceof StoneItem)) return false;
        for (ShrineInstance.SlotConfig cfg : shrine.getLayout()) {
            if (cfg.inventoryIndex == index) {
                return playerLevel >= cfg.requiredLevel;
            }
        }
        return false;
    }
}

================================================================================
FILE: event\StonesAttributeEvents.java
================================================================================

package net.stones.event;

import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerXpEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.stones.StonesMod;
import net.stones.logic.RuneCalculator;

import java.util.List;

@Mod.EventBusSubscriber(modid = StonesMod.MODID)
public class StonesAttributeEvents {

    // 1. Level Up/Down: Verzögerte Berechnung!
    @SubscribeEvent
    public static void onXpChange(PlayerXpEvent.LevelChange event) {
        if (event.getEntity() instanceof ServerPlayer player) {
            // FIX: Wir warten 1 Tick, damit player.experienceLevel sicher aktualisiert ist.
            // Sonst rechnet der Calculator mit dem alten Level!
            StonesMod.queueServerWork(1, () -> {
                // Berechnen & Liste holen
                List<Component> bonuses = RuneCalculator.updatePlayer(player);
                
                if (bonuses != null && !bonuses.isEmpty()) {
                    player.sendSystemMessage(Component.literal("§d§l[Schrein Resonanz] §r§7Level " + player.experienceLevel));
                    for (Component line : bonuses) {
                        player.sendSystemMessage(line);
                    }
                }
            });
        }
    }

    // 2. Login
    @SubscribeEvent
    public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
        if (event.getEntity() instanceof ServerPlayer player) {
            // Auch hier sicherheitshalber 1 Tick warten, um Race Conditions beim Laden zu vermeiden
            StonesMod.queueServerWork(1, () -> RuneCalculator.updatePlayer(player));
        }
    }

    // 3. Respawn
    @SubscribeEvent
    public static void onPlayerRespawn(PlayerEvent.PlayerRespawnEvent event) {
        if (event.getEntity() instanceof ServerPlayer player) {
            StonesMod.queueServerWork(1, () -> RuneCalculator.updatePlayer(player));
        }
    }
    
    // 4. Dimension
    @SubscribeEvent
    public static void onDimensionChange(PlayerEvent.PlayerChangedDimensionEvent event) {
        if (event.getEntity() instanceof ServerPlayer player) {
            StonesMod.queueServerWork(1, () -> RuneCalculator.updatePlayer(player));
        }
    }
}

================================================================================
FILE: event\StonesCapabilityEvents.java
================================================================================

package net.stones.event;

import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.network.PacketDistributor;
import net.stones.StonesMod;
import net.stones.cap.IPlayerShrineLink;
import net.stones.cap.PlayerShrineCapProvider;
import net.stones.data.ShrineInstance;
import net.stones.data.ShrineSavedData;
import net.stones.network.PacketSyncPlayerShrine;

@Mod.EventBusSubscriber(modid = StonesMod.MODID)
public class StonesCapabilityEvents {

    @SubscribeEvent
    public static void onRegisterCapabilities(RegisterCapabilitiesEvent event) {
        event.register(IPlayerShrineLink.class);
    }

    @SubscribeEvent
    public static void onAttachCapabilitiesPlayer(AttachCapabilitiesEvent<Entity> event) {
        if (event.getObject() instanceof Player) {
            if (!event.getObject().getCapability(PlayerShrineCapProvider.SHRINE_LINK).isPresent()) {
                event.addCapability(new ResourceLocation(StonesMod.MODID, "shrine_link"), new PlayerShrineCapProvider());
            }
        }
    }

    // --- ROGUE-LITE TODES LOGIK ---
    
    // 1. Beim Tod: Aus dem Schrein austragen!
    @SubscribeEvent
    public static void onPlayerDeath(LivingDeathEvent event) {
        if (event.getEntity() instanceof ServerPlayer player) {
            player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
                if (cap.isLinked()) {
                    ServerLevel level = player.serverLevel();
                    ShrineInstance shrine = ShrineSavedData.get(level).getShrine(cap.getLinkedShrine());
                    
                    if (shrine != null) {
                        shrine.removeOwner(player.getUUID());
                        
                        // Visuals updaten (Kopf entfernen), falls Chunk geladen
                        if (shrine.getLocation() != null && shrine.getLocation().dimension() == level.dimension()) {
                            BlockPos pos = shrine.getLocation().pos();
                            if (level.isLoaded(pos)) {
                                level.sendBlockUpdated(pos, level.getBlockState(pos), level.getBlockState(pos), 3);
                            }
                        }
                    }
                }
            });
        }
    }

    // 2. Beim Respawn (Cloning): Capability NICHT kopieren -> Reset
    @SubscribeEvent
    public static void onPlayerCloned(PlayerEvent.Clone event) {
        if (!event.isWasDeath()) {
            // Nur bei Dimension Travel kopieren
            event.getOriginal().getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(oldStore -> {
                event.getEntity().getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(newStore -> {
                    newStore.copyFrom(oldStore);
                });
            });
        }
        // Bei Tod: Nichts tun -> Capability ist leer -> Spieler ist ungebunden.
    }

    // Sync beim Login
    @SubscribeEvent
    public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
        if (event.getEntity() instanceof ServerPlayer player) {
            player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
                StonesMod.PACKET_HANDLER.send(
                    PacketDistributor.PLAYER.with(() -> player), 
                    new PacketSyncPlayerShrine(cap.getLinkedShrine(), cap.getShrinePos())
                );
            });
        }
    }
    
    // Sync beim Dimensionswechsel
    @SubscribeEvent
    public static void onPlayerDimChange(PlayerEvent.PlayerChangedDimensionEvent event) {
         if (event.getEntity() instanceof ServerPlayer player) {
            player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
                StonesMod.PACKET_HANDLER.send(
                    PacketDistributor.PLAYER.with(() -> player), 
                    new PacketSyncPlayerShrine(cap.getLinkedShrine(), cap.getShrinePos())
                );
            });
         }
    }
}

================================================================================
FILE: gui\RunestoneMenu.java
================================================================================

package net.stones.gui;

import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec2;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.ItemStackHandler;
import net.minecraftforge.items.SlotItemHandler;
import net.stones.gui.layout.ShrineLayout;
import net.stones.data.ShrineInstance.SlotConfig;
import net.stones.data.ShrineInstance.SlotType;
import net.stones.init.StonesModMenus;
import net.stones.init.StonesModTags;
import net.stones.item.StoneItem;
import net.stones.logic.RuneCalculator;

import java.util.ArrayList;
import java.util.List;

public class RunestoneMenu extends AbstractContainerMenu {

    private final IItemHandler shrineInventory;
    private final int slotCount;
    public final List<SlotConfig> layoutData = new ArrayList<>();
    private final Player player;

    public RunestoneMenu(int containerId, Inventory playerInv, FriendlyByteBuf data) {
        this(containerId, playerInv, new ItemStackHandler(data.readInt()), false);
        int layoutSize = data.readInt();
        for (int i = 0; i < layoutSize; i++) {
            SlotType type = data.readEnum(SlotType.class);
            int lvl = data.readInt();
            int idx = data.readInt();
            layoutData.add(new SlotConfig(type, lvl, idx));
        }
        initSlots();
        addPlayerInventory(playerInv);
    }

    public RunestoneMenu(int containerId, Inventory playerInv, IItemHandler shrineInventory, List<SlotConfig> layout) {
        super(StonesModMenus.RUNESTONE_MENU.get(), containerId);
        this.shrineInventory = shrineInventory;
        this.slotCount = shrineInventory.getSlots();
        this.layoutData.addAll(layout);
        this.player = playerInv.player;
        initSlots();
        addPlayerInventory(playerInv);
    }

    private RunestoneMenu(int containerId, Inventory playerInv, IItemHandler shrineInventory, boolean dummy) {
        super(StonesModMenus.RUNESTONE_MENU.get(), containerId);
        this.shrineInventory = shrineInventory;
        this.slotCount = shrineInventory.getSlots();
        this.player = playerInv.player;
    }

    private void initSlots() {
        List<Vec2> positions = ShrineLayout.generateSpiralPositions(layoutData.size());

        for (int i = 0; i < layoutData.size(); i++) {
            SlotConfig cfg = layoutData.get(i);
            Vec2 pos = positions.get(i);
            
            this.addSlot(new SlotItemHandler(shrineInventory, cfg.inventoryIndex, (int)pos.x, (int)pos.y) {
                
				@Override
				public boolean mayPlace(ItemStack stack) {
					// 1. Tag Check bleibt (da wir hier die Gems hinzufügen werden) [cite: 1779]
					boolean isTypeAllowed = switch (cfg.type) {
						case MINOR -> stack.is(StonesModTags.RUNE_MINOR);
						case MAJOR -> stack.is(StonesModTags.RUNE_MAJOR) || stack.is(StonesModTags.RUNE_MINOR);
						case MILESTONE -> stack.is(StonesModTags.RUNE_MILESTONE);
					};
					if (!isTypeAllowed) return false;

					// 2. Dynamische Level-Abfrage statt StoneItem check [cite: 1780]
					int runeReq = RuneCalculator.getRequiredLevel(stack);
					return cfg.requiredLevel >= runeReq;
				}

                
                @Override
                public int getMaxStackSize() { return 1; }
                
                @Override
                public void onTake(Player pPlayer, ItemStack pStack) {
                    tryApplyLevelCost(pPlayer);
                    super.onTake(pPlayer, pStack);
                }

                @Override
                public void setChanged() {
                    super.setChanged();
                    if (player instanceof ServerPlayer serverPlayer) {
                        RuneCalculator.updatePlayer(serverPlayer);
                    }
                }
            });
        }
    }
    
    // --- KOSTEN LOGIK ---
    private boolean tryApplyLevelCost(Player pPlayer) {
        if (pPlayer.isCreative()) return true;
        
        if (pPlayer.experienceLevel > 16) {
            if (!pPlayer.level().isClientSide) {
                pPlayer.giveExperienceLevels(-1);
                pPlayer.level().playSound(null, pPlayer.getX(), pPlayer.getY(), pPlayer.getZ(), 
                    net.minecraft.sounds.SoundEvents.EXPERIENCE_ORB_PICKUP, net.minecraft.sounds.SoundSource.PLAYERS, 0.5f, 0.5f);
            }
            return true;
        }
        return false; 
    }

    // --- KLICK INTERCEPTION (Insert) ---
    @Override
    public void clicked(int slotId, int button, ClickType clickType, Player player) {
        if (slotId >= 0 && slotId < this.slotCount) {
            Slot slot = this.slots.get(slotId);
            ItemStack cursor = this.getCarried();
            
            // Nur wenn wir wirklich versuchen, etwas reinzulegen (Cursor voll)
            if (!cursor.isEmpty() && (clickType == ClickType.PICKUP || clickType == ClickType.QUICK_MOVE)) {
                // FIX: Zuerst prüfen, ob der Slot das Item überhaupt erlaubt!
                if (slot.mayPlace(cursor)) {
                    // Wenn ja, ziehen wir Kosten ab (wir gehen davon aus, dass der Rest klappt)
                    tryApplyLevelCost(player);
                }
            }
        }
        super.clicked(slotId, button, clickType, player);
    }

    // --- SHIFT-KLICK LOGIK (Quick Move) ---
    @Override
    public ItemStack quickMoveStack(Player playerIn, int index) {
        ItemStack itemstack = ItemStack.EMPTY;
        Slot slot = this.slots.get(index);
        
        if (slot != null && slot.hasItem()) {
            ItemStack itemstack1 = slot.getItem();
            itemstack = itemstack1.copy();
            
            // Schrein -> Inventar (Entnahme)
            if (index < this.slotCount) {
                // Erst versuchen zu verschieben...
                if (!this.moveItemStackTo(itemstack1, this.slotCount, this.slots.size(), true)) {
                    return ItemStack.EMPTY;
                }
                // ...wenn erfolgreich verschoben (wir sind noch hier), dann Kosten abziehen
                tryApplyLevelCost(playerIn);
            } 
            // Inventar -> Schrein (Einfügen)
            else {
                // Erst versuchen zu verschieben...
                if (!this.moveItemStackTo(itemstack1, 0, this.slotCount, false)) {
                    return ItemStack.EMPTY;
                }
                // ...wenn erfolgreich (Item ist im Slot gelandet), dann Kosten abziehen
                tryApplyLevelCost(playerIn);
            }
            
            if (itemstack1.isEmpty()) slot.set(ItemStack.EMPTY);
            else slot.setChanged();
        }
        return itemstack;
    }

    @Override
    public boolean stillValid(Player player) { return true; }

    private void addPlayerInventory(Inventory playerInv) {
        int xOffset = 48;
        int yStartMain = 140; 
        int yStartHotbar = 198;

        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 9; ++j) {
                this.addSlot(new Slot(playerInv, j + i * 9 + 9, xOffset + j * 18, yStartMain + i * 18));
            }
        }
        for (int k = 0; k < 9; ++k) {
            this.addSlot(new Slot(playerInv, k, xOffset + k * 18, yStartHotbar));
        }
    }

}

================================================================================
FILE: gui\layout\ShrineLayout.java
================================================================================

package net.stones.gui.layout;

import net.minecraft.world.phys.Vec2;
import java.util.ArrayList;
import java.util.List;

/**
 * Kern-Mathematik für das thaumaturgische Interface.
 * Implementiert die Vogel-Spirale (Phyllotaxis) für organische Verteilung.
 */
public class ShrineLayout {

    // Der Goldene Winkel: 137.508 Grad in Radiant
    // Dies ist der Schlüssel, um Moiré-Muster zu vermeiden und "natürliche" Unordnung zu schaffen.
    private static final double GOLDEN_ANGLE = Math.PI * (3.0 - Math.sqrt(5.0));

    // Skalierungsfaktor: Wie weit sind die Punkte auseinander?
    // 24.0 ist empirisch ermittelt für 18px Slots + 6px "Luft".
    private static final double SPREAD_CONSTANT = 24.0;

    /**
     * Berechnet die Positionen für alle Slots basierend auf der Spirale.
     * 0,0 ist hierbei der mathematische Ursprung (Zentrum).
     */
    public static List<Vec2> generateSpiralPositions(int slotCount) {
        List<Vec2> positions = new ArrayList<>(slotCount);

        for (int i = 0; i < slotCount; i++) {
            // Formel: r = c * sqrt(n), theta = n * 137.5 deg
            double theta = i * GOLDEN_ANGLE;
            double radius = SPREAD_CONSTANT * Math.sqrt(i);

            // Polar zu Kartesisch
            float x = (float) (radius * Math.cos(theta));
            float y = (float) (radius * Math.sin(theta));

            positions.add(new Vec2(x, y));
        }
        return positions;
    }

    /**
     * Einfacher Algorithmus zur Generierung von Ley-Linien (Verbindungen).
     * Verbindet Punkte, die nah beieinander liegen, um ein Netz zu bilden.
     */
    public static List<int[]> generateConnections(List<Vec2> positions) {
        List<int[]> connections = new ArrayList<>();
        double maxDistSq = 45.0 * 45.0; // Maximale Verbindungslänge (quadriert für Performance)

        for (int i = 0; i < positions.size(); i++) {
            Vec2 p1 = positions.get(i);
            
            // Wir schauen nur auf vorherige Punkte, um Dopplungen zu vermeiden
            // und die Spirale "nach innen" zu verbinden.
            for (int j = Math.max(0, i - 8); j < i; j++) {
                Vec2 p2 = positions.get(j);
                
                double dx = p1.x - p2.x;
                double dy = p1.y - p2.y;
                double distSq = dx*dx + dy*dy;

                if (distSq < maxDistSq) {
                    connections.add(new int[]{i, j});
                }
            }
        }
        return connections;
    }
}

================================================================================
FILE: init\ConditionRegistry.java
================================================================================

package net.stones.init;

import com.google.gson.JsonObject;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.stones.enchantment.behavior.ActionContext;
import net.stones.enchantment.behavior.RuneCondition;

import java.util.HashMap;
import java.util.Map;

public class ConditionRegistry {
    
    private static final Map<String, ConditionFactory> CONDITIONS = new HashMap<>();
    
    @FunctionalInterface
    public interface ConditionFactory {
        RuneCondition create(JsonObject params);
    }
    
    public static void register(String id, ConditionFactory factory) {
        CONDITIONS.put(id, factory);
    }
    
    public static RuneCondition create(String id, JsonObject params) {
        ConditionFactory factory = CONDITIONS.get(id);
        if (factory == null) return null;
        return factory.create(params);
    }
    
    public static void init() {
        
        // Existierende Conditions...
        
        register("stones:health_below", params -> {
            final float threshold = params.has("percent") ? params.get("percent").getAsFloat() : 0.5f;
            return new RuneCondition() {
                @Override public String getId() { return "stones:health_below"; }
                @Override public boolean test(ActionContext ctx) {
                    return (ctx.getPlayer().getHealth() / ctx.getPlayer().getMaxHealth()) <= threshold;
                }
            };
        });

        register("stones:chance", params -> {
            final float chance = params.has("value") ? params.get("value").getAsFloat() : 0.5f;
            return new RuneCondition() {
                @Override public String getId() { return "stones:chance"; }
                @Override public boolean test(ActionContext ctx) {
                    return ctx.getPlayer().getRandom().nextFloat() < chance;
                }
            };
        });
        
        register("stones:is_day", params -> new RuneCondition() {
            @Override public String getId() { return "stones:is_day"; }
            @Override public boolean test(ActionContext ctx) { return ctx.getPlayer().level().isDay(); }
        });

        register("stones:variable_compare", params -> {
            final String varName = params.get("variable").getAsString();
            final String operator = params.has("operator") ? params.get("operator").getAsString() : ">";
            final float value = params.get("value").getAsFloat();
            return new RuneCondition() {
                @Override public String getId() { return "stones:variable_compare"; }
                @Override public boolean test(ActionContext ctx) {
                    float varValue = ctx.getFloat(varName, 0);
                    return switch (operator) {
                        case ">" -> varValue > value;
                        case "<" -> varValue < value;
                        case ">=" -> varValue >= value;
                        case "<=" -> varValue <= value;
                        case "==" -> Math.abs(varValue - value) < 0.001f;
                        default -> false;
                    };
                }
            };
        });

        register("stones:has_air", params -> {
            final int minAir = params.has("min") ? params.get("min").getAsInt() : 1;
            return new RuneCondition() {
                @Override public String getId() { return "stones:has_air"; }
                @Override public boolean test(ActionContext ctx) { return ctx.getPlayer().getAirSupply() >= minAir; }
            };
        });

		register("stones:persistent_var_compare", p -> {
			String name = p.get("name").getAsString();
			String op = p.get("operator").getAsString();
			float val = p.get("value").getAsFloat();

			return new RuneCondition() {
				@Override
				public String getId() {
					return "stones:persistent_var_compare";
				}

				@Override
				public boolean test(ActionContext ctx) {
					float cur = ctx.getPlayer()
								   .getPersistentData()
								   .getFloat("stones_" + name);

					return switch (op) {
						case ">" -> cur > val;
						case "<" -> cur < val;
						case "<=" -> cur <= val;
						default -> cur >= val;
					};
				}
			};
		});
		
        // --- NEU: Prüfen ob Waffe verzaubert ist ---
        register("stones:has_enchantment", params -> {
            final String targetName = params.has("target") ? params.get("target").getAsString() : "attacker";
            
            return new RuneCondition() {
                @Override
                public String getId() { return "stones:has_enchantment"; }
                
                @Override
                public boolean test(ActionContext ctx) {
                    LivingEntity target = ctx.getVariable(targetName, LivingEntity.class);
                    if (target == null && targetName.equals("player")) target = ctx.getPlayer();
                    
                    if (target != null) {
                        ItemStack mainHand = target.getMainHandItem();
                        return !mainHand.isEmpty() && mainHand.isEnchanted();
                    }
                    return false;
                }
                
                @Override
                public String getDescription() { return "Checks if target's weapon has enchantments"; }
            };
        });
    }
}

================================================================================
FILE: init\MilestoneActionRegistry.java
================================================================================

package net.stones.init;

import com.google.gson.JsonObject;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.level.Level;
import net.minecraftforge.registries.ForgeRegistries;
import net.stones.enchantment.behavior.ActionContext;
import net.stones.enchantment.behavior.RuneAction;
import net.stones.enchantment.behavior.reflection.ReflectionCallParser;
import net.stones.enchantment.behavior.reflection.ReflectionInvoker;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Aufgeräumte Action Registry mit klarer Struktur:
 * 
 * 1. REFLECTION SYSTEM (Das "Power User Feature")
 * 2. RESOURCE MANAGEMENT (Health, Air, Food, XP)
 * 3. COMBAT ACTIONS (Damage, Effects, Explosions)
 * 4. UTILITY ACTIONS (Particles, Variables, Math)
 */
public class MilestoneActionRegistry {

    private static final Logger LOGGER = LogManager.getLogger();
    private static final Map<String, RuneAction> ACTIONS = new HashMap<>();

    public static void register(RuneAction action) {
        ACTIONS.put(action.getId(), action);
        LOGGER.debug("Registered action: {}", action.getId());
    }

    public static RuneAction get(String id) {
        return ACTIONS.get(id);
    }

    /**
     * Helper: Liest Wert aus JSON mit Variable-Support
     * Unterstützt: "key": value ODER "key_var": "variableName"
     */
    private static float getVal(JsonObject params, String key, ActionContext ctx, float defaultValue) {
        // Variable Reference
        if (params.has(key + "_var")) {
            return ctx.getFloat(params.get(key + "_var").getAsString(), defaultValue);
        }
        // Direct Value
        if (params.has(key)) {
            return params.get(key).getAsFloat();
        }
        // Aliase (für Backward-Compatibility)
        if (key.equals("amount")) {
            if (params.has("add")) return params.get("add").getAsFloat();
            if (params.has("value")) return params.get("value").getAsFloat();
        }
        return defaultValue;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 1. REFLECTION SYSTEM - "Die Büchse der Pandora"
    // ═══════════════════════════════════════════════════════════════════════════

    public static void init() {
        
        // === REFLECTION INVOKE ===
        // Syntax: "player.capability.modid:capname.method(args)"
        // Beispiel: "player.capability.irons_spellbooks:mana.setMana(666)"
        register(new RuneAction() {
            @Override 
            public String getId() { 
                return "stones:invoke"; 
            }

            @Override 
            public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("call")) {
                    LOGGER.warn("invoke action without 'call' parameter");
                    return;
                }
                
                try {
                    String callString = params.get("call").getAsString();
                    
                    // Parse den Call
                    ReflectionCallParser.ParsedCall call = ReflectionCallParser.parse(callString);
                    
                    // Optional: Args aus JSON Array überschreiben
                    if (params.has("args")) {
                        call.args.clear();
                        call.args.addAll(ReflectionCallParser.parseArgumentsFromJson(
                            params.getAsJsonArray("args")
                        ));
                    }
                    
                    // Optional: Variable als Argument hinzufügen
                    if (params.has("arg_var")) {
                        String varName = params.get("arg_var").getAsString();
                        call.args.add((double) ctx.getFloat(varName, 0));
                    }
                    
                    // Execute!
                    Object result = ReflectionInvoker.execute(ctx.getPlayer(), call);
                    
                    // Optional: Result speichern
                    if (params.has("save_result_to") && result != null) {
                        String varName = params.get("save_result_to").getAsString();
                        if (result instanceof Number num) {
                            ctx.setVariable(varName, num.floatValue());
                        }
                    }
                    
                } catch (Exception e) {
                    LOGGER.error("Reflection invoke failed", e);
                }
            }
            
            @Override
            public String getDescription() {
                return "Power-User Reflection API - call any method on player/capabilities";
            }
        });

        // ═══════════════════════════════════════════════════════════════════════════
        // 2. RESOURCE MANAGEMENT
        // ═══════════════════════════════════════════════════════════════════════════

        // === MODIFY RESOURCE (Unified System) ===
        register(new RuneAction() {
            @Override 
            public String getId() { 
                return "stones:modify_resource"; 
            }

            @Override 
            public void execute(ActionContext ctx, JsonObject params) {
                String resource = params.has("resource") 
                    ? params.get("resource").getAsString().toLowerCase() 
                    : "health";
                    
                String operation = params.has("operation") 
                    ? params.get("operation").getAsString().toLowerCase() 
                    : "add";
                    
                float amount = getVal(params, "amount", ctx, 0.0f);
                
                // Percent of Max (z.B. 50% der max Health)
                if (params.has("percent_of_max")) {
                    float percent = params.get("percent_of_max").getAsFloat();
                    amount = switch (resource) {
                        case "health" -> ctx.getPlayer().getMaxHealth() * percent;
                        case "air" -> ctx.getPlayer().getMaxAirSupply() * percent;
                        case "food" -> 20 * percent; // Max Food = 20
                        default -> amount;
                    };
                }
                
                // Percent of Current (z.B. 20% vom aktuellen Damage)
                if (params.has("percent_of_damage")) {
                    amount = ctx.getFloat("damage", 0) * params.get("percent_of_damage").getAsFloat();
                }
                
                ServerPlayer p = ctx.getPlayer();
                
                switch (resource) {
                    case "health" -> {
                        if (operation.equals("set")) {
                            p.setHealth(amount);
                        } else {
                            if (amount > 0) p.heal(amount);
                            else p.setHealth(Math.max(0, p.getHealth() + amount));
                        }
                    }
                    case "food" -> {
                        if (operation.equals("set")) {
                            p.getFoodData().setFoodLevel((int) amount);
                        } else {
                            float saturation = params.has("saturation") 
                                ? params.get("saturation").getAsFloat() 
                                : 0.0f;
                            p.getFoodData().eat((int) amount, saturation);
                        }
                    }
                    case "air" -> {
                        int newAir = operation.equals("set") 
                            ? (int) amount 
                            : p.getAirSupply() + (int) amount;
                        p.setAirSupply(Math.max(-20, Math.min(p.getMaxAirSupply(), newAir)));
                    }
                    case "xp_levels" -> {
                        if (operation.equals("set")) {
                            p.setExperienceLevels((int) amount);
                        } else {
                            p.giveExperienceLevels((int) amount);
                        }
                    }
                    case "xp_points" -> {
                        p.giveExperiencePoints((int) amount);
                    }
                }
            }
            
            @Override
            public String getDescription() {
                return "Unified resource management: health, food, air, xp";
            }
        });
        
        // === LEGACY WRAPPERS für Backward-Compatibility ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:heal"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                params.addProperty("resource", "health");
                get("stones:modify_resource").execute(ctx, params);
            }
        });
        
        register(new RuneAction() {
            @Override public String getId() { return "stones:modify_air"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                params.addProperty("resource", "air");
                get("stones:modify_resource").execute(ctx, params);
            }
        });

        // ═══════════════════════════════════════════════════════════════════════════
        // 3. COMBAT ACTIONS
        // ═══════════════════════════════════════════════════════════════════════════

        // === MODIFY DAMAGE ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:modify_damage"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                float multiplier = getVal(params, "multiplier", ctx, 1.0f);
                float addition = getVal(params, "add", ctx, 0.0f);
                ctx.modifyDamage(multiplier, addition);
            }
        });

        // === EXPLOSION ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:explode"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                float radius = getVal(params, "radius", ctx, 2.0f);
                boolean fire = params.has("fire") && params.get("fire").getAsBoolean();
                
                ServerPlayer p = ctx.getPlayer();
                p.level().explode(p, p.getX(), p.getY(), p.getZ(), radius, fire, 
                    Level.ExplosionInteraction.NONE);
            }
        });

        // === APPLY EFFECT ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:apply_effect"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("effect")) return;
                
                MobEffect effect = ForgeRegistries.MOB_EFFECTS.getValue(
                    new ResourceLocation(params.get("effect").getAsString())
                );
                
                if (effect != null) {
                    int duration = (int) getVal(params, "duration", ctx, 100);
                    int amplifier = (int) getVal(params, "amplifier", ctx, 0);
                    ctx.getPlayer().addEffect(new MobEffectInstance(effect, duration, amplifier));
                }
            }
        });

        // === CANCEL EVENT ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:cancel"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (ctx.getEvent().isCancelable()) {
                    ctx.getEvent().setCanceled(true);
                }
                // Extra: Bei LivingHurtEvent Damage auf 0 setzen
                if (ctx.getEvent() instanceof net.minecraftforge.event.entity.living.LivingHurtEvent hurt) {
                    hurt.setAmount(0);
                }
            }
        });

        // ═══════════════════════════════════════════════════════════════════════════
        // 4. UTILITY ACTIONS
        // ═══════════════════════════════════════════════════════════════════════════

        // === SET VARIABLE ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:set_variable"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("name")) return;
                String name = params.get("name").getAsString();
                float value = getVal(params, "value", ctx, 0);
                ctx.setVariable(name, value);
            }
        });

        // === MATH OPERATION ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:math"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("variable") || !params.has("operation")) return;
                
                String varName = params.get("variable").getAsString();
                String op = params.get("operation").getAsString().toLowerCase();
                float value = getVal(params, "value", ctx, 0);
                float current = ctx.getFloat(varName, 0);
                
                float result = switch (op) {
                    case "add" -> current + value;
                    case "subtract" -> current - value;
                    case "multiply" -> current * value;
                    case "divide" -> value != 0 ? current / value : current;
                    case "set" -> value;
                    case "min" -> Math.min(current, value);
                    case "max" -> Math.max(current, value);
                    default -> current;
                };
                
                ctx.setVariable(varName, result);
            }
        });

        // === PERSISTENT VARIABLES (Überleben zwischen Sessions) ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:set_persistent"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("name")) return;
                String name = params.get("name").getAsString();
                float value = getVal(params, "value", ctx, 0);
                ctx.getPlayer().getPersistentData().putFloat("stones_" + name, value);
            }
        });
        
        register(new RuneAction() {
            @Override public String getId() { return "stones:get_persistent"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("name") || !params.has("into")) return;
                float value = ctx.getPlayer().getPersistentData()
                    .getFloat("stones_" + params.get("name").getAsString());
                ctx.setVariable(params.get("into").getAsString(), value);
            }
        });

        // === SPAWN PARTICLES ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:spawn_particles"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                String particleId = params.has("particle") 
                    ? params.get("particle").getAsString() 
                    : "minecraft:poof";
                    
                int count = (int) getVal(params, "count", ctx, 10);
                double spread = getVal(params, "spread", ctx, 0.5f);
                
                LivingEntity target = (LivingEntity) ctx.getVariable("victim");
                if (target == null) target = ctx.getPlayer();
                
                if (target.level() instanceof ServerLevel level) {
                    var particleType = ForgeRegistries.PARTICLE_TYPES.getValue(
                        new ResourceLocation(particleId)
                    );
                    
                    if (particleType instanceof net.minecraft.core.particles.ParticleOptions po) {
                        level.sendParticles(po, 
                            target.getX(), target.getY() + 1, target.getZ(),
                            count, spread, spread, spread, 0.1
                        );
                    }
                }
            }
        });

        // === REMOVE RANDOM ENCHANTMENT (Sacrifice Mechanic) ===
        register(new RuneAction() {
            @Override public String getId() { return "stones:remove_random_enchantment"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                ServerPlayer player = ctx.getPlayer();
                ItemStack item = player.getMainHandItem();
                
                if (item.isEmpty() || !item.isEnchanted()) return;
                
                var enchants = EnchantmentHelper.getEnchantments(item);
                if (enchants.isEmpty()) return;
                
                // Zufälliges Enchantment entfernen
                List<Enchantment> keys = new ArrayList<>(enchants.keySet());
                Enchantment toRemove = keys.get(player.getRandom().nextInt(keys.size()));
                int level = enchants.get(toRemove);
                
                enchants.remove(toRemove);
                EnchantmentHelper.setEnchantments(enchants, item);
                
                // Optional: Level in Variable speichern
                if (params.has("save_level_to")) {
                    ctx.setVariable(params.get("save_level_to").getAsString(), (float) level);
                }
                
                // Feedback
                player.sendSystemMessage(net.minecraft.network.chat.Component.literal(
                    "§cMagic sacrificed: " + toRemove.getFullname(level).getString()
                ));
            }
        });

        register(new RuneAction() {
            @Override public String getId() { return "stones:particle_orbit"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                String pId = params.has("particle") ? params.get("particle").getAsString() : "minecraft:crit";
                float radius = getVal(params, "radius", ctx, 1.2f);
                int count = (int) getVal(params, "count", ctx, 1);
                float height = getVal(params, "height", ctx, 1.0f);
                
                if (count <= 0) return;

                LivingEntity target = ctx.getPlayer();
                if (target.level() instanceof ServerLevel sl) {
                    var ptRaw = ForgeRegistries.PARTICLE_TYPES.getValue(new ResourceLocation(pId));
                    if (ptRaw instanceof ParticleOptions po) {
                        long time = target.level().getGameTime();
                        double anglePer = (2 * Math.PI) / Math.max(1, count);
                        for (int i = 0; i < count; i++) {
                            double angle = (time * 0.15) + (i * anglePer);
                            double x = target.getX() + Math.cos(angle) * radius;
                            double z = target.getZ() + Math.sin(angle) * radius;
                            double y = target.getY() + height + (Math.sin(time * 0.1 + i) * 0.1);
                            sl.sendParticles(po, x, y, z, 1, 0, 0, 0, 0.01);
                        }
                    }
                }
            }
        });

        // --- 3. PERSISTENT VARIABLES (Für Timer/Combo) ---
        register(new RuneAction() {
            @Override public String getId() { return "stones:set_persistent_var"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("name")) return;
                String name = params.get("name").getAsString();
                float val = getVal(params, "value", ctx, 0);
                ctx.getPlayer().getPersistentData().putFloat("stones_" + name, val);
            }
        });
        register(new RuneAction() {
            @Override public String getId() { return "stones:get_persistent_var"; }
            @Override public void execute(ActionContext ctx, JsonObject params) {
                if (!params.has("name") || !params.has("into")) return;
                float val = ctx.getPlayer().getPersistentData().getFloat("stones_" + params.get("name").getAsString());
                ctx.setVariable(params.get("into").getAsString(), val);
            }
        });        
        LOGGER.info("Initialized {} actions", ACTIONS.size());
    }
}

================================================================================
FILE: init\StonesModBlockEntities.java
================================================================================


/*
 *    MCreator note: This file will be REGENERATED on each build.
 */
package net.stones.init;

import net.stones.block.entity.RunestoneBlockEntity;
import net.stones.StonesMod;

import net.minecraftforge.registries.RegistryObject;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.DeferredRegister;

import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.Block;

public class StonesModBlockEntities {
	public static final DeferredRegister<BlockEntityType<?>> REGISTRY = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, StonesMod.MODID);
	public static final RegistryObject<BlockEntityType<?>> RUNESTONE = register("runestone", StonesModBlocks.RUNESTONE, RunestoneBlockEntity::new);

	// Start of user code block custom block entities
	// End of user code block custom block entities
	private static RegistryObject<BlockEntityType<?>> register(String registryname, RegistryObject<Block> block, BlockEntityType.BlockEntitySupplier<?> supplier) {
		return REGISTRY.register(registryname, () -> BlockEntityType.Builder.of(supplier, block.get()).build(null));
	}
}


================================================================================
FILE: init\StonesModBlocks.java
================================================================================


/*
 *    MCreator note: This file will be REGENERATED on each build.
 */
package net.stones.init;

import net.stones.block.RunestoneBlock;
import net.stones.StonesMod;

import net.minecraftforge.registries.RegistryObject;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.DeferredRegister;

import net.minecraft.world.level.block.Block;

public class StonesModBlocks {
	public static final DeferredRegister<Block> REGISTRY = DeferredRegister.create(ForgeRegistries.BLOCKS, StonesMod.MODID);
	public static final RegistryObject<Block> RUNESTONE = REGISTRY.register("runestone", () -> new RunestoneBlock());
	// Start of user code block custom blocks
	// End of user code block custom blocks
}


================================================================================
FILE: init\StonesModClient.java
================================================================================

package net.stones.init;

import net.minecraft.client.gui.screens.MenuScreens;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.stones.StonesMod;
import net.stones.block.entity.RunestoneBlockEntity;
import net.stones.client.gui.RunestoneScreen;
import net.stones.client.renderer.RunestoneRenderer;

@Mod.EventBusSubscriber(modid = StonesMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public class StonesModClient {

    @SubscribeEvent
    public static void onClientSetup(FMLClientSetupEvent event) {
        event.enqueueWork(() -> {
            MenuScreens.register(StonesModMenus.RUNESTONE_MENU.get(), RunestoneScreen::new);
        });
    }

    @SubscribeEvent
    public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
        // FIX: Wir müssen dem Compiler helfen, den Typen zuzuordnen.
        // Da unsere Registry "BlockEntityType<?>" liefert, müssen wir es casten.
        // Wir wissen sicher, dass RUNESTONE eine RunestoneBlockEntity ist.
        event.registerBlockEntityRenderer(
            (BlockEntityType<RunestoneBlockEntity>) StonesModBlockEntities.RUNESTONE.get(), 
            RunestoneRenderer::new
        );
    }
}

================================================================================
FILE: init\StonesModEnchantments.java
================================================================================

package net.stones.init;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegisterEvent;
import net.stones.StonesMod;
import net.stones.enchantment.RuneEnchantment;
import net.stones.enchantment.behavior.RuneAction;
import net.stones.enchantment.behavior.RuneBehavior;
import net.stones.enchantment.behavior.RuneCondition;
import net.stones.util.JsonExporter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

@Mod.EventBusSubscriber(modid = StonesMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class StonesModEnchantments {

    private static final Logger LOGGER = LogManager.getLogger();
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
    
    // Versionsordner NUR für die Config (Nutzer-Sicherheit)
    private static final String CONFIG_VERSION = "v1";
    
    // Sauberer Pfad ohne Version für Mod-Interne Ressourcen (Datapacks/Bridge-JAR)
    private static final String INTERNAL_DATA_PATH = "data/stones/enchantments";

    @SubscribeEvent
    public static void onRegisterEnchantments(RegisterEvent event) {
        if (event.getRegistryKey().equals(ForgeRegistries.Keys.ENCHANTMENTS)) {
            MilestoneActionRegistry.init();
            ConditionRegistry.init();
            
            // DUAL-LÖSUNG:
            // 1. Sammle alle Runen aus JAR-Ressourcen (sauberer Pfad)
            Map<String, JsonObject> discoveredRunes = scanModResources();
            
            // 2. Sammle alle Runen aus dem Config-Verzeichnis (v1 Pfad)
            // Config überschreibt JAR-Ressourcen bei Namensgleichheit
            scanConfigFolder(discoveredRunes);

            // 3. Registriere das kombinierte Ergebnis
            discoveredRunes.forEach((id, json) -> {
                registerRuneFromJson(id, json, event);
            });
        }
    }

    /**
     * Scannt alle installierten Mods nach Runen im data/stones/enchantments Ordner.
     */
    private static Map<String, JsonObject> scanModResources() {
        Map<String, JsonObject> map = new HashMap<>();
        
        for (IModInfo mod : ModList.get().getMods()) {
            String modId = mod.getModId();
            Path path = ModList.get().getModFileById(modId).getFile().findResource(INTERNAL_DATA_PATH);
            
            if (Files.exists(path)) {
                try (Stream<Path> walk = Files.walk(path, 1)) {
                    walk.filter(p -> p.toString().endsWith(".json"))
                        .forEach(file -> {
                            try (InputStream is = Files.newInputStream(file)) {
                                JsonObject json = GSON.fromJson(new InputStreamReader(is), JsonObject.class);
                                String id = file.getFileName().toString().replace(".json", "").toLowerCase();
                                map.put(id, json);
                            } catch (Exception e) {
                                LOGGER.error("Failed to read JAR resource from {}: {}", modId, file, e);
                            }
                        });
                } catch (Exception e) {
                    LOGGER.error("Failed to scan internal resources for mod {}: {}", modId, e.getMessage());
                }
            }
        }
        return map;
    }

    /**
     * Scannt den config/stones/v1/enchantments Ordner und generiert Defaults falls nötig.
     */
    private static void scanConfigFolder(Map<String, JsonObject> map) {
        Path configDir = FMLPaths.CONFIGDIR.get()
                .resolve("stones")
                .resolve(CONFIG_VERSION)
                .resolve("enchantments");
                
        File folder = configDir.toFile();
        
        if (!folder.exists()) {
            folder.mkdirs();
        }
        
        LOGGER.info("Loading Runes from: " + folder.getAbsolutePath());

        File[] files = folder.listFiles((dir, name) -> name.endsWith(".json"));

        // Wenn keine Runen gefunden wurden, generieren wir Defaults über den JsonExporter (Original-Logik)
        if (files == null || files.length == 0) {
            LOGGER.info("No runes found, generating defaults using JsonExporter...");
            JsonExporter.main(new String[]{});
            files = folder.listFiles((dir, name) -> name.endsWith(".json"));
        }

        if (files != null) {
            for (File file : files) {
                try (FileReader reader = new FileReader(file)) {
                    JsonObject json = GSON.fromJson(reader, JsonObject.class);
                    String id = file.getName().replace(".json", "").toLowerCase();
                    map.put(id, json); 
                } catch (Exception e) {
                    LOGGER.error("Failed to load rune json: " + file.getName(), e);
                }
            }
        }
    }

    private static void registerRuneFromJson(String id, JsonObject json, RegisterEvent event) {
        try {
            String typeStr = json.get("type").getAsString().toUpperCase();
            RuneEnchantment.Type type = RuneEnchantment.Type.valueOf(typeStr);
            
            String customName = json.has("name") ? json.get("name").getAsString() : null;
            String customDesc = json.has("description") ? json.get("description").getAsString() : null;
            double factor = json.has("factor") ? json.get("factor").getAsDouble() : 0.0;

            RuneEnchantment tempEnchantment = null;

            if (json.has("attribute")) {
                String attrStr = json.get("attribute").getAsString();
                Attribute attribute = ForgeRegistries.ATTRIBUTES.getValue(new ResourceLocation(attrStr));
                if (attribute != null) {
                    String opStr = json.get("operation").getAsString().toUpperCase();
                    AttributeModifier.Operation operation = AttributeModifier.Operation.valueOf(opStr);
                    tempEnchantment = new RuneEnchantment(type, attribute, operation, factor, customName, customDesc);
                } else {
                    LOGGER.error("Attribute not found: " + attrStr);
                }
            } 
            else if (json.has("effect")) {
                String effStr = json.get("effect").getAsString();
                MobEffect effect = ForgeRegistries.MOB_EFFECTS.getValue(new ResourceLocation(effStr));
                if (effect != null) {
                    tempEnchantment = new RuneEnchantment(type, effect, factor, customName, customDesc);
                } else {
                    LOGGER.error("Effect not found: " + effStr);
                }
            }
            else {
                tempEnchantment = new RuneEnchantment(type, (Attribute)null, null, 0.0, customName, customDesc);
            }

            if (tempEnchantment != null) {
                if (json.has("behaviors")) {
                    JsonArray behaviors = json.getAsJsonArray("behaviors");
                    for (JsonElement el : behaviors) {
                        JsonObject bObj = el.getAsJsonObject();
                        String trigStr = bObj.get("trigger").getAsString().toUpperCase();
                        RuneBehavior.TriggerType trigger = RuneBehavior.TriggerType.valueOf(trigStr);
                        
                        List<RuneCondition> conditionsList = new ArrayList<>();
                        if (bObj.has("conditions")) {
                            JsonElement condElement = bObj.get("conditions");
                            if (condElement.isJsonArray()) {
                                for (JsonElement ce : condElement.getAsJsonArray()) parseAndAddCondition(ce.getAsJsonObject(), conditionsList);
                            } else if (condElement.isJsonObject()) {
                                parseAndAddCondition(condElement.getAsJsonObject(), conditionsList);
                            }
                        }
                        
                        List<RuneBehavior.ConfiguredRuneAction> actionsList = new ArrayList<>();
                        if (bObj.has("actions")) {
                            for (JsonElement actEl : bObj.getAsJsonArray("actions")) {
                                JsonObject actObj = actEl.getAsJsonObject();
                                if (actObj.has("type")) {
                                    RuneAction action = MilestoneActionRegistry.get(actObj.get("type").getAsString());
                                    if (action != null) actionsList.add(new RuneBehavior.ConfiguredRuneAction(action, actObj));
                                }
                            }
                        }
                        tempEnchantment.addBehavior(new RuneBehavior(trigger, conditionsList, actionsList));
                    }
                }

                final RuneEnchantment finalEnchantment = tempEnchantment;
                event.register(ForgeRegistries.Keys.ENCHANTMENTS, new ResourceLocation(StonesMod.MODID, id), () -> finalEnchantment);
                LOGGER.info("Registered Rune: stones:{}", id);
            }
        } catch (Exception e) {
            LOGGER.error("Critical error registering rune '" + id + "'", e);
        }
    }

    private static void parseAndAddCondition(JsonObject json, List<RuneCondition> list) {
        if (json.has("type")) {
            String type = json.get("type").getAsString();
            RuneCondition condition = ConditionRegistry.create(type, json);
            if (condition != null) {
                list.add(condition);
            } else {
                LOGGER.warn("Unknown condition type: " + type);
            }
        }
    }
}

================================================================================
FILE: init\StonesModItems.java
================================================================================

package net.stones.init;

import net.stones.StonesMod;
import net.minecraftforge.registries.RegistryObject;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.BlockItem;
import net.stones.item.StoneItem;

public class StonesModItems {
	public static final DeferredRegister<Item> REGISTRY = DeferredRegister.create(ForgeRegistries.ITEMS, StonesMod.MODID);
	
    public static final RegistryObject<Item> RUNESTONE = block(StonesModBlocks.RUNESTONE);
    
    public static final RegistryObject<Item> RUNE_MINOR = REGISTRY.register("rune_minor", () -> new StoneItem(StoneItem.Type.MINOR));
    public static final RegistryObject<Item> RUNE_MAJOR = REGISTRY.register("rune_major", () -> new StoneItem(StoneItem.Type.MAJOR));
    public static final RegistryObject<Item> RUNE_MILESTONE = REGISTRY.register("rune_milestone", () -> new StoneItem(StoneItem.Type.MILESTONE));

	private static RegistryObject<Item> block(RegistryObject<Block> block) {
		return REGISTRY.register(block.getId().getPath(), () -> new BlockItem(block.get(), new Item.Properties()));
	}
}

================================================================================
FILE: init\StonesModMenus.java
================================================================================

package net.stones.init;

import net.minecraft.world.inventory.MenuType;
import net.minecraftforge.common.extensions.IForgeMenuType;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import net.stones.StonesMod;
import net.stones.gui.RunestoneMenu;

public class StonesModMenus {
    public static final DeferredRegister<MenuType<?>> REGISTRY = DeferredRegister.create(ForgeRegistries.MENU_TYPES, StonesMod.MODID);

    // Wir nutzen IForgeMenuType.create, um extra Daten (z.B. die Slot-Anzahl) beim Öffnen senden zu können
    public static final RegistryObject<MenuType<RunestoneMenu>> RUNESTONE_MENU = REGISTRY.register("runestone",
            () -> IForgeMenuType.create(RunestoneMenu::new));
}

================================================================================
FILE: init\StonesModTags.java
================================================================================

package net.stones.init;

import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.ItemTags;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.stones.StonesMod;

public class StonesModTags {
    // Definiert tags unter data/stones/tags/items/runes/
    public static final TagKey<Item> RUNE_MINOR = ItemTags.create(new ResourceLocation(StonesMod.MODID, "runes/minor"));
    public static final TagKey<Item> RUNE_MAJOR = ItemTags.create(new ResourceLocation(StonesMod.MODID, "runes/major"));
    public static final TagKey<Item> RUNE_MILESTONE = ItemTags.create(new ResourceLocation(StonesMod.MODID, "runes/milestone"));
}

================================================================================
FILE: item\StoneItem.java
================================================================================

package net.stones.item;

import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Rarity;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.Level;
import net.stones.enchantment.RuneEnchantment;
import net.stones.logic.RuneCalculator;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Map;

public class StoneItem extends Item {

    public enum Type { MINOR, MAJOR, MILESTONE }
    private final Type type;

    public StoneItem(Type type) {
        super(new Item.Properties().stacksTo(1).rarity(
            type == Type.MAJOR ? Rarity.RARE : (type == Type.MILESTONE ? Rarity.EPIC : Rarity.COMMON)
        ));
        this.type = type;
    }

    public Type getType() { return type; }

    @Override
    public boolean isEnchantable(ItemStack pStack) { return true; }

    @Override
    public int getEnchantmentValue() { return 15; }

    @Override
    public boolean canApplyAtEnchantingTable(ItemStack stack, Enchantment enchantment) {
        if (enchantment instanceof RuneEnchantment runeEnch) {
            return runeEnch.type == switch(this.type) {
                case MINOR -> RuneEnchantment.Type.MINOR;
                case MAJOR -> RuneEnchantment.Type.MAJOR;
                case MILESTONE -> RuneEnchantment.Type.MILESTONE;
            };
        }
        return enchantment == Enchantments.VANISHING_CURSE || enchantment == Enchantments.BINDING_CURSE;
    }

    @Override
    public void appendHoverText(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
        super.appendHoverText(stack, level, tooltip, flag);
        
        int reqLevel = RuneCalculator.getRequiredLevel(stack);
        boolean hasCurse = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.VANISHING_CURSE, stack) > 0 
                        || EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BINDING_CURSE, stack) > 0;

        ChatFormatting color = hasCurse ? ChatFormatting.GREEN : ChatFormatting.BLUE;
        String suffix = hasCurse ? " (Reduziert durch Fluch)" : "";
        
        tooltip.add(Component.literal("Benötigtes Level: " + reqLevel).withStyle(color)
                .append(Component.literal(suffix).withStyle(ChatFormatting.DARK_GREEN)));

        Map<Enchantment, Integer> enchants = EnchantmentHelper.getEnchantments(stack);
        boolean hasRuneEffect = false;

        for (Map.Entry<Enchantment, Integer> entry : enchants.entrySet()) {
            if (entry.getKey() instanceof RuneEnchantment runeEnch) {
                hasRuneEffect = true;
                
                // --- NEU: CUSTOM DESCRIPTION ANZEIGEN ---
                if (runeEnch.getCustomDescription() != null && !runeEnch.getCustomDescription().isEmpty()) {
                    // Zeige Beschreibung in Grau/Kursiv
                    tooltip.add(Component.literal(runeEnch.getCustomDescription())
                        .withStyle(ChatFormatting.GRAY, ChatFormatting.ITALIC));
                    // Kleiner Abstand
                    tooltip.add(Component.empty());
                }
                
                // Vorschau-Logik (wie vorher)
                Player player = Minecraft.getInstance().player;
                if (player != null) {
                    if (runeEnch.targetAttribute != null) {
                        double bonus = runeEnch.calculateBonus(entry.getValue(), player.experienceLevel);
                        String sign = bonus >= 0 ? "+" : "";
                        String valueStr = String.format("%.1f", bonus);
                        if (runeEnch.operation != AttributeModifier.Operation.ADDITION && runeEnch.operation != null) {
                            valueStr = String.format("%.1f%%", bonus * 100);
                        }
                        Component attrName = Component.translatable(runeEnch.targetAttribute.getDescriptionId());
                        tooltip.add(Component.literal("Effekt: ").withStyle(ChatFormatting.GRAY)
                            .append(Component.literal(sign + valueStr + " ").withStyle(ChatFormatting.GOLD))
                            .append(attrName.copy().withStyle(ChatFormatting.WHITE)));
                    } else if (runeEnch.targetEffect != null) {
                        Component effName = runeEnch.targetEffect.getDisplayName();
                        tooltip.add(Component.literal("Effekt: ").withStyle(ChatFormatting.GRAY)
                            .append(effName.copy().withStyle(ChatFormatting.LIGHT_PURPLE)));
                    }
                    
                    if (runeEnch.type == RuneEnchantment.Type.MAJOR) {
                        tooltip.add(Component.literal(" (Skaliert mit deinem Level)").withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC));
                    }
                }
            }
        }
        
        if (!hasRuneEffect) {
            tooltip.add(Component.literal("⚠ Leer - Benutze Enchanting Table!").withStyle(ChatFormatting.GRAY, ChatFormatting.ITALIC));
        }
    }
}

================================================================================
FILE: logic\RuneCalculator.java
================================================================================

package net.stones.logic;

import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraftforge.registries.ForgeRegistries;
import net.stones.cap.PlayerShrineCapProvider;
import net.stones.data.ShrineInstance;
import net.stones.data.ShrineInstance.SlotConfig;
import net.stones.data.ShrineSavedData;
import net.stones.enchantment.RuneEnchantment;
import net.stones.item.StoneItem;
import net.stones.init.StonesModTags;
import net.minecraft.world.item.enchantment.Enchantments;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

public class RuneCalculator {

    private static UUID getSlotUUID(int slotIndex) {
        return UUID.nameUUIDFromBytes(("Runestone_Slot_Modifier_" + slotIndex).getBytes());
    }

    public static List<Component> updatePlayer(ServerPlayer player) {
        List<Component> activeEffectsSummary = new ArrayList<>();

        player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
            UUID shrineId = cap.getLinkedShrine();
            if (shrineId == null) return;

            ShrineInstance shrine = ShrineSavedData.get(player.serverLevel()).getShrine(shrineId);
            if (shrine == null) return;

            // Cleanup
            for (int i = 0; i < shrine.getInventory().getSlots(); i++) {
                removeAllModifiersFromSlot(player, getSlotUUID(i));
            }

            // Calculation
            for (int i = 0; i < shrine.getInventory().getSlots(); i++) {
                ItemStack stack = shrine.getInventory().getStackInSlot(i);
                if (stack.isEmpty() || !isRune(stack)) continue;

                SlotConfig config = getSlotConfig(shrine, i);
                if (config == null) continue;

                if (player.experienceLevel < config.requiredLevel) continue;

                Map<Enchantment, Integer> enchants = EnchantmentHelper.getEnchantments(stack);
                RuneEnchantment activeRune = null;
                double totalBonus = 0;

                for (Map.Entry<Enchantment, Integer> entry : enchants.entrySet()) {
                    if (entry.getKey() instanceof RuneEnchantment rune) {
                        if (rune.targetAttribute != null) {
                            activeRune = rune;
                            totalBonus += rune.calculateBonus(entry.getValue(), player.experienceLevel);
                        }
                    }
                }

                if (activeRune != null && totalBonus != 0) {
                    AttributeInstance inst = player.getAttribute(activeRune.targetAttribute);
                    if (inst != null) {
                        AttributeModifier mod = new AttributeModifier(
                                getSlotUUID(i), 
                                "Runestone Bonus " + i, 
                                totalBonus, 
                                activeRune.operation
                        );
                        if (!inst.hasModifier(mod)) {
                            inst.addTransientModifier(mod);
                        }
                        activeEffectsSummary.add(formatEffectText(activeRune, totalBonus));
                    }
                }
            }
        });

        return activeEffectsSummary;
    }

    private static SlotConfig getSlotConfig(ShrineInstance shrine, int index) {
        for (SlotConfig cfg : shrine.getLayout()) {
            if (cfg.inventoryIndex == index) return cfg;
        }
        return null;
    }

    private static void removeAllModifiersFromSlot(Player player, UUID slotUUID) {
        ForgeRegistries.ATTRIBUTES.getValues().forEach(attr -> {
            AttributeInstance inst = player.getAttribute(attr);
            if (inst != null && inst.getModifier(slotUUID) != null) {
                inst.removeModifier(slotUUID);
            }
        });
    }

	public static int getRequiredLevel(ItemStack stack) {
		if (stack.isEmpty()) return 1;

		int calculatedLevel = 0;
		Map<Enchantment, Integer> enchants = EnchantmentHelper.getEnchantments(stack);
		
		for (Map.Entry<Enchantment, Integer> entry : enchants.entrySet()) {
			Enchantment ench = entry.getKey();
			int lvl = entry.getValue();

			if (ench instanceof RuneEnchantment) {
				calculatedLevel += (lvl * 5);
			} else if (ench == Enchantments.VANISHING_CURSE) {
				calculatedLevel -= 15;
			} else if (ench == Enchantments.BINDING_CURSE) {
				calculatedLevel -= 10;
			}
		}


		return Math.max(1, calculatedLevel);
	}

    private static Component formatEffectText(RuneEnchantment rune, double value) {
        MutableComponent line = Component.literal(" ");
        String sign = value >= 0 ? "+" : "";
        String valStr = (rune.operation == AttributeModifier.Operation.ADDITION) 
            ? String.format("%.1f", value) 
            : String.format("%.1f%%", value * 100);
            
        line.append(Component.literal(sign + valStr + " ").withStyle(ChatFormatting.GREEN));
        line.append(Component.translatable(rune.targetAttribute.getDescriptionId()).withStyle(ChatFormatting.GRAY));
        return line;
    }
	
	private static boolean isRune(ItemStack stack) {
		return stack.is(StonesModTags.RUNE_MINOR) || 
			   stack.is(StonesModTags.RUNE_MAJOR) || 
			   stack.is(StonesModTags.RUNE_MILESTONE);
	}
}

================================================================================
FILE: network\PacketBindShrine.java
================================================================================

package net.stones.network;

import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.PacketDistributor;
import net.stones.StonesMod;
import net.stones.block.entity.RunestoneBlockEntity;
import net.stones.cap.PlayerShrineCapProvider;
import net.stones.data.ShrineInstance;
import net.stones.data.ShrineSavedData;
import net.stones.logic.RuneCalculator;

import java.util.UUID;
import java.util.function.Supplier;

public class PacketBindShrine {

    private final BlockPos pos;

    public PacketBindShrine(BlockPos pos) {
        this.pos = pos;
    }

    public PacketBindShrine(FriendlyByteBuf buf) {
        this.pos = buf.readBlockPos();
    }

    public void toBytes(FriendlyByteBuf buf) {
        buf.writeBlockPos(pos);
    }

    public boolean handle(Supplier<NetworkEvent.Context> supplier) {
        NetworkEvent.Context context = supplier.get();
        context.enqueueWork(() -> {
            ServerPlayer player = context.getSender();
            if (player != null) {
                ServerLevel level = player.serverLevel();
                
                if (level.isLoaded(pos)) {
                    BlockEntity be = level.getBlockEntity(pos);
                    if (be instanceof RunestoneBlockEntity runeBe) {
                        UUID shrineId = runeBe.getShrineId();
                        if (shrineId != null) {
                            
                            ShrineInstance shrine = ShrineSavedData.get(level).getShrine(shrineId);
                            if (shrine != null) {
                                GlobalPos globalPos = GlobalPos.of(level.dimension(), pos);
                                
                                // 1. CLEANUP: Aus altem Schrein austragen (falls vorhanden)
                                player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
                                    if (cap.isLinked() && !cap.getLinkedShrine().equals(shrineId)) {
                                        ShrineInstance oldShrine = ShrineSavedData.get(level).getShrine(cap.getLinkedShrine());
                                        if (oldShrine != null) {
                                            oldShrine.removeOwner(player.getUUID());
                                            
                                            // Optional: Alten Block updaten (Visuals entfernen)
                                            if (oldShrine.getLocation() != null && oldShrine.getLocation().dimension() == level.dimension()) {
                                                BlockPos oldPos = oldShrine.getLocation().pos();
                                                if (level.isLoaded(oldPos)) {
                                                    level.sendBlockUpdated(oldPos, level.getBlockState(oldPos), level.getBlockState(oldPos), 3);
                                                }
                                            }
                                        }
                                    }
                                    
                                    // 2. BINDUNG: Neuen Link setzen
                                    cap.setLinkedShrine(shrineId, globalPos);
                                    
                                    // 3. TEAM: Spieler zum neuen Schrein hinzufügen
                                    shrine.addOwner(player.getUUID());
                                    shrine.setLocation(globalPos);
                                    
                                    // Block Update für neue Visuals
                                    level.sendBlockUpdated(pos, level.getBlockState(pos), level.getBlockState(pos), 3);
                                    
                                    // Feedback & Sync
                                    level.playSound(null, pos, net.minecraft.sounds.SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, net.minecraft.sounds.SoundSource.PLAYERS, 1.0f, 0.5f);
                                    player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§d[Runestone] §fSeelengebunden an Schrein: §e" + shrineId.toString().substring(0, 8) + "..."));
                                    
                                    RuneCalculator.updatePlayer(player);
                                    StonesMod.PACKET_HANDLER.send(PacketDistributor.PLAYER.with(() -> player), new PacketSyncPlayerShrine(shrineId, globalPos));
                                });
                            }
                        }
                    }
                }
            }
        });
        return true;
    }
}

================================================================================
FILE: network\PacketOpenShrine.java
================================================================================

package net.stones.network;

import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.NetworkHooks;
import net.stones.block.entity.RunestoneBlockEntity;
import net.stones.data.ShrineInstance;
import net.stones.data.ShrineSavedData;
import net.stones.data.ShrineInstance.SlotConfig;

import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;

public class PacketOpenShrine {

    private final BlockPos pos;

    public PacketOpenShrine(BlockPos pos) {
        this.pos = pos;
    }

    public PacketOpenShrine(FriendlyByteBuf buf) {
        this.pos = buf.readBlockPos();
    }

    public void toBytes(FriendlyByteBuf buf) {
        buf.writeBlockPos(pos);
    }

    public boolean handle(Supplier<NetworkEvent.Context> supplier) {
        NetworkEvent.Context context = supplier.get();
        context.enqueueWork(() -> {
            ServerPlayer player = context.getSender();
            if (player != null) {
                ServerLevel level = player.serverLevel();
                if (level.isLoaded(pos)) {
                    BlockEntity be = level.getBlockEntity(pos);
                    if (be instanceof RunestoneBlockEntity runeBe) {
                        UUID shrineId = runeBe.getShrineId();
                        if (shrineId != null) {
                            ShrineInstance shrine = ShrineSavedData.get(level).getShrine(shrineId);
                            if (shrine != null) {
                                // Hier ist die Logik, die früher im Block war
                                NetworkHooks.openScreen(player, runeBe, buf -> {
                                    buf.writeInt(shrine.getInventory().getSlots());
                                    List<SlotConfig> layout = shrine.getLayout();
                                    buf.writeInt(layout.size());
                                    for (SlotConfig cfg : layout) {
                                        buf.writeEnum(cfg.type);
                                        buf.writeInt(cfg.requiredLevel);
                                        buf.writeInt(cfg.inventoryIndex);
                                    }
                                });
                            }
                        }
                    }
                }
            }
        });
        return true;
    }
}

================================================================================
FILE: network\PacketSyncPlayerShrine.java
================================================================================

package net.stones.network;

import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraftforge.network.NetworkEvent;
import net.stones.cap.PlayerShrineCapProvider;

import java.util.UUID;
import java.util.function.Supplier;

public class PacketSyncPlayerShrine {
    private final UUID shrineId;
    private final GlobalPos shrinePos;

    // Konstruktor Server-Side
    public PacketSyncPlayerShrine(UUID id, GlobalPos pos) {
        this.shrineId = id;
        this.shrinePos = pos;
    }

    // Decoder
    public PacketSyncPlayerShrine(FriendlyByteBuf buf) {
        if (buf.readBoolean()) {
            this.shrineId = buf.readUUID();
            BlockPos bp = buf.readBlockPos();
            ResourceKey<Level> dim = buf.readResourceKey(Registries.DIMENSION);
            this.shrinePos = GlobalPos.of(dim, bp);
        } else {
            this.shrineId = null;
            this.shrinePos = null;
        }
    }

    // Encoder
    public void toBytes(FriendlyByteBuf buf) {
        if (shrineId != null && shrinePos != null) {
            buf.writeBoolean(true);
            buf.writeUUID(shrineId);
            buf.writeBlockPos(shrinePos.pos());
            buf.writeResourceKey(shrinePos.dimension());
        } else {
            buf.writeBoolean(false); // Kein Link -> Reset
        }
    }

    public boolean handle(Supplier<NetworkEvent.Context> supplier) {
        NetworkEvent.Context context = supplier.get();
        context.enqueueWork(() -> {
            // CLIENT SIDE LOGIC
            // Wir updaten die Capability des lokalen Spielers, damit das HUD sie lesen kann
            Player player = Minecraft.getInstance().player;
            if (player != null) {
                player.getCapability(PlayerShrineCapProvider.SHRINE_LINK).ifPresent(cap -> {
                    cap.setLinkedShrine(shrineId, shrinePos);
                });
            }
        });
        return true;
    }
}

================================================================================
FILE: util\JsonExporter.java
================================================================================

package net.stones.util;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import net.minecraftforge.fml.loading.FMLPaths;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class JsonExporter {
    
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
    
    public static void main(String[] args) {
        Path configDir;
        try {
            configDir = FMLPaths.CONFIGDIR.get();
        } catch (Exception e) {
            configDir = Paths.get("run/config");
        }

        Path outputDir = configDir.resolve("stones").resolve("v1").resolve("enchantments");
        File folder = outputDir.toFile();
        if (!folder.exists()) folder.mkdirs();
        
        System.out.println("Exporting to: " + folder.getAbsolutePath());
        
        exportAttributeRune(folder, "minor_armor", "minor", "minecraft:generic.armor", "ADDITION", 1.0, "Rune of Hardness", "Increases Armor rating.");
        exportAttributeRune(folder, "major_wrath", "major", "minecraft:generic.attack_damage", "MULTIPLY_TOTAL", 0.01, "Rune of Wrath", "Scales damage with your level.");
        
        exportDrownedWarrior(folder);
        exportBerserker(folder);
        exportNightHunter(folder);
        exportBloodMage(folder);
        exportStormCaller(folder);
        exportPhoenix(folder);
        exportGladiator(folder);
        exportNecromancer(folder);
        exportGamblersRuin(folder);
        
        System.out.println("Export complete!");
    }

    private static void exportAttributeRune(File folder, String name, String type, String attr, String op, double factor, String displayName, String desc) {
        JsonObject json = new JsonObject();
        json.addProperty("type", type.toUpperCase());
        json.addProperty("attribute", attr);
        json.addProperty("operation", op);
        json.addProperty("factor", factor);
        json.addProperty("name", displayName);
        json.addProperty("description", desc);
        writeToFile(folder, name + ".json", json);
    }

    private static void exportGamblersRuin(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Gambler's Ruin");
        json.addProperty("description", "Deal massive damage (400% + sacrificed level), but sacrifice a random enchantment.");
        
        JsonArray behaviors = new JsonArray();
        JsonObject behavior = new JsonObject();
        behavior.addProperty("trigger", "ON_ATTACK");
        
        // 1. Condition: Hat der Spieler ein Enchantment? (Damit der Bonus nicht "umsonst" ist)
        JsonArray conds = new JsonArray();
        JsonObject c1 = new JsonObject();
        c1.addProperty("type", "stones:has_enchantment");
        c1.addProperty("target", "attacker");
        conds.add(c1);
        behavior.add("conditions", conds);
        
        JsonArray acts = new JsonArray();
        
        // 2. Entfernen & Level speichern
        JsonObject a1 = new JsonObject();
        a1.addProperty("type", "stones:remove_random_enchantment");
        a1.addProperty("save_level_to", "sacrificed_power");
        acts.add(a1);
        
        // 3. Multiplier Damage (400%)
        JsonObject a2 = new JsonObject();
        a2.addProperty("type", "stones:modify_damage");
        a2.addProperty("multiplier", 4.0);
        acts.add(a2);
        
        // 4. Flat Damage (+Level) - Hier nutzen wir das gespeicherte Level für Schaden!
        JsonObject a3 = new JsonObject();
        a3.addProperty("type", "stones:modify_damage");
        a3.addProperty("add_var", "sacrificed_power");
        acts.add(a3);
        
        // 5. Partikel
        JsonObject a4 = new JsonObject();
        a4.addProperty("type", "stones:spawn_particles");
        a4.addProperty("particle", "minecraft:witch");
        a4.addProperty("count", 20);
        acts.add(a4);

        behavior.add("actions", acts);
        behaviors.add(behavior);
        json.add("behaviors", behaviors);
        
        writeToFile(folder, "milestone_gamblers_ruin.json", json);
    }

    private static void exportDrownedWarrior(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Drowned Warrior");
        json.addProperty("description", "Your attacks consume air but deal massive damage. Don't suffocate!");
        JsonArray behaviors = new JsonArray();
        JsonObject bSwing = new JsonObject();
        bSwing.addProperty("trigger", "ON_SWING");
        JsonArray condsSwing = new JsonArray();
        JsonObject cond1 = new JsonObject();
        cond1.addProperty("type", "stones:has_air");
        cond1.addProperty("min", 60); 
        condsSwing.add(cond1);
        bSwing.add("conditions", condsSwing);
        JsonArray actsSwing = new JsonArray();
        JsonObject actAir = new JsonObject();
        actAir.addProperty("type", "stones:modify_air");
        actAir.addProperty("add", -100); 
        actsSwing.add(actAir);
        bSwing.add("actions", actsSwing);
        behaviors.add(bSwing);
        JsonObject bAttack = new JsonObject();
        bAttack.addProperty("trigger", "ON_ATTACK");
        JsonArray condsAttack = new JsonArray();
        JsonObject cond2 = new JsonObject();
        cond2.addProperty("type", "stones:has_air");
        cond2.addProperty("min", 60); 
        condsAttack.add(cond2);
        bAttack.add("conditions", condsAttack);
        JsonArray actsAttack = new JsonArray();
        JsonObject actDmg = new JsonObject();
        actDmg.addProperty("type", "stones:modify_damage");
        actDmg.addProperty("multiplier", 1.5);
        actDmg.addProperty("add", 5.0);
        actsAttack.add(actDmg);
        bAttack.add("actions", actsAttack);
        behaviors.add(bAttack);
        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_drowned_warrior.json", json);
    }
    
    private static void exportBerserker(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Berserker");
        json.addProperty("description", "Deal more damage at low health. Heal on kills.");
        JsonArray behaviors = new JsonArray();
        JsonObject beh1 = new JsonObject();
        beh1.addProperty("trigger", "ON_ATTACK");
        JsonArray conds1 = new JsonArray();
        JsonObject cond1 = new JsonObject();
        cond1.addProperty("type", "stones:health_below");
        cond1.addProperty("percent", 0.3);
        conds1.add(cond1);
        beh1.add("conditions", conds1);
        JsonArray acts1 = new JsonArray();
        JsonObject act1 = new JsonObject();
        act1.addProperty("type", "stones:modify_damage");
        act1.addProperty("multiplier", 2.0);
        acts1.add(act1);
        JsonObject act2 = new JsonObject();
        act2.addProperty("type", "stones:apply_effect");
        act2.addProperty("effect", "minecraft:strength");
        act2.addProperty("duration", 60);
        act2.addProperty("amplifier", 0);
        acts1.add(act2);
        beh1.add("actions", acts1);
        behaviors.add(beh1);
        JsonObject beh2 = new JsonObject();
        beh2.addProperty("trigger", "ON_KILL");
        JsonArray acts2 = new JsonArray();
        JsonObject act3 = new JsonObject();
        act3.addProperty("type", "stones:heal");
        act3.addProperty("percent_of_max_health", 0.2);
        acts2.add(act3);
        beh2.add("actions", acts2);
        behaviors.add(beh2);
        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_berserker.json", json);
    }
    
    private static void exportNightHunter(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Night Hunter");
        json.addProperty("description", "Gain power during night. Weakened during day.");
        JsonArray behaviors = new JsonArray();
        JsonObject beh1 = new JsonObject();
        beh1.addProperty("trigger", "ON_ATTACK");
        JsonArray conds1 = new JsonArray();
        JsonObject cond1 = new JsonObject();
        cond1.addProperty("type", "stones:is_day");
        conds1.add(cond1);
        beh1.add("conditions", conds1);
        JsonArray acts1 = new JsonArray();
        JsonObject act1 = new JsonObject();
        act1.addProperty("type", "stones:modify_damage");
        act1.addProperty("multiplier", 0.5);
        acts1.add(act1);
        beh1.add("actions", acts1);
        behaviors.add(beh1);
        JsonObject beh2 = new JsonObject();
        beh2.addProperty("trigger", "ON_ATTACK");
        JsonArray conds2 = new JsonArray();
        JsonObject cond2 = new JsonObject();
        cond2.addProperty("type", "stones:chance");
        cond2.addProperty("value", 0.3);
        conds2.add(cond2);
        beh2.add("conditions", conds2);
        JsonArray acts2 = new JsonArray();
        JsonObject act2 = new JsonObject();
        act2.addProperty("type", "stones:modify_damage");
        act2.addProperty("multiplier", 1.5);
        acts2.add(act2);
        JsonObject act3 = new JsonObject();
        act3.addProperty("type", "stones:apply_effect");
        act3.addProperty("effect", "minecraft:invisibility");
        act3.addProperty("duration", 40);
        act3.addProperty("amplifier", 0);
        acts2.add(act3);
        beh2.add("actions", acts2);
        behaviors.add(beh2);
        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_night_hunter.json", json);
    }
    
    private static void exportBloodMage(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Blood Mage");
        json.addProperty("description", "Convert health to power. Lifesteal on attacks.");
        JsonArray behaviors = new JsonArray();
        JsonObject behavior = new JsonObject();
        behavior.addProperty("trigger", "ON_ATTACK");
        JsonArray conditions = new JsonArray();
        JsonObject cond1 = new JsonObject();
        cond1.addProperty("type", "stones:health_below");
        cond1.addProperty("percent", 0.95);
        conditions.add(cond1);
        behavior.add("conditions", conditions);
        JsonArray actions = new JsonArray();
        JsonObject act1 = new JsonObject();
        act1.addProperty("type", "stones:set_variable");
        act1.addProperty("name", "blood_cost");
        act1.addProperty("value", 2.0);
        actions.add(act1);
        JsonObject act2 = new JsonObject();
        act2.addProperty("type", "stones:modify_damage");
        act2.addProperty("add", 8.0);
        actions.add(act2);
        JsonObject act3 = new JsonObject();
        act3.addProperty("type", "stones:heal");
        act3.addProperty("amount", -2.0);
        actions.add(act3);
        JsonObject act4 = new JsonObject();
        act4.addProperty("type", "stones:heal");
        act4.addProperty("percent_of_damage", 0.3);
        actions.add(act4);
        behavior.add("actions", actions);
        behaviors.add(behavior);
        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_blood_mage.json", json);
    }
    
    private static void exportStormCaller(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Storm Caller");
        json.addProperty("description", "Build up charge with attacks. Release lightning at max charge.");
        JsonArray behaviors = new JsonArray();
        JsonObject beh1 = new JsonObject();
        beh1.addProperty("trigger", "ON_ATTACK");
        JsonArray acts1 = new JsonArray();
        JsonObject act1 = new JsonObject();
        act1.addProperty("type", "stones:math");
        act1.addProperty("variable", "lightning_charge");
        act1.addProperty("operation", "add");
        act1.addProperty("value", 1.0);
        acts1.add(act1);
        beh1.add("actions", acts1);
        behaviors.add(beh1);
        JsonObject beh2 = new JsonObject();
        beh2.addProperty("trigger", "ON_ATTACK");
        JsonArray conds2 = new JsonArray();
        JsonObject cond1 = new JsonObject();
        cond1.addProperty("type", "stones:variable_compare");
        cond1.addProperty("variable", "lightning_charge");
        cond1.addProperty("operator", ">=");
        cond1.addProperty("value", 5.0);
        conds2.add(cond1);
        beh2.add("conditions", conds2);
        JsonArray acts2 = new JsonArray();
        JsonObject act2 = new JsonObject();
        act2.addProperty("type", "stones:explode");
        act2.addProperty("radius", 3.0);
        act2.addProperty("fire", false);
        acts2.add(act2);
        JsonObject act3 = new JsonObject();
        act3.addProperty("type", "stones:apply_effect");
        act3.addProperty("effect", "minecraft:speed");
        act3.addProperty("duration", 100);
        act3.addProperty("amplifier", 1);
        acts2.add(act3);
        JsonObject act4 = new JsonObject();
        act4.addProperty("type", "stones:set_variable");
        act4.addProperty("name", "lightning_charge");
        act4.addProperty("value", 0.0);
        acts2.add(act4);
        beh2.add("actions", acts2);
        behaviors.add(beh2);
        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_storm_caller.json", json);
    }
    
    private static void exportPhoenix(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Phoenix");
        json.addProperty("description", "Burning empowers you. Once per life, revive from ashes.");
        JsonArray behaviors = new JsonArray();
        JsonObject beh1 = new JsonObject();
        beh1.addProperty("trigger", "ON_ATTACK");
        JsonArray conds1 = new JsonArray();
        JsonObject cond1 = new JsonObject();
        cond1.addProperty("type", "stones:on_fire");
        conds1.add(cond1);
        beh1.add("conditions", conds1);
        JsonArray acts1 = new JsonArray();
        JsonObject act1 = new JsonObject();
        act1.addProperty("type", "stones:modify_damage");
        act1.addProperty("multiplier", 1.8);
        acts1.add(act1);
        JsonObject act2 = new JsonObject();
        act2.addProperty("type", "stones:explode");
        act2.addProperty("radius", 2.0);
        act2.addProperty("fire", true);
        acts1.add(act2);
        beh1.add("actions", acts1);
        behaviors.add(beh1);
        JsonObject beh2 = new JsonObject();
        beh2.addProperty("trigger", "ON_HURT");
        JsonArray conds2 = new JsonArray();
        JsonObject cond2 = new JsonObject();
        cond2.addProperty("type", "stones:health_below");
        cond2.addProperty("percent", 0.1);
        conds2.add(cond2);
        JsonObject cond3 = new JsonObject();
        cond3.addProperty("type", "stones:variable_compare");
        cond3.addProperty("variable", "phoenix_revived");
        cond3.addProperty("operator", "<");
        cond3.addProperty("value", 1.0);
        conds2.add(cond3);
        beh2.add("conditions", conds2);
        JsonArray acts2 = new JsonArray();
        JsonObject act3 = new JsonObject();
        act3.addProperty("type", "stones:cancel");
        acts2.add(act3);
        JsonObject act4 = new JsonObject();
        act4.addProperty("type", "stones:heal");
        act4.addProperty("percent_of_max_health", 1.0);
        acts2.add(act4);
        JsonObject act5 = new JsonObject();
        act5.addProperty("type", "stones:explode");
        act5.addProperty("radius", 5.0);
        act5.addProperty("fire", true);
        acts2.add(act5);
        JsonObject act6 = new JsonObject();
        act6.addProperty("type", "stones:set_variable");
        act6.addProperty("name", "phoenix_revived");
        act6.addProperty("value", 1.0);
        acts2.add(act6);
        beh2.add("actions", acts2);
        behaviors.add(beh2);
        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_phoenix.json", json);
    }
    
    private static void exportGladiator(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Gladiator");
        json.addProperty("description", "Combo attacks increase damage. Combo resets after 5s.");
        JsonArray behaviors = new JsonArray();

        // 1. ON_ATTACK: Combo erhöhen, Swirls auslösen, Timer setzen
        JsonObject bAtk = new JsonObject();
        bAtk.addProperty("trigger", "ON_ATTACK");
        JsonArray aAtk = new JsonArray();
        
        // Combo Variable laden, erhöhen und speichern
        JsonObject getC = new JsonObject(); getC.addProperty("type", "stones:get_persistent_var"); getC.addProperty("name", "combo"); getC.addProperty("into", "c"); aAtk.add(getC);
        JsonObject addC = new JsonObject(); addC.addProperty("type", "stones:math"); addC.addProperty("variable", "c"); addC.addProperty("operation", "add"); addC.addProperty("value", 1.0); aAtk.add(addC);
        JsonObject setC = new JsonObject(); setC.addProperty("type", "stones:set_persistent_var"); setC.addProperty("name", "combo"); setC.addProperty("value_var", "c"); aAtk.add(setC);
        
        // Timer auf 100 Ticks setzen (Persistent!)
        JsonObject setT = new JsonObject(); setT.addProperty("type", "stones:set_persistent_var"); setT.addProperty("name", "timer"); setT.addProperty("value", 100.0); aAtk.add(setT);
        
        // Schaden basierend auf Combo
        JsonObject dmg = new JsonObject(); dmg.addProperty("type", "stones:modify_damage"); dmg.addProperty("add_var", "c"); aAtk.add(dmg);
        
        bAtk.add("actions", aAtk); behaviors.add(bAtk);

        // 2. ON_TICK: Timer verringern und Swirls anzeigen
        JsonObject bTick = new JsonObject();
        bTick.addProperty("trigger", "ON_TICK");
        JsonArray aTick = new JsonArray();
        
        // Timer laden, subtrahieren, speichern
        JsonObject getT = new JsonObject(); getT.addProperty("type", "stones:get_persistent_var"); getT.addProperty("name", "timer"); getT.addProperty("into", "t"); aTick.add(getT);
        JsonObject subT = new JsonObject(); subT.addProperty("type", "stones:math"); subT.addProperty("variable", "t"); subT.addProperty("operation", "subtract"); subT.addProperty("value", 1.0); aTick.add(subT);
        JsonObject saveT = new JsonObject(); saveT.addProperty("type", "stones:set_persistent_var"); saveT.addProperty("name", "timer"); saveT.addProperty("value_var", "t"); aTick.add(saveT);
        
        // Swirls basierend auf Combo Variable
        JsonObject getC2 = new JsonObject(); getC2.addProperty("type", "stones:get_persistent_var"); getC2.addProperty("name", "combo"); getC2.addProperty("into", "c_swirl"); aTick.add(getC2);
        JsonObject swirl = new JsonObject(); swirl.addProperty("type", "stones:particle_orbit"); swirl.addProperty("count_var", "c_swirl"); swirl.addProperty("particle", "minecraft:crit"); aTick.add(swirl);
        
        bTick.add("actions", aTick); behaviors.add(bTick);

        // 3. ON_TICK: Reset wenn Timer <= 0
        JsonObject bRes = new JsonObject(); bRes.addProperty("trigger", "ON_TICK");
        JsonArray cRes = new JsonArray(); 
        JsonObject checkT = new JsonObject(); checkT.addProperty("type", "stones:get_persistent_var"); checkT.addProperty("name", "timer"); checkT.addProperty("into", "t_check"); 
        JsonObject cond = new JsonObject(); cond.addProperty("type", "stones:variable_compare"); cond.addProperty("variable", "t_check"); cond.addProperty("operator", "<="); cond.addProperty("value", 0.0);
        cRes.add(cond); bRes.add("conditions", cRes);
        
        JsonArray aRes = new JsonArray();
        JsonObject rC = new JsonObject(); rC.addProperty("type", "stones:set_persistent_var"); rC.addProperty("name", "combo"); rC.addProperty("value", 0.0); aRes.add(rC);
        bRes.add("actions", aRes);
        behaviors.add(bRes);

        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_gladiator.json", json);
    }
    
    private static void exportNecromancer(File folder) {
        JsonObject json = new JsonObject();
        json.addProperty("type", "MILESTONE");
        json.addProperty("name", "Necromancer");
        json.addProperty("description", "Gain souls on kill. Spend souls to avoid death.");
        JsonArray behaviors = new JsonArray();
        JsonObject beh1 = new JsonObject();
        beh1.addProperty("trigger", "ON_KILL");
        JsonArray acts1 = new JsonArray();
        JsonObject act1 = new JsonObject();
        act1.addProperty("type", "stones:math");
        act1.addProperty("variable", "souls");
        act1.addProperty("operation", "add");
        act1.addProperty("value", 1.0);
        acts1.add(act1);
        JsonObject act2 = new JsonObject();
        act2.addProperty("type", "stones:apply_effect");
        act2.addProperty("effect", "minecraft:regeneration");
        act2.addProperty("duration", 40);
        act2.addProperty("amplifier", 0);
        acts1.add(act2);
        beh1.add("actions", acts1);
        behaviors.add(beh1);
        JsonObject beh2 = new JsonObject();
        beh2.addProperty("trigger", "ON_HURT");
        JsonArray conds2 = new JsonArray();
        JsonObject cond1 = new JsonObject();
        cond1.addProperty("type", "stones:health_below");
        cond1.addProperty("percent", 0.15);
        conds2.add(cond1);
        JsonObject cond2 = new JsonObject();
        cond2.addProperty("type", "stones:variable_compare");
        cond2.addProperty("variable", "souls");
        cond2.addProperty("operator", ">=");
        cond2.addProperty("value", 5.0);
        conds2.add(cond2);
        beh2.add("conditions", conds2);
        JsonArray acts2 = new JsonArray();
        JsonObject act3 = new JsonObject();
        act3.addProperty("type", "stones:cancel");
        acts2.add(act3);
        JsonObject act4 = new JsonObject();
        act4.addProperty("type", "stones:heal");
        act4.addProperty("percent_of_max_health", 0.5);
        acts2.add(act4);
        JsonObject act5 = new JsonObject();
        act5.addProperty("type", "stones:math");
        act5.addProperty("variable", "souls");
        act5.addProperty("operation", "subtract");
        act5.addProperty("value", 5.0);
        acts2.add(act5);
        beh2.add("actions", acts2);
        behaviors.add(beh2);
        json.add("behaviors", behaviors);
        writeToFile(folder, "milestone_necromancer.json", json);
    }
    
    private static void writeToFile(File folder, String filename, JsonObject json) {
        File file = new File(folder, filename);
        try (FileWriter writer = new FileWriter(file)) {
            GSON.toJson(json, writer);
            System.out.println("✓ Created: " + filename);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
