Jan 1 сар өмнө
parent
commit
c6457506d0
34 өөрчлөгдсөн 721 нэмэгдсэн , 209 устгасан
  1. 2 2
      pom.xml
  2. 18 0
      src/main/java/me/lethunderhawk/custom/block/RegeneratingBlockListener.java
  3. 166 0
      src/main/java/me/lethunderhawk/custom/block/registry/BlockRegistry.java
  4. 17 13
      src/main/java/me/lethunderhawk/custom/item/CustomItemModule.java
  5. 14 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityDefinition.java
  6. 10 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemVisualDefinition.java
  7. 9 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstance.java
  8. 2 4
      src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstanceReader.java
  9. 0 12
      src/main/java/me/lethunderhawk/custom/item/abstraction/migration/ItemMigration.java
  10. 68 12
      src/main/java/me/lethunderhawk/custom/item/abstraction/migration/MigrationService.java
  11. 3 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/registry/CustomItemRegistry.java
  12. 27 13
      src/main/java/me/lethunderhawk/custom/item/abstraction/visual/ItemRenderer.java
  13. 10 0
      src/main/java/me/lethunderhawk/custom/item/command/CustomItemCommand.java
  14. 171 153
      src/main/java/me/lethunderhawk/custom/item/concrete/ability/HyperionAbility.java
  15. 42 0
      src/main/java/me/lethunderhawk/custom/item/concrete/ability/RegeneratingBlockAbility.java
  16. 10 0
      src/main/java/me/lethunderhawk/custom/item/listener/CustomItemListener.java
  17. 14 0
      src/main/resources/custom/items/claim_tool.yml
  18. 18 0
      src/main/resources/custom/items/hyperion.yml
  19. 19 0
      src/main/resources/custom/items/regen_block.yml
  20. 22 0
      src/main/resources/custom/items/rolling_dice.yml
  21. 25 0
      src/main/resources/custom/items/rolling_dice_test.yml
  22. 9 0
      src/main/resources/rooms/T_connection/room_T_north_east_south.json
  23. BIN
      src/main/resources/rooms/T_connection/room_T_north_east_south.schem
  24. 9 0
      src/main/resources/rooms/hubs/room_hub.json
  25. BIN
      src/main/resources/rooms/hubs/room_hub.schem
  26. 9 0
      src/main/resources/rooms/single_entrance/room_single_entrance.json
  27. BIN
      src/main/resources/rooms/single_entrance/room_single_entrance.schem
  28. 9 0
      src/main/resources/rooms/some_room.json
  29. BIN
      src/main/resources/rooms/some_room.schem
  30. BIN
      src/main/resources/rooms/start_room.schem
  31. 9 0
      src/main/resources/rooms/straight/room_walkthrough_north_south.json
  32. BIN
      src/main/resources/rooms/straight/room_walkthrough_north_south.schem
  33. 9 0
      src/main/resources/rooms/turn_left/room_turn_north_east.json
  34. BIN
      src/main/resources/rooms/turn_left/room_turn_north_east.schem

+ 2 - 2
pom.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 

+ 18 - 0
src/main/java/me/lethunderhawk/custom/block/RegeneratingBlockListener.java

@@ -0,0 +1,18 @@
+package me.lethunderhawk.custom.block;
+
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+
+public class RegeneratingBlockListener implements Listener {
+    @EventHandler
+    public void onBlockPlace(BlockPlaceEvent e){
+
+    }
+
+    @EventHandler
+    public void onBlockBreak(BlockBreakEvent e){
+
+    }
+}

+ 166 - 0
src/main/java/me/lethunderhawk/custom/block/registry/BlockRegistry.java

@@ -0,0 +1,166 @@
+package me.lethunderhawk.custom.block.registry;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.main.Main;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class BlockRegistry implements Listener {
+
+    private final Map<BlockKey, RegeneratingBlock> blocks = new HashMap<>();
+
+    /* =====================================================
+       REGISTRATION
+       ===================================================== */
+
+    /**
+     * @param block the block to register
+     * @param regenTimeSeconds the time needed before the block should regenerate
+     */
+    public void register(Block block, long regenTimeSeconds) {
+        BlockKey key = BlockKey.from(block);
+        blocks.put(key, new RegeneratingBlock(
+                key,
+                block.getBlockData().clone(),
+                regenTimeSeconds
+        ));
+    }
+
+    public void unregister(Block clicked) {
+        BlockKey key = BlockKey.from(clicked);
+        blocks.remove(key);
+    }
+
+    public boolean contains(Block clicked) {
+        return blocks.containsKey(BlockKey.from(clicked));
+    }
+    /* =====================================================
+       EVENTS
+       ===================================================== */
+
+    @EventHandler(ignoreCancelled = true)
+    public void onBlockBreak(BlockBreakEvent event) {
+        Block block = event.getBlock();
+        RegeneratingBlock data = blocks.get(BlockKey.from(block));
+        if (data == null) return;
+
+        data.onDestroyed();
+    }
+
+    @EventHandler(ignoreCancelled = true)
+    public void onBlockPlace(BlockPlaceEvent event) {
+        Block block = event.getBlockPlaced();
+        RegeneratingBlock data = blocks.get(BlockKey.from(block));
+        if (data == null) return;
+
+        data.onPlaced();
+    }
+
+
+
+    /* =====================================================
+       INTERNAL DATA CLASS
+       ===================================================== */
+
+    private static final class RegeneratingBlock {
+
+        private final BlockKey key;
+        private final BlockData originalData;
+        private final long regenTimeSeconds;
+
+        private BukkitTask regenTask;
+        private boolean broken;
+
+        private RegeneratingBlock(BlockKey key, BlockData originalData, long regenTimeSeconds) {
+            this.key = key;
+            this.originalData = originalData;
+            this.regenTimeSeconds = regenTimeSeconds;
+        }
+
+        /* ---------- lifecycle ---------- */
+
+        void onDestroyed() {
+            if (broken) return;
+            broken = true;
+
+            // hook: destroyed
+            onDestroyLogic();
+
+            regenTask = Bukkit.getScheduler().runTaskLater(
+                    Services.get(Main.class),
+                    this::regenerate,
+                    regenTimeSeconds * 20L
+            );
+        }
+
+        void onPlaced() {
+            if (!broken) return;
+            broken = false;
+
+            // cancel pending regen
+            if (regenTask != null) {
+                regenTask.cancel();
+                regenTask = null;
+            }
+
+            // hook: placed
+            onPlaceLogic();
+        }
+
+        private void regenerate() {
+            Block block = key.getBlock();
+            if (block == null || !block.getType().isAir()) return;
+
+            block.setBlockData(originalData, false);
+            broken = false;
+
+            // hook: regenerated
+            onRegenLogic();
+        }
+
+        /* ---------- hooks ---------- */
+
+        private void onDestroyLogic() {
+
+        }
+
+        private void onPlaceLogic() {
+            // maybe reward player, cancel cooldown, etc.
+        }
+
+        private void onRegenLogic() {
+            // effects, sounds, mining reset, etc.
+        }
+    }
+
+    /* =====================================================
+       BLOCK KEY
+       ===================================================== */
+
+    public record BlockKey(String world, int x, int y, int z) {
+
+        public static BlockKey from(Block block) {
+            return new BlockKey(
+                    block.getWorld().getName(),
+                    block.getX(),
+                    block.getY(),
+                    block.getZ()
+            );
+        }
+
+        public Block getBlock() {
+            World w = Bukkit.getWorld(world);
+            return w == null ? null : w.getBlockAt(x, y, z);
+        }
+    }
+}

