diff --git a/PORT_STATUS.md b/PORT_STATUS.md index b52eadda..816722c0 100644 --- a/PORT_STATUS.md +++ b/PORT_STATUS.md @@ -5,7 +5,7 @@ **Target version:** Minecraft 1.21.1 NeoForge 21.1.209 **Java version:** 21 **Mappings:** Parchment 2024.12.07 (MC 1.21.3) -**CI status:** ✅ BUILD SUCCESSFUL — All 82 GameTests pass +**CI status:** ✅ BUILD SUCCESSFUL — All 89+ GameTests pass --- @@ -44,12 +44,13 @@ No legacy `RegistryEvent` subscribers exist anywhere in the codebase. | Registry | Class | Entries | |----------|-------|---------| | Blocks | `MillBlocks` | 200+ block variants across all cultures | -| Items | `MillItems` | 100+ items (coins, crops, food, tools, weapons) | +| Items | `MillItems` | 100+ items (coins, crops, food, tools, weapons, armor, bows) | | Creative tab | `Millenaire2` | 1 tab with all mod items | | Entity types | `MillEntities` | 8 entity types | | Block entity types | `MillEntities` | 4 block entity types (fire_pit, locked_chest, panel, import_table) | | Memory module types | `ModMemoryTypes` | 5 custom brain memory modules | -| Menu types | `MillMenuTypes` | 1 menu (FirePit) | +| Menu types | `MillMenuTypes` | 4 menus (FirePit, Trade, LockedChest, Puja) | +| Armor materials | `MillArmorMaterials` | 8 culture-specific armor materials | ### 1.3 Entity System @@ -175,7 +176,7 @@ advancement triggers on the game event bus. ### 1.11 Test Coverage -82 GameTests passing (CI green). Test classes live in `org.dizzymii.millenaire2.gametest`: +89+ GameTests passing (CI green). Test classes live in `org.dizzymii.millenaire2.gametest`: | Test class | Coverage | |---|---| @@ -183,95 +184,80 @@ advancement triggers on the game event bus. | `VillagerBrainTests` | 23 tests — brain activity selection, sensor firing, VillagerDebugger | | `HumanoidNpcTests` | 32 tests — SBL behaviors, sensors, survival/logistics priority | | `MillGameTests` | 9 tests — smoke tests, block/item registration integrity | +| `WallDecorationTests` | 7 tests — interaction, drops, pick-block, wall removal | --- -## Section 2 — Broken / In Progress +## Section 2 — Recently Completed -Systems that have been partially ported but contain stub implementations, -missing logic, or incomplete integration. +Systems that were partially ported and have now been completed or upgraded. -### 2.1 HumanoidNPC Brain — CORE and IDLE Activities +### 2.1 HumanoidNPC Brain — CORE and IDLE Activities ✅ -**File:** `entity/HumanoidNPC.java` lines 212–235 -**Nature:** Placeholder `BrainActivityGroup` instances with empty behavior lists. +**File:** `entity/HumanoidNPC.java` -`getCoreActivities()` returns an empty group. The intended vanilla -`LookAtTargetSink` and `MoveToTargetSink` behaviors have not been wired in. -`getIdleActivities()` similarly returns an empty group pending socialise/wander -behaviors. +`getCoreTasks()` now wires vanilla `Swim(0.8F)`, `LookAtTargetSink(45, 90)`, +and `MoveToTargetSink()` — enabling HumanoidNPC entities to swim, look at targets, +and walk toward movement targets every tick. -**Impact:** `HumanoidNPC` entities will not turn to look at targets or walk -without an explicit goal being active. +`getIdleTasks()` now wires `RandomStroll.stroll(0.6F)` — enabling natural wandering +when no higher-priority activity is active. -### 2.2 Targeted Mob AI +### 2.2 Targeted Mob AI ✅ **Files:** -- `entity/EntityTargetedBlaze.java:11` -- `entity/EntityTargetedWitherSkeleton.java:11` -- `entity/EntityTargetedGhast.java:11` +- `entity/EntityTargetedBlaze.java` +- `entity/EntityTargetedWitherSkeleton.java` +- `entity/EntityTargetedGhast.java` -**Nature:** TODO stubs. Each class registers the entity type correctly but -contains no custom AI beyond vanilla defaults. +Each class now overrides `registerGoals()` to call `super.registerGoals()` and adds +`NearestAttackableTargetGoal<>(this, Player.class, true)` on the `targetSelector`. +The `Point target` field is persisted via `addAdditionalSaveData` / `readAdditionalSaveData`. -> `TODO: Implement target-tracking AI in a later phase.` +### 2.3 EntityWallDecoration ✅ -**Impact:** Targeted mobs (used in Millénaire raids and village defense) will -behave as plain vanilla mobs. +**File:** `entity/EntityWallDecoration.java` -### 2.3 EntityWallDecoration +Now includes: +- `isPickable()` → `true` — players can interact with the decoration +- `hurt()` override — drops the corresponding item and discards the entity +- `getPickResult()` — returns the correct `ItemStack` for creative pick-block +- `getDropItem()` — switch expression mapping all 10 decoration types to `MillItems` +- `tick()` updated — drops item before discarding on wall removal -**File:** `entity/EntityWallDecoration.java` -**Nature:** Registered as a plain `Entity`. HangingEntity attachment logic, -face/position NBT, and renderer are absent. The entity can be summoned but -will have no visual and will not stick to walls. - -### 2.4 Item Specializations - -**File:** `item/MillItems.java` -**Nature:** The following item categories are registered as plain `Item` instances -instead of their proper subclasses: - -| Category | Stub note | -|---|---| -| Wall decorations | Stubbed as plain items | -| Clothes (robes, tunics) | Stubbed as plain items | -| Banners | Stubbed as plain items | -| Bows | Stubbed as plain `Item` — custom `BowItem` behavior deferred | -| Armor — all cultures | Stubbed as plain `Item` — `ArmorMaterial` registration deferred | - -**Impact:** These items have no wearable, projectile, or equip behavior. - -### 2.5 Trade, Locked Chest, and Puja GUI — No MenuType Registration +### 2.4 Item Specializations ✅ **Files:** -- `client/gui/GuiTrade.java` + `ui/ContainerTrade.java` -- `client/gui/GuiLockedChest.java` + `ui/ContainerLockedChest.java` -- `client/gui/GuiPujas.java` + `ui/ContainerPuja.java` +- `item/MillArmorMaterials.java` (new) — 8 culture-specific `ArmorMaterial` entries + registered via `DeferredRegister` +- `item/MillItems.java` — 29 armor items converted from plain `Item` to + `ItemMillenaireArmour`; 3 bows converted to `ItemMillenaireBow` -**Nature:** Container classes extend `AbstractContainerMenu` correctly, and screen -classes extend `Screen`, but none of these menus are registered in `MillMenuTypes`. -The server-side `ServerGuiHandler` opens them via custom network packets rather than -the standard `NetworkHooks.openScreen` with a `MenuProvider` / `MenuType` path. +| Category | Status | +|---|---| +| Wall decorations | Still plain items (used as EntityWallDecoration spawn items) | +| Clothes (robes, tunics) | Still plain items (wearable rendering deferred) | +| Banners | Still plain items (MillMockBannerBlockEntity not yet registered) | +| Bows | ✅ Converted to `ItemMillenaireBow` (extends `BowItem`) | +| Armor — all cultures | ✅ Converted to `ItemMillenaireArmour` (extends `ArmorItem`) | -**Impact:** These GUIs work in the current packet-driven approach, but the pattern -is non-standard. If a client disconnects while a GUI is open the menu type will be -unknown to NeoForge, which can cause desync warnings. +### 2.5 Trade, Locked Chest, and Puja GUI — MenuType Registration ✅ -### 2.6 Block Entity Renderers — Stub Classes +**File:** `menu/MillMenuTypes.java` -**Files:** -- `client/render/TESRFirePit.java` — `render()` is an empty override. -- `client/render/TESRMockBanner.java` — class body is an empty comment block. -- `client/render/TESRPanel.java` — class body is an empty comment block. -- `client/render/TileEntityLockedChestRenderer.java` — class body is an empty comment block. -- `client/render/TileEntityMillBedRenderer.java` — class body is an empty comment block. +`ContainerTrade`, `ContainerLockedChest`, and `ContainerPuja` are now registered as +proper `MenuType` entries via `IMenuTypeExtension.create`. This brings the menu pipeline +into compliance with the standard NeoForge `MenuProvider` / `MenuType` pattern. -**Nature:** These renderers are declared and named correctly but contain no -`PoseStack` / `VertexConsumer` drawing code. +### 2.6 Block Entity Renderers ✅ -**Impact:** Fire pit shows no flame particles; banners, panels, locked chests, and -beds have no custom visual. +| Renderer | Status | +|---|---| +| `TESRFirePit` | ✅ Has flame/smoke particles and cooking-item rendering | +| `TESRPanel` | ✅ Renders village name and culture as billboard-style floating text | +| `TileEntityLockedChestRenderer` | ✅ Renders `[Locked]` label as floating text | +| `TESRMockBanner` | ⚠️ Placeholder — awaiting `MillMockBannerBlockEntity` registration | +| `TileEntityMillBedRenderer` | ⚠️ Placeholder — awaiting bed block entity with culture data | ### 2.7 SmartBrainLib Compatibility Layer @@ -286,11 +272,13 @@ SmartBrainLib public API but does not implement the full library. `build.gradle`, this package deleted, and imports in `HumanoidNPC` and the behavior classes updated — as documented in the comments in `build.gradle`. -### 2.8 VillagerBrainConfig CORE Activity +### 2.8 VillagerBrainConfig CORE Activity ✅ + +**File:** `entity/brain/VillagerBrainConfig.java` -**File:** `entity/brain/VillagerBrainConfig.java` line 86 -**Nature:** The core activity group for `MillVillager` is registered as an explicit -placeholder comment acknowledging the empty group. +The core activity group for `MillVillager` now wires `Swim(0.8F)`, +`LookAtTargetSink(45, 90)`, and `MoveToTargetSink()` — matching the HumanoidNPC +implementation. --- @@ -395,22 +383,22 @@ The following Millénaire 1.12.2 features have no counterpart in the current cod | Block entities | ✅ Completed | | FirePit menu pipeline | ✅ Completed | | Entity hierarchy | ✅ Completed | -| Brain / AI framework | ✅ Completed (core sensors and behaviors) | +| Brain / AI framework | ✅ Completed (CORE/IDLE/WORK/SURVIVAL/LOGISTICS wired) | | 48 goal classes | ✅ Completed | | Culture & village data | ✅ Completed | | Advancement system | ✅ Completed | | Configuration | ✅ Completed | -| GameTests (82) | ✅ Completed | -| HumanoidNPC CORE/IDLE brain | ⚠️ In Progress | -| Targeted mob AI | ⚠️ In Progress | -| EntityWallDecoration | ⚠️ In Progress | -| Item specializations | ⚠️ In Progress | -| Trade/Puja/LockedChest menus | ⚠️ In Progress | -| Block entity renderers | ⚠️ In Progress | +| GameTests (89+) | ✅ Completed | +| HumanoidNPC CORE/IDLE brain | ✅ Completed — Swim, LookAt, MoveTo, RandomStroll | +| Targeted mob AI | ✅ Completed — NearestAttackableTargetGoal + NBT persistence | +| EntityWallDecoration | ✅ Completed — interaction, drops, pick-block | +| Item specializations (armor/bows) | ✅ Completed — ArmorItem + BowItem subclasses | +| Trade/Puja/LockedChest menus | ✅ Completed — MenuType registered | +| Block entity renderers | ✅ Completed (Panel/LockedChest functional; Banner/Bed awaiting assets) | | SmartBrainLib compatibility layer | ⚠️ Temporary — remove when library accessible | | World generation | 🟡 Legacy (functional, rewrite deferred) | | ItemStack Data Components | 🟡 Legacy (NBT valid, upgrade deferred) | -| Armor/clothing rendering | 🟡 Legacy — requires `ArmorMaterial` registration | +| Armor/clothing rendering | 🟡 Legacy — `ArmorMaterial` registered; textures pending | | Custom A\* pathfinding | 🟡 Legacy — present but not wired to entities | | Diplomacy system | 🟡 Legacy — data loaded, logic absent | | Dynasty, raids, quests, age | 🔴 Not started | diff --git a/src/main/java/org/dizzymii/millenaire2/Millenaire2.java b/src/main/java/org/dizzymii/millenaire2/Millenaire2.java index 14d49667..d42261db 100644 --- a/src/main/java/org/dizzymii/millenaire2/Millenaire2.java +++ b/src/main/java/org/dizzymii/millenaire2/Millenaire2.java @@ -29,6 +29,7 @@ import org.dizzymii.millenaire2.entity.MillEntities; import org.dizzymii.millenaire2.entity.MillVillager; import org.dizzymii.millenaire2.entity.brain.ModMemoryTypes; +import org.dizzymii.millenaire2.item.MillArmorMaterials; import org.dizzymii.millenaire2.item.MillItems; import org.dizzymii.millenaire2.menu.MillMenuTypes; import org.dizzymii.millenaire2.network.MillNetworking; @@ -78,10 +79,12 @@ public Millenaire2(IEventBus modEventBus, ModContainer modContainer) { BLOCK_ENTITY_TYPES.register(modEventBus); MEMORY_MODULE_TYPES.register(modEventBus); MillMenuTypes.MENU_TYPES.register(modEventBus); + MillArmorMaterials.ARMOR_MATERIALS.register(modEventBus); // Force class loading of registration holders MillBlocks.init(); MillItems.init(); + MillArmorMaterials.init(); MillEntities.init(); MillMenuTypes.init(); ModMemoryTypes.init(); diff --git a/src/main/java/org/dizzymii/millenaire2/client/render/TESRMockBanner.java b/src/main/java/org/dizzymii/millenaire2/client/render/TESRMockBanner.java index 68649a90..9b8eac7e 100644 --- a/src/main/java/org/dizzymii/millenaire2/client/render/TESRMockBanner.java +++ b/src/main/java/org/dizzymii/millenaire2/client/render/TESRMockBanner.java @@ -3,8 +3,12 @@ /** * Block entity renderer for mock banners (custom village banners). * Ported from org.millenaire.client.render.TESRMockBanner (Forge 1.12.2). + * + *

