From 9744209938793fcf2e408d64d8534d514ad93c18 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 7 Nov 2025 20:15:49 -0500 Subject: [PATCH 01/35] Refactor mount helpers and expose NPC flight state --- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumPlayerPacket.java | 1 + .../packets/player/SpecialKeyStatePacket.java | 60 ++++ .../npcs/ai/EntityAILeapAtTargetNpc.java | 63 ++++ .../noppes/npcs/ai/EntityAIPounceTarget.java | 12 +- .../java/noppes/npcs/client/ClientProxy.java | 3 + .../noppes/npcs/client/ClientTickHandler.java | 29 ++ .../npcs/client/gui/roles/GuiNpcMount.java | 109 ++++-- .../npcs/controllers/data/PlayerData.java | 9 + .../noppes/npcs/entity/EntityNPCFlying.java | 14 +- .../npcs/entity/EntityNPCInterface.java | 210 +++-------- .../noppes/npcs/entity/EntityNpcDragon.java | 1 + .../noppes/npcs/entity/EntityNpcPony.java | 6 + .../java/noppes/npcs/roles/RoleMount.java | 59 ++- .../java/noppes/npcs/util/NPCMountUtil.java | 335 ++++++++++++++++++ .../assets/customnpcs/lang/cs_CZ.lang | 13 +- .../assets/customnpcs/lang/de_DE.lang | 13 +- .../assets/customnpcs/lang/en_US.lang | 18 +- .../assets/customnpcs/lang/es_AR.lang | 13 +- .../assets/customnpcs/lang/es_MX.lang | 13 +- .../assets/customnpcs/lang/fr_FR.lang | 13 +- .../assets/customnpcs/lang/hu_HU.lang | 13 +- .../assets/customnpcs/lang/id_ID.lang | 13 +- .../assets/customnpcs/lang/ko_KR.lang | 13 +- .../assets/customnpcs/lang/nl_NL.lang | 13 +- .../assets/customnpcs/lang/pl_PL.lang | 13 +- .../assets/customnpcs/lang/pt_BR.lang | 13 +- .../assets/customnpcs/lang/ru_RU.lang | 13 +- .../assets/customnpcs/lang/sv_SE.lang | 13 +- .../assets/customnpcs/lang/zh_CN.lang | 13 +- .../assets/customnpcs/lang/zh_TW.lang | 13 +- 31 files changed, 821 insertions(+), 305 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java create mode 100644 src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java create mode 100644 src/main/java/noppes/npcs/util/NPCMountUtil.java diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 4dcf50bac..745962aba 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -60,6 +60,7 @@ import kamkeel.npcs.network.packets.player.MailActionPacket; import kamkeel.npcs.network.packets.player.SaveBookPacket; import kamkeel.npcs.network.packets.player.SaveSignPacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; +import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import kamkeel.npcs.network.packets.player.TransportPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiButtonPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiClosePacket; @@ -575,6 +576,7 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new InputDevicePacket()); PLAYER_PACKET.registerPacket(new ScreenSizePacket()); + PLAYER_PACKET.registerPacket(new SpecialKeyStatePacket()); PLAYER_PACKET.registerPacket(new SyncRevisionInfoPacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 04d3d3396..0c1db3cd9 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -14,6 +14,7 @@ public enum EnumPlayerPacket { CompanionAction, GetRole, InputDevice, + SpecialKeyState, ScreenSize, MagicCycles, diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java new file mode 100644 index 000000000..3b9f56690 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -0,0 +1,60 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +public final class SpecialKeyStatePacket extends AbstractPacket { + private boolean pressed; + + public SpecialKeyStatePacket() { + } + + private SpecialKeyStatePacket(boolean pressed) { + this.pressed = pressed; + } + + public static void send(boolean pressed) { + PacketClient.sendClient(new SpecialKeyStatePacket(pressed)); + } + + @Override + public Enum getType() { + return EnumPlayerPacket.SpecialKeyState; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeBoolean(pressed); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + boolean keyDown = in.readBoolean(); + if (!(player instanceof EntityPlayerMP)) { + return; + } + + PlayerData data = PlayerDataController.Instance.getPlayerData((EntityPlayerMP) player); + if (data != null) { + data.setSpecialKeyDown(keyDown); + } + } +} diff --git a/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java b/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java new file mode 100644 index 000000000..437dbd6d3 --- /dev/null +++ b/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java @@ -0,0 +1,63 @@ +package noppes.npcs.ai; + +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.ai.EntityAIBase; +import net.minecraft.util.MathHelper; +import noppes.npcs.entity.EntityNPCInterface; + +public class EntityAILeapAtTargetNpc extends EntityAIBase { + private final EntityNPCInterface npc; + private EntityLivingBase leapTarget; + private final float leapMotionY; + + public EntityAILeapAtTargetNpc(EntityNPCInterface npc, float leapMotionY) { + this.npc = npc; + this.leapMotionY = leapMotionY; + this.setMutexBits(5); + } + + @Override + public boolean shouldExecute() { + this.leapTarget = this.npc.getAttackTarget(); + if (this.leapTarget == null) { + return false; + } + double distanceSq = this.npc.getDistanceSqToEntity(this.leapTarget); + if (distanceSq < 4.0D || distanceSq > 16.0D) { + return false; + } + if (!this.npc.onGround) { + return false; + } + return this.npc.getRNG().nextInt(5) == 0; + } + + @Override + public boolean continueExecuting() { + boolean airborne = !this.npc.onGround; + if (!airborne) { + this.npc.setNpcJumpingState(false); + } + return airborne; + } + + @Override + public void startExecuting() { + double d0 = this.leapTarget.posX - this.npc.posX; + double d1 = this.leapTarget.posZ - this.npc.posZ; + float f = MathHelper.sqrt_double(d0 * d0 + d1 * d1); + + if ((double) f >= 1.0E-4D) { + this.npc.motionX += d0 / (double) f * 0.5D * 0.8D + this.npc.motionX * 0.2D; + this.npc.motionZ += d1 / (double) f * 0.5D * 0.8D + this.npc.motionZ * 0.2D; + } + + this.npc.motionY = this.leapMotionY; + this.npc.setNpcJumpingState(true); + } + + @Override + public void resetTask() { + this.npc.setNpcJumpingState(false); + } +} diff --git a/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java b/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java index 5dbe5f233..4f87575bf 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java @@ -30,7 +30,11 @@ public class EntityAIPounceTarget extends EntityAIBase { @Override public boolean continueExecuting() { - return !this.npc.onGround; + boolean airborne = !this.npc.onGround; + if (!airborne) { + this.npc.setNpcJumpingState(false); + } + return airborne; } @Override @@ -47,6 +51,12 @@ public class EntityAIPounceTarget extends EntityAIBase { this.npc.motionX *= this.leapSpeed; this.npc.motionZ *= this.leapSpeed; this.npc.motionY *= this.leapSpeed; + this.npc.setNpcJumpingState(true); + } + + @Override + public void resetTask() { + this.npc.setNpcJumpingState(false); } public float getAngleForXYZ(double varX, double varY, double varZ, double horiDist) { diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index f0346a5d3..7cc3d7b76 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -222,6 +222,7 @@ import java.util.Random; public class ClientProxy extends CommonProxy { public static KeyBinding NPCButton; + public static KeyBinding SpecialKey; public static final Random RAND = new Random(); public static FontContainer Font; @@ -288,8 +289,10 @@ public class ClientProxy extends CommonProxy { Minecraft mc = Minecraft.getMinecraft(); NPCButton = new KeyBinding("NPC Inventory", Keyboard.KEY_N, "key.categories.customnpc"); + SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_C, "key.categories.customnpc"); ClientRegistry.registerKeyBinding(NPCButton); + ClientRegistry.registerKeyBinding(SpecialKey); new PresetController(CustomNpcs.Dir); diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 46af7df1a..af44405dc 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -8,6 +8,7 @@ import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.InputDevicePacket; +import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; import net.minecraft.client.Minecraft; import net.minecraft.entity.Entity; @@ -23,9 +24,12 @@ import noppes.npcs.client.gui.hud.EnumHudComponent; import noppes.npcs.client.gui.hud.HudComponent; import noppes.npcs.client.gui.player.inventory.GuiCNPCInventory; import noppes.npcs.client.renderer.RenderNPCInterface; +import noppes.npcs.constants.EnumRoleType; import noppes.npcs.constants.MarkType; import noppes.npcs.controllers.data.MarkData; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.roles.RoleMount; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import tconstruct.client.tabs.InventoryTabCustomNpc; @@ -42,6 +46,7 @@ public class ClientTickHandler { private int buttonPressed = -1; private long buttonTime = 0L; private final int[] ignoreKeys = new int[]{157, 29, 54, 42, 184, 56, 220, 219}; + private boolean lastSpecialKeyDown = false; @SubscribeEvent(priority = EventPriority.LOWEST) public void onClientTick(TickEvent.ClientTickEvent event) { @@ -52,6 +57,30 @@ public class ClientTickHandler { } this.prevWorld = mc.theWorld; } + if (event.phase == Phase.START) { + EntityPlayer player = mc.thePlayer; + if (player != null) { + boolean specialKeyDown = ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode()); + if (specialKeyDown != lastSpecialKeyDown) { + PlayerData data = CustomNpcs.proxy.getPlayerData(player); + if (data != null) { + data.setSpecialKeyDown(specialKeyDown); + } + SpecialKeyStatePacket.send(specialKeyDown); + lastSpecialKeyDown = specialKeyDown; + } + + if (player.ridingEntity instanceof EntityNPCInterface) { + EntityNPCInterface mount = (EntityNPCInterface) player.ridingEntity; + if (mount.advanced.role == EnumRoleType.Mount && mount.roleInterface instanceof RoleMount) { + RoleMount role = (RoleMount) mount.roleInterface; + if (!role.isSprintAllowed() && player.isSprinting()) { + player.setSprinting(false); + } + } + } + } + } if (event.phase == Phase.END) { if (mc.thePlayer != null && mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) { renderCNPCPlayer.itemRenderer.updateEquippedItem(); diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java index 5f4d228e2..8c63d2c99 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java @@ -13,8 +13,6 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleMount; -import java.util.Locale; - public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener { private final RoleMount role; @@ -32,55 +30,86 @@ public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener return; } - int y = guiTop + 6; - addOffsetRow(0, "role.mount.offsetx", role.getOffsetX(), y); - y += 24; - addOffsetRow(1, "role.mount.offsety", role.getOffsetY(), y); - y += 24; - addOffsetRow(2, "role.mount.offsetz", role.getOffsetZ(), y); - y += 24; - addJumpRow(3, "role.mount.jumpheight", role.getJumpStrength(), y); - y += 24; + int leftLabelX = guiLeft + 4; + int leftFieldX = guiLeft + 140; + int leftY = guiTop + 6; - addLabel(new GuiNpcLabel(5, "role.mount.sprint", guiLeft + 4, y + 5)); - addButton(new GuiNpcButton(5, guiLeft + 140, y, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isSprintAllowed() ? 1 : 0)); + leftY = addOffsetRow(0, 10, "role.mount.offsetx", role.getOffsetX(), leftLabelX, leftFieldX, leftY); + leftY = addOffsetRow(1, 11, "role.mount.offsety", role.getOffsetY(), leftLabelX, leftFieldX, leftY); + leftY = addOffsetRow(2, 12, "role.mount.offsetz", role.getOffsetZ(), leftLabelX, leftFieldX, leftY); + leftY = addJumpRow(3, 13, "role.mount.jumpheight", role.getJumpStrength(), leftLabelX, leftFieldX, leftY); - addButton(new GuiNpcButton(4, guiLeft + 4, y + 30, 80, 20, "role.mount.reset")); + addLabel(new GuiNpcLabel(4, "role.mount.sprint", leftLabelX, leftY + 5)); + addButton(new GuiNpcButton(101, leftFieldX, leftY, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isSprintAllowed() ? 1 : 0)); + leftY += 24; - GuiNpcLabel speedLabel = new GuiNpcLabel(6, "", guiLeft + 4, y + 56); + GuiNpcLabel speedLabel = new GuiNpcLabel(5, "", leftLabelX, leftY + 5); speedLabel.label = StatCollector.translateToLocalFormatted("role.mount.walkspeed", npc.ais.getWalkingSpeed()); addLabel(speedLabel); + + int rightLabelX = guiLeft + 214; + int rightFieldX = guiLeft + 320; + int rightY = guiTop + 6; + + addLabel(new GuiNpcLabel(20, "role.mount.fly", rightLabelX, rightY + 5)); + addButton(new GuiNpcButton(102, rightFieldX, rightY, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isFlyingMountEnabled() ? 1 : 0)); + rightY += 24; + + if (role.isFlyingMountEnabled()) { + addLabel(new GuiNpcLabel(21, "role.mount.hover", rightLabelX, rightY + 5)); + addButton(new GuiNpcButton(103, rightFieldX, rightY, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isHoverModeEnabled() ? 1 : 0)); + rightY += 24; + + rightY = addSpeedRow(22, 14, "role.mount.ascendspeed", role.getFlyingAscendSpeed(), rightLabelX, rightFieldX, rightY, 0.1F, 3.0F, 0.60F); + rightY = addSpeedRow(23, 15, "role.mount.descendspeed", role.getFlyingDescendSpeed(), rightLabelX, rightFieldX, rightY, 0.05F, 3.0F, 0.35F); + } } - private void addOffsetRow(int id, String translationKey, float value, int y) { - addLabel(new GuiNpcLabel(id, translationKey, guiLeft + 4, y + 5)); - GuiNpcTextField field = new GuiNpcTextField(id, this, guiLeft + 140, y, 80, 20, format(value)); + private int addOffsetRow(int labelId, int fieldId, String translationKey, float value, int labelX, int fieldX, int y) { + addLabel(new GuiNpcLabel(labelId, translationKey, labelX, y + 5)); + GuiNpcTextField field = new GuiNpcTextField(fieldId, this, fieldX, y, 80, 20, format(value)); field.floatsOnly = true; field.setMinMaxDefaultFloat(-5.0F, 5.0F, 0.0F); addTextField(field); + return y + 24; } - private void addJumpRow(int id, String translationKey, float value, int y) { - addLabel(new GuiNpcLabel(id, translationKey, guiLeft + 4, y + 5)); - GuiNpcTextField field = new GuiNpcTextField(id, this, guiLeft + 140, y, 80, 20, format(value)); + private int addJumpRow(int labelId, int fieldId, String translationKey, float value, int labelX, int fieldX, int y) { + addLabel(new GuiNpcLabel(labelId, translationKey, labelX, y + 5)); + GuiNpcTextField field = new GuiNpcTextField(fieldId, this, fieldX, y, 80, 20, format(value)); field.floatsOnly = true; field.setMinMaxDefaultFloat(0.1F, 15.0F, 1.0F); addTextField(field); + return y + 24; + } + + private int addSpeedRow(int labelId, int fieldId, String translationKey, float value, int labelX, int fieldX, int y, float min, float max, float defaultValue) { + addLabel(new GuiNpcLabel(labelId, translationKey, labelX, y + 5)); + GuiNpcTextField field = new GuiNpcTextField(fieldId, this, fieldX, y, 80, 20, format(value)); + field.floatsOnly = true; + field.setMinMaxDefaultFloat(min, max, defaultValue); + addTextField(field); + return y + 24; } private String format(float value) { - return String.format(Locale.ROOT, "%.2f", value); + return String.format(java.util.Locale.ROOT, "%.2f", value); } @Override public void buttonEvent(GuiButton guibutton) { if (role == null) return; - if (guibutton.id == 4) { - role.resetOffsets(); - initGui(); - } else if (guibutton.id == 5 && guibutton instanceof GuiNpcButton) { - role.setSprintAllowed(((GuiNpcButton) guibutton).getValue() == 1); + if (guibutton instanceof GuiNpcButton) { + GuiNpcButton button = (GuiNpcButton) guibutton; + if (guibutton.id == 101) { + role.setSprintAllowed(button.getValue() == 1); + } else if (guibutton.id == 102) { + role.setFlyingMountEnabled(button.getValue() == 1); + initGui(); + } else if (guibutton.id == 103) { + role.setHoverModeEnabled(button.getValue() == 1); + } } } @@ -90,32 +119,42 @@ public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener return; float value = textfield.isFloat() ? textfield.getFloat() : 0.0F; switch (textfield.id) { - case 0: + case 10: role.setOffsetX(value); break; - case 1: + case 11: role.setOffsetY(value); break; - case 2: + case 12: role.setOffsetZ(value); break; - case 3: + case 13: role.setJumpStrength(value); break; + case 14: + role.setFlyingAscendSpeed(value); + break; + case 15: + role.setFlyingDescendSpeed(value); + break; } textfield.setText(format(valueForField(textfield.id))); } private float valueForField(int id) { switch (id) { - case 0: + case 10: return role.getOffsetX(); - case 1: + case 11: return role.getOffsetY(); - case 2: + case 12: return role.getOffsetZ(); - case 3: + case 13: return role.getJumpStrength(); + case 14: + return role.getFlyingAscendSpeed(); + case 15: + return role.getFlyingDescendSpeed(); default: return 0.0F; } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 5f6a5e00d..599f81ff1 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -84,6 +84,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public ScreenSize screenSize = new ScreenSize(-1, -1); public int profileSlot = 0; + private boolean specialKeyDown = false; public void onLogin() { // Continue playing animation for self when re-logging @@ -267,6 +268,14 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return this.isGUIOpen; } + public boolean isSpecialKeyDown() { + return specialKeyDown; + } + + public void setSpecialKeyDown(boolean specialKeyDown) { + this.specialKeyDown = specialKeyDown; + } + public ScreenSize getScreenSize() { return screenSize; } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java index 78a7ee23a..2996ca7f3 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java @@ -45,6 +45,7 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { if (handleMountedMovement(p_70612_1_, p_70612_2_)) return; if (!this.canFly() || this.hurtTime != 0 || !this.canBreathe()) { + setNpcFlyingState(false); super.moveEntityWithHeading(p_70612_1_, p_70612_2_); return; } @@ -66,11 +67,13 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { if (!this.getNavigator().noPath() && this.motionY > 0) { this.motionY = 0; } else { + setNpcFlyingState(false); super.moveEntityWithHeading(p_70612_1_, p_70612_2_); return; } } this.flyLimitAllow = true; + setNpcFlyingState(true); double d3 = this.motionY; super.moveEntityWithHeading(p_70612_1_, p_70612_2_); @@ -97,9 +100,10 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { } @Override - protected void doMountedMovement(float strafe, float forward, float moveSpeed) { + public void performMountedMovement(float strafe, float forward, float moveSpeed) { if (!this.canFly() || this.hurtTime != 0 || !this.canBreathe()) { - super.doMountedMovement(strafe, forward, moveSpeed); + setNpcFlyingState(false); + super.performMountedMovement(strafe, forward, moveSpeed); return; } @@ -120,14 +124,16 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { if (!this.getNavigator().noPath() && this.motionY > 0) { this.motionY = 0; } else { - super.doMountedMovement(strafe, forward, moveSpeed); + setNpcFlyingState(false); + super.performMountedMovement(strafe, forward, moveSpeed); return; } } this.flyLimitAllow = true; + setNpcFlyingState(true); double d3 = this.motionY; - super.doMountedMovement(strafe, forward, moveSpeed); + super.performMountedMovement(strafe, forward, moveSpeed); this.motionY = d3; if (this.getNavigator().noPath()) { diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 96310a01e..305a9098b 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -32,7 +32,6 @@ import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.EntityAIAttackOnCollide; import net.minecraft.entity.ai.EntityAIBase; import net.minecraft.entity.ai.EntityAIHurtByTarget; -import net.minecraft.entity.ai.EntityAILeapAtTarget; import net.minecraft.entity.ai.EntityAIOpenDoor; import net.minecraft.entity.ai.EntityAIRestrictSun; import net.minecraft.entity.ai.EntityAISwimming; @@ -101,6 +100,7 @@ import noppes.npcs.ai.EntityAIMoveIndoors; import noppes.npcs.ai.EntityAIMovingPath; import noppes.npcs.ai.EntityAIOrbitTarget; import noppes.npcs.ai.EntityAIPanic; +import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAIPounceTarget; import noppes.npcs.ai.EntityAIRangedAttack; import noppes.npcs.ai.EntityAIReturn; @@ -160,6 +160,7 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; import noppes.npcs.util.GameProfileAlt; +import noppes.npcs.util.NPCMountUtil; import java.io.IOException; import java.util.ArrayList; @@ -211,7 +212,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti public ResourceLocation textureLocation = null; - private Entity lastRider; + private final NPCMountUtil.MountState mountState = new NPCMountUtil.MountState(); + private static final int NPC_STATE_FLAG_FLYING = 1; + private static final int NPC_STATE_FLAG_JUMPING = 1 << 1; public EnumAnimation currentAnimation = EnumAnimation.NONE; @@ -282,6 +285,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.dataWatcher.addObject(14, Integer.valueOf(0)); // Animation this.dataWatcher.addObject(15, Integer.valueOf(0)); // isWalking this.dataWatcher.addObject(16, ""); // Role + this.dataWatcher.addObject(17, Byte.valueOf((byte) 0)); // Mount state flags } protected boolean isAIEnabled() { @@ -967,7 +971,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti */ public void setLeapTask() { if (this.ais.leapType == 1) - this.tasks.addTask(this.taskCount++, aiLeap = new EntityAILeapAtTarget(this, 0.4F)); + this.tasks.addTask(this.taskCount++, aiLeap = new EntityAILeapAtTargetNpc(this, 0.4F)); if (this.ais.leapType == 2) this.tasks.addTask(this.taskCount++, aiLeap = new EntityAIPounceTarget(this)); } @@ -1486,13 +1490,14 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti delete(); } else { if (this.riddenByEntity != null) { - stabilizeDismountedRider(this.riddenByEntity); + NPCMountUtil.stabilizeDismountedRider(this.riddenByEntity); this.riddenByEntity.mountEntity(null); - haltMountedMotion(); + NPCMountUtil.haltMountedMotion(this, mountState); } if (this.ridingEntity != null) this.mountEntity(null); - lastRider = null; + mountState.lastRider = null; + NPCMountUtil.resetMountedFlightState(this, mountState); setHealth(-1); setSprinting(false); getNavigator().clearPathEntity(); @@ -1623,7 +1628,32 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } public boolean isFlying() { - return isWalking() && !onGround; + return isNpcFlying(); + } + + public boolean isNpcFlying() { + return (dataWatcher.getWatchableObjectByte(17) & NPC_STATE_FLAG_FLYING) != 0; + } + + public boolean isNpcJumping() { + return (dataWatcher.getWatchableObjectByte(17) & NPC_STATE_FLAG_JUMPING) != 0; + } + + private void setNpcStateFlag(int mask, boolean value) { + byte current = dataWatcher.getWatchableObjectByte(17); + byte updated = value ? (byte) (current | mask) : (byte) (current & ~mask); + if (current != updated) { + dataWatcher.updateObject(17, updated); + } + } + + public void setNpcFlyingState(boolean flying) { + setNpcStateFlag(NPC_STATE_FLAG_FLYING, flying); + } + + public void setNpcJumpingState(boolean jumping) { + setNpcStateFlag(NPC_STATE_FLAG_JUMPING, jumping); + this.isJumping = jumping; } public void setBoolFlag(boolean bo, int id) { @@ -1872,9 +1902,12 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } @Override - protected void fall(float par1) { + protected void fall(float distance) { + if (NPCMountUtil.isFlyingMountWithFlightEnabled(this)) { + return; + } if (!this.stats.noFallDamage) - super.fall(par1); + super.fall(distance); } @Override @@ -2089,57 +2122,10 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } protected boolean handleMountedMovement(float strafe, float forward) { - if (advanced.role != EnumRoleType.Mount || !(roleInterface instanceof RoleMount)) - return false; - if (riddenByEntity != null && !(riddenByEntity instanceof EntityPlayer)) { - riddenByEntity.mountEntity(null); - return false; - } - if (!(riddenByEntity instanceof EntityPlayer)) - return false; - - RoleMount mount = (RoleMount) roleInterface; - EntityPlayer rider = (EntityPlayer) riddenByEntity; - this.prevRotationYaw = this.rotationYaw = rider.rotationYaw; - this.rotationPitch = rider.rotationPitch * 0.5F; - this.setRotation(this.rotationYaw, this.rotationPitch); - this.renderYawOffset = this.rotationYaw; - this.rotationYawHead = this.rotationYaw; - - float controlledStrafe = rider.moveStrafing * 0.5F; - float controlledForward = rider.moveForward; - if (controlledForward <= 0.0F) { - controlledForward *= 0.25F; - } - - float moveSpeed = Math.max(0.1F, getSpeed()); - boolean riderSprinting = rider.isSprinting() && mount.isSprintAllowed(); - if (riderSprinting) { - moveSpeed *= 1.2F; - } - - if (!mount.isSprintAllowed() && rider.isSprinting()) { - rider.setSprinting(false); - } - - this.setSprinting(riderSprinting); - this.stepHeight = 1.0F; - this.jumpMovementFactor = moveSpeed * 0.1F; - - this.getNavigator().clearPathEntity(); - this.setAttackTarget(null); - this.setRevengeTarget(null); - - this.fallDistance = 0.0F; - rider.fallDistance = 0.0F; - - applyMountedVerticalMotion(rider, moveSpeed, controlledForward); - doMountedMovement(controlledStrafe, controlledForward, moveSpeed); - - return true; + return NPCMountUtil.handleMountedMovement(this, mountState, strafe, forward); } - - protected void doMountedMovement(float strafe, float forward, float moveSpeed) { + + public void performMountedMovement(float strafe, float forward, float moveSpeed) { this.moveStrafing = strafe; this.moveForward = forward; if (!worldObj.isRemote) { @@ -2162,46 +2148,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.limbSwing += this.limbSwingAmount; } - protected void applyMountedVerticalMotion(EntityLivingBase rider, float moveSpeed, float forward) { - if (!(roleInterface instanceof RoleMount)) { - return; - } - RoleMount mount = (RoleMount) roleInterface; - double jumpFactor = MathHelper.clamp_double(mount.getJumpStrength(), 0.1D, 3.0D); - - if (canFly()) { - double verticalBase = Math.max(0.1D, moveSpeed * 0.6D); - double vertical = MathHelper.clamp_double(verticalBase * jumpFactor, 0.05D, 2.0D); - if (rider.isJumping) { - this.motionY = Math.min(this.motionY + vertical, vertical); - this.isAirBorne = true; - this.velocityChanged = true; - } else { - float pitch = rider.rotationPitch; - if (pitch < -10F) { - this.motionY = vertical; - } else if (pitch > 10F) { - this.motionY = -vertical; - } else if (!this.onGround) { - this.motionY *= 0.6D; - } - } - } else if (rider.isJumping && this.onGround) { - double baseVelocity = MathHelper.clamp_double(0.4D + moveSpeed * 1.5D, 0.2D, 1.2D); - double jumpVelocity = MathHelper.clamp_double(baseVelocity * jumpFactor, 0.2D, 2.0D); - this.motionY = jumpVelocity; - this.isAirBorne = true; - this.velocityChanged = true; - if (forward > 0.0F) { - float yawRadians = this.rotationYaw * (float) Math.PI / 180.0F; - double boost = 0.4D * forward; - this.motionX += -MathHelper.sin(yawRadians) * boost; - this.motionZ += MathHelper.cos(yawRadians) * boost; - } - ForgeHooks.onLivingJump(this); - } - } - @Override public boolean allowLeashing() { return false; @@ -2240,71 +2186,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } private void handleMountRiderState() { - if (worldObj.isRemote) { - if (advanced.role != EnumRoleType.Mount || !(roleInterface instanceof RoleMount)) { - lastRider = null; - } - return; - } - - if (advanced.role != EnumRoleType.Mount || !(roleInterface instanceof RoleMount)) { - lastRider = null; - return; - } - - Entity currentRider = this.riddenByEntity; - if (currentRider != null && !(currentRider instanceof EntityPlayer)) { - Entity dismount = currentRider; - dismount.mountEntity(null); - stabilizeDismountedRider(dismount); - haltMountedMotion(); - currentRider = null; - } - if (currentRider != lastRider) { - if (lastRider != null) { - stabilizeDismountedRider(lastRider); - haltMountedMotion(); - } - lastRider = currentRider; - } - } - - private void stabilizeDismountedRider(Entity rider) { - boolean wasOnGround = rider.onGround; - double previousMotionX = rider.motionX; - double previousMotionY = rider.motionY; - double previousMotionZ = rider.motionZ; - float previousFall = rider.fallDistance; - - if (wasOnGround) { - rider.motionX = 0.0D; - rider.motionY = 0.0D; - rider.motionZ = 0.0D; - rider.fallDistance = 0.0F; - rider.onGround = true; - } else { - rider.motionX = previousMotionX; - rider.motionY = previousMotionY; - rider.motionZ = previousMotionZ; - rider.fallDistance = previousFall; - } - - rider.velocityChanged = true; - } - - private void haltMountedMotion() { - this.motionX = 0.0D; - this.motionY = 0.0D; - this.motionZ = 0.0D; - this.velocityChanged = true; - this.isAirBorne = false; - this.setSprinting(false); - this.moveForward = 0.0F; - this.moveStrafing = 0.0F; - this.setAIMoveSpeed(0.0F); - this.limbSwingAmount = 0.0F; - this.limbSwing = 0.0F; - this.getNavigator().clearPathEntity(); + NPCMountUtil.handleMountRiderState(this, mountState); } // Model Types: 0: Steve 64x32, 1: Steve 64x64, 2: Alex 64x64 diff --git a/src/main/java/noppes/npcs/entity/EntityNpcDragon.java b/src/main/java/noppes/npcs/entity/EntityNpcDragon.java index 6f490583f..a9df9da99 100644 --- a/src/main/java/noppes/npcs/entity/EntityNpcDragon.java +++ b/src/main/java/noppes/npcs/entity/EntityNpcDragon.java @@ -94,6 +94,7 @@ public class EntityNpcDragon extends EntityNPCInterface { field_40172_ax += f1 * 0.5F; } super.onLivingUpdate(); + this.isFlying = isNpcFlying(); } // public float ticks = 0.0f; diff --git a/src/main/java/noppes/npcs/entity/EntityNpcPony.java b/src/main/java/noppes/npcs/entity/EntityNpcPony.java index ed6ca617c..81ba41233 100644 --- a/src/main/java/noppes/npcs/entity/EntityNpcPony.java +++ b/src/main/java/noppes/npcs/entity/EntityNpcPony.java @@ -35,4 +35,10 @@ public class EntityNpcPony extends EntityNPCInterface { super.onUpdate(); } + @Override + public void onLivingUpdate() { + super.onLivingUpdate(); + this.isFlying = isNpcFlying(); + } + } diff --git a/src/main/java/noppes/npcs/roles/RoleMount.java b/src/main/java/noppes/npcs/roles/RoleMount.java index a1a07c800..f930a4975 100644 --- a/src/main/java/noppes/npcs/roles/RoleMount.java +++ b/src/main/java/noppes/npcs/roles/RoleMount.java @@ -10,12 +10,13 @@ public class RoleMount extends RoleInterface { private float offsetX; private float offsetY; private float offsetZ; - private static final float MIN_JUMP_STRENGTH = 0.1F; - private static final float MAX_JUMP_STRENGTH = 3.0F; - private boolean storedReturnToStart; private float jumpStrength = 1.0F; private boolean allowSprint = true; + private boolean flyingMountEnabled = false; + private boolean hoverMode = false; + private float flyingAscendSpeed = 0.60F; + private float flyingDescendSpeed = 0.35F; public RoleMount(EntityNPCInterface npc) { super(npc); @@ -31,6 +32,10 @@ public class RoleMount extends RoleInterface { compound.setBoolean("MountReturnFlag", storedReturnToStart); compound.setFloat("MountJumpStrength", jumpStrength); compound.setBoolean("MountAllowSprint", allowSprint); + compound.setBoolean("MountAllowFlying", flyingMountEnabled); + compound.setBoolean("MountHoverMode", hoverMode); + compound.setFloat("MountFlyingAscendSpeed", flyingAscendSpeed); + compound.setFloat("MountFlyingDescendSpeed", flyingDescendSpeed); return compound; } @@ -50,6 +55,20 @@ public class RoleMount extends RoleInterface { jumpStrength = 1.0F; } allowSprint = !compound.hasKey("MountAllowSprint") || compound.getBoolean("MountAllowSprint"); + flyingMountEnabled = compound.hasKey("MountAllowFlying") && compound.getBoolean("MountAllowFlying"); + hoverMode = compound.hasKey("MountHoverMode") && compound.getBoolean("MountHoverMode"); + if (compound.hasKey("MountFlyingAscendSpeed")) { + setFlyingAscendSpeed(compound.getFloat("MountFlyingAscendSpeed")); + } else { + flyingAscendSpeed = 0.60F; + } + if (compound.hasKey("MountFlyingDescendSpeed")) { + setFlyingDescendSpeed(compound.getFloat("MountFlyingDescendSpeed")); + } else if (compound.hasKey("MountFlyingFallSpeed")) { + setFlyingDescendSpeed(compound.getFloat("MountFlyingFallSpeed")); + } else { + flyingDescendSpeed = 0.35F; + } npc.ais.returnToStart = false; } @@ -106,7 +125,7 @@ public class RoleMount extends RoleInterface { } public void setJumpStrength(float value) { - this.jumpStrength = ValueUtil.clamp(value, MIN_JUMP_STRENGTH, MAX_JUMP_STRENGTH); + this.jumpStrength = ValueUtil.clamp(value, 0.1F, 3.0F); } public float getJumpStrength() { @@ -121,6 +140,38 @@ public class RoleMount extends RoleInterface { return allowSprint; } + public void setFlyingMountEnabled(boolean enabled) { + this.flyingMountEnabled = enabled; + } + + public boolean isFlyingMountEnabled() { + return flyingMountEnabled; + } + + public void setHoverModeEnabled(boolean enabled) { + this.hoverMode = enabled; + } + + public boolean isHoverModeEnabled() { + return hoverMode; + } + + public void setFlyingAscendSpeed(float value) { + this.flyingAscendSpeed = ValueUtil.clamp(value, 0.1F, 3.0F); + } + + public float getFlyingAscendSpeed() { + return flyingAscendSpeed; + } + + public void setFlyingDescendSpeed(float value) { + this.flyingDescendSpeed = ValueUtil.clamp(value, 0.05F, 3.0F); + } + + public float getFlyingDescendSpeed() { + return flyingDescendSpeed; + } + public void setReturnToStartPreference(boolean value) { this.storedReturnToStart = value; npc.ais.returnToStart = false; diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java new file mode 100644 index 000000000..fc04248a8 --- /dev/null +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -0,0 +1,335 @@ +package noppes.npcs.util; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.MathHelper; +import noppes.npcs.CustomNpcs; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.roles.RoleMount; + +public final class NPCMountUtil { + private static final int MOUNT_FLIGHT_TOGGLE_WINDOW = 7; + + private NPCMountUtil() { + } + + public static class MountState { + public Entity lastRider; + public boolean flightMode; + public boolean jumpPressed; + public int flightToggleTimer; + } + + public static boolean handleMountedMovement(EntityNPCInterface npc, MountState state, float strafe, float forward) { + if (npc.advanced.role != EnumRoleType.Mount || !(npc.roleInterface instanceof RoleMount)) { + resetMountedFlightState(npc, state); + return false; + } + if (npc.riddenByEntity != null && !(npc.riddenByEntity instanceof EntityPlayer)) { + npc.riddenByEntity.mountEntity(null); + return false; + } + if (!(npc.riddenByEntity instanceof EntityPlayer)) { + resetMountedFlightState(npc, state); + return false; + } + + RoleMount mount = (RoleMount) npc.roleInterface; + EntityPlayer rider = (EntityPlayer) npc.riddenByEntity; + updateMountedFlightState(npc, state, mount, rider); + + npc.prevRotationYaw = npc.rotationYaw = rider.rotationYaw; + npc.rotationPitch = rider.rotationPitch * 0.5F; + npc.setPositionAndRotation(npc.posX, npc.posY, npc.posZ, npc.rotationYaw, npc.rotationPitch); + npc.renderYawOffset = npc.rotationYaw; + npc.rotationYawHead = npc.rotationYaw; + + float controlledStrafe = rider.moveStrafing; + float controlledForward = rider.moveForward; + if (controlledForward <= 0.0F) { + controlledForward *= 0.25F; + } + + float moveSpeed = Math.max(0.0F, npc.getSpeed()); + boolean riderSprinting = rider.isSprinting() && mount.isSprintAllowed(); + if (riderSprinting) { + moveSpeed *= 1.2F; + } + + if (!mount.isSprintAllowed() && rider.isSprinting()) { + rider.setSprinting(false); + } + + if (moveSpeed <= 0.0F) { + controlledStrafe = 0.0F; + controlledForward = 0.0F; + } + + npc.setSprinting(riderSprinting); + npc.stepHeight = 1.0F; + npc.jumpMovementFactor = moveSpeed * 0.1F; + + npc.getNavigator().clearPathEntity(); + npc.setAttackTarget(null); + npc.setRevengeTarget(null); + + npc.fallDistance = 0.0F; + rider.fallDistance = 0.0F; + + applyMountedVerticalMotion(npc, state, mount, rider, moveSpeed, controlledForward); + npc.performMountedMovement(controlledStrafe, controlledForward, moveSpeed); + + return true; + } + + private static void applyMountedVerticalMotion(EntityNPCInterface npc, MountState state, RoleMount mount, EntityLivingBase rider, float moveSpeed, float forward) { + if (mount == null) { + return; + } + double jumpFactor = MathHelper.clamp_double(mount.getJumpStrength(), 0.1D, 3.0D); + boolean flyingEnabled = mount.isFlyingMountEnabled(); + + npc.setNpcJumpingState(false); + + if (flyingEnabled) { + double ascendSpeed = MathHelper.clamp_double(mount.getFlyingAscendSpeed(), 0.1D, 3.0D); + double descendSpeed = MathHelper.clamp_double(mount.getFlyingDescendSpeed(), 0.05D, 3.0D); + boolean flightMode = isMountInFlightMode(state); + boolean jumpPressed = rider.isJumping; + boolean descendPressed = rider instanceof EntityPlayer && isSpecialKeyDown((EntityPlayer) rider); + + if (flightMode) { + npc.motionY = 0.0D; + + if (jumpPressed) { + npc.motionY += ascendSpeed; + } else if (descendPressed) { + npc.motionY -= descendSpeed; + } + + npc.motionY = MathHelper.clamp_double(npc.motionY, -descendSpeed, ascendSpeed); + + npc.setNpcFlyingState(true); + npc.setNpcJumpingState(jumpPressed); + npc.isAirBorne = true; + npc.velocityChanged = true; + npc.fallDistance = 0.0F; + + if (!npc.worldObj.isRemote && rider instanceof EntityPlayer) { + EntityPlayer player = (EntityPlayer) rider; + if (!player.capabilities.isFlying) { + player.motionY = npc.motionY; + player.velocityChanged = true; + } + } + return; + } + + if (jumpPressed && npc.onGround) { + npc.motionY = jumpFactor; + npc.setNpcJumpingState(true); + npc.isAirBorne = true; + npc.velocityChanged = true; + npc.fallDistance = 0.0F; + if (!npc.worldObj.isRemote) { + net.minecraftforge.common.ForgeHooks.onLivingJump(npc); + } + return; + } + + if (!npc.onGround) { + npc.motionY = Math.max(npc.motionY, -descendSpeed); + npc.setNpcFlyingState(!npc.isInWater()); + } else { + npc.setNpcFlyingState(false); + } + + return; + } + + if (npc.onGround) { + if (rider.isJumping) { + npc.motionY = jumpFactor; + npc.setNpcJumpingState(true); + npc.isAirBorne = true; + npc.velocityChanged = true; + npc.fallDistance = 0.0F; + if (!npc.worldObj.isRemote) { + net.minecraftforge.common.ForgeHooks.onLivingJump(npc); + } + } else { + npc.motionY = 0.0D; + } + return; + } + + npc.setNpcFlyingState(false); + } + + public static void handleMountRiderState(EntityNPCInterface npc, MountState state) { + if (npc.worldObj.isRemote) { + if (npc.advanced.role != EnumRoleType.Mount || !(npc.roleInterface instanceof RoleMount)) { + state.lastRider = null; + } + return; + } + + if (npc.advanced.role != EnumRoleType.Mount || !(npc.roleInterface instanceof RoleMount)) { + state.lastRider = null; + return; + } + + RoleMount mount = (RoleMount) npc.roleInterface; + Entity currentRider = npc.riddenByEntity; + if (currentRider != null && !(currentRider instanceof EntityPlayer)) { + Entity dismount = currentRider; + dismount.mountEntity(null); + stabilizeDismountedRider(dismount); + haltMountedMotion(npc, state); + applyUnriddenFlightDescent(npc, state, mount); + currentRider = null; + } + if (currentRider != state.lastRider) { + if (state.lastRider != null) { + stabilizeDismountedRider(state.lastRider); + } + if (currentRider == null) { + haltMountedMotion(npc, state); + applyUnriddenFlightDescent(npc, state, mount); + } else { + resetMountedFlightState(npc, state); + } + state.lastRider = currentRider; + } else if (currentRider == null) { + applyUnriddenFlightDescent(npc, state, mount); + } + } + + public static boolean isFlyingMountWithFlightEnabled(EntityNPCInterface npc) { + return npc.advanced.role == EnumRoleType.Mount && npc.roleInterface instanceof RoleMount && ((RoleMount) npc.roleInterface).isFlyingMountEnabled(); + } + + public static void haltMountedMotion(EntityNPCInterface npc, MountState state) { + npc.motionX = 0.0D; + npc.motionY = 0.0D; + npc.motionZ = 0.0D; + npc.velocityChanged = true; + npc.isAirBorne = false; + npc.setSprinting(false); + npc.moveForward = 0.0F; + npc.moveStrafing = 0.0F; + npc.setAIMoveSpeed(0.0F); + npc.limbSwingAmount = 0.0F; + npc.limbSwing = 0.0F; + npc.getNavigator().clearPathEntity(); + npc.fallDistance = 0.0F; + resetMountedFlightState(npc, state); + } + + public static void applyUnriddenFlightDescent(EntityNPCInterface npc, MountState state, RoleMount mount) { + if (mount == null || !mount.isFlyingMountEnabled()) { + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + return; + } + if (npc.onGround) { + npc.motionY = 0.0D; + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + return; + } + + double descendSpeed = MathHelper.clamp_double(mount.getFlyingDescendSpeed(), 0.05D, 3.0D); + if (descendSpeed <= 0.0D) { + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + return; + } + + double desired = -descendSpeed; + npc.motionY = desired; + npc.isAirBorne = true; + npc.velocityChanged = true; + npc.fallDistance = 0.0F; + npc.setNpcFlyingState(true); + npc.setNpcJumpingState(false); + } + + public static void stabilizeDismountedRider(Entity rider) { + boolean wasOnGround = rider.onGround; + double previousMotionX = rider.motionX; + double previousMotionY = rider.motionY; + double previousMotionZ = rider.motionZ; + float previousFall = rider.fallDistance; + + if (wasOnGround) { + rider.motionX = 0.0D; + rider.motionY = 0.0D; + rider.motionZ = 0.0D; + rider.fallDistance = 0.0F; + rider.onGround = true; + } else { + rider.motionX = previousMotionX; + rider.motionY = previousMotionY; + rider.motionZ = previousMotionZ; + rider.fallDistance = previousFall; + } + + rider.velocityChanged = true; + } + + private static void updateMountedFlightState(EntityNPCInterface npc, MountState state, RoleMount mount, EntityPlayer rider) { + if (mount == null || rider == null || !mount.isFlyingMountEnabled()) { + resetMountedFlightState(npc, state); + return; + } + + if (npc.onGround) { + state.flightMode = false; + } + + boolean jumpPressed = rider.isJumping; + if (jumpPressed && !state.jumpPressed) { + if (state.flightToggleTimer > 0) { + state.flightMode = !state.flightMode; + state.flightToggleTimer = 0; + } else { + state.flightToggleTimer = MOUNT_FLIGHT_TOGGLE_WINDOW; + } + } + + if (state.flightToggleTimer > 0) { + state.flightToggleTimer--; + } + + state.jumpPressed = jumpPressed; + } + + public static boolean isMountInFlightMode(MountState state) { + return state.flightMode; + } + + public static void resetMountedFlightState(EntityNPCInterface npc, MountState state) { + state.flightMode = false; + state.jumpPressed = false; + state.flightToggleTimer = 0; + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + } + + private static boolean isSpecialKeyDown(EntityPlayer rider) { + if (rider == null) { + return false; + } + if (rider.worldObj.isRemote) { + PlayerData data = CustomNpcs.proxy.getPlayerData(rider); + return data != null && data.isSpecialKeyDown(); + } + PlayerData data = PlayerDataController.Instance.getPlayerData(rider); + return data != null && data.isSpecialKeyDown(); + } +} diff --git a/src/main/resources/assets/customnpcs/lang/cs_CZ.lang b/src/main/resources/assets/customnpcs/lang/cs_CZ.lang index e574a1fb1..c45e3dade 100644 --- a/src/main/resources/assets/customnpcs/lang/cs_CZ.lang +++ b/src/main/resources/assets/customnpcs/lang/cs_CZ.lang @@ -440,10 +440,11 @@ item.npcCrown.name=Koruna item.npcCrown2.name=Koruna key.npcInventory=NPC inventář role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/de_DE.lang b/src/main/resources/assets/customnpcs/lang/de_DE.lang index 86269b7b0..32170875b 100644 --- a/src/main/resources/assets/customnpcs/lang/de_DE.lang +++ b/src/main/resources/assets/customnpcs/lang/de_DE.lang @@ -872,10 +872,11 @@ companion.stage=Bühne key.npcInventory=NPC Inventar role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 6026ed59a..466acfb99 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1,6 +1,7 @@ customnpcs.defaultTextColor=404040 key.categories.customnpc=CustomNPC+ +key.customnpcs.special=Special Key key.npcInventory=NPC Inventory entity.CustomNpc.name=Custom Npc @@ -418,13 +419,16 @@ role.mailman=Mailman role.marketname=Linked Marketname role.companion=Companion role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting -role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint +role.mount.fly=Flying +role.mount.hover=Hover +role.mount.ascendspeed=Ascend Speed +role.mount.descendspeed=Descend Speed +role.mount.walkspeed=Walk Speed: %s trader.insufficient=Insufficient Currency trader.sufficient=Sufficient Currency diff --git a/src/main/resources/assets/customnpcs/lang/es_AR.lang b/src/main/resources/assets/customnpcs/lang/es_AR.lang index f0d9533a2..03e80082e 100644 --- a/src/main/resources/assets/customnpcs/lang/es_AR.lang +++ b/src/main/resources/assets/customnpcs/lang/es_AR.lang @@ -500,10 +500,11 @@ item.npcMusket.name=Mosquete item.npcExcalibur.name=Excalibur key.npcInventory=Inventario de NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/es_MX.lang b/src/main/resources/assets/customnpcs/lang/es_MX.lang index 9b3ee6a1f..7ea1b25b2 100644 --- a/src/main/resources/assets/customnpcs/lang/es_MX.lang +++ b/src/main/resources/assets/customnpcs/lang/es_MX.lang @@ -500,10 +500,11 @@ item.npcMusket.name=Mosquete item.npcExcalibur.name=Excalibur key.npcInventory=Inventario de NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/fr_FR.lang b/src/main/resources/assets/customnpcs/lang/fr_FR.lang index 6fc66154c..cea83f824 100644 --- a/src/main/resources/assets/customnpcs/lang/fr_FR.lang +++ b/src/main/resources/assets/customnpcs/lang/fr_FR.lang @@ -542,10 +542,11 @@ item.npcMusket.name=Mousquet item.npcExcalibur.name=Excalibur key.npcInventory=Inventaire Pnj role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/hu_HU.lang b/src/main/resources/assets/customnpcs/lang/hu_HU.lang index 63ee943af..db2fc6c38 100644 --- a/src/main/resources/assets/customnpcs/lang/hu_HU.lang +++ b/src/main/resources/assets/customnpcs/lang/hu_HU.lang @@ -542,10 +542,11 @@ item.npcMusket.name=Muskéta item.npcExcalibur.name=Excalibur key.npcInventory=NPC felszerelés role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/id_ID.lang b/src/main/resources/assets/customnpcs/lang/id_ID.lang index 2b53a9639..025320030 100644 --- a/src/main/resources/assets/customnpcs/lang/id_ID.lang +++ b/src/main/resources/assets/customnpcs/lang/id_ID.lang @@ -317,10 +317,11 @@ item.npcRuby.name=Permata Rubi role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/ko_KR.lang b/src/main/resources/assets/customnpcs/lang/ko_KR.lang index 89f7c8d7d..e3a672289 100644 --- a/src/main/resources/assets/customnpcs/lang/ko_KR.lang +++ b/src/main/resources/assets/customnpcs/lang/ko_KR.lang @@ -797,10 +797,11 @@ enchantment.poison=독 key.npcInventory=NPC 인벤토리 role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/nl_NL.lang b/src/main/resources/assets/customnpcs/lang/nl_NL.lang index acae2f273..297cfa1fc 100644 --- a/src/main/resources/assets/customnpcs/lang/nl_NL.lang +++ b/src/main/resources/assets/customnpcs/lang/nl_NL.lang @@ -232,10 +232,11 @@ item.npcNinjaHead.name=Ninja Helm item.npcNinjaChest.name=Ninja Borst item.npcNinjaPants.name=Ninja Broek role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/pl_PL.lang b/src/main/resources/assets/customnpcs/lang/pl_PL.lang index ff860e0ab..e84de64ee 100644 --- a/src/main/resources/assets/customnpcs/lang/pl_PL.lang +++ b/src/main/resources/assets/customnpcs/lang/pl_PL.lang @@ -498,10 +498,11 @@ item.npcMusket.name=Muszkiet item.npcExcalibur.name=Excalibur key.npcInventory=Inwentarz npc role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/pt_BR.lang b/src/main/resources/assets/customnpcs/lang/pt_BR.lang index 066697e8c..6d07b2df0 100644 --- a/src/main/resources/assets/customnpcs/lang/pt_BR.lang +++ b/src/main/resources/assets/customnpcs/lang/pt_BR.lang @@ -498,10 +498,11 @@ item.npcMusket.name=Mosquete item.npcExcalibur.name=Excalibur key.npcInventory=Inventário NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/ru_RU.lang b/src/main/resources/assets/customnpcs/lang/ru_RU.lang index a34739f0d..2c6f76422 100644 --- a/src/main/resources/assets/customnpcs/lang/ru_RU.lang +++ b/src/main/resources/assets/customnpcs/lang/ru_RU.lang @@ -1021,10 +1021,11 @@ questgui.accept=Принять key.npcInventory=Инвентарь NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/sv_SE.lang b/src/main/resources/assets/customnpcs/lang/sv_SE.lang index 70a29ebc9..7e536400b 100644 --- a/src/main/resources/assets/customnpcs/lang/sv_SE.lang +++ b/src/main/resources/assets/customnpcs/lang/sv_SE.lang @@ -308,10 +308,11 @@ item.npcPendant.name=Pendant item.npcBlueprint.name=Blueprint item.npcRing.name=Ring role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/zh_CN.lang b/src/main/resources/assets/customnpcs/lang/zh_CN.lang index 00fb8c796..32105c7db 100644 --- a/src/main/resources/assets/customnpcs/lang/zh_CN.lang +++ b/src/main/resources/assets/customnpcs/lang/zh_CN.lang @@ -1244,10 +1244,11 @@ hitbox.height=身高比例 key.npcInventory=NPC物品栏 role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/zh_TW.lang b/src/main/resources/assets/customnpcs/lang/zh_TW.lang index a4f3d4ae5..b7f2c04f5 100644 --- a/src/main/resources/assets/customnpcs/lang/zh_TW.lang +++ b/src/main/resources/assets/customnpcs/lang/zh_TW.lang @@ -314,10 +314,11 @@ item.npcSaphire.name=藍寶石 item.npcAmethyst.name=紫水晶 item.npcRuby.name=紅寶石 role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key From c33ac04d11f21dead944968e8445128cf84cc3e0 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 7 Nov 2025 22:44:22 -0500 Subject: [PATCH 02/35] Refine mount GUI layout and flight handling --- .../npcs/client/gui/roles/GuiNpcMount.java | 7 +-- .../noppes/npcs/entity/EntityNPCFlying.java | 8 +++ .../npcs/entity/EntityNPCInterface.java | 54 +++++++++++++++++-- .../java/noppes/npcs/util/NPCMountUtil.java | 3 ++ 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java index 8c63d2c99..0639113af 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java @@ -4,7 +4,6 @@ import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.role.RoleSavePacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNPCInterface2; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -31,7 +30,7 @@ public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener } int leftLabelX = guiLeft + 4; - int leftFieldX = guiLeft + 140; + int leftFieldX = guiLeft + 116; int leftY = guiTop + 6; leftY = addOffsetRow(0, 10, "role.mount.offsetx", role.getOffsetX(), leftLabelX, leftFieldX, leftY); @@ -43,10 +42,6 @@ public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener addButton(new GuiNpcButton(101, leftFieldX, leftY, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isSprintAllowed() ? 1 : 0)); leftY += 24; - GuiNpcLabel speedLabel = new GuiNpcLabel(5, "", leftLabelX, leftY + 5); - speedLabel.label = StatCollector.translateToLocalFormatted("role.mount.walkspeed", npc.ais.getWalkingSpeed()); - addLabel(speedLabel); - int rightLabelX = guiLeft + 214; int rightFieldX = guiLeft + 320; int rightY = guiTop + 6; diff --git a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java index 2996ca7f3..3f57ce85f 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java @@ -107,6 +107,8 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { return; } + boolean flightModeActive = isMountFlightModeActive(); + boolean aboveLimit = false; double heightOffGround = this.posY - this.worldObj.getTopSolidOrLiquidBlock((int) this.posX, (int) this.posZ); if (heightOffGround < 0) { @@ -132,6 +134,12 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { this.flyLimitAllow = true; setNpcFlyingState(true); + if (flightModeActive) { + super.performMountedMovement(strafe, forward, moveSpeed); + this.velocityChanged = true; + return; + } + double d3 = this.motionY; super.performMountedMovement(strafe, forward, moveSpeed); this.motionY = d3; diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 305a9098b..ff269b964 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -2128,11 +2128,59 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti public void performMountedMovement(float strafe, float forward, float moveSpeed) { this.moveStrafing = strafe; this.moveForward = forward; - if (!worldObj.isRemote) { - this.setAIMoveSpeed(moveSpeed); - super.moveEntityWithHeading(strafe, forward); + + if (isMountFlightModeActive()) { + performMountedFlightMovement(strafe, forward, moveSpeed); + return; } + if (!worldObj.isRemote) { + this.setAIMoveSpeed(moveSpeed); + } + super.moveEntityWithHeading(strafe, forward); + updateMountedLimbSwing(); + } + + protected boolean isMountFlightModeActive() { + return NPCMountUtil.isMountInFlightMode(mountState); + } + + private void performMountedFlightMovement(float strafe, float forward, float moveSpeed) { + if (!worldObj.isRemote) { + this.setAIMoveSpeed(moveSpeed); + } + + float inputMagnitude = MathHelper.sqrt_float(strafe * strafe + forward * forward); + if (inputMagnitude < 1.0E-3F) { + this.motionX *= 0.6D; + this.motionZ *= 0.6D; + if (Math.abs(this.motionX) < 0.003D) { + this.motionX = 0.0D; + } + if (Math.abs(this.motionZ) < 0.003D) { + this.motionZ = 0.0D; + } + } else { + float normStrafe = strafe / inputMagnitude; + float normForward = forward / inputMagnitude; + float yawRad = this.rotationYaw * (float) Math.PI / 180.0F; + double sinYaw = Math.sin(yawRad); + double cosYaw = Math.cos(yawRad); + double targetMotionX = (normStrafe * cosYaw - normForward * sinYaw) * moveSpeed; + double targetMotionZ = (normForward * cosYaw + normStrafe * sinYaw) * moveSpeed; + this.motionX = targetMotionX; + this.motionZ = targetMotionZ; + } + + this.moveEntity(this.motionX, this.motionY, this.motionZ); + this.velocityChanged = true; + if (!this.onGround) { + this.isAirBorne = true; + } + updateMountedLimbSwing(); + } + + private void updateMountedLimbSwing() { this.prevLimbSwingAmount = this.limbSwingAmount; double deltaX = this.posX - this.prevPosX; double deltaZ = this.posZ - this.prevPosZ; diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java index fc04248a8..1caa4ba14 100644 --- a/src/main/java/noppes/npcs/util/NPCMountUtil.java +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -236,6 +236,9 @@ public final class NPCMountUtil { npc.setNpcJumpingState(false); return; } + if (npc.canFly()) { + return; + } if (npc.onGround) { npc.motionY = 0.0D; npc.setNpcFlyingState(false); From b13be2969ac550e2418839252b0c89172b2d2c4b Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 13 Nov 2025 16:47:20 -0500 Subject: [PATCH 03/35] 1.10.3 --- build.gradle | 2 +- src/main/java/noppes/npcs/CustomNpcs.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index dc3a7dd3b..e0873b3e9 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.10.2" +version = "1.10.3-beta1" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 68eaa60b7..4b5750c6a 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -100,7 +100,7 @@ import noppes.npcs.scripted.NpcAPI; import java.io.File; import java.util.Set; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.2") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") From ee413c31b95957f16306166817d21193dc4c9b4f Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 13 Nov 2025 19:54:10 -0500 Subject: [PATCH 04/35] Improve mount client sync and flight speed scaling --- .../npcs/entity/EntityNPCInterface.java | 90 ++++++++++++------- .../java/noppes/npcs/util/NPCMountUtil.java | 61 ++++++++++--- 2 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index ff269b964..58aadb253 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -2129,15 +2129,38 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.moveStrafing = strafe; this.moveForward = forward; - if (isMountFlightModeActive()) { - performMountedFlightMovement(strafe, forward, moveSpeed); - return; - } + boolean flightMode = isMountFlightModeActive(); + double prevMotionX = this.motionX; + double prevMotionY = this.motionY; + double prevMotionZ = this.motionZ; + float previousJumpFactor = this.jumpMovementFactor; + float appliedJumpFactor = getMountedAirStrafeFactor(moveSpeed, flightMode); + + this.jumpMovementFactor = appliedJumpFactor; if (!worldObj.isRemote) { this.setAIMoveSpeed(moveSpeed); } + super.moveEntityWithHeading(strafe, forward); + + if (flightMode) { + this.motionY = prevMotionY; + this.isAirBorne = true; + } + + this.jumpMovementFactor = previousJumpFactor; + + if (!worldObj.isRemote) { + double newMotionX = this.motionX; + double newMotionY = this.motionY; + double newMotionZ = this.motionZ; + if (Math.abs(newMotionX - prevMotionX) > 1.0E-5D || Math.abs(newMotionY - prevMotionY) > 1.0E-5D || Math.abs(newMotionZ - prevMotionZ) > 1.0E-5D) { + this.velocityChanged = true; + } + } + + syncMountedRiderVelocity(); updateMountedLimbSwing(); } @@ -2145,39 +2168,38 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti return NPCMountUtil.isMountInFlightMode(mountState); } - private void performMountedFlightMovement(float strafe, float forward, float moveSpeed) { + private float getMountedAirStrafeFactor(float moveSpeed, boolean flightMode) { + if (moveSpeed <= 0.0F) { + return 0.0F; + } + if (!flightMode) { + return moveSpeed * 0.1F; + } + final float AIR_FRICTION = 0.91F; + final float BASE = 0.16277136F; + return moveSpeed * (BASE / (AIR_FRICTION * AIR_FRICTION * AIR_FRICTION)); + } + + private void syncMountedRiderVelocity() { + if (!(this.riddenByEntity instanceof EntityLivingBase)) { + return; + } + EntityLivingBase rider = (EntityLivingBase) this.riddenByEntity; + double riderPrevMotionX = rider.motionX; + double riderPrevMotionY = rider.motionY; + double riderPrevMotionZ = rider.motionZ; + + rider.motionX = this.motionX; + rider.motionY = this.motionY; + rider.motionZ = this.motionZ; + rider.fallDistance = 0.0F; + rider.isAirBorne = !this.onGround; + if (!worldObj.isRemote) { - this.setAIMoveSpeed(moveSpeed); - } - - float inputMagnitude = MathHelper.sqrt_float(strafe * strafe + forward * forward); - if (inputMagnitude < 1.0E-3F) { - this.motionX *= 0.6D; - this.motionZ *= 0.6D; - if (Math.abs(this.motionX) < 0.003D) { - this.motionX = 0.0D; + if (Math.abs(rider.motionX - riderPrevMotionX) > 1.0E-5D || Math.abs(rider.motionY - riderPrevMotionY) > 1.0E-5D || Math.abs(rider.motionZ - riderPrevMotionZ) > 1.0E-5D) { + rider.velocityChanged = true; } - if (Math.abs(this.motionZ) < 0.003D) { - this.motionZ = 0.0D; - } - } else { - float normStrafe = strafe / inputMagnitude; - float normForward = forward / inputMagnitude; - float yawRad = this.rotationYaw * (float) Math.PI / 180.0F; - double sinYaw = Math.sin(yawRad); - double cosYaw = Math.cos(yawRad); - double targetMotionX = (normStrafe * cosYaw - normForward * sinYaw) * moveSpeed; - double targetMotionZ = (normForward * cosYaw + normStrafe * sinYaw) * moveSpeed; - this.motionX = targetMotionX; - this.motionZ = targetMotionZ; } - - this.moveEntity(this.motionX, this.motionY, this.motionZ); - this.velocityChanged = true; - if (!this.onGround) { - this.isAirBorne = true; - } - updateMountedLimbSwing(); } private void updateMountedLimbSwing() { diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java index 1caa4ba14..708bf3b76 100644 --- a/src/main/java/noppes/npcs/util/NPCMountUtil.java +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -54,7 +54,7 @@ public final class NPCMountUtil { controlledForward *= 0.25F; } - float moveSpeed = Math.max(0.0F, npc.getSpeed()); + float moveSpeed = getMountMoveSpeed(npc); boolean riderSprinting = rider.isSprinting() && mount.isSprintAllowed(); if (riderSprinting) { moveSpeed *= 1.2F; @@ -71,7 +71,6 @@ public final class NPCMountUtil { npc.setSprinting(riderSprinting); npc.stepHeight = 1.0F; - npc.jumpMovementFactor = moveSpeed * 0.1F; npc.getNavigator().clearPathEntity(); npc.setAttackTarget(null); @@ -86,6 +85,10 @@ public final class NPCMountUtil { return true; } + private static float getMountMoveSpeed(EntityNPCInterface npc) { + return Math.max(0.0F, npc.getSpeed()); + } + private static void applyMountedVerticalMotion(EntityNPCInterface npc, MountState state, RoleMount mount, EntityLivingBase rider, float moveSpeed, float forward) { if (mount == null) { return; @@ -103,46 +106,65 @@ public final class NPCMountUtil { boolean descendPressed = rider instanceof EntityPlayer && isSpecialKeyDown((EntityPlayer) rider); if (flightMode) { - npc.motionY = 0.0D; + double previousMotionY = npc.motionY; + double newMotionY = 0.0D; if (jumpPressed) { - npc.motionY += ascendSpeed; + newMotionY += ascendSpeed; } else if (descendPressed) { - npc.motionY -= descendSpeed; + newMotionY -= descendSpeed; } - npc.motionY = MathHelper.clamp_double(npc.motionY, -descendSpeed, ascendSpeed); - + newMotionY = MathHelper.clamp_double(newMotionY, -descendSpeed, ascendSpeed); + npc.motionY = newMotionY; npc.setNpcFlyingState(true); npc.setNpcJumpingState(jumpPressed); npc.isAirBorne = true; - npc.velocityChanged = true; npc.fallDistance = 0.0F; + if (!npc.worldObj.isRemote && Math.abs(previousMotionY - newMotionY) > 1.0E-4D) { + npc.velocityChanged = true; + } + if (!npc.worldObj.isRemote && rider instanceof EntityPlayer) { EntityPlayer player = (EntityPlayer) rider; if (!player.capabilities.isFlying) { + double playerPrevMotion = player.motionY; player.motionY = npc.motionY; - player.velocityChanged = true; + if (Math.abs(playerPrevMotion - player.motionY) > 1.0E-4D) { + player.velocityChanged = true; + } } } return; } if (jumpPressed && npc.onGround) { + double previousMotionY = npc.motionY; npc.motionY = jumpFactor; npc.setNpcJumpingState(true); npc.isAirBorne = true; - npc.velocityChanged = true; npc.fallDistance = 0.0F; if (!npc.worldObj.isRemote) { + if (Math.abs(previousMotionY - npc.motionY) > 1.0E-4D) { + npc.velocityChanged = true; + } net.minecraftforge.common.ForgeHooks.onLivingJump(npc); } return; } if (!npc.onGround) { - npc.motionY = Math.max(npc.motionY, -descendSpeed); + double previousMotionY = npc.motionY; + double limitedMotionY = Math.max(npc.motionY, -descendSpeed); + if (Math.abs(previousMotionY - limitedMotionY) > 1.0E-4D) { + npc.motionY = limitedMotionY; + if (!npc.worldObj.isRemote) { + npc.velocityChanged = true; + } + } else { + npc.motionY = limitedMotionY; + } npc.setNpcFlyingState(!npc.isInWater()); } else { npc.setNpcFlyingState(false); @@ -152,13 +174,16 @@ public final class NPCMountUtil { } if (npc.onGround) { + double previousMotionY = npc.motionY; if (rider.isJumping) { npc.motionY = jumpFactor; npc.setNpcJumpingState(true); npc.isAirBorne = true; - npc.velocityChanged = true; npc.fallDistance = 0.0F; if (!npc.worldObj.isRemote) { + if (Math.abs(previousMotionY - npc.motionY) > 1.0E-4D) { + npc.velocityChanged = true; + } net.minecraftforge.common.ForgeHooks.onLivingJump(npc); } } else { @@ -214,10 +239,12 @@ public final class NPCMountUtil { } public static void haltMountedMotion(EntityNPCInterface npc, MountState state) { + double prevMotionX = npc.motionX; + double prevMotionY = npc.motionY; + double prevMotionZ = npc.motionZ; npc.motionX = 0.0D; npc.motionY = 0.0D; npc.motionZ = 0.0D; - npc.velocityChanged = true; npc.isAirBorne = false; npc.setSprinting(false); npc.moveForward = 0.0F; @@ -227,6 +254,9 @@ public final class NPCMountUtil { npc.limbSwing = 0.0F; npc.getNavigator().clearPathEntity(); npc.fallDistance = 0.0F; + if (!npc.worldObj.isRemote && (Math.abs(prevMotionX) > 1.0E-4D || Math.abs(prevMotionY) > 1.0E-4D || Math.abs(prevMotionZ) > 1.0E-4D)) { + npc.velocityChanged = true; + } resetMountedFlightState(npc, state); } @@ -254,12 +284,15 @@ public final class NPCMountUtil { } double desired = -descendSpeed; + double previousMotionY = npc.motionY; npc.motionY = desired; npc.isAirBorne = true; - npc.velocityChanged = true; npc.fallDistance = 0.0F; npc.setNpcFlyingState(true); npc.setNpcJumpingState(false); + if (!npc.worldObj.isRemote && Math.abs(previousMotionY - desired) > 1.0E-4D) { + npc.velocityChanged = true; + } } public static void stabilizeDismountedRider(Entity rider) { From 000cefdab93f6d4d4a74db1410d82b8d3609a403 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:27:27 +0000 Subject: [PATCH 05/35] Fix mount movement sync and speed issues - Remove server-side check for setAIMoveSpeed to fix client prediction desync on ground mounts - Increase air strafe factor for flying mounts to match ground speed feel --- src/main/java/noppes/npcs/entity/EntityNPCInterface.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 58aadb253..8285cf213 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -2138,9 +2138,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.jumpMovementFactor = appliedJumpFactor; - if (!worldObj.isRemote) { - this.setAIMoveSpeed(moveSpeed); - } + this.setAIMoveSpeed(moveSpeed); super.moveEntityWithHeading(strafe, forward); @@ -2175,9 +2173,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti if (!flightMode) { return moveSpeed * 0.1F; } - final float AIR_FRICTION = 0.91F; - final float BASE = 0.16277136F; - return moveSpeed * (BASE / (AIR_FRICTION * AIR_FRICTION * AIR_FRICTION)); + return moveSpeed * 0.5F; } private void syncMountedRiderVelocity() { From e5e897ab1eea0384a6467cbcaecce9b309f33a4a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 15 Dec 2025 17:33:58 +0200 Subject: [PATCH 06/35] Completely remove CustomNPC-Plus/src/api submodule --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitmodules b/.gitmodules index d8252cb6a..4723bbcd2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = src/api url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git branch = dev +[submodule "CustomNPC-Plus/src/api"] + path = CustomNPC-Plus/src/api + url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git From 2c91cd4286ef9b9811e71677269476ada994399e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 15 Dec 2025 17:46:46 +0200 Subject: [PATCH 07/35] oops --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 4723bbcd2..d8252cb6a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,6 +2,3 @@ path = src/api url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git branch = dev -[submodule "CustomNPC-Plus/src/api"] - path = CustomNPC-Plus/src/api - url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git From e767f4f46c3dba89ea2f1a3a67850419a83a75f0 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 19 Dec 2025 20:39:54 +0100 Subject: [PATCH 08/35] Fix Party Crash --- src/main/java/noppes/npcs/ServerEventsHandler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index a00274beb..1bd184a4a 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -433,6 +433,8 @@ public class ServerEventsHandler { return; String name = EntityList.getEntityString(entity); + if (entity instanceof EntityPlayer) + name = "Player"; QuestKill quest = (QuestKill) data.quest.questInterface; if (data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.Leader && !party.getLeaderUUID().equals(player.getUniqueID())) return; From 63ca8da22cc5a9da9c702567454b3b81cb81f2da Mon Sep 17 00:00:00 2001 From: PeTcHeNkA <60981731+PeTcHeNkA@users.noreply.github.com> Date: Sat, 20 Dec 2025 02:43:42 +0300 Subject: [PATCH 09/35] check: throw GradleException if api folder is not exist --- build.gradle | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index dc3a7dd3b..5ae3b3b5c 100644 --- a/build.gradle +++ b/build.gradle @@ -31,12 +31,14 @@ tasks.create('buildNoMixin', Exec) { commandLine gradleWrapper, 'build', '-Pnomixin' } -sourceSets { - named("main").configure { - java { - srcDirs += ['src/api/java'] - } +sourceSets.main.java { + def apiDir = file('src/api/java') + + if (!apiDir.exists()) { + throw new GradleException("Missing API sources: Run './gradlew updateAPI' or enable checkout submodules in CI.") } + + srcDirs += apiDir } // Modify the existing 'build' task to depend on 'updateAPI' From de094ea0a23805ea2e13abf94953811800a4cb47 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 30 Dec 2025 17:50:29 -0500 Subject: [PATCH 10/35] 1.10.3 Setting --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 950d6d3d6..b76ad9d0c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.10.3-beta1" +version = "1.10.3" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" @@ -33,7 +33,7 @@ tasks.create('buildNoMixin', Exec) { sourceSets.main.java { def apiDir = file('src/api/java') - + if (!apiDir.exists()) { throw new GradleException("Missing API sources: Run './gradlew updateAPI' or enable checkout submodules in CI.") } From 370abd2f94d49a91235d3e2021ca9fc8239ba139 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Dec 2025 23:04:32 +0000 Subject: [PATCH 11/35] Fix Player Data search functionality bugs - Fix category search logic for Quest/Dialog/Transport tabs that was incorrectly checking selectedCategory.isEmpty() before filtering, which cleared the category list instead of filtering it - Add resetScroll() calls to all search methods so scroll position resets when search results change --- .../npcs/client/gui/SubGuiPlayerData.java | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java index 0d39c0cd8..9025023b1 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java @@ -706,10 +706,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo if (!questCatSearch.equals(newText)) { questCatSearch = newText; if (questCatScroll != null) { - if (selectedQuestCategory.isEmpty()) - questCatScroll.setList(new ArrayList<>()); - else - questCatScroll.setList(filterList(questCatData, questCatSearch)); + questCatScroll.setList(filterList(questCatData, questCatSearch)); + questCatScroll.resetScroll(); } } } @@ -718,12 +716,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questFinishedSearch.equals(newText)) { questFinishedSearch = newText; - if (questFinishedScroll != null) + if (questFinishedScroll != null) { questFinishedScroll.setList( selectedQuestCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(questFinishedData, selectedQuestCategory, questFinishedSearch) ); + questFinishedScroll.resetScroll(); + } } } tf = getTextField(activeFieldId); @@ -731,12 +731,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questActiveSearch.equals(newText)) { questActiveSearch = newText; - if (questActiveScroll != null) + if (questActiveScroll != null) { questActiveScroll.setList( selectedQuestCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(questActiveData, selectedQuestCategory, questActiveSearch) ); + questActiveScroll.resetScroll(); + } } } } @@ -747,8 +749,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questFinishedSearch.equals(newText)) { questFinishedSearch = newText; - if (questFinishedScroll != null) + if (questFinishedScroll != null) { questFinishedScroll.setList(filterList(questFinishedData, questFinishedSearch)); + questFinishedScroll.resetScroll(); + } } } tf = getTextField(activeFieldId); @@ -756,8 +760,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questActiveSearch.equals(newText)) { questActiveSearch = newText; - if (questActiveScroll != null) + if (questActiveScroll != null) { questActiveScroll.setList(filterList(questActiveData, questActiveSearch)); + questActiveScroll.resetScroll(); + } } } } @@ -768,12 +774,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!dialogCatSearch.equals(newText)) { dialogCatSearch = newText; - if (dialogCatScroll != null) - dialogCatScroll.setList( - selectedDialogCategory.isEmpty() ? - new ArrayList<>() : - filterList(dialogCatData, dialogCatSearch) - ); + if (dialogCatScroll != null) { + dialogCatScroll.setList(filterList(dialogCatData, dialogCatSearch)); + dialogCatScroll.resetScroll(); + } } } tf = getTextField(readFieldId); @@ -781,12 +785,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!dialogReadSearch.equals(newText)) { dialogReadSearch = newText; - if (dialogReadScroll != null) + if (dialogReadScroll != null) { dialogReadScroll.setList( selectedDialogCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(dialogReadData, selectedDialogCategory, dialogReadSearch) ); + dialogReadScroll.resetScroll(); + } } } } @@ -797,8 +803,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!dialogCompactSearch.equals(newText)) { dialogCompactSearch = newText; - if (dialogCompactScroll != null) + if (dialogCompactScroll != null) { dialogCompactScroll.setList(filterList(dialogReadData, dialogCompactSearch)); + dialogCompactScroll.resetScroll(); + } } } } @@ -809,12 +817,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!transCatSearch.equals(newText)) { transCatSearch = newText; - if (transCatScroll != null) - transCatScroll.setList( - selectedTransCategory.isEmpty() ? - new ArrayList<>() : - filterList(transCatData, transCatSearch) - ); + if (transCatScroll != null) { + transCatScroll.setList(filterList(transCatData, transCatSearch)); + transCatScroll.resetScroll(); + } } } tf = getTextField(locFieldId); @@ -822,12 +828,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!transLocSearch.equals(newText)) { transLocSearch = newText; - if (transLocScroll != null) + if (transLocScroll != null) { transLocScroll.setList( selectedTransCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(transLocData, selectedTransCategory, transLocSearch) ); + transLocScroll.resetScroll(); + } } } } @@ -838,8 +846,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!transCompactSearch.equals(newText)) { transCompactSearch = newText; - if (transCompactScroll != null) + if (transCompactScroll != null) { transCompactScroll.setList(filterList(transLocData, transCompactSearch)); + transCompactScroll.resetScroll(); + } } } } @@ -850,8 +860,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!singleSearch.equals(newText)) { singleSearch = newText; - if (singleScroll != null) + if (singleScroll != null) { singleScroll.setList(filterList(currentTab == 13 ? bankData : factionData, singleSearch)); + singleScroll.resetScroll(); + } } } } From 096c9b0a69378de22bbb7b2a8131bdbd69a245b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Dec 2025 23:06:16 +0000 Subject: [PATCH 12/35] Fix mount movement jitter by following vanilla horse/boat sync pattern - Remove all velocityChanged=true flags from mount movement code (NPCMountUtil, EntityNPCInterface, EntityNPCFlying) - Make performMountedMovement physics server-side only like vanilla horse - Remove syncMountedRiderVelocity method that forced rider velocity packets - Fix ground movement to not reset motionY every tick - Let Minecraft's entity tracker handle position sync naturally The jitter was caused by S12PacketEntityVelocity being sent every tick, which constantly reset client interpolation. Vanilla horses/boats never set velocityChanged and rely on the entity tracker for smooth sync. --- .../noppes/npcs/entity/EntityNPCFlying.java | 3 -- .../npcs/entity/EntityNPCInterface.java | 54 ++++--------------- .../java/noppes/npcs/util/NPCMountUtil.java | 50 +---------------- 3 files changed, 11 insertions(+), 96 deletions(-) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java index 3f57ce85f..18c730dd3 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java @@ -84,7 +84,6 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { } this.updateLimbSwing(); - this.velocityChanged = true; } public void updateLimbSwing() { @@ -136,7 +135,6 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { if (flightModeActive) { super.performMountedMovement(strafe, forward, moveSpeed); - this.velocityChanged = true; return; } @@ -149,6 +147,5 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { } this.updateLimbSwing(); - this.velocityChanged = true; } } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 8285cf213..94f26acaa 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -2130,35 +2130,21 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.moveForward = forward; boolean flightMode = isMountFlightModeActive(); - double prevMotionX = this.motionX; double prevMotionY = this.motionY; - double prevMotionZ = this.motionZ; - float previousJumpFactor = this.jumpMovementFactor; - float appliedJumpFactor = getMountedAirStrafeFactor(moveSpeed, flightMode); - - this.jumpMovementFactor = appliedJumpFactor; - - this.setAIMoveSpeed(moveSpeed); - - super.moveEntityWithHeading(strafe, forward); - - if (flightMode) { - this.motionY = prevMotionY; - this.isAirBorne = true; - } - - this.jumpMovementFactor = previousJumpFactor; + // Server-side only physics (like vanilla horse) if (!worldObj.isRemote) { - double newMotionX = this.motionX; - double newMotionY = this.motionY; - double newMotionZ = this.motionZ; - if (Math.abs(newMotionX - prevMotionX) > 1.0E-5D || Math.abs(newMotionY - prevMotionY) > 1.0E-5D || Math.abs(newMotionZ - prevMotionZ) > 1.0E-5D) { - this.velocityChanged = true; + this.jumpMovementFactor = getMountedAirStrafeFactor(moveSpeed, flightMode); + this.setAIMoveSpeed(moveSpeed); + super.moveEntityWithHeading(strafe, forward); + + if (flightMode) { + this.motionY = prevMotionY; + this.isAirBorne = true; } } - syncMountedRiderVelocity(); + // Both sides do animation (like vanilla horse) updateMountedLimbSwing(); } @@ -2176,28 +2162,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti return moveSpeed * 0.5F; } - private void syncMountedRiderVelocity() { - if (!(this.riddenByEntity instanceof EntityLivingBase)) { - return; - } - EntityLivingBase rider = (EntityLivingBase) this.riddenByEntity; - double riderPrevMotionX = rider.motionX; - double riderPrevMotionY = rider.motionY; - double riderPrevMotionZ = rider.motionZ; - - rider.motionX = this.motionX; - rider.motionY = this.motionY; - rider.motionZ = this.motionZ; - rider.fallDistance = 0.0F; - rider.isAirBorne = !this.onGround; - - if (!worldObj.isRemote) { - if (Math.abs(rider.motionX - riderPrevMotionX) > 1.0E-5D || Math.abs(rider.motionY - riderPrevMotionY) > 1.0E-5D || Math.abs(rider.motionZ - riderPrevMotionZ) > 1.0E-5D) { - rider.velocityChanged = true; - } - } - } - private void updateMountedLimbSwing() { this.prevLimbSwingAmount = this.limbSwingAmount; double deltaX = this.posX - this.prevPosX; diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java index 708bf3b76..b83ef00a7 100644 --- a/src/main/java/noppes/npcs/util/NPCMountUtil.java +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -122,49 +122,22 @@ public final class NPCMountUtil { npc.isAirBorne = true; npc.fallDistance = 0.0F; - if (!npc.worldObj.isRemote && Math.abs(previousMotionY - newMotionY) > 1.0E-4D) { - npc.velocityChanged = true; - } - - if (!npc.worldObj.isRemote && rider instanceof EntityPlayer) { - EntityPlayer player = (EntityPlayer) rider; - if (!player.capabilities.isFlying) { - double playerPrevMotion = player.motionY; - player.motionY = npc.motionY; - if (Math.abs(playerPrevMotion - player.motionY) > 1.0E-4D) { - player.velocityChanged = true; - } - } - } return; } if (jumpPressed && npc.onGround) { - double previousMotionY = npc.motionY; npc.motionY = jumpFactor; npc.setNpcJumpingState(true); npc.isAirBorne = true; npc.fallDistance = 0.0F; if (!npc.worldObj.isRemote) { - if (Math.abs(previousMotionY - npc.motionY) > 1.0E-4D) { - npc.velocityChanged = true; - } net.minecraftforge.common.ForgeHooks.onLivingJump(npc); } return; } if (!npc.onGround) { - double previousMotionY = npc.motionY; - double limitedMotionY = Math.max(npc.motionY, -descendSpeed); - if (Math.abs(previousMotionY - limitedMotionY) > 1.0E-4D) { - npc.motionY = limitedMotionY; - if (!npc.worldObj.isRemote) { - npc.velocityChanged = true; - } - } else { - npc.motionY = limitedMotionY; - } + npc.motionY = Math.max(npc.motionY, -descendSpeed); npc.setNpcFlyingState(!npc.isInWater()); } else { npc.setNpcFlyingState(false); @@ -174,20 +147,14 @@ public final class NPCMountUtil { } if (npc.onGround) { - double previousMotionY = npc.motionY; if (rider.isJumping) { npc.motionY = jumpFactor; npc.setNpcJumpingState(true); npc.isAirBorne = true; npc.fallDistance = 0.0F; if (!npc.worldObj.isRemote) { - if (Math.abs(previousMotionY - npc.motionY) > 1.0E-4D) { - npc.velocityChanged = true; - } net.minecraftforge.common.ForgeHooks.onLivingJump(npc); } - } else { - npc.motionY = 0.0D; } return; } @@ -239,9 +206,6 @@ public final class NPCMountUtil { } public static void haltMountedMotion(EntityNPCInterface npc, MountState state) { - double prevMotionX = npc.motionX; - double prevMotionY = npc.motionY; - double prevMotionZ = npc.motionZ; npc.motionX = 0.0D; npc.motionY = 0.0D; npc.motionZ = 0.0D; @@ -254,9 +218,6 @@ public final class NPCMountUtil { npc.limbSwing = 0.0F; npc.getNavigator().clearPathEntity(); npc.fallDistance = 0.0F; - if (!npc.worldObj.isRemote && (Math.abs(prevMotionX) > 1.0E-4D || Math.abs(prevMotionY) > 1.0E-4D || Math.abs(prevMotionZ) > 1.0E-4D)) { - npc.velocityChanged = true; - } resetMountedFlightState(npc, state); } @@ -283,16 +244,11 @@ public final class NPCMountUtil { return; } - double desired = -descendSpeed; - double previousMotionY = npc.motionY; - npc.motionY = desired; + npc.motionY = -descendSpeed; npc.isAirBorne = true; npc.fallDistance = 0.0F; npc.setNpcFlyingState(true); npc.setNpcJumpingState(false); - if (!npc.worldObj.isRemote && Math.abs(previousMotionY - desired) > 1.0E-4D) { - npc.velocityChanged = true; - } } public static void stabilizeDismountedRider(Entity rider) { @@ -314,8 +270,6 @@ public final class NPCMountUtil { rider.motionZ = previousMotionZ; rider.fallDistance = previousFall; } - - rider.velocityChanged = true; } private static void updateMountedFlightState(EntityNPCInterface npc, MountState state, RoleMount mount, EntityPlayer rider) { From de8c31702317c80487ecef781131f1559d4b064e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Dec 2025 23:20:54 +0000 Subject: [PATCH 13/35] Add mount edge case handling for dismount scenarios - Player logout: Dismount from NPC mount before logout to prevent orphaned mount state (ServerTickHandler) - Teleportation: Dismount before cross-dimension teleport to prevent mount being left behind in old dimension (NoppesUtilPlayer) - Dead rider: Validate rider is alive, dismount dead/invalid riders to prevent stuck mount state (NPCMountUtil) - Role disable: Dismount rider when mount role is disabled to prevent mounted player on non-mount NPC (RoleMount) --- src/main/java/noppes/npcs/NoppesUtilPlayer.java | 12 ++++++++++-- src/main/java/noppes/npcs/ServerTickHandler.java | 6 ++++++ src/main/java/noppes/npcs/roles/RoleMount.java | 4 ++++ src/main/java/noppes/npcs/util/NPCMountUtil.java | 11 +++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/NoppesUtilPlayer.java b/src/main/java/noppes/npcs/NoppesUtilPlayer.java index 2c9c3b647..c78ffdf46 100644 --- a/src/main/java/noppes/npcs/NoppesUtilPlayer.java +++ b/src/main/java/noppes/npcs/NoppesUtilPlayer.java @@ -123,8 +123,12 @@ public class NoppesUtilPlayer { } public static void teleportPlayer(EntityPlayerMP player, double posX, double posY, double posZ, int dimension) { + // Dismount from NPC mount before teleportation to prevent mount desync + if (player.ridingEntity instanceof EntityNPCInterface) { + player.mountEntity(null); + } + if (player.dimension != dimension) { - int dim = player.dimension; MinecraftServer server = MinecraftServer.getServer(); WorldServer wor = server.worldServerForDimension(dimension); if (wor == null) { @@ -144,8 +148,12 @@ public class NoppesUtilPlayer { } public static void teleportPlayer(EntityPlayerMP player, double posX, double posY, double posZ, float yaw, float pitch, int dimension) { + // Dismount from NPC mount before teleportation to prevent mount desync + if (player.ridingEntity instanceof EntityNPCInterface) { + player.mountEntity(null); + } + if (player.dimension != dimension) { - int dim = player.dimension; MinecraftServer server = MinecraftServer.getServer(); WorldServer wor = server.worldServerForDimension(dimension); if (wor == null) { diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 62f53399b..3648f00d7 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -11,6 +11,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; import java.net.InetAddress; @@ -68,6 +69,11 @@ public class ServerTickHandler { @SubscribeEvent public void playerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + // Dismount player from NPC mount before logout to prevent orphaned mount state + if (event.player.ridingEntity instanceof EntityNPCInterface) { + event.player.mountEntity(null); + } + PlayerData playerData = PlayerData.get(event.player); if (playerData != null) { playerData.onLogout(); diff --git a/src/main/java/noppes/npcs/roles/RoleMount.java b/src/main/java/noppes/npcs/roles/RoleMount.java index f930a4975..dd1ec0ca7 100644 --- a/src/main/java/noppes/npcs/roles/RoleMount.java +++ b/src/main/java/noppes/npcs/roles/RoleMount.java @@ -192,6 +192,10 @@ public class RoleMount extends RoleInterface { } public void onDisable() { + // Dismount any rider when mount role is disabled + if (npc.riddenByEntity != null) { + npc.riddenByEntity.mountEntity(null); + } npc.ais.returnToStart = storedReturnToStart; } diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java index b83ef00a7..6303f67b9 100644 --- a/src/main/java/noppes/npcs/util/NPCMountUtil.java +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -177,6 +177,17 @@ public final class NPCMountUtil { RoleMount mount = (RoleMount) npc.roleInterface; Entity currentRider = npc.riddenByEntity; + + // Validate rider is alive and valid - dismount dead/invalid riders + if (currentRider != null && !currentRider.isEntityAlive()) { + currentRider.mountEntity(null); + stabilizeDismountedRider(currentRider); + haltMountedMotion(npc, state); + applyUnriddenFlightDescent(npc, state, mount); + currentRider = null; + } + + // Only allow EntityPlayer as riders if (currentRider != null && !(currentRider instanceof EntityPlayer)) { Entity dismount = currentRider; dismount.mountEntity(null); From 592a910cbcc950e4a85ff3fc0c3b70291127a5b7 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 30 Dec 2025 19:29:44 -0500 Subject: [PATCH 14/35] Add some more Attack Speed Handling --- src/api | 2 +- .../java/noppes/npcs/ServerEventsHandler.java | 4 --- .../npcs/controllers/data/LinkedItem.java | 26 ++++++++++++------- .../npcs/scripted/item/ScriptCustomItem.java | 15 +++++++++++ .../scripted/item/ScriptCustomizableItem.java | 1 + .../npcs/scripted/item/ScriptLinkedItem.java | 5 ++++ 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/api b/src/api index 305a3778c..d003d6d87 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 +Subproject commit d003d6d870db13d56fce223050a06faf15050eae diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 1bd184a4a..eab17aff1 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -173,9 +173,6 @@ public class ServerEventsHandler { @SubscribeEvent public void invoke(LivingHurtEvent event) { - - - if(event.source.getEntity() instanceof EntityPlayerMP && event.entity instanceof EntityLivingBase ){ EntityPlayerMP ep = (EntityPlayerMP) event.source.getEntity(); if(ep.getHeldItem() != null && ep.getHeldItem().getItem() instanceof ItemLinked){ @@ -185,7 +182,6 @@ public class ServerEventsHandler { } } - if (!(event.entityLiving instanceof EntityPlayer)) return; diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java index 4fb5fc6e7..f1840a625 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java @@ -29,7 +29,7 @@ public class LinkedItem implements ILinkedItem { public boolean isNormalItem = false; public boolean isTool = false; public int digSpeed = 1; - public int attackSpeed = 20; + public int attackSpeed = 10; public int armorType = -2; //-2: Fits in no armor slot, -1: Fits in all slots, 0 - 3: Fits in Head -> Boots slot respectively public int enchantability; @@ -59,7 +59,8 @@ public class LinkedItem implements ILinkedItem { compound.setBoolean("IsTool", this.isTool); compound.setBoolean("IsNormalItem", this.isNormalItem); compound.setInteger("DigSpeed", this.digSpeed); - compound.setInteger("AttackSpeed",this.attackSpeed); + compound.setInteger("AttackSpeed", this.attackSpeed); + compound.setInteger("ArmorType", this.armorType); compound.setInteger("Enchantability", this.enchantability); @@ -89,7 +90,7 @@ public class LinkedItem implements ILinkedItem { this.isTool = compound.getBoolean("IsTool"); this.isNormalItem = compound.getBoolean("IsNormalItem"); this.digSpeed = compound.getInteger("DigSpeed"); - this.attackSpeed = compound.getInteger("AttackSpeed"); + this.setAttackSpeed(compound.getInteger("AttackSpeed")); this.armorType = compound.getInteger("ArmorType"); this.enchantability = compound.getInteger("Enchantability"); @@ -241,6 +242,19 @@ public class LinkedItem implements ILinkedItem { this.enchantability = enchantability; } + @Override + public int getAttackSpeed() { + return attackSpeed; + } + + @Override + public void setAttackSpeed(int time) { + if(time <= 0) { + time = 10; + } + this.attackSpeed = time; + } + public LinkedItem clone() { NBTTagCompound nbt = this.writeToNBT(true); LinkedItem clone = new LinkedItem(this.name); @@ -254,10 +268,4 @@ public class LinkedItem implements ILinkedItem { setScriptHandler(data = new LinkedItemScript()); return data; } - - @Override - public int getAttackSpeed() {return attackSpeed;} - - @Override - public void setAttackSpeed(int time) {this.attackSpeed = time;} } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 0103a9af2..6e85e1555 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -36,6 +36,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus public int digSpeed = 1; public int armorType = -2; //-2: Fits in no armor slot, -1: Fits in all slots, 0 - 4: Fits in Head -> Boots slot respectively public int enchantability; + public int attackSpeed = 10; public long lastInited = -1; @@ -228,6 +229,18 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus return this.enchantability; } + public int getAttackSpeed() { + return this.attackSpeed; + } + + public void setAttackSpeed(int speed) { + if(speed <= 0) { + speed = 10; + } + this.attackSpeed = speed; + saveItemData(); + } + public void setEnchantability(int enchantability) { this.enchantability = enchantability; saveItemData(); @@ -270,6 +283,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus compound.setInteger("DigSpeed", this.digSpeed); compound.setInteger("ArmorType", this.armorType); compound.setInteger("Enchantability", this.enchantability); + compound.setInteger("AttackSpeed", this.attackSpeed); compound.setInteger("MaxItemUseDuration", this.maxItemUseDuration); compound.setInteger("ItemUseAction", this.itemUseAction); @@ -286,6 +300,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus this.digSpeed = compound.getInteger("DigSpeed"); this.armorType = compound.getInteger("ArmorType"); this.enchantability = compound.getInteger("Enchantability"); + setAttackSpeed(compound.getInteger("AttackSpeed")); this.maxItemUseDuration = compound.getInteger("MaxItemUseDuration"); this.itemUseAction = compound.getInteger("ItemUseAction"); diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java index 224427c12..77e51df2c 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java @@ -30,6 +30,7 @@ public abstract class ScriptCustomizableItem extends ScriptItemStack implements public abstract int getEnchantability(); + public abstract int getAttackSpeed(); public String getTexture() { return this.itemDisplay.texture == null ? "" : this.itemDisplay.texture; diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java index 6e5e10b76..3a777134c 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java @@ -102,6 +102,11 @@ public class ScriptLinkedItem extends ScriptCustomizableItem implements IItemLin return this.linkedItem.enchantability; } + @Override + public int getAttackSpeed() { + return this.linkedItem.attackSpeed; + } + @Override public String getTexture() { return this.itemDisplay.texture == null ? this.linkedItem.display.texture : this.itemDisplay.texture; From ce8e38b6bdc9f6c190aa49a4ff727e65103b18e4 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 30 Dec 2025 19:41:02 -0500 Subject: [PATCH 15/35] Update ScriptPlayerEventHandler.java --- .../java/noppes/npcs/ScriptPlayerEventHandler.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index f5a2f4d42..d9dcb06ab 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -44,6 +44,7 @@ import net.minecraftforge.event.world.BlockEvent; import noppes.npcs.api.IWorld; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.item.IItemCustomizable; +import noppes.npcs.api.item.IItemStack; import noppes.npcs.config.ConfigMain; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; @@ -60,6 +61,8 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.items.ItemNpcTool; import noppes.npcs.quests.QuestItem; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.scripted.item.ScriptCustomizableItem; +import noppes.npcs.scripted.item.ScriptItemStack; import static noppes.npcs.config.ConfigMain.TrackedQuestUpdateFrequency; @@ -545,6 +548,16 @@ public class ScriptPlayerEventHandler { } if (event.source.getEntity() instanceof EntityPlayerMP) { + EntityPlayerMP playerMP = (EntityPlayerMP) event.source.getEntity(); + if(playerMP.getHeldItem() != null) { + IItemStack itemStack = NpcAPI.Instance().getIItemStack(playerMP.getHeldItem()); + if (itemStack instanceof ScriptCustomizableItem) { + ScriptCustomizableItem sci = (ScriptCustomizableItem) itemStack; + int attackSpeed = sci.getAttackSpeed(); + // TODO: Check Hurt Resistant Time. + } + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.source.getEntity()); float attackAmount = event.ammount; if (ConfigMain.AttributesEnabled && !DBCAddon.IsAvailable()) From 185a8bc9176fa772bad857ef3d3d7e3b4353da2a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 00:45:26 +0000 Subject: [PATCH 16/35] Move hurt resistant time handling from LivingHurtEvent to LivingAttackEvent - Remove legacy attack speed handling from ServerEventsHandler.LivingHurtEvent (was setting hurtResistantTime AFTER immunity check, too late) - Add proper handling in ScriptPlayerEventHandler.LivingAttackEvent (sets hurtResistantTime=0 BEFORE immunity check for fast weapons) - For weapons with attackSpeed < 20, bypass invincibility frames to allow rapid attacks to land --- src/main/java/noppes/npcs/ScriptPlayerEventHandler.java | 5 ++++- src/main/java/noppes/npcs/ServerEventsHandler.java | 9 --------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index d9dcb06ab..7d8f45890 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -554,7 +554,10 @@ public class ScriptPlayerEventHandler { if (itemStack instanceof ScriptCustomizableItem) { ScriptCustomizableItem sci = (ScriptCustomizableItem) itemStack; int attackSpeed = sci.getAttackSpeed(); - // TODO: Check Hurt Resistant Time. + if (attackSpeed < 20) { + event.entityLiving.hurtResistantTime = 0; + event.entityLiving.hurtTime = attackSpeed; + } } } diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index eab17aff1..47a1148df 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -173,15 +173,6 @@ public class ServerEventsHandler { @SubscribeEvent public void invoke(LivingHurtEvent event) { - if(event.source.getEntity() instanceof EntityPlayerMP && event.entity instanceof EntityLivingBase ){ - EntityPlayerMP ep = (EntityPlayerMP) event.source.getEntity(); - if(ep.getHeldItem() != null && ep.getHeldItem().getItem() instanceof ItemLinked){ - int time = ep.getHeldItem().stackTagCompound.getCompoundTag("ItemData").getCompoundTag("LinkedData").getInteger("AttackSpeed"); - event.entity.hurtResistantTime = time; - ((EntityLivingBase) event.entity).hurtTime = time; - } - } - if (!(event.entityLiving instanceof EntityPlayer)) return; From d06e6803507ff660e8c342ae57bf00203682d80e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 00:52:06 +0000 Subject: [PATCH 17/35] Fix attack speed formula to check elapsed time properly Instead of blindly resetting hurtResistantTime for fast weapons, now checks if enough time has elapsed based on the weapon's attackSpeed: - timeElapsed = maxHurtResistantTime - hurtResistantTime - Allow attack when: hurtResistantTime <= maxHurtResistantTime - attackSpeed Example: attackSpeed=10, maxHurtResistantTime=20 - Allows attack when hurtResistantTime <= 10 (10 ticks elapsed) --- src/main/java/noppes/npcs/ScriptPlayerEventHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 7d8f45890..e9531296a 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -554,9 +554,8 @@ public class ScriptPlayerEventHandler { if (itemStack instanceof ScriptCustomizableItem) { ScriptCustomizableItem sci = (ScriptCustomizableItem) itemStack; int attackSpeed = sci.getAttackSpeed(); - if (attackSpeed < 20) { + if (event.entityLiving.hurtResistantTime <= event.entityLiving.maxHurtResistantTime - attackSpeed) { event.entityLiving.hurtResistantTime = 0; - event.entityLiving.hurtTime = attackSpeed; } } } From 9b654621e8ec96bd247d25719096fa2cce856756 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 01:00:33 +0000 Subject: [PATCH 18/35] Add attack speed check to NPC's attackEntityFrom before immunity check The LivingAttackEvent handler was firing too late for NPCs because EntityNPCInterface.attackEntityFrom() has its own immunity check that runs BEFORE calling super (which fires LivingAttackEvent). Now the attack speed bypass is checked directly in the NPC's attackEntityFrom() method, before the immunity check at line 658. --- .../noppes/npcs/entity/EntityNPCInterface.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 94f26acaa..bf382e768 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -159,6 +159,7 @@ import noppes.npcs.roles.RoleInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; +import noppes.npcs.scripted.item.ScriptCustomizableItem; import noppes.npcs.util.GameProfileAlt; import noppes.npcs.util.NPCMountUtil; @@ -638,6 +639,22 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti reset(); } + // Check for custom weapon attack speed - bypass immunity if enough time has passed + Entity sourceEntity = damagesource.getEntity(); + if (sourceEntity instanceof EntityPlayer) { + EntityPlayer player = (EntityPlayer) sourceEntity; + ItemStack heldItem = player.getHeldItem(); + if (heldItem != null) { + IItemStack itemStack = NpcAPI.Instance().getIItemStack(heldItem); + if (itemStack instanceof ScriptCustomizableItem) { + int attackSpeed = ((ScriptCustomizableItem) itemStack).getAttackSpeed(); + if (this.hurtResistantTime <= this.maxHurtResistantTime - attackSpeed) { + this.hurtResistantTime = 0; + } + } + } + } + if ((float) this.hurtResistantTime > (float) this.maxHurtResistantTime / 2.0F && i <= this.lastDamage) return false; From f19c53758e33c1ffc6e8033a630f3d21edb0d6be Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 1 Jan 2026 21:47:17 -0500 Subject: [PATCH 19/35] 1.10.3-beta --- build.gradle | 2 +- gradle.properties | 2 +- src/main/java/noppes/npcs/CustomNpcs.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b76ad9d0c..d3f9587ca 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.10.3" +version = "1.10.3-beta" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/gradle.properties b/gradle.properties index 39f2d510a..48416e9a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -199,7 +199,7 @@ disableCheckstyle = true # ideaCheckSpotlessOnBuild = true # Non-GTNH properties -modVersion = 1.10 +modVersion = 1.10.3 # Increase Gradle's JVM memory usage to avoid daemon expiration due to # insufficient heap space on CI builds. diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 4b5750c6a..cea440c0a 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -100,7 +100,7 @@ import noppes.npcs.scripted.NpcAPI; import java.io.File; import java.util.Set; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3-beta") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") From e2291f8ccc479c1f2000f282e5eefb8ed785960b Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 1 Jan 2026 22:02:54 -0500 Subject: [PATCH 20/35] Fix Debugging Error --- .../packets/request/script/item/LinkedItemScriptPacket.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java index c62a36553..faa14f4d1 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java @@ -92,7 +92,7 @@ public final class LinkedItemScriptPacket extends AbstractPacket { } else { data.saveScript(in); if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { - LogWriter.script(String.format("[%s] (Player) %s SAVED EFFECT %s [%s]", "LINKED ITEM SCRIPTS", player.getCommandSenderName(), linkedItem.getName())); + LogWriter.script(String.format("[%s] (Player) %s SAVED LINKED ITEM [%s]", "LINKED ITEM SCRIPTS", player.getCommandSenderName(), linkedItem.getName())); } } } From 07a10b8414bae8ca413d5420e8cf5978bdf4c823 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 1 Jan 2026 22:35:32 -0500 Subject: [PATCH 21/35] Remove this --- .../java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index f4296a882..227f7cecc 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -125,12 +125,6 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe y += spacing; - // Attack Speed Field (integers only) - //addLabel(new GuiNpcLabel(13,"Attack Speed",x,y + 5, CustomNpcResourceListener.DefaultTextColor)); - - - //y += spacing; - // TEXTURE field addLabel(new GuiNpcLabel(3, "display.texture", x, y + 5, CustomNpcResourceListener.DefaultTextColor)); addTextField(new GuiNpcTextField(2, this, x + 60, y, 200, 20, linkedItem.display.texture)); From f5deb8a823bb00d29ccd2f81e84b9c3a3bbaf8f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 03:54:19 +0000 Subject: [PATCH 22/35] Fix Bard music restarting/duplicating when opening dialogs Two issues were causing the bug: 1. GuiDialogInteract.close() called stopAllSounds() unconditionally, which stopped Bard music even when the dialog had no sound or when a different Bard was playing nearby. 2. MusicController.stopMusic() didn't set playDelay, allowing Bards to immediately restart their music after dialog close. Changes: - MusicController.stopMusic() now sets playDelay=20 and clears entity reference to prevent immediate music restart - Added MusicController.stopSound(key) to stop specific sounds - Dialog GUIs now track their dialog sound and only stop that on close, preserving Bard music and other sounds --- .../client/controllers/MusicController.java | 24 +++++++++++++++++++ .../client/gui/player/GuiDialogInteract.java | 17 ++++++++++++- .../modern/GuiModernDialogInteract.java | 16 +++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/controllers/MusicController.java b/src/main/java/noppes/npcs/client/controllers/MusicController.java index fc2d9369b..98c30f2ac 100644 --- a/src/main/java/noppes/npcs/client/controllers/MusicController.java +++ b/src/main/java/noppes/npcs/client/controllers/MusicController.java @@ -66,6 +66,8 @@ public class MusicController { this.playingSound.stopSound(); this.sounds.remove(this.playingSound.sound); this.playingSound = null; + this.entity = null; + this.playDelay = 20; // Prevent immediate restart when music is stopped } else if (soundHandler != null) { try { soundHandler.stopSounds(); @@ -193,4 +195,26 @@ public class MusicController { } this.sounds.clear(); } + + /** + * Stops a specific sound by its resource key. + * This is used to stop dialog sounds without affecting Bard music. + * @param soundKey The sound resource key to stop + */ + public void stopSound(String soundKey) { + if (soundKey == null || soundKey.isEmpty()) { + return; + } + ScriptClientSound sound = this.sounds.remove(soundKey); + if (sound != null) { + SoundHandler soundHandler = Minecraft.getMinecraft().getSoundHandler(); + if (soundHandler != null) { + try { + soundHandler.stopSound(sound); + } catch (Exception ignored) { + } + } + sound.stopSound(); + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java b/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java index a49db2873..728649f6d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java @@ -75,6 +75,12 @@ public class GuiDialogInteract extends GuiNPCInterface implements IGuiClose { */ private boolean sentClosePacket = false; + /** + * Tracks the dialog sound that was played, so we can stop only this sound + * on close without affecting Bard music or other sounds. + */ + private String dialogSound = null; + public GuiDialogInteract(EntityNPCInterface npc, Dialog dialog) { super(npc); this.dialog = dialog; @@ -671,8 +677,13 @@ public class GuiDialogInteract extends GuiNPCInterface implements IGuiClose { this.options = new ArrayList(); if (dialog.sound != null && !dialog.sound.isEmpty()) { + // Stop any previous dialog sound before playing new one + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + } MusicController.Instance.stopMusic(); MusicController.Instance.playSound(dialog.sound, (float) npc.posX, (float) npc.posY, (float) npc.posZ); + this.dialogSound = dialog.sound; } if (!dialog.showPreviousBlocks) { @@ -707,7 +718,11 @@ public class GuiDialogInteract extends GuiNPCInterface implements IGuiClose { super.close(); if (parent != null) NoppesUtil.openGUI(player, parent); - MusicController.Instance.stopAllSounds(); + // Only stop the dialog sound, not Bard music or other sounds + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + this.dialogSound = null; + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java b/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java index fc4a221c7..3f382d98c 100644 --- a/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java +++ b/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java @@ -63,6 +63,12 @@ public class GuiModernDialogInteract extends GuiNPCInterface implements IGuiClos */ private boolean sentClosePacket = false; + /** + * Tracks the dialog sound that was played, so we can stop only this sound + * on close without affecting Bard music or other sounds. + */ + private String dialogSound = null; + public GuiModernDialogInteract(EntityNPCInterface npc, Dialog dialog) { super(npc); this.dialog = dialog; @@ -380,8 +386,13 @@ public class GuiModernDialogInteract extends GuiNPCInterface implements IGuiClos this.options = new ArrayList(); if (dialog.sound != null && !dialog.sound.isEmpty()) { + // Stop any previous dialog sound before playing new one + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + } MusicController.Instance.stopMusic(); MusicController.Instance.playSound(dialog.sound, (float) npc.posX, (float) npc.posY, (float) npc.posZ); + this.dialogSound = dialog.sound; } if (!dialog.showPreviousBlocks) { @@ -440,6 +451,11 @@ public class GuiModernDialogInteract extends GuiNPCInterface implements IGuiClos } closed(); } + // Only stop the dialog sound, not Bard music or other sounds + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + this.dialogSound = null; + } super.onGuiClosed(); } } From 1d191413fb72089198bc9fdffc41aff4778561dc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 03:58:52 +0000 Subject: [PATCH 23/35] Fix quest cooldown not working for MCCUSTOM and RLCUSTOM types The canQuestBeAccepted() method was missing cases for MCCUSTOM and RLCUSTOM repeat types, causing quests with custom cooldowns to become permanently inaccessible after first completion. Added the missing checks using quest.customCooldown for both MC ticks and real-life milliseconds. --- .../java/noppes/npcs/controllers/PlayerQuestController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/noppes/npcs/controllers/PlayerQuestController.java b/src/main/java/noppes/npcs/controllers/PlayerQuestController.java index e08203136..7b99f40d8 100644 --- a/src/main/java/noppes/npcs/controllers/PlayerQuestController.java +++ b/src/main/java/noppes/npcs/controllers/PlayerQuestController.java @@ -136,10 +136,14 @@ public class PlayerQuestController { return player.worldObj.getTotalWorldTime() - questTime >= 24000; } else if (quest.repeat == EnumQuestRepeat.MCWEEKLY) { return player.worldObj.getTotalWorldTime() - questTime >= 168000; + } else if (quest.repeat == EnumQuestRepeat.MCCUSTOM) { + return player.worldObj.getTotalWorldTime() - questTime >= quest.customCooldown; } else if (quest.repeat == EnumQuestRepeat.RLDAILY) { return System.currentTimeMillis() - questTime >= 86400000; } else if (quest.repeat == EnumQuestRepeat.RLWEEKLY) { return System.currentTimeMillis() - questTime >= 604800000; + } else if (quest.repeat == EnumQuestRepeat.RLCUSTOM) { + return System.currentTimeMillis() - questTime >= quest.customCooldown; } return false; } From f6114dfa5e06c7a651587f9c92ae0abbb3ec42dd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 09:43:12 +0000 Subject: [PATCH 24/35] Fix NPC biome spawn settings not persisting after editing When users modified biomes, dimensions, or spawning options through the sub-GUI dialogs, changes were only stored in client memory and never sent to the server. This caused spawn configurations to appear correct in the UI but fail to work after server restarts. The closeSubGui() method now calls save() when SubGuiNpcBiomes, SubGuiNpcDimensions, or SubGuiSpawningOptions dialogs close, ensuring changes are immediately sent to the server via NaturalSpawnSavePacket. Added @Override annotation for consistency with other GUI classes. --- .../noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index a5ebd6e1f..41e105a38 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -262,6 +262,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I public void setSelected(String selected) { } + @Override public void closeSubGui(SubGuiInterface gui) { super.closeSubGui(gui); if (gui instanceof GuiNpcMobSpawnerSelector && this.spawnEntryScroll.hasSelected()) { @@ -270,6 +271,9 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I this.spawn.spawnCompounds.put(selected, selector.getCompound()); initGui(); } + if (gui instanceof SubGuiNpcBiomes || gui instanceof SubGuiNpcDimensions || gui instanceof SubGuiSpawningOptions) { + save(); + } } @Override From 12696996a6ae847c521bdd4621e4402984e59dd4 Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 3 Jan 2026 01:23:45 -0500 Subject: [PATCH 25/35] 1.10.3 --- build.gradle | 2 +- src/main/java/noppes/npcs/CustomNpcs.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d3f9587ca..b76ad9d0c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.10.3-beta" +version = "1.10.3" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index cea440c0a..4b5750c6a 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -100,7 +100,7 @@ import noppes.npcs.scripted.NpcAPI; import java.io.File; import java.util.Set; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3-beta") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") From c0f2846907ecc013360b4ec77c4667e34ad5a26c Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 3 Jan 2026 01:23:56 -0500 Subject: [PATCH 26/35] Update api --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index d003d6d87..305a3778c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit d003d6d870db13d56fce223050a06faf15050eae +Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 From 47fc4ebcc19fb46319a4a2f1873b143871e9f784 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Jan 2026 21:12:55 +0000 Subject: [PATCH 27/35] Add comprehensive implementation plan for CNPC+ 1.11 Market Update This plan covers three major features: - Auction House: New Auctioneer NPC role with global auction system - Currency System: VaultAPI integration for Bukkit hybrid servers - Trader Enhancements: Stock/inventory system with reset timers Includes detailed architecture, data structures, and implementation order. --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 1871 ++++++++++++++++++++++++++ 1 file changed, 1871 insertions(+) create mode 100644 docs/CNPC_1.11_MARKET_UPDATE_PLAN.md diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md new file mode 100644 index 000000000..fb1c33158 --- /dev/null +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -0,0 +1,1871 @@ +# CustomNPC+ 1.11 Market Update - Implementation Plan + +## Overview + +This document outlines the implementation plan for the CustomNPC+ 1.11 update, focusing on three major features: + +1. **Auction House** - A new NPC role allowing players to access a global auction system +2. **Currency System** - Built-in currency integration with VaultAPI for Bukkit hybrid servers +3. **Trader Enhancements** - Stock/Inventory system with per-player/per-server options and reset timers + +--- + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Feature 1: Currency System](#feature-1-currency-system) +3. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) +4. [Feature 3: Auction House](#feature-3-auction-house) +5. [Implementation Order](#implementation-order) +6. [File Summary](#file-summary) +7. [Testing Strategy](#testing-strategy) + +--- + +## Architecture Overview + +### Current Codebase Patterns + +The implementation will follow existing patterns found in the codebase: + +| Pattern | Used By | Will Be Applied To | +|---------|---------|-------------------| +| Singleton Controller | `BankController`, `QuestController` | `AuctionController`, `CurrencyController` | +| Role Interface | `RoleTrader`, `RoleBank` | `RoleAuctioneer` | +| Player Data Extension | `PlayerBankData`, `PlayerQuestData` | `PlayerAuctionData`, `PlayerCurrencyData` | +| Cooldown System | `Quest.repeat`, `JobItemGiver` | Trader Stock Reset | +| Optional Addon | `PixelmonHelper`, `DBCAddon` | `VaultHelper` | +| NBT Serialization | All data classes | All new data classes | +| Cache System | `Market.java`, `CacheHashMap` | Auction listings cache | + +### Package Structure for New Features + +``` +noppes/npcs/ +├── controllers/ +│ ├── AuctionController.java # Manages global auction house +│ ├── CurrencyController.java # Manages currency settings +│ └── data/ +│ ├── AuctionListing.java # Single auction listing +│ ├── AuctionData.java # World auction data +│ ├── PlayerAuctionData.java # Per-player auction history +│ ├── PlayerCurrencyData.java # Per-player currency balance +│ └── TraderStock.java # Trader stock configuration +├── roles/ +│ └── RoleAuctioneer.java # Auctioneer NPC role +├── containers/ +│ ├── ContainerAuction*.java # Auction GUI containers +│ └── ContainerNPCTraderStock.java # Stock management container +├── client/gui/ +│ ├── player/ +│ │ └── GuiAuction*.java # Player-facing auction GUIs +│ └── roles/ +│ └── GuiNpcAuctioneerSetup.java # Auctioneer configuration GUI + +kamkeel/npcs/ +├── controllers/ +│ └── CurrencyController.java # Currency management +├── network/packets/ +│ ├── data/ +│ │ ├── AuctionSyncPacket.java # Sync auction data +│ │ └── CurrencySyncPacket.java # Sync currency data +│ └── player/ +│ ├── AuctionActionPacket.java # Auction actions (bid, buy, sell) +│ └── CurrencyActionPacket.java # Currency transactions +└── compat/ + └── VaultHelper.java # VaultAPI integration +``` + +--- + +## Feature 1: Currency System + +### 1.1 Overview + +The currency system provides a unified economy layer that can: +- Use VaultAPI when available on Bukkit hybrid servers (Cauldron, Thermos, Mohist) +- Fall back to an internal item-based currency when VaultAPI is unavailable +- Be configured per-world or globally + +### 1.2 VaultAPI Integration + +#### 1.2.1 Dependency Setup + +**File: `dependencies.gradle`** +```gradle +// VaultAPI - compile-only for optional integration +compileOnly("net.milkbowl.vault:VaultAPI:1.4") { + transitive = false +} +``` + +**File: `repositories.gradle`** +```gradle +maven { + name = "vault-repo" + url = "https://nexus.hc.to/content/repositories/pub_releases/" +} +``` + +> **Note:** VaultAPI 1.4.1 works with 1.7.10 Bukkit hybrid servers. The dependency is compile-only so the mod works without Vault present. + +#### 1.2.2 VaultHelper Class + +**File: `src/main/java/kamkeel/npcs/compat/VaultHelper.java`** + +```java +package kamkeel.npcs.compat; + +import cpw.mods.fml.common.Loader; +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.LogWriter; +import org.bukkit.Bukkit; +import org.bukkit.plugin.RegisteredServiceProvider; + +public class VaultHelper { + public static boolean Enabled = false; + public static boolean Available = false; + + // Cached via reflection + private static Object economy = null; + private static Class economyClass = null; + private static java.lang.reflect.Method getBalance; + private static java.lang.reflect.Method withdrawPlayer; + private static java.lang.reflect.Method depositPlayer; + private static java.lang.reflect.Method hasMethod; + private static java.lang.reflect.Method format; + + public static void load() { + // Check if we're on a Bukkit hybrid server + try { + Class.forName("org.bukkit.Bukkit"); + Available = true; + } catch (ClassNotFoundException e) { + Available = false; + return; + } + + // Try to hook into Vault + try { + Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); + economyClass = vaultClass; + + RegisteredServiceProvider rsp = Bukkit.getServicesManager() + .getRegistration(vaultClass); + + if (rsp != null) { + economy = rsp.getProvider(); + + // Cache reflection methods + getBalance = economyClass.getMethod("getBalance", String.class); + withdrawPlayer = economyClass.getMethod("withdrawPlayer", + String.class, double.class); + depositPlayer = economyClass.getMethod("depositPlayer", + String.class, double.class); + hasMethod = economyClass.getMethod("has", String.class, double.class); + format = economyClass.getMethod("format", double.class); + + Enabled = true; + LogWriter.info("VaultAPI Economy hooked successfully!"); + } + } catch (Exception e) { + LogWriter.info("VaultAPI not available: " + e.getMessage()); + Enabled = false; + } + } + + // Economy Methods + public static double getBalance(EntityPlayer player) { + if (!Enabled) return 0.0; + try { + return (Double) getBalance.invoke(economy, player.getCommandSenderName()); + } catch (Exception e) { + LogWriter.except(e); + return 0.0; + } + } + + public static boolean withdraw(EntityPlayer player, double amount) { + if (!Enabled) return false; + try { + Object response = withdrawPlayer.invoke(economy, + player.getCommandSenderName(), amount); + return (Boolean) response.getClass() + .getMethod("transactionSuccess").invoke(response); + } catch (Exception e) { + LogWriter.except(e); + return false; + } + } + + public static boolean deposit(EntityPlayer player, double amount) { + if (!Enabled) return false; + try { + Object response = depositPlayer.invoke(economy, + player.getCommandSenderName(), amount); + return (Boolean) response.getClass() + .getMethod("transactionSuccess").invoke(response); + } catch (Exception e) { + LogWriter.except(e); + return false; + } + } + + public static boolean has(EntityPlayer player, double amount) { + if (!Enabled) return false; + try { + return (Boolean) hasMethod.invoke(economy, + player.getCommandSenderName(), amount); + } catch (Exception e) { + LogWriter.except(e); + return false; + } + } + + public static String formatCurrency(double amount) { + if (!Enabled) return String.format("%.2f", amount); + try { + return (String) format.invoke(economy, amount); + } catch (Exception e) { + return String.format("%.2f", amount); + } + } +} +``` + +#### 1.2.3 Initialization Hook + +**File: `src/main/java/noppes/npcs/CustomNpcs.java`** + +Add to `load()` method after AddonManager: +```java +// Initialize VaultAPI integration (for Bukkit hybrid servers) +VaultHelper.load(); +``` + +### 1.3 Currency Controller + +**File: `src/main/java/kamkeel/npcs/controllers/CurrencyController.java`** + +```java +package kamkeel.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import kamkeel.npcs.compat.VaultHelper; +import noppes.npcs.NoppesUtilPlayer; + +public class CurrencyController { + private static CurrencyController instance; + + // Configuration + public boolean useVaultIfAvailable = true; + public ItemStack fallbackCurrency = null; // Item to use when Vault unavailable + public double itemToVaultRatio = 1.0; // 1 item = X vault currency + + public static CurrencyController getInstance() { + if (instance == null) { + instance = new CurrencyController(); + } + return instance; + } + + /** + * Check if Vault economy is being used + */ + public boolean isUsingVault() { + return useVaultIfAvailable && VaultHelper.Enabled; + } + + /** + * Get player's balance (Vault or item count) + */ + public double getBalance(EntityPlayer player) { + if (isUsingVault()) { + return VaultHelper.getBalance(player); + } + // Fall back to counting items + if (fallbackCurrency != null) { + return NoppesUtilPlayer.countItems(player, fallbackCurrency, false, false); + } + return 0.0; + } + + /** + * Check if player can afford amount + */ + public boolean canAfford(EntityPlayer player, double amount) { + if (isUsingVault()) { + return VaultHelper.has(player, amount); + } + return getBalance(player) >= amount; + } + + /** + * Withdraw currency from player + */ + public boolean withdraw(EntityPlayer player, double amount) { + if (isUsingVault()) { + return VaultHelper.withdraw(player, amount); + } + // Remove items + if (fallbackCurrency != null) { + ItemStack toRemove = fallbackCurrency.copy(); + toRemove.stackSize = (int) Math.ceil(amount / itemToVaultRatio); + return NoppesUtilPlayer.consumeItem(player, toRemove, false, false); + } + return false; + } + + /** + * Deposit currency to player + */ + public boolean deposit(EntityPlayer player, double amount) { + if (isUsingVault()) { + return VaultHelper.deposit(player, amount); + } + // Give items + if (fallbackCurrency != null) { + ItemStack toGive = fallbackCurrency.copy(); + toGive.stackSize = (int) Math.floor(amount / itemToVaultRatio); + return player.inventory.addItemStackToInventory(toGive); + } + return false; + } + + /** + * Format currency for display + */ + public String format(double amount) { + if (isUsingVault()) { + return VaultHelper.formatCurrency(amount); + } + return String.format("%.0f", amount); + } + + // NBT Persistence + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("UseVault", useVaultIfAvailable); + compound.setDouble("ItemRatio", itemToVaultRatio); + if (fallbackCurrency != null) { + compound.setTag("FallbackItem", fallbackCurrency.writeToNBT(new NBTTagCompound())); + } + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + useVaultIfAvailable = compound.getBoolean("UseVault"); + itemToVaultRatio = compound.getDouble("ItemRatio"); + if (compound.hasKey("FallbackItem")) { + fallbackCurrency = ItemStack.loadItemStackFromNBT( + compound.getCompoundTag("FallbackItem")); + } + } +} +``` + +### 1.4 Player Currency Data + +**File: `src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java`** + +```java +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import java.util.HashMap; + +public class PlayerCurrencyData { + private final PlayerData parent; + + // Transaction history (for auditing/display) + public HashMap transactionHistory = new HashMap<>(); + public double totalEarned = 0.0; + public double totalSpent = 0.0; + + public PlayerCurrencyData(PlayerData parent) { + this.parent = parent; + } + + public void recordTransaction(double amount, boolean isEarning) { + long timestamp = System.currentTimeMillis(); + transactionHistory.put(timestamp, isEarning ? amount : -amount); + if (isEarning) { + totalEarned += amount; + } else { + totalSpent += amount; + } + // Limit history size + if (transactionHistory.size() > 100) { + // Remove oldest entries + Long oldest = transactionHistory.keySet().stream() + .min(Long::compare).orElse(null); + if (oldest != null) { + transactionHistory.remove(oldest); + } + } + } + + public void loadNBTData(NBTTagCompound compound) { + if (compound.hasKey("CurrencyData")) { + NBTTagCompound data = compound.getCompoundTag("CurrencyData"); + totalEarned = data.getDouble("TotalEarned"); + totalSpent = data.getDouble("TotalSpent"); + } + } + + public void saveNBTData(NBTTagCompound compound) { + NBTTagCompound data = new NBTTagCompound(); + data.setDouble("TotalEarned", totalEarned); + data.setDouble("TotalSpent", totalSpent); + compound.setTag("CurrencyData", data); + } +} +``` + +### 1.5 Configuration UI + +Add currency configuration to the global settings GUI: + +**File: `src/main/java/noppes/npcs/client/gui/global/GuiNPCManageCurrency.java`** + +This GUI will allow server operators to: +- Enable/disable Vault integration +- Set fallback currency item +- Configure item-to-Vault ratio +- View server economy statistics + +--- + +## Feature 2: Trader Enhancements + +### 2.1 Overview + +Enhance the existing `RoleTrader` with: +- **Stock System**: Limited inventory that depletes when players buy +- **Per-Player vs Per-Server Stock**: Configure whether stock is shared or individual +- **Stock Reset Timer**: Automatic restocking based on configurable cooldown (like quest cooldowns) +- **Currency Integration**: Use Vault currency alongside or instead of item-based trading + +### 2.2 Trader Stock Configuration + +#### 2.2.1 Stock Data Class + +**File: `src/main/java/noppes/npcs/controllers/data/TraderStock.java`** + +```java +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import java.util.HashMap; + +public class TraderStock { + // Stock configuration + public boolean enableStock = false; + public boolean perPlayer = false; // false = per-server, true = per-player + + // Reset configuration (mirrors Quest cooldown system) + public EnumStockReset resetType = EnumStockReset.NONE; + public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types + + // Stock quantities per slot (max stock) + public int[] maxStock = new int[18]; + + // Current stock (per-server mode) + public int[] currentStock = new int[18]; + public long lastResetTime = 0; + + // Per-player stock tracking + public HashMap playerStock = new HashMap<>(); + + public TraderStock() { + for (int i = 0; i < 18; i++) { + maxStock[i] = -1; // -1 = unlimited + currentStock[i] = -1; + } + } + + /** + * Get available stock for a slot + */ + public int getAvailableStock(int slot, String playerName) { + if (!enableStock || maxStock[slot] < 0) { + return Integer.MAX_VALUE; // Unlimited + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.get(playerName); + if (pStock == null) { + return maxStock[slot]; // Not purchased yet + } + return pStock.getStock(slot, maxStock[slot]); + } + + return currentStock[slot]; + } + + /** + * Consume stock when purchase is made + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!enableStock || maxStock[slot] < 0) { + return true; // Unlimited + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.computeIfAbsent( + playerName, k -> new PlayerTraderStock()); + return pStock.consumeStock(slot, maxStock[slot], amount); + } + + if (currentStock[slot] >= amount) { + currentStock[slot] -= amount; + return true; + } + return false; + } + + /** + * Check if stock should reset + */ + public boolean shouldReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return false; + } + + long elapsed = currentTime - lastResetTime; + long resetInterval = getResetInterval(); + + return elapsed >= resetInterval; + } + + /** + * Reset all stock to max values + */ + public void resetStock(long currentTime) { + lastResetTime = currentTime; + + for (int i = 0; i < 18; i++) { + currentStock[i] = maxStock[i]; + } + + // Clear per-player stock + playerStock.clear(); + } + + /** + * Get reset interval in milliseconds + */ + public long getResetInterval() { + switch (resetType) { + case MCDAILY: + return 24000 * 50; // 24000 ticks * 50ms + case MCWEEKLY: + return 168000 * 50; + case MCCUSTOM: + return customResetTime * 50; + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + case RLCUSTOM: + return customResetTime; + default: + return Long.MAX_VALUE; + } + } + + /** + * Get time until next reset (for display) + */ + public long getTimeUntilReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return -1; + } + long elapsed = currentTime - lastResetTime; + long interval = getResetInterval(); + return Math.max(0, interval - elapsed); + } + + // NBT Serialization + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("EnableStock", enableStock); + compound.setBoolean("PerPlayer", perPlayer); + compound.setInteger("ResetType", resetType.ordinal()); + compound.setLong("CustomResetTime", customResetTime); + compound.setIntArray("MaxStock", maxStock); + compound.setIntArray("CurrentStock", currentStock); + compound.setLong("LastResetTime", lastResetTime); + + // Save per-player stock + NBTTagList playerList = new NBTTagList(); + for (Map.Entry entry : playerStock.entrySet()) { + NBTTagCompound playerTag = new NBTTagCompound(); + playerTag.setString("Player", entry.getKey()); + entry.getValue().writeToNBT(playerTag); + playerList.appendTag(playerTag); + } + compound.setTag("PlayerStock", playerList); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + enableStock = compound.getBoolean("EnableStock"); + perPlayer = compound.getBoolean("PerPlayer"); + resetType = EnumStockReset.values()[compound.getInteger("ResetType")]; + customResetTime = compound.getLong("CustomResetTime"); + maxStock = compound.getIntArray("MaxStock"); + currentStock = compound.getIntArray("CurrentStock"); + lastResetTime = compound.getLong("LastResetTime"); + + // Load per-player stock + NBTTagList playerList = compound.getTagList("PlayerStock", 10); + for (int i = 0; i < playerList.tagCount(); i++) { + NBTTagCompound playerTag = playerList.getCompoundTagAt(i); + String playerName = playerTag.getString("Player"); + PlayerTraderStock pStock = new PlayerTraderStock(); + pStock.readFromNBT(playerTag); + playerStock.put(playerName, pStock); + } + } +} + +// Inner class for per-player stock tracking +class PlayerTraderStock { + public int[] purchasedAmounts = new int[18]; + public long lastPurchaseTime = 0; + + public int getStock(int slot, int maxStock) { + return Math.max(0, maxStock - purchasedAmounts[slot]); + } + + public boolean consumeStock(int slot, int maxStock, int amount) { + if (getStock(slot, maxStock) >= amount) { + purchasedAmounts[slot] += amount; + lastPurchaseTime = System.currentTimeMillis(); + return true; + } + return false; + } + + public void writeToNBT(NBTTagCompound compound) { + compound.setIntArray("Purchased", purchasedAmounts); + compound.setLong("LastPurchase", lastPurchaseTime); + } + + public void readFromNBT(NBTTagCompound compound) { + purchasedAmounts = compound.getIntArray("Purchased"); + lastPurchaseTime = compound.getLong("LastPurchase"); + } +} +``` + +#### 2.2.2 Stock Reset Enum + +**File: `src/main/java/noppes/npcs/constants/EnumStockReset.java`** + +```java +package noppes.npcs.constants; + +public enum EnumStockReset { + NONE, // Never resets + MCDAILY, // Every 24000 MC ticks (1 MC day) + MCWEEKLY, // Every 168000 MC ticks (7 MC days) + MCCUSTOM, // Custom MC tick interval + RLDAILY, // Every 24 real hours + RLWEEKLY, // Every 7 real days + RLCUSTOM // Custom real-time interval +} +``` + +### 2.3 RoleTrader Modifications + +**File: `src/main/java/noppes/npcs/roles/RoleTrader.java`** + +Add the following to `RoleTrader`: + +```java +// Add new fields +public TraderStock stock = new TraderStock(); +public boolean useCurrency = false; // Use Vault/Currency system +public double[] slotPrices = new double[18]; // Currency prices per slot + +// Modify interact() to check stock reset +@Override +public void interact(EntityPlayer player) { + // Check for stock reset before opening GUI + if (stock.enableStock) { + long currentTime = stock.resetType.name().startsWith("RL") + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + // Save to market if using shared market + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + } + } + + npc.say(player, npc.advanced.getInteractLine()); + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); +} + +// Modify writeToNBT +@Override +public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) { + // ... existing code ... + + // Add stock data + nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); + nbttagcompound.setBoolean("UseCurrency", useCurrency); + + // Save currency prices + NBTTagList priceList = new NBTTagList(); + for (double price : slotPrices) { + NBTTagCompound priceTag = new NBTTagCompound(); + priceTag.setDouble("Price", price); + priceList.appendTag(priceTag); + } + nbttagcompound.setTag("SlotPrices", priceList); + + return nbttagcompound; +} + +// Modify readFromNBT +@Override +public void readFromNBT(NBTTagCompound nbttagcompound) { + // ... existing code ... + + // Load stock data + if (nbttagcompound.hasKey("Stock")) { + stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); + } + useCurrency = nbttagcompound.getBoolean("UseCurrency"); + + // Load currency prices + if (nbttagcompound.hasKey("SlotPrices")) { + NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); + for (int i = 0; i < priceList.tagCount() && i < 18; i++) { + slotPrices[i] = priceList.getCompoundTagAt(i).getDouble("Price"); + } + } +} +``` + +### 2.4 ContainerNPCTrader Modifications + +**File: `src/main/java/noppes/npcs/containers/ContainerNPCTrader.java`** + +Modify `slotClick()` to handle stock and currency: + +```java +public ItemStack slotClick(int slotIndex, EntityPlayer player) { + RoleTrader role = (RoleTrader) npc.roleInterface; + String playerName = player.getCommandSenderName(); + + // ... existing validation ... + + // Check stock availability + if (role.stock.enableStock) { + int available = role.stock.getAvailableStock(slotIndex, playerName); + if (available <= 0) { + // Out of stock - send message to player + return null; + } + } + + // Handle currency-based purchase + if (role.useCurrency && role.slotPrices[slotIndex] > 0) { + double price = role.slotPrices[slotIndex]; + CurrencyController currency = CurrencyController.getInstance(); + + if (!currency.canAfford(player, price)) { + // Can't afford - send message + return null; + } + + // Withdraw currency + if (!currency.withdraw(player, price)) { + return null; + } + } else { + // ... existing item-based currency logic ... + } + + // Consume stock + if (role.stock.enableStock) { + role.stock.consumeStock(slotIndex, playerName, 1); + } + + // ... existing item delivery logic ... + + // Save if using market + if (!role.marketName.isEmpty()) { + Market.save(role, role.marketName); + } + + return soldItem; +} +``` + +### 2.5 Trader Setup GUI Additions + +**File: `src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java`** + +Add new configuration options: + +```java +// Add buttons/fields for: +// - "Enable Stock" toggle +// - "Per Player" / "Per Server" radio buttons +// - "Reset Type" dropdown (None, MC Daily, MC Weekly, etc.) +// - "Custom Reset Time" field +// - "Use Currency" toggle +// - Per-slot stock quantity inputs +// - Per-slot currency price inputs + +private void initStockGui() { + addButton(new GuiNpcButton(20, x, y, 80, 20, + "Enable Stock: " + (role.stock.enableStock ? "Yes" : "No"))); + + addButton(new GuiNpcButton(21, x, y + 22, 80, 20, + role.stock.perPlayer ? "Per Player" : "Per Server")); + + String[] resetTypes = {"None", "MC Daily", "MC Weekly", "MC Custom", + "RL Daily", "RL Weekly", "RL Custom"}; + addButton(new GuiNpcButton(22, x, y + 44, 80, 20, resetTypes, + role.stock.resetType.ordinal())); + + addButton(new GuiNpcButton(23, x, y + 66, 80, 20, + "Use Currency: " + (role.useCurrency ? "Yes" : "No"))); +} +``` + +--- + +## Feature 3: Auction House + +### 3.1 Overview + +The Auction House is a global trading system where players can: +- **List items for sale** at fixed prices or as auctions with bidding +- **Browse listings** by category, search, and filter +- **Bid on auctions** with automatic outbid notifications +- **Buy instantly** for buy-it-now prices +- **Manage their listings** (cancel, modify) +- **Collect proceeds and expired items** via mailbox or direct claim + +### 3.2 Auction Data Structures + +#### 3.2.1 AuctionListing + +**File: `src/main/java/noppes/npcs/controllers/data/AuctionListing.java`** + +```java +package noppes.npcs.controllers.data; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import java.util.UUID; + +public class AuctionListing { + public int id; + public UUID sellerUUID; + public String sellerName; + public ItemStack item; + + // Pricing + public double startingPrice; + public double buyoutPrice; // 0 = no buyout + public double currentBid; + public UUID highBidderUUID; + public String highBidderName; + + // Timing + public long listingTime; + public long expirationTime; + public EnumAuctionDuration duration; + + // Status + public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; + + // Category + public EnumAuctionCategory category = EnumAuctionCategory.MISC; + + public AuctionListing() {} + + public AuctionListing(UUID seller, String sellerName, ItemStack item, + double startPrice, double buyout, EnumAuctionDuration duration) { + this.sellerUUID = seller; + this.sellerName = sellerName; + this.item = item.copy(); + this.startingPrice = startPrice; + this.buyoutPrice = buyout; + this.currentBid = startPrice; + this.duration = duration; + this.listingTime = System.currentTimeMillis(); + this.expirationTime = listingTime + duration.getMillis(); + this.category = categorizeItem(item); + } + + /** + * Check if auction has expired + */ + public boolean isExpired() { + return System.currentTimeMillis() >= expirationTime; + } + + /** + * Get time remaining in milliseconds + */ + public long getTimeRemaining() { + return Math.max(0, expirationTime - System.currentTimeMillis()); + } + + /** + * Place a bid + */ + public boolean placeBid(UUID bidder, String bidderName, double amount) { + if (status != EnumAuctionStatus.ACTIVE) return false; + if (amount <= currentBid) return false; + if (bidder.equals(sellerUUID)) return false; // Can't bid on own item + + // Minimum bid increment (5% or 1, whichever is greater) + double minIncrement = Math.max(1, currentBid * 0.05); + if (amount < currentBid + minIncrement) return false; + + highBidderUUID = bidder; + highBidderName = bidderName; + currentBid = amount; + + return true; + } + + /** + * Execute buyout + */ + public boolean buyout(UUID buyer, String buyerName) { + if (status != EnumAuctionStatus.ACTIVE) return false; + if (buyoutPrice <= 0) return false; // No buyout available + if (buyer.equals(sellerUUID)) return false; + + highBidderUUID = buyer; + highBidderName = buyerName; + currentBid = buyoutPrice; + status = EnumAuctionStatus.SOLD; + + return true; + } + + /** + * Cancel listing (seller only, before any bids) + */ + public boolean cancel(UUID requester) { + if (!requester.equals(sellerUUID)) return false; + if (highBidderUUID != null) return false; // Has bids, can't cancel + + status = EnumAuctionStatus.CANCELLED; + return true; + } + + /** + * Auto-categorize item based on type + */ + private EnumAuctionCategory categorizeItem(ItemStack item) { + // Implement item categorization logic + // Check for weapons, armor, tools, food, blocks, etc. + return EnumAuctionCategory.MISC; + } + + // NBT Serialization + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setInteger("Id", id); + compound.setString("SellerUUID", sellerUUID.toString()); + compound.setString("SellerName", sellerName); + compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); + compound.setDouble("StartPrice", startingPrice); + compound.setDouble("BuyoutPrice", buyoutPrice); + compound.setDouble("CurrentBid", currentBid); + if (highBidderUUID != null) { + compound.setString("BidderUUID", highBidderUUID.toString()); + compound.setString("BidderName", highBidderName); + } + compound.setLong("ListingTime", listingTime); + compound.setLong("ExpirationTime", expirationTime); + compound.setInteger("Duration", duration.ordinal()); + compound.setInteger("Status", status.ordinal()); + compound.setInteger("Category", category.ordinal()); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getInteger("Id"); + sellerUUID = UUID.fromString(compound.getString("SellerUUID")); + sellerName = compound.getString("SellerName"); + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + startingPrice = compound.getDouble("StartPrice"); + buyoutPrice = compound.getDouble("BuyoutPrice"); + currentBid = compound.getDouble("CurrentBid"); + if (compound.hasKey("BidderUUID")) { + highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); + highBidderName = compound.getString("BidderName"); + } + listingTime = compound.getLong("ListingTime"); + expirationTime = compound.getLong("ExpirationTime"); + duration = EnumAuctionDuration.values()[compound.getInteger("Duration")]; + status = EnumAuctionStatus.values()[compound.getInteger("Status")]; + category = EnumAuctionCategory.values()[compound.getInteger("Category")]; + } +} +``` + +#### 3.2.2 Supporting Enums + +**File: `src/main/java/noppes/npcs/constants/EnumAuctionDuration.java`** + +```java +package noppes.npcs.constants; + +public enum EnumAuctionDuration { + SHORT(2 * 60 * 60 * 1000L), // 2 hours + MEDIUM(12 * 60 * 60 * 1000L), // 12 hours + LONG(24 * 60 * 60 * 1000L), // 24 hours + VERY_LONG(48 * 60 * 60 * 1000L); // 48 hours + + private final long millis; + + EnumAuctionDuration(long millis) { + this.millis = millis; + } + + public long getMillis() { + return millis; + } + + public String getDisplayName() { + switch (this) { + case SHORT: return "2 Hours"; + case MEDIUM: return "12 Hours"; + case LONG: return "24 Hours"; + case VERY_LONG: return "48 Hours"; + default: return "Unknown"; + } + } +} +``` + +**File: `src/main/java/noppes/npcs/constants/EnumAuctionStatus.java`** + +```java +package noppes.npcs.constants; + +public enum EnumAuctionStatus { + ACTIVE, // Currently listed + SOLD, // Successfully sold (buyout or auction end with bids) + EXPIRED, // Ended without bids + CANCELLED, // Cancelled by seller + CLAIMED // Item/money has been claimed +} +``` + +**File: `src/main/java/noppes/npcs/constants/EnumAuctionCategory.java`** + +```java +package noppes.npcs.constants; + +public enum EnumAuctionCategory { + WEAPONS, + ARMOR, + TOOLS, + FOOD, + POTIONS, + BLOCKS, + MATERIALS, + ENCHANTED, + MISC +} +``` + +### 3.3 Auction Controller + +**File: `src/main/java/noppes/npcs/controllers/AuctionController.java`** + +```java +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.constants.*; + +import java.io.File; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class AuctionController { + private static AuctionController instance; + + // All listings (thread-safe for async operations) + public ConcurrentHashMap listings = new ConcurrentHashMap<>(); + + // Index by seller for quick lookup + private Map> sellerIndex = new ConcurrentHashMap<>(); + + // Pending claims (seller -> items/money to claim) + public Map> pendingClaims = new ConcurrentHashMap<>(); + + private int nextId = 1; + private long lastSaveTime = 0; + private static final long SAVE_INTERVAL = 60000; // Save every minute + + // Configuration + public double listingFee = 0.0; // Fee to list item + public double saleTax = 0.05; // 5% tax on sales + public int maxListingsPerPlayer = 20; + public boolean requireAuctioneer = true; // Require Auctioneer NPC to access + + public static AuctionController getInstance() { + if (instance == null) { + instance = new AuctionController(); + instance.load(); + } + return instance; + } + + /** + * Create a new auction listing + */ + public AuctionListing createListing(EntityPlayer seller, ItemStack item, + double startPrice, double buyout, + EnumAuctionDuration duration) { + UUID sellerUUID = seller.getUniqueID(); + + // Check max listings + Set sellerListings = sellerIndex.get(sellerUUID); + if (sellerListings != null && sellerListings.size() >= maxListingsPerPlayer) { + return null; + } + + // Charge listing fee + if (listingFee > 0) { + CurrencyController currency = CurrencyController.getInstance(); + if (!currency.withdraw(seller, listingFee)) { + return null; + } + } + + // Create listing + AuctionListing listing = new AuctionListing( + sellerUUID, seller.getCommandSenderName(), + item, startPrice, buyout, duration + ); + listing.id = nextId++; + + // Add to indexes + listings.put(listing.id, listing); + sellerIndex.computeIfAbsent(sellerUUID, k -> new HashSet<>()).add(listing.id); + + // Remove item from seller inventory + item.stackSize = 0; + + scheduleSave(); + return listing; + } + + /** + * Place a bid on an auction + */ + public boolean placeBid(EntityPlayer bidder, int listingId, double amount) { + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.isExpired()) return false; + + CurrencyController currency = CurrencyController.getInstance(); + + // Check if bidder can afford + if (!currency.canAfford(bidder, amount)) return false; + + // Refund previous bidder + if (listing.highBidderUUID != null) { + addClaim(listing.highBidderUUID, listing.highBidderName, + null, listing.currentBid, "Outbid refund for: " + + listing.item.getDisplayName()); + } + + // Place bid + if (!listing.placeBid(bidder.getUniqueID(), + bidder.getCommandSenderName(), amount)) { + return false; + } + + // Hold currency from bidder + currency.withdraw(bidder, amount); + + scheduleSave(); + return true; + } + + /** + * Buyout an auction + */ + public boolean buyout(EntityPlayer buyer, int listingId) { + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.buyoutPrice <= 0) return false; + + CurrencyController currency = CurrencyController.getInstance(); + + // Check if buyer can afford + if (!currency.canAfford(buyer, listing.buyoutPrice)) return false; + + // Refund previous bidder if any + if (listing.highBidderUUID != null) { + addClaim(listing.highBidderUUID, listing.highBidderName, + null, listing.currentBid, "Buyout refund for: " + + listing.item.getDisplayName()); + } + + // Execute buyout + if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { + return false; + } + + // Withdraw from buyer + currency.withdraw(buyer, listing.buyoutPrice); + + // Calculate seller proceeds (minus tax) + double proceeds = listing.buyoutPrice * (1 - saleTax); + + // Add claims for both parties + addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, + "Sale of: " + listing.item.getDisplayName()); + addClaim(buyer.getUniqueID(), buyer.getCommandSenderName(), + listing.item, 0, "Purchased: " + listing.item.getDisplayName()); + + scheduleSave(); + return true; + } + + /** + * Cancel a listing (seller only) + */ + public boolean cancelListing(EntityPlayer player, int listingId) { + AuctionListing listing = listings.get(listingId); + if (listing == null) return false; + + if (!listing.cancel(player.getUniqueID())) return false; + + // Return item to seller + addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, + "Cancelled listing: " + listing.item.getDisplayName()); + + scheduleSave(); + return true; + } + + /** + * Process expired auctions (called periodically) + */ + public void processExpiredAuctions() { + long now = System.currentTimeMillis(); + + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (!listing.isExpired()) continue; + + if (listing.highBidderUUID != null) { + // Auction successful - has bids + listing.status = EnumAuctionStatus.SOLD; + + double proceeds = listing.currentBid * (1 - saleTax); + addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, + "Auction sold: " + listing.item.getDisplayName()); + addClaim(listing.highBidderUUID, listing.highBidderName, + listing.item, 0, "Won auction: " + listing.item.getDisplayName()); + } else { + // No bids - return to seller + listing.status = EnumAuctionStatus.EXPIRED; + addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, + "Expired listing: " + listing.item.getDisplayName()); + } + } + + scheduleSave(); + } + + /** + * Get active listings with optional filters + */ + public List getActiveListings(EnumAuctionCategory category, + String search, + int page, int pageSize) { + return listings.values().stream() + .filter(l -> l.status == EnumAuctionStatus.ACTIVE) + .filter(l -> !l.isExpired()) + .filter(l -> category == null || l.category == category) + .filter(l -> search == null || search.isEmpty() || + l.item.getDisplayName().toLowerCase() + .contains(search.toLowerCase())) + .sorted(Comparator.comparingLong(l -> l.expirationTime)) + .skip((long) page * pageSize) + .limit(pageSize) + .collect(Collectors.toList()); + } + + /** + * Get player's own listings + */ + public List getPlayerListings(UUID playerUUID) { + Set ids = sellerIndex.get(playerUUID); + if (ids == null) return Collections.emptyList(); + + return ids.stream() + .map(listings::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Get pending claims for a player + */ + public List getPendingClaims(UUID playerUUID) { + return pendingClaims.getOrDefault(playerUUID, Collections.emptyList()); + } + + /** + * Claim pending items/money + */ + public boolean claim(EntityPlayer player, int claimIndex) { + List claims = pendingClaims.get(player.getUniqueID()); + if (claims == null || claimIndex >= claims.size()) return false; + + AuctionClaim claim = claims.get(claimIndex); + + // Give item or money + if (claim.item != null) { + if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + return false; // Inventory full + } + } + if (claim.money > 0) { + CurrencyController.getInstance().deposit(player, claim.money); + } + + claims.remove(claimIndex); + scheduleSave(); + return true; + } + + private void addClaim(UUID playerUUID, String playerName, + ItemStack item, double money, String description) { + AuctionClaim claim = new AuctionClaim(item, money, description); + pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(claim); + } + + // Persistence + private void scheduleSave() { + long now = System.currentTimeMillis(); + if (now - lastSaveTime > SAVE_INTERVAL) { + save(); + lastSaveTime = now; + } + } + + public void save() { + try { + NBTTagCompound compound = new NBTTagCompound(); + compound.setInteger("NextId", nextId); + compound.setDouble("ListingFee", listingFee); + compound.setDouble("SaleTax", saleTax); + compound.setInteger("MaxListings", maxListingsPerPlayer); + + // Save listings + NBTTagList listingList = new NBTTagList(); + for (AuctionListing listing : listings.values()) { + listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + compound.setTag("Listings", listingList); + + // Save claims + NBTTagList claimList = new NBTTagList(); + for (Map.Entry> entry : pendingClaims.entrySet()) { + NBTTagCompound playerClaims = new NBTTagCompound(); + playerClaims.setString("UUID", entry.getKey().toString()); + NBTTagList claims = new NBTTagList(); + for (AuctionClaim claim : entry.getValue()) { + claims.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + playerClaims.setTag("Claims", claims); + claimList.appendTag(playerClaims); + } + compound.setTag("PendingClaims", claimList); + + File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); + CompressedStreamTools.writeCompressed(compound, + new java.io.FileOutputStream(file)); + + } catch (Exception e) { + LogWriter.except(e); + } + } + + public void load() { + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); + if (!file.exists()) return; + + NBTTagCompound compound = CompressedStreamTools.readCompressed( + new java.io.FileInputStream(file)); + + nextId = compound.getInteger("NextId"); + listingFee = compound.getDouble("ListingFee"); + saleTax = compound.getDouble("SaleTax"); + maxListingsPerPlayer = compound.getInteger("MaxListings"); + + // Load listings + NBTTagList listingList = compound.getTagList("Listings", 10); + for (int i = 0; i < listingList.tagCount(); i++) { + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(listingList.getCompoundTagAt(i)); + listings.put(listing.id, listing); + sellerIndex.computeIfAbsent(listing.sellerUUID, k -> new HashSet<>()) + .add(listing.id); + } + + // Load claims + NBTTagList claimList = compound.getTagList("PendingClaims", 10); + for (int i = 0; i < claimList.tagCount(); i++) { + NBTTagCompound playerClaims = claimList.getCompoundTagAt(i); + UUID playerUUID = UUID.fromString(playerClaims.getString("UUID")); + NBTTagList claims = playerClaims.getTagList("Claims", 10); + List claimsList = new ArrayList<>(); + for (int j = 0; j < claims.tagCount(); j++) { + AuctionClaim claim = new AuctionClaim(); + claim.readFromNBT(claims.getCompoundTagAt(j)); + claimsList.add(claim); + } + pendingClaims.put(playerUUID, claimsList); + } + + } catch (Exception e) { + LogWriter.except(e); + } + } +} + +// Claim data structure +class AuctionClaim { + public ItemStack item; + public double money; + public String description; + public long timestamp; + + public AuctionClaim() {} + + public AuctionClaim(ItemStack item, double money, String description) { + this.item = item != null ? item.copy() : null; + this.money = money; + this.description = description; + this.timestamp = System.currentTimeMillis(); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + if (item != null) { + compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); + } + compound.setDouble("Money", money); + compound.setString("Description", description); + compound.setLong("Timestamp", timestamp); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("Item")) { + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } + money = compound.getDouble("Money"); + description = compound.getString("Description"); + timestamp = compound.getLong("Timestamp"); + } +} +``` + +### 3.4 Auctioneer Role + +**File: `src/main/java/noppes/npcs/roles/RoleAuctioneer.java`** + +```java +package noppes.npcs.roles; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.entity.EntityNPCInterface; + +public class RoleAuctioneer extends RoleInterface { + + // Configuration + public String auctionHouseName = "Global"; // For future multi-auction support + public boolean allowSelling = true; + public boolean allowBuying = true; + public boolean showExpiredListings = true; + + // Custom messages + public String welcomeMessage = ""; + public String sellDisabledMessage = "This auctioneer does not accept listings."; + public String buyDisabledMessage = "This auctioneer does not sell items."; + + public RoleAuctioneer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("AuctionHouse", auctionHouseName); + compound.setBoolean("AllowSelling", allowSelling); + compound.setBoolean("AllowBuying", allowBuying); + compound.setBoolean("ShowExpired", showExpiredListings); + compound.setString("WelcomeMsg", welcomeMessage); + compound.setString("SellDisabledMsg", sellDisabledMessage); + compound.setString("BuyDisabledMsg", buyDisabledMessage); + return compound; + } + + @Override + public void readFromNBT(NBTTagCompound compound) { + auctionHouseName = compound.getString("AuctionHouse"); + allowSelling = compound.getBoolean("AllowSelling"); + allowBuying = compound.getBoolean("AllowBuying"); + showExpiredListings = compound.getBoolean("ShowExpired"); + welcomeMessage = compound.getString("WelcomeMsg"); + sellDisabledMessage = compound.getString("SellDisabledMsg"); + buyDisabledMessage = compound.getString("BuyDisabledMsg"); + } + + @Override + public void interact(EntityPlayer player) { + // Show welcome message if set + if (!welcomeMessage.isEmpty()) { + npc.say(player, npc.advanced.getInteractLine()); + } + + // Open auction house GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); + } +} +``` + +### 3.5 Update EnumRoleType + +**File: `src/main/java/noppes/npcs/constants/EnumRoleType.java`** + +```java +public enum EnumRoleType { + None, + Trader, + Follower, + Bank, + Transporter, + Postman, + Companion, + Mount, + Auctioneer // Add this +} +``` + +### 3.6 Update DataAdvanced + +**File: `src/main/java/noppes/npcs/DataAdvanced.java`** + +Add to `setRole()` method: + +```java +else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) + npc.roleInterface = new RoleAuctioneer(npc); +``` + +### 3.7 Update EnumGuiType + +**File: `src/main/java/noppes/npcs/constants/EnumGuiType.java`** + +Add new GUI types: + +```java +// Auction House GUIs +PlayerAuction, // Main auction browse/buy interface +PlayerAuctionSell, // Create new listing +PlayerAuctionMyListings, // View own listings +PlayerAuctionClaims, // Claim items/money +AuctioneerSetup, // NPC configuration +ManageAuctions // Admin management +``` + +### 3.8 Auction GUI System + +The auction GUI system will consist of multiple interconnected screens: + +#### 3.8.1 Main Auction Browser + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionBrowse.java`** + +Features: +- Category filter tabs (Weapons, Armor, Tools, etc.) +- Search bar for item names +- Sort options (Time remaining, Price, Recently listed) +- Paginated listing display +- Quick buy/bid buttons + +#### 3.8.2 Create Listing GUI + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java`** + +Features: +- Item slot for the item to sell +- Starting price input +- Buyout price input (optional) +- Duration selector +- Listing fee display +- Confirm button + +#### 3.8.3 My Listings GUI + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionMyListings.java`** + +Features: +- List of player's active listings +- Current bid status +- Time remaining +- Cancel button (if no bids) + +#### 3.8.4 Claims GUI + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionClaims.java`** + +Features: +- List of pending claims (items won, money from sales, refunds) +- Claim button for each item +- Claim all button + +### 3.9 Network Packets + +#### 3.9.1 Auction Action Packet + +**File: `src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java`** + +```java +package kamkeel.npcs.network.packets.player; + +public class AuctionActionPacket extends AbstractPacket { + + public enum Action { + CREATE_LISTING, + PLACE_BID, + BUYOUT, + CANCEL, + CLAIM, + CLAIM_ALL, + BROWSE, + MY_LISTINGS + } + + private Action action; + private int listingId; + private double amount; + private NBTTagCompound itemData; + private int duration; + private double buyoutPrice; + private int category; + private String search; + private int page; + + // Implement sendData/receiveData... +} +``` + +#### 3.9.2 Auction Sync Packet + +**File: `src/main/java/kamkeel/npcs/network/packets/data/AuctionSyncPacket.java`** + +Used to send auction listing data to clients for GUI display. + +### 3.10 Periodic Tasks + +Add to server tick handler to process expired auctions: + +**File: `src/main/java/noppes/npcs/ServerTickHandler.java`** + +```java +private int auctionCheckCounter = 0; + +@SubscribeEvent +public void onServerTick(TickEvent.ServerTickEvent event) { + // ... existing code ... + + // Check expired auctions every 5 minutes (6000 ticks) + if (++auctionCheckCounter >= 6000) { + auctionCheckCounter = 0; + AuctionController.getInstance().processExpiredAuctions(); + } +} +``` + +--- + +## Implementation Order + +The features should be implemented in this order due to dependencies: + +### Phase 1: Currency System (Foundation) +1. Add VaultAPI dependency to build files +2. Create `VaultHelper.java` with reflection-based integration +3. Create `CurrencyController.java` +4. Add initialization hook in `CustomNpcs.java` +5. Create `PlayerCurrencyData.java` and integrate with `PlayerData` +6. Create currency configuration GUI +7. Test with and without Vault present + +### Phase 2: Trader Enhancements +1. Create `EnumStockReset.java` +2. Create `TraderStock.java` +3. Modify `RoleTrader.java` to add stock and currency fields +4. Modify `ContainerNPCTrader.java` to handle stock/currency +5. Update `GuiNpcTraderSetup.java` with new options +6. Update network packets for stock synchronization +7. Test stock depletion and reset timers + +### Phase 3: Auction House +1. Create auction enums (`EnumAuctionDuration`, `EnumAuctionStatus`, `EnumAuctionCategory`) +2. Create `AuctionListing.java` and `AuctionClaim` classes +3. Create `AuctionController.java` +4. Update `EnumRoleType.java` to add Auctioneer +5. Create `RoleAuctioneer.java` +6. Update `DataAdvanced.java` for role instantiation +7. Update `EnumGuiType.java` with auction GUIs +8. Create auction containers +9. Create auction GUI classes +10. Create network packets +11. Add periodic task for expired auctions +12. Create auctioneer setup GUI +13. Comprehensive testing + +--- + +## File Summary + +### New Files to Create + +| File | Description | +|------|-------------| +| `kamkeel/npcs/compat/VaultHelper.java` | VaultAPI integration | +| `kamkeel/npcs/controllers/CurrencyController.java` | Currency management | +| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data | +| `noppes/npcs/controllers/data/TraderStock.java` | Trader stock configuration | +| `noppes/npcs/constants/EnumStockReset.java` | Stock reset types | +| `noppes/npcs/controllers/AuctionController.java` | Auction house management | +| `noppes/npcs/controllers/data/AuctionListing.java` | Auction listing data | +| `noppes/npcs/constants/EnumAuctionDuration.java` | Auction duration options | +| `noppes/npcs/constants/EnumAuctionStatus.java` | Auction status states | +| `noppes/npcs/constants/EnumAuctionCategory.java` | Item categories | +| `noppes/npcs/roles/RoleAuctioneer.java` | Auctioneer NPC role | +| `noppes/npcs/containers/ContainerAuction*.java` | Auction containers | +| `noppes/npcs/client/gui/player/GuiAuction*.java` | Auction GUIs | +| `noppes/npcs/client/gui/roles/GuiNpcAuctioneerSetup.java` | Auctioneer config | +| `kamkeel/npcs/network/packets/player/AuctionActionPacket.java` | Auction actions | +| `kamkeel/npcs/network/packets/data/AuctionSyncPacket.java` | Auction data sync | + +### Files to Modify + +| File | Changes | +|------|---------| +| `dependencies.gradle` | Add VaultAPI dependency | +| `repositories.gradle` | Add Vault repository | +| `CustomNpcs.java` | Initialize VaultHelper, AuctionController | +| `EnumRoleType.java` | Add Auctioneer | +| `EnumGuiType.java` | Add auction GUI types | +| `DataAdvanced.java` | Handle Auctioneer role | +| `RoleTrader.java` | Add stock and currency support | +| `ContainerNPCTrader.java` | Handle stock/currency transactions | +| `GuiNpcTraderSetup.java` | Add stock/currency UI | +| `PlayerData.java` | Add PlayerCurrencyData | +| `CommonProxy.java` | Register auction containers | +| `ServerTickHandler.java` | Add auction expiration check | +| `PacketHandler.java` | Register auction packets | + +--- + +## Testing Strategy + +### Unit Testing +- Currency operations (deposit, withdraw, balance) +- Stock depletion and reset calculations +- Auction bid validation +- Auction expiration handling + +### Integration Testing +- VaultAPI integration on hybrid servers +- Fallback to item currency when Vault unavailable +- Stock persistence across server restarts +- Auction persistence and recovery + +### User Acceptance Testing +- Complete trading flow with stock limits +- Complete auction flow (list → bid → win → claim) +- Currency display formatting +- GUI usability + +### Edge Cases +- Negative currency amounts +- Concurrent bids on same auction +- Server crash during transaction +- Player offline when auction ends +- Maximum listings per player +- Inventory full when claiming + +--- + +## API/Scripting Extensions + +Consider adding these scripting hooks: + +```java +// ICurrency interface for scripts +double getBalance(IPlayer player); +boolean withdraw(IPlayer player, double amount); +boolean deposit(IPlayer player, double amount); +String format(double amount); + +// IAuction interface for scripts +IAuctionListing createListing(IPlayer seller, IItemStack item, + double startPrice, double buyout, int durationHours); +boolean cancelListing(int listingId); +List getPlayerListings(IPlayer player); + +// Events +onAuctionCreated(IAuctionListing listing); +onAuctionBid(IAuctionListing listing, IPlayer bidder, double amount); +onAuctionSold(IAuctionListing listing, IPlayer buyer, double price); +onAuctionExpired(IAuctionListing listing); +onTraderPurchase(ITrader trader, IPlayer buyer, int slot, int stockRemaining); +onTraderStockReset(ITrader trader); +``` + +--- + +## Conclusion + +This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The three features are designed to work together seamlessly: + +1. **Currency System** provides the foundation for monetary transactions +2. **Trader Enhancements** modernize the existing trader with stock and currency support +3. **Auction House** creates a global marketplace leveraging the currency system + +The implementation follows existing codebase patterns and can be developed incrementally, with each phase building on the previous one. From c5c35a111366c8d1cc615eceb5acc6aba30bf8b8 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 02:10:58 -0500 Subject: [PATCH 28/35] Update api --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index 305a3778c..d003d6d87 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 +Subproject commit d003d6d870db13d56fce223050a06faf15050eae From a33acee11776898f1bf5339d8c2aa347bcc753c7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 16:49:48 +0200 Subject: [PATCH 29/35] Added Animation task consumers (much better than animation events) --- .../npcs/controllers/data/Animation.java | 44 ++++++++++++++++++- .../npcs/controllers/data/AnimationData.java | 3 ++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index b60f9ad4d..c74a163b2 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -12,7 +12,8 @@ import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.controllers.AnimationController; import java.util.ArrayList; - +import java.util.function.BiConsumer; +import java.util.function.Consumer; public class Animation implements IAnimation { public AnimationData parent; //Client-sided only @@ -34,6 +35,10 @@ public class Animation implements IAnimation { //Client-sided public boolean paused; + public Consumer onAnimationStart; + public BiConsumer onAnimationFrame; + public Consumer onAnimationEnd; + public Animation() { } @@ -254,8 +259,10 @@ public class Animation implements IAnimation { if (this.currentFrame() != null) { EventHooks.onAnimationFrameEntered(this, this.currentFrame()); + fireFrameTask(this.currentFrame); } else { EventHooks.onAnimationEnded(this); + fireEndTask(); } if (nextFrame != null) { @@ -294,4 +301,39 @@ public class Animation implements IAnimation { } } } + + public IAnimation onStart(Consumer task) { + this.onAnimationStart = task; + return this; + } + + public IAnimation onFrame(BiConsumer task) { + this.onAnimationFrame = task; + return this; + } + + public IAnimation onEnd(Consumer task) { + this.onAnimationEnd = task; + return this; + } + + protected void fireStartTask() { + if (this.onAnimationStart != null) { + this.onAnimationStart.accept(this); + } + } + + protected void fireFrameTask(int frame) { + if (this.onAnimationFrame != null) { + this.onAnimationFrame.accept(frame, this); + } + } + + protected void fireEndTask() { + if (this.onAnimationEnd != null) { + this.onAnimationEnd.accept(this); + } + } + + } diff --git a/src/main/java/noppes/npcs/controllers/data/AnimationData.java b/src/main/java/noppes/npcs/controllers/data/AnimationData.java index c537d94e1..ca1e25900 100644 --- a/src/main/java/noppes/npcs/controllers/data/AnimationData.java +++ b/src/main/java/noppes/npcs/controllers/data/AnimationData.java @@ -98,15 +98,18 @@ public class AnimationData implements IAnimationData { this.isClientAnimating = this.allowAnimation && this.animation != null; if (prevIsClientAnimating && (!this.isClientAnimating || this.animation != this.currentClientAnimation)) { EventHooks.onAnimationEnded(this.currentClientAnimation); + this.currentClientAnimation.fireEndTask(); } if (this.isClientAnimating) { this.currentClientAnimation = this.animation; } if (this.animation != null && this.allowAnimation) { + this.animation.fireStartTask(); if (EventHooks.onAnimationStarted(this.animation)) return; EventHooks.onAnimationFrameEntered(this.animation, this.animation.currentFrame()); + this.animation.fireFrameTask(this.animation.currentFrame); } List entities = sendingEntity.worldObj.getEntitiesWithinAABB(EntityPlayer.class, AxisAlignedBB.getBoundingBox( From 68681931f826b37a5384dbd053a654a3fc9a94c6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 16:53:58 +0200 Subject: [PATCH 30/35] Animation Data Store (for cross event/frame data handling) --- src/api | 2 +- .../noppes/npcs/controllers/data/Animation.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/api b/src/api index d003d6d87..305a3778c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit d003d6d870db13d56fce223050a06faf15050eae +Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index c74a163b2..89081467b 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -12,6 +12,8 @@ import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.controllers.AnimationController; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -35,6 +37,8 @@ public class Animation implements IAnimation { //Client-sided public boolean paused; + protected final Map dataStore = new HashMap<>(); + public Consumer onAnimationStart; public BiConsumer onAnimationFrame; public Consumer onAnimationEnd; @@ -180,6 +184,18 @@ public class Animation implements IAnimation { return time; } + @Override + public boolean hasData(String key) {return dataStore.containsKey(key);} + + @Override + public Object getData(String key) {return dataStore.get(key);} + + @Override + public IAnimation setData(String key, Object v) {dataStore.put(key, v);return this;} + + @Override + public IAnimation removeData(String key) {dataStore.remove(key);return this;} + public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("ID")) { id = compound.getInteger("ID"); From a39178c75b4767b4b819664a17ed3aa6c9700158 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 16:54:41 +0200 Subject: [PATCH 31/35] Animation Data Store (for cross event/frame data handling) --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index 305a3778c..aefae8ace 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 +Subproject commit aefae8ace3555442da7debff5b95cf410fa057f7 From eb660b38ae73166547680a1428606186d644367f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 17:14:45 +0200 Subject: [PATCH 32/35] Added IAnimation consumers to API --- src/api | 2 +- src/main/java/noppes/npcs/controllers/data/Animation.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api b/src/api index aefae8ace..801d62c06 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit aefae8ace3555442da7debff5b95cf410fa057f7 +Subproject commit 801d62c0607f301759b627f251e93a5fc6de9d67 diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index 89081467b..bddcb3f7e 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -318,16 +318,19 @@ public class Animation implements IAnimation { } } + @Override public IAnimation onStart(Consumer task) { this.onAnimationStart = task; return this; } - + + @Override public IAnimation onFrame(BiConsumer task) { this.onAnimationFrame = task; return this; } - + + @Override public IAnimation onEnd(Consumer task) { this.onAnimationEnd = task; return this; From db0aa94a3aeaaf154bb1954d45982878ec6abeca Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 20:17:55 +0200 Subject: [PATCH 33/35] IAnimation instead of Animation --- .../java/noppes/npcs/controllers/data/Animation.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index bddcb3f7e..9daa7e7b9 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -39,9 +39,9 @@ public class Animation implements IAnimation { protected final Map dataStore = new HashMap<>(); - public Consumer onAnimationStart; - public BiConsumer onAnimationFrame; - public Consumer onAnimationEnd; + public Consumer onAnimationStart; + public BiConsumer onAnimationFrame; + public Consumer onAnimationEnd; public Animation() { } @@ -319,19 +319,19 @@ public class Animation implements IAnimation { } @Override - public IAnimation onStart(Consumer task) { + public IAnimation onStart(Consumer task) { this.onAnimationStart = task; return this; } @Override - public IAnimation onFrame(BiConsumer task) { + public IAnimation onFrame(BiConsumer task) { this.onAnimationFrame = task; return this; } @Override - public IAnimation onEnd(Consumer task) { + public IAnimation onEnd(Consumer task) { this.onAnimationEnd = task; return this; } From e8ef60deb4e1ceee87e40bcb1851599c42c21608 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 20:25:34 +0200 Subject: [PATCH 34/35] Important IAnimation takeTasksFrom global instance in AnimationController to local instance in entity AnimationData --- .../noppes/npcs/controllers/data/Animation.java | 16 ++++++++++++++++ .../npcs/controllers/data/AnimationData.java | 1 + 2 files changed, 17 insertions(+) diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index 9daa7e7b9..fa290975c 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -353,6 +353,22 @@ public class Animation implements IAnimation { this.onAnimationEnd.accept(this); } } + + /** + * Only called on setAnimation, to transfer tasks from + * this global instance stored in AnimationController + * to the new local instance on entity's AnimationData, + * and clear them from the global instance. + */ + public void takeTasksFrom(Animation animation) { + this.onAnimationStart = animation.onAnimationStart; + this.onAnimationFrame = animation.onAnimationFrame; + this.onAnimationEnd = animation.onAnimationEnd; + + animation.onAnimationEnd = null; + animation.onAnimationFrame = null; + animation.onAnimationStart = null; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/AnimationData.java b/src/main/java/noppes/npcs/controllers/data/AnimationData.java index ca1e25900..ea5da6a84 100644 --- a/src/main/java/noppes/npcs/controllers/data/AnimationData.java +++ b/src/main/java/noppes/npcs/controllers/data/AnimationData.java @@ -272,6 +272,7 @@ public class AnimationData implements IAnimationData { newAnim.currentFrame = 0; newAnim.currentFrameTime = 0; newAnim.parent = this; + newAnim.takeTasksFrom((Animation) animation); } From 60af20316583d6692dbf54bc1ebd7b6e95661248 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 20:32:18 +0200 Subject: [PATCH 35/35] Renamed to moveFromGlobalToLocal --- src/main/java/noppes/npcs/controllers/data/Animation.java | 5 ++++- .../java/noppes/npcs/controllers/data/AnimationData.java | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index fa290975c..c7d006198 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -360,7 +360,7 @@ public class Animation implements IAnimation { * to the new local instance on entity's AnimationData, * and clear them from the global instance. */ - public void takeTasksFrom(Animation animation) { + public void moveFromGlobalToLocal(Animation animation) { this.onAnimationStart = animation.onAnimationStart; this.onAnimationFrame = animation.onAnimationFrame; this.onAnimationEnd = animation.onAnimationEnd; @@ -368,6 +368,9 @@ public class Animation implements IAnimation { animation.onAnimationEnd = null; animation.onAnimationFrame = null; animation.onAnimationStart = null; + + this.dataStore.putAll(animation.dataStore); + animation.dataStore.clear(); } diff --git a/src/main/java/noppes/npcs/controllers/data/AnimationData.java b/src/main/java/noppes/npcs/controllers/data/AnimationData.java index ea5da6a84..f5e6d80b4 100644 --- a/src/main/java/noppes/npcs/controllers/data/AnimationData.java +++ b/src/main/java/noppes/npcs/controllers/data/AnimationData.java @@ -272,7 +272,7 @@ public class AnimationData implements IAnimationData { newAnim.currentFrame = 0; newAnim.currentFrameTime = 0; newAnim.parent = this; - newAnim.takeTasksFrom((Animation) animation); + newAnim.moveFromGlobalToLocal((Animation) animation); }