+ 17 - 13
src/main/java/me/lethunderhawk/custom/item/CustomItemModule.java

@@ -2,6 +2,7 @@ package me.lethunderhawk.custom.item;
 
 import me.lethunderhawk.bazaarflux.service.Services;
 import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+import me.lethunderhawk.custom.block.registry.BlockRegistry;
 import me.lethunderhawk.custom.item.abstraction.definition.DefaultItemDefinitionValidator;
 import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
 import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinitionLoader;
@@ -9,11 +10,8 @@ import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinitionValidat
 import me.lethunderhawk.custom.item.abstraction.registry.AbilityRegistry;
 import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
 import me.lethunderhawk.custom.item.abstraction.runtime.AbilityDispatchService;
-import me.lethunderhawk.custom.item.concrete.ability.TestAbilityHandler;
 import me.lethunderhawk.custom.item.command.CustomItemCommand;
-import me.lethunderhawk.custom.item.concrete.ability.ClaimToolAbility;
-import me.lethunderhawk.custom.item.concrete.ability.HyperionAbility;
-import me.lethunderhawk.custom.item.concrete.ability.RollingDiceAbility;
+import me.lethunderhawk.custom.item.concrete.ability.*;
 import me.lethunderhawk.custom.item.listener.CustomItemListener;
 import me.lethunderhawk.main.Main;
 import org.bukkit.command.CommandSender;
@@ -22,19 +20,27 @@ import org.bukkit.event.HandlerList;
 import java.io.File;
 import java.util.Map;
 