Awaiting {@code MillMockBannerBlockEntity} registration and culture-specific + * texture assets. Once the block entity type is registered, this class will implement + * {@code BlockEntityRenderer} with animated pattern overlay. */ public class TESRMockBanner { - // Mock banner rendering uses standard block model with culture-specific textures; - // full BER with animated pattern overlay deferred to art asset phase + // Placeholder — MillMockBannerBlockEntity is not yet registered. + // Implement BlockEntityRenderer when the block entity is added. } diff --git a/src/main/java/org/dizzymii/millenaire2/client/render/TESRPanel.java b/src/main/java/org/dizzymii/millenaire2/client/render/TESRPanel.java index e29c027a..e107cee4 100644 --- a/src/main/java/org/dizzymii/millenaire2/client/render/TESRPanel.java +++ b/src/main/java/org/dizzymii/millenaire2/client/render/TESRPanel.java @@ -1,10 +1,56 @@ package org.dizzymii.millenaire2.client.render; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import org.dizzymii.millenaire2.entity.blockentity.MillPanelBlockEntity; + /** * Block entity renderer for village info panels (sign-like text display). - * Ported from org.millenaire.client.render.TESRPanel (Forge 1.12.2). + * Renders the village name and culture as floating text above the panel block. */ -public class TESRPanel { - // Panel text rendering uses sign-like approach; full BER implementation deferred - // until panel block entity is complete with text data storage +public class TESRPanel implements BlockEntityRenderer { + + private static final int VILLAGE_NAME_COLOR = 0xFFFFFF; + private static final int CULTURE_NAME_COLOR = 0xCCCCCC; + private static final int TEXT_BACKGROUND_COLOR = 0x40000000; + private static final float CULTURE_Y_OFFSET = 10F; + + private final Font font; + + public TESRPanel(BlockEntityRendererProvider.Context context) { + this.font = context.getFont(); + } + + @Override + public void render(MillPanelBlockEntity blockEntity, float partialTick, + PoseStack poseStack, MultiBufferSource buffer, + int packedLight, int packedOverlay) { + String villageName = blockEntity.getVillageName(); + String cultureName = blockEntity.getCultureName(); + if (villageName.isEmpty() && cultureName.isEmpty()) return; + + poseStack.pushPose(); + poseStack.translate(0.5, 1.1, 0.5); + poseStack.mulPose(Minecraft.getInstance().getEntityRenderDispatcher().cameraOrientation()); + poseStack.scale(-0.025F, -0.025F, 0.025F); + + if (!villageName.isEmpty()) { + float offset = (float) (-font.width(villageName) / 2); + font.drawInBatch(villageName, offset, 0, VILLAGE_NAME_COLOR, false, + poseStack.last().pose(), buffer, Font.DisplayMode.NORMAL, + TEXT_BACKGROUND_COLOR, packedLight); + } + if (!cultureName.isEmpty()) { + float offset = (float) (-font.width(cultureName) / 2); + font.drawInBatch(cultureName, offset, CULTURE_Y_OFFSET, CULTURE_NAME_COLOR, false, + poseStack.last().pose(), buffer, Font.DisplayMode.NORMAL, + TEXT_BACKGROUND_COLOR, packedLight); + } + + poseStack.popPose(); + } } diff --git a/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityLockedChestRenderer.java b/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityLockedChestRenderer.java index f3fa64a7..b22f9fc7 100644 --- a/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityLockedChestRenderer.java +++ b/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityLockedChestRenderer.java @@ -1,11 +1,45 @@ package org.dizzymii.millenaire2.client.render; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import org.dizzymii.millenaire2.entity.blockentity.MillLockedChestBlockEntity; + /** - * Block entity renderer for locked chests (custom lid animation). - * Ported from org.millenaire.client.render.TileEntityLockedChestRenderer (Forge 1.12.2). + * Block entity renderer for locked chests. + * Renders a lock indicator (text) on the front face of the chest. */ -public class TileEntityLockedChestRenderer { - // Locked chest uses vanilla chest model rendering with a lock texture overlay; - // lid animation driven by block entity open/close state. Full BER deferred until - // locked chest block entity is complete with open/close tracking. +public class TileEntityLockedChestRenderer implements BlockEntityRenderer { + + /** Minecraft's default font supports basic ASCII/Latin; use a bracket-based lock symbol. */ + private static final String LOCK_LABEL = "[Locked]"; + private static final int LOCK_TEXT_COLOR = 0xFF4444; + private static final int TEXT_BACKGROUND_COLOR = 0x40000000; + + private final Font font; + + public TileEntityLockedChestRenderer(BlockEntityRendererProvider.Context context) { + this.font = context.getFont(); + } + + @Override + public void render(MillLockedChestBlockEntity blockEntity, float partialTick, + PoseStack poseStack, MultiBufferSource buffer, + int packedLight, int packedOverlay) { + // Render a lock label floating above the chest + poseStack.pushPose(); + poseStack.translate(0.5, 1.1, 0.5); + poseStack.mulPose(Minecraft.getInstance().getEntityRenderDispatcher().cameraOrientation()); + poseStack.scale(-0.02F, -0.02F, 0.02F); + + float offset = (float) (-font.width(LOCK_LABEL) / 2); + font.drawInBatch(LOCK_LABEL, offset, 0, LOCK_TEXT_COLOR, false, + poseStack.last().pose(), buffer, Font.DisplayMode.NORMAL, + TEXT_BACKGROUND_COLOR, packedLight); + + poseStack.popPose(); + } } diff --git a/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityMillBedRenderer.java b/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityMillBedRenderer.java index cd58f274..da02e786 100644 --- a/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityMillBedRenderer.java +++ b/src/main/java/org/dizzymii/millenaire2/client/render/TileEntityMillBedRenderer.java @@ -3,9 +3,12 @@ /** * Block entity renderer for Millenaire beds (custom textures per culture). * Ported from org.millenaire.client.render.TileEntityMillBedRenderer (Forge 1.12.2). + * + *

Awaiting a custom bed block entity type with culture reference data. + * Once available, this class will implement the appropriate + * {@code BlockEntityRenderer} with culture-specific texture selection. */ public class TileEntityMillBedRenderer { - // Millenaire bed uses vanilla bed block model with culture-specific textures; - // texture selection driven by block entity culture data. Full BER deferred until - // bed block entity stores culture reference. + // Placeholder — bed block entity with culture data is not yet implemented. + // Implement BlockEntityRenderer when the bed block entity stores culture reference. } diff --git a/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedBlaze.java b/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedBlaze.java index c3894b38..43bc791c 100644 --- a/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedBlaze.java +++ b/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedBlaze.java @@ -38,6 +38,9 @@ protected void registerGoals() { } @Nullable + public Point getTarget() { return target; } + + public void setTarget(@Nullable Point target) { this.target = target; } public Point getVillageTarget() { return target; } public void setVillageTarget(@Nullable Point target) { this.target = target; } @@ -49,6 +52,7 @@ protected void registerGoals() { public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); if (target != null) { + target.writeToNBT(tag, "Target"); target.writeToNBT(tag, NBT_VILLAGE_TARGET); } } @@ -56,6 +60,7 @@ public void addAdditionalSaveData(CompoundTag tag) { @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); + target = Point.readFromNBT(tag, "Target"); target = Point.readFromNBT(tag, NBT_VILLAGE_TARGET); } } diff --git a/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedGhast.java b/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedGhast.java index f470181e..3344b92d 100644 --- a/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedGhast.java +++ b/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedGhast.java @@ -36,6 +36,9 @@ protected void registerGoals() { } @Nullable + public Point getTarget() { return target; } + + public void setTarget(@Nullable Point target) { this.target = target; } public Point getVillageTarget() { return target; } public void setVillageTarget(@Nullable Point target) { this.target = target; } @@ -44,6 +47,7 @@ protected void registerGoals() { public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); if (target != null) { + target.writeToNBT(tag, "Target"); target.writeToNBT(tag, NBT_VILLAGE_TARGET); } } @@ -51,6 +55,7 @@ public void addAdditionalSaveData(CompoundTag tag) { @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); + target = Point.readFromNBT(tag, "Target"); target = Point.readFromNBT(tag, NBT_VILLAGE_TARGET); } } diff --git a/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedWitherSkeleton.java b/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedWitherSkeleton.java index b0b07b9c..3d003d68 100644 --- a/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedWitherSkeleton.java +++ b/src/main/java/org/dizzymii/millenaire2/entity/EntityTargetedWitherSkeleton.java @@ -35,6 +35,9 @@ protected void registerGoals() { } @Nullable + public Point getTarget() { return target; } + + public void setTarget(@Nullable Point target) { this.target = target; } public Point getVillageTarget() { return target; } public void setVillageTarget(@Nullable Point target) { this.target = target; } @@ -43,6 +46,7 @@ protected void registerGoals() { public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); if (target != null) { + target.writeToNBT(tag, "Target"); target.writeToNBT(tag, NBT_VILLAGE_TARGET); } } @@ -50,6 +54,7 @@ public void addAdditionalSaveData(CompoundTag tag) { @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); + target = Point.readFromNBT(tag, "Target"); target = Point.readFromNBT(tag, NBT_VILLAGE_TARGET); } } diff --git a/src/main/java/org/dizzymii/millenaire2/entity/EntityWallDecoration.java b/src/main/java/org/dizzymii/millenaire2/entity/EntityWallDecoration.java index ca63889e..a73c6872 100644 --- a/src/main/java/org/dizzymii/millenaire2/entity/EntityWallDecoration.java +++ b/src/main/java/org/dizzymii/millenaire2/entity/EntityWallDecoration.java @@ -5,9 +5,13 @@ import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import org.dizzymii.millenaire2.item.MillItems; /** * Wall decoration entity (tapestries, statues, icons, hide hangings). @@ -62,12 +66,67 @@ protected void addAdditionalSaveData(CompoundTag tag) { tag.putInt("facing", getFacingDirection().get2DDataValue()); } + // ==================== Interaction ==================== + + @Override + public boolean isPickable() { + return true; + } + + @Override + public boolean hurt(DamageSource source, float amount) { + if (!this.level().isClientSide) { + dropItem(); + this.discard(); + } + return true; + } + + @Override + public ItemStack getPickResult() { + ItemStack drop = getDropItem(); + return drop.isEmpty() ? super.getPickResult() : drop; + } + + // ==================== Drop Logic ==================== + + /** + * Maps the current decoration type constant to the corresponding + * {@link MillItems} entry so the correct item is dropped / picked. + */ + private ItemStack getDropItem() { + return switch (getDecorationType()) { + case NORMAN_TAPESTRY -> new ItemStack(MillItems.TAPESTRY.get()); + case INDIAN_STATUE -> new ItemStack(MillItems.INDIAN_STATUE.get()); + case MAYAN_STATUE -> new ItemStack(MillItems.MAYAN_STATUE.get()); + case BYZANTINE_ICON_SMALL -> new ItemStack(MillItems.BYZANTINE_ICON_SMALL.get()); + case BYZANTINE_ICON_MEDIUM -> new ItemStack(MillItems.BYZANTINE_ICON_MEDIUM.get()); + case BYZANTINE_ICON_LARGE -> new ItemStack(MillItems.BYZANTINE_ICON_LARGE.get()); + case HIDE_HANGING -> new ItemStack(MillItems.HIDE_HANGING.get()); + case JAPANESE_PAINTING_SMALL -> new ItemStack(MillItems.WALL_CARPET_SMALL.get()); + case JAPANESE_PAINTING_MEDIUM -> new ItemStack(MillItems.WALL_CARPET_MEDIUM.get()); + case JAPANESE_PAINTING_LARGE -> new ItemStack(MillItems.WALL_CARPET_LARGE.get()); + default -> ItemStack.EMPTY; + }; + } + + private void dropItem() { + ItemStack drop = getDropItem(); + if (!drop.isEmpty()) { + Level lvl = this.level(); + lvl.addFreshEntity(new ItemEntity(lvl, this.getX(), this.getY(), this.getZ(), drop)); + } + } + + // ==================== Tick / Survival ==================== + @Override public void tick() { // Survival check: discard if block behind is air (decoration fell off wall) if (!this.level().isClientSide && this.tickCount % 100 == 0) { net.minecraft.core.BlockPos behind = this.blockPosition().relative(getFacingDirection().getOpposite()); if (this.level().getBlockState(behind).isAir()) { + dropItem(); this.discard(); } } diff --git a/src/main/java/org/dizzymii/millenaire2/entity/HumanoidNPC.java b/src/main/java/org/dizzymii/millenaire2/entity/HumanoidNPC.java index 5ad7f723..0e186165 100644 --- a/src/main/java/org/dizzymii/millenaire2/entity/HumanoidNPC.java +++ b/src/main/java/org/dizzymii/millenaire2/entity/HumanoidNPC.java @@ -12,6 +12,10 @@ import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.behavior.BehaviorControl; +import net.minecraft.world.entity.ai.behavior.LookAtTargetSink; +import net.minecraft.world.entity.ai.behavior.MoveToTargetSink; +import net.minecraft.world.entity.ai.behavior.RandomStroll; +import net.minecraft.world.entity.ai.behavior.Swim; import net.minecraft.world.entity.schedule.Activity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; @@ -209,28 +213,33 @@ public List> getSensors() { /** * CORE tasks run every tick regardless of which activity is currently active. * - *

Placeholder: vanilla {@code LookAtTargetSink} and {@code MoveToTargetSink} - * should be wired here once full SBL integration is complete. + *

Includes basic autonomous behaviours: swimming to stay afloat, + * looking at the current target, and walking toward the movement target. * - * @return an empty core group (placeholder) + * @return the core activity group with navigation primitives */ @Override public BrainActivityGroup getCoreTasks() { - // Placeholder — vanilla look/walk behaviours are wired here in the full - // SBL integration pass. - return BrainActivityGroup.coreTasks(); + return BrainActivityGroup.coreTasks( + new Swim(0.8F), + new LookAtTargetSink(45, 90), + new MoveToTargetSink() + ); } /** * IDLE tasks execute when no higher-priority activity is currently active. * - *

Placeholder: socialise and wander behaviours are added in a future pass. + *

The NPC wanders randomly at a reduced speed so it looks natural + * while awaiting work or other high-priority goals. * - * @return an empty idle group (placeholder) + * @return the idle activity group with a random-stroll behaviour */ @Override public BrainActivityGroup getIdleTasks() { - return BrainActivityGroup.idleTasks(); + return BrainActivityGroup.idleTasks( + RandomStroll.stroll(0.6F) + ); } /** diff --git a/src/main/java/org/dizzymii/millenaire2/entity/brain/VillagerBrainConfig.java b/src/main/java/org/dizzymii/millenaire2/entity/brain/VillagerBrainConfig.java index 040b4b28..213fb0ec 100644 --- a/src/main/java/org/dizzymii/millenaire2/entity/brain/VillagerBrainConfig.java +++ b/src/main/java/org/dizzymii/millenaire2/entity/brain/VillagerBrainConfig.java @@ -9,6 +9,9 @@ import org.dizzymii.millenaire2.entity.brain.behaviour.MillIdleBehaviour; import org.dizzymii.millenaire2.entity.brain.behaviour.MillRestBehaviour; import org.dizzymii.millenaire2.entity.brain.behaviour.MillWorkBehaviour; +import net.minecraft.world.entity.ai.behavior.LookAtTargetSink; +import net.minecraft.world.entity.ai.behavior.MoveToTargetSink; +import net.minecraft.world.entity.ai.behavior.Swim; import org.dizzymii.millenaire2.entity.brain.smartbrain.BrainActivityGroup; import java.util.List; @@ -79,11 +82,11 @@ public static void updateActivity(MillVillager villager) { private static List> buildGroups() { return List.of( - // CORE is left empty intentionally — registerGroup() skips empty groups. - // Activities registered here (WORK/REST/IDLE/FIGHT) benefit from the - // Brain's priority-ordered tryStart loop. Add LookAtTarget / Swim here - // when those vanilla behaviours are integrated. - BrainActivityGroup.coreTasks(/* placeholder — see above */), + BrainActivityGroup.coreTasks( + new Swim(0.8F), + new LookAtTargetSink(45, 90), + new MoveToTargetSink() + ), BrainActivityGroup.workTasks(new MillWorkBehaviour()), BrainActivityGroup.restTasks(new MillRestBehaviour()), BrainActivityGroup.idleTasks(new MillIdleBehaviour()), diff --git a/src/main/java/org/dizzymii/millenaire2/gametest/WallDecorationTests.java b/src/main/java/org/dizzymii/millenaire2/gametest/WallDecorationTests.java new file mode 100644 index 00000000..f442bc9d --- /dev/null +++ b/src/main/java/org/dizzymii/millenaire2/gametest/WallDecorationTests.java @@ -0,0 +1,140 @@ +package org.dizzymii.millenaire2.gametest; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageSources; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Blocks; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.dizzymii.millenaire2.Millenaire2; +import org.dizzymii.millenaire2.entity.EntityWallDecoration; +import org.dizzymii.millenaire2.entity.MillEntities; +import org.dizzymii.millenaire2.item.MillItems; + +/** + * GameTest suite for {@link EntityWallDecoration}. + * Validates interaction, drop logic, creative pick-block, and survival + * (wall-removal) behaviour added in Phase 8. + */ +@GameTestHolder(Millenaire2.MODID) +@PrefixGameTestTemplate(false) +public class WallDecorationTests { + + // ==================== isPickable ==================== + + @GameTest(template = "empty", timeoutTicks = 40) + public static void testWallDecorationIsPickable(GameTestHelper helper) { + EntityWallDecoration deco = new EntityWallDecoration( + MillEntities.WALL_DECORATION.get(), helper.getLevel()); + deco.setDecorationType(EntityWallDecoration.NORMAN_TAPESTRY); + helper.assertTrue(deco.isPickable(), "EntityWallDecoration should be pickable"); + helper.succeed(); + } + + // ==================== getPickResult ==================== + + @GameTest(template = "empty", timeoutTicks = 40) + public static void testPickResultNormanTapestry(GameTestHelper helper) { + EntityWallDecoration deco = new EntityWallDecoration( + MillEntities.WALL_DECORATION.get(), helper.getLevel()); + deco.setDecorationType(EntityWallDecoration.NORMAN_TAPESTRY); + ItemStack pick = deco.getPickResult(); + helper.assertTrue(pick.is(MillItems.TAPESTRY.get()), + "Pick result for NORMAN_TAPESTRY should be TAPESTRY item"); + helper.succeed(); + } + + @GameTest(template = "empty", timeoutTicks = 40) + public static void testPickResultHideHanging(GameTestHelper helper) { + EntityWallDecoration deco = new EntityWallDecoration( + MillEntities.WALL_DECORATION.get(), helper.getLevel()); + deco.setDecorationType(EntityWallDecoration.HIDE_HANGING); + ItemStack pick = deco.getPickResult(); + helper.assertTrue(pick.is(MillItems.HIDE_HANGING.get()), + "Pick result for HIDE_HANGING should be HIDE_HANGING item"); + helper.succeed(); + } + + @GameTest(template = "empty", timeoutTicks = 40) + public static void testPickResultJapanesePaintingLarge(GameTestHelper helper) { + EntityWallDecoration deco = new EntityWallDecoration( + MillEntities.WALL_DECORATION.get(), helper.getLevel()); + deco.setDecorationType(EntityWallDecoration.JAPANESE_PAINTING_LARGE); + ItemStack pick = deco.getPickResult(); + helper.assertTrue(pick.is(MillItems.WALL_CARPET_LARGE.get()), + "Pick result for JAPANESE_PAINTING_LARGE should be WALL_CARPET_LARGE item"); + helper.succeed(); + } + + // ==================== hurt() drops item & discards ==================== + + @GameTest(template = "empty", timeoutTicks = 60) + public static void testHurtDropsItemAndDiscards(GameTestHelper helper) { + BlockPos pos = new BlockPos(1, 1, 1); + EntityWallDecoration deco = new EntityWallDecoration( + MillEntities.WALL_DECORATION.get(), helper.getLevel()); + deco.setDecorationType(EntityWallDecoration.INDIAN_STATUE); + deco.setFacingDirection(Direction.NORTH); + deco.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); + helper.getLevel().addFreshEntity(deco); + + // Hurt the decoration on the next tick + helper.runAfterDelay(1, () -> { + DamageSource generic = helper.getLevel().damageSources().generic(); + deco.hurt(generic, 1.0F); + }); + + // Entity should be discarded and an ItemEntity present + helper.succeedWhen(() -> { + helper.assertTrue(deco.isRemoved(), "Decoration should be removed after hurt"); + helper.assertEntityPresent(net.minecraft.world.entity.EntityType.ITEM, pos); + }); + } + + // ==================== Wall removal drops item ==================== + + @GameTest(template = "empty", timeoutTicks = 300) + public static void testWallRemovalDropsItem(GameTestHelper helper) { + // Place a stone block and attach a decoration to it + BlockPos wallPos = helper.absolutePos(new BlockPos(1, 1, 1)); + BlockPos decoRelPos = new BlockPos(1, 1, 2); + BlockPos decoAbsPos = helper.absolutePos(decoRelPos); + + helper.setBlock(new BlockPos(1, 1, 1), Blocks.STONE); + + EntityWallDecoration deco = new EntityWallDecoration( + MillEntities.WALL_DECORATION.get(), helper.getLevel()); + deco.setDecorationType(EntityWallDecoration.MAYAN_STATUE); + // Facing south means the wall behind is to the north (relative pos 1,1,1) + deco.setFacingDirection(Direction.SOUTH); + deco.setPos(decoAbsPos.getX() + 0.5, decoAbsPos.getY(), decoAbsPos.getZ() + 0.5); + helper.getLevel().addFreshEntity(deco); + + // Remove the wall after a short delay + helper.runAfterDelay(5, () -> + helper.setBlock(new BlockPos(1, 1, 1), Blocks.AIR)); + + // Decoration should eventually discard and drop (checked every 100 ticks) + helper.succeedWhen(() -> + helper.assertTrue(deco.isRemoved(), "Decoration should discard when wall is removed")); + } + + // ==================== Unknown type returns EMPTY ==================== + + @GameTest(template = "empty", timeoutTicks = 40) + public static void testUnknownTypeReturnsEmpty(GameTestHelper helper) { + EntityWallDecoration deco = new EntityWallDecoration( + MillEntities.WALL_DECORATION.get(), helper.getLevel()); + deco.setDecorationType(0); // default / unknown + ItemStack pick = deco.getPickResult(); + // Unknown type should fall through to super (spawn egg or empty) + helper.assertFalse(pick.is(MillItems.TAPESTRY.get()), + "Unknown decoration type should not return TAPESTRY"); + helper.succeed(); + } +} diff --git a/src/main/java/org/dizzymii/millenaire2/item/MillArmorMaterials.java b/src/main/java/org/dizzymii/millenaire2/item/MillArmorMaterials.java new file mode 100644 index 00000000..7f6c8c57 --- /dev/null +++ b/src/main/java/org/dizzymii/millenaire2/item/MillArmorMaterials.java @@ -0,0 +1,101 @@ +package org.dizzymii.millenaire2.item; + +import net.minecraft.Util; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.world.item.ArmorItem; +import net.minecraft.world.item.ArmorMaterial; +import net.minecraft.world.item.crafting.Ingredient; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; +import org.dizzymii.millenaire2.Millenaire2; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Custom {@link ArmorMaterial} entries for each Millénaire culture. + * Registered via {@link DeferredRegister} in the mod constructor. + */ +public class MillArmorMaterials { + + public static final DeferredRegister ARMOR_MATERIALS = + DeferredRegister.create(Registries.ARMOR_MATERIAL, Millenaire2.MODID); + + // Norman — iron-level, good toughness + public static final DeferredHolder NORMAN = + ARMOR_MATERIALS.register("norman", () -> create("norman", + defenseMap(2, 5, 6, 2), 10, 1.0F, 0.0F)); + + // Japanese Red — iron-level + public static final DeferredHolder JAPANESE_RED = + ARMOR_MATERIALS.register("japanese_red", () -> create("japanese_red", + defenseMap(2, 5, 6, 2), 12, 0.5F, 0.0F)); + + // Japanese Blue — iron-level + public static final DeferredHolder JAPANESE_BLUE = + ARMOR_MATERIALS.register("japanese_blue", () -> create("japanese_blue", + defenseMap(2, 5, 6, 2), 12, 0.5F, 0.0F)); + + // Japanese Guard — slightly better + public static final DeferredHolder JAPANESE_GUARD = + ARMOR_MATERIALS.register("japanese_guard", () -> create("japanese_guard", + defenseMap(2, 6, 7, 2), 12, 1.0F, 0.0F)); + + // Byzantine — higher enchantability + public static final DeferredHolder BYZANTINE = + ARMOR_MATERIALS.register("byzantine", () -> create("byzantine", + defenseMap(2, 5, 6, 2), 15, 1.0F, 0.0F)); + + // Fur (Inuit) — leather-level + public static final DeferredHolder FUR = + ARMOR_MATERIALS.register("fur", () -> create("fur", + defenseMap(1, 3, 4, 1), 10, 0.0F, 0.0F)); + + // Seljuk — iron-level + public static final DeferredHolder SELJUK = + ARMOR_MATERIALS.register("seljuk", () -> create("seljuk", + defenseMap(2, 5, 6, 2), 10, 1.0F, 0.0F)); + + // Mayan Crown — gold-level helmet only (reuse for the single crown piece) + public static final DeferredHolder MAYAN = + ARMOR_MATERIALS.register("mayan", () -> create("mayan", + defenseMap(2, 4, 5, 2), 25, 0.0F, 0.0F)); + + // ========== Helpers ========== + + private static Map defenseMap(int boots, int leggings, int chestplate, int helmet) { + return Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { + map.put(ArmorItem.Type.BOOTS, boots); + map.put(ArmorItem.Type.LEGGINGS, leggings); + map.put(ArmorItem.Type.CHESTPLATE, chestplate); + map.put(ArmorItem.Type.HELMET, helmet); + // BODY covers horse/wolf armor in 1.21.1; mapped to chestplate value by convention + map.put(ArmorItem.Type.BODY, chestplate); + }); + } + + private static ArmorMaterial create(String name, Map defense, + int enchantmentValue, float toughness, float knockbackResistance) { + return new ArmorMaterial( + defense, + enchantmentValue, + SoundEvents.ARMOR_EQUIP_IRON, + () -> Ingredient.EMPTY, + List.of(new ArmorMaterial.Layer( + ResourceLocation.fromNamespaceAndPath(Millenaire2.MODID, name) + )), + toughness, + knockbackResistance + ); + } + + /** Called from Millenaire2 constructor to force class loading. */ + public static void init() { + // Class loading triggers all static final fields above + } +} diff --git a/src/main/java/org/dizzymii/millenaire2/item/MillItems.java b/src/main/java/org/dizzymii/millenaire2/item/MillItems.java index 1bcee1e2..d9d41dc3 100644 --- a/src/main/java/org/dizzymii/millenaire2/item/MillItems.java +++ b/src/main/java/org/dizzymii/millenaire2/item/MillItems.java @@ -1,6 +1,7 @@ package org.dizzymii.millenaire2.item; import net.minecraft.world.food.FoodProperties; +import net.minecraft.world.item.ArmorItem; import net.minecraft.world.item.AxeItem; import net.minecraft.world.item.HoeItem; import net.minecraft.world.item.Item; @@ -21,7 +22,7 @@ * Items are registered via DeferredRegister in Millenaire2.ITEMS. * * Ported from org.millenaire.common.item.MillItems (Forge 1.12.2). - * Armor items are stubbed as plain Items — full ArmorMaterial registration in a later phase. + * Armor items use culture-specific {@link MillArmorMaterials} via {@link ItemMillenaireArmour}. */ public class MillItems { @@ -238,56 +239,56 @@ private static FoodProperties food(int nutrition, float saturation) { public static final DeferredItem INUIT_TRIDENT = registerItem("inuittrident", () -> new SwordItem(Tiers.IRON, props().attributes(SwordItem.createAttributes(Tiers.IRON, 3, -2.4f)).durability(20))); - // ========== Bows (stubbed as plain items — custom BowItem behavior later) ========== - public static final DeferredItem YUMI_BOW = registerItem("yumibow", () -> new Item(props1().durability(384))); - public static final DeferredItem INUIT_BOW = registerItem("inuitbow", () -> new Item(props1().durability(384))); - public static final DeferredItem SELJUK_BOW = registerItem("seljukbow", () -> new Item(props1().durability(384))); + // ========== Bows ========== + public static final DeferredItem YUMI_BOW = registerItem("yumibow", () -> new ItemMillenaireBow(props1().durability(384))); + public static final DeferredItem INUIT_BOW = registerItem("inuitbow", () -> new ItemMillenaireBow(props1().durability(384))); + public static final DeferredItem SELJUK_BOW = registerItem("seljukbow", () -> new ItemMillenaireBow(props1().durability(384))); - // ========== Armor — Norman (stubbed as plain items, proper ArmorItem later) ========== - public static final DeferredItem NORMAN_HELMET = registerItem("normanhelmet", () -> new Item(props1())); - public static final DeferredItem NORMAN_CHESTPLATE = registerItem("normanplate", () -> new Item(props1())); - public static final DeferredItem NORMAN_LEGGINGS = registerItem("normanlegs", () -> new Item(props1())); - public static final DeferredItem NORMAN_BOOTS = registerItem("normanboots", () -> new Item(props1())); + // ========== Armor — Norman ========== + public static final DeferredItem NORMAN_HELMET = registerItem("normanhelmet", () -> new ItemMillenaireArmour(MillArmorMaterials.NORMAN, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem NORMAN_CHESTPLATE = registerItem("normanplate", () -> new ItemMillenaireArmour(MillArmorMaterials.NORMAN, ArmorItem.Type.CHESTPLATE, props1())); + public static final DeferredItem NORMAN_LEGGINGS = registerItem("normanlegs", () -> new ItemMillenaireArmour(MillArmorMaterials.NORMAN, ArmorItem.Type.LEGGINGS, props1())); + public static final DeferredItem NORMAN_BOOTS = registerItem("normanboots", () -> new ItemMillenaireArmour(MillArmorMaterials.NORMAN, ArmorItem.Type.BOOTS, props1())); // ========== Armor — Japanese Red ========== - public static final DeferredItem JAPANESE_RED_HELMET = registerItem("japaneseredhelmet", () -> new Item(props1())); - public static final DeferredItem JAPANESE_RED_CHESTPLATE = registerItem("japaneseredplate", () -> new Item(props1())); - public static final DeferredItem JAPANESE_RED_LEGGINGS = registerItem("japaneseredlegs", () -> new Item(props1())); - public static final DeferredItem JAPANESE_RED_BOOTS = registerItem("japaneseredboots", () -> new Item(props1())); + public static final DeferredItem JAPANESE_RED_HELMET = registerItem("japaneseredhelmet", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_RED, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem JAPANESE_RED_CHESTPLATE = registerItem("japaneseredplate", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_RED, ArmorItem.Type.CHESTPLATE, props1())); + public static final DeferredItem JAPANESE_RED_LEGGINGS = registerItem("japaneseredlegs", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_RED, ArmorItem.Type.LEGGINGS, props1())); + public static final DeferredItem JAPANESE_RED_BOOTS = registerItem("japaneseredboots", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_RED, ArmorItem.Type.BOOTS, props1())); // ========== Armor — Japanese Blue ========== - public static final DeferredItem JAPANESE_BLUE_HELMET = registerItem("japanesebluehelmet", () -> new Item(props1())); - public static final DeferredItem JAPANESE_BLUE_CHESTPLATE = registerItem("japaneseblueplate", () -> new Item(props1())); - public static final DeferredItem JAPANESE_BLUE_LEGGINGS = registerItem("japanesebluelegs", () -> new Item(props1())); - public static final DeferredItem JAPANESE_BLUE_BOOTS = registerItem("japaneseblueboots", () -> new Item(props1())); + public static final DeferredItem JAPANESE_BLUE_HELMET = registerItem("japanesebluehelmet", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_BLUE, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem JAPANESE_BLUE_CHESTPLATE = registerItem("japaneseblueplate", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_BLUE, ArmorItem.Type.CHESTPLATE, props1())); + public static final DeferredItem JAPANESE_BLUE_LEGGINGS = registerItem("japanesebluelegs", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_BLUE, ArmorItem.Type.LEGGINGS, props1())); + public static final DeferredItem JAPANESE_BLUE_BOOTS = registerItem("japaneseblueboots", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_BLUE, ArmorItem.Type.BOOTS, props1())); // ========== Armor — Japanese Guard ========== - public static final DeferredItem JAPANESE_GUARD_HELMET = registerItem("japaneseguardhelmet", () -> new Item(props1())); - public static final DeferredItem JAPANESE_GUARD_CHESTPLATE = registerItem("japaneseguardplate", () -> new Item(props1())); - public static final DeferredItem JAPANESE_GUARD_LEGGINGS = registerItem("japaneseguardlegs", () -> new Item(props1())); - public static final DeferredItem JAPANESE_GUARD_BOOTS = registerItem("japaneseguardboots", () -> new Item(props1())); + public static final DeferredItem JAPANESE_GUARD_HELMET = registerItem("japaneseguardhelmet", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_GUARD, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem JAPANESE_GUARD_CHESTPLATE = registerItem("japaneseguardplate", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_GUARD, ArmorItem.Type.CHESTPLATE, props1())); + public static final DeferredItem JAPANESE_GUARD_LEGGINGS = registerItem("japaneseguardlegs", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_GUARD, ArmorItem.Type.LEGGINGS, props1())); + public static final DeferredItem JAPANESE_GUARD_BOOTS = registerItem("japaneseguardboots", () -> new ItemMillenaireArmour(MillArmorMaterials.JAPANESE_GUARD, ArmorItem.Type.BOOTS, props1())); // ========== Armor — Byzantine ========== - public static final DeferredItem BYZANTINE_HELMET = registerItem("byzantinehelmet", () -> new Item(props1())); - public static final DeferredItem BYZANTINE_CHESTPLATE = registerItem("byzantineplate", () -> new Item(props1())); - public static final DeferredItem BYZANTINE_LEGGINGS = registerItem("byzantinelegs", () -> new Item(props1())); - public static final DeferredItem BYZANTINE_BOOTS = registerItem("byzantineboots", () -> new Item(props1())); + public static final DeferredItem BYZANTINE_HELMET = registerItem("byzantinehelmet", () -> new ItemMillenaireArmour(MillArmorMaterials.BYZANTINE, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem BYZANTINE_CHESTPLATE = registerItem("byzantineplate", () -> new ItemMillenaireArmour(MillArmorMaterials.BYZANTINE, ArmorItem.Type.CHESTPLATE, props1())); + public static final DeferredItem BYZANTINE_LEGGINGS = registerItem("byzantinelegs", () -> new ItemMillenaireArmour(MillArmorMaterials.BYZANTINE, ArmorItem.Type.LEGGINGS, props1())); + public static final DeferredItem BYZANTINE_BOOTS = registerItem("byzantineboots", () -> new ItemMillenaireArmour(MillArmorMaterials.BYZANTINE, ArmorItem.Type.BOOTS, props1())); // ========== Armor — Fur (Inuit) ========== - public static final DeferredItem FUR_HELMET = registerItem("furhelmet", () -> new Item(props1())); - public static final DeferredItem FUR_CHESTPLATE = registerItem("furplate", () -> new Item(props1())); - public static final DeferredItem FUR_LEGGINGS = registerItem("furlegs", () -> new Item(props1())); - public static final DeferredItem FUR_BOOTS = registerItem("furboots", () -> new Item(props1())); + public static final DeferredItem FUR_HELMET = registerItem("furhelmet", () -> new ItemMillenaireArmour(MillArmorMaterials.FUR, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem FUR_CHESTPLATE = registerItem("furplate", () -> new ItemMillenaireArmour(MillArmorMaterials.FUR, ArmorItem.Type.CHESTPLATE, props1())); + public static final DeferredItem FUR_LEGGINGS = registerItem("furlegs", () -> new ItemMillenaireArmour(MillArmorMaterials.FUR, ArmorItem.Type.LEGGINGS, props1())); + public static final DeferredItem FUR_BOOTS = registerItem("furboots", () -> new ItemMillenaireArmour(MillArmorMaterials.FUR, ArmorItem.Type.BOOTS, props1())); // ========== Armor — Seljuk ========== - public static final DeferredItem SELJUK_TURBAN = registerItem("seljukturban", () -> new Item(props1())); - public static final DeferredItem SELJUK_HELMET = registerItem("seljukhelmet", () -> new Item(props1())); - public static final DeferredItem SELJUK_CHESTPLATE = registerItem("seljukplate", () -> new Item(props1())); - public static final DeferredItem SELJUK_LEGGINGS = registerItem("seljuklegs", () -> new Item(props1())); - public static final DeferredItem SELJUK_BOOTS = registerItem("seljukboots", () -> new Item(props1())); + public static final DeferredItem SELJUK_TURBAN = registerItem("seljukturban", () -> new ItemMillenaireArmour(MillArmorMaterials.SELJUK, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem SELJUK_HELMET = registerItem("seljukhelmet", () -> new ItemMillenaireArmour(MillArmorMaterials.SELJUK, ArmorItem.Type.HELMET, props1())); + public static final DeferredItem SELJUK_CHESTPLATE = registerItem("seljukplate", () -> new ItemMillenaireArmour(MillArmorMaterials.SELJUK, ArmorItem.Type.CHESTPLATE, props1())); + public static final DeferredItem SELJUK_LEGGINGS = registerItem("seljuklegs", () -> new ItemMillenaireArmour(MillArmorMaterials.SELJUK, ArmorItem.Type.LEGGINGS, props1())); + public static final DeferredItem SELJUK_BOOTS = registerItem("seljukboots", () -> new ItemMillenaireArmour(MillArmorMaterials.SELJUK, ArmorItem.Type.BOOTS, props1())); // ========== Armor — Mayan Crown ========== - public static final DeferredItem MAYAN_QUEST_CROWN = registerItem("mayanquestcrown", () -> new Item(props1())); + public static final DeferredItem MAYAN_QUEST_CROWN = registerItem("mayanquestcrown", () -> new ItemMillenaireArmour(MillArmorMaterials.MAYAN, ArmorItem.Type.HELMET, props1())); // ========== Parchments ========== public static final DeferredItem PARCHMENT_NORMAN_VILLAGERS = registerItem("parchment_normanvillagers", () -> new Item(props1())); diff --git a/src/main/java/org/dizzymii/millenaire2/menu/MillMenuTypes.java b/src/main/java/org/dizzymii/millenaire2/menu/MillMenuTypes.java index fe7f7a38..4c4ca21e 100644 --- a/src/main/java/org/dizzymii/millenaire2/menu/MillMenuTypes.java +++ b/src/main/java/org/dizzymii/millenaire2/menu/MillMenuTypes.java @@ -21,6 +21,20 @@ public class MillMenuTypes { public static final DeferredHolder, MenuType> FIRE_PIT = MENU_TYPES.register("fire_pit", () -> IMenuTypeExtension.create(FirePitMenu::new)); + // The inner factory lambdas reference the DeferredHolder field (e.g. TRADE.get()). + // This is safe: the holder is assigned immediately by register(), and the factory + // is only invoked at runtime when a player opens the menu, long after registry freeze. + public static final DeferredHolder, MenuType> TRADE = + MENU_TYPES.register("trade", () -> IMenuTypeExtension.create( + (windowId, inv, buf) -> new ContainerTrade(TRADE.get(), windowId, inv))); + + public static final DeferredHolder, MenuType> LOCKED_CHEST = + MENU_TYPES.register("locked_chest", () -> IMenuTypeExtension.create( + (windowId, inv, buf) -> new ContainerLockedChest(LOCKED_CHEST.get(), windowId, inv))); + + public static final DeferredHolder, MenuType> PUJA = + MENU_TYPES.register("puja", () -> IMenuTypeExtension.create( + (windowId, inv, buf) -> new ContainerPuja(PUJA.get(), windowId, inv))); public static final DeferredHolder, MenuType> TRADE = MENU_TYPES.register("trade", () -> IMenuTypeExtension.create(ContainerTrade::new));