-public class CustomItemModule extends BazaarFluxModule{
+public class CustomItemModule extends BazaarFluxModule {
     private CustomItemListener listener;
 
     public CustomItemModule() {
     }
 
+    private static void registerAbilities(AbilityRegistry abilityRegistry) {
+        abilityRegistry.register("test_handler", new TestAbilityHandler());
+        abilityRegistry.register("rolling_dice", new RollingDiceAbility());
+        abilityRegistry.register("claim_tool", new ClaimToolAbility());
+        abilityRegistry.register("hyperion", new HyperionAbility());
+        abilityRegistry.register("regenerating_block", new RegeneratingBlockAbility());
+    }
+
     @Override
     public String getPrefix() {
         return "[CustomItem]";
     }
 
     @Override
-    public void onEnable(){
+    public void onEnable() {
         AbilityRegistry abilityRegistry = new AbilityRegistry();
         registerAbilities(abilityRegistry);
 
@@ -55,6 +61,7 @@ public class CustomItemModule extends BazaarFluxModule{
         listener = new CustomItemListener(
                 new AbilityDispatchService(itemRegistry, abilityRegistry)
         );
+
         main.getServer().getPluginManager().registerEvents(listener, main);
 
         // --- Services ---
@@ -62,14 +69,11 @@ public class CustomItemModule extends BazaarFluxModule{
         Services.register(ItemDefinitionLoader.class, loader);
         Services.register(ItemDefinitionValidator.class, validator);
         Services.register(AbilityRegistry.class, abilityRegistry);
+        BlockRegistry blockRegistry = new BlockRegistry();
+        main.getServer().getPluginManager().registerEvents(blockRegistry, main);
+        Services.register(BlockRegistry.class, blockRegistry);
         registerCommands();
     }
-    private static void registerAbilities(AbilityRegistry abilityRegistry) {
-        abilityRegistry.register("test_handler", new TestAbilityHandler());
-        abilityRegistry.register("rolling_dice", new RollingDiceAbility());
-        abilityRegistry.register("claim_tool", new ClaimToolAbility());
-        abilityRegistry.register("hyperion", new HyperionAbility());
-    }
 
     private void registerCommands() {
         CustomItemCommand customItemCommand = new CustomItemCommand(this);
@@ -85,7 +89,7 @@ public class CustomItemModule extends BazaarFluxModule{
     }
 
     public void reload(CommandSender sender, String[] strings) {
-        if(sender.hasPermission("customItem.reload")) {
+        if (sender.hasPermission("customItem.reload")) {
             onDisable();
             onEnable();
         }

+ 14 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityDefinition.java

@@ -2,8 +2,10 @@ package me.lethunderhawk.custom.item.abstraction.ability;
 
 import me.lethunderhawk.bazaarflux.util.itemdesign.LoreDesigner;
 import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
 
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -31,6 +33,7 @@ public class AbilityDefinition implements Serializable {
     public String getName(){
         return this.name;
     }
+
     public AbilityTrigger trigger() {
         return trigger;
     }
@@ -39,6 +42,7 @@ public class AbilityDefinition implements Serializable {
         String replacedDescription = getDescriptionWithParams();
         return LoreDesigner.createLore(replacedDescription, firstLine);
     }
+
     public String handlerId() {
         return handlerId;
     }
@@ -46,6 +50,7 @@ public class AbilityDefinition implements Serializable {
     public Map<String, String> getParams() {
         return params;
     }
+
     public String getDescriptionWithParams() {
         String result = description;
         for (Map.Entry<String, String> entry : params.entrySet()) {
@@ -53,6 +58,7 @@ public class AbilityDefinition implements Serializable {
         }
         return result;
     }
+
     public long getCooldownMillis() {
         String cd = params.get("cooldownMillis");
         if (cd == null) return 0L;
@@ -62,4 +68,12 @@ public class AbilityDefinition implements Serializable {
             return 0L;
         }
     }
+
+    public List<Component> renderLore() {
+        List<Component> lore = new ArrayList<>();
+        String abilityName = "Ability: " + getName() + " [" + trigger().getDisplayName() + "]";
+        lore.add(Component.text(abilityName, NamedTextColor.GOLD));
+        lore.addAll(getLore(abilityName));
+        return lore;
+    }
 }

+ 10 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemVisualDefinition.java

@@ -3,6 +3,8 @@ package me.lethunderhawk.custom.item.abstraction.definition;
 import me.lethunderhawk.bazaarflux.util.itemdesign.LoreDesigner;
 import net.kyori.adventure.text.Component;
 import org.bukkit.Material;
+import org.bukkit.inventory.ItemFlag;
+import org.jetbrains.annotations.NotNull;
 
 import java.io.Serializable;
 import java.util.List;
@@ -43,5 +45,13 @@ public final class ItemVisualDefinition implements Serializable {
     public List<Component> loreTemplate() {
         return loreTemplate;
     }
+
+    public boolean isHead() {
+        return headTexture.isPresent();
+    }
+
+    public @NotNull ItemFlag itemFlags() {
+        return ItemFlag.HIDE_ATTRIBUTES;
+    }
 }
 

+ 9 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstance.java

@@ -23,6 +23,15 @@ public final class ItemInstance implements Serializable {
         this.itemId = definition.id();
         this.version = definition.version();
     }
+    public ItemInstance(String itemId, int version) {
+        this.itemId = itemId;
+        this.version = version;
+        this.definition = null;
+    }
+
+    public static ItemInstance unresolved(String itemId, Integer version) {
+        return new ItemInstance(itemId, version);
+    }
 
     public String itemId() {
         return itemId;

+ 2 - 4
src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstanceReader.java

@@ -25,11 +25,9 @@ public final class ItemInstanceReader {
         Integer version = pdc.get(ItemPersistentData.VERSION, PersistentDataType.INTEGER);
 
         if (itemId == null || version == null) return null;
-        CustomItemRegistry registry = Services.get(CustomItemRegistry.class);
-        ItemDefinition definition = registry.get(itemId);
-        if (definition == null) return null;
 
-        ItemInstance instance = new ItemInstance(definition);
+        // IMPORTANT: do NOT require definition here
+        ItemInstance instance = ItemInstance.unresolved(itemId, version);
 
         String json = pdc.get(ItemPersistentData.INSTANCE_DATA, PersistentDataType.STRING);
         if (json != null) {

+ 0 - 12
src/main/java/me/lethunderhawk/custom/item/abstraction/migration/ItemMigration.java

@@ -1,12 +0,0 @@
-package me.lethunderhawk.custom.item.abstraction.migration;
-
-import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
-
-public interface ItemMigration {
-
-    int fromVersion();
-    int toVersion();
-
-    void migrate(ItemInstance instance);
-}
-

+ 68 - 12
src/main/java/me/lethunderhawk/custom/item/abstraction/migration/MigrationService.java

@@ -1,27 +1,83 @@
 package me.lethunderhawk.custom.item.abstraction.migration;
 
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
 import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
-
-import java.util.List;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstanceReader;
+import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
+import me.lethunderhawk.custom.item.abstraction.visual.ItemRenderer;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
 
 public final class MigrationService {
 
-    private final List<ItemMigration> migrations;
+    /**
+     * Migrates a single stack.
+     * Returns the migrated stack or the original if no migration was needed.
+     */
+    public ItemStack migrate(ItemStack original) {
+        if (original == null) return null;
+
+        ItemInstance current = ItemInstanceReader.read(original);
+        if (current == null) return original;
+
+        ItemDefinition latest = Services.get(CustomItemRegistry.class).get(current.itemId());
+        if (latest == null) return original;
+
+        if (current.version() == latest.version()) {
+            return original;
+        }
+
+        // --- Ensure correct material ---
+        ItemStack stack = original;
+        if (!latest.visual().isHead()
+                && stack.getType() != latest.visual().material()) {
+            stack = stack.withType(latest.visual().material());
+        }
+
+        // --- Create migrated instance ---
+        ItemInstance migrated = new ItemInstance(latest);
+        migrated.data().putAll(current.data());
+        migrated.setVersion(latest.version());
 
-    public MigrationService(List<ItemMigration> migrations) {
-        this.migrations = migrations;
+        // --- Render ---
+        ItemRenderer.renderInto(stack, migrated);
+
+        return stack;
     }
 
-    public void migrate(ItemInstance instance, int targetVersion) {
-        for (ItemMigration migration : migrations) {
-            if (instance.version() == migration.fromVersion()) {
-                migration.migrate(instance);
-                instance.setVersion(migration.toVersion());
+    public void migratePlayer(Player player) {
+        ItemStack[] contents = player.getInventory().getContents();
+        boolean changed = false;
+
+        for (int i = 0; i < contents.length; i++) {
+            ItemStack item = contents[i];
+            if (item == null) continue;
+
+            ItemInstance before = ItemInstanceReader.read(item);
+            if (before == null) continue;
+
+            int oldVersion = before.version();
+
+            ItemStack migrated = migrate(item);
+            contents[i] = migrated;
+
+            ItemInstance after = ItemInstanceReader.read(migrated);
+            if (after != null && after.version() != oldVersion) {
+                /*player.sendMessage(
+                        "Migrated " + after.itemId() +
+                                " from v" + oldVersion +
+                                " to v" + after.version()
+                );*/
+                changed = true;
             }
         }
 
-        if (instance.version() != targetVersion) {
-            throw new IllegalStateException("Migration incomplete for " + instance.itemId());
+        if (changed) {
+            player.getInventory().setContents(contents);
+            player.updateInventory();
         }
     }
 }
+
+

+ 3 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/registry/CustomItemRegistry.java

@@ -8,6 +8,9 @@ import java.util.HashMap;
 import java.util.Map;
 
 public class CustomItemRegistry {
+
+    public CustomItemRegistry() {}
+
     private Map<String, ItemDefinition> definitions = new HashMap<>();
 
     public CustomItemRegistry fromRegistry(Map<String, ItemDefinition> definitionsMap) {

+ 27 - 13
src/main/java/me/lethunderhawk/custom/item/abstraction/visual/ItemRenderer.java

@@ -19,39 +19,53 @@ public final class ItemRenderer {
 
     private ItemRenderer() {}
 
-    public static ItemStack render(ItemInstance instance) {
+    public static void renderInto(ItemStack stack, ItemInstance instance) {
         ItemDefinition def = instance.definition();
         ItemVisualDefinition visual = def.visual();
 
-        ItemStack stack = visual.headTexture().isPresent()
-                ? CustomHeadCreator.createCustomHead(visual.headTexture().get())
-                : new ItemStack(visual.material());
-
         ItemMeta meta = stack.getItemMeta();
+        if (meta == null) return;
 
+        // --- Name ---
         meta.displayName(visual.displayName());
 
+        // --- Lore ---
         List<Component> lore = new ArrayList<>();
 
         def.stats().forEach((key, value) ->
-
-                lore.add(Component.text(key + ": " + (value >= 0 ? "+" : "") + value, NamedTextColor.RED))
+                lore.add(Component.text(
+                        key + ": " + (value >= 0 ? "+" : "") + value,
+                        NamedTextColor.RED
+                ))
         );
 
-        def.abilities().forEach(ability -> {
-                    String abilityName = "Ability: " + ability.getName() + " [" + ability.trigger().getDisplayName() + "]";
-                    lore.add(Component.text(abilityName, NamedTextColor.GOLD));
-                    lore.addAll(ability.getLore(abilityName));
-                });
+        def.abilities().forEach(ability ->
+                lore.addAll(ability.renderLore())
+        );
 
         lore.addAll(visual.loreTemplate());
         meta.lore(lore);
 
-        meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES);
+        // --- Flags ---
+        meta.removeItemFlags(ItemFlag.values());
+        meta.addItemFlags(visual.itemFlags());
 
+        // --- Persist instance ---
         ItemPersistentData.write(meta, instance);
 
         stack.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+    }
+
+    public static ItemStack render(ItemInstance instance) {
+        ItemVisualDefinition visual = instance.definition().visual();
+
+        ItemStack stack = visual.isHead()
+                ? CustomHeadCreator.createCustomHead(visual.headTexture().get())
+                : new ItemStack(visual.material());
+
+        renderInto(stack, instance);
         return stack;
     }
 }
+
+

+ 10 - 0
src/main/java/me/lethunderhawk/custom/item/command/CustomItemCommand.java

@@ -6,6 +6,7 @@ import me.lethunderhawk.bazaarflux.util.command.CustomCommand;
 import me.lethunderhawk.custom.item.CustomItemModule;
 import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
 import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+import me.lethunderhawk.custom.item.abstraction.migration.MigrationService;
 import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
 import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
@@ -26,7 +27,16 @@ public class CustomItemCommand extends CustomCommand {
     public void createCommands() {
         rootCommand.registerSubCommand("getAll", "Get all custom items", this::getAllItems);
         rootCommand.registerSubCommand("reload", "Reload", module::reload);
+        rootCommand.registerSubCommand("migrateMe", "Migrate your inventory to the newest version", this::migrateInventory);
     }
+
+    private void migrateInventory(CommandSender sender, String[] strings) {
+        if(!(sender instanceof Player player)) return;
+        if(!sender.hasPermission("custom.items.migrate")) return;
+        new MigrationService().migratePlayer(player);
+        player.sendMessage("Attempted to migrate your inventory to the newest version");
+    }
+
     private void getAllItems(Player player) {
         for (ItemDefinition definition : Services.get(CustomItemRegistry.class).getAll()) {
             ItemInstance instance = new ItemInstance(definition);

+ 171 - 153
src/main/java/me/lethunderhawk/custom/item/concrete/ability/HyperionAbility.java

@@ -1,192 +1,210 @@
 package me.lethunderhawk.custom.item.concrete.ability;
 
-import me.lethunderhawk.bazaarflux.service.Services;
 import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
 import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
 import me.lethunderhawk.custom.item.abstraction.handling.ResolvedParams;
-import me.lethunderhawk.main.Main;
-import org.bukkit.Location;
-import org.bukkit.Particle;
-import org.bukkit.Sound;
-import org.bukkit.World;
-import org.bukkit.entity.LivingEntity;
+import org.bukkit.*;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
 import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitRunnable;
+import org.bukkit.util.RayTraceResult;
 import org.bukkit.util.Vector;
 
 public class HyperionAbility implements AbilityHandler {
-
-    // Configuration constants
-    private static final double DEFAULT_DAMAGE = 5.0;
     private static final double DEFAULT_RANGE = 8.0;
-    private static final double DEFAULT_DAMAGE_RADIUS = 5.0;
-    private static final int DASH_STEPS = 4; // More steps for smoother animation
-    private static final double STEP_INTERVAL = 1.0; // Ticks between steps (20ms)
 
     @Override
     public void execute(AbilityContext context, ResolvedParams params) {
-        double damage = params.getDoubleOrDefault("damage", DEFAULT_DAMAGE);
-        double range = params.getDoubleOrDefault("range", DEFAULT_RANGE);
-        double damageRadius = params.getDoubleOrDefault("damageRadius", DEFAULT_DAMAGE_RADIUS);
-
         Player player = context.player();
+        if (player == null || !player.isOnline()) {
+            return;
+        }
 
-        Location start = player.getLocation().clone().add(0, player.getEyeHeight(true),0);
-        Vector direction = start.getDirection().normalize();
-        World world = player.getWorld();
+        double range = params.getDoubleOrDefault("range", DEFAULT_RANGE);
+        Location currentLoc = player.getEyeLocation();
+
+        // Get the direction the player is looking
+        Vector direction = currentLoc.getDirection().normalize();
+
+        // Perform ray trace to find obstructions
+        RayTraceResult rayTraceResult = player.rayTraceBlocks(range, FluidCollisionMode.NEVER);
+
+        Location targetLocation;
+
+        if (rayTraceResult != null && rayTraceResult.getHitBlock() != null) {
+            Block hitBlock = rayTraceResult.getHitBlock();
+            BlockFace hitFace = rayTraceResult.getHitBlockFace();
+            Vector hitPosition = rayTraceResult.getHitPosition();
+
+            // Handle each face differently
+            if (hitFace == BlockFace.UP) {
+                // Top face - stand on top of the block
+                targetLocation = new Location(
+                        player.getWorld(),
+                        hitBlock.getX() + 0.5,  // Center of block in X
+                        hitBlock.getY() + 1.0,  // Top of block + player height
+                        hitBlock.getZ() + 0.5,  // Center of block in Z
+                        currentLoc.getYaw(),
+                        currentLoc.getPitch()
+                );
 
-        // Find the teleport destination - ALWAYS teleport forward
-        Location target = findTeleportDestination(start, direction, range);
+            } else if (hitFace == BlockFace.DOWN) {
+                // Bottom face - be below the block
+                targetLocation = new Location(
+                        player.getWorld(),
+                        hitBlock.getX() + 0.5,  // Center of block in X
+                        hitBlock.getY() - 1.8,  // Head just below block, feet at blockY - 1.8
+                        hitBlock.getZ() + 0.5,  // Center of block in Z
+                        currentLoc.getYaw(),
+                        currentLoc.getPitch()
+                );
 
-        // Preserve player's original orientation
-        target.setYaw(start.getYaw());
-        target.setPitch(start.getPitch());
+            } else {
+                // Side faces - use the exact hit position and adjust away from face
+                targetLocation = new Location(
+                        player.getWorld(),
+                        hitPosition.getX(),
+                        hitPosition.getY(),
+                        hitPosition.getZ(),
+                        currentLoc.getYaw(),
+                        currentLoc.getPitch()
+                );
 
-        // Perform smooth dash animation
-        smoothDashTeleport(player, start, target, () -> {
-            playExplosionEffect(world, target);
-            damageNearbyEntities(player, target, damageRadius, damage);
-        });
-    }
+                // Move away from the face
+                Vector faceDirection = hitFace.getDirection().normalize();
+                targetLocation.add(faceDirection.multiply(0.3));
 
-    private Location findTeleportDestination(Location start, Vector direction, double maxRange) {
-        // First, try to find a solid block in front of the player
-        Location hitLocation = findSolidBlockHit(start, direction, maxRange);
+                // Find ground level for side teleports
+                int groundY = player.getWorld().getHighestBlockYAt(
+                        targetLocation.getBlockX(),
+                        targetLocation.getBlockZ()
+                );
+                if (groundY > player.getWorld().getMinHeight()) {
+                    targetLocation.setY(groundY + 1.0);
+                }
+            }
 
-        if (hitLocation != null) {
-            // We hit a block, teleport to just in front of it
-            return getPositionInFrontOfBlock(hitLocation, direction);
+            // DEBUG
+            player.sendMessage("§eFace: " + hitFace +
+                    " | Block: " + hitBlock.getX() + "," + hitBlock.getY() + "," + hitBlock.getZ() +
+                    " | Teleport: " + String.format("%.2f,%.2f,%.2f",
+                    targetLocation.getX(), targetLocation.getY(), targetLocation.getZ()));
         } else {
-            // No block hit, teleport to max range
-            return start.clone().add(direction.clone().multiply(maxRange));
+            // No obstruction - teleport exactly in the direction the player is looking
+            // Calculate the exact target position
+            targetLocation = calculateExactTarget(currentLoc, direction, range);
+            targetLocation = currentLoc;
+            // DEBUG: Remove this after testing
+            player.sendMessage("§eTeleporting into air/direction, no block hit");
         }
-    }
-    private Location findSolidBlockHit(Location start, Vector direction, double maxRange) {
-        final double STEP = 0.2;
 
-        for (double distance = STEP; distance <= maxRange; distance += STEP) {
-            Location check = start.clone().add(direction.clone().multiply(distance));
+        // Perform the teleportation
+        boolean teleported = player.teleport(targetLocation);
 
-            // Check if this location contains a solid block
-            if (check.getBlock().getType().isSolid()) {
-                return check;
-            }
-        }
+        if (teleported) {
+            // Optional effects
+            player.playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f);
 
-        return null; // No solid block found
+            // Particle effects
+            spawnTeleportParticles(currentLoc, player.getWorld());
+            spawnTeleportParticles(targetLocation, player.getWorld());
+        }
     }
 
-    private Location getPositionInFrontOfBlock(Location blockHit, Vector direction) {
-        // Move back slightly from the hit block to be in front of it
-        // We use a small offset to ensure we're not inside the block
-        double offset = 0.3;
-
-        // Reverse the direction slightly
-        Vector reverseDir = direction.clone().multiply(-offset);
-
-        // Return position just in front of the block
-        return blockHit.clone().add(reverseDir);
+    // Calculate exact target position when no block is hit
+    private Location calculateExactTarget(Location startLoc, Vector direction, double range) {
+        // Calculate the target position
+        double targetX = startLoc.getX() + (direction.getX() * range);
+        double targetY = startLoc.getY() + (direction.getY() * range);
+        double targetZ = startLoc.getZ() + (direction.getZ() * range);
+
+        return new Location(
+                startLoc.getWorld(),
+                targetX,
+                targetY,
+                targetZ,
+                startLoc.getYaw(),
+                startLoc.getPitch()
+        );
     }
 
-    private void smoothDashTeleport(Player player, Location start, Location target, Runnable onComplete) {
-        Vector startVec = start.toVector();
-        Vector targetVec = target.toVector();
-        Vector totalMovement = targetVec.clone().subtract(startVec);
-
-        // Calculate movement per step
-        Vector stepMovement = totalMovement.clone().multiply(1.0 / DASH_STEPS);
-
-        // Store player's original look direction
-        float originalYaw = start.getYaw();
-        float originalPitch = start.getPitch();
-
-        new BukkitRunnable() {
-            int currentStep = 0;
-            Location currentLocation = start.clone();
-
-            @Override
-            public void run() {
-                if (currentStep > DASH_STEPS) {
-                    // Final teleport to exact target
-                    Location finalLocation = target.clone();
-                    finalLocation.setYaw(originalYaw);
-                    finalLocation.setPitch(originalPitch);
-
-                    player.teleport(finalLocation);
-                    onComplete.run();
-                    cancel();
-                    return;
-                }
-
-                // Calculate next position
-                Vector nextVec = startVec.clone().add(stepMovement.clone().multiply(currentStep));
-                Location nextLocation = new Location(
-                        start.getWorld(),
-                        nextVec.getX(),
-                        nextVec.getY(),
-                        nextVec.getZ(),
-                        originalYaw,
-                        originalPitch
-                );
+    // Helper method to check if a location is safe
+    private boolean isLocationSafe(Location location) {
+        World world = location.getWorld();
+        if (world == null) return false;
 
-                // Teleport to next position
-                player.teleport(nextLocation);
-                currentLocation = nextLocation.clone();
-
-                // Visual and sound effects during dash
-                player.getWorld().spawnParticle(
-                        Particle.ELECTRIC_SPARK,
-                        nextLocation,
-                        3, 0.1, 0.1, 0.1, 0
-                );
+        int x = location.getBlockX();
+        int y = (int) Math.floor(location.getY());
+        int z = location.getBlockZ();
 
-                if (currentStep % 2 == 0) {
-                    player.getWorld().playSound(
-                            nextLocation,
-                            Sound.BLOCK_BEACON_AMBIENT,
-                            0.3f, 2.0f
-                    );
-                }
-
-                currentStep++;
+        // Check the two blocks where player would be (feet and head)
+        for (int i = 0; i <= 1; i++) {
+            Material block = world.getBlockAt(x, y + i, z).getType();
+            if (block.isSolid() && !isPassable(block)) {
+                return false;
             }
-        }.runTaskTimer(Services.get(Main.class), 0L, (long) STEP_INTERVAL);
-    }
-
-    private void playExplosionEffect(World world, Location loc) {
-        world.spawnParticle(Particle.EXPLOSION_EMITTER, loc, 1);
-        world.spawnParticle(Particle.EXPLOSION, loc, 30, 0.5, 0.5, 0.5, 0.05);
-        world.playSound(loc, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 1.0f);
-        //world.spawnParticle(Particle.FLASH, loc, 1);
-
-        // Create a ring of smoke particles
-        for (int i = 0; i < 12; i++) {
-            double angle = 2 * Math.PI * i / 12;
-            double x = Math.cos(angle) * 0.5;
-            double z = Math.sin(angle) * 0.5;
-            world.spawnParticle(Particle.SMOKE,
-                    loc.getX(), loc.getY(), loc.getZ(),
-                    0, x, 0.2, z, 0.1);
         }
-    }
-
-    private void damageNearbyEntities(Player source, Location center, double radius, double damage) {
-        double radiusSquared = radius * radius;
 
-        for (LivingEntity entity : center.getWorld().getNearbyLivingEntities(center, radius, radius, radius)) {
-            if (entity.equals(source)) continue;
-            if (entity.isDead()) continue;
+        // Check if there's something to stand on (not required for air teleportation)
+        // But good to have for when we're near ground
+        Material groundBlock = world.getBlockAt(x, y - 1, z).getType();
+        return !groundBlock.isSolid() || isPassable(groundBlock);
+    }
 
-            if (entity.getLocation().distanceSquared(center) <= radiusSquared) {
-                entity.damage(damage, source);
+    // Check if a material is passable (like water, lava, air)
+    private boolean isPassable(Material material) {
+        return material == Material.AIR ||
+                material == Material.WATER ||
+                material == Material.LAVA ||
+                material == Material.CAVE_AIR ||
+                material == Material.VOID_AIR ||
+                material.name().endsWith("_PLANT") ||
+                material.name().contains("VINE");
+    }
 
-                // Add a small knockback effect
-                Vector knockback = entity.getLocation().toVector()
-                        .subtract(center.toVector())
-                        .normalize()
-                        .multiply(0.5);
-                entity.setVelocity(knockback);
+    // Find safe location near target
+    private Location findSafeLocation(Location target) {
+        World world = target.getWorld();
+        if (world == null) return null;
+
+        int x = target.getBlockX();
+        int y = target.getBlockY();
+        int z = target.getBlockZ();
+
+        // Search in expanding circles around the target
+        for (int radius = 0; radius <= 3; radius++) {
+            for (int dx = -radius; dx <= radius; dx++) {
+                for (int dz = -radius; dz <= radius; dz++) {
+                    // Skip positions outside the current radius
+                    if (Math.abs(dx) < radius && Math.abs(dz) < radius) continue;
+
+                    // Try different Y levels
+                    for (int dy = -2; dy <= 2; dy++) {
+                        Location testLoc = new Location(world, x + dx, y + dy, z + dz);
+                        testLoc.setPitch(target.getPitch());
+                        testLoc.setYaw(target.getYaw());
+
+                        if (isLocationSafe(testLoc)) {
+                            return testLoc;
+                        }
+                    }
+                }
             }
         }
+
+        return null;
+    }
+
+    // Spawn teleport particles
+    private void spawnTeleportParticles(Location location, World world) {
+        if (location == null || world == null) return;
+
+        world.spawnParticle(
+                Particle.PORTAL,
+                location.clone().add(0, 1, 0),
+                30,
+                0.5, 1, 0.5,
+                0.5
+        );
     }
-}
+}

+ 42 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/ability/RegeneratingBlockAbility.java

@@ -0,0 +1,42 @@
+package me.lethunderhawk.custom.item.concrete.ability;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.custom.block.registry.BlockRegistry;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
+import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
+import me.lethunderhawk.custom.item.abstraction.handling.ResolvedParams;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerInteractEvent;
+
+public class RegeneratingBlockAbility implements AbilityHandler {
+    @Override
+    public void execute(AbilityContext context, ResolvedParams params) {
+        PlayerInteractEvent event = ((PlayerInteractEvent) context.event());
+        event.setCancelled(true);
+        Player player = context.player();
+
+        if (!player.isOp()) {
+            player.sendMessage("§cYou are not allowed to use this! How did you even get this item?");
+            return;
+        }
+        if (event.getClickedBlock() == null) {
+            player.sendMessage("§cNo block found to convert!");
+            return;
+        }
+
+        
+        Block clicked = event.getClickedBlock();
+        clicked.setType(clicked.getType(), false);
+        BlockRegistry registry = Services.get(BlockRegistry.class);
+
+
+        if (!registry.contains(clicked)) {
+            registry.register(clicked, params.getInt("regen_time"));
+            player.sendMessage("Turned into regenerating block!");
+        } else {
+            registry.unregister(clicked);
+            player.sendMessage("Turned into a normal block again!");
+        }
+    }
+}

+ 10 - 0
src/main/java/me/lethunderhawk/custom/item/listener/CustomItemListener.java

@@ -1,10 +1,13 @@
 package me.lethunderhawk.custom.item.listener;
 
 import me.lethunderhawk.custom.item.abstraction.ability.AbilityTrigger;
+import me.lethunderhawk.custom.item.abstraction.migration.MigrationService;
 import me.lethunderhawk.custom.item.abstraction.runtime.AbilityDispatchService;
+import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
 import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.event.player.PlayerJoinEvent;
 
 /**
  * Handles events for custom items
@@ -16,6 +19,13 @@ public class CustomItemListener implements Listener {
         this.dispatchService = dispatchService;
     }
 
+    @EventHandler
+    public void onJoin(PlayerJoinEvent event) {
+        Player player = event.getPlayer();
+        new MigrationService().migratePlayer(player);
+    }
+
+
     @EventHandler
     public void onAction(PlayerInteractEvent event) {
         AbilityTrigger triggered = switch (event.getAction()) {

+ 14 - 0
src/main/resources/custom/items/claim_tool.yml

@@ -0,0 +1,14 @@
+id: "claim_tool"
+version: 1
+
+material: GOLDEN_SHOVEL
+
+name: "<blue><bold>Claim Tool"
+
+abilities:
+  select:
+    name: Gamble with a Dice
+    description: <gray>Click on a block to set the first corner of your claim.<gray> <br>The next click will create the claim dedicated to your clan
+    description_width_ref: Click on a block to set the first
+    trigger: LEFT_CLICK_BLOCK
+    handler_id: claim_tool

+ 18 - 0
src/main/resources/custom/items/hyperion.yml

@@ -0,0 +1,18 @@
+id: "hyperion"
+version: 1
+
+material: IRON_SWORD
+
+name: "<red>Hyperion"
+
+abilities:
+  select:
+    name: Sonic Boom
+    description: Dash forward <red>{range}</red> blocks and create a sonic boom on impact, dealing <red>{damage}</red> damage to nearby entities on impact!
+    description_width_ref: Dash forward 7 blocks and
+    trigger: RIGHT_CLICK
+    handler_id: hyperion
+    params:
+      range: 8.0
+      damage: 5.0
+      damageRadius: 5.0

+ 19 - 0
src/main/resources/custom/items/regen_block.yml

@@ -0,0 +1,19 @@
+id: "regenerating_block"
+version: 1
+
+material: PLAYER_HEAD
+
+head:
+  texture: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzE1YjdjMmIwYzgxMDBjYTczODY1YTAzNDBmZjY4ZTQxZDNhOGU5ZTNlZmYxYWY3NjhkMTUzY2QwMjczNTlhZCJ9fX0="
+
+name: "<blue><bold>Regenerating Block"
+
+abilities:
+  place:
+    name: Place a block
+    description: <gray>Turn the clicked block into a <red>regenerating</red> block! The current regeneration period is {regen_time}s
+    description_width_ref: Allows you to gamble away your
+    trigger: RIGHT_CLICK
+    handler_id: regenerating_block
+    params:
+      regen_time: 10

+ 22 - 0
src/main/resources/custom/items/rolling_dice.yml

@@ -0,0 +1,22 @@
+id: "gamblers_dice"
+version: 1
+
+material: PLAYER_HEAD
+
+head:
+  texture: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmFlMTU0YTU3NzE5N2M1NWVkYzVlNTk3NjlmZTg4ZmRhMzY5M2U3ZWM0MDc4ZWQ0M2FjNjk5OGE0ZGY2NGJiOSJ9fX0="
+
+name: "<red><bold>Gamblers Dice"
+
+abilities:
+  gamble:
+    name: Gamble with a Dice
+    description: <gray>Allows you to gamble away your money and potentially your life <br>Costs {cost} flux to roll between 1-6. <br><dark_gray>It feels a bit odd in the hand... <br>Maybe someone tampered with it? <br><br><gray>If you roll a <red>6</red>, you earn <bold>{reward}<reset><gray> flux
+    description_width_ref: Allows you to gamble away your
+    trigger: RIGHT_CLICK
+    handler_id: rolling_dice
+    params:
+      reward: 12000
+      cost: 1000
+      rewardHealth: 40
+      cooldownMillis: 2000

+ 25 - 0
src/main/resources/custom/items/rolling_dice_test.yml

@@ -0,0 +1,25 @@
+id: "test_dice"
+version: 1
+
+material: PLAYER_HEAD
+
+head:
+  texture: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmFlMTU0YTU3NzE5N2M1NWVkYzVlNTk3NjlmZTg4ZmRhMzY5M2U3ZWM0MDc4ZWQ0M2FjNjk5OGE0ZGY2NGJiOSJ9fX0="
+
+name: "<yellow><bold>Test Dice"
+
+stats:
+  strength: 2
+  intelligence: -3
+  penislänge: 10000
+
+abilities:
+  gamble:
+    name: Trigger custom handler
+    description: <gray>This is a custom Ability description, designed to test if the plugin works. You <red>probably dont</red> have to worry about it.
+    trigger: RIGHT_CLICK
+    handler_id: test_handler
+    params:
+      custom_param: "custom string"
+      
+lore: "<dark_gray>Gamble whatever you want"

+ 9 - 0
src/main/resources/rooms/T_connection/room_T_north_east_south.json

@@ -0,0 +1,9 @@
+{
+  "roomType": "REGULAR",
+  "anchor": { "x": 0, "y": 0, "z": 0 },
+  "boundingBox": {
+    "width": 21,
+    "height": 3,
+    "depth": 21
+  }
+}

BIN
src/main/resources/rooms/T_connection/room_T_north_east_south.schem


+ 9 - 0
src/main/resources/rooms/hubs/room_hub.json

@@ -0,0 +1,9 @@
+{
+  "roomType": "REGULAR",
+  "anchor": { "x": 0, "y": 0, "z": 0 },
+  "boundingBox": {
+    "width": 21,
+    "height": 3,
+    "depth": 21
+  }
+}

BIN
src/main/resources/rooms/hubs/room_hub.schem


+ 9 - 0
src/main/resources/rooms/single_entrance/room_single_entrance.json

@@ -0,0 +1,9 @@
+{
+  "roomType": "REGULAR",
+  "anchor": { "x": 0, "y": 0, "z": 0 },
+  "boundingBox": {
+    "width": 21,
+    "height": 3,
+    "depth": 21
+  }
+}

BIN
src/main/resources/rooms/single_entrance/room_single_entrance.schem


+ 9 - 0
src/main/resources/rooms/some_room.json

@@ -0,0 +1,9 @@
+{
+  "roomType": "REGULAR",
+  "anchor": { "x": 0, "y": 0, "z": 0 },
+  "boundingBox": {
+    "width": 21,
+    "height": 3,
+    "depth": 21
+  }
+}

BIN
src/main/resources/rooms/some_room.schem


BIN
src/main/resources/rooms/start_room.schem


+ 9 - 0
src/main/resources/rooms/straight/room_walkthrough_north_south.json

@@ -0,0 +1,9 @@
+{
+  "roomType": "REGULAR",
+  "anchor": { "x": 0, "y": 0, "z": 0 },
+  "boundingBox": {
+    "width": 21,
+    "height": 3,
+    "depth": 21
+  }
+}

BIN
src/main/resources/rooms/straight/room_walkthrough_north_south.schem


+ 9 - 0
src/main/resources/rooms/turn_left/room_turn_north_east.json

@@ -0,0 +1,9 @@
+{
+  "roomType": "REGULAR",
+  "anchor": { "x": 0, "y": 0, "z": 0 },
+  "boundingBox": {
+    "width": 21,
+    "height": 3,
+    "depth": 21
+  }
+}

BIN
src/main/resources/rooms/turn_left/room_turn_north_east.schem