From 2e2b52252da7e21cb3d03d56bdae3c17be9f770d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 16 Mar 2026 05:55:35 +0200 Subject: [PATCH] HUGE CORE MIGRATION (WIP, WONT BUILD) --- .AGENTS/SESSION_MEMORY.md | 121 +- core/build.gradle | 7 + .../main/java/io/netty/buffer/ByteBuf.java | 26 + .../java/kamkeel/npcs/CustomAttributes.java | 77 + .../java/kamkeel/npcs/addon/AddonHooks.java | 12 + .../java/kamkeel/npcs/addon/DBCAddon.java | 11 + .../npcs/controllers/AbilityController.java | 1521 +++++++++ .../npcs/controllers/AttributeController.java | 104 + .../npcs/controllers/EnergyController.java | 230 ++ .../npcs/controllers/ProfileController.java | 854 +++++ .../npcs/controllers/SyncController.java | 1121 +++++++ .../npcs/controllers/TelegraphController.java | 400 +++ .../controllers/data/ability/Ability.java | 2915 +++++++++++++++++ .../data/ability/AbilityAction.java | 330 ++ .../data/ability/AbilityVariant.java | 54 + .../data/ability/ConcurrentSlot.java | 117 + .../data/ability/CustomAbility.java | 507 +++ .../ability/conditions/AbilityCondition.java | 147 + .../ability/conditions/ConditionCompare.java | 138 + .../conditions/ConditionHPThreshold.java | 33 + .../conditions/ConditionHasEffect.java | 128 + .../ability/conditions/ConditionHitCount.java | 101 + .../ability/conditions/ConditionItem.java | 208 ++ .../conditions/ConditionQuestCompleted.java | 110 + .../conditions/ConditionThreshold.java | 195 ++ .../data/ability/data/AbilityIconData.java | 344 ++ .../data/ability/data/ChainedAbility.java | 567 ++++ .../data/ability/data/IAbilityAction.java | 70 + .../data/ability/data/ProjectileData.java | 13 +- .../data/effect/AbilityCustomEffect.java | 118 + .../data/effect/AbilityPotionEffect.java | 138 + .../ability/data/effect/IEffectAction.java | 50 + .../data/energy/EnergyBarrierData.java | 14 +- .../ability/data/energy/EnergyCombatData.java | 69 +- .../data/energy/EnergyDisplayData.java | 11 +- .../data/energy/EnergyLifespanData.java | 11 +- .../data/energy/EnergyLightningData.java | 11 +- .../data/ability/data/energy/PanelData.java | 15 + .../data/entry/AbilityEffectActionEntry.java | 93 + .../data/entry/AbilityToggleEntry.java | 51 + .../data/entry/ChainedAbilityEntry.java | 230 ++ .../data/ability/enums/AnchorPoint.java | 19 +- .../data/ability/enums/UserType.java | 13 +- .../ability/extender/IAbilityExtender.java | 141 + .../data/ability/gui/AbilityFieldDefs.java | 29 + .../data/ability/gui/FieldDef.java | 25 + .../data/ability/gui/GuiQuestSelection.java | 12 + .../ability/gui/IAbilityConfigCallback.java | 25 + .../data/ability/gui/IAbilityExtender.java | 11 + .../ability/gui/IAbilityFieldProvider.java | 25 + .../gui/IChainedAbilityFieldProvider.java | 25 + .../data/ability/gui/SubGuiAbilityConfig.java | 25 + .../ability/gui/SubGuiCustomEffectSelect.java | 13 + .../ability/gui/SubGuiZonePresetSelector.java | 12 + .../ability/preview/PreviewEntityHandler.java | 25 + .../data/ability/type/AbilityCharge.java | 331 ++ .../data/ability/type/AbilityCounter.java | 227 ++ .../data/ability/type/AbilityCutter.java | 492 +++ .../data/ability/type/AbilityDash.java | 424 +++ .../data/ability/type/AbilityDefend.java | 356 ++ .../data/ability/type/AbilityDodge.java | 217 ++ .../data/ability/type/AbilityEffect.java | 540 +++ .../data/ability/type/AbilityGuard.java | 110 + .../data/ability/type/AbilityHazard.java | 200 ++ .../data/ability/type/AbilityHeavyHit.java | 260 ++ .../data/ability/type/AbilityMovement.java | 323 ++ .../data/ability/type/AbilityProjectile.java | 391 +++ .../data/ability/type/AbilityShockwave.java | 255 ++ .../data/ability/type/AbilitySlam.java | 841 +++++ .../data/ability/type/AbilityTeleport.java | 602 ++++ .../data/ability/type/AbilityTrap.java | 370 +++ .../data/ability/type/AbilityVortex.java | 426 +++ .../data/ability/type/AbilityZone.java | 788 +++++ .../data/ability/type/AccentStyle.java | 12 + .../data/ability/type/EntityAbilityZone.java | 24 + .../data/ability/type/ParticleMotion.java | 13 + .../data/ability/type/ZoneShape.java | 11 + .../ability/type/energy/AbilityBarrier.java | 443 +++ .../data/ability/type/energy/AbilityBeam.java | 259 ++ .../data/ability/type/energy/AbilityDisc.java | 258 ++ .../data/ability/type/energy/AbilityDome.java | 164 + .../ability/type/energy/AbilityEnergy.java | 211 ++ .../type/energy/AbilityEnergyProjectile.java | 946 ++++++ .../ability/type/energy/AbilityLaser.java | 246 ++ .../data/ability/type/energy/AbilityOrb.java | 218 ++ .../ability/type/energy/AbilityShield.java | 178 + .../ability/type/energy/AbilitySlicer.java | 207 ++ .../ability/type/energy/AbilitySweeper.java | 336 ++ .../data/ability/type/energy/AbilityWall.java | 255 ++ .../ability/util/AbilityTargetHelper.java | 136 + .../data/attribute/PlayerAttributeMap.java | 12 +- .../requirement/IRequirementChecker.java | 31 + .../RequirementCheckerRegistry.java | 48 + .../types/ProfileSlotRequirement.java | 59 + .../types/QuestActiveRequirement.java | 62 + .../types/QuestCompletedRequirement.java | 62 + .../types/SoulbindRequirement.java | 132 + .../tracker/AttributeRecalcEvent.java | 44 + .../tracker/IAttributeRecalcListener.java | 19 + .../tracker/PlayerAttributeTracker.java | 266 ++ .../tracker/PlayerEquipmentTracker.java | 94 + .../data/energy/IEnergyExtender.java | 56 + .../EnergyChargePreviewManager.java | 103 + .../energycharge/EnergyChargeTracker.java | 116 + .../controllers/data/profile/CNPCData.java | 90 + .../data/profile/IProfileData.java | 62 + .../controllers/data/profile/Profile.java | 118 + .../controllers/data/telegraph/Telegraph.java | 13 +- .../data/telegraph/TelegraphInstance.java | 402 +++ .../data/telegraph/TelegraphManager.java | 149 + .../npcs/entity/EntityAbilityBeam.java | 25 + .../npcs/entity/EntityAbilityDisc.java | 25 + .../npcs/entity/EntityAbilityLaser.java | 25 + .../kamkeel/npcs/entity/EntityAbilityOrb.java | 25 + .../npcs/entity/EntityAbilityZone.java | 17 + .../npcs/entity/EntityEnergyBarrier.java | 27 + .../kamkeel/npcs/entity/EntityEnergyDome.java | 27 + .../npcs/entity/EntityEnergyExplosion.java | 10 + .../npcs/entity/EntityEnergyPanel.java | 30 + .../npcs/entity/EntityEnergyProjectile.java | 31 + .../npcs/entity/EntityEnergySlicer.java | 8 + .../npcs/entity/EntityEnergySweeper.java | 10 + .../npcs/network/enums/EnumDataPacket.java | 16 +- .../npcs/network/enums/EnumRequestPacket.java | 15 +- .../npcs/network/enums/EnumSyncType.java | 6 +- .../kamkeel/npcs/util/AnchorPointHelper.java | 622 ++++ .../npcs/util/AttributeAttackUtil.java | 449 +++ .../kamkeel/npcs/util/AttributeItemUtil.java | 527 +++ .../java/kamkeel/npcs/util/ByteBufUtils.java | 12 + .../main/java/kamkeel/npcs/util/IVector3.java | 28 + .../main/java/kamkeel/npcs/util/Register.java | 243 ++ .../java/kamkeel/npcs/util/VaultUtil.java | 611 ++++ .../noppes/npcs/AbstractDataAbilities.java | 8 + .../src/main/java/noppes/npcs/CustomNpcs.java | 12 + .../noppes/npcs/CustomNpcsPermissions.java | 11 + .../main/java/noppes/npcs/DataDisplay.java | 10 + .../src/main/java/noppes/npcs/EventHooks.java | 10 + .../main/java/noppes/npcs/ICompatibilty.java | 11 + core/src/main/java/noppes/npcs/LogWriter.java | 13 + core/src/main/java/noppes/npcs/NBTTags.java | 326 +- .../java/noppes/npcs/NoppesUtilPlayer.java | 11 + .../java/noppes/npcs/NoppesUtilServer.java | 15 + .../java/noppes/npcs/NpcDamageSource.java | 10 + .../java/noppes/npcs/NpcMiscInventory.java | 19 + .../noppes/npcs/VersionCompatibility.java | 8 + .../java/noppes/npcs/config/ConfigClient.java | 380 +++ .../java/noppes/npcs/config/ConfigDebug.java | 150 + .../java/noppes/npcs/config/ConfigEnergy.java | 49 + .../npcs/config/ConfigExperimental.java | 34 + .../java/noppes/npcs/config/ConfigItem.java | 71 + .../java/noppes/npcs/config/ConfigMain.java | 339 ++ .../java/noppes/npcs/config/ConfigMarket.java | 255 ++ .../java/noppes/npcs/config/ConfigMixin.java | 67 + .../java/noppes/npcs/config/ConfigScript.java | 217 ++ .../java/noppes/npcs/config/GlyphCache.java | 838 +++++ .../noppes/npcs/config/LoadConfiguration.java | 34 + .../java/noppes/npcs/config/StringCache.java | 1366 ++++++++ .../noppes/npcs/config/legacy/ConfigProp.java | 14 + .../npcs/config/legacy/LegacyConfig.java | 157 + .../npcs/config/legacy/LegacyLoader.java | 140 + .../noppes/npcs/constants/ClientOnly.java | 29 + .../java/noppes/npcs/constants/Direction.java | 26 + .../npcs/constants/EnumAuctionLogAction.java | 44 + .../npcs/constants/EnumAuctionStatus.java | 45 + .../noppes/npcs/constants/EnumClaimType.java | 36 + .../npcs/constants/EnumCompanionTalent.java | 25 + .../npcs/constants/EnumNotificationType.java | 30 + .../npcs/constants/EnumNpcToolMaterial.java | 19 +- .../noppes/npcs/constants/EnumPotionType.java | 92 + .../noppes/npcs/constants/EnumScriptType.java | 19 +- .../noppes/npcs/constants/EnumStockReset.java | 13 +- .../noppes/npcs/constants/ItemAction.java | 9 + .../noppes/npcs/constants/ScriptContext.java | 326 ++ .../containers/ContainerNPCBankInterface.java | 11 + .../npcs/controllers/AnimationController.java | 610 ++++ .../npcs/controllers/AuctionConfigSync.java | 57 + .../npcs/controllers/AuctionController.java | 1927 +++++++++++ .../npcs/controllers/BankController.java | 173 + .../npcs/controllers/ChunkController.java | 108 + .../controllers/CustomEffectController.java | 734 +++++ .../npcs/controllers/CustomGuiController.java | 156 + .../npcs/controllers/DialogController.java | 350 ++ .../npcs/controllers/FactionController.java | 47 +- .../controllers/GlobalDataController.java | 37 +- .../npcs/controllers/HookDefinition.java | 303 ++ .../controllers/LinkedItemController.java | 434 +++ .../npcs/controllers/LinkedNpcController.java | 224 ++ .../npcs/controllers/MagicController.java | 53 +- .../npcs/controllers/MarketRegistry.java | 220 ++ .../npcs/controllers/PartyController.java | 266 ++ .../controllers/PlayerDataController.java | 601 ++++ .../controllers/PlayerQuestController.java | 167 + .../npcs/controllers/ProfileConfigSync.java | 40 + .../npcs/controllers/QuestController.java | 278 ++ .../npcs/controllers/RecipeController.java | 390 +++ .../npcs/controllers/ScriptConfigSync.java | 33 + .../npcs/controllers/ScriptContainer.java | 436 +++ .../npcs/controllers/ScriptController.java | 414 +++ .../npcs/controllers/ScriptEntityData.java | 32 + .../controllers/ScriptHookController.java | 548 ++++ .../controllers/ServerCloneController.java | 754 +++++ .../controllers/ServerTagMapController.java | 20 +- .../npcs/controllers/SpawnController.java | 249 ++ .../npcs/controllers/SyncController.java | 10 + .../npcs/controllers/TagController.java | 49 +- .../npcs/controllers/TransportController.java | 47 +- .../npcs/controllers/data/AbilityScript.java | 65 + .../npcs/controllers/data/Animation.java | 410 +++ .../npcs/controllers/data/AnimationData.java | 326 ++ .../controllers/data/AuctionBlacklist.java | 279 ++ .../npcs/controllers/data/AuctionClaim.java | 260 ++ .../npcs/controllers/data/AuctionListing.java | 273 ++ .../npcs/controllers/data/Availability.java | 529 +++ .../noppes/npcs/controllers/data/Bank.java | 72 + .../npcs/controllers/data/BankData.java | 139 + .../controllers/data/BuiltInAnimation.java | 105 + .../data/ChainedAbilityScript.java | 65 + .../npcs/controllers/data/CustomEffect.java | 359 ++ .../npcs/controllers/data/DataScript.java | 387 +++ .../controllers/data/DataSkinOverlays.java | 9 + .../npcs/controllers/data/DataTimers.java | 9 + .../npcs/controllers/data/DataTransform.java | 263 ++ .../noppes/npcs/controllers/data/Dialog.java | 612 ++++ .../npcs/controllers/data/DialogCategory.java | 86 + .../npcs/controllers/data/DialogOption.java | 69 + .../npcs/controllers/data/EffectScript.java | 67 + .../noppes/npcs/controllers/data/Event.java | 10 + .../noppes/npcs/controllers/data/Faction.java | 36 +- .../npcs/controllers/data/FactionOptions.java | 76 + .../controllers/data/ForgeDataScript.java | 189 ++ .../controllers/data/GlobalNPCDataScript.java | 108 + .../npcs/controllers/data/HitboxData.java | 27 +- .../controllers/data/INpcScriptHandler.java | 7 + .../controllers/data/IScriptBlockHandler.java | 8 + .../npcs/controllers/data/IScriptHandler.java | 210 ++ .../data/IScriptHandlerPacket.java | 100 + .../npcs/controllers/data/IScriptUnit.java | 225 ++ .../controllers/data/JaninoScriptHandler.java | 141 + .../noppes/npcs/controllers/data/Lines.java | 43 +- .../npcs/controllers/data/LinkedItem.java | 283 ++ .../controllers/data/LinkedItemScript.java | 66 + .../noppes/npcs/controllers/data/Magic.java | 34 +- .../npcs/controllers/data/MarkData.java | 162 + .../npcs/controllers/data/ModelScalePart.java | 9 + .../controllers/data/MultiScriptHandler.java | 116 + .../noppes/npcs/controllers/data/Party.java | 480 +++ .../controllers/data/PlayerAbilityData.java | 1105 +++++++ .../data/PlayerAbilityHotbarData.java | 93 + .../npcs/controllers/data/PlayerBankData.java | 74 + .../npcs/controllers/data/PlayerData.java | 506 +++ .../controllers/data/PlayerDataScript.java | 213 ++ .../controllers/data/PlayerDialogData.java | 25 +- .../npcs/controllers/data/PlayerEffect.java | 15 +- .../npcs/controllers/data/PlayerEvent.java | 15 + .../controllers/data/PlayerFactionData.java | 110 + .../controllers/data/PlayerItemGiverData.java | 71 + .../npcs/controllers/data/PlayerMail.java | 289 ++ .../npcs/controllers/data/PlayerMailData.java | 112 + .../controllers/data/PlayerQuestData.java | 315 ++ .../controllers/data/PlayerTradeData.java | 427 +++ .../controllers/data/PlayerTransportData.java | 27 +- .../noppes/npcs/controllers/data/Quest.java | 425 +++ .../npcs/controllers/data/QuestCategory.java | 98 + .../npcs/controllers/data/QuestData.java | 33 + .../npcs/controllers/data/RecipeAnvil.java | 188 ++ .../controllers/data/RecipeCarpentry.java | 309 ++ .../npcs/controllers/data/RecipeScript.java | 82 + .../npcs/controllers/data/RecipesDefault.java | 199 ++ .../npcs/controllers/data/ScreenSize.java | 11 + .../npcs/controllers/data/ScriptHandler.java | 77 + .../controllers/data/SingleScriptHandler.java | 158 + .../npcs/controllers/data/SpawnData.java | 276 ++ .../noppes/npcs/controllers/data/TagMap.java | 29 +- .../npcs/controllers/data/TintData.java | 39 +- .../npcs/controllers/data/TraderStock.java | 21 +- .../npcs/controllers/data/action/Action.java | 718 ++++ .../controllers/data/action/ActionList.java | 58 + .../data/action/ActionListener.java | 91 + .../controllers/data/action/ActionLogger.java | 204 ++ .../data/action/ActionManager.java | 551 ++++ .../controllers/data/action/ActionQueue.java | 396 +++ .../controllers/data/action/ActionThread.java | 168 + .../data/action/action/ConditionalAction.java | 208 ++ .../data/action/chain/ActionChain.java | 63 + .../npcs/entity/EntityNPCInterface.java | 25 + .../java/noppes/npcs/quests/QuestDialog.java | 547 ++++ .../noppes/npcs/quests/QuestInterface.java | 57 + .../java/noppes/npcs/quests/QuestItem.java | 689 ++++ .../java/noppes/npcs/quests/QuestKill.java | 441 +++ .../noppes/npcs/quests/QuestLocation.java | 448 +++ .../java/noppes/npcs/quests/QuestManual.java | 389 +++ .../main/java/noppes/npcs/roles/JobBard.java | 162 + .../noppes/npcs/roles/JobChunkLoader.java | 107 + .../noppes/npcs/roles/JobConversation.java | 271 ++ .../java/noppes/npcs/roles/JobFollower.java | 99 + .../main/java/noppes/npcs/roles/JobGuard.java | 85 + .../java/noppes/npcs/roles/JobHealer.java | 95 + .../java/noppes/npcs/roles/JobInterface.java | 78 + .../java/noppes/npcs/roles/JobItemGiver.java | 311 ++ .../java/noppes/npcs/roles/JobSpawner.java | 487 +++ .../noppes/npcs/roles/RoleAuctioneer.java | 64 + .../main/java/noppes/npcs/roles/RoleBank.java | 66 + .../java/noppes/npcs/roles/RoleCompanion.java | 696 ++++ .../java/noppes/npcs/roles/RoleFollower.java | 213 ++ .../java/noppes/npcs/roles/RoleInnkeeper.java | 98 + .../java/noppes/npcs/roles/RoleInterface.java | 77 + .../java/noppes/npcs/roles/RoleMount.java | 230 ++ .../java/noppes/npcs/roles/RolePostman.java | 82 + .../java/noppes/npcs/roles/RoleTrader.java | 475 +++ .../noppes/npcs/roles/RoleTransporter.java | 130 + .../npcs/roles/companion/CompanionFarmer.java | 36 + .../roles/companion/CompanionFoodStats.java | 165 + .../npcs/roles/companion/CompanionGuard.java | 61 + .../companion/CompanionJobInterface.java | 43 + .../npcs/roles/companion/CompanionTrader.java | 31 + .../npcs/scripted/constants/EntityType.java | 6 +- .../java/noppes/npcs/util/GameProfileAlt.java | 44 + .../java/noppes/npcs/util/JsonException.java | 9 + .../java/noppes/npcs/util/NBTJsonUtil.java | 391 +++ .../npcs/util/ScriptToStringHelper.java | 210 ++ .../minecraft/command/ICommandSender.java | 16 + .../java/common/minecraft/nbt/INbtBase.java | 12 + .../minecraft/potion/IPotionEffect.java | 13 + .../common/minecraft/potion/IPotionType.java | 12 + .../common/minecraft/util/IBoundingBox.java | 18 + .../minecraft/util/IRayTraceResult.java | 19 + .../minecraft/util/IResourceLocation.java | 12 + .../common/minecraft/util/ITextComponent.java | 13 + .../common/minecraft/util/ITextStyle.java | 15 + .../java/common/minecraft/util/IVector3.java | 19 + .../common/minecraft/world/IExplosion.java | 13 + .../common/config/IConfigProperty.java | 21 + .../common/config/IConfiguration.java | 22 + .../npcs/platform/PlatformService.java | 56 + tools/migration/Symbol-Swap.ps1 | 7 +- 335 files changed, 64280 insertions(+), 454 deletions(-) create mode 100644 core/src/main/java/io/netty/buffer/ByteBuf.java create mode 100644 core/src/main/java/kamkeel/npcs/CustomAttributes.java create mode 100644 core/src/main/java/kamkeel/npcs/addon/AddonHooks.java create mode 100644 core/src/main/java/kamkeel/npcs/addon/DBCAddon.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/AbilityController.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/AttributeController.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/EnergyController.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/ProfileController.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/SyncController.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/TelegraphController.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/ConcurrentSlot.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionCompare.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHPThreshold.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionThreshold.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityCustomEffect.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/IEffectAction.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/PanelData.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityEffectActionEntry.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityToggleEntry.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/GuiQuestSelection.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityConfigCallback.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityExtender.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityFieldProvider.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IChainedAbilityFieldProvider.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiAbilityConfig.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiCustomEffectSelect.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiZonePresetSelector.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/preview/PreviewEntityHandler.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDodge.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEffect.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AccentStyle.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/EntityAbilityZone.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ParticleMotion.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ZoneShape.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/ability/util/AbilityTargetHelper.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/IRequirementChecker.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/RequirementCheckerRegistry.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/ProfileSlotRequirement.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestActiveRequirement.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestCompletedRequirement.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/SoulbindRequirement.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/AttributeRecalcEvent.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/IAttributeRecalcListener.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerAttributeTracker.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerEquipmentTracker.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/energy/IEnergyExtender.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargePreviewManager.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/profile/IProfileData.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/profile/Profile.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java create mode 100644 core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java create mode 100644 core/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java create mode 100644 core/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java create mode 100644 core/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java create mode 100644 core/src/main/java/kamkeel/npcs/util/AttributeItemUtil.java create mode 100644 core/src/main/java/kamkeel/npcs/util/ByteBufUtils.java create mode 100644 core/src/main/java/kamkeel/npcs/util/IVector3.java create mode 100644 core/src/main/java/kamkeel/npcs/util/Register.java create mode 100644 core/src/main/java/kamkeel/npcs/util/VaultUtil.java create mode 100644 core/src/main/java/noppes/npcs/AbstractDataAbilities.java create mode 100644 core/src/main/java/noppes/npcs/CustomNpcs.java create mode 100644 core/src/main/java/noppes/npcs/CustomNpcsPermissions.java create mode 100644 core/src/main/java/noppes/npcs/DataDisplay.java create mode 100644 core/src/main/java/noppes/npcs/EventHooks.java create mode 100644 core/src/main/java/noppes/npcs/ICompatibilty.java create mode 100644 core/src/main/java/noppes/npcs/LogWriter.java create mode 100644 core/src/main/java/noppes/npcs/NoppesUtilPlayer.java create mode 100644 core/src/main/java/noppes/npcs/NoppesUtilServer.java create mode 100644 core/src/main/java/noppes/npcs/NpcDamageSource.java create mode 100644 core/src/main/java/noppes/npcs/NpcMiscInventory.java create mode 100644 core/src/main/java/noppes/npcs/VersionCompatibility.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigClient.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigDebug.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigEnergy.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigExperimental.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigItem.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigMain.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigMarket.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigMixin.java create mode 100644 core/src/main/java/noppes/npcs/config/ConfigScript.java create mode 100644 core/src/main/java/noppes/npcs/config/GlyphCache.java create mode 100644 core/src/main/java/noppes/npcs/config/LoadConfiguration.java create mode 100644 core/src/main/java/noppes/npcs/config/StringCache.java create mode 100644 core/src/main/java/noppes/npcs/config/legacy/ConfigProp.java create mode 100644 core/src/main/java/noppes/npcs/config/legacy/LegacyConfig.java create mode 100644 core/src/main/java/noppes/npcs/config/legacy/LegacyLoader.java create mode 100644 core/src/main/java/noppes/npcs/constants/ClientOnly.java create mode 100644 core/src/main/java/noppes/npcs/constants/Direction.java create mode 100644 core/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java create mode 100644 core/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java create mode 100644 core/src/main/java/noppes/npcs/constants/EnumClaimType.java create mode 100644 core/src/main/java/noppes/npcs/constants/EnumCompanionTalent.java create mode 100644 core/src/main/java/noppes/npcs/constants/EnumNotificationType.java create mode 100644 core/src/main/java/noppes/npcs/constants/EnumPotionType.java create mode 100644 core/src/main/java/noppes/npcs/constants/ItemAction.java create mode 100644 core/src/main/java/noppes/npcs/constants/ScriptContext.java create mode 100644 core/src/main/java/noppes/npcs/containers/ContainerNPCBankInterface.java create mode 100644 core/src/main/java/noppes/npcs/controllers/AnimationController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java create mode 100644 core/src/main/java/noppes/npcs/controllers/AuctionController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/BankController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ChunkController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/CustomEffectController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/CustomGuiController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/DialogController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/HookDefinition.java create mode 100644 core/src/main/java/noppes/npcs/controllers/LinkedItemController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/LinkedNpcController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/MarketRegistry.java create mode 100644 core/src/main/java/noppes/npcs/controllers/PartyController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/PlayerDataController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/PlayerQuestController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ProfileConfigSync.java create mode 100644 core/src/main/java/noppes/npcs/controllers/QuestController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/RecipeController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ScriptContainer.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ScriptController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ScriptEntityData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ScriptHookController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/ServerCloneController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/SpawnController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/SyncController.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/AbilityScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/Animation.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/AnimationData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/AuctionListing.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/Availability.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/Bank.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/BankData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/ChainedAbilityScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/CustomEffect.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/DataScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/DataSkinOverlays.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/DataTimers.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/DataTransform.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/Dialog.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/DialogCategory.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/DialogOption.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/EffectScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/Event.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/FactionOptions.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/LinkedItem.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/MarkData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/ModelScalePart.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/Party.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerBankData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerEvent.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerFactionData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerItemGiverData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerMail.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerMailData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerQuestData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/Quest.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/QuestCategory.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/QuestData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/RecipeAnvil.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/RecipeCarpentry.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/RecipeScript.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/RecipesDefault.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/ScreenSize.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/SpawnData.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/Action.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/ActionList.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/ActionListener.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/ActionLogger.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/ActionThread.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/action/ConditionalAction.java create mode 100644 core/src/main/java/noppes/npcs/controllers/data/action/chain/ActionChain.java create mode 100644 core/src/main/java/noppes/npcs/entity/EntityNPCInterface.java create mode 100644 core/src/main/java/noppes/npcs/quests/QuestDialog.java create mode 100644 core/src/main/java/noppes/npcs/quests/QuestInterface.java create mode 100644 core/src/main/java/noppes/npcs/quests/QuestItem.java create mode 100644 core/src/main/java/noppes/npcs/quests/QuestKill.java create mode 100644 core/src/main/java/noppes/npcs/quests/QuestLocation.java create mode 100644 core/src/main/java/noppes/npcs/quests/QuestManual.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobBard.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobChunkLoader.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobConversation.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobFollower.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobGuard.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobHealer.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobInterface.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobItemGiver.java create mode 100644 core/src/main/java/noppes/npcs/roles/JobSpawner.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleAuctioneer.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleBank.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleCompanion.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleFollower.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleInnkeeper.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleInterface.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleMount.java create mode 100644 core/src/main/java/noppes/npcs/roles/RolePostman.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleTrader.java create mode 100644 core/src/main/java/noppes/npcs/roles/RoleTransporter.java create mode 100644 core/src/main/java/noppes/npcs/roles/companion/CompanionFarmer.java create mode 100644 core/src/main/java/noppes/npcs/roles/companion/CompanionFoodStats.java create mode 100644 core/src/main/java/noppes/npcs/roles/companion/CompanionGuard.java create mode 100644 core/src/main/java/noppes/npcs/roles/companion/CompanionJobInterface.java create mode 100644 core/src/main/java/noppes/npcs/roles/companion/CompanionTrader.java create mode 100644 core/src/main/java/noppes/npcs/util/GameProfileAlt.java create mode 100644 core/src/main/java/noppes/npcs/util/JsonException.java create mode 100644 core/src/main/java/noppes/npcs/util/NBTJsonUtil.java create mode 100644 core/src/main/java/noppes/npcs/util/ScriptToStringHelper.java create mode 100644 platform-api/src/main/java/common/minecraft/command/ICommandSender.java create mode 100644 platform-api/src/main/java/common/minecraft/nbt/INbtBase.java create mode 100644 platform-api/src/main/java/common/minecraft/potion/IPotionEffect.java create mode 100644 platform-api/src/main/java/common/minecraft/potion/IPotionType.java create mode 100644 platform-api/src/main/java/common/minecraft/util/IBoundingBox.java create mode 100644 platform-api/src/main/java/common/minecraft/util/IRayTraceResult.java create mode 100644 platform-api/src/main/java/common/minecraft/util/IResourceLocation.java create mode 100644 platform-api/src/main/java/common/minecraft/util/ITextComponent.java create mode 100644 platform-api/src/main/java/common/minecraft/util/ITextStyle.java create mode 100644 platform-api/src/main/java/common/minecraft/util/IVector3.java create mode 100644 platform-api/src/main/java/common/minecraft/world/IExplosion.java create mode 100644 platform-api/src/main/java/common/minecraftforge/common/config/IConfigProperty.java create mode 100644 platform-api/src/main/java/common/minecraftforge/common/config/IConfiguration.java diff --git a/.AGENTS/SESSION_MEMORY.md b/.AGENTS/SESSION_MEMORY.md index dab7fd869..554e5e8ed 100644 --- a/.AGENTS/SESSION_MEMORY.md +++ b/.AGENTS/SESSION_MEMORY.md @@ -1,3 +1,112 @@ +________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ +## 🔴 VITAL AGENT CONTEXT — MIGRATION WORKFLOW (READ BEFORE ANY MIGRATION WORK) + +### 🚨 ABSOLUTE NON-NEGOTIABLE RULE — TOOLCHAIN ONLY (UPDATED 2026-03-16) +**Agents MUST use the migration toolchain scripts for ALL write operations during migration. NEVER use internal write/edit tools.** + +The following tools exist in `tools/migration/` and MUST be used: + +| Operation | Tool | NEVER Use Instead | +|-----------|------|-------------------| +| Create PA interface files | `pa_batch.py` | `write()` | +| Edit PlatformService | `pa_editor.py` | `edit()` | +| Copy files to core/ | `Copy-Recursive.ps1` | `cp` / `write()` | +| Replace MC types | `Symbol-Swap.ps1` | `edit()` / `ast_grep_replace` | +| Remove forbidden imports | `Nuke-Imports.ps1` | `edit()` | +| Regex transforms | `Static-Transform.ps1` | `edit()` / `sed` | +| Create stub classes | `Generate-Stubs.ps1` | `write()` | +| Deduplicate imports | `Dedupe-Imports.ps1` | manual | +| Create PA + propagate | `Create-And-Propagate.ps1` | `write()` + `edit()` | +| Build diagnostics | `Build-Report.ps1` | manual | +| Generate PA manifests | `generate_pa_manifest.py` | manual | +| Generate wave scripts | `generate_wave_script.py` | manual | +| Generate type mappings | `analyze.py generate-mappings` | manual | +| Batch diff all files | `batch_migration_diff.py` | manual | + +**WHY**: The internal write/edit tools are 100-1000x SLOWER and produce inconsistent results. The toolchain scripts are BATCH operations that process hundreds of files in milliseconds. This is the ONLY acceptable approach. + +**VIOLATION = IMMEDIATE REJECTION OF WORK.** + +### Sisyphus Execution Model (UPDATED 2026-03-16) +**Sisyphus delegates migration Parts to agents that MUST use the toolchain. Agents run PowerShell/Python commands via `bash()` tool ONLY.** + +### 🚀 CRITICAL EXECUTION RULES — DO NOT DEVIATE + +**1. TOOLCHAIN-ONLY Writes (MANDATORY — NON-NEGOTIABLE)** +- ALL file creation → `pa_batch.py`, `Generate-Stubs.ps1`, `Create-And-Propagate.ps1` +- ALL file edits → `Symbol-Swap.ps1`, `Static-Transform.ps1`, `Nuke-Imports.ps1`, `Dedupe-Imports.ps1` +- ALL PlatformService changes → `pa_editor.py` +- Agents use `bash()` to invoke these tools. They may use `read()` and `grep` to inspect files. +- The ONLY acceptable use of `edit()` is for fixing individual LSP errors AFTER toolchain scripts have run. + +**2. Parallel Bulk Edits via Toolchain (MANDATORY)** +- Copy-Recursive copies ALL files in one command +- Symbol-Swap replaces ALL types in one command +- Static-Transform applies ALL patterns in one command +- Result: surgical edits, formatting preserved, comments intact, minimal diff bloat + +**2. LSP-Driven Development (MANDATORY — THIS IS THE ONLY VERIFICATION)** +- LSP is the PRIMARY and ONLY source of truth — NOT code analysis, NOT grepping +- After applying ALL edits for a Part, run `lsp_diagnostics` on EVERY moved file +- **Do NOT proceed until LSP shows ZERO errors** +- If LSP shows errors → STOP and fix those specific errors immediately (same response or next) +- **If LSP is broken, unresponsive, or malfunctioning → STOP WORK and notify human immediately** +- Per-wave rule: **Do NOT move to next Part/Wave until LSP shows zero errors for current Part** + +**3. NO Commits by Sisyphus (MANDATORY)** +- DO NOT commit after Parts complete +- Committing requires build verification (which is slow and blocking) +- Human will verify build afterward and commit +- Sisyphus responsibility: code correctness via LSP. Human responsibility: build verification + commit. +- Just complete the Part, report status, move to next + +**4. Error Recovery (MANDATORY)** +- If LSP shows errors after edits: analyze the error, fix the `oldString` pattern, re-apply edit +- If multiple errors: fix them all in ONE response, then re-run LSP +- **Do NOT create partial fixes** — fix the entire pattern once + +### Execution Pattern (Per Part) + +``` +PART N: + Response 1: Read ALL files for Part N (parallel) + Response 2: + - Apply ALL edits (parallel edit() calls, bulk changes per file) + - Run lsp_diagnostics on ALL moved files + Response 3+ (if needed): + - IF LSP shows errors → FIX and re-run lsp_diagnostics + - REPEAT until zero errors + When LSP ✅ ZERO errors → Part N COMPLETE, move to Part N+1 +``` + +### Parallel Capability +- Sisyphus can edit 8-10 files in a single response +- All `edit()` calls execute concurrently +- Typical Part has 3-8 files → all edited at once + +### Why This Matters +- **Speed:** No agent spawn overhead, bulk edits, parallel execution → ~2-3 hours total +- **Quality:** LSP verification ensures correctness, format preservation ensures clean diffs +- **Reliability:** Sisyphus maintains full context and can fix errors immediately +- **Human Control:** Build verification and committing stays with human (prevents broken builds) + +--- + +### Human-Agent Division of Labor (OLD — OUTDATED, KEPT FOR REFERENCE) +~~**The human (project owner) will manually move all packages/files from m7 → core/.**~~ +**UPDATED: Sisyphus moves files + fixes them via LSP verification.** + +### Why LSP is Essential (REAFFIRMED) +- After files are moved to core/, they will have broken MC imports +- `lsp_diagnostics` produces the EXACT list of what's broken: file, line, column, error message +- This is the ONLY reliable guide — NOT guessing, NOT grepping +- Sisyphus MUST run `lsp_diagnostics` after applying ALL edits for a Part +- Sisyphus MUST re-run `lsp_diagnostics` after EACH fix until convergence to zero errors + +### PA Package Placement Rule (UPDATED 2026-03-15) +- If the abstraction interface already exists under `noppes.npcs.api.*` → use it as-is +- If it's a NEW abstraction for MC classes → create under `common.minecraft.*` in platform-api (NOT `noppes.npcs.common.minecraft`, just `common.minecraft`) + ________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ ## ⚠️ MIGRATION ORCHESTRATION RULES (APPLY TO ALL MIGRATION WORK) @@ -30,6 +139,7 @@ The orchestrating agent (Sisyphus) will manually verify ALL delegated work: ### 5. Context Passing - Every delegated agent MUST receive the full content of SESSION_MEMORY.md in their prompt +- Every agent must be of the UNSPECIFIED HIGH CATEGORY, unless explicitly told to use another - This ensures every agent understands the vision, architecture, constraints, and forbidden practices ### 6. Phase Reports @@ -63,7 +173,7 @@ The orchestrating agent (Sisyphus) will manually verify ALL delegated work: - Record any discoveries, surprises, or deviations in SESSION_MEMORY.md ________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ -CURRENT USER PROMPT: +MAIN USER VISION: ## 1) MINECRAFT/FORGE PLATFORM ABSTRACTION: @@ -122,14 +232,7 @@ to supply us with an ICustomNpc or an IPlayer, etc. which represent the system. DO NOT CHANGE THEIR NAMES AND DO NOT CREATE SEMANTICALLY DUPLICATE INTERFACES/CLASSES . 2) TO BE DECIDED BY YOU. -> **AGENT RECOMMENDATION FOR POINT 1:** -> Feasible. Move the API into platform-api in phases: -> - **Phase 1:** Move ~131 MC-free interfaces directly (retain package structure, strip 5 generic type params) -> - **Phase 2:** Migrate 71 MC-contaminated files by replacing MC type references with platform-api equivalents or Object returns -> - **Phase 3:** Reconcile the 30 existing split-package shadow conflicts (keep one authoritative version) -> - **Phase 4:** Update addon mods (DBC, Gecko, AW) to depend on platform-api artifact instead of git submodule -> -> **AGENT RECOMMENDATION FOR POINT 2 — SEE MIGRATION ROADMAP BELOW** +> **AGENT RECOMMENDATION — SEE .AGENTS/MIGRATION_ROADMAP.md* ________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ diff --git a/core/build.gradle b/core/build.gradle index 1dd0cb75a..6cc928f72 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -9,8 +9,15 @@ version = '1.0' sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 +repositories { + mavenCentral() +} + dependencies { api project(':platform-api') + implementation 'com.google.code.gson:gson:2.8.9' + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation 'org.apache.logging.log4j:log4j-api:2.17.1' } tasks.withType(JavaCompile).configureEach { diff --git a/core/src/main/java/io/netty/buffer/ByteBuf.java b/core/src/main/java/io/netty/buffer/ByteBuf.java new file mode 100644 index 000000000..743f1c143 --- /dev/null +++ b/core/src/main/java/io/netty/buffer/ByteBuf.java @@ -0,0 +1,26 @@ +package io.netty.buffer; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Netty ByteBuf stub for core compilation. + */ +public abstract class ByteBuf { + public abstract ByteBuf writeInt(int value); + public abstract int readInt(); + public abstract ByteBuf writeByte(int value); + public abstract byte readByte(); + public abstract ByteBuf writeShort(int value); + public abstract short readShort(); + public abstract ByteBuf writeLong(long value); + public abstract long readLong(); + public abstract ByteBuf writeFloat(float value); + public abstract float readFloat(); + public abstract ByteBuf writeDouble(double value); + public abstract double readDouble(); + public abstract ByteBuf writeBoolean(boolean value); + public abstract boolean readBoolean(); + public abstract ByteBuf writeBytes(byte[] src); + public abstract ByteBuf readBytes(byte[] dst); + public abstract int readableBytes(); + public abstract boolean isReadable(); +} diff --git a/core/src/main/java/kamkeel/npcs/CustomAttributes.java b/core/src/main/java/kamkeel/npcs/CustomAttributes.java new file mode 100644 index 000000000..cc417f1e2 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/CustomAttributes.java @@ -0,0 +1,77 @@ +package kamkeel.npcs; + +import kamkeel.npcs.controllers.AttributeController; +import kamkeel.npcs.controllers.data.attribute.AttributeDefinition; +import kamkeel.npcs.controllers.data.attribute.AttributeValueType; +import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; +import kamkeel.npcs.controllers.data.attribute.requirement.types.ProfileSlotRequirement; +import kamkeel.npcs.controllers.data.attribute.requirement.types.QuestActiveRequirement; +import kamkeel.npcs.controllers.data.attribute.requirement.types.QuestCompletedRequirement; +import kamkeel.npcs.controllers.data.attribute.requirement.types.SoulbindRequirement; + +/** + * ModAttributes registers the core attributes (both non-magic and magic-based). + */ +public class CustomAttributes { + public static final String HEALTH_KEY = "health"; + public static final String HEALTH_BOOST_KEY = "health_boost"; + public static final String MAIN_ATTACK_KEY = "main_attack"; + public static final String MAIN_ATTACK_BOOST_KEY = "main_attack_boost"; + public static final String NEUTRAL_KEY = "neutral"; + public static final String NEUTRAL_BOOST_KEY = "neutral_boost"; + public static final String CRITICAL_DAMAGE_KEY = "critical_damage"; + public static final String CRITICAL_CHANCE_KEY = "critical_chance"; + public static final String MAGIC_DAMAGE_KEY = "magic_damage"; + public static final String MAGIC_BOOST_KEY = "magic_boost"; + public static final String MAGIC_DEFENSE_KEY = "magic_defense"; + public static final String MAGIC_RESISTANCE_KEY = "magic_resistance"; + public static final String MOVEMENT_SPEED_KEY = "movement_speed"; + public static final String KNOCKBACK_RES_KEY = "knockback_res"; + + public static AttributeDefinition HEALTH; + public static AttributeDefinition HEALTH_BOOST; + public static AttributeDefinition MAIN_ATTACK; + public static AttributeDefinition MAIN_BOOST; + public static AttributeDefinition NEUTRAL_ATTACK; + public static AttributeDefinition NEUTRAL_BOOST; + public static AttributeDefinition CRITICAL_CHANCE; + public static AttributeDefinition CRITICAL_DAMAGE; + public static AttributeDefinition MAGIC_DAMAGE; + public static AttributeDefinition MAGIC_BOOST; + public static AttributeDefinition MAGIC_DEFENSE; + public static AttributeDefinition MAGIC_RESISTANCE; + public static AttributeDefinition MOVEMENT_SPEED; + public static AttributeDefinition KNOCKBACK_RES; + + public CustomAttributes() { + HEALTH = AttributeController.registerAttribute(HEALTH_KEY, "Health", 'c', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.BASE); + HEALTH_BOOST = AttributeController.registerAttribute(HEALTH_BOOST_KEY, "Health Boost", 'c', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER); + HEALTH_BOOST.setTranslationKey("attribute.name.generic.maxHealth"); + + MAIN_ATTACK = AttributeController.registerAttribute(MAIN_ATTACK_KEY, "Main Attack Damage", '4', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.BASE); + MAIN_BOOST = AttributeController.registerAttribute(MAIN_ATTACK_BOOST_KEY, "Main Attack Damage", '4', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER); + + NEUTRAL_ATTACK = AttributeController.registerAttribute(NEUTRAL_KEY, "Neutral Damage", '6', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.BASE); + NEUTRAL_BOOST = AttributeController.registerAttribute(NEUTRAL_BOOST_KEY, "Neutral Damage", '6', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER); + + CRITICAL_CHANCE = AttributeController.registerAttribute(CRITICAL_CHANCE_KEY, "Critical Chance", '6', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.INFO); + CRITICAL_DAMAGE = AttributeController.registerAttribute(CRITICAL_DAMAGE_KEY, "Critical Damage", '6', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.INFO); + + MAGIC_DAMAGE = AttributeController.registerAttribute(MAGIC_DAMAGE_KEY, "Magic Damage", '3', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.BASE); + MAGIC_BOOST = AttributeController.registerAttribute(MAGIC_BOOST_KEY, "Magic Damage", '3', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.MODIFIER); + + MAGIC_DEFENSE = AttributeController.registerAttribute(MAGIC_DEFENSE_KEY, "Magic Defense", 'b', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.BASE); + MAGIC_RESISTANCE = AttributeController.registerAttribute(MAGIC_RESISTANCE_KEY, "Magic Resistance", 'b', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.MODIFIER); + + MOVEMENT_SPEED = AttributeController.registerAttribute(MOVEMENT_SPEED_KEY, "Movement Speed", 'a', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER); + MOVEMENT_SPEED.setTranslationKey("attribute.name.generic.movementSpeed"); + + KNOCKBACK_RES = AttributeController.registerAttribute(KNOCKBACK_RES_KEY, "Knockback Resistance", 'b', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER); + KNOCKBACK_RES.setTranslationKey("attribute.name.generic.knockbackResistance"); + + RequirementCheckerRegistry.registerChecker(new QuestCompletedRequirement()); + RequirementCheckerRegistry.registerChecker(new QuestActiveRequirement()); + RequirementCheckerRegistry.registerChecker(new ProfileSlotRequirement()); + RequirementCheckerRegistry.registerChecker(new SoulbindRequirement()); + } +} diff --git a/core/src/main/java/kamkeel/npcs/addon/AddonHooks.java b/core/src/main/java/kamkeel/npcs/addon/AddonHooks.java new file mode 100644 index 000000000..a1d831130 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/addon/AddonHooks.java @@ -0,0 +1,12 @@ +package kamkeel.npcs.addon; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + * + * Addon hook interface. + */ +public class AddonHooks { + public static void onAbilityCounter(Object ability, Object attacker, Object target) {} +} diff --git a/core/src/main/java/kamkeel/npcs/addon/DBCAddon.java b/core/src/main/java/kamkeel/npcs/addon/DBCAddon.java new file mode 100644 index 000000000..d8f564694 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/addon/DBCAddon.java @@ -0,0 +1,11 @@ +package kamkeel.npcs.addon; + +import kamkeel.npcs.util.ByteBufUtils; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class DBCAddon { + public static boolean isLoaded() { return false; } +} \ No newline at end of file diff --git a/core/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/core/src/main/java/kamkeel/npcs/controllers/AbilityController.java new file mode 100644 index 000000000..ce89ca50d --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -0,0 +1,1521 @@ +package kamkeel.npcs.controllers; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionHPThreshold; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionHitCount; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionItem; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionHasEffect; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionQuestCompleted; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; +import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.extender.IAbilityExtender; +import kamkeel.npcs.controllers.data.ability.CustomAbility; +import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; +import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; +import kamkeel.npcs.controllers.data.ability.type.AbilityDash; +import kamkeel.npcs.controllers.data.ability.type.AbilityCounter; +import kamkeel.npcs.controllers.data.ability.type.AbilityDodge; +import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; +import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; +import kamkeel.npcs.controllers.data.ability.type.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; +import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; +import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; +import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; +import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; +import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDisc; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaser; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityOrb; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityShield; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySweeper; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityWall; +import noppes.npcs.controllers.data.AbilityScript; +import noppes.npcs.controllers.data.ChainedAbilityScript; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IChainedAbility; +import noppes.npcs.api.handler.IAbilityHandler; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.CategoryManager; +import noppes.npcs.controllers.TagController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.util.NBTJsonUtil; +import kamkeel.npcs.util.FileNameHelper; + +import java.io.File; +import java.util.*; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class AbilityController implements IAbilityHandler { + + public static AbilityController Instance = new AbilityController(); + + // ── Core Registries ────────────────────────────────────────────────────── + private final Map> abilityTypes = new LinkedHashMap<>(); + private final Map builtAbilities = new LinkedHashMap<>(); + private final Map customAbilities = new LinkedHashMap<>(); // name → Ability + private final Map customAbilitiesById = new LinkedHashMap<>(); // UUID → Ability + + // ── Script Handlers ──────────────────────────────────────────────────── + public final HashMap abilityScriptHandlers = new HashMap<>(); + public final HashMap chainedAbilityScriptHandlers = new HashMap<>(); + + // ── Extension Points ───────────────────────────────────────────────────── + private final Map> externalVariants = new LinkedHashMap<>(); + private final List fieldProviders = new ArrayList<>(); + private final List chainedFieldProviders = new ArrayList<>(); + private final List extenders = new ArrayList<>(); + private final Map> conditionTypes = new HashMap<>(); + private final List> flightCheckers = new ArrayList<>(); + private final List> activationCheckers = new ArrayList<>(); + + // ── Chained Abilities ────────────────────────────────────────────────── + private final Map chainedAbilities = new LinkedHashMap<>(); // name → ChainedAbility + private final Map chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility + private int chainedAbilityRevision = 0; + + // ── Category Managers ───────────────────────────────────────────────── + public final CategoryManager customAbilityCategories = new CategoryManager(); + public final CategoryManager chainedAbilityCategories = new CategoryManager(); + private final Map customAbilityCatMap = new HashMap<>(); // abilityName → catId + private final Map chainedAbilityCatMap = new HashMap<>(); // chainName → catId + + // ── Effect Action Registry ────────────────────────────────────────────── + private final Map effectActions = new LinkedHashMap<>(); + + // ── Derived State ──────────────────────────────────────────────────────── + private final Set builtInTypeIds = new HashSet<>(); + + public AbilityController() { + registerBuiltinTypes(); + registerBuiltinConditionTypes(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TYPE REGISTRATION + // ═══════════════════════════════════════════════════════════════════════════ + + public void registerType(Supplier factory) { + Ability temp = factory.get(); + String typeId = temp.getTypeId(); + if (abilityTypes.containsKey(typeId)) { + LogWriter.info("AbilityController: Overwriting type: " + typeId); + } + abilityTypes.put(typeId, factory); + if (temp.isBuiltIn()) { + builtInTypeIds.add(typeId); + } + } + + public Ability create(String typeId) { + Supplier factory = abilityTypes.get(typeId); + return factory != null ? factory.get() : null; + } + + public Ability fromNBT(INbt nbt) { + if (nbt == null || !nbt.hasKey("typeId")) { + return null; + } + + String typeId = nbt.getString("typeId"); + Supplier factory = abilityTypes.get(typeId); + if (factory == null) { + LogWriter.info("AbilityController: Unknown ability type: " + typeId); + return null; + } + + Ability ability = factory.get(); + ability.readNBT(nbt); + return ability; + } + + private void registerBuiltinTypes() { + registerType(CustomAbility::new); + + registerType(AbilitySlam::new); + registerType(AbilityCharge::new); + + registerType(AbilityHeavyHit::new); + registerType(AbilityCutter::new); + + registerType(AbilitySweeper::new); + registerType(AbilityOrb::new); + registerType(AbilityDisc::new); + registerType(AbilityLaser::new); + registerType(AbilityBeam::new); + + registerType(AbilityDash::new); + registerType(AbilityTeleport::new); + + registerType(AbilityVortex::new); + registerType(AbilityShockwave::new); + + registerType(AbilityGuard::new); + registerType(AbilityCounter::new); + registerType(AbilityDodge::new); + registerType(AbilityEffect::new); + + registerType(AbilityHazard::new); + registerType(AbilityTrap::new); + + registerType(AbilityDome::new); + registerType(AbilityWall::new); + registerType(AbilityShield::new); + } + + private void registerBuiltinConditionTypes() { + registerCondition(ConditionHPThreshold::new); + registerCondition(ConditionHitCount::new); + registerCondition(ConditionItem::new); + registerCondition(ConditionQuestCompleted::new); + registerCondition(ConditionHasEffect::new); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // BUILT-IN ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + + public void registerAbility(String name, Ability ability) { + if (builtAbilities.containsKey(name)) { + LogWriter.info("AbilityController: Overwriting built-in ability: " + name); + } + if (!ability.isBuiltIn()) { + ability.setName(name); + } + builtAbilities.put(name, ability); + LogWriter.info("Registered ability: " + name); + } + + public Ability getAbility(String name) { + return builtAbilities.get(name); + } + + public Ability getAbilityByDisplayName(String displayName) { + for (Ability ability : builtAbilities.values()) { + if (ability.getDisplayName().equals(displayName)) { + return ability; + } + } + return null; + } + + public Set getAbilityNames() { + return new LinkedHashSet<>(builtAbilities.keySet()); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CUSTOM ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + + // Incremented on any custom ability change; used by AbilitySlot for cache invalidation + private int customAbilityRevision = 0; + + public void load() { + customAbilities.clear(); + customAbilitiesById.clear(); + abilityScriptHandlers.clear(); + customAbilityCatMap.clear(); + + File dir = getDir(); + customAbilityCategories.loadCategories(dir, "chained"); + + // Load from root directory (Uncategorized) + loadAbilitiesFromDir(dir, CategoryManager.UNCATEGORIZED_ID); + + // Load from category subdirectories + for (Map.Entry entry : customAbilityCategories.getCategories().entrySet()) { + File catDir = new File(dir, entry.getValue().title); + loadAbilitiesFromDir(catDir, entry.getKey()); + } + + customAbilityRevision++; + LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); + + loadChainedAbilities(); + } + + private void loadAbilitiesFromDir(File dir, int catId) { + File[] files = dir.exists() ? dir.listFiles() : null; + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + String filename = file.getName(); + String fileKey = filename.substring(0, filename.length() - 5); + + INbt nbt = NBTJsonUtil.LoadFile(file); + Ability ability = fromNBT(nbt); + if (ability == null) continue; + + String name = FileNameHelper.sanitizeName(fileKey, "Ability"); + name = makeUniqueNameForLoad(customAbilities, dir, file, name, fileKey); + + boolean dirty = false; + if (!name.equals(fileKey)) { + File renamedFile = new File(dir, name + ".json"); + if (file.renameTo(renamedFile)) { + file = renamedFile; + dirty = true; + } + } + + ability.setName(name); + + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); + dirty = true; + } + + if (dirty) { + NBTJsonUtil.SaveFile(file, ability.writeNBT(true)); + } + + customAbilities.put(name, ability); + customAbilitiesById.put(uuid, ability); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + customAbilityCatMap.put(name, catId); + } + } catch (Exception e) { + LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); + } + } + } + + private boolean nameFileExists(File dir, String name, File ignoredFile) { + File existing = new File(dir, name + ".json"); + if (!existing.exists()) return false; + return ignoredFile == null || !existing.getAbsolutePath().equals(ignoredFile.getAbsolutePath()); + } + + private String makeUniqueNameForLoad(Map loadedMap, File dir, File currentFile, String baseName, String selfKey) { + String candidate = baseName; + while ((loadedMap.containsKey(candidate) && !candidate.equals(selfKey)) + || nameFileExists(dir, candidate, currentFile)) { + candidate = candidate + "_"; + } + return candidate; + } + + private String makeUniqueNameForMap(Map map, String baseName, String selfName) { + String candidate = baseName; + while (map.containsKey(candidate) && (selfName == null || !candidate.equals(selfName))) { + candidate = candidate + "_"; + } + return candidate; + } + + public boolean saveCustomAbility(Ability ability) { + if (ability == null) return false; + if (ability.isBuiltIn()) return false; + + String name = FileNameHelper.sanitizeName(ability.getName(), "Ability"); + ability.setName(name); + + String uuid = ability.getId(); + boolean isNew = (uuid == null || uuid.isEmpty()); + int catId = CategoryManager.UNCATEGORIZED_ID; + + if (isNew) { + // Generate UUID for new ability + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); + + // Ensure unique name + name = makeUniqueNameForMap(customAbilities, name, null); + ability.setName(name); + } else { + // Existing ability — check for rename via UUID lookup + Ability existing = customAbilitiesById.get(uuid); + if (existing != null) { + String oldName = existing.getName(); + catId = customAbilityCatMap.getOrDefault(oldName, CategoryManager.UNCATEGORIZED_ID); + name = makeUniqueNameForMap(customAbilities, name, oldName); + ability.setName(name); + + if (!oldName.equals(name)) { + // Delete old file and remove old map entry + File oldDir = getCustomAbilityDir(oldName); + File oldFile = new File(oldDir, oldName + ".json"); + if (oldFile.exists()) { + oldFile.delete(); + } + customAbilities.remove(oldName); + // Update catMap key + customAbilityCatMap.remove(oldName); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + customAbilityCatMap.put(name, catId); + } + } + } else { + name = makeUniqueNameForMap(customAbilities, name, null); + ability.setName(name); + } + } + + TagController.validateTagUUIDs(ability.getTagUUIDs()); + + File dir = getCustomAbilityDir(name); + File fileNew = new File(dir, name + ".json_new"); + File fileCurrent = new File(dir, name + ".json"); + + try { + INbt nbt = ability.writeNBT(true); + + NBTJsonUtil.SaveFile(fileNew, nbt); + + if (fileCurrent.exists()) { + fileCurrent.delete(); + } + fileNew.renameTo(fileCurrent); + if (fileNew.exists()) { + fileNew.delete(); + } + + customAbilities.put(name, ability); + customAbilitiesById.put(uuid, ability); + customAbilityRevision++; + LogWriter.script("Saved custom ability: " + name + " [" + uuid + "]"); + SyncController.syncAllCustomAbilities(); + return true; + } catch (Exception e) { + LogWriter.error("Error saving custom ability: " + name, e); + return false; + } + } + + public Ability cloneCustomAbility(String originalName) { + Ability original = customAbilities.get(originalName); + if (original == null || original.isBuiltIn()) return null; + + int originalCatId = customAbilityCatMap.getOrDefault(originalName, CategoryManager.UNCATEGORIZED_ID); + + INbt nbt = original.writeNBT(true); + nbt.setString("id", UUID.randomUUID().toString()); + + Ability clone = fromNBT(nbt); + if (clone == null) return null; + + String name = clone.getName(); + while (customAbilities.containsKey(name)) name += "_"; + clone.setName(name); + + saveCustomAbility(clone); + + if (originalCatId > CategoryManager.UNCATEGORIZED_ID) { + customAbilityCatMap.put(clone.getName(), originalCatId); + } + + return clone; + } + + public boolean deleteCustomAbility(String name) { + if (name == null || name.isEmpty()) return false; + + Ability removed = customAbilities.remove(name); + if (removed == null) { + String sanitized = FileNameHelper.sanitizeTextInput(name); + if (!sanitized.equals(name)) { + removed = customAbilities.remove(sanitized); + if (removed != null) { + name = sanitized; + } + } + } + if (removed == null) return false; + + // Also remove from UUID index and script handler + String uuid = removed.getId(); + if (uuid != null && !uuid.isEmpty()) { + customAbilitiesById.remove(uuid); + abilityScriptHandlers.remove(uuid); + } + + File dir = getCustomAbilityDir(name); + File file = new File(dir, name + ".json"); + if (file.exists()) { + file.delete(); + } + customAbilityCatMap.remove(name); + + // Clean up online players' unlocked ability lists + if (PlayerDataController.Instance != null) { + List snapshot = new ArrayList<>(PlayerDataController.Instance.getAllPlayerData()); + for (PlayerData pData : snapshot) { + if (pData.abilityData != null) { + boolean changed = false; + if (uuid != null && !uuid.isEmpty() && pData.abilityData.hasUnlockedAbility(uuid)) { + pData.abilityData.lockAbility(uuid); + changed = true; + } + if (pData.abilityData.hasUnlockedAbility(name)) { + pData.abilityData.lockAbility(name); + changed = true; + } + if (changed) { + pData.save(); + } + } + } + } + + customAbilityRevision++; + LogWriter.script("Deleted custom ability: " + name); + SyncController.syncAllCustomAbilities(); + return true; + } + + public Ability getCustomAbility(String name) { + Ability ability = customAbilities.get(name); + if (ability != null) return ability; + String sanitized = FileNameHelper.sanitizeTextInput(name); + if (!sanitized.equals(name)) { + return customAbilities.get(sanitized); + } + return null; + } + + public Set getCustomAbilityNames() { + return new LinkedHashSet<>(customAbilities.keySet()); + } + + public Map getCustomAbilities() { + return customAbilities; + } + + public synchronized void setCustomAbilities(Map synced) { + customAbilities.clear(); + customAbilitiesById.clear(); + customAbilities.putAll(synced); + for (Ability ability : synced.values()) { + String uuid = ability.getId(); + if (uuid != null && !uuid.isEmpty()) { + customAbilitiesById.put(uuid, ability); + } + } + customAbilityRevision++; + } + + public int getCustomAbilityRevision() { + return customAbilityRevision; + } + + private File getDir() { + File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities"); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir; + } + + private File getCustomAbilityDir(String name) { + int catId = customAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + return customAbilityCategories.getCategoryDir(catId); + } + + private File getChainedAbilityDir(String name) { + int catId = chainedAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + return chainedAbilityCategories.getCategoryDir(catId); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CUSTOM ABILITY CATEGORY HELPERS + // ═══════════════════════════════════════════════════════════════════════════ + + public Map getCustomAbilityCategoryScrollData() { + return customAbilityCategories.getCategoryScrollData(); + } + + public Map getCustomAbilityItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + for (Map.Entry entry : customAbilities.entrySet()) { + int assignedCat = customAbilityCatMap.getOrDefault(entry.getKey(), CategoryManager.UNCATEGORIZED_ID); + if (assignedCat == catId) { + map.put(entry.getKey(), entry.getValue().getAllowedBy().ordinal()); + } + } + return map; + } + + public HashMap> getCustomAbilityTagMapForCategory(int catId) { + HashMap> tagMap = new HashMap<>(); + for (Map.Entry entry : customAbilities.entrySet()) { + int assignedCat = customAbilityCatMap.getOrDefault(entry.getKey(), CategoryManager.UNCATEGORIZED_ID); + if (assignedCat == catId && !entry.getValue().getTagUUIDs().isEmpty()) { + tagMap.put(entry.getKey(), entry.getValue().getTagUUIDs()); + } + } + return tagMap; + } + + public void moveCustomAbilityToCategory(String name, int destCatId) { + Ability ability = customAbilities.get(name); + if (ability == null) return; + + int oldCatId = customAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + if (oldCatId == destCatId) return; + + File oldDir = customAbilityCategories.getCategoryDir(oldCatId); + File newDir = customAbilityCategories.getCategoryDir(destCatId); + if (!newDir.exists()) newDir.mkdirs(); + + File oldFile = new File(oldDir, name + ".json"); + File newFile = new File(newDir, name + ".json"); + if (oldFile.exists()) oldFile.renameTo(newFile); + + if (destCatId == CategoryManager.UNCATEGORIZED_ID) { + customAbilityCatMap.remove(name); + } else { + customAbilityCatMap.put(name, destCatId); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CHAINED ABILITY CATEGORY HELPERS + // ═══════════════════════════════════════════════════════════════════════════ + + public Map getChainedAbilityCategoryScrollData() { + return chainedAbilityCategories.getCategoryScrollData(); + } + + public Map getChainedAbilityItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + for (Map.Entry entry : chainedAbilities.entrySet()) { + int assignedCat = chainedAbilityCatMap.getOrDefault(entry.getKey(), CategoryManager.UNCATEGORIZED_ID); + if (assignedCat == catId) { + map.put(entry.getKey(), entry.getValue().getAllowedBy().ordinal()); + } + } + return map; + } + + public void moveChainedAbilityToCategory(String name, int destCatId) { + ChainedAbility chain = chainedAbilities.get(name); + if (chain == null) return; + + int oldCatId = chainedAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + if (oldCatId == destCatId) return; + + File oldDir = chainedAbilityCategories.getCategoryDir(oldCatId); + File newDir = chainedAbilityCategories.getCategoryDir(destCatId); + if (!newDir.exists()) newDir.mkdirs(); + + File oldFile = new File(oldDir, name + ".json"); + File newFile = new File(newDir, name + ".json"); + if (oldFile.exists()) oldFile.renameTo(newFile); + + if (destCatId == CategoryManager.UNCATEGORIZED_ID) { + chainedAbilityCatMap.remove(name); + } else { + chainedAbilityCatMap.put(name, destCatId); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CHAINED ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + + private void loadChainedAbilities() { + chainedAbilities.clear(); + chainedAbilitiesById.clear(); + chainedAbilityScriptHandlers.clear(); + chainedAbilityCatMap.clear(); + + File dir = getChainedDir(); + chainedAbilityCategories.loadCategories(dir); + + // Load from root directory (Uncategorized) + loadChainedFromDir(dir, CategoryManager.UNCATEGORIZED_ID); + + // Load from category subdirectories + for (Map.Entry entry : chainedAbilityCategories.getCategories().entrySet()) { + File catDir = new File(dir, entry.getValue().title); + loadChainedFromDir(catDir, entry.getKey()); + } + + chainedAbilityRevision++; + LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities"); + } + + private void loadChainedFromDir(File dir, int catId) { + File[] files = dir.exists() ? dir.listFiles() : null; + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + String filename = file.getName(); + String fileKey = filename.substring(0, filename.length() - 5); + + INbt nbt = NBTJsonUtil.LoadFile(file); + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt); + + String name = FileNameHelper.sanitizeName(fileKey, "Chain"); + name = makeUniqueNameForLoad(chainedAbilities, dir, file, name, fileKey); + + boolean dirty = false; + if (!name.equals(fileKey)) { + File renamedFile = new File(dir, name + ".json"); + if (file.renameTo(renamedFile)) { + file = renamedFile; + dirty = true; + } + } + + chain.setName(name); + + String uuid = chain.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + chain.setId(uuid); + dirty = true; + } + + if (dirty) { + NBTJsonUtil.SaveFile(file, chain.writeNBT(true)); + } + + chainedAbilities.put(name, chain); + chainedAbilitiesById.put(uuid, chain); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + chainedAbilityCatMap.put(name, catId); + } + } catch (Exception e) { + LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e); + } + } + } + + public boolean saveChainedAbility(ChainedAbility chain) { + if (chain == null) return false; + + String name = FileNameHelper.sanitizeName(chain.getName(), "Chain"); + chain.setName(name); + + String uuid = chain.getId(); + boolean isNew = (uuid == null || uuid.isEmpty()); + + if (isNew) { + uuid = UUID.randomUUID().toString(); + chain.setId(uuid); + + name = makeUniqueNameForMap(chainedAbilities, name, null); + chain.setName(name); + } else { + ChainedAbility existing = chainedAbilitiesById.get(uuid); + if (existing != null) { + String oldName = existing.getName(); + int catId = chainedAbilityCatMap.getOrDefault(oldName, CategoryManager.UNCATEGORIZED_ID); + name = makeUniqueNameForMap(chainedAbilities, name, oldName); + chain.setName(name); + + if (!oldName.equals(name)) { + File oldDir = getChainedAbilityDir(oldName); + File oldFile = new File(oldDir, oldName + ".json"); + if (oldFile.exists()) { + oldFile.delete(); + } + chainedAbilities.remove(oldName); + chainedAbilityCatMap.remove(oldName); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + chainedAbilityCatMap.put(name, catId); + } + } + } else { + name = makeUniqueNameForMap(chainedAbilities, name, null); + chain.setName(name); + } + } + + File dir = getChainedAbilityDir(name); + File fileNew = new File(dir, name + ".json_new"); + File fileCurrent = new File(dir, name + ".json"); + + try { + INbt nbt = chain.writeNBT(true); + + NBTJsonUtil.SaveFile(fileNew, nbt); + + if (fileCurrent.exists()) { + fileCurrent.delete(); + } + fileNew.renameTo(fileCurrent); + if (fileNew.exists()) { + fileNew.delete(); + } + + chainedAbilities.put(name, chain); + chainedAbilitiesById.put(uuid, chain); + chainedAbilityRevision++; + LogWriter.script("Saved chained ability: " + name + " [" + uuid + "]"); + SyncController.syncAllChainedAbilities(); + return true; + } catch (Exception e) { + LogWriter.error("Error saving chained ability: " + name, e); + return false; + } + } + + public ChainedAbility cloneChainedAbility(String originalName) { + ChainedAbility original = chainedAbilities.get(originalName); + if (original == null) return null; + + int originalCatId = chainedAbilityCatMap.getOrDefault(originalName, CategoryManager.UNCATEGORIZED_ID); + + INbt nbt = original.writeNBT(true); + nbt.setString("Id", UUID.randomUUID().toString()); + + ChainedAbility clone = new ChainedAbility(); + clone.readNBT(nbt); + + String name = clone.getName(); + while (chainedAbilities.containsKey(name)) name += "_"; + clone.setName(name); + + saveChainedAbility(clone); + + if (originalCatId > CategoryManager.UNCATEGORIZED_ID) { + chainedAbilityCatMap.put(clone.getName(), originalCatId); + } + + return clone; + } + + public boolean deleteChainedAbility(String name) { + if (name == null || name.isEmpty()) return false; + + ChainedAbility removed = chainedAbilities.remove(name); + if (removed == null) { + String sanitized = FileNameHelper.sanitizeTextInput(name); + if (!sanitized.equals(name)) { + removed = chainedAbilities.remove(sanitized); + if (removed != null) { + name = sanitized; + } + } + } + if (removed == null) return false; + + String uuid = removed.getId(); + if (uuid != null && !uuid.isEmpty()) { + chainedAbilitiesById.remove(uuid); + chainedAbilityScriptHandlers.remove(uuid); + } + + File dir = getChainedAbilityDir(name); + File file = new File(dir, name + ".json"); + if (file.exists()) { + file.delete(); + } + chainedAbilityCatMap.remove(name); + + // Clean up online players' unlocked ability lists (chain keys use "chain:" prefix) + if (PlayerDataController.Instance != null) { + List snapshot = new ArrayList<>(PlayerDataController.Instance.getAllPlayerData()); + for (PlayerData pData : snapshot) { + if (pData.abilityData != null) { + String chainKey = "chain:" + (uuid != null && !uuid.isEmpty() ? uuid : name); + if (pData.abilityData.hasUnlockedAbility(chainKey)) { + pData.abilityData.lockAbility(chainKey); + } + String chainNameKey = "chain:" + name; + if (!chainNameKey.equals(chainKey) && pData.abilityData.hasUnlockedAbility(chainNameKey)) { + pData.abilityData.lockAbility(chainNameKey); + } + } + } + } + + chainedAbilityRevision++; + LogWriter.script("Deleted chained ability: " + name); + SyncController.syncAllChainedAbilities(); + return true; + } + + public ChainedAbility resolveChainedAbility(String key) { + if (key == null || key.isEmpty()) return null; + + ChainedAbility byId = chainedAbilitiesById.get(key); + if (byId != null) return byId.deepCopy(); + + ChainedAbility chain = chainedAbilities.get(key); + if (chain != null) return chain.deepCopy(); + + for (Map.Entry entry : chainedAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue().deepCopy(); + } + } + + String sanitizedKey = FileNameHelper.sanitizeTextInput(key); + if (!sanitizedKey.equals(key)) { + ChainedAbility sanitized = chainedAbilities.get(sanitizedKey); + if (sanitized != null) return sanitized.deepCopy(); + } + + return null; + } + + /** + * Resolve a chained ability by key and return the live (non-copied) reference. + * For validation/permission checks only — do NOT expose or modify the returned chain. + * Uses the same lookup chain as {@link #resolveChainedAbility(String)}. + */ + public ChainedAbility peekChainedAbility(String key) { + if (key == null || key.isEmpty()) return null; + + ChainedAbility byId = chainedAbilitiesById.get(key); + if (byId != null) return byId; + + ChainedAbility chain = chainedAbilities.get(key); + if (chain != null) return chain; + + for (Map.Entry entry : chainedAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + + String sanitizedKey = FileNameHelper.sanitizeTextInput(key); + if (!sanitizedKey.equals(key)) { + ChainedAbility sanitized = chainedAbilities.get(sanitizedKey); + if (sanitized != null) return sanitized; + } + + return null; + } + + public boolean canResolveChainedAbility(String key) { + if (key == null || key.isEmpty()) return false; + if (chainedAbilitiesById.containsKey(key)) return true; + if (chainedAbilities.containsKey(key)) return true; + for (String name : chainedAbilities.keySet()) { + if (name.equalsIgnoreCase(key)) return true; + } + String sanitizedKey = FileNameHelper.sanitizeTextInput(key); + if (!sanitizedKey.equals(key) && chainedAbilities.containsKey(sanitizedKey)) return true; + return false; + } + + @Override + public ChainedAbility getChainedAbility(String name) { + ChainedAbility chain = chainedAbilities.get(name); + if (chain == null) { + String sanitized = FileNameHelper.sanitizeTextInput(name); + if (!sanitized.equals(name)) { + chain = chainedAbilities.get(sanitized); + } + } + return chain != null ? chain.deepCopy() : null; + } + + public ChainedAbility getChainedAbilityByUUID(String uuid) { + return chainedAbilitiesById.get(uuid); + } + + public Set getChainedAbilityNamesSet() { + return new LinkedHashSet<>(chainedAbilities.keySet()); + } + + public Map getChainedAbilities() { + return chainedAbilities; + } + + @Override + public String[] getChainedAbilityNames() { + return chainedAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasChainedAbilityName(String name) { + if (chainedAbilities.containsKey(name)) return true; + String sanitized = FileNameHelper.sanitizeTextInput(name); + return !sanitized.equals(name) && chainedAbilities.containsKey(sanitized); + } + + @Override + public boolean deleteChainedAbilityByName(String name) { + return deleteChainedAbility(name); + } + + @Override + public boolean saveChainedAbility(IChainedAbility chain) { + if (chain instanceof ChainedAbility) { + return saveChainedAbility((ChainedAbility) chain); + } + return false; + } + + public int getChainedAbilityRevision() { + return chainedAbilityRevision; + } + + public synchronized void setChainedAbilities(Map synced) { + chainedAbilities.clear(); + chainedAbilitiesById.clear(); + chainedAbilities.putAll(synced); + for (ChainedAbility chain : synced.values()) { + String uuid = chain.getId(); + if (uuid != null && !uuid.isEmpty()) { + chainedAbilitiesById.put(uuid, chain); + } + } + chainedAbilityRevision++; + } + + private File getChainedDir() { + File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities" + File.separator + "chained"); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // RESOLUTION + // ═══════════════════════════════════════════════════════════════════════════ + + public Ability resolveAbility(String key) { + if (key == null || key.isEmpty()) return null; + + // Custom: UUID lookup (most common for persistent references) + Ability byUuid = customAbilitiesById.get(key); + if (byUuid != null) return byUuid.deepCopy(); + + // Built-in: exact key + Ability builtIn = builtAbilities.get(key); + if (builtIn != null) return builtIn.deepCopy(); + + // Built-in: case-insensitive key + String lowerKey = key.toLowerCase(); + for (Map.Entry entry : builtAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(lowerKey)) { + return entry.getValue().deepCopy(); + } + } + + // Built-in: by ID (case-insensitive) + for (Ability ability : builtAbilities.values()) { + if (key.equalsIgnoreCase(ability.getId())) { + return ability.deepCopy(); + } + } + + // Custom: exact name + Ability custom = customAbilities.get(key); + if (custom != null) return custom.deepCopy(); + + // Custom: case-insensitive name + for (Map.Entry entry : customAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue().deepCopy(); + } + } + + return null; + } + + public Set getAbilityKeys() { + Set keys = new LinkedHashSet<>(); + for (Ability ability : builtAbilities.values()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + // Custom abilities keyed by UUID (stable references) + for (Ability ability : customAbilities.values()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + return keys; + } + + public Set getPlayerAbilityKeys() { + Set keys = new LinkedHashSet<>(); + for (Ability ability : builtAbilities.values()) { + if (ability.getAllowedBy().allowsPlayer()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + } + // Custom abilities keyed by UUID (stable references) + for (Ability ability : customAbilities.values()) { + if (ability.getAllowedBy().allowsPlayer()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + } + return keys; + } + + public boolean hasAbility(String key) { + return builtAbilities.containsKey(key) || customAbilities.containsKey(key) + || customAbilitiesById.containsKey(key); + } + + public Ability getCustomAbilityByUUID(String uuid) { + return customAbilitiesById.get(uuid); + } + + public Ability getCustomAbilityByName(String name) { + return customAbilities.get(name); + } + + /** + * Resolve an ability by key and return the live (non-copied) reference. + * For validation/permission checks only — do NOT expose or modify the returned ability. + * Uses the same lookup chain as {@link #resolveAbility(String)}. + */ + public Ability peekAbility(String key) { + if (key == null || key.isEmpty()) return null; + + // Custom: UUID lookup + Ability byUuid = customAbilitiesById.get(key); + if (byUuid != null) return byUuid; + + // Built-in: exact key + Ability builtIn = builtAbilities.get(key); + if (builtIn != null) return builtIn; + + // Built-in: case-insensitive key + for (Map.Entry entry : builtAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + + // Built-in: by ID (case-insensitive) + for (Ability ability : builtAbilities.values()) { + if (key.equalsIgnoreCase(ability.getId())) return ability; + } + + // Custom: exact name + Ability custom = customAbilities.get(key); + if (custom != null) return custom; + + // Custom: case-insensitive name + for (Map.Entry entry : customAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + + return null; + } + + /** + * Check if a key can be resolved to a valid ability without creating a deep copy. + * Uses the same lookup chain as {@link #resolveAbility(String)}. + */ + public boolean canResolveAbility(String key) { + if (key == null || key.isEmpty()) return false; + + // Custom: UUID lookup + if (customAbilitiesById.containsKey(key)) return true; + + // Built-in: exact key + if (builtAbilities.containsKey(key)) return true; + + // Built-in: by ID + for (Ability ability : builtAbilities.values()) { + if (key.equals(ability.getId())) return true; + } + + // Custom: exact name + if (customAbilities.containsKey(key)) return true; + + return false; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // UNIFIED ACTION RESOLUTION + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Resolve any action key to an IAbilityAction. + * Checks abilities first, then chained abilities. + */ + public IAbilityAction resolveAction(String key) { + if (key == null || key.isEmpty()) return null; + + Ability ability = resolveAbility(key); + if (ability != null) return ability; + + return resolveChainedAbility(key); + } + + /** + * Check if a key can be resolved as any action (ability or chain). + */ + public boolean canResolveAction(String key) { + if (canResolveAbility(key)) return true; + return canResolveChainedAbility(key); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // EXTENSION POINTS + // ═══════════════════════════════════════════════════════════════════════════ + + public void registerVariant(String typeId, AbilityVariant variant) { + externalVariants.computeIfAbsent(typeId, k -> new ArrayList<>()).add(variant); + } + + public List getVariantsForType(String typeId) { + List result = new ArrayList<>(); + Ability temp = create(typeId); + if (temp != null) { + result.addAll(temp.getVariants()); + } + List ext = externalVariants.get(typeId); + if (ext != null) { + // If no built-in variants exist but external ones do, + // inject a "Base" variant so the user always gets a choice + if (result.isEmpty()) { + result.add(new AbilityVariant("ability.variant.base", a -> { + })); + } + result.addAll(ext); + } + return result; + } + + public void registerFieldProvider(IAbilityFieldProvider provider) { + fieldProviders.add(provider); + } + + public List getFieldProviders() { + return fieldProviders; + } + + public void registerChainedFieldProvider(IChainedAbilityFieldProvider provider) { + chainedFieldProviders.add(provider); + } + + public List getChainedFieldProviders() { + return chainedFieldProviders; + } + + public void registerExtender(IAbilityExtender extender) { + extenders.add(extender); + } + + public List getExtenders() { + return extenders; + } + + public void registerCondition(Supplier conditionFactory) { + AbilityCondition temp = conditionFactory.get(); + String typeId = temp.getTypeId(); + if (conditionTypes.containsKey(typeId)) { + LogWriter.info("AbilityController: Overwriting Condition type: " + typeId); + } + conditionTypes.put(typeId, conditionFactory); + } + + public Supplier getConditionType(String key) { + return conditionTypes.get(key); + } + + public String[] getConditionTypes() { + return conditionTypes.keySet().toArray(new String[0]); + } + + public List getConditionNamespaces() { + LinkedHashSet seen = new LinkedHashSet<>(); + for (String typeId : conditionTypes.keySet()) { + String[] parts = typeId.split("\\.", 3); + if (parts.length >= 2) { + seen.add(parts[1]); + } + } + return new ArrayList<>(seen); + } + + public String[] getConditionTypesByNamespace(String namespace) { + String prefix = "condition." + namespace + "."; + List result = new ArrayList<>(); + for (String typeId : conditionTypes.keySet()) { + if (typeId.startsWith(prefix)) { + result.add(typeId); + } + } + return result.toArray(new String[0]); + } + + public void registerFlightChecker(Predicate checker) { + flightCheckers.add(checker); + } + + public boolean isPlayerFlying(IPlayer player) { + if (player.capabilities.isFlying) return true; + for (Predicate checker : flightCheckers) { + if (checker.test(player)) return true; + } + return false; + } + + public void registerActivationChecker(Predicate checker) { + activationCheckers.add(checker); + } + + public boolean canPlayerActivate(IPlayer player) { + for (Predicate checker : activationCheckers) { + if (!checker.test(player)) return false; + } + return true; + } + + /** + * Fire onAbilityStart on all extenders. Returns false if ANY extender cancels. + */ + public boolean fireOnAbilityStart(Ability ability, IEntityLivingBase caster, IEntityLivingBase target) { + for (IAbilityExtender ext : extenders) { + if (!ext.onAbilityStart(ability, caster, target)) { + return false; + } + } + return true; + } + + /** + * Fire onAbilityTick on all extenders. Returns false if ANY extender says to interrupt. + */ + public boolean fireOnAbilityTick(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, + AbilityPhase phase, int tick) { + for (IAbilityExtender ext : extenders) { + if (!ext.onAbilityTick(ability, caster, target, phase, tick)) { + return false; + } + } + return true; + } + + /** + * Fire onAbilityComplete on all extenders. + */ + public void fireOnAbilityComplete(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, + boolean interrupted) { + for (IAbilityExtender ext : extenders) { + ext.onAbilityComplete(ability, caster, target, interrupted); + } + } + + /** + * Fire onAbilityDamage on all extenders. Chain of responsibility — first true wins. + */ + public boolean fireOnAbilityDamage(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ, + float damageMultiplier) { + for (IAbilityExtender ext : extenders) { + if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp, + knockbackDirX, knockbackDirZ, damageMultiplier)) { + return true; + } + } + return false; + } + + /** + * Fire modifyProjectileDamage on all extenders. Cumulative — each extender's output feeds the next. + */ + public float fireModifyProjectileDamage(Ability ability, IEntityLivingBase caster, float baseDamage) { + float damage = baseDamage; + for (IAbilityExtender ext : extenders) { + damage = ext.modifyProjectileDamage(ability, caster, damage); + } + return damage; + } + + /** + * Fire modifyBarrierHealth on all extenders. Cumulative — each extender's output feeds the next. + */ + public float fireModifyBarrierHealth(Ability ability, IEntityLivingBase caster, float baseHealth) { + float health = baseHealth; + for (IAbilityExtender ext : extenders) { + health = ext.modifyBarrierHealth(ability, caster, health); + } + return health; + } + + /** + * Fire onAbilityHeal on all extenders. Chain of responsibility — first true wins. + */ + public boolean fireOnAbilityHeal(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, float healAmount) { + for (IAbilityExtender ext : extenders) { + if (ext.onAbilityHeal(ability, caster, target, healAmount)) { + return true; + } + } + return false; + } + + public Boolean fireCheckConditions(AbilityCondition condition, IEntityLivingBase caster, IEntityLivingBase target) { + for (IAbilityExtender ext : extenders) { + Boolean result = ext.onCheckCondition(condition, caster, target); + if (result != null) { + return result; + } + } + + return null; + } + + public Boolean fireCheckConditionsForPlayer(AbilityCondition condition, IEntityLivingBase player) { + for (IAbilityExtender ext : extenders) { + Boolean result = ext.onCheckConditionForPlayer(condition, player); + if (result != null) { + return result; + } + } + return null; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // IAbilityHandler + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public String[] getTypes() { + return abilityTypes.keySet().toArray(new String[0]); + } + + public boolean isBuiltInType(String typeId) { + return builtInTypeIds.contains(typeId); + } + + public boolean isConcurrentCapableType(String typeId) { + Supplier factory = abilityTypes.get(typeId); + if (factory == null) return false; + return factory.get().isConcurrentCapable(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // EFFECT ACTION REGISTRY + // ═══════════════════════════════════════════════════════════════════════════ + + public void registerEffectAction(IEffectAction action) { + if (action == null || action.getId() == null) return; + effectActions.put(action.getId(), action); + } + + public IEffectAction getEffectAction(String id) { + return effectActions.get(id); + } + + public String[] getEffectActionIds() { + return effectActions.keySet().toArray(new String[0]); + } + + public java.util.Collection getEffectActions() { + return effectActions.values(); + } + + public boolean hasEffectActions() { + return !effectActions.isEmpty(); + } + + @Override + public boolean hasType(String typeId) { + return abilityTypes.containsKey(typeId); + } + + public boolean isAllowedByPlayer(String typeId) { + if (abilityTypes.containsKey(typeId)) { + Ability temp = abilityTypes.get(typeId).get(); + return temp.getAllowedBy().allowsPlayer(); + } + + return false; + } + + public boolean isAllowedByNPC(String typeId) { + if (abilityTypes.containsKey(typeId)) { + Ability temp = abilityTypes.get(typeId).get(); + return temp.getAllowedBy().allowsNpc(); + } + + return false; + } + + public boolean isAllowedByBoth(String typeId) { + if (abilityTypes.containsKey(typeId)) { + Ability temp = abilityTypes.get(typeId).get(); + return temp.getAllowedBy() == UserType.BOTH; + } + + return false; + } + + @Override + public String[] getAbilityNameArray() { + return builtAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasAbilityName(String name) { + return builtAbilities.containsKey(name); + } + + @Override + public String[] getCustomAbilityNameArray() { + return customAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasCustomAbilityName(String name) { + if (customAbilities.containsKey(name)) return true; + String sanitized = FileNameHelper.sanitizeTextInput(name); + return !sanitized.equals(name) && customAbilities.containsKey(sanitized); + } + + @Override + public boolean deleteCustomAbilityByName(String name) { + return deleteCustomAbility(name); + } + +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/AttributeController.java b/core/src/main/java/kamkeel/npcs/controllers/AttributeController.java new file mode 100644 index 000000000..1e3fc8fc7 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/AttributeController.java @@ -0,0 +1,104 @@ +package kamkeel.npcs.controllers; + + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import kamkeel.npcs.controllers.data.attribute.AttributeDefinition; +import kamkeel.npcs.controllers.data.attribute.AttributeValueType; +import kamkeel.npcs.controllers.data.attribute.tracker.PlayerAttributeTracker; +import kamkeel.npcs.CustomAttributes; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IAttributeDefinition; +import noppes.npcs.api.handler.data.IPlayerAttributes; +import noppes.npcs.api.handler.IAttributeHandler; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +/** + * AttributeController is the central registry for attribute definitions. + * It ensures that keys are unique. + */ +public class AttributeController implements IAttributeHandler { + + public static AttributeController Instance; + public CustomAttributes attributes; + + // Tracking Player Attributes + private static final Map trackers = new HashMap<>(); + private static final Map definitions = new HashMap<>(); + + public AttributeController() { + Instance = this; + definitions.clear(); + trackers.clear(); + attributes = new CustomAttributes(); + } + + public static AttributeDefinition registerAttribute(String key, String displayName, char colorCode, AttributeValueType valueType, AttributeDefinition.AttributeSection section) { + if (definitions.containsKey(key)) { + throw new IllegalArgumentException("Attribute already registered with key: " + key); + } + AttributeDefinition def = new AttributeDefinition(key, displayName, colorCode, valueType, section); + definitions.put(key, def); + return def; + } + + public static AttributeDefinition registerAttribute(AttributeDefinition definition) { + if (definitions.containsKey(definition.getKey())) { + throw new IllegalArgumentException("Attribute already registered with key: " + definition.getKey()); + } else { + definitions.put(definition.getKey(), definition); + return definition; + } + } + + public static AttributeDefinition getAttribute(String key) { + return definitions.get(key); + } + + public static Collection getAllAttributes() { + return definitions.values(); + } + + public static PlayerAttributeTracker getTracker(IPlayer player) { + return trackers.computeIfAbsent(player.getUniqueID(), id -> new PlayerAttributeTracker(id)); + } + + public static void removeTracker(UUID playerId) { + trackers.remove(playerId); + } + + /** + * Update all trackers (e.g. called every 10 ticks for all online players). + */ + public static void updateAllTrackers(Iterable players) { + for (IPlayer player : players) { + PlayerAttributeTracker tracker = getTracker(player); + tracker.updateIfChanged(player); + } + } + + public IPlayerAttributes getPlayerAttributes(IPlayer player) { + if (player == null || player.getMCEntity() == null) + return null; + + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + return trackers.computeIfAbsent(IPlayer.getUniqueID(), id -> new PlayerAttributeTracker(id)); + } + + public IAttributeDefinition getAttributeDefinition(String key) { + return definitions.get(key); + } + + public IAttributeDefinition[] getAllAttributesArray() { + return definitions.values().toArray(new AttributeDefinition[definitions.size()]); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/EnergyController.java b/core/src/main/java/kamkeel/npcs/controllers/EnergyController.java new file mode 100644 index 000000000..22d910c63 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/EnergyController.java @@ -0,0 +1,230 @@ +package kamkeel.npcs.controllers; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.entity.EntityAbilityZone; +import kamkeel.npcs.entity.EntityEnergySweeper; +import kamkeel.npcs.entity.EntityEnergyExplosion; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.energy.IEnergyExtender; +import noppes.npcs.api.IEnergyHandler; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.*; +import noppes.npcs.api.entity.IEnergyExplosion; +import java.util.ArrayList; +import java.util.List; + +/** + * Central controller for script-created energy entities and handler registration. + * Provides factory methods for creating all energy IEntity types and routes + * damage through registered handlers when entities have customDamageData + * but no sourceAbility. + */ +public class EnergyController implements IEnergyHandler { + + public static EnergyController Instance = new EnergyController(); + + private final List extenders = new ArrayList<>(); + + // ═══════════════════════════════════════════════════════════════════ + // HANDLER REGISTRATION + // ═══════════════════════════════════════════════════════════════════ + + public void registerExtender(IEnergyExtender handler) { + extenders.add(handler); + } + + public List getExtenders() { + return extenders; + } + + // ═══════════════════════════════════════════════════════════════════ + // DAMAGE ROUTING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Fire onEnergyDamage on all handlers. Chain of responsibility — first true wins. + */ + public boolean fireOnEnergyDamage(IEntity energyEntity, IEntityLivingBase owner, + IEntityLivingBase target, float damage, + float knockback, float knockbackUp, + double kbDirX, double kbDirZ, + float damageMultiplier, + INbt damageData) { + for (IEnergyExtender handler : extenders) { + if (handler.onEnergyDamage(energyEntity, owner, target, damage, + knockback, knockbackUp, kbDirX, kbDirZ, damageMultiplier, damageData)) { + return true; + } + } + return false; + } + + /** + * Fire modifyEnergyDamage on all handlers. Cumulative — each handler's output feeds the next. + */ + public float fireModifyEnergyDamage(IEntity energyEntity, IEntityLivingBase owner, + float baseDamage, INbt damageData) { + float damage = baseDamage; + for (IEnergyExtender handler : extenders) { + damage = handler.modifyEnergyDamage(energyEntity, owner, damage, damageData); + } + return damage; + } + + // ═══════════════════════════════════════════════════════════════════ + // INTERNAL FACTORY METHODS (MC types) + // ═══════════════════════════════════════════════════════════════════ + + public EntityAbilityOrb createOrbInternal(IWorld IWorld, IEntity owner, double x, double y, double z, float size) { + EntityAbilityOrb IEntity = new EntityAbilityOrb(IWorld); + IEntity.setPosition(x, y, z); + IEntity.setStartPosition(x, y, z); + IEntity.setOwnerEntityId(owner.getEntityId()); + IEntity.setProjectileSize(size); + return IEntity; + } + + public EntityAbilityBeam createBeamInternal(IWorld IWorld, IEntity owner, double x, double y, double z, + float beamWidth, float headSize) { + EntityAbilityBeam IEntity = new EntityAbilityBeam(IWorld); + IEntity.setPosition(x, y, z); + IEntity.setStartPosition(x, y, z); + IEntity.setOwnerEntityId(owner.getEntityId()); + IEntity.setBeamWidth(beamWidth); + IEntity.setHeadSize(headSize); + return IEntity; + } + + public EntityAbilityDisc createDiscInternal(IWorld IWorld, IEntity owner, double x, double y, double z, + float radius, float thickness) { + EntityAbilityDisc IEntity = new EntityAbilityDisc(IWorld); + IEntity.setPosition(x, y, z); + IEntity.setStartPosition(x, y, z); + IEntity.setOwnerEntityId(owner.getEntityId()); + IEntity.setDiscRadius(radius); + IEntity.setDiscThickness(thickness); + return IEntity; + } + + public EntityAbilityLaser createLaserInternal(IWorld IWorld, IEntity owner, double x, double y, double z, + float laserWidth) { + EntityAbilityLaser IEntity = new EntityAbilityLaser(IWorld); + IEntity.setPosition(x, y, z); + IEntity.setStartPosition(x, y, z); + IEntity.setOwnerEntityId(owner.getEntityId()); + IEntity.setLaserWidth(laserWidth); + return IEntity; + } + + public EntityAbilityZone createHazardInternal(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityAbilityZone IEntity = new EntityAbilityZone(IWorld); + IEntity.initAsHazard(owner, x, y, z); + return IEntity; + } + + public EntityAbilityZone createTrapInternal(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityAbilityZone IEntity = new EntityAbilityZone(IWorld); + IEntity.initAsTrap(owner, x, y, z); + return IEntity; + } + + public EntityEnergySweeper createSweeperInternal(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityEnergySweeper IEntity = new EntityEnergySweeper(IWorld); + IEntity.setPosition(x, y, z); + IEntity.setOwnerEntityId(owner.getEntityId()); + return IEntity; + } + + public EntityEnergyPanel createPanelInternal(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityEnergyPanel IEntity = new EntityEnergyPanel(IWorld); + IEntity.setPosition(x, y, z); + IEntity.setOwnerEntityId(owner.getEntityId()); + return IEntity; + } + + public EntityEnergyExplosion createExplosionInternal(IWorld IWorld, IEntity owner, double x, double y, double z, float radius) { + return new EntityEnergyExplosion(IWorld, owner, x, y, z, radius); + } + + // ═══════════════════════════════════════════════════════════════════ + // IEnergyHandler (Script API methods) + // ═══════════════════════════════════════════════════════════════════ + + @Override + public IEnergyOrb createOrb(IWorld IWorld, IEntity owner, double x, double y, double z, float size) { + EntityAbilityOrb IEntity = createOrbInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, size); + return (IEnergyOrb) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergyBeam createBeam(IWorld IWorld, IEntity owner, double x, double y, double z, float beamWidth, float headSize) { + EntityAbilityBeam IEntity = createBeamInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, beamWidth, headSize); + return (IEnergyBeam) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergyDisc createDisc(IWorld IWorld, IEntity owner, double x, double y, double z, float radius, float thickness) { + EntityAbilityDisc IEntity = createDiscInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, radius, thickness); + return (IEnergyDisc) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergyLaser createLaser(IWorld IWorld, IEntity owner, double x, double y, double z, float laserWidth) { + EntityAbilityLaser IEntity = createLaserInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, laserWidth); + return (IEnergyLaser) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergyZone createHazard(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityAbilityZone IEntity = createHazardInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergyZone) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergyZone createTrap(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityAbilityZone IEntity = createTrapInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergyZone) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergySweeper createSweeper(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityEnergySweeper IEntity = createSweeperInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergySweeper) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergyPanel createPanel(IWorld IWorld, IEntity owner, double x, double y, double z) { + EntityEnergyPanel IEntity = createPanelInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergyPanel) NpcAPI.Instance().getIEntity(IEntity); + } + + @Override + public IEnergyExplosion createExplosion(IWorld IWorld, IEntity owner, double x, double y, double z, float radius) { + EntityEnergyExplosion IEntity = createExplosionInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, radius); + return new ScriptEnergyExplosion<>(IEntity); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/ProfileController.java b/core/src/main/java/kamkeel/npcs/controllers/ProfileController.java new file mode 100644 index 000000000..30a0800e4 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/ProfileController.java @@ -0,0 +1,854 @@ +package kamkeel.npcs.controllers; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import kamkeel.npcs.controllers.data.profile.CNPCData; +import kamkeel.npcs.controllers.data.profile.EnumProfileOperation; +import kamkeel.npcs.controllers.data.profile.IProfileData; +import kamkeel.npcs.controllers.data.profile.Profile; +import kamkeel.npcs.controllers.data.profile.ProfileInfoEntry; +import kamkeel.npcs.controllers.data.profile.ProfileOperation; +import kamkeel.npcs.controllers.data.profile.Slot; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IProfile; +import noppes.npcs.api.handler.data.ISlot; +import noppes.npcs.api.handler.IPlayerData; +import noppes.npcs.api.handler.IProfileHandler; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerDataScript; +import noppes.npcs.controllers.data.PlayerQuestData; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.CustomNpcs; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.EventHooks; +import noppes.npcs.LogWriter; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.util.CustomNPCsThreader; +import noppes.npcs.util.NBTJsonUtil; + +public class ProfileController implements IProfileHandler { + + // Message constants + private static final String MSG_PLAYER_NOT_FOUND = "Player not found."; + private static final String MSG_PROFILE_LOCKED_CLONE = "Profile is locked; cannot clone slot."; + private static final String MSG_SOURCE_SLOT_NOT_EXIST = "Source slot does not exist."; + private static final String MSG_CANNOT_CLONE_CURRENT = "Cannot clone to the current active slot."; + private static final String MSG_INVALID_DEST_SLOT = "Invalid destination slot id."; + private static final String MSG_CLONE_SUCCESS = "Slot cloned successfully."; + private static final String MSG_PROFILE_LOCKED_REMOVE = "Profile is locked; cannot remove slot."; + private static final String MSG_CANNOT_REMOVE_ACTIVE = "Cannot remove the currently active slot."; + private static final String MSG_SLOT_NOT_EXIST = "Slot does not exist."; + private static final String MSG_REMOVE_SUCCESS = "Slot removed successfully."; + private static final String MSG_PROFILE_LOCKED_CREATE = "Profile is locked; cannot create slot."; + private static final String MSG_MAX_SLOTS_REACHED = "Maximum allowed slots reached."; + private static final String MSG_NEW_SLOT_CREATED = "New slot created successfully."; + private static final String MSG_PROFILE_LOCKED_CHANGE = "Profile is locked; cannot change slot."; + private static final String MSG_SLOT_ALREADY_ACTIVE = "Slot is already active."; + private static final String MSG_REGION_NOT_ALLOWED = "Profile switching not allowed from your current location."; + private static final String MSG_CHANGE_SUCCESS = "Slot changed successfully."; + private static final String MSG_CANCELLED = "Operation cancelled."; + + public static Map profileTypes = new HashMap<>(); + public static Map activeProfiles = new HashMap<>(); + public static String profile_directory = "profiles"; + + public static ProfileController Instance; + + public ProfileController() { + Instance = this; + profileTypes = new HashMap<>(); + activeProfiles = new HashMap<>(); + } + + public static boolean registerProfileType(IProfileData type) { + if (profileTypes.containsKey(type.getTagName())) + return false; + profileTypes.put(type.getTagName(), type); + return true; + } + + public static File getProfileDir() { + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), profile_directory); + if (!file.exists()) + file.mkdir(); + return file; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static File getBackupDir() { + File base = getProfileDir(); + File backup = new File(base, "backup"); + if (!backup.exists()) { + backup.mkdir(); + } + return backup; + } + + public synchronized void login(IPlayer player) { + if (!ConfigMain.ProfilesEnabled) + return; + + if (player == null) + return; + Profile profile; + if (activeProfiles.containsKey(player.getUniqueID())) { + profile = activeProfiles.get(player.getUniqueID()); + profile.player = player; + } else { + INbt compound = load(player); + profile = new Profile(player, compound); + if (profile.getSlots().isEmpty()) { + Slot defaultSlot = new Slot(0, "Default Slot"); + defaultSlot.setLastLoaded(System.currentTimeMillis()); + profile.getSlots().put(0, defaultSlot); + profile.currentSlotId = 0; + saveSlotData(player); + } + if (!profile.getSlots().containsKey(profile.currentSlotId)) { + profile.currentSlotId = 0; + } + + profile.player = player; + activeProfiles.put(player.getUniqueID(), profile); + + PlayerData pdata = PlayerData.get(player); + if (profile.getSlots().containsKey(pdata.profileSlot)) { + profile.currentSlotId = pdata.profileSlot; + } + + saveSlotData(player); + verifySlotQuests(profile.player); + } + } + + public synchronized INbt load(IPlayer player) { + File saveDir = getProfileDir(); + String filename = player.getUniqueID().toString() + ".dat"; + try { + File file = new File(saveDir, filename); + if (file.exists()) { + return NBTJsonUtil.loadNBTData(file); + } + } catch (Exception e) { + LogWriter.error("Error loading profile file: " + filename, e); + } + return new INbt(); + } + + public synchronized INbt load(UUID uuid) { + File saveDir = getProfileDir(); + String filename = uuid.toString() + ".dat"; + try { + File file = new File(saveDir, filename); + if (file.exists()) { + return NBTJsonUtil.loadNBTData(file); + } + } catch (Exception e) { + LogWriter.error("Error loading profile file: " + filename, e); + } + return new INbt(); + } + + public synchronized void saveOffline(Profile profile, UUID uuid) { + profile.setLocked(true); + final INbt compound = profile.writeToNBT(); + final String filename = uuid.toString() + ".dat"; + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + File saveDir = getProfileDir(); + File fileNew = new File(saveDir, filename + "_new"); + File fileOld = new File(saveDir, filename); + NBTIO.writeCompressed(compound, new FileOutputStream(fileNew)); + if (fileOld.exists()) { + fileOld.delete(); + } + fileNew.renameTo(fileOld); + backupProfile(uuid, compound); + } catch (Exception e) { + LogWriter.except(e); + } finally { + profile.setLocked(false); + } + }); + } + + public synchronized void save(IPlayer player, Profile profile) { + profile.setLocked(true); + final INbt compound = profile.writeToNBT(); + final String filename = player.getUniqueID() + ".dat"; + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + File saveDir = getProfileDir(); + File fileNew = new File(saveDir, filename + "_new"); + File fileOld = new File(saveDir, filename); + NBTIO.writeCompressed(compound, new FileOutputStream(fileNew)); + if (fileOld.exists()) { + fileOld.delete(); + } + fileNew.renameTo(fileOld); + if (ConfigMain.AllowProfileBackups) { + backupProfile(player.getUniqueID(), compound); + } + } catch (Exception e) { + LogWriter.except(e); + } finally { + profile.setLocked(false); + } + }); + } + + private void backupProfile(UUID uuid, INbt compound) { + try { + File backupDir = new File(getBackupDir(), uuid.toString()); + if (!backupDir.exists()) { + backupDir.mkdirs(); + } + String dateStr = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + File backupFile = new File(backupDir, dateStr + ".dat"); + NBTIO.writeCompressed(compound, new FileOutputStream(backupFile)); + File[] backups = backupDir.listFiles((dir, name) -> name.endsWith(".dat")); + if (backups != null && backups.length > ConfigMain.ProfileBackupAmount) { + Arrays.sort(backups, Comparator.comparingLong(File::lastModified)); + for (int i = 0; i < backups.length - ConfigMain.ProfileBackupAmount; i++) { + backups[i].delete(); + } + } + } catch (Exception e) { + LogWriter.except(e); + } + } + + public boolean rollbackProfile(String username, File backupFile) { + UUID uuid = getUUIDFromUsername(username); + if (uuid == null) + return false; + try (FileInputStream fis = new FileInputStream(backupFile)) { + INbt compound = NBTIO.readCompressed(fis); + File saveDir = getProfileDir(); + File mainFile = new File(saveDir, uuid.toString() + ".dat"); + File fileNew = new File(saveDir, uuid.toString() + "_new"); + NBTIO.writeCompressed(compound, new FileOutputStream(fileNew)); + if (mainFile.exists()) { + mainFile.delete(); + } + fileNew.renameTo(mainFile); + IPlayer player = NoppesUtilServer.getPlayerByName(username); + Profile newProfile; + if (player != null) { + newProfile = new Profile(player, compound); + activeProfiles.put(uuid, newProfile); + loadSlotData(player); + return true; + } + } catch (Exception e) { + LogWriter.except(e); + } + return false; + } + + public synchronized void logout(IPlayer player) { + if (player != null && activeProfiles.containsKey(player.getUniqueID())) { + saveSlotData(player); + Profile profile = activeProfiles.get(player.getUniqueID()); + save(player, profile); + } + } + + public Profile getProfile(IPlayer player) { + if (player == null) + return null; + if (!activeProfiles.containsKey(player.getUniqueID())) + login(player); + return activeProfiles.get(player.getUniqueID()); + } + + public Profile getProfile(UUID uuid) { + if (activeProfiles.containsKey(uuid)) + return activeProfiles.get(uuid); + INbt compound = load(uuid); + return new Profile(null, compound); + } + + public Profile getProfile(String username) { + IPlayer player = NoppesUtilServer.getPlayerByName(username); + if (player != null) { + return getProfile(player); + } else { + UUID uuid = getUUIDFromUsername(username); + if (uuid == null) + return null; + return getProfile(uuid); + } + } + + public UUID getUUIDFromUsername(String username) { + try { + URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + username); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + con.setConnectTimeout(5000); + con.setReadTimeout(5000); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + StringBuilder temp = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) { + temp.append(line); + } + in.close(); + con.disconnect(); + String response = temp.toString(); + if (response.contains("\"id\"")) { + int idIndex = response.indexOf("\"id\""); + int colonIndex = response.indexOf(":", idIndex); + int quoteStart = response.indexOf("\"", colonIndex); + int quoteEnd = response.indexOf("\"", quoteStart + 1); + String idString = response.substring(quoteStart + 1, quoteEnd); + idString = idString.replaceFirst( + "(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", + "$1-$2-$3-$4-$5" + ); + return UUID.fromString(idString); + } + } catch (IOException e) { + LogWriter.error("Error retrieving UUID for " + username, e); + } + return null; + } + + private int getNextAvailableTempSlot(Profile profile) { + int id = -1; + while (profile.getSlots().containsKey(id)) { + id--; + } + return id; + } + + private ProfileOperation cloneSlotInternal(Profile profile, int sourceSlotId, int destinationSlotId, boolean temporary) { + if (profile.isLocked()) { + return ProfileOperation.locked(MSG_PROFILE_LOCKED_CLONE); + } + if (profile.player != null) { + List dataList = new ArrayList<>(profileTypes.values()); + dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority)); + for (IProfileData pd : dataList) { + if (pd.verifySwitch(profile.player).getResult() != EnumProfileOperation.SUCCESS) { + return pd.verifySwitch(profile.player); + } + } + } + if (!profile.getSlots().containsKey(sourceSlotId)) { + return ProfileOperation.error(MSG_SOURCE_SLOT_NOT_EXIST); + } + if (destinationSlotId == profile.getCurrentSlotId()) { + return ProfileOperation.error(MSG_CANNOT_CLONE_CURRENT); + } + if (temporary) { + destinationSlotId = getNextAvailableTempSlot(profile); + } else { + if (destinationSlotId <= 0) { + return ProfileOperation.error(MSG_INVALID_DEST_SLOT); + } + } + ISlot sourceSlot = profile.getSlots().get(sourceSlotId); + Map newComponents = new HashMap<>(); + for (String key : sourceSlot.getComponents().keySet()) { + newComponents.put(key, (INbt) sourceSlot.getComponentData(key).copy()); + } + Slot clonedSlot = new Slot(destinationSlotId, "Cloned Slot " + destinationSlotId, System.currentTimeMillis(), temporary, newComponents); + profile.getSlots().put(destinationSlotId, clonedSlot); + return ProfileOperation.success(MSG_CLONE_SUCCESS); + } + + private ProfileOperation removeSlotInternal(Profile profile, int slotId) { + if (profile.isLocked()) { + return ProfileOperation.locked(MSG_PROFILE_LOCKED_REMOVE); + } + if (slotId == profile.getCurrentSlotId()) { + return ProfileOperation.error(MSG_CANNOT_REMOVE_ACTIVE); + } + if (!profile.getSlots().containsKey(slotId)) { + return ProfileOperation.error(MSG_SLOT_NOT_EXIST); + } + + if (profile.player != null) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player); + IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player); + if (EventHooks.onProfileRemove(handler, scriptPlayer, profile, slotId, false)) + return ProfileOperation.error(MSG_CANCELLED); + } + + profile.getSlots().remove(slotId); + + if (profile.player != null) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player); + IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player); + EventHooks.onProfileRemove(handler, scriptPlayer, profile, slotId, true); + } + + return ProfileOperation.success(MSG_REMOVE_SUCCESS); + } + + public ProfileOperation createSlotInternal(Profile profile) { + if (profile.isLocked()) { + return ProfileOperation.locked(MSG_PROFILE_LOCKED_CREATE); + } + int newSlotId = 0; + while (profile.getSlots().containsKey(newSlotId)) { + newSlotId++; + } + if (!allowSlotPermission(profile.player)) { + return ProfileOperation.error(MSG_MAX_SLOTS_REACHED); + } + if (profile.player != null) { + List dataList = new ArrayList<>(profileTypes.values()); + dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority)); + for (IProfileData pd : dataList) { + if (pd.verifySwitch(profile.player).getResult() != EnumProfileOperation.SUCCESS) { + return pd.verifySwitch(profile.player); + } + } + } + + if (profile.player != null) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player); + IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player); + if (EventHooks.onProfileCreate(handler, scriptPlayer, profile, newSlotId, false)) + return ProfileOperation.error(MSG_CANCELLED); + } + + Slot newSlot = new Slot(newSlotId, "Slot " + newSlotId); + newSlot.setLastLoaded(System.currentTimeMillis()); + profile.getSlots().put(newSlotId, newSlot); + if (profile.player != null) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player); + verifySlotQuests(profile.player); + save(profile.player, profile); + if (profile.player instanceof IPlayer) { + ProfileSharedQuestPacket.sendToPlayer((IPlayer) profile.player); + } + + IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player); + EventHooks.onProfileCreate(handler, scriptPlayer, profile, newSlotId, true); + } + return ProfileOperation.success(MSG_NEW_SLOT_CREATED); + } + + private ProfileOperation changeSlotInternal(Profile profile, int newSlotId) { + if (profile.isLocked()) { + return ProfileOperation.locked(MSG_PROFILE_LOCKED_CHANGE); + } + if (profile.getCurrentSlotId() == newSlotId) { + return ProfileOperation.error(MSG_SLOT_ALREADY_ACTIVE); + } + if (!profile.getSlots().containsKey(newSlotId)) { + return ProfileOperation.error(MSG_SLOT_NOT_EXIST); + } + if (profile.player != null) { + if (ConfigMain.RegionProfileSwitching) { + boolean allowed = hasCustomPermission(profile.player, PROFILE_REGION_BYPASS.name); + if (!allowed) { + int playerDim = profile.player.dimension; + int playerX = (int) profile.player.posX; + int playerY = (int) profile.player.posY; + int playerZ = (int) profile.player.posZ; + for (List region : ConfigMain.RestrictedProfileRegions) { + if (region.size() == 7) { + int dim = region.get(0); + int x1 = Math.min(region.get(1), region.get(4)); + int y1 = Math.min(region.get(2), region.get(5)); + int z1 = Math.min(region.get(3), region.get(6)); + int x2 = Math.max(region.get(1), region.get(4)); + int y2 = Math.max(region.get(2), region.get(5)); + int z2 = Math.max(region.get(3), region.get(6)); + if (playerDim == dim && + playerX >= x1 && playerX <= x2 && + playerY >= y1 && playerY <= y2 && + playerZ >= z1 && playerZ <= z2) { + allowed = true; + break; + } + } + } + } + if (!allowed) { + return ProfileOperation.error(MSG_REGION_NOT_ALLOWED); + } + } + + List dataList = new ArrayList<>(profileTypes.values()); + dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority)); + for (IProfileData pd : dataList) { + if (pd.verifySwitch(profile.player).getResult() != EnumProfileOperation.SUCCESS) { + return pd.verifySwitch(profile.player); + } + } + + int prevSlot = profile.getCurrentSlotId(); + + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player); + IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player); + if (EventHooks.onProfileChange(handler, scriptPlayer, profile, newSlotId, prevSlot, false)) + return ProfileOperation.error(MSG_CANCELLED); + + // Persist the outgoing slot using its original id. Previously we relied on + // profile.currentSlotId inside saveSlotData, but scripts firing during the + // transition could change the active slot before the save completed and + // overwrite the destination slot with stale data. + saveSlotData(profile.player, profile, prevSlot); + loadSlotData(profile.player, profile, newSlotId); + syncSharedQuestsToPlayer(profile.player); + if (profile.player instanceof IPlayer) { + ProfileSharedQuestPacket.sendToPlayer((IPlayer) profile.player); + } + + EventHooks.onProfileChange(handler, scriptPlayer, profile, newSlotId, prevSlot, true); + } else { + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + } + return ProfileOperation.success(MSG_CHANGE_SUCCESS); + } + + public ProfileOperation cloneSlot(IPlayer player, int sourceSlotId, int destinationSlotId, boolean temporary) { + Profile profile = getProfile(player); + if (profile == null) + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + ProfileOperation result = cloneSlotInternal(profile, sourceSlotId, destinationSlotId, temporary); + if (result.getResult() == EnumProfileOperation.SUCCESS && player != null) { + save(player, profile); + } + return result; + } + + public ProfileOperation cloneSlot(UUID uuid, int sourceSlotId, int destinationSlotId, boolean temporary) { + Profile profile = getProfile(uuid); + if (profile == null) + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + ProfileOperation result = cloneSlotInternal(profile, sourceSlotId, destinationSlotId, temporary); + if (result.getResult() == EnumProfileOperation.SUCCESS) { + if (profile.player != null) + save(profile.player, profile); + else + saveOffline(profile, uuid); + } + return result; + } + + public ProfileOperation cloneSlot(String username, int sourceSlotId, int destinationSlotId, boolean temporary) { + Profile profile = getProfile(username); + if (profile == null) + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + ProfileOperation result = cloneSlotInternal(profile, sourceSlotId, destinationSlotId, temporary); + if (result.getResult() == EnumProfileOperation.SUCCESS) { + if (profile.player != null) + save(profile.player, profile); + else { + UUID uuid = getUUIDFromUsername(username); + if (uuid != null) + saveOffline(profile, uuid); + } + } + return result; + } + + public ProfileOperation removeSlot(IPlayer player, int slotId) { + Profile profile = getProfile(player); + if (profile == null) + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + ProfileOperation result = removeSlotInternal(profile, slotId); + if (result.getResult() == EnumProfileOperation.SUCCESS && player != null) { + save(player, profile); + } + return result; + } + + public ProfileOperation removeSlot(String username, int slotId) { + Profile profile = getProfile(username); + if (profile == null) + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + ProfileOperation result = removeSlotInternal(profile, slotId); + if (result.getResult() == EnumProfileOperation.SUCCESS) { + if (profile.player != null) + save(profile.player, profile); + else { + UUID uuid = getUUIDFromUsername(username); + if (uuid != null) + saveOffline(profile, uuid); + } + } + return result; + } + + public ProfileOperation changeSlot(IPlayer player, int newSlotId) { + Profile profile = getProfile(player); + if (profile == null) + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + ProfileOperation result = changeSlotInternal(profile, newSlotId); + if (result.getResult() == EnumProfileOperation.SUCCESS && player != null) + save(player, profile); + return result; + } + + public ProfileOperation changeSlot(String username, int newSlotId) { + Profile profile = getProfile(username); + if (profile == null) + return ProfileOperation.error(MSG_PLAYER_NOT_FOUND); + ProfileOperation result = changeSlotInternal(profile, newSlotId); + if (result.getResult() == EnumProfileOperation.SUCCESS) { + if (profile.player != null) + save(profile.player, profile); + else { + UUID uuid = getUUIDFromUsername(username); + if (uuid != null) + saveOffline(profile, uuid); + } + } + return result; + } + + private void saveSlotData(IPlayer player, Profile profile, int slotId) { + if (player == null || profile == null) + return; + if (profile.isLocked()) { + return; + } + ISlot slot = profile.getSlots().get(slotId); + if (slot == null) { + slot = new Slot(slotId, "Slot " + slotId); + profile.getSlots().put(slotId, slot); + } + for (IProfileData profileData : profileTypes.values()) { + INbt cloned = (INbt) profileData.getCurrentNBT(player).copy(); + slot.setComponentData(profileData.getTagName(), cloned); + } + slot.setLastLoaded(System.currentTimeMillis()); + } + + public void saveSlotData(IPlayer player) { + if (player == null || !activeProfiles.containsKey(player.getUniqueID())) + return; + Profile profile = activeProfiles.get(player.getUniqueID()); + saveSlotData(player, profile, profile.getCurrentSlotId()); + } + + private void loadSlotData(IPlayer player, Profile profile, int slotId) { + if (player == null || profile == null) + return; + + ISlot slot = profile.getSlots().get(slotId); + if (slot == null) + return; + + for (IProfileData profileData : profileTypes.values()) { + INbt data; + if (slot.getComponents().containsKey(profileData.getTagName())) + data = (INbt) slot.getComponentData(profileData.getTagName()).copy(); + else + data = new INbt(); + profileData.setNBT(player, data); + } + for (IProfileData profileData : profileTypes.values()) { + profileData.save(player); + } + + // Only flip the active slot after the player's live data matches the + // requested slot. This way, any additional saveSlotData() calls triggered by + // scripts during the load can't persist stale information to the new slot. + profile.currentSlotId = slotId; + + PlayerData pdata = PlayerData.get(player); + pdata.profileSlot = slotId; + pdata.save(); + + if (ConfigMain.AttributesEnabled) + AttributeController.getTracker(player).recalcAttributes(player); + } + + public void loadSlotData(IPlayer player) { + if (player == null || !activeProfiles.containsKey(player.getUniqueID())) + return; + Profile profile = activeProfiles.get(player.getUniqueID()); + // Delay flipping profile.currentSlotId until after the data for the target slot + // has been restored to the player so other saves observe consistent state. + loadSlotData(player, profile, profile.getCurrentSlotId()); + } + + public List getProfileInfo(IPlayer player, int slotId) { + List infoList = new ArrayList<>(); + Profile profile = getProfile(player); + if (profile == null) + return infoList; + + List dataList = new ArrayList<>(profileTypes.values()); + dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority)); + if (slotId == profile.getCurrentSlotId()) { + for (IProfileData pd : dataList) { + INbt currentNBT = pd.getCurrentNBT(player); + List subInfo = pd.getInfo(player, currentNBT); + infoList.addAll(subInfo); + } + } else { + if (!profile.getSlots().containsKey(slotId)) + return infoList; + ISlot slot = profile.getSlots().get(slotId); + for (IProfileData pd : dataList) { + if (slot.getComponents().containsKey(pd.getTagName())) { + INbt sub = (INbt) slot.getComponentData(pd.getTagName()).copy(); + List subInfo = pd.getInfo(player, sub); + infoList.addAll(subInfo); + } + } + } + return infoList; + } + + public boolean allowSlotPermission(IPlayer player) { + Profile profile = getProfile(player); + int currentSlots = profile.getSlots().size(); + if (CustomNpcsPermissions.hasCustomPermission(player, PROFILE_MAX.name)) { + return true; + } + int highestAllowed = 0; + for (int i = 1; i <= 50; i++) { + String perm = "customnpcs.profile.max." + i; + if (CustomNpcsPermissions.hasCustomPermission(player, perm)) { + highestAllowed = i; + } + } + if (highestAllowed == 0 || highestAllowed < ConfigMain.DefaultProfileSlots) { + highestAllowed = ConfigMain.DefaultProfileSlots; + } + return currentSlots < highestAllowed; + } + + + public IPlayerData getSlotPlayerData(IPlayer player, int slotID) { + PlayerData playerData; + Profile profile = getProfile(player); + if (profile == null) + return null; + + if (profile.currentSlotId == slotID) { + playerData = PlayerData.get(player); + } else { + Slot slot = (Slot) profile.getSlots().get(slotID); + playerData = getSlotPlayerData(player, slot); + } + return playerData; + } + + public PlayerData getSlotPlayerData(IPlayer player, Slot slot) { + if (slot == null) + return null; + + PlayerData playerData = new PlayerData(); + playerData.player = player; + + INbt compound = slot.getComponentData(new CNPCData().getTagName()); + if (compound == null) + compound = new INbt(); + else + compound = (INbt) compound.copy(); + playerData.setNBT(compound); + return playerData; + } + + public void verifySlotQuests(IPlayer player) { + Profile profile = getProfile(player); + if (profile == null) { + return; + } + + Map universalFinished = new HashMap<>(); + for (Integer questId : QuestController.Instance.sharedQuests.keySet()) { + long maxTime = 0; + for (ISlot slot : profile.getSlots().values()) { + IPlayerData data = getSlotPlayerData(player, slot.getId()); + if (data == null) { + continue; + } + PlayerQuestData questData = (PlayerQuestData) data.getQuestData(); + Long t = questData.finishedQuests.get(questId); + if (t != null && t > maxTime) { + maxTime = t; + } + } + if (maxTime > 0) { + universalFinished.put(questId, maxTime); + } + } + + // Update the profile cache + profile.sharedQuestTimestamps = universalFinished; + + // Push the universal shared completion times into every slot. + for (ISlot slot : profile.getSlots().values()) { + IPlayerData data = getSlotPlayerData(player, slot.getId()); + if (data != null) { + PlayerData playerData = (PlayerData) data; + PlayerQuestData questData = (PlayerQuestData) data.getQuestData(); + questData.finishedQuests.putAll(universalFinished); + slot.setComponentData(new CNPCData().getTagName(), playerData.getNBT()); + } + } + } + + public void syncSharedQuestsToPlayer(IPlayer player) { + Profile profile = getProfile(player); + if (profile == null || profile.sharedQuestTimestamps.isEmpty()) { + return; + } + PlayerQuestData questData = PlayerData.get(player).questData; + for (Map.Entry entry : profile.sharedQuestTimestamps.entrySet()) { + Long existing = questData.finishedQuests.get(entry.getKey()); + if (existing == null || entry.getValue() > existing) { + questData.finishedQuests.put(entry.getKey(), entry.getValue()); + } + } + } + + public void shareQuestCompletion(IPlayer player, int questId, long completeTime) { + Profile profile = getProfile(player); + if (profile == null) { + return; + } + + Long existing = profile.sharedQuestTimestamps.get(questId); + if (existing == null || completeTime > existing) { + profile.sharedQuestTimestamps.put(questId, completeTime); + } + save(player, profile); + if (player instanceof IPlayer) { + ProfileSharedQuestPacket.sendToPlayer((IPlayer) player); + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/SyncController.java b/core/src/main/java/kamkeel/npcs/controllers/SyncController.java new file mode 100644 index 000000000..dfccb158c --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -0,0 +1,1121 @@ +package kamkeel.npcs.controllers; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.DialogController; +import noppes.npcs.controllers.FactionController; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.RecipeController; +import noppes.npcs.controllers.data.CustomEffect; +import noppes.npcs.controllers.data.Dialog; +import noppes.npcs.controllers.data.DialogCategory; +import noppes.npcs.controllers.data.EffectKey; +import noppes.npcs.controllers.data.Faction; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicCycle; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerEffect; +import noppes.npcs.controllers.data.Quest; +import noppes.npcs.controllers.data.QuestCategory; +import noppes.npcs.controllers.data.RecipeAnvil; +import noppes.npcs.controllers.data.RecipeCarpentry; +import java.io.IOException; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public class SyncController { + + public static boolean DEBUG_SYNC_LOGGING = false; + + private static final EnumMap cacheEntries = new EnumMap<>(EnumSyncType.class); + private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); + + + private static void debug(String message, Object... args) { + if (DEBUG_SYNC_LOGGING) { + System.out.println("[SyncController] " + String.format(message, args)); + } + } + + private static final EnumSyncType[] LOGIN_SYNC_TYPES = new EnumSyncType[]{ + EnumSyncType.FACTION, + EnumSyncType.DIALOG_CATEGORY, + EnumSyncType.QUEST_CATEGORY, + EnumSyncType.WORKBENCH_RECIPES, + EnumSyncType.CARPENTRY_RECIPES, + EnumSyncType.ANVIL_RECIPES, + EnumSyncType.CUSTOM_EFFECTS, + EnumSyncType.MAGIC, + EnumSyncType.MAGIC_CYCLE, + EnumSyncType.CUSTOM_ABILITY, + EnumSyncType.CHAINED_ABILITY + }; + + public static void load() { + cacheEntries.clear(); + playerSyncState.clear(); + + registerCache(EnumSyncType.FACTION, SyncController::factionsNBT); + registerCache(EnumSyncType.DIALOG_CATEGORY, SyncController::dialogCategoriesNBT); + registerCache(EnumSyncType.QUEST_CATEGORY, SyncController::questCategoriesNBT); + registerCache(EnumSyncType.WORKBENCH_RECIPES, SyncController::workbenchNBT); + registerCache(EnumSyncType.CARPENTRY_RECIPES, SyncController::carpentryNBT); + registerCache(EnumSyncType.ANVIL_RECIPES, SyncController::anvilNBT); + registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); + registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT); + registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); + registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); + registerCache(EnumSyncType.CHAINED_ABILITY, SyncController::chainedAbilitiesNBT); + } + + public static void syncPlayer(IPlayer player) { + SyncController.syncPlayerInternal(player, true); + } + + private static void syncPlayerInternal(IPlayer player, boolean includePostPackets) { + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + + for (EnumSyncType type : LOGIN_SYNC_TYPES) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry == null) { + continue; + } + + int currentRevision = entry.getRevisionValue(); + int lastRevision = state.getRevision(type); + if (lastRevision == currentRevision) { + continue; + } + + CachedSyncPayload payload = entry.getPayload(type); + if (payload == null) { + continue; + } + + if (lastRevision != payload.getRevision()) { + debug("Sending %s data to %s", type, player.getCommandSenderName()); + PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); + state.updateRevision(type, payload.getRevision()); + } + } + + if (includePostPackets) { + sendPostLoginPackets(player); + } + } + + public static void beginLogin(IPlayer player) { + playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + PacketHandler.Instance.sendToPlayer( + new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), + player + ); + } + + public static void handleClientRevisionReport( + IPlayer player, + String serverKey, + String previousServerKey, + Map clientRevisions + ) { + String currentServerKey = getServerCacheKey(); + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + + if (!currentServerKey.equals(serverKey)) { + state.reset(); + syncPlayer(player); + return; + } + + if (!currentServerKey.isEmpty()) { + if (previousServerKey == null || !currentServerKey.equals(previousServerKey)) { + state.reset(); + syncPlayer(player); + return; + } + } + + if (clientRevisions == null || clientRevisions.isEmpty()) { + debug( + "Client %s reported no cached revisions; forcing full sync", + player.getCommandSenderName() + ); + state.reset(); + } else { + state.applyHandshake(clientRevisions); + } + + syncPlayer(player); + } + + private static void sendPostLoginPackets(IPlayer player) { + DBCAddon.instance.syncPlayer(player); + syncPlayerData(player, false); + PartyInfoPacket.sendPartyData(player); + ProfileSharedQuestPacket.sendToPlayer(player); + + // Sync skin overlays after full handshake to ensure the client is ready. + // Overlays are not part of getSyncNBTFull(), and packets sent during + // PlayerLoggedInEvent can arrive before the client IEntity exists. + PlayerData data = PlayerData.get(player); + if (data != null) { + data.skinOverlays.updateClient(); + } + } + + private static EnumMap getServerRevisionSnapshot() { + EnumMap snapshot = new EnumMap<>(EnumSyncType.class); + for (EnumSyncType type : LOGIN_SYNC_TYPES) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry != null) { + snapshot.put(type, entry.getRevisionValue()); + } + } + return snapshot; + } + + private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); + + private static String getServerCacheKey() { + MinecraftServer server = PlatformServiceHolder.get().getServer(); + if (server == null) { + return ""; + } + + if (!server.isDedicatedServer()) { + return ""; + } + + return SERVER_IDENTITY_KEY; + } + + public static int getCurrentRevision(EnumSyncType type) { + SyncCacheEntry entry = cacheEntries.get(type); + return entry == null ? -1 : entry.getRevisionValue(); + } + + public static INbt workbenchNBT() { + RecipeController controller = RecipeController.Instance; + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (RecipeCarpentry recipe : controller.globalRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + public static INbt carpentryNBT() { + RecipeController controller = RecipeController.Instance; + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (RecipeCarpentry recipe : controller.carpentryRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + public static INbt anvilNBT() { + RecipeController controller = RecipeController.Instance; + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (RecipeAnvil recipe : controller.anvilRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + public static INbt factionsNBT() { + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (Faction faction : FactionController.getInstance().factions.values()) { + INbt factioNBT = new INbt(); + faction.writeNBT(factioNBT); + list.appendTag(factioNBT); + } + compound.setTag("Factions", list); + return compound; + } + + public static INbt dialogCategoriesNBT() { + INbt compound = new INbt(); + INbtList categoryList = new INbtList(); + for (DialogCategory category : DialogController.Instance.categories.values()) { + INbt questCompound = new INbt(); + INbtList dialogList = new INbtList(); + for (int dialogID : category.dialogs.keySet()) { + Dialog quest = category.dialogs.get(dialogID); + dialogList.appendTag(quest.writeToNBT(new INbt())); + } + questCompound.setTag("Data", dialogList); + questCompound.setTag("CatNBT", category.writeSmallNBT(new INbt())); + categoryList.appendTag(questCompound); + } + compound.setTag("DialogCategories", categoryList); + return compound; + } + + public static INbt questCategoriesNBT() { + INbt compound = new INbt(); + INbtList categoryList = new INbtList(); + for (QuestCategory category : QuestController.Instance.categories.values()) { + INbt questCompound = new INbt(); + ; + INbtList questList = new INbtList(); + for (int questID : category.quests.keySet()) { + Quest quest = category.quests.get(questID); + questList.appendTag(quest.writeToNBT(new INbt())); + } + questCompound.setTag("Data", questList); + questCompound.setTag("CatNBT", category.writeSmallNBT(new INbt())); + categoryList.appendTag(questCompound); + } + compound.setTag("QuestCategories", categoryList); + return compound; + } + + public static INbt customEffectsNBT() { + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (CustomEffect effect : CustomEffectController.getInstance().getCustomEffects().values()) { + list.appendTag(effect.writeToNBT(false)); + } + compound.setTag("Data", list); + return compound; + } + + public static INbt magicsNBT() { + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (Magic magic : MagicController.getInstance().magics.values()) { + INbt magicCompound = new INbt(); + magic.writeNBT(magicCompound); + list.appendTag(magicCompound); + } + compound.setTag("Data", list); + return compound; + } + + public static INbt magicCyclesNBT() { + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { + INbt cycleCompound = new INbt(); + cycle.writeNBT(new NBTWrapper(cycleCompound)); + list.appendTag(cycleCompound); + } + compound.setTag("Data", list); + return compound; + } + + public static INbt customAbilitiesNBT() { + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { + INbt abilityNBT = entry.getValue().writeNBT(false); + abilityNBT.setString("CustomAbilityId", entry.getKey()); + list.appendTag(abilityNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllCustomAbilities() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_ABILITY, payload)); + updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision()); + } + + public static INbt chainedAbilitiesNBT() { + INbtList list = new INbtList(); + INbt compound = new INbt(); + for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { + INbt chainNBT = entry.getValue().writeNBT(false); + chainNBT.setString("ChainedAbilityId", entry.getKey()); + list.appendTag(chainNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllChainedAbilities() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CHAINED_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CHAINED_ABILITY, payload)); + updateAllPlayerRevisions(EnumSyncType.CHAINED_ABILITY, payload.getRevision()); + } + + public static void syncPlayerData(IPlayer player, boolean update) { + PlayerData data = PlayerData.get(player); + if (data != null) { + if (update) { + PacketHandler.Instance.sendToPlayer(new SyncPacket( + EnumSyncType.PLAYERDATA, + EnumSyncAction.UPDATE, + -1, + data.getSyncNBT()) + , player); + } else { + PacketHandler.Instance.sendToPlayer(new SyncPacket( + EnumSyncType.PLAYERDATA, + EnumSyncAction.RELOAD, + -1, + data.getSyncNBTFull()), player); + } + } + } + + public static void syncRemove(EnumSyncType enumSyncType, int id) { + Map revisions = invalidateCaches(enumSyncType); + int revision = revisions.getOrDefault(enumSyncType, getCurrentRevision(enumSyncType)); + PacketHandler.Instance.sendToAll(new SyncPacket( + enumSyncType, + EnumSyncAction.REMOVE, + id, + revision, + new INbt() + )); + updateAllPlayerRevisions(revisions); + } + + public static void syncAllDialogs() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.DIALOG_CATEGORY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.DIALOG_CATEGORY, payload)); + updateAllPlayerRevisions(EnumSyncType.DIALOG_CATEGORY, payload.getRevision()); + } + + public static void syncAllQuests() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.QUEST_CATEGORY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.QUEST_CATEGORY, payload)); + updateAllPlayerRevisions(EnumSyncType.QUEST_CATEGORY, payload.getRevision()); + } + + public static void syncAllWorkbenchRecipes() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.WORKBENCH_RECIPES); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.WORKBENCH_RECIPES, payload)); + updateAllPlayerRevisions(EnumSyncType.WORKBENCH_RECIPES, payload.getRevision()); + } + + public static void syncAllCarpentryRecipes() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CARPENTRY_RECIPES); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CARPENTRY_RECIPES, payload)); + updateAllPlayerRevisions(EnumSyncType.CARPENTRY_RECIPES, payload.getRevision()); + } + + public static void syncAllAnvilRecipes() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.ANVIL_RECIPES); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.ANVIL_RECIPES, payload)); + updateAllPlayerRevisions(EnumSyncType.ANVIL_RECIPES, payload.getRevision()); + } + + public static void syncAllCustomEffects() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_EFFECTS); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_EFFECTS, payload)); + updateAllPlayerRevisions(EnumSyncType.CUSTOM_EFFECTS, payload.getRevision()); + } + + @ClientOnly + public static void clientSync(EnumSyncType enumSyncType, int revision, INbt fullCompound) { + switch (enumSyncType) { + case FACTION: { + INbtList list = fullCompound.getTagList("Factions", 10); + for (int i = 0; i < list.tagCount(); i++) { + Faction faction = new Faction(); + faction.readNBT(list.getCompoundTagAt(i)); + FactionController.getInstance().factionsSync.put(faction.id, faction); + } + FactionController.getInstance().factions = FactionController.getInstance().factionsSync; + FactionController.getInstance().factionsSync = new HashMap(); + break; + } + case DIALOG_CATEGORY: { + if (!fullCompound.hasNoTags()) { + INbtList categories = fullCompound.getTagList("DialogCategories", 10); + for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { + INbt categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; + + DialogCategory category = new DialogCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + INbtList dialogList = categoryCompound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < dialogList.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(dialogList.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categoriesSync.put(category.id, category); + } + } + + HashMap dialogs = new HashMap(); + for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { + for (Dialog dialog : category.dialogs.values()) { + dialogs.put(dialog.id, dialog); + } + } + + DialogController.Instance.categories = DialogController.Instance.categoriesSync; + DialogController.Instance.dialogs = dialogs; + DialogController.Instance.categoriesSync = new HashMap(); + break; + } + case QUEST_CATEGORY: { + if (!fullCompound.hasNoTags()) { + INbtList categories = fullCompound.getTagList("QuestCategories", 10); + for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { + INbt categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; + + QuestCategory category = new QuestCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + INbtList questList = categoryCompound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < questList.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(questList.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categoriesSync.put(category.id, category); + } + } + + HashMap quests = new HashMap(); + for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { + for (Quest quest : category.quests.values()) { + quests.put(quest.id, quest); + } + } + + QuestController.Instance.categories = QuestController.Instance.categoriesSync; + QuestController.Instance.quests = quests; + QuestController.Instance.categoriesSync = new HashMap(); + break; + } + case PLAYERDATA: { + ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); + break; + } + case MAGIC: { + INbtList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.magicSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + Magic magic = new Magic(); + magic.readNBT(list.getCompoundTagAt(i)); + mc.magicSync.put(magic.id, magic); + } + + mc.magics.clear(); + mc.magics.putAll(mc.magicSync); + mc.magicSync.clear(); + break; + } + case MAGIC_CYCLE: { + INbtList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.cyclesSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(new NBTWrapper(list.getCompoundTagAt(i))); + mc.cyclesSync.put(cycle.id, cycle); + } + + mc.cycles.clear(); + mc.cycles.putAll(mc.cyclesSync); + mc.cyclesSync.clear(); + break; + } + case WORKBENCH_RECIPES: { + INbtList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + INbt recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); + RecipeController.syncRecipes = new HashMap(); + break; + } + case CARPENTRY_RECIPES: { + INbtList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + INbt recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; + RecipeController.syncRecipes = new HashMap(); + break; + } + case ANVIL_RECIPES: { + INbtList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + RecipeAnvil recipe = new RecipeAnvil(); + recipe.readNBT(list.getCompoundTagAt(i)); + RecipeController.syncAnvilRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; + RecipeController.syncAnvilRecipes = new HashMap(); + break; + } + case CUSTOM_EFFECTS: { + INbtList list = fullCompound.getTagList("Data", 10); + CustomEffectController ce = CustomEffectController.getInstance(); + ce.customEffectsSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(list.getCompoundTagAt(i)); + ClientCacheHandler.getImageData(effect.icon); + ce.customEffectsSync.put(effect.id, effect); + } + + ce.getCustomEffects().clear(); + ce.getCustomEffects().putAll(ce.customEffectsSync); + ce.customEffectsSync.clear(); + break; + } + case CUSTOM_ABILITY: { + INbtList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + INbt nbt = list.getCompoundTagAt(i); + String name = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setName(name); + sync.put(name, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + break; + } + case CHAINED_ABILITY: { + INbtList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + INbt nbt = list.getCompoundTagAt(i); + String id = nbt.getString("ChainedAbilityId"); + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt); + chain.setName(id); + sync.put(id, chain); + } + AbilityController.Instance.setChainedAbilities(sync); + break; + } + } + + ClientCacheHandler.updateClientRevision(enumSyncType, revision); + } + + /** + * Update Related Tasks + */ + @ClientOnly + public static void clientUpdate(EnumSyncType enumSyncType, int category_id, int revision, INbt compound) { + switch (enumSyncType) { + case FACTION: { + Faction faction = new Faction(); + faction.readNBT(compound); + FactionController.getInstance().factions.put(faction.id, faction); + break; + } + case DIALOG: { + DialogCategory category = DialogController.Instance.categories.get(category_id); + Dialog dialog = new Dialog(); + dialog.category = category; + dialog.readNBT(compound); + DialogController.Instance.dialogs.put(dialog.id, dialog); + category.dialogs.put(dialog.id, dialog); + break; + } + case QUEST: { + QuestCategory category = QuestController.Instance.categories.get(category_id); + Quest quest = new Quest(); + quest.category = category; + quest.readNBT(compound); + QuestController.Instance.quests.put(quest.id, quest); + category.quests.put(quest.id, quest); + break; + } + case QUEST_CATEGORY: { + QuestCategory category = new QuestCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + INbtList list = compound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(list.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categories.put(category.id, category); + break; + } + case DIALOG_CATEGORY: { + DialogCategory category = new DialogCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + INbtList list = compound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(list.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categories.put(category.id, category); + break; + } + case MAGIC_CYCLE: { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(new NBTWrapper(compound)); + MagicController.getInstance().cycles.put(cycle.id, cycle); + break; + } + case MAGIC: { + Magic magic = new Magic(); + magic.readNBT(compound); + MagicController.getInstance().magics.put(magic.id, magic); + break; + } + case PLAYERDATA: { + ClientCacheHandler.playerData.setSyncNBT(compound); + break; + } + case CUSTOM_EFFECTS: { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(compound); + ClientCacheHandler.getImageData(effect.icon); + + CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); + break; + } + } + + ClientCacheHandler.updateClientRevision(enumSyncType, revision); + } + + public static void syncUpdate(EnumSyncType type, int cat, INbt compound) { + Map revisions = invalidateCaches(type); + int revision = revisions.getOrDefault(type, getCurrentRevision(type)); + PacketHandler.Instance.sendToAll(new SyncPacket( + type, + EnumSyncAction.UPDATE, + cat, + revision, + compound + )); + updateAllPlayerRevisions(revisions); + } + + + public static INbt updateQuestCat(QuestCategory questCategory) { + INbt questCompound = new INbt(); + INbtList questList = new INbtList(); + for (int questID : questCategory.quests.keySet()) { + Quest quest = questCategory.quests.get(questID); + questList.appendTag(quest.writeToNBT(new INbt())); + } + questCompound.setTag("Data", questList); + questCompound.setTag("CatNBT", questCategory.writeSmallNBT(new INbt())); + return questCompound; + } + + public static INbt updateDialogCat(DialogCategory dialogCategory) { + INbt dialogCompound = new INbt(); + INbtList dialogList = new INbtList(); + for (int questID : dialogCategory.dialogs.keySet()) { + Dialog dialog = dialogCategory.dialogs.get(questID); + dialogList.appendTag(dialog.writeToNBT(new INbt())); + } + dialogCompound.setTag("Data", dialogList); + dialogCompound.setTag("CatNBT", dialogCategory.writeSmallNBT(new INbt())); + return dialogCompound; + } + + @ClientOnly + public static void clientSyncRemove(EnumSyncType enumSyncType, int id, int revision) { + switch (enumSyncType) { + case FACTION: + FactionController.getInstance().factions.remove(id); + break; + case DIALOG: + Dialog dialog = DialogController.Instance.dialogs.remove(id); + if (dialog != null) { + dialog.category.dialogs.remove(id); + } + break; + case DIALOG_CATEGORY: + DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); + if (dialogCategory != null) { + DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); + } + break; + case QUEST: + Quest quest = QuestController.Instance.quests.remove(id); + if (quest != null) { + quest.category.quests.remove(id); + } + break; + case QUEST_CATEGORY: + QuestCategory questCategory = QuestController.Instance.categories.remove(id); + if (questCategory != null) { + QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); + } + break; + case MAGIC: + for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { + cycle.associations.remove(id); + } + MagicController.getInstance().cycles.remove(id); + break; + case MAGIC_CYCLE: + MagicController.getInstance().cycles.remove(id); + break; + case CUSTOM_EFFECTS: + CustomEffectController.Instance.getCustomEffects().remove(id); + break; + } + + ClientCacheHandler.updateClientRevision(enumSyncType, revision); + } + + public static void syncEffects(IPlayer playerMP) { + ConcurrentHashMap playerEffects = CustomEffectController.getInstance().getPlayerEffects(playerMP); + PlayerData playerData = PlayerData.get(playerMP); + playerData.effectData.setEffects(playerEffects); + + INbt compound = playerData.getPlayerEffects(); + PacketHandler.Instance.sendToPlayer(new SyncEffectPacket(compound), playerMP); + } + + /** + * Full sync of all player ability state to the client: unlocked abilities, + * selection, toggles, hotbar layout, and cooldowns. + * Used for IEntity reconstruction and other full-refresh scenarios. + */ + public static void syncAbilities(IPlayer playerMP) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData == null) return; + + PlayerAbilitySyncPacket.sendToPlayer(playerMP); + AbilityHotbarSyncPacket.sendToPlayer(playerMP); + AbilityCooldownSyncPacket.sendToPlayer(playerMP); + } + + /** + * Lightweight sync of cooldown state only (global + per-ability). + * Called periodically (every 10 ticks) to keep the client's cooldown + * display accurate. Cooldowns are transient and not part of NBT sync. + */ + public static void syncAbilityCooldowns(IPlayer playerMP) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData == null) return; + + AbilityCooldownSyncPacket.sendToPlayer(playerMP); + } + + @ClientOnly + public static void clientSyncEffects(INbt compound) { + PlayerData playerData = PlayerData.get(Minecraft.getMinecraft().thePlayer); + if (playerData != null) { + playerData.setPlayerEffects(compound); + } + } + + private static void registerCache(EnumSyncType type, Supplier supplier) { + cacheEntries.put(type, new SyncCacheEntry(type, supplier)); + } + + private static CachedSyncPayload rebuildNow(EnumSyncType type) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry == null) { + return null; + } + entry.invalidate(); + return entry.getPayload(type); + } + + public static Map invalidateCaches(EnumSyncType type) { + EnumSet targets = getInvalidationTargets(type); + EnumMap revisions = new EnumMap<>(EnumSyncType.class); + for (EnumSyncType target : targets) { + SyncCacheEntry entry = cacheEntries.get(target); + if (entry != null) { + int newRevision = entry.invalidate(); + revisions.put(target, newRevision); + } + } + return revisions; + } + + private static EnumSet getInvalidationTargets(EnumSyncType type) { + switch (type) { + case DIALOG: + case DIALOG_CATEGORY: + return EnumSet.of(EnumSyncType.DIALOG_CATEGORY); + case QUEST: + case QUEST_CATEGORY: + return EnumSet.of(EnumSyncType.QUEST_CATEGORY); + case FACTION: + return EnumSet.of(EnumSyncType.FACTION); + case MAGIC: + return EnumSet.of(EnumSyncType.MAGIC); + case MAGIC_CYCLE: + return EnumSet.of(EnumSyncType.MAGIC_CYCLE); + case WORKBENCH_RECIPES: + return EnumSet.of(EnumSyncType.WORKBENCH_RECIPES); + case CARPENTRY_RECIPES: + return EnumSet.of(EnumSyncType.CARPENTRY_RECIPES); + case ANVIL_RECIPES: + return EnumSet.of(EnumSyncType.ANVIL_RECIPES); + case CUSTOM_EFFECTS: + return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS); + case CUSTOM_ABILITY: + return EnumSet.of(EnumSyncType.CUSTOM_ABILITY); + case CHAINED_ABILITY: + return EnumSet.of(EnumSyncType.CHAINED_ABILITY); + default: + return EnumSet.noneOf(EnumSyncType.class); + } + } + + private static void updateAllPlayerRevisions(EnumSyncType type, int revision) { + if (revision < 0) { + return; + } + for (PlayerSyncState state : playerSyncState.values()) { + state.updateRevision(type, revision); + } + } + + private static void updateAllPlayerRevisions(Map revisions) { + if (revisions.isEmpty()) { + return; + } + for (PlayerSyncState state : playerSyncState.values()) { + for (Map.Entry entry : revisions.entrySet()) { + state.updateRevision(entry.getKey(), entry.getValue()); + } + } + } + + public static final class CachedSyncPayload { + private final int revision; + private final byte[] payload; + private final byte[][] chunks; + + private CachedSyncPayload(int revision, byte[] payload, byte[][] chunks) { + this.revision = revision; + this.payload = payload; + this.chunks = chunks; + } + + public int getRevision() { + return revision; + } + + public byte[] getPayload() { + return payload; + } + + public byte[][] getChunks() { + return chunks; + } + } + + private static final class SyncCacheEntry { + private final Supplier supplier; + private volatile CachedSyncPayload payload; + private int revision = 0; + private boolean dirty = true; + + private SyncCacheEntry(EnumSyncType type, Supplier supplier) { + this.supplier = supplier; + } + + private synchronized CachedSyncPayload getPayload(EnumSyncType requestedType) { + if (!dirty && payload != null) { + return payload; + } + + int payloadRevision = revision; + INbt data = supplier.get(); + ByteBuf buffer = Unpooled.buffer(); + byte[] bytes; + try { + buffer.writeInt(requestedType.ordinal()); + buffer.writeInt(EnumSyncAction.RELOAD.ordinal()); + buffer.writeInt(-1); + buffer.writeInt(payloadRevision); + ByteBufUtils.writeBigNBT(buffer, data); + + bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + } catch (IOException e) { + throw new RuntimeException("Failed to serialize sync payload for " + requestedType, e); + } finally { + buffer.release(); + } + + byte[][] chunks = splitIntoChunks(bytes); + payload = new CachedSyncPayload(payloadRevision, bytes, chunks); + dirty = false; + return payload; + } + + private synchronized int invalidate() { + dirty = true; + payload = null; + revision++; + return revision; + } + + private synchronized int getRevisionValue() { + return revision; + } + + private synchronized void reset() { + dirty = true; + payload = null; + revision = 0; + } + + private static byte[][] splitIntoChunks(byte[] payload) { + int totalSize = payload.length; + int chunkCount = (totalSize + LargeAbstractPacket.CHUNK_SIZE - 1) / LargeAbstractPacket.CHUNK_SIZE; + if (chunkCount <= 0) { + chunkCount = 1; + } + byte[][] chunks = new byte[chunkCount][]; + for (int i = 0; i < chunkCount; i++) { + int offset = i * LargeAbstractPacket.CHUNK_SIZE; + int length = Math.min(LargeAbstractPacket.CHUNK_SIZE, totalSize - offset); + byte[] chunk = new byte[length]; + System.arraycopy(payload, offset, chunk, 0, length); + chunks[i] = chunk; + } + return chunks; + } + } + + private static final class PlayerSyncState { + private final EnumMap revisions = new EnumMap<>(EnumSyncType.class); + + private PlayerSyncState(UUID ignored) { + } + + private synchronized int getRevision(EnumSyncType type) { + Integer value = revisions.get(type); + return value == null ? -1 : value; + } + + private synchronized void updateRevision(EnumSyncType type, int revision) { + revisions.put(type, revision); + } + + private synchronized void applyHandshake(Map incoming) { + if (incoming == null || incoming.isEmpty()) { + return; + } + revisions.putAll(incoming); + } + + private synchronized void reset() { + revisions.clear(); + } + + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/TelegraphController.java b/core/src/main/java/kamkeel/npcs/controllers/TelegraphController.java new file mode 100644 index 000000000..fba07b43b --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/TelegraphController.java @@ -0,0 +1,400 @@ +package kamkeel.npcs.controllers; + + +import java.io.File; +import java.util.concurrent.ConcurrentHashMap; +import java.util.HashMap; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.ITelegraphHandler; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.ITelegraph; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.util.NBTJsonUtil; + +/** + * Server-side controller for managing telegraphs. + * Provides factory methods for creating telegraphs and spawn methods for sending to clients. + * Also manages saved telegraph presets. + */ +public class TelegraphController implements ITelegraphHandler { + + public static TelegraphController Instance; + + // Saved telegraph presets (name -> Telegraph) + private HashMap savedTelegraphs = new HashMap<>(); + + // Active server-side telegraph instances (for removal tracking) + private ConcurrentHashMap activeInstances = new ConcurrentHashMap<>(); + + public TelegraphController() { + } + + public static void init() { + Instance = new TelegraphController(); + Instance.load(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INTERNAL SPAWN METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Spawn a telegraph at a fixed position. + * + * @param telegraph The telegraph IConfiguration + * @param IWorld The IWorld + * @param x X position + * @param y Y position + * @param z Z position + * @param yaw Rotation yaw (for directional telegraphs like LINE/CONE) + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, IWorld IWorld, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(IWorld); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToDimension(instance, IWorld.provider.dimensionId); + return instance; + } + + /** + * Spawn a telegraph at a position with default yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, IWorld IWorld, double x, double y, double z) { + return spawn(telegraph, IWorld, x, y, z, 0); + } + + /** + * Spawn a telegraph that follows an IEntity. + * + * @param telegraph The telegraph IConfiguration + * @param IEntity The IEntity to follow + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, IEntity IEntity) { + return spawn(telegraph, IEntity, 0); + } + + /** + * Spawn a telegraph that follows an IEntity with yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, IEntity IEntity, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, IEntity.posX, IEntity.posY, IEntity.posZ, yaw); + instance.setWorld(IEntity.worldObj); + instance.setEntityIdToFollow(IEntity.getEntityId()); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, IEntity); + return instance; + } + + /** + * Spawn a telegraph to a specific player only. + */ + public TelegraphInstance spawnToPlayer(Telegraph telegraph, IPlayer player, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(player.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.send(instance, player); + return instance; + } + + /** + * Spawn a telegraph to all players on the server. + */ + public TelegraphInstance spawnToAll(Telegraph telegraph, IWorld IWorld, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(IWorld); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToAll(instance); + return instance; + } + + /** + * Spawn a telegraph to all players tracking an IEntity. + */ + public TelegraphInstance spawnToTracking(Telegraph telegraph, IEntity IEntity, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(IEntity.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, IEntity); + return instance; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // REMOVAL METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Remove a telegraph by instance ID. + */ + public void remove(String instanceId) { + TelegraphInstance instance = activeInstances.remove(instanceId); + if (instance != null && instance.getWorld() != null) { + TelegraphRemovePacket.sendToDimension(instanceId, instance.getDimensionId()); + } + } + + /** + * Remove a telegraph instance. + */ + public void remove(TelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } + + /** + * Remove a telegraph for a specific player only. + */ + public void removeFromPlayer(String instanceId, IPlayer player) { + activeInstances.remove(instanceId); + TelegraphRemovePacket.send(instanceId, player); + } + + /** + * Get an active telegraph instance by ID. + */ + public TelegraphInstance getActiveInstance(String instanceId) { + return activeInstances.get(instanceId); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PRESET MANAGEMENT + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Save a telegraph preset (internal use). + * + * @param name The name for the preset + * @param telegraph The telegraph IConfiguration to save + */ + public void saveInternal(String name, Telegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + saveTelegraphFile(name, telegraph); + } + + /** + * Get a saved telegraph preset by name (internal use). + * + * @param name The preset name + * @return The telegraph IConfiguration, or null if not found + */ + public Telegraph getInternal(String name) { + return savedTelegraphs.get(name); + } + + /** + * Delete a saved telegraph preset. + * + * @param name The preset name + */ + public void delete(String name) { + Telegraph removed = savedTelegraphs.remove(name); + if (removed != null) { + File file = new File(getDir(), name + ".json"); + if (file.exists()) { + file.delete(); + } + } + } + + /** + * Check if a preset exists. + */ + public boolean has(String name) { + return savedTelegraphs.containsKey(name); + } + + /** + * Get all saved preset names. + */ + public String[] getSavedNames() { + return savedTelegraphs.keySet().toArray(new String[0]); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PERSISTENCE + // ═══════════════════════════════════════════════════════════════════════════ + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "telegraphs"); + } + + public void load() { + savedTelegraphs.clear(); + LogWriter.info("Loading telegraph presets..."); + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + return; + } + + File[] files = dir.listFiles(); + if (files == null) return; + + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + Telegraph telegraph = new Telegraph(); + telegraph.readNBT(new NBTWrapper(NBTJsonUtil.LoadFile(file))); + String name = file.getName().substring(0, file.getName().length() - 5); + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + } catch (Exception e) { + LogWriter.error("Error loading telegraph preset: " + file.getName(), e); + } + } + + LogWriter.info("Loaded " + savedTelegraphs.size() + " telegraph presets."); + } + + private void saveTelegraphFile(String name, Telegraph telegraph) { + try { + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + } + + File file = new File(dir, name + ".json_new"); + File file2 = new File(dir, name + ".json"); + + NBTJsonUtil.SaveFile(file, ((NBTWrapper) telegraph.writeNBT()).getMCTag()); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + } catch (Exception e) { + LogWriter.error("Error saving telegraph preset: " + name, e); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ITelegraphHandler INTERFACE IMPLEMENTATION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public ITelegraph createCircle(float radius) { + return new ScriptTelegraph(Telegraph.circle(radius)); + } + + @Override + public ITelegraph createRing(float outerRadius, float innerRadius) { + return new ScriptTelegraph(Telegraph.ring(outerRadius, innerRadius)); + } + + @Override + public ITelegraph createLine(float length, float width) { + return new ScriptTelegraph(Telegraph.line(length, width)); + } + + @Override + public ITelegraph createCone(float length, float angle) { + return new ScriptTelegraph(Telegraph.cone(length, angle)); + } + + @Override + public ITelegraph createSquare(float radius) { + return new ScriptTelegraph(Telegraph.square(radius)); + } + + @Override + public ITelegraph createPoint() { + return new ScriptTelegraph(Telegraph.point()); + } + + @Override + public ITelegraph create(String type) { + if (type == null) return null; + try { + TelegraphType telegraphType = TelegraphType.valueOf(type.toUpperCase()); + Telegraph telegraph; + switch (telegraphType) { + case CIRCLE: + telegraph = Telegraph.circle(3.0f); + break; + case RING: + telegraph = Telegraph.ring(5.0f, 2.0f); + break; + case LINE: + telegraph = Telegraph.line(5.0f, 2.0f); + break; + case CONE: + telegraph = Telegraph.cone(5.0f, 45.0f); + break; + case SQUARE: + telegraph = Telegraph.square(3.0f); + break; + case POINT: + telegraph = Telegraph.point(); + break; + default: + telegraph = Telegraph.circle(3.0f); + } + return new ScriptTelegraph(telegraph); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld IWorld, double x, double y, double z) { + return spawn(telegraph, IWorld, x, y, z, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld IWorld, double x, double y, double z, float yaw) { + if (telegraph == null || IWorld == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (IWorld) IWorld.getMCWorld(), x, y, z, yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity IEntity) { + return spawn(telegraph, IEntity, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity IEntity, float yaw) { + if (telegraph == null || IEntity == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (IEntity) IEntity.getMCEntity(), yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraph get(String name) { + Telegraph telegraph = savedTelegraphs.get(name); + return telegraph != null ? new ScriptTelegraph(new Telegraph(telegraph)) : null; + } + + @Override + public void save(String name, ITelegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + saveInternal(name, internal); + } + + @Override + public void remove(ITelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java new file mode 100644 index 000000000..3d8ce3973 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -0,0 +1,2915 @@ +package kamkeel.npcs.controllers.data.ability; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.data.AbilityIconData; +import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect; +import kamkeel.npcs.controllers.data.ability.enums.InvulnerableMode; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.enums.RotationMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.util.AttributeAttackUtil; +import kamkeel.npcs.util.FileNameHelper; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.api.INbt; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.controllers.TagController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.data.AbilityScript; +import noppes.npcs.EventHooks; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.MagicEntry; +import noppes.npcs.controllers.data.PlayerData; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Predicate; + +public abstract class Ability implements IAbility, IAbilityAction { + + // ═══════════════════════════════════════════════════════════════════ + // IConfiguration (saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + protected String id = ""; // UUID for custom, registry key for built-in + protected String name = ""; // Unique file key / identifier + protected String displayName = ""; // Cosmetic name (falls back to name when empty) + protected String typeId = ""; // e.g., "ability.cnpc.slam" (also used as lang key) + + // Selection + protected int weight = 10; + protected boolean enabled = true; + protected List conditions = new ArrayList<>(); + + // Targeting + protected TargetingMode targetingMode = TargetingMode.AGGRO_TARGET; + protected float minRange = 0; + protected float maxRange = 20; + + // Timing (ticks) + protected int cooldownTicks = 0; // Added to global cooldown after ability completes + protected int windUpTicks = 20; + protected boolean syncWindupWithAnimation = true; + protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) + + // Interruption + protected boolean interruptible = true; + protected InvulnerableMode invulnerableMode = InvulnerableMode.NONE; + protected boolean ignoreIFrames = false; + + // Feedback + protected LockMode lockMovement = LockMode.WINDUP; + protected RotationMode rotationMode = RotationMode.FREE; + protected LockMode rotationPhase = LockMode.WINDUP_AND_ACTIVE; + protected float trackSpeed = 0; // 0 = instant tracking. Linear tracking speed in blocks/second. + protected int windUpColor = 0x80FF4400; // Telegraph color during wind up + protected int activeColor = 0xC0FF0000; // Telegraph warning/active color + + // Sounds + protected String windUpSound = ""; // Sound to play when wind up starts + protected String activeSound = ""; // Sound to play when active phase starts + + // Animations (global animation IDs, -1 = none) + protected int windUpAnimationId = -1; // Animation to play during wind up (user animations) + protected int activeAnimationId = -1; // Animation to play during active phase (user animations) + protected int dazedAnimationId = -1; // Animation to play during dazed phase (user animations) + + // Built-in animation names (empty = none, takes priority over IDs if set) + protected String windUpAnimationName = ""; // Built-in animation name for wind up + protected String activeAnimationName = ""; // Built-in animation name for active phase + protected String dazedAnimationName = "Ability_Generic_Dazed"; // Built-in animation name for dazed phase + + // Telegraph IConfiguration + protected boolean showTelegraph = true; + protected TelegraphType telegraphType = TelegraphType.CIRCLE; + protected float telegraphHeightOffset = 0.1f; + + // Custom data for external mods + protected INbt customData = new INbt(); + + // Tags + protected HashSet tagUUIDs = new HashSet<>(); + + // Magic data — defines magic types and damage splits for this ability + protected MagicData magicData = new MagicData(); + + // User type restriction + protected UserType allowedBy = UserType.BOTH; + + // Cooldown override + protected boolean ignoreCooldown = false; + + // Per-ability cooldown: if true, this ability has its own independent cooldown + // instead of sharing the global cooldown with other abilities + protected boolean perAbilityCooldown = false; + + // Free on Cast: if true, ability enters cooldown immediately after entities are spawned, + // allowing the caster to use other abilities while summoned entities remain active + protected boolean freeOnCast = false; + + // ═══════════════════════════════════════════════════════════════════ + // BUILT-IN (set in constructor via configureAsBuiltIn, NOT persisted) + // ═══════════════════════════════════════════════════════════════════ + + protected transient boolean builtIn = false; + protected transient String registryKey; + + // Default icon layers (set in subclass constructors, NOT persisted) + protected transient DefaultIconLayer[] defaultIconLayers = null; + protected transient int defaultIconWidth = 48; + protected transient int defaultIconHeight = 48; + + /** + * Optional player requirement predicate. When set, the ability is only visible + * and usable by players for whom this predicate returns true. + * Evaluated on-demand (no polling). Null means no requirement (always available). + *

+ * This is transient — set programmatically by addons, not saved to NBT. + */ + protected transient Predicate playerRequirement; + + /** + * A single layer of a default ability icon. Each layer has a texture, + * an optional dynamic color source, and optional per-state texture variants. + */ + public static class DefaultIconLayer { + public final String texture; + public final String[] stateTextures; + public final java.util.function.IntSupplier colorSource; + + public DefaultIconLayer(String texture, String[] stateTextures, java.util.function.IntSupplier colorSource) { + this.texture = texture; + this.stateTextures = stateTextures; + this.colorSource = colorSource; + } + + public DefaultIconLayer(String texture, java.util.function.IntSupplier colorSource) { + this(texture, null, colorSource); + } + + /** Layer with no color tinting (renders as white). */ + public DefaultIconLayer(String texture) { + this(texture, null, null); + } + + /** Resolve texture for a given toggle state. Falls back to base texture. */ + public String getTextureForState(int state) { + if (state > 0 && stateTextures != null && state - 1 < stateTextures.length) { + return stateTextures[state - 1]; + } + return texture; + } + + public int getColor() { + return colorSource != null ? colorSource.getAsInt() : 0xFFFFFF; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // TOGGLE IConfiguration (saved to NBT for custom abilities) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Number of toggle states. 0 = not toggleable, 1 = binary on/off, 2+ = multi-state cycling. + * Cycling order: Off(0) -> State 1 -> State 2 -> ... -> State N -> Off(0) + */ + protected int toggleStates = 0; + + /** + * Whether the toggled-ON state ticks each game tick (e.g., Kaioken drains ki per tick) + */ + protected boolean hasActiveToggle = false; + + /** + * Optional display labels for each toggle state (1-indexed). + * Used in ability wheel/hotbar to show current state name. + * Null means no labels. Array length should match toggleStates. + */ + protected String[] toggleStateLabels; + + // Configurable IPotionType effects + protected List effects = new ArrayList<>(); + + // Burst + protected boolean burstEnabled = false; + protected int burstAmount = 0; // number of EXTRA repetitions + protected int burstDelay = 0; // ticks between bursts + protected boolean burstReplayAnimations = true; + protected boolean burstOverlap = false; // if true, don't wait for IEntity death between bursts + + // ═══════════════════════════════════════════════════════════════════ + // EXECUTION STATE (not saved, reset each combat) + // ═══════════════════════════════════════════════════════════════════ + + protected transient AbilityPhase phase = AbilityPhase.IDLE; + protected transient int currentTick = 0; + protected transient IEntityLivingBase currentTarget; + protected transient long executionStartTime; + protected transient List telegraphInstances = new ArrayList<>(); + + // Burst execution state + protected transient int burstIndex = 0; + protected transient List burstEntities = new ArrayList<>(); + + // Per-instance script (isolated per execution, not saved to NBT) + protected transient AbilityScript instanceScript; + protected transient float damageMultiplier = 1.0f; // Ability-internal damage scaling (e.g., Slam height) + + // ═══════════════════════════════════════════════════════════════════ + // PREVIEW STATE (client-side only, not saved) + // ═══════════════════════════════════════════════════════════════════ + + protected transient boolean previewMode = false; + protected transient PreviewEntityHandler previewEntityHandler; + + // ═══════════════════════════════════════════════════════════════════ + // GUI CONTEXT (client-side only, not saved) + // ═══════════════════════════════════════════════════════════════════ + + protected transient boolean npcInlineEdit = false; + + // ═══════════════════════════════════════════════════════════════════ + // ABSTRACT METHODS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Called first tick of ACTIVE phase. + * For burst without replay (burstReplayAnimations=false), this is called directly + * after BURST_DELAY without a preceding windup. Check isBurstRefire() or null-check + * IEntity state to detect and handle the burst refire case. + */ + public abstract void onExecute(IEntityLivingBase caster, IEntityLivingBase target); + + /** + * Called every tick of ACTIVE phase + */ + public abstract void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick); + + /** + * Write type-specific config to NBT + */ + public abstract void writeTypeNBT(INbt nbt); + + /** + * Read type-specific config from NBT + */ + public abstract void readTypeNBT(INbt nbt); + + // ═══════════════════════════════════════════════════════════════════ + // OPTIONAL OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + /** + * Returns true if this ability type deals damage. + * Override in non-damaging subclasses to return false. + * Used by external mods to determine whether to show + * damage-related IConfiguration (e.g., DBC stats tab). + */ + public boolean hasDamage() { + return true; + } + + /** + * Returns true if this ability type uses magic data. + * By default, any ability that deals damage also has magic. + * Override in subclasses like Barrier that need magic but don't deal direct damage. + */ + public boolean hasMagic() { + return hasDamage(); + } + + // ── Display methods for inventory preview ────────────────────── + + /** Base damage for preview display. Override in damaging types. */ + public float getDisplayDamage() { return 0; } + + /** True if getDisplayDamage() is damage-per-second (e.g., Hazard). */ + public boolean isDisplayDamageDPS() { return false; } + + /** Base barrier health for preview display. Override in barrier types. */ + public float getDisplayBarrierHealth() { return 0; } + + /** True if this ability reflects projectiles. */ + public boolean isDisplayReflect() { return false; } + + /** Reflection strength percentage (0-100). */ + public float getDisplayReflectStrength() { return 0; } + + /** True if this ability absorbs projectiles. */ + public boolean isDisplayAbsorbing() { return false; } + + /** + * Override to provide variant templates for this ability type. + * When a user creates this ability type, they will be shown a selection dialog + * with the returned variants. If empty or single-entry, the dialog is skipped. + */ + public List getVariants() { + return Collections.emptyList(); + } + + /** + * Whether this ability type supports the burst system. + * Override to return false for abilities that shouldn't repeat (Guard, Heal, Sweeper, etc.). + */ + public boolean allowBurst() { + return true; + } + + /** + * Whether this ability supports overlap mode for burst (entities from previous bursts keep flying). + * Override to return true for IEntity-spawning projectile abilities (Orb, Beam, Disc, LaserShot). + */ + public boolean allowOverlap() { + return false; + } + + /** + * Whether this ability can run concurrently alongside the primary ability in a chain. + * Override to return true for passive/effect abilities that don't need exclusive animations. + * Concurrent abilities skip animations entirely and run their tick/execute logic independently. + */ + public boolean isConcurrentCapable() { + return false; + } + + /** + * Whether this ability type supports the Free on Cast option. + * Override to return true for abilities that spawn persistent entities (projectiles, barriers, zones, sweeper). + */ + public boolean allowFreeOnCast() { + return false; + } + + /** + * Detach all spawned entities (let them live independently) without killing them. + * Called during signalCompletion() when freeOnCast is enabled. + * Override in subclasses to null IEntity references without calling setDead(). + */ + public void detach() { + } + + // ═══════════════════════════════════════════════════════════════════ + // TOGGLE CALLBACKS (optional overrides for toggleable abilities) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Called whenever the toggle state changes (activation, deactivation, or mid-cycling). + * Override to apply/remove effects based on state transitions. + * + * @param caster The IEntity + * @param oldState Previous state (0 = was off) + * @param newState New state (0 = now off, 1+ = active state) + */ + public void onToggle(IEntityLivingBase caster, int oldState, int newState) { + } + + /** + * Called every tick while this toggle is active (only if hasActiveToggle is true). + * Override for per-tick effects (e.g., ki drain for Kaioken). + * + * @param state Current toggle state (1-based) + * @return true to keep the toggle active, false to auto-deactivate + */ + public boolean onToggleTick(IEntityLivingBase caster, int tickCount, int state) { + return true; + } + + /** + * Whether this ability is ready for auto-completion during burst overlap mode. + * Override to delay auto-completion until all staggered projectiles have been fired. + * + * @param activeTick the current tick within the ACTIVE phase + */ + public boolean isReadyForBurstCompletion(int activeTick) { + return true; + } + + public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {} + + public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) {} + + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {} + + /** + * Called when the ability enters BURST_DELAY between burst iterations. + * Override in subclasses to reset transient state that should not carry + * across burst repetitions (e.g., movement direction, hit IEntity lists). + *

+ * Unlike {@link #cleanup()}, this does NOT kill spawned entities (they may + * still be in flight for overlap mode). + */ + public void resetForBurst() {} + + /** + * Apply damage to an IEntity with ability hit event support. + * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. + * + * @param caster The IEntity executing the ability (NPC or Player) + * @param hitEntity The IEntity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @return true if damage was applied (not cancelled), false if cancelled + */ + protected boolean applyAbilityDamage(IEntityLivingBase caster, IEntityLivingBase hitEntity, + float damage, float knockback) { + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, dx, dz); + } + + /** + * Apply damage to an IEntity with ability hit event support and custom knockback direction. + * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. + * + * @param caster The IEntity executing the ability (NPC or Player) + * @param hitEntity The IEntity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction + * @return true if damage was applied (not cancelled), false if cancelled + */ + protected boolean applyAbilityDamageWithDirection(IEntityLivingBase caster, IEntityLivingBase hitEntity, + float damage, float knockback, + double knockbackDirX, double knockbackDirZ) { + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); + } + + /** + * Clears hurt resistance before damage and returns the previous value so it can be restored later. + */ + public static int clearHurtResistanceIfNeeded(IEntityLivingBase target, boolean ignoreIFrames) { + if (!ignoreIFrames || target == null) { + return -1; + } + int previous = target.hurtResistantTime; + target.hurtResistantTime = 0; + return previous; + } + + /** + * Restores hurt resistance after damage when {@link #clearHurtResistanceIfNeeded} was used. + */ + public static void restoreHurtResistanceIfNeeded(IEntityLivingBase target, boolean ignoreIFrames, int previous) { + if (!ignoreIFrames || target == null || previous < 0) { + return; + } + target.hurtResistantTime = previous; + } + + /** + * Internal unified damage application method. + * All public damage methods delegate to this. + * Supports both NPC and Player casters. + */ + private boolean applyAbilityDamageInternal(IEntityLivingBase caster, IEntityLivingBase hitEntity, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { + // Fire hit event (unified — works for both NPC and Player casters) + AbilityEvent.HitEvent hitEvent = new AbilityEvent.HitEvent( + caster, this, currentTarget, hitEntity, damage, knockback, knockbackUp); + if (EventHooks.onAbilityHit(this, hitEvent)) { + return false; // Cancelled + } + damage = hitEvent.getDamage(); + knockback = hitEvent.getKnockback(); + knockbackUp = hitEvent.getKnockbackUp(); + + // Save velocity before damage to cancel vanilla knockback when ability knockback is zero + double prevMotionX = hitEntity.motionX; + double prevMotionY = hitEntity.motionY; + double prevMotionZ = hitEntity.motionZ; + + // Apply damage + if (damage > 0) { + int previousHurtResistantTime = clearHurtResistanceIfNeeded(hitEntity, ignoreIFrames); + try { + // Check for ability extenders (e.g., DBC Addon damage routing) + boolean handled = AbilityController.Instance.fireOnAbilityDamage( + this, caster, hitEntity, damage, knockback, knockbackUp, + knockbackDirX, knockbackDirZ, 1.0f); + if (!handled) { + float finalDamage = damage; + + // Apply magic pipeline for player and NPC casters + MagicData resolved = resolveMagicData(caster); + if (resolved != null && !resolved.isEmpty()) { + finalDamage = AttributeAttackUtil.calculateAbilityDamage( + caster, hitEntity, damage, resolved); + } + + // Default damage path + if (caster instanceof EntityNPCInterface) { + hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), finalDamage); + } else if (caster instanceof IPlayer) { + hitEntity.attackEntityFrom(IDamageSource.causePlayerDamage((IPlayer) caster), finalDamage); + } else { + hitEntity.attackEntityFrom(IDamageSource.causeMobDamage(caster), finalDamage); + } + } + } finally { + restoreHurtResistanceIfNeeded(hitEntity, ignoreIFrames, previousHurtResistantTime); + } + } + + // Apply knockback if any, otherwise restore velocity to cancel vanilla knockback + if (knockback > 0 || knockbackUp > 0) { + applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp); + } else { + hitEntity.motionX = prevMotionX; + hitEntity.motionY = prevMotionY; + hitEntity.motionZ = prevMotionZ; + hitEntity.velocityChanged = true; + } + + return true; + } + + private void applyKnockback(IEntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { + double len = Math.sqrt(dirX * dirX + dirZ * dirZ); + double x = 0; + double z = 0; + if (len > 0) { + x = (dirX / len) * knockback * 0.5; + z = (dirZ / len) * knockback * 0.5; + } + double y = knockbackUp > 0 ? knockbackUp : 0.1D; + hitEntity.addVelocity(x, y, z); + hitEntity.velocityChanged = true; + } + + /** + * Applies all configured effects to the given IEntity. + * Call this after damage is applied to the target. + * + * @param IEntity The IEntity to apply effects to + */ + protected void applyEffects(IEntityLivingBase IEntity) { + if (IEntity == null || effects.isEmpty()) { + return; + } + for (AbilityPotionEffect effect : effects) { + effect.apply(IEntity); + } + } + + public float getTelegraphRadius() { + return 5.0f; + } + + public float getTelegraphLength() { + return 5.0f; + } + + public float getTelegraphWidth() { + return 2.0f; + } + + public float getTelegraphAngle() { + return 45.0f; + } + + public float getTelegraphInnerRadius() { + return 0.0f; + } + + /** + * Override to return true if the ability type manages its own telegraph GUI controls. + * When true, the base class will skip adding the default telegraph section. + */ + public boolean hasOwnTelegraphControls() { + return false; + } + + /** + * Returns true if the caster is a player (not an NPC). + * Use this instead of checking {@code target != null} to make NPC/Player + * code paths explicit and readable. + *

+ * NPCs always have an aggro target passed to ability methods. + * Players never do — they use look direction for aiming instead. + */ + protected final boolean isPlayerCaster(IEntityLivingBase caster) { + return caster instanceof IPlayer; + } + + /** + * Whether the telegraph should remain visible during the ACTIVE phase. + * Default: false (telegraph removed at WINDUP→ACTIVE transition). + * Override for abilities where the active phase IS the attack (e.g., sweeping/spinning attacks) + * so the telegraph stays visible while damage is being dealt. + */ + public boolean keepTelegraphDuringActive() { + return false; + } + + /** + * Returns true if this ability controls NPC movement during ACTIVE phase. + * When true, AI pathfinding will be blocked during ability execution. + * Override in abilities that move the NPC (Charge, Dash, Slam, etc.) + */ + public boolean hasAbilityMovement() { + return false; + } + + /** + * Returns true if the targeting mode for this ability type is locked and cannot be changed. + * Most abilities have fixed targeting (e.g., Heal is always SELF, Charge is always AGGRO_TARGET). + * Override and return false for abilities with flexible targeting. + */ + public boolean isTargetingModeLocked() { + return true; + } + + /** + * Returns the allowed targeting modes for this ability, or null if all modes are allowed. + * Override in abilities that only support specific targeting modes. + */ + public TargetingMode[] getAllowedTargetingModes() { + return null; + } + + /** + * Add field definitions for this ability's GUI. + * Override in subclasses to add type-specific fields. The list already contains + * the base fields (General, Target, Effects tabs), so subclasses can also + * modify, insert into, or remove base fields using {@link FieldDef#insertBefore}, + * {@link FieldDef#insertAfter}, or direct list manipulation. + *

+ * Fields added here without an explicit {@code .tab()} will default to the "Type" tab. + * + * @param defs The mutable list of field definitions to add to or modify + */ + @ClientOnly + public void getAbilityDefinitions(List defs) { + } + + /** + * Builds and returns all field definitions for this ability's GUI. + * Base fields (General, Target, Effects) are added first, then + * {@link #getAbilityDefinitions(List)} is called so subclasses can add + * type-specific fields and modify any existing fields. + */ + @ClientOnly + public final List getAllDefinitions() { + List defs = new ArrayList<>(); + + // ── General tab ────────────────────────────────────────────── + defs.add(FieldDef.stringField("gui.name", this::getName, this::setName) + .tab("General")); + defs.add(FieldDef.stringField("gui.displayName", this::getRawDisplayName, this::setDisplayName) + .tab("General")); + defs.add(FieldDef.labelField("ability.validFor", () -> + "\u00A7e" + PlatformServiceHolder.get().translateToLocal("ability.userType." + getAllowedBy().name())) + .tab("General")); + defs.add(FieldDef.row( + FieldDef.intField("ability.weight", this::getWeight, this::setWeight).range(1, 1000), + FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) + ).tab("General")); + defs.add(FieldDef.section("ability.section.timing").tab("General")); + defs.add(FieldDef.intField("ability.windUpTicks", this::getRawWindUpTicks, this::setWindUpTicks) + .tab("General").range(0, 1000) + .visibleWhen(() -> !hasWindUpAnimation())); + defs.add(FieldDef.row( + FieldDef.intField("ability.windUpTicks", this::getRawWindUpTicks, this::setWindUpTicks).range(0, 1000), + FieldDef.boolField("ability.syncWindup", this::isSyncWindupWithAnimation, this::setSyncWindupWithAnimation) + .hover("ability.hover.sync") + ).tab("General").visibleWhen(() -> hasWindUpAnimation() && !isSyncWindupWithAnimation())); + defs.add(FieldDef.row( + FieldDef.labelField("ability.windUpTicks", () -> getWindUpTicks() + "t"), + FieldDef.boolField("ability.syncWindup", this::isSyncWindupWithAnimation, this::setSyncWindupWithAnimation) + .hover("ability.hover.sync") + ).tab("General").visibleWhen(() -> hasWindUpAnimation() && isSyncWindupWithAnimation())); + defs.add(FieldDef.row( + FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks).range(0, 10000), + FieldDef.boolField("ability.perAbilityCooldown", this::isPerAbilityCooldown, this::setPerAbilityCooldown) + ).tab("General")); + if (allowFreeOnCast()) { + defs.add(FieldDef.boolField("ability.freeOnCast", this::isFreeOnCast, this::setFreeOnCast) + .tab("General").hover("ability.hover.freeOnCast")); + } + defs.add(FieldDef.section("ability.section.movement").tab("General")); + defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMode.getDisplayKeys(), + () -> this.getLockMovement().getDisplayKey(), + v -> { + for (LockMode t : LockMode.values()) { + if (t.getDisplayKey().equals(v)) { + this.setLockMovement(t); + break; + } + } + }) + .hover("ability.hover.lockMovement") + .tab("General")); + defs.add(FieldDef.row( + FieldDef.stringEnumField("ability.rotationMode", RotationMode.getDisplayKeys(), + () -> this.getRotationMode().getDisplayKey(), + v -> { + for (RotationMode m : RotationMode.values()) { + if (m.getDisplayKey().equals(v)) { + this.setRotationMode(m); + break; + } + } + }) + .hover("ability.hover.rotationMode"), + FieldDef.stringEnumField("ability.rotationPhase", getRotationPhaseKeys(), + () -> this.getRotationPhase().getDisplayKey(), + v -> { + for (LockMode t : LockMode.values()) { + if (t.getDisplayKey().equals(v)) { + this.setRotationPhase(t); + break; + } + } + }) + .hover("ability.hover.rotationPhase") + .visibleWhen(() -> this.rotationMode != RotationMode.FREE) + ).tab("General")); + defs.add(FieldDef.floatField("ability.trackSpeed", this::getTrackSpeed, this::setTrackSpeed) + .range(0.0f, 12.0f) + .hover("ability.hover.trackSpeed") + .visibleWhen(() -> this.rotationMode == RotationMode.TRACK) + .tab("General")); + defs.add(FieldDef.row( + FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) + .hover("ability.hover.interruptible") + .enabledWhen(() -> !invulnerableMode.invulnerableDuringWindup()), + FieldDef.intField("ability.dazedTicks", this::getDazedTicks, this::setDazedTicks) + .range(0, 1000) + .visibleWhen(() -> isInterruptible() && !invulnerableMode.invulnerableDuringWindup()) + ).tab("General")); + defs.add(FieldDef.stringEnumField("ability.invulnerable", InvulnerableMode.getDisplayKeys(), + () -> this.getInvulnerableMode().getDisplayKey(), + v -> { + for (InvulnerableMode mode : InvulnerableMode.values()) { + if (mode.getDisplayKey().equals(v)) { + this.setInvulnerableMode(mode); + break; + } + } + }) + .hover("ability.hover.invulnerable") + .tab("General")); + defs.add(FieldDef.boolField("ability.ignoreIFrames", this::isIgnoreIFrames, this::setIgnoreIFrames) + .hover("ability.hover.ignoreIFrames") + .visibleWhen(this::hasDamage) + .tab("General")); + + // ── Burst section ──────────────────────────────────────────── + if (allowBurst()) { + defs.add(FieldDef.section("ability.section.burst").tab("General")); + defs.add(FieldDef.boolField("ability.burstEnabled", this::isBurstEnabled, this::setBurstEnabled) + .tab("General")); + defs.add(FieldDef.row( + FieldDef.intField("ability.burstAmount", this::getBurstAmount, this::setBurstAmount).range(1, 100), + FieldDef.intField("ability.burstDelay", this::getBurstDelay, this::setBurstDelay).range(0, 1000) + ).tab("General").visibleWhen(this::isBurstEnabled)); + defs.add(FieldDef.boolField("ability.burstReplayAnimations", this::isBurstReplayAnimations, this::setBurstReplayAnimations) + .tab("General").visibleWhen(this::isBurstEnabled) + .hover("ability.hover.burstReplay")); + if (allowOverlap()) { + defs.add(FieldDef.boolField("ability.burstOverlap", this::isBurstOverlap, this::setBurstOverlap) + .tab("General").visibleWhen(this::isBurstEnabled) + .hover("ability.hover.burstOverlap")); + } + } + + // ── Magic section ──────────────────────────────────────────── + if (hasMagic()) { + defs.add(FieldDef.section("ability.section.magic").tab("General")); + + defs.add(FieldDef.labelField("ability.magic.note", () -> { + if (magicData.isEmpty()) { + return PlatformServiceHolder.get().translateToLocal("ability.magic.usesCaster"); + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : magicData.getMagics().entrySet()) { + Magic magic = MagicController.getInstance().getMagic(entry.getKey()); + if (magic != null) { + if (sb.length() > 0) sb.append(", "); + sb.append(magic.getDisplayName()).append(": ") + .append(Math.round(entry.getValue().split * 100)).append("%"); + } + } + return sb.toString(); + }).tab("General")); + + defs.add(FieldDef.subGuiField("ability.magic.editor", + () -> new SubGuiAbilityMagic(magicData.copy()), + gui -> { + MagicData result = ((SubGuiAbilityMagic) gui).magicData; + magicData.getMagics().clear(); + for (Map.Entry e : result.getMagics().entrySet()) { + magicData.getMagics().put(e.getKey(), e.getValue()); + } + }) + .buttonLabel(() -> magicData.isEmpty() + ? PlatformServiceHolder.get().translateToLocal("ability.magic.usesCaster") + : magicData.getMagics().size() + " Magic(s)") + .tab("General")); + } + + // ── Target tab ─────────────────────────────────────────────── + defs.add(FieldDef.row( + FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)).range(0, 100), + FieldDef.intField("ability.maxRange", () -> (int) getMaxRange(), v -> setMaxRange(v)).range(1, 100) + ).tab("Target")); + if (!isTargetingModeLocked()) { + TargetingMode[] allowed = getAllowedTargetingModes(); + if (allowed != null && allowed.length > 0) { + String[] allowedKeys = new String[allowed.length]; + for (int i = 0; i < allowed.length; i++) { + allowedKeys[i] = allowed[i].name(); + } + defs.add(FieldDef.stringEnumField("ability.targetingMode", allowedKeys, + () -> this.getTargetingMode().name(), + v -> { + try { + this.setTargetingMode(TargetingMode.valueOf(v)); + } catch (Exception ignored) { + } + }) + .tab("Target").hover("ability.hover.targeting")); + } else { + defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class, + this::getTargetingMode, this::setTargetingMode) + .tab("Target").hover("ability.hover.targeting")); + } + } + + // ── Effects tab ────────────────────────────────────────────── + defs.add(FieldDef.section("ability.section.sounds").tab("Effects")); + defs.add(FieldDef.soundSubGui("ability.windUpSound", this::getWindUpSound, this::setWindUpSound) + .tab("Effects")); + defs.add(FieldDef.soundSubGui("ability.activeSound", this::getActiveSound, this::setActiveSound) + .tab("Effects")); + defs.add(FieldDef.section("ability.section.animations").tab("Effects")); + defs.add(FieldDef.animSubGui("ability.windUpAnimation", + this::getWindUpAnimationId, this::setWindUpAnimationId, + this::getWindUpAnimationName, this::setWindUpAnimationName) + .tab("Effects")); + defs.add(FieldDef.animSubGui("ability.activeAnimation", + this::getActiveAnimationId, this::setActiveAnimationId, + this::getActiveAnimationName, this::setActiveAnimationName) + .tab("Effects")); + defs.add(FieldDef.animSubGui("ability.dazedAnimation", + this::getDazedAnimationId, this::setDazedAnimationId, + this::getDazedAnimationName, this::setDazedAnimationName) + .tab("Effects")); + + TelegraphType tType = getTelegraphType(); + if (tType != null && tType != TelegraphType.NONE && !hasOwnTelegraphControls()) { + defs.add(FieldDef.section("ability.section.telegraph").tab("Effects") + .hover("telegraph." + tType.name().toLowerCase())); + defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph) + .tab("Effects").hover("ability.hover.showTelegraph")); + defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor) + .tab("Effects").visibleWhen(this::isShowTelegraph)); + defs.add(FieldDef.colorSubGui("ability.activeColor", this::getActiveColor, this::setActiveColor) + .tab("Effects").visibleWhen(this::isShowTelegraph)); + } + + // ── Type-specific fields ───────────────────────────────────── + int baseSize = defs.size(); + getAbilityDefinitions(defs); + // Default tab for ability-added fields that don't specify one + for (int i = baseSize; i < defs.size(); i++) { + if (defs.get(i).getTab() == null) { + defs.get(i).tab("Type"); + } + } + + // ── Icon section (Effects tab) ─────────────────────────── + if (!isNpcInlineEdit()) { + AbilityIconData icon = AbilityIconData.fromAbility(this); + java.util.function.BooleanSupplier isCustom = icon::isEnabled; + + defs.add(FieldDef.section("ability.section.icon").tab("Effects")); + defs.add(FieldDef.boolField("ability.hasIcon", icon::isEnabled, icon::setEnabled) + .tab("Effects")); + defs.add(FieldDef.row( + FieldDef.intField("gui.width", icon::getWidth, icon::setWidth).range(1, 256), + FieldDef.intField("gui.height", icon::getHeight, icon::setHeight).range(1, 256) + ).tab("Effects").visibleWhen(isCustom)); + defs.add(FieldDef.floatField("gui.scale", icon::getScale, icon::setScale) + .tab("Effects").range(0.1f, 10.0f).visibleWhen(isCustom)); + defs.add(FieldDef.intField("ability.icon.layers", icon::getLayerCount, icon::setLayerCount) + .tab("Effects").range(1, AbilityIconData.MAX_LAYERS).visibleWhen(isCustom)); + + addIconLayerFields(defs, icon, 0, isCustom); + addIconLayerFields(defs, icon, 1, () -> isCustom.getAsBoolean() && icon.getLayerCount() >= 2); + addIconLayerFields(defs, icon, 2, () -> isCustom.getAsBoolean() && icon.getLayerCount() >= 3); + + // Animation + defs.add(FieldDef.section("Animation").tab("Effects").visibleWhen(isCustom)); + defs.add(FieldDef.boolField("gui.animated", icon::isAnimated, icon::setAnimated) + .tab("Effects").visibleWhen(isCustom).hover("gui.animated.hover")); + defs.add(FieldDef.intField("gui.frameCount", icon::getFrameCount, icon::setFrameCount) + .tab("Effects").range(1, 256).visibleWhen(() -> isCustom.getAsBoolean() && icon.isAnimated())); + defs.add(FieldDef.intField("gui.frameTime", icon::getFrameTime, icon::setFrameTime) + .tab("Effects").range(1, 100).visibleWhen(() -> isCustom.getAsBoolean() && icon.isAnimated())); + } + + // External field providers (e.g., DBC Addon injecting a "DBC" tab) + for (IAbilityFieldProvider provider : AbilityController.Instance.getFieldProviders()) { + provider.addFieldDefinitions(this, defs); + } + + return defs; + } + + private void addIconLayerFields(List defs, AbilityIconData icon, int layerIndex, + java.util.function.BooleanSupplier visible) { + String sectionLabel = PlatformServiceHolder.get().translateToLocal("ability.icon.layer") + " " + (layerIndex + 1); + defs.add(FieldDef.section(sectionLabel).tab("Effects").visibleWhen(visible)); + defs.add(FieldDef.textureSubGui("gui.texture", + () -> icon.getLayer(layerIndex).texture, + t -> icon.setLayerTexture(layerIndex, t)) + .tab("Effects").visibleWhen(visible)); + defs.add(FieldDef.row( + FieldDef.intField("ability.icon.x", + () -> icon.getLayer(layerIndex).iconX, + x -> icon.setLayerIconX(layerIndex, x)).range(0, 4096), + FieldDef.intField("ability.icon.y", + () -> icon.getLayer(layerIndex).iconY, + y -> icon.setLayerIconY(layerIndex, y)).range(0, 4096) + ).tab("Effects").visibleWhen(visible)); + defs.add(FieldDef.colorSubGui("ability.icon.tint", + () -> icon.getLayer(layerIndex).tintColor, + c -> icon.setLayerTintColor(layerIndex, c)) + .tab("Effects").visibleWhen(visible)); + } + + /** + * Creates the GUI for configuring this ability. + * Override this method to provide a custom GUI subclass for type-specific settings. + *

+ * Third-party mods can extend SubGuiAbilityConfig and override initTypeTab(), + * handleTypeButton(), and handleTypeTextField() to provide custom type settings. + * + * @param callback The callback to notify when the ability is saved + * @return A SubGuiAbilityConfig instance (or subclass) for configuring this ability + */ + @ClientOnly + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityConfig(this, callback); + } + + /** + * Create a telegraph instance for this ability. + * Override for custom telegraph shapes. + * + * @param caster The caster + * @param target The target (for position calculation) + * @return The telegraph instance, or null if no telegraph + */ + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE) { + return null; + } + + Telegraph telegraph; + double x, y, z; + float yaw = caster.rotationYaw; + + // Determine position based on targeting mode + boolean positionAtCaster = targetingMode == TargetingMode.AOE_SELF || + targetingMode == TargetingMode.SELF || + telegraphType == TelegraphType.LINE || + telegraphType == TelegraphType.CONE; + + if (positionAtCaster) { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; + } else if (target != null) { + x = target.posX; + y = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ); + z = target.posZ; + } else { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; + } + + // Create telegraph based on type + switch (telegraphType) { + case CIRCLE: + telegraph = Telegraph.circle(getTelegraphRadius()); + break; + case RING: + telegraph = Telegraph.ring(getTelegraphRadius(), getTelegraphInnerRadius()); + break; + case LINE: + telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth()); + break; + case CONE: + telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle(), getTelegraphInnerRadius()); + break; + case POINT: + telegraph = new Telegraph("", TelegraphType.POINT); + break; + case SQUARE: + telegraph = Telegraph.square(getTelegraphRadius()); + break; + default: + return null; + } + + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setCasterEntityId(caster.getEntityId()); + + // Set IEntity to follow based on targeting mode + if (positionAtCaster) { + // AOE_SELF abilities: telegraph follows caster during windup + instance.setEntityIdToFollow(caster.getEntityId()); + + // For LINE/CONE telegraphs: track direction during windup + // Only track if rotation is NOT locked during windup (NPC can still turn) + if (telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) { + if (!isRotationLockedDuringWindup()) { + if (target != null) { + // Face the target IEntity + instance.setTargetEntityId(target.getEntityId()); + } else { + // No target (AOE_SELF etc.): track caster's own rotationYaw + instance.setTrackFollowedEntityYaw(true); + } + } + // If rotation is locked, yaw stays fixed at creation time + } + } else if (target != null) { + // AOE_TARGET abilities: telegraph follows target during windup + instance.setEntityIdToFollow(target.getEntityId()); + } + + return instance; + } + + /** + * Create all telegraph instances for this ability. + * Override for abilities that need multiple telegraphs (e.g., zone abilities). + * Default wraps createTelegraph() in a singleton list. + */ + public List createTelegraphs(IEntityLivingBase caster, IEntityLivingBase target) { + TelegraphInstance instance = createTelegraph(caster, target); + if (instance == null) return new ArrayList<>(); + return new ArrayList<>(Collections.singletonList(instance)); + } + + /** + * Find the ground level at a given position. + * Searches downward from the given Y to find a solid block. + * Uses default search range of 3 blocks — if no ground is found, + * returns slightly below the IEntity for airborne telegraph placement. + * + * @param IWorld The IWorld + * @param x X coordinate + * @param startY Starting Y coordinate (IEntity feet position) + * @param z Z coordinate + * @return The Y coordinate of the ground surface, or startY - 0.5 if airborne + */ + public static double findGroundLevel(IWorld IWorld, double x, double startY, double z) { + return findGroundLevel(IWorld, x, startY, z, 3); + } + + /** + * Find the ground level at a given position with configurable search range. + * Searches downward from the given Y to find a solid block. + * + * @param IWorld The IWorld + * @param x X coordinate + * @param startY Starting Y coordinate (IEntity feet position) + * @param z Z coordinate + * @param maxSearchDown Maximum blocks to search downward + * @return The Y coordinate of the ground surface, or startY - 0.5 if no ground found + */ + public static double findGroundLevel(IWorld IWorld, double x, double startY, double z, int maxSearchDown) { + if (IWorld == null) return startY - 0.5; + + int blockX = (int) Math.floor(x); + int blockZ = (int) Math.floor(z); + int startBlockY = (int) Math.floor(startY); + + for (int checkY = startBlockY; checkY >= startBlockY - maxSearchDown && checkY >= 0; checkY--) { + Block block = IWorld.getBlock(blockX, checkY, blockZ); + if (block != null && block.getMaterial().isSolid()) { + return checkY + 1; + } + } + + // No ground found within range — place slightly under IEntity + return startY - 0.5; + } + + /** + * Get the current telegraph instances. + */ + public List getTelegraphInstances() { + return telegraphInstances; + } + + /** + * Set the telegraph instances (called by DataAbilities). + */ + public void setTelegraphInstances(List instances) { + this.telegraphInstances = instances != null ? instances : new ArrayList<>(); + } + + // ═══════════════════════════════════════════════════════════════════ + // EXECUTION LOGIC (called by CombatHandler) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Start executing this ability. + * If windUpTicks is 0, skips WINDUP and goes directly to ACTIVE. + */ + public void start(IEntityLivingBase target) { + this.currentTick = 0; + this.currentTarget = target; + this.executionStartTime = System.currentTimeMillis(); + this.burstIndex = 0; + this.burstEntities.clear(); + + if (windUpTicks <= 0) { + this.phase = AbilityPhase.ACTIVE; + } else { + this.phase = AbilityPhase.WINDUP; + } + } + + /** + * Tick the ability forward. Called every game tick while executing. + *

+ * Normal flow: WINDUP -> ACTIVE -> IDLE + * Interrupted flow: WINDUP -> DAZED -> IDLE (dazed only on interrupt during WINDUP) + * + * @return true if phase changed this tick + */ + public boolean tick() { + if (phase == AbilityPhase.IDLE) return false; + + currentTick++; + + switch (phase) { + case WINDUP: + if (currentTick >= windUpTicks) { + phase = AbilityPhase.ACTIVE; + currentTick = 0; + return true; + } + break; + case ACTIVE: + // Abilities control their own completion by calling signalCompletion() + // No auto-complete - each ability determines when it's done + break; + case DAZED: + // Dazed phase is only entered on interrupt during WINDUP + // NPC cannot attack during this time + if (currentTick >= dazedTicks) { + phase = AbilityPhase.IDLE; + currentTick = 0; + return true; + } + break; + + case BURST_DELAY: + // Waiting between burst repetitions - free movement/rotation + if (currentTick >= burstDelay) { + if (burstReplayAnimations && getWindUpTicks() > 0) { + phase = AbilityPhase.WINDUP; + } else { + phase = AbilityPhase.ACTIVE; + } + currentTick = 0; + return true; + } + break; + } + return false; + } + + /** + * Signal that the ability has completed its active phase. + * Called by abilities when they are done. + * For example, AbilitySlam calls this when the NPC lands. + * + * @return true if phase changed to IDLE + */ + public boolean signalCompletion() { + if (phase == AbilityPhase.ACTIVE) { + // Check if more burst iterations remain + if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) { + // Reset transient state for next burst iteration (e.g., movement direction, hit lists) + // Does NOT kill entities - they may still be in flight (overlap mode) + resetForBurst(); + burstIndex++; + phase = AbilityPhase.BURST_DELAY; + currentTick = 0; + return true; + } + + // Final completion - let burst entities die naturally (don't force-kill) + burstEntities.clear(); + if (freeOnCast) { + // Free on Cast: detach entities (let them live independently) instead of killing + detach(); + } else { + cleanup(); + } + phase = AbilityPhase.IDLE; + currentTick = 0; + return true; + } + return false; + } + + /** + * Interrupt this ability. Transitions to DAZED phase if interruptible during WINDUP. + * Called when NPC takes direct damage during WINDUP. + */ + public void interrupt() { + cleanupBurstEntities(); + cleanup(); + if (interruptible && phase == AbilityPhase.WINDUP) { + // Interrupted during windup - go to dazed state + phase = AbilityPhase.DAZED; + currentTick = 0; + } else { + // Force stop - go directly to IDLE + phase = AbilityPhase.IDLE; + currentTick = 0; + } + burstIndex = 0; + currentTarget = null; + telegraphInstances.clear(); + } + + /** + * Cancel this ability (voluntary player action). Goes directly to IDLE, skipping DAZED. + * Entities are killed via cleanup(). Unlike interrupt(), this never enters DAZED phase. + */ + public void cancel() { + cleanupBurstEntities(); + cleanup(); + phase = AbilityPhase.IDLE; + currentTick = 0; + burstIndex = 0; + currentTarget = null; + telegraphInstances.clear(); + } + + /** + * Check if executing + */ + public boolean isExecuting() { + return phase != AbilityPhase.IDLE; + } + + /** + * Check if can be interrupted by this damage source. + * Only direct physical hits (not magic, fire, or other indirect damage) can interrupt. + * Interruption only occurs during WINDUP phase (including burst replay windups). + * BURST_DELAY is not interruptible - only the windup animation itself can be interrupted. + * For chained abilities, windUpAll controls whether subsequent entries even have a windup. + * + * @param source The damage source + * @return true if the ability should be interrupted + */ + public boolean canInterrupt(IDamageSource source) { + if (!interruptible || phase != AbilityPhase.WINDUP) { + return false; + } + if (isInvulnerableForCurrentPhase()) { + return false; + } + + // Only direct physical hits can interrupt, not magic, fire, or other indirect damage + if (source == null) { + return false; + } + + // Reject indirect damage types + if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) { + return false; + } + + // Reject damage without a direct attacker IEntity + if (source.getEntity() == null) { + return false; + } + + // Direct hit from an IEntity - can interrupt + return true; + } + + /** + * Reset all execution state (called on combat end, NPC death, target lost, etc.) + */ + public void reset() { + cleanupBurstEntities(); + cleanup(); + phase = AbilityPhase.IDLE; + currentTick = 0; + burstIndex = 0; + currentTarget = null; + telegraphInstances.clear(); + previewMode = false; + previewEntityHandler = null; + instanceScript = null; + } + + /** + * Cleanup any resources created by this ability. + * Override this to kill spawned entities, cancel telegraphs, etc. + * Called when ability ends for any reason (completion, interrupt, reset, NPC death). + */ + public void cleanup() { + // Override in subclasses to clean up spawned entities, etc. + } + + /** + * Kill all entities tracked during burst overlap and clear the list. + */ + protected void cleanupBurstEntities() { + for (IEntity e : burstEntities) { + if (e != null && !e.isDead) { + killAbilityEntity(e); + } + } + burstEntities.clear(); + } + + // ═══════════════════════════════════════════════════════════════════ + // PREVIEW MODE SUPPORT + // ═══════════════════════════════════════════════════════════════════ + + /** + * Whether this ability is running in preview mode (GUI preview). + * When true, abilities should skip damage, effects, sounds, and particles + * but still run their core logic (IEntity spawning, movement, timing). + */ + public boolean isPreview() { + return previewMode; + } + + public void setPreviewMode(boolean preview) { + this.previewMode = preview; + } + + public void setPreviewEntityHandler(PreviewEntityHandler handler) { + this.previewEntityHandler = handler; + } + + /** + * Spawn an IEntity during ability execution. + * In preview mode, routes to PreviewEntityHandler instead of IWorld.spawnEntityInWorld(). + */ + protected void spawnAbilityEntity(IEntity IEntity) { + if (previewMode && previewEntityHandler != null) { + previewEntityHandler.onEntitySpawned(IEntity); + } else { + IEntity.worldObj.spawnEntityInWorld(IEntity); + if (!IEntity.worldObj.isRemote && IEntity instanceof EntityEnergyProjectile) { + EventHooks.onEnergyProjectileFired((EntityEnergyProjectile) IEntity); + } + } + if (burstEnabled && burstOverlap) { + burstEntities.add(IEntity); + } + } + + /** + * Kill an IEntity spawned by this ability. + * In preview mode, notifies PreviewEntityHandler of removal. + */ + protected void killAbilityEntity(IEntity IEntity) { + if (IEntity == null) return; + IEntity.setDead(); + if (previewMode && previewEntityHandler != null) { + previewEntityHandler.onEntityRemoved(IEntity); + } + } + + /** + * Maximum duration (in ticks) for preview before auto-stopping. + * Override in movement abilities that have variable duration. + * Default: 200 ticks (10 seconds). + */ + public int getMaxPreviewDuration() { + return 200; + } + + // ═══════════════════════════════════════════════════════════════════ + // CONDITION CHECKING + // ═══════════════════════════════════════════════════════════════════ + + public boolean checkConditions(IEntityLivingBase caster, IEntityLivingBase target) { + for (AbilityCondition c : conditions) { + if (!c.getUserType().allowsNpc()) continue; + Boolean extendedCondition = AbilityController.Instance.fireCheckConditions(c, caster, target); + + if (extendedCondition != null) { + if (!extendedCondition) return false; + continue; + } + + if (!c.check(caster, target)) return false; + } + return true; + } + + /** + * Check conditions for a player caster, skipping target-requiring conditions. + */ + public boolean checkConditionsForPlayer(IEntityLivingBase caster) { + for (AbilityCondition c : conditions) { + if (!c.getUserType().allowsPlayer()) continue; + if (c.requiresTarget()) continue; + Boolean extendedCondition = AbilityController.Instance.fireCheckConditionsForPlayer(c, caster); + + if (extendedCondition != null) { + if (!extendedCondition) return false; + continue; + } + + if (!c.check(caster, null)) return false; + } + return true; + } + + + // ═══════════════════════════════════════════════════════════════════ + // NBT (config only, execution state is transient) + // ═══════════════════════════════════════════════════════════════════ + + public INbt writeNBT(boolean saveScripts) { + INbt nbt = new INbt(); + nbt.setString("id", id); + nbt.setString("name", name); + nbt.setString("displayName", displayName); + nbt.setString("typeId", typeId); + nbt.setInteger("weight", weight); + nbt.setBoolean("enabled", enabled); + nbt.setString("targetingMode", targetingMode.name()); + nbt.setFloat("minRange", minRange); + nbt.setFloat("maxRange", maxRange); + nbt.setInteger("cooldown", cooldownTicks); + nbt.setInteger("windUp", windUpTicks); + nbt.setBoolean("syncWindup", syncWindupWithAnimation); + nbt.setInteger("recovery", dazedTicks); + nbt.setBoolean("interruptible", interruptible); + nbt.setInteger("invulnerableMode", invulnerableMode.ordinal()); + nbt.setBoolean("ignoreIFrames", ignoreIFrames); + nbt.setInteger("lockMovement", lockMovement.ordinal()); + nbt.setInteger("rotationMode", rotationMode.ordinal()); + nbt.setInteger("rotationPhase", rotationPhase.ordinal()); + nbt.setFloat("trackSpeed", trackSpeed); + nbt.setInteger("windUpColor", windUpColor); + nbt.setInteger("activeColor", activeColor); + nbt.setString("windUpSound", windUpSound); + nbt.setString("activeSound", activeSound); + nbt.setInteger("windUpAnimationId", windUpAnimationId); + nbt.setInteger("activeAnimationId", activeAnimationId); + nbt.setInteger("dazedAnimationId", dazedAnimationId); + // Resolve current animation names from controller before writing + nbt.setString("windUpAnimationName", resolveAnimationName(windUpAnimationId, windUpAnimationName)); + nbt.setString("activeAnimationName", resolveAnimationName(activeAnimationId, activeAnimationName)); + nbt.setString("dazedAnimationName", resolveAnimationName(dazedAnimationId, dazedAnimationName)); + nbt.setBoolean("showTelegraph", showTelegraph); + nbt.setString("telegraphType", telegraphType.name()); + nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); + nbt.setTag("customData", (INbt) customData.copy()); + TagController.writeTagUUIDs(nbt, "TagUUIDs", tagUUIDs); + magicData.writeToNBT(new NBTWrapper(nbt)); + nbt.setInteger("allowedBy", allowedBy.ordinal()); + nbt.setBoolean("ignoreCooldown", ignoreCooldown); + nbt.setBoolean("perAbilityCooldown", perAbilityCooldown); + nbt.setBoolean("freeOnCast", freeOnCast); + + // Toggle + nbt.setInteger("toggleStates", toggleStates); + nbt.setBoolean("hasActiveToggle", hasActiveToggle); + if (toggleStateLabels != null && toggleStateLabels.length > 0) { + INbtList labelList = new INbtList(); + for (String label : toggleStateLabels) { + labelList.appendTag(new NBTTagString(label != null ? label : "")); + } + nbt.setTag("toggleStateLabels", labelList); + } + + // Burst + nbt.setBoolean("burstEnabled", burstEnabled); + nbt.setInteger("burstAmount", burstAmount); + nbt.setInteger("burstDelay", burstDelay); + nbt.setBoolean("burstReplayAnimations", burstReplayAnimations); + nbt.setBoolean("burstOverlap", burstOverlap); + + // Conditions + INbtList condList = new INbtList(); + for (AbilityCondition c : conditions) condList.appendTag(c.writeNBT()); + nbt.setTag("conditions", condList); + + // Effects + INbtList effectList = new INbtList(); + for (AbilityPotionEffect effect : effects) { + effectList.appendTag(effect.writeNBT()); + } + nbt.setTag("effects", effectList); + + // Type-specific + INbt typeNBT = new INbt(); + writeTypeNBT(typeNBT); + nbt.setTag("typeData", typeNBT); + + // Script handler data (matching CustomEffect.writeToNBT pattern) + if (saveScripts) { + AbilityScript handler = getScriptHandler(); + if (handler != null) { + INbt scriptData = new INbt(); + handler.writeToNBT(scriptData); + nbt.setTag("ScriptData", scriptData); + } + } + + return nbt; + } + + public void readNBT(INbt nbt) { + id = nbt.getString("id"); + name = nbt.getString("name"); + displayName = nbt.getString("displayName"); + typeId = nbt.getString("typeId"); + weight = nbt.hasKey("weight") ? nbt.getInteger("weight") : 10; + enabled = nbt.hasKey("enabled") ? nbt.getBoolean("enabled") : true; + try { + targetingMode = TargetingMode.valueOf(nbt.getString("targetingMode")); + } catch (Exception e) { + targetingMode = TargetingMode.AGGRO_TARGET; + } + minRange = nbt.getFloat("minRange"); + maxRange = nbt.getFloat("maxRange"); + cooldownTicks = nbt.getInteger("cooldown"); + windUpTicks = nbt.getInteger("windUp"); + syncWindupWithAnimation = nbt.hasKey("syncWindup") ? nbt.getBoolean("syncWindup") : true; + dazedTicks = nbt.hasKey("recovery") ? nbt.getInteger("recovery") : 80; + interruptible = nbt.hasKey("interruptible") ? nbt.getBoolean("interruptible") : true; + invulnerableMode = nbt.hasKey("invulnerableMode") + ? InvulnerableMode.fromOrdinal(nbt.getInteger("invulnerableMode")) + : InvulnerableMode.NONE; + ignoreIFrames = nbt.hasKey("ignoreIFrames") && nbt.getBoolean("ignoreIFrames"); + lockMovement = LockMode.fromOrdinal(nbt.getInteger("lockMovement")); + rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); + rotationPhase = LockMode.fromOrdinal(nbt.getInteger("rotationPhase")); + trackSpeed = nbt.hasKey("trackSpeed") ? nbt.getFloat("trackSpeed") : 0; + windUpColor = nbt.hasKey("windUpColor") ? nbt.getInteger("windUpColor") : 0x80FF4400; + activeColor = nbt.hasKey("activeColor") ? nbt.getInteger("activeColor") : 0xC0FF0000; + windUpSound = nbt.getString("windUpSound"); + activeSound = nbt.getString("activeSound"); + windUpAnimationId = nbt.getInteger("windUpAnimationId"); + activeAnimationId = nbt.getInteger("activeAnimationId"); + dazedAnimationId = nbt.hasKey("dazedAnimationId") ? nbt.getInteger("dazedAnimationId") : -1; + windUpAnimationName = nbt.getString("windUpAnimationName"); + activeAnimationName = nbt.getString("activeAnimationName"); + dazedAnimationName = nbt.hasKey("dazedAnimationName") ? nbt.getString("dazedAnimationName") : "Ability_Generic_Dazed"; + showTelegraph = nbt.hasKey("showTelegraph") ? nbt.getBoolean("showTelegraph") : true; + try { + telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType")); + } catch (Exception e) { + telegraphType = TelegraphType.CIRCLE; + } + telegraphHeightOffset = nbt.getFloat("telegraphHeightOffset"); + customData = (INbt) nbt.getCompoundTag("customData").copy(); + tagUUIDs = TagController.readTagUUIDs(nbt, "TagUUIDs"); + magicData.readToNBT(new NBTWrapper(nbt)); + allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy")); + ignoreCooldown = nbt.getBoolean("ignoreCooldown"); + perAbilityCooldown = nbt.getBoolean("perAbilityCooldown"); + freeOnCast = nbt.getBoolean("freeOnCast"); + + // Toggle + toggleStates = nbt.getInteger("toggleStates"); + hasActiveToggle = nbt.getBoolean("hasActiveToggle"); + if (nbt.hasKey("toggleStateLabels")) { + INbtList labelList = nbt.getTagList("toggleStateLabels", 8); + toggleStateLabels = new String[labelList.tagCount()]; + for (int i = 0; i < labelList.tagCount(); i++) { + String label = labelList.getStringTagAt(i); + toggleStateLabels[i] = (label != null && !label.isEmpty()) ? label : null; + } + } else { + toggleStateLabels = null; + } + + // Burst + burstEnabled = nbt.getBoolean("burstEnabled"); + burstAmount = nbt.getInteger("burstAmount"); + burstDelay = nbt.getInteger("burstDelay"); + burstReplayAnimations = nbt.hasKey("burstReplayAnimations") ? nbt.getBoolean("burstReplayAnimations") : true; + burstOverlap = nbt.getBoolean("burstOverlap"); + + // Conditions + conditions.clear(); + INbtList condList = nbt.getTagList("conditions", 10); + for (int i = 0; i < condList.tagCount(); i++) { + AbilityCondition c = AbilityCondition.fromNBT(condList.getCompoundTagAt(i)); + if (c != null) conditions.add(c); + } + + // Effects + effects.clear(); + if (nbt.hasKey("effects")) { + INbtList effectList = nbt.getTagList("effects", 10); + for (int i = 0; i < effectList.tagCount(); i++) { + AbilityPotionEffect effect = AbilityPotionEffect.fromNBT(effectList.getCompoundTagAt(i)); + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + } + + // Type-specific + if (nbt.hasKey("typeData")) { + readTypeNBT(nbt.getCompoundTag("typeData")); + } + + // Script handler data (matching CustomEffect.readFromNBT pattern) + // Only create a new handler if one doesn't already exist in the global map, + // to avoid clobbering a live handler's script state during deepCopy/resolve. + if (nbt.hasKey("ScriptData", 10)) { + if (getScriptHandler() == null) { + AbilityScript handler = new AbilityScript(this.id); + handler.readFromNBT(nbt.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS & SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + if (builtIn) { + this.name = name != null ? name : ""; + return; + } + String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Ability"; + this.name = FileNameHelper.sanitizeName(name, fallback); + } + + /** + * Get the display name for this ability. + * Returns displayName if set, otherwise falls back to name. + * Converts & color codes to § for rendering. + */ + public String getDisplayName() { + String result = (displayName != null && !displayName.isEmpty()) + ? displayName + : FileNameHelper.toDisplayName(name); + return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : ""; + } + + /** + * Get the raw display name (may be empty). + */ + public String getRawDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName != null ? displayName : ""; + } + + public String getTypeId() { + return typeId; + } + + public boolean isBuiltIn() { + return builtIn; + } + + /** + * Get the registry key for built-in abilities. + * Returns null for custom abilities. + */ + public String getRegistryKey() { + return registryKey; + } + + /** + * Configure this ability as a built-in ability with fixed config. + * Sets builtIn flag, assigns registry key, id, name, and derives typeId. + * Call this in the constructor of any ability that should be non-customizable. + */ + protected void configureAsBuiltIn(String registryKey) { + this.builtIn = true; + this.registryKey = registryKey; + this.id = registryKey; + this.name = registryKey; + this.typeId = "ability." + registryKey.replace(':', '.'); + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFAULT ICON + // ═══════════════════════════════════════════════════════════════════ + + public DefaultIconLayer[] getDefaultIconLayers() { + return defaultIconLayers; + } + + public int getDefaultIconWidth() { + return defaultIconWidth; + } + + public int getDefaultIconHeight() { + return defaultIconHeight; + } + + public boolean hasDefaultIcon() { + return defaultIconLayers != null && defaultIconLayers.length > 0 + && defaultIconLayers[0].texture != null && !defaultIconLayers[0].texture.isEmpty(); + } + + public Ability deepCopy() { + Ability copy = AbilityController.Instance.fromNBT(this.writeNBT(false)); + if (copy != null) + copy.playerRequirement = this.playerRequirement; + return copy; + } + + @Override + public boolean isChain() { + return false; + } + + @Override + public IAbilityAction deepCopyAction() { + return deepCopy(); + } + + public boolean isNpcInlineEdit() { + return npcInlineEdit; + } + + public void setNpcInlineEdit(boolean npcInlineEdit) { + this.npcInlineEdit = npcInlineEdit; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public TargetingMode getTargetingMode() { + return targetingMode; + } + + public void setTargetingMode(TargetingMode targetingMode) { + this.targetingMode = targetingMode; + } + + public float getMinRange() { + return minRange; + } + + public void setMinRange(float minRange) { + this.minRange = minRange; + } + + public float getMaxRange() { + return maxRange; + } + + public void setMaxRange(float maxRange) { + this.maxRange = maxRange; + } + + public int getCooldownTicks() { + return cooldownTicks; + } + + public void setCooldownTicks(int cooldownTicks) { + this.cooldownTicks = Math.max(0, cooldownTicks); + } + + public int getWindUpTicks() { + return calculateWindupFromAnimation(); + } + + public int getRawWindUpTicks() { + return windUpTicks; + } + + public void setWindUpTicks(int windUpTicks) { + this.windUpTicks = Math.max(0, windUpTicks); + } + + public boolean isSyncWindupWithAnimation() { + return syncWindupWithAnimation; + } + + public boolean hasWindUpAnimation() { + return (windUpAnimationName != null && !windUpAnimationName.isEmpty()) || windUpAnimationId >= 0; + } + + public void setSyncWindupWithAnimation(boolean syncWindupWithAnimation) { + this.syncWindupWithAnimation = syncWindupWithAnimation; + } + + public int getDazedTicks() { + return dazedTicks; + } + + public void setDazedTicks(int dazedTicks) { + this.dazedTicks = Math.max(0, dazedTicks); + } + + public boolean isInterruptible() { + return interruptible; + } + + public void setInterruptible(boolean interruptible) { + this.interruptible = interruptible; + } + + public InvulnerableMode getInvulnerableMode() { + return invulnerableMode; + } + + public void setInvulnerableMode(InvulnerableMode invulnerableMode) { + this.invulnerableMode = invulnerableMode != null ? invulnerableMode : InvulnerableMode.NONE; + } + + public boolean isInvulnerableDuringWindup() { + return invulnerableMode.invulnerableDuringWindup(); + } + + public boolean isInvulnerableDuringActive() { + return invulnerableMode.invulnerableDuringActive(); + } + + @Override + public boolean isIgnoreIFrames() { + return ignoreIFrames; + } + + @Override + public void setIgnoreIFrames(boolean ignore) { + this.ignoreIFrames = ignore; + } + + public LockMode getLockMovement() { + return lockMovement; + } + + public void setLockMovement(LockMode lockMovement) { + this.lockMovement = lockMovement; + } + + public RotationMode getRotationMode() { + return rotationMode; + } + + public void setRotationMode(RotationMode rotationMode) { + this.rotationMode = rotationMode; + } + + public LockMode getRotationPhase() { + return rotationPhase; + } + + public void setRotationPhase(LockMode rotationPhase) { + this.rotationPhase = rotationPhase; + } + + public float getTrackSpeed() { + return trackSpeed; + } + + public void setTrackSpeed(float trackSpeed) { + this.trackSpeed = trackSpeed; + } + + + /** + * API method: Get lock movement type as integer. + * + * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public int getLockMovementType() { + return lockMovement.ordinal(); + } + + /** + * API method: Set lock movement type from integer. + * + * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public void setLockMovementType(int type) { + this.lockMovement = LockMode.fromOrdinal(type); + } + + /** + * API method: Get rotation mode as integer. + * + * @return 0=FREE, 1=LOCKED, 2=TRACK + */ + @Override + public int getRotationModeType() { + return rotationMode.ordinal(); + } + + /** + * API method: Set rotation mode from integer. + * + * @param type 0=FREE, 1=LOCKED, 2=TRACK + */ + @Override + public void setRotationModeType(int type) { + this.rotationMode = RotationMode.fromOrdinal(type); + } + + /** + * API method: Get rotation phase as integer. + * + * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public int getRotationPhaseType() { + return rotationPhase.ordinal(); + } + + /** + * API method: Set rotation phase from integer. + * + * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public void setRotationPhaseType(int type) { + this.rotationPhase = LockMode.fromOrdinal(type); + } + + /** + * API method: Get track speed for TRACK rotation mode (blocks/second). + * 0 = instant tracking. Comparable to player sprint speed (~5.6 blocks/sec). + */ + @Override + public float getTrackSpeedValue() { + return trackSpeed; + } + + /** + * API method: Set track speed for TRACK rotation mode (blocks/second). + * + * @param speed 0 = instant tracking. ~4 = dodgeable while sprinting. ~6 = barely undodgeable. + */ + @Override + public void setTrackSpeedValue(float speed) { + this.trackSpeed = Math.max(0, speed); + } + + /** + * Check if movement (pathfinding/motion) should be locked during WINDUP phase. + */ + @Override + public boolean isMovementLockedDuringWindup() { + return lockMovement.locksWindup(); + } + + /** + * Check if movement (pathfinding/motion) should be locked during ACTIVE phase. + */ + @Override + public boolean isMovementLockedDuringActive() { + return lockMovement.locksActive(); + } + + /** + * Check if rotation (yaw/pitch) should be locked during WINDUP phase. + */ + @Override + public boolean isRotationLockedDuringWindup() { + return rotationMode == RotationMode.LOCKED && rotationPhase.locksWindup(); + } + + /** + * Check if rotation (yaw/pitch) should be locked during ACTIVE phase. + */ + @Override + public boolean isRotationLockedDuringActive() { + return rotationMode == RotationMode.LOCKED && rotationPhase.locksActive(); + } + + /** + * Check if movement should be locked during the current phase. + */ + public boolean isMovementLockedForCurrentPhase() { + switch (phase) { + case WINDUP: + return isMovementLockedDuringWindup(); + case ACTIVE: + return isMovementLockedDuringActive(); + case BURST_DELAY: + return false; // Free movement during burst delay + default: + return false; + } + } + + /** + * Check if this ability grants invulnerability during its current execution phase. + */ + public boolean isInvulnerableForCurrentPhase() { + return invulnerableMode.isInvulnerableInPhase(phase); + } + + /** + * Check if this ability grants invulnerability for a specific phase. + */ + public boolean isInvulnerableForPhase(AbilityPhase phase) { + return invulnerableMode.isInvulnerableInPhase(phase); + } + + /** + * Check if rotation should be locked during the current phase. + */ + public boolean isRotationLockedForCurrentPhase() { + switch (phase) { + case WINDUP: + return isRotationLockedDuringWindup(); + case ACTIVE: + return isRotationLockedDuringActive(); + case BURST_DELAY: + return false; // Free rotation during burst delay + default: + return false; + } + } + + /** + * Check if hit scan (force face target) is active during the current phase. + */ + public boolean isHitScanForCurrentPhase() { + if (rotationMode != RotationMode.TRACK) return false; + switch (phase) { + case WINDUP: + return rotationPhase.locksWindup(); + case ACTIVE: + return rotationPhase.locksActive(); + case BURST_DELAY: + return false; // Free rotation during burst delay + default: + return false; + } + } + + public int getWindUpColor() { + return windUpColor; + } + + public void setWindUpColor(int windUpColor) { + this.windUpColor = windUpColor; + } + + public int getActiveColor() { + return activeColor; + } + + public void setActiveColor(int activeColor) { + this.activeColor = activeColor; + } + + public String getWindUpSound() { + return windUpSound; + } + + public void setWindUpSound(String windUpSound) { + this.windUpSound = windUpSound; + } + + public String getActiveSound() { + return activeSound; + } + + public void setActiveSound(String activeSound) { + this.activeSound = activeSound; + } + + /** + * Resolves the current animation name for a custom animation ID. + * If the ID refers to a custom animation, looks up the current name from the controller. + * For built-in animations (id == -1), returns the stored name as-is. + */ + protected String resolveAnimationName(int animId, String storedName) { + if (animId >= 0 && AnimationController.Instance != null) { + Animation anim = (Animation) AnimationController.Instance.get(animId); + if (anim != null && anim.name != null && !anim.name.isEmpty()) { + return anim.name; + } + } + return storedName; + } + + public Animation getWindUpAnimation() { + if (AnimationController.Instance == null) return null; + + // Custom animation by ID (most reliable, survives renames) + if (windUpAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(windUpAnimationId); + } + // Built-in animation by name + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(windUpAnimationName, true); + } + return null; + } + + public int getWindUpAnimationId() { + return windUpAnimationId; + } + + public void setWindUpAnimationId(int windUpAnimationId) { + this.windUpAnimationId = windUpAnimationId; + } + + public String getWindUpAnimationName() { + return windUpAnimationName; + } + + public void setWindUpAnimationName(String windUpAnimationName) { + this.windUpAnimationName = windUpAnimationName != null ? windUpAnimationName : ""; + } + + public Animation getActiveAnimation() { + if (AnimationController.Instance == null) return null; + + // Custom animation by ID (most reliable, survives renames) + if (activeAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(activeAnimationId); + } + // Built-in animation by name + if (activeAnimationName != null && !activeAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(activeAnimationName, true); + } + return null; + } + + public int getActiveAnimationId() { + return activeAnimationId; + } + + public void setActiveAnimationId(int activeAnimationId) { + this.activeAnimationId = activeAnimationId; + } + + public String getActiveAnimationName() { + return activeAnimationName; + } + + public void setActiveAnimationName(String activeAnimationName) { + this.activeAnimationName = activeAnimationName != null ? activeAnimationName : ""; + } + + public Animation getDazedAnimation() { + if (AnimationController.Instance == null) return null; + + // Custom animation by ID (most reliable, survives renames) + if (dazedAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(dazedAnimationId); + } + // Built-in animation by name + if (dazedAnimationName != null && !dazedAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(dazedAnimationName, true); + } + return null; + } + + public int getDazedAnimationId() { + return dazedAnimationId; + } + + public void setDazedAnimationId(int dazedAnimationId) { + this.dazedAnimationId = dazedAnimationId; + } + + public String getDazedAnimationName() { + return dazedAnimationName; + } + + public void setDazedAnimationName(String dazedAnimationName) { + this.dazedAnimationName = dazedAnimationName != null ? dazedAnimationName : ""; + } + + public boolean isShowTelegraph() { + return showTelegraph; + } + + public void setShowTelegraph(boolean showTelegraph) { + this.showTelegraph = showTelegraph; + } + + public TelegraphType getTelegraphType() { + return telegraphType; + } + + public void setTelegraphType(TelegraphType telegraphType) { + this.telegraphType = telegraphType; + } + + public float getTelegraphHeightOffset() { + return telegraphHeightOffset; + } + + public void setTelegraphHeightOffset(float telegraphHeightOffset) { + this.telegraphHeightOffset = telegraphHeightOffset; + } + + public AbilityPhase getPhase() { + return phase; + } + + /** + * Get the damage multiplier applied by the ability itself (e.g. Slam height scaling). + * Extenders should apply this after their own damage calculation. + */ + public float getDamageMultiplier() { + return damageMultiplier; + } + + public void setDamageMultiplier(float damageMultiplier) { + this.damageMultiplier = damageMultiplier; + } + + /** + * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=DAZED). + * Required for IAbility interface. + */ + @Override + public int getPhaseInt() { + return phase.ordinal(); + } + + public int getCurrentTick() { + return currentTick; + } + + public IEntityLivingBase getCurrentTarget() { + return currentTarget; + } + + public INbt getCustomData() { + return customData; + } + + public HashSet getTagUUIDs() { + return tagUUIDs; + } + + public void setTagUUIDs(HashSet tagUUIDs) { + this.tagUUIDs = tagUUIDs; + } + + public MagicData getMagicData() { + return magicData; + } + + /** + * Resolve magic data for this ability + caster combination. + * If the ability has its own magic data, use that (override). + * Otherwise, fall back to the caster's magic data (player or NPC). + * + * @return Resolved MagicData, or null if neither has magic + */ + public MagicData resolveMagicData(IEntityLivingBase caster) { + if (!this.magicData.isEmpty()) { + return this.magicData; + } + if (caster instanceof IPlayer) { + PlayerData data = PlayerData.get((IPlayer) caster); + if (data != null && data.magicData != null && !data.magicData.isEmpty()) { + return data.magicData; + } + } else if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.stats != null && npc.stats.magicData != null && !npc.stats.magicData.isEmpty()) { + return npc.stats.magicData; + } + } + return null; + } + + public List getConditions() { + return conditions; + } + + public void addCondition(AbilityCondition c) { + conditions.add(c); + } + + public UserType getAllowedBy() { + return allowedBy; + } + + public void setAllowedBy(UserType allowedBy) { + this.allowedBy = allowedBy; + } + + public Predicate getPlayerRequirement() { + return playerRequirement; + } + + public void setPlayerRequirement(Predicate requirement) { + this.playerRequirement = requirement; + } + + /** + * Check if this ability is available for the given player. + * Returns true if no requirement is set, or if the requirement predicate passes. + */ + public boolean isAvailableFor(IPlayer player) { + return playerRequirement == null || playerRequirement.test(player); + } + + public boolean isIgnoreCooldown() { + return ignoreCooldown; + } + + public void setIgnoreCooldown(boolean ignoreCooldown) { + this.ignoreCooldown = ignoreCooldown; + } + + public boolean isPerAbilityCooldown() { + return perAbilityCooldown; + } + + public void setPerAbilityCooldown(boolean perAbilityCooldown) { + this.perAbilityCooldown = perAbilityCooldown; + } + + public boolean isFreeOnCast() { + return freeOnCast; + } + + public void setFreeOnCast(boolean freeOnCast) { + this.freeOnCast = freeOnCast; + } + + public boolean isToggleable() { + return toggleStates > 0; + } + + public int getToggleStates() { + return toggleStates; + } + + public void setToggleStates(int toggleStates) { + this.toggleStates = Math.max(0, toggleStates); + } + + public boolean hasActiveToggle() { + return hasActiveToggle; + } + + public void setHasActiveToggle(boolean hasActiveToggle) { + this.hasActiveToggle = hasActiveToggle; + } + + /** + * Get the display label for a specific toggle state. + * @param state 1-based state index + * @return the label, or null if none set + */ + public String getToggleStateLabel(int state) { + if (toggleStateLabels != null && state >= 1 && state <= toggleStateLabels.length) { + return toggleStateLabels[state - 1]; + } + return null; + } + + /** + * Set display labels for toggle states (1-indexed). + * @param labels one label per state, null entries allowed + */ + public void setToggleStateLabels(String... labels) { + this.toggleStateLabels = (labels != null && labels.length > 0) ? labels : null; + } + + public List getEffects() { + return effects; + } + + public void setEffects(List effects) { + this.effects = effects != null ? effects : new ArrayList<>(); + } + + public void addEffect(AbilityPotionEffect effect) { + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + + public void clearEffects() { + effects.clear(); + } + + // Burst getters/setters + + public boolean isBurstEnabled() { + return burstEnabled; + } + + public void setBurstEnabled(boolean burstEnabled) { + this.burstEnabled = burstEnabled; + } + + public int getBurstAmount() { + return burstAmount; + } + + public void setBurstAmount(int burstAmount) { + this.burstAmount = Math.max(0, burstAmount); + } + + public int getBurstDelay() { + return burstDelay; + } + + public void setBurstDelay(int burstDelay) { + this.burstDelay = Math.max(0, burstDelay); + } + + public boolean isBurstReplayAnimations() { + return burstReplayAnimations; + } + + public void setBurstReplayAnimations(boolean burstReplayAnimations) { + this.burstReplayAnimations = burstReplayAnimations; + } + + public boolean isBurstOverlap() { + return burstOverlap; + } + + public void setBurstOverlap(boolean burstOverlap) { + this.burstOverlap = burstOverlap; + } + + public int getBurstIndex() { + return burstIndex; + } + + /** + * Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that). + */ + private static String[] getRotationPhaseKeys() { + return new String[]{ + "ability.lockMove.windup", + "ability.lockMove.active", + "ability.lockMove.both" + }; + } + + // ═══════════════════════════════════════════════════════════════════ + // WINDUP DURATION CALCULATION + // ═══════════════════════════════════════════════════════════════════ + + private int calculateWindupFromAnimation() { + if (!syncWindupWithAnimation) { + return windUpTicks; + } + + if (AnimationController.Instance == null) { + return this.windUpTicks; + } + + Animation animation = null; + // Custom animation by ID (most reliable, survives renames) + if (windUpAnimationId >= 0) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + } + // Built-in animation by name + else if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationName, true); + } + + if (animation == null || animation.frames.isEmpty()) { + return this.windUpTicks; + } + + // Calculate total duration by summing all frame durations + int totalDuration = 0; + for (Frame frame : animation.frames) { + totalDuration += frame.getDuration(); + } + + // Return total duration + return totalDuration; + } + + // ═══════════════════════════════════════════════════════════════════ + // MOVEMENT COLLISION UTILITY + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check if a caster's movement in a given direction is blocked by walls/blocks. + * Used by Charge and Dash abilities to detect wall collision. + * + * @param caster The IEntity moving + * @param dirX X component of normalized movement direction + * @param dirZ Z component of normalized movement direction + * @param speed Movement speed (blocks per tick) + * @return true if movement is blocked + */ + protected static boolean isMovementBlocked(IEntityLivingBase caster, double dirX, double dirZ, double speed) { + double nextX = dirX * speed; + double nextZ = dirZ * speed; + IBoundingBox nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ); + double stepThreshold = nextBox.minY + Math.max(caster.stepHeight, 0.5); + + int x1 = (int) Math.floor(nextBox.minX); + int x2 = (int) Math.floor(nextBox.maxX + 1.0); + int y1 = (int) Math.floor(nextBox.minY) - 1; + int y2 = (int) Math.floor(nextBox.maxY + 1.0); + int z1 = (int) Math.floor(nextBox.minZ); + int z2 = (int) Math.floor(nextBox.maxZ + 1.0); + + java.util.ArrayList collisionBoxes = new java.util.ArrayList<>(); + for (int bx = x1; bx < x2; bx++) { + for (int bz = z1; bz < z2; bz++) { + for (int by = y1; by < y2; by++) { + Block block = caster.worldObj.getBlock(bx, by, bz); + if (!block.getMaterial().blocksMovement()) continue; + collisionBoxes.clear(); + block.addCollisionBoxesToList(caster.worldObj, bx, by, bz, nextBox, collisionBoxes, caster); + for (IBoundingBox box : collisionBoxes) { + if (box.maxY > stepThreshold) { + return true; + } + } + } + } + } + return false; + } + + /** + * Check if a caster's movement in a given direction is blocked by a solid enemy barrier. + * Tests a short segment from the caster's current position along the movement direction + * against all active dome and panel barriers. + * + * @return true if a solid enemy barrier blocks the movement + */ + protected static boolean isMovementBlockedByBarrier(IEntityLivingBase caster, double dirX, double dirZ, double speed) { + if (caster.worldObj == null) return false; + + double nextX = caster.posX + dirX * speed; + double nextZ = caster.posZ + dirZ * speed; + double eyeY = caster.posY + caster.getEyeHeight() * 0.5; + + java.util.List barriers = EntityEnergyBarrier.getActiveBarriers(caster.worldObj); + for (EntityEnergyBarrier barrier : barriers) { + if (barrier.isDead || barrier.isCharging()) continue; + if (!barrier.getBarrierData().solid) continue; + + // Skip barriers owned by or allied to the caster + if (barrier.getOwnerEntityId() == caster.getEntityId()) continue; + IEntity barrierOwner = barrier.getOwnerEntity(); + if (barrierOwner instanceof IEntityLivingBase) { + if (AbilityTargetHelper.isAlly(caster, (IEntityLivingBase) barrierOwner)) continue; + } + + if (barrier instanceof EntityEnergyDome) { + EntityEnergyDome dome = (EntityEnergyDome) barrier; + float radius = dome.getDomeRadius(); + + // Check if the movement segment crosses the dome boundary (either direction) + double ocX = caster.posX - dome.posX; + double ocY = eyeY - dome.posY; + double ocZ = caster.posZ - dome.posZ; + + double rdX = nextX - caster.posX; + double rdZ = nextZ - caster.posZ; + double a = rdX * rdX + rdZ * rdZ; + double b = 2.0 * (ocX * rdX + ocZ * rdZ); + double c = ocX * ocX + ocZ * ocZ - (double) radius * radius; + double discriminant = b * b - 4.0 * a * c; + if (discriminant < 0 || a < 1e-10) continue; + double sqrtD = Math.sqrt(discriminant); + double t1 = (-b - sqrtD) / (2.0 * a); + double t2 = (-b + sqrtD) / (2.0 * a); + if ((t1 >= 0.0 && t1 <= 1.0) || (t2 >= 0.0 && t2 <= 1.0)) return true; + } else if (barrier instanceof EntityEnergyPanel) { + EntityEnergyPanel panel = (EntityEnergyPanel) barrier; + float halfW = panel.getPanelData().getPanelWidth() * 0.5f; + float halfH = panel.getPanelData().getPanelHeight() * 0.5f; + float yawRad = (float) Math.toRadians(panel.getPanelYaw()); + double normalX = -Math.sin(yawRad); + double normalZ = Math.cos(yawRad); + double cos = Math.cos(yawRad); + double sin = Math.sin(yawRad); + + double relX = caster.posX - panel.posX; + double relZ = caster.posZ - panel.posZ; + double prevDist = relX * normalX + relZ * normalZ; + + double relNX = nextX - panel.posX; + double relNZ = nextZ - panel.posZ; + double currDist = relNX * normalX + relNZ * normalZ; + + // Check if segment crosses the panel plane + if (prevDist * currDist > 0) continue; + + double rayDirX = nextX - caster.posX; + double rayDirZ = nextZ - caster.posZ; + double denom = rayDirX * normalX + rayDirZ * normalZ; + if (Math.abs(denom) < 1e-10) continue; + + double t = -prevDist / denom; + if (t < 0.0 || t > 1.0) continue; + + double hitX = caster.posX + rayDirX * t; + double hitZ = caster.posZ + rayDirZ * t; + + double localRight = (hitX - panel.posX) * cos + (hitZ - panel.posZ) * sin; + if (Math.abs(localRight) > halfW) continue; + double localY = eyeY - panel.posY; + if (Math.abs(localY) > halfH) continue; + + return true; + } + } + return false; + } + + // ═══════════════════════════════════════════════════════════════════ + // LINE-OF-SIGHT UTILITIES + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check if there is a clear line of sight (no solid blocks) between caster and target. + * Raycasts from caster's eye position to target's body center. + * + * @return true if there IS line of sight (no blocks in the way) + */ + public static boolean hasLineOfSight(IWorld IWorld, IEntityLivingBase caster, IEntityLivingBase target) { + IVector3 start = IVector3.createVectorHelper(caster.posX, caster.posY + caster.getEyeHeight(), caster.posZ); + IVector3 end = IVector3.createVectorHelper(target.posX, target.posY + target.height * 0.5, target.posZ); + IRayTraceResult result = IWorld.rayTraceBlocks(start, end); + return result == null; + } + + /** + * Check if an enemy barrier (dome or panel) blocks the line between caster and target. + * Barriers owned by the caster or allies of the caster are not considered blocking. + * + * @return true if an enemy barrier blocks the attack + */ + @SuppressWarnings("unchecked") + public static boolean isBlockedByBarrier(IWorld IWorld, IEntityLivingBase caster, IEntityLivingBase target) { + // Barrier entities have tiny Minecraft bounding boxes (1x1 for domes, 0.5x0.5 for panels) + // but their actual collision geometry extends up to MAX_ENTITY_RADIUS (50) blocks. + // Must expand search to match EntityEnergyProjectile.checkBarrierCollision() pattern. + double searchRange = 55.0; // MAX_ENTITY_RADIUS(50) + 5 + double minX = Math.min(caster.posX, target.posX) - searchRange; + double minY = Math.min(caster.posY, target.posY) - searchRange; + double minZ = Math.min(caster.posZ, target.posZ) - searchRange; + double maxX = Math.max(caster.posX, target.posX) + searchRange; + double maxY = Math.max(caster.posY, target.posY) + searchRange; + double maxZ = Math.max(caster.posZ, target.posZ) + searchRange; + + IBoundingBox searchBox = IBoundingBox.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + List barriers = IWorld.getEntitiesWithinAABB(EntityEnergyBarrier.class, searchBox); + + for (EntityEnergyBarrier barrier : barriers) { + if (barrier.isDead) continue; + if (barrier.isCharging()) continue; + + // Skip barriers owned by caster or allies + if (barrier.getOwnerEntityId() == caster.getEntityId()) continue; + IEntity barrierOwner = barrier.getOwnerEntity(); + if (barrierOwner instanceof IEntityLivingBase) { + if (AbilityTargetHelper.isAlly(caster, (IEntityLivingBase) barrierOwner)) continue; + } + + if (barrier instanceof EntityEnergyDome) { + if (isDomeBlocking((EntityEnergyDome) barrier, caster, target)) return true; + } else if (barrier instanceof EntityEnergyPanel) { + if (isPanelBlocking((EntityEnergyPanel) barrier, caster, target)) return true; + } + } + + return false; + } + + /** + * Check if a dome blocks an attack from caster to target. + * Blocks when caster is outside and target is inside the dome. + */ + private static boolean isDomeBlocking(EntityEnergyDome dome, IEntityLivingBase caster, IEntityLivingBase target) { + float radius = dome.getDomeRadius(); + + double cdx = caster.posX - dome.posX; + double cdy = (caster.posY + caster.getEyeHeight()) - dome.posY; + double cdz = caster.posZ - dome.posZ; + double casterDist = Math.sqrt(cdx * cdx + cdy * cdy + cdz * cdz); + + double tdx = target.posX - dome.posX; + double tdy = (target.posY + target.height * 0.5) - dome.posY; + double tdz = target.posZ - dome.posZ; + double targetDist = Math.sqrt(tdx * tdx + tdy * tdy + tdz * tdz); + + // Block when caster is outside and target is inside + return casterDist > radius && targetDist < radius; + } + + /** + * Check if a panel blocks an attack from caster to target. + * Checks if the line from caster to target crosses through the panel bounds. + */ + private static boolean isPanelBlocking(EntityEnergyPanel panel, IEntityLivingBase caster, IEntityLivingBase target) { + float halfW = panel.getPanelData().panelWidth * 0.5f; + float halfH = panel.getPanelData().panelHeight * 0.5f; + + // Panel normal direction + float yawRad = (float) Math.toRadians(panel.getPanelYaw()); + double normalX = -Math.sin(yawRad); + double normalZ = Math.cos(yawRad); + + // Line from caster to target + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; + double dx = target.posX - startX; + double dy = (target.posY + target.height * 0.5) - startY; + double dz = target.posZ - startZ; + + // Dot product of line direction with panel normal + double denom = dx * normalX + dz * normalZ; + if (Math.abs(denom) < 0.0001) return false; // Line parallel to panel + + // Parameter t where line crosses panel plane + double relX = panel.posX - startX; + double relZ = panel.posZ - startZ; + double t = (relX * normalX + relZ * normalZ) / denom; + + if (t < 0 || t > 1) return false; // Intersection outside the line segment + + // Calculate intersection point + double hitX = startX + dx * t; + double hitY = startY + dy * t; + double hitZ = startZ + dz * t; + + // Transform to panel local space (right axis) + double cos = Math.cos(yawRad); + double sin = Math.sin(yawRad); + double localRight = (hitX - panel.posX) * cos + (hitZ - panel.posZ) * sin; + double localUp = hitY - panel.posY; + + // Check bounds + return Math.abs(localRight) <= halfW && Math.abs(localUp) <= halfH; + } + + // ═══════════════════════════════════════════════════════════════════ + // IAbility API METHODS + // ═══════════════════════════════════════════════════════════════════ + + @Override + public int getAllowedByType() { + return allowedBy.ordinal(); + } + + @Override + public void setAllowedByType(int type) { + this.allowedBy = UserType.fromOrdinal(type); + } + + @Override + public INbt getNbt() { + return NpcAPI.Instance().getINbt(writeNBT(false)); + } + + @Override + public void setNbt(INbt nbt) { + readNBT(nbt.getMCNBT()); + } + + // ═══════════════════════════════════════════════════════════════════ + // SCRIPT HANDLER + // ═══════════════════════════════════════════════════════════════════ + + public AbilityScript getScriptHandler() { + if (this.id == null || this.id.isEmpty()) return null; + return AbilityController.Instance.abilityScriptHandlers.get(this.id); + } + + public void setScriptHandler(AbilityScript handler) { + if (this.id == null || this.id.isEmpty()) return; + AbilityController.Instance.abilityScriptHandlers.put(this.id, handler); + } + + public AbilityScript getOrCreateScriptHandler() { + if (this.id == null || this.id.isEmpty()) return null; + AbilityScript handler = getScriptHandler(); + if (handler == null) { + handler = new AbilityScript(this.id); + AbilityController.Instance.abilityScriptHandlers.put(this.id, handler); + } + return handler; + } + + /** + * Get or create a per-instance script handler for this ability execution. + * Shares the template's engine but creates isolated Bindings for variable state. + * Returns null if no script is configured on the template. + */ + public AbilityScript getOrCreateInstanceScript() { + if (instanceScript != null) return instanceScript; + + AbilityScript template = getScriptHandler(); + if (template == null || !template.getEnabled() || template.container == null) return null; + + instanceScript = new AbilityScript(this.id); + instanceScript.setLanguage(template.getLanguage()); + instanceScript.setEnabled(true); + + IScriptUnit clone = template.container.createInstanceScope(instanceScript); + instanceScript.addScriptUnit(clone); + + return instanceScript; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java new file mode 100644 index 000000000..1867644c2 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java @@ -0,0 +1,330 @@ +package kamkeel.npcs.controllers.data.ability; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; +import java.util.UUID; + +/** + * Unified slot for storing any combat action: inline ability, ability reference, or chain reference. + * Support for all {@link IAbilityAction} types. + *

+ * References are lazy-resolved via their respective controllers and cached until + * the controller revision changes. + */ +public class AbilityAction { + + public enum SlotType { + INLINE_ABILITY, + ABILITY_REFERENCE, + CHAIN_REFERENCE, + INLINE_CHAIN + } + + private SlotType slotType; + private Ability inlineAbility; + private ChainedAbility inlineChain; + private String referenceId; + + /** + * Per-slot enabled override for reference slots (null = use master's default). + */ + private Boolean enabledOverride; + + // Cache + private transient IAbilityAction cachedAction; + private transient int cachedRevision = -1; + + private AbilityAction() { + } + + // ═══════════════════════════════════════════════════════════════════ + // FACTORY METHODS + // ═══════════════════════════════════════════════════════════════════ + + public static AbilityAction inline(Ability ability) { + AbilityAction slot = new AbilityAction(); + slot.slotType = SlotType.INLINE_ABILITY; + slot.inlineAbility = ability; + return slot; + } + + public static AbilityAction abilityReference(String referenceId) { + AbilityAction slot = new AbilityAction(); + slot.slotType = SlotType.ABILITY_REFERENCE; + slot.referenceId = referenceId; + return slot; + } + + public static AbilityAction chainReference(String referenceId) { + AbilityAction slot = new AbilityAction(); + slot.slotType = SlotType.CHAIN_REFERENCE; + slot.referenceId = referenceId; + return slot; + } + + public static AbilityAction inlineChain(ChainedAbility chain) { + AbilityAction slot = new AbilityAction(); + slot.slotType = SlotType.INLINE_CHAIN; + slot.inlineChain = chain; + return slot; + } + + // ═══════════════════════════════════════════════════════════════════ + // QUERIES + // ═══════════════════════════════════════════════════════════════════ + + public SlotType getSlotType() { + return slotType; + } + + public boolean isReference() { + return slotType == SlotType.ABILITY_REFERENCE || slotType == SlotType.CHAIN_REFERENCE; + } + + public boolean isChainReference() { + return slotType == SlotType.CHAIN_REFERENCE; + } + + public boolean isInlineChain() { + return slotType == SlotType.INLINE_CHAIN; + } + + public boolean isChain() { + return slotType == SlotType.CHAIN_REFERENCE || slotType == SlotType.INLINE_CHAIN; + } + + public boolean isAbilityReference() { + return slotType == SlotType.ABILITY_REFERENCE; + } + + public String getReferenceId() { + return referenceId; + } + + // ═══════════════════════════════════════════════════════════════════ + // RESOLUTION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Resolve the action. Inline returns directly; references resolve + * from the appropriate controller with version-based caching. + */ + public IAbilityAction getAction() { + switch (slotType) { + case INLINE_ABILITY: + return inlineAbility; + + case INLINE_CHAIN: + return inlineChain; + + case ABILITY_REFERENCE: { + AbilityController ctrl = AbilityController.Instance; + if (ctrl == null) return null; + int rev = ctrl.getCustomAbilityRevision(); + if (cachedAction != null && cachedRevision == rev) { + return cachedAction; + } + Ability resolved = ctrl.resolveAbility(referenceId); + cachedAction = resolved; + cachedRevision = rev; + return cachedAction; + } + + case CHAIN_REFERENCE: { + AbilityController ctrl = AbilityController.Instance; + if (ctrl == null) return null; + int rev = ctrl.getChainedAbilityRevision(); + if (cachedAction != null && cachedRevision == rev) { + return cachedAction; + } + ChainedAbility resolved = ctrl.resolveChainedAbility(referenceId); + cachedAction = resolved; + cachedRevision = rev; + return cachedAction; + } + + default: + return null; + } + } + + /** + * Get as Ability (returns null if this is a chain slot or resolution fails). + */ + public Ability getAbility() { + IAbilityAction action = getAction(); + return (action instanceof Ability) ? (Ability) action : null; + } + + /** + * Get as ChainedAbility (returns null if this is an ability slot or resolution fails). + */ + public ChainedAbility getChainedAbility() { + IAbilityAction action = getAction(); + return (action instanceof ChainedAbility) ? (ChainedAbility) action : null; + } + + // ═══════════════════════════════════════════════════════════════════ + // ENABLED OVERRIDE + // ═══════════════════════════════════════════════════════════════════ + + public void setEnabled(boolean enabled) { + if (slotType == SlotType.INLINE_ABILITY) { + if (inlineAbility != null) { + inlineAbility.setEnabled(enabled); + } + } else if (slotType == SlotType.INLINE_CHAIN) { + if (inlineChain != null) { + inlineChain.setEnabled(enabled); + } + } else { + this.enabledOverride = enabled; + } + } + + /** + * Check if this slot is enabled. For inline slots, delegates to the action directly. + * For reference slots, uses enabledOverride if set, otherwise the resolved action's own state. + * This avoids mutating the shared controller instance. + */ + public boolean isSlotEnabled() { + if (enabledOverride != null) { + return enabledOverride; + } + IAbilityAction action = getAction(); + return action != null && action.isEnabled(); + } + + // ═══════════════════════════════════════════════════════════════════ + // INLINE CONVERSION (ability references only) + // ═══════════════════════════════════════════════════════════════════ + + public Ability getInlineAbility() { + return inlineAbility; + } + + public ChainedAbility getInlineChain() { + return inlineChain; + } + + /** + * Convert this reference slot to an inline slot by copying the resolved data. + * Works for ability references (→ INLINE_ABILITY) and chain references (→ INLINE_CHAIN). + * Chain entries are kept as-is (not auto-converted to inline). + */ + public boolean convertToInline() { + if (slotType == SlotType.ABILITY_REFERENCE) { + Ability resolved = getAbility(); + if (resolved == null) return false; + + INbt nbt = resolved.writeNBT(true); + nbt.setString("id", UUID.randomUUID().toString()); + inlineAbility = AbilityController.Instance.fromNBT(nbt); + slotType = SlotType.INLINE_ABILITY; + referenceId = null; + cachedAction = null; + cachedRevision = -1; + return true; + } + + if (slotType == SlotType.CHAIN_REFERENCE) { + ChainedAbility resolved = getChainedAbility(); + if (resolved == null) return false; + + inlineChain = new ChainedAbility(); + INbt chainNbt = resolved.writeNBT(true); + chainNbt.setString("Id", UUID.randomUUID().toString()); + inlineChain.readNBT(chainNbt); + slotType = SlotType.INLINE_CHAIN; + referenceId = null; + cachedAction = null; + cachedRevision = -1; + return true; + } + + return false; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + public INbt writeNBT(boolean saveScripts) { + switch (slotType) { + case INLINE_CHAIN: { + INbt nbt = new INbt(); + if (inlineChain != null) { + nbt.setTag("InlineChain", inlineChain.writeNBT(saveScripts)); + } + return nbt; + } + + case CHAIN_REFERENCE: { + INbt nbt = new INbt(); + nbt.setString("ChainReference", referenceId); + if (enabledOverride != null) { + nbt.setBoolean("RefEnabled", enabledOverride); + } + return nbt; + } + + case ABILITY_REFERENCE: { + INbt nbt = new INbt(); + nbt.setString("Reference", referenceId); + if (enabledOverride != null) { + nbt.setBoolean("RefEnabled", enabledOverride); + } + return nbt; + } + + case INLINE_ABILITY: + default: + return inlineAbility != null ? inlineAbility.writeNBT(saveScripts) : new INbt(); + } + } + + public static AbilityAction fromNBT(INbt nbt) { + if (nbt == null) return null; + + // Inline chain (new format) + if (nbt.hasKey("InlineChain")) { + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt.getCompoundTag("InlineChain")); + return inlineChain(chain); + } + + // Chain reference + if (nbt.hasKey("ChainReference")) { + AbilityAction slot = chainReference(nbt.getString("ChainReference")); + if (nbt.hasKey("RefEnabled")) { + slot.enabledOverride = nbt.getBoolean("RefEnabled"); + } + return slot; + } + + // Ability reference + if (nbt.hasKey("Reference")) { + AbilityAction slot = abilityReference(nbt.getString("Reference")); + if (nbt.hasKey("RefEnabled")) { + slot.enabledOverride = nbt.getBoolean("RefEnabled"); + } + return slot; + } + + // Inline ability + if (AbilityController.Instance == null) return null; + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability == null) return null; + return inline(ability); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java new file mode 100644 index 000000000..69a6d4953 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java @@ -0,0 +1,54 @@ +package kamkeel.npcs.controllers.data.ability; + +import java.util.function.Consumer; + +/** + * Represents a variant/template for an ability type. + * When a user creates an ability that has registered variants, + * they are presented with a selection dialog before the ability is created. + *

+ * Each variant has a display name (lang key) and a configurator that + * applies preset values to the newly created ability. + */ +public class AbilityVariant { + + private final String displayKey; + private final String group; + private final Consumer configurator; + + /** + * @param displayKey Lang key for the variant display name (e.g., "ability.variant.dual") + * @param configurator Lambda that configures the ability with preset values + */ + public AbilityVariant(String displayKey, Consumer configurator) { + this(displayKey, null, configurator); + } + + /** + * @param displayKey Lang key for the variant display name + * @param group Optional group header shown in the variant selection GUI (e.g., "DBC Addon") + * @param configurator Lambda that configures the ability with preset values + */ + public AbilityVariant(String displayKey, String group, Consumer configurator) { + this.displayKey = displayKey; + this.group = group; + this.configurator = configurator; + } + + public String getDisplayKey() { + return displayKey; + } + + public String getGroup() { + return group; + } + + /** + * Apply this variant's preset IConfiguration to the given ability. + */ + public void apply(Ability ability) { + if (configurator != null) { + configurator.accept(ability); + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/ConcurrentSlot.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/ConcurrentSlot.java new file mode 100644 index 000000000..ae4242ab4 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/ConcurrentSlot.java @@ -0,0 +1,117 @@ +package kamkeel.npcs.controllers.data.ability; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +/** + * Wraps a concurrent ability execution with its own independent lifecycle. + * Concurrent abilities skip animations, sounds, and movement/rotation locks. + * They run their execute/tick logic in parallel with the primary ability. + *

+ * Used by the chain system when an entry is concurrent-capable and concurrent-enabled. + */ +public class ConcurrentSlot { + + private final Ability ability; + private boolean started = false; + private boolean completed = false; + + public ConcurrentSlot(Ability ability) { + this.ability = ability; + } + + public Ability getAbility() { + return ability; + } + + public boolean isCompleted() { + return completed; + } + + /** + * Start the concurrent ability. Skips windup — goes straight to ACTIVE. + * Called once when the slot is activated. + */ + public void start(IEntityLivingBase caster, IEntityLivingBase target) { + // Skip windup for concurrent execution — fire immediately + ability.setWindUpTicks(0); + ability.start(target); + started = true; + + // Ability should now be in ACTIVE phase (0 windup → immediate) + if (ability.getPhase() == AbilityPhase.ACTIVE) { + ability.onExecute(caster, target); + } + + // Check if ability completed during onExecute (instant abilities like signalCompletion) + if (ability.getPhase() == AbilityPhase.IDLE) { + completed = true; + } + } + + /** + * Tick the concurrent ability. Called every game tick from AbstractDataAbilities. + * Handles phase transitions including burst delay → re-execute cycles. + */ + public void tick(IEntityLivingBase caster, IEntityLivingBase target) { + if (completed || !started) return; + + AbilityPhase oldPhase = ability.getPhase(); + boolean phaseChanged = ability.tick(); + AbilityPhase newPhase = ability.getPhase(); + + switch (newPhase) { + case ACTIVE: + if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { + // Re-entering ACTIVE after burst delay → re-execute (burst re-fire) + ability.onExecute(caster, target); + if (ability.getPhase() == AbilityPhase.IDLE) { + completed = true; + return; + } + } + // Normal active tick + ability.onActiveTick(caster, target, ability.getCurrentTick()); + if (ability.getPhase() == AbilityPhase.IDLE) { + completed = true; + } + break; + + case BURST_DELAY: + // Waiting between bursts — nothing to do, tick() handles countdown + break; + + case WINDUP: + // Burst replay with windup — for concurrent, skip it by advancing + // (windUpTicks was set to 0 at start, but burst replay may re-enter WINDUP) + // The next tick() call will transition to ACTIVE + break; + + case IDLE: + completed = true; + break; + + case DAZED: + // Concurrent abilities don't get dazed — treat as completed + completed = true; + break; + } + } + + /** + * Force cleanup on interrupt. Called when the NPC is interrupted or combat ends. + */ + public void interrupt() { + if (!completed) { + ability.interrupt(); + completed = true; + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java new file mode 100644 index 000000000..5d1b38136 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java @@ -0,0 +1,507 @@ +package kamkeel.npcs.controllers.data.ability; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.ICustomAbility; +import java.util.List; + +/** + * A blank-slate ability that does nothing by itself. + * All behavior is driven by script event hooks (abilityStart, abilityTick, abilityComplete, + * abilityToggle, abilityToggleTick). + *

+ * Supports two modes based on toggle IConfiguration: + *

    + *
  • Instant mode (toggleStates = 0): Uses standard execution lifecycle with + * configurable duration. Script hooks: abilityStart, abilityTick, abilityComplete.
  • + *
  • Toggle mode (toggleStates >= 1): Uses toggle lifecycle. + * Script hooks: abilityToggle, abilityToggleTick.
  • + *
+ */ +public class CustomAbility extends Ability implements ICustomAbility { + + private int durationTicks = 20; + + // Telegraph dimension fields + private float tRadius = 5.0f; + private float tInnerRadius = 0.0f; + private float tLength = 5.0f; + private float tWidth = 2.0f; + private float tAngle = 45.0f; + + // Telegraph timing + private int telegraphActiveTicks = 0; + private boolean syncTelegraphWithDuration = true; + + public CustomAbility() { + this.typeId = "ability.cnpc.custom"; + this.name = "Custom"; + this.targetingMode = TargetingMode.SELF; + this.lockMovement = LockMode.NO; + this.cooldownTicks = 0; + this.windUpTicks = 0; + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + } + + // ==================== EXECUTION ==================== + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + if (durationTicks <= 0) { + signalCompletion(); + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (durationTicks > 0 && tick >= durationTicks) { + signalCompletion(); + } + } + + // ==================== PROPERTIES ==================== + + @Override + public boolean hasDamage() { + return false; + } + + @Override + public boolean isConcurrentCapable() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public boolean allowFreeOnCast() { + return true; + } + + @Override + public boolean hasOwnTelegraphControls() { + return true; + } + + // ==================== TELEGRAPH OVERRIDES ==================== + + @Override + public boolean keepTelegraphDuringActive() { + return getResolvedTelegraphActiveTicks() > 0; + } + + /** + * Returns the effective telegraph active ticks: synced from duration or manual value. + */ + public int getResolvedTelegraphActiveTicks() { + if (syncTelegraphWithDuration) { + return durationTicks; + } + return telegraphActiveTicks; + } + + @Override + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { + return null; + } + + Telegraph telegraph; + double x, y, z; + float yaw = caster.rotationYaw; + TelegraphType tType = getTelegraphType(); + + boolean positionAtCaster = targetingMode == TargetingMode.AOE_SELF || + targetingMode == TargetingMode.SELF || + tType == TelegraphType.LINE || + tType == TelegraphType.CONE; + + if (positionAtCaster) { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; + } else if (target != null) { + x = target.posX; + y = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ); + z = target.posZ; + } else { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; + } + + switch (tType) { + case CIRCLE: + telegraph = Telegraph.circle(tRadius); + break; + case RING: + telegraph = Telegraph.ring(tRadius, tInnerRadius); + break; + case LINE: + telegraph = Telegraph.line(tLength, tWidth); + break; + case CONE: + telegraph = Telegraph.cone(tLength, tAngle, tInnerRadius); + break; + case POINT: + telegraph = new Telegraph("", TelegraphType.POINT); + break; + case SQUARE: + telegraph = Telegraph.square(tRadius); + break; + default: + return null; + } + + int activeTicks = getResolvedTelegraphActiveTicks(); + int totalDuration = windUpTicks + activeTicks; + telegraph.setDurationTicks(Math.max(1, totalDuration)); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + if (windUpTicks > 0) { + telegraph.setWarningStartTick(windUpTicks); + } else { + telegraph.setWarningStartTick(0); + } + telegraph.setHeightOffset(telegraphHeightOffset); + + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setCasterEntityId(caster.getEntityId()); + + if (positionAtCaster) { + instance.setEntityIdToFollow(caster.getEntityId()); + if (tType == TelegraphType.LINE || tType == TelegraphType.CONE) { + if (!isRotationLockedDuringWindup()) { + if (target != null) { + instance.setTargetEntityId(target.getEntityId()); + } else { + instance.setTrackFollowedEntityYaw(true); + } + } + } + } else if (target != null) { + instance.setEntityIdToFollow(target.getEntityId()); + } + + return instance; + } + + @Override + public float getTelegraphRadius() { + return tRadius; + } + + @Override + public float getTelegraphInnerRadius() { + return tInnerRadius; + } + + @Override + public float getTelegraphLength() { + return tLength; + } + + @Override + public float getTelegraphWidth() { + return tWidth; + } + + @Override + public float getTelegraphAngle() { + return tAngle; + } + + // ==================== FIELDS ==================== + + @Override + public int getDurationTicks() { + return durationTicks; + } + + @Override + public void setDurationTicks(int ticks) { + this.durationTicks = Math.max(0, ticks); + } + + public float getTRadius() { + return tRadius; + } + + public void setTRadius(float radius) { + this.tRadius = Math.max(0, radius); + } + + public float getTInnerRadius() { + return tInnerRadius; + } + + public void setTInnerRadius(float innerRadius) { + this.tInnerRadius = Math.max(0, innerRadius); + } + + public float getTLength() { + return tLength; + } + + public void setTLength(float length) { + this.tLength = Math.max(0, length); + } + + public float getTWidth() { + return tWidth; + } + + public void setTWidth(float width) { + this.tWidth = Math.max(0, width); + } + + public float getTAngle() { + return tAngle; + } + + public void setTAngle(float angle) { + this.tAngle = Math.max(0, angle); + } + + public int getTelegraphActiveTicks() { + return telegraphActiveTicks; + } + + public void setTelegraphActiveTicks(int ticks) { + this.telegraphActiveTicks = Math.max(0, ticks); + } + + public boolean isSyncTelegraphWithDuration() { + return syncTelegraphWithDuration; + } + + public void setSyncTelegraphWithDuration(boolean sync) { + this.syncTelegraphWithDuration = sync; + } + + // ==================== ICustomAbility ==================== + + @Override + public int getTelegraphShapeType() { + return getTelegraphType().ordinal(); + } + + @Override + public void setTelegraphShapeType(int type) { + TelegraphType[] values = TelegraphType.values(); + if (type >= 0 && type < values.length) { + setTelegraphType(values[type]); + } + } + + @Override + public void setTelegraphRadius(float radius) { + this.tRadius = Math.max(0, radius); + } + + @Override + public void setTelegraphInnerRadius(float innerRadius) { + this.tInnerRadius = Math.max(0, innerRadius); + } + + @Override + public void setTelegraphLength(float length) { + this.tLength = Math.max(0, length); + } + + @Override + public void setTelegraphWidth(float width) { + this.tWidth = Math.max(0, width); + } + + @Override + public void setTelegraphAngle(float angle) { + this.tAngle = Math.max(0, angle); + } + + @Override + public int getTargetingModeType() { + return getTargetingMode().ordinal(); + } + + @Override + public void setTargetingModeType(int type) { + TargetingMode[] values = TargetingMode.values(); + if (type >= 0 && type < values.length) { + setTargetingMode(values[type]); + } + } + + // ==================== NBT ==================== + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setInteger("durationTicks", durationTicks); + nbt.setFloat("tRadius", tRadius); + nbt.setFloat("tInnerRadius", tInnerRadius); + nbt.setFloat("tLength", tLength); + nbt.setFloat("tWidth", tWidth); + nbt.setFloat("tAngle", tAngle); + nbt.setInteger("telegraphActiveTicks", telegraphActiveTicks); + nbt.setBoolean("syncTelegraphWithDuration", syncTelegraphWithDuration); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.durationTicks = Math.max(0, nbt.getInteger("durationTicks")); + this.tRadius = nbt.hasKey("tRadius") ? nbt.getFloat("tRadius") : 5.0f; + this.tInnerRadius = nbt.hasKey("tInnerRadius") ? nbt.getFloat("tInnerRadius") : 0.0f; + this.tLength = nbt.hasKey("tLength") ? nbt.getFloat("tLength") : 5.0f; + this.tWidth = nbt.hasKey("tWidth") ? nbt.getFloat("tWidth") : 2.0f; + this.tAngle = nbt.hasKey("tAngle") ? nbt.getFloat("tAngle") : 45.0f; + this.telegraphActiveTicks = nbt.hasKey("telegraphActiveTicks") ? nbt.getInteger("telegraphActiveTicks") : 0; + this.syncTelegraphWithDuration = nbt.hasKey("syncTelegraphWithDuration") ? nbt.getBoolean("syncTelegraphWithDuration") : true; + } + + // ==================== GUI ==================== + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + // User Type (NPC / Player / Both) + defs.add(FieldDef.enumField("ability.allowedBy", UserType.class, + this::getAllowedBy, this::setAllowedBy)); + + // Targeting Mode + defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class, + this::getTargetingMode, this::setTargetingMode)); + + // Toggle section + defs.add(FieldDef.section("ability.section.toggle")); + defs.add(FieldDef.boolField("ability.toggleEnabled", + this::isToggleable, enabled -> setToggleStates(enabled ? Math.max(1, getToggleStates()) : 0))); + defs.add(FieldDef.intField("ability.toggleStates", this::getToggleStates, this::setToggleStates) + .range(1, 10) + .visibleWhen(this::isToggleable)); + defs.add(FieldDef.boolField("ability.hasActiveToggle", this::hasActiveToggle, this::setHasActiveToggle) + .visibleWhen(this::isToggleable)); + + // Duration (instant mode only) + defs.add(FieldDef.section("ability.section.execution") + .visibleWhen(() -> !this.isToggleable())); + defs.add(FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(0, 6000) + .visibleWhen(() -> !this.isToggleable())); + + // ── Telegraph Controls (Effects tab) ───────────────────────── + defs.add(FieldDef.section("ability.section.telegraph").tab("Effects")); + defs.add(FieldDef.enumField("ability.telegraphShape", TelegraphType.class, + this::getTelegraphType, this::setTelegraphType).tab("Effects") + .hover("ability.hover.telegraphShape")); + defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph) + .tab("Effects").visibleWhen(this::hasTelegraphShape) + .hover("ability.hover.showTelegraph")); + defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor) + .tab("Effects").visibleWhen(this::isTelegraphVisible) + .hover("ability.hover.windUpColor")); + defs.add(FieldDef.colorSubGui("ability.activeColor", this::getActiveColor, this::setActiveColor) + .tab("Effects").visibleWhen(this::isTelegraphVisible) + .hover("ability.hover.activeColor")); + defs.add(FieldDef.floatField("ability.telegraphHeight", this::getTelegraphHeightOffset, this::setTelegraphHeightOffset) + .tab("Effects").visibleWhen(this::isTelegraphVisible) + .hover("ability.hover.telegraphHeight")); + + // Telegraph active duration with sync toggle (mirrors windup sync pattern) + defs.add(FieldDef.row( + FieldDef.intField("ability.telegraphActiveTicks", this::getTelegraphActiveTicks, this::setTelegraphActiveTicks).range(0, 6000) + .hover("ability.hover.telegraphActiveTicks"), + FieldDef.boolField("ability.syncTelegraph", this::isSyncTelegraphWithDuration, this::setSyncTelegraphWithDuration) + .hover("ability.hover.syncTelegraph") + ).tab("Effects").visibleWhen(() -> isTelegraphVisible() && !isSyncTelegraphWithDuration())); + defs.add(FieldDef.row( + FieldDef.labelField("ability.telegraphActiveTicks", () -> getResolvedTelegraphActiveTicks() + "t"), + FieldDef.boolField("ability.syncTelegraph", this::isSyncTelegraphWithDuration, this::setSyncTelegraphWithDuration) + .hover("ability.hover.syncTelegraph") + ).tab("Effects").visibleWhen(() -> isTelegraphVisible() && isSyncTelegraphWithDuration())); + + // Shape-specific dimension fields + defs.add(FieldDef.floatField("ability.telegraphRadius", this::getTRadius, this::setTRadius) + .tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsRadius()) + .hover("ability.hover.telegraphRadius")); + defs.add(FieldDef.floatField("ability.telegraphInnerRadius", this::getTInnerRadius, this::setTInnerRadius) + .tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsInnerRadius()) + .hover("ability.hover.telegraphInnerRadius")); + defs.add(FieldDef.floatField("ability.telegraphLength", this::getTLength, this::setTLength) + .tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsLength()) + .hover("ability.hover.telegraphLength")); + defs.add(FieldDef.floatField("ability.telegraphWidth", this::getTWidth, this::setTWidth) + .tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsWidth()) + .hover("ability.hover.telegraphWidth")); + defs.add(FieldDef.floatField("ability.telegraphAngle", this::getTAngle, this::setTAngle) + .tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsAngle()) + .hover("ability.hover.telegraphAngle")); + } + + // ==================== TELEGRAPH VISIBILITY HELPERS ==================== + + private boolean hasTelegraphShape() { + return getTelegraphType() != TelegraphType.NONE; + } + + private boolean isTelegraphVisible() { + return hasTelegraphShape() && isShowTelegraph(); + } + + private boolean needsRadius() { + TelegraphType t = getTelegraphType(); + return t == TelegraphType.CIRCLE || t == TelegraphType.RING || t == TelegraphType.SQUARE; + } + + private boolean needsInnerRadius() { + TelegraphType t = getTelegraphType(); + return t == TelegraphType.RING || t == TelegraphType.CONE; + } + + private boolean needsLength() { + TelegraphType t = getTelegraphType(); + return t == TelegraphType.LINE || t == TelegraphType.CONE; + } + + private boolean needsWidth() { + return getTelegraphType() == TelegraphType.LINE; + } + + private boolean needsAngle() { + return getTelegraphType() == TelegraphType.CONE; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java new file mode 100644 index 000000000..b5500f79f --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java @@ -0,0 +1,147 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import noppes.npcs.LogWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public abstract class AbilityCondition { + public static final int MAX_CONDITIONS = 5; + + protected String typeId = ""; + protected String name = ""; + protected UserType userType = UserType.BOTH; + protected ConditionFilter conditionFilter = ConditionFilter.CASTER; + + public boolean check(IEntityLivingBase caster, IEntityLivingBase target) { + switch (getFilter()) { + case CASTER: + return checkEntity(caster); + case TARGET: + return target != null && checkEntity(target); + case BOTH: + return checkEntity(caster) && (target != null && checkEntity(target)); + default: + return checkEntity(caster); + } + } + + protected abstract boolean checkEntity(IEntityLivingBase IEntity); + + public boolean requiresTarget() { + return false; + } + + /** + * Returns true if all required fields are filled in. + * Used to disable the Done button in SubGuiConditionEdit when incomplete. + */ + public boolean isConfigured() { + return true; + } + + public UserType getUserType() { + return userType; + } + + public String getTypeId() { + return typeId; + } + + public String getName() { + return name; + } + + public ConditionFilter getFilter() { + return conditionFilter; + } + + public void setFilter(ConditionFilter filter) { + this.conditionFilter = filter; + } + + @ClientOnly + public abstract void getConditionDefinitions(List defs); + + /** + * Returns a human-readable summary of this condition's IConfiguration. + * Used for tooltips when hovering over condition buttons. + */ + @ClientOnly + public abstract String getConditionSummary(); + + @ClientOnly + public final List getAllDefinitions() { + List defs = new ArrayList<>(); + + defs.add(FieldDef.labelField("ability.validFor", () -> + "\u00A7e" + PlatformServiceHolder.get().translateToLocal("ability.userType." + getUserType().name()))); + defs.add(FieldDef.enumField("condition.filter", ConditionFilter.class, this::getFilter, this::setFilter)); + getConditionDefinitions(defs); + return defs; + } + + public final INbt writeNBT() { + INbt nbt = new INbt(); + nbt.setString("typeId", getTypeId()); + nbt.setString("name", getName()); + nbt.setInteger("userType", getUserType().ordinal()); + nbt.setInteger("filter", getFilter().ordinal()); + writeTypeNBT(nbt); + return nbt; + } + + public abstract void writeTypeNBT(INbt nbt); + + public abstract void readTypeNBT(INbt nbt); + + public final void readNBT(INbt nbt) { + typeId = nbt.getString("typeId"); + name = nbt.getString("name"); + userType = UserType.fromOrdinal(nbt.getInteger("userType")); + conditionFilter = ConditionFilter.fromOrdinal(nbt.getInteger("filter")); + readTypeNBT(nbt); + } + + public static AbilityCondition fromNBT(INbt nbt) { + String typeId = nbt.getString("typeId"); + Supplier factory = AbilityController.Instance.getConditionType(typeId); + if (factory == null) { + LogWriter.info("AbilityController: Unknown condition type: " + typeId); + return null; + } + AbilityCondition condition = factory.get(); + condition.readNBT(nbt); + + return condition; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionCompare.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionCompare.java new file mode 100644 index 000000000..5a7856795 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionCompare.java @@ -0,0 +1,138 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.List; + +/** + * Abstract base for float comparison-based conditions. + * Provides shared CompareType enum, NBT serialization, and FieldDef generation. + */ +public abstract class ConditionCompare extends AbilityCondition { + + public enum CompareType { + ABOVE { + @Override + public boolean test(float value, float threshold) { + return value >= threshold; + } + }, + BELOW { + @Override + public boolean test(float value, float threshold) { + return value <= threshold; + } + }, + EQUAL { + @Override + public boolean test(float value, float threshold) { + return value == threshold; + } + }; + + public abstract boolean test(float value, float threshold); + + public static CompareType fromOrdinal(int ordinal) { + CompareType[] values = values(); + return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : ABOVE; + } + + @Override + public String toString() { + return "condition." + name().toLowerCase(); + } + } + + protected CompareType compareType = CompareType.ABOVE; + + /** + * Returns the value to be compared for this IEntity (e.g. level, skill level). + * Return -1 to indicate the condition should fail immediately. + */ + protected abstract float getEntityValue(IEntityLivingBase IEntity); + + /** + * Returns the threshold that getEntityValue() is compared against. + */ + protected abstract float getThreshold(); + + /** + * Appends subclass-specific FieldDefs (e.g. skill picker, level input). + * Called before the shared compareType field is added. + */ + @ClientOnly + protected abstract void getExtraDefinitions(List defs); + + /** + * Subclasses write their own NBT fields here. + */ + protected abstract void writeExtraNBT(INbt nbt); + + /** + * Subclasses read their own NBT fields here. + */ + protected abstract void readExtraNBT(INbt nbt); + + @Override + protected boolean checkEntity(IEntityLivingBase IEntity) { + float value = getEntityValue(IEntity); + if (value < 0) return false; + return compareType.test(value, getThreshold()); + } + + @ClientOnly + @Override + public void getConditionDefinitions(List defs) { + defs.add(FieldDef.enumField("condition.compare_type", CompareType.class, this::getCompareType, this::setCompareType)); + getExtraDefinitions(defs); + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setInteger("compareType", compareType.ordinal()); + writeExtraNBT(nbt); + } + + @Override + public void readTypeNBT(INbt nbt) { + compareType = CompareType.fromOrdinal(nbt.getInteger("compareType")); + readExtraNBT(nbt); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public CompareType getCompareType() { + return compareType; + } + + public void setCompareType(CompareType compareType) { + this.compareType = compareType; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHPThreshold.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHPThreshold.java new file mode 100644 index 000000000..822264c1e --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHPThreshold.java @@ -0,0 +1,33 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +public class ConditionHPThreshold extends ConditionThreshold { + + public ConditionHPThreshold() { + this.typeId = "condition.cnpc.hp_threshold"; + this.name = "condition.hp_threshold"; + } + + @Override + protected float getEntityValue(IEntityLivingBase IEntity) { + return IEntity.getHealth(); + } + + @Override + protected float getEntityMaxValue(IEntityLivingBase IEntity) { + return IEntity.getMaxHealth(); + } + + @Override + protected String getStatName() { + return "HP"; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java new file mode 100644 index 000000000..a00ac09c9 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java @@ -0,0 +1,128 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.data.CustomEffect; + +import java.util.HashMap; +import java.util.List; + +public class ConditionHasEffect extends AbilityCondition { + private int effectId = -1; + private int effectIndex = 0; + + public ConditionHasEffect() { + this.typeId = "condition.cnpc.has_effect"; + this.name = "condition.has_effect"; + this.userType = UserType.PLAYER_ONLY; + } + + @Override + protected boolean checkEntity(IEntityLivingBase IEntity) { + if (!(IEntity instanceof IPlayer)) return false; + if (effectId < 0) return false; + + IPlayer player = (IPlayer) IEntity; + return CustomEffectController.getInstance().hasEffect(player, effectId, effectIndex); + } + + @ClientOnly + @Override + public void getConditionDefinitions(List defs) { + defs.add(FieldDef.subGuiField("condition.select_effect", + () -> new SubGuiCustomEffectSelect(effectId, effectIndex), + gui -> { + SubGuiCustomEffectSelect sel = (SubGuiCustomEffectSelect) gui; + if (sel.getSelectedEffectId() >= 0) { + effectId = sel.getSelectedEffectId(); + effectIndex = sel.getSelectedIndex(); + } + }) + .buttonLabel(() -> { + if (effectId < 0) return "None"; + CustomEffect effect = getEffect(); + return effect != null ? effect.getName() : "ID:" + effectId; + }) + .clearable(() -> { + effectId = -1; + effectIndex = 0; + })); + } + + @ClientOnly + @Override + public String getConditionSummary() { + String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString()); + String effectName = "None"; + if (effectId >= 0) { + CustomEffect effect = getEffect(); + effectName = effect != null ? effect.getName() : "ID:" + effectId; + } + return "[" + filterLabel + "] Effect: " + effectName; + } + + @Override + public boolean isConfigured() { + return effectId >= 0; + } + + private CustomEffect getEffect() { + HashMap map = CustomEffectController.getInstance().getEffectMap(effectIndex); + return map != null ? map.get(effectId) : null; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setInteger("effectId", effectId); + nbt.setInteger("effectIndex", effectIndex); + } + + @Override + public void readTypeNBT(INbt nbt) { + effectId = nbt.getInteger("effectId"); + effectIndex = nbt.getInteger("effectIndex"); + } + + public int getEffectId() { + return effectId; + } + + public void setEffectId(int effectId) { + this.effectId = effectId; + } + + public int getEffectIndex() { + return effectIndex; + } + + public void setEffectIndex(int effectIndex) { + this.effectIndex = effectIndex; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java new file mode 100644 index 000000000..76e0c157e --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java @@ -0,0 +1,101 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.List; + +public class ConditionHitCount extends AbilityCondition { + private int requiredHits = 3; + private int withinTicks = 60; + + public ConditionHitCount() { + this.typeId = "condition.cnpc.hit_count"; + this.name = "condition.hit_count"; + this.conditionFilter = ConditionFilter.CASTER; + } + + @Override + protected boolean checkEntity(IEntityLivingBase IEntity) { + // Not used — this condition overrides check() directly + return false; + } + + @Override + public boolean check(IEntityLivingBase caster, IEntityLivingBase target) { + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.abilities != null) { + return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + } + } + + // For players, hit count tracking is not yet supported + return false; + } + + public int getRequiredHits() { + return requiredHits; + } + + public void setRequiredHits(int requiredHits) { + this.requiredHits = Math.max(0, requiredHits); + } + + public int getWithinTicks() { + return withinTicks; + } + + public void setWithinTicks(int withinTicks) { + this.withinTicks = Math.max(0, withinTicks); + } + + @ClientOnly + @Override + public void getConditionDefinitions(List defs) { + defs.add(FieldDef.intField("condition.required_hits", this::getRequiredHits, this::setRequiredHits).min(1)); + defs.add(FieldDef.intField("condition.within_ticks", this::getWithinTicks, this::setWithinTicks).min(0)); + } + + @ClientOnly + @Override + public String getConditionSummary() { + return "[Caster] Hit " + requiredHits + " times in " + withinTicks + " ticks"; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setInteger("requiredHits", requiredHits); + nbt.setInteger("withinTicks", withinTicks); + } + + @Override + public void readTypeNBT(INbt nbt) { + requiredHits = Math.max(0, nbt.getInteger("requiredHits")); + withinTicks = Math.max(0, nbt.getInteger("withinTicks")); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java new file mode 100644 index 000000000..d326f8ffd --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java @@ -0,0 +1,208 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.List; + +public class ConditionItem extends AbilityCondition { + + public enum UsageType { + ARMOR, HOLDING, COUNT, OFFHAND; + + public static UsageType fromOrdinal(int ordinal) { + UsageType[] values = values(); + return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : HOLDING; + } + } + + public enum ArmorSlot { + BOOTS, LEGS, CHEST, HELMET; + + public static ArmorSlot fromOrdinal(int ordinal) { + ArmorSlot[] values = values(); + return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : BOOTS; + } + } + + // Shared + private UsageType usageType = UsageType.HOLDING; + private String itemName = ""; + + // ARMOR only + private ArmorSlot armorSlot = ArmorSlot.BOOTS; + private boolean fullArmorSet = false; + + // COUNT + PLAYER only + private int requiredCount = 1; + + public ConditionItem() { + this.typeId = "condition.cnpc.item"; + this.name = "condition.item"; + } + + @Override + protected boolean checkEntity(IEntityLivingBase IEntity) { + switch (usageType) { + case HOLDING: + return checkHolding(IEntity); + case ARMOR: + return checkArmor(IEntity); + case COUNT: + return checkCount(IEntity); + case OFFHAND: + return checkOffhand(IEntity); + default: + return false; + } + } + + private boolean checkHolding(IEntityLivingBase IEntity) { + IItemStack held = IEntity.getHeldItem(); + return matchesItem(held); + } + + private boolean checkArmor(IEntityLivingBase IEntity) { + // MC getEquipmentInSlot: 1=boots, 2=legs, 3=chest, 4=helmet + int slot = armorSlot.ordinal() + 1; // BOOTS=0, LEGS=1, CHEST=2, HELMET=3 + return matchesItem(IEntity.getEquipmentInSlot(slot)); + } + + private boolean checkCount(IEntityLivingBase IEntity) { + if (IEntity instanceof IPlayer) { + return checkPlayerInventoryCount((IPlayer) IEntity); + } + if (IEntity instanceof EntityNPCInterface) { + return checkNPCProjectile((EntityNPCInterface) IEntity); + } + + return false; + } + + private boolean checkOffhand(IEntityLivingBase IEntity) { + // No offhand for players + if (IEntity instanceof IPlayer) return false; + if (!(IEntity instanceof EntityNPCInterface)) return false; + + IItemStack item = ((EntityNPCInterface) IEntity).getOffHand(); + return matchesItem(item); + } + + private boolean checkPlayerInventoryCount(IPlayer player) { + int count = 0; + for (IItemStack stack : player.inventory.mainInventory) { + if (matchesItem(stack)) { + count += stack.stackSize; + } + } + return count >= requiredCount; + } + + private boolean checkNPCProjectile(EntityNPCInterface npc) { + IItemStack projectile = npc.inventory.getProjectile(); + return matchesItem(projectile); + } + + private boolean matchesItem(IItemStack stack) { + if (stack == null || itemName == null || itemName.isEmpty()) return false; + String registryName = net.minecraft.item.Item.itemRegistry.getNameForObject(stack.getItem()); + return itemName.equals(registryName); + } + + @ClientOnly + @Override + public void getConditionDefinitions(List defs) { + defs.add(FieldDef.enumField("condition.usage_type", UsageType.class, + this::getUsageType, this::setUsageType)); + + defs.add(FieldDef.stringField("condition.item_name", + this::getItemName, this::setItemName) + .hover("condition.hover.item_name")); + + defs.add(FieldDef.enumField("condition.armor_slot", ArmorSlot.class, + this::getArmorSlot, this::setArmorSlot) + .visibleWhen(() -> usageType == UsageType.ARMOR) + .hover("condition.hover.armor_slots")); + + defs.add(FieldDef.intField("condition.item_count", + this::getRequiredCount, this::setRequiredCount) + .range(1, 64) + .visibleWhen(() -> usageType == UsageType.COUNT && userType.allowsPlayer()) + .hover("condition.hover.required_count")); + } + + @ClientOnly + @Override + public String getConditionSummary() { + String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString()); + String usage = usageType.name(); + String item = itemName.isEmpty() ? "None" : itemName; + return "[" + filterLabel + "] " + usage + ": " + item; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setString("itemName", itemName); + nbt.setInteger("usageType", usageType.ordinal()); + nbt.setInteger("armorSlot", armorSlot.ordinal()); + nbt.setInteger("requiredCount", requiredCount); + } + + @Override + public void readTypeNBT(INbt nbt) { + itemName = nbt.getString("itemName"); + usageType = UsageType.fromOrdinal(nbt.getInteger("usageType")); + armorSlot = ArmorSlot.fromOrdinal(nbt.getInteger("armorSlot")); + requiredCount = Math.max(1, nbt.getInteger("requiredCount")); + } + + + @Override + public boolean isConfigured() { + return itemName != null && !itemName.isEmpty(); + } + + public UsageType getUsageType() { return usageType; } + public void setUsageType(UsageType usageType) { this.usageType = usageType; } + + public String getItemName() { return itemName; } + public void setItemName(String itemName) { this.itemName = itemName; } + + public ArmorSlot getArmorSlot() { return armorSlot; } + public void setArmorSlot(ArmorSlot armorSlot) { this.armorSlot = armorSlot; } + + public boolean isFullArmorSet() { + return fullArmorSet; + } + + public void setFullArmorSet(boolean fullArmorSet) { + this.fullArmorSet = fullArmorSet; + } + + public int getRequiredCount() { return requiredCount; } + public void setRequiredCount(int requiredCount) { this.requiredCount = Math.max(1, requiredCount); } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java new file mode 100644 index 000000000..d18d97d40 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java @@ -0,0 +1,110 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.Quest; + +import java.util.List; + +public class ConditionQuestCompleted extends AbilityCondition { + private int questId = -1; + + public ConditionQuestCompleted() { + this.typeId = "condition.cnpc.quest_completed"; + this.name = "condition.quest_completed"; + this.userType = UserType.PLAYER_ONLY; + } + + @Override + protected boolean checkEntity(IEntityLivingBase IEntity) { + if (!(IEntity instanceof IPlayer)) return false; + if (questId < 0) return false; + + IPlayer player = (IPlayer) IEntity; + PlayerData data = PlayerData.get(player); + if (data == null || data.questData == null) return false; + return data.questData.hasFinishedQuest(questId); + } + + @ClientOnly + @Override + public void getConditionDefinitions(List defs) { + defs.add(FieldDef.subGuiField("condition.select_quest", + () -> new GuiQuestSelection(questId), + gui -> { + GuiQuestSelection sel = (GuiQuestSelection) gui; + if (sel.selectedQuest != null) { + questId = sel.selectedQuest.id; + } + }) + .buttonLabel(() -> { + if (questId < 0) return "None"; + Quest q = QuestController.Instance.quests.get(questId); + return q != null ? q.title : "ID:" + questId; + }) + .clearable(() -> questId = -1)); + } + + @ClientOnly + @Override + public String getConditionSummary() { + String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString()); + String questName = "None"; + if (questId >= 0) { + Quest q = QuestController.Instance.quests.get(questId); + questName = q != null ? q.title : "ID:" + questId; + } + return "[" + filterLabel + "] Quest: " + questName; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setInteger("questId", questId); + } + + @Override + public void readTypeNBT(INbt nbt) { + questId = nbt.getInteger("questId"); + } + + @Override + public boolean isConfigured() { + return questId >= 0; + } + + public int getQuestId() { + return questId; + } + + public void setQuestId(int questId) { + this.questId = questId; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionThreshold.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionThreshold.java new file mode 100644 index 000000000..88b0b9074 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionThreshold.java @@ -0,0 +1,195 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.List; + +/** + * Abstract base for threshold-based conditions (HP, Ki, etc.). + * Provides shared percent/flat toggle, ThresholdType enum, and FieldDef generation. + */ +public abstract class ConditionThreshold extends AbilityCondition { + + public enum ThresholdType { + ABOVE { + @Override + public boolean test(float value, float threshold) { + return value >= threshold; + } + }, + BELOW { + @Override + public boolean test(float value, float threshold) { + return value <= threshold; + } + }, + EQUAL { + @Override + public boolean test(float value, float threshold) { + return Math.abs(value - threshold) < 0.001f; + } + }; + + public abstract boolean test(float value, float threshold); + + public static ThresholdType fromOrdinal(int ordinal) { + ThresholdType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return ABOVE; + } + + @Override + public String toString() { + switch (this) { + case ABOVE: + return "condition.above"; + case BELOW: + return "condition.below"; + case EQUAL: + return "condition.equal"; + default: + return name(); + } + } + } + + protected float thresholdFlat = 100f; + protected float thresholdPercent = 0.5f; + protected boolean percent = true; + protected ThresholdType thresholdType = ThresholdType.ABOVE; + + /** + * Returns the raw stat value for the IEntity (e.g. current HP, current Ki). + */ + protected abstract float getEntityValue(IEntityLivingBase IEntity); + + /** + * Returns the maximum stat value for the IEntity (e.g. max HP, max Ki). + * Used to compute the percentage ratio. + */ + protected abstract float getEntityMaxValue(IEntityLivingBase IEntity); + + /** + * Returns the display name for the stat being checked (e.g. "HP", "Ki"). + * Used in getConditionSummary(). + */ + protected abstract String getStatName(); + + @Override + protected boolean checkEntity(IEntityLivingBase IEntity) { + float value; + float threshold; + if (percent) { + float max = getEntityMaxValue(IEntity); + value = max > 0 ? getEntityValue(IEntity) / max : 0; + threshold = thresholdPercent; + } else { + value = getEntityValue(IEntity); + threshold = thresholdFlat; + } + return thresholdType.test(value, threshold); + } + + @ClientOnly + @Override + public void getConditionDefinitions(List defs) { + defs.add(FieldDef.floatField("condition.threshold_flat", this::getThresholdFlat, this::setThresholdFlat) + .min(0).visibleWhen(() -> !isPercent())); + defs.add(FieldDef.floatField("condition.threshold_percent", this::getThresholdPercent, this::setThresholdPercent) + .min(0).max(1).visibleWhen(this::isPercent)); + defs.add(FieldDef.boolField("condition.percent", this::isPercent, this::setIsPercent)); + defs.add(FieldDef.enumField("condition.threshold_type", ThresholdType.class, + this::getThresholdType, this::setThresholdType)); + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("thresholdFlat", thresholdFlat); + nbt.setFloat("thresholdPercent", thresholdPercent); + nbt.setBoolean("percent", percent); + nbt.setInteger("thresholdType", thresholdType.ordinal()); + } + + @Override + public void readTypeNBT(INbt nbt) { + thresholdFlat = nbt.getFloat("thresholdFlat"); + thresholdPercent = nbt.getFloat("thresholdPercent"); + percent = nbt.getBoolean("percent"); + thresholdType = ThresholdType.fromOrdinal(nbt.getInteger("thresholdType")); + } + + @ClientOnly + @Override + public String getConditionSummary() { + String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString()); + String typeName = getStatName(); + String typeLabel = PlatformServiceHolder.get().translateToLocal(thresholdType.toString()); + String value = percent + ? String.format("%.0f%%", thresholdPercent * 100) + : String.format("%.0f", thresholdFlat); + return "[" + filterLabel + "] " + typeName + " " + typeLabel + " " + value; + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public float getThresholdFlat() { + return thresholdFlat; + } + + public void setThresholdFlat(float value) { + this.thresholdFlat = Math.max(0, value); + } + + public float getThresholdPercent() { + return thresholdPercent; + } + + public void setThresholdPercent(float value) { + this.thresholdPercent = Math.max(0, Math.min(1, value)); + } + + public boolean isPercent() { + return percent; + } + + public void setIsPercent(boolean percent) { + this.percent = percent; + } + + public ThresholdType getThresholdType() { + return thresholdType; + } + + public void setThresholdType(ThresholdType type) { + this.thresholdType = type; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java new file mode 100644 index 000000000..c581301b4 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java @@ -0,0 +1,344 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +/** + * Icon data for abilities, stored in the ability's customData NBT under "AbilityIcon". + * Supports up to 3 layers, each with its own texture, UV coordinates, and tint color. + * Width, height, and scale are shared across all layers. + */ +public class AbilityIconData { + public static final String NBT_KEY = "AbilityIcon"; + public static final int MAX_LAYERS = 3; + + private final INbt customData; + + // Shared across all layers + public int width = 32; + public int height = 32; + public float scale = 1.0f; + + /** Whether a custom icon override is active. Default false = use type's default icon. */ + private boolean enabled = false; + + /** Number of active layers (1-3). */ + private int layerCount = 1; + + /** Layer data (always 3 slots allocated). */ + private Layer[] layers = { new Layer(), new Layer(), new Layer() }; + + /** Per-state icon UV overrides for layer 0 (0-indexed, maps to toggle states 1, 2, ...). */ + private int[][] stateIcons = null; + + // Animation + private boolean animated = false; + private int frameCount = 1; + private int frametime = 2; + + // ═══════════════════════════════════════════════════════════════════ + // LAYER INNER CLASS + // ═══════════════════════════════════════════════════════════════════ + + public static class Layer { + public String texture = ""; + public int iconX = 0; + public int iconY = 0; + public int tintColor = 0xFFFFFF; + + public Layer() {} + + public boolean hasTexture() { + return texture != null && !texture.isEmpty(); + } + + public void writeToNBT(INbt nbt) { + nbt.setString("Texture", texture); + nbt.setInteger("IconX", iconX); + nbt.setInteger("IconY", iconY); + nbt.setInteger("TintColor", tintColor); + } + + public void readFromNBT(INbt nbt) { + texture = nbt.getString("Texture"); + iconX = nbt.getInteger("IconX"); + iconY = nbt.getInteger("IconY"); + tintColor = nbt.hasKey("TintColor") ? nbt.getInteger("TintColor") : 0xFFFFFF; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTION + // ═══════════════════════════════════════════════════════════════════ + + private AbilityIconData(INbt customData) { + this.customData = customData; + } + + /** + * Create an AbilityIconData instance from a raw customData compound. + * Reads existing values if present, otherwise uses defaults. + */ + public static AbilityIconData fromCustomData(INbt customData) { + AbilityIconData icon = new AbilityIconData(customData); + if (customData.hasKey(NBT_KEY)) { + icon.readFromNBT(customData.getCompoundTag(NBT_KEY)); + } + return icon; + } + + /** + * Create an AbilityIconData instance from an ability's customData. + */ + public static AbilityIconData fromAbility(Ability ability) { + return fromCustomData(ability.getCustomData()); + } + + /** + * Create an AbilityIconData instance from a chained ability's customData. + */ + public static AbilityIconData fromChainedAbility(ChainedAbility chain) { + return fromCustomData(chain.getCustomData()); + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Write current state back to the customData NBT. + */ + public void save() { + INbt tag = new INbt(); + writeToNBT(tag); + customData.setTag(NBT_KEY, tag); + } + + public void writeToNBT(INbt nbt) { + nbt.setBoolean("Enabled", enabled); + nbt.setInteger("Width", width); + nbt.setInteger("Height", height); + nbt.setFloat("Scale", scale); + nbt.setInteger("LayerCount", layerCount); + + INbtList layerList = new INbtList(); + for (int i = 0; i < MAX_LAYERS; i++) { + INbt layerNBT = new INbt(); + layers[i].writeToNBT(layerNBT); + layerList.appendTag(layerNBT); + } + nbt.setTag("Layers", layerList); + + nbt.setBoolean("Animated", animated); + nbt.setInteger("FrameCount", frameCount); + nbt.setInteger("FrameTime", frametime); + + if (stateIcons != null && stateIcons.length > 0) { + INbtList list = new INbtList(); + for (int[] pair : stateIcons) { + INbt comp = new INbt(); + comp.setInteger("IconX", pair[0]); + comp.setInteger("IconY", pair[1]); + list.appendTag(comp); + } + nbt.setTag("StateIcons", list); + } + } + + public void readFromNBT(INbt nbt) { + enabled = nbt.getBoolean("Enabled"); + width = nbt.getInteger("Width"); + if (width <= 0) width = 32; + height = nbt.getInteger("Height"); + if (height <= 0) height = 32; + scale = nbt.getFloat("Scale"); + if (scale <= 0) scale = 1.0f; + + layerCount = nbt.getInteger("LayerCount"); + if (layerCount < 1) layerCount = 1; + if (layerCount > MAX_LAYERS) layerCount = MAX_LAYERS; + + if (nbt.hasKey("Animated")) + animated = nbt.getBoolean("Animated"); + if (nbt.hasKey("FrameCount")) + frameCount = Math.max(1, nbt.getInteger("FrameCount")); + if (nbt.hasKey("FrameTime")) + frametime = Math.max(1, nbt.getInteger("FrameTime")); + + if (nbt.hasKey("Layers")) { + INbtList layerList = nbt.getTagList("Layers", 10); + for (int i = 0; i < MAX_LAYERS; i++) { + layers[i] = new Layer(); + if (i < layerList.tagCount()) { + layers[i].readFromNBT(layerList.getCompoundTagAt(i)); + } + } + } + + if (nbt.hasKey("StateIcons")) { + INbtList list = nbt.getTagList("StateIcons", 10); + stateIcons = new int[list.tagCount()][2]; + for (int i = 0; i < list.tagCount(); i++) { + INbt comp = list.getCompoundTagAt(i); + stateIcons[i][0] = comp.getInteger("IconX"); + stateIcons[i][1] = comp.getInteger("IconY"); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // LAYER API + // ═══════════════════════════════════════════════════════════════════ + + public int getLayerCount() { + return layerCount; + } + + public void setLayerCount(int count) { + this.layerCount = Math.max(1, Math.min(MAX_LAYERS, count)); + save(); + } + + public Layer getLayer(int index) { + return (index >= 0 && index < MAX_LAYERS) ? layers[index] : layers[0]; + } + + public void setLayerTexture(int index, String tex) { + if (index >= 0 && index < MAX_LAYERS) { + layers[index].texture = tex != null ? tex : ""; + save(); + } + } + + public void setLayerIconX(int index, int x) { + if (index >= 0 && index < MAX_LAYERS) { + layers[index].iconX = Math.max(0, x); + save(); + } + } + + public void setLayerIconY(int index, int y) { + if (index >= 0 && index < MAX_LAYERS) { + layers[index].iconY = Math.max(0, y); + save(); + } + } + + public void setLayerTintColor(int index, int color) { + if (index >= 0 && index < MAX_LAYERS) { + layers[index].tintColor = color & 0xFFFFFF; + save(); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // SHARED IConfigProperty GETTERS/SETTERS (with auto-save) + // ═══════════════════════════════════════════════════════════════════ + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + save(); + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = Math.max(1, width); + save(); + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = Math.max(1, height); + save(); + } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = Math.max(0.1f, scale); + save(); + } + + // ═══════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════ + + public boolean isAnimated() { + return animated; + } + + public void setAnimated(boolean animated) { + this.animated = animated; + save(); + } + + public int getFrameCount() { + return frameCount; + } + + public void setFrameCount(int frameCount) { + this.frameCount = Math.max(1, frameCount); + save(); + } + + public int getFrameTime() { + return frametime; + } + + public void setFrameTime(int frametime) { + this.frametime = Math.max(1, frametime); + save(); + } + + // ═══════════════════════════════════════════════════════════════════ + // PER-STATE ICON OVERRIDES (applies to layer 0) + // ═══════════════════════════════════════════════════════════════════ + + public void setStateIcons(int[][] stateIcons) { + this.stateIcons = stateIcons; + save(); + } + + /** + * Get iconX for a specific toggle state (layer 0). + * Falls back to layer 0 iconX if no state override exists. + * @param state 1-indexed toggle state (0 = off/default) + */ + public int getIconXForState(int state) { + if (state > 0 && stateIcons != null && state - 1 < stateIcons.length) { + return stateIcons[state - 1][0]; + } + return layers[0].iconX; + } + + /** + * Get iconY for a specific toggle state (layer 0). + * Falls back to layer 0 iconY if no state override exists. + * @param state 1-indexed toggle state (0 = off/default) + */ + public int getIconYForState(int state) { + if (state > 0 && stateIcons != null && state - 1 < stateIcons.length) { + return stateIcons[state - 1][1]; + } + return layers[0].iconY; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java new file mode 100644 index 000000000..accc2030e --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java @@ -0,0 +1,567 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import noppes.npcs.controllers.data.ChainedAbilityScript; +import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; +import noppes.npcs.api.ability.IChainedAbility; +import noppes.npcs.controllers.data.IScriptUnit; +import kamkeel.npcs.util.FileNameHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * A chained ability is an ordered sequence of ability references that execute + * one after another. It has its own conditions, cooldown, weight, and per-entry + * delay IConfiguration. Individual ability conditions are skipped during chain execution. + *

+ * All entries must be ability references (by name/key). Inline abilities are not supported. + * When saving, all references are validated against {@link AbilityController}. + */ +public class ChainedAbility implements IChainedAbility, IAbilityAction { + + // ═══════════════════════════════════════════════════════════════════ + // IConfiguration (saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + private String id = ""; // UUID (stable persistent reference) + private String name = ""; // Unique file key / identifier + private String displayName = ""; // Cosmetic name (falls back to name when empty) + + private boolean enabled = true; + + /** + * Weight for NPC weighted random selection. + */ + private int weight = 10; + + /** + * Whether each ability in the chain uses its own windup phase. + */ + private boolean windUpAll = true; + + /** + * Chain-level cooldown in ticks (applied after entire chain completes). + */ + private int cooldownTicks = 100; + + /** + * If true, only this chain goes on cooldown (not the global cooldown). + */ + private boolean perAbilityCooldown = false; + + /** + * If true, skip cooldown entirely after chain completes. + */ + private boolean ignoreCooldown = false; + + /** + * Minimum range for NPC eligibility check. + */ + private float minRange = 0; + + /** + * Maximum range for NPC eligibility check. + */ + private float maxRange = 20; + + /** + * Chain-level conditions (individual ability conditions are ignored). + */ + private List conditions = new ArrayList<>(); + + /** + * Ordered list of ability entries to execute. + */ + private List entries = new ArrayList<>(); + + /** + * Extension data for external mods (e.g., icons, DBC stats). + */ + private INbt customData = new INbt(); + + // Per-instance script (isolated per execution, not saved to NBT) + private transient ChainedAbilityScript instanceScript; + + public ChainedAbility() { + } + + public ChainedAbility(String name) { + setName(name); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id != null ? id : ""; + } + + public String getName() { + return name; + } + + public void setName(String name) { + String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Chain"; + this.name = FileNameHelper.sanitizeName(name, fallback); + } + + /** + * Get the display name for this chained ability. + * Returns displayName if set, otherwise falls back to name. + * Converts & color codes to § for rendering. + */ + public String getDisplayName() { + String result = (displayName != null && !displayName.isEmpty()) + ? displayName + : FileNameHelper.toDisplayName(name); + return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : ""; + } + + public String getRawDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName != null ? displayName : ""; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = Math.max(1, weight); + } + + public boolean isWindUpAll() { + return windUpAll; + } + + public void setWindUpAll(boolean windUpAll) { + this.windUpAll = windUpAll; + } + + public int getCooldownTicks() { + return cooldownTicks; + } + + public void setCooldownTicks(int cooldownTicks) { + this.cooldownTicks = Math.max(0, cooldownTicks); + } + + public boolean isPerAbilityCooldown() { + return perAbilityCooldown; + } + + public void setPerAbilityCooldown(boolean perAbilityCooldown) { + this.perAbilityCooldown = perAbilityCooldown; + } + + public boolean isIgnoreCooldown() { + return ignoreCooldown; + } + + public void setIgnoreCooldown(boolean ignoreCooldown) { + this.ignoreCooldown = ignoreCooldown; + } + + public float getMinRange() { + return minRange; + } + + public void setMinRange(float minRange) { + this.minRange = Math.max(0, minRange); + } + + public float getMaxRange() { + return maxRange; + } + + public void setMaxRange(float maxRange) { + this.maxRange = Math.max(0, maxRange); + } + + /** + * Compute the allowed user type from child abilities. + * A chain allows players if ALL resolved entries allow players. + * A chain allows NPCs if ALL resolved entries allow NPCs. + * Unresolvable entries are skipped (don't restrict). + */ + public UserType getAllowedBy() { + if (entries.isEmpty()) return UserType.BOTH; + boolean allAllowPlayer = true; + boolean allAllowNpc = true; + for (ChainedAbilityEntry entry : entries) { + Ability a = entry.resolve(); + if (a == null) continue; + UserType ut = a.getAllowedBy(); + if (!ut.allowsPlayer()) allAllowPlayer = false; + if (!ut.allowsNpc()) allAllowNpc = false; + } + if (allAllowPlayer && allAllowNpc) return UserType.BOTH; + if (allAllowPlayer) return UserType.PLAYER_ONLY; + if (allAllowNpc) return UserType.NPC_ONLY; + return UserType.NONE; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions != null ? conditions : new ArrayList<>(); + } + + public List getEntries() { + return entries; + } + + public void setEntries(List entries) { + this.entries = entries != null ? entries : new ArrayList<>(); + } + + public INbt getCustomData() { + return customData; + } + + // ═══════════════════════════════════════════════════════════════════ + // IChainedAbility INTERFACE + // ═══════════════════════════════════════════════════════════════════ + + @Override + public int getEntryCount() { + return entries.size(); + } + + @Override + public String getEntryReference(int index) { + if (index < 0 || index >= entries.size()) return null; + ChainedAbilityEntry entry = entries.get(index); + if (entry.isInline()) { + Ability a = entry.getInlineAbility(); + return a != null ? a.getName() : ""; + } + return entry.getAbilityReference(); + } + + @Override + public boolean isEntryInline(int index) { + if (index < 0 || index >= entries.size()) return false; + return entries.get(index).isInline(); + } + + @Override + public int getEntryDelay(int index) { + if (index < 0 || index >= entries.size()) return 0; + return entries.get(index).getDelayTicks(); + } + + // ═══════════════════════════════════════════════════════════════════ + // ENTRY MANAGEMENT + // ═══════════════════════════════════════════════════════════════════ + + public void addEntry(ChainedAbilityEntry entry) { + if (entry != null) { + entries.add(entry); + } + } + + public void addEntry(String abilityReference, int delayTicks) { + entries.add(new ChainedAbilityEntry(abilityReference, delayTicks)); + } + + public void removeEntry(int index) { + if (index >= 0 && index < entries.size()) { + entries.remove(index); + } + } + + public void moveEntryUp(int index) { + if (index > 0 && index < entries.size()) { + ChainedAbilityEntry entry = entries.remove(index); + entries.add(index - 1, entry); + } + } + + public void moveEntryDown(int index) { + if (index >= 0 && index < entries.size() - 1) { + ChainedAbilityEntry entry = entries.remove(index); + entries.add(index + 1, entry); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // CONDITIONS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check all chain-level conditions against the caster and target. + */ + public boolean checkConditions(IEntityLivingBase caster, IEntityLivingBase target) { + for (AbilityCondition c : conditions) { + if (!c.getUserType().allowsNpc()) continue; + Boolean extendedCondition = AbilityController.Instance.fireCheckConditions(c, caster, target); + + if (extendedCondition != null) { + if (!extendedCondition) return false; + continue; + } + + if (!c.check(caster, target)) return false; + } + return true; + } + + /** + * Check conditions for player use (skip target-requiring conditions). + */ + public boolean checkConditionsForPlayer(IEntityLivingBase caster) { + for (AbilityCondition c : conditions) { + if (!c.getUserType().allowsPlayer()) continue; + if (c.requiresTarget()) continue; + Boolean extendedCondition = AbilityController.Instance.fireCheckConditionsForPlayer(c, caster); + + if (extendedCondition != null) { + if (!extendedCondition) return false; + continue; + } + + if (!c.check(caster, null)) return false; + } + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // VALIDATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Validate this chained ability. Returns a list of error messages. + * An empty list means valid. + */ + public List validate() { + List errors = new ArrayList<>(); + + if (name == null || name.isEmpty()) { + errors.add("Name is required"); + } + + if (entries.isEmpty()) { + errors.add("At least one ability entry is required"); + } + + AbilityController controller = AbilityController.Instance; + if (controller != null) { + for (int i = 0; i < entries.size(); i++) { + ChainedAbilityEntry entry = entries.get(i); + if (entry.isInline()) { + if (entry.getInlineAbility() == null) { + errors.add("Entry " + (i + 1) + ": inline ability is null"); + } + } else { + String ref = entry.getAbilityReference(); + if (ref == null || ref.isEmpty()) { + errors.add("Entry " + (i + 1) + ": ability reference is empty"); + } else if (!controller.canResolveAbility(ref)) { + errors.add("Entry " + (i + 1) + ": cannot resolve ability '" + ref + "'"); + } + } + } + } + + return errors; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEEP COPY + // ═══════════════════════════════════════════════════════════════════ + + /** + * Create a deep copy via NBT round-trip. + */ + public ChainedAbility deepCopy() { + ChainedAbility copy = new ChainedAbility(); + copy.readNBT(this.writeNBT(false)); + return copy; + } + + @Override + public boolean isChain() { + return true; + } + + @Override + public IAbilityAction deepCopyAction() { + return deepCopy(); + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + public INbt writeNBT(boolean saveScripts) { + INbt nbt = new INbt(); + nbt.setString("Id", id); + nbt.setString("Name", name); + nbt.setString("DisplayName", displayName); + nbt.setBoolean("Enabled", enabled); + nbt.setInteger("Weight", weight); + nbt.setBoolean("WindUpAll", windUpAll); + nbt.setInteger("CooldownTicks", cooldownTicks); + nbt.setBoolean("PerAbilityCooldown", perAbilityCooldown); + nbt.setBoolean("IgnoreCooldown", ignoreCooldown); + nbt.setFloat("MinRange", minRange); + nbt.setFloat("MaxRange", maxRange); + + // Conditions + INbtList condList = new INbtList(); + for (AbilityCondition c : conditions) { + condList.appendTag(c.writeNBT()); + } + nbt.setTag("Conditions", condList); + + // Entries + INbtList entryList = new INbtList(); + for (ChainedAbilityEntry entry : entries) { + entryList.appendTag(entry.writeNBT(saveScripts)); + } + nbt.setTag("Entries", entryList); + + // Custom data (for external mods) + nbt.setTag("customData", (INbt) customData.copy()); + + // Script handler data (matching CustomEffect.writeToNBT pattern) + if (saveScripts) { + ChainedAbilityScript handler = getScriptHandler(); + if (handler != null) { + INbt scriptData = new INbt(); + handler.writeToNBT(scriptData); + nbt.setTag("ScriptData", scriptData); + } + } + + return nbt; + } + + public void readNBT(INbt nbt) { + id = nbt.getString("Id"); + setName(nbt.getString("Name")); + displayName = nbt.getString("DisplayName"); + enabled = !nbt.hasKey("Enabled") || nbt.getBoolean("Enabled"); + weight = nbt.hasKey("Weight") ? nbt.getInteger("Weight") : 10; + windUpAll = !nbt.hasKey("WindUpAll") || nbt.getBoolean("WindUpAll"); + cooldownTicks = nbt.hasKey("CooldownTicks") ? nbt.getInteger("CooldownTicks") : 100; + perAbilityCooldown = nbt.getBoolean("PerAbilityCooldown"); + ignoreCooldown = nbt.getBoolean("IgnoreCooldown"); + minRange = nbt.getFloat("MinRange"); + maxRange = nbt.hasKey("MaxRange") ? nbt.getFloat("MaxRange") : 20; + // AllowedBy is computed from child abilities + + // Conditions + conditions.clear(); + INbtList condList = nbt.getTagList("Conditions", 10); + for (int i = 0; i < condList.tagCount(); i++) { + AbilityCondition c = AbilityCondition.fromNBT(condList.getCompoundTagAt(i)); + if (c != null) { + conditions.add(c); + } + } + + // Entries + entries.clear(); + INbtList entryList = nbt.getTagList("Entries", 10); + for (int i = 0; i < entryList.tagCount(); i++) { + ChainedAbilityEntry entry = ChainedAbilityEntry.fromNBT(entryList.getCompoundTagAt(i)); + if (entry != null) { + entries.add(entry); + } + } + + // Custom data (for external mods) + customData = (INbt) nbt.getCompoundTag("customData").copy(); + + // Script handler data (matching CustomEffect.readFromNBT pattern) + if (nbt.hasKey("ScriptData", 10)) { + if (getScriptHandler() == null) { + ChainedAbilityScript handler = new ChainedAbilityScript(this.id); + handler.readFromNBT(nbt.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // SCRIPT HANDLER + // ═══════════════════════════════════════════════════════════════════ + + public ChainedAbilityScript getScriptHandler() { + return AbilityController.Instance.chainedAbilityScriptHandlers.get(this.id); + } + + public void setScriptHandler(ChainedAbilityScript handler) { + AbilityController.Instance.chainedAbilityScriptHandlers.put(this.id, handler); + } + + public ChainedAbilityScript getOrCreateScriptHandler() { + ChainedAbilityScript handler = getScriptHandler(); + if (handler == null) { + handler = new ChainedAbilityScript(this.id); + AbilityController.Instance.chainedAbilityScriptHandlers.put(this.id, handler); + } + return handler; + } + + /** + * Get or create a per-instance script handler for this chain execution. + * Shares the template's engine but creates isolated Bindings for variable state. + * Returns null if no script is configured on the template. + */ + public ChainedAbilityScript getOrCreateInstanceScript() { + if (instanceScript != null) return instanceScript; + + ChainedAbilityScript template = getScriptHandler(); + if (template == null || !template.getEnabled() || template.container == null) return null; + + instanceScript = new ChainedAbilityScript(this.id); + instanceScript.setLanguage(template.getLanguage()); + instanceScript.setEnabled(true); + + IScriptUnit clone = template.container.createInstanceScope(instanceScript); + instanceScript.addScriptUnit(clone); + + return instanceScript; + } + + /** + * Clear the per-instance script handler (call on chain completion/reset). + */ + public void clearInstanceScript() { + this.instanceScript = null; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java new file mode 100644 index 000000000..214c5c064 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java @@ -0,0 +1,70 @@ +package kamkeel.npcs.controllers.data.ability.data; + + +import java.util.List; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityAction; +import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +/** + * Common interface for all combat actions that can be assigned to NPCs or players. + * Implemented by {@link Ability} (individual ability) and {@link ChainedAbility} (sequence). + *

+ * This enables unified storage via {@link AbilityAction}, unified weighted random selection, + * and unified eligibility checking without instanceof or separate codepaths. + */ +public interface IAbilityAction { + + String getName(); + + boolean isEnabled(); + + void setEnabled(boolean enabled); + + int getWeight(); + + int getCooldownTicks(); + + float getMinRange(); + + float getMaxRange(); + + UserType getAllowedBy(); + + List getConditions(); + + boolean checkConditions(IEntityLivingBase caster, IEntityLivingBase target); + + boolean checkConditionsForPlayer(IEntityLivingBase caster); + + /** + * Check if this action is available for the given player. + * Used for visibility filtering (HUD, selection) and activation gating. + * Returns true by default (no requirement). + */ + default boolean isAvailableFor(IPlayer player) { + return true; + } + + /** + * Whether this action is a chained ability (sequence of abilities) vs an individual ability. + */ + boolean isChain(); + + /** + * Create a deep copy of this action. + */ + IAbilityAction deepCopyAction(); + + INbt writeNBT(boolean saveScripts); +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java index 5129032c3..3c0c2c2c1 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java @@ -1,9 +1,18 @@ package kamkeel.npcs.controllers.data.ability.data; -import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint; + import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; /** * Per-projectile data: anchor point + optional color override. @@ -26,7 +35,7 @@ public class ProjectileData { * If colorOverride is true, returns a copy with overridden inner/outer colors. */ public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) { - // Always return a copy so entity mutations never bleed back into the ability template. + // Always return a copy so IEntity mutations never bleed back into the ability template. EnergyDisplayData resolved = primary.copy(); if (colorOverride) { resolved.innerColor = this.innerColor; diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityCustomEffect.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityCustomEffect.java new file mode 100644 index 000000000..7628645ce --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityCustomEffect.java @@ -0,0 +1,118 @@ +package kamkeel.npcs.controllers.data.ability.data.effect; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.util.ValueUtil; + +/** + * Represents a custom effect reference that can be applied by abilities. + * Wraps the CustomEffectController system (player-only effects with script callbacks). + */ +public class AbilityCustomEffect { + + private int effectId = -1; + private int durationTicks = 60; + private byte level = 0; + private int index = 0; + + public AbilityCustomEffect() { + } + + public AbilityCustomEffect(int effectId, int durationTicks, byte level) { + this.effectId = effectId; + this.durationTicks = Math.max(1, durationTicks); + this.level = (byte) Math.max(0, Math.min(10, level)); + this.index = 0; + } + + public AbilityCustomEffect(int effectId, int durationTicks, byte level, int index) { + this(effectId, durationTicks, level); + this.index = index; + } + + public AbilityCustomEffect copy() { + return new AbilityCustomEffect(effectId, durationTicks, level, index); + } + + /** + * Applies this custom effect to the given IEntity. + * Only works on players (CustomEffectController is player-only). + */ + public void apply(IEntityLivingBase IEntity) { + if (IEntity == null || effectId <= 0) return; + if (IEntity instanceof IPlayer) { + CustomEffectController.getInstance().applyEffect( + (IPlayer) IEntity, effectId, durationTicks, level, index); + } + } + + public boolean isValid() { + return effectId > 0; + } + + // ── NBT ── + + public INbt writeNBT() { + INbt nbt = new INbt(); + nbt.setInteger("effectId", effectId); + nbt.setInteger("duration", durationTicks); + nbt.setByte("level", level); + nbt.setInteger("index", index); + return nbt; + } + + public void readNBT(INbt nbt) { + this.effectId = nbt.getInteger("effectId"); + this.durationTicks = nbt.getInteger("duration"); + this.level = nbt.getByte("level"); + this.index = nbt.getInteger("index"); + } + + public static AbilityCustomEffect fromNBT(INbt nbt) { + AbilityCustomEffect e = new AbilityCustomEffect(); + e.readNBT(nbt); + return e; + } + + // ── Getters/Setters ── + + public int getEffectId() { + return effectId; + } + + public void setEffectId(int effectId) { + this.effectId = effectId; + } + + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + public byte getLevel() { + return level; + } + + public void setLevel(byte level) { + this.level = (byte) ValueUtil.clamp(level, 0, 10); + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = Math.max(0, index); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java new file mode 100644 index 000000000..a09decc61 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java @@ -0,0 +1,138 @@ +package kamkeel.npcs.controllers.data.ability.data.effect; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.EnumPotionType; + +/** + * Represents a configurable IPotionType effect that can be applied by abilities. + * Uses the unified EnumPotionType enum shared across melee, ranged, and ability systems. + */ +public class AbilityPotionEffect { + + private EnumPotionType type = EnumPotionType.None; + private int manualPotionId = 0; + private int durationTicks = 60; + private int amplifier = 0; // 0-255 (Manual), 0-10 (preset) + + public AbilityPotionEffect() { + } + + public AbilityPotionEffect(EnumPotionType type, int durationTicks, int amplifier) { + this.type = type; + this.durationTicks = Math.max(1, durationTicks); + this.amplifier = Math.max(0, Math.min(255, amplifier)); + } + + /** + * Creates a copy of this effect. + */ + public AbilityPotionEffect copy() { + AbilityPotionEffect copy = new AbilityPotionEffect(type, durationTicks, amplifier); + copy.manualPotionId = this.manualPotionId; + return copy; + } + + /** + * Applies this effect to the given IEntity. + * Does nothing if type is None. Validates IPotionType IDs before applying. + */ + public void apply(IEntityLivingBase IEntity) { + if (IEntity == null || type == EnumPotionType.None) { + return; + } + + if (type == EnumPotionType.Fire) { + IEntity.setFire(Math.max(1, durationTicks / 20)); + return; + } + + int potionId = type.getResolvedPotionId(manualPotionId); + if (EnumPotionType.isValidPotionId(potionId)) { + IEntity.addPotionEffect(new IPotionEffect(potionId, durationTicks, amplifier)); + } + } + + /** + * Writes this effect to NBT. + */ + public INbt writeNBT() { + INbt nbt = new INbt(); + nbt.setInteger("potionType", type.ordinal()); + nbt.setInteger("duration", durationTicks); + nbt.setInteger("amplifier", amplifier); + if (type == EnumPotionType.Manual) { + nbt.setInteger("manualPotionId", manualPotionId); + } + return nbt; + } + + /** + * Reads this effect from NBT. + */ + public void readNBT(INbt nbt) { + this.type = EnumPotionType.fromOrdinal(nbt.getInteger("potionType")); + this.durationTicks = nbt.getInteger("duration"); + this.amplifier = nbt.getInteger("amplifier"); + if (type == EnumPotionType.Manual) { + this.manualPotionId = nbt.getInteger("manualPotionId"); + } + } + + /** + * Creates an AbilityPotionEffect from NBT. + */ + public static AbilityPotionEffect fromNBT(INbt nbt) { + AbilityPotionEffect effect = new AbilityPotionEffect(); + effect.readNBT(nbt); + return effect; + } + + // Getters & Setters + + public EnumPotionType getType() { + return type; + } + + public void setType(EnumPotionType type) { + this.type = type != null ? type : EnumPotionType.None; + } + + public int getManualPotionId() { + return manualPotionId; + } + + public void setManualPotionId(int manualPotionId) { + this.manualPotionId = Math.max(0, manualPotionId); + } + + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + public int getAmplifier() { + return amplifier; + } + + public void setAmplifier(int amplifier) { + this.amplifier = Math.max(0, Math.min(255, amplifier)); + } + + /** + * Returns true if this effect is configured (not None). + */ + public boolean isValid() { + return type != EnumPotionType.None; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/IEffectAction.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/IEffectAction.java new file mode 100644 index 000000000..8d49d32a0 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/IEffectAction.java @@ -0,0 +1,50 @@ +package kamkeel.npcs.controllers.data.ability.data.effect; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +/** + * Interface for mod-registered effect actions that can be applied by AbilityEffect. + *

+ * Mods register implementations via {@code Register.EffectActions} or + * {@code AbilityController.Instance.registerEffectAction()}. + * Each action has a unique ID, display name, and apply logic. + * Actions can optionally provide custom IConfiguration fields for the GUI. + */ +public interface IEffectAction { + + /** + * Unique identifier, e.g. "dbc:kiRestore", "mymod:buffSpeed". + */ + String getId(); + + /** + * Localized display name shown in the GUI selector. + */ + String getDisplayName(); + + /** + * Apply this action to a target IEntity. + * Called once per affected IEntity during ability execution. + * + * @param caster The IEntity that used the ability + * @param target The IEntity being affected + * @param config Action-specific IConfiguration NBT (from GUI) + */ + void apply(IEntityLivingBase caster, IEntityLivingBase target, INbt config); + + /** + * Create default IConfiguration NBT for a new instance of this action. + */ + default INbt createDefaultConfig() { + return new INbt(); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java index b269cd11f..b3148f22b 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java @@ -1,11 +1,19 @@ package kamkeel.npcs.controllers.data.ability.data.energy; -import noppes.npcs.core.NBT; -import noppes.npcs.api.INbt; import java.util.HashMap; import java.util.Map; import java.util.Set; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.core.NBT; /** * Groups barrier properties shared by energy barrier abilities (Dome, Wall, Shield). @@ -37,7 +45,7 @@ public class EnergyBarrierData { // Absorb radius: max distance from barrier's current position for absorption to work // -1 = no radius limit (absorb regardless of distance) // 0 = use barrier's own extent as the radius (dome radius, panel half-size, etc.) - // positive = owner must be within this many blocks of the barrier entity + // positive = owner must be within this many blocks of the barrier IEntity public float absorbRadius = 0.0f; // Melee: barrier can be hit by melee attacks diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java index 7e5a50f76..e9593d877 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java @@ -1,15 +1,24 @@ package kamkeel.npcs.controllers.data.ability.data.energy; + import kamkeel.npcs.controllers.data.ability.enums.HitType; import noppes.npcs.api.ability.data.IEnergyCombatData; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; /** * Groups combat properties shared by energy projectile abilities. - * Used as a parameter object for entity constructors and ability configuration. + * Used as a parameter object for IEntity constructors and ability IConfiguration. */ public class EnergyCombatData implements IEnergyCombatData { - public static final float MAX_EXPLOSION_RADIUS = 15.0f; + public static final float MAX_IExplosion_RADIUS = 15.0f; public static final int DEFAULT_MAX_HITS = 5; public static final int MAX_HITS = 200; @@ -17,8 +26,8 @@ public class EnergyCombatData implements IEnergyCombatData { public float knockback = 1.0f; public float knockbackUp = 0.1f; public boolean explosive = false; - public float explosionRadius = 3.0f; - public float explosionDamageFalloff = 0.5f; + public float IExplosionRadius = 3.0f; + public float IExplosionDamageFalloff = 0.5f; public HitType hitType = HitType.SINGLE; public int multiHitDelayTicks = 5; public int maxHits = DEFAULT_MAX_HITS; @@ -27,33 +36,33 @@ public class EnergyCombatData implements IEnergyCombatData { } public EnergyCombatData(float damage, float knockback, float knockbackUp, - boolean explosive, float explosionRadius, float explosionDamageFalloff, + boolean explosive, float IExplosionRadius, float IExplosionDamageFalloff, HitType hitType, int multiHitDelay) { - this(damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, hitType, multiHitDelay, DEFAULT_MAX_HITS); + this(damage, knockback, knockbackUp, explosive, IExplosionRadius, IExplosionDamageFalloff, hitType, multiHitDelay, DEFAULT_MAX_HITS); } public EnergyCombatData(float damage, float knockback, float knockbackUp, - boolean explosive, float explosionRadius, float explosionDamageFalloff, + boolean explosive, float IExplosionRadius, float IExplosionDamageFalloff, HitType hitType, int multiHitDelay, int maxHits) { this.damage = damage; this.knockback = knockback; this.knockbackUp = knockbackUp; this.explosive = explosive; - this.explosionRadius = clampExplosionRadius(explosionRadius); - this.explosionDamageFalloff = explosionDamageFalloff; + this.IExplosionRadius = clampIExplosionRadius(IExplosionRadius); + this.IExplosionDamageFalloff = IExplosionDamageFalloff; this.hitType = hitType; this.multiHitDelayTicks = multiHitDelay; this.maxHits = clampMaxHits(maxHits); } public EnergyCombatData(float damage, float knockback, float knockbackUp, - boolean explosive, float explosionRadius, float explosionDamageFalloff) { + boolean explosive, float IExplosionRadius, float IExplosionDamageFalloff) { this.damage = damage; this.knockback = knockback; this.knockbackUp = knockbackUp; this.explosive = explosive; - this.explosionRadius = clampExplosionRadius(explosionRadius); - this.explosionDamageFalloff = explosionDamageFalloff; + this.IExplosionRadius = clampIExplosionRadius(IExplosionRadius); + this.IExplosionDamageFalloff = IExplosionDamageFalloff; } @Override @@ -97,23 +106,23 @@ public class EnergyCombatData implements IEnergyCombatData { } @Override - public float getExplosionRadius() { - return explosionRadius; + public float getIExplosionRadius() { + return IExplosionRadius; } @Override - public void setExplosionRadius(float explosionRadius) { - this.explosionRadius = clampExplosionRadius(explosionRadius); + public void setIExplosionRadius(float IExplosionRadius) { + this.IExplosionRadius = clampIExplosionRadius(IExplosionRadius); } @Override - public float getExplosionDamageFalloff() { - return explosionDamageFalloff; + public float getIExplosionDamageFalloff() { + return IExplosionDamageFalloff; } @Override - public void setExplosionDamageFalloff(float explosionDamageFalloff) { - this.explosionDamageFalloff = explosionDamageFalloff; + public void setIExplosionDamageFalloff(float IExplosionDamageFalloff) { + this.IExplosionDamageFalloff = IExplosionDamageFalloff; } public int getMaxHits() { @@ -129,8 +138,8 @@ public class EnergyCombatData implements IEnergyCombatData { nbt.setFloat("knockback", knockback); nbt.setFloat("knockbackUp", knockbackUp); nbt.setBoolean("explosive", explosive); - nbt.setFloat("explosionRadius", explosionRadius); - nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + nbt.setFloat("IExplosionRadius", IExplosionRadius); + nbt.setFloat("IExplosionDamageFalloff", IExplosionDamageFalloff); nbt.setInteger("hitType", hitType.ordinal()); nbt.setInteger("multiHitDelayTicks", multiHitDelayTicks); nbt.setInteger("maxHits", maxHits); @@ -141,8 +150,8 @@ public class EnergyCombatData implements IEnergyCombatData { knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; - explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + IExplosionRadius = nbt.hasKey("IExplosionRadius") ? nbt.getFloat("IExplosionRadius") : 3.0f; + IExplosionDamageFalloff = nbt.hasKey("IExplosionDamageFalloff") ? nbt.getFloat("IExplosionDamageFalloff") : 0.5f; hitType = HitType.fromOrdinal(nbt.hasKey("hitType") ? nbt.getInteger("hitType") : 0); multiHitDelayTicks = nbt.hasKey("multiHitDelayTicks") ? nbt.getInteger("multiHitDelayTicks") : 5; maxHits = nbt.hasKey("maxHits") ? nbt.getInteger("maxHits") : DEFAULT_MAX_HITS; @@ -150,15 +159,15 @@ public class EnergyCombatData implements IEnergyCombatData { // Sanitize if (Float.isNaN(damage) || Float.isInfinite(damage)) damage = 7.0f; if (Float.isNaN(knockback) || Float.isInfinite(knockback) || knockback < 0) knockback = 1.0f; - if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius) || explosionRadius < 0) explosionRadius = 3.0f; - explosionRadius = clampExplosionRadius(explosionRadius); + if (Float.isNaN(IExplosionRadius) || Float.isInfinite(IExplosionRadius) || IExplosionRadius < 0) IExplosionRadius = 3.0f; + IExplosionRadius = clampIExplosionRadius(IExplosionRadius); if (multiHitDelayTicks < 1) multiHitDelayTicks = 1; maxHits = clampMaxHits(maxHits); } - private static float clampExplosionRadius(float explosionRadius) { - if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius)) return 0.0f; - return Math.max(0.0f, Math.min(MAX_EXPLOSION_RADIUS, explosionRadius)); + private static float clampIExplosionRadius(float IExplosionRadius) { + if (Float.isNaN(IExplosionRadius) || Float.isInfinite(IExplosionRadius)) return 0.0f; + return Math.max(0.0f, Math.min(MAX_IExplosion_RADIUS, IExplosionRadius)); } private static int clampMaxHits(int maxHits) { @@ -167,7 +176,7 @@ public class EnergyCombatData implements IEnergyCombatData { public EnergyCombatData copy() { EnergyCombatData copy = new EnergyCombatData(damage, knockback, knockbackUp, - explosive, explosionRadius, explosionDamageFalloff); + explosive, IExplosionRadius, IExplosionDamageFalloff); copy.hitType = hitType; copy.multiHitDelayTicks = multiHitDelayTicks; copy.maxHits = maxHits; diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java index 0a2cad5a2..02796910e 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java @@ -1,11 +1,20 @@ package kamkeel.npcs.controllers.data.ability.data.energy; + import noppes.npcs.api.ability.data.IEnergyDisplayData; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; /** * Groups visual color properties shared by all energy abilities. - * Used as a parameter object for entity constructors and ability configuration. + * Used as a parameter object for IEntity constructors and ability IConfiguration. */ public class EnergyDisplayData implements IEnergyDisplayData { public int innerColor = 0xFFFFFF; diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLifespanData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLifespanData.java index 96f4fc396..bcc788189 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLifespanData.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLifespanData.java @@ -1,11 +1,20 @@ package kamkeel.npcs.controllers.data.ability.data.energy; + import noppes.npcs.api.ability.data.IEnergyLifespanData; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; /** * Groups lifespan/range properties for energy projectile abilities. - * Used as a parameter object for entity constructors and ability configuration. + * Used as a parameter object for IEntity constructors and ability IConfiguration. */ public class EnergyLifespanData implements IEnergyLifespanData { public float maxDistance = 150.0f; diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLightningData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLightningData.java index 76e8cfb22..a604346bf 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLightningData.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyLightningData.java @@ -1,11 +1,20 @@ package kamkeel.npcs.controllers.data.ability.data.energy; + import noppes.npcs.api.ability.data.IEnergyLightningData; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; /** * Groups lightning visual effect properties for energy abilities. - * Used as a parameter object for entity constructors and ability configuration. + * Used as a parameter object for IEntity constructors and ability IConfiguration. */ public class EnergyLightningData implements IEnergyLightningData { public boolean lightningEffect = false; diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/PanelData.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/PanelData.java new file mode 100644 index 000000000..8024126d8 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/PanelData.java @@ -0,0 +1,15 @@ +package kamkeel.npcs.controllers.data.ability.data.energy; + +import noppes.npcs.api.INbt; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Panel data for energy panel entities. + */ +public class PanelData { + public int panelCount = 1; + public float panelSize = 1.0f; + public float panelSpacing = 0.5f; + public void readNBT(INbt nbt) {} + public INbt writeNBT(INbt nbt) { return nbt; } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityEffectActionEntry.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityEffectActionEntry.java new file mode 100644 index 000000000..00280aad2 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityEffectActionEntry.java @@ -0,0 +1,93 @@ +package kamkeel.npcs.controllers.data.ability.data.entry; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction; +/** + * A configured instance of an {@link IEffectAction} stored on an ability. + * Holds the action ID and its IConfiguration NBT. + */ +public class AbilityEffectActionEntry { + + private String actionId = ""; + private INbt config = new INbt(); + + public AbilityEffectActionEntry() { + } + + public AbilityEffectActionEntry(String actionId) { + this.actionId = actionId != null ? actionId : ""; + IEffectAction action = AbilityController.Instance.getEffectAction(this.actionId); + if (action != null) { + this.config = action.createDefaultConfig(); + } + } + + public AbilityEffectActionEntry copy() { + AbilityEffectActionEntry copy = new AbilityEffectActionEntry(); + copy.actionId = this.actionId; + copy.config = (INbt) this.config.copy(); + return copy; + } + + /** + * Apply this action to a target IEntity. + */ + public void apply(IEntityLivingBase caster, IEntityLivingBase target) { + if (actionId.isEmpty()) return; + IEffectAction action = AbilityController.Instance.getEffectAction(actionId); + if (action != null) { + action.apply(caster, target, config); + } + } + + public boolean isValid() { + return !actionId.isEmpty() && AbilityController.Instance.getEffectAction(actionId) != null; + } + + // ── NBT ── + + public INbt writeNBT() { + INbt nbt = new INbt(); + nbt.setString("actionId", actionId); + nbt.setTag("config", config); + return nbt; + } + + public void readNBT(INbt nbt) { + this.actionId = nbt.getString("actionId"); + this.config = nbt.hasKey("config") ? nbt.getCompoundTag("config") : new INbt(); + } + + public static AbilityEffectActionEntry fromNBT(INbt nbt) { + AbilityEffectActionEntry entry = new AbilityEffectActionEntry(); + entry.readNBT(nbt); + return entry; + } + + // ── Getters/Setters ── + + public String getActionId() { + return actionId; + } + + public void setActionId(String actionId) { + this.actionId = actionId != null ? actionId : ""; + } + + public INbt getConfig() { + return config; + } + + public void setConfig(INbt config) { + this.config = config != null ? config : new INbt(); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityToggleEntry.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityToggleEntry.java new file mode 100644 index 000000000..2e8f90c80 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/AbilityToggleEntry.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.controllers.data.ability.data.entry; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; + +/** + * Tracks the state of a single active toggle for an IEntity. + * Stored in AbstractDataAbilities.activeToggles map. + *

+ * State is 1-based: state 1 = first ON state, state 2 = second, etc. + * State 0 means OFF (entry should not exist in the map). + */ +public class AbilityToggleEntry { + private final Ability ability; + private int tickCount; + private int state; + + public AbilityToggleEntry(Ability ability, int state) { + this.ability = ability; + this.tickCount = 0; + this.state = Math.max(1, state); + } + + public Ability getAbility() { + return ability; + } + + public int getTickCount() { + return tickCount; + } + + public void incrementTick() { + tickCount++; + } + + public int getState() { + return state; + } + + public void setState(int state) { + this.state = Math.max(1, state); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java new file mode 100644 index 000000000..be6b08705 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java @@ -0,0 +1,230 @@ +package kamkeel.npcs.controllers.data.ability.data.entry; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import java.util.UUID; + +/** + * A single entry in a {@link ChainedAbility}, representing one ability + * to execute in sequence along with its delay IConfiguration. + *

+ * Entries can be either a reference to a registered ability (by name/key) + * or an inline ability that is owned directly by this entry. + */ +public class ChainedAbilityEntry { + + public enum EntryType {REFERENCE, INLINE} + + private EntryType entryType = EntryType.REFERENCE; + + /** + * Reference key (built-in name or custom ability name). Used when entryType == REFERENCE. + */ + private String abilityReference = ""; + + /** + * Inline ability data. Used when entryType == INLINE. + */ + private Ability inlineAbility = null; + + /** + * Delay in ticks BEFORE this ability starts (after previous entry completes). + */ + private int delayTicks = 0; + + /** + * Whether this entry should execute concurrently (alongside the previous ability) + * rather than sequentially. Only takes effect if the resolved ability isConcurrentCapable(). + */ + private boolean concurrentEnabled = true; + + public ChainedAbilityEntry() { + } + + public ChainedAbilityEntry(String abilityReference, int delayTicks) { + this.entryType = EntryType.REFERENCE; + this.abilityReference = abilityReference != null ? abilityReference : ""; + this.delayTicks = Math.max(0, delayTicks); + } + + // ═══════════════════════════════════════════════════════════════════ + // FACTORY METHODS + // ═══════════════════════════════════════════════════════════════════ + + public static ChainedAbilityEntry reference(String ref, int delay) { + ChainedAbilityEntry entry = new ChainedAbilityEntry(); + entry.entryType = EntryType.REFERENCE; + entry.abilityReference = ref != null ? ref : ""; + entry.delayTicks = Math.max(0, delay); + return entry; + } + + public static ChainedAbilityEntry inline(Ability ability, int delay) { + ChainedAbilityEntry entry = new ChainedAbilityEntry(); + entry.entryType = EntryType.INLINE; + entry.inlineAbility = ability; + entry.delayTicks = Math.max(0, delay); + return entry; + } + + // ═══════════════════════════════════════════════════════════════════ + // TYPE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + public EntryType getEntryType() { + return entryType; + } + + public boolean isInline() { + return entryType == EntryType.INLINE; + } + + public boolean isReference() { + return entryType == EntryType.REFERENCE; + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public String getAbilityReference() { + return abilityReference; + } + + public void setAbilityReference(String abilityReference) { + this.abilityReference = abilityReference != null ? abilityReference : ""; + } + + public Ability getInlineAbility() { + return inlineAbility; + } + + public void setInlineAbility(Ability ability) { + this.inlineAbility = ability; + } + + public int getDelayTicks() { + return delayTicks; + } + + public void setDelayTicks(int delayTicks) { + this.delayTicks = Math.max(0, delayTicks); + } + + public boolean isConcurrentEnabled() { + return concurrentEnabled; + } + + public void setConcurrentEnabled(boolean concurrentEnabled) { + this.concurrentEnabled = concurrentEnabled; + } + + // ═══════════════════════════════════════════════════════════════════ + // RESOLUTION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Resolve this entry to an Ability instance. + * Inline entries return their ability directly. + * Reference entries resolve via AbilityController. + */ + public Ability resolve() { + if (entryType == EntryType.INLINE) { + return inlineAbility; + } + AbilityController ctrl = AbilityController.Instance; + if (ctrl == null) return null; + return ctrl.resolveAbility(abilityReference); + } + + // ═══════════════════════════════════════════════════════════════════ + // CONVERSION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Convert a reference entry to inline by cloning the resolved ability. + * Returns false if already inline or the reference cannot be resolved. + */ + public boolean convertToInline() { + if (entryType != EntryType.REFERENCE) return false; + + AbilityController ctrl = AbilityController.Instance; + if (ctrl == null) return false; + + Ability resolved = ctrl.resolveAbility(abilityReference); + if (resolved == null) return false; + + INbt nbt = resolved.writeNBT(true); + nbt.setString("id", UUID.randomUUID().toString()); + inlineAbility = ctrl.fromNBT(nbt); + entryType = EntryType.INLINE; + abilityReference = ""; + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEEP COPY + // ═══════════════════════════════════════════════════════════════════ + + /** + * Create a deep copy of this entry. Inline abilities are copied via NBT round-trip. + */ + public ChainedAbilityEntry deepCopy() { + ChainedAbilityEntry copy = new ChainedAbilityEntry(); + copy.entryType = this.entryType; + copy.abilityReference = this.abilityReference; + copy.delayTicks = this.delayTicks; + copy.concurrentEnabled = this.concurrentEnabled; + if (this.inlineAbility != null) { + copy.inlineAbility = AbilityController.Instance.fromNBT(this.inlineAbility.writeNBT(true)); + } + return copy; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + public INbt writeNBT(boolean saveScripts) { + INbt nbt = new INbt(); + nbt.setInteger("Delay", delayTicks); + nbt.setBoolean("Concurrent", concurrentEnabled); + + if (entryType == EntryType.INLINE && inlineAbility != null) { + nbt.setTag("InlineAbility", inlineAbility.writeNBT(saveScripts)); + } else { + nbt.setString("Reference", abilityReference); + } + return nbt; + } + + public static ChainedAbilityEntry fromNBT(INbt nbt) { + if (nbt == null) return null; + + ChainedAbilityEntry entry = new ChainedAbilityEntry(); + entry.delayTicks = Math.max(0, nbt.getInteger("Delay")); + entry.concurrentEnabled = nbt.hasKey("Concurrent") ? nbt.getBoolean("Concurrent") : true; + + if (nbt.hasKey("InlineAbility")) { + INbt abilityNBT = nbt.getCompoundTag("InlineAbility"); + entry.entryType = EntryType.INLINE; + entry.inlineAbility = AbilityController.Instance != null + ? AbilityController.Instance.fromNBT(abilityNBT) : null; + } else { + entry.entryType = EntryType.REFERENCE; + entry.abilityReference = nbt.getString("Reference"); + } + + return entry; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/AnchorPoint.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/AnchorPoint.java index 567713d34..91ff2eb4c 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/AnchorPoint.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/AnchorPoint.java @@ -1,15 +1,26 @@ package kamkeel.npcs.controllers.data.ability.enums; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + /** * Defines anchor points for ability charging effects. - * Determines where the charging visual (orb, disc, etc.) appears relative to the entity. + * Determines where the charging visual (orb, disc, etc.) appears relative to the IEntity. */ public enum AnchorPoint { - FRONT("Front"), // In front of entity face (default) - CENTER("Center"), // Entity center + FRONT("Front"), // In front of IEntity face (default) + CENTER("Center"), // IEntity center RIGHT_HAND("Right Hand"), // Right arm/hand position LEFT_HAND("Left Hand"), // Left arm/hand position - ABOVE_HEAD("Above Head"), // Above the entity + ABOVE_HEAD("Above Head"), // Above the IEntity CHEST("Chest"), // Chest level, centered EYE("Eye"); // Eye level, centered diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/UserType.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/UserType.java index cc5960ff6..07f088f5b 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/UserType.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/enums/UserType.java @@ -1,7 +1,18 @@ package kamkeel.npcs.controllers.data.ability.enums; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + /** - * Defines which entity types can use an ability. + * Defines which IEntity types can use an ability. */ public enum UserType { NPC_ONLY, diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java new file mode 100644 index 000000000..eeea61030 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java @@ -0,0 +1,141 @@ +package kamkeel.npcs.controllers.data.ability.extender; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +/** + * General-purpose extension point for abilities. Multiple extenders can be registered. + * Covers lifecycle hooks (start/tick/complete) and damage handling. + *

+ * All methods have no-op defaults so extenders only override what they need. + */ +public interface IAbilityExtender { + + /** + * Called before an ability starts executing. + * Return false to cancel the ability (e.g., insufficient resources). + * + * @param ability The ability about to start + * @param caster The IEntity executing the ability (NPC or Player) + * @param target The target IEntity (may be null for player self-cast) + * @return true to allow, false to cancel + */ + default boolean onAbilityStart(Ability ability, IEntityLivingBase caster, IEntityLivingBase target) { + return true; + } + + /** + * Called each tick during ability execution. + * Return false to interrupt the ability (e.g., resource depleted). + * + * @param ability The executing ability + * @param caster The IEntity executing the ability + * @param target The target IEntity (may be null) + * @param phase The current ability phase (WINDUP, ACTIVE, DAZED, BURST_DELAY) + * @param tick The current tick within the phase + * @return true to continue, false to interrupt + */ + default boolean onAbilityTick(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, + AbilityPhase phase, int tick) { + return true; + } + + /** + * Called when an ability completes (normally or via interruption). + * + * @param ability The completed ability + * @param caster The IEntity that was executing the ability + * @param target The target IEntity (may be null) + * @param interrupted true if the ability was interrupted, false if it completed normally + */ + default void onAbilityComplete(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, + boolean interrupted) { + } + + /** + * Called when an ability deals damage, AFTER script events but BEFORE default attackIEntityFrom. + * Return true to handle the damage (skips default damage application). + *

+ * Chain of responsibility: extenders are called in registration order. + * The first extender returning true claims the damage; remaining extenders are skipped. + * + * @param ability The ability dealing damage + * @param caster The IEntity executing the ability + * @param target The IEntity being hit + * @param damage The damage amount (after script event modifications) + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction + * @param damageMultiplier Accumulated proportional damage multiplier (0.0-1.0). + * 1.0 means full damage (no reduction). Extenders that recalculate + * damage from stats should multiply by this value. + * @return true if damage was handled (skip default), false to pass to next extender or default + */ + default boolean onAbilityDamage(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ, + float damageMultiplier) { + return false; + } + + /** + * Called to modify outgoing projectile damage before it hits a barrier or is used in other contexts. + * Unlike onAbilityDamage (chain of responsibility), this is cumulative — all extenders apply their + * modifications in sequence, each receiving the previous extender's output. + * + * @param ability The source ability of the projectile + * @param caster The IEntity that fired the projectile + * @param baseDamage The base damage from the ability IConfiguration + * @return The modified damage value + */ + default float modifyProjectileDamage(Ability ability, IEntityLivingBase caster, float baseDamage) { + return baseDamage; + } + + /** + * Called when a barrier IEntity is about to be created, allowing extenders to modify its max health. + * Cumulative: all extenders apply in sequence, each receiving the previous output. + * + * @param ability The barrier ability being executed + * @param caster The IEntity creating the barrier + * @param baseHealth The base max health from EnergyBarrierData.maxHealth + * @return The modified max health value + */ + default float modifyBarrierHealth(Ability ability, IEntityLivingBase caster, float baseHealth) { + return baseHealth; + } + + /** + * Called when an ability is about to heal an IEntity. + * Return true to handle the healing (skip default IEntity.heal()). + * Chain of responsibility: first returning true wins. + * + * @param ability The ability performing the heal + * @param caster The IEntity casting the ability + * @param target The IEntity being healed + * @param healAmount The heal amount calculated by the ability + * @return true if healing was handled (skip default), false to pass to next extender or default + */ + default boolean onAbilityHeal(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, float healAmount) { + return false; + } + + default Boolean onCheckCondition(AbilityCondition condition, IEntityLivingBase caster, IEntityLivingBase target) { + return null; + } + + default Boolean onCheckConditionForPlayer(AbilityCondition condition, IEntityLivingBase player) { + return null; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java new file mode 100644 index 000000000..4e8d5aed9 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java @@ -0,0 +1,29 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class AbilityFieldDefs { + @SuppressWarnings("rawtypes") + public static FieldDef effectsListField(String label, java.util.function.Supplier getter, java.util.function.Consumer setter) { return new FieldDef(); } +} \ No newline at end of file diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java new file mode 100644 index 000000000..165e14c42 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Field definition for ability GUI. + */ +public class FieldDef { +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/GuiQuestSelection.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/GuiQuestSelection.java new file mode 100644 index 000000000..090800c31 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/GuiQuestSelection.java @@ -0,0 +1,12 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.util.ByteBufUtils; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class GuiQuestSelection { + public Object selectedQuest; + public GuiQuestSelection(int questId) {} +} \ No newline at end of file diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityConfigCallback.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityConfigCallback.java new file mode 100644 index 000000000..239445761 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityConfigCallback.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Callback interface for ability config GUI. + */ +public interface IAbilityConfigCallback { +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityExtender.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityExtender.java new file mode 100644 index 000000000..f33ef0d7c --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityExtender.java @@ -0,0 +1,11 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.util.ByteBufUtils; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public interface IAbilityExtender { + // TEMPORARY STUB +} \ No newline at end of file diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityFieldProvider.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityFieldProvider.java new file mode 100644 index 000000000..ccaf16fa4 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IAbilityFieldProvider.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Field provider interface for ability GUI system. + */ +public interface IAbilityFieldProvider { +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IChainedAbilityFieldProvider.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IChainedAbilityFieldProvider.java new file mode 100644 index 000000000..c501b7910 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/IChainedAbilityFieldProvider.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Chained ability field provider interface. + */ +public interface IChainedAbilityFieldProvider { +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiAbilityConfig.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiAbilityConfig.java new file mode 100644 index 000000000..3d0063c59 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiAbilityConfig.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Sub-GUI for ability configuration. + */ +public class SubGuiAbilityConfig { +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiCustomEffectSelect.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiCustomEffectSelect.java new file mode 100644 index 000000000..b247f61df --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiCustomEffectSelect.java @@ -0,0 +1,13 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.util.ByteBufUtils; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class SubGuiCustomEffectSelect { + public SubGuiCustomEffectSelect(int effectId, int effectIndex) {} + public int getSelectedEffectId() { return -1; } + public int getSelectedIndex() { return 0; } +} \ No newline at end of file diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiZonePresetSelector.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiZonePresetSelector.java new file mode 100644 index 000000000..5fe708f37 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/gui/SubGuiZonePresetSelector.java @@ -0,0 +1,12 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.util.ByteBufUtils; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class SubGuiZonePresetSelector { + public Object selectedPreset; + public SubGuiZonePresetSelector() {} +} \ No newline at end of file diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/preview/PreviewEntityHandler.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/preview/PreviewEntityHandler.java new file mode 100644 index 000000000..3fd32a1a9 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/preview/PreviewEntityHandler.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability.preview; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Preview entity handler for ability system. + */ +public class PreviewEntityHandler { +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java new file mode 100644 index 000000000..36c99da01 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -0,0 +1,331 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityCharge; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Charge ability: Rush attack where the caster charges in a line, damaging all targets hit. + * Extends AbilityMovement for shared direction locking, stall detection, and velocity application. + */ +public class AbilityCharge extends AbilityMovement implements IAbilityCharge { + + // Type-specific parameters + private float chargeSpeed = 0.8f; + private float damage = 8.0f; + private float knockback = 3.0f; + private float hitWidth = 1.5f; + + // Type-specific runtime state + private transient Set hitEntities = new HashSet<>(); + + public AbilityCharge() { + this.typeId = "ability.cnpc.charge"; + this.name = "Charge"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 4.0f; + this.lockMovement = LockMode.WINDUP; + this.cooldownTicks = 0; + this.windUpTicks = 20; + // LINE telegraph showing charge path + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + this.windUpSound = "mob.zombie.wood"; + this.activeSound = "mob.zombie.attack"; + this.windUpAnimationName = "Ability_Charge_Windup"; + this.activeAnimationName = "Ability_Charge_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/charge.png") + }; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + /** + * During windup, NPCs face toward their target for animation purposes. + * Players aim freely — the telegraph follows their look direction on the client, + * and direction is locked at the end of windup in onExecute(). + */ + @Override + public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (!isPlayerCaster(caster) && target != null) { + lockDirectionToTarget(caster, target); + enforceLockedRotation(caster); + } + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + // Lock final direction at end of windup so it matches the telegraph + lockDirection(caster, target); + initMovement(caster, maxRange, chargeSpeed); + hitEntities.clear(); + + enforceLockedRotation(caster); + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (checkTimeout(tick)) { + stopMomentum(caster); + signalCompletion(); + return; + } + + if (movementDirection == null) { + stopMomentum(caster); + signalCompletion(); + return; + } + + if (!isPreview()) { + enforceLockedRotation(caster); + } + + if (checkStall(caster, tick)) { + stopMomentum(caster); + signalCompletion(); + return; + } + updatePrevPosition(caster); + + if (getDistanceTraveled(caster) >= maxRange) { + stopMomentum(caster); + signalCompletion(); + return; + } + + if (checkBlocked(caster, chargeSpeed)) { + stopMomentum(caster); + signalCompletion(); + return; + } + + // Move caster (flat: zero vertical motion) + applyVelocityFlat(caster, chargeSpeed); + + // Server-side collision damage (skip in preview) + if (!caster.worldObj.isRemote && !isPreview()) { + IBoundingBox hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); + + @SuppressWarnings("unchecked") + List entities = caster.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, hitBox); + + for (IEntity IEntity : entities) { + if (!(IEntity instanceof IEntityLivingBase)) continue; + if (IEntity == caster) continue; + if (hitEntities.contains(IEntity.getEntityId())) continue; + if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue; + + IEntityLivingBase livingEntity = (IEntityLivingBase) IEntity; + + hitEntities.add(IEntity.getEntityId()); + + boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback, + movementDirection.xCoord, movementDirection.zCoord); + + if (wasHit) { + applyEffects(livingEntity); + caster.worldObj.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f); + } + } + } + } + + @Override + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) { + stopMomentum(caster); + super.onComplete(caster, target); + } + + @Override + public void onInterrupt(IEntityLivingBase caster, noppes.npcs.api.IDamageSource source, float damage) { + stopMomentum(caster); + super.onInterrupt(caster, source, damage); + } + + @Override + public void cleanup() { + super.cleanup(); + hitEntities.clear(); + } + + @Override + public void resetForBurst() { + super.resetForBurst(); + hitEntities.clear(); + } + + @Override + public float getTelegraphLength() { + return maxRange; + } + + @Override + public float getTelegraphWidth() { + return hitWidth * 2; + } + + /** + * Creates a LINE telegraph from caster position towards target. + * Telegraph follows caster during windup (so caster can reposition before charging). + * Direction is locked at creation based on target position. + */ + @Override + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { + return null; + } + + // NPC: telegraph points toward aggro target + // Player: telegraph points in look direction + float yaw; + if (!isPlayerCaster(caster) && target != null) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + yaw = caster.rotationYaw; + } + + // Create LINE telegraph + Telegraph telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth()); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at caster ground level, direction towards target + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, yaw); + instance.setCasterEntityId(caster.getEntityId()); + // Telegraph follows caster during windup - allows caster to reposition + instance.setEntityIdToFollow(caster.getEntityId()); + + // For player casters, track their rotation so telegraph follows look direction + if (isPlayerCaster(caster)) { + instance.setTrackFollowedEntityYaw(true); + } + + return instance; + } + + @Override + public int getMaxPreviewDuration() { + return (int) Math.ceil(maxRange / chargeSpeed) + 5; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("chargeSpeed", chargeSpeed); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("hitWidth", hitWidth); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.chargeSpeed = nbt.hasKey("chargeSpeed") ? Math.max(0.01f, nbt.getFloat("chargeSpeed")) : 0.8f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 3.0f; + this.hitWidth = nbt.hasKey("hitWidth") ? nbt.getFloat("hitWidth") : 1.5f; + } + + // Getters & Setters + public float getChargeSpeed() { + return chargeSpeed; + } + + public void setChargeSpeed(float chargeSpeed) { + this.chargeSpeed = chargeSpeed; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public float getHitWidth() { + return hitWidth; + } + + public void setHitWidth(float hitWidth) { + this.hitWidth = hitWidth; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.chargeSpeed", this::getChargeSpeed, this::setChargeSpeed) + ), + FieldDef.row( + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth) + ), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java new file mode 100644 index 000000000..35f2a5256 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java @@ -0,0 +1,227 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.AnimationController; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.api.ability.type.IAbilityCounter; +import java.util.Arrays; +import java.util.List; + +/** + * Counter ability: Absorbs incoming damage and counter-attacks the attacker. + */ +public class AbilityCounter extends AbilityDefend implements IAbilityCounter { + + private CounterType counterType = CounterType.FLAT; + private float counterValue = 6.0f; + private int counterAnimationId = -1; + private String counterAnimationName = "Ability_Guard_Counter"; + + public AbilityCounter() { + this.typeId = "ability.cnpc.counter"; + this.name = "Counter"; + this.cooldownTicks = 0; + this.windUpTicks = 20; + this.allowedBy = UserType.BOTH; + this.activeSound = "random.anvil_land"; + + this.activeAnimationName = "Ability_Guard_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png") + }; + } + + public enum CounterType { + FLAT, + PERCENT; + + @Override + public String toString() { + switch (this) { + case FLAT: + return "ability.counter.flat"; + case PERCENT: + return "ability.counter.percent"; + default: + return name(); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOKS + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected float performDefend(IEntityLivingBase attacker, float amount) { + // For PERCENT mode with DBC, use the full DBC-calculated damage, not vanilla base + float incomingDamage = amount; + if (DBCAddon.IsAvailable()) { + incomingDamage = DBCAddon.instance.getAttackerDBCDamage(amount); + } + + // Calculate counter damage + float damage; + if (counterType == CounterType.FLAT) { + damage = counterValue; + } else { + damage = incomingDamage * (counterValue / 100.0f); + } + + // Counter-attack through the standard ability damage pipeline. + // DBCAbilityExtender handles DBC stat routing (ignore flags, scaling, etc.) automatically. + if (caster != null && attacker.isEntityAlive() && damage > 0) { + applyAbilityDamage(caster, attacker, damage, 0); + } + + return 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean hasDamage() { + return true; + } + + @Override + protected Animation getDefendAnimation() { + return getCounterAnimation(); + } + + public Animation getCounterAnimation() { + if (AnimationController.Instance == null) return null; + if (counterAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(counterAnimationId); + } + if (counterAnimationName != null && !counterAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(counterAnimationName, true); + } + return null; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void writeSubTypeNBT(INbt nbt) { + nbt.setString("counterType", counterType.name()); + nbt.setFloat("counterValue", counterValue); + nbt.setInteger("counterAnimationId", counterAnimationId); + nbt.setString("counterAnimationName", resolveAnimationName(counterAnimationId, counterAnimationName)); + } + + @Override + protected void readSubTypeNBT(INbt nbt) { + try { + this.counterType = CounterType.valueOf(nbt.getString("counterType")); + } catch (Exception e) { + this.counterType = CounterType.FLAT; + } + this.counterValue = nbt.getFloat("counterValue"); + this.counterAnimationId = nbt.getInteger("counterAnimationId"); + this.counterAnimationName = nbt.getString("counterAnimationName"); + } + + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + + @ClientOnly + @Override + protected void getTypeDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.enumField("gui.type", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) + .hover("ability.hover.counterType"), + FieldDef.floatField("gui.value", this::getCounterValue, this::setCounterValue), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + + FieldDef.insertAfter(defs, "ability.dazedAnimation", + FieldDef.animSubGui("ability.counterAnimation", + this::getCounterAnimationId, this::setCounterAnimationId, + this::getCounterAnimationName, this::setCounterAnimationName) + .tab("Effects")); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public CounterType getCounterTypeEnum() { + return counterType; + } + + public void setCounterTypeEnum(CounterType counterType) { + this.counterType = counterType; + } + + @Override + public int getCounterType() { + return counterType.ordinal(); + } + + @Override + public void setCounterType(int type) { + CounterType[] values = CounterType.values(); + this.counterType = type >= 0 && type < values.length ? values[type] : CounterType.FLAT; + } + + @Override + public float getCounterValue() { + return counterValue; + } + + @Override + public void setCounterValue(float counterValue) { + this.counterValue = counterValue; + } + + @Override + public int getCounterAnimationId() { + return counterAnimationId; + } + + @Override + public void setCounterAnimationId(int counterAnimationId) { + this.counterAnimationId = counterAnimationId; + } + + public String getCounterAnimationName() { + return counterAnimationName; + } + + public void setCounterAnimationName(String name) { + this.counterAnimationName = name != null ? name : ""; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java new file mode 100644 index 000000000..3cc330c1c --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -0,0 +1,492 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityCutter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Cutter ability: Sweeping fan attack in an arc. + * Deals damage to all entities in a cone/arc in front of the caster. + * Can sweep outward over time or hit instantly. + */ +public class AbilityCutter extends Ability implements IAbilityCutter { + + public enum SweepMode { + SWIPE, + SPIN; + + @Override + public String toString() { + switch (this) { + case SWIPE: + return "ability.sweep.swipe"; + case SPIN: + return "ability.sweep.spin"; + default: + return name(); + } + } + } + + private float arcAngle = 90.0f; + private float range = 6.0f; + private float damage = 7.0f; + private float knockback = 1.5f; + + private SweepMode sweepMode = SweepMode.SWIPE; + private float sweepSpeed = 6.0f; + private int spinDurationTicks = 60; // Duration for SPIN mode + + private boolean piercing = true; + private float innerRadius = 0.0f; + + // Runtime state + private transient Set hitEntities = new HashSet<>(); + private transient float currentRotation = 0.0f; + private transient boolean activeSoundPlayed = false; + private transient float sweepBaseYaw = 0.0f; + + public AbilityCutter() { + this.typeId = "ability.cnpc.cutter"; + this.name = "Cutter"; + this.targetingMode = TargetingMode.AOE_SELF; + this.maxRange = 8.0f; + this.lockMovement = LockMode.NO; + this.cooldownTicks = 0; + this.windUpTicks = 20; + this.telegraphType = TelegraphType.CONE; + this.windUpSound = "random.bow"; + this.activeSound = "random.break"; + this.windUpAnimationName = "Ability_Cutter_Windup"; + this.activeAnimationName = "Ability_Cutter_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/cutter.png", + this::getActiveColor) + }; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + /** + * Calculate how many ticks the active sweep phase takes. + * SWIPE: arcAngle / sweepSpeed ticks + * SPIN: spinDurationTicks + */ + private int getActiveDurationTicks() { + if (sweepMode == SweepMode.SPIN) { + return spinDurationTicks; + } + return sweepSpeed > 0 ? (int) Math.ceil(arcAngle / sweepSpeed) : 1; + } + + @Override + public boolean keepTelegraphDuringActive() { + return true; + } + + /** + * Creates a CONE telegraph that lasts through both windup and active sweep phases. + * Warning color transitions when the sweep actually begins (at windUpTicks). + */ + @Override + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { + return null; + } + + Telegraph telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle(), getTelegraphInnerRadius()); + int totalDuration = windUpTicks + getActiveDurationTicks(); + telegraph.setDurationTicks(totalDuration); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + // Warning transition starts when the sweep begins (active phase) + telegraph.setWarningStartTick(windUpTicks); + telegraph.setHeightOffset(telegraphHeightOffset); + + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); + + if (target != null && !isRotationLockedDuringWindup()) { + instance.setTargetEntityId(target.getEntityId()); + } else { + // No target to track yaw from — track the caster's rotation instead + // so the telegraph follows where the player/NPC is facing + instance.setTrackFollowedEntityYaw(true); + } + + return instance; + } + + @Override + public float getTelegraphRadius() { + return range; + } + + @Override + public float getTelegraphLength() { + return range; + } + + @Override + public float getTelegraphAngle() { + return arcAngle; + } + + @Override + public float getTelegraphInnerRadius() { + return innerRadius; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + hitEntities.clear(); + currentRotation = -arcAngle / 2.0f; + activeSoundPlayed = false; + + // Compute sweep direction from caster/target at execution time. + // Server-side telegraph instances are not ticked, so their yaw is stale + // from windup start — use the caster's current rotation instead. + if (target != null) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + if (dx * dx + dz * dz > 0.0001) { + sweepBaseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + sweepBaseYaw = caster.rotationYaw; + } + } else { + sweepBaseYaw = caster.rotationYaw; + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (caster.worldObj.isRemote && !isPreview()) return; + + switch (sweepMode) { + case SWIPE: + float prevRotation = currentRotation; + currentRotation += sweepSpeed; + + // Play active sound when sweep reaches center (where target typically stands) + if (!activeSoundPlayed && currentRotation >= 0) { + playActiveSound(caster, caster.worldObj); + } + + if (currentRotation > arcAngle / 2.0f) { + if (!isPreview()) { + performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevRotation, arcAngle / 2.0f); + } + signalCompletion(); + return; + } + if (!isPreview()) { + performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevRotation, currentRotation); + } + break; + + case SPIN: + // Play active sound at spin start + if (!activeSoundPlayed) { + playActiveSound(caster, caster.worldObj); + } + + if (tick >= spinDurationTicks) { + signalCompletion(); + return; + } + float prevSpin = currentRotation; + currentRotation = (currentRotation + sweepSpeed) % 360.0f; + if (!isPreview()) { + hitEntities.clear(); + performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevSpin, currentRotation); + } + break; + } + } + + private void playActiveSound(IEntityLivingBase caster, IWorld IWorld) { + activeSoundPlayed = true; + if (activeSound != null && !activeSound.isEmpty()) { + IWorld.playSoundAtEntity(caster, activeSound, 1.0f, 1.0f); + } + } + + /** + * Check for entities between two sweep angles, ensuring no gaps between ticks. + * For SWIPE mode, this covers the continuous arc from startAngle to endAngle. + */ + private void performSweepDamageRange(IEntityLivingBase caster, IWorld IWorld, float minDist, float maxDist, + float startAngle, float endAngle) { + // Calculate the angular range covered this tick + float minAngle = Math.min(startAngle, endAngle); + float maxAngle = Math.max(startAngle, endAngle); + + IBoundingBox searchBox = IBoundingBox.getBoundingBox( + caster.posX - maxDist, caster.posY - 1, caster.posZ - maxDist, + caster.posX + maxDist, caster.posY + 3, caster.posZ + maxDist + ); + + @SuppressWarnings("unchecked") + List entities = IWorld.getEntitiesWithinAABB(IEntityLivingBase.class, searchBox); + + for (IEntityLivingBase IEntity : entities) { + if (IEntity == caster) continue; + if (hitEntities.contains(IEntity.getEntityId())) continue; + if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue; + // If not piercing, stop after hitting one IEntity this sweep + if (!piercing && !hitEntities.isEmpty()) break; + + double dx = IEntity.posX - caster.posX; + double dz = IEntity.posZ - caster.posZ; + double dist = Math.sqrt(dx * dx + dz * dz); + + if (dist < minDist || dist > maxDist) continue; + if (!isInSweepRange(dx, dz, sweepBaseYaw, minAngle, maxAngle)) continue; + + // Line-of-sight check: skip targets behind solid blocks or enemy barriers + if (!hasLineOfSight(IWorld, caster, IEntity)) continue; + if (isBlockedByBarrier(IWorld, caster, IEntity)) continue; + + hitEntities.add(IEntity.getEntityId()); + + float distFactor = 1.0f - ((float) dist / maxDist) * 0.3f; + float actualDamage = damage * distFactor; + + // Apply damage with scripted event support + boolean wasHit = applyAbilityDamage(caster, IEntity, actualDamage, knockback); + + // Apply effects if hit wasn't cancelled + if (wasHit) { + applyEffects(IEntity); + } + } + } + + /** + * Check if an IEntity falls within the angular range between two sweep positions. + * This ensures entities at long range can't slip between tick-gaps. + */ + private boolean isInSweepRange(double dx, double dz, float casterYaw, float minAngle, float maxAngle) { + double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz)); + double entityRelative = normalizeAngle(angleToEntity - casterYaw); + return entityRelative >= minAngle && entityRelative <= maxAngle; + } + + private double normalizeAngle(double angle) { + while (angle > 180) angle -= 360; + while (angle < -180) angle += 360; + return angle; + } + + @Override + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) { + hitEntities.clear(); + currentRotation = 0.0f; + activeSoundPlayed = false; + sweepBaseYaw = 0.0f; + } + + @Override + public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) { + hitEntities.clear(); + currentRotation = 0.0f; + activeSoundPlayed = false; + sweepBaseYaw = 0.0f; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("arcAngle", arcAngle); + nbt.setFloat("range", range); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setString("sweepMode", sweepMode.name()); + nbt.setFloat("sweepSpeed", sweepSpeed); + nbt.setInteger("spinDurationTicks", spinDurationTicks); + nbt.setBoolean("piercing", piercing); + nbt.setFloat("innerRadius", innerRadius); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.arcAngle = nbt.getFloat("arcAngle"); + this.range = nbt.getFloat("range"); + this.damage = nbt.getFloat("damage"); + this.knockback = nbt.getFloat("knockback"); + try { + this.sweepMode = SweepMode.valueOf(nbt.getString("sweepMode")); + } catch (Exception e) { + this.sweepMode = SweepMode.SWIPE; + } + this.sweepSpeed = nbt.getFloat("sweepSpeed"); + this.spinDurationTicks = nbt.getInteger("spinDurationTicks"); + this.piercing = nbt.getBoolean("piercing"); + this.innerRadius = nbt.getFloat("innerRadius"); + } + + // Getters & Setters + public float getArcAngle() { + return arcAngle; + } + + public void setArcAngle(float arcAngle) { + this.arcAngle = arcAngle; + } + + public float getRange() { + return range; + } + + public void setRange(float range) { + this.range = range; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public SweepMode getSweepModeEnum() { + return sweepMode; + } + + public void setSweepModeEnum(SweepMode sweepMode) { + this.sweepMode = sweepMode; + } + + @Override + public int getSweepMode() { + return sweepMode.ordinal(); + } + + @Override + public void setSweepMode(int mode) { + SweepMode[] values = SweepMode.values(); + this.sweepMode = mode >= 0 && mode < values.length ? values[mode] : SweepMode.SWIPE; + } + + public float getSweepSpeed() { + return sweepSpeed; + } + + public void setSweepSpeed(float sweepSpeed) { + this.sweepSpeed = sweepSpeed; + } + + public int getSpinDurationTicks() { + return spinDurationTicks; + } + + public void setSpinDurationTicks(int spinDurationTicks) { + this.spinDurationTicks = Math.max(1, spinDurationTicks); + } + + public boolean isPiercing() { + return piercing; + } + + public void setPiercing(boolean piercing) { + this.piercing = piercing; + } + + public float getInnerRadius() { + return innerRadius; + } + + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } + + public float getCurrentRotation() { + return currentRotation; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("gui.range", this::getRange, this::setRange) + ), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.sweep"), + FieldDef.enumField("ability.sweepMode", SweepMode.class, this::getSweepModeEnum, this::setSweepModeEnum) + .hover("ability.hover.sweepMode"), + FieldDef.row( + FieldDef.floatField("ability.arcAngle", this::getArcAngle, this::setArcAngle), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius) + .hover("ability.hover.innerRadius") + ), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.intField("ability.spinDuration", this::getSpinDurationTicks, this::setSpinDurationTicks) + .range(1, 1000) + .visibleWhen(() -> this.getSweepModeEnum() == SweepMode.SPIN), + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing"), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java new file mode 100644 index 000000000..71f9a2516 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -0,0 +1,424 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityDash; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * Dash ability: Quick evasive sidestep with NO telegraph. + * Extends AbilityMovement for shared direction locking, stall detection, and velocity application. + */ +public class AbilityDash extends AbilityMovement implements IAbilityDash { + + /** + * Dash behavior mode. + */ + public enum DashMode { + AGGRESSIVE, + DEFENSIVE, + DIRECTIONAL; + + @Override + public String toString() { + switch (this) { + case AGGRESSIVE: + return "ability.dash.aggressive"; + case DEFENSIVE: + return "ability.dash.defensive"; + case DIRECTIONAL: + return "ability.dash.directional"; + default: + return name(); + } + } + } + + /** + * Specific dash direction. + */ + public enum DashDirection { + FORWARD(0), + DIAGONAL_FORWARD_LEFT(45), + DIAGONAL_FORWARD_RIGHT(-45), + LEFT(90), + RIGHT(-90), + DIAGONAL_BACK_LEFT(135), + DIAGONAL_BACK_RIGHT(-135), + BACK(180), + CUSTOM(0); + + private final float angleOffset; + + DashDirection(float angleOffset) { + this.angleOffset = angleOffset; + } + + public float getAngleOffset() { + return angleOffset; + } + + @Override + public String toString() { + switch (this) { + case FORWARD: + return "ability.dash.forward"; + case DIAGONAL_FORWARD_LEFT: + return "ability.dash.diagonalForwardLeft"; + case DIAGONAL_FORWARD_RIGHT: + return "ability.dash.diagonalForwardRight"; + case LEFT: + return "ability.dash.left"; + case RIGHT: + return "ability.dash.right"; + case DIAGONAL_BACK_LEFT: + return "ability.dash.diagonalBackLeft"; + case DIAGONAL_BACK_RIGHT: + return "ability.dash.diagonalBackRight"; + case BACK: + return "ability.dash.back"; + case CUSTOM: + return "ability.dash.custom"; + default: + return name(); + } + } + + public static DashDirection fromOrdinal(int ordinal) { + DashDirection[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return DashDirection.FORWARD; + } + } + + private static final Random RANDOM = new Random(); + + private static final DashDirection[] AGGRESSIVE_DIRECTIONS = { + DashDirection.FORWARD, + DashDirection.DIAGONAL_FORWARD_LEFT, + DashDirection.DIAGONAL_FORWARD_RIGHT + }; + + private static final DashDirection[] DEFENSIVE_DIRECTIONS = { + DashDirection.LEFT, + DashDirection.RIGHT, + DashDirection.BACK, + DashDirection.DIAGONAL_BACK_LEFT, + DashDirection.DIAGONAL_BACK_RIGHT + }; + + // Type-specific parameters + private DashMode dashMode = DashMode.DEFENSIVE; + private DashDirection dashDirection = DashDirection.FORWARD; + private float dashDistance = 4.0f; + private float dashSpeed = 0.5f; + private float dashAngle = 0.0f; + + // Type-specific runtime state + private transient DashDirection chosenDirection; + private transient double preDashMotionX; + private transient double preDashMotionZ; + + public AbilityDash() { + this.typeId = "ability.cnpc.dash"; + this.name = "Dash"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 0.0f; + this.lockMovement = LockMode.NO; + this.cooldownTicks = 0; + this.windUpTicks = 5; + // No telegraph for dash - it's a quick evasive move + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + this.windUpSound = "mob.bat.takeoff"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/dash.png") + }; + } + + @Override + public boolean hasDamage() { + return false; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + preDashMotionX = caster.motionX; + preDashMotionZ = caster.motionZ; + initMovement(caster, dashDistance, dashSpeed); + + if (dashMode == DashMode.DIRECTIONAL) { + // Dash straight in the caster's look direction + chosenDirection = dashDirection; + float baseYaw = getBaseYaw(caster, target); + float angleOffset = chosenDirection == DashDirection.CUSTOM ? dashAngle : chosenDirection.getAngleOffset(); + float dashYaw = baseYaw + angleOffset; + setDirectionFromYaw(dashYaw); + } else { + // Choose random direction based on mode + DashDirection[] directions = dashMode == DashMode.AGGRESSIVE + ? AGGRESSIVE_DIRECTIONS + : DEFENSIVE_DIRECTIONS; + chosenDirection = directions[RANDOM.nextInt(directions.length)]; + + // Calculate direction: base yaw (toward target or look dir) + direction offset + float baseYaw = getBaseYaw(caster, target); + float dashYaw = baseYaw + chosenDirection.getAngleOffset(); + setDirectionFromYaw(dashYaw); + } + + // Small upward impulse for a skip/hop arc (gravity handles the descent) + caster.motionY = 0.2; + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (checkTimeout(tick)) { + finishDash(caster); + signalCompletion(); + return; + } + + if (movementDirection == null) { + finishDash(caster); + signalCompletion(); + return; + } + + if (checkStall(caster, tick)) { + finishDash(caster); + signalCompletion(); + return; + } + updatePrevPosition(caster); + + if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) { + finishDash(caster); + signalCompletion(); + return; + } + + if (checkBlocked(caster, dashSpeed)) { + finishDash(caster); + signalCompletion(); + return; + } + + // Move caster (motionY left to gravity for skip arc). + // Directional mode layers the dash over incoming movement so momentum + // entering/exiting the dash stays fluid. + if (dashMode == DashMode.DIRECTIONAL) { + applyHorizontalMomentum(caster, + preDashMotionX + movementDirection.xCoord * dashSpeed, + preDashMotionZ + movementDirection.zCoord * dashSpeed); + } else { + applyVelocity(caster, dashSpeed); + } + + if (!isPreview()) { + // Trail particles + caster.worldObj.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); + } + } + + @Override + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) { + finishDash(caster); + } + + @Override + public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) { + finishDash(caster); + } + + @Override + public void cleanup() { + super.cleanup(); + chosenDirection = null; + preDashMotionX = 0; + preDashMotionZ = 0; + } + + @Override + public void resetForBurst() { + super.resetForBurst(); + chosenDirection = null; + preDashMotionX = 0; + preDashMotionZ = 0; + } + + @Override + public int getMaxPreviewDuration() { + return (int) Math.ceil(dashDistance / dashSpeed) + 5; + } + + @Override + public float getTelegraphRadius() { + return 0; // No telegraph for dash + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setString("dashMode", dashMode.name()); + nbt.setString("dashDirection", dashDirection.name()); + nbt.setFloat("dashDistance", dashDistance); + nbt.setFloat("dashSpeed", dashSpeed); + nbt.setFloat("dashAngle", dashAngle); + } + + @Override + public void readTypeNBT(INbt nbt) { + try { + this.dashMode = DashMode.valueOf(nbt.getString("dashMode")); + } catch (Exception e) { + this.dashMode = DashMode.DEFENSIVE; + } + + try { + this.dashDirection = DashDirection.valueOf(nbt.getString("dashDirection")); + } catch (Exception e) { + this.dashDirection = DashDirection.FORWARD; + } + + this.dashDistance = nbt.hasKey("dashDistance") ? nbt.getFloat("dashDistance") : 4.0f; + this.dashSpeed = nbt.hasKey("dashSpeed") ? Math.max(0.01f, nbt.getFloat("dashSpeed")) : 0.5f; + this.dashAngle = nbt.hasKey("dashAngle") ? nbt.getFloat("dashAngle") : 0.0f; + } + + // Getters & Setters + public DashMode getDashModeEnum() { + return dashMode; + } + + public void setDashModeEnum(DashMode dashMode) { + this.dashMode = dashMode; + } + + @Override + public int getDashMode() { + return dashMode.ordinal(); + } + + @Override + public void setDashMode(int mode) { + DashMode[] values = DashMode.values(); + this.dashMode = mode >= 0 && mode < values.length ? values[mode] : DashMode.AGGRESSIVE; + } + + public float getDashDistance() { + return dashDistance; + } + + public void setDashDistance(float dashDistance) { + this.dashDistance = dashDistance; + } + + public float getDashSpeed() { + return dashSpeed; + } + + public void setDashSpeed(float dashSpeed) { + this.dashSpeed = dashSpeed; + } + + public float getDashAngle() { + return dashAngle; + } + + public void setDashAngle(float dashAngle) { + this.dashAngle = dashAngle; + } + + public DashDirection getDashDirectionEnum() { + return dashDirection; + } + + public void setDashDirectionEnum(DashDirection dashDirection) { + this.dashDirection = dashDirection; + } + + @Override + public int getDashDirection() { + return dashDirection.ordinal(); + } + + @Override + public void setDashDirection(int mode) { + DashDirection[] values = DashDirection.values(); + this.dashDirection = mode >= 0 && mode < values.length ? values[mode] : DashDirection.FORWARD; + } + + + public DashDirection getChosenDirection() { + return chosenDirection; + } + + private void finishDash(IEntityLivingBase caster) { + if (dashMode == DashMode.DIRECTIONAL) { + applyHorizontalMomentum(caster, preDashMotionX, preDashMotionZ); + } else { + stopMomentum(caster); + } + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.enumField("ability.dashMode", DashMode.class, this::getDashModeEnum, this::setDashModeEnum) + .hover("ability.hover.dashMode"), + FieldDef.row( + FieldDef.floatField("ability.dashDistance", this::getDashDistance, this::setDashDistance), + FieldDef.floatField("ability.dashSpeed", this::getDashSpeed, this::setDashSpeed) + ), + FieldDef.row( + FieldDef.enumField("ability.dashDirection", DashDirection.class, this::getDashDirectionEnum, this::setDashDirectionEnum), + FieldDef.floatField("ability.dashAngle", this::getDashAngle, this::setDashAngle) + .min(Float.NEGATIVE_INFINITY).visibleWhen(() -> getDashDirectionEnum() == DashDirection.CUSTOM) + ).visibleWhen(() -> this.getDashModeEnum() == DashMode.DIRECTIONAL) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java new file mode 100644 index 000000000..fa21565f4 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java @@ -0,0 +1,356 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityDefend; +import noppes.npcs.controllers.data.Animation; + +import java.util.List; + +/** + * Abstract base class for defensive abilities (Guard, Counter, Dodge). + *

+ * {@link #onDefend} handles shared checks then delegates to {@link #performDefend(IEntityLivingBase, float)} + * which subclasses override for their specific behavior. + *

+ * Damage source filtering is controlled by {@link #isValidDamageSource(IDamageSource)}. + * By default only physical melee is accepted; subclasses can override to widen (e.g. Guard accepts all). + */ +public abstract class AbilityDefend extends Ability implements IAbilityDefend { + + // ═══════════════════════════════════════════════════════════════════ + // SHARED PERSIST STATE + // ═══════════════════════════════════════════════════════════════════ + + protected int durationTicks = 60; + protected int maxHitAmount = 3; + + // ═══════════════════════════════════════════════════════════════════ + // TRANSIENT RUNTIME STATE + // ═══════════════════════════════════════════════════════════════════ + + protected transient IEntityLivingBase caster; + protected transient int hitCount; + protected transient IEntityLivingBase lastAttacker; + protected transient float lastDamageTaken; + protected transient Animation pendingDefendAnimation; + protected transient int defendAnimEndTick = -1; + /** Prevents double-counting hits when multiple damage paths call onDefend for the same hit in a single tick. */ + protected transient long lastDefendTick = -1; + /** Defers signalCompletion to the next tick so all damage paths for the last hit still see isDefending()=true. */ + protected transient boolean pendingCompletion; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTOR DEFAULTS + // ═══════════════════════════════════════════════════════════════════ + + protected AbilityDefend() { + this.targetingMode = TargetingMode.SELF; + this.lockMovement = LockMode.ACTIVE; + this.interruptible = false; + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + this.windUpTicks = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // LIFECYCLE + // ═══════════════════════════════════════════════════════════════════ + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + this.caster = caster; + hitCount = 0; + lastAttacker = null; + lastDamageTaken = 0.0f; + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (pendingCompletion || tick >= durationTicks) { + signalCompletion(); + } + } + + @Override + public void reset() { + super.reset(); + caster = null; + hitCount = 0; + lastAttacker = null; + lastDamageTaken = 0.0f; + pendingDefendAnimation = null; + defendAnimEndTick = -1; + lastDefendTick = -1; + pendingCompletion = false; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOK + // ═══════════════════════════════════════════════════════════════════ + + /** + * Called when the caster is hit while defending. + * Checks damage source validity via {@link #isValidDamageSource}, then delegates + * to {@link #performDefend(IEntityLivingBase, float)}. + * + * @param attacker The IEntity that hit the caster + * @param source The damage source + * @param amount The original incoming damage + * @return The modified damage to apply (unchanged if source is invalid) + */ + public final float onDefend(IEntityLivingBase attacker, IDamageSource source, float amount) { + if (!isDefending()) { + return amount; + } + if (attacker == null || source == null) { + return amount; + } + + if (!isValidDamageSource(source)) { + return amount; + } + + // Deduplicate per tick: when multiple damage paths (e.g. vanilla + DBC) call onDefend + // for the same hit, only count it once for hitCount / signalCompletion / animation. + long currentTick = (caster != null && caster.worldObj != null) + ? caster.worldObj.getTotalWorldTime() : -1; + boolean firstCallThisTick = (currentTick < 0 || currentTick != lastDefendTick); + + // If max hits reached on a previous tick and this is a NEW hit, reject it + if (pendingCompletion && firstCallThisTick) { + return amount; + } + + if (firstCallThisTick) { + lastDefendTick = currentTick; + lastAttacker = attacker; + lastDamageTaken = amount; + hitCount++; + + // Queue defend reaction animation (played on next tick by AbstractDataAbilities) + pendingDefendAnimation = getDefendAnimation(); + + // Defer completion to next tick so all damage paths in this tick + // still see isDefending()=true and apply guard reduction. + if (maxHitAmount > 0 && hitCount >= maxHitAmount) { + pendingCompletion = true; + } + } + + // Always calculate the correct reduction for the caller's damage amount + return performDefend(attacker, amount); + } + + /** + * Subclass-specific defend behavior. Called only for valid hits while defending + * (as determined by {@link #isValidDamageSource}). + * + * @param attacker The IEntity that hit the caster + * @param amount The incoming damage + * @return The modified damage the caster should take + */ + protected abstract float performDefend(IEntityLivingBase attacker, float amount); + + /** + * Determines whether this defend ability reacts to the given damage source. + * Default implementation accepts only physical melee damage (rejects magic, fire, IExplosion, projectile). + * Subclasses can override to accept additional damage types (e.g. Guard accepts all). + */ + protected boolean isValidDamageSource(IDamageSource source) { + return !source.isMagicDamage() && !source.isFireDamage() && !source.isExplosion() && !source.isProjectile(); + } + + // ═══════════════════════════════════════════════════════════════════ + // SUBCLASS HOOKS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Subclass-specific NBT persistence. + */ + protected abstract void writeSubTypeNBT(INbt nbt); + protected abstract void readSubTypeNBT(INbt nbt); + + @ClientOnly + protected abstract void getTypeDefinitions(List defs); + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND ANIMATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Returns the animation to play when this defend ability reacts to a hit. + * Subclasses override to provide their specific animation (counter strike, dodge roll, etc). + * Guard returns null (no reaction animation). + */ + protected Animation getDefendAnimation() { + return null; + } + + /** + * Consume and return the pending defend animation (set during onDefend). + * Called by AbstractDataAbilities to play the animation on the next tick. + */ + public Animation consumeDefendAnimation() { + Animation anim = pendingDefendAnimation; + pendingDefendAnimation = null; + return anim; + } + + /** + * Schedule return to active animation after the defend animation finishes. + */ + public void scheduleReturnToActive(int currentTick, Animation defendAnim) { + if (defendAnim != null) { + defendAnimEndTick = currentTick + (int) defendAnim.getTotalTime(); + } + } + + /** + * Check if the defend animation has finished and we should return to the active animation. + */ + public boolean shouldReturnToActiveAnimation(int currentTick) { + if (defendAnimEndTick >= 0 && currentTick >= defendAnimEndTick) { + defendAnimEndTick = -1; + return true; + } + return false; + } + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean isDefending() { + return isExecuting() && getPhase() == AbilityPhase.ACTIVE; + } + + // ═══════════════════════════════════════════════════════════════════ + // OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean hasDamage() { + return false; + } + + @Override + public boolean allowBurst() { + return false; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.SELF}; + } + + @Override + public float getTelegraphRadius() { + return 0; + } + + /** + * Suppresses the auto-play of activeSound on ACTIVE phase entry. + * Defend abilities play their sound on hit reaction, not on phase start. + */ + @Override + public boolean keepTelegraphDuringActive() { + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + @Override + public final void writeTypeNBT(INbt nbt) { + nbt.setInteger("durationTicks", durationTicks); + nbt.setInteger("maxHitAmount", maxHitAmount); + writeSubTypeNBT(nbt); + } + + @Override + public final void readTypeNBT(INbt nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; + this.maxHitAmount = nbt.hasKey("maxHitAmount") ? nbt.getInteger("maxHitAmount") : 3; + readSubTypeNBT(nbt); + } + + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + + @ClientOnly + @Override + public final void getAbilityDefinitions(List defs) { + defs.add(FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 6000)); + defs.add(FieldDef.intField("ability.maxHitAmount", this::getMaxHitAmount, this::setMaxHitAmount).range(0, 100) + .hover("ability.hover.maxHitAmount")); + + getTypeDefinitions(defs); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + @Override + public int getDurationTicks() { + return durationTicks; + } + + @Override + public void setDurationTicks(int ticks) { + this.durationTicks = Math.max(1, ticks); + } + + @Override + public int getMaxHitAmount() { + return maxHitAmount; + } + + @Override + public void setMaxHitAmount(int amount) { + this.maxHitAmount = Math.max(0, amount); + } + + public int getHitCount() { + return hitCount; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDodge.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDodge.java new file mode 100644 index 000000000..4568ce8dd --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDodge.java @@ -0,0 +1,217 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.AnimationController; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.api.ability.type.IAbilityDodge; +import java.util.ArrayList; +import java.util.List; + +/** + * Dodge ability: Completely cancels incoming attacks and plays a random dodge animation. + */ +public class AbilityDodge extends AbilityDefend implements IAbilityDodge { + + private int dodgeAnimation1Id = -1; + private String dodgeAnimation1Name = ""; + private int dodgeAnimation2Id = -1; + private String dodgeAnimation2Name = ""; + private int dodgeAnimation3Id = -1; + private String dodgeAnimation3Name = ""; + + public AbilityDodge() { + this.typeId = "ability.cnpc.dodge"; + this.name = "Dodge"; + this.cooldownTicks = 0; + this.windUpTicks = 10; + this.allowedBy = UserType.BOTH; + + this.activeAnimationName = ""; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png") + }; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOKS + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected float performDefend(IEntityLivingBase attacker, float amount) { + return 0; + } + + @Override + protected Animation getDefendAnimation() { + return getRandomDodgeAnimation(); + } + + /** + * Returns a random dodge animation from the configured slots (non-empty only). + */ + public Animation getRandomDodgeAnimation() { + if (AnimationController.Instance == null) return null; + + List available = new ArrayList<>(); + Animation a1 = getDodgeAnimation(dodgeAnimation1Id, dodgeAnimation1Name); + Animation a2 = getDodgeAnimation(dodgeAnimation2Id, dodgeAnimation2Name); + Animation a3 = getDodgeAnimation(dodgeAnimation3Id, dodgeAnimation3Name); + if (a1 != null) available.add(a1); + if (a2 != null) available.add(a2); + if (a3 != null) available.add(a3); + + if (available.isEmpty()) return null; + return available.get((int) (Math.random() * available.size())); + } + + private Animation getDodgeAnimation(int id, String name) { + if (id >= 0) { + return (Animation) AnimationController.Instance.get(id); + } + if (name != null && !name.isEmpty()) { + return (Animation) AnimationController.Instance.get(name, true); + } + return null; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void writeSubTypeNBT(INbt nbt) { + nbt.setInteger("dodgeAnimation1Id", dodgeAnimation1Id); + nbt.setString("dodgeAnimation1Name", resolveAnimationName(dodgeAnimation1Id, dodgeAnimation1Name)); + nbt.setInteger("dodgeAnimation2Id", dodgeAnimation2Id); + nbt.setString("dodgeAnimation2Name", resolveAnimationName(dodgeAnimation2Id, dodgeAnimation2Name)); + nbt.setInteger("dodgeAnimation3Id", dodgeAnimation3Id); + nbt.setString("dodgeAnimation3Name", resolveAnimationName(dodgeAnimation3Id, dodgeAnimation3Name)); + } + + @Override + protected void readSubTypeNBT(INbt nbt) { + this.dodgeAnimation1Id = nbt.getInteger("dodgeAnimation1Id"); + this.dodgeAnimation1Name = nbt.getString("dodgeAnimation1Name"); + this.dodgeAnimation2Id = nbt.getInteger("dodgeAnimation2Id"); + this.dodgeAnimation2Name = nbt.getString("dodgeAnimation2Name"); + this.dodgeAnimation3Id = nbt.getInteger("dodgeAnimation3Id"); + this.dodgeAnimation3Name = nbt.getString("dodgeAnimation3Name"); + } + + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + + @ClientOnly + @Override + protected void getTypeDefinitions(List defs) { + FieldDef.insertAfter(defs, "ability.dazedAnimation", + FieldDef.section("ability.section.dodgeAnimations").tab("Effects")); + + FieldDef.insertAfter(defs, "ability.section.dodgeAnimations", + FieldDef.animSubGui("ability.dodgeAnimation1", + this::getDodgeAnimation1Id, this::setDodgeAnimation1Id, + this::getDodgeAnimation1Name, this::setDodgeAnimation1Name) + .tab("Effects")); + + FieldDef.insertAfter(defs, "ability.dodgeAnimation1", + FieldDef.animSubGui("ability.dodgeAnimation2", + this::getDodgeAnimation2Id, this::setDodgeAnimation2Id, + this::getDodgeAnimation2Name, this::setDodgeAnimation2Name) + .tab("Effects")); + + FieldDef.insertAfter(defs, "ability.dodgeAnimation2", + FieldDef.animSubGui("ability.dodgeAnimation3", + this::getDodgeAnimation3Id, this::setDodgeAnimation3Id, + this::getDodgeAnimation3Name, this::setDodgeAnimation3Name) + .tab("Effects")); + + FieldDef.modifyVisibility(defs, "ability.windUpAnimation", () -> false); + FieldDef.modifyVisibility(defs, "ability.windUpSound", () -> false); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + @Override + public int getDodgeAnimation1Id() { + return dodgeAnimation1Id; + } + + @Override + public void setDodgeAnimation1Id(int animationId) { + this.dodgeAnimation1Id = animationId; + } + + public String getDodgeAnimation1Name() { + return dodgeAnimation1Name; + } + + public void setDodgeAnimation1Name(String name) { + this.dodgeAnimation1Name = name != null ? name : ""; + } + + @Override + public int getDodgeAnimation2Id() { + return dodgeAnimation2Id; + } + + @Override + public void setDodgeAnimation2Id(int animationId) { + this.dodgeAnimation2Id = animationId; + } + + public String getDodgeAnimation2Name() { + return dodgeAnimation2Name; + } + + public void setDodgeAnimation2Name(String name) { + this.dodgeAnimation2Name = name != null ? name : ""; + } + + @Override + public int getDodgeAnimation3Id() { + return dodgeAnimation3Id; + } + + @Override + public void setDodgeAnimation3Id(int animationId) { + this.dodgeAnimation3Id = animationId; + } + + public String getDodgeAnimation3Name() { + return dodgeAnimation3Name; + } + + public void setDodgeAnimation3Name(String name) { + this.dodgeAnimation3Name = name != null ? name : ""; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEffect.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEffect.java new file mode 100644 index 000000000..f73ac377a --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEffect.java @@ -0,0 +1,540 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.data.effect.AbilityCustomEffect; +import kamkeel.npcs.controllers.data.ability.data.entry.AbilityEffectActionEntry; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityEffect; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Effect ability: Apply healing, IPotionType effects, custom effects, and mod-registered + * effect actions to self or nearby entities. + *

+ * Supports: + *

    + *
  • SELF or AOE_SELF targeting modes
  • + *
  • AOE target filtering: ALLIES, ENEMIES, or ALL
  • + *
  • Healing (fixed amount + percentage)
  • + *
  • IPotionType effects via base Ability.effects list
  • + *
  • Custom effects from CustomEffectController (player-only)
  • + *
  • Mod-registered effect actions via IEffectAction registry
  • + *
  • Burst system for repeated reapplication (auras, debuffs)
  • + *
  • Concurrent execution alongside other abilities in chains
  • + *
+ */ +public class AbilityEffect extends Ability implements IAbilityEffect { + + // Type-specific parameters + private int durationTicks = 60; + private float healAmount = 10.0f; + private float healPercent = 0.0f; + private boolean includeSelf = true; + private float radius = 8.0f; + private boolean instantHeal = true; + private TargetFilter targetFilter = TargetFilter.ALLIES; + + // Custom effects (from CustomEffectController — player-only) + private List customEffects = new ArrayList<>(); + + // Mod-registered effect actions + private List effectActions = new ArrayList<>(); + + // Runtime state + private transient List affectedEntities; + + private List getAffectedEntities() { + if (affectedEntities == null) { + affectedEntities = new ArrayList<>(); + } + return affectedEntities; + } + + public AbilityEffect() { + this.typeId = "ability.cnpc.effect"; + this.name = "Effect"; + this.targetingMode = TargetingMode.SELF; + this.lockMovement = LockMode.WINDUP; + this.cooldownTicks = 0; + this.windUpTicks = 30; + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/effect.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/effect_overlay.png", + this::getActiveColor) + }; + } + + // ==================== ABILITY TYPE OVERRIDES ==================== + + @Override + public boolean hasDamage() { + return false; + } + + @Override + public boolean allowBurst() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.SELF, TargetingMode.AOE_SELF}; + } + + @Override + public boolean isConcurrentCapable() { + return true; + } + + @Override + public float getTelegraphRadius() { + return targetingMode == TargetingMode.AOE_SELF && radius > 0 ? radius : 0; + } + + // ==================== EXECUTION ==================== + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + if (caster.worldObj.isRemote && !isPreview()) return; + + if (!isPreview()) { + getAffectedEntities().clear(); + + if (targetingMode == TargetingMode.AOE_SELF && radius > 0) { + findEntitiesInRadius(caster, caster.worldObj); + } else { + if (includeSelf) { + getAffectedEntities().add(caster); + } + } + + if (instantHeal) { + for (IEntityLivingBase IEntity : getAffectedEntities()) { + healEntity(caster, IEntity); + applyAllEffects(caster, IEntity); + spawnHealParticles(caster.worldObj, IEntity); + } + } + } + + if (instantHeal) { + signalCompletion(); + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if ((caster.worldObj.isRemote && !isPreview()) || instantHeal) return; + + if (tick % 10 == 0) { + float tickHealFixed = (healAmount / (float) durationTicks) * 10; + + for (IEntityLivingBase IEntity : getAffectedEntities()) { + if (IEntity.isDead) continue; + + float totalTickHeal = tickHealFixed; + if (healPercent > 0) { + totalTickHeal += (IEntity.getMaxHealth() * healPercent / (float) durationTicks) * 10; + } + + if (totalTickHeal > 0) { + if (!AbilityController.Instance.fireOnAbilityHeal(this, caster, IEntity, totalTickHeal)) { + IEntity.heal(totalTickHeal); + } + } + + if (tick % 20 == 0) { + applyAllEffects(caster, IEntity); + spawnHealParticles(caster.worldObj, IEntity); + } + } + } + + if (tick >= durationTicks) { + signalCompletion(); + return; + } + + // Early completion if all affected entities are dead + boolean allDead = true; + for (IEntityLivingBase IEntity : getAffectedEntities()) { + if (!IEntity.isDead) { + allDead = false; + break; + } + } + if (!getAffectedEntities().isEmpty() && allDead) { + signalCompletion(); + } + } + + // ==================== EFFECT APPLICATION ==================== + + /** + * Applies all configured effect types to a single IEntity: + * vanilla IPotionType effects, custom effects, and mod-registered effect actions. + */ + private void applyAllEffects(IEntityLivingBase caster, IEntityLivingBase IEntity) { + applyEffects(IEntity); + applyCustomEffects(IEntity); + applyEffectActions(caster, IEntity); + } + + private void applyCustomEffects(IEntityLivingBase IEntity) { + for (AbilityCustomEffect ce : customEffects) { + if (ce.isValid()) { + ce.apply(IEntity); + } + } + } + + private void applyEffectActions(IEntityLivingBase caster, IEntityLivingBase IEntity) { + for (AbilityEffectActionEntry ea : effectActions) { + if (ea.isValid()) { + ea.apply(caster, IEntity); + } + } + } + + // ==================== TARGET GATHERING ==================== + + @SuppressWarnings("unchecked") + private void findEntitiesInRadius(IEntityLivingBase caster, IWorld IWorld) { + IBoundingBox aabb = IBoundingBox.getBoundingBox( + caster.posX - radius, caster.posY - 2, caster.posZ - radius, + caster.posX + radius, caster.posY + 3, caster.posZ + radius + ); + + List entities = IWorld.getEntitiesWithinAABB(IEntityLivingBase.class, aabb); + + for (IEntity IEntity : entities) { + if (!(IEntity instanceof IEntityLivingBase)) continue; + IEntityLivingBase living = (IEntityLivingBase) IEntity; + + float dist = caster.getDistanceToEntity(living); + if (dist > radius) continue; + + if (!AbilityTargetHelper.shouldAffect(caster, living, targetFilter, includeSelf)) continue; + + getAffectedEntities().add(living); + } + } + + private void healEntity(IEntityLivingBase caster, IEntityLivingBase IEntity) { + float totalHeal = healAmount; + if (healPercent > 0) { + totalHeal += IEntity.getMaxHealth() * healPercent; + } + if (totalHeal > 0) { + if (!AbilityController.Instance.fireOnAbilityHeal(this, caster, IEntity, totalHeal)) { + IEntity.heal(totalHeal); + } + } + } + + private void spawnHealParticles(IWorld IWorld, IEntityLivingBase IEntity) { + for (int i = 0; i < 10; i++) { + double offsetX = (IWorld.rand.nextDouble() - 0.5) * IEntity.width; + double offsetY = IWorld.rand.nextDouble() * IEntity.height; + double offsetZ = (IWorld.rand.nextDouble() - 0.5) * IEntity.width; + IWorld.spawnParticle("happyVillager", + IEntity.posX + offsetX, + IEntity.posY + offsetY, + IEntity.posZ + offsetZ, + 0, 0.1, 0); + } + } + + // ==================== LIFECYCLE ==================== + + @Override + public void resetForBurst() { + getAffectedEntities().clear(); + } + + @Override + public void cleanup() { + getAffectedEntities().clear(); + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.selfHeal", a -> { + AbilityEffect e = (AbilityEffect) a; + a.setName("Self Heal"); + e.setTargetingMode(TargetingMode.SELF); + e.setIncludeSelf(true); + e.setInstantHeal(true); + }), + new AbilityVariant("ability.variant.healingAura", a -> { + AbilityEffect e = (AbilityEffect) a; + a.setName("Healing Aura"); + e.setTargetingMode(TargetingMode.AOE_SELF); + e.setTargetFilter(TargetFilter.ALLIES); + e.setIncludeSelf(true); + e.setRadius(8.0f); + e.setBurstEnabled(true); + e.setBurstAmount(5); + e.setBurstDelay(20); + }), + new AbilityVariant("ability.variant.poisonDebuff", a -> { + AbilityEffect e = (AbilityEffect) a; + a.setName("Poison Debuff"); + e.setTargetingMode(TargetingMode.AOE_SELF); + e.setTargetFilter(TargetFilter.ENEMIES); + e.setIncludeSelf(false); + e.setHealAmount(0); + e.setHealPercent(0); + e.setRadius(6.0f); + e.setBurstEnabled(true); + e.setBurstAmount(3); + e.setBurstDelay(40); + }) + ); + } + + // ==================== NBT ==================== + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setInteger("durationTicks", durationTicks); + nbt.setFloat("healAmount", healAmount); + nbt.setFloat("healPercent", healPercent); + nbt.setBoolean("includeSelf", includeSelf); + nbt.setFloat("radius", radius); + nbt.setBoolean("instantHeal", instantHeal); + nbt.setString("targetFilter", targetFilter.name()); + + // Custom effects + INbtList ceList = new INbtList(); + for (AbilityCustomEffect ce : customEffects) { + ceList.appendTag(ce.writeNBT()); + } + nbt.setTag("customEffects", ceList); + + // Effect actions + INbtList eaList = new INbtList(); + for (AbilityEffectActionEntry ea : effectActions) { + eaList.appendTag(ea.writeNBT()); + } + nbt.setTag("effectActions", eaList); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.durationTicks = nbt.getInteger("durationTicks"); + this.healAmount = nbt.getFloat("healAmount"); + this.healPercent = nbt.getFloat("healPercent"); + this.includeSelf = nbt.getBoolean("includeSelf"); + this.radius = nbt.getFloat("radius"); + this.instantHeal = nbt.getBoolean("instantHeal"); + this.targetFilter = TargetFilter.fromString(nbt.getString("targetFilter")); + + // Custom effects + customEffects.clear(); + if (nbt.hasKey("customEffects")) { + INbtList ceList = nbt.getTagList("customEffects", 10); + for (int i = 0; i < ceList.tagCount(); i++) { + AbilityCustomEffect ce = AbilityCustomEffect.fromNBT(ceList.getCompoundTagAt(i)); + if (ce.isValid()) customEffects.add(ce); + } + } + + // Effect actions + effectActions.clear(); + if (nbt.hasKey("effectActions")) { + INbtList eaList = nbt.getTagList("effectActions", 10); + for (int i = 0; i < eaList.tagCount(); i++) { + AbilityEffectActionEntry ea = AbilityEffectActionEntry.fromNBT(eaList.getCompoundTagAt(i)); + effectActions.add(ea); + } + } + } + + // ==================== GUI ==================== + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.boolField("ability.instantHeal", this::isInstantHeal, this::setInstantHeal) + .hover("ability.hover.instant"), + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 1000).visibleWhen(() -> !this.isInstantHeal()), + FieldDef.section("ability.section.healing"), + FieldDef.row( + FieldDef.floatField("ability.healAmount", this::getHealAmount, this::setHealAmount), + FieldDef.floatField("ability.healPercent", this::getHealPercent, this::setHealPercent) + ), + FieldDef.section("ability.section.targeting") + .tab("Target").visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF), + FieldDef.enumField("ability.targetFilter", TargetFilter.class, + this::getTargetFilter, this::setTargetFilter) + .tab("Target").visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF), + FieldDef.row( + FieldDef.boolField("ability.includeSelf", this::isIncludeSelf, this::setIncludeSelf) + .visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius) + .visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF) + ).tab("Target"), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), + AbilityFieldDefs.customEffectsListField("ability.customEffects", this::getCustomEffects, this::setCustomEffects), + AbilityFieldDefs.effectActionsListField("ability.effectActions", this::getEffectActionEntries, this::setEffectActionEntries) + .visibleWhen(() -> AbilityController.Instance.hasEffectActions()) + )); + } + + // ==================== GETTERS & SETTERS ==================== + + @Override + public int getDurationTicks() { + return durationTicks; + } + + @Override + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + @Override + public float getHealAmount() { + return healAmount; + } + + @Override + public void setHealAmount(float healAmount) { + this.healAmount = healAmount; + } + + @Override + public float getHealPercent() { + return healPercent; + } + + @Override + public void setHealPercent(float healPercent) { + this.healPercent = healPercent; + } + + @Override + public boolean isIncludeSelf() { + return includeSelf; + } + + @Override + public void setIncludeSelf(boolean includeSelf) { + this.includeSelf = includeSelf; + } + + @Override + public float getRadius() { + return radius; + } + + @Override + public void setRadius(float radius) { + this.radius = radius; + } + + @Override + public boolean isInstantHeal() { + return instantHeal; + } + + @Override + public void setInstantHeal(boolean instantHeal) { + this.instantHeal = instantHeal; + } + + public TargetFilter getTargetFilter() { + return targetFilter; + } + + public void setTargetFilter(TargetFilter targetFilter) { + this.targetFilter = targetFilter; + } + + @Override + public int getTargetFilterType() { + return targetFilter.ordinal(); + } + + @Override + public void setTargetFilterType(int filter) { + TargetFilter[] values = TargetFilter.values(); + if (filter >= 0 && filter < values.length) { + this.targetFilter = values[filter]; + } + } + + public List getCustomEffects() { + return customEffects; + } + + public void setCustomEffects(List customEffects) { + this.customEffects = customEffects != null ? customEffects : new ArrayList<>(); + } + + @Override + public int getCustomEffectCount() { + return customEffects.size(); + } + + public List getEffectActionEntries() { + return effectActions; + } + + public void setEffectActionEntries(List effectActions) { + this.effectActions = effectActions != null ? effectActions : new ArrayList<>(); + } + + @Override + public int getEffectActionCount() { + return effectActions.size(); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java new file mode 100644 index 000000000..593c6d3cb --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -0,0 +1,110 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import noppes.npcs.api.ability.type.IAbilityGuard; +import java.util.List; + +/** + * Guard ability: Defensive stance that reduces incoming damage. + */ +public class AbilityGuard extends AbilityDefend implements IAbilityGuard { + + private float damageReduction = 0.5f; + + public AbilityGuard() { + this.typeId = "ability.cnpc.guard"; + this.name = "Guard"; + this.cooldownTicks = 0; + this.allowedBy = UserType.BOTH; + + this.activeAnimationName = "Ability_Guard_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png") + }; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOKS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Guard accepts ALL damage types (melee, projectile, magic, fire, IExplosion). + */ + @Override + protected boolean isValidDamageSource(IDamageSource source) { + return true; + } + + @Override + protected float performDefend(IEntityLivingBase attacker, float amount) { + return Math.max(0, amount * (1.0f - damageReduction)); + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void writeSubTypeNBT(INbt nbt) { + nbt.setFloat("damageReduction", damageReduction); + } + + @Override + protected void readSubTypeNBT(INbt nbt) { + this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f; + } + + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + + @ClientOnly + @Override + protected void getTypeDefinitions(List defs) { + defs.add(FieldDef.floatField("ability.damageReduction", this::getDamageReduction, this::setDamageReduction)); + + FieldDef.modifyVisibility(defs, "ability.windUpAnimation", () -> false); + FieldDef.modifyVisibility(defs, "ability.windUpSound", () -> false); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + @Override + public float getDamageReduction() { + return damageReduction; + } + + @Override + public void setDamageReduction(float damageReduction) { + this.damageReduction = damageReduction; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java new file mode 100644 index 000000000..a650f29ca --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -0,0 +1,200 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import noppes.npcs.api.ability.type.IAbilityHazard; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AbilityHazard extends AbilityZone implements IAbilityHazard { + + private float radius = 2.0f; + private float damagePerSecond = 1.0f; + private int damageInterval = 20; + private boolean affectsCaster = false; + + public AbilityHazard() { + super(300, new EnergyDisplayData(0x00CC00, 0x006600, true, 1.0f, 0.5f, 1.5f)); + this.typeId = "ability.cnpc.hazard"; + this.name = "Hazard"; + this.windUpTicks = 30; + this.windUpAnimationName = "Ability_Zone_Windup"; + this.activeAnimationName = "Ability_Zone_Active"; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/hazard.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/hazard_overlay.png", + this::getActiveColor) + }; + } + + @Override + public boolean isConcurrentCapable() { + return true; + } + + @Override + public float getZoneRadius() { + return radius; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + activeEntities.clear(); + + List positions; + if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) { + positions = new ArrayList<>(preCalculatedPositions); + preCalculatedPositions.clear(); + } else { + positions = new ArrayList<>(); + List placedPositions = new ArrayList<>(); + float minSeparation = radius * 2.0f; + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + positions.add(pos); + } + } + + for (int i = 0; i < zoneCount; i++) { + double[] pos = positions.get(i); + + EntityAbilityZone IEntity = EntityAbilityZone.createHazard(caster.worldObj, caster, + pos[0], caster.posY, pos[1], + zoneShape, + radius, + damagePerSecond, damageInterval, + isIgnoreIFrames(), affectsCaster, + durationTicks, + colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled, + zoneHeight, + particleDensity, particleScale, animSpeed, lightningDensity, + getEffects()); + IEntity.setSourceAbility(this); + + applyVisualToEntity(IEntity); + + if (isPreview()) { + IEntity.setupPreview(caster); + } + + spawnAbilityEntity(IEntity); + activeEntities.add(IEntity); + } + } + + @Override + public void writeTypeNBT(INbt nbt) { + writeZoneNBT(nbt); + nbt.setFloat("radius", radius); + nbt.setFloat("damagePerSecond", damagePerSecond); + nbt.setInteger("damageInterval", damageInterval); + nbt.setBoolean("affectsCaster", affectsCaster); + } + + @Override + public void readTypeNBT(INbt nbt) { + readZoneNBT(nbt, 300); + this.radius = nbt.getFloat("radius"); + this.damagePerSecond = nbt.getFloat("damagePerSecond"); + this.damageInterval = nbt.getInteger("damageInterval"); + this.affectsCaster = nbt.getBoolean("affectsCaster"); + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public float getDamagePerSecond() { + return damagePerSecond; + } + + public void setDamagePerSecond(float damagePerSecond) { + this.damagePerSecond = damagePerSecond; + } + + @Override + public float getDisplayDamage() { return damagePerSecond; } + + @Override + public boolean isDisplayDamageDPS() { return true; } + + public int getDamageInterval() { + return damageInterval; + } + + public void setDamageInterval(int damageInterval) { + this.damageInterval = damageInterval; + } + + public boolean isAffectsCaster() { + return affectsCaster; + } + + public void setAffectsCaster(boolean affectsCaster) { + this.affectsCaster = affectsCaster; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + addPresetFieldDef(defs); + + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), + FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum) + ), + FieldDef.section("ability.section.zone"), + FieldDef.row( + FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius), + FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20) + ), + FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight), + FieldDef.section("ability.section.area"), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius), + FieldDef.section("ability.section.damage"), + FieldDef.row( + FieldDef.floatField("gui.dps", this::getDamagePerSecond, this::setDamagePerSecond), + FieldDef.intField("gui.interval", this::getDamageInterval, this::setDamageInterval) + ), + FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) + .hover("ability.hover.affectsCaster"), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + + addVisualFieldDefs(defs); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java new file mode 100644 index 000000000..856e218c7 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -0,0 +1,260 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityHeavyHit; +import java.util.Arrays; +import java.util.List; + +/** + * Heavy Hit ability: AOE rectangle melee attack in front of the caster. + * Deals high damage to all entities within a rectangular zone and can apply effects. + * Has a LINE telegraph showing the hit area during windup, making it dodgeable. + */ +public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { + + private float damage = 8.0f; + private float knockback = 2.0f; + private float hitLength = 4.0f; // How far in front of the caster the hit reaches + private float hitWidth = 3.0f; // How wide to each side (total width = hitWidth * 2) + private int hitDelayTicks = 0; + private int activeDisplayTicks = 10; // How long active animation plays before completing + + public AbilityHeavyHit() { + this.typeId = "ability.cnpc.heavy_hit"; + this.name = "Heavy Hit"; + this.targetingMode = TargetingMode.AOE_SELF; + this.maxRange = 5.0f; + this.minRange = 0.0f; + this.lockMovement = LockMode.WINDUP; + this.cooldownTicks = 0; + this.windUpTicks = 30; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + this.windUpSound = "random.anvil_use"; + this.activeSound = "random.anvil_land"; + this.windUpAnimationName = "Ability_HeavyHit_Windup"; + this.activeAnimationName = "Ability_HeavyHit_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/heavy_hit.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/heavy_hit_overlay.png", + this::getActiveColor) + }; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + @Override + public float getTelegraphLength() { + return hitLength; + } + + @Override + public float getTelegraphWidth() { + return hitWidth * 2.0f; // Total width (hitWidth is distance to each side) + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + if (caster.worldObj.isRemote && !isPreview()) { + signalCompletion(); + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (isPreview()) { + if (tick >= (activeDisplayTicks + hitDelayTicks)) + signalCompletion(); + + return; + } + + if (tick >= (activeDisplayTicks + hitDelayTicks)) { + signalCompletion(); + return; + } + + if (tick == hitDelayTicks && !caster.worldObj.isRemote) { + // Calculate forward and right vectors from caster yaw + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double forwardX = -Math.sin(yawRad); + double forwardZ = Math.cos(yawRad); + double rightX = forwardZ; // perpendicular right + double rightZ = -forwardX; + + // Search area: AABB that encompasses the rectangle + float searchDist = Math.max(hitLength, hitWidth) + 1.0f; + @SuppressWarnings("unchecked") + List entities = caster.worldObj.getEntitiesWithinAABBExcludingEntity( + caster, caster.boundingBox.expand(searchDist, 2, searchDist)); + + boolean anyHit = false; + for (IEntity IEntity : entities) { + if (!(IEntity instanceof IEntityLivingBase) || IEntity == caster) continue; + IEntityLivingBase livingTarget = (IEntityLivingBase) IEntity; + if (!AbilityTargetHelper.shouldAffect(caster, livingTarget, TargetFilter.ENEMIES, false)) continue; + + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; + + // Project onto forward direction (must be in front, within hitLength) + double forwardDist = dx * forwardX + dz * forwardZ; + if (forwardDist < 0 || forwardDist > hitLength) continue; + + // Project onto right direction (must be within hitWidth to each side) + double sideDist = dx * rightX + dz * rightZ; + if (Math.abs(sideDist) > hitWidth) continue; + + // Line-of-sight check: skip targets behind solid blocks or enemy barriers + if (!hasLineOfSight(caster.worldObj, caster, livingTarget)) continue; + if (isBlockedByBarrier(caster.worldObj, caster, livingTarget)) continue; + + // IEntity is within the rectangle - apply damage + boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback); + if (wasHit) { + applyEffects(livingTarget); + anyHit = true; + } + } + + // Play hit sound even if nothing was hit (the attack still happens) + if (!anyHit) { + caster.worldObj.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f); + } + } + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("hitLength", hitLength); + nbt.setFloat("hitWidth", hitWidth); + nbt.setInteger("activeDisplayTicks", activeDisplayTicks); + nbt.setInteger("hitDelayTicks", hitDelayTicks); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.damage = nbt.getFloat("damage"); + this.knockback = nbt.getFloat("knockback"); + this.hitLength = nbt.getFloat("hitLength"); + this.hitWidth = nbt.getFloat("hitWidth"); + this.hitDelayTicks = nbt.getInteger("hitDelayTicks"); + this.activeDisplayTicks = nbt.getInteger("activeDisplayTicks"); + } + + // Getters & Setters + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public float getHitLength() { + return hitLength; + } + + public void setHitLength(float hitLength) { + this.hitLength = hitLength; + } + + public float getHitWidth() { + return hitWidth; + } + + public void setHitWidth(float hitWidth) { + this.hitWidth = hitWidth; + } + + public int getActiveDisplayTicks() { + return activeDisplayTicks; + } + + public void setActiveDisplayTicks(int activeDisplayTicks) { + this.activeDisplayTicks = activeDisplayTicks; + } + + public int getHitDelayTicks() { + return hitDelayTicks; + } + + public void setHitDelayTicks(int hitDelayTicks) { + this.hitDelayTicks = hitDelayTicks; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback) + ), + FieldDef.section("ability.section.hitZone"), + FieldDef.row( + FieldDef.floatField("ability.hitLength", this::getHitLength, this::setHitLength), + FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth) + ), + FieldDef.section("ability.section.timing"), + FieldDef.row( + FieldDef.intField("ability.hitDelayTicks", this::getHitDelayTicks, this::setHitDelayTicks).min(0), + FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200) + ), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java new file mode 100644 index 000000000..b242c31c3 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java @@ -0,0 +1,323 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +/** + * Abstract base class for abilities that move the caster along the ground + * in a specific direction (Charge, Dash, etc.). + *

+ * Provides shared movement logic: direction locking, stall detection with + * player-aware grace period, velocity application, distance tracking, + * rotation enforcement, and wall collision checking. + *

+ * NOT for ballistic/arc movement (Slam uses a different pattern). + */ +public abstract class AbilityMovement extends Ability { + + /** + * Grace period (in active ticks) before stall detection activates. + * Players need more time because: + * - Server sets motionX/Z and sends S12 velocity packet to client + * - Client receives S12 and starts moving + * - Client sends C03 position packet with updated position back to server + * Until the round trip completes, the server sees the old (stationary) position, + * which would falsely trigger stall detection. + */ + private static final int PLAYER_STALL_GRACE_TICKS = 8; + private static final int NPC_STALL_GRACE_TICKS = 2; + + // ═══════════════════════════════════════════════════════════════════ + // COMMON MOVEMENT STATE (transient, not saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + protected transient double startX, startY, startZ; + protected transient double prevTickX, prevTickZ; + protected transient IVector3 movementDirection; + protected transient float lockedYaw; + protected transient int maxActiveTicks; + + // ═══════════════════════════════════════════════════════════════════ + // OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean hasAbilityMovement() { + return true; + } + + @Override + public void cleanup() { + movementDirection = null; + maxActiveTicks = 0; + } + + @Override + public void resetForBurst() { + movementDirection = null; + maxActiveTicks = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // DIRECTION HELPERS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Lock direction based on caster type. + * NPC: toward target. Player: look direction. + */ + protected void lockDirection(IEntityLivingBase caster, IEntityLivingBase target) { + if (!isPlayerCaster(caster) && target != null) { + lockDirectionToTarget(caster, target); + } else { + lockDirectionFromLook(caster); + } + } + + /** + * Lock direction to the caster's look direction (horizontal only). + */ + protected void lockDirectionFromLook(IEntityLivingBase caster) { + float yaw = (float) Math.toRadians(caster.rotationYaw); + movementDirection = IVector3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); + lockedYaw = computeYawFromDirection(); + } + + /** + * Lock direction toward a target IEntity. + * Falls back to look direction if target is at the same position. + */ + protected void lockDirectionToTarget(IEntityLivingBase caster, IEntityLivingBase target) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + movementDirection = IVector3.createVectorHelper(dx / len, 0, dz / len); + lockedYaw = computeYawFromDirection(); + } else { + lockDirectionFromLook(caster); + } + } + + /** + * Get the base yaw for direction calculation. + * NPC: yaw toward target. Player: current look yaw. + */ + protected float getBaseYaw(IEntityLivingBase caster, IEntityLivingBase target) { + if (!isPlayerCaster(caster) && target != null) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + return (float) Math.toDegrees(Math.atan2(-dx, dz)); + } + return caster.rotationYaw; + } + + /** + * Set movement direction from a yaw angle (degrees). + */ + protected void setDirectionFromYaw(float yawDegrees) { + float yawRad = (float) Math.toRadians(yawDegrees); + movementDirection = IVector3.createVectorHelper(-Math.sin(yawRad), 0, Math.cos(yawRad)); + lockedYaw = yawDegrees; + } + + // ═══════════════════════════════════════════════════════════════════ + // MOVEMENT INITIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Initialize movement tracking. Call from onExecute(). + * + * @param caster The IEntity performing the movement + * @param distance Maximum travel distance (for timeout computation) + * @param speed Movement speed in blocks/tick (for timeout computation) + */ + protected void initMovement(IEntityLivingBase caster, float distance, float speed) { + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; + prevTickX = caster.posX; + prevTickZ = caster.posZ; + maxActiveTicks = speed > 0 ? (int) (distance / speed) + 10 : 10; + } + + // ═══════════════════════════════════════════════════════════════════ + // ACTIVE TICK CHECKS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check for timeout or missing movement direction. + * + * @return true if the ability should stop + */ + protected boolean checkTimeout(int tick) { + return !isPreview() && (movementDirection == null || tick > maxActiveTicks); + } + + /** + * Check stall detection with appropriate grace period for player vs NPC. + * Players need a longer grace period because velocity sync via S12 packet + * takes time to reach the client, and C03 position packets lag behind. + * + * @return true if the IEntity is stalled (not moving) + */ + protected boolean checkStall(IEntityLivingBase caster, int tick) { + if (isPreview()) return false; + int graceTicks = isPlayerCaster(caster) ? PLAYER_STALL_GRACE_TICKS : NPC_STALL_GRACE_TICKS; + if (tick <= graceTicks) return false; + + double dx = caster.posX - prevTickX; + double dz = caster.posZ - prevTickZ; + return dx * dx + dz * dz < 0.0001; + } + + /** + * Update previous tick position for stall detection. + * Call after stall check, before movement application. + */ + protected void updatePrevPosition(IEntityLivingBase caster) { + prevTickX = caster.posX; + prevTickZ = caster.posZ; + } + + /** + * Get total linear distance traveled from start position. + */ + protected double getDistanceTraveled(IEntityLivingBase caster) { + double dx = caster.posX - startX; + double dz = caster.posZ - startZ; + return Math.sqrt(dx * dx + dz * dz); + } + + /** + * Get total distance traveled squared (more efficient for comparison). + */ + protected double getDistanceTraveledSq(IEntityLivingBase caster) { + double dx = caster.posX - startX; + double dz = caster.posZ - startZ; + return dx * dx + dz * dz; + } + + /** + * Check if movement direction is blocked by walls or solid barriers. + * + * @return true if blocked + */ + protected boolean checkBlocked(IEntityLivingBase caster, float speed) { + if (isPreview() || movementDirection == null) return false; + return isMovementBlocked(caster, movementDirection.xCoord, movementDirection.zCoord, speed) + || isMovementBlockedByBarrier(caster, movementDirection.xCoord, movementDirection.zCoord, speed); + } + + // ═══════════════════════════════════════════════════════════════════ + // VELOCITY APPLICATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Send an authoritative velocity packet to a player. + *

+ * The ability tick runs BEFORE moveEntityWithHeading (at PlayerTickEvent.START). + * If we use velocityChanged, the IEntity tracker sends S12 AFTER friction has + * reduced the motion (~55% of set value). The client then moves at reduced speed, + * reports a short position via C04, and the server resets to the client's position. + *

+ * By sending S12 manually here — before friction — the client receives the full + * velocity and moves at the correct speed. + */ + protected void sendPlayerVelocity(IEntityLivingBase caster) { + if (caster instanceof IPlayer) { + ((IPlayer) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } + } + + /** + * Apply horizontal momentum and sync to client. + */ + protected void applyHorizontalMomentum(IEntityLivingBase caster, double motionX, double motionZ) { + caster.motionX = motionX; + caster.motionZ = motionZ; + if (!isPreview()) { + if (caster instanceof IPlayer) { + sendPlayerVelocity(caster); + } else { + caster.velocityChanged = true; + } + } + } + + /** + * Apply movement velocity to the caster (horizontal only, Y unchanged). + */ + protected void applyVelocity(IEntityLivingBase caster, float speed) { + applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed); + } + + /** + * Apply movement velocity with zero vertical motion. + * Used for ground-locked movement like Charge. + * motionY is set BEFORE sending velocity packet to prevent client-server desync + * (which causes "moved too fast" kicks when used in mid-air). + */ + protected void applyVelocityFlat(IEntityLivingBase caster, float speed) { + caster.motionY = 0; + applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed); + } + + /** + * Stop all horizontal momentum. + */ + protected void stopMomentum(IEntityLivingBase caster) { + applyHorizontalMomentum(caster, 0, 0); + } + + // ═══════════════════════════════════════════════════════════════════ + // ROTATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Compute yaw angle (degrees) from current movement direction. + */ + protected float computeYawFromDirection() { + if (movementDirection == null) return 0; + return (float) Math.toDegrees(Math.atan2(-movementDirection.xCoord, movementDirection.zCoord)); + } + + /** + * Enforce locked rotation on all yaw fields. + * Sets rotationYaw, rotationYawHead, prevRotationYaw, prevRotationYawHead, + * renderYawOffset, and prevRenderYawOffset to lockedYaw. + */ + protected void enforceLockedRotation(IEntityLivingBase caster) { + caster.rotationYaw = lockedYaw; + caster.rotationYawHead = lockedYaw; + caster.prevRotationYaw = lockedYaw; + caster.prevRotationYawHead = lockedYaw; + caster.renderYawOffset = lockedYaw; + caster.prevRenderYawOffset = lockedYaw; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java new file mode 100644 index 000000000..307e4a6c6 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -0,0 +1,391 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityProjectile; +import java.util.Arrays; +import java.util.List; + +/** + * Projectile ability: Ranged attack that deals damage to target. + * Currently instant damage - can be enhanced with custom projectile entities. + */ +public class AbilityProjectile extends Ability implements IAbilityProjectile { + + // Type-specific parameters + private float damage = 6.0f; + private float speed = 1.5f; + private float knockback = 0.5f; + private String projectileType = "fireball"; + private boolean explosive = false; + private float explosionRadius = 0.0f; + private boolean homing = false; + private float homingStrength = 0.1f; + + public AbilityProjectile() { + this.typeId = "ability.cnpc.projectile"; + this.name = "Projectile"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.minRange = 5.0f; + this.maxRange = 20.0f; + this.cooldownTicks = 0; + this.windUpTicks = 15; + this.lockMovement = LockMode.NO; + // No telegraph for projectile - it's a ranged attack + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + } + + @Override + public boolean allowBurst() { + return false; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + if (caster.worldObj.isRemote && !isPreview()) { + signalCompletion(); + return; + } + + if (isPlayerCaster(caster)) { + // Player: instant hit-scan in look direction + executePlayerProjectile(caster, caster.worldObj); + } else if (target != null) { + // NPC: instant hit-scan on aggro target + executeNpcProjectile(caster, target, caster.worldObj); + } + + signalCompletion(); + } + + /** + * NPC projectile: instant hit-scan damage on the aggro target. + */ + private void executeNpcProjectile(IEntityLivingBase caster, IEntityLivingBase target, IWorld IWorld) { + if (isPreview()) return; + + double dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + dx /= len; + dy /= len; + dz /= len; + } + + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); + IWorld.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + + if (explosive && explosionRadius > 0) { + applyExplosionDamage(caster, target, IWorld, target.posX, target.posY, target.posZ); + } + + spawnProjectileParticles(IWorld, caster, target); + } + + /** + * Player projectile: fires instant hit-scan in look direction. + * Raycasts for the first IEntity in range and applies damage. + */ + private void executePlayerProjectile(IEntityLivingBase caster, IWorld IWorld) { + if (isPreview()) return; + + IVector3 look = caster.getLookVec(); + double eyeX = caster.posX; + double eyeY = caster.posY + caster.getEyeHeight(); + double eyeZ = caster.posZ; + + // Raycast: find the closest IEntity along look direction + IEntityLivingBase hitTarget = null; + double closestDist = maxRange; + + @SuppressWarnings("unchecked") + List candidates = IWorld.getEntitiesWithinAABBExcludingEntity(caster, + caster.boundingBox.expand(maxRange, maxRange, maxRange)); + + for (IEntity IEntity : candidates) { + if (!(IEntity instanceof IEntityLivingBase)) continue; + if (IEntity.isDead) continue; + + // Check if IEntity is roughly along look direction + double dx = IEntity.posX - eyeX; + double dy = (IEntity.posY + IEntity.height / 2) - eyeY; + double dz = IEntity.posZ - eyeZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist > maxRange || dist < 0.5) continue; + + // Dot product to check alignment with look vector + double dot = (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord) / dist; + if (dot < 0.95) continue; // Must be within ~18 degree cone + + if (dist < closestDist) { + closestDist = dist; + hitTarget = (IEntityLivingBase) IEntity; + } + } + + IWorld.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + + if (hitTarget != null) { + applyAbilityDamageWithDirection(caster, hitTarget, damage, knockback, look.xCoord, look.zCoord); + + if (explosive && explosionRadius > 0) { + applyExplosionDamage(caster, hitTarget, IWorld, hitTarget.posX, hitTarget.posY, hitTarget.posZ); + } + + spawnProjectileParticles(IWorld, caster, hitTarget); + } else { + // No hit — spawn particles along look direction anyway + spawnProjectileParticlesInDirection(IWorld, caster, look); + } + } + + /** + * Apply IExplosion splash damage around a point. + */ + private void applyExplosionDamage(IEntityLivingBase caster, IEntityLivingBase primaryTarget, IWorld IWorld, + double x, double y, double z) { + @SuppressWarnings("unchecked") + List entities = IWorld.getEntitiesWithinAABBExcludingEntity(primaryTarget, + primaryTarget.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); + + for (IEntity IEntity : entities) { + if (IEntity instanceof IEntityLivingBase && IEntity != caster) { + IEntityLivingBase living = (IEntityLivingBase) IEntity; + float dist = primaryTarget.getDistanceToEntity(living); + if (dist < explosionRadius) { + float falloff = 1.0f - (dist / explosionRadius); + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); + } + } + } + + IWorld.playSoundAtEntity(primaryTarget, "random.explode", 0.5f, 1.0f); + for (int i = 0; i < 10; i++) { + IWorld.spawnParticle("explode", + x + (IWorld.rand.nextDouble() - 0.5) * explosionRadius, + y + IWorld.rand.nextDouble() * 2, + z + (IWorld.rand.nextDouble() - 0.5) * explosionRadius, + 0, 0.1, 0); + } + } + + private void spawnProjectileParticles(IWorld IWorld, IEntityLivingBase caster, IEntityLivingBase target) { + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; + double endX = target.posX; + double endY = target.posY + target.height / 2; + double endZ = target.posZ; + + double dx = endX - startX; + double dy = endY - startY; + double dz = endZ - startZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + String particle = "flame"; + if (projectileType.equals("arrow")) { + particle = "crit"; + } else if (projectileType.equals("magic")) { + particle = "witchMagic"; + } + + // Spawn particles along path + int steps = (int) Math.max(5, dist * 2); + for (int i = 0; i < steps; i++) { + double progress = (double) i / steps; + double px = startX + dx * progress; + double py = startY + dy * progress; + double pz = startZ + dz * progress; + IWorld.spawnParticle(particle, px, py, pz, 0, 0, 0); + } + } + + /** + * Spawn projectile particles along a direction (player miss — no IEntity hit). + */ + private void spawnProjectileParticlesInDirection(IWorld IWorld, IEntityLivingBase caster, IVector3 look) { + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; + + String particle = "flame"; + if (projectileType.equals("arrow")) { + particle = "crit"; + } else if (projectileType.equals("magic")) { + particle = "witchMagic"; + } + + int steps = (int) Math.max(5, maxRange * 2); + for (int i = 0; i < steps; i++) { + double progress = (double) i / steps * maxRange; + double px = startX + look.xCoord * progress; + double py = startY + look.yCoord * progress; + double pz = startZ + look.zCoord * progress; + IWorld.spawnParticle(particle, px, py, pz, 0, 0, 0); + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + // Projectile is instant, nothing to do per-tick + } + + @Override + public float getTelegraphRadius() { + return 0; // No telegraph for projectile + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("damage", damage); + nbt.setFloat("speed", speed); + nbt.setFloat("knockback", knockback); + nbt.setString("projectileType", projectileType); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setBoolean("homing", homing); + nbt.setFloat("homingStrength", homingStrength); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.damage = nbt.getFloat("damage"); + this.speed = nbt.getFloat("speed"); + this.knockback = nbt.getFloat("knockback"); + this.projectileType = nbt.getString("projectileType"); + this.explosive = nbt.getBoolean("explosive"); + this.explosionRadius = nbt.getFloat("explosionRadius"); + this.homing = nbt.getBoolean("homing"); + this.homingStrength = nbt.getFloat("homingStrength"); + } + + // Getters & Setters + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getSpeed() { + return speed; + } + + public void setSpeed(float speed) { + this.speed = speed; + } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public String getProjectileType() { + return projectileType; + } + + public void setProjectileType(String projectileType) { + this.projectileType = projectileType; + } + + public boolean isExplosive() { + return explosive; + } + + public void setExplosive(boolean explosive) { + this.explosive = explosive; + } + + public float getExplosionRadius() { + return explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + this.explosionRadius = explosionRadius; + } + + public boolean isHoming() { + return homing; + } + + public void setHoming(boolean homing) { + this.homing = homing; + } + + public float getHomingStrength() { + return homingStrength; + } + + public void setHomingStrength(float homingStrength) { + this.homingStrength = homingStrength; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) + ), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.stringEnumField("ability.projectileType", new String[]{"fireball", "arrow", "magic"}, this::getProjectileType, this::setProjectileType), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java new file mode 100644 index 000000000..72e4e5443 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -0,0 +1,255 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityShockwave; +import java.util.Arrays; +import java.util.List; + +/** + * Shockwave ability: Pushes targets away from the caster with damage. + * Opposite of Vortex - instant knockback rather than gradual pull. + */ +public class AbilityShockwave extends Ability implements IAbilityShockwave { + + private float pushRadius = 8.0f; + private float pushStrength = 1.5f; + private float damage = 8.0f; + private boolean aoe = true; + private int activeDisplayTicks = 10; + + public AbilityShockwave() { + this.typeId = "ability.cnpc.shockwave"; + this.name = "Shockwave"; + this.targetingMode = TargetingMode.AOE_SELF; + this.maxRange = 8.0f; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; + this.windUpTicks = 25; + this.telegraphType = TelegraphType.CIRCLE; + this.windUpSound = "game.tnt.primed"; + this.activeSound = "random.explode"; + this.windUpAnimationName = "Ability_Shockwave_Windup"; + this.activeAnimationName = "Ability_Shockwave_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/shockwave.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/shockwave_overlay.png", + this::getActiveColor) + }; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + @Override + public float getTelegraphRadius() { + return pushRadius; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + if (!isPreview() && !caster.worldObj.isRemote) { + // Shockwave is instant - apply effect immediately after windup + + // Get all entities in radius + IBoundingBox box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); + @SuppressWarnings("unchecked") + List entities = caster.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, box); + + if (aoe) { + // ALL: push every valid enemy in range + for (IEntityLivingBase IEntity : entities) { + if (IEntity == caster) continue; + if (IEntity.isDead) continue; + if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue; + + double dist = caster.getDistanceToEntity(IEntity); + if (dist > pushRadius) continue; + + applyShockwavePush(caster, IEntity, dist); + } + } else { + // SINGULAR: push one target + // NPC: push aggro target if valid, otherwise nearest enemy + // Player: push nearest enemy in range + if (!isPlayerCaster(caster) && target != null && !target.isDead) { + double dist = caster.getDistanceToEntity(target); + if (dist <= pushRadius && AbilityTargetHelper.shouldAffect(caster, target, TargetFilter.ENEMIES, false)) { + applyShockwavePush(caster, target, dist); + return; + } + } + + // Find nearest valid enemy + IEntityLivingBase nearest = null; + double nearestDist = Double.MAX_VALUE; + for (IEntityLivingBase IEntity : entities) { + if (IEntity == caster) continue; + if (IEntity.isDead) continue; + if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue; + + double dist = caster.getDistanceToEntity(IEntity); + if (dist <= pushRadius && dist < nearestDist) { + nearest = IEntity; + nearestDist = dist; + } + } + if (nearest != null) { + applyShockwavePush(caster, nearest, nearestDist); + } + } + } + } + + private void applyShockwavePush(IEntityLivingBase caster, IEntityLivingBase IEntity, double dist) { + // Calculate push direction (away from caster) + double dx = IEntity.posX - caster.posX; + double dz = IEntity.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dz * dz); + + if (len > 0) { + dx /= len; + dz /= len; + } else { + // IEntity is directly on top of caster, push in random direction + double angle = Math.random() * Math.PI * 2; + dx = Math.cos(angle); + dz = Math.sin(angle); + } + + // Scale knockback by distance (closer = stronger) + float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; + float finalPush = pushStrength * distFactor; + // Apply damage with custom knockback direction + boolean wasHit = applyAbilityDamageWithDirection(caster, IEntity, damage * distFactor, finalPush, dx, dz); + + // Apply effects if hit connected + if (wasHit) { + applyEffects(IEntity); + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (tick >= activeDisplayTicks) + signalCompletion(); + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("pushRadius", pushRadius); + nbt.setFloat("pushStrength", pushStrength); + nbt.setFloat("damage", damage); + nbt.setBoolean("aoe", aoe); + nbt.setInteger("activeDisplayTicks", activeDisplayTicks); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.pushRadius = nbt.getFloat("pushRadius"); + this.pushStrength = nbt.getFloat("pushStrength"); + this.damage = nbt.getFloat("damage"); + this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe"); + this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10; + } + + // Getters & Setters + public float getPushRadius() { + return pushRadius; + } + + public void setPushRadius(float pushRadius) { + this.pushRadius = pushRadius; + } + + public float getPushStrength() { + return pushStrength; + } + + public void setPushStrength(float pushStrength) { + this.pushStrength = pushStrength; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public boolean isAoe() { + return aoe; + } + + public void setAoe(boolean aoe) { + this.aoe = aoe; + } + + public int getActiveDisplayTicks() { + return activeDisplayTicks; + } + + public void setActiveDisplayTicks(int activeDisplayTicks) { + this.activeDisplayTicks = activeDisplayTicks; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.section("ability.section.push"), + FieldDef.row( + FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius), + FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength) + ), + FieldDef.section("ability.section.aoe"), + FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe) + .hover("ability.hover.aoe"), + FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java new file mode 100644 index 000000000..7f8046372 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -0,0 +1,841 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilitySlam; +import java.util.Arrays; +import java.util.List; + +/** + * Slam ability: NPC leaps toward target in an arc and slams down, dealing AOE damage on landing. + *

+ * Phases: + * - WINDUP: NPC prepares to leap, telegraph shows landing zone + * - ACTIVE: NPC is in the air, traveling in arc toward destination + * - Landing: When NPC hits ground, deal AOE damage + * - RECOVERY: Landing recovery animation + */ +public class AbilitySlam extends Ability implements IAbilitySlam { + + // Type-specific config + private float damage = 10.0f; + private float radius = 5.0f; + private float knockbackStrength = 1.5f; + private float leapSpeed = 1.0f; + private float leapHeight = 4.0f; + + /** + * Grace period (in active ticks) before landing detection activates. + * Players need more time because: + * - Server sets motionY and sends S12 velocity packet to client + * - Client receives S12 and starts moving upward + * - Client sends C03 position packet back to server with onGround=false + * Until the round trip completes, the server's player.onGround (from stale C03) + * remains true, which would falsely trigger landing detection. + */ + private static final int PLAYER_LANDING_GRACE_TICKS = 10; + private static final int NPC_LANDING_GRACE_TICKS = 3; + + // Runtime state + private transient double targetX, targetY, targetZ; + private transient double startY; // Y position at launch for rise detection + private transient boolean hasLaunched = false; + private transient boolean hasLanded = false; + private transient boolean hasRisen = false; // Whether IEntity has risen above launch position + private transient int airTicks = 0; + private transient int maxAirTicks = 60; // Timeout to prevent stuck in air + private transient boolean airSlam = false; // True when slam initiated while already in the air + private transient double airSlamStartY = 0; // Start height for air-slam damage scaling + private transient boolean wasFlying = false; // Restore player flight after air slam + private transient double peakY = 0; // Maximum Y position during arc (for height scaling) + + public AbilitySlam() { + this.typeId = "ability.cnpc.slam"; + this.name = "Slam"; + this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target + this.windUpTicks = 30; + this.cooldownTicks = 0; + this.lockMovement = LockMode.WINDUP; + this.minRange = 2.0f; + this.maxRange = 15.0f; + this.telegraphType = TelegraphType.CIRCLE; + this.windUpSound = "mob.irongolem.throw"; + this.activeSound = ""; + // Default built-in animations + this.windUpAnimationName = "Ability_Slam_Windup"; + this.activeAnimationName = "Ability_Slam_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/slam.png") + }; + } + + /** + * Slam can be AOE_SELF (jump in place) or AOE_TARGET (leap to target). + */ + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET}; + } + + @Override + public boolean hasAbilityMovement() { + return true; // This ability moves the NPC + } + + @Override + public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (isPlayerCaster(caster)) { + // Player: update target along look direction during windup + if (targetingMode == TargetingMode.AOE_SELF) { + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } else { + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + double launchDist = Math.max(4.0, maxRange * 0.5); + targetX = caster.posX + dirX * launchDist; + targetY = caster.posY; + targetZ = caster.posZ + dirZ * launchDist; + } + } else { + // NPC: update target position during windup for telegraph tracking + if (targetingMode == TargetingMode.AOE_SELF) { + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { + targetX = target.posX; + targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64); + targetZ = target.posZ; + } + } + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + hasLaunched = false; + hasLanded = false; + hasRisen = false; + airTicks = 0; + startY = caster.posY; + peakY = caster.posY; + caster.fallDistance = 0; + + if (isPlayerCaster(caster)) { + executePlayerSlam(caster); + } else { + executeNpcSlam(caster, target); + } + } + + /** + * NPC slam: Lock destination based on targeting mode, then launch in a ballistic arc toward it. + */ + private void executeNpcSlam(IEntityLivingBase caster, IEntityLivingBase target) { + if (targetingMode == TargetingMode.AOE_SELF) { + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { + targetX = target.posX; + targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64); + targetZ = target.posZ; + } else { + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } + launchTowardTarget(caster); + } + + /** + * Player slam: behavior depends on whether the player is on the ground or in the air. + *

+ * On ground: Launch in look direction using ballistic arc, full damage on landing. + * In air: Slam straight down from current height, damage scales with fall distance. + * Works regardless of flying state — position is forced downward each tick. + */ + private void executePlayerSlam(IEntityLivingBase caster) { + // Determine if this qualifies as an air slam (at least 3 blocks above ground) + boolean isAirborne = !caster.onGround; + boolean highEnoughForAirSlam = false; + + if (isAirborne) { + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ, 64); + highEnoughForAirSlam = (caster.posY - groundY) >= 3.0; + } + + if (isAirborne && highEnoughForAirSlam) { + // Air slam: player is already airborne (flying, falling, jumping) and high enough + airSlam = true; + airSlamStartY = caster.posY; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + + // If the player is flying, temporarily disable it so gravity/velocity apply normally. + if (caster instanceof IPlayer) { + IPlayer mp = (IPlayer) caster; + wasFlying = mp.capabilities.isFlying; + if (wasFlying) { + mp.capabilities.isFlying = false; + mp.sendPlayerAbilities(); + } + } + + hasLaunched = true; + hasRisen = true; // Skip rise detection — we're already above ground + + // Start descent smoothly; physics/collision handles exact landing surface. + // Only damp horizontal drift when movement is locked during ACTIVE. + if (isMovementLockedDuringActive()) { + caster.motionX *= 0.25; + caster.motionZ *= 0.25; + } + caster.motionY = Math.min(caster.motionY, -0.2); + + if (!isPreview()) { + if (caster instanceof IPlayer) { + ((IPlayer) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } else { + caster.velocityChanged = true; + } + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); + } + } else { + // Ground slam: normal ballistic arc (also used when airborne but not high enough) + airSlam = false; + airSlamStartY = 0; + wasFlying = false; + + // Calculate target position along player's look direction (horizontal only) + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + + double launchDist = Math.max(4.0, maxRange * 0.5); + + targetX = caster.posX + dirX * launchDist; + targetY = caster.posY; + targetZ = caster.posZ + dirZ * launchDist; + + launchTowardTarget(caster); + } + } + + /** + * Calculate the initial vertical velocity needed to reach a target height + * in Minecraft's physics (gravity=0.08, vertical drag=0.98). + * Uses iterative simulation since the drag makes a closed-form solution impractical. + */ + private static double calculateLaunchVelocity(double targetHeight) { + // Binary search for the initial velocity that reaches targetHeight + double lo = 0.0, hi = 10.0; + for (int iter = 0; iter < 30; iter++) { + double mid = (lo + hi) / 2.0; + double maxY = simulateMaxHeight(mid); + if (maxY < targetHeight) { + lo = mid; + } else { + hi = mid; + } + } + return (lo + hi) / 2.0; + } + + /** + * Simulate Minecraft vertical physics to find peak height reached + * with a given initial upward velocity. + */ + private static double simulateMaxHeight(double vy) { + double y = 0; + double maxY = 0; + for (int t = 0; t < 200 && vy > 0; t++) { + y += vy; + vy -= 0.08; // gravity + vy *= 0.98; // vertical drag + if (y > maxY) maxY = y; + } + return maxY; + } + + /** + * Calculate ballistic arc and launch caster toward target. + * leapHeight = exact peak height in blocks above launch point. + * leapSpeed = velocity multiplier (faster motion, same height). + */ + private void launchTowardTarget(IEntityLivingBase caster) { + double dx = targetX - caster.posX; + double dy = targetY - caster.posY; + double dz = targetZ - caster.posZ; + double horizontalDist = Math.sqrt(dx * dx + dz * dz); + + // Calculate vertical velocity to reach exact leapHeight + double arcHeight = Math.max(1.0, leapHeight); + double vy = calculateLaunchVelocity(arcHeight); + + // For AOE_SELF or very close targets, just hop in place + if (horizontalDist < 0.5) { + caster.motionX = 0; + caster.motionZ = 0; + caster.motionY = vy; + hasLaunched = true; + if (!isPreview() && caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + if (!isPreview()) { + if (caster instanceof IPlayer) { + ((IPlayer) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } else { + caster.velocityChanged = true; + } + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); + } + return; + } + + // Cap the distance if too far + if (horizontalDist > maxRange) { + double scale = maxRange / horizontalDist; + dx *= scale; + dz *= scale; + horizontalDist = maxRange; + targetX = caster.posX + dx; + targetZ = caster.posZ + dz; + } + + // Choose flight time based on distance, scaled by speed (faster = fewer ticks) + double speedFactor = Math.max(0.1, leapSpeed); + int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40)); + + // Minecraft applies drag of 0.91 per tick to horizontal motion in air + // Total distance traveled with initial velocity v0 over n ticks: + // distance = v0 * (1 - drag^n) / (1 - drag) + // Solving for v0: v0 = distance * (1 - drag) / (1 - drag^n) + double drag = 0.91; + double dragPowN = Math.pow(drag, flightTicks); + double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN); + + // Adjust vertical velocity for height difference between start and target + if (dy > 0) { + vy += dy / flightTicks * 1.5; // Going up - need more velocity + } else if (dy < 0) { + vy += dy / flightTicks * 0.3; // Going down - gravity helps + } + + // Normalize horizontal direction and apply velocity + double dirX = dx / horizontalDist; + double dirZ = dz / horizontalDist; + + caster.motionX = dirX * vHorizontal; + caster.motionZ = dirZ * vHorizontal; + caster.motionY = vy; + + hasLaunched = true; + if (!isPreview() && caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + if (!isPreview()) { + // Use direct S12 packet for players to ensure velocity reaches client + // before flight code can override it + if (caster instanceof IPlayer) { + ((IPlayer) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } else { + caster.velocityChanged = true; + } + } + + // Face the target + float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (!hasLaunched) return; + if (hasLanded) return; + + boolean playerCaster = isPlayerCaster(caster); + boolean activeMovementLocked = isMovementLockedDuringActive(); + boolean forceHorizontalControl = !playerCaster || activeMovementLocked; + + airTicks++; + + // Continuously reset fall distance during slam to prevent fall damage + if (!isPreview()) { + caster.fallDistance = 0; + } + + // Track peak height for damage scaling + if (caster.posY > peakY) { + peakY = caster.posY; + } + + // Air slam: accelerate descent smoothly via velocity; landing uses normal collision. + if (airSlam && !isPreview() && !hasLanded) { + if (forceHorizontalControl) { + caster.motionX *= 0.2; + caster.motionZ *= 0.2; + } + double nextMotionY = caster.motionY - 0.18; + caster.motionY = Math.max(-1.6, nextMotionY); + if (caster instanceof IPlayer) { + // Only push authoritative velocity every tick when ACTIVE movement is locked. + if (forceHorizontalControl) { + ((IPlayer) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } + } else { + caster.velocityChanged = true; + } + } + + // Prevent NPC navigator from interfering with ballistic arc. + // lockMovement=WINDUP means applyMovementControl() doesn't clear + // the navigator during ACTIVE, so AI pathfinding could add motion. + if (!isPreview() && caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).getNavigator().clearPathEntity(); + } + + // Check if slam trajectory is blocked by a solid barrier + if (!isPreview() && !caster.worldObj.isRemote) { + double moveX = caster.motionX; + double moveZ = caster.motionZ; + double hSpeed = Math.sqrt(moveX * moveX + moveZ * moveZ); + if (hSpeed > 0.01) { + double normX = moveX / hSpeed; + double normZ = moveZ / hSpeed; + if (isMovementBlockedByBarrier(caster, normX, normZ, hSpeed)) { + caster.motionX = 0; + caster.motionZ = 0; + if (caster instanceof IPlayer) { + ((IPlayer) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } else { + caster.velocityChanged = true; + } + } + } + } + + // Track if IEntity has risen above launch position. + // This prevents premature landing detection for players, where + // server-side caster.onGround remains true from stale C03 packets + // until the velocity S12 round-trip completes (can take 4-8+ ticks). + if (!hasRisen && caster.posY > startY + 0.5) { + hasRisen = true; + } + + // Landing detection uses different grace periods for NPC vs Player. + // NPCs: server physics is authoritative, onGround is accurate after 3 ticks. + // Players: onGround depends on C03 packets, needs longer grace + rise confirmation. + int graceTicks = isPlayerCaster(caster) ? PLAYER_LANDING_GRACE_TICKS : NPC_LANDING_GRACE_TICKS; + if (airTicks > graceTicks) { + if (hasRisen && (caster.onGround || caster.isCollidedHorizontally)) { + onLanding(caster, caster.worldObj); + return; + } + // Safety: if IEntity hasn't risen after triple the grace period, + // the launch may have failed (blocked by ceiling, etc.) - force landing + if (!hasRisen && airTicks > graceTicks * 3) { + onLanding(caster, caster.worldObj); + return; + } + } + + // Timeout protection - force landing after max air time + if (airTicks >= maxAirTicks) { + onLanding(caster, caster.worldObj); + return; + } + + // Face toward destination while in air unless player ACTIVE movement is unlocked. + // Unlocked players keep free horizontal drift/look control. + if (!isPreview()) { + if (forceHorizontalControl) { + double dx = targetX - caster.posX; + double dz = targetZ - caster.posZ; + if (dx * dx + dz * dz > 0.25) { + float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; + if (playerCaster) { + caster.velocityChanged = true; + } + } + } + } + } + + /** + * Called when caster lands - deal AOE damage and signal completion. + */ + private void onLanding(IEntityLivingBase caster, IWorld IWorld) { + hasLanded = true; + if (!isPreview() && caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } + restoreFlightIfNeeded(caster); + + // Stop horizontal momentum only when movement is controlled by the ability. + if (!isPlayerCaster(caster) || isMovementLockedDuringActive()) { + caster.motionX = 0; + caster.motionZ = 0; + } + + if (!isPreview()) { + // Reset fall distance to prevent fall damage on landing + caster.fallDistance = 0; + caster.velocityChanged = true; + } + + if (IWorld.isRemote && !isPreview()) return; + if (isPreview()) return; + + // Play slam impact sound on landing + IWorld.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); + + // Scale damage based on height for both air and ground slams + float effectiveDamage = damage; + float heightMultiplier = 1.0f; + if (airSlam) { + // Air slam: scale based on total fall distance + double fallDistance = airSlamStartY - caster.posY; + if (fallDistance < 0) fallDistance = 0; + float heightFactor = (float) Math.min(1.0, fallDistance / Math.max(1.0, leapHeight)); + heightMultiplier = Math.max(0.25f, heightFactor); + effectiveDamage = damage * heightMultiplier; + } else { + // Ground slam: scale based on arc height reached + double arcHeight = peakY - startY; + if (arcHeight > 0) { + float heightFactor = (float) Math.min(1.0, arcHeight / Math.max(1.0, leapHeight)); + heightMultiplier = Math.max(0.25f, heightFactor); + effectiveDamage = damage * heightMultiplier; + } + } + + // Set multiplier so extenders (e.g. DBC Addon) can apply height scaling to their own damage calc + setDamageMultiplier(heightMultiplier); + + // Find all entities in radius + @SuppressWarnings("unchecked") + List entities = IWorld.getEntitiesWithinAABBExcludingEntity( + caster, caster.boundingBox.expand(radius, radius / 2, radius)); + + for (IEntity IEntity : entities) { + if (IEntity instanceof IEntityLivingBase && IEntity != caster) { + IEntityLivingBase livingTarget = (IEntityLivingBase) IEntity; + if (!AbilityTargetHelper.shouldAffect(caster, livingTarget, TargetFilter.ENEMIES, false)) continue; + + // Check if actually within radius (bounding box is a cube, we want a circle) + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; + if (dx * dx + dz * dz <= radius * radius) { + // Apply damage with scripted event support + boolean wasHit = applyAbilityDamage(caster, livingTarget, effectiveDamage, knockbackStrength); + if (wasHit) { + applyEffects(livingTarget); + } + } + } + } + + setDamageMultiplier(1.0f); // Reset after damage loop + + // Spawn particles + spawnSlamParticles(IWorld, caster.posX, caster.posY, caster.posZ); + + // Signal completion AFTER damage — signalCompletion triggers onComplete which resets state + signalCompletion(); + } + + /** + * Spawn visual particles for the slam effect. + */ + private void spawnSlamParticles(IWorld IWorld, double x, double y, double z) { + // Spawn IExplosion particles in a ring + for (int i = 0; i < 20; i++) { + double angle = (i / 20.0) * Math.PI * 2; + double px = x + Math.cos(angle) * radius * 0.8; + double pz = z + Math.sin(angle) * radius * 0.8; + IWorld.spawnParticle("explode", px, y + 0.5, pz, 0, 0.1, 0); + } + + // Spawn smoke at center + for (int i = 0; i < 10; i++) { + IWorld.spawnParticle("smoke", x, y + 0.2, z, + (IWorld.rand.nextDouble() - 0.5) * 0.3, + IWorld.rand.nextDouble() * 0.2, + (IWorld.rand.nextDouble() - 0.5) * 0.3); + } + + // Ground crack particles - check if block is valid + net.minecraft.block.Block groundBlock = IWorld.getBlock((int) x, (int) y - 1, (int) z); + if (groundBlock != null && groundBlock.getMaterial().isSolid()) { + int blockId = net.minecraft.block.Block.getIdFromBlock(groundBlock); + if (blockId > 0) { + for (int i = 0; i < 15; i++) { + double angle = IWorld.rand.nextDouble() * Math.PI * 2; + double dist = IWorld.rand.nextDouble() * radius * 0.6; + double px = x + Math.cos(angle) * dist; + double pz = z + Math.sin(angle) * dist; + IWorld.spawnParticle("blockcrack_" + blockId + "_0", px, y + 0.1, pz, 0, 0.2, 0); + } + } + } + } + + @Override + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) { + if (!isPreview() && caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } + restoreFlightIfNeeded(caster); + hasLaunched = false; + hasLanded = false; + hasRisen = false; + airTicks = 0; + airSlam = false; + airSlamStartY = 0; + peakY = 0; + wasFlying = false; + } + + @Override + public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) { + if (!isPreview() && caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } + restoreFlightIfNeeded(caster); + hasLaunched = false; + hasLanded = false; + hasRisen = false; + airTicks = 0; + airSlam = false; + airSlamStartY = 0; + peakY = 0; + wasFlying = false; + } + + @Override + public void cleanup() { + // Caster may be unavailable here; flight restoration is handled in onComplete/onInterrupt. + hasLaunched = false; + hasLanded = false; + hasRisen = false; + airTicks = 0; + airSlam = false; + airSlamStartY = 0; + peakY = 0; + wasFlying = false; + } + + @Override + public void reset() { + super.reset(); + hasLaunched = false; + hasLanded = false; + hasRisen = false; + airTicks = 0; + airSlam = false; + airSlamStartY = 0; + peakY = 0; + wasFlying = false; + } + + /** + * Restore player flight if it was disabled for an air slam. + */ + private void restoreFlightIfNeeded(IEntityLivingBase caster) { + if (airSlam && wasFlying && caster instanceof IPlayer) { + IPlayer mp = (IPlayer) caster; + if (!mp.capabilities.isFlying) { + mp.capabilities.isFlying = true; + mp.sendPlayerAbilities(); + } + } + } + + @Override + public int getMaxPreviewDuration() { + return 60; + } + + @Override + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + // Check if telegraph should be shown + if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { + return null; + } + + // Telegraph shows at landing zone + if (targetingMode == TargetingMode.AOE_SELF) { + // AOE_SELF: telegraph at caster position, does NOT follow + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } else if (targetingMode == TargetingMode.AOE_TARGET && target != null) { + // AOE_TARGET: telegraph at target position, follows target during windup + targetX = target.posX; + targetY = target.posY; + targetZ = target.posZ; + } else { + // Fallback to caster position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } + + // Create telegraph at the appropriate position — always project to actual ground for landing zone + double groundY = findGroundLevel(caster.worldObj, targetX, targetY, targetZ, 64); + + Telegraph telegraph = Telegraph.circle(radius); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + telegraph.setGroundSearchRange(64); // Slam always projects to actual ground (landing zone) + + TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + + // NPC AOE_TARGET: telegraph follows target during windup + // Player: telegraph stays at caster position (no target to follow) + if (!isPlayerCaster(caster) && targetingMode == TargetingMode.AOE_TARGET && target != null) { + instance.setEntityIdToFollow(target.getEntityId()); + } + + return instance; + } + + @Override + public float getTelegraphRadius() { + return radius; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("damage", damage); + nbt.setFloat("radius", radius); + nbt.setFloat("knockback", knockbackStrength); + nbt.setFloat("leapSpeed", leapSpeed); + nbt.setFloat("leapHeight", leapHeight); + } + + @Override + public void readTypeNBT(INbt nbt) { + damage = nbt.getFloat("damage"); + radius = nbt.getFloat("radius"); + knockbackStrength = nbt.getFloat("knockback"); + leapSpeed = nbt.getFloat("leapSpeed"); + leapHeight = nbt.getFloat("leapHeight"); + } + + // Getters & Setters + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public float getKnockbackStrength() { + return knockbackStrength; + } + + public void setKnockbackStrength(float knockbackStrength) { + this.knockbackStrength = knockbackStrength; + } + + public float getLeapSpeed() { + return leapSpeed; + } + + public void setLeapSpeed(float leapSpeed) { + this.leapSpeed = leapSpeed; + } + + public float getLeapHeight() { + return leapHeight; + } + + public void setLeapHeight(float leapHeight) { + this.leapHeight = leapHeight; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.row( + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius), + FieldDef.floatField("ability.knockback", this::getKnockbackStrength, this::setKnockbackStrength) + ), + FieldDef.section("ability.section.leap"), + FieldDef.row( + FieldDef.floatField("gui.speed", this::getLeapSpeed, this::setLeapSpeed), + FieldDef.floatField("gui.height", this::getLeapHeight, this::setLeapHeight) + ), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java new file mode 100644 index 000000000..b1e0634ac --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -0,0 +1,602 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityTeleport; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * Teleport ability: Instant reposition with wall/line-of-sight checks. + */ +public class AbilityTeleport extends Ability implements IAbilityTeleport { + + /** + * Teleport behavior mode. + */ + public enum TeleportMode { + BLINK, + BEHIND, + SINGLE; + + @Override + public String toString() { + switch (this) { + case BLINK: + return "ability.teleport.blink"; + case BEHIND: + return "ability.teleport.behind"; + case SINGLE: + return "ability.teleport.single"; + default: + return name(); + } + } + } + + private static final Random RANDOM = new Random(); + + // Type-specific parameters + private TeleportMode mode = TeleportMode.BLINK; + private int blinkCount = 3; + private int blinkDelayTicks = 10; + private float blinkRadius = 8.0f; + private float behindDistance = 2.0f; + private boolean requireLineOfSight = true; + private boolean damageAtStart = false; + private boolean damageAtEnd = false; + private float damage = 5.0f; + private float damageRadius = 2.0f; + + // Runtime state + private transient int currentBlink = 0; + private transient int ticksSinceLastBlink = 0; + + public AbilityTeleport() { + this.typeId = "ability.cnpc.teleport"; + this.name = "Teleport"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.allowedBy = UserType.NPC_ONLY; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 10; + this.lockMovement = LockMode.NO; + // No telegraph for teleport - it's instant repositioning + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + this.windUpSound = "mob.endermen.portal"; + this.activeSound = "mob.endermen.portal"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/teleport.png") + }; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + currentBlink = 0; + ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; + if (currentBlink >= blinkLimit) { + signalCompletion(); + return; + } + + ticksSinceLastBlink++; + + if (ticksSinceLastBlink >= blinkDelayTicks) { + performBlink(caster, target, caster.worldObj); + currentBlink++; + ticksSinceLastBlink = 0; + + // Check if this was the last blink + if (currentBlink >= blinkLimit) { + signalCompletion(); + } + } + } + + private void performBlink(IEntityLivingBase caster, IEntityLivingBase target, IWorld IWorld) { + if (IWorld.isRemote && !isPreview()) return; + + double oldX = caster.posX; + double oldY = caster.posY; + double oldZ = caster.posZ; + + IVector3 destination = calculateDestination(caster, target, IWorld); + if (destination == null) return; + + if (!isPreview()) { + boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; + if (mustHaveLOS && !hasLineOfSight(IWorld, oldX, oldY + caster.getEyeHeight(), oldZ, + destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { + destination = findValidPositionAlongLine(IWorld, caster, oldX, oldY, oldZ, + destination.xCoord, destination.yCoord, destination.zCoord); + if (destination == null) return; + } + + destination = findSafeDestination(IWorld, oldX, oldY, oldZ, + destination.xCoord, destination.yCoord, destination.zCoord); + if (destination == null) return; + + // Damage at origin + if (damageAtStart) { + dealDamageAt(caster, IWorld, oldX, oldY, oldZ); + } + + // Spawn particles at origin + spawnTeleportParticles(IWorld, oldX, oldY, oldZ); + } + + // Teleport + if (isPreview()) { + caster.setPosition(destination.xCoord, destination.yCoord, destination.zCoord); + caster.prevPosX = destination.xCoord; + caster.prevPosY = destination.yCoord; + caster.prevPosZ = destination.zCoord; + } else { + caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + } + caster.fallDistance = 0; + + if (!isPreview()) { + // Spawn particles at destination + spawnTeleportParticles(IWorld, destination.xCoord, destination.yCoord, destination.zCoord); + + // Damage at destination + if (damageAtEnd) { + dealDamageAt(caster, IWorld, destination.xCoord, destination.yCoord, destination.zCoord); + } + } + + // Face target after teleport + if (target != null) { + double dx = target.posX - destination.xCoord; + double dz = target.posZ - destination.zCoord; + float newYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + caster.rotationYaw = newYaw; + caster.rotationYawHead = newYaw; + } + } + + private void spawnTeleportParticles(IWorld IWorld, double x, double y, double z) { + for (int i = 0; i < 20; i++) { + IWorld.spawnParticle("portal", + x + (RANDOM.nextDouble() - 0.5) * 2, + y + RANDOM.nextDouble() * 2, + z + (RANDOM.nextDouble() - 0.5) * 2, + (RANDOM.nextDouble() - 0.5) * 0.5, + RANDOM.nextDouble() * 0.5, + (RANDOM.nextDouble() - 0.5) * 0.5); + } + } + + private IVector3 calculateDestination(IEntityLivingBase caster, IEntityLivingBase target, IWorld IWorld) { + double newX; + double newY; + double newZ; + + if (target != null) { + switch (mode) { + case BEHIND: + double yaw = Math.toRadians(target.rotationYaw); + newX = target.posX + Math.sin(yaw) * behindDistance; + newZ = target.posZ - Math.cos(yaw) * behindDistance; + newY = target.posY; + return IVector3.createVectorHelper(newX, newY, newZ); + case SINGLE: + case BLINK: + default: + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = Math.min(blinkRadius, maxRange); + double offset = RANDOM.nextDouble() * dist; + newX = target.posX + Math.cos(angle) * offset; + newZ = target.posZ + Math.sin(angle) * offset; + newY = target.posY; + return IVector3.createVectorHelper(newX, newY, newZ); + } + } + + // NPC-only ability — should not reach here + return null; + } + + private IVector3 findSafeDestination(IWorld IWorld, double oldX, double oldY, double oldZ, + double destX, double destY, double destZ) { + int oldBlockX = (int) Math.floor(oldX); + int oldBlockY = (int) Math.floor(oldY); + int oldBlockZ = (int) Math.floor(oldZ); + int destBlockX = (int) Math.floor(destX); + int destBlockY = (int) Math.floor(destY); + int destBlockZ = (int) Math.floor(destZ); + + if (oldBlockX == destBlockX && oldBlockY == destBlockY && oldBlockZ == destBlockZ) { + return null; + } + + double safeY = findSafeYNear(IWorld, destX, destY, destZ); + IVector3 safe = IVector3.createVectorHelper(destX, safeY, destZ); + + if (!isSafeLocation(IWorld, (int) Math.floor(safe.xCoord), + (int) Math.floor(safe.yCoord), (int) Math.floor(safe.zCoord))) { + return null; + } + + return safe; + } + + private double findSafeYNear(IWorld IWorld, double x, double baseY, double z) { + int[] offsets = new int[]{0, 1, -1, 2, -2}; + for (int offset : offsets) { + double y = baseY + offset; + if (isSafeLocation(IWorld, (int) Math.floor(x), (int) Math.floor(y), (int) Math.floor(z))) { + return y; + } + } + return findSafeY(IWorld, x, baseY, z); + } + + private boolean hasLineOfSight(IWorld IWorld, double x1, double y1, double z1, + double x2, double y2, double z2) { + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (distance < 0.1) return true; + + dx /= distance; + dy /= distance; + dz /= distance; + + double step = 0.5; + for (double d = step; d < distance; d += step) { + double checkX = x1 + dx * d; + double checkY = y1 + dy * d; + double checkZ = z1 + dz * d; + + int blockX = ValueUtil.floorDouble(checkX); + int blockY = ValueUtil.floorDouble(checkY); + int blockZ = ValueUtil.floorDouble(checkZ); + + // Skip out-of-IWorld coordinates + if (blockY < 0 || blockY >= 256) { + continue; + } + + Block block = IWorld.getBlock(blockX, blockY, blockZ); + if (block != null && block.getMaterial().isSolid() && block.isOpaqueCube()) { + return false; + } + } + + return true; + } + + private IVector3 findValidPositionAlongLine(IWorld IWorld, IEntityLivingBase caster, + double x1, double y1, double z1, + double x2, double y2, double z2) { + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (distance < 1.0) return null; + + dx /= distance; + dy /= distance; + dz /= distance; + + IVector3 lastValid = null; + double step = 0.5; + + for (double d = 1.0; d < distance; d += step) { + double checkX = x1 + dx * d; + double checkY = y1 + dy * d; + double checkZ = z1 + dz * d; + + if (hasLineOfSight(IWorld, x1, y1 + caster.getEyeHeight(), z1, + checkX, checkY + caster.getEyeHeight(), checkZ)) { + int blockX = ValueUtil.floorDouble(checkX); + int blockY = ValueUtil.floorDouble(checkY); + int blockZ = ValueUtil.floorDouble(checkZ); + + if (isSafeLocation(IWorld, blockX, blockY, blockZ)) { + lastValid = IVector3.createVectorHelper(checkX, checkY, checkZ); + } + } else { + break; + } + } + + return lastValid; + } + + private double findSafeY(IWorld IWorld, double x, double baseY, double z) { + int blockX = ValueUtil.floorDouble(x); + int blockZ = ValueUtil.floorDouble(z); + int blockY = ValueUtil.floorDouble(baseY); + + for (int offset = 0; offset <= 5; offset++) { + if (isSafeLocation(IWorld, blockX, blockY + offset, blockZ)) { + return blockY + offset; + } + if (offset > 0 && isSafeLocation(IWorld, blockX, blockY - offset, blockZ)) { + return blockY - offset; + } + } + + return baseY; + } + + private boolean isSafeLocation(IWorld IWorld, int x, int y, int z) { + // Bounds check - ensure we're within valid IWorld coordinates + if (y < 1 || y >= 255) { + return false; + } + + Block groundBlock = IWorld.getBlock(x, y - 1, z); + Block feetBlock = IWorld.getBlock(x, y, z); + Block headBlock = IWorld.getBlock(x, y + 1, z); + + // Defensive null checks (shouldn't happen in MC 1.7.10, but safe) + if (groundBlock == null || feetBlock == null || headBlock == null) { + return false; + } + + return groundBlock.getMaterial().isSolid() && + !feetBlock.getMaterial().isSolid() && + !headBlock.getMaterial().isSolid(); + } + + private void dealDamageAt(IEntityLivingBase caster, IWorld IWorld, double x, double y, double z) { + IBoundingBox aabb = IBoundingBox.getBoundingBox( + x - damageRadius, y - 1, z - damageRadius, + x + damageRadius, y + 2, z + damageRadius + ); + + @SuppressWarnings("unchecked") + List entities = IWorld.getEntitiesWithinAABB(IEntityLivingBase.class, aabb); + + for (IEntity IEntity : entities) { + if (!(IEntity instanceof IEntityLivingBase)) continue; + if (IEntity == caster) continue; + if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue; + + IEntityLivingBase living = (IEntityLivingBase) IEntity; + double dist = Math.sqrt(Math.pow(living.posX - x, 2) + Math.pow(living.posZ - z, 2)); + if (dist <= damageRadius) { + // Apply damage with scripted event support (no knockback for teleport damage) + boolean wasHit = applyAbilityDamage(caster, living, damage, 0); + if (wasHit) { + applyEffects(living); + } + } + } + } + + @Override + public void reset() { + super.reset(); + currentBlink = 0; + ticksSinceLastBlink = 0; + } + + @Override + public float getTelegraphRadius() { + return 0; // No telegraph for teleport + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setString("mode", mode.name()); + nbt.setInteger("blinkCount", blinkCount); + nbt.setInteger("blinkDelayTicks", blinkDelayTicks); + nbt.setFloat("blinkRadius", blinkRadius); + nbt.setFloat("behindDistance", behindDistance); + nbt.setBoolean("requireLineOfSight", requireLineOfSight); + nbt.setBoolean("damageAtStart", damageAtStart); + nbt.setBoolean("damageAtEnd", damageAtEnd); + nbt.setFloat("damage", damage); + nbt.setFloat("damageRadius", damageRadius); + } + + @Override + public void readTypeNBT(INbt nbt) { + try { + this.mode = TeleportMode.valueOf(nbt.getString("mode")); + } catch (Exception e) { + this.mode = TeleportMode.BLINK; + } + this.blinkCount = nbt.hasKey("blinkCount") ? nbt.getInteger("blinkCount") : 3; + this.blinkDelayTicks = nbt.hasKey("blinkDelayTicks") ? nbt.getInteger("blinkDelayTicks") : 10; + this.blinkRadius = nbt.getFloat("blinkRadius"); + this.behindDistance = nbt.getFloat("behindDistance"); + this.requireLineOfSight = nbt.getBoolean("requireLineOfSight"); + this.damageAtStart = nbt.getBoolean("damageAtStart"); + this.damageAtEnd = nbt.getBoolean("damageAtEnd"); + this.damage = nbt.getFloat("damage"); + this.damageRadius = nbt.getFloat("damageRadius"); + } + + // Getters & Setters + public TeleportMode getModeEnum() { + return mode; + } + + public void setModeEnum(TeleportMode mode) { + this.mode = mode; + } + + @Override + public int getMode() { + return mode.ordinal(); + } + + @Override + public void setMode(int mode) { + TeleportMode[] values = TeleportMode.values(); + this.mode = mode >= 0 && mode < values.length ? values[mode] : TeleportMode.BLINK; + } + + public int getBlinkCount() { + return blinkCount; + } + + public void setBlinkCount(int blinkCount) { + this.blinkCount = blinkCount; + } + + public int getBlinkDelayTicks() { + return blinkDelayTicks; + } + + public void setBlinkDelayTicks(int blinkDelayTicks) { + this.blinkDelayTicks = blinkDelayTicks; + } + + public float getBlinkRadius() { + return blinkRadius; + } + + public void setBlinkRadius(float blinkRadius) { + this.blinkRadius = blinkRadius; + } + + public float getBehindDistance() { + return behindDistance; + } + + public void setBehindDistance(float behindDistance) { + this.behindDistance = behindDistance; + } + + public boolean isRequireLineOfSight() { + return requireLineOfSight; + } + + public void setRequireLineOfSight(boolean requireLineOfSight) { + this.requireLineOfSight = requireLineOfSight; + } + + public boolean isDamageAtStart() { + return damageAtStart; + } + + public void setDamageAtStart(boolean damageAtStart) { + this.damageAtStart = damageAtStart; + } + + public boolean isDamageAtEnd() { + return damageAtEnd; + } + + public void setDamageAtEnd(boolean damageAtEnd) { + this.damageAtEnd = damageAtEnd; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getDamageRadius() { + return damageRadius; + } + + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } + + @Override + public int getMaxPreviewDuration() { + int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; + return blinkLimit * blinkDelayTicks + 10; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.enumField("ability.mode", TeleportMode.class, this::getModeEnum, this::setModeEnum), + FieldDef.floatField("ability.blinkRadius", this::getBlinkRadius, this::setBlinkRadius) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.row( + FieldDef.intField("ability.blinkCount", this::getBlinkCount, this::setBlinkCount) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK), + FieldDef.intField("ability.blinkDelay", this::getBlinkDelayTicks, this::setBlinkDelayTicks) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK) + ), + FieldDef.floatField("ability.behindDistance", this::getBehindDistance, this::setBehindDistance) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BEHIND), + FieldDef.boolField("ability.lineOfSight", this::isRequireLineOfSight, this::setRequireLineOfSight) + .hover("ability.hover.lineOfSight") + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.section("ability.section.damage"), + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius) + ), + FieldDef.row( + FieldDef.boolField("ability.damageAtStart", this::isDamageAtStart, this::setDamageAtStart) + .hover("ability.hover.dmgAtStart"), + FieldDef.boolField("ability.damageAtEnd", this::isDamageAtEnd, this::setDamageAtEnd) + .hover("ability.hover.dmgAtEnd") + ), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java new file mode 100644 index 000000000..12d90630c --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -0,0 +1,370 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import noppes.npcs.api.ability.type.IAbilityTrap; +import noppes.npcs.constants.EnumPotionType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AbilityTrap extends AbilityZone implements IAbilityTrap { + + private float triggerRadius = 2.0f; + private int armTime = 20; + private int maxTriggers = 1; + private int triggerCooldown = 20; + private float damage = 6.0f; + private float damageRadius = 0.0f; + private float knockback = 0.5f; + private boolean visible = true; + + public AbilityTrap() { + super(300, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f)); + this.typeId = "ability.cnpc.trap"; + this.name = "Trap"; + this.windUpTicks = 30; + this.windUpAnimationName = "Ability_Zone_Windup"; + this.activeAnimationName = "Ability_Zone_Active"; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/trap.png", + this::getActiveColor) + }; + } + + @Override + public boolean isConcurrentCapable() { + return true; + } + + @Override + public float getZoneRadius() { + return triggerRadius; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + activeEntities.clear(); + + List positions; + if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) { + positions = new ArrayList<>(preCalculatedPositions); + preCalculatedPositions.clear(); + } else { + positions = new ArrayList<>(); + List placedPositions = new ArrayList<>(); + float minSeparation = triggerRadius * 2.0f; + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + positions.add(pos); + } + } + + for (int i = 0; i < zoneCount; i++) { + double[] pos = positions.get(i); + + EntityAbilityZone IEntity = EntityAbilityZone.createTrap(caster.worldObj, caster, + pos[0], caster.posY, pos[1], + zoneShape, + triggerRadius, armTime, maxTriggers, triggerCooldown, + damage, damageRadius, knockback, durationTicks, + isIgnoreIFrames(), + colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled, + zoneHeight, + particleDensity, particleScale, animSpeed, lightningDensity, + visible, + getEffects()); + IEntity.setSourceAbility(this); + + applyVisualToEntity(IEntity); + + if (isPreview()) { + IEntity.setupPreview(caster); + } + + spawnAbilityEntity(IEntity); + activeEntities.add(IEntity); + } + } + + @ClientOnly + public enum TrapPreset { + HIDDEN, VENOM, EXPLOSIVE, CURSED, SHOCK, SNARE; + + @Override + public String toString() { + return "ability.preset." + name().toLowerCase(); + } + } + + private void applyTrapPresetDefaults(String preset) { + effects.clear(); + particleDir = ""; + + groundFill = true; + rings = false; + ringCount = 1; + border = false; + borderSpeed = 1.0f; + accents = false; + accentStyle = 0; + lightning = false; + particleGlow = false; + + switch (preset) { + case "VENOM": + groundAlpha = 0.04f; + particles = true; + particleDensity = 0.3f; + particleScale = 0.5f; + particleMotion = 1; + particleDir = "mc:mobSpell"; + colorData.innerColor = 0x44DD44; + colorData.outerColor = 0x116611; + windUpColor = 0x6044DD44; + activeColor = 0xC044FF44; + effects.add(new AbilityPotionEffect(EnumPotionType.Poison, 100, 0)); + break; + case "EXPLOSIVE": + groundAlpha = 0.04f; + particles = true; + particleDensity = 0.3f; + particleScale = 0.6f; + particleMotion = 0; + particleDir = "mc:smoke"; + colorData.innerColor = 0xFF6611; + colorData.outerColor = 0xCC2200; + windUpColor = 0x60FF6611; + activeColor = 0xC0FF4400; + effects.add(new AbilityPotionEffect(EnumPotionType.Fire, 60, 0)); + break; + case "CURSED": + groundAlpha = 0.04f; + particles = true; + particleDensity = 0.3f; + particleScale = 0.5f; + particleMotion = 1; + particleDir = "mc:portal"; + colorData.innerColor = 0xAA44FF; + colorData.outerColor = 0x6622BB; + windUpColor = 0x60AA44FF; + activeColor = 0xC0CC66FF; + effects.add(new AbilityPotionEffect(EnumPotionType.Weakness, 100, 0)); + break; + case "SHOCK": + groundAlpha = 0.04f; + particles = true; + particleDensity = 0.3f; + particleScale = 0.4f; + particleMotion = 2; + particleDir = "mc:enchantmenttable"; + colorData.innerColor = 0x4488FF; + colorData.outerColor = 0x2244BB; + windUpColor = 0x604488FF; + activeColor = 0xC066AAFF; + effects.add(new AbilityPotionEffect(EnumPotionType.MiningFatigue, 80, 1)); + break; + case "SNARE": + groundAlpha = 0.04f; + particles = true; + particleDensity = 0.3f; + particleScale = 0.5f; + particleMotion = 1; + particleDir = "mc:snowshovel"; + colorData.innerColor = 0x88CCFF; + colorData.outerColor = 0x4488CC; + windUpColor = 0x6088CCFF; + activeColor = 0xC0AADDFF; + effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 100, 1)); + break; + case "HIDDEN": + default: + groundAlpha = 0.03f; + particles = false; + particleDensity = 0.0f; + particleScale = 1.0f; + particleMotion = 0; + colorData.innerColor = 0x888888; + colorData.outerColor = 0x444444; + windUpColor = 0x60888888; + activeColor = 0xC0AAAAAA; + break; + } + } + + @Override + public void writeTypeNBT(INbt nbt) { + writeZoneNBT(nbt); + nbt.setFloat("triggerRadius", triggerRadius); + nbt.setInteger("armTime", armTime); + nbt.setInteger("maxTriggers", maxTriggers); + nbt.setInteger("triggerCooldown", triggerCooldown); + nbt.setFloat("damage", damage); + nbt.setFloat("damageRadius", damageRadius); + nbt.setFloat("knockback", knockback); + nbt.setBoolean("visible", visible); + } + + @Override + public void readTypeNBT(INbt nbt) { + readZoneNBT(nbt, 300); + this.triggerRadius = nbt.getFloat("triggerRadius"); + this.armTime = nbt.getInteger("armTime"); + this.maxTriggers = nbt.getInteger("maxTriggers"); + this.triggerCooldown = nbt.getInteger("triggerCooldown"); + this.damage = nbt.getFloat("damage"); + this.damageRadius = nbt.getFloat("damageRadius"); + this.knockback = nbt.getFloat("knockback"); + this.visible = nbt.getBoolean("visible"); + } + + public float getTriggerRadius() { + return triggerRadius; + } + + public void setTriggerRadius(float triggerRadius) { + this.triggerRadius = triggerRadius; + } + + public int getArmTime() { + return armTime; + } + + public void setArmTime(int armTime) { + this.armTime = armTime; + } + + public int getMaxTriggers() { + return maxTriggers; + } + + public void setMaxTriggers(int maxTriggers) { + this.maxTriggers = maxTriggers; + } + + public int getTriggerCooldown() { + return triggerCooldown; + } + + public void setTriggerCooldown(int triggerCooldown) { + this.triggerCooldown = triggerCooldown; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getDamageRadius() { + return damageRadius; + } + + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + @ClientOnly + @Override + protected void addPresetFieldDef(List defs) { + defs.add(FieldDef.subGuiField("gui.applyPreset", + SubGuiTrapPresetSelector::new, + gui -> { + SubGuiTrapPresetSelector selector = (SubGuiTrapPresetSelector) gui; + if (selector.selectedPreset != null) { + applyTrapPresetDefaults(selector.selectedPreset); + } + })); + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + addPresetFieldDef(defs); + + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), + FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum) + ), + FieldDef.section("ability.section.zone"), + FieldDef.row( + FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius), + FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20) + ), + FieldDef.row( + FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight), + FieldDef.boolField("ability.visible", this::isVisible, this::setVisible) + ), + FieldDef.section("ability.section.trigger"), + FieldDef.row( + FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius), + FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime) + ), + FieldDef.row( + FieldDef.intField("gui.max", this::getMaxTriggers, this::setMaxTriggers), + FieldDef.intField("gui.cooldown", this::getTriggerCooldown, this::setTriggerCooldown) + ), + FieldDef.section("ability.section.damage"), + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius) + ), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + + addVisualFieldDefs(defs); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java new file mode 100644 index 000000000..a3e3eedd3 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -0,0 +1,426 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityVortex; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Vortex ability: Pulls targets toward the caster. + * Can pull single target or AOE, with optional damage and stun on arrival. + */ +public class AbilityVortex extends Ability implements IAbilityVortex { + + private float pullRadius = 8.0f; + private float pullStrength = 0.8f; + private float damage = 0.0f; + private float knockback = 0.0f; + private boolean aoe = true; + private boolean damageOnPull = false; + private float pullDamage = 0.0f; + + // Runtime state — keyed by IEntity ID (not UUID, since cloned NPCs share UUIDs) + private transient Map pulledEntities; + private transient boolean pullComplete = false; + private transient int ticksSincePullDamage = 0; + + private static class PullState { + double lastX, lastY, lastZ; + int stuckTicks; + + PullState(IEntityLivingBase IEntity) { + this.lastX = IEntity.posX; + this.lastY = IEntity.posY; + this.lastZ = IEntity.posZ; + this.stuckTicks = 0; + } + } + + private Map getPulledEntities() { + if (pulledEntities == null) { + pulledEntities = new HashMap<>(); + } + return pulledEntities; + } + + public AbilityVortex() { + this.typeId = "ability.cnpc.vortex"; + this.name = "Vortex"; + this.targetingMode = TargetingMode.AOE_SELF; + this.maxRange = 15.0f; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; + this.windUpTicks = 30; + this.telegraphType = TelegraphType.CIRCLE; + this.windUpSound = "mob.ghast.charge"; + this.activeSound = "mob.ghast.fireball"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/vortex.png", + this::getActiveColor) + }; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + @Override + public float getTelegraphRadius() { + return pullRadius; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + getPulledEntities().clear(); + pullComplete = false; + ticksSincePullDamage = 0; + + if (!isPreview() && !caster.worldObj.isRemote) { + IBoundingBox box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + @SuppressWarnings("unchecked") + List entities = caster.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, box); + + if (aoe) { + // ALL: pull every valid enemy in range + for (IEntityLivingBase IEntity : entities) { + if (IEntity == caster) continue; + if (IEntity.isDead) continue; + if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue; + + double dist = caster.getDistanceToEntity(IEntity); + if (dist <= pullRadius) { + getPulledEntities().put(IEntity.getEntityId(), new PullState(IEntity)); + } + } + } else { + // SINGULAR: pull one random target within range + // NPC: pull aggro target if valid, otherwise random enemy + // Player: pull random enemy in range + if (!isPlayerCaster(caster) && target != null && !target.isDead) { + double dist = caster.getDistanceToEntity(target); + if (dist <= pullRadius) { + getPulledEntities().put(target.getEntityId(), new PullState(target)); + return; + } + } + + // Collect all valid enemies, then pick one at random + List validTargets = new ArrayList<>(); + for (IEntityLivingBase IEntity : entities) { + if (IEntity == caster) continue; + if (IEntity.isDead) continue; + if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue; + + double dist = caster.getDistanceToEntity(IEntity); + if (dist <= pullRadius) { + validTargets.add(IEntity); + } + } + if (!validTargets.isEmpty()) { + IEntityLivingBase chosen = validTargets.get(caster.worldObj.rand.nextInt(validTargets.size())); + getPulledEntities().put(chosen.getEntityId(), new PullState(chosen)); + } + } + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (isPreview()) { + // No entities to pull in preview, just run animation for a duration + if (tick >= 60) signalCompletion(); + return; + } + + if (pullComplete || getPulledEntities().isEmpty()) { + signalCompletion(); + return; + } + + if (caster.worldObj.isRemote) return; + + // Safety cap: force-complete if active too long + int maxActiveTicks = Math.max(40, (int)(pullRadius / pullStrength * 3)); + if (tick >= maxActiveTicks) { + for (Map.Entry entry : new HashMap<>(getPulledEntities()).entrySet()) { + IEntityLivingBase IEntity = findEntity(caster.worldObj, entry.getKey()); + if (IEntity != null && !IEntity.isDead) { + onTargetArrived(caster, IEntity, caster.worldObj); + } + } + getPulledEntities().clear(); + pullComplete = true; + signalCompletion(); + return; + } + + double destX = caster.posX; + double destY = caster.posY; + double destZ = caster.posZ; + + boolean anyStillPulling = false; + ticksSincePullDamage++; + boolean shouldDealPullDamage = damageOnPull && pullDamage > 0 && ticksSincePullDamage >= 10; + if (shouldDealPullDamage) { + ticksSincePullDamage = 0; + } + + for (Map.Entry entry : new HashMap<>(getPulledEntities()).entrySet()) { + int entityId = entry.getKey(); + PullState state = entry.getValue(); + + IEntityLivingBase IEntity = findEntity(caster.worldObj, entityId); + if (IEntity == null || IEntity.isDead) { + getPulledEntities().remove(entityId); + continue; + } + + double dx = destX - IEntity.posX; + double dy = destY - IEntity.posY; + double dz = destZ - IEntity.posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= 1.5) { + getPulledEntities().remove(entityId); + onTargetArrived(caster, IEntity, caster.worldObj); + continue; + } + + // Stuck detection: check if IEntity made progress since last tick + double lastDx = destX - state.lastX; + double lastDy = destY - state.lastY; + double lastDz = destZ - state.lastZ; + double lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy + lastDz * lastDz); + double progress = lastDist - dist; // positive = IEntity moved closer + + double expectedProgress = Math.min(pullStrength, dist * 0.5); + if (progress < expectedProgress * 0.3) { + state.stuckTicks++; + } else { + state.stuckTicks = 0; + } + + if (state.stuckTicks >= 5) { + // IEntity is stuck (wall, block, partial obstruction) - treat as arrived + getPulledEntities().remove(entityId); + onTargetArrived(caster, IEntity, caster.worldObj); + continue; + } + + anyStillPulling = true; + + // Record position BEFORE movement so next tick's stuck detection + // can measure the actual progress (pull + AI movement combined) + state.lastX = IEntity.posX; + state.lastY = IEntity.posY; + state.lastZ = IEntity.posZ; + + // Clamp speed to never exceed half the remaining distance, preventing overshoot/slingshot + double maxSpeed = dist * 0.5; + double effectiveSpeed = Math.min(pullStrength, maxSpeed); + double factor = effectiveSpeed / dist; + double motionX = dx * factor; + double motionY = dy * factor * 0.5; + double motionZ = dz * factor; + + if (IEntity instanceof IPlayer) { + // Players: send velocity packet — client applies movement + IEntity.motionX = motionX; + IEntity.motionY = motionY; + IEntity.motionZ = motionZ; + IEntity.velocityChanged = true; + } else { + // NPCs/mobs: directly apply movement with collision detection. + // Setting motionX alone is unreliable because the IEntity's AI tick + // (moveFlying/moveEntityWithHeading) can override the velocity before + // moveEntity runs, depending on IEntity tick order relative to the caster. + IEntity.moveEntity(motionX, motionY, motionZ); + } + + if (shouldDealPullDamage) { + applyAbilityDamage(caster, IEntity, pullDamage * 0.5f, 0); + } + } + + if (!anyStillPulling && getPulledEntities().isEmpty()) { + pullComplete = true; + signalCompletion(); + } + } + + private void onTargetArrived(IEntityLivingBase caster, IEntityLivingBase IEntity, IWorld IWorld) { + // Apply damage with scripted event support + boolean wasHit = applyAbilityDamage(caster, IEntity, damage, knockback * 0.5f); + + // Only apply effects if hit wasn't cancelled + if (wasHit) { + applyEffects(IEntity); + } + } + + /** + * Find an IEntity by IEntity ID. + */ + private IEntityLivingBase findEntity(IWorld IWorld, int entityId) { + IEntity IEntity = IWorld.getEntityByID(entityId); + if (IEntity instanceof IEntityLivingBase) { + return (IEntityLivingBase) IEntity; + } + return null; + } + + @Override + public void cleanup() { + getPulledEntities().clear(); + pullComplete = false; + ticksSincePullDamage = 0; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("pullRadius", pullRadius); + nbt.setFloat("pullStrength", pullStrength); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setBoolean("aoe", aoe); + nbt.setBoolean("damageOnPull", damageOnPull); + nbt.setFloat("pullDamage", pullDamage); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.pullRadius = nbt.getFloat("pullRadius"); + this.pullStrength = nbt.getFloat("pullStrength"); + this.damage = nbt.getFloat("damage"); + this.knockback = nbt.getFloat("knockback"); + this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe"); + this.damageOnPull = nbt.getBoolean("damageOnPull"); + this.pullDamage = nbt.getFloat("pullDamage"); + } + + // Getters & Setters + public float getPullRadius() { + return pullRadius; + } + + public void setPullRadius(float pullRadius) { + this.pullRadius = pullRadius; + } + + public float getPullStrength() { + return pullStrength; + } + + public void setPullStrength(float pullStrength) { + this.pullStrength = pullStrength; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public boolean isAoe() { + return aoe; + } + + public void setAoe(boolean aoe) { + this.aoe = aoe; + } + + public boolean isDamageOnPull() { + return damageOnPull; + } + + public void setDamageOnPull(boolean damageOnPull) { + this.damageOnPull = damageOnPull; + } + + public float getPullDamage() { + return pullDamage; + } + + public void setPullDamage(float pullDamage) { + this.pullDamage = pullDamage; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.floatField("ability.pullRadius", this::getPullRadius, this::setPullRadius), + FieldDef.floatField("ability.pullStrength", this::getPullStrength, this::setPullStrength) + ), + FieldDef.section("ability.section.damage"), + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback) + ), + FieldDef.section("ability.section.aoe"), + FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe) + .hover("ability.hover.aoe"), + FieldDef.section("ability.section.pullDamage"), + FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull) + .hover("ability.hover.dmgOnPull"), + FieldDef.floatField("enchantment.damage", this::getPullDamage, this::setPullDamage) + .visibleWhen(this::isDamageOnPull), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java new file mode 100644 index 000000000..eec22cc5e --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -0,0 +1,788 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import noppes.npcs.constants.EnumPotionType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +public abstract class AbilityZone extends Ability { + + protected int durationTicks; + protected ZoneShape zoneShape = ZoneShape.CIRCLE; + protected float spawnRadius = 10.0f; + protected int zoneCount = 3; + protected float zoneHeight = 2.0f; + + protected float particleDensity = 1.0f; + protected float particleScale = 1.0f; + protected float animSpeed = 1.0f; + protected float lightningDensity = 1.0f; + protected EnergyDisplayData colorData; + + protected boolean groundFill = true; + protected float groundAlpha = 0.25f; + protected boolean rings = true; + protected int ringCount = 3; + protected boolean border = true; + protected float borderSpeed = 1.0f; + protected boolean accents = true; + protected int accentStyle = 0; + protected boolean lightning = false; + protected boolean particles = true; + protected int particleMotion = 0; + protected String particleDir = ""; + protected int particleSize = 32; + protected boolean particleGlow = true; + + protected static final Random RANDOM = new Random(); + protected transient List activeEntities = new ArrayList<>(); + protected transient List preCalculatedPositions = new ArrayList<>(); + + protected AbilityZone(int defaultDuration, EnergyDisplayData defaultColors) { + this.durationTicks = defaultDuration; + this.colorData = defaultColors; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.lockMovement = LockMode.WINDUP; + this.cooldownTicks = 0; + this.allowedBy = UserType.BOTH; + } + + @Override + public boolean allowBurst() { + return false; + } + + @Override + public boolean allowFreeOnCast() { + return true; + } + + @Override + public void detach() { + activeEntities.clear(); + preCalculatedPositions.clear(); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + public abstract float getZoneRadius(); + + @Override + public List createTelegraphs(IEntityLivingBase caster, IEntityLivingBase target) { + if (!showTelegraph || windUpTicks <= 0) return new ArrayList<>(); + + preCalculatedPositions.clear(); + List placedPositions = new ArrayList<>(); + float minSeparation = getZoneRadius() * 2.0f; + + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + preCalculatedPositions.add(pos); + } + + List telegraphs = new ArrayList<>(); + float zoneRadius = getZoneRadius(); + + for (double[] pos : preCalculatedPositions) { + Telegraph telegraph; + if (zoneShape == ZoneShape.SQUARE) { + telegraph = Telegraph.square(zoneRadius); + } else { + telegraph = Telegraph.circle(zoneRadius); + } + + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + double groundY = findGroundLevel(caster.worldObj, pos[0], caster.posY, pos[1]); + TelegraphInstance instance = new TelegraphInstance(telegraph, pos[0], groundY, pos[1], 0); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(-1); + telegraphs.add(instance); + } + + return telegraphs; + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + // Free on Cast: complete immediately — zones live independently + if (isFreeOnCast()) { + signalCompletion(); + return; + } + + Iterator it = activeEntities.iterator(); + while (it.hasNext()) { + EntityAbilityZone IEntity = it.next(); + if (IEntity == null || IEntity.isDead) { + it.remove(); + } + } + + if (activeEntities.isEmpty()) { + signalCompletion(); + } + } + + @Override + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) { + cleanup(); + } + + @Override + public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) { + cleanup(); + } + + @Override + public void cleanup() { + for (EntityAbilityZone IEntity : activeEntities) { + if (IEntity != null && !IEntity.isDead) { + killAbilityEntity(IEntity); + } + } + activeEntities.clear(); + preCalculatedPositions.clear(); + } + + @Override + public int getMaxPreviewDuration() { + return durationTicks + 10; + } + + protected double[] findSpawnPosition(IEntityLivingBase caster, List placedPositions, float minSeparation) { + double spawnX = caster.posX; + double spawnZ = caster.posZ; + + for (int attempt = 0; attempt < 15; attempt++) { + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = Math.sqrt(RANDOM.nextDouble()) * spawnRadius; + double candidateX = caster.posX + Math.cos(angle) * dist; + double candidateZ = caster.posZ + Math.sin(angle) * dist; + + boolean overlaps = false; + for (double[] placed : placedPositions) { + double dx = candidateX - placed[0]; + double dz = candidateZ - placed[1]; + if (Math.sqrt(dx * dx + dz * dz) < minSeparation) { + overlaps = true; + break; + } + } + + spawnX = candidateX; + spawnZ = candidateZ; + if (!overlaps) break; + } + + return new double[]{spawnX, spawnZ}; + } + + protected void applyVisualToEntity(EntityAbilityZone IEntity) { + IEntity.applyVisual(groundFill, groundAlpha, + rings, ringCount, border, borderSpeed, + accents, accentStyle, lightning, + particles, particleMotion, particleDir, + particleSize, particleGlow); + } + + private void applyPresetDefaults(String styleName) { + effects.clear(); + particleDir = ""; + switch (styleName) { + case "TOXIC": + groundFill = true; + groundAlpha = 0.30f; + rings = true; + ringCount = 3; + border = false; + borderSpeed = 1.0f; + accents = true; + accentStyle = 1; + lightning = false; + particles = true; + particleDensity = 1.5f; + particleScale = 0.8f; + particleMotion = 1; + particleGlow = true; + particleDir = "mc:mobSpell"; + colorData.innerColor = 0x44DD44; + colorData.outerColor = 0x116611; + windUpColor = 0x6044DD44; + activeColor = 0xC044FF44; + effects.add(new AbilityPotionEffect(EnumPotionType.Poison, 100, 0)); + break; + case "INFERNO": + groundFill = true; + groundAlpha = 0.35f; + rings = true; + ringCount = 3; + border = true; + borderSpeed = 2.0f; + accents = true; + accentStyle = 2; + lightning = false; + particles = true; + particleDensity = 2.0f; + particleScale = 1.2f; + particleMotion = 0; + particleGlow = true; + particleDir = "mc:flame"; + colorData.innerColor = 0xFF6611; + colorData.outerColor = 0xCC2200; + windUpColor = 0x60FF6611; + activeColor = 0xC0FF4400; + effects.add(new AbilityPotionEffect(EnumPotionType.Fire, 60, 0)); + break; + case "ARCANE": + groundFill = true; + groundAlpha = 0.20f; + rings = true; + ringCount = 1; + border = true; + borderSpeed = 0.8f; + accents = true; + accentStyle = 0; + lightning = false; + particles = true; + particleDensity = 1.0f; + particleScale = 1.0f; + particleMotion = 1; + particleGlow = true; + particleDir = "mc:portal"; + colorData.innerColor = 0xAA44FF; + colorData.outerColor = 0x6622BB; + windUpColor = 0x60AA44FF; + activeColor = 0xC0CC66FF; + effects.add(new AbilityPotionEffect(EnumPotionType.Weakness, 100, 0)); + break; + case "ELECTRIC": + groundFill = true; + groundAlpha = 0.15f; + rings = false; + ringCount = 1; + border = false; + borderSpeed = 1.0f; + accents = false; + accentStyle = 0; + lightning = true; + particles = true; + particleDensity = 0.8f; + particleScale = 0.6f; + particleMotion = 2; + particleGlow = true; + particleDir = "mc:enchantmenttable"; + colorData.innerColor = 0x4488FF; + colorData.outerColor = 0x2244BB; + windUpColor = 0x604488FF; + activeColor = 0xC066AAFF; + effects.add(new AbilityPotionEffect(EnumPotionType.MiningFatigue, 80, 1)); + break; + case "FROST": + groundFill = true; + groundAlpha = 0.25f; + rings = true; + ringCount = 3; + border = true; + borderSpeed = 0.3f; + accents = true; + accentStyle = 0; + lightning = false; + particles = true; + particleDensity = 1.5f; + particleScale = 1.0f; + particleMotion = 1; + particleGlow = true; + particleDir = "mc:snowshovel"; + colorData.innerColor = 0x88CCFF; + colorData.outerColor = 0x4488CC; + windUpColor = 0x6088CCFF; + activeColor = 0xC0AADDFF; + effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 100, 1)); + break; + case "DEFAULT": + groundFill = true; + groundAlpha = 0.25f; + rings = true; + ringCount = 3; + border = true; + borderSpeed = 1.0f; + accents = true; + accentStyle = 0; + lightning = false; + particles = false; + particleDensity = 1.0f; + particleScale = 1.0f; + particleMotion = 0; + particleGlow = true; + colorData.innerColor = 0xCCCCCC; + colorData.outerColor = 0x666666; + windUpColor = 0x60CCCCCC; + activeColor = 0xC0FFFFFF; + break; + default: + break; + } + } + + @ClientOnly + public enum ZonePreset { + DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST; + + @Override + public String toString() { + return "ability.preset." + name().toLowerCase(); + } + } + + protected void writeZoneNBT(INbt nbt) { + nbt.setInteger("durationTicks", durationTicks); + nbt.setString("zoneShape", zoneShape.name()); + nbt.setFloat("spawnRadius", spawnRadius); + nbt.setInteger("zoneCount", zoneCount); + nbt.setFloat("zoneHeight", zoneHeight); + nbt.setFloat("particleDensity", particleDensity); + nbt.setFloat("particleScale", particleScale); + nbt.setFloat("animSpeed", animSpeed); + nbt.setFloat("lightningDensity", lightningDensity); + colorData.writeNBT(new NBTWrapper(nbt)); + + nbt.setBoolean("groundFill", groundFill); + nbt.setFloat("groundAlpha", groundAlpha); + nbt.setBoolean("rings", rings); + nbt.setInteger("ringCount", ringCount); + nbt.setBoolean("border", border); + nbt.setFloat("borderSpeed", borderSpeed); + nbt.setBoolean("accents", accents); + nbt.setInteger("accentStyle", accentStyle); + nbt.setBoolean("lightning", lightning); + nbt.setBoolean("particles", particles); + nbt.setInteger("particleMotion", particleMotion); + nbt.setString("particleDir", particleDir); + nbt.setInteger("particleSize", particleSize); + nbt.setBoolean("particleGlow", particleGlow); + } + + protected void readZoneNBT(INbt nbt, int defaultDuration) { + this.durationTicks = nbt.getInteger("durationTicks"); + try { + this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape")); + } catch (Exception e) { + this.zoneShape = ZoneShape.CIRCLE; + } + this.spawnRadius = nbt.getFloat("spawnRadius"); + this.zoneCount = nbt.getInteger("zoneCount"); + this.zoneHeight = nbt.getFloat("zoneHeight"); + this.particleDensity = nbt.getFloat("particleDensity"); + this.particleScale = nbt.getFloat("particleScale"); + this.animSpeed = nbt.getFloat("animSpeed"); + this.lightningDensity = nbt.getFloat("lightningDensity"); + colorData.readNBT(new NBTWrapper(nbt)); + + this.groundFill = nbt.getBoolean("groundFill"); + this.groundAlpha = nbt.getFloat("groundAlpha"); + this.rings = nbt.getBoolean("rings"); + this.ringCount = nbt.getInteger("ringCount"); + this.border = nbt.getBoolean("border"); + this.borderSpeed = nbt.getFloat("borderSpeed"); + this.accents = nbt.getBoolean("accents"); + this.accentStyle = nbt.getInteger("accentStyle"); + this.lightning = nbt.getBoolean("lightning"); + this.particles = nbt.getBoolean("particles"); + this.particleMotion = nbt.getInteger("particleMotion"); + this.particleDir = nbt.getString("particleDir"); + this.particleSize = nbt.getInteger("particleSize"); + this.particleGlow = nbt.getBoolean("particleGlow"); + } + + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + public ZoneShape getZoneShapeEnum() { + return zoneShape; + } + + public void setZoneShapeEnum(ZoneShape shape) { + this.zoneShape = shape; + } + + public int getZoneShapeOrdinal() { + return zoneShape.ordinal(); + } + + public void setZoneShapeOrdinal(int shape) { + ZoneShape[] values = ZoneShape.values(); + this.zoneShape = shape >= 0 && shape < values.length ? values[shape] : ZoneShape.CIRCLE; + } + + public float getSpawnRadius() { + return spawnRadius; + } + + public void setSpawnRadius(float spawnRadius) { + this.spawnRadius = Math.max(0, spawnRadius); + } + + public int getZoneCount() { + return zoneCount; + } + + public void setZoneCount(int zoneCount) { + this.zoneCount = Math.max(1, zoneCount); + } + + public float getZoneHeight() { + return zoneHeight; + } + + public void setZoneHeight(float zoneHeight) { + this.zoneHeight = Math.max(0.5f, zoneHeight); + } + + public float getParticleDensity() { + return particleDensity; + } + + public void setParticleDensity(float v) { + this.particleDensity = Math.max(0, Math.min(5, v)); + } + + public float getParticleScale() { + return particleScale; + } + + public void setParticleScale(float v) { + this.particleScale = Math.max(0.1f, Math.min(5, v)); + } + + public float getAnimSpeed() { + return animSpeed; + } + + public void setAnimSpeed(float v) { + this.animSpeed = Math.max(0.1f, Math.min(5, v)); + } + + public float getLightningDensity() { + return lightningDensity; + } + + public void setLightningDensity(float v) { + this.lightningDensity = Math.max(0, Math.min(3, v)); + } + + public int getInnerColor() { + return colorData.innerColor; + } + + public void setInnerColor(int color) { + colorData.innerColor = color; + } + + public int getOuterColor() { + return colorData.outerColor; + } + + public void setOuterColor(int color) { + colorData.outerColor = color; + } + + public boolean isOuterColorEnabled() { + return colorData.outerColorEnabled; + } + + public void setOuterColorEnabled(boolean enabled) { + colorData.outerColorEnabled = enabled; + } + + public float getInnerAlpha() { + return colorData.innerAlpha; + } + + public void setInnerAlpha(float alpha) { + colorData.innerAlpha = alpha; + } + + public float getOuterColorWidth() { + return colorData.outerColorWidth; + } + + public void setOuterColorWidth(float width) { + colorData.outerColorWidth = width; + } + + public float getOuterColorAlpha() { + return colorData.outerColorAlpha; + } + + public void setOuterColorAlpha(float alpha) { + colorData.outerColorAlpha = alpha; + } + + public boolean isGroundFill() { + return groundFill; + } + + public void setGroundFill(boolean v) { + this.groundFill = v; + } + + public float getGroundAlpha() { + return groundAlpha; + } + + public void setGroundAlpha(float v) { + this.groundAlpha = Math.max(0, Math.min(1, v)); + } + + public boolean isRings() { + return rings; + } + + public void setRings(boolean v) { + this.rings = v; + } + + public int getRingCount() { + return ringCount; + } + + public void setRingCount(int v) { + this.ringCount = Math.max(1, Math.min(5, v)); + } + + public boolean isBorder() { + return border; + } + + public void setBorder(boolean v) { + this.border = v; + } + + public float getBorderSpeed() { + return borderSpeed; + } + + public void setBorderSpeed(float v) { + this.borderSpeed = Math.max(0.1f, Math.min(5, v)); + } + + public boolean isAccents() { + return accents; + } + + public void setAccents(boolean v) { + this.accents = v; + } + + public int getAccentStyle() { + return accentStyle; + } + + public void setAccentStyle(int v) { + this.accentStyle = Math.max(0, Math.min(AccentStyle.values().length - 1, v)); + } + + public AccentStyle getAccentStyleEnum() { + return AccentStyle.values()[Math.max(0, Math.min(accentStyle, AccentStyle.values().length - 1))]; + } + + public void setAccentStyleEnum(AccentStyle s) { + this.accentStyle = s.ordinal(); + } + + public boolean isLightning() { + return lightning; + } + + public void setLightning(boolean v) { + this.lightning = v; + } + + public boolean isParticles() { + return particles; + } + + public void setParticles(boolean v) { + this.particles = v; + } + + public int getParticleMotion() { + return particleMotion; + } + + public void setParticleMotion(int v) { + this.particleMotion = Math.max(0, Math.min(ParticleMotion.values().length - 1, v)); + } + + public ParticleMotion getParticleMotionEnum() { + return ParticleMotion.values()[Math.max(0, Math.min(particleMotion, ParticleMotion.values().length - 1))]; + } + + public void setParticleMotionEnum(ParticleMotion m) { + this.particleMotion = m.ordinal(); + } + + public String getParticleDir() { + return particleDir; + } + + public void setParticleDir(String v) { + this.particleDir = v != null ? v : ""; + } + + public int getParticleSize() { + return particleSize; + } + + public void setParticleSize(int v) { + this.particleSize = Math.max(1, Math.min(256, v)); + } + + public boolean isParticleGlow() { + return particleGlow; + } + + public void setParticleGlow(boolean v) { + this.particleGlow = v; + } + + @ClientOnly + protected void addPresetFieldDef(List defs) { + defs.add(FieldDef.subGuiField("gui.applyPreset", + SubGuiZonePresetSelector::new, + gui -> { + SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui; + if (selector.selectedPreset != null) { + applyPresetDefaults(selector.selectedPreset); + } + })); + } + + @ClientOnly + protected void addVisualFieldDefs(List defs) { + defs.addAll(Arrays.asList( + FieldDef.section("ability.section.ground").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isGroundFill, this::setGroundFill), + FieldDef.floatField("gui.alpha", this::getGroundAlpha, this::setGroundAlpha) + ).tab("ability.tab.visual"), + + FieldDef.section("ability.section.rings").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isRings, this::setRings), + FieldDef.intField("gui.count", this::getRingCount, this::setRingCount).range(1, 5) + ).tab("ability.tab.visual"), + + FieldDef.section("ability.section.border").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isBorder, this::setBorder), + FieldDef.floatField("gui.speed", this::getBorderSpeed, this::setBorderSpeed) + ).tab("ability.tab.visual"), + + FieldDef.section("ability.section.accents").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isAccents, this::setAccents), + FieldDef.enumField("gui.style", AccentStyle.class, this::getAccentStyleEnum, this::setAccentStyleEnum) + ).tab("ability.tab.visual"), + + FieldDef.section("ability.section.lightning").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isLightning, this::setLightning), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + ).tab("ability.tab.visual"), + + FieldDef.section("ability.section.particles").tab("ability.tab.visual"), + FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles) + .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity), + FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale) + ).tab("ability.tab.visual").visibleWhen(this::isParticles), + FieldDef.row( + FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum), + FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow) + ).tab("ability.tab.visual").visibleWhen(this::isParticles), + FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir) + .tab("ability.tab.visual").visibleWhen(this::isParticles), + + FieldDef.section("ability.section.animation").tab("ability.tab.visual"), + FieldDef.floatField("gui.speed", this::getAnimSpeed, this::setAnimSpeed) + .tab("ability.tab.visual"), + + FieldDef.section("ability.section.colors").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor), + FieldDef.floatField("ability.innerAlpha", this::getInnerAlpha, this::setInnerAlpha).range(0, 1) + ).tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.row( + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .range(0, 1).visibleWhen(this::isOuterColorEnabled) + ).tab("ability.tab.visual") + )); + } + +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AccentStyle.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AccentStyle.java new file mode 100644 index 000000000..ae95abf44 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/AccentStyle.java @@ -0,0 +1,12 @@ +package kamkeel.npcs.controllers.data.ability.type; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Accent style enum used by AbilityZone. + */ +public enum AccentStyle { + NONE, + RING, + CROSS, + STAR; +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/EntityAbilityZone.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/EntityAbilityZone.java new file mode 100644 index 000000000..4fc7ed192 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/EntityAbilityZone.java @@ -0,0 +1,24 @@ +package kamkeel.npcs.controllers.data.ability.type; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + * + * Entity for ability zones (hazards, traps, etc.). + * Actual entity stays in mc1710, this is just for core compilation. + */ +public class EntityAbilityZone { + public void applyVisual(boolean groundFill, float groundAlpha, + boolean rings, int ringCount, boolean border, float borderSpeed, + boolean accents, int accentStyle, boolean lightning, + boolean particles, int particleMotion, String particleDir, + int particleSize, boolean particleGlow) {} + + public void setDurationTicks(int ticks) {} + public float getZoneRadius() { return 0; } + public float getZoneHeight() { return 0; } + public double posX = 0; + public double posY = 0; + public double posZ = 0; +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ParticleMotion.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ParticleMotion.java new file mode 100644 index 000000000..a4a1835e3 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ParticleMotion.java @@ -0,0 +1,13 @@ +package kamkeel.npcs.controllers.data.ability.type; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Particle motion enum used by AbilityZone. + */ +public enum ParticleMotion { + NONE, + INWARD, + OUTWARD, + UPWARD, + SPIRAL; +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ZoneShape.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ZoneShape.java new file mode 100644 index 000000000..e522e1356 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/ZoneShape.java @@ -0,0 +1,11 @@ +package kamkeel.npcs.controllers.data.ability.type; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Zone shape enum used by AbilityZone. + */ +public enum ZoneShape { + CIRCLE, + SQUARE, + LINE; +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java new file mode 100644 index 000000000..3769fb181 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java @@ -0,0 +1,443 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import noppes.npcs.controllers.data.MagicData; +import java.util.List; + +/** + * Abstract base for energy barrier abilities (Dome, Wall, Shield). + * Handles shared visual data, barrier IConfiguration, and lifecycle management. + */ +public abstract class AbilityBarrier extends AbilityEnergy { + + protected final EnergyBarrierData barrierData; + + // Spawn position offsets (relative to caster) + protected float offsetX = 0.0f; + protected float offsetY = 0.0f; + protected float offsetZ = 0.0f; + + // Runtime IEntity tracking + protected transient EntityEnergyBarrier barrierEntity; + + protected AbilityBarrier(EnergyDisplayData displayData, EnergyBarrierData barrierData) { + super(displayData); + this.barrierData = barrierData; + } + + // ==================== ABSTRACT METHODS ==================== + + /** + * Create and spawn the barrier IEntity during execution. + */ + protected abstract EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target); + + /** + * Add type-specific GUI field definitions. + */ + @ClientOnly + protected abstract void addBarrierTypeDefinitions(List defs); + + /** + * Write type-specific fields to NBT. + */ + protected abstract void writeBarrierTypeNBT(INbt nbt); + + /** + * Read type-specific fields from NBT. + */ + protected abstract void readBarrierTypeNBT(INbt nbt); + + // ==================== LIFECYCLE ==================== + + @Override + public boolean allowBurst() { + return false; + } + + @Override + public boolean hasDamage() { + return false; // Barriers are defensive + } + + @Override + public boolean hasMagic() { + return true; // Barriers use magic for defense interactions + } + + @Override + public boolean allowFreeOnCast() { + return true; + } + + @Override + public void detach() { + barrierEntity = null; + } + + @Override + public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (caster.worldObj.isRemote) return; + + if (tick == 1) { + barrierEntity = createBarrierEntity(caster, target); + if (barrierEntity != null) { + inheritMagicData(barrierEntity, caster); + applyBarrierHealthModifiers(caster); + barrierEntity.setupCharging(getWindUpTicks()); + spawnAbilityEntity(barrierEntity); + } + } + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + if (barrierEntity != null && !barrierEntity.isDead) { + // Barrier was spawned during windup — transition to active + barrierEntity.finishCharging(); + } else { + // No windup or IEntity died — create fresh + barrierEntity = createBarrierEntity(caster, target); + if (barrierEntity != null) { + inheritMagicData(barrierEntity, caster); + applyBarrierHealthModifiers(caster); + spawnAbilityEntity(barrierEntity); + } + } + } + + private void inheritMagicData(EntityEnergyBarrier IEntity, IEntityLivingBase caster) { + MagicData resolved = resolveMagicData(caster); + if (resolved != null) { + IEntity.setMagicData(resolved.copy()); + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (barrierEntity == null || barrierEntity.isDead) { + signalCompletion(); + return; + } + + // Free on Cast: complete immediately after barrier becomes active + if (isFreeOnCast()) { + signalCompletion(); + return; + } + + // Duration tracked by IEntity itself, but check here too as a failsafe + if (barrierData.useDuration && tick >= barrierData.durationTicks) { + cleanup(); + signalCompletion(); + } + } + + @Override + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) { + } + + /** + * Allow extenders to modify the barrier's max health at spawn time. + */ + private void applyBarrierHealthModifiers(IEntityLivingBase caster) { + float modHealth = AbilityController.Instance.fireModifyBarrierHealth(this, caster, barrierData.maxHealth); + if (modHealth != barrierData.maxHealth) { + barrierEntity.setBarrierMaxHealth(modHealth); + } + } + + @Override + public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) { + cleanup(); + } + + @Override + public void cleanup() { + if (barrierEntity != null && !barrierEntity.isDead) { + barrierEntity.setDead(); + } + barrierEntity = null; + } + + // ==================== NBT ==================== + + @Override + public final void writeTypeNBT(INbt nbt) { + writeBarrierTypeNBT(nbt); + writeEnergyNBT(nbt); + barrierData.writeNBT(new NBTWrapper(nbt)); + nbt.setFloat("barrierOffsetX", offsetX); + nbt.setFloat("barrierOffsetY", offsetY); + nbt.setFloat("barrierOffsetZ", offsetZ); + } + + @Override + public final void readTypeNBT(INbt nbt) { + offsetX = nbt.hasKey("barrierOffsetX") ? nbt.getFloat("barrierOffsetX") : 0.0f; + offsetY = nbt.hasKey("barrierOffsetY") ? nbt.getFloat("barrierOffsetY") : 0.0f; + offsetZ = nbt.hasKey("barrierOffsetZ") ? nbt.getFloat("barrierOffsetZ") : 0.0f; + readBarrierTypeNBT(nbt); + readEnergyNBT(nbt); + barrierData.readNBT(new NBTWrapper(nbt)); + } + + // ==================== API GETTERS & SETTERS ==================== + + // Display and lightning data inherited from AbilityEnergy + + // Barrier data + public float getBarrierMaxHealth() { + return barrierData.maxHealth; + } + + public void setBarrierMaxHealth(float maxHealth) { + barrierData.setMaxHealth(maxHealth); + } + + public boolean isUseHealth() { + return barrierData.useHealth; + } + + public void setUseHealth(boolean useHealth) { + barrierData.useHealth = useHealth; + } + + @Override + public float getDisplayBarrierHealth() { return barrierData.useHealth ? barrierData.maxHealth : 0; } + + @Override + public boolean isDisplayReflect() { return barrierData.reflect; } + + @Override + public float getDisplayReflectStrength() { return barrierData.reflectStrengthPct; } + + @Override + public boolean isDisplayAbsorbing() { return barrierData.absorbing; } + + public int getBarrierDuration() { + return barrierData.durationTicks; + } + + public void setBarrierDuration(int ticks) { + barrierData.setDurationTicks(ticks); + } + + public boolean isUseDuration() { + return barrierData.useDuration; + } + + public void setUseDuration(boolean useDuration) { + barrierData.useDuration = useDuration; + } + + public float getDefaultMultiplier() { + return barrierData.defaultMultiplier; + } + + public void setDefaultMultiplier(float mult) { + barrierData.defaultMultiplier = mult; + } + + public void setDamageMultiplier(String typeId, float mult) { + barrierData.setMultiplier(typeId, mult); + } + + public float getDamageMultiplier(String typeId) { + return barrierData.getMultiplier(typeId); + } + + // Solid data + public boolean isSolid() { + return barrierData.solid; + } + + public void setSolid(boolean solid) { + barrierData.solid = solid; + } + + // Knockback data + public boolean isKnockbackEnabled() { + return barrierData.knockbackEnabled; + } + + public void setKnockbackEnabled(boolean enabled) { + barrierData.knockbackEnabled = enabled; + } + + public float getKnockbackStrength() { + return barrierData.knockbackStrength; + } + + public void setKnockbackStrength(float strength) { + barrierData.knockbackStrength = strength; + } + + // Absorbing data + public boolean isAbsorbing() { + return barrierData.absorbing; + } + + public void setAbsorbing(boolean absorbing) { + barrierData.absorbing = absorbing; + } + + public float getAbsorbRadius() { + return barrierData.absorbRadius; + } + + public void setAbsorbRadius(float radius) { + barrierData.absorbRadius = radius; + } + + // Reflection data + public boolean isReflect() { + return barrierData.reflect; + } + + public void setReflect(boolean reflect) { + barrierData.reflect = reflect; + } + + public float getReflectStrengthPct() { + return barrierData.reflectStrengthPct; + } + + public void setReflectStrengthPct(float strengthPct) { + barrierData.setReflectStrengthPct(strengthPct); + } + + public boolean isTargetOwner() { + return barrierData.targetOwner; + } + + public void setTargetOwner(boolean targetOwner) { + barrierData.targetOwner = targetOwner; + } + + // Melee data + public boolean isMeleeEnabled() { + return barrierData.meleeEnabled; + } + + public void setMeleeEnabled(boolean enabled) { + barrierData.meleeEnabled = enabled; + } + + public float getMeleeDamageMultiplier() { + return barrierData.meleeDamageMultiplier; + } + + public void setMeleeDamageMultiplier(float mult) { + barrierData.meleeDamageMultiplier = mult; + } + + // Offset data + public float getOffsetX() { + return offsetX; + } + + public void setOffsetX(float offsetX) { + this.offsetX = offsetX; + } + + public float getOffsetY() { + return offsetY; + } + + public void setOffsetY(float offsetY) { + this.offsetY = offsetY; + } + + public float getOffsetZ() { + return offsetZ; + } + + public void setOffsetZ(float offsetZ) { + this.offsetZ = offsetZ; + } + + // ==================== GUI ==================== + + @ClientOnly + @Override + public final void getAbilityDefinitions(List defs) { + // Type-specific fields first + addBarrierTypeDefinitions(defs); + + // Offset section + defs.add(FieldDef.section("ability.section.offset")); + defs.add(FieldDef.row( + FieldDef.floatField("ability.offsetX", this::getOffsetX, this::setOffsetX).min(Float.NEGATIVE_INFINITY), + FieldDef.floatField("ability.offsetY", this::getOffsetY, this::setOffsetY).min(Float.NEGATIVE_INFINITY) + )); + defs.add(FieldDef.floatField("ability.offsetZ", this::getOffsetZ, this::setOffsetZ).min(Float.NEGATIVE_INFINITY)); + + // Barrier section + defs.add(FieldDef.section("ability.section.barrier")); + defs.add(FieldDef.boolField("ability.useHealth", this::isUseHealth, this::setUseHealth)); + defs.add(FieldDef.floatField("ability.maxHealth", this::getBarrierMaxHealth, this::setBarrierMaxHealth) + .visibleWhen(this::isUseHealth)); + defs.add(FieldDef.boolField("ability.useDuration", this::isUseDuration, this::setUseDuration)); + defs.add(FieldDef.intField("ability.duration", this::getBarrierDuration, this::setBarrierDuration) + .range(1, 12000).visibleWhen(this::isUseDuration)); + defs.add(FieldDef.floatField("ability.defaultMultiplier", this::getDefaultMultiplier, this::setDefaultMultiplier)); + + // Properties section + defs.add(FieldDef.section("ability.section.properties")); + defs.add(FieldDef.boolField("ability.solid", this::isSolid, this::setSolid) + .hover("ability.hover.solid")); + defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled) + .hover("ability.hover.knockbackEnabled")); + defs.add(FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength) + .range(0, 10).visibleWhen(this::isKnockbackEnabled)); + defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing) + .hover("ability.hover.absorbing")); + defs.add(FieldDef.floatField("ability.absorbRadius", this::getAbsorbRadius, this::setAbsorbRadius) + .range(-1, 128).visibleWhen(this::isAbsorbing) + .hover("ability.hover.absorbRadius")); + defs.add(FieldDef.boolField("ability.reflect", this::isReflect, this::setReflect) + .hover("ability.hover.reflect")); + defs.add(FieldDef.floatField("ability.reflectStrength", this::getReflectStrengthPct, this::setReflectStrengthPct) + .range(0, 100).visibleWhen(this::isReflect)); + defs.add(FieldDef.boolField("ability.targetOwner", this::isTargetOwner, this::setTargetOwner) + .hover("ability.hover.targetOwner") + .visibleWhen(this::isReflect)); + defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled) + .hover("ability.hover.meleeEnabled")); + defs.add(FieldDef.floatField("ability.meleeDamageMultiplier", this::getMeleeDamageMultiplier, this::setMeleeDamageMultiplier) + .range(0, 10).visibleWhen(this::isMeleeEnabled)); + + // Visual tab - colors + effects (from AbilityEnergy) + addEnergyVisualDefinitions(defs); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java new file mode 100644 index 000000000..71cf3b5fa --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java @@ -0,0 +1,259 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.HitType; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.RotationMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.ProjectileData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.util.AnchorPointHelper; +import noppes.npcs.api.ability.type.IAbilityEnergyBeam; +import java.util.Arrays; +import java.util.List; + +/** + * Energy Beam ability: A homing head with a trailing path. + * Supports 1-8 projectiles with shared visuals and per-projectile anchor points. + */ +public class AbilityBeam extends AbilityEnergyProjectile implements IAbilityEnergyBeam { + + private float beamWidth = 0.4f; + private float headSize = 0.6f; + private boolean freeAim = false; + + public AbilityBeam() { + super( + new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 10.0f), + new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f, HitType.SINGLE, 5), + new EnergyHomingData(0.4f, true, 0.1f, 15.0f), + new EnergyLifespanData(150.0f, 200) + ); + this.typeId = "ability.cnpc.beam"; + this.name = "Beam"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 40; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; + this.rotationMode = RotationMode.LOCKED; + this.rotationPhase = LockMode.ACTIVE; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + this.windUpAnimationName = "Ability_Beam_Windup"; + this.activeAnimationName = "Ability_Beam_Active"; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/beam.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityAbilityBeam createEntity(IEntityLivingBase caster, IEntityLivingBase target, + IVector3 spawnPos, EnergyDisplayData resolved, int index) { + EntityAbilityBeam beam = new EntityAbilityBeam( + caster.worldObj, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + beamWidth, headSize, + resolved, combatData, homingData, lightningData, lifespanData, + lockMovement.locksActive()); + beam.setFreeAim(freeAim); + return beam; + } + + @Override + protected void fireEntity(EntityAbilityBeam beam, IEntityLivingBase target) { + if (isPreview()) { + beam.startPreviewFiring(); + } else { + beam.startFiring(target); + } + } + + @Override + protected void setupEntityCharging(EntityAbilityBeam beam, ProjectileData projData, int index) { + beam.setupCharging(projData.anchor, windUpTicks, 1.0f); + } + + @Override + protected void setupEntityPreview(EntityAbilityBeam beam, IEntityLivingBase caster, + EnergyDisplayData resolved, ProjectileData projData, int index) { + beam.setupPreview(caster, beamWidth, headSize, resolved, lightningData, projData.anchor, windUpTicks, 1.0f); + } + + @Override + protected EntityAbilityBeam[] createEntityArray(int size) { + return new EntityAbilityBeam[size]; + } + + @Override + public void detach() { + if (entities != null) { + for (EntityAbilityBeam beam : entities) { + if (beam != null && !beam.isDead) { + beam.setAttachedToOwner(false); + } + } + } + super.detach(); + } + + @Override + protected float getProjectileTelegraphRadius() { + return headSize * 2.0f; + } + + @Override + protected IVector3 getSpawnPosition(IEntityLivingBase caster, int index) { + return AnchorPointHelper.calculateAnchorPosition(caster, projectiles[index].anchor, 1.0f); + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.single", a -> { + a.setName("Beam"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilityBeam beam = (AbilityBeam) a; + a.setName("Dual Beam"); + beam.setProjectileCount(2); + beam.setFireDelay(0); + beam.projectiles[1].colorOverride = true; + beam.projectiles[1].innerColor = 0xFFFFFF; + beam.projectiles[1].outerColor = 0xFF0000; + a.setWindUpTicks(80); + a.setWindUpAnimationName("Ability_BeamDual_Windup"); + a.setActiveAnimationName("Ability_BeamDual_Active"); + }) + ); + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeTypeSpecificNBT(INbt nbt) { + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + nbt.setBoolean("freeAim", freeAim); + } + + @Override + protected void readTypeSpecificNBT(INbt nbt) { + this.beamWidth = nbt.getFloat("beamWidth"); + this.headSize = nbt.getFloat("headSize"); + this.freeAim = nbt.getBoolean("freeAim"); + } + + // ==================== TYPE-SPECIFIC GETTERS ==================== + + public float getSpeed() { + return homingData.speed; + } + + public void setSpeed(float speed) { + homingData.speed = speed; + } + + public float getBeamWidth() { + return beamWidth; + } + + public void setBeamWidth(float beamWidth) { + this.beamWidth = beamWidth; + } + + public float getHeadSize() { + return headSize; + } + + public void setHeadSize(float headSize) { + this.headSize = headSize; + } + + public boolean isFreeAim() { + return freeAim; + } + + public void setFreeAim(boolean freeAim) { + this.freeAim = freeAim; + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addTypeDefinitions(List defs) { + defs.add(FieldDef.row( + FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES), + FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay) + .range(0, 200).visibleWhen(() -> projectileCount > 1) + )); + defs.add(FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) + )); + defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)); + defs.add(FieldDef.section("ability.section.beam")); + defs.add(FieldDef.boolField("ability.freeAim", this::isFreeAim, this::setFreeAim) + .hover("ability.hover.freeAim") + .enabledWhen(() -> lockMovement.locksActive() && rotationMode == RotationMode.FREE && !freeOnCast)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).range(0.1f, 100.0f), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).range(0.1f, 100.0f) + )); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200) + )); + defs.add(FieldDef.section("ability.section.homing")); + defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange).visibleWhen(this::isHoming) + )); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java new file mode 100644 index 000000000..5344b4996 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java @@ -0,0 +1,258 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.HitType; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.ProjectileData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityDisc; +import java.util.Arrays; +import java.util.List; + +/** + * Disc ability: Spawns a flat spinning disc projectile. + * Supports 1-8 projectiles with shared visuals and per-projectile anchor points. + * Has optional boomerang behavior to return to owner. + */ +public class AbilityDisc extends AbilityEnergyProjectile implements IAbilityDisc { + + private float discRadius = 1.0f; + private float discThickness = 0.2f; + private boolean vertical = false; + private boolean boomerang = false; + private int boomerangDelay = 40; + + public AbilityDisc() { + super( + new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 10.0f), + new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f, HitType.PIERCE, 5), + new EnergyHomingData(0.6f, true, 0.12f, 18.0f), + new EnergyLifespanData(150.0f, 200) + ); + this.typeId = "ability.cnpc.disc"; + this.name = "Disc"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 60; + this.lockMovement = LockMode.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + this.windUpAnimationName = "Ability_Disc_Windup"; + this.activeAnimationName = "Ability_Disc_Active"; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/disc.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/disc_overlay.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityAbilityDisc createEntity(IEntityLivingBase caster, IEntityLivingBase target, + IVector3 spawnPos, EnergyDisplayData resolved, int index) { + return new EntityAbilityDisc( + caster.worldObj, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + discRadius, discThickness, + resolved, combatData, homingData, lightningData, lifespanData, + boomerang, boomerangDelay); + } + + @Override + protected void fireEntity(EntityAbilityDisc disc, IEntityLivingBase target) { + if (isPreview()) { + disc.startPreviewFiring(); + } else { + disc.startMoving(target); + } + } + + @Override + protected void setupEntityCharging(EntityAbilityDisc disc, ProjectileData projData, int index) { + disc.setupCharging(projData.anchor, windUpTicks, vertical); + } + + @Override + protected void setupEntityPreview(EntityAbilityDisc disc, IEntityLivingBase caster, + EnergyDisplayData resolved, ProjectileData projData, int index) { + disc.setupPreview(caster, discRadius, discThickness, resolved, lightningData, projData.anchor, windUpTicks, vertical); + } + + @Override + protected EntityAbilityDisc[] createEntityArray(int size) { + return new EntityAbilityDisc[size]; + } + + @Override + protected float getProjectileTelegraphRadius() { + return discRadius * 1.5f; + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.single", a -> { + a.setName("Disc"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilityDisc disc = (AbilityDisc) a; + a.setName("Dual Disc"); + disc.setProjectileCount(2); + disc.setFireDelay(5); + disc.projectiles[1].colorOverride = true; + disc.projectiles[1].innerColor = 0xFFFFFF; + disc.projectiles[1].outerColor = 0x8800FF; + a.setWindUpAnimationName("Ability_DiscDual_Windup"); + a.setActiveAnimationName("Ability_DiscDual_Active"); + }) + ); + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeTypeSpecificNBT(INbt nbt) { + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("vertical", vertical); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + } + + @Override + protected void readTypeSpecificNBT(INbt nbt) { + this.discRadius = nbt.getFloat("discRadius"); + this.discThickness = nbt.getFloat("discThickness"); + this.vertical = nbt.getBoolean("vertical"); + this.boomerang = nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.getInteger("boomerangDelay"); + } + + // ==================== TYPE-SPECIFIC GETTERS ==================== + + public float getSpeed() { + return homingData.speed; + } + + public void setSpeed(float speed) { + homingData.speed = speed; + } + + public float getDiscRadius() { + return discRadius; + } + + public void setDiscRadius(float discRadius) { + this.discRadius = discRadius; + } + + public float getDiscThickness() { + return discThickness; + } + + public void setDiscThickness(float discThickness) { + this.discThickness = discThickness; + } + + public boolean isVertical() { + return vertical; + } + + public void setVertical(boolean vertical) { + this.vertical = vertical; + } + + public boolean isBoomerang() { + return boomerang; + } + + public void setBoomerang(boolean boomerang) { + this.boomerang = boomerang; + } + + public int getBoomerangDelay() { + return boomerangDelay; + } + + public void setBoomerangDelay(int boomerangDelay) { + this.boomerangDelay = boomerangDelay; + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addTypeDefinitions(List defs) { + defs.add(FieldDef.row( + FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES), + FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay) + .range(0, 200).visibleWhen(() -> projectileCount > 1) + )); + defs.add(FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) + )); + defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)); + defs.add(FieldDef.section("ability.section.disc")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius).range(0.1f, 100.0f), + FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness).range(0.05f, 100.0f) + )); + defs.add(FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) + .hover("ability.hover.vertical")); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200) + )); + defs.add(FieldDef.section("ability.section.homing")); + defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing")); + defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming)); + defs.add(FieldDef.section("ability.section.boomerang")); + defs.add(FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang).hover("ability.hover.boomerang")); + defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang)); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java new file mode 100644 index 000000000..05998774d --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java @@ -0,0 +1,164 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import java.util.Arrays; +import java.util.List; + +/** + * Energy Dome ability: Spawns a spherical barrier centered on the caster. + * Blocks incoming energy projectiles with configurable damage multipliers. + * Duration and/or HP based. + */ +public class AbilityDome extends AbilityBarrier { + + private float domeRadius = 5.0f; + private boolean followCaster = false; + + public AbilityDome() { + super( + new EnergyDisplayData(0x44CCFF, 0x2288FF, true, 0.3f, 0.35f, 0.5f, 0.0f), + new EnergyBarrierData(100.0f, true, 200, true) + ); + this.typeId = "ability.cnpc.dome"; + this.name = "Dome"; + this.targetingMode = TargetingMode.SELF; + this.cooldownTicks = 100; + this.windUpTicks = 30; + this.lockMovement = LockMode.WINDUP; + this.telegraphType = TelegraphType.RING; + this.showTelegraph = true; + this.windUpAnimationName = ""; + this.activeAnimationName = ""; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/dome.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target) { + EntityEnergyDome dome = new EntityEnergyDome( + caster.worldObj, caster, + caster.posX + offsetX, caster.posY + offsetY, caster.posZ + offsetZ, + domeRadius, displayData.copy(), lightningData.copy(), barrierData.copy() + ); + dome.setFollowCaster(followCaster); + dome.setOffsets(offsetX, offsetY, offsetZ); + dome.setSourceAbility(this); + return dome; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.SELF}; + } + + @Override + public float getTelegraphRadius() { + return domeRadius; + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.small", a -> { + AbilityDome dome = (AbilityDome) a; + a.setName("Small Dome"); + dome.setDomeRadius(3.0f); + dome.setBarrierMaxHealth(60.0f); + }), + new AbilityVariant("ability.variant.large", a -> { + AbilityDome dome = (AbilityDome) a; + a.setName("Large Dome"); + dome.setDomeRadius(8.0f); + dome.setBarrierMaxHealth(200.0f); + dome.setBarrierDuration(300); + }) + ); + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeBarrierTypeNBT(INbt nbt) { + nbt.setFloat("domeRadius", domeRadius); + nbt.setBoolean("followCaster", followCaster); + } + + @Override + protected void readBarrierTypeNBT(INbt nbt) { + this.domeRadius = nbt.hasKey("domeRadius") ? nbt.getFloat("domeRadius") : 5.0f; + this.followCaster = nbt.hasKey("followCaster") && nbt.getBoolean("followCaster"); + } + + // ==================== GETTERS & SETTERS ==================== + + public float getDomeRadius() { + return domeRadius; + } + + public void setDomeRadius(float radius) { + this.domeRadius = Math.max(1.0f, radius); + } + + public boolean isFollowCaster() { + return followCaster; + } + + public void setFollowCaster(boolean follow) { + this.followCaster = follow; + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addBarrierTypeDefinitions(List defs) { + defs.add(FieldDef.floatField("ability.domeRadius", this::getDomeRadius, this::setDomeRadius) + .range(1.0f, 50.0f)); + defs.add(FieldDef.boolField("ability.followCaster", this::isFollowCaster, this::setFollowCaster) + .hover("ability.hover.followCaster")); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java new file mode 100644 index 000000000..374ebe035 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java @@ -0,0 +1,211 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; +import java.util.List; + +/** + * Abstract base for all energy-based abilities (Projectile, Barrier, Sweeper, etc.). + * Provides shared visual data (colors, lightning) and corresponding API/GUI/NBT. + */ +public abstract class AbilityEnergy extends Ability { + + protected EnergyDisplayData displayData; + protected EnergyLightningData lightningData; + + protected AbilityEnergy(EnergyDisplayData displayData) { + this.displayData = displayData; + this.lightningData = new EnergyLightningData(); + } + + // ==================== DISPLAY DATA ==================== + + public int getInnerColor() { + return displayData.innerColor; + } + + public void setInnerColor(int color) { + displayData.innerColor = color; + } + + public float getInnerAlpha() { + return displayData.innerAlpha; + } + + public void setInnerAlpha(float alpha) { + displayData.innerAlpha = alpha; + } + + public int getOuterColor() { + return displayData.outerColor; + } + + public void setOuterColor(int color) { + displayData.outerColor = color; + } + + public boolean isOuterColorEnabled() { + return displayData.outerColorEnabled; + } + + public void setOuterColorEnabled(boolean enabled) { + displayData.outerColorEnabled = enabled; + } + + public float getOuterColorWidth() { + return displayData.outerColorWidth; + } + + public void setOuterColorWidth(float width) { + displayData.outerColorWidth = width; + } + + public float getOuterColorAlpha() { + return displayData.outerColorAlpha; + } + + public void setOuterColorAlpha(float alpha) { + displayData.outerColorAlpha = alpha; + } + + public float getRotationSpeed() { + return displayData.rotationSpeed; + } + + public void setRotationSpeed(float speed) { + displayData.rotationSpeed = speed; + } + + // ==================== LIGHTNING DATA ==================== + + public boolean hasLightningEffect() { + return lightningData.lightningEffect; + } + + public void setLightningEffect(boolean enabled) { + lightningData.lightningEffect = enabled; + } + + public float getLightningDensity() { + return lightningData.lightningDensity; + } + + public void setLightningDensity(float density) { + lightningData.lightningDensity = density; + } + + public float getLightningRadius() { + return lightningData.lightningRadius; + } + + public void setLightningRadius(float radius) { + lightningData.lightningRadius = radius; + } + + public int getLightningFadeTime() { + return lightningData.lightningFadeTime; + } + + public void setLightningFadeTime(int fadeTime) { + lightningData.lightningFadeTime = fadeTime; + } + + // ==================== NBT HELPERS ==================== + + protected void writeEnergyNBT(INbt nbt) { + displayData.writeNBT(new NBTWrapper(nbt)); + lightningData.writeNBT(new NBTWrapper(nbt)); + } + + protected void readEnergyNBT(INbt nbt) { + displayData.readNBT(new NBTWrapper(nbt)); + lightningData.readNBT(new NBTWrapper(nbt)); + } + + // ==================== GUI HELPERS ==================== + + /** + * Add complete visual tab definitions (colors section + effects/lightning section). + * Subclasses can call this directly or use the individual methods for more control. + */ + @ClientOnly + protected void addEnergyVisualDefinitions(List defs) { + addEnergyColorDefinitions(defs); + addEnergyEffectDefinitions(defs); + } + + /** + * Add the colors section to the visual tab. + */ + @ClientOnly + protected void addEnergyColorDefinitions(List defs) { + defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual")); + defs.add(FieldDef.row( + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor), + FieldDef.floatField("ability.innerAlpha", this::getInnerAlpha, this::setInnerAlpha).range(0, 1) + ).tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .range(0, 1).visibleWhen(this::isOuterColorEnabled) + ).tab("ability.tab.visual")); + } + + /** + * Add the effects section (lightning) to the visual tab. + * Override to insert additional fields before lightning (e.g., rotationSpeed). + */ + @ClientOnly + protected void addEnergyEffectDefinitions(List defs) { + defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual")); + addEnergyLightningDefinitions(defs); + } + + /** + * Add just the lightning fields (no section header) to the visual tab. + */ + @ClientOnly + protected void addEnergyLightningDefinitions(List defs) { + defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab("ability.tab.visual")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .visibleWhen(this::hasLightningEffect).range(0.01f, 100f), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) + ).tab("ability.tab.visual")); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java new file mode 100644 index 000000000..42c6341f8 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -0,0 +1,946 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.enums.HitType; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.ProjectileData; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.util.AnchorPointHelper; +import noppes.npcs.api.ability.type.IAbilityEnergyProjectile; +import noppes.npcs.controllers.data.MagicData; +import java.util.List; + +/** + * Abstract base for energy projectile abilities (Orb, Disc, Beam, LaserShot). + * Handles shared lifecycle (windup → execute → active → completion), multi-projectile + * support with staggered firing, telegraph creation, NBT persistence, and GUI definitions. + *

+ * Subclasses provide IEntity creation/firing/charging logic and type-specific fields. + * + * @param The IEntity type this ability spawns + */ +public abstract class AbilityEnergyProjectile extends AbilityEnergy implements IAbilityEnergyProjectile { + + protected static final int MAX_PROJECTILES = 8; + + // Multi-projectile support + protected int projectileCount = 1; + protected int fireDelay = 0; + + // Shared combat/movement data + protected final EnergyCombatData combatData; + protected final EnergyHomingData homingData; + protected final EnergyLifespanData lifespanData; + + // Per-projectile data (anchor + optional color override) + protected ProjectileData[] projectiles; + + // Transient IEntity state + protected transient E[] entities; + protected transient boolean[] projectileSpawned; + protected transient int spawnedCount; + protected transient String[] chargeVisualIds; + protected transient IEntityLivingBase chargeVisualCaster; + + // ==================== CONSTRUCTOR ==================== + + protected AbilityEnergyProjectile( + EnergyDisplayData displayData, + EnergyCombatData combatData, + EnergyHomingData homingData, + EnergyLifespanData lifespanData + ) { + super(displayData); + this.combatData = combatData; + this.homingData = homingData; + this.lifespanData = lifespanData; + this.burstOverlap = true; + initProjectiles(1); + } + + // ==================== ABSTRACT METHODS ==================== + + /** + * Create a single IEntity for the given projectile index. + * Called when a projectile actually fires. + */ + protected abstract E createEntity(IEntityLivingBase caster, IEntityLivingBase target, + IVector3 spawnPos, EnergyDisplayData resolved, int index); + + /** + * Fire an IEntity (start moving toward target). IEntity is guaranteed non-null and alive. + */ + protected abstract void fireEntity(E IEntity, IEntityLivingBase target); + + /** + * Setup IEntity for charging animation during windup (non-preview). + */ + protected abstract void setupEntityCharging(E IEntity, ProjectileData projData, int index); + + /** + * Setup IEntity for preview mode during windup. + */ + protected abstract void setupEntityPreview(E IEntity, IEntityLivingBase caster, + EnergyDisplayData resolved, ProjectileData projData, int index); + + /** + * Create typed array (Java generics can't create generic arrays). + */ + protected abstract E[] createEntityArray(int size); + + /** + * Get the telegraph radius for this projectile type. + */ + protected abstract float getProjectileTelegraphRadius(); + + /** + * Add type-specific GUI field definitions (Type tab). + * Called before shared visual tab definitions. + */ + @ClientOnly + protected abstract void addTypeDefinitions(List defs); + + /** + * Write type-specific fields to NBT (e.g., orbSize, discRadius). + * Shared data classes are written by the base class. + */ + protected abstract void writeTypeSpecificNBT(INbt nbt); + + /** + * Read type-specific fields from NBT. + * Shared data classes are read by the base class. + */ + protected abstract void readTypeSpecificNBT(INbt nbt); + + // ==================== OVERRIDABLE HOOKS ==================== + + /** + * Get the default anchor point for a projectile index. + * Override for types with different anchor defaults. + */ + protected AnchorPoint getDefaultAnchor(int index) { + switch (index) { + case 0: + return AnchorPoint.RIGHT_HAND; + case 1: + return AnchorPoint.LEFT_HAND; + default: + return AnchorPoint.FRONT; + } + } + + /** + * Get the spawn position for a projectile. Override for types that need + * an offset distance (e.g., Beam uses offsetDist=1.0). + */ + protected IVector3 getSpawnPosition(IEntityLivingBase caster, int index) { + return AnchorPointHelper.calculateAnchorPosition(caster, projectiles[index].anchor); + } + + // ==================== PROJECTILE MANAGEMENT ==================== + + protected void initProjectiles(int count) { + this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + this.projectiles = new ProjectileData[projectileCount]; + for (int i = 0; i < projectileCount; i++) { + projectiles[i] = new ProjectileData(getDefaultAnchor(i)); + } + } + + public void setProjectileCount(int count) { + int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + if (newCount == projectileCount) return; + + ProjectileData[] oldProjectiles = projectiles; + initProjectiles(newCount); + + if (oldProjectiles != null) { + int copyCount = Math.min(oldProjectiles.length, newCount); + for (int i = 0; i < copyCount; i++) { + if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i]; + } + } + } + + public int getProjectileCount() { + return projectileCount; + } + + public int getFireDelay() { + return fireDelay; + } + + public void setFireDelay(int delay) { + this.fireDelay = Math.max(0, delay); + } + + protected int clampIndex(int index) { + return Math.max(0, Math.min(index, projectileCount - 1)); + } + + // ==================== IEntity CREATION ==================== + + protected void initRuntimeState(IEntityLivingBase caster) { + entities = createEntityArray(projectileCount); + projectileSpawned = new boolean[projectileCount]; + spawnedCount = 0; + chargeVisualIds = new String[projectileCount]; + chargeVisualCaster = caster; + } + + protected E createProjectileEntity(IEntityLivingBase caster, IEntityLivingBase target, int index) { + IVector3 spawnPos = getSpawnPosition(caster, index); + EnergyDisplayData resolved = projectiles[index].resolveDisplay(displayData); + E IEntity = createEntity(caster, target, spawnPos, resolved, index); + IEntity.setAnchorData(projectiles[index].anchor); + IEntity.setEffects(this.effects); + IEntity.setIgnoreIFrames(this.isIgnoreIFrames()); + IEntity.setSourceAbility(this); + MagicData resolvedMagic = resolveMagicData(caster); + if (resolvedMagic != null) { + IEntity.setMagicData(resolvedMagic.copy()); + } + return IEntity; + } + + protected void spawnProjectileEntity(E IEntity, int index) { + if (IEntity == null) return; + spawnAbilityEntity(IEntity); + entities[index] = IEntity; + if (!projectileSpawned[index]) { + projectileSpawned[index] = true; + spawnedCount++; + } + } + + protected void spawnAndFireProjectile(IEntityLivingBase caster, IEntityLivingBase target, int index) { + if (entities != null && entities[index] != null && !entities[index].isDead) return; + E IEntity = createProjectileEntity(caster, target, index); + + // Initialize launch position/motion BEFORE spawn so the + // initial spawn packet already has centered look-vector data on all clients. + fireEntitySafe(IEntity, caster, target); + spawnProjectileEntity(IEntity, index); + } + + /** + * Hook for launch target selection. + * Players launch along their look vector (crosshair-aligned), so target is ignored. + * NPCs use their resolved target so launch math can track the target immediately. + */ + protected IEntityLivingBase getLaunchTarget(IEntityLivingBase caster, IEntityLivingBase target) { + return isPlayerCaster(caster) ? null : target; + } + + /** + * Fire an IEntity if it's valid. + * Clears charging state first so the IEntity can move and check collisions. + */ + protected void fireEntitySafe(E IEntity, IEntityLivingBase caster, IEntityLivingBase target) { + if (IEntity != null && !IEntity.isDead) { + if (IEntity.isCharging()) { + IEntity.setCharging(false); + } + fireEntity(IEntity, getLaunchTarget(caster, target)); + } + } + + protected String getChargeVisualId(IEntityLivingBase caster, int index) { + return "energy_charge:" + caster.getEntityId() + ":" + executionStartTime + ":" + burstIndex + ":" + index; + } + + protected void spawnChargeVisual(IEntityLivingBase caster, IEntityLivingBase target, int index, int chargeDuration) { + if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return; + if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return; + + E previewEntity = createProjectileEntity(caster, target, index); + previewEntity.setPreviewMode(true); + previewEntity.setPreviewOwner(caster); + setupEntityCharging(previewEntity, projectiles[index], index); + previewEntity.setChargeDuration(Math.max(1, chargeDuration)); + + String id = getChargeVisualId(caster, index); + chargeVisualIds[index] = id; + chargeVisualCaster = caster; + EnergyChargeSpawnPacket.sendToTracking(id, previewEntity, caster); + + // Register with server-side tracker for late-join support + EnergyChargeTracker.Instance.add(new EnergyChargeTracker.ChargeEntry( + id, + previewEntity.getClass().getName(), + previewEntity.exportSpawnNBT(), + caster.getEntityId(), + (int) caster.worldObj.getTotalWorldTime() + )); + } + + protected void removeChargeVisual(IEntityLivingBase caster, int index) { + if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return; + if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return; + + String id = chargeVisualIds[index]; + if (id != null && !id.isEmpty()) { + EnergyChargeRemovePacket.sendToTracking(id, caster); + EnergyChargeTracker.Instance.remove(id, caster.getEntityId()); + chargeVisualIds[index] = null; + } + } + + protected void removeAllChargeVisuals(IEntityLivingBase caster) { + if (chargeVisualIds == null || caster == null) return; + for (int i = 0; i < chargeVisualIds.length; i++) { + removeChargeVisual(caster, i); + } + // Safety net: ensure tracker is fully cleaned for this caster + if (!isPreview() && caster.worldObj != null && !caster.worldObj.isRemote) { + EnergyChargeTracker.Instance.removeAllForCaster(caster.getEntityId()); + } + } + + // ==================== LIFECYCLE ==================== + + @Override + public boolean allowOverlap() { + return true; + } + + @Override + public boolean allowFreeOnCast() { + return true; + } + + @Override + public void detach() { + entities = null; + projectileSpawned = null; + spawnedCount = 0; + } + + /** + * Remove a specific IEntity from this ability's tracking. + * Used when a projectile is reflected — it no longer belongs to this ability, + * so the ability should stop waiting for it and free the caster. + */ + public void detachEntity(IEntity IEntity) { + if (entities == null || IEntity == null) return; + for (int i = 0; i < entities.length; i++) { + if (entities[i] == IEntity) { + entities[i] = null; + break; + } + } + } + + @Override + public boolean isReadyForBurstCompletion(int activeTick) { + return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (caster.worldObj.isRemote && !isPreview()) return; + + if (tick == 1) { + initRuntimeState(caster); + + if (isPreview()) { + for (int i = 0; i < projectileCount; i++) { + E previewEntity = createProjectileEntity(caster, target, i); + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i); + spawnAbilityEntity(previewEntity); + entities[i] = previewEntity; + projectileSpawned[i] = true; + spawnedCount++; + } + return; + } + + for (int i = 0; i < projectileCount; i++) { + spawnChargeVisual(caster, target, i, windUpTicks); + } + } + } + + @Override + public void resetForBurst() { + removeAllChargeVisuals(chargeVisualCaster); + + // Non-overlap: kill old entities before next burst creates new ones + if (!burstOverlap) { + cleanup(); + } + // Clear reference so the next iteration creates fresh entities. + // For overlap mode, old entities stay alive (tracked in burstEntities). + entities = null; + projectileSpawned = null; + spawnedCount = 0; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + // GUI preview keeps the original non-IWorld preview IEntity flow. + if (isPreview()) { + if (entities == null) { + initRuntimeState(caster); + for (int i = 0; i < projectileCount; i++) { + E previewEntity = createProjectileEntity(caster, target, i); + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i); + spawnAbilityEntity(previewEntity); + entities[i] = previewEntity; + projectileSpawned[i] = true; + spawnedCount++; + } + } + + fireEntitySafe(entities[0], caster, target); + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireEntitySafe(entities[i], caster, target); + } + } + return; + } + + if (entities == null || projectileSpawned == null || entities.length != projectileCount) { + initRuntimeState(caster); + } + + removeAllChargeVisuals(caster); + + // Fire first projectile immediately. + spawnAndFireProjectile(caster, target, 0); + + // Fire remaining projectiles immediately if there is no stagger delay. + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + spawnAndFireProjectile(caster, target, i); + } + } else { + // During active stagger, keep client-only charging visuals for unfired projectiles. + for (int i = 1; i < projectileCount; i++) { + spawnChargeVisual(caster, target, i, fireDelay * i); + } + } + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + if (entities == null || projectileSpawned == null) { + signalCompletion(); + return; + } + + // Fire staggered projectiles: spawn real IWorld IEntity only when it is time to fire. + if (fireDelay > 0) { + for (int i = 1; i < projectileCount; i++) { + if (tick == fireDelay * i) { + removeChargeVisual(caster, i); + spawnAndFireProjectile(caster, target, i); + } + } + } + + // Free on Cast: complete once all projectiles have been fired + if (isFreeOnCast()) { + int lastFireTick = fireDelay > 0 ? fireDelay * (projectileCount - 1) : 0; + if (tick >= lastFireTick) { + signalCompletion(); + return; + } + } + + // Before all projectiles have fired, the ability is still active by definition. + boolean allDead = spawnedCount >= projectileCount; + if (allDead) { + for (E IEntity : entities) { + if (IEntity == null || IEntity.isDead) continue; + + if (!isPreview() && tick > 5 && IEntity.worldObj != null + && IEntity.worldObj.getEntityByID(IEntity.getEntityId()) != IEntity) { + IEntity.setDead(); + continue; + } + + allDead = false; + break; + } + } + + // Failsafe timeout: force completion if active phase exceeded max possible flight time + if (!allDead) { + int lastFireTick = fireDelay > 0 ? fireDelay * (projectileCount - 1) : 0; + int maxFlightTime = lifespanData.maxLifetime > 0 ? lifespanData.maxLifetime : 200; + if (tick > lastFireTick + maxFlightTime + 20) { + cleanup(); + allDead = true; + } + } + + if (allDead) { + signalCompletion(); + } + } + + @Override + public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) { + removeAllChargeVisuals(caster); + } + + @Override + public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) { + removeAllChargeVisuals(caster); + cleanup(); + } + + @Override + public void cleanup() { + removeAllChargeVisuals(chargeVisualCaster); + + if (entities != null) { + for (int i = 0; i < entities.length; i++) { + if (entities[i] != null && !entities[i].isDead) { + entities[i].setDead(); + } + entities[i] = null; + } + } + entities = null; + projectileSpawned = null; + spawnedCount = 0; + chargeVisualIds = null; + chargeVisualCaster = null; + } + + // ==================== TELEGRAPH ==================== + + @Override + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) { + return null; + } + + Telegraph telegraph = Telegraph.circle(getProjectileTelegraphRadius()); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + double groundY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, groundY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return getProjectileTelegraphRadius(); + } + + // ==================== NBT ==================== + + @Override + public final void writeTypeNBT(INbt nbt) { + // Type-specific fields + writeTypeSpecificNBT(nbt); + + // Shared fields + nbt.setInteger("projectileCount", projectileCount); + nbt.setInteger("fireDelay", fireDelay); + + // Shared data classes + writeEnergyNBT(nbt); + combatData.writeNBT(new NBTWrapper(nbt)); + homingData.writeNBT(new NBTWrapper(nbt)); + lifespanData.writeNBT(new NBTWrapper(nbt)); + + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + INbt projNbt = new INbt(); + projectiles[i].writeNBT(projNbt); + nbt.setTag("Projectile_" + i, projNbt); + } + } + + @Override + public final void readTypeNBT(INbt nbt) { + // Type-specific fields + readTypeSpecificNBT(nbt); + + // Shared fields + int count = nbt.getInteger("projectileCount"); + initProjectiles(count); + this.fireDelay = nbt.getInteger("fireDelay"); + + // Shared data classes + readEnergyNBT(nbt); + combatData.readNBT(new NBTWrapper(nbt)); + homingData.readNBT(new NBTWrapper(nbt)); + lifespanData.readNBT(new NBTWrapper(nbt)); + + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); + } + } + } + + // ==================== PREVIEW ==================== + + @Override + public int getMaxPreviewDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } + + // ==================== API GETTERS & SETTERS ==================== + + // Combat data + @Override + public float getDamage() { + return combatData.damage; + } + + @Override + public void setDamage(float damage) { + combatData.damage = damage; + } + + @Override + public float getDisplayDamage() { return combatData.damage; } + + @Override + public float getKnockback() { + return combatData.knockback; + } + + @Override + public void setKnockback(float knockback) { + combatData.knockback = knockback; + } + + @Override + public float getKnockbackUp() { + return combatData.knockbackUp; + } + + @Override + public void setKnockbackUp(float knockbackUp) { + combatData.knockbackUp = knockbackUp; + } + + @Override + public boolean isExplosive() { + return combatData.explosive; + } + + @Override + public void setExplosive(boolean explosive) { + combatData.explosive = explosive; + } + + @Override + public float getExplosionRadius() { + return combatData.getExplosionRadius(); + } + + @Override + public void setExplosionRadius(float explosionRadius) { + combatData.setExplosionRadius(explosionRadius); + } + + @Override + public float getExplosionDamageFalloff() { + return combatData.explosionDamageFalloff; + } + + @Override + public void setExplosionDamageFalloff(float falloff) { + combatData.explosionDamageFalloff = falloff; + } + + public int getHitType() { + return combatData.hitType.ordinal(); + } + + public void setHitType(int hitType) { + this.combatData.hitType = HitType.fromOrdinal(hitType); + } + + public int getMultiHitDelayTicks() { + return combatData.multiHitDelayTicks; + } + + public void setMultiHitDelayTicks(int delay) { + this.combatData.multiHitDelayTicks = Math.max(1, delay); + } + + public int getMaxHits() { + return combatData.getMaxHits(); + } + + public void setMaxHits(int maxHits) { + combatData.setMaxHits(maxHits); + } + + // Lifespan data + @Override + public float getMaxDistance() { + return lifespanData.maxDistance; + } + + @Override + public void setMaxDistance(float maxDistance) { + lifespanData.maxDistance = maxDistance; + } + + @Override + public int getMaxLifetime() { + return lifespanData.maxLifetime; + } + + @Override + public void setMaxLifetime(int maxLifetime) { + lifespanData.maxLifetime = maxLifetime; + } + + // Homing data + public boolean isHoming() { + return homingData.homing; + } + + public void setHoming(boolean homing) { + homingData.homing = homing; + } + + public float getHomingStrength() { + return homingData.homingStrength; + } + + public void setHomingStrength(float strength) { + homingData.homingStrength = strength; + } + + public float getHomingRange() { + return homingData.homingRange; + } + + public void setHomingRange(float range) { + homingData.homingRange = range; + } + + // Display and lightning data inherited from AbilityEnergy + + // Anchor - default to projectile 0 + public AnchorPoint getAnchorPointEnum() { + return projectiles[0].anchor.anchorPoint; + } + + public void setAnchorPointEnum(AnchorPoint point) { + projectiles[0].anchor.anchorPoint = point; + } + + public float getAnchorOffsetX() { + return projectiles[0].anchor.anchorOffsetX; + } + + public void setAnchorOffsetX(float x) { + projectiles[0].anchor.anchorOffsetX = x; + } + + public float getAnchorOffsetY() { + return projectiles[0].anchor.anchorOffsetY; + } + + public void setAnchorOffsetY(float y) { + projectiles[0].anchor.anchorOffsetY = y; + } + + public float getAnchorOffsetZ() { + return projectiles[0].anchor.anchorOffsetZ; + } + + public void setAnchorOffsetZ(float z) { + projectiles[0].anchor.anchorOffsetZ = z; + } + + public boolean getLaunchFromAnchor() { + return projectiles[0].anchor.launchFromAnchor; + } + + public void setLaunchFromAnchor(boolean launchFromAnchor) { + projectiles[0].anchor.launchFromAnchor = launchFromAnchor; + } + + @Override + public int getAnchorPoint() { + return projectiles[0].anchor.anchorPoint.ordinal(); + } + + @Override + public void setAnchorPoint(int point) { + projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); + } + + // Indexed API methods + public int getInnerColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.innerColor : displayData.innerColor; + } + + public void setInnerColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.innerColor = color; + } + + public int getOuterColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.outerColor : displayData.outerColor; + } + + public void setOuterColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.outerColor = color; + } + + public int getAnchorPoint(int index) { + return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); + } + + public void setAnchorPoint(int index, int point) { + projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); + } + + public void setAnchorOffsetX(int index, float offset) { + projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; + } + + public void setAnchorOffsetY(int index, float offset) { + projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; + } + + public void setAnchorOffsetZ(int index, float offset) { + projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; + } + + public boolean getLaunchFromAnchor(int index) { + return projectiles[clampIndex(index)].anchor.launchFromAnchor; + } + + public void setLaunchFromAnchor(int index, boolean launchFromAnchor) { + projectiles[clampIndex(index)].anchor.launchFromAnchor = launchFromAnchor; + } + + // ==================== GUI FIELD DEFINITIONS ==================== + + @ClientOnly + @Override + public final void getAbilityDefinitions(List defs) { + defs.add(FieldDef.enumField("ability.hitType", HitType.class, + () -> HitType.fromOrdinal(getHitType()), + (v) -> setHitType(v.ordinal())) + .hover("ability.hover.hitType")); + defs.add(FieldDef.intField("ability.multiHitDelay", this::getMultiHitDelayTicks, this::setMultiHitDelayTicks) + .range(1, 200) + .visibleWhen(() -> getHitType() == HitType.MULTI.ordinal()) + .hover("ability.hover.multiHitDelay")); + defs.add(FieldDef.intField("ability.maxHits", this::getMaxHits, this::setMaxHits) + .range(1, EnergyCombatData.MAX_HITS) + .visibleWhen(() -> getHitType() != HitType.SINGLE.ordinal()) + .hover("ability.hover.maxHits")); + + // Type-specific fields first + addTypeDefinitions(defs); + + // Shared visual tab - colors + effects (from AbilityEnergy) + addEnergyColorDefinitions(defs); + + // Effects section with projectile-specific rotationSpeed before lightning + defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual")); + defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab("ability.tab.visual")); + addEnergyLightningDefinitions(defs); + + // Shared visual tab - per-projectile sections + for (int i = 0; i < MAX_PROJECTILES; i++) { + final int idx = i; + String sectionKey = "ability.section.projectile" + (i + 1); + + defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual") + .visibleWhen(() -> idx < projectileCount)); + + defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v) + .min(Float.NEGATIVE_INFINITY), + FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v) + .min(Float.NEGATIVE_INFINITY) + ).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount).min(Float.NEGATIVE_INFINITY)); + defs.add(FieldDef.boolField("ability.launchFromAnchor", () -> projectiles[idx].anchor.launchFromAnchor, v -> projectiles[idx].anchor.launchFromAnchor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount) + .hover("ability.hover.launchFromAnchor")); + + // Color override (only when multiple projectiles) + defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1)); + defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java new file mode 100644 index 000000000..691fe7f61 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java @@ -0,0 +1,246 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.*; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.ProjectileData; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityLaser; +import java.util.List; + +/** + * Laser ability: Sweeping beam that follows the caster's look vector. + * Expands from origin to maxLength, then stays active until maxLifetime expires. + * Single-projectile only (always projectileCount=1). + */ +public class AbilityLaser extends AbilityEnergyProjectile implements IAbilityLaser { + + private float laserWidth = 0.3f; + private float expansionSpeed = 3.0f; + private float maxLength = 32.0f; + + public AbilityLaser() { + super( + new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 4.0f), + new EnergyCombatData(4.0f, 0.5f, 0.05f, false, 2.0f, 0.5f, HitType.MULTI, 5), + new EnergyHomingData(), + new EnergyLifespanData(150.0f, 100) + ); + this.typeId = "ability.cnpc.laser_shot"; + this.name = "Laser"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 3.0f; + this.cooldownTicks = 0; + this.windUpTicks = 15; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; + this.rotationMode = RotationMode.TRACK; + this.rotationPhase = LockMode.WINDUP_AND_ACTIVE; + this.trackSpeed = 4.0f; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + this.windUpAnimationName = "Ability_Laser_Windup"; + this.activeAnimationName = "Ability_Laser_Active"; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/laser_shot.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/laser_shot_overlay.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + // ==================== OVERRIDES ==================== + + @Override + protected AnchorPoint getDefaultAnchor(int index) { + return AnchorPoint.FRONT; + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public int getMaxPreviewDuration() { + int lifetime = lifespanData.maxLifetime; + return lifetime > 0 ? Math.min(lifetime, 60) : 60; + } + + /** + * LINE telegraph: positioned at caster, aimed toward target. + * Overrides the circle telegraph from AbilityEnergyProjectile. + */ + @Override + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster)) { + return null; + } + + Telegraph telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth()); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); + if (target != null) { + instance.setTargetEntityId(target.getEntityId()); + } + return instance; + } + + @Override + public float getTelegraphLength() { + return maxLength; + } + + @Override + public float getTelegraphWidth() { + return laserWidth * 2.0f; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityAbilityLaser createEntity(IEntityLivingBase caster, IEntityLivingBase target, + IVector3 spawnPos, EnergyDisplayData resolved, int index) { + EntityAbilityLaser laser = new EntityAbilityLaser( + caster.worldObj, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + laserWidth, + resolved, combatData, lightningData, lifespanData, + expansionSpeed, maxLength); + return laser; + } + + @Override + protected void fireEntity(EntityAbilityLaser laser, IEntityLivingBase target) { + laser.startMoving(target); + } + + @Override + protected void setupEntityCharging(EntityAbilityLaser laser, ProjectileData projData, int index) { + laser.setupCharging(projData.anchor, windUpTicks); + } + + @Override + protected void setupEntityPreview(EntityAbilityLaser laser, IEntityLivingBase caster, + EnergyDisplayData resolved, ProjectileData projData, int index) { + laser.setupPreview(caster, laserWidth, resolved, lightningData, projData.anchor, windUpTicks, expansionSpeed, maxLength); + } + + @Override + protected EntityAbilityLaser[] createEntityArray(int size) { + return new EntityAbilityLaser[size]; + } + + @Override + protected float getProjectileTelegraphRadius() { + return laserWidth * 2.0f; + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeTypeSpecificNBT(INbt nbt) { + nbt.setFloat("laserWidth", laserWidth); + nbt.setFloat("expansionSpeed", expansionSpeed); + nbt.setFloat("maxLength", maxLength); + } + + @Override + protected void readTypeSpecificNBT(INbt nbt) { + this.laserWidth = nbt.getFloat("laserWidth"); + this.expansionSpeed = nbt.getFloat("expansionSpeed"); + this.maxLength = nbt.hasKey("maxLength") ? nbt.getFloat("maxLength") : 32.0f; + } + + // ==================== TYPE-SPECIFIC GETTERS ==================== + + public float getLaserWidth() { + return laserWidth; + } + + public void setLaserWidth(float laserWidth) { + this.laserWidth = laserWidth; + } + + public float getExpansionSpeed() { + return expansionSpeed; + } + + public void setExpansionSpeed(float expansionSpeed) { + this.expansionSpeed = expansionSpeed; + } + + public float getMaxLength() { + return maxLength; + } + + public void setMaxLength(float maxLength) { + this.maxLength = maxLength; + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addTypeDefinitions(List defs) { + defs.add(FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp) + )); + defs.add(FieldDef.section("ability.section.beam")); + defs.add(FieldDef.row( + FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth).range(0.1f, 100.0f), + FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed).range(0.1f, 50.0f) + )); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxLength", this::getMaxLength, this::setMaxLength).range(1.0f, 500.0f), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200) + )); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java new file mode 100644 index 000000000..8649c5c5d --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java @@ -0,0 +1,218 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.ProjectileData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilityOrb; +import java.util.Arrays; +import java.util.List; + +/** + * Orb ability: Spawns homing projectile sphere(s) that track target. + * Supports 1-8 projectiles with shared visuals and per-projectile anchor points. + */ +public class AbilityOrb extends AbilityEnergyProjectile implements IAbilityOrb { + + private float orbSize = 1.0f; + + public AbilityOrb() { + super( + new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 10.0f), + new EnergyCombatData(), + new EnergyHomingData(), + new EnergyLifespanData() + ); + this.typeId = "ability.cnpc.orb"; + this.name = "Orb"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 25.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 30; + this.lockMovement = LockMode.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + this.windUpAnimationName = "Ability_Orb_Windup"; + this.activeAnimationName = "Ability_Orb_Active"; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/orb.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityAbilityOrb createEntity(IEntityLivingBase caster, IEntityLivingBase target, + IVector3 spawnPos, EnergyDisplayData resolved, int index) { + return new EntityAbilityOrb( + caster.worldObj, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, + resolved, combatData, homingData, lightningData, lifespanData); + } + + @Override + protected void fireEntity(EntityAbilityOrb orb, IEntityLivingBase target) { + if (isPreview()) { + orb.startPreviewFiring(); + } else { + orb.startMoving(target); + } + } + + @Override + protected void setupEntityCharging(EntityAbilityOrb orb, ProjectileData projData, int index) { + orb.setupCharging(projData.anchor, windUpTicks); + } + + @Override + protected void setupEntityPreview(EntityAbilityOrb orb, IEntityLivingBase caster, + EnergyDisplayData resolved, ProjectileData projData, int index) { + orb.setupPreview(caster, orbSize, resolved, lightningData, projData.anchor, windUpTicks); + } + + @Override + protected EntityAbilityOrb[] createEntityArray(int size) { + return new EntityAbilityOrb[size]; + } + + @Override + protected float getProjectileTelegraphRadius() { + return orbSize * 1.5f; + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.single", a -> { + a.setName("Orb"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilityOrb orb = (AbilityOrb) a; + a.setName("Dual Orb"); + orb.setProjectileCount(2); + orb.setFireDelay(5); + a.setWindUpAnimationName("Ability_OrbDual_Windup"); + a.setActiveAnimationName("Ability_OrbDual_Active"); + }), + new AbilityVariant("ability.variant.barrage", a -> { + AbilityOrb orb = (AbilityOrb) a; + a.setName("Orb Barrage"); + orb.setOrbSize(0.5f); + orb.setDamage(4.0f); + orb.setKnockback(0.0f); + orb.setKnockbackUp(0.0f); + orb.setProjectileCount(2); + orb.setFireDelay(2); + a.setLockMovement(LockMode.WINDUP_AND_ACTIVE); + a.setMaxRange(75.0f); + a.setBurstEnabled(true); + a.setBurstAmount(15); + a.setBurstDelay(5); + a.setBurstReplayAnimations(false); + a.setBurstOverlap(true); + a.setWindUpAnimationName("Ability_OrbBarrage_Windup"); + a.setActiveAnimationName("Ability_OrbBarrage_Active"); + }) + ); + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeTypeSpecificNBT(INbt nbt) { + nbt.setFloat("orbSize", orbSize); + } + + @Override + protected void readTypeSpecificNBT(INbt nbt) { + this.orbSize = nbt.getFloat("orbSize"); + } + + // ==================== TYPE-SPECIFIC GETTERS ==================== + + public float getOrbSpeed() { + return homingData.speed; + } + + public void setOrbSpeed(float speed) { + homingData.speed = speed; + } + + public float getOrbSize() { + return orbSize; + } + + public void setOrbSize(float size) { + this.orbSize = size; + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addTypeDefinitions(List defs) { + defs.add(FieldDef.row( + FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES), + FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay) + .range(0, 200).visibleWhen(() -> projectileCount > 1) + )); + defs.add(FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed) + )); + defs.add(FieldDef.row( + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize).range(0.1f, 100.0f), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback) + )); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200) + )); + defs.add(FieldDef.section("ability.section.homing")); + defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing")); + defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming)); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java new file mode 100644 index 000000000..2803ed598 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java @@ -0,0 +1,178 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyPanelData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import java.util.Arrays; +import java.util.List; + +/** + * Energy Shield ability: Spawns a flat energy panel held in front of the caster. + * The shield follows the caster's position and rotates with their look direction. + * Cannot be placed — it's always held. + */ +public class AbilityShield extends AbilityBarrier { + + private float shieldWidth = 2.5f; + private float shieldHeight = 2.5f; + + public AbilityShield() { + super( + new EnergyDisplayData(0xFFDD44, 0xFFAA00, true, 0.3f, 0.4f, 0.5f, 0.0f), + new EnergyBarrierData(60.0f, true, 120, true) + ); + this.typeId = "ability.cnpc.shield"; + this.name = "Shield"; + this.targetingMode = TargetingMode.SELF; + this.cooldownTicks = 60; + this.windUpTicks = 10; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + this.windUpAnimationName = ""; + this.activeAnimationName = ""; + this.allowedBy = UserType.BOTH; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/shield.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target) { + EnergyPanelData panelData = new EnergyPanelData(shieldWidth, shieldHeight, offsetY); + panelData.offsetX = offsetX; + panelData.offsetZ = offsetZ; + + float frontDist = 1.5f; + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double spawnX = caster.posX + (-Math.sin(yawRad) * frontDist) + offsetX; + double spawnY = caster.posY + (caster.height * 0.5f); + double spawnZ = caster.posZ + (Math.cos(yawRad) * frontDist) + offsetZ; + + EntityEnergyPanel panel = new EntityEnergyPanel( + caster.worldObj, caster, + spawnX, spawnY, spawnZ, caster.rotationYaw, + EntityEnergyPanel.PanelMode.HELD, + displayData.copy(), lightningData.copy(), barrierData.copy(), panelData + ); + panel.setSourceAbility(this); + return panel; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.SELF}; + } + + @Override + public float getTelegraphRadius() { + return 0; // No telegraph for shield + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.small", a -> { + AbilityShield shield = (AbilityShield) a; + a.setName("Small Shield"); + shield.setShieldWidth(1.5f); + shield.setShieldHeight(1.5f); + shield.setBarrierMaxHealth(40.0f); + }), + new AbilityVariant("ability.variant.large", a -> { + AbilityShield shield = (AbilityShield) a; + a.setName("Large Shield"); + shield.setShieldWidth(4.0f); + shield.setShieldHeight(4.0f); + shield.setBarrierMaxHealth(120.0f); + shield.setBarrierDuration(200); + }) + ); + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeBarrierTypeNBT(INbt nbt) { + nbt.setFloat("shieldWidth", shieldWidth); + nbt.setFloat("shieldHeight", shieldHeight); + } + + @Override + protected void readBarrierTypeNBT(INbt nbt) { + this.shieldWidth = nbt.hasKey("shieldWidth") ? nbt.getFloat("shieldWidth") : 2.5f; + this.shieldHeight = nbt.hasKey("shieldHeight") ? nbt.getFloat("shieldHeight") : 2.5f; + } + + // ==================== GETTERS & SETTERS ==================== + + public float getShieldWidth() { + return shieldWidth; + } + + public void setShieldWidth(float width) { + this.shieldWidth = Math.max(0.5f, width); + } + + public float getShieldHeight() { + return shieldHeight; + } + + public void setShieldHeight(float height) { + this.shieldHeight = Math.max(0.5f, height); + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addBarrierTypeDefinitions(List defs) { + defs.add(FieldDef.row( + FieldDef.floatField("gui.width", this::getShieldWidth, this::setShieldWidth).range(0.5f, 100.0f), + FieldDef.floatField("gui.height", this::getShieldHeight, this::setShieldHeight).range(0.5f, 100.0f) + )); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java new file mode 100644 index 000000000..c613a9be8 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java @@ -0,0 +1,207 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.entity.EntityEnergySlicer; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.ProjectileData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import java.util.Arrays; +import java.util.List; + +/** + * Energy Slicer ability: Fires a thin, wide blade projectile in a straight line. + * JJK Cleave/Dismantle inspired. + * Extends AbilityEnergyProjectile to leverage the existing projectile infrastructure. + */ +public class AbilitySlicer extends AbilityEnergyProjectile { + + private float sliceWidth = 3.0f; + private float sliceThickness = 0.15f; + + public AbilitySlicer() { + super( + new EnergyDisplayData(0xFF4444, 0xFF0000, true, 0.2f, 0.6f, 0.0f), + new EnergyCombatData(12.0f, 0.5f, 0.0f, false, 3.0f, 0.5f), + new EnergyHomingData(0.8f, false, 0.0f, 0.0f), + new EnergyLifespanData(150.0f, 100) + ); + this.typeId = "ability.cnpc.slicer"; + this.name = "Slicer"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 3.0f; + this.cooldownTicks = 40; + this.windUpTicks = 15; + this.lockMovement = LockMode.WINDUP; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + this.windUpAnimationName = ""; + this.activeAnimationName = ""; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityEnergySlicer createEntity(IEntityLivingBase caster, IEntityLivingBase target, + IVector3 spawnPos, EnergyDisplayData resolved, int index) { + return new EntityEnergySlicer( + caster.worldObj, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + sliceWidth, sliceThickness, + resolved, combatData, homingData, lightningData, lifespanData + ); + } + + @Override + protected void fireEntity(EntityEnergySlicer slicer, IEntityLivingBase target) { + if (isPreview()) { + slicer.startPreviewFiring(); + } else { + slicer.startMoving(target); + } + } + + @Override + protected void setupEntityCharging(EntityEnergySlicer slicer, ProjectileData projData, int index) { + slicer.setupSlicerCharging(projData.anchor, windUpTicks, sliceWidth, sliceThickness); + } + + @Override + protected void setupEntityPreview(EntityEnergySlicer slicer, IEntityLivingBase caster, + EnergyDisplayData resolved, ProjectileData projData, int index) { + slicer.setupPreview(caster, sliceWidth, sliceThickness, resolved, lightningData, projData.anchor, windUpTicks); + } + + @Override + protected EntityEnergySlicer[] createEntityArray(int size) { + return new EntityEnergySlicer[size]; + } + + @Override + protected float getProjectileTelegraphRadius() { + return sliceWidth * 0.75f; + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.single", a -> { + a.setName("Slicer"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilitySlicer slicer = (AbilitySlicer) a; + a.setName("Double Slicer"); + slicer.setProjectileCount(2); + slicer.setFireDelay(5); + a.setWindUpAnimationName(""); + a.setActiveAnimationName(""); + }) + ); + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeTypeSpecificNBT(INbt nbt) { + nbt.setFloat("sliceWidth", sliceWidth); + nbt.setFloat("sliceThickness", sliceThickness); + } + + @Override + protected void readTypeSpecificNBT(INbt nbt) { + this.sliceWidth = nbt.hasKey("sliceWidth") ? nbt.getFloat("sliceWidth") : 3.0f; + this.sliceThickness = nbt.hasKey("sliceThickness") ? nbt.getFloat("sliceThickness") : 0.15f; + } + + // ==================== TYPE-SPECIFIC GETTERS ==================== + + public float getSlicerSpeed() { + return homingData.speed; + } + + public void setSlicerSpeed(float speed) { + homingData.speed = speed; + } + + public float getSliceWidth() { + return sliceWidth; + } + + public void setSliceWidth(float width) { + this.sliceWidth = Math.max(0.5f, width); + } + + public float getSliceThickness() { + return sliceThickness; + } + + public void setSliceThickness(float thickness) { + this.sliceThickness = Math.max(0.05f, thickness); + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addTypeDefinitions(List defs) { + defs.add(FieldDef.row( + FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES), + FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay) + .range(0, 200).visibleWhen(() -> projectileCount > 1) + )); + defs.add(FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSlicerSpeed, this::setSlicerSpeed) + )); + defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)); + defs.add(FieldDef.section("ability.section.slicer")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.width", this::getSliceWidth, this::setSliceWidth).range(0.5f, 100.0f), + FieldDef.floatField("gui.thickness", this::getSliceThickness, this::setSliceThickness).range(0.05f, 100.0f) + )); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200) + )); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java new file mode 100644 index 000000000..4242f6f63 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java @@ -0,0 +1,336 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.entity.EntityEnergySweeper; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.api.ability.type.IAbilitySweeper; +import java.util.Arrays; +import java.util.List; + +/** + * Sweeper ability: A low sweeping beam that rotates around the NPC. + * Players can jump over it. The beam completes a set number of rotations before ending. + * Telegraph shows a large circle on the ground indicating range. + *

+ * All damage logic is handled by the EntityEnergySweeper IEntity to ensure + * visual and damage hitbox alignment. + */ +public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper { + + // Type-specific parameters + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 1.0f; // Beam vertical size in blocks (1 block tall, jumpable) + private float damage = 5.0f; + private int damageInterval = 5; + private boolean piercing = true; + private float sweepSpeed = 3.0f; + private int numberOfRotations = 2; // How many full rotations before ability ends + private boolean lockOnTarget = false; + + // Runtime state (transient) + private transient EntityEnergySweeper activeEntity = null; + + public AbilitySweeper() { + super(new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.8f, 0.5f, 0.0f)); + this.typeId = "ability.cnpc.sweeper"; + this.name = "Sweeper"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 15.0f; + this.minRange = 0.0f; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; + this.windUpTicks = 60; + // Circle telegraph to show range + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/sweeper.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/sweeper_overlay.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + @Override + public boolean allowBurst() { + return false; + } + + @Override + public boolean isConcurrentCapable() { + return true; + } + + @Override + public boolean allowFreeOnCast() { + return true; + } + + @Override + public void detach() { + activeEntity = null; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) { + // Spawn the sweeper IEntity that handles BOTH visuals AND damage. + // NPC: target is the aggro target — lockOnTarget tracks it during sweep. + // Player: target is null — sweep rotates around caster's facing direction, lockOnTarget has no effect. + activeEntity = new EntityEnergySweeper(caster.worldObj, caster, target, + beamLength, beamWidth, beamHeight, + displayData, + sweepSpeed, numberOfRotations, + damage, damageInterval, piercing, + lockOnTarget); + activeEntity.setIgnoreIFrames(this.isIgnoreIFrames()); + activeEntity.setSourceAbility(this); + + if (isPreview()) { + activeEntity.setupPreview(caster); + } + + spawnAbilityEntity(activeEntity); + } + + @Override + public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) { + // Signal completion when IEntity dies + if (activeEntity == null || activeEntity.isDead) { + activeEntity = null; + signalCompletion(); + return; + } + + // Free on Cast: complete immediately — sweeper lives independently + if (isFreeOnCast()) { + signalCompletion(); + } + } + + @Override + public void onInterrupt(IEntityLivingBase caster, noppes.npcs.api.IDamageSource source, float damage) { + cleanup(); + } + + @Override + public void cleanup() { + // Clean up IEntity if still alive + if (activeEntity != null && !activeEntity.isDead) { + activeEntity.setDead(); + } + activeEntity = null; + } + + @Override + public float getTelegraphRadius() { + return beamLength; + } + + /** + * Override to position telegraph at NPC (not target) since the sweep is centered on NPC. + */ + @Override + public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE) { + return null; + } + + // Create circle telegraph centered on caster + Telegraph telegraph = Telegraph.circle(beamLength); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at caster ground level, not target + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); // Follow caster + + return instance; + } + + @Override + public void writeTypeNBT(INbt nbt) { + nbt.setFloat("beamLength", beamLength); + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("beamHeight", beamHeight); + nbt.setFloat("damage", damage); + nbt.setInteger("damageInterval", damageInterval); + nbt.setBoolean("piercing", piercing); + nbt.setFloat("sweepSpeed", sweepSpeed); + nbt.setInteger("numberOfRotations", numberOfRotations); + nbt.setBoolean("lockOnTarget", lockOnTarget); + writeEnergyNBT(nbt); + } + + @Override + public void readTypeNBT(INbt nbt) { + this.beamLength = nbt.getFloat("beamLength"); + this.beamWidth = nbt.getFloat("beamWidth"); + this.beamHeight = nbt.getFloat("beamHeight"); + this.damage = nbt.getFloat("damage"); + this.damageInterval = nbt.getInteger("damageInterval"); + this.piercing = nbt.getBoolean("piercing"); + this.sweepSpeed = nbt.getFloat("sweepSpeed"); + this.numberOfRotations = nbt.getInteger("numberOfRotations"); + this.lockOnTarget = nbt.getBoolean("lockOnTarget"); + readEnergyNBT(nbt); + } + + // Getters & Setters (type-specific only; color/lightning inherited from AbilityEnergy) + public float getBeamLength() { + return beamLength; + } + + public void setBeamLength(float beamLength) { + this.beamLength = beamLength; + } + + public float getBeamWidth() { + return beamWidth; + } + + public void setBeamWidth(float beamWidth) { + this.beamWidth = beamWidth; + } + + public float getBeamHeight() { + return beamHeight; + } + + public void setBeamHeight(float beamHeight) { + this.beamHeight = beamHeight; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getDisplayDamage() { return damage; } + + public int getDamageInterval() { + return damageInterval; + } + + public void setDamageInterval(int damageInterval) { + this.damageInterval = damageInterval; + } + + public boolean isPiercing() { + return piercing; + } + + public void setPiercing(boolean piercing) { + this.piercing = piercing; + } + + public float getSweepSpeed() { + return sweepSpeed; + } + + public void setSweepSpeed(float sweepSpeed) { + this.sweepSpeed = sweepSpeed; + } + + public int getNumberOfRotations() { + return numberOfRotations; + } + + public void setNumberOfRotations(int numberOfRotations) { + this.numberOfRotations = numberOfRotations; + } + + public boolean isLockOnTarget() { + return lockOnTarget; + } + + public void setLockOnTarget(boolean lockOnTarget) { + this.lockOnTarget = lockOnTarget; + } + + @Override + public int getMaxPreviewDuration() { + return (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10; + } + + @ClientOnly + @Override + public void getAbilityDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval) + ), + FieldDef.section("ability.section.beam"), + FieldDef.row( + FieldDef.floatField("gui.length", this::getBeamLength, this::setBeamLength).range(0.5f, 100.0f), + FieldDef.floatField("gui.width", this::getBeamWidth, this::setBeamWidth).range(0.1f, 100.0f) + ), + FieldDef.row( + FieldDef.floatField("gui.height", this::getBeamHeight, this::setBeamHeight).range(0.1f, 100.0f), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed).range(0.1f, 30.0f) + ), + FieldDef.intField("ability.rotations", this::getNumberOfRotations, this::setNumberOfRotations).range(1, 20), + FieldDef.row( + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing"), + FieldDef.boolField("ability.lockTarget", this::isLockOnTarget, this::setLockOnTarget) + .hover("ability.hover.lockTarget") + ), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + + // Visual tab - colors + effects (from AbilityEnergy) + addEnergyVisualDefinitions(defs); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java new file mode 100644 index 000000000..0411581f3 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java @@ -0,0 +1,255 @@ +package kamkeel.npcs.controllers.data.ability.type.energy; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyPanelData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import java.util.Arrays; +import java.util.List; + +/** + * Energy Wall ability: Spawns a placeable flat energy barrier. + * Can float above ground (configurable height offset). + * Optional launching mode fires the wall forward with damage/knockback. + */ +public class AbilityWall extends AbilityBarrier { + + private final EnergyPanelData panelData; + + public AbilityWall() { + super( + new EnergyDisplayData(0x44FFCC, 0x22CCAA, true, 0.3f, 0.4f, 0.5f, 0.0f), + new EnergyBarrierData(80.0f, true, 160, true) + ); + this.panelData = new EnergyPanelData(4.0f, 3.0f, 0.0f); + this.typeId = "ability.cnpc.wall"; + this.name = "Wall"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 15.0f; + this.minRange = 2.0f; + this.cooldownTicks = 80; + this.windUpTicks = 20; + this.lockMovement = LockMode.WINDUP; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + this.windUpAnimationName = ""; + this.activeAnimationName = ""; + + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/wall.png"), + new DefaultIconLayer("customnpcs:textures/gui/ability/wall_overlay.png", + () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) + }; + } + + @Override + public boolean hasDamage() { + return panelData.launching; // Only has damage when launching + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target) { + // Place wall between caster and target + double placeX, placeY, placeZ; + float yaw; + + if (target != null) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + double dist = Math.sqrt(dx * dx + dz * dz); + + if (dist < 0.01) { + // Target at same position — fall through to look-direction placement + float yawRad = (float) Math.toRadians(caster.rotationYaw); + placeX = caster.posX + (-Math.sin(yawRad) * 3.0); + placeY = caster.posY; + placeZ = caster.posZ + (Math.cos(yawRad) * 3.0); + yaw = caster.rotationYaw; + } else { + double placeDist = Math.min(dist * 0.5, 5.0); + placeX = caster.posX + (dx / dist) * placeDist; + placeY = caster.posY; + placeZ = caster.posZ + (dz / dist) * placeDist; + yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); + } + } else { + float yawRad = (float) Math.toRadians(caster.rotationYaw); + placeX = caster.posX + (-Math.sin(yawRad) * 3.0); + placeY = caster.posY; + placeZ = caster.posZ + (Math.cos(yawRad) * 3.0); + yaw = caster.rotationYaw; + } + + placeX += offsetX; + placeY += offsetY; + placeZ += offsetZ; + + EntityEnergyPanel.PanelMode mode = panelData.launching + ? EntityEnergyPanel.PanelMode.LAUNCHED + : EntityEnergyPanel.PanelMode.PLACED; + + EnergyPanelData data = panelData.copy(); + data.heightOffset = 0.0f; // Y offset now handled by base class via placeY + + EntityEnergyPanel panel = new EntityEnergyPanel( + caster.worldObj, caster, + placeX, placeY, placeZ, yaw, mode, + displayData.copy(), lightningData.copy(), barrierData.copy(), data + ); + panel.setSourceAbility(this); + return panel; + } + + @Override + public float getTelegraphRadius() { + return Math.max(panelData.panelWidth, panelData.panelHeight) * 0.5f; + } + + // ==================== VARIANTS ==================== + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.static", a -> { + a.setName("Static Wall"); + }), + new AbilityVariant("ability.variant.launched", a -> { + AbilityWall wall = (AbilityWall) a; + a.setName("Launched Wall"); + wall.panelData.launching = true; + wall.panelData.launchSpeed = 0.6f; + wall.panelData.launchDamage = 10.0f; + wall.panelData.launchKnockback = 3.0f; + }) + ); + } + + // ==================== TYPE-SPECIFIC NBT ==================== + + @Override + protected void writeBarrierTypeNBT(INbt nbt) { + panelData.writeNBT(new NBTWrapper(nbt)); + } + + @Override + protected void readBarrierTypeNBT(INbt nbt) { + panelData.readNBT(new NBTWrapper(nbt)); + // Migrate legacy heightOffset → base class offsetY + if (!nbt.hasKey("barrierOffsetY") && panelData.heightOffset != 0.0f) { + offsetY = panelData.heightOffset; + panelData.heightOffset = 0.0f; + } + } + + // ==================== GETTERS & SETTERS ==================== + + public EnergyPanelData getPanelData() { + return panelData; + } + + public float getPanelWidth() { + return panelData.panelWidth; + } + + public void setPanelWidth(float width) { + panelData.setPanelWidth(width); + } + + public float getPanelHeight() { + return panelData.panelHeight; + } + + public void setPanelHeight(float height) { + panelData.setPanelHeight(height); + } + + public boolean isLaunching() { + return panelData.launching; + } + + public void setLaunching(boolean launching) { + panelData.launching = launching; + } + + public float getLaunchSpeed() { + return panelData.launchSpeed; + } + + public void setLaunchSpeed(float speed) { + panelData.setLaunchSpeed(speed); + } + + public float getLaunchDamage() { + return panelData.launchDamage; + } + + public void setLaunchDamage(float damage) { + panelData.setLaunchDamage(damage); + } + + @Override + public float getDisplayDamage() { return panelData.launching ? panelData.launchDamage : 0; } + + public float getLaunchKnockback() { + return panelData.launchKnockback; + } + + public void setLaunchKnockback(float kb) { + panelData.setLaunchKnockback(kb); + } + + // ==================== TYPE-SPECIFIC GUI ==================== + + @ClientOnly + @Override + protected void addBarrierTypeDefinitions(List defs) { + defs.add(FieldDef.section("ability.section.panel")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.width", this::getPanelWidth, this::setPanelWidth).range(0.5f, 100.0f), + FieldDef.floatField("gui.height", this::getPanelHeight, this::setPanelHeight).range(0.5f, 100.0f) + )); + defs.add(FieldDef.section("ability.section.launch")); + defs.add(FieldDef.boolField("gui.enabled", this::isLaunching, this::setLaunching) + .hover("ability.hover.launching")); + defs.add(FieldDef.row( + FieldDef.floatField("stats.speed", this::getLaunchSpeed, this::setLaunchSpeed) + .visibleWhen(this::isLaunching), + FieldDef.floatField("enchantment.damage", this::getLaunchDamage, this::setLaunchDamage) + .visibleWhen(this::isLaunching) + )); + defs.add(FieldDef.floatField("ability.knockback", this::getLaunchKnockback, this::setLaunchKnockback) + .visibleWhen(this::isLaunching)); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/ability/util/AbilityTargetHelper.java b/core/src/main/java/kamkeel/npcs/controllers/data/ability/util/AbilityTargetHelper.java new file mode 100644 index 000000000..ab0dbafc8 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/ability/util/AbilityTargetHelper.java @@ -0,0 +1,136 @@ +package kamkeel.npcs.controllers.data.ability.util; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +/** + * Utility class for determining friend/foe relationships between entities. + * Extracted from EntityEnergyProjectile.shouldIgnoreEntity() for reuse + * across all AoE abilities and AbilityEffect targeting. + */ +public class AbilityTargetHelper { + + /** + * Determines if target is an ally of caster. + *

    + *
  • NPC vs NPC: same faction ID = ally
  • + *
  • NPC vs Player: faction friendly to player = ally
  • + *
  • Player vs NPC: NPC's faction friendly to player = ally
  • + *
  • Player vs Player: same party (if exists) = ally
  • + *
  • Passive NPCs are always considered allies (never enemies)
  • + *
+ */ + public static boolean isAlly(IEntityLivingBase caster, IEntity target) { + if (caster == null || target == null) return false; + if (target == caster) return true; + // Ally resolution relies on server-owned player/party/faction state. + if (caster.worldObj == null || caster.worldObj.isRemote) return false; + + // NPC target checks + if (target instanceof EntityNPCInterface) { + EntityNPCInterface targetNpc = (EntityNPCInterface) target; + + // Passive NPCs are always considered allies + if (targetNpc.faction.isPassive) return true; + + // NPC caster: same faction = ally + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface casterNpc = (EntityNPCInterface) caster; + if (casterNpc.faction.id == targetNpc.faction.id) return true; + } + + // Player caster: friendly faction = ally + if (caster instanceof IPlayer) { + IPlayer casterPlayer = (IPlayer) caster; + if (PlayerData.get(casterPlayer) == null) return false; + if (targetNpc.faction.isFriendlyToPlayer(casterPlayer)) return true; + } + + return false; + } + + // Player target checks + if (target instanceof IPlayer && caster instanceof EntityNPCInterface) { + // NPC caster vs Player target: faction friendly to player = ally + EntityNPCInterface casterNpc = (EntityNPCInterface) caster; + IPlayer targetPlayer = (IPlayer) target; + if (PlayerData.get(targetPlayer) == null) return false; + return casterNpc.faction.isFriendlyToPlayer(targetPlayer); + } + + if (target instanceof IPlayer && caster instanceof IPlayer) { + // Player vs Player: same party = ally + IPlayer casterPlayer = (IPlayer) caster; + IPlayer targetPlayer = (IPlayer) target; + PlayerData casterData = PlayerData.get(casterPlayer); + PlayerData targetData = PlayerData.get(targetPlayer); + if (casterData == null || targetData == null) return false; + if (casterData.partyUUID != null && casterData.partyUUID.equals(targetData.partyUUID)) { + Party party = PartyController.Instance().getParty(casterData.partyUUID); + if (party != null && !party.friendlyFire()) return true; + } + return false; + } + + // Unknown IEntity types: not an ally + return false; + } + + /** + * Determines if a target should be affected by an ability, given the + * filter mode and includeSelf setting. + * + * @param caster The IEntity using the ability + * @param target The potential target IEntity + * @param filter ALLIES, ENEMIES, or ALL + * @param includeSelf Whether to include the caster themselves + * @return true if the target should be affected + */ + public static boolean shouldAffect(IEntityLivingBase caster, IEntity target, + TargetFilter filter, boolean includeSelf) { + if (caster == null || target == null || filter == null) return false; + if (caster.worldObj == null || caster.worldObj.isRemote) return false; + if (target == caster) return includeSelf; + if (!(target instanceof IEntityLivingBase)) return false; + if (!target.isEntityAlive()) return false; + + switch (filter) { + case ALLIES: + return isAlly(caster, target); + case ENEMIES: + return !isAlly(caster, target); + case ALL: + return true; + default: + return false; + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/PlayerAttributeMap.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/PlayerAttributeMap.java index 8eec9414b..5507fc4fc 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/PlayerAttributeMap.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/PlayerAttributeMap.java @@ -1,10 +1,20 @@ package kamkeel.npcs.controllers.data.attribute; + import java.util.HashMap; import java.util.Map; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; /** - * CustomAttributeMap holds a set of attribute instances for an entity (or item). + * CustomAttributeMap holds a set of attribute instances for an IEntity (or item). */ public class PlayerAttributeMap { public final Map map = new HashMap<>(); diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/IRequirementChecker.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/IRequirementChecker.java new file mode 100644 index 000000000..fd0f97af4 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/IRequirementChecker.java @@ -0,0 +1,31 @@ +package kamkeel.npcs.controllers.data.attribute.requirement; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +public interface IRequirementChecker { + + String getKey(); + + String getTranslation(); + + String getTooltipValue(INbt nbt); + + Object getValue(INbt nbt); + + void apply(INbt nbt, Object value); + + /** + * Checks the requirement in the provided NBT. + * Return false if the requirement isn’t met. + */ + boolean check(IPlayer player, INbt nbt); +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/RequirementCheckerRegistry.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/RequirementCheckerRegistry.java new file mode 100644 index 000000000..d599777d7 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/RequirementCheckerRegistry.java @@ -0,0 +1,48 @@ +package kamkeel.npcs.controllers.data.attribute.requirement; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class RequirementCheckerRegistry { + private static final Map checkers = new HashMap<>(); + + public static void registerChecker(IRequirementChecker checker) { + if (!checkers.containsKey(checker.getKey())) { + checkers.put(checker.getKey(), checker); + } + } + + /** + * Loops over all registered requirement keys. + * For each key present in the NBT, its checker must return true. + */ + public static boolean checkRequirements(IPlayer player, INbt nbt) { + for (Map.Entry entry : checkers.entrySet()) { + String reqKey = entry.getKey(); + if (nbt.hasKey(reqKey)) { + if (!entry.getValue().check(player, nbt)) { + return false; + } + } + } + return true; + } + + public static Set getAllKeys() { + return checkers.keySet(); + } + + public static IRequirementChecker getChecker(String key) { + return checkers.get(key); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/ProfileSlotRequirement.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/ProfileSlotRequirement.java new file mode 100644 index 000000000..63250a472 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/ProfileSlotRequirement.java @@ -0,0 +1,59 @@ +package kamkeel.npcs.controllers.data.attribute.requirement.types; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; +import noppes.npcs.controllers.data.PlayerData; + +public class ProfileSlotRequirement implements IRequirementChecker { + + @Override + public String getKey() { + return "cnpc_profile_slot"; + } + + @Override + public String getTranslation() { + return "profile.slot"; + } + + @Override + public String getTooltipValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + int profileSlot = nbt.getInteger(getKey()); + return String.valueOf(profileSlot); + } + return "null"; + } + + @Override + public void apply(INbt nbt, Object value) { + if (value instanceof Integer) { + nbt.setInteger(getKey(), (Integer) value); + } + } + + @Override + public Object getValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + return nbt.getInteger(getKey()); + } + return null; + } + + @Override + public boolean check(IPlayer player, INbt nbt) { + if (nbt.hasKey(getKey())) { + int profileSlot = nbt.getInteger(getKey()); + return PlayerData.get(player).profileSlot == profileSlot; + } + return true; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestActiveRequirement.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestActiveRequirement.java new file mode 100644 index 000000000..8b5455285 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestActiveRequirement.java @@ -0,0 +1,62 @@ +package kamkeel.npcs.controllers.data.attribute.requirement.types; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.data.PlayerData; + +public class QuestActiveRequirement implements IRequirementChecker { + + @Override + public String getKey() { + return "cnpc_quest_active"; + } + + @Override + public String getTranslation() { + return "quest.active"; + } + + @Override + public String getTooltipValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + int questID = nbt.getInteger(getKey()); + if (QuestController.Instance.get(questID) != null) { + return QuestController.Instance.get(questID).getName(); + } + } + return "null"; + } + + @Override + public Object getValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + return nbt.getInteger(getKey()); + } + return null; + } + + @Override + public void apply(INbt nbt, Object value) { + if (value instanceof Integer) { + nbt.setInteger(getKey(), (Integer) value); + } + } + + @Override + public boolean check(IPlayer player, INbt nbt) { + if (nbt.hasKey(getKey())) { + int questID = nbt.getInteger(getKey()); + return PlayerData.get(player).questData.hasActiveQuest(questID); + } + return true; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestCompletedRequirement.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestCompletedRequirement.java new file mode 100644 index 000000000..8db583856 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/QuestCompletedRequirement.java @@ -0,0 +1,62 @@ +package kamkeel.npcs.controllers.data.attribute.requirement.types; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.data.PlayerData; + +public class QuestCompletedRequirement implements IRequirementChecker { + + @Override + public String getKey() { + return "cnpc_quest_completed"; + } + + @Override + public String getTranslation() { + return "quest.quest"; + } + + @Override + public String getTooltipValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + int questID = nbt.getInteger(getKey()); + if (QuestController.Instance.get(questID) != null) { + return QuestController.Instance.get(questID).getName(); + } + } + return "null"; + } + + @Override + public void apply(INbt nbt, Object value) { + if (value instanceof Integer) { + nbt.setInteger(getKey(), (Integer) value); + } + } + + @Override + public Object getValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + return nbt.getInteger(getKey()); + } + return null; + } + + @Override + public boolean check(IPlayer player, INbt nbt) { + if (nbt.hasKey(getKey())) { + int questID = nbt.getInteger(getKey()); + return PlayerData.get(player).questData.hasFinishedQuest(questID); + } + return true; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/SoulbindRequirement.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/SoulbindRequirement.java new file mode 100644 index 000000000..565b85473 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/requirement/types/SoulbindRequirement.java @@ -0,0 +1,132 @@ +package kamkeel.npcs.controllers.data.attribute.requirement.types; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.UUID; + +public class SoulbindRequirement implements IRequirementChecker { + + public static HashMap uuidToUsername = new HashMap<>(); + + @Override + public String getKey() { + return "cnpc_soulbind"; + } + + @Override + public String getTranslation() { + return "requirement.soulbind"; + } + + @Override + public String getTooltipValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + String uuid = nbt.getString(getKey()); + if (uuid != null && !uuid.isEmpty()) { + return getUsernameFromUUID(uuid); + } + } + return "null"; + } + + private String getUsernameFromUUID(String uuid) { + if (uuidToUsername.containsKey(uuid)) { + if (!uuidToUsername.get(uuid).equals("null")) { + return uuidToUsername.get(uuid); + } + } + try { + String strippedUUID = uuid.replace("-", ""); + String urlStr = "https://sessionserver.mojang.com/session/minecraft/profile/" + strippedUUID; + HttpURLConnection connection = (HttpURLConnection) new URL(urlStr).openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.setRequestMethod("GET"); + connection.connect(); + + InputStream stream = connection.getInputStream(); + InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); + + JsonElement element = new JsonParser().parse(reader); + JsonObject jsonObj = element.getAsJsonObject(); + + if (jsonObj.has("name")) { + String name = jsonObj.get("name").getAsString(); + uuidToUsername.put(uuid, name); + return name; + } + } catch (Exception e) { + e.printStackTrace(); + } + return "null"; + } + + + @Override + public Object getValue(INbt nbt) { + if (nbt.hasKey(getKey())) { + return nbt.getString(getKey()); + } + return null; + } + + @Override + public void apply(INbt nbt, Object value) { + if (value instanceof String) { + String entry = (String) value; + if (entry.isEmpty()) + return; + + // Check if the provided value is already a valid UUID + UUID uuid = null; + try { + uuid = UUID.fromString(entry); + } catch (Exception ignored) { + } + + // If not a UUID, attempt to treat the value as a username + if (uuid == null) { + uuid = ProfileController.Instance.getUUIDFromUsername(entry); + } + + if (uuid != null) { + nbt.setString(getKey(), uuid.toString()); + } + } + } + + @Override + public boolean check(IPlayer player, INbt nbt) { + if (nbt.hasKey(getKey())) { + String uuidString = nbt.getString(getKey()); + if (uuidString != null && !uuidString.isEmpty()) { + UUID convert = null; + try { + convert = UUID.fromString(uuidString); + } catch (Exception ignored) { + } + if (convert != null) + return player.getUniqueID().equals(convert); + } + return false; + } + return true; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/AttributeRecalcEvent.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/AttributeRecalcEvent.java new file mode 100644 index 000000000..1c22f9720 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/AttributeRecalcEvent.java @@ -0,0 +1,44 @@ +package kamkeel.npcs.controllers.data.attribute.tracker; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.ArrayList; +import java.util.List; + +public final class AttributeRecalcEvent { + @FunctionalInterface + public interface PreListener { + void onPre(IPlayer player, PlayerAttributeTracker tracker); + } + + private static final List preListeners = new ArrayList<>(); + private static final List postListeners = new ArrayList<>(); + + public static void pre(IPlayer player, PlayerAttributeTracker tracker) { + for (PreListener l : preListeners) l.onPre(player, tracker); + } + + @FunctionalInterface + public interface Listener { + void onPost(IPlayer player, PlayerAttributeTracker tracker); + } + + public static void registerListener(Listener l) { + postListeners.add(l); + } + + public static void post(IPlayer player, PlayerAttributeTracker tracker) { + for (Listener l : postListeners) l.onPost(player, tracker); + } + + public static void registerPreListener(PreListener l) { + preListeners.add(l); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/IAttributeRecalcListener.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/IAttributeRecalcListener.java new file mode 100644 index 000000000..ca27222ca --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/IAttributeRecalcListener.java @@ -0,0 +1,19 @@ +package kamkeel.npcs.controllers.data.attribute.tracker; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +/** + * Implement this interface to receive a callback after a player's attributes have been recalculated. + */ +public interface IAttributeRecalcListener { + void onAttributesRecalculated(IPlayer player, PlayerAttributeTracker tracker); +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerAttributeTracker.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerAttributeTracker.java new file mode 100644 index 000000000..bc2a4e938 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerAttributeTracker.java @@ -0,0 +1,266 @@ +package kamkeel.npcs.controllers.data.attribute.tracker; + + +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.Map; +import java.util.UUID; +import kamkeel.npcs.controllers.AttributeController; +import kamkeel.npcs.controllers.data.attribute.AttributeDefinition; +import kamkeel.npcs.controllers.data.attribute.PlayerAttribute; +import kamkeel.npcs.controllers.data.attribute.PlayerAttributeMap; +import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; +import kamkeel.npcs.CustomAttributes; +import kamkeel.npcs.util.AttributeAttackUtil; +import kamkeel.npcs.util.AttributeItemUtil; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.ICustomAttribute; +import noppes.npcs.api.handler.data.IPlayerAttributes; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +public class PlayerAttributeTracker implements IPlayerAttributes { + + public static UUID healthUUID = UUID.fromString("48a0ad75-2cf8-4838-ad2f-9a0aadc57dfe"); + public static UUID healthBoostUUID = UUID.fromString("93cfba41-294a-4e6b-a98b-bee76a9f813b"); + public static UUID movementSpeedUUID = UUID.fromString("e7e0dd10-8ed5-42fb-8167-e71f8de4ea0c"); + public static UUID knockbackResUUID = UUID.fromString("b2f261e8-34f0-4140-9828-b56c1f6e0ff2"); + + private final UUID playerId; + public PlayerAttributeMap playerAttributes = new PlayerAttributeMap(); + + public float extraHealth = 0; + public float extraHealthBoost = 0; + public float movementSpeed = 0; + public float knockbackRes = 0; + + public float gearOutput = 0.0f; + + public final Map magicDamage = new HashMap<>(); + public final Map magicBoost = new HashMap<>(); + public final Map magicDefense = new HashMap<>(); + public final Map magicResistance = new HashMap<>(); + + private PlayerEquipmentTracker equipmentTracker = new PlayerEquipmentTracker(); + + public PlayerAttributeTracker(UUID playerId) { + this.playerId = playerId; + // Initial recalc with base damage 0. + recalcAttributes(null); + } + + public void recalcAttributes(IPlayer player) { + // Reset aggregated attributes and magic maps. + playerAttributes = new PlayerAttributeMap(); + magicDamage.clear(); + magicBoost.clear(); + magicDefense.clear(); + magicResistance.clear(); + if (player != null) { + // Pre Event Listener + AttributeRecalcEvent.pre(player, this); + + // Update equipment tracker. + PlayerEquipmentTracker currentEquip = new PlayerEquipmentTracker(); + currentEquip.updateFrom(player); + IItemStack[] equipment = new IItemStack[]{currentEquip.heldItem, currentEquip.boots, currentEquip.leggings, currentEquip.chestplate, currentEquip.helmet}; + for (IItemStack piece : equipment) { + if (piece != null) { + if (piece.stackTagCompound != null && piece.stackTagCompound.hasKey(AttributeItemUtil.TAG_RPGCORE)) { + INbt rpgCore = piece.stackTagCompound.getCompoundTag(AttributeItemUtil.TAG_RPGCORE); + if (rpgCore.hasKey(AttributeItemUtil.TAG_REQUIREMENTS)) { + INbt reqTag = rpgCore.getCompoundTag(AttributeItemUtil.TAG_REQUIREMENTS); + if (!RequirementCheckerRegistry.checkRequirements(player, reqTag)) { + continue; + } + } + } + + for (Entry entry : AttributeItemUtil.readAttributes(piece).entrySet()) { + String key = entry.getKey(); + float value = entry.getValue(); + AttributeDefinition def = AttributeController.getAttribute(key); + if (def != null) { + PlayerAttribute inst = playerAttributes.getAttributeInstance(def); + if (inst == null) { + inst = playerAttributes.registerAttribute(def, 0.0f); + } + inst.value = (inst.getValue() + value); + } + } + // Process Magic + + // OFFENSIVE MAGIC + Map magicFlat = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_DAMAGE_KEY); + for (Entry entry : magicFlat.entrySet()) { + int magicId = entry.getKey(); + float value = entry.getValue(); + magicDamage.put(magicId, magicDamage.getOrDefault(magicId, 0.0f) + value); + } + Map magicPercent = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_BOOST_KEY); + for (Entry entry : magicPercent.entrySet()) { + int magicId = entry.getKey(); + float value = entry.getValue(); + magicBoost.put(magicId, magicBoost.getOrDefault(magicId, 0.0f) + value); + } + + // DEFENSIVE MAGIC + Map magicDefFlat = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_DEFENSE_KEY); + for (Entry entry : magicDefFlat.entrySet()) { + int magicId = entry.getKey(); + float value = entry.getValue(); + magicDefense.put(magicId, magicDefense.getOrDefault(magicId, 0.0f) + value); + } + Map magicResist = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_RESISTANCE_KEY); + for (Entry entry : magicResist.entrySet()) { + int magicId = entry.getKey(); + float value = entry.getValue(); + magicResistance.put(magicId, magicResistance.getOrDefault(magicId, 0.0f) + value); + } + } + } + equipmentTracker = currentEquip; + gearOutput = AttributeAttackUtil.calculateGearOutput(this); + + extraHealth = getAttributeValue(CustomAttributes.HEALTH); + extraHealthBoost = getAttributeValue(CustomAttributes.HEALTH_BOOST); + movementSpeed = getAttributeValue(CustomAttributes.MOVEMENT_SPEED); + knockbackRes = getAttributeValue(CustomAttributes.KNOCKBACK_RES); + + // Update vanilla attributes with our aggregated values. + updatePlayerMaxHealth(player); + updatePlayerMovementSpeed(player); + updatePlayerKnockbackRes(player); + + // Post Recalculation Event to Listeners + AttributeRecalcEvent.post(player, this); + } + } + + /** + * Updates the player's maximum health attribute by applying a custom attribute modifier. + * In this system, the aggregated HEALTH attribute is treated as bonus health on top of a base of 20. + * + * @param player The IPlayer whose max health should be updated. + */ + public void updatePlayerMaxHealth(IPlayer player) { + float bonusHealth = extraHealth; + float healthBoostPercent = extraHealthBoost / 100; + IAttributeInstance maxHealthAttr = player.getAttributeMap().getAttributeInstance(Attributes.MAX_HEALTH); + try { + AttributeModifier oldFlat = maxHealthAttr.getModifier(healthUUID); + if (oldFlat != null) { + maxHealthAttr.removeModifier(oldFlat); + } + AttributeModifier oldBoost = maxHealthAttr.getModifier(healthBoostUUID); + if (oldBoost != null) { + maxHealthAttr.removeModifier(oldBoost); + } + } catch (Exception ignored) { + } + // Apply the flat bonus (operation 0) + if (bonusHealth != 0) { + maxHealthAttr.applyModifier(new AttributeModifier(healthUUID, "RPGCoreHealthBonus", bonusHealth, 0)); + } + // Apply the percent boost (operation 1 multiplies the base health, here assumed to be 20) + if (healthBoostPercent != 0) { + maxHealthAttr.applyModifier(new AttributeModifier(healthBoostUUID, "RPGCoreHealthBoost", healthBoostPercent, 1)); + } + // Ensure current health is within the new maximum. + float currentHealth = player.getHealth(); + if (currentHealth > maxHealthAttr.getAttributeValue()) { + player.setHealth((float) maxHealthAttr.getAttributeValue()); + } + } + + public void updatePlayerMovementSpeed(IPlayer player) { + float extraSpeed = movementSpeed / 100; + IAttributeInstance speedAttr = player.getAttributeMap().getAttributeInstance(Attributes.MOVEMENT_SPEED); + try { + AttributeModifier old = speedAttr.getModifier(movementSpeedUUID); + if (old != null) { + speedAttr.removeModifier(old); + } + } catch (Exception ignored) { + } + if (extraSpeed != 0) { + speedAttr.applyModifier(new AttributeModifier(movementSpeedUUID, "RPGCoreMovementSpeed", extraSpeed, 1)); + } + } + + public void updatePlayerKnockbackRes(IPlayer player) { + float extraRes = knockbackRes / 100; + IAttributeInstance knockResAttr = player.getAttributeMap().getAttributeInstance(Attributes.KNOCKBACK_RESISTANCE); + try { + AttributeModifier old = knockResAttr.getModifier(knockbackResUUID); + if (old != null) { + knockResAttr.removeModifier(old); + } + } catch (Exception ignored) { + } + if (extraRes != 0) { + knockResAttr.applyModifier(new AttributeModifier(knockbackResUUID, "RPGCoreKnockbackRes", extraRes, 1)); + } + } + + /** + * Checks if the player's equipment has changed and recalculates attributes if needed. + */ + public void updateIfChanged(IPlayer player) { + PlayerEquipmentTracker current = new PlayerEquipmentTracker(); + current.updateFrom(player); + if (!equipmentTracker.equals(current)) { + recalcAttributes(player); + equipmentTracker = current; + } + } + + public float getAttributeValue(AttributeDefinition def) { + ICustomAttribute inst = playerAttributes.getAttributeInstance(def); + return inst != null ? inst.getValue() : 0.0f; + } + + public void recalculate(IPlayer player) { + if (player == null || player.getMCEntity() == null) + return; + + recalcAttributes((IPlayer) player.getMCEntity()); + } + + public ICustomAttribute[] getAttributes() { + return playerAttributes.map.values().toArray(new ICustomAttribute[playerAttributes.map.size()]); + } + + public float getAttributeValue(String key) { + for (ICustomAttribute inst : getAttributes()) { + if (inst.getAttribute().getKey().equalsIgnoreCase(key)) { + return inst.getValue(); + } + } + return 0.0f; + } + + public boolean hasAttribute(String key) { + for (ICustomAttribute inst : getAttributes()) { + if (inst.getAttribute().getKey().equalsIgnoreCase(key)) { + return true; + } + } + return false; + } + + public ICustomAttribute getAttribute(String key) { + for (ICustomAttribute inst : getAttributes()) { + if (inst.getAttribute().getKey().equalsIgnoreCase(key)) { + return inst; + } + } + return null; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerEquipmentTracker.java b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerEquipmentTracker.java new file mode 100644 index 000000000..6aa34a8cc --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/attribute/tracker/PlayerEquipmentTracker.java @@ -0,0 +1,94 @@ +package kamkeel.npcs.controllers.data.attribute.tracker; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.Objects; + +import static kamkeel.npcs.util.AttributeItemUtil.TAG_RPGCORE; + +public class PlayerEquipmentTracker { + public IItemStack heldItem; + public IItemStack helmet; + public IItemStack chestplate; + public IItemStack leggings; + public IItemStack boots; + + /** + * Updates the stored equipment based on the player's current held item and armor. + */ + public void updateFrom(IPlayer player) { + heldItem = copyIfNotNull(player.getHeldItem()); + IItemStack[] armor = player.inventory.armorInventory; + // Armor inventory order: 0=boots, 1=leggings, 2=chestplate, 3=helmet. + boots = copyIfNotNull(armor[0]); + leggings = copyIfNotNull(armor[1]); + chestplate = copyIfNotNull(armor[2]); + helmet = copyIfNotNull(armor[3]); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PlayerEquipmentTracker)) return false; + PlayerEquipmentTracker other = (PlayerEquipmentTracker) o; + return areItemStacksEquivalent(heldItem, other.heldItem) + && areItemStacksEquivalent(boots, other.boots) + && areItemStacksEquivalent(leggings, other.leggings) + && areItemStacksEquivalent(chestplate, other.chestplate) + && areItemStacksEquivalent(helmet, other.helmet); + } + + @Override + public int hashCode() { + return Objects.hash( + itemHash(heldItem), + itemHash(boots), + itemHash(leggings), + itemHash(chestplate), + itemHash(helmet) + ); + } + + private int itemHash(IItemStack s) { + if (s == null) return 0; + INbt root = s.stackTagCompound; + if (root == null || !root.hasKey(TAG_RPGCORE)) return 0; + INbt tag = root.getCompoundTag(TAG_RPGCORE); + return Objects.hash(s.getItem(), tag); + } + + /** + * Returns a copy of the given IItemStack, or null if it is null. + */ + private IItemStack copyIfNotNull(IItemStack stack) { + return (stack == null) ? null : stack.copy(); + } + + /** + * Compares two ItemStacks for equivalence, ignoring durability differences. + * It checks if the items are the same and if the "CNPCAttributes" NBT compound is equal. + */ + private boolean areItemStacksEquivalent(IItemStack a, IItemStack b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.getItem() != b.getItem()) return false; + + INbt tagA = a.stackTagCompound != null + ? a.stackTagCompound.getCompoundTag(TAG_RPGCORE) + : null; + INbt tagB = b.stackTagCompound != null + ? b.stackTagCompound.getCompoundTag(TAG_RPGCORE) + : null; + if (tagA == null ^ tagB == null) return false; + if (tagA != null && !tagA.equals(tagB)) return false; + + return true; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/energy/IEnergyExtender.java b/core/src/main/java/kamkeel/npcs/controllers/data/energy/IEnergyExtender.java new file mode 100644 index 000000000..e1a5b78b5 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/energy/IEnergyExtender.java @@ -0,0 +1,56 @@ +package kamkeel.npcs.controllers.data.energy; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +/** + * Extension point for handling energy IEntity damage without a sourceAbility. + * Registered with EnergyController. DBC Addon implements this to route + * script-created energy IEntity damage through the DBC damage pipeline. + */ +public interface IEnergyExtender { + + /** + * Handle damage for an energy IEntity with custom damage data. + * Chain of Responsibility — first handler returning true claims the damage. + * + * @param energyIEntity The energy IEntity dealing damage + * @param owner The IEntity that owns/created the energy IEntity + * @param target The IEntity being damaged + * @param damage Base damage amount + * @param knockback Horizontal knockback strength + * @param knockbackUp Vertical knockback strength + * @param kbDirX Knockback direction X + * @param kbDirZ Knockback direction Z + * @param damageMultiplier Damage multiplier (e.g. falloff) + * @param damageData Custom damage IConfiguration NBT (e.g. DBC stats) + * @return true if this handler processed the damage, false to pass to next handler + */ + boolean onEnergyDamage(IEntity energyIEntity, IEntityLivingBase owner, + IEntityLivingBase target, float damage, + float knockback, float knockbackUp, + double kbDirX, double kbDirZ, + float damageMultiplier, + INbt damageData); + + /** + * Modify outgoing damage for an energy IEntity with custom damage data. + * Cumulative — each handler receives the previous handler's output. + * + * @param energyIEntity The energy IEntity dealing damage + * @param owner The IEntity that owns/created the energy IEntity + * @param baseDamage Current damage value (after previous handlers) + * @param damageData Custom damage IConfiguration NBT + * @return Modified damage value + */ + float modifyEnergyDamage(IEntity energyIEntity, IEntityLivingBase owner, + float baseDamage, INbt damageData); +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargePreviewManager.java b/core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargePreviewManager.java new file mode 100644 index 000000000..d92667f7e --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargePreviewManager.java @@ -0,0 +1,103 @@ +package kamkeel.npcs.controllers.data.energycharge; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Client-side manager for packet-driven charging preview entities. + * These entities are never spawned into the IWorld; they are ticked and rendered manually. + */ +public class EnergyChargePreviewManager { + + /** Hard age cap for preview entities — last-resort safety net (60 seconds). */ + private static final int MAX_PREVIEW_AGE = 1200; + + public static EnergyChargePreviewManager ClientInstance; + + private final ConcurrentHashMap previews = new ConcurrentHashMap(); + + public static void initClient() { + ClientInstance = new EnergyChargePreviewManager(); + } + + public void addPreview(String instanceId, EntityEnergyProjectile IEntity) { + if (instanceId == null || IEntity == null) return; + previews.put(instanceId, IEntity); + } + + public void removePreview(String instanceId) { + if (instanceId == null) return; + previews.remove(instanceId); + } + + public Collection getPreviews() { + return previews.values(); + } + + public boolean hasPreviews() { + return !previews.isEmpty(); + } + + public void clear() { + previews.clear(); + } + + /** + * Tick all preview entities and remove invalid/expired entries. + */ + public void tick(IWorld IWorld) { + if (IWorld == null) { + clear(); + return; + } + + Iterator iterator = previews.values().iterator(); + while (iterator.hasNext()) { + EntityEnergyProjectile IEntity = iterator.next(); + if (IEntity == null || IEntity.isDead || IEntity.worldObj != IWorld) { + iterator.remove(); + continue; + } + + // Hard age cap: remove previews that have existed far too long + if (IEntity.ticksExisted > MAX_PREVIEW_AGE) { + IEntity.setDead(); + iterator.remove(); + continue; + } + + IEntity.onUpdate(); + if (IEntity.isDead) { + iterator.remove(); + } + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java b/core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java new file mode 100644 index 000000000..5f76c656f --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java @@ -0,0 +1,116 @@ +package kamkeel.npcs.controllers.data.energycharge; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Server-side tracker for active charging preview visuals. + * Stores enough data to resend {@link EnergyChargeSpawnPacket} to late-joining players + * who enter tracking range after the initial broadcast. + *

+ * Keyed by caster IEntity ID so {@code PlayerEvent.StartTracking} can look up + * active charges for a newly tracked IEntity. + */ +public class EnergyChargeTracker { + public static final EnergyChargeTracker Instance = new EnergyChargeTracker(); + + public static class ChargeEntry { + public final String instanceId; + public final String entityClassName; + public final INbt spawnNbt; + public final int casterEntityId; + public final int startTick; + + public ChargeEntry(String instanceId, String entityClassName, + INbt spawnNbt, int casterEntityId, int startTick) { + this.instanceId = instanceId; + this.entityClassName = entityClassName; + this.spawnNbt = spawnNbt; + this.casterEntityId = casterEntityId; + this.startTick = startTick; + } + } + + private final Map> activeCharges = new HashMap<>(); + + public void add(ChargeEntry entry) { + List list = activeCharges.get(entry.casterEntityId); + if (list == null) { + list = new ArrayList<>(); + activeCharges.put(entry.casterEntityId, list); + } + list.add(entry); + } + + public void remove(String instanceId, int casterEntityId) { + List list = activeCharges.get(casterEntityId); + if (list == null) return; + Iterator it = list.iterator(); + while (it.hasNext()) { + if (it.next().instanceId.equals(instanceId)) { + it.remove(); + break; + } + } + if (list.isEmpty()) { + activeCharges.remove(casterEntityId); + } + } + + public void removeAllForCaster(int casterEntityId) { + activeCharges.remove(casterEntityId); + } + + /** + * Send all active charges for a caster to a specific player. + * Adjusts ChargeTick in the NBT to reflect elapsed time so the + * late-joining client starts at the correct visual progress. + */ + public void sendToPlayer(int casterEntityId, IPlayer player, int currentWorldTick) { + List entries = activeCharges.get(casterEntityId); + if (entries == null || entries.isEmpty()) return; + + for (ChargeEntry entry : entries) { + int elapsed = currentWorldTick - entry.startTick; + int chargeDuration = entry.spawnNbt.hasKey("ChargeDuration") + ? entry.spawnNbt.getInteger("ChargeDuration") : 0; + + // Charge already complete — server remove packet should follow shortly + if (chargeDuration > 0 && elapsed >= chargeDuration) { + continue; + } + + INbt adjustedNbt = (INbt) entry.spawnNbt.copy(); + if (elapsed > 0) { + adjustedNbt.setInteger("ChargeTick", elapsed); + } + + PacketHandler.Instance.sendToPlayer( + new EnergyChargeSpawnPacket(entry.instanceId, entry.entityClassName, adjustedNbt), + player + ); + } + } + + public boolean hasCharges(int casterEntityId) { + List list = activeCharges.get(casterEntityId); + return list != null && !list.isEmpty(); + } + + public void clear() { + activeCharges.clear(); + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/core/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java new file mode 100644 index 000000000..cbd6bf2a9 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java @@ -0,0 +1,90 @@ +package kamkeel.npcs.controllers.data.profile; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.SyncController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; + +import java.util.ArrayList; +import java.util.List; + +public class CNPCData implements IProfileData { + + @Override + public String getTagName() { + return "CNPC+"; + } + + @Override + public INbt getCurrentNBT(IPlayer player) { + PlayerData customNPCData = PlayerData.get(player); + INbt compound = customNPCData.getNBT(); + compound.removeTag(PlayerTradeData.NBT_KEY); + + return compound; + } + + @Override + public void save(IPlayer player) { + PlayerData customNPCData = PlayerData.get(player); + customNPCData.save(); + SyncController.syncPlayerData((IPlayer) player, false); + } + + @Override + public void setNBT(IPlayer player, INbt replace) { + PlayerData customNPCData = PlayerData.get(player); + + INbt sharedData = new INbt(); + customNPCData.tradeData.writeToNBT(sharedData); + + if (replace.hasNoTags()) { + PlayerData newData = new PlayerData(); + newData.player = player; + customNPCData.setNBT((INbt) newData.getNBT().copy()); + } else { + customNPCData.setNBT(replace); + } + + customNPCData.tradeData.readFromNBT(sharedData); + customNPCData.updateClient = true; + } + + @Override + public int getSwitchPriority() { + return 0; + } + + @Override + public ProfileOperation verifySwitch(IPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData.partyUUID != null) + return ProfileOperation.error("Cannot switch while in Party"); + + if (playerData.abilityData.isExecutingAbility()) + return ProfileOperation.error("Cannot switch while performing an Ability"); + + return ProfileOperation.success(""); + } + + @Override + public List getInfo(IPlayer player, INbt compound) { + PlayerData playerData = new PlayerData(); + playerData.player = player; + playerData.setNBT(compound); + + List info = new ArrayList<>(); + info.add(new ProfileInfoEntry("profile.info.quest.finished", 0x60fa57, playerData.questData.finishedQuests.size(), 0xFFFFFF)); + info.add(new ProfileInfoEntry("profile.info.quest.active", 0xf75336, playerData.questData.activeQuests.size(), 0xFFFFFF)); + info.add(new ProfileInfoEntry("profile.info.dialog.read", 0x47acf5, playerData.dialogData.dialogsRead.size(), 0xFFFFFF)); + return info; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/profile/IProfileData.java b/core/src/main/java/kamkeel/npcs/controllers/data/profile/IProfileData.java new file mode 100644 index 000000000..34da4540f --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/profile/IProfileData.java @@ -0,0 +1,62 @@ +package kamkeel.npcs.controllers.data.profile; + + +import java.util.List; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +public interface IProfileData { + + /** + * Do not use the tags: Name, lastLoaded, ID. + * + * @return The save tag utilized in the Profile. + */ + String getTagName(); + + /** + * @return INbt of the current data for a given IPlayer. + */ + INbt getCurrentNBT(IPlayer player); + + /** + * Called immediately after setNBT() to replace existing NBT. + */ + void save(IPlayer player); + + /** + * Replaces the current data operation when changing profiles. + */ + void setNBT(IPlayer player, INbt replace); + + /** + * Returns the priority for this ProfileData's verification check. + * Lower numbers run first. + */ + int getSwitchPriority(); + + /** + * Called before a profile switch occurs. + * + * @param player The player switching profiles. + * @return true if allowed, false if not. + */ + ProfileOperation verifySwitch(IPlayer player); + + /** + * NEW: Builds and returns a list of informational strings about this profile data, + * based on the provided INbt from the slot. + * + * @param player The player in question. + * @param compound The NBT data from the slot under this profile data's tag. + * @return A list of InfoEntry with information + */ + List getInfo(IPlayer player, INbt compound); +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/profile/Profile.java b/core/src/main/java/kamkeel/npcs/controllers/data/profile/Profile.java new file mode 100644 index 000000000..1136af7eb --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/profile/Profile.java @@ -0,0 +1,118 @@ +package kamkeel.npcs.controllers.data.profile; + + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IProfile; +import noppes.npcs.api.handler.data.ISlot; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.NoppesUtilServer; + +public class Profile implements IProfile { + public IPlayer player; + public int currentSlotId; + private final Map slots = new HashMap<>(); + public Map sharedQuestTimestamps = new HashMap<>(); + private boolean locked = false; + + public Profile(IPlayer player, INbt compound) { + this.player = player; + if (compound.hasKey("CurrentSlotId")) { + this.currentSlotId = compound.getInteger("CurrentSlotId"); + } else { + this.currentSlotId = 0; + } + + if (compound.hasKey("Slots")) { + INbt slotsCompound = compound.getCompoundTag("Slots"); + Set keys = slotsCompound.func_150296_c(); + for (String key : keys) { + try { + int slotId = Integer.parseInt(key); + INbt slotNBT = slotsCompound.getCompoundTag(key); + ISlot slot = Slot.fromNBT(slotId, slotNBT); + slots.put(slotId, slot); + } catch (NumberFormatException e) { + // Skip keys that are not valid slot IDs. + } + } + } + + if (compound.hasKey("SharedQuestTimestamps")) { + INbtList list = compound.getTagList("SharedQuestTimestamps", 10); + for (int i = 0; i < list.tagCount(); i++) { + INbt entry = list.getCompoundTagAt(i); + sharedQuestTimestamps.put(entry.getInteger("Quest"), entry.getLong("Date")); + } + } + } + + public Profile(IPlayer player) { + this.player = player; + this.currentSlotId = 0; + // Create a default slot. + Slot defaultSlot = new Slot(0, "Default Slot"); + defaultSlot.setLastLoaded(System.currentTimeMillis()); + slots.put(0, defaultSlot); + } + + @Override + public IPlayer getPlayer() { + return NoppesUtilServer.getIPlayer(player); + } + + @Override + public int getCurrentSlotId() { + return currentSlotId; + } + + @Override + public Map getSlots() { + return slots; + } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + @Override + public INbt writeToNBT() { + INbt compound = new INbt(); + if (player != null) + compound.setString("Name", player.getCommandSenderName()); + compound.setInteger("CurrentSlotId", currentSlotId); + INbt slotsCompound = new INbt(); + for (Map.Entry entry : slots.entrySet()) { + // Temporary slots (typically using negative IDs) are kept in memory + // only and skipped here so they never persist to disk. + if (entry.getValue().isTemporary()) + continue; + slotsCompound.setTag(String.valueOf(entry.getKey()), entry.getValue().toNBT()); + } + compound.setTag("Slots", slotsCompound); + + INbtList questList = new INbtList(); + for (Map.Entry entry : sharedQuestTimestamps.entrySet()) { + INbt questEntry = new INbt(); + questEntry.setInteger("Quest", entry.getKey()); + questEntry.setLong("Date", entry.getValue()); + questList.appendTag(questEntry); + } + compound.setTag("SharedQuestTimestamps", questList); + + return compound; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java b/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java index f3d7d98b4..1f94c0de1 100644 --- a/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java +++ b/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java @@ -1,15 +1,24 @@ package kamkeel.npcs.controllers.data.telegraph; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; import noppes.npcs.core.NBT; /** - * Telegraph configuration - defines a visual warning shape. + * Telegraph IConfiguration - defines a visual warning shape. * Telegraphs warn players about incoming attacks or mark areas of effect. */ public class Telegraph { - // Identity + // IdIEntity private String id = ""; private TelegraphType type = TelegraphType.CIRCLE; diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java b/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java new file mode 100644 index 000000000..ddbd32f7c --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -0,0 +1,402 @@ +package kamkeel.npcs.controllers.data.telegraph; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.Ability; +import java.util.UUID; + +/** + * Runtime instance of an active telegraph. + * Tracks position, timing, and state for rendering. + */ +public class TelegraphInstance { + + // Identity + private String instanceId; + private Telegraph telegraph; + + // Position (current) + private double x; + private double y; + private double z; + private float yaw; + + // Position (previous tick - for interpolation) + private double prevX; + private double prevY; + private double prevZ; + private float prevYaw; + + // IEntity tracking + private int entityIdToFollow = -1; + private int casterEntityId = -1; + private int targetEntityId = -1; // For LINE telegraphs: IEntity to face (yaw tracking) + private boolean trackFollowedEntityYaw = false; // Track followed IEntity's rotationYaw when no targetEntity + + // Timing + private int remainingTicks; + private int totalTicks; + + // State + private boolean isWarning = false; + + // Server-side tracking (for removal) + private transient IWorld IWorld; + private transient int dimensionId; + + public TelegraphInstance() { + this.instanceId = UUID.randomUUID().toString().substring(0, 8); + } + + public TelegraphInstance(Telegraph telegraph, double x, double y, double z, float yaw) { + this(); + this.telegraph = new Telegraph(telegraph); + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + // Initialize prev positions to current for first frame + this.prevX = x; + this.prevY = y; + this.prevZ = z; + this.prevYaw = yaw; + this.remainingTicks = telegraph.getDurationTicks(); + this.totalTicks = telegraph.getDurationTicks(); + } + + // ═══════════════════════════════════════════════════════════════════ + // TICK LOGIC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Called every tick to update telegraph state. + * + * @param IWorld The IWorld (for IEntity lookup) + * @return false if telegraph should be removed + */ + public boolean tick(IWorld IWorld) { + if (remainingTicks <= 0) { + return false; + } + + // Save previous position for interpolation + this.prevX = this.x; + this.prevY = this.y; + this.prevZ = this.z; + this.prevYaw = this.yaw; + + remainingTicks--; + + // Update position if following an IEntity + if (entityIdToFollow >= 0 && IWorld != null) { + IEntity IEntity = IWorld.getEntityByID(entityIdToFollow); + if (IEntity != null) { + this.x = IEntity.posX; + this.z = IEntity.posZ; + // Find ground level below the IEntity for proper telegraph placement + int searchRange = (telegraph != null) ? telegraph.getGroundSearchRange() : 3; + this.y = Ability.findGroundLevel(IWorld, IEntity.posX, IEntity.posY, IEntity.posZ, searchRange); + + // Track followed IEntity's yaw for directional telegraphs (player casters) + if (trackFollowedEntityYaw && targetEntityId < 0) { + this.yaw = IEntity.rotationYaw; + } + } + } + + // Update yaw to face target IEntity (for LINE telegraphs) + if (targetEntityId >= 0 && IWorld != null) { + IEntity target = IWorld.getEntityByID(targetEntityId); + if (target != null) { + double dx = target.posX - this.x; + double dz = target.posZ - this.z; + this.yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); + } + } + + // Check for warning phase transition + if (!isWarning && telegraph != null) { + if (remainingTicks <= telegraph.getWarningStartTick()) { + isWarning = true; + } + } + + return true; + } + + /** + * Get progress from 0.0 (just started) to 1.0 (finished). + */ + public float getProgress() { + if (totalTicks <= 0) return 1.0f; + return 1.0f - ((float) remainingTicks / (float) totalTicks); + } + + /** + * Get the current display color (normal or warning). + */ + public int getCurrentColor() { + if (telegraph == null) return 0x80FF0000; + return isWarning ? telegraph.getWarningColor() : telegraph.getColor(); + } + + /** + * Get alpha-modulated color for breathing animation. + */ + public int getAnimatedColor(float partialTicks) { + int baseColor = getCurrentColor(); + if (telegraph == null || !telegraph.isAnimated()) { + return baseColor; + } + + // Breathing animation: slow, smooth modulation of alpha + float time = (totalTicks - remainingTicks) + partialTicks; + // Slower breathing: 0.08 for ~4 second cycle, smoother sine curve + float breathPhase = (float) Math.sin(time * 0.08); + // Smooth easing: square the sine for more organic feel, range 0.5 to 1.0 + float pulse = 0.5f + (breathPhase * breathPhase * 0.5f * Math.signum(breathPhase)); + + int alpha = (baseColor >> 24) & 0xFF; + int r = (baseColor >> 16) & 0xFF; + int g = (baseColor >> 8) & 0xFF; + int b = baseColor & 0xFF; + + alpha = (int) (alpha * pulse); + + return (alpha << 24) | (r << 16) | (g << 8) | b; + } + + // ═══════════════════════════════════════════════════════════════════ + // INTERPOLATED POSITION (for smooth rendering) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get interpolated X position for smooth rendering. + */ + public double getInterpolatedX(float partialTicks) { + return prevX + (x - prevX) * partialTicks; + } + + /** + * Get interpolated Y position for smooth rendering. + */ + public double getInterpolatedY(float partialTicks) { + return prevY + (y - prevY) * partialTicks; + } + + /** + * Get interpolated Z position for smooth rendering. + */ + public double getInterpolatedZ(float partialTicks) { + return prevZ + (z - prevZ) * partialTicks; + } + + /** + * Get interpolated yaw for smooth rendering. + * Handles angle wrapping for smooth rotation. + */ + public float getInterpolatedYaw(float partialTicks) { + // Handle angle wrapping to prevent spinning the wrong way + float diff = yaw - prevYaw; + while (diff > 180.0f) diff -= 360.0f; + while (diff < -180.0f) diff += 360.0f; + return prevYaw + diff * partialTicks; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + public INbt writeNBT() { + INbt nbt = new INbt(); + nbt.setString("instanceId", instanceId); + + if (telegraph != null) { + nbt.setTag("telegraph", ((NBTWrapper) telegraph.writeNBT()).getMCTag()); + } + + nbt.setDouble("x", x); + nbt.setDouble("y", y); + nbt.setDouble("z", z); + nbt.setFloat("yaw", yaw); + + nbt.setInteger("entityIdToFollow", entityIdToFollow); + nbt.setInteger("casterEntityId", casterEntityId); + nbt.setInteger("targetEntityId", targetEntityId); + nbt.setBoolean("trackFollowedYaw", trackFollowedEntityYaw); + + nbt.setInteger("remainingTicks", remainingTicks); + nbt.setInteger("totalTicks", totalTicks); + + nbt.setBoolean("isWarning", isWarning); + + return nbt; + } + + public void readNBT(INbt nbt) { + this.instanceId = nbt.getString("instanceId"); + + if (nbt.hasKey("telegraph")) { + this.telegraph = new Telegraph(); + this.telegraph.readNBT(new NBTWrapper(nbt.getCompoundTag("telegraph"))); + } + + this.x = nbt.getDouble("x"); + this.y = nbt.getDouble("y"); + this.z = nbt.getDouble("z"); + this.yaw = nbt.getFloat("yaw"); + + this.entityIdToFollow = nbt.getInteger("entityIdToFollow"); + this.casterEntityId = nbt.getInteger("casterEntityId"); + this.targetEntityId = nbt.getInteger("targetEntityId"); + this.trackFollowedEntityYaw = nbt.hasKey("trackFollowedYaw") && nbt.getBoolean("trackFollowedYaw"); + + this.remainingTicks = nbt.getInteger("remainingTicks"); + this.totalTicks = nbt.getInteger("totalTicks"); + + this.isWarning = nbt.getBoolean("isWarning"); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS & SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public String getInstanceId() { + return instanceId; + } + + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } + + public Telegraph getTelegraph() { + return telegraph; + } + + public void setTelegraph(Telegraph telegraph) { + this.telegraph = telegraph; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public int getEntityIdToFollow() { + return entityIdToFollow; + } + + public void setEntityIdToFollow(int entityIdToFollow) { + this.entityIdToFollow = entityIdToFollow; + } + + /** + * Locks the telegraph at its current position and direction. + * Stops following any IEntity - used when ability commits to action. + */ + public void lockPosition() { + this.entityIdToFollow = -1; + this.targetEntityId = -1; + } + + public int getCasterEntityId() { + return casterEntityId; + } + + public void setCasterEntityId(int casterEntityId) { + this.casterEntityId = casterEntityId; + } + + public int getTargetEntityId() { + return targetEntityId; + } + + public void setTargetEntityId(int targetEntityId) { + this.targetEntityId = targetEntityId; + } + + public boolean isTrackFollowedEntityYaw() { + return trackFollowedEntityYaw; + } + + public void setTrackFollowedEntityYaw(boolean track) { + this.trackFollowedEntityYaw = track; + } + + public int getRemainingTicks() { + return remainingTicks; + } + + public void setRemainingTicks(int remainingTicks) { + this.remainingTicks = remainingTicks; + } + + public int getTotalTicks() { + return totalTicks; + } + + public void setTotalTicks(int totalTicks) { + this.totalTicks = totalTicks; + } + + public boolean isWarning() { + return isWarning; + } + + public void setWarning(boolean warning) { + isWarning = warning; + } + + public IWorld getWorld() { + return IWorld; + } + + public void setWorld(IWorld IWorld) { + this.IWorld = IWorld; + if (IWorld != null) { + this.dimensionId = IWorld.provider.dimensionId; + } + } + + public int getDimensionId() { + return dimensionId; + } + + public void setDimensionId(int dimensionId) { + this.dimensionId = dimensionId; + } +} diff --git a/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java b/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java new file mode 100644 index 000000000..f5b0a43a7 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java @@ -0,0 +1,149 @@ +package kamkeel.npcs.controllers.data.telegraph; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages active telegraph instances. + * There's one instance per IWorld (client and server). + */ +public class TelegraphManager { + + /** + * Client-side instance for rendering + */ + public static TelegraphManager ClientInstance; + + private final ConcurrentHashMap telegraphs = new ConcurrentHashMap<>(); + + public TelegraphManager() { + } + + /** + * Initialize the client-side manager. + * Call from client proxy during mod init. + */ + public static void initClient() { + ClientInstance = new TelegraphManager(); + } + + // ═══════════════════════════════════════════════════════════════════ + // TELEGRAPH MANAGEMENT + // ═══════════════════════════════════════════════════════════════════ + + /** + * Add a new telegraph instance. + */ + public void addTelegraph(TelegraphInstance instance) { + telegraphs.put(instance.getInstanceId(), instance); + } + + /** + * Remove a telegraph by ID. + */ + public void removeTelegraph(String instanceId) { + telegraphs.remove(instanceId); + } + + /** + * Get a telegraph by ID. + */ + public TelegraphInstance getTelegraph(String instanceId) { + return telegraphs.get(instanceId); + } + + /** + * Get all active telegraphs for rendering. + */ + public Collection getTelegraphs() { + return telegraphs.values(); + } + + /** + * Clear all telegraphs. + */ + public void clear() { + telegraphs.clear(); + } + + /** + * Check if any telegraphs are active. + */ + public boolean hasTelegraphs() { + return !telegraphs.isEmpty(); + } + + // ═══════════════════════════════════════════════════════════════════ + // TICK + // ═══════════════════════════════════════════════════════════════════ + + /** + * Tick all telegraphs, removing expired ones. + * Call from client tick handler. + */ + public void tick(IWorld IWorld) { + Iterator iterator = telegraphs.values().iterator(); + while (iterator.hasNext()) { + TelegraphInstance instance = iterator.next(); + if (!instance.tick(IWorld)) { + iterator.remove(); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // HELPER METHODS FOR SPAWNING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Spawn a circle telegraph at a position. + */ + public TelegraphInstance spawnCircle(double x, double y, double z, float radius, int durationTicks, int color) { + Telegraph telegraph = Telegraph.circle(radius); + telegraph.setDurationTicks(durationTicks); + telegraph.setColor(color); + telegraph.setWarningColor((color & 0x00FFFFFF) | 0xC0000000); + + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, 0); + addTelegraph(instance); + return instance; + } + + /** + * Spawn a line telegraph from a position in a direction. + */ + public TelegraphInstance spawnLine(double x, double y, double z, float yaw, float length, float width, int durationTicks, int color) { + Telegraph telegraph = Telegraph.line(length, width); + telegraph.setDurationTicks(durationTicks); + telegraph.setColor(color); + telegraph.setWarningColor((color & 0x00FFFFFF) | 0xC0000000); + + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + addTelegraph(instance); + return instance; + } + + /** + * Spawn a cone telegraph from a position in a direction. + */ + public TelegraphInstance spawnCone(double x, double y, double z, float yaw, float length, float angle, int durationTicks, int color) { + Telegraph telegraph = Telegraph.cone(length, angle); + telegraph.setDurationTicks(durationTicks); + telegraph.setColor(color); + telegraph.setWarningColor((color & 0x00FFFFFF) | 0xC0000000); + + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + addTelegraph(instance); + return instance; + } +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java new file mode 100644 index 000000000..1dc0d571e --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for ability beams. Actual entity stays in mc1710. + */ +public class EntityAbilityBeam extends EntityEnergyProjectile { +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java new file mode 100644 index 000000000..179f2f425 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for ability discs. Actual entity stays in mc1710. + */ +public class EntityAbilityDisc extends EntityEnergyProjectile { +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java new file mode 100644 index 000000000..a373c926d --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for ability lasers. Actual entity stays in mc1710. + */ +public class EntityAbilityLaser extends EntityEnergyProjectile { +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java new file mode 100644 index 000000000..cc3f96da3 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for ability orbs. Actual entity stays in mc1710. + */ +public class EntityAbilityOrb extends EntityEnergyProjectile { +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java new file mode 100644 index 000000000..030927a74 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -0,0 +1,17 @@ +package kamkeel.npcs.entity; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for ability zones. + */ +public class EntityAbilityZone { + public double posX, posY, posZ; + public void applyVisual(boolean groundFill, float groundAlpha, + boolean rings, int ringCount, boolean border, float borderSpeed, + boolean accents, int accentStyle, boolean lightning, + boolean particles, int particleMotion, String particleDir, + int particleSize, boolean particleGlow) {} + public void setDurationTicks(int ticks) {} + public float getZoneRadius() { return 0; } + public float getZoneHeight() { return 0; } +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java new file mode 100644 index 000000000..33ceecdd4 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -0,0 +1,27 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for energy barriers. Actual entity stays in mc1710. + */ +public class EntityEnergyBarrier { + public double posX, posY, posZ; + public void setDead() {} +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java new file mode 100644 index 000000000..bb3f6019c --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java @@ -0,0 +1,27 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for energy domes. Actual entity stays in mc1710. + */ +public class EntityEnergyDome { + public double posX, posY, posZ; + public void setDead() {} +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java new file mode 100644 index 000000000..f47b4ea0a --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java @@ -0,0 +1,10 @@ +package kamkeel.npcs.entity; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for energy explosions. + */ +public class EntityEnergyExplosion { + public double posX, posY, posZ; + public void setDead() {} +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java new file mode 100644 index 000000000..20eb52dbb --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java @@ -0,0 +1,30 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.controllers.data.ability.data.energy.PanelData; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for energy panels. Actual entity stays in mc1710. + */ +public class EntityEnergyPanel { + public double posX, posY, posZ; + public PanelData getPanelData() { return new PanelData(); } + public void setDead() {} +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java new file mode 100644 index 000000000..a960ee5d2 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -0,0 +1,31 @@ +package kamkeel.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import noppes.npcs.api.entity.IEntityLivingBase; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for energy projectiles. Actual entity stays in mc1710. + */ +public class EntityEnergyProjectile { + public double posX, posY, posZ; + public float rotationYaw, rotationPitch; + public void setPosition(double x, double y, double z) {} + public void setDead() {} +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java b/core/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java new file mode 100644 index 000000000..d376fa31a --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java @@ -0,0 +1,8 @@ +package kamkeel.npcs.entity; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for energy slicers. + */ +public class EntityEnergySlicer extends EntityEnergyProjectile { +} diff --git a/core/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java b/core/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java new file mode 100644 index 000000000..09ee65f30 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java @@ -0,0 +1,10 @@ +package kamkeel.npcs.entity; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Entity for energy sweepers. + */ +public class EntityEnergySweeper { + public double posX, posY, posZ; + public void setDead() {} +} diff --git a/core/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/core/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 598126a2d..8d72f6ba2 100644 --- a/core/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/core/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -1,5 +1,16 @@ package kamkeel.npcs.network.enums; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + // Packets to the Client public enum EnumDataPacket { // Synchronization @@ -15,7 +26,6 @@ public enum EnumDataPacket { DIALOG, QUEST_COMPLETION, - // NPC EDIT_NPC, UPDATE_NPC, @@ -77,8 +87,8 @@ public enum EnumDataPacket { ENERGY_CHARGE_SPAWN, ENERGY_CHARGE_REMOVE, - // Energy Explosion Preview - ENERGY_EXPLOSION_SPAWN, + // Energy IExplosion Preview + ENERGY_IExplosion_SPAWN, // Energy Projectile Reflection Sync PROJECTILE_REFLECT, diff --git a/core/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/core/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 27a319abd..e2b5519b8 100644 --- a/core/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/core/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -1,5 +1,16 @@ package kamkeel.npcs.network.enums; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + public enum EnumRequestPacket { NPCDelete, RemoteMainMenu, @@ -98,8 +109,8 @@ public enum EnumRequestPacket { RemoteNpcsGet, RemoteTpToNpc, - TileEntitySave, - TileEntityGet, + TileIEntitySave, + TileIEntityGet, Gui, IsGuiOpen, CacheAnimation, diff --git a/core/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/core/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java index ee6cf352f..f48a0562f 100644 --- a/core/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java +++ b/core/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java @@ -1,5 +1,9 @@ package kamkeel.npcs.network.enums; +/** + * Sync type enum for network synchronization. + * Values must match usage in SyncController. + */ public enum EnumSyncType { FACTION, DIALOG, @@ -14,5 +18,5 @@ public enum EnumSyncType { ANVIL_RECIPES, CUSTOM_EFFECTS, CUSTOM_ABILITY, - CHAINED_ABILITY + CHAINED_ABILITY; } diff --git a/core/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/core/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java new file mode 100644 index 000000000..263f03778 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -0,0 +1,622 @@ +package kamkeel.npcs.util; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData; +import noppes.npcs.constants.EnumAnimationPart; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.data.FramePart; +/** + * Utility class for calculating IWorld positions based on anchor points. + * Supports both EntityNPCInterface and IPlayer with animation and model size awareness. + */ +public class AnchorPointHelper { + + // Y offset correction for the local client player. + // EntityPlayerSP.posY is at feet, but the renderer adds yOffset (1.62) visually, + // so anchor positions computed from posY appear ~1.6 blocks too high for the local player. + // Remote players and NPCs don't have this discrepancy. + private static final float CLIENT_PLAYER_Y_OFFSET = -1.6F; + + // Default biped model constants (in model units, 1 unit = 1/16 block) + private static final float MODEL_SCALE = 1f / 16f; + + // Shoulder position relative to model origin + private static final float SHOULDER_Y = 2f; + private static final float SHOULDER_X_OFFSET = 5f; // Distance from center to shoulder + + // Palm position (in arm's local space, relative to shoulder) + // Arm is 12 units long; palm is near the end + private static final float PALM_DOWN = 11f; // Distance down the arm to palm center + private static final float PALM_FORWARD = 2f; // Forward offset to palm surface (not center of hand) + private static final float PALM_INWARD = 0f; // Lateral offset (0 = centered on arm) + + // Default height offsets as fraction of IEntity height + private static final float FRONT_HEIGHT = 0.7f; + private static final float CENTER_HEIGHT = 0.5f; + private static final float ARM_HEIGHT = 0.75f; + private static final float ABOVE_HEAD_HEIGHT = 1.2f; + private static final float CHEST_HEIGHT = 0.65f; + private static final float EYE_HEIGHT = 0.85f; + + // Distance in front for FRONT anchor + private static final float DEFAULT_FRONT_DISTANCE = 1.0f; + + // Fallback arm positioning (when no animation, arms hang at sides) + private static final float FALLBACK_LATERAL = 0.35f; // Distance from body center to arm + private static final float FALLBACK_FORWARD = 0.1f; // Slight forward offset + private static final float FALLBACK_ARM_HEIGHT = 0.45f; // Hand height when arms hang down (near hip) + + // Default arm scale (no scaling) + private static final ModelScalePart DEFAULT_ARM_SCALE = new ModelScalePart(); + + /** + * Calculate IWorld position for an anchor point on an IEntity. + */ + public static IVector3 calculateAnchorPosition(IEntityLivingBase IEntity, EnergyAnchorData anchorData) { + return calculateAnchorPosition(IEntity, anchorData, DEFAULT_FRONT_DISTANCE); + } + + /** + * Calculate IWorld position for an anchor point with custom front offset. + */ + public static IVector3 calculateAnchorPosition(IEntityLivingBase IEntity, EnergyAnchorData anchorData, float frontOffset) { + float scale = getModelScale(IEntity); + float height = IEntity.height; + + AnchorPoint anchor = anchorData.anchorPoint; + + float anchorX = anchorData.anchorOffsetX; + float anchorY = anchorData.anchorOffsetY; + float anchorZ = anchorData.anchorOffsetZ; + + double x = IEntity.posX; + // EYE uses getEyeHeight() which already gives the correct offset from posY, + // so the client player correction would double-subtract and place it at feet level + double y = IEntity.posY + (anchor == AnchorPoint.EYE ? 0 : getClientPlayerYCorrection(IEntity)); + double z = IEntity.posZ; + + float bodyYaw = (float) Math.toRadians(IEntity.renderYawOffset); + + switch (anchor) { + case FRONT: + IVector3 look = IEntity.getLookVec(); + x += look.xCoord * frontOffset * scale; + y += height * FRONT_HEIGHT + look.yCoord * frontOffset * scale; + z += look.zCoord * frontOffset * scale; + break; + + case CENTER: + y += height * CENTER_HEIGHT; + break; + + case RIGHT_HAND: + return calculateHandPosition(IEntity, anchorData, true, scale); + + case LEFT_HAND: + return calculateHandPosition(IEntity, anchorData, false, scale); + + case ABOVE_HEAD: + y += height * ABOVE_HEAD_HEIGHT; + break; + + case CHEST: + y += height * CHEST_HEIGHT; + break; + + case EYE: + return calculateEyePosition(IEntity, anchorData, scale); + } + + // Rotate offsets relative to IEntity's body yaw + // +X = IEntity's right, +Y = up, +Z = IEntity's forward + IVector3 rotatedOffset = rotateOffsetByYaw(anchorX * scale, anchorY * scale, anchorZ * scale, bodyYaw); + x += rotatedOffset.xCoord; + y += rotatedOffset.yCoord; + z += rotatedOffset.zCoord; + + // Apply FULL_MODEL rotation if active + FramePart fullModel = getFullModelPart(IEntity); + if (fullModel != null) { + double dx = x - IEntity.posX; + double dy = y - IEntity.posY; + double dz = z - IEntity.posZ; + IVector3 rotated = applyFullModelToWorldOffset(fullModel, IEntity, dx, dy, dz, bodyYaw, scale); + return IVector3.createVectorHelper( + IEntity.posX + rotated.xCoord, + IEntity.posY + rotated.yCoord, + IEntity.posZ + rotated.zCoord + ); + } + + return IVector3.createVectorHelper(x, y, z); + } + + private static IVector3 calculateEyePosition(IEntityLivingBase IEntity, EnergyAnchorData anchorData, float scale) { + double pivotWorldX = IEntity.posX; + double pivotWorldY = IEntity.posY + IEntity.getEyeHeight(); + double pivotWorldZ = IEntity.posZ; + + float bodyYaw = (float) Math.toRadians(IEntity.renderYawOffset); + + float[] rotations = getHeadBoneRotations(IEntity); + + double ox = -anchorData.anchorOffsetX * scale; + double oy = -anchorData.anchorOffsetY * scale; + double oz = -anchorData.anchorOffsetZ * scale; + + // X rotation + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); + double ry = oy * cosX - oz * sinX; + double rz = oy * sinX + oz * cosX; + double rx = ox; + oy = ry; oz = rz; + + // Y rotation + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + double rx2 = rx * cosY + oz * sinY; + double rz2 = -rx * sinY + oz * cosY; + rx = rx2; oz = rz2; + + // Z rotation + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + double rx3 = rx * cosZ - oy * sinZ; + double ry3 = rx * sinZ + oy * cosZ; + rx = rx3; oy = ry3; + + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + + double worldOffsetX = rx * cosYaw + oz * sinYaw; + double worldOffsetZ = rx * sinYaw - oz * cosYaw; + double worldOffsetY = -oy; // model Y-down → IWorld Y-up + + double x = pivotWorldX + worldOffsetX; + double y = pivotWorldY + worldOffsetY; + double z = pivotWorldZ + worldOffsetZ; + + FramePart fullModel = getFullModelPart(IEntity); + if (fullModel != null) { + double dx = x - IEntity.posX; + double dy = y - IEntity.posY; + double dz = z - IEntity.posZ; + IVector3 rotatedFull = applyFullModelToWorldOffset(fullModel, IEntity, dx, dy, dz, bodyYaw, scale); + return IVector3.createVectorHelper( + IEntity.posX + rotatedFull.xCoord, + IEntity.posY + rotatedFull.yCoord, + IEntity.posZ + rotatedFull.zCoord + ); + } + + return IVector3.createVectorHelper(x, y, z); + } + + private static float[] getHeadBoneRotations(IEntityLivingBase IEntity) { + // Try animated HEAD part first + AnimationData animData = AnimationData.getData(IEntity); + if (animData != null && animData.isActive() && animData.animation != null) { + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame != null) { + FramePart headPart = frame.frameParts.get(EnumAnimationPart.HEAD); + if (headPart != null) { + return getRotations(headPart, IEntity); + } + } + } + + // Fallback: live head angles + float pitch = (float) Math.toRadians(IEntity.rotationPitch); + float yawDelta = (float) Math.toRadians(IEntity.rotationYawHead - IEntity.renderYawOffset); + + return new float[]{ pitch, yawDelta, 0f }; + } + + /** + * Get model scale factor for an IEntity. + * NPCs have configurable model size, players use default scale. + */ + private static float getModelScale(IEntityLivingBase IEntity) { + if (IEntity instanceof EntityNPCInterface) { + int modelSize = ((EntityNPCInterface) IEntity).display.modelSize; + return modelSize / 5f; // modelSize 5 = 100% scale + } + return 1f; + } + + /** + * Returns Y offset correction for the local client player. + * On the client, EntityPlayerSP.posY is at feet level but the renderer adds yOffset (1.62), + * causing anchor positions to appear ~1.6 blocks too high. This matches DBC's correction. + */ + private static double getClientPlayerYCorrection(IEntityLivingBase IEntity) { + if (IEntity.worldObj.isRemote && IEntity instanceof EntityPlayerSP) { + return CLIENT_PLAYER_Y_OFFSET; + } + return 0.0; + } + + /** + * Get arm scale for an IEntity. + * EntityCustomNpc has per-part scaling, others use default (1,1,1). + */ + private static ModelScalePart getArmScale(IEntityLivingBase IEntity) { + if (IEntity instanceof EntityCustomNpc) { + return ((EntityCustomNpc) IEntity).modelData.modelScale.arms; + } + return DEFAULT_ARM_SCALE; + } + + /** + * Calculate hand position using animation data if available. + * Applies FULL_MODEL rotation if an active animation has it. + */ + private static IVector3 calculateHandPosition(IEntityLivingBase IEntity, EnergyAnchorData anchor, boolean rightHand, float scale) { + // Try to get animated position first + IVector3 pos = getAnimatedHandPosition(IEntity, anchor, rightHand, scale); + if (pos == null) { + // Fallback to static position + pos = calculateFallbackHandPosition(IEntity, anchor, rightHand, scale); + } + + // Apply FULL_MODEL rotation if active + FramePart fullModel = getFullModelPart(IEntity); + if (fullModel != null) { + double dx = pos.xCoord - IEntity.posX; + double dy = pos.yCoord - IEntity.posY; + double dz = pos.zCoord - IEntity.posZ; + float bodyYaw = (float) Math.toRadians(IEntity.renderYawOffset); + IVector3 rotated = applyFullModelToWorldOffset(fullModel, IEntity, dx, dy, dz, bodyYaw, scale); + return IVector3.createVectorHelper( + IEntity.posX + rotated.xCoord, + IEntity.posY + rotated.yCoord, + IEntity.posZ + rotated.zCoord + ); + } + + return pos; + } + + /** + * Calculate hand position from animation data. + * Works for both NPCs and Players. + */ + private static IVector3 getAnimatedHandPosition(IEntityLivingBase IEntity, EnergyAnchorData anchor, boolean rightHand, float scale) { + AnimationData animData = AnimationData.getData(IEntity); + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + + EnumAnimationPart armPart = rightHand ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; + FramePart part = frame.frameParts.get(armPart); + if (part == null) { + return null; + } + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(IEntity); + + // Get rotation and pivot values + float[] rotations = getRotations(part, IEntity); + float[] pivots = getPivots(part, IEntity); + + // Shoulder position in model space + float shoulderX = rightHand ? -SHOULDER_X_OFFSET : SHOULDER_X_OFFSET; + float shoulderY = SHOULDER_Y; + float shoulderZ = 0f; + + // Add pivot offsets + shoulderX += pivots[0]; + shoulderY += pivots[1]; + shoulderZ += pivots[2]; + + // Palm offset in arm's local space (before rotation) + // Apply arm scaling to the palm position + float palmX = (rightHand ? -PALM_INWARD : PALM_INWARD) * armScale.scaleX; + float palmY = PALM_DOWN * armScale.scaleY; + float palmZ = PALM_FORWARD * armScale.scaleZ; + + // Apply arm rotations (order: X -> Y -> Z, matching GL's reverse application) + double hx = palmX, hy = palmY, hz = palmZ; + + // X rotation first + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); + double newY = hy * cosX - hz * sinX; + double newZ = hy * sinX + hz * cosX; + hy = newY; + hz = newZ; + + // Y rotation second + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + double newX = hx * cosY + hz * sinY; + newZ = -hx * sinY + hz * cosY; + hx = newX; + hz = newZ; + + // Z rotation last + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + newX = hx * cosZ - hy * sinZ; + newY = hx * sinZ + hy * cosZ; + hx = newX; + hy = newY; + + // Combine shoulder + rotated hand offset + double modelX = shoulderX + hx; + double modelY = shoulderY + hy; + double modelZ = shoulderZ + hz; + + // Convert to blocks and apply model scale + double blockX = modelX * MODEL_SCALE * scale; + double blockY = modelY * MODEL_SCALE * scale; + double blockZ = modelZ * MODEL_SCALE * scale; + + // Apply body yaw rotation + // Renderer applies glRotatef(180 - yaw) then glScalef(-1, -1, 1). + // Combined transform: x' = mx*cos(yaw) + mz*sin(yaw), z' = mx*sin(yaw) - mz*cos(yaw) + float bodyYaw = (float) Math.toRadians(IEntity.renderYawOffset); + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + + double worldOffsetX = blockX * cosYaw + blockZ * sinYaw; + double worldOffsetZ = blockX * sinYaw - blockZ * cosYaw; + + // Calculate shoulder height based on IEntity and scale + float shoulderHeight = IEntity.height * ARM_HEIGHT; + + // Final IWorld position + double worldX = IEntity.posX + worldOffsetX; + double worldY = IEntity.posY + getClientPlayerYCorrection(IEntity) + shoulderHeight - blockY; + double worldZ = IEntity.posZ + worldOffsetZ; + + // Rotate offsets relative to IEntity's body yaw + IVector3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw); + worldX += rotatedOffset.xCoord; + worldY += rotatedOffset.yCoord; + worldZ += rotatedOffset.zCoord; + + return IVector3.createVectorHelper(worldX, worldY, worldZ); + } + + /** + * Get rotation values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getRotations(FramePart part, IEntityLivingBase IEntity) { + if (IEntity.worldObj.isRemote) { + // Client side - use interpolated values (already in radians) + part.interpolateAngles(); + return part.prevRotations; + } else { + // Server side - convert raw degrees to radians + float pi = (float) Math.PI / 180f; + return new float[]{ + part.rotation[0] * pi, + part.rotation[1] * pi, + part.rotation[2] * pi + }; + } + } + + /** + * Get pivot values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getPivots(FramePart part, IEntityLivingBase IEntity) { + if (IEntity.worldObj.isRemote) { + // Client side - use interpolated values + part.interpolateOffset(); + return part.prevPivots; + } else { + // Server side - use raw values + return part.pivot; + } + } + + /** + * Calculate fallback hand position when no animation is active. + * Represents arms hanging at sides in default idle pose. + */ + private static IVector3 calculateFallbackHandPosition(IEntityLivingBase IEntity, EnergyAnchorData anchor, boolean rightHand, float scale) { + float bodyYaw = (float) Math.toRadians(IEntity.renderYawOffset); + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(IEntity); + + // Perpendicular direction for lateral offset (right = +90°, left = -90° from body facing) + float perpYaw = bodyYaw + (rightHand ? (float) (Math.PI / 2) : (float) (-Math.PI / 2)); + + // Calculate lateral offset - arms hang at sides + // Scale by model size, but not by arm scale (lateral distance is fixed by skeleton) + double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale; + double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale; + + // Small forward offset + double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale; + double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale; + + // Arm length affects vertical position - longer arms = lower hands + double armLengthOffset = armScale.scaleY * 0.3 * scale; + + double x = IEntity.posX + lateralX + forwardX; + double y = IEntity.posY + getClientPlayerYCorrection(IEntity) + IEntity.height * FALLBACK_ARM_HEIGHT + armLengthOffset; + double z = IEntity.posZ + lateralZ + forwardZ; + + // Rotate offsets relative to IEntity's body yaw + IVector3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw); + x += rotatedOffset.xCoord; + y += rotatedOffset.yCoord; + z += rotatedOffset.zCoord; + + return IVector3.createVectorHelper(x, y, z); + } + + /** + * Get default height multiplier for an anchor point. + */ + public static float getHeightMultiplier(AnchorPoint anchor) { + switch (anchor) { + case FRONT: + return FRONT_HEIGHT; + case CENTER: + return CENTER_HEIGHT; + case RIGHT_HAND: + case LEFT_HAND: + return ARM_HEIGHT; + case ABOVE_HEAD: + return ABOVE_HEAD_HEIGHT; + case CHEST: + return CHEST_HEIGHT; + case EYE: + return EYE_HEIGHT; + default: + return FRONT_HEIGHT; + } + } + + /** + * Rotate an offset vector by the IEntity's body yaw so offsets are IEntity-relative. + * +X = IEntity's right, +Y = up, +Z = IEntity's forward. + * + * @param bodyYawRad body yaw in radians + */ + private static IVector3 rotateOffsetByYaw(double offsetX, double offsetY, double offsetZ, float bodyYawRad) { + double cos = Math.cos(bodyYawRad); + double sin = Math.sin(bodyYawRad); + double worldX = -offsetX * cos - offsetZ * sin; + double worldZ = -offsetX * sin + offsetZ * cos; + return IVector3.createVectorHelper(worldX, offsetY, worldZ); + } + + // ==================== FULL_MODEL ANIMATION SUPPORT ==================== + + /** + * Get the FULL_MODEL FramePart if the IEntity has an active animation with it. + * Returns null if no FULL_MODEL animation is active. + */ + private static FramePart getFullModelPart(IEntityLivingBase IEntity) { + AnimationData animData = AnimationData.getData(IEntity); + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + FramePart part = frame.frameParts.get(EnumAnimationPart.FULL_MODEL); + return part; + } + + /** + * Apply FULL_MODEL rotation to a IWorld-space offset from IEntity position. + * Converts the offset to model space, applies FULL_MODEL rotation and pivots, + * then converts back to IWorld space. + *

+ * The rendering pipeline is: entity_pos * body_yaw * scale(-1,-1,1) * FULL_MODEL * parts + * Combined body_yaw + scale maps model space to IWorld space: + * worldX = mx*cos(yaw) + mz*sin(yaw) + * worldY = -my (model Y-down → IWorld Y-up) + * worldZ = mx*sin(yaw) - mz*cos(yaw) + * This transform is its own inverse for X,Z. + *

+ * FULL_MODEL rotation order matches renderer: vertex sees Z, then Y, then X. + */ + private static IVector3 applyFullModelToWorldOffset(FramePart fullModel, IEntityLivingBase IEntity, + double worldDX, double worldDY, double worldDZ, + float bodyYaw, float scale) { + float[] rotations = getRotations(fullModel, IEntity); + float[] pivots = getPivots(fullModel, IEntity); + + // Quick exit if no transform needed + boolean hasRotation = rotations[0] != 0 || rotations[1] != 0 || rotations[2] != 0; + boolean hasPivot = pivots[0] != 0 || pivots[1] != 0 || pivots[2] != 0; + if (!hasRotation && !hasPivot) { + return IVector3.createVectorHelper(worldDX, worldDY, worldDZ); + } + + // Convert IWorld offset to model space + // Body yaw + scale combined transform is an involution (self-inverse) for X,Z + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + + double mx = worldDX * cosYaw + worldDZ * sinYaw; + double mz = worldDX * sinYaw - worldDZ * cosYaw; + double my = -worldDY; // Invert Y (IWorld Y-up → model Y-down) + + // Apply FULL_MODEL rotations (vertex order: Z first, then Y, then X) + // Matches renderer: glRotatef(X), glRotatef(Y), glRotatef(Z) → vertex sees Z,Y,X + + // Z rotation + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + double nx = mx * cosZ - my * sinZ; + double ny = mx * sinZ + my * cosZ; + mx = nx; + my = ny; + + // Y rotation + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + nx = mx * cosY + mz * sinY; + double nz = -mx * sinY + mz * cosY; + mx = nx; + mz = nz; + + // X rotation + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); + ny = my * cosX - mz * sinX; + nz = my * sinX + mz * cosX; + my = ny; + mz = nz; + + // Apply pivots in model space (after rotation, matching GL vertex ordering) + // Renderer: glTranslatef(px, -py, pz) → pivot Y is negated for model Y-down space + mx += pivots[0] * MODEL_SCALE * scale; + my -= pivots[1] * MODEL_SCALE * scale; + mz += pivots[2] * MODEL_SCALE * scale; + + // Convert back to IWorld space (same involution for X,Z; invert Y back) + double newDX = mx * cosYaw + mz * sinYaw; + double newDZ = mx * sinYaw - mz * cosYaw; + double newDY = -my; + + return IVector3.createVectorHelper(newDX, newDY, newDZ); + } +} diff --git a/core/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java b/core/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java new file mode 100644 index 000000000..9392dc653 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java @@ -0,0 +1,449 @@ +package kamkeel.npcs.util; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.CustomAttributes; +import kamkeel.npcs.controllers.data.attribute.tracker.PlayerAttributeTracker; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicEntry; +import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.PlayerData; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import static kamkeel.npcs.controllers.AttributeController.getTracker; + +public class AttributeAttackUtil { + private static final Random random = new Random(); + + /** + * Crit decision from the last LivingAttackEvent for vanilla/modded mob targets. + * Set in LivingAttackEvent, consumed in LivingHurtEvent. + * null = no roll occurred, true = critted, false = did not crit. + */ + public static Boolean lastAttackCritted = null; + // --- Helper Classes & Methods --- + + // Container for allocation results. + public static class AllocationResult { + public Map allocation; + public float leftover; + + public AllocationResult(Map allocation, float leftover) { + this.allocation = allocation; + this.leftover = leftover; + } + } + + /** + * Allocates physical damage into magic allocations based on MagicData splits. + * Returns both a mapping (magic ID -> allocated damage) and any leftover physical damage. + */ + public static AllocationResult allocateMagicDamage(float physicalDamage, Map magicData) { + Map allocation = new HashMap<>(); + float totalSplit = 0f; + if (magicData != null) { + for (Map.Entry entry : magicData.entrySet()) { + int magicId = entry.getKey(); + float splitVal = entry.getValue().split; + float allocated = (physicalDamage * splitVal) + entry.getValue().damage; + allocation.put(magicId, allocated); + totalSplit += splitVal; + } + } + // Ensure double arithmetic for subtraction, then cast back to float. + float leftover = physicalDamage * (float) Math.max(0d, 1d - totalSplit); + return new AllocationResult(allocation, leftover); + } + + /** + * Adds extra magic damage from attribute sources if attributes are enabled. + */ + public static void addAttributeMagicDamage(Map allocation, + Map attributeDamage, + Map magicBoost) { + if (!ConfigMain.AttributesEnabled) return; + for (Map.Entry entry : attributeDamage.entrySet()) { + int magicId = entry.getKey(); + float damage = entry.getValue(); + float boost = magicBoost.getOrDefault(magicId, 0f); + damage *= (1 + (boost / 100f)); + allocation.put(magicId, allocation.getOrDefault(magicId, 0f) + damage); + } + } + + /** + * Applies magic interactions. For each magic in the allocation, multiplies its damage by (1 + bonus) + * for each opposing magic the defender possesses. + */ + public static void applyMagicInteractions(Map allocation, + Set defenderMagicIDs, + MagicController magicController) { + for (Map.Entry entry : allocation.entrySet()) { + int magicId = entry.getKey(); + float magicDamage = entry.getValue(); + Magic magic = magicController.getMagic(magicId); + if (magic != null && magic.interactions != null) { + float multiplier = 1f; + for (Map.Entry inter : magic.interactions.entrySet()) { + if (defenderMagicIDs.contains(inter.getKey())) { + multiplier *= (1 + inter.getValue()); + } + } + entry.setValue(magicDamage * multiplier); + } + } + } + + /** + * Applies defender magic defenses if attributes are enabled. + * Subtracts tracker-based magic defense (scaled by resistance) from each magic allocation. + */ + public static float applyDefenderMagicDefense(Map allocation, + PlayerAttributeTracker defender, + MagicController magicController) { + if (!ConfigMain.AttributesEnabled) return 0f; + float adjusted = 0f; + for (Map.Entry entry : allocation.entrySet()) { + int magicId = entry.getKey(); + if (magicController.getMagic(magicId) == null) continue; + float damage = entry.getValue(); + float defense = defender.magicDefense.getOrDefault(magicId, 0f); + float resistance = defender.magicResistance.getOrDefault(magicId, 0f); + float totalDefense = defense * (1 + (resistance / 100)); + damage -= totalDefense; + adjusted += Math.max(0, damage); + } + return adjusted; + } + + // --- Damage Calculation Functions --- + + /** + * Calculates damage when a player attacks another player. + */ + public static float calculateDamagePlayerToPlayer(IPlayer attackPlayer, IPlayer defendPlayer, float baseDamage) { + PlayerData attackerData = PlayerData.get(attackPlayer); + PlayerData defenderData = PlayerData.get(defendPlayer); + PlayerAttributeTracker attacker = getTracker(attackPlayer); + PlayerAttributeTracker defender = getTracker(defendPlayer); + + // Calculate physical damage. + float physicalDamage = applyMainAttack(baseDamage, attacker); + AllocationResult result = allocateMagicDamage(physicalDamage, attackerData.magicData.getMagics()); + float leftover = applyNeutral(result.leftover, attacker); + addAttributeMagicDamage(result.allocation, attacker.magicDamage, attacker.magicBoost); + + MagicController magicController = MagicController.getInstance(); + Set defenderMagicIDs = new HashSet<>(defenderData.magicData.getMagics().keySet()); + applyMagicInteractions(result.allocation, defenderMagicIDs, magicController); + float adjustedMagic = applyDefenderMagicDefense(result.allocation, defender, magicController); + + return applyCrit(leftover + adjustedMagic, attacker); + } + + /** + * Calculates damage when a player attacks an NPC. + */ + public static float calculateDamagePlayerToNPC(IPlayer attackPlayer, EntityNPCInterface npc, float baseDamage) { + PlayerData attackerData = PlayerData.get(attackPlayer); + PlayerAttributeTracker attacker = getTracker(attackPlayer); + + float physicalDamage = applyMainAttack(baseDamage, attacker); + AllocationResult result = allocateMagicDamage(physicalDamage, attackerData.magicData.getMagics()); + float leftover = applyNeutral(result.leftover, attacker); + + addAttributeMagicDamage(result.allocation, attacker.magicDamage, attacker.magicBoost); + + MagicController magicController = MagicController.getInstance(); + Set npcMagicIDs = new HashSet<>(); + if (npc.stats != null && npc.stats.magicData != null) + npcMagicIDs = new HashSet<>(npc.stats.magicData.getMagics().keySet()); + applyMagicInteractions(result.allocation, npcMagicIDs, magicController); + + float adjustedMagic = 0f; + for (float val : result.allocation.values()) + adjustedMagic += val; + + return applyCrit(leftover + adjustedMagic, attacker); + } + + public static float applyCrit(float damage, PlayerAttributeTracker tracker) { + if (ConfigMain.AttributesEnabled) { + if (rollCrit(tracker)) + damage = applyCritDamage(damage, tracker); + } + return damage; + } + + /** + * Rolls for a critical hit based on the tracker's CRITICAL_CHANCE attribute. + */ + public static boolean rollCrit(PlayerAttributeTracker tracker) { + if (ConfigMain.AttributesEnabled && tracker != null) { + float critChance = tracker.getAttributeValue(CustomAttributes.CRITICAL_CHANCE); + return random.nextFloat() < (critChance / 100f); + } + return false; + } + + /** + * Applies crit damage unconditionally (no chance roll). + * Use when the crit decision was already made via {@link #rollCrit}. + */ + public static float applyCritDamage(float damage, PlayerAttributeTracker tracker) { + if (ConfigMain.AttributesEnabled && tracker != null) { + float critBonus = tracker.getAttributeValue(CustomAttributes.CRITICAL_DAMAGE); + damage = (damage * (1 + (float) ConfigMain.AttributesCriticalBoost / 100f)) + critBonus; + } + return damage; + } + + public static float applyMainAttack(float damage, PlayerAttributeTracker tracker) { + if (ConfigMain.AttributesEnabled) { + float mainAttack = tracker.getAttributeValue(CustomAttributes.MAIN_ATTACK); + float mainBoost = tracker.getAttributeValue(CustomAttributes.MAIN_BOOST) / 100f; + damage = (damage * (1 + mainBoost)) + mainAttack; + } + return damage; + } + + public static float applyNeutral(float leftover, PlayerAttributeTracker tracker) { + if (ConfigMain.AttributesEnabled) { + float neutralDamage = tracker.getAttributeValue(CustomAttributes.NEUTRAL_ATTACK); + float neutralBoost = tracker.getAttributeValue(CustomAttributes.NEUTRAL_BOOST); + leftover += neutralDamage * (1 + (neutralBoost / 100f)); + } + return leftover; + } + + /** + * Calculates damage when an NPC attacks a player. + */ + public static float calculateDamageNPCtoPlayer(EntityNPCInterface npc, IPlayer defendingPlayer, float baseDamage) { + PlayerData defenderData = PlayerData.get(defendingPlayer); + PlayerAttributeTracker defender = getTracker(defendingPlayer); + + if (npc.stats == null || npc.stats.magicData == null) + return baseDamage; + + AllocationResult result = allocateMagicDamage(baseDamage, npc.stats.magicData.getMagics()); + float leftover = result.leftover; + + MagicController magicController = MagicController.getInstance(); + Set defenderMagicIDs = new HashSet<>(defenderData.magicData.getMagics().keySet()); + applyMagicInteractions(result.allocation, defenderMagicIDs, magicController); + float adjustedMagic = applyDefenderMagicDefense(result.allocation, defender, magicController); + return leftover + adjustedMagic; + } + + /** + * Calculates a player's maximum output ignoring defensive modifiers. + */ + public static float calculateGearOutput(PlayerAttributeTracker attacker) { + float neutralDamage = attacker.getAttributeValue(CustomAttributes.NEUTRAL_ATTACK); + float neutralBoost = 1 + (attacker.getAttributeValue(CustomAttributes.NEUTRAL_BOOST) / 100f); + float neutralTotal = neutralDamage * neutralBoost; + + float magicTotal = 0f; + MagicController magicController = MagicController.getInstance(); + for (Map.Entry entry : attacker.magicDamage.entrySet()) { + int magicId = entry.getKey(); + if (magicController.getMagic(magicId) != null) { + float attackMagic = entry.getValue(); + float boostMagic = attacker.magicBoost.getOrDefault(magicId, 0f); + magicTotal += Math.max(0, attackMagic * ((boostMagic / 100f) + 1f)); + } + } + return neutralTotal + magicTotal; + } + + /** + * Calculates a player's outgoing damage based on weapon damage + main-attack attributes, + * plus all neutral/magic damage from gear. + */ + public static float calculateOutgoing(IPlayer player, float baseDamage) { + if (!ConfigMain.AttributesEnabled) { + return baseDamage; + } + + PlayerAttributeTracker tracker = getTracker(player); + + // 1) Main-attack component + float mainAttackFlat = tracker.getAttributeValue(CustomAttributes.MAIN_ATTACK); + float mainBoostPercent = tracker.getAttributeValue(CustomAttributes.MAIN_BOOST) / 100f; + float mainDamage = (baseDamage * (1 + mainBoostPercent)) + mainAttackFlat; + + // 2) Gear component (neutral + magic) – gearOutput must itself divide its boosts by 100 internally + float gearDamage = tracker.gearOutput; + + return mainDamage + gearDamage; + } + + // --- Magic Interaction Utility --- + + /** + * Calculate magic interaction multiplier between attacker's magic types + * and defender's magic types. Used for barrier defense and IEntity-vs-IEntity interactions. + * + * @param attackerMagic Attacker's magic data (projectile or weapon) + * @param defenderMagic Defender's magic data (barrier or IEntity) + * @return Multiplier (1.0 = no interaction, >1.0 = bonus damage, <1.0 = resistance) + */ + public static float calculateMagicInteractionMultiplier(MagicData attackerMagic, MagicData defenderMagic) { + if (attackerMagic == null || defenderMagic == null) return 1.0f; + if (attackerMagic.isEmpty() || defenderMagic.isEmpty()) return 1.0f; + + MagicController mc = MagicController.getInstance(); + float multiplier = 1.0f; + + for (int attackMagicId : attackerMagic.getMagics().keySet()) { + Magic magic = mc.getMagic(attackMagicId); + if (magic == null || magic.interactions == null) continue; + for (Map.Entry interaction : magic.interactions.entrySet()) { + if (defenderMagic.getMagics().containsKey(interaction.getKey())) { + multiplier *= (1 + interaction.getValue()); + } + } + } + return multiplier; + } + + // --- Ability Damage Pipeline --- + + /** + * Apply gear magic boost percentages to the split-allocated magic damage. + * This multiplies each magic type's allocated damage by (1 + boost/100). + * Unlike {@link #addAttributeMagicDamage}, this does NOT add flat gear damage — + * it only applies the percentage multiplier from gear to the existing allocation. + */ + public static void applyMagicBoostToAllocation(Map allocation, Map magicBoost) { + if (!ConfigMain.AttributesEnabled) return; + for (Map.Entry entry : allocation.entrySet()) { + float boost = magicBoost.getOrDefault(entry.getKey(), 0f); + if (boost != 0f) { + entry.setValue(entry.getValue() * (1 + boost / 100f)); + } + } + } + + /** + * Apply gear magic boost percentages to barrier/dome health. + * Each magic type's split determines what portion of health is affected by its boost. + * E.g., Thunder [100%] dome + 50% Thunder gear boost = health * 1.5 + */ + public static float applyMagicBoostToHealth(IPlayer caster, MagicData magicData, float health) { + if (!ConfigMain.AttributesEnabled) return health; + if (magicData == null || magicData.isEmpty()) return health; + + PlayerAttributeTracker tracker = getTracker(caster); + if (tracker == null) return health; + + float multiplier = 1.0f; + for (Map.Entry entry : magicData.getMagics().entrySet()) { + float split = entry.getValue().split; + float boost = tracker.magicBoost.getOrDefault(entry.getKey(), 0f); + multiplier += split * (boost / 100f); + } + return health * multiplier; + } + + /** + * Apply the magic pipeline to ability damage. Works for both player and NPC casters. + * Abilities do NOT get main attack or critical boosts — they have their own scaling. + * This handles: + *

    + *
  • Magic allocation (splits from ability/caster magic data)
  • + *
  • Gear magic boost multipliers on split damage (player casters only, no flat gear damage)
  • + *
  • Magic interactions vs defender
  • + *
  • Defender magic defense/resistance (player defenders only)
  • + *
+ * If no magic data is present, damage passes through unchanged. + * + * @param caster Caster (player or NPC) + * @param target Target IEntity + * @param damage Base ability damage (already scaled by ability's own system) + * @param abilityMagic Resolved magic data (ability's own, or caster's fallback) + * @return Final damage after magic splits, boosts, interactions, and defense + */ + public static float calculateAbilityDamage(IEntityLivingBase caster, IEntityLivingBase target, + float damage, MagicData abilityMagic) { + if (abilityMagic == null || abilityMagic.isEmpty()) return damage; + + // 1. Allocate base damage into magic types using ability's splits + AllocationResult result = allocateMagicDamage(damage, abilityMagic.getMagics()); + float leftover = result.leftover; + + // 2. Apply gear magic boost multipliers to the split allocation (player casters only) + if (caster instanceof IPlayer && ConfigMain.AttributesEnabled) { + PlayerAttributeTracker tracker = getTracker((IPlayer) caster); + if (tracker != null) { + applyMagicBoostToAllocation(result.allocation, tracker.magicBoost); + } + } + + // 3. Magic interactions against target + MagicController magicController = MagicController.getInstance(); + Set defenderMagicIDs = null; + + if (target instanceof IPlayer) { + PlayerData defenderData = PlayerData.get((IPlayer) target); + if (defenderData != null) { + defenderMagicIDs = new HashSet<>(defenderData.magicData.getMagics().keySet()); + } + } else if (target instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) target; + if (npc.stats != null && npc.stats.magicData != null) { + defenderMagicIDs = new HashSet<>(npc.stats.magicData.getMagics().keySet()); + } + } + + if (defenderMagicIDs != null) { + applyMagicInteractions(result.allocation, defenderMagicIDs, magicController); + } + + // 4. Apply defender magic defense (player defenders with attributes only) + if (target instanceof IPlayer && ConfigMain.AttributesEnabled) { + PlayerAttributeTracker defenderTracker = getTracker((IPlayer) target); + if (defenderTracker != null) { + float adjustedMagic = applyDefenderMagicDefense(result.allocation, defenderTracker, magicController); + return leftover + adjustedMagic; + } + } + + // Sum magic damage + float magicTotal = 0f; + for (float val : result.allocation.values()) magicTotal += val; + return leftover + magicTotal; + } +} diff --git a/core/src/main/java/kamkeel/npcs/util/AttributeItemUtil.java b/core/src/main/java/kamkeel/npcs/util/AttributeItemUtil.java new file mode 100644 index 000000000..ee7eae76b --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/util/AttributeItemUtil.java @@ -0,0 +1,527 @@ +package kamkeel.npcs.util; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.CustomAttributes; +import kamkeel.npcs.controllers.AttributeController; +import kamkeel.npcs.controllers.data.attribute.AttributeDefinition; +import kamkeel.npcs.controllers.data.attribute.AttributeValueType; +import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; +import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Provides helper functions to write/read item attributes to/from NBT. + * All data is now stored under a hierarchical structure: + *

+ * "RPGCore" + * ├─ "Attributes" + * ├─ "Magic" + * └─ "Requirements" + */ +public class AttributeItemUtil { + // New hierarchical keys. + public static final String TAG_RPGCORE = "RPGCore"; + public static final String TAG_ATTRIBUTES = "Attributes"; + public static final String TAG_MAGIC = "Magic"; + public static final String TAG_REQUIREMENTS = "Requirements"; + + /** + * Applies a non–magic attribute to an item. + */ + public static void applyAttribute(IItemStack item, String attributeKey, float value) { + if (item == null) return; + if (item.stackTagCompound == null) { + item.stackTagCompound = new INbt(); + } + INbt root = item.stackTagCompound; + // Get (or create) the RPGCore compound. + INbt rpgCore = root.hasKey(TAG_RPGCORE) ? root.getCompoundTag(TAG_RPGCORE) : new INbt(); + // Get (or create) the Attributes compound. + INbt attrTag = rpgCore.hasKey(TAG_ATTRIBUTES) ? rpgCore.getCompoundTag(TAG_ATTRIBUTES) : new INbt(); + attrTag.setFloat(attributeKey, value); + rpgCore.setTag(TAG_ATTRIBUTES, attrTag); + root.setTag(TAG_RPGCORE, rpgCore); + } + + public static void applyAttribute(IItemStack item, AttributeDefinition definition, float value) { + applyAttribute(item, definition.getKey(), value); + } + + /** + * Removes a non–magic attribute from an item. + */ + public static void removeAttribute(IItemStack item, String attributeKey) { + if (item == null || item.stackTagCompound == null) return; + INbt root = item.stackTagCompound; + if (root.hasKey(TAG_RPGCORE)) { + INbt rpgCore = root.getCompoundTag(TAG_RPGCORE); + if (rpgCore.hasKey(TAG_ATTRIBUTES)) { + INbt attrTag = rpgCore.getCompoundTag(TAG_ATTRIBUTES); + attrTag.removeTag(attributeKey); + if (attrTag.func_150296_c().isEmpty()) { + rpgCore.removeTag(TAG_ATTRIBUTES); + } else { + rpgCore.setTag(TAG_ATTRIBUTES, attrTag); + } + root.setTag(TAG_RPGCORE, rpgCore); + } + } + } + + /** + * Reads non–magic attributes from an item. + */ + public static Map readAttributes(IItemStack item) { + Map map = new HashMap<>(); + if (item == null || item.stackTagCompound == null) return map; + INbt root = item.stackTagCompound; + if (root.hasKey(TAG_RPGCORE)) { + INbt rpgCore = root.getCompoundTag(TAG_RPGCORE); + if (rpgCore.hasKey(TAG_ATTRIBUTES)) { + INbt attrTag = rpgCore.getCompoundTag(TAG_ATTRIBUTES); + Set keys = attrTag.func_150296_c(); + for (String key : keys) { + map.put(key, attrTag.getFloat(key)); + } + } + } + return map; + } + + /** + * Reads a magic attribute map from an item. + * The given attributeTag is the key under which the compound is stored (e.g., MAGIC_DAMAGE_KEY). + */ + public static Map readMagicAttributeMap(IItemStack item, String attributeTag) { + Map map = new HashMap<>(); + if (item == null || item.stackTagCompound == null) return map; + INbt root = item.stackTagCompound; + if (root.hasKey(TAG_RPGCORE)) { + INbt rpgCore = root.getCompoundTag(TAG_RPGCORE); + if (rpgCore.hasKey(TAG_MAGIC)) { + INbt magicCompound = rpgCore.getCompoundTag(TAG_MAGIC); + if (magicCompound.hasKey(attributeTag)) { + INbt magicMap = magicCompound.getCompoundTag(attributeTag); + Set keys = magicMap.func_150296_c(); + for (String key : keys) { + try { + int magicId = Integer.parseInt(key); + map.put(magicId, magicMap.getFloat(key)); + } catch (NumberFormatException e) { + // Skip invalid key. + } + } + } + } + } + return map; + } + + /** + * Applies (writes) a magic attribute to an item. + */ + public static void applyMagicAttribute(IItemStack item, String attributeTag, int magicId, float value) { + writeMagicAttribute(item, attributeTag, magicId, value); + } + + /** + * Writes a magic attribute value to the given attributeTag. + */ + public static void writeMagicAttribute(IItemStack item, String attributeTag, int magicId, float value) { + if (item == null) return; + if (item.stackTagCompound == null) + item.stackTagCompound = new INbt(); + INbt root = item.stackTagCompound; + // Get or create RPGCore compound. + INbt rpgCore = root.hasKey(TAG_RPGCORE) ? root.getCompoundTag(TAG_RPGCORE) : new INbt(); + // Get or create Magic compound. + INbt magicCompound = rpgCore.hasKey(TAG_MAGIC) ? rpgCore.getCompoundTag(TAG_MAGIC) : new INbt(); + // Get or create the specific magic map. + INbt magicMap = magicCompound.hasKey(attributeTag) ? magicCompound.getCompoundTag(attributeTag) : new INbt(); + magicMap.setFloat(String.valueOf(magicId), value); + magicCompound.setTag(attributeTag, magicMap); + rpgCore.setTag(TAG_MAGIC, magicCompound); + root.setTag(TAG_RPGCORE, rpgCore); + } + + /** + * Removes a magic attribute value from the given attributeTag. + */ + public static void removeMagicAttribute(IItemStack item, String attributeTag, int magicId) { + if (item == null || item.stackTagCompound == null) + return; + INbt root = item.stackTagCompound; + if (root.hasKey(TAG_RPGCORE)) { + INbt rpgCore = root.getCompoundTag(TAG_RPGCORE); + if (rpgCore.hasKey(TAG_MAGIC)) { + INbt magicCompound = rpgCore.getCompoundTag(TAG_MAGIC); + if (magicCompound.hasKey(attributeTag)) { + INbt magicMap = magicCompound.getCompoundTag(attributeTag); + magicMap.removeTag(String.valueOf(magicId)); + if (magicMap.func_150296_c().isEmpty()) + magicCompound.removeTag(attributeTag); + else + magicCompound.setTag(attributeTag, magicMap); + rpgCore.setTag(TAG_MAGIC, magicCompound); + root.setTag(TAG_RPGCORE, rpgCore); + } + } + } + } + + + public static void applyRequirement(IItemStack item, String reqKey, Object value) { + if (item == null) return; + if (item.stackTagCompound == null) + item.stackTagCompound = new INbt(); + INbt root = item.stackTagCompound; + // Get (or create) the RPGCore compound. + INbt rpgCore = root.hasKey(TAG_RPGCORE) ? root.getCompoundTag(TAG_RPGCORE) : new INbt(); + // Get (or create) the Requirements compound. + INbt reqTag = rpgCore.hasKey(TAG_REQUIREMENTS) ? rpgCore.getCompoundTag(TAG_REQUIREMENTS) : new INbt(); + // Retrieve the checker from the registry. + IRequirementChecker checker = RequirementCheckerRegistry.getChecker(reqKey); + if (checker != null) { + checker.apply(reqTag, value); + } + rpgCore.setTag(TAG_REQUIREMENTS, reqTag); + root.setTag(TAG_RPGCORE, rpgCore); + } + + public static void removeRequirement(IItemStack item, String reqKey) { + if (item == null || item.stackTagCompound == null) return; + INbt root = item.stackTagCompound; + if (root.hasKey(TAG_RPGCORE)) { + INbt rpgCore = root.getCompoundTag(TAG_RPGCORE); + if (rpgCore.hasKey(TAG_REQUIREMENTS)) { + INbt reqTag = rpgCore.getCompoundTag(TAG_REQUIREMENTS); + reqTag.removeTag(reqKey); + if (reqTag.func_150296_c().isEmpty()) { + rpgCore.removeTag(TAG_REQUIREMENTS); + } else { + rpgCore.setTag(TAG_REQUIREMENTS, reqTag); + } + root.setTag(TAG_RPGCORE, rpgCore); + } + } + } + + // ----------------- Tooltip Generation with Custom Sorting ------------------- + + /** + * Returns the item tooltip. + */ + @ClientOnly + public static List getToolTip(List original, INbt compound, boolean override) { + List tooltip = new ArrayList<>(original); + // Get the RPGCore compound and then the Attributes compound. + INbt rpgCore = compound.hasKey(TAG_RPGCORE) ? compound.getCompoundTag(TAG_RPGCORE) : new INbt(); + INbt attrTag = rpgCore.hasKey(TAG_ATTRIBUTES) ? rpgCore.getCompoundTag(TAG_ATTRIBUTES) : new INbt(); + if (KeyPressHandler.isKeyBindDown(ClientProxy.NPCButton) || override) { + List newTooltips = new ArrayList<>(); + if (!tooltip.isEmpty() && !override) { + newTooltips.add(tooltip.get(0)); + } + + // Instead of storing plain strings, we wrap each line in a TooltipEntry + List baseList = new ArrayList<>(); + List modifierList = new ArrayList<>(); + List statsList = new ArrayList<>(); + List infoList = new ArrayList<>(); + List extraList = new ArrayList<>(); + + // Process non–magic attributes. + Set keys = attrTag.func_150296_c(); + for (String key : keys) { + // For non–magic we include everything. + Float value = attrTag.getFloat(key); + AttributeDefinition def = AttributeController.getAttribute(key); + if (def == null) + continue; + + AttributeDefinition.AttributeSection section = def != null ? def.getSection() : AttributeDefinition.AttributeSection.EXTRA; + String plainName = getTranslatedAttributeName(key, def); // unformatted name + String formattedLine = formatAttributeLine(def, section, value, plainName); + TooltipEntry entry = new TooltipEntry(plainName, formattedLine); + switch (section) { + case BASE: + baseList.add(entry); + break; + case MODIFIER: + modifierList.add(entry); + break; + case STATS: + statsList.add(entry); + break; + case INFO: + infoList.add(entry); + break; + default: + extraList.add(entry); + break; + } + } + + // Process magic attributes. + processMagicAttributes(compound, baseList, modifierList, infoList, extraList); + + // Define custom order maps for Base and Modifier sections. + // For Base: Health, Main Attack Damage, Neutral Damage come first. + Map baseOrder = new HashMap<>(); + baseOrder.put("Health", 1); + baseOrder.put("Main Attack Damage", 2); + baseOrder.put("Neutral Damage", 3); + + // For Modifier: Main Attack Damage then Neutral Damage. + Map modOrder = new HashMap<>(); + modOrder.put("Health Boost", 1); + modOrder.put("Main Attack Damage", 2); + modOrder.put("Neutral Damage", 3); + modOrder.put("Movement Speed", 4); + modOrder.put("Knockback Resistance", 5); + + // Build sections using our custom sorting. + newTooltips.addAll(buildSection(baseList, baseOrder)); + newTooltips.addAll(buildSection(modifierList, modOrder)); + newTooltips.addAll(buildSection(statsList)); // alphabetical + newTooltips.addAll(buildSection(infoList)); // alphabetical + newTooltips.addAll(buildSection(extraList)); // alphabetical + + if (rpgCore.hasKey(TAG_REQUIREMENTS)) { + INbt reqTag = rpgCore.getCompoundTag(TAG_REQUIREMENTS); + List reqEntries = new ArrayList<>(); + + Minecraft mc = Minecraft.getMinecraft(); + IPlayer clientPlayer = mc.thePlayer; + Set requirements = reqTag.func_150296_c(); + for (String reqKey : requirements) { + IRequirementChecker checker = RequirementCheckerRegistry.getChecker(reqKey); + if (checker != null) { + boolean met = clientPlayer != null && checker.check(clientPlayer, reqTag); + String tooltipValue = checker.getTooltipValue(reqTag); + String color = met ? TextFormatting.GRAY.toString() : TextFormatting.RED.toString(); + String line = TextFormatting.GRAY + PlatformServiceHolder.get().translateToLocal(checker.getTranslation()) + ": " + color + tooltipValue; + reqEntries.add(new TooltipEntry(stripFormatting(PlatformServiceHolder.get().translateToLocal(checker.getTranslation())), line)); + } + } + newTooltips.addAll(buildSection(reqEntries)); + } + + if(override){ + tooltip.addAll(newTooltips); + } else { + tooltip = newTooltips; + } + } else { + String keyName = GameSettings.getKeyDisplayString(ClientProxy.NPCButton.getKeyCode()); + tooltip.add(TextFormatting.YELLOW + "" + TextFormatting.ITALIC + + PlatformServiceHolder.get().translateToLocal("rpgcore:tooltip").replace("%key%", keyName)); + } + return tooltip; + } + + /** + * Processes magic attributes from the compound. + * For each magic key, it retrieves the stored magic attributes from the "Magic" compound, + * formats each line and adds it to the proper section list. + */ + private static void processMagicAttributes(INbt compound, List baseList, List modifierList, + List infoList, List extraList) { + if (!compound.hasKey(TAG_RPGCORE)) return; + INbt rpgCore = compound.getCompoundTag(TAG_RPGCORE); + if (!rpgCore.hasKey(TAG_MAGIC)) return; + INbt magicCompound = rpgCore.getCompoundTag(TAG_MAGIC); + String[] magicKeys = { + CustomAttributes.MAGIC_DAMAGE_KEY, + CustomAttributes.MAGIC_BOOST_KEY, + CustomAttributes.MAGIC_DEFENSE_KEY, + CustomAttributes.MAGIC_RESISTANCE_KEY + }; + for (String magicKey : magicKeys) { + if (magicCompound.hasKey(magicKey)) { + INbt magicTag = magicCompound.getCompoundTag(magicKey); + AttributeDefinition def = AttributeController.getAttribute(magicKey); + AttributeDefinition.AttributeSection section = def != null ? def.getSection() : AttributeDefinition.AttributeSection.EXTRA; + Set keys = magicTag.func_150296_c(); + for (String key : keys) { + try { + int magicId = Integer.parseInt(key); + Float value = magicTag.getFloat(key); + Magic magic = MagicController.getInstance().getMagic(magicId); + if (magic != null) { + // Build the magic display name without formatting for sorting. + String rawMagicName = magic.getDisplayName().replace("&", "\u00A7") + " \u00A77" + getMagicAppendix(magicKey); + String plainName = stripFormatting(rawMagicName); + String formattedLine = formatAttributeLine(def, section, value, rawMagicName); + TooltipEntry entry = new TooltipEntry(plainName, formattedLine); + switch (section) { + case BASE: + baseList.add(entry); + break; + case MODIFIER: + modifierList.add(entry); + break; + case INFO: + infoList.add(entry); + break; + default: + extraList.add(entry); + break; + } + } + } catch (NumberFormatException e) { + // Skip invalid key. + } + } + } + } + } + + @ClientOnly + private static String getMagicAppendix(String type) { + switch (type) { + case CustomAttributes.MAGIC_DEFENSE_KEY: + return PlatformServiceHolder.get().translateToLocal("rpgcore:attribute.defense"); + case CustomAttributes.MAGIC_RESISTANCE_KEY: + return PlatformServiceHolder.get().translateToLocal("rpgcore:attribute.resistance"); + default: + return PlatformServiceHolder.get().translateToLocal("rpgcore:attribute.damage"); + } + } + + @ClientOnly + private static String getTranslatedAttributeName(String key, AttributeDefinition def) { + key = def != null ? def.getTranslationKey() : key; + String translation = null; + if (StatCollector.canTranslate(key)) + translation = PlatformServiceHolder.get().translateToLocal(key); + if (translation == null && def != null) + translation = def.getDisplayName(); + else if (translation == null) + translation = key; + return translation; + } + + private static String formatAttributeLine(AttributeDefinition def, AttributeDefinition.AttributeSection section, + Float value, String displayName) { + String formattedValue = formatFloat(value); + if (section == AttributeDefinition.AttributeSection.STATS) { + String sign = value >= 0 ? "+" : ""; + String color = value >= 0 ? TextFormatting.GREEN.toString() : TextFormatting.RED.toString(); + String valueString = color + sign + formattedValue; + if (def != null && def.getValueType() == AttributeValueType.PERCENT) + valueString += "%"; + valueString += TextFormatting.GRAY; + if (def != null) + displayName = "\u00A7" + def.getColorCode() + displayName; + else + displayName = TextFormatting.AQUA + displayName; + return valueString + " " + displayName; + } else if (section == AttributeDefinition.AttributeSection.MODIFIER || section == AttributeDefinition.AttributeSection.INFO) { + String sign = value >= 0 ? "+" : ""; + String color = value >= 0 ? TextFormatting.GREEN.toString() : TextFormatting.RED.toString(); + String valueString = color + sign + formattedValue; + if (def != null && (def.getValueType() == AttributeValueType.PERCENT || def.getValueType() == AttributeValueType.MAGIC)) + valueString += "%"; + valueString += TextFormatting.GRAY; + return valueString + " " + displayName; + } else { + if (def != null) + displayName = "\u00A7" + def.getColorCode() + displayName; + else + displayName = TextFormatting.AQUA + displayName; + + String sign = value >= 0 ? "+" : ""; + String color = value >= 0 ? TextFormatting.GREEN.toString() : TextFormatting.RED.toString(); + String valueString = color + sign + formattedValue; + return displayName + "\u00A77: " + valueString; + } + } + + private static String formatFloat(Float value) { + return new java.math.BigDecimal(Float.toString(value)).stripTrailingZeros().toPlainString(); + } + + // ---------------- Helper Methods for Sorting Tooltip Entries ---------------- + + // A small container to hold a tooltip line and its plain sort key. + private static class TooltipEntry { + public String sortKey; + public String line; + + public TooltipEntry(String sortKey, String line) { + this.sortKey = sortKey; + this.line = line; + } + } + + // Removes formatting codes (e.g., '§') from a string. + private static String stripFormatting(String input) { + return input == null ? "" : Pattern.compile("(?i)§[0-9A-FK-OR]").matcher(input).replaceAll(""); + } + + // Build a section with default alphabetical order. + private static List buildSection(List entries) { + Collections.sort(entries, (a, b) -> a.sortKey.compareToIgnoreCase(b.sortKey)); + List section = new ArrayList<>(); + if (!entries.isEmpty()) { + section.add(""); + for (TooltipEntry entry : entries) { + section.add(entry.line); + } + } + return section; + } + + // Build a section using a custom order map. + private static List buildSection(List entries, Map orderMap) { + Collections.sort(entries, (a, b) -> { + int pa = orderMap.containsKey(a.sortKey) ? orderMap.get(a.sortKey) : Integer.MAX_VALUE; + int pb = orderMap.containsKey(b.sortKey) ? orderMap.get(b.sortKey) : Integer.MAX_VALUE; + if (pa != pb) return Integer.compare(pa, pb); + return a.sortKey.compareToIgnoreCase(b.sortKey); + }); + List section = new ArrayList<>(); + if (!entries.isEmpty()) { + section.add(""); + for (TooltipEntry entry : entries) { + section.add(entry.line); + } + } + return section; + } +} diff --git a/core/src/main/java/kamkeel/npcs/util/ByteBufUtils.java b/core/src/main/java/kamkeel/npcs/util/ByteBufUtils.java new file mode 100644 index 000000000..5905aaaf6 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/util/ByteBufUtils.java @@ -0,0 +1,12 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.util.ByteBufUtils; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class ByteBufUtils { + public static noppes.npcs.api.INbt readNBT(Object buffer) { return null; } + public static void writeNBT(Object buffer, Object nbt) {} +} \ No newline at end of file diff --git a/core/src/main/java/kamkeel/npcs/util/IVector3.java b/core/src/main/java/kamkeel/npcs/util/IVector3.java new file mode 100644 index 000000000..fd64389c2 --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/util/IVector3.java @@ -0,0 +1,28 @@ +package kamkeel.npcs.util; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * 3D vector utility class. + */ +public class IVector3 { + public double x, y, z; + public IVector3() {} + public IVector3(double x, double y, double z) { this.x = x; this.y = y; this.z = z; } +} diff --git a/core/src/main/java/kamkeel/npcs/util/Register.java b/core/src/main/java/kamkeel/npcs/util/Register.java new file mode 100644 index 000000000..53f4f092b --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/util/Register.java @@ -0,0 +1,243 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction; +import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; +import noppes.npcs.LogWriter; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class Register { + public static final Map> REGISTERED_NAMESPACES = new LinkedHashMap<>(); + public static final Map NAMESPACE_DISPLAY_NAMES = new LinkedHashMap<>(); + protected final String registryKey; + protected final String namespace; + protected final Map> entries = new LinkedHashMap<>(); + + private Register(String registryKey, String namespace) { + this.registryKey = registryKey; + this.namespace = namespace; + } + + public T register(String factoryName, Supplier factory) { + entries.put(registryKey + "." + namespace + "." + factoryName, factory); + return factory.get(); + } + + public static boolean isEmpty(String registryKey) { + if (REGISTERED_NAMESPACES.isEmpty()) return true; + + List list = REGISTERED_NAMESPACES.get(registryKey); + if (list == null || list.isEmpty()) return true; + + return false; + } + + public static class Abilities extends Register { + protected final Map>> variantEntries = new LinkedHashMap<>(); + protected final Map uniqueNames = new LinkedHashMap<>(); + protected final Set typeOnly = new HashSet<>(); + + private Abilities(String namespace) { + super("ability", namespace); + } + + @Override + public Ability register(String factoryName, Supplier factory) { + String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"); + entries.put(name, factory); + uniqueNames.put(name, factoryName); + return factory.get(); + } + + /** + * Register a type only (shell template, no built-in preset). + * The type will appear in the ability type picker but won't be + * available as a pre-configured ability by name. + */ + public Ability registerType(String factoryName, Supplier factory) { + String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"); + entries.put(name, factory); + uniqueNames.put(name, factoryName); + typeOnly.add(name); + return factory.get(); + } + + public AbilityVariant registerVariant(String typeId, String variantName, String group, Consumer configurator) { + return registerVariant(typeId, variantName, () -> new AbilityVariant(variantName, group, configurator)); + } + + public AbilityVariant registerVariant(String typeId, String factoryName, Supplier factory) { + if (!variantEntries.containsKey(typeId)) { + variantEntries.put(typeId, new HashMap<>()); + } + + Map> type = variantEntries.get(typeId); + + String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"); + type.put(name, factory); + return factory.get(); + } + + public void register() { + for (Map.Entry> entry : entries.entrySet()) { + AbilityController.Instance.registerType(entry.getValue()); + if (!typeOnly.contains(entry.getKey())) { + AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get()); + } + } + + for (Map.Entry>> entry : variantEntries.entrySet()) { + String typeId = entry.getKey(); + Map> map = entry.getValue(); + for (Map.Entry> innerEntry : map.entrySet()) { + AbilityController.Instance.registerVariant(typeId, innerEntry.getValue().get()); + } + } + } + + public static Register.Abilities create(String namespace, String displayName) { + if (!REGISTERED_NAMESPACES.containsKey("ability")) + REGISTERED_NAMESPACES.put("ability", new ArrayList<>()); + + if (REGISTERED_NAMESPACES.get("ability").contains(namespace)) { + LogWriter.error("REGISTER ABILITIES: Namespace " + namespace + " already registered!"); + } + + REGISTERED_NAMESPACES.get("ability").add(namespace); + NAMESPACE_DISPLAY_NAMES.put(namespace, displayName); + + return new Register.Abilities(namespace); + } + } + + public static class Animations extends Register { + private final Class modClass; + private final String animationsPath; + + private Animations(Class modClass, String animationsPath, String namespace) { + super("animation", namespace); + this.animationsPath = animationsPath; + this.modClass = modClass; + } + + @Override + public Animation register(String factoryName, Supplier factory) { + return super.register(factoryName, factory); + } + + public Animation[] registerBundle(String factoryName, Supplier factory, String... appendixes) { + List animations = new ArrayList<>(); + for (String appendix : appendixes) { + entries.put(registryKey + "." + namespace + "." + factoryName + "_" + appendix, factory); + animations.add(factory.get()); + } + + return animations.toArray(new Animation[0]); + } + + public void register() { + try { + String path = "/assets/" + namespace + "/" + animationsPath; + for (Map.Entry> entry : entries.entrySet()) { + String prefix = registryKey + "." + namespace + "."; + String key = entry.getKey(); + String name = key.substring(prefix.length()); + AnimationController.Instance.loadBuiltInAnimation(modClass, path, name); + } + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + + public static Register.Animations create(Class modClass, String animationsPath, String namespace) { + if (!REGISTERED_NAMESPACES.containsKey("animation")) + REGISTERED_NAMESPACES.put("animation", new ArrayList<>()); + + if (REGISTERED_NAMESPACES.get("animation").contains(namespace)) { + LogWriter.error("REGISTER ANIMATIONS: Namespace " + namespace + " already registered!"); + } + + REGISTERED_NAMESPACES.get("animation").add(namespace); + + return new Register.Animations(modClass, animationsPath, namespace); + } + } + + public static class EffectActions extends Register { + + private EffectActions(String namespace) { + super("effect_action", namespace); + } + + public IEffectAction register(String name, IEffectAction action) { + String key = namespace + ":" + name.trim().toLowerCase().replaceAll(" ", "_"); + entries.put(key, () -> action); + return action; + } + + public void register() { + for (Map.Entry> entry : entries.entrySet()) { + AbilityController.Instance.registerEffectAction(entry.getValue().get()); + } + } + + public static Register.EffectActions create(String namespace, String displayName) { + if (!REGISTERED_NAMESPACES.containsKey("effect_action")) + REGISTERED_NAMESPACES.put("effect_action", new ArrayList<>()); + + if (REGISTERED_NAMESPACES.get("effect_action").contains(namespace)) { + LogWriter.error("REGISTER EFFECT ACTIONS: Namespace " + namespace + " already registered!"); + } + + REGISTERED_NAMESPACES.get("effect_action").add(namespace); + NAMESPACE_DISPLAY_NAMES.put(namespace, displayName); + + return new Register.EffectActions(namespace); + } + } + + public static class Conditions extends Register { + + private Conditions(String namespace) { + super("condition", namespace); + } + + public AbilityCondition register(String name, Supplier factory) { + entries.put(registryKey + "." + namespace + "." + name, factory); + return factory.get(); + } + + public void register() { + for (Map.Entry> entry : entries.entrySet()) { + AbilityController.Instance.registerCondition(entry.getValue()); + } + } + + public static Register.Conditions create(String namespace, String displayName) { + if (!REGISTERED_NAMESPACES.containsKey("condition")) + REGISTERED_NAMESPACES.put("condition", new ArrayList<>()); + + if (REGISTERED_NAMESPACES.get("condition").contains(namespace)) { + LogWriter.error("REGISTER CONDITIONS: Namespace " + namespace + " already registered!"); + } + + REGISTERED_NAMESPACES.get("condition").add(namespace); + NAMESPACE_DISPLAY_NAMES.put(namespace, displayName); + + return new Register.Conditions(namespace); + } + } +} diff --git a/core/src/main/java/kamkeel/npcs/util/VaultUtil.java b/core/src/main/java/kamkeel/npcs/util/VaultUtil.java new file mode 100644 index 000000000..0b88e97bc --- /dev/null +++ b/core/src/main/java/kamkeel/npcs/util/VaultUtil.java @@ -0,0 +1,611 @@ +package kamkeel.npcs.util; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.Method; + +/** + * Utility class for Vault Economy integration. + * Uses reflection to access Vault API when available. + * Requires BukkitUtil to be initialized first. + */ +public class VaultUtil { + private static final Logger logger = LogManager.getLogger(VaultUtil.class); + + private static boolean initialized = false; + private static boolean vaultEnabled = false; + + // Vault classes + private static Class economyClass; + private static Class economyResponseClass; + + // Economy methods + private static Method isEnabled; + private static Method getName; + private static Method hasAccount; + private static Method hasAccountOffline; + private static Method getBalance; + private static Method getBalanceOffline; + private static Method has; + private static Method hasOffline; + private static Method withdrawPlayer; + private static Method withdrawPlayerOffline; + private static Method depositPlayer; + private static Method depositPlayerOffline; + private static Method format; + private static Method currencyNamePlural; + private static Method currencyNameSingular; + private static Method createPlayerAccount; + private static Method createPlayerAccountOffline; + private static Method hasBankSupport; + private static Method getBanks; + private static Method bankBalance; + private static Method bankHas; + private static Method bankWithdraw; + private static Method bankDeposit; + + // EconomyResponse methods + private static Method transactionSuccess; + + // Cached economy instance + private static Object economyInstance; + + /** + * Initializes the Vault integration using reflection. + * Called automatically by BukkitUtil after Bukkit is confirmed available. + */ + public static void init() { + if (initialized) { + return; + } + initialized = true; + + if (!BukkitUtil.isEnabled()) { + logger.debug("Bukkit not available, Vault integration disabled"); + return; + } + + try { + Class offlinePlayerClass = BukkitUtil.getOfflinePlayerClass(); + if (offlinePlayerClass == null) { + logger.error("Could not get OfflinePlayer class from BukkitUtil"); + return; + } + + // Load Vault classes + Object vault = BukkitUtil.getPlugin("Vault"); + if (vault == null) + return; + ClassLoader vaultLoader = vault.getClass().getClassLoader(); + economyClass = vaultLoader.loadClass("net.milkbowl.vault.economy.Economy"); + economyResponseClass = vaultLoader.loadClass("net.milkbowl.vault.economy.EconomyResponse"); + + // Get Economy methods (using deprecated String methods for broader compatibility) + isEnabled = economyClass.getMethod("isEnabled"); + getName = economyClass.getMethod("getName"); + hasAccount = economyClass.getMethod("hasAccount", String.class); + hasAccountOffline = economyClass.getMethod("hasAccount", offlinePlayerClass); + getBalance = economyClass.getMethod("getBalance", String.class); + getBalanceOffline = economyClass.getMethod("getBalance", offlinePlayerClass); + has = economyClass.getMethod("has", String.class, double.class); + hasOffline = economyClass.getMethod("has", offlinePlayerClass, double.class); + withdrawPlayer = economyClass.getMethod("withdrawPlayer", String.class, double.class); + withdrawPlayerOffline = economyClass.getMethod("withdrawPlayer", offlinePlayerClass, double.class); + depositPlayer = economyClass.getMethod("depositPlayer", String.class, double.class); + depositPlayerOffline = economyClass.getMethod("depositPlayer", offlinePlayerClass, double.class); + format = economyClass.getMethod("format", double.class); + currencyNamePlural = economyClass.getMethod("currencyNamePlural"); + currencyNameSingular = economyClass.getMethod("currencyNameSingular"); + createPlayerAccount = economyClass.getMethod("createPlayerAccount", String.class); + createPlayerAccountOffline = economyClass.getMethod("createPlayerAccount", offlinePlayerClass); + hasBankSupport = economyClass.getMethod("hasBankSupport"); + getBanks = economyClass.getMethod("getBanks"); + bankBalance = economyClass.getMethod("bankBalance", String.class); + bankHas = economyClass.getMethod("bankHas", String.class, double.class); + bankWithdraw = economyClass.getMethod("bankWithdraw", String.class, double.class); + bankDeposit = economyClass.getMethod("bankDeposit", String.class, double.class); + + // Get EconomyResponse methods + transactionSuccess = economyResponseClass.getMethod("transactionSuccess"); + + // Get the economy instance + economyInstance = BukkitUtil.getServiceProvider(economyClass); + + if (economyInstance != null) { + vaultEnabled = true; + logger.info("Vault Economy integration enabled - Provider: " + getEconomyName()); + } else { + logger.info("Vault found but no Economy provider registered"); + } + + } catch (ClassNotFoundException e) { + logger.debug("Vault not found, economy integration disabled"); + } catch (NoSuchMethodException e) { + logger.error("Vault API method not found, economy integration disabled", e); + } catch (Exception e) { + logger.error("Error initializing Vault integration", e); + } + } + + /** + * Refreshes the economy provider. Call this if the economy plugin loads after initialization. + */ + public static void refreshEconomyProvider() { + if (!initialized || !BukkitUtil.isEnabled()) { + return; + } + + try { + economyInstance = BukkitUtil.getServiceProvider(economyClass); + if (economyInstance != null) { + vaultEnabled = true; + logger.info("Vault Economy provider refreshed: " + getEconomyName()); + } + } catch (Exception e) { + logger.error("Error refreshing Vault economy provider", e); + } + } + + /** + * @return true if Vault economy integration is enabled and working + */ + public static boolean isEnabled() { + if (!vaultEnabled || economyInstance == null) return false; + + try { + return (Boolean) isEnabled.invoke(economyInstance); + } catch (Exception e) { + logger.error("Error checking if economy is enabled", e); + return false; + } + } + + /** + * @return the name of the economy provider, or null if not available + */ + public static String getEconomyName() { + if (!vaultEnabled || economyInstance == null) return null; + + try { + return (String) getName.invoke(economyInstance); + } catch (Exception e) { + logger.error("Error getting economy name", e); + return null; + } + } + + /** + * Checks if a player has an account in the economy system. + * + * @param player the player to check + * @return true if the player has an account + */ + public static boolean hasAccount(IPlayer player) { + return hasAccount(player.getCommandSenderName()); + } + + /** + * Checks if a player has an account in the economy system. + * + * @param playerName the player name to check + * @return true if the player has an account + */ + public static boolean hasAccount(String playerName) { + if (!vaultEnabled || economyInstance == null) return false; + + try { + Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName); + if (offlinePlayer != null) { + return (Boolean) hasAccountOffline.invoke(economyInstance, offlinePlayer); + } + return (Boolean) hasAccount.invoke(economyInstance, playerName); + } catch (Exception e) { + logger.error("Error checking if player has account: " + playerName, e); + return false; + } + } + + /** + * Gets the balance of a player. + * + * @param player the player + * @return the player's balance, or 0 if not available + */ + public static double getBalance(IPlayer player) { + return getBalance(player.getCommandSenderName()); + } + + /** + * Gets the balance of a player by name. + * + * @param playerName the player name + * @return the player's balance, or 0 if not available + */ + public static double getBalance(String playerName) { + if (!vaultEnabled || economyInstance == null) return 0; + + try { + Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName); + if (offlinePlayer != null) { + return (Double) getBalanceOffline.invoke(economyInstance, offlinePlayer); + } + return (Double) getBalance.invoke(economyInstance, playerName); + } catch (Exception e) { + logger.error("Error getting balance for player: " + playerName, e); + return 0; + } + } + + /** + * Checks if a player has at least the specified amount. + * + * @param player the player + * @param amount the amount to check + * @return true if the player has at least the specified amount + */ + public static boolean has(IPlayer player, double amount) { + return has(player.getCommandSenderName(), amount); + } + + /** + * Checks if a player has at least the specified amount. + * + * @param playerName the player name + * @param amount the amount to check + * @return true if the player has at least the specified amount + */ + public static boolean has(String playerName, double amount) { + if (!vaultEnabled || economyInstance == null) return false; + + try { + Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName); + if (offlinePlayer != null) { + return (Boolean) hasOffline.invoke(economyInstance, offlinePlayer, amount); + } + return (Boolean) has.invoke(economyInstance, playerName, amount); + } catch (Exception e) { + logger.error("Error checking if player has amount: " + playerName, e); + return false; + } + } + + /** + * Withdraws money from a player's account. + * + * @param player the player + * @param amount the amount to withdraw + * @return true if the transaction was successful + */ + public static boolean withdrawMoney(IPlayer player, double amount) { + return withdrawMoney(player.getCommandSenderName(), amount); + } + + /** + * Withdraws money from a player's account. + * + * @param playerName the player name + * @param amount the amount to withdraw + * @return true if the transaction was successful + */ + public static boolean withdrawMoney(String playerName, double amount) { + if (!vaultEnabled || economyInstance == null) return false; + + try { + Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName); + Object response; + if (offlinePlayer != null) { + response = withdrawPlayerOffline.invoke(economyInstance, offlinePlayer, amount); + } else { + response = withdrawPlayer.invoke(economyInstance, playerName, amount); + } + return (Boolean) transactionSuccess.invoke(response); + } catch (Exception e) { + logger.error("Error withdrawing money from player: " + playerName, e); + return false; + } + } + + /** + * Adds money to a player's account (deposit). + * + * @param player the player + * @param amount the amount to add + * @return true if the transaction was successful + */ + public static boolean addMoney(IPlayer player, double amount) { + return addMoney(player.getCommandSenderName(), amount); + } + + /** + * Adds money to a player's account (deposit). + * + * @param playerName the player name + * @param amount the amount to add + * @return true if the transaction was successful + */ + public static boolean addMoney(String playerName, double amount) { + if (!vaultEnabled || economyInstance == null) return false; + + try { + Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName); + Object response; + if (offlinePlayer != null) { + response = depositPlayerOffline.invoke(economyInstance, offlinePlayer, amount); + } else { + response = depositPlayer.invoke(economyInstance, playerName, amount); + } + return (Boolean) transactionSuccess.invoke(response); + } catch (Exception e) { + logger.error("Error adding money to player: " + playerName, e); + return false; + } + } + + /** + * Sets a player's balance to the specified amount. + * This withdraws all money first, then deposits the new amount. + * + * @param player the player + * @param amount the new balance + * @return true if the transaction was successful + */ + public static boolean setBalance(IPlayer player, double amount) { + return setBalance(player.getCommandSenderName(), amount); + } + + /** + * Sets a player's balance to the specified amount. + * This withdraws all money first, then deposits the new amount. + * + * @param playerName the player name + * @param amount the new balance + * @return true if the transaction was successful + */ + public static boolean setBalance(String playerName, double amount) { + if (!vaultEnabled || economyInstance == null) return false; + + double currentBalance = getBalance(playerName); + if (currentBalance > 0) { + if (!withdrawMoney(playerName, currentBalance)) { + return false; + } + } + if (amount > 0) { + return addMoney(playerName, amount); + } + return true; + } + + /** + * Formats an amount according to the economy plugin's settings. + * + * @param amount the amount to format + * @return the formatted string, or the raw number as string if not available + */ + public static String format(double amount) { + if (!vaultEnabled || economyInstance == null) return String.valueOf(amount); + + try { + return (String) format.invoke(economyInstance, amount); + } catch (Exception e) { + logger.error("Error formatting amount", e); + return String.valueOf(amount); + } + } + + /** + * Gets the plural currency name (e.g., "Dollars"). + * + * @return the plural currency name, or "coins" if not available + */ + public static String getCurrencyNamePlural() { + if (!vaultEnabled || economyInstance == null) return "coins"; + + try { + return (String) currencyNamePlural.invoke(economyInstance); + } catch (Exception e) { + logger.error("Error getting currency name plural", e); + return "coins"; + } + } + + /** + * Gets the singular currency name (e.g., "Dollar"). + * + * @return the singular currency name, or "coin" if not available + */ + public static String getCurrencyNameSingular() { + if (!vaultEnabled || economyInstance == null) return "coin"; + + try { + return (String) currencyNameSingular.invoke(economyInstance); + } catch (Exception e) { + logger.error("Error getting currency name singular", e); + return "coin"; + } + } + + /** + * Creates an account for the player if one doesn't exist. + * + * @param player the player + * @return true if the account was created or already exists + */ + public static boolean createAccount(IPlayer player) { + return createAccount(player.getCommandSenderName()); + } + + /** + * Creates an account for the player if one doesn't exist. + * + * @param playerName the player name + * @return true if the account was created or already exists + */ + public static boolean createAccount(String playerName) { + if (!vaultEnabled || economyInstance == null) return false; + + if (hasAccount(playerName)) return true; + + try { + Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName); + if (offlinePlayer != null) { + return (Boolean) createPlayerAccountOffline.invoke(economyInstance, offlinePlayer); + } + return (Boolean) createPlayerAccount.invoke(economyInstance, playerName); + } catch (Exception e) { + logger.error("Error creating account for player: " + playerName, e); + return false; + } + } + + /** + * @return true if the economy supports bank accounts + */ + public static boolean hasBankSupport() { + if (!vaultEnabled || economyInstance == null) return false; + + try { + return (Boolean) hasBankSupport.invoke(economyInstance); + } catch (Exception e) { + logger.error("Error checking bank support", e); + return false; + } + } + + /** + * Gets a list of all bank names. + * + * @return list of bank names, or empty list if not available + */ + @SuppressWarnings("unchecked") + public static java.util.List getBanks() { + if (!vaultEnabled || economyInstance == null) return java.util.Collections.emptyList(); + + try { + return (java.util.List) getBanks.invoke(economyInstance); + } catch (Exception e) { + logger.error("Error getting banks", e); + return java.util.Collections.emptyList(); + } + } + + /** + * Gets the balance of a bank. + * + * @param bankName the bank name + * @return the bank balance, or 0 if not available + */ + public static double getBankBalance(String bankName) { + if (!vaultEnabled || economyInstance == null) return 0; + + try { + Object response = bankBalance.invoke(economyInstance, bankName); + return economyResponseClass.getField("balance").getDouble(response); + } catch (Exception e) { + logger.error("Error getting bank balance: " + bankName, e); + return 0; + } + } + + /** + * Checks if a bank has at least the specified amount. + * + * @param bankName the bank name + * @param amount the amount to check + * @return true if the bank has at least the specified amount + */ + public static boolean bankHas(String bankName, double amount) { + if (!vaultEnabled || economyInstance == null) return false; + + try { + Object response = bankHas.invoke(economyInstance, bankName, amount); + return (Boolean) transactionSuccess.invoke(response); + } catch (Exception e) { + logger.error("Error checking bank balance: " + bankName, e); + return false; + } + } + + /** + * Withdraws money from a bank. + * + * @param bankName the bank name + * @param amount the amount to withdraw + * @return true if the transaction was successful + */ + public static boolean bankWithdraw(String bankName, double amount) { + if (!vaultEnabled || economyInstance == null) return false; + + try { + Object response = bankWithdraw.invoke(economyInstance, bankName, amount); + return (Boolean) transactionSuccess.invoke(response); + } catch (Exception e) { + logger.error("Error withdrawing from bank: " + bankName, e); + return false; + } + } + + /** + * Deposits money into a bank. + * + * @param bankName the bank name + * @param amount the amount to deposit + * @return true if the transaction was successful + */ + public static boolean bankDeposit(String bankName, double amount) { + if (!vaultEnabled || economyInstance == null) return false; + + try { + Object response = bankDeposit.invoke(economyInstance, bankName, amount); + return (Boolean) transactionSuccess.invoke(response); + } catch (Exception e) { + logger.error("Error depositing to bank: " + bankName, e); + return false; + } + } + + /** + * Transfers money from one player to another. + * + * @param from the player to take money from + * @param to the player to give money to + * @param amount the amount to transfer + * @return true if the transfer was successful + */ + public static boolean transfer(IPlayer from, IPlayer to, double amount) { + return transfer(from.getCommandSenderName(), to.getCommandSenderName(), amount); + } + + /** + * Transfers money from one player to another. + * + * @param fromPlayerName the player name to take money from + * @param toPlayerName the player name to give money to + * @param amount the amount to transfer + * @return true if the transfer was successful + */ + public static boolean transfer(String fromPlayerName, String toPlayerName, double amount) { + if (!has(fromPlayerName, amount)) { + return false; + } + if (!withdrawMoney(fromPlayerName, amount)) { + return false; + } + if (!addMoney(toPlayerName, amount)) { + // Rollback + addMoney(fromPlayerName, amount); + return false; + } + return true; + } +} diff --git a/core/src/main/java/noppes/npcs/AbstractDataAbilities.java b/core/src/main/java/noppes/npcs/AbstractDataAbilities.java new file mode 100644 index 000000000..c7268f5c9 --- /dev/null +++ b/core/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -0,0 +1,8 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Abstract data abilities base class. + */ +public abstract class AbstractDataAbilities { +} diff --git a/core/src/main/java/noppes/npcs/CustomNpcs.java b/core/src/main/java/noppes/npcs/CustomNpcs.java new file mode 100644 index 000000000..3826ab83f --- /dev/null +++ b/core/src/main/java/noppes/npcs/CustomNpcs.java @@ -0,0 +1,12 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class CustomNpcs { + public static java.io.File Dir = new java.io.File("."); + public static int DefaultInteractLine = 0; + public static java.util.logging.Logger logger; +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/core/src/main/java/noppes/npcs/CustomNpcsPermissions.java new file mode 100644 index 000000000..b41c9fea1 --- /dev/null +++ b/core/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -0,0 +1,11 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Custom NPC permissions system. + */ +public class CustomNpcsPermissions { + public static final String PROFILE_MAX = "customnpcs.profile.max"; + public static final String PROFILE_REGION_BYPASS = "customnpcs.profile.region.bypass"; + public static boolean hasCustomPermission(noppes.npcs.api.entity.IPlayer player, String perm) { return false; } +} diff --git a/core/src/main/java/noppes/npcs/DataDisplay.java b/core/src/main/java/noppes/npcs/DataDisplay.java new file mode 100644 index 000000000..482da1df2 --- /dev/null +++ b/core/src/main/java/noppes/npcs/DataDisplay.java @@ -0,0 +1,10 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class DataDisplay { + // TEMPORARY STUB +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/EventHooks.java b/core/src/main/java/noppes/npcs/EventHooks.java new file mode 100644 index 000000000..475fb78f8 --- /dev/null +++ b/core/src/main/java/noppes/npcs/EventHooks.java @@ -0,0 +1,10 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class EventHooks { + // STUB - event dispatch stays in mc1710 +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/ICompatibilty.java b/core/src/main/java/noppes/npcs/ICompatibilty.java new file mode 100644 index 000000000..7a3c0ed9c --- /dev/null +++ b/core/src/main/java/noppes/npcs/ICompatibilty.java @@ -0,0 +1,11 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Version compatibility interface. + */ +public interface ICompatibilty { + void readNBTPartial(noppes.npcs.api.INbt nbt); + int getVersion(); + void setVersion(int version); +} diff --git a/core/src/main/java/noppes/npcs/LogWriter.java b/core/src/main/java/noppes/npcs/LogWriter.java new file mode 100644 index 000000000..4de028172 --- /dev/null +++ b/core/src/main/java/noppes/npcs/LogWriter.java @@ -0,0 +1,13 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class LogWriter { + public static void info(String msg) {} + public static void warn(String msg) {} + public static void error(String msg) {} + public static void error(String msg, Throwable e) {} +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/NBTTags.java b/core/src/main/java/noppes/npcs/NBTTags.java index d0d7bf164..daa9084df 100644 --- a/core/src/main/java/noppes/npcs/NBTTags.java +++ b/core/src/main/java/noppes/npcs/NBTTags.java @@ -1,8 +1,5 @@ package noppes.npcs; -import noppes.npcs.core.NBT; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; import java.util.ArrayList; import java.util.HashMap; @@ -12,12 +9,22 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.Vector; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.core.NBT; /** * Core NBT serialization utilities for Map/Set/List collections. * Uses INbt/INbtList abstractions. * - * NOTE: ItemStack-related methods, IScriptUnit methods, and raw NBT type methods + * NOTE: IItemStack-related methods, IScriptUnit methods, and raw NBT type methods * (nbtDoubleList, getIntAt) remain in the mc1710 version only since they require * direct MC types. */ @@ -36,11 +43,11 @@ public class NBTTags { public static int TAG_Compound = 10; public static int TAG_Int_Array = 11; - // TODO: ItemStack methods stay in mc1710 version: - // OLD: getItemStackList(NBTTagList) - uses NoppesUtilServer.readItem() - // OLD: getItemStackArray(NBTTagList) - uses NoppesUtilServer.readItem() - // OLD: nbtItemStackList(HashMap) - uses NoppesUtilServer.writeItem() - // OLD: nbtItemStackArray(ItemStack[]) - uses NoppesUtilServer.writeItem() + // TODO: IItemStack methods stay in mc1710 version: + // OLD: getIItemStackList(INbtList) - uses NoppesUtilServer.readItem() + // OLD: getIItemStackArray(INbtList) - uses NoppesUtilServer.readItem() + // OLD: nbtIItemStackList(HashMap) - uses NoppesUtilServer.writeItem() + // OLD: nbtIItemStackArray(IItemStack[]) - uses NoppesUtilServer.writeItem() public static ArrayList getIntegerArraySet(INbtList tagList) { ArrayList set = new ArrayList(); @@ -54,8 +61,8 @@ public class NBTTags { public static HashMap getBooleanList(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getInteger("Slot"), nbttagcompound.getBoolean("Boolean")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getInteger("Slot"), INbt.getBoolean("Boolean")); } return list; } @@ -63,8 +70,8 @@ public class NBTTags { public static HashMap getIntegerIntegerMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getInteger("Slot"), nbttagcompound.getInteger("Integer")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getInteger("Slot"), INbt.getInteger("Integer")); } return list; } @@ -72,8 +79,8 @@ public class NBTTags { public static HashMap getIntegerFloatMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getInteger("Slot"), nbttagcompound.getFloat("Float")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getInteger("Slot"), INbt.getFloat("Float")); } return list; } @@ -81,8 +88,8 @@ public class NBTTags { public static HashMap getIntegerDoubleMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getInteger("Slot"), nbttagcompound.getDouble("Double")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getInteger("Slot"), INbt.getDouble("Double")); } return list; } @@ -90,8 +97,8 @@ public class NBTTags { public static HashMap getIntegerLongMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getInteger("Slot"), nbttagcompound.getLong("Long")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getInteger("Slot"), INbt.getLong("Long")); } return list; } @@ -99,8 +106,8 @@ public class NBTTags { public static HashSet getStringSet(INbtList tagList) { HashSet list = new HashSet<>(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.add(nbttagcompound.getString("String")); + INbt INbt = tagList.getCompound(i); + list.add(INbt.getString("String")); } return list; } @@ -108,63 +115,63 @@ public class NBTTags { public static HashMap getIntegerByteMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getInteger("Slot"), nbttagcompound.getByte("Byte")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getInteger("Slot"), INbt.getByte("Byte")); } return list; } public static INbtList nbtIntegerByteMap(Map lines) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (lines == null) - return nbttaglist; + return INbtList; for (int slot : lines.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setByte("Byte", lines.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setByte("Byte", lines.get(slot)); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtStringSet(HashSet collection) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (collection == null) - return nbttaglist; + return INbtList; for (String slot : collection) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setString("String", slot); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setString("String", slot); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static HashSet getIntegerSet(INbtList tagList) { HashSet list = new HashSet(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.add(nbttagcompound.getInteger("Integer")); + INbt INbt = tagList.getCompound(i); + list.add(INbt.getInteger("Integer")); } return list; } public static INbtList nbtIntegerSet(HashSet set) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (set == null) - return nbttaglist; + return INbtList; for (int slot : set) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Integer", slot); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Integer", slot); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static HashMap getStringStringMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getString("Slot"), nbttagcompound.getString("Value")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getString("Slot"), INbt.getString("Value")); } return list; } @@ -172,8 +179,8 @@ public class NBTTags { public static HashMap getIntegerStringMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getInteger("Slot"), nbttagcompound.getString("Value")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getInteger("Slot"), INbt.getString("Value")); } return list; } @@ -181,8 +188,8 @@ public class NBTTags { public static HashMap getStringIntegerMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getString("Slot"), nbttagcompound.getInteger("Value")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getString("Slot"), INbt.getInteger("Value")); } return list; } @@ -190,8 +197,8 @@ public class NBTTags { public static HashMap getStringIntegerArrayMap(INbtList tagList) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getString("Slot"), nbttagcompound.getIntArray("Value")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getString("Slot"), INbt.getIntArray("Value")); } return list; } @@ -199,10 +206,10 @@ public class NBTTags { public static HashMap getStringIntegerArrayMap(INbtList tagList, int arrayLength) { HashMap list = new HashMap(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - int[] a = nbttagcompound.getIntArray("Value"); + INbt INbt = tagList.getCompound(i); + int[] a = INbt.getIntArray("Value"); if (a.length != arrayLength) a = new int[arrayLength]; - list.put(nbttagcompound.getString("Slot"), a); + list.put(INbt.getString("Slot"), a); } return list; } @@ -211,24 +218,23 @@ public class NBTTags { HashMap> map = new HashMap>(); for (int i = 0; i < tagList.size(); i++) { Vector values = new Vector(); - INbt nbttagcompound = tagList.getCompound(i); - INbtList list = nbttagcompound.getTagList("Values", 10); + INbt INbt = tagList.getCompound(i); + INbtList list = INbt.getTagList("Values", 10); for (int j = 0; j < list.size(); j++) { INbt value = list.getCompound(j); values.add(value.getString("Value")); } - map.put(nbttagcompound.getString("Key"), values); + map.put(INbt.getString("Key"), values); } return map; } - public static List getStringList(INbtList tagList) { List list = new ArrayList(); for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - String line = nbttagcompound.getString("Line"); + INbt INbt = tagList.getCompound(i); + String line = INbt.getString("Line"); list.add(line); } return list; @@ -237,91 +243,91 @@ public class NBTTags { public static String[] getStringArray(INbtList tagList, int size) { String[] arr = new String[size]; for (int i = 0; i < tagList.size(); i++) { - INbt nbttagcompound = tagList.getCompound(i); - String line = nbttagcompound.getString("Value"); - int slot = nbttagcompound.getInteger("Slot"); + INbt INbt = tagList.getCompound(i); + String line = INbt.getString("Value"); + int slot = INbt.getInteger("Slot"); arr[slot] = line; } return arr; } public static INbtList nbtIntegerArraySet(List set) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (set == null) - return nbttaglist; + return INbtList; for (int[] arr : set) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setIntArray("Array", arr); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setIntArray("Array", arr); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtBooleanList(HashMap updatedSlots) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (updatedSlots == null) - return nbttaglist; + return INbtList; HashMap inventory2 = updatedSlots; for (Integer slot : inventory2.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setBoolean("Boolean", inventory2.get(slot)); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setBoolean("Boolean", inventory2.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtIntegerIntegerMap(Map lines) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (lines == null) - return nbttaglist; + return INbtList; for (int slot : lines.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setInteger("Integer", lines.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setInteger("Integer", lines.get(slot)); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtIntegerFloatMap(Map lines) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (lines == null) - return nbttaglist; + return INbtList; for (int slot : lines.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setDouble("Float", lines.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setDouble("Float", lines.get(slot)); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtIntegerDoubleMap(Map lines) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (lines == null) - return nbttaglist; + return INbtList; for (int slot : lines.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setDouble("Double", lines.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setDouble("Double", lines.get(slot)); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtIntegerLongMap(Map lines) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (lines == null) - return nbttaglist; + return INbtList; for (int slot : lines.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setLong("Long", lines.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setLong("Long", lines.get(slot)); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtVectorMap(Map> map) { @@ -344,88 +350,88 @@ public class NBTTags { } public static INbtList nbtStringStringMap(Map map) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (map == null) - return nbttaglist; + return INbtList; for (String slot : map.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setString("Slot", slot); - nbttagcompound.setString("Value", map.get(slot)); + INbt INbt = NBT.compound(); + INbt.setString("Slot", slot); + INbt.setString("Value", map.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtStringIntegerMap(Map map) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (map == null) - return nbttaglist; + return INbtList; for (String slot : map.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setString("Slot", slot); - nbttagcompound.setInteger("Value", map.get(slot)); + INbt INbt = NBT.compound(); + INbt.setString("Slot", slot); + INbt.setInteger("Value", map.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtStringIntegerArrayMap(Map map) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (map == null) - return nbttaglist; + return INbtList; for (String slot : map.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setString("Slot", slot); - nbttagcompound.setIntArray("Value", map.get(slot)); + INbt INbt = NBT.compound(); + INbt.setString("Slot", slot); + INbt.setIntArray("Value", map.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtIntegerStringMap(HashMap map) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (map == null) - return nbttaglist; + return INbtList; for (int slot : map.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setString("Value", map.get(slot)); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setString("Value", map.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtStringArray(String[] list) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (list == null) - return nbttaglist; + return INbtList; for (int i = 0; i < list.length; i++) { if (list[i] == null) continue; - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setString("Value", list[i]); - nbttagcompound.setInteger("Slot", i); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setString("Value", list[i]); + INbt.setInteger("Slot", i); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } public static INbtList nbtStringList(List list) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); for (String s : list) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setString("Line", s); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setString("Line", s); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } // TODO: nbtDoubleList stays in mc1710 version - uses NBTTagDouble directly - // OLD: public static NBTTagList nbtDoubleList(double... par1ArrayOfDouble) + // OLD: public static INbtList nbtDoubleList(double... par1ArrayOfDouble) public static INbt NBTMerge(INbt data, INbt merge) { INbt compound = data.copy(); @@ -489,34 +495,34 @@ public class NBTTags { TreeMap list = new TreeMap<>(); for (int i = 0; i < tagList.size(); ++i) { - INbt nbttagcompound = tagList.getCompound(i); - list.put(nbttagcompound.getLong("Long"), nbttagcompound.getString("String")); + INbt INbt = tagList.getCompound(i); + list.put(INbt.getLong("Long"), INbt.getString("String")); } return list; } public static INbtList NBTLongStringMap(Map map) { - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); if (map == null) { - return nbttaglist; + return INbtList; } else { for (long slot : map.keySet()) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setLong("Long", slot); - nbttagcompound.setString("String", map.get(slot)); - nbttaglist.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setLong("Long", slot); + INbt.setString("String", map.get(slot)); + INbtList.addCompound(INbt); } - return nbttaglist; + return INbtList; } } - // TODO: Script methods stay in mc1710 version - use IScriptHandler/IScriptUnit/NBTTagCompound directly - // OLD: public static List GetScriptOld(NBTTagList list, IScriptHandler handler) - // OLD: public static List GetScript(NBTTagCompound compound, IScriptHandler handler) - // OLD: public static NBTTagList NBTScript(List scripts) + // TODO: Script methods stay in mc1710 version - use IScriptHandler/IScriptUnit/INbt directly + // OLD: public static List GetScriptOld(INbtList list, IScriptHandler handler) + // OLD: public static List GetScript(INbt compound, IScriptHandler handler) + // OLD: public static INbtList NBTScript(List scripts) // TODO: getIntAt stays in mc1710 version - uses NBTTagInt.func_150287_d() directly - // OLD: public static int getIntAt(NBTTagList tagList, int index) + // OLD: public static int getIntAt(INbtList tagList, int index) } diff --git a/core/src/main/java/noppes/npcs/NoppesUtilPlayer.java b/core/src/main/java/noppes/npcs/NoppesUtilPlayer.java new file mode 100644 index 000000000..8d4108d18 --- /dev/null +++ b/core/src/main/java/noppes/npcs/NoppesUtilPlayer.java @@ -0,0 +1,11 @@ +package noppes.npcs; + +import noppes.npcs.api.entity.IPlayer; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Player utility methods. + */ +public class NoppesUtilPlayer { + public static void sendMessage(IPlayer player, String msg) {} +} diff --git a/core/src/main/java/noppes/npcs/NoppesUtilServer.java b/core/src/main/java/noppes/npcs/NoppesUtilServer.java new file mode 100644 index 000000000..555e8daf1 --- /dev/null +++ b/core/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -0,0 +1,15 @@ +package noppes.npcs; + +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.controllers.data.PlayerEffect; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class NoppesUtilServer { + public static IPlayer getIPlayer(Object player) { return null; } + public static void sendMessage(IPlayer player, String msg) {} + public static void sendTranslatedMessage(IPlayer player, String key, Object... args) {} +} diff --git a/core/src/main/java/noppes/npcs/NpcDamageSource.java b/core/src/main/java/noppes/npcs/NpcDamageSource.java new file mode 100644 index 000000000..0377fe803 --- /dev/null +++ b/core/src/main/java/noppes/npcs/NpcDamageSource.java @@ -0,0 +1,10 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class NpcDamageSource { + // TEMPORARY STUB +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/NpcMiscInventory.java b/core/src/main/java/noppes/npcs/NpcMiscInventory.java new file mode 100644 index 000000000..1ff0970fe --- /dev/null +++ b/core/src/main/java/noppes/npcs/NpcMiscInventory.java @@ -0,0 +1,19 @@ +package noppes.npcs; + +import noppes.npcs.api.INbt; +import java.util.List; +import java.util.ArrayList; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Miscellaneous inventory container. + */ +public class NpcMiscInventory { + public int size; + public NpcMiscInventory(int size) { this.size = size; } + public noppes.npcs.api.item.IItemStack getStackInSlot(int slot) { return null; } + public void setInventorySlotContents(int slot, noppes.npcs.api.item.IItemStack stack) {} + public INbt getToNBT() { return noppes.npcs.api.NBT.compound(); } + public void setFromNBT(INbt nbt) {} + public int getSizeInventory() { return size; } +} diff --git a/core/src/main/java/noppes/npcs/VersionCompatibility.java b/core/src/main/java/noppes/npcs/VersionCompatibility.java new file mode 100644 index 000000000..ec9eecc1f --- /dev/null +++ b/core/src/main/java/noppes/npcs/VersionCompatibility.java @@ -0,0 +1,8 @@ +package noppes.npcs; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Version compatibility utilities. + */ +public class VersionCompatibility { +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigClient.java b/core/src/main/java/noppes/npcs/config/ConfigClient.java new file mode 100644 index 000000000..f3e6124ea --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigClient.java @@ -0,0 +1,380 @@ +package noppes.npcs.config; + +import common.minecraftforge.common.config.IConfigProperty; +import common.minecraftforge.common.config.IConfiguration; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.legacy.LegacyConfig; +import org.apache.logging.log4j.Level; + +import java.io.File; + +public class ConfigClient { + public static IConfiguration config; + + public final static String GENERAL = "General"; + public final static String VISUAL = "Visual"; + public final static String MODEL = "Model"; + public final static String TEXTURE = "Texture"; + public final static String HUD = "Hud"; + public final static String RENDERING = "Rendering"; + + + /** + * General Properties + **/ + public static IConfigProperty AllowClientScriptsProperty; + public static boolean AllowClientScripts = true; + + public static IConfigProperty CacheLifeProperty; + public static int CacheLife = 10; + + /** + * Visual Properties + **/ + public static IConfigProperty EnableChatBubblesProperty; + public static boolean EnableChatBubbles = true; + + public static IConfigProperty InventoryGuiEnabledProperty; + public static boolean InventoryGuiEnabled = true; + + public static IConfigProperty enableFactionTabProperty; + public static boolean enableFactionTab = true; + + public static IConfigProperty FontTypeProperty; + public static String FontType = "Default"; + + public static IConfigProperty FontSizeProperty; + public static int FontSize = 18; + + public static IConfigProperty ChatBubblesFontTypeProperty; + public static boolean ChatBubblesFontType = false; + + public static IConfigProperty HideEffectsBarProperty; + public static boolean HideEffectsBar = false; + + // Rendering Properties + public static IConfigProperty LowResExplosionProperty; + public static boolean LowResExplosion = false; + + // Proximity Alpha (owner-only fade for energy projectiles near camera) + public static IConfigProperty ProximityAlphaMinProperty; + public static float ProximityAlphaMin = 0.15f; + + public static IConfigProperty ProximityAlphaDistanceProperty; + public static float ProximityAlphaDistance = 10.0f; + + public static IConfigProperty ProximityAlphaAgeTicksProperty; + public static int ProximityAlphaAgeTicks = 60; + + /** + * Questing Properties + **/ + public static IConfigProperty DialogSpeedProperty; + public static int DialogSpeed = 10; + + public static IConfigProperty DialogSoundProperty; + public static boolean DialogSound = true; + + public static IConfigProperty ChatAlertsProperty; + public static boolean ChatAlerts = true; + + public static IConfigProperty BannerAlertsProperty; + public static boolean BannerAlerts = true; + + // HUD PROPERTIES + public static IConfigProperty QuestOverlayXProperty; + public static float QuestOverlayX = 0; + + public static IConfigProperty QuestOverlayYProperty; + public static float QuestOverlayY = 14; + + public static IConfigProperty QuestOverlayScaleProperty; + public static int QuestOverlayScale = 246; + + public static IConfigProperty QuestOverlayTextAlignProperty; + public static int QuestOverlayTextAlign = 0; // 0: Left, 1: Center, 2: Right + + public static IConfigProperty CompassEnabledProperty; + public static boolean CompassEnabled = true; + + public static IConfigProperty CompassOverlayXProperty; + public static float CompassOverlayX = 36; + + public static IConfigProperty CompassOverlayYProperty; + public static float CompassOverlayY = 0; + + public static IConfigProperty CompassOverlayScaleProperty; + public static int CompassOverlayScale = 344; + + public static IConfigProperty CompassOverlayWidthProperty; + public static int CompassOverlayWidth = 158; + + // Ability Hotbar HUD + public static IConfigProperty AbilityHotbarEnabledProperty; + public static boolean AbilityHotbarEnabled = true; + + public static IConfigProperty AbilityHotbarXProperty; + public static float AbilityHotbarX = 0; + + public static IConfigProperty AbilityHotbarYProperty; + public static float AbilityHotbarY = 43; + + public static IConfigProperty AbilityHotbarScaleProperty; + public static int AbilityHotbarScale = 227; + + public static IConfigProperty AbilityHotbarHorizontalProperty; + public static boolean AbilityHotbarHorizontal = false; + + public static IConfigProperty AbilityHotbarAltTextureProperty; + public static boolean AbilityHotbarAltTexture = true; + + // 1=Above(H)/Left(V), 2=Below(H)/Right(V) + public static IConfigProperty AbilityHotbarTextPositionProperty; + public static int AbilityHotbarTextPosition = 2; + + // Max visible slots: 3, 5, or 7 + public static IConfigProperty AbilityHotbarVisibleSlotsProperty; + public static int AbilityHotbarVisibleSlots = 5; + + // Show Always: true = always visible, false = only visible while HUD key held + public static IConfigProperty AbilityHotbarShowAlwaysProperty; + public static boolean AbilityHotbarShowAlways = true; + + // Text Visibility: 0=Shown, 1=Hidden, 2=Held (only while HUD key held) + public static IConfigProperty AbilityHotbarTextVisibilityProperty; + public static int AbilityHotbarTextVisibility = 0; + + /** + * Texture Properties + **/ + public static boolean WoodTextures = false; + + /** + * Model Properties + **/ + public static boolean LegacyCampfire = false; + public static boolean LegacyBanner = false; + public static boolean LegacyChair = false; + public static boolean LegacyStool = false; + public static boolean LegacyCouch = false; + public static boolean LegacyTable = false; + public static boolean LegacyBarrel = false; + public static boolean LegacyCarpentryBench = false; + public static boolean LegacyAnvil = false; + public static boolean LegacyLantern = false; + public static boolean LegacyCandle = false; + public static boolean LegacyTallLamp = false; + public static boolean LegacyPedestal = false; + public static boolean LegacyMailbox = false; + public static boolean ImprovedImageDownloadConnection = true; + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + // Hud Quest Overlay settings + QuestOverlayXProperty = config.get(HUD, "Quest Hud X", 0.0, "X position of the quest overlay."); + QuestOverlayX = (float) QuestOverlayXProperty.getDouble(0); + + QuestOverlayYProperty = config.get(HUD, "Quest Hud Y", 14.0, "Y position of the quest overlay."); + QuestOverlayY = (float) QuestOverlayYProperty.getDouble(14); + + QuestOverlayScaleProperty = config.get(HUD, "Quest Hud Scale", 246, "Scale percentage of the quest overlay."); + QuestOverlayScale = QuestOverlayScaleProperty.getInt(246); + + QuestOverlayTextAlignProperty = config.get(HUD, "Quest Hud Text Alignment", 0, "Text alignment in quest overlay (0: Left, 1: Center, 2: Right)."); + QuestOverlayTextAlign = QuestOverlayTextAlignProperty.getInt(0); + + CompassEnabledProperty = config.get( + HUD, + "Compass Hud Enabled", + true, + "Enable Compass Hud Component" + ); + CompassEnabled = CompassEnabledProperty.getBoolean(); + + // Compass HUD Configs + CompassOverlayXProperty = config.get( + HUD, + "Compass Hud X", + 36.0, + "Horizontal position of compass overlay (0-100 percentage)" + ); + CompassOverlayX = (float) CompassOverlayXProperty.getDouble(); + + CompassOverlayYProperty = config.get( + HUD, + "Compass Hud Y", + 0.0, + "Vertical position of compass overlay (0-100 percentage)" + ); + CompassOverlayY = (float) CompassOverlayYProperty.getDouble(); + + CompassOverlayScaleProperty = config.get( + HUD, + "Compass Hud Scale", + 344, + "Scale percentage of compass overlay", + 50, + 500 + ); + CompassOverlayScale = CompassOverlayScaleProperty.getInt(); + + CompassOverlayWidthProperty = config.get( + HUD, + "Compass Hud Width", + 158, + "Base width of compass bar in pixels", + 50, + 1000 + ); + CompassOverlayWidth = CompassOverlayWidthProperty.getInt(); + + // Ability Hotbar HUD Configs + AbilityHotbarEnabledProperty = config.get(HUD, "Ability Hotbar Enabled", true, "Enable Ability Hotbar HUD Component"); + AbilityHotbarEnabled = AbilityHotbarEnabledProperty.getBoolean(); + + AbilityHotbarXProperty = config.get(HUD, "Ability Hotbar X", 0.0, "Horizontal position (0-100 percentage)"); + AbilityHotbarX = (float) AbilityHotbarXProperty.getDouble(); + + AbilityHotbarYProperty = config.get(HUD, "Ability Hotbar Y", 43.0, "Vertical position (0-100 percentage)"); + AbilityHotbarY = (float) AbilityHotbarYProperty.getDouble(); + + AbilityHotbarScaleProperty = config.get(HUD, "Ability Hotbar Scale", 227, "Scale percentage", 50, 500); + AbilityHotbarScale = AbilityHotbarScaleProperty.getInt(); + + AbilityHotbarHorizontalProperty = config.get(HUD, "Ability Hotbar Horizontal", false, "Display horizontally instead of vertically"); + AbilityHotbarHorizontal = AbilityHotbarHorizontalProperty.getBoolean(); + + AbilityHotbarAltTextureProperty = config.get(HUD, "Ability Hotbar Alt Texture", true, "Use rounded square instead of circle slots"); + AbilityHotbarAltTexture = AbilityHotbarAltTextureProperty.getBoolean(); + + AbilityHotbarTextPositionProperty = config.get(HUD, "Ability Hotbar Text Position", 2, "Text label position (1=Above/Left, 2=Below/Right)"); + AbilityHotbarTextPosition = AbilityHotbarTextPositionProperty.getInt(2); + + AbilityHotbarVisibleSlotsProperty = config.get(HUD, "Ability Hotbar Visible Slots", 5, "Max visible slots in hotbar (3, 5, or 7)"); + AbilityHotbarVisibleSlots = AbilityHotbarVisibleSlotsProperty.getInt(5); + + AbilityHotbarShowAlwaysProperty = config.get(HUD, "Ability Hotbar Show Always", true, "Always show hotbar (false = only while HUD key is held)"); + AbilityHotbarShowAlways = AbilityHotbarShowAlwaysProperty.getBoolean(true); + + AbilityHotbarTextVisibilityProperty = config.get(HUD, "Ability Hotbar Text Visibility", 0, "Text visibility (0=Shown, 1=Hidden, 2=Held)"); + AbilityHotbarTextVisibility = AbilityHotbarTextVisibilityProperty.getInt(0); + + // General + AllowClientScriptsProperty = config.get(GENERAL, "Allow Client Scripts", true, "Allow the server to run scripts on the client. If disabled, no server scripts will execute client-side."); + AllowClientScripts = AllowClientScriptsProperty.getBoolean(true); + + CacheLifeProperty = config.get(GENERAL, "Cache Life", 10, "How long should downloaded imagery data be saved client side? (In minutes)"); + CacheLife = CacheLifeProperty.getInt(10); + + // Visual + EnableChatBubblesProperty = config.get(VISUAL, "Enable Chat Bubbles", true, "Enable/Disable Chat Bubbles"); + EnableChatBubbles = EnableChatBubblesProperty.getBoolean(true); + + InventoryGuiEnabledProperty = config.get(VISUAL, "Enable Inventory Tabs", true, "Enable/Disable Inventory Tabs"); + InventoryGuiEnabled = InventoryGuiEnabledProperty.getBoolean(true); + + enableFactionTabProperty = config.get(VISUAL, "Enable Faction Tab", true, "Enable the Faction Tab"); + enableFactionTab = enableFactionTabProperty.getBoolean(true); + + FontTypeProperty = config.get(VISUAL, "Font Type", "Default", "When set to Minecraft it will use minecrafts font, when Default it will use OpenSans. Can only use fonts installed on your PC"); + FontType = FontTypeProperty.getString(); + + FontSizeProperty = config.get(VISUAL, "Font Size", 18, "Font size for custom fonts (doesn't work with minecrafts font)"); + FontSize = FontSizeProperty.getInt(18); + + ChatBubblesFontTypeProperty = config.get(VISUAL, "Chat Bubbles Font Type", false, "Enable the use of Custom Font for Chat Bubbles"); + ChatBubblesFontType = ChatBubblesFontTypeProperty.getBoolean(false); + + HideEffectsBarProperty = config.get(VISUAL, "Hide Effects Bar", false, "Hides CNPC+ Inventory Effects Bar"); + HideEffectsBar = HideEffectsBarProperty.getBoolean(false); + + LowResExplosionProperty = config.get( + RENDERING, + "Low Res IExplosion", + false, + "Disables most Energy IExplosion voxel rendering and keeps particle effects only." + ); + LowResExplosion = LowResExplosionProperty.getBoolean(false); + + ProximityAlphaMinProperty = config.get( + RENDERING, + "Proximity Alpha Min", + 0.15, + "Minimum alpha for owner's energy projectiles when very close to camera. Set to 1.0 to disable proximity fade." + ); + ProximityAlphaMin = (float) Math.max(0.0, Math.min(1.0, ProximityAlphaMinProperty.getDouble(0.15))); + + ProximityAlphaDistanceProperty = config.get( + RENDERING, + "Proximity Alpha Distance", + 7.0, + "Distance in blocks at which owner's energy projectiles reach full alpha." + ); + ProximityAlphaDistance = (float) Math.max(1.0, ProximityAlphaDistanceProperty.getDouble(10.0)); + + ProximityAlphaAgeTicksProperty = config.get( + RENDERING, + "Proximity Alpha Age Ticks", + 60, + "After this many ticks alive, proximity alpha fade is disabled (projectile always renders at full alpha). Does not apply while charging or to attached beams/lasers." + ); + ProximityAlphaAgeTicks = Math.max(0, ProximityAlphaAgeTicksProperty.getInt(60)); + + DialogSpeedProperty = config.get(VISUAL, "Dialog Speed", true, "Only set for gradual dialogs"); + DialogSpeed = DialogSpeedProperty.getInt(10); + + DialogSoundProperty = config.get(VISUAL, "Dialog Sound", true, "Only for dialogs with sounds"); + DialogSound = DialogSoundProperty.getBoolean(true); + + ChatAlertsProperty = config.get(VISUAL, "All Chat Alerts", true, "Universal enable/disable for Banner Alerts"); + ChatAlerts = ChatAlertsProperty.getBoolean(true); + + BannerAlertsProperty = config.get(VISUAL, "All Banner Alerts", true, "Universal enable/disable for Banner Alerts"); + BannerAlerts = BannerAlertsProperty.getBoolean(true); + + WoodTextures = config.get(TEXTURE, "Wood Textures", false, "Models like Chairs and Stools will use default MC Wood Textures").getBoolean(false); + ImprovedImageDownloadConnection = config.get(TEXTURE, "DEBUG: Better handling of image downloads: ", true).getBoolean(true); + + LegacyCampfire = config.get(MODEL, "Legacy Campfire Model", false).getBoolean(false); + LegacyBanner = config.get(MODEL, "Legacy Banner Model", false).getBoolean(false); + LegacyBarrel = config.get(MODEL, "Legacy Barrel Model", false).getBoolean(false); + LegacyChair = config.get(MODEL, "Legacy Chair Model", false).getBoolean(false); + LegacyStool = config.get(MODEL, "Legacy Stool Model", false).getBoolean(false); + LegacyCouch = config.get(MODEL, "Legacy Couch Model", false).getBoolean(false); + LegacyTable = config.get(MODEL, "Legacy Table Model", false).getBoolean(false); + LegacyAnvil = config.get(MODEL, "Legacy Anvil Model", false).getBoolean(false); + LegacyCarpentryBench = config.get(MODEL, "Legacy Carpentry Bench Model", false).getBoolean(false); + LegacyLantern = config.get(MODEL, "Legacy Lantern Model", false).getBoolean(false); + LegacyCandle = config.get(MODEL, "Legacy Candle Model", false).getBoolean(false); + LegacyTallLamp = config.get(MODEL, "Legacy Tall Lamp Model", false).getBoolean(false); + LegacyPedestal = config.get(MODEL, "Legacy Pedestal Model", false).getBoolean(false); + LegacyMailbox = config.get(MODEL, "Legacy Mailbox Model", false).getBoolean(false); + + // Convert to Legacy + if (CustomNpcs.legacyExist) { + EnableChatBubbles = LegacyConfig.EnableChatBubbles; + EnableChatBubblesProperty.set(EnableChatBubbles); + + FontType = LegacyConfig.FontType; + FontTypeProperty.set(FontType); + + InventoryGuiEnabled = LegacyConfig.InventoryGuiEnabled; + InventoryGuiEnabledProperty.set(InventoryGuiEnabled); + + FontSize = LegacyConfig.FontSize; + FontSizeProperty.set(FontSize); + } + + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its client IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigDebug.java b/core/src/main/java/noppes/npcs/config/ConfigDebug.java new file mode 100644 index 000000000..1e6fdf3ed --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigDebug.java @@ -0,0 +1,150 @@ +package noppes.npcs.config; + +import common.minecraftforge.common.config.IConfigProperty; +import common.minecraftforge.common.config.IConfiguration; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.legacy.LegacyConfig; +import noppes.npcs.constants.EnumScriptType; +import org.apache.logging.log4j.Level; + +import java.io.File; + +public class ConfigDebug { + public static IConfiguration config; + + public final static String LOGGING = "Logging"; + + /** + * Logging Properties + **/ + public static IConfigProperty PlayerLoggingProperty; + public static boolean PlayerLogging = false; + + public static IConfigProperty ScriptLoggingProperty; + public static boolean ScriptLogging = false; + + public static IConfigProperty ScriptFrequencyProperty; + public static int ScriptFrequency = 20; + + public static IConfigProperty ScriptIgnoreTimeProperty; + public static int ScriptIgnoreTime = 2000; + + public static IConfigProperty ScriptLogIgnoreTypeProperty; + public static String ScriptLogIgnoreType = "TICK"; + + public static boolean LogActionManager = false; + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + // Logging + PlayerLoggingProperty = config.get(LOGGING, "Enable Player Logging", false, "Enables if Player Information (WAND-USE) should be printed to CustomNPCs Logs. IF on Server \n" + + "Logs will only be present SERVER-SIDE only in CustomNPCs-latest, -1, -2, and -3"); + PlayerLogging = PlayerLoggingProperty.getBoolean(false); + + ScriptLoggingProperty = config.get(LOGGING, "Enable Script Logging", false, "Enables if Scripting Information should be printed to CustomNPCs Logs. IF on Server \n" + + "Logs will only be present SERVER-SIDE only in CustomNPCs-latest, -1, -2, and -3"); + ScriptLogging = ScriptLoggingProperty.getBoolean(false); + + ScriptFrequencyProperty = config.get(LOGGING, "Script Log Frequency Limit", 20, "Amount of Messages marked as SPAM [5, 3000]. Lower Number means MORE accurate messages \n" + + "This frequency will determine if the log will print a line with [SPAM] to warn the console."); + ScriptFrequency = ScriptFrequencyProperty.getInt(20); + + ScriptIgnoreTimeProperty = config.get(LOGGING, "Script Ignore Time Buffer", 2000, "IN Milliseconds 1s = 1000s. If a recent LOG of the same event is SENT within this threshold it will be ignored."); + ScriptIgnoreTime = ScriptIgnoreTimeProperty.getInt(2000); + + ScriptLogIgnoreTypeProperty = config.get(LOGGING, "Script Type Ignore", "TICK", "Comma separated list of NPC Script Types that will omit these from the logs,\n" + + "INIT,TICK,INTERACT,DIALOG,DAMAGED,KILLED,ATTACK,TARGET,COLLIDE,KILLS,DIALOG_CLOSE,TIMER"); + ScriptLogIgnoreType = ScriptLogIgnoreTypeProperty.getString(); + + LogActionManager = config.get(LOGGING, "Log Action Manager", LogActionManager, "Log the Performance of the Action Manager").getBoolean(LogActionManager); + + + // Convert to Legacy + if (CustomNpcs.legacyExist) { + PlayerLogging = LegacyConfig.PlayerLogging; + PlayerLoggingProperty.set(PlayerLogging); + + ScriptLogging = LegacyConfig.ScriptLogging; + ScriptLoggingProperty.set(ScriptLogging); + + ScriptFrequency = LegacyConfig.ScriptFrequency; + ScriptFrequencyProperty.set(ScriptFrequency); + + ScriptIgnoreTime = LegacyConfig.ScriptIgnoreTime; + ScriptIgnoreTimeProperty.set(ScriptIgnoreTime); + + ScriptLogIgnoreType = LegacyConfig.ScriptLogIgnoreType; + ScriptLogIgnoreTypeProperty.set(ScriptLogIgnoreType); + } + + if (ScriptFrequency < 5) + ScriptFrequency = 5; + + if (ScriptFrequency > 3000) + ScriptFrequency = 3000; + + if (ScriptIgnoreTime < 0) + ScriptIgnoreTime = 0; + + try { + String[] ignoreTypes = ScriptLogIgnoreType.split(","); + for (String s : ignoreTypes) { + EnumScriptType type = EnumScriptType.valueOfIgnoreCase(s); + if (type != null) { + switch (type) { + case INIT: + CustomNpcs.InitIgnore = true; + break; + case TICK: + CustomNpcs.TickIgnore = true; + break; + case INTERACT: + CustomNpcs.InteractIgnore = true; + break; + case DIALOG: + CustomNpcs.DialogIgnore = true; + break; + case DAMAGED: + CustomNpcs.DamagedIgnore = true; + break; + case KILLED: + CustomNpcs.KilledIgnore = true; + break; + case ATTACK: + CustomNpcs.AttackIgnore = true; + break; + case TARGET: + CustomNpcs.TargetIgnore = true; + break; + case COLLIDE: + CustomNpcs.CollideIgnore = true; + break; + case KILLS: + CustomNpcs.KillsIgnore = true; + break; + case DIALOG_CLOSE: + CustomNpcs.DialogCloseIgnore = true; + break; + case TIMER: + CustomNpcs.TimerIgnore = true; + break; + default: + break; + } + } + } + } catch (Exception ignored) { + } + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its debug IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigEnergy.java b/core/src/main/java/noppes/npcs/config/ConfigEnergy.java new file mode 100644 index 000000000..710127458 --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigEnergy.java @@ -0,0 +1,49 @@ +package noppes.npcs.config; + +import common.minecraftforge.common.config.IConfiguration; +import org.apache.logging.log4j.Level; + +import java.io.File; + +public class ConfigEnergy { + public static IConfiguration config; + + public final static String DOMES = "Domes"; + public final static String EXPLOSIONS = "Explosions"; + + public static String[] DomeItemBlacklist; + /** + * Server-side toggle for energy projectile terrain destruction. + * Disabled by default to avoid block grief. + */ + public static boolean EnableEnergyExplosionBlockDamage; + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + DomeItemBlacklist = config.getStringList( + "Item Blacklist", DOMES, + new String[]{"minecraft:ender_pearl"}, + "Items that cannot be used while inside an Energy Dome.\n" + + "Format: modid:itemname (e.g., minecraft:ender_pearl)" + ); + + EnableEnergyExplosionBlockDamage = config.getBoolean( + "Enable Energy IExplosion Block Damage", EXPLOSIONS, + false, + "If true, explosive ENERGY projectile abilities can destroy terrain.\n" + + "This is intended for server-side use and is disabled by default." + ); + + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CustomNPC+ has had a problem loading its energy IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigExperimental.java b/core/src/main/java/noppes/npcs/config/ConfigExperimental.java new file mode 100644 index 000000000..8752ca8bc --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigExperimental.java @@ -0,0 +1,34 @@ +package noppes.npcs.config; + +import common.minecraftforge.common.config.IConfigProperty; +import common.minecraftforge.common.config.IConfiguration; +import org.apache.logging.log4j.Level; + +import java.io.File; + +public class ConfigExperimental { + public static IConfiguration config; + + public final static String CLIENT = "Client"; + public final static String SERVER = "Server"; + public static IConfigProperty ModernGuiSystemProperty; + public static boolean ModernGuiSystem = false; + + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + ModernGuiSystemProperty = config.get(CLIENT, "Experimental Dialog GUI", false, "Enables the new CNPC+ Modern GUI for Dialog and Quest information"); + ModernGuiSystem = ModernGuiSystemProperty.getBoolean(false); + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its experimental IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigItem.java b/core/src/main/java/noppes/npcs/config/ConfigItem.java new file mode 100644 index 000000000..69dc626b8 --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigItem.java @@ -0,0 +1,71 @@ +package noppes.npcs.config; + +import common.minecraftforge.common.config.IConfigProperty; +import common.minecraftforge.common.config.IConfiguration; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.legacy.LegacyConfig; +import org.apache.logging.log4j.Level; + +import java.io.File; + +public class ConfigItem { + public static IConfiguration config; + + public final static String ALL = "All"; + public final static String GUN = "Gun"; + /** + * Item Properties + **/ + public static IConfigProperty DisableExtraBlockProperty; + public static boolean DisableExtraBlock = false; + public static IConfigProperty DisableExtraItemsProperty; + public static boolean DisableExtraItems = false; + public static IConfigProperty GunsEnabledProperty; + public static boolean GunsEnabled = true; + public static int MachineGunAmmo = 8; + public static int MachineGunTickSpeed = 6; + + /** + * Item Properties + **/ + + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + // ITEMS + DisableExtraBlockProperty = config.get(ALL, "Disable Extra Blocks", false); + DisableExtraBlock = DisableExtraBlockProperty.getBoolean(false); + + DisableExtraItemsProperty = config.get(ALL, "Disable Extra Items", false); + DisableExtraItems = DisableExtraItemsProperty.getBoolean(false); + + // Gun + GunsEnabledProperty = config.get(GUN, "Guns Enabled", true, "Set to false if you want to disable guns"); + GunsEnabled = GunsEnabledProperty.getBoolean(true); + MachineGunAmmo = config.get(GUN, "Machine Gun Ammo", 8, "Max Clip of a Machine Gun").getInt(8); + MachineGunTickSpeed = config.get(GUN, "Machine Gun Fire Rate", 6, "How many ticks to shoot bullets at").getInt(6); + + // Convert to Legacy + if (CustomNpcs.legacyExist) { + DisableExtraBlock = LegacyConfig.DisableExtraBlock; + DisableExtraBlockProperty.set(DisableExtraBlock); + + DisableExtraItems = LegacyConfig.DisableExtraItems; + DisableExtraItemsProperty.set(DisableExtraItems); + + GunsEnabled = LegacyConfig.GunsEnabled; + GunsEnabledProperty.set(GunsEnabled); + } + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its item IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigMain.java b/core/src/main/java/noppes/npcs/config/ConfigMain.java new file mode 100644 index 000000000..34184db2b --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigMain.java @@ -0,0 +1,339 @@ +package noppes.npcs.config; + +import common.minecraftforge.common.config.IConfigProperty; +import common.minecraftforge.common.config.IConfiguration; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.legacy.LegacyConfig; +import org.apache.logging.log4j.Level; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ConfigMain { + public static IConfiguration config; + + public final static String GENERAL = "General"; + public final static String NPC = "NPC"; + public final static String UPDATE = "Update"; + public final static String PARTY = "PARTY"; + public final static String PROFILES = "Profile"; + public final static String ATTRIBUTES = "Attributes"; + + /** + * General Main Properties + **/ + + public static IConfigProperty EnableUpdateCheckerProperty; + public static boolean EnableUpdateChecker = true; + + public static IConfigProperty DisableEnchantsProperty; + public static boolean DisableEnchants = false; + + public static IConfigProperty EnchantStartIdProperty; + public static int EnchantStartId = 100; + + public static IConfigProperty LeavesDecayEnabledProperty; + public static boolean LeavesDecayEnabled = true; + + public static IConfigProperty VineGrowthEnabledProperty; + public static boolean VineGrowthEnabled = true; + + public static IConfigProperty IceMeltsEnabledProperty; + public static boolean IceMeltsEnabled = true; + + public static IConfigProperty SoulStoneAnimalsProperty; + public static boolean SoulStoneAnimals = true; + + public static IConfigProperty SoulStoneVillagersProperty; + public static boolean SoulStoneVillagers = false; + + public static IConfigProperty SoulStoneNPCsProperty; + public static boolean SoulStoneNPCs = false; + + public static IConfigProperty SoulStoneFriendlyNPCsProperty; + public static boolean SoulStoneFriendlyNPCs = false; + + public static IConfigProperty DatFormatProperty; + public static boolean DatFormat = false; + + public static IConfigProperty MarketDatFormatProperty; + public static boolean MarketDatFormat = false; + + public static IConfigProperty PartiesEnabledProperty; + public static boolean PartiesEnabled; + + public static IConfigProperty PartyFriendlyFireEnabledProperty; + public static boolean PartyFriendlyFireEnabled; + + public static IConfigProperty DefaultMinPartySizeProperty; + public static int DefaultMinPartySize = 1; + + public static IConfigProperty DefaultMaxPartySizeProperty; + public static int DefaultMaxPartySize = 4; + + /** + * Attribute Properties + **/ + public static boolean AttributesEnabled = true; + public static int AttributesCriticalBoost = 100; + + /** + * Profile Properties + **/ + public static boolean ProfilesEnabled = true; + public static boolean AllowProfileBackups = true; + public static int ProfileBackupAmount = 5; + + public static IConfigProperty RegionProfileSwitchingProperty; + public static boolean RegionProfileSwitching = false; + + public static IConfigProperty RestrictedProfileRegionsProperty; + public static List> RestrictedProfileRegions = new ArrayList<>(); + + public static int DefaultProfileSlots = 5; + + + /** + * General NPC Properties + **/ + + public static IConfigProperty OpsOnlyProperty; + public static boolean OpsOnly = false; + + public static IConfigProperty NpcNavRangeProperty; + public static int NpcNavRange = 32; + + public static IConfigProperty NpcSizeLimitProperty; + public static int NpcSizeLimit = 100; + + public static IConfigProperty DefaultInteractLineProperty; + public static String DefaultInteractLine = "Hello @p"; + + public static IConfigProperty ChunkLoadersProperty; + public static int ChunkLoaders = 20; + + public static IConfigProperty DialogImageLimitProperty; + public static int DialogImageLimit = 10; + + public static IConfigProperty SkinOverlayLimitProperty; + public static int SkinOverlayLimit = 10; + + public static IConfigProperty NpcUseOpCommandsProperty; + public static boolean NpcUseOpCommands = false; + + public static IConfigProperty HitBoxScaleMaxProperty; + public static int HitBoxScaleMax = 15; + + /** + * Update Properties + **/ + + public static IConfigProperty TrackedQuestUpdateFrequencyProperty; + public static int TrackedQuestUpdateFrequency = 5; + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + // General + EnableUpdateCheckerProperty = config.get(GENERAL, "Enables Update Checker", true); + EnableUpdateChecker = EnableUpdateCheckerProperty.getBoolean(true); + + DisableEnchantsProperty = config.get(GENERAL, "Disable Enchants", false); + DisableEnchants = DisableEnchantsProperty.getBoolean(false); + + EnchantStartIdProperty = config.get(GENERAL, "Enchant Start ID", 100, "Start ID for enchants. IDs can only range from 0-256"); + EnchantStartId = EnchantStartIdProperty.getInt(100); + + LeavesDecayEnabledProperty = config.get(GENERAL, "Leaves Decay Enabled", true); + LeavesDecayEnabled = LeavesDecayEnabledProperty.getBoolean(true); + + VineGrowthEnabledProperty = config.get(GENERAL, "Vine Growth Enabled", true); + VineGrowthEnabled = VineGrowthEnabledProperty.getBoolean(true); + + IceMeltsEnabledProperty = config.get(GENERAL, "Ice Melt Enabled", true); + IceMeltsEnabled = IceMeltsEnabledProperty.getBoolean(true); + + SoulStoneAnimalsProperty = config.get(GENERAL, "Normal playes can use soulstone on Animals", true); + SoulStoneAnimals = SoulStoneAnimalsProperty.getBoolean(true); + + SoulStoneVillagersProperty = config.get(GENERAL, "Normal playes can use soulstone on Villagers", true); + SoulStoneVillagers = SoulStoneVillagersProperty.getBoolean(true); + + SoulStoneNPCsProperty = config.get(GENERAL, "Normal playes can use soulstone on NPCs", false); + SoulStoneNPCs = SoulStoneNPCsProperty.getBoolean(false); + + SoulStoneFriendlyNPCsProperty = config.get(GENERAL, "Normal playes can use soulstone on Friendly NPCs", false); + SoulStoneFriendlyNPCs = SoulStoneFriendlyNPCsProperty.getBoolean(false); + + DatFormatProperty = config.get(GENERAL, "Dat Format for PlayerData", false, "You need to use '/kamkeel config playerdata' to convert existing playerdata to new format."); + DatFormat = DatFormatProperty.getBoolean(false); + + MarketDatFormatProperty = config.get(GENERAL, "Dat Format for Market", false, "You need to use '/kamkeel config market' to convert existing market to new format."); + MarketDatFormat = MarketDatFormatProperty.getBoolean(false); + + // NPC + NpcNavRangeProperty = config.get(NPC, "NPC Navigation Range", 32, "Navigation search range for NPCs. Not recommended to increase if you have a slow pc or on a server. Minimum of 16, maximum of 96."); + NpcNavRange = NpcNavRangeProperty.getInt(32); + + OpsOnlyProperty = config.get(NPC, "Only Ops Edit NPCs", false, "Only ops can create and edit npcs"); + OpsOnly = OpsOnlyProperty.getBoolean(false); + + NpcUseOpCommandsProperty = config.get(NPC, "NPC Use Op Commands", false, "Set to true if you want the dialog command option to be able to use op commands like tp etc"); + NpcUseOpCommands = NpcUseOpCommandsProperty.getBoolean(false); + + DefaultInteractLineProperty = config.get(NPC, "Default Interaction Line", "Hello @p", "Default interact line. Leave empty to not have one"); + DefaultInteractLine = DefaultInteractLineProperty.getString(); + + ChunkLoadersProperty = config.get(NPC, "Chunk Loader Limit", 20, "Number of chunk loading npcs that can be active at the same time"); + ChunkLoaders = ChunkLoadersProperty.getInt(20); + + DialogImageLimitProperty = config.get(NPC, "Dialog Image Limit", 10, "The maximum number of images any dialog can hold."); + DialogImageLimit = DialogImageLimitProperty.getInt(10); + + NpcSizeLimitProperty = config.get(NPC, "NPC Size Limit", 100, "Size limit for NPCs. Default 100, larger sizes may cause lag on clients and servers that can't take it!"); + NpcSizeLimit = NpcSizeLimitProperty.getInt(100); + + SkinOverlayLimitProperty = config.get(NPC, "Skin Overlay Limit", 10, "The maximum number of overlays any npc/player can hold."); + SkinOverlayLimit = SkinOverlayLimitProperty.getInt(10); + + HitBoxScaleMaxProperty = config.get(NPC, "HitBox Scale Limit", 15, "The maximum scale factor for a custom hitbox"); + HitBoxScaleMax = HitBoxScaleMaxProperty.getInt(15); + + //PARTY + PartiesEnabledProperty = config.get(PARTY, "Parties Enabled", true, "Determines whether the party system is enabled or not."); + PartiesEnabled = PartiesEnabledProperty.getBoolean(); + + PartyFriendlyFireEnabledProperty = config.get(PARTY, "Party Friendly Fire", true, "Determines whether friendly fire can be toggled in parties."); + PartyFriendlyFireEnabled = PartyFriendlyFireEnabledProperty.getBoolean(); + + DefaultMinPartySizeProperty = config.get(PARTY, "Default Min Party Size", 1, "When creating a new Quest sets the default min party size"); + DefaultMinPartySize = DefaultMinPartySizeProperty.getInt(4); + + DefaultMaxPartySizeProperty = config.get(PARTY, "Default Max Party Size", 4, "When creating a new Quest sets the default max party size"); + DefaultMaxPartySize = DefaultMaxPartySizeProperty.getInt(4); + + // Update + TrackedQuestUpdateFrequencyProperty = config.get(UPDATE, "Tracked Quest Update Frequency", 5, "How often in seconds to update a players tracked quest. [Only applies to Item Quest currently]"); + TrackedQuestUpdateFrequency = TrackedQuestUpdateFrequencyProperty.getInt(5); + + // PROFILES + ProfilesEnabled = config.get(PROFILES, "Enable Profiles", true, "Allow the use of character Profiles").getBoolean(true); + AllowProfileBackups = config.get(PROFILES, "Enable Profile Backups", true, "Will create backups of profile changes").getBoolean(true); + ProfileBackupAmount = config.get(PROFILES, "Number of Backups", 5, "How many backups per player to save").getInt(5); + + RegionProfileSwitchingProperty = config.get(PROFILES, "Region Profile Switching", false, "If true, only allows profile switching in certain regions."); + RegionProfileSwitching = RegionProfileSwitchingProperty.getBoolean(false); + + RestrictedProfileRegionsProperty = config.get(PROFILES, "Restricted Profile Regions", new String[]{ + "0, 100, 64, 100, 200, 80, 200", + "1, 50, 60, 50, 150, 75, 150" + }, "List of restricted regions where profile switching is enabled. Format: DIM, X1, Y1, Z1, X2, Y2, Z2"); + + DefaultProfileSlots = config.get(PROFILES, "Default Profile Slots", 5, "This is the default amount without any permission handling").getInt(5); + + RestrictedProfileRegions.clear(); + for (String region : RestrictedProfileRegionsProperty.getStringList()) { + String[] parts = region.split(", "); + List regionList = new ArrayList<>(); + for (String part : parts) { + regionList.add(Integer.parseInt(part)); + } + RestrictedProfileRegions.add(regionList); + } + + config.setCategoryPropertyOrder(ATTRIBUTES, new ArrayList<>(Arrays.asList("Enable Attributes", + "Critical Amount"))); + + // Attributes + AttributesEnabled = config.get(ATTRIBUTES, "Enable Attributes", true, "Allows Attributes to be applied to Items and Armors").getBoolean(true); + AttributesCriticalBoost = config.get(ATTRIBUTES, "Critical Amount", 100, "The boost in damage received by achieving a critical hit. Takes an number so 100 is 100% extra damage").getInt(100); + + // Convert to Legacy + if (CustomNpcs.legacyExist) { + EnableUpdateChecker = LegacyConfig.EnableUpdateChecker; + EnableUpdateCheckerProperty.set(EnableUpdateChecker); + + DisableEnchants = LegacyConfig.DisableEnchants; + DisableEnchantsProperty.set(DisableEnchants); + + EnchantStartId = LegacyConfig.EnchantStartId; + EnchantStartIdProperty.set(EnchantStartId); + + LeavesDecayEnabled = LegacyConfig.LeavesDecayEnabled; + LeavesDecayEnabledProperty.set(LeavesDecayEnabled); + + VineGrowthEnabled = LegacyConfig.VineGrowthEnabled; + VineGrowthEnabledProperty.set(VineGrowthEnabled); + + IceMeltsEnabled = LegacyConfig.IceMeltsEnabled; + IceMeltsEnabledProperty.set(IceMeltsEnabled); + + SoulStoneAnimals = LegacyConfig.SoulStoneAnimals; + SoulStoneAnimalsProperty.set(SoulStoneAnimals); + + SoulStoneVillagers = LegacyConfig.SoulStoneVillagers; + SoulStoneVillagersProperty.set(SoulStoneVillagers); + + SoulStoneNPCs = LegacyConfig.SoulStoneNPCs; + SoulStoneNPCsProperty.set(SoulStoneNPCs); + + SoulStoneFriendlyNPCs = LegacyConfig.SoulStoneFriendlyNPCs; + SoulStoneFriendlyNPCsProperty.set(SoulStoneFriendlyNPCs); + + OpsOnly = LegacyConfig.OpsOnly; + OpsOnlyProperty.set(OpsOnly); + + NpcNavRange = LegacyConfig.NpcNavRange; + NpcNavRangeProperty.set(NpcNavRange); + + NpcSizeLimit = LegacyConfig.NpcSizeLimit; + NpcSizeLimitProperty.set(NpcSizeLimit); + + DefaultInteractLine = LegacyConfig.DefaultInteractLine; + DefaultInteractLineProperty.set(DefaultInteractLine); + + ChunkLoaders = LegacyConfig.ChunkLoaders; + ChunkLoadersProperty.set(ChunkLoaders); + + DialogImageLimit = LegacyConfig.DialogImageLimit; + DialogImageLimitProperty.set(DialogImageLimit); + + SkinOverlayLimit = LegacyConfig.SkinOverlayLimit; + SkinOverlayLimitProperty.set(SkinOverlayLimit); + + NpcUseOpCommands = LegacyConfig.NpcUseOpCommands; + NpcUseOpCommandsProperty.set(NpcUseOpCommands); + } + + if (NpcNavRange < 16) { + NpcNavRange = 16; + } + if (NpcNavRange > 96) { + NpcNavRange = 96; + } + + if (NpcSizeLimit < 1) { + NpcSizeLimit = 1; + } + + if (DialogImageLimit < 0) { + DialogImageLimit = 0; + } + + if (SkinOverlayLimit < 0) { + SkinOverlayLimit = 0; + } + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its main IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigMarket.java b/core/src/main/java/noppes/npcs/config/ConfigMarket.java new file mode 100644 index 000000000..a9019f85a --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -0,0 +1,255 @@ +package noppes.npcs.config; + +import org.apache.logging.log4j.Level; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; + +public class ConfigMarket { + public static IConfiguration config; + + public final static String CURRENCY = "Market.Currency"; + public final static String AUCTION = "Market.Auction"; + public final static String AUCTION_BLACKLIST = "Market.Auction.Blacklist"; + public final static String AUCTION_LOGGING = "Market.Auction.Logging"; + + // Max trades grid = 9x5 = 45 slots + public static final int MAX_TRADE_SLOTS = 45; + public static final int MIN_TRADE_SLOTS = 1; + + // ========================================= + // Currency Settings + // ========================================= + public static boolean UseVault = false; + public static String CurrencyName = "Coins"; + public static long StartingBalance = 0; + public static long MaxBalance = Long.MAX_VALUE; + + // ========================================= + // Auction Settings + // ========================================= + public static boolean AuctionEnabled = true; + public static int AuctionDurationHours = 24; + public static long ListingFee = 10; + public static long MinimumListingPrice = 1; + public static double SalesTaxPercent = 0.05; + public static int DefaultMaxTrades = 8; + public static int SnipeProtectionMinutes = 2; + public static int ClaimExpirationDays = 20; + public static double MinBidIncrementPercent = 0.05; + public static double CancellationPenaltyPercent = 0.10; + + // ========================================= + // Auction Blacklist Settings + // ========================================= + public static boolean BlacklistEnabled = true; + public static String[] BlacklistedItems = new String[]{}; + public static String[] BlacklistedMods = new String[]{}; + public static String[] BlacklistedNBTTags = new String[]{}; + + // ========================================= + // Auction Logging Settings + // ========================================= + public static boolean AuctionLoggingEnabled = false; + public static boolean LogAuctionCreated = true; + public static boolean LogAuctionBid = true; + public static boolean LogAuctionBuyout = true; + public static boolean LogAuctionSold = true; + public static boolean LogAuctionExpired = true; + public static boolean LogAuctionCancelled = true; + public static boolean LogAuctionClaimed = true; + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + // ========================================= + // Currency Settings + // ========================================= + config.setCategoryComment(CURRENCY, "CNPC+ Currency system settings. When UseVault is enabled, all currency operations use Vault instead of the built-in system."); + + UseVault = config.get(CURRENCY, "Use Vault", false, + "If true, use Vault API for all currency operations instead of CNPC+ built-in currency. The built-in currency data will be preserved but unused.").getBoolean(false); + + CurrencyName = config.get(CURRENCY, "Currency Name", "Coins", + "Display name for the currency (e.g. 'Coins', 'Gold', 'Credits')").getString(); + + StartingBalance = parseLongConfig(config.get(CURRENCY, "Starting Balance", "0", + "Starting currency balance for new players").getString(), 0); + + MaxBalance = parseLongConfig(config.get(CURRENCY, "Max Balance", String.valueOf(Long.MAX_VALUE), + "Maximum currency balance a player can have").getString(), Long.MAX_VALUE); + if (MaxBalance < 0) MaxBalance = Long.MAX_VALUE; + + // ========================================= + // Auction Settings + // ========================================= + config.setCategoryComment(AUCTION, "Auction system settings. The Auction allows players to list items for sale with bidding and buyout options."); + + AuctionEnabled = config.get(AUCTION, "Enable Auction", true, + "Enable the Auction system").getBoolean(true); + + AuctionDurationHours = config.get(AUCTION, "Auction Duration Hours", 24, + "Default duration for auctions in hours").getInt(24); + if (AuctionDurationHours < 1) AuctionDurationHours = 1; + + ListingFee = parseLongConfig(config.get(AUCTION, "Listing Fee", "10", + "Flat fee charged when creating a listing").getString(), 10); + if (ListingFee < 0) ListingFee = 0; + + MinimumListingPrice = parseLongConfig(config.get(AUCTION, "Minimum Listing Price", "1", + "Minimum starting price for auction listings").getString(), 1); + if (MinimumListingPrice < 1) MinimumListingPrice = 1; + + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, + "Percentage of sale price taken as tax (0.05 = 5%). Tax is deleted as a currency sink.").getDouble(0.05); + SalesTaxPercent = Math.max(0.0, Math.min(1.0, SalesTaxPercent)); + + int maxTrades = config.get(AUCTION, "Default Max Trades", 8, + "Default maximum number of trade slots per player (listings + bids + claims). Min: 1, Max: 45. Players can have more via customnpcs.auction.trades.X permissions.").getInt(8); + DefaultMaxTrades = Math.max(MIN_TRADE_SLOTS, Math.min(MAX_TRADE_SLOTS, maxTrades)); + + SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 2, + "When a bid is placed with less than this many minutes remaining, the auction is extended to this duration").getInt(2); + if (SnipeProtectionMinutes < 0) SnipeProtectionMinutes = 0; + + ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 20, + "Number of days before unclaimed items/currency are deleted").getInt(20); + if (ClaimExpirationDays < 1) ClaimExpirationDays = 1; + + MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, + "Minimum bid increment as a percentage of current bid (0.05 = 5%)").getDouble(0.05); + MinBidIncrementPercent = Math.max(0.0, Math.min(1.0, MinBidIncrementPercent)); + + CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10, + "Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10); + CancellationPenaltyPercent = Math.max(0.0, Math.min(1.0, CancellationPenaltyPercent)); + + // ========================================= + // Auction Blacklist Settings + // ========================================= + config.setCategoryComment(AUCTION_BLACKLIST, + "Item Blacklist settings for the Auction House.\n" + + "Prevents specific items, mods, or items with certain NBT tags from being listed.\n\n" + + "ITEM FORMAT: Use 'modid:itemname' format (e.g., 'minecraft:bedrock', 'customnpcs:npcWand')\n" + + "WILDCARDS: Use * for wildcards (e.g., 'customnpcs:npc*' blocks all items starting with 'npc')\n" + + "MOD FORMAT: Use just the mod ID (e.g., 'projecte' blocks all items from that mod)\n" + + "NBT FORMAT: Use the NBT tag key name (e.g., 'AdminOnly' blocks items with that tag)"); + + BlacklistEnabled = config.get(AUCTION_BLACKLIST, "Enable Blacklist", true, + "Enable item blacklist checking when creating listings").getBoolean(true); + + BlacklistedItems = config.get(AUCTION_BLACKLIST, "Blacklisted Items", new String[]{ + "minecraft:bedrock", + "minecraft:command_block", + "customnpcs:npcWand", + "customnpcs:npcMobCloner", + "customnpcs:npcScripter", + "customnpcs:npcMovingPath", + "customnpcs:npcMounter", + "customnpcs:npcTeleporter", + "customnpcs:npcTool", + "customnpcs:npcSoulstoneFilled" + }, + "Items that cannot be listed on the Auction House.\n" + + "Format: modid:itemname (supports * wildcards)\n" + + "Example: 'minecraft:diamond_sword' or 'customnpcs:npc*'").getStringList(); + + BlacklistedMods = config.get(AUCTION_BLACKLIST, "Blacklisted Mods", new String[]{}, + "All items from these mods are blocked from the Auction House.\n" + + "Format: modid (e.g., 'projecte', 'thaumcraft')").getStringList(); + + BlacklistedNBTTags = config.get(AUCTION_BLACKLIST, "Blacklisted NBT Tags", new String[]{}, + "Items containing any of these NBT tag keys are blocked.\n" + + "Checks the root level of the item's NBT compound.\n" + + "Example: 'AdminOnly', 'CreativeMode'").getStringList(); + + // ========================================= + // Auction Logging Settings + // ========================================= + config.setCategoryComment(AUCTION_LOGGING, "Auction logging settings. Enable specific log types to track auction activity."); + + AuctionLoggingEnabled = config.get(AUCTION_LOGGING, "Enable Auction Logging", false, + "Master switch for auction logging. If false, no auction events are logged.").getBoolean(false); + + LogAuctionCreated = config.get(AUCTION_LOGGING, "Log Created", true, + "Log when auctions are created").getBoolean(true); + + LogAuctionBid = config.get(AUCTION_LOGGING, "Log Bid", true, + "Log when bids are placed").getBoolean(true); + + LogAuctionBuyout = config.get(AUCTION_LOGGING, "Log Buyout", true, + "Log when auctions are bought out").getBoolean(true); + + LogAuctionSold = config.get(AUCTION_LOGGING, "Log Sold", true, + "Log when auctions end with a winner").getBoolean(true); + + LogAuctionExpired = config.get(AUCTION_LOGGING, "Log Expired", true, + "Log when auctions expire with no bids").getBoolean(true); + + LogAuctionCancelled = config.get(AUCTION_LOGGING, "Log Cancelled", true, + "Log when auctions are cancelled").getBoolean(true); + + LogAuctionClaimed = config.get(AUCTION_LOGGING, "Log Claimed", true, + "Log when claims are collected").getBoolean(true); + + // Set category order + config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( + "Use Vault", + "Currency Name", + "Starting Balance", + "Max Balance" + ))); + + config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( + "Enable Auction", + "Auction Duration Hours", + "Listing Fee", + "Minimum Listing Price", + "Sales Tax Percent", + "Default Max Trades", + "Snipe Protection Minutes", + "Claim Expiration Days", + "Min Bid Increment Percent", + "Cancellation Penalty Percent" + ))); + + config.setCategoryPropertyOrder(AUCTION_BLACKLIST, new ArrayList<>(Arrays.asList( + "Enable Blacklist", + "Blacklisted Items", + "Blacklisted Mods", + "Blacklisted NBT Tags" + ))); + + config.setCategoryPropertyOrder(AUCTION_LOGGING, new ArrayList<>(Arrays.asList( + "Enable Auction Logging", + "Log Created", + "Log Bid", + "Log Buyout", + "Log Sold", + "Log Expired", + "Log Cancelled", + "Log Claimed" + ))); + + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } + + private static long parseLongConfig(String value, long defaultValue) { + if (value == null || value.trim().isEmpty()) return defaultValue; + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigMixin.java b/core/src/main/java/noppes/npcs/config/ConfigMixin.java new file mode 100644 index 000000000..620f6f7d6 --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigMixin.java @@ -0,0 +1,67 @@ +package noppes.npcs.config; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import org.apache.logging.log4j.Level; + +import java.io.File; + +public class ConfigMixin { + public static IConfiguration config; + + public final static String CLIENT = "CLIENT"; + public final static String SERVER = "SERVER"; + public final static String GENERAL = "GENERAL"; + + /** + * General Properties + **/ + public static IConfigProperty EntityRendererMixinProperty; + public static boolean EntityRendererMixin = true; + + public static IConfigProperty AnimationMixinProperty; + public static boolean AnimationMixin = true; + + public static IConfigProperty FirstPersonAnimationMixinProperty; + public static boolean FirstPersonAnimationMixin = true; + + public static IConfigProperty EntitySpawnFixMixinProperty; + public static boolean EntitySpawnFixMixin = true; + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + EntityRendererMixinProperty = config.get(CLIENT, "IEntity Render Mixin", true, "Enables Overlay Mixins for Conflicts relating to Optifine or other Skin Renderers. If crashes occur, please disable."); + EntityRendererMixin = EntityRendererMixinProperty.getBoolean(true); + + AnimationMixinProperty = config.get(CLIENT, "Animation Mixin", true, "Enables mixins for the ModelRenderer and RenderPlayer classes, allowing for additional animation functionality in the API. If crashes or visual errors occur, please disable."); + AnimationMixin = AnimationMixinProperty.getBoolean(true); + + if (AnimationMixin) { + FirstPersonAnimationMixinProperty = config.get(CLIENT, "First Person Animation Mixin", true, "Enables mixins for the ItemRenderer class, allowing for animations to be visible in first person. Can only be enabled if the animation mixin is enabled. If crashes or visual errors occur, please disable."); + FirstPersonAnimationMixin = FirstPersonAnimationMixinProperty.getBoolean(true); + } else { + FirstPersonAnimationMixin = false; + } + + EntitySpawnFixMixinProperty = config.get(SERVER, "IEntity Spawn Packet Fix", true, "Fixes a Forge race condition where mod IEntity spawn packets can be lost or sent to the wrong player. This prevents invisible entities on servers with multiple players."); + EntitySpawnFixMixin = EntitySpawnFixMixinProperty.getBoolean(true); + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its mixin IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/config/ConfigScript.java b/core/src/main/java/noppes/npcs/config/ConfigScript.java new file mode 100644 index 000000000..57f621f6f --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/ConfigScript.java @@ -0,0 +1,217 @@ +package noppes.npcs.config; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.developer.Developer; +import noppes.npcs.CustomNpcs; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.legacy.LegacyConfig; +import org.apache.logging.log4j.Level; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.UUID; + +public class ConfigScript { + public static IConfiguration config; + + public final static String GENERAL = "Scripting General"; + public final static String CUSTOMIZATION = "Scripting Customization"; + + /** + * General Properties + **/ + public static IConfigProperty ScriptingEnabledProperty; + public static boolean ScriptingEnabled = true; + + public static IConfigProperty ScriptOpsOnlyProperty; + public static boolean ScriptOpsOnly = false; + + public static IConfigProperty GlobalPlayerScriptsProperty; + public static boolean GlobalPlayerScripts = true; + + public static IConfigProperty GlobalForgeScriptsProperty; + public static boolean GlobalForgeScripts = true; + + public static IConfigProperty GlobalNPCScriptsProperty; + public static boolean GlobalNPCScripts = false; + + /** + * Customization Properties + **/ + public static IConfigProperty ScriptingECMA6Property; + public static boolean ScriptingECMA6 = false; + + public static IConfigProperty ExpandedScriptLimitProperty; + public static int ExpandedScriptLimit = 2; + + public static IConfigProperty ScriptDevIDsProperty; + public static String ScriptDevIDs = ""; + + public static IConfigProperty EnableBannedClassesProperty; + public static boolean EnableBannedClasses = false; + + public static IConfigProperty RunLoadedScriptsFirstProperty; + public static boolean RunLoadedScriptsFirst = true; + + public static IConfigProperty BannedClassesProperty; + public final static HashSet BannedClasses = new HashSet<>(); + + public static int ActionManagerTickDefault = 5; + + public static IConfigProperty ClearActionsOnDeathProperty; + public static boolean ClearActionsOnDeath = true; + + public static IConfigProperty ShowImplementationFieldsInAutocompleteProperty; + public static boolean ShowImplementationFieldsInAutocomplete = false; + + public static boolean IndividualPlayerScripts = false; + + public static void init(File configFile) { + config = new IConfiguration(configFile); + + try { + config.load(); + + // General + ScriptingEnabledProperty = config.get(GENERAL, "Enable Scripting", true, "Enables/Disables ALL scripting. You can still see and write code in the scripter, but these scripts won't run. True by default"); + ScriptingEnabled = ScriptingEnabledProperty.getBoolean(true); + + ScriptOpsOnlyProperty = config.get(GENERAL, "OPs Only", false, "Only ops can see and edit scripts"); + ScriptOpsOnly = ScriptOpsOnlyProperty.getBoolean(false); + + GlobalPlayerScriptsProperty = config.get(GENERAL, "Enable Global Player Scripts", true, "Enables global player event scripts to be used in the scripter. You can still see and write code in the scripter, but these scripts won't run. True by default."); + GlobalPlayerScripts = GlobalPlayerScriptsProperty.getBoolean(true); + + GlobalForgeScriptsProperty = config.get(GENERAL, "Enable Global Forge Event Scripts", true, "Enables global forge event scripts to be used in the scripter. You can still see and write code in the scripter, but these scripts won't run. True by default."); + GlobalForgeScripts = GlobalForgeScriptsProperty.getBoolean(true); + + GlobalNPCScriptsProperty = config.get(GENERAL, "Enable Global NPC Scripts", false, "Enables global NPC scripts to be used in the scripter. You can still see and write code in the scripter, but these scripts won't run. False by default, use with caution!"); + GlobalNPCScripts = GlobalNPCScriptsProperty.getBoolean(false); + + // Customization + ScriptingECMA6Property = config.get(CUSTOMIZATION, "ECMA6 Scripting Language", false, + "Enables/Disables the use of the the ECMA6 Javascript standard instead of ECMA5.1." + + "\nEnabling this adds many more features to JS in scripts. Only use if on Java 8 or higher." + + "\nNot all ECMA 6 language is supported through this functionality."); + ScriptingECMA6 = ScriptingECMA6Property.getBoolean(false); + + ExpandedScriptLimitProperty = config.get(CUSTOMIZATION, "Script Tag Limit", 2, + "If scripts are too long (>65535 characters), they normally won't be saved in NBT data.\n" + + "This config adds additional compound tags to scripts that need it, so you can store much larger scripts!\n" + + "Every additional compound tag adds 65535 more characters to your script length limit. Use incrementally, with caution."); + ExpandedScriptLimit = ExpandedScriptLimitProperty.getInt(2); + + ScriptDevIDsProperty = config.get(CUSTOMIZATION, "Script Dev UUIDs", "", + "Comma separated list of player UUIDs that can see and edit scripts. If ScriptsOpsOnly is true,\n" + + "ops and players with these IDs can see and edit scripts. Example:\n" + + "b876ec32-e396-476b-a115-8438d83c67d4,069a79f4-44e9-4726-a5be-fca90e38aaf5,be951074-c7ea-4f02-a725-bf017bc88650\n" + + "Get a player's UUID from a site like NameMC or the API IPlayer.getUniqueID() function!\n" + + "If left empty and ScriptsOpsOnly is false, anyone can see and edit scripts with a scripter."); + ScriptDevIDs = ScriptDevIDsProperty.getString(); + + EnableBannedClassesProperty = config.get(CUSTOMIZATION, "Enable Banned Classes", false, "Enables the Banned Classes Functionality"); + EnableBannedClasses = EnableBannedClassesProperty.getBoolean(false); + + RunLoadedScriptsFirstProperty = config.get(CUSTOMIZATION, "Run Loaded Scripts First", true, + "If scripts have been loaded from the scripting GUI, the script engine will evaluates them by\n" + + "merging your loaded scripts with your main script, then running the combined script.\n" + + "This config determines the order in while the loaded scripts are merged with your main scripts: before (true), or after (false)."); + RunLoadedScriptsFirst = RunLoadedScriptsFirstProperty.getBoolean(true); + + BannedClassesProperty = config.get(CUSTOMIZATION, "Banned Classes", "java.net.URL,java.net.URI", + "Comma separated list of classes that cannot be used in scripts through Java.for().\n" + + "Classes must be fully written out with library names preceding them.\n" + + "This is a feature ONLY AVAILABLE ON NASHORN."); + + + IndividualPlayerScripts = config.get(CUSTOMIZATION, "Individual Player Scripts", false, "Acts similar to CNPC 1.12 where Player Scripts like Init are run PER Player").getBoolean(false); + ActionManagerTickDefault = config.get(CUSTOMIZATION, "Action Manager Tick Default", ActionManagerTickDefault, "How frequent to update the action manager ticking tasks").getInt(ActionManagerTickDefault); + ClearActionsOnDeathProperty = config.get(CUSTOMIZATION, "Clear Actions On Death", true, "If true, clears the Action Manager for players and NPCs when they die"); + ClearActionsOnDeath = ClearActionsOnDeathProperty.getBoolean(true); + + ShowImplementationFieldsInAutocompleteProperty = config.get(CUSTOMIZATION, "Autocomplete Implementation Fields", false, + "If true, shows fields from implementation classes in autocomplete menu like 'event.npc'.\n"+ + " This can be useful for advanced scripters who want to use fields from implementation classes."); + ShowImplementationFieldsInAutocomplete = ShowImplementationFieldsInAutocompleteProperty.getBoolean(false); + + // Convert to Legacy + if (CustomNpcs.legacyExist) { + ScriptingEnabled = LegacyConfig.ScriptingEnabled; + ScriptingEnabledProperty.set(ScriptingEnabled); + + ScriptOpsOnly = LegacyConfig.ScriptOpsOnly; + ScriptOpsOnlyProperty.set(ScriptOpsOnly); + + GlobalPlayerScripts = LegacyConfig.GlobalPlayerScripts; + GlobalPlayerScriptsProperty.set(GlobalPlayerScripts); + + GlobalForgeScripts = LegacyConfig.GlobalForgeScripts; + GlobalForgeScriptsProperty.set(GlobalForgeScripts); + + GlobalNPCScripts = LegacyConfig.GlobalNPCScripts; + GlobalNPCScriptsProperty.set(GlobalNPCScripts); + + ScriptingECMA6 = LegacyConfig.ScriptingECMA6; + ScriptingECMA6Property.set(ScriptingECMA6); + + ExpandedScriptLimit = LegacyConfig.ExpandedScriptLimit; + ExpandedScriptLimitProperty.set(ExpandedScriptLimit); + + ScriptDevIDs = LegacyConfig.ScriptDevIDs; + ScriptDevIDsProperty.set(ScriptDevIDs); + } + + if (ExpandedScriptLimit < 0) + ExpandedScriptLimit = 0; + + if (ActionManagerTickDefault < 0) { + ActionManagerTickDefault = 1; + } + + try { + Developer.ScriptUser.clear(); + String[] uuidStrings = ScriptDevIDs.split(","); + for (String s : uuidStrings) { + Developer.ScriptUser.add(UUID.fromString(s)); + } + } catch (Exception ignored) { + } + + String bannedClassesString = BannedClassesProperty.getString(); + try { + BannedClasses.clear(); + BannedClasses.addAll(Arrays.asList(bannedClassesString.split(","))); + } catch (Exception ignored) { + } + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its scripting IConfiguration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } + + public static boolean isScriptDev(IPlayer player) { + return Developer.ScriptUser.contains(player.getUniqueID()) || Developer.Universal.contains(player.getUniqueID()); + } + + public static boolean canScript(IPlayer player, CustomNpcsPermissions.Permission perm) { + boolean isOp = PlatformServiceHolder.get().getServer().getConfigurationManager().func_152596_g(player.getGameProfile()); + boolean scriptDev = isScriptDev(player); + if (ScriptOpsOnly) { + return isOp || scriptDev; + } + + return scriptDev || CustomNpcsPermissions.hasPermission(player, perm); + } +} diff --git a/core/src/main/java/noppes/npcs/config/GlyphCache.java b/core/src/main/java/noppes/npcs/config/GlyphCache.java new file mode 100644 index 000000000..ac29ccd8f --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/GlyphCache.java @@ -0,0 +1,838 @@ +package noppes.npcs.config; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; + +/** + * @author https://github.com/secretdataz/BetterFonts + * The GlyphCache class is responsible for caching pre-rendered images of every glyph using OpenGL textures. This class is also + * responsible for selecting the proper fonts to render each glyph, since Java's own "SansSerif" logical font does not always + * select the proper physical font to use (especially on less common Linux distributions). Once a pre-rendered glyph image is + * cached, it will remain stored in an OpenGL texture for the entire lifetime of the application (StringCache depends on this + * behavior). + * @todo Should have a separate glyph cache and a separate smaller point size font for rendering the GUI at its smallest size + * and for use in the F3 debug screen; may need some explicit argument in StringCache.renderString() to select the size + * @todo Need to have a config file that allows overring the font search order by locale to properly support Traditional Chinese + * hanzi, Simplified Chinese hanzi, Japanese kanji, and Korean hanja + */ +public class GlyphCache { + /** + * The width in pixels of every texture used for caching pre-rendered glyph images. Used by GlyphCache when calculating + * floating point 0.0-1.0 texture coordinates. Must be a power of two for mip-mapping to work. + */ + private static final int TEXTURE_WIDTH = 512; + + /** + * The height in pixels of every texture used for caching pre-rendered glyph images. Used by GlyphCache when calculating + * floating point 0.0-1.0 texture coordinates. Must be a power of two for mip-mapping to work. + */ + private static final int TEXTURE_HEIGHT = 512; + + /** + * Initial width in pixels of the stringImage buffer used to extract individual glyph images. + */ + private static final int STRING_WIDTH = 256; + + /** + * Initial height in pixels of the stringImage buffer used to extract individual glyph images. + */ + private static final int STRING_HEIGHT = 64; + + /** + * The width in pixels of a transparent border between individual glyphs in the cache texture. This border keeps neighboring + * glyphs from "bleeding through" when the scaled GUI resolution is not pixel aligned and sometimes results in off-by-one + * sampling of the glyph cache textures. + */ + private static final int GLYPH_BORDER = 2; + + /** + * Extra padding (in pixels) added around each glyph's reported pixel bounds before blitting into the atlas. + * Font hinting (especially GASP-aware rendering) can cause rendered pixels to extend beyond the bounds + * reported by GlyphVector.getGlyphPixelBounds(). This padding ensures those overflow pixels are captured. + */ + private static final int GLYPH_PADDING = 2; + + /** + * Minimum spacing (in pixels) inserted between adjacent glyphs in the temporary string image during + * rasterization. Each glyph is shifted right by glyphSpacing * glyphIndex to prevent kerning from + * merging adjacent glyphs. This base value is used for plain/bold styles. For italic fonts, the + * spacing is increased dynamically by {@link #computeGlyphSpacing(Font)} to account for the + * horizontal overhang caused by the italic slant. + */ + private static final int GLYPH_SPACING_BASE = 2 * GLYPH_PADDING + 1; + + /** + * Transparent (alpha zero) white background color for use with BufferedImage.clearRect(). + */ + private static Color BACK_COLOR = new Color(255, 255, 255, 0); + + /** + * The point size at which every OpenType font is rendered. + */ + private int fontSize = 18; + + /** + * If true, then enble anti-aliasing when rendering the font glyph + */ + private boolean antiAliasEnabled = false; + + /** + * The current Minecraft GUI scale factor (1-4). Glyphs are rasterized at fontSize * guiScaleFactor + * so the atlas has enough texel density for the actual display resolution. This prevents + * GL_LINEAR texture filtering from diluting alpha values at high resolutions. + */ + private int guiScaleFactor = 1; + + /** + * Return the GUI scale factor used for glyph rasterization. StringCache uses this + * to compute an effective render scale that compensates for the larger atlas images. + */ + int getGuiScaleFactor() { + return guiScaleFactor; + } + + + /** + * Temporary image for rendering a string to and then extracting the glyph images from. + */ + private BufferedImage stringImage; + + /** + * The Graphics2D associated with stringImage and used for string drawing to extract the individual glyph shapes. + */ + private Graphics2D stringGraphics; + + + + /** + * All font glyphs are packed inside this image and are then loaded from here into an OpenGL texture. + */ + private BufferedImage glyphCacheImage = new BufferedImage(TEXTURE_WIDTH, TEXTURE_HEIGHT, BufferedImage.TYPE_INT_ARGB); + + /** + * The Graphics2D associated with glyphCacheImage and used for bit blitting between stringImage. + */ + private Graphics2D glyphCacheGraphics = glyphCacheImage.createGraphics(); + + /** + * Needed for all text layout operations that create GlyphVectors (maps point size to pixel size). + * Rebuilt after setRenderingHints() so layout FRC matches rendering FRC (especially for GASP hinting). + */ + private FontRenderContext fontRenderContext = glyphCacheGraphics.getFontRenderContext(); + + /** + * Return the current FontRenderContext used for glyph layout and rendering. + * StringCache must use this same FRC when computing glyph pixel bounds for positioning, + * so that layout positions match the GASP-aware hinting used during rasterization. + */ + FontRenderContext getFontRenderContext() { + return fontRenderContext; + } + + int getGlyphPadding() { + return GLYPH_PADDING; + } + + /** + * Compute the inter-glyph spacing needed for a given font when rasterizing into the temporary + * string image. For plain and bold fonts, the base spacing (2*GLYPH_PADDING+1) is sufficient. + * For italic fonts, the slant causes the top of each glyph to lean horizontally, and at large + * raster sizes (e.g. 54pt at scale 3) this overhang can be 8+ pixels — far exceeding the base + * spacing's 1-pixel effective gap. Without adequate spacing, adjacent italic glyphs' rendered + * pixels overlap in the string image, and narrow glyphs like 'I'/'i' capture neighboring + * fragments as visible dot artifacts in their cached texture. + * + * The italic overhang is computed as: lineHeight * |italicAngle|. Java2D's standard italic + * angle is ~0.2 (approx 12°). A safety factor of 1.5× is applied since antialiasing can + * produce faint alpha slightly beyond the geometric slant boundary. + */ + private int computeGlyphSpacing(Font font) { + float italicAngle = font.getItalicAngle(); + if (italicAngle == 0 && font.isItalic()) { + italicAngle = -0.2f; // Java2D synthetic italic default shear + } + + if (italicAngle == 0) { + return GLYPH_SPACING_BASE; + } + + java.awt.font.LineMetrics lm = font.getLineMetrics("I", fontRenderContext); + float lineHeight = lm.getAscent() + lm.getDescent(); + int italicOverhang = (int) Math.ceil(lineHeight * Math.abs(italicAngle) * 1.5f); + + return Math.max(GLYPH_SPACING_BASE, 2 * GLYPH_PADDING + italicOverhang + 1); + } + + + /** + * Intermediate data array for use with textureImage.getRgb(). + */ + private int imageData[] = new int[TEXTURE_WIDTH * TEXTURE_HEIGHT]; + + /** + * A big-endian direct int buffer used with glTexSubImage2D() and glTexImage2D(). Used for loading the pre-rendered glyph + * images from the glyphCacheImage BufferedImage into OpenGL textures. This buffer uses big-endian byte ordering to ensure + * that the integers holding packed RGBA colors are stored into memory in a predictable order. + */ + private IntBuffer imageBuffer = ByteBuffer.allocateDirect(4 * TEXTURE_WIDTH * TEXTURE_HEIGHT).order(ByteOrder.BIG_ENDIAN).asIntBuffer(); + + /** + * A single integer direct buffer with native byte ordering used for returning values from glGenTextures(). + */ + private IntBuffer singleIntBuffer = GLAllocation.createDirectIntBuffer(1); + + /** + * List of all available physical fonts on the system. Used by lookupFont() to find alternate fonts. + */ + private List allFonts = Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()); + + /** + * A list of all fonts that have been returned so far by lookupFont(), and that will always be searched first for a usable font before + * searching through allFonts[]. This list will only have plain variation of a font at a dummy point size, unlike fontCache which could + * have multiple entries for the various styles (i.e. bold, italic, etc.) of a font. This list starts with Java's "SansSerif" logical + * font. + */ + protected List usedFonts = new ArrayList(); + + + /** + * ID of current OpenGL cache texture being used by cacheGlyphs() to store pre-rendered glyph images. + */ + private int textureName; + + /** + * A cache of all fonts that have at least one glyph pre-rendered in a texture. Each font maps to an integer (monotonically + * increasing) which forms the upper 32 bits of the key into the glyphCache map. This font cache can include different styles + * of the same font family like bold or italic. + */ + private LinkedHashMap fontCache = new LinkedHashMap(); + + /** + * A cache of pre-rendered glyphs mapping each glyph by its glyphcode to the position of its pre-rendered image within + * the cache texture. The key is a 64 bit number such that the lower 32 bits are the glyphcode and the upper 32 are the + * index of the font in the fontCache. This makes for a single globally unique number to identify any glyph from any font. + */ + private LinkedHashMap glyphCache = new LinkedHashMap(); + + + /** + * The X coordinate of the upper=left corner in glyphCacheImage where the next glyph image should be stored. Glyphs are + * always added left-to-right on the curren tline until it fills up, at which point they continue filling the texture on + * the next line. + */ + private int cachePosX = GLYPH_BORDER; + + /** + * The Y coordinate of the upper-left corner in glyphCacheImage where the next glyph image should be stored. Glyphs are + * stored left-to-right in horizontal lines, and top-to-bottom until the entire texture fills up. At that point, a new + * texture is allocated to keep storing additional glyphs, and the original texture remains allocated for the lifetime of + * the application. + */ + private int cachePosY = GLYPH_BORDER; + + /** + * The height in pixels of the current line of glyphs getting written into the texture. This value determines by how much + * cachePosY will get incremented when the current horizontal line in the texture fills up. + */ + private int cacheLineHeight = 0; + + /** + * This class holds information for a glyph about its pre-rendered image in an OpenGL texture. The texture coordinates in + * this class are normalized in the standard 0.0 - 1.0 OpenGL range. + */ + static class Entry { + /** + * The OpenGL texture ID that contains this glyph image. + */ + public int textureName; + + /** + * The width in pixels of the glyph image. + */ + public int width; + + /** + * The height in pixels of the glyph image. + */ + public int height; + + /** + * The horizontal texture coordinate of the upper-left corner. + */ + public float u1; + + /** + * The vertical texture coordinate of the upper-left corner. + */ + public float v1; + + /** + * The horizontal texture coordinate of the lower-right corner. + */ + public float u2; + + /** + * The vertical texture coordinate of the lower-right corner. + */ + public float v2; + } + + /** + * A single instance of GlyphCache is allocated for internal use by the StringCache class. + */ + public GlyphCache() { + /* Set background color for use with clearRect() */ + glyphCacheGraphics.setBackground(BACK_COLOR); + + /* The drawImage() to this buffer will copy all source pixels instead of alpha blending them into the current image */ + glyphCacheGraphics.setComposite(AlphaComposite.Src); + + allocateGlyphCacheTexture(); + allocateStringImage(STRING_WIDTH, STRING_HEIGHT); + + /* Use Java's logical font as the default initial font if user does not override it in some IConfiguration file */ + java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().preferLocaleFonts(); + usedFonts.add(new Font(Font.SANS_SERIF, Font.PLAIN, 72)); //size 1 > 72 + } + + /** + * Change the default font used to pre-render glyph images. If this method is called at runtime, the existing glyph images will remain cached + * in their respective textures and will remain accessible to StringCache. This method is normally called by StringCache.setDefaultFont() since + * the StringCache must also invalidate itself so it can re-layout and re-cache the glyphs for all strings using the new font. + * + * @param name the new font name + * @param size the new point size + */ + void setDefaultFont(String name, int size, boolean antiAlias) { + + //System.out.println("BetterFonts loading font \"" + name + "\""); + usedFonts.clear(); + usedFonts.add(new Font(name, Font.PLAIN, 72)); //size 1 > 72 + + fontSize = size; + antiAliasEnabled = antiAlias; + guiScaleFactor = queryGuiScaleFactor(); + setRenderingHints(); + } + + void setCustomFont(IResourceLocation location, int size, boolean antiAlias) throws Exception { + try (InputStream stream = Minecraft.getMinecraft().getResourceManager().getResource(location).getInputStream()) { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + Font font = Font.createFont(Font.TRUETYPE_FONT, stream); + ge.registerFont(font); + font = font.deriveFont(Font.PLAIN, 72); + + usedFonts.clear(); + usedFonts.add(font); + + fontSize = size; + antiAliasEnabled = antiAlias; + guiScaleFactor = queryGuiScaleFactor(); + setRenderingHints(); + } + } + + private static int queryGuiScaleFactor() { + try { + Minecraft mc = Minecraft.getMinecraft(); + if (mc != null && mc.displayWidth > 0 && mc.displayHeight > 0) { + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + return Math.max(1, sr.getScaleFactor()); + } + } catch (Exception ignored) { + } + return 1; + } + + /** + * Check if the GUI scale factor has changed (e.g. due to window resize) and invalidate the glyph + * cache if so. Glyphs must be re-rasterized at the new density to prevent GL_LINEAR filtering + * from diluting alpha at high display resolutions. + * + * @return true if the scale factor changed and glyphs were invalidated + */ + boolean checkAndUpdateScaleFactor() { + int newScale = queryGuiScaleFactor(); + if (newScale != guiScaleFactor) { + guiScaleFactor = newScale; + invalidateGlyphCache(); + return true; + } + return false; + } + + private void invalidateGlyphCache() { + glyphCache.clear(); + fontCache.clear(); + cachePosX = GLYPH_BORDER; + cachePosY = GLYPH_BORDER; + cacheLineHeight = 0; + allocateGlyphCacheTexture(); + allocateStringImage(STRING_WIDTH, STRING_HEIGHT); + } + + int fontHeight(String s) { + Font font = lookupFont(s.toCharArray(), 0, s.length(), 0); + return (int) (font.getLineMetrics(s, this.fontRenderContext).getHeight()); + } + + java.awt.font.LineMetrics getLineMetrics(String s) { + Font font = lookupFont(s.toCharArray(), 0, s.length(), 0); + return font.getLineMetrics(s, this.fontRenderContext); + } + + /** + * Given a single OpenType font, perform full text layout and create a new GlyphVector for a string. + * + * @param font the Font used to layout a GlyphVector for the string + * @param text the string to layout + * @param start the offset into text at which to start the layout + * @param limit the (offset + length) at which to stop performing the layout + * @param layoutFlags either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT + * @return the newly created GlyphVector + */ + GlyphVector layoutGlyphVector(Font font, char text[], int start, int limit, int layoutFlags) { + /* Ensure this font is already in fontCache so it can be referenced by cacheGlyphs() later on */ + if (!fontCache.containsKey(font)) { + fontCache.put(font, fontCache.size()); + } + return font.layoutGlyphVector(fontRenderContext, text, start, limit, layoutFlags); + } + + /** + * Find the first font in the system able to render at least one character from a given string. The function always tries searching first + * in the fontCache (based on the request style). Failing that, it searches the usedFonts list followed by the allFonts[] array. + * + * @param text the string to check against the font + * @param start the offset into text at which to start checking characters for being supported by a font + * @param limit the (offset + length) at which to stop checking characters + * @param style combination of the Font.PLAIN, Font.BOLD, and Font.ITALIC to request a particular font style + * @return an OpenType font capable of displaying at least the first character at the start position in text + */ + Font lookupFont(char text[], int start, int limit, int style) { + /* Rasterize at scaled size so atlas texel density matches display resolution */ + int renderSize = fontSize * guiScaleFactor; + + /* Try using an already known base font; the first font in usedFonts list is the one set with setDefaultFont() */ + Iterator iterator = usedFonts.iterator(); + while (iterator.hasNext()) { + /* Only use the font if it can layout at least the first character of the requested string range */ + Font font = iterator.next(); + if (font.canDisplayUpTo(text, start, limit) != start) { + /* Return a font instance of the proper point size and style; usedFonts has only 1pt sized plain style fonts */ + return font.deriveFont(style, renderSize); + } + } + + /* If still not found, try searching through all fonts installed on the system for the first that can layout this string */ + iterator = allFonts.iterator(); + while (iterator.hasNext()) { + /* Only use the font if it can layout at least the first character of the requested string range */ + Font font = iterator.next(); + if (font.canDisplayUpTo(text, start, limit) != start) { + /* If found, add this font to the usedFonts list so it can be looked up faster next time */ + //System.out.println("BetterFonts loading font \"" + font.getFontName() + "\""); + usedFonts.add(font); + + /* Return a font instance of the proper point size and style; allFonts has only 1pt sized plain style fonts */ + return font.deriveFont(style, renderSize); + } + } + + /* If no supported fonts found, use the default one (first in usedFonts) so it can draw its unknown character glyphs */ + Font font = usedFonts.get(0); + + /* Return a font instance of the proper point size and style; usedFonts only 1pt sized plain style fonts */ + return font.deriveFont(style, renderSize); + } + + /** + * Given an OpenType font and a glyph code within that font, locate the glyph's pre-rendered image in the glyph cache and return its + * cache entry,. The entry stores the texture ID with the pre-rendered glyph image, as well as the position and size of that image + * within the texture. This function assumes that any glyph lookup requests passed to it have been already cached by an earlier call + * to cacheGlyphs(). + * + * @param font the font to which this glyphCode belongs and which was used to pre-render the glyph image in cacheGlyphs() + * @param glyphCode the font specific flyph code to lookup in the cache + * @return the cache entry for this font/glyphCode pair + */ + Entry lookupGlyph(Font font, int glyphCode) { + long fontKey = (long) fontCache.get(font) << 32; + return glyphCache.get(fontKey | glyphCode); + } + + /** + * Given an OpenType font and a string, make sure that every glyph used by that string is pre-rendered into an OpenGL texture and cached + * in the glyphCache map for later retrieval by lookupGlyph() + * + * @param font the font used to create a GlyphVector for the string and to actually draw the individual glyphs + * @param text the string from which to cache glyph images + * @param start the offset into text at which to start caching glyphs + * @param limit the (offset + length) at which to stop caching glyphs + * @param layoutFlags either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT; needed for weak bidirectional characters like + * parenthesis which are mapped to two different glyph codes depending on the surrounding text direction + * @todo May need a blank border of pixels around everything for mip-map/tri-linear filtering with Optifine + */ + void cacheGlyphs(Font font, char text[], int start, int limit, int layoutFlags) { + /* Create new GlyphVector so glyphs can be moved around (kerning workaround; see below) without affecting caller */ + GlyphVector vector = layoutGlyphVector(font, text, start, limit, layoutFlags); + + /* Pixel aligned bounding box for the entire vector; only set if the vector has to be drawn to cache a glyph image */ + Rectangle vectorBounds = null; + + /* This forms the upper 32 bits of the fontCache key to make every font/glyph code point unique */ + long fontKey = (long) fontCache.get(font) << 32; + + int numGlyphs = vector.getNumGlyphs(); /* Length of the GlyphVector */ + Rectangle dirty = null; /* Total area within texture that needs to be updated with glTexSubImage2D() */ + boolean vectorRendered = false; /* True if entire GlyphVector was rendered into stringImage */ + int glyphSpacing = computeGlyphSpacing(font); + + for (int index = 0; index < numGlyphs; index++) { + int charIndex = start + vector.getGlyphCharIndex(index); + if (charIndex >= start && charIndex < limit) { + char c = text[charIndex]; + if (c == '\n' || c == '\r') { + continue; + } + } + + /* If this glyph code is already in glyphCache, then there is no reason to pre-render it again */ + int glyphCode = vector.getGlyphCode(index); + if (glyphCache.containsKey(fontKey | glyphCode)) { + continue; + } + + /* + * Skip whitespace glyphs that have no visible ink (spaces, tabs, etc.). These characters + * contribute only advance width, not visible pixels. Without this check, GLYPH_PADDING + * expansion would create a small non-zero rect for the empty glyph, and the blit from + * stringImage could capture stray antialiased fringe pixels from neighboring glyphs, + * causing whitespace to render as visible dots. + */ + { + Rectangle glyphBounds = vector.getGlyphPixelBounds(index, fontRenderContext, 0, 0); + if (glyphBounds.width == 0 || glyphBounds.height == 0) { + continue; + } + } + + /* + * The only way to get glyph shapes with font hinting is to draw the entire glyph vector into a + * temporary BufferedImage, and then bit blit the individual glyphs based on their bounding boxes + * returned by the glyph vector. Although it is possible to call font.createGlyphVector() with an + * array of glyphcodes (and therefore render only a few glyphs at a time), this produces corrupted + * Davengari glyphs under Windows 7. The vectorRendered flag will draw the string at most one time. + */ + if (!vectorRendered) { + vectorRendered = true; + + /* + * Kerning can make it impossible to cleanly separate adjacent glyphs. To work around this, + * each glyph is manually advanced by 2 pixels to the right of its neighbor before rendering + * the entire string. The getGlyphPixelBounds() later on will return the new adjusted bounds + * for the glyph. + */ + for (int i = 0; i < numGlyphs; i++) { + Point2D pos = vector.getGlyphPosition(i); + pos.setLocation(pos.getX() + glyphSpacing * i, pos.getY()); + vector.setGlyphPosition(i, pos); + } + + /* + * Compute the exact area that the rendered string will take up in the image buffer. Note that + * the string will actually be drawn at a positive (x,y) offset from (0,0) to leave enough room + * for the ascent above the baseline and to correct for a few glyphs that appear to have negative + * horizontal bearing (e.g. U+0423 Cyrillic uppercase letter U on Windows 7). + */ + vectorBounds = vector.getPixelBounds(fontRenderContext, 0, 0); + + /* Expand bounds to account for hinting overflow */ + vectorBounds.grow(GLYPH_PADDING, GLYPH_PADDING); + + /* Enlarge the stringImage if it is too small to store the entire rendered string */ + if (stringImage == null || vectorBounds.width > stringImage.getWidth() || vectorBounds.height > stringImage.getHeight()) { + int width = Math.max(vectorBounds.width, stringImage.getWidth()); + int height = Math.max(vectorBounds.height, stringImage.getHeight()); + allocateStringImage(width, height); + } + + /* + * Clear the ENTIRE stringImage, not just the vectorBounds area. The stringImage is reused + * across calls and may be larger than the current vectorBounds. Individual glyph pixel bounds + * (from getGlyphPixelBounds()) can differ slightly from the aggregate vector bounds + * (from getPixelBounds()) due to integer rounding. After GLYPH_PADDING expansion, a glyph's + * padded rect could extend 1-2 pixels beyond the vectorBounds area, capturing stale pixels + * from a previous render. This caused tiny dot artifacts above capital letters whose ink + * extends to the top of the line bounds. Clearing the full image eliminates this class of bugs. + */ + stringGraphics.clearRect(0, 0, stringImage.getWidth(), stringImage.getHeight()); + + /* Draw string with opaque white color and baseline adjustment so the upper-left corner of the image is at (0,0) */ + stringGraphics.drawGlyphVector(vector, -vectorBounds.x, -vectorBounds.y); + } + + /* + * Get the glyph's pixel-aligned bounding box. The JavaDoc claims that the "The outline returned + * by this method is positioned around the origin of each individual glyph." However, the actual + * bounds are all relative to the start of the entire GlyphVector, which is actually more useful + * for extracting the glyph's image from the rendered string. + * + * Passing fontRenderContext ensures bounds match the GASP-aware rendering hints. + */ + Rectangle rect = vector.getGlyphPixelBounds(index, fontRenderContext, -vectorBounds.x, -vectorBounds.y); + rect.grow(GLYPH_PADDING, GLYPH_PADDING); + + /* + * Clamp the padded rect to the stringImage bounds. Due to rounding differences between + * getPixelBounds() and getGlyphPixelBounds(), a padded glyph rect can occasionally extend + * beyond the image edge. Reading pixels outside the image would cause an exception or + * capture garbage data. + */ + if (rect.x < 0) { + rect.width += rect.x; // shrink width by the overshoot + rect.x = 0; + } + if (rect.y < 0) { + rect.height += rect.y; // shrink height by the overshoot + rect.y = 0; + } + if (rect.x + rect.width > stringImage.getWidth()) { + rect.width = stringImage.getWidth() - rect.x; + } + if (rect.y + rect.height > stringImage.getHeight()) { + rect.height = stringImage.getHeight() - rect.y; + } + if (rect.width <= 0 || rect.height <= 0) { + continue; + } + + /* If the current line in cache image is full, then advance to the next line */ + if (cachePosX + rect.width + GLYPH_BORDER > TEXTURE_WIDTH) { + cachePosX = GLYPH_BORDER; + cachePosY += cacheLineHeight + GLYPH_BORDER; + cacheLineHeight = 0; + } + + /* + * If the entire image is full, update the current OpenGL texture with everything changed so far in the image + * (i.e. the dirty rectangle), allocate a new cache texture, and then continue storing glyph images to the + * upper-left corner of the new texture. + */ + if (cachePosY + rect.height + GLYPH_BORDER > TEXTURE_HEIGHT) { + updateTexture(dirty); + dirty = null; + + /* Note that allocateAndSetupTexture() will leave the GL texture already bound */ + allocateGlyphCacheTexture(); + cachePosY = cachePosX = GLYPH_BORDER; + cacheLineHeight = 0; + } + + /* The tallest glyph on this line determines the total vertical advance in the texture */ + if (rect.height > cacheLineHeight) { + cacheLineHeight = rect.height; + } + + /* + * Blit the individual glyph from it's position in the temporary string buffer to its (cachePosX, + * cachePosY) position in the texture. NOTE: We don't have to erase the area in the texture image + * first because the composite method in the Graphics object is always set to AlphaComposite.Src. + */ + glyphCacheGraphics.drawImage(stringImage, + cachePosX, cachePosY, cachePosX + rect.width, cachePosY + rect.height, + rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, null); + + /* + * Store this glyph's position in texture and its origin offset. Note that "rect" will not be modified after + * this point, and getGlyphPixelBounds() always returns a new Rectangle. + */ + rect.setLocation(cachePosX, cachePosY); + + /* + * Create new cache entry to record both the texture used by the glyph and its position within that texture. + * Texture coordinates are normalized to 0.0-1.0 by dividing with TEXTURE_WIDTH and TEXTURE_HEIGHT. + */ + Entry entry = new Entry(); + entry.textureName = textureName; + entry.width = rect.width; + entry.height = rect.height; + entry.u1 = ((float) rect.x) / TEXTURE_WIDTH; + entry.v1 = ((float) rect.y) / TEXTURE_HEIGHT; + entry.u2 = ((float) (rect.x + rect.width)) / TEXTURE_WIDTH; + entry.v2 = ((float) (rect.y + rect.height)) / TEXTURE_HEIGHT; + + /* + * The lower 32 bits of the glyphCache key are the glyph codepoint. The upper 64 bits are the font number + * stored in the fontCache. This creates a unique numerical id for every font/glyph combination. + */ + glyphCache.put(fontKey | glyphCode, entry); + + /* + * Track the overall modified region in the texture by performing a union of this glyph's texture position + * with the update region created so far. Reusing "rect" here makes it easier to extend the dirty rectangle + * region than using the add(x, y) method to extend by a single point. Also note that creating the first + * dirty rectangle here avoids having to deal with the special rules for empty/non-existent rectangles. + */ + if (dirty == null) { + dirty = new Rectangle(cachePosX, cachePosY, rect.width, rect.height); + } else { + dirty.add(rect); + } + + /* Advance cachePosX so the next glyph can be stored immediately to the right of this one */ + cachePosX += rect.width + GLYPH_BORDER; + } + + /* Update OpenGL texture if any part of the glyphCacheImage has changed */ + updateTexture(dirty); + } + + /** + * Update a portion of the current glyph cache texture using the contents of the glyphCacheImage with glTexSubImage2D(). + * + * @param dirty The rectangular region in glyphCacheImage that has changed and needs to be copied into the texture + * @todo Add mip-mapping support here + * @todo Test with bilinear texture interpolation and possibly add a 1 pixel transparent border around each glyph to avoid + * bleedover when interpolation is active or add a small "fudge factor" to the UV coordinates like already n FontRenderer + */ + private void updateTexture(Rectangle dirty) { + /* Only update OpenGL texture if changes were made to the texture */ + if (dirty != null) { + /* Load imageBuffer with pixel data ready for transfer to OpenGL texture */ + updateImageBuffer(dirty.x, dirty.y, dirty.width, dirty.height); + + GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureName); + + GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, dirty.x, dirty.y, dirty.width, dirty.height, + GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, imageBuffer); + } + } + + /** + * Allocte and initialize a new BufferedImage and Graphics2D context for rendering strings into. May need to be called + * at runtime to re-allocate a bigger BufferedImage if cacheGlyphs() is called with a very long string. + */ + private void allocateStringImage(int width, int height) { + stringImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + stringGraphics = stringImage.createGraphics(); + setRenderingHints(); + + /* Set background color for use with clearRect() */ + stringGraphics.setBackground(BACK_COLOR); + + /* + * Full white (1.0, 1.0, 1.0, 1.0) can be modulated by vertex color to produce a full gamut of text colors, although with + * a GL_ALPHA8 texture, only the alpha component of the color will actually get loaded into the texture. + */ + stringGraphics.setPaint(Color.WHITE); + } + + /** + * Set rendering hints on stringGraphics object. Uses current antiAliasEnabled settings and is therefore called both from + * allocateStringImage() when expanding the size of the BufferedImage and from setDefaultFont() when changing current + * IConfiguration. + */ + private void setRenderingHints() { + stringGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + antiAliasEnabled ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); + +// if (antiAliasEnabled) { +// stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_ON); +// stringGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, +// RenderingHints.VALUE_RENDER_QUALITY); +// } else { +// stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); +// } + + stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + stringGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + stringGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF); + + fontRenderContext = stringGraphics.getFontRenderContext(); + } + + /** + * Allocate a new OpenGL texture for caching pre-rendered glyph images. The new texture is initialized to fully transparent + * white so the individual glyphs images within can have a transparent border between them. The new texture remains bound + * after returning from the function. + * + * @todo use GL_ALPHA4 if anti-alias is turned off for even smaller textures + */ + private void allocateGlyphCacheTexture() { + /* Initialize the background to all white but fully transparent. */ + glyphCacheGraphics.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + + /* Allocate new OpenGL texure */ + singleIntBuffer.clear(); + + GL11.glGenTextures(singleIntBuffer); + textureName = singleIntBuffer.get(0); + + /* Load imageBuffer with pixel data ready for transfer to OpenGL texture */ + updateImageBuffer(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + + /* + * Initialize texture with the now cleared BufferedImage. Using a texture with GL_ALPHA8 internal format may result in + * faster rendering since the GPU has to only fetch 1 byte per texel instead of 4 with a regular RGBA texture. + */ + GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureName); + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_ALPHA8, TEXTURE_WIDTH, TEXTURE_HEIGHT, 0, + GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, imageBuffer); + + /* Explicitely disable mipmap support becuase updateTexture() will only update the base level 0 */ + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + } + + /** + * Copy pixel data from a region in glyphCacheImage into imageBuffer and prepare it for use with glText(Sub)Image2D(). This + * function takes care of converting the ARGB format used with BufferedImage into the RGBA format used by OpenGL. + * + * @param x the horizontal coordinate of the region's upper-left corner + * @param y the vertical coordinate of the region's upper-left corner + * @param width the width of the pixel region that will be copied into the buffer + * @param height the height of the pixel region that will be copied into the buffer + */ + private void updateImageBuffer(int x, int y, int width, int height) { + /* Copy raw pixel data from BufferedImage to imageData array with one integer per pixel in 0xAARRGGBB form */ + glyphCacheImage.getRGB(x, y, width, height, imageData, 0, width); + + /* Swizzle each color integer from Java's ARGB format to OpenGL's RGBA */ + for (int i = 0; i < width * height; i++) { + int color = imageData[i]; + imageData[i] = (color << 8) | (color >>> 24); + } + + /* Copy int array to direct buffer; big-endian order ensures a 0xRR, 0xGG, 0xBB, 0xAA byte layout */ + imageBuffer.clear(); + imageBuffer.put(imageData); + imageBuffer.flip(); + } +} diff --git a/core/src/main/java/noppes/npcs/config/LoadConfiguration.java b/core/src/main/java/noppes/npcs/config/LoadConfiguration.java new file mode 100644 index 000000000..0fc46e3e3 --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/LoadConfiguration.java @@ -0,0 +1,34 @@ +package noppes.npcs.config; + +import java.io.File; + +public class LoadConfiguration { + public static File mainConfigFile; + public static File itemConfigFile; + public static File scriptConfigFile; + public static File debugConfigFile; + public static File clientConfigFile; + public static File experimentalConfigFile; + public static File marketConfigFile; + public static File energyConfigFile; + + public static void init(String configpath) { + mainConfigFile = new File(configpath + "main.cfg"); + itemConfigFile = new File(configpath + "item.cfg"); + clientConfigFile = new File(configpath + "client.cfg"); + scriptConfigFile = new File(configpath + "script.cfg"); + debugConfigFile = new File(configpath + "debug.cfg"); + experimentalConfigFile = new File(configpath + "experimental.cfg"); + marketConfigFile = new File(configpath + "market.cfg"); + energyConfigFile = new File(configpath + "energy.cfg"); + + ConfigMain.init(mainConfigFile); + ConfigItem.init(itemConfigFile); + ConfigClient.init(clientConfigFile); + ConfigScript.init(scriptConfigFile); + ConfigDebug.init(debugConfigFile); + ConfigExperimental.init(experimentalConfigFile); + ConfigMarket.init(marketConfigFile); + ConfigEnergy.init(energyConfigFile); + } +} diff --git a/core/src/main/java/noppes/npcs/config/StringCache.java b/core/src/main/java/noppes/npcs/config/StringCache.java new file mode 100644 index 000000000..89add3ad4 --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/StringCache.java @@ -0,0 +1,1366 @@ +package noppes.npcs.config; + +import noppes.npcs.LogWriter; +import org.lwjgl.opengl.GL11; + +import java.awt.Font; +import java.awt.Rectangle; +import java.awt.font.GlyphVector; +import java.awt.geom.Point2D; +import java.lang.ref.WeakReference; +import java.text.Bidi; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.WeakHashMap; + + +/** + * @author https://github.com/secretdataz/BetterFonts + * The StringCache is the public interface for rendering of all Unicode strings using OpenType fonts. It caches the glyph layout + * of individual strings, and it uses a GlyphCache instance to cache the pre-rendered images for individual glyphs. Once a string + * and its glyph images are cached, the critical path in renderString() will draw the glyphs as fast as if using a bitmap font. + * Strings are cached using weak references through a two layer string cache. Strings that are no longer in use by Minecraft will + * be evicted from the cache, while the pre-rendered images of individual glyphs remains cached forever. The following diagram + * illustrates how this works: + * + *

+ * String passed to Key object considers Entry object holds Each Glyph object GlyphCache.Entry stores
+ * renderString(); all ASCII digits equal an array of Glyph belongs to only one the texture ID, image
+ * mapped by weak to zero ('0'); objects which may Entry object; it has width/height and
+ * weakRefCache mapped by weak not directly the glyph x/y pos normalized texture
+ * to Key object stringCahe to Entry correspond to Unicode within the string coordinates.
+ * chars in string
+ * String("Fi1") ------------\ ---> Glyph("F") ----------> GlyphCache.Entry("F")
+ * N:1 \ 1:1 1:N / N:1
+ * String("Fi4") ------------> Key("Fi0") -------------> Entry("Fi0") -----+----> Glyph("i") ----------> GlyphCache.Entry("i")
+ * \ N:1
+ * ---> Glyph("0") -----\
+ * ----> GlyphCache.Entry("0")
+ * ---> Glyph("0") -----/
+ * N:1 1:1 1:N / N:1
+ * String("Be1") ------------> Key("Be0") -------------> Entry("Be0") -----+----> Glyph("e") ----------> GlyphCache.Entry("e")
+ * \ N:1
+ * ---> Glyph("B") ----------> GlyphCache.Entry("B")
+ * 
+ */ +public class StringCache { + /** + * Vertical adjustment (in pixels * 2) to string position because Minecraft uses top of string instead of baseline + */ + private static final int BASELINE_OFFSET = 7; + + /** + * Offset from the string's baseline as which to draw the underline (in pixels) + */ + private static final int UNDERLINE_OFFSET = 1; + + /** + * Thickness of the underline (in pixels) + */ + private static final int UNDERLINE_THICKNESS = 2; + + /** + * Offset from the string's baseline as which to draw the strikethrough line (in pixels) + */ + private static final int STRIKETHROUGH_OFFSET = -6; + + /** + * Thickness of the strikethrough line (in pixels) + */ + private static final int STRIKETHROUGH_THICKNESS = 2; + + // BetterFonts base scale: maps internal 2x glyph coords to GUI coords. + // With scale-aware rasterization, the effective scale becomes BASE_RENDER_SCALE / guiScaleFactor. + private static final float BASE_RENDER_SCALE = 0.5F; + private static final boolean FONT_DEBUG_LOG = Boolean.getBoolean("cnpc.font.debug"); + private static final boolean FONT_DEBUG_DISABLE_INK_OFFSET = Boolean.getBoolean("cnpc.font.debug.disableInkOffset"); + + private static boolean isLineBreak(char c) { + return c == '\n' || c == '\r'; + } + + private static boolean isSpacingDebugGlyph(char c) { + return c == '(' || c == '"' || c == 't' || c == '.'; + } + + private static String debugGlyphLabel(char c) { + return c == '"' ? "\\\"" : String.valueOf(c); + } + + private float renderScale() { + return BASE_RENDER_SCALE / glyphCache.getGuiScaleFactor(); + } + + private float pixelSnap(float coord) { + float scale = renderScale(); + float invScale = 1.0F / scale; + return (float) Math.floor(coord * invScale + 0.5F) * scale; + } + + private int baselineOffset = BASELINE_OFFSET; + private int underlineOffset = UNDERLINE_OFFSET; + private int strikethroughOffset = STRIKETHROUGH_OFFSET; + + /** + * Reference to the unicode.FontRenderer class. Needed for creating GlyphVectors and retrieving glyph texture coordinates. + */ + private GlyphCache glyphCache; + + /** + * Color codes from original FontRender class. First 16 entries are the primary chat colors; second 16 are darker versions + * used for drop shadows. + */ + private int colorTable[]; + + /** + * A cache of recently seen strings to their fully layed-out state, complete with color changes and texture coordinates of + * all pre-rendered glyph images needed to display this string. The weakRefCache holds strong references to the Key + * objects used in this map. + */ + private WeakHashMap stringCache = new WeakHashMap(); + + /** + * Every String passed to the public renderString() function is added to this WeakHashMap. As long as As long as Minecraft + * continues to hold a strong reference to the String object (i.e. from TileEntitySign and ChatLine) passed here, the + * weakRefCache map will continue to hold a strong reference to the Key object that said strings all map to (multiple strings + * in weakRefCache can map to a single Key if those strings only differ by their ASCII digits). + */ + private WeakHashMap weakRefCache = new WeakHashMap(); + + /** + * Temporary Key object re-used for lookups with stringCache.get(). Using a temporary object like this avoids the overhead + * of allocating new objects in the critical rendering path. Of course, new Key objects are always created when adding + * a mapping to stringCache. + */ + private Key lookupKey = new Key(); + + /** + * Pre-cached glyphs for the ASCII digits 0-9 (in that order). Used by renderString() to substiture digit glyphs on the fly + * as a performance boost. The speed up is most noticable on the F3 screen which rapidly displays lots of changing numbers. + * The 4 element array is index by the font style (combination of Font.PLAIN, Font.BOLD, and Font.ITALIC), and each of the + * nested elements is index by the digit value 0-9. + */ + private Glyph[][] digitGlyphs = new Glyph[4][]; + + /** + * True if digitGlyphs[] has been assigned and cacheString() can begin replacing all digits with '0' in the string. + */ + private boolean digitGlyphsReady = false; + + /** + * If true, then enble GL_BLEND in renderString() so anti-aliasing font glyphs show up properly. + */ + private boolean antiAliasEnabled = false; + + /** + * Reference to the main Minecraft thread that created this GlyphCache object. Starting with Minecraft 1.3.1, it is possible + * for GlyphCache.cacheGlyphs() to be invoked from the TcpReaderThread while processing a chat packet and computing the width + * of the incoming chat text. Unfortunately, if cacheGlyphs() makes any OpenGL calls from any thread except the main one, + * it will crash LWJGL with a NullPointerException. By remembering the initial thread and comparing it later against + * Thread.currentThread(), the StringCache code can avoid calling cacheGlyphs() when it's not safe to do so. + */ + private Thread mainThread; + + public int fontHeight; + + /** + * Wraps a String and acts as the key into stringCache. The hashCode() and equals() methods consider all ASCII digits + * to be equal when hashing and comparing Key objects together. Therefore, Strings which only differ in their digits will + * be all hashed together into the same entry. The renderString() method will then substitute the correct digit glyph on + * the fly. This special digit handling gives a significant speedup on the F3 debug screen. + */ + static private class Key { + /** + * A copy of the String which this Key is indexing. A copy is used to avoid creating a strong reference to the original + * passed into renderString(). When the original String is no longer needed by Minecraft, it will be garbage collected + * and the WeakHashMaps in StringCache will allow this Key object and its associated Entry object to be garbage + * collected as well. + */ + public String str; + + /** + * Computes a hash code on str in the same manner as the String class, except all ASCII digits hash as '0' + * + * @return the augmented hash code on str + */ + @Override + public int hashCode() { + int code = 0, length = str.length(); + + /* + * True if a section mark character was last seen. In this case, if the next character is a digit, it must + * not be considered equal to any other digit. This forces any string that differs in color codes only to + * have a separate entry in the StringCache. + */ + boolean colorCode = false; + + for (int index = 0; index < length; index++) { + char c = str.charAt(index); + if (c >= '0' && c <= '9' && !colorCode) { + c = '0'; + } + code = (code * 31) + c; + colorCode = (c == '\u00A7'); + } + + return code; + } + + /** + * Compare str against another object (specifically, the object's string representation as returned by toString). + * All ASCII digits are considered equal by this method, as long as they are at the same index within the string. + * + * @return true if the strings are the identical, or only differ in their ASCII digits + */ + @Override + public boolean equals(Object o) { + /* + * There seems to be a timing window inside WeakHashMap itself where a null object can be passed to this + * equals() method. Presumably it happens between computing a hash code for the weakly referenced Key object + * while it still exists and calling its equals() method after it was garbage collected. + */ + if (o == null) { + return false; + } + + /* Calling toString on a String object simply returns itself so no new object allocation is performed */ + String other = o.toString(); + int length = str.length(); + + if (length != other.length()) { + return false; + } + + /* + * True if a section mark character was last seen. In this case, if the next character is a digit, it must + * not be considered equal to any other digit. This forces any string that differs in color codes only to + * have a separate entry in the StringCache. + */ + boolean colorCode = false; + + for (int index = 0; index < length; index++) { + char c1 = str.charAt(index); + char c2 = other.charAt(index); + + if (c1 != c2 && (c1 < '0' || c1 > '9' || c2 < '0' || c2 > '9' || colorCode)) { + return false; + } + colorCode = (c1 == '\u00A7'); + } + + return true; + } + + /** + * Returns the contained String object within this Key. + * + * @return the str object + */ + @Override + public String toString() { + return str; + } + } + + /** + * This entry holds the layed out glyph positions for the cached string along with some relevant metadata. + */ + static private class Entry { + /** + * A weak reference back to the Key object in stringCache that maps to this Entry. + */ + public WeakReference keyRef; + + /** + * The total horizontal advance (i.e. width) for this string in pixels. + */ + public int advance; + + /** + * Array of fully layed out glyphs for the string. Sorted by logical order of characters (i.e. glyph.stringIndex) + */ + public Glyph glyphs[]; + + /** + * Array of color code locations from the original string + */ + public ColorCode colors[]; + + /** + * True if the string uses strikethrough or underlines anywhere and needs an extra pass in renderString() + */ + public boolean specialRender; + } + + /** + * Identifies the location and value of a single color code in the original string + */ + static private class ColorCode implements Comparable { + /** + * Bit flag used with renderStyle to request the underline style + */ + public static final byte UNDERLINE = 1; + + /** + * Bit flag used with renderStyle to request the strikethrough style + */ + public static final byte STRIKETHROUGH = 2; + + /** + * The index into the original string (i.e. with color codes) for the location of this color code. + */ + public int stringIndex; + + /** + * The index into the stripped string (i.e. with no color codes) of where this color code would have appeared + */ + public int stripIndex; + + /** + * The numeric color code (i.e. index into the colorCode[] array); -1 to reset default color + */ + public byte colorCode; + + /** + * Combination of Font.PLAIN, Font.BOLD, and Font.ITALIC specifying font specific syles + */ + public byte fontStyle; + + /** + * Combination of UNDERLINE and STRIKETHROUGH flags specifying effects performed by renderString() + */ + public byte renderStyle; + + /** + * Performs numeric comparison on stripIndex. Allows binary search on ColorCode arrays in layoutStyle. + * + * @param i the Integer object being compared + * @return either -1, 0, or 1 if this < other, this == other, or this > other + */ + @Override + public int compareTo(Integer i) { + return (stringIndex == i.intValue()) ? 0 : (stringIndex < i.intValue()) ? -1 : 1; + } + } + + /** + * Identifies a single glyph in the layed-out string. Includes a reference to a GlyphCache.Entry with the OpenGL texture ID + * and position of the pre-rendered glyph image, and includes the x/y pixel coordinates of where this glyph occurs within + * the string to which this Glyph object belongs. + */ + static private class Glyph implements Comparable { + public int stringIndex; + public GlyphCache.Entry texture; + + /** Logical horizontal position from getGlyphPosition() — used for spacing/advance */ + public int x; + + /** Ink vertical position from getGlyphPixelBounds() — used for texture quad placement */ + public int y; + + /** Horizontal advance (logical) used for strikethrough and underline effects */ + public int advance; + + /** Ink X offset from logical position (inkBoundsX - logicalX) for texture quad placement */ + public int inkOffsetX; + + /** + * Allows arrays of Glyph objects to be sorted. Performs numeric comparison on stringIndex. + * + * @param o the other Glyph object being compared with this one + * @return either -1, 0, or 1 if this < other, this == other, or this > other + */ + @Override + public int compareTo(Glyph o) { + return (stringIndex == o.stringIndex) ? 0 : (stringIndex < o.stringIndex) ? -1 : 1; + } + } + + /** + * A single StringCache object is allocated by Minecraft's FontRenderer which forwards all string drawing and requests for + * string width to this class. + * + * @param colors 32 element array of RGBA colors corresponding to the 16 text color codes followed by 16 darker version of the + * color codes for use as drop shadows + */ + public StringCache() { + /* StringCache is created by the main game thread; remember it for later thread safety checks */ + mainThread = Thread.currentThread(); + + glyphCache = new GlyphCache(); + colorTable = new int[32]; + + for (int i = 0; i < 32; ++i) { + int j = (i >> 3 & 1) * 85; + int k = (i >> 2 & 1) * 170 + j; + int l = (i >> 1 & 1) * 170 + j; + int i1 = (i >> 0 & 1) * 170 + j; + + if (i == 6) { + k += 85; + } + + if (Minecraft.getMinecraft().gameSettings.anaglyph) { + int j1 = (k * 30 + l * 59 + i1 * 11) / 100; + int k1 = (k * 30 + l * 70) / 100; + int l1 = (k * 30 + i1 * 70) / 100; + k = j1; + l = k1; + i1 = l1; + } + + if (i >= 16) { + k /= 4; + l /= 4; + i1 /= 4; + } + + this.colorTable[i] = (k & 255) << 16 | (l & 255) << 8 | i1 & 255; + } + + /* Pre-cache the ASCII digits to allow for fast glyph substitution */ + cacheDightGlyphs(); + } + + /** + * Change the default font used to pre-render glyph images. If this method is called at runtime, the string cache is flushed so that + * all visible strings will be immediately re-layed out using the new font selection. + * + * @param fontName the new font name + * @param fontSize the new point size + * @param antiAlias turn on anti aliasing + */ + public void setDefaultFont(String fontName, int fontSize, boolean antiAlias) { + /* Change the font in the glyph cache and clear the string cache so all strings have to be re-layed out and re-rendered */ + glyphCache.setDefaultFont(fontName, fontSize, antiAlias); + antiAliasEnabled = antiAlias; + weakRefCache.clear(); + stringCache.clear(); + + /* Pre-cache the ASCII digits to allow for fast glyph substitution */ + cacheDightGlyphs(); + updateHeight(); + } + + public Font usedFont() { + return glyphCache.usedFonts.get(0); + } + + public void setCustomFont(IResourceLocation resource, int fontSize, boolean antiAlias) throws Exception { + /* Change the font in the glyph cache and clear the string cache so all strings have to be re-layed out and re-rendered */ + glyphCache.setCustomFont(resource, fontSize, antiAlias); + antiAliasEnabled = antiAlias; + weakRefCache.clear(); + stringCache.clear(); + + /* Pre-cache the ASCII digits to allow for fast glyph substitution */ + cacheDightGlyphs(); + updateHeight(); + } + + public void updateHeight() { + int height = 0; + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + + for (Glyph g : cacheString("AaBbCcDdEeHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz").glyphs) { + if (g.texture.height > height) + height = g.texture.height; + if (g.y < minY) + minY = g.y; + if (g.y > maxY) + maxY = g.y; + } + int scaleFactor = glyphCache.getGuiScaleFactor(); + fontHeight = (int) (((maxY + height) - minY) / 2.0F / scaleFactor); + updateFontMetrics(); + } + + private void updateFontMetrics() { + java.awt.font.LineMetrics lm = glyphCache.getLineMetrics("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + float ascent = lm.getAscent(); + float descent = lm.getDescent(); + + /* + * baselineOffset is added directly to startY (GUI pixels) at render time, so it must be a + * scale-invariant GUI-pixel value. The constant BASELINE_OFFSET (7) is correct for the default + * 18pt font and remains stable across all GUI scale factors because it operates in GUI coordinate + * space — independent of the glyph rasterization scale. + * + * A dynamic formula like (ascent - padding) * renderScale is fragile: ascent comes from the + * scaled font (fontSize * scaleFactor) and rounding/font-metric variations across scales can + * produce values 1-2 pixels too large, visibly shifting text down within its highlight box. + */ + baselineOffset = BASELINE_OFFSET; + + /* + * underlineOffset and strikethroughOffset are multiplied by renderScale() at draw time + * (same as glyph.x/glyph.y), so they must be in glyph-space pixels. These scale correctly + * because ascent/descent are proportional to the rasterization size. + */ + underlineOffset = (int) (descent * 0.25f + 0.5f); + strikethroughOffset = -(int) (ascent * 0.35f + 0.5f); + } + + private void checkForScaleChange() { + if (glyphCache.checkAndUpdateScaleFactor()) { + weakRefCache.clear(); + stringCache.clear(); + cacheDightGlyphs(); + updateHeight(); + } + } + + /** + * Pre-cache the ASCII digits to allow for fast glyph substitution. Called once from the constructor and called any time the font selection + * changes at runtime via setDefaultFont(). + */ + private void cacheDightGlyphs() { + /* Need to cache each font style combination; the digitGlyphsReady = false disabled the normal glyph substitution mechanism */ + digitGlyphsReady = false; + digitGlyphs[Font.PLAIN] = cacheString("0123456789").glyphs; + digitGlyphs[Font.BOLD] = cacheString("\u00A7l0123456789").glyphs; + digitGlyphs[Font.ITALIC] = cacheString("\u00A7o0123456789").glyphs; + digitGlyphs[Font.BOLD | Font.ITALIC] = cacheString("\u00A7l\u00A7o0123456789").glyphs; + digitGlyphsReady = true; + } + + /** + * Render a single-line string to the screen using the current OpenGL color. The (x,y) coordinates are of the uppet-left + * corner of the string's bounding box, rather than the baseline position as is typical with fonts. This function will also + * add the string to the cache so the next renderString() call with the same string is faster. + * + * @param str the string being rendered; it can contain color codes + * @param startX the x coordinate to draw at + * @param startY the y coordinate to draw at + * @param initialColor the initial RGBA color to use when drawing the string; embedded color codes can override the RGB component + * @param shadowFlag if true, color codes are replaces by a darker version used for drop shadows + * @return the total advance (horizontal distance) of this string + * @todo Add optional NumericShaper to replace ASCII digits with locale specific ones + * @todo Add support for the "k" code which randomly replaces letters on each render (used only by splash screen) + * @todo Pre-sort by texture to minimize binds; can store colors per glyph in string cache + * @todo Optimize the underline/strikethrough drawing to draw a single line for each run + */ + public int renderString(String str, int startX, int startY, int initialColor, boolean shadowFlag) { + /* Check for invalid arguments */ + if (str == null || str.isEmpty()) { + return 0; + } + + /* Detect window resize: if GUI scale factor changed, invalidate all caches and re-rasterize */ + checkForScaleChange(); + + /* Make sure the entire string is cached before rendering and return its glyph representation */ + Entry entry = cacheString(str); + + /* Adjust the baseline of the string because the startY coordinate in Minecraft is for the top of the string */ + startY += baselineOffset; + + /* Color currently selected by color code; reapplied to Tessellator instance after glBindTexture() */ + + if ((initialColor >> 24 & 0xff) == 0) + initialColor += 0xFF000000; + int color = initialColor; + /* Track which texture is currently bound to minimize the number of glBindTexture() and Tessellator.draw() calls needed */ + int boundTextureName = 0; + + /* + * This color change will have no effect on the actual text (since colors are included in the Tessellator vertex + * array), however GuiEditSign of all things depends on having the current color set to white when it renders its + * "Edit sign message:" text. Otherwise, the sign which is rendered underneath would look too dark. + */ + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE); //fixes black text + GL11.glColor4f((color >> 16 & 0xff) / 255f, (color >> 8 & 0xff) / 255f, (color & 0xff) / 255f, 1); + /* + * Enable GL_BLEND in case the font is drawn anti-aliased because Minecraft itself only enables blending for chat text + * (so it can fade out), but not GUI text or signs. Minecraft uses multiple blend functions so it has to be specified here + * as well for consistent blending. To reduce the overhead of OpenGL state changes and making native LWJGL calls, this + * function doesn't try to save/restore the blending state. Hopefully everything else that depends on blending in Minecraft + * will set its own state as needed. + */ + if (antiAliasEnabled) { + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + } + + /* Using the Tessellator to queue up data in a vertex array and then draw all at once should be faster than immediate mode */ + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff); + + /* The currently active font syle is needed to select the proper ASCII digit style for fast replacement */ + int fontStyle = Font.PLAIN; + + for (int glyphIndex = 0, colorIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) { + /* + * If the original string had a color code at this glyph's position, then change the current GL color that gets added + * to the vertex array. Note that only the RGB component of the color is replaced by a color code; the alpha component + * of the original color passed into this function will remain. The while loop handles multiple consecutive color codes, + * in which case only the last such color code takes effect. + */ + while (colorIndex < entry.colors.length && entry.glyphs[glyphIndex].stringIndex >= entry.colors[colorIndex].stringIndex) { + color = applyColorCode(entry.colors[colorIndex].colorCode, initialColor, shadowFlag); + fontStyle = entry.colors[colorIndex].fontStyle; + colorIndex++; + } + + /* Select the current glyph's texture information and horizontal layout position within this string */ + Glyph glyph = entry.glyphs[glyphIndex]; + GlyphCache.Entry texture = glyph.texture; + + /* Skip glyphs with no cached texture (whitespace characters like spaces and tabs) */ + if (texture == null) { + continue; + } + + int glyphX = glyph.x + glyph.inkOffsetX; + + /* + * Replace ASCII digits in the string with their respective glyphs; strings differing by digits are only cached once. + * If the new replacement glyph has a different width than the original placeholder glyph (e.g. the '1' glyph is often + * narrower than other digits), re-center the new glyph over the placeholder's position to minimize the visual impact + * of the width mismatch. + */ + char c = str.charAt(glyph.stringIndex); + if (isLineBreak(c)) { + continue; + } + + if (c >= '0' && c <= '9') { + int oldWidth = texture.width; + texture = digitGlyphs[fontStyle][c - '0'].texture; + int newWidth = texture.width; + glyphX += (oldWidth - newWidth) >> 1; + } + + /* + * Make sure the OpenGL texture storing this glyph's image is bound (if not already bound). All pending glyphs in the + * Tessellator's vertex array must be drawn before switching textures, otherwise they would erroneously use the new + * texture as well. + */ + if (boundTextureName != texture.textureName) + //if(true) + { + tessellator.draw(); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture.textureName); + boundTextureName = texture.textureName; + } + + /* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */ + if (FONT_DEBUG_DISABLE_INK_OFFSET) { + glyphX = glyph.x; + } + float rs = renderScale(); + float x1 = pixelSnap(startX + (glyphX) * rs); + float x2 = pixelSnap(startX + (glyphX + texture.width) * rs); + float y1 = pixelSnap(startY + (glyph.y) * rs); + float y2 = pixelSnap(startY + (glyph.y + texture.height) * rs); + + if (FONT_DEBUG_LOG && isSpacingDebugGlyph(c)) { + LogWriter.info("[StringCache glyph-debug] char='" + debugGlyphLabel(c) + "' idx=" + glyph.stringIndex + + " x=" + glyph.x + " advance=" + glyph.advance + " texW=" + texture.width + + " inkOffsetX=" + glyph.inkOffsetX + " quadX1=" + x1 + " quadX2=" + x2 + + " y=" + glyph.y + " tex=" + texture.textureName); + } + + tessellator.addVertexWithUV(x1, y1, 0, texture.u1, texture.v1); + tessellator.addVertexWithUV(x1, y2, 0, texture.u1, texture.v2); + tessellator.addVertexWithUV(x2, y2, 0, texture.u2, texture.v2); + tessellator.addVertexWithUV(x2, y1, 0, texture.u2, texture.v1); + } + + /* Draw any remaining glyphs in the Tessellator's vertex array (there should be at least one glyph pending) */ + tessellator.draw(); + + /* Draw strikethrough and underlines if the string uses them anywhere */ + if (entry.specialRender) { + int renderStyle = 0; + + /* Use initial color passed to renderString(); disable texturing to draw solid color lines */ + color = initialColor; + GL11.glDisable(GL11.GL_TEXTURE_2D); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff); + + for (int glyphIndex = 0, colorIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) { + /* + * If the original string had a color code at this glyph's position, then change the current GL color that gets added + * to the vertex array. The while loop handles multiple consecutive color codes, in which case only the last such + * color code takes effect. + */ + while (colorIndex < entry.colors.length && entry.glyphs[glyphIndex].stringIndex >= entry.colors[colorIndex].stringIndex) { + color = applyColorCode(entry.colors[colorIndex].colorCode, initialColor, shadowFlag); + renderStyle = entry.colors[colorIndex].renderStyle; + colorIndex++; + } + + /* Select the current glyph within this string for its layout position */ + Glyph glyph = entry.glyphs[glyphIndex]; + + /* The strike/underlines are drawn beyond the glyph's width to include the extra space between glyphs */ + int glyphSpace = glyph.texture != null ? glyph.advance - glyph.texture.width : 0; + + /* Draw underline under glyph if the style is enabled */ + if ((renderStyle & ColorCode.UNDERLINE) != 0) { + /* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */ + float urs = renderScale(); + float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * urs); + float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * urs); + float y1 = pixelSnap(startY + (underlineOffset) * urs); + float y2 = pixelSnap(startY + (underlineOffset + UNDERLINE_THICKNESS) * urs); + + tessellator.addVertex(x1, y1, 0); + tessellator.addVertex(x1, y2, 0); + tessellator.addVertex(x2, y2, 0); + tessellator.addVertex(x2, y1, 0); + } + + /* Draw strikethrough in the middle of glyph if the style is enabled */ + if ((renderStyle & ColorCode.STRIKETHROUGH) != 0) { + /* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */ + float srs = renderScale(); + float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * srs); + float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * srs); + float y1 = pixelSnap(startY + (strikethroughOffset) * srs); + float y2 = pixelSnap(startY + (strikethroughOffset + STRIKETHROUGH_THICKNESS) * srs); + + tessellator.addVertex(x1, y1, 0); + tessellator.addVertex(x1, y2, 0); + tessellator.addVertex(x2, y2, 0); + tessellator.addVertex(x2, y1, 0); + } + } + + /* Finish drawing the last strikethrough/underline segments */ + tessellator.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + + /* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */ + return (int) (entry.advance * renderScale()); + } + + /** + * Return the width of a string in pixels. Used for centering strings inside GUI buttons. + * + * @param str compute the width of this string + * @return the width in pixels (divided by 2; this matches the scaled coordinate system used by GUIs in Minecraft) + */ + public int getStringWidth(String str) { + /* Check for invalid arguments */ + if (str == null || str.isEmpty()) { + return 0; + } + + checkForScaleChange(); + + /* Make sure the entire string is cached and rendered since it will probably be used again in a renderString() call */ + Entry entry = cacheString(str); + + /* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */ + return (int) (entry.advance * renderScale()); + } + + /** + * Return the width of a string in pixels when rendered with the specified font style. + * This is needed when measuring text that will be rendered with bold/italic formatting, + * since bold glyphs are wider than plain glyphs. + * + *

The font style is applied by prepending the appropriate Minecraft formatting codes + * (§l for bold, §o for italic) to the string before measuring. This causes cacheString() + * to lay out the entire string using the specified font style. + * + * @param str compute the width of this string + * @param fontStyle combination of {@link java.awt.Font#BOLD} and {@link java.awt.Font#ITALIC} + * @return the width in pixels (in the scaled GUI coordinate system) + */ + public int getStringWidth(String str, int fontStyle) { + /* Check for invalid arguments */ + if (str == null || str.isEmpty()) { + return 0; + } + + /* If plain style, delegate to the normal method (no prefix needed) */ + if (fontStyle == Font.PLAIN) { + return getStringWidth(str); + } + + /* Build the style prefix: §l for bold, §o for italic */ + StringBuilder sb = new StringBuilder(); + if ((fontStyle & Font.BOLD) != 0) { + sb.append('\u00A7').append('l'); + } + if ((fontStyle & Font.ITALIC) != 0) { + sb.append('\u00A7').append('o'); + } + sb.append(str); + + return getStringWidth(sb.toString()); + } + + /** + * Return the number of characters in a string that will completly fit inside the specified width when rendered, with + * or without prefering to break the line at whitespace instead of breaking in the middle of a word. This private provides + * the real implementation of both sizeStringToWidth() and trimStringToWidth(). + * + * @param str the String to analyze + * @param width the desired string width (in GUI coordinate system) + * @param breakAtSpaces set to prefer breaking line at spaces than in the middle of a word + * @return the number of characters from str that will fit inside width + */ + private int sizeString(String str, int width, boolean breakAtSpaces) { + /* Check for invalid arguments */ + if (str == null || str.isEmpty()) { + return 0; + } + + /* Convert the width from GUI coordinate system to internal glyph pixels */ + width = (int) (width / renderScale()); + + /* The glyph array for a string is sorted by the string's logical character position */ + Glyph glyphs[] = cacheString(str).glyphs; + + /* Index of the last whitespace found in the string; used if breakAtSpaces is true */ + int wsIndex = -1; + + /* Add up the individual advance of each glyph until it exceeds the specified width */ + int advance = 0, index = 0; + while (index < glyphs.length && advance <= width) { + /* Keep track of spaces if breakAtSpaces it set */ + if (breakAtSpaces) { + char c = str.charAt(glyphs[index].stringIndex); + if (c == ' ') { + wsIndex = index; + } else if (c == '\n') { + wsIndex = index; + break; + } + } + + advance += glyphs[index].advance; + index++; + } + + /* Avoid splitting individual words if breakAtSpaces set; same test condition as in Minecraft's FontRenderer */ + if (index < glyphs.length && wsIndex != -1 && wsIndex < index) { + index = wsIndex; + } + + /* The string index of the last glyph that wouldn't fit gives the total desired length of the string in characters */ + return index < glyphs.length ? glyphs[index].stringIndex : str.length(); + } + + /** + * Return the number of characters in a string that will completly fit inside the specified width when rendered. + * + * @param str the String to analyze + * @param width the desired string width (in GUI coordinate system) + * @return the number of characters from str that will fit inside width + */ + public int sizeStringToWidth(String str, int width) { + return sizeString(str, width, true); + } + + /** + * Trim a string so that it fits in the specified width when rendered, optionally reversing the string + * + * @param str the String to trim + * @param width the desired string width (in GUI coordinate system) + * @param reverse if true, the returned string will also be reversed + * @return the trimmed and optionally reversed string + */ + public String trimStringToWidth(String str, int width, boolean reverse) { + int length = sizeString(str, width, false); + str = str.substring(0, length); + + if (reverse) { + str = (new StringBuilder(str)).reverse().toString(); + } + + return str; + } + + /** + * Apply a new vertex color to the Tessellator instance based on the numeric chat color code. Only the RGB component of the + * color is replaced by a color code; the alpha component of the original default color will remain. + * + * @param colorCode the chat color code as a number 0-15 or -1 to reset the default color + * @param color the default color used when the colorCode is -1 + * @param shadowFlag ir true, the color code will select a darker version of the color suitable for drop shadows + * @return the new RGBA color set by this function + */ + private int applyColorCode(int colorCode, int color, boolean shadowFlag) { + /* A -1 color code indicates a reset to the initial color passed into renderString() */ + if (colorCode != -1) { + colorCode = shadowFlag ? colorCode + 16 : colorCode; + color = colorTable[colorCode] & 0xffffff | color & 0xff000000; + } + + Tessellator.instance.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff); + return color; + } + + /** + * Add a string to the string cache by perform full layout on it, remembering its glyph positions, and making sure that + * every font glyph used by the string is pre-rendering. If this string has already been cached, then simply return its + * existing Entry from the cahe. Note that for caching purposes, this method considers two strings to be identical if they + * only differ in their ASCII digits; the renderString() method performs fast glyph substitution based on the actual digits + * in the string at the time. + * + * @param str this String will be layed out and added to the cache (or looked up, if alraedy cached) + * @return the string's cache entry containing all the glyph positions + */ + private Entry cacheString(String str) { + /* + * New Key object allocated only if the string was not found in the StringCache using lookupKey. This variable must + * be outside the (entry == null) code block to have a temporary strong reference between the time when the Key is + * added to stringCache and added to weakRefCache. + */ + Key key; + + /* Either a newly created Entry object for the string, or the cached Entry if the string is already in the cache */ + Entry entry = null; + + /* Don't perform a cache lookup from other threads because the stringCache is not synchronized */ + if (mainThread == Thread.currentThread()) { + /* Re-use existing lookupKey to avoid allocation overhead on the critical rendering path */ + lookupKey.str = str; + + /* If this string is already in the cache, simply return the cached Entry object */ + entry = stringCache.get(lookupKey); + } + + /* If string is not cached (or not on main thread) then layout the string */ + if (entry == null) { + /* layoutGlyphVector() requires a char[] so create it here and pass it around to avoid duplication later on */ + char text[] = str.toCharArray(); + + /* Strip all color codes from the string */ + entry = new Entry(); + int length = stripColorCodes(entry, str, text); + + /* Layout the entire string, splitting it up by color codes and the Unicode bidirectional algorithm */ + List glyphList = new ArrayList(); + entry.advance = (int) layoutBidiString(glyphList, text, 0, length, entry.colors); + + /* Convert the accumulated Glyph list to an array for efficient storage */ + entry.glyphs = new Glyph[glyphList.size()]; + entry.glyphs = glyphList.toArray(entry.glyphs); + + /* + * Sort Glyph array by stringIndex so it can be compared during rendering to the already sorted ColorCode array. + * This will apply color codes in the string's logical character order and not the visual order on screen. + */ + Arrays.sort(entry.glyphs); + + /* Do some post-processing on each Glyph object */ + int colorIndex = 0, shift = 0; + for (int glyphIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) { + Glyph glyph = entry.glyphs[glyphIndex]; + + /* + * Adjust the string index for each glyph to point into the original string with unstripped color codes. The while + * loop is necessary to handle multiple consecutive color codes with no visible glyphs between them. These new adjusted + * stringIndex can now be compared against the color stringIndex during rendering. It also allows lookups of ASCII + * digits in the original string for fast glyph replacement during rendering. + */ + while (colorIndex < entry.colors.length && glyph.stringIndex + shift >= entry.colors[colorIndex].stringIndex) { + shift += 2; + colorIndex++; + } + glyph.stringIndex += shift; + } + + /* + * Do not actually cache the string when called from other threads because GlyphCache.cacheGlyphs() will not have been called + * and the cache entry does not contain any texture data needed for rendering. + */ + if (mainThread == Thread.currentThread()) { + /* Wrap the string in a Key object (to change how ASCII digits are compared) and cache it along with the newly generated Entry */ + key = new Key(); + + /* Make a copy of the original String to avoid creating a strong reference to it */ + key.str = new String(str); + entry.keyRef = new WeakReference(key); + stringCache.put(key, entry); + } + } + + /* Do not access weakRefCache from other threads since it is unsynchronized, and for a newly created entry, the keyRef is null */ + if (mainThread == Thread.currentThread()) { + /* + * Add the String passed into this method to the stringWeakMap so it keeps the Key reference live as long as the String is in use. + * If an existing Entry was already found in the stringCache, it's possible that its Key has already been garbage collected. The + * code below checks for this to avoid adding (str, null) entries into weakRefCache. Note that if a new Key object was created, it + * will still be live because of the strong reference created by the "key" variable. + */ + Key oldKey = entry.keyRef.get(); + if (oldKey != null) { + weakRefCache.put(str, oldKey); + } + lookupKey.str = null; + } + + /* Return either the existing or the newly created entry so it can be accessed immediately */ + return entry; + } + + /** + * Remove all color codes from the string by shifting data in the text[] array over so it overwrites them. The value of each + * color code and its position (relative to the new stripped text[]) is also recorded in a separate array. The color codes must + * be removed for a font's context sensitive glyph substitution to work (like Arabic letter middle form). + * + * @param cacheEntry each color change in the string will add a new ColorCode object to this list + * @param str the string from which color codes will be stripped + * @param text on input it should be an identical copy of str; on output it will be string with all color codes removed + * @return the length of the new stripped string in text[]; actual text.length will not change because the array is not reallocated + */ + private int stripColorCodes(Entry cacheEntry, String str, char text[]) { + List colorList = new ArrayList(); + int start = 0, shift = 0, next; + + byte fontStyle = Font.PLAIN; + byte renderStyle = 0; + byte colorCode = -1; + + /* Search for section mark characters indicating the start of a color code (but only if followed by at least one character) */ + while ((next = str.indexOf('\u00A7', start)) != -1 && next + 1 < str.length()) { + /* + * Remove the two char color code from text[] by shifting the remaining data in the array over on top of it. + * The "start" and "next" variables all contain offsets into the original unmodified "str" string. The "shift" + * variable keeps track of how many characters have been sripped so far, and it's used to compute offsets into + * the text[] array based on the start/next offsets in the original string. + */ + System.arraycopy(text, next - shift + 2, text, next - shift, text.length - next - 2); + + /* Decode escape code used in the string and change current font style / color based on it */ + int code = "0123456789abcdefklmnor".indexOf(Character.toLowerCase(str.charAt(next + 1))); + switch (code) { + /* Random style; TODO: NOT IMPLEMENTED YET */ + case 16: + break; + + /* Bold style */ + case 17: + fontStyle |= Font.BOLD; + break; + + /* Strikethrough style */ + case 18: + renderStyle |= ColorCode.STRIKETHROUGH; + cacheEntry.specialRender = true; + break; + + /* Underline style */ + case 19: + renderStyle |= ColorCode.UNDERLINE; + cacheEntry.specialRender = true; + break; + + /* Italic style */ + case 20: + fontStyle |= Font.ITALIC; + break; + + /* Plain style */ + case 21: + fontStyle = Font.PLAIN; + renderStyle = 0; + colorCode = -1; // This may be a bug in Minecraft's original FontRenderer + break; + + /* Otherwise, must be a color code or some other unsupported code */ + default: + if (code >= 0 && code <= 15) { + colorCode = (byte) code; + fontStyle = Font.PLAIN; // This may be a bug in Minecraft's original FontRenderer + renderStyle = 0; // This may be a bug in Minecraft's original FontRenderer + } + break; + } + + /* Create a new ColorCode object that tracks the position of the code in the original string */ + ColorCode entry = new ColorCode(); + entry.stringIndex = next; + entry.stripIndex = next - shift; + entry.colorCode = colorCode; + entry.fontStyle = fontStyle; + entry.renderStyle = renderStyle; + colorList.add(entry); + + /* Resume search for section marks after skipping this one */ + start = next + 2; + shift += 2; + } + + /* Convert the accumulated ColorCode list to an array for efficient storage */ + cacheEntry.colors = new ColorCode[colorList.size()]; + cacheEntry.colors = colorList.toArray(cacheEntry.colors); + + /* Return the new length of the string after all color codes were removed */ + return text.length - shift; + } + + /** + * Split a string into contiguous LTR or RTL sections by applying the Unicode Bidirectional Algorithm. Calls layoutString() + * for each contiguous run to perform further analysis. + * + * @param glyphList will hold all new Glyph objects allocated by layoutFont() + * @param text the string to lay out + * @param start the offset into text at which to start the layout + * @param limit the (offset + length) at which to stop performing the layout + * @return the total advance (horizontal distance) of this string + */ + private int layoutBidiString(List glyphList, char text[], int start, int limit, ColorCode colors[]) { + int advance = 0; + + /* Avoid performing full bidirectional analysis if text has no "strong" right-to-left characters */ + if (Bidi.requiresBidi(text, start, limit)) { + /* Note that while requiresBidi() uses start/limit the Bidi constructor uses start/length */ + Bidi bidi = new Bidi(text, start, null, 0, limit - start, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT); + + /* If text is entirely right-to-left, then insert an EntryText node for the entire string */ + if (bidi.isRightToLeft()) { + return layoutStyle(glyphList, text, start, limit, Font.LAYOUT_RIGHT_TO_LEFT, advance, colors); + } + + /* Otherwise text has a mixture of LTR and RLT, and it requires full bidirectional analysis */ + else { + int runCount = bidi.getRunCount(); + byte levels[] = new byte[runCount]; + Integer ranges[] = new Integer[runCount]; + + /* Reorder contiguous runs of text into their display order from left to right */ + for (int index = 0; index < runCount; index++) { + levels[index] = (byte) bidi.getRunLevel(index); + ranges[index] = new Integer(index); + } + Bidi.reorderVisually(levels, 0, ranges, 0, runCount); + + /* + * Every GlyphVector must be created on a contiguous run of left-to-right or right-to-left text. Keep track of + * the horizontal advance between each run of text, so that the glyphs in each run can be assigned a position relative + * to the start of the entire string and not just relative to that run. + */ + for (int visualIndex = 0; visualIndex < runCount; visualIndex++) { + int logicalIndex = ranges[visualIndex]; + + /* An odd numbered level indicates right-to-left ordering */ + int layoutFlag = (bidi.getRunLevel(logicalIndex) & 1) == 1 ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT; + advance = layoutStyle(glyphList, text, start + bidi.getRunStart(logicalIndex), start + bidi.getRunLimit(logicalIndex), + layoutFlag, advance, colors); + } + } + + return advance; + } + + /* If text is entirely left-to-right, then insert an EntryText node for the entire string */ + else { + return layoutStyle(glyphList, text, start, limit, Font.LAYOUT_LEFT_TO_RIGHT, advance, colors); + } + } + + private int layoutStyle(List glyphList, char text[], int start, int limit, int layoutFlags, int advance, ColorCode colors[]) { + int currentFontStyle = Font.PLAIN; + + /* Find ColorCode object with stripIndex <= start; that will have the font style in effect at the beginning of this text run */ + int colorIndex = Arrays.binarySearch(colors, start); + + /* + * If no exact match is found, Arrays.binarySearch() returns (-(insertion point) - 1) where the insertion point is the index + * of the first ColorCode with a stripIndex > start. In that case, colorIndex is adjusted to select the immediately preceding + * ColorCode whose stripIndex < start. + */ + if (colorIndex < 0) { + colorIndex = -colorIndex - 2; + } + + /* Break up the string into segments, where each segment has the same font style in use */ + while (start < limit) { + int next = limit; + + /* In case of multiple consecutive color codes with the same stripIndex, select the last one which will have active font style */ + while (colorIndex >= 0 && colorIndex < (colors.length - 1) && colors[colorIndex].stripIndex == colors[colorIndex + 1].stripIndex) { + colorIndex++; + } + + /* If an actual ColorCode object was found (colorIndex within the array), use its fontStyle for layout and render */ + if (colorIndex >= 0 && colorIndex < colors.length) { + currentFontStyle = colors[colorIndex].fontStyle; + } + + /* + * Search for the next ColorCode that uses a different fontStyle than the current one. If found, the stripIndex of that + * new code is the split point where the string must be split into a separately styled segment. + */ + while (++colorIndex < colors.length) { + if (colors[colorIndex].fontStyle != currentFontStyle) { + next = colors[colorIndex].stripIndex; + break; + } + } + + /* Layout the string segment with the style currently selected by the last color code */ + advance = layoutString(glyphList, text, start, next, layoutFlags, advance, currentFontStyle); + start = next; + } + + return advance; + } + + /** + * Given a string that runs contiguously LTR or RTL, break it up into individual segments based on which fonts can render + * which characters in the string. Calls layoutFont() for each portion of the string that can be layed out with a single + * font. + * + * @param glyphList will hold all new Glyph objects allocated by layoutFont() + * @param text the string to lay out + * @param start the offset into text at which to start the layout + * @param limit the (offset + length) at which to stop performing the layout + * @param layoutFlags either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT + * @param advance the horizontal advance (i.e. X position) returned by previous call to layoutString() + * @param style combination of Font.PLAIN, Font.BOLD, and Font.ITALIC to select a fonts with some specific style + * @return the advance (horizontal distance) of this string plus the advance passed in as an argument + * @todo Correctly handling RTL font selection requires scanning the sctring from RTL as well. + * @todo Use bitmap fonts as a fallback if no OpenType font could be found + */ + private int layoutString(List glyphList, char text[], int start, int limit, int layoutFlags, int advance, int style) { + /* + * Convert all digits in the string to a '0' before layout to ensure that any glyphs replaced on the fly will all have + * the same positions. Under Windows, Java's "SansSerif" logical font uses the "Arial" font for digits, in which the "1" + * digit is slightly narrower than all other digits. Checking the digitGlyphsReady flag prevents a chicken-and-egg + * problem where the digit glyphs have to be initially cached and the digitGlyphs[] array initialized without replacing + * every digit with '0'. + */ + if (digitGlyphsReady) { + for (int index = start; index < limit; index++) { + if (text[index] >= '0' && text[index] <= '9') { + text[index] = '0'; + } + } + } + + /* Break the string up into segments, where each segment can be displayed using a single font */ + while (start < limit) { + if (isLineBreak(text[start])) { + start++; + continue; + } + + Font font = glyphCache.lookupFont(text, start, limit, style); + int next = font.canDisplayUpTo(text, start, limit); + + /* canDisplayUpTo returns -1 if the entire string range is supported by this font */ + if (next == -1) { + next = limit; + } + + /* + * canDisplayUpTo() returns start if the starting character is not supported at all. In that case, draw just the + * one unsupported character (which will use the font's "missing glyph code"), then retry the lookup again at the + * next character after that. + */ + if (next == start) { + next++; + } + + advance = layoutFont(glyphList, text, start, next, layoutFlags, advance, font); + start = next; + } + + return advance; + } + + /** + * Allocate new Glyph objects and add them to the glyph list. This sequence of Glyphs represents a portion of the + * string where all glyphs run contiguously in either LTR or RTL and come from the same physical/logical font. + * + * @param glyphList all newly created Glyph objects are added to this list + * @param text the string to layout + * @param start the offset into text at which to start the layout + * @param limit the (offset + length) at which to stop performing the layout + * @param layoutFlags either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT + * @param advance the horizontal advance (i.e. X position) returned by previous call to layoutString() + * @param font the Font used to layout a GlyphVector for the string + * @return the advance (horizontal distance) of this string plus the advance passed in as an argument + * @todo need to ajust position of all glyphs if digits are present, by assuming every digit should be 0 in length + */ + private int layoutFont(List glyphList, char text[], int start, int limit, int layoutFlags, int advance, Font font) { + /* + * Ensure that all glyphs used by the string are pre-rendered and cached in the texture. Only safe to do so from the + * main thread because cacheGlyphs() can crash LWJGL if it makes OpenGL calls from any other thread. In this case, + * cacheString() will also not insert the entry into the stringCache since it may be incomplete if lookupGlyph() + * returns null for any glyphs not yet stored in the glyph cache. + */ + if (mainThread == Thread.currentThread()) { + glyphCache.cacheGlyphs(font, text, start, limit, layoutFlags); + } + + /* Creating a GlyphVector takes care of all language specific OpenType glyph substitutions and positionings */ + GlyphVector vector = glyphCache.layoutGlyphVector(font, text, start, limit, layoutFlags); + + /* + * Use the same GASP-aware FontRenderContext that was used to rasterize glyphs into the atlas. + * Passing null here would use a default FRC that can produce different pixel bounds for hinted + * glyphs, causing position mismatches and visible spacing gaps (especially on punctuation). + */ + java.awt.font.FontRenderContext frc = glyphCache.getFontRenderContext(); + + /* + * Extract all needed information for each glyph from the GlyphVector so it won't be needed for actual rendering. + * Note that initially, glyph.start holds the character index into the stripped text array. But after the entire + * string is layed out, this field will be adjusted on every Glyph object to correctly index the original unstripped + * string. + * + * Use getGlyphPosition() (logical font metrics) for horizontal placement and advance calculation. + * Use getGlyphPixelBounds() only for the Y position and to compute the ink X offset from the logical X. + * This separation ensures proper inter-glyph spacing (from logical metrics) while keeping correct + * texture quad alignment (from ink bounds). Under GASP hinting, ink bounds can be tighter than + * logical positions, causing glyphs to visually touch or overlap if used for spacing. + */ + Glyph glyph = null; + int numGlyphs = vector.getNumGlyphs(); + int glyphPadding = glyphCache.getGlyphPadding(); + for (int index = 0; index < numGlyphs; index++) { + int charIndex = start + vector.getGlyphCharIndex(index); + if (charIndex >= start && charIndex < limit && isLineBreak(text[charIndex])) { + continue; + } + + Point2D logicalPos = vector.getGlyphPosition(index); + int logicalX = advance + (int) logicalPos.getX(); + Rectangle inkBounds = vector.getGlyphPixelBounds(index, frc, advance, 0); + + /* Compute horizontal advance for the previous glyph based on logical positions */ + if (glyph != null) { + glyph.advance = logicalX - glyph.x; + } + + /* + * Allocate a new glyph object and add to the glyphList. The glyph.stringIndex here is really like stripIndex but + * it will be corrected later to account for the color codes that have been stripped out. + */ + glyph = new Glyph(); + glyph.stringIndex = charIndex; + glyph.texture = glyphCache.lookupGlyph(font, vector.getGlyphCode(index)); + glyph.x = logicalX; + glyph.y = inkBounds.y - glyphPadding; + glyph.inkOffsetX = (inkBounds.x - logicalX) - glyphPadding; + glyphList.add(glyph); + } + + /* Compute the advance position of the last glyph (or only glyph) since it can't be done by the above loop */ + advance += (int) vector.getGlyphPosition(numGlyphs).getX(); + if (glyph != null) { + glyph.advance = advance - glyph.x; + } + + /* Return the overall horizontal advance in pixels from the start of string */ + return advance; + } +} diff --git a/core/src/main/java/noppes/npcs/config/legacy/ConfigProp.java b/core/src/main/java/noppes/npcs/config/legacy/ConfigProp.java new file mode 100644 index 000000000..c232aa75d --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/legacy/ConfigProp.java @@ -0,0 +1,14 @@ +package noppes.npcs.config.legacy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigProp { + String name() default ""; + + String info() default ""; +} diff --git a/core/src/main/java/noppes/npcs/config/legacy/LegacyConfig.java b/core/src/main/java/noppes/npcs/config/legacy/LegacyConfig.java new file mode 100644 index 000000000..f1cc0d633 --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/legacy/LegacyConfig.java @@ -0,0 +1,157 @@ +package noppes.npcs.config.legacy; + +import java.io.File; + +public class LegacyConfig { + @ConfigProp(info = "Disable Chat Bubbles") + public static boolean EnableChatBubbles = true; + + @ConfigProp(info = "Enables/Disables ALL scripting. You can still see and write code in the scripter, but these scripts won't run. True by default") + public static boolean ScriptingEnabled = true; + + @ConfigProp(info = "Enables/Disables the use of the the ECMA6 Javascript standard instead of ECMA5.1." + + "\nEnabling this adds many more features to JS in scripts. Only use if on Java 8 or higher." + + "\nNot all ECMA 6 language is supported through this functionality.") + public static boolean ScriptingECMA6 = false; + + @ConfigProp(info = "Enables global player event scripts to be used in the scripter. You can still see and write code in the scripter, but these scripts won't run. True by default.") + public static boolean GlobalPlayerScripts = true; + + @ConfigProp(info = "Enables global forge event scripts to be used in the scripter. You can still see and write code in the scripter, but these scripts won't run. True by default.") + public static boolean GlobalForgeScripts = true; + + @ConfigProp(info = "Enables global NPC scripts to be used in the scripter. You can still see and write code in the scripter, but these scripts won't run. False by default, use with caution!") + public static boolean GlobalNPCScripts = false; + + @ConfigProp(info = "If scripts are too long (>65535 characters), they normally won't be saved in NBT data.\n" + + "This config adds additional compound tags to scripts that need it, so you can store much larger scripts!\n" + + "Every additional compound tag adds 65535 more characters to your script length limit. Use incrementally, with caution.") + public static int ExpandedScriptLimit = 2; + + @ConfigProp(info = "Enables if Player Information (WAND-USE) should be printed to CustomNPCs Logs. IF on Server \n" + + "Logs will only be present SERVER-SIDE only in CustomNPCs-latest, -1, -2, and -3") + public static boolean PlayerLogging = false; + + @ConfigProp(info = "Enables if Scripting Information should be printed to CustomNPCs Logs. IF on Server \n" + + "Logs will only be present SERVER-SIDE only in CustomNPCs-latest, -1, -2, and -3") + public static boolean ScriptLogging = false; + + @ConfigProp(info = "Amount of Messages marked as SPAM [5, 3000]. Lower Number means MORE accurate messages \n" + + "This frequency will determine if the log will print a line with [SPAM] to warn the console.") + public static int ScriptFrequency = 20; + + @ConfigProp(info = "IN Milliseconds 1s = 1000s. If a recent LOG of the same event is SENT within this threshold it will be ignored.") + public static int ScriptIgnoreTime = 2000; + + @ConfigProp(info = "Comma separated list of NPC Script Types that will omit these from the logs,\n" + + "INIT,TICK,INTERACT,DIALOG,DAMAGED,KILLED,ATTACK,TARGET,COLLIDE,KILLS,DIALOG_CLOSE,TIMER") + public static String ScriptLogIgnoreType = "TICK"; + + @ConfigProp(info = "Navigation search range for NPCs. Not recommended to increase if you have a slow pc or on a server. Minimum of 16, maximum of 96.") + public static int NpcNavRange = 32; + + @ConfigProp(info = "Size limit for NPCs. Default 100, larger sizes may cause lag on clients and servers that can't take it!") + public static int NpcSizeLimit = 100; + + @ConfigProp(info = "Set to true if you want the dialog command option to be able to use op commands like tp etc") + public static boolean NpcUseOpCommands = false; + + @ConfigProp(info = "Client sided! Determines where tracking quest info shows up on the screen based on a number from 0 to 8. Default: 3") + public static int TrackingInfoAlignment = 3; + + @ConfigProp(info = "Client sided! Offsets the tracking info GUI by this amount in the X direction.") + public static int TrackingInfoX = 0; + + @ConfigProp(info = "Client sided! Offsets the tracking info GUI by this amount in the Y direction.") + public static int TrackingInfoY = 0; + + @ConfigProp + public static boolean InventoryGuiEnabled = true; + + @ConfigProp + public static boolean DisableExtraItems = false; + + @ConfigProp + public static boolean DisableExtraBlock = false; + + @ConfigProp(info = "Only ops can create and edit npcs") + public static boolean OpsOnly = false; + + @ConfigProp(info = "Only ops can see and edit scripts") + public static boolean ScriptOpsOnly = false; + + @ConfigProp(info = "Comma separated list of player UUIDs that can see and edit scripts. If ScriptsOpsOnly is true,\n" + + "ops and players with these IDs can see and edit scripts. Example:\n" + + "b876ec32-e396-476b-a115-8438d83c67d4,069a79f4-44e9-4726-a5be-fca90e38aaf5,be951074-c7ea-4f02-a725-bf017bc88650\n" + + "Get a player's UUID from a site like NameMC or the API IPlayer.getUniqueID() function!\n" + + "If left empty and ScriptsOpsOnly is false, anyone can see and edit scripts with a scripter.") + public static String ScriptDevIDs = ""; + + @ConfigProp(info = "Default interact line. Leave empty to not have one") + public static String DefaultInteractLine = "Hello @p"; + + @ConfigProp + public static boolean DisableEnchants = false; + @ConfigProp(info = "Start Id for enchants. IDs can only range from 0-256") + public static int EnchantStartId = 100; + + @ConfigProp(info = "Number of chunk loading npcs that can be active at the same time") + public static int ChunkLoaders = 20; + + @ConfigProp(info = "The maximum number of images any dialog can hold.") + public static int DialogImageLimit = 10; + + @ConfigProp(info = "The maximum number of overlays any npc/player can hold.") + public static int SkinOverlayLimit = 10; + + @ConfigProp(info = "Set to false if you want to disable guns") + public static boolean GunsEnabled = true; + + @ConfigProp(info = "Enables leaves decay") + public static boolean LeavesDecayEnabled = true; + + @ConfigProp(info = "Enables Vine Growth") + public static boolean VineGrowthEnabled = true; + + @ConfigProp(info = "Enables Ice Melting") + public static boolean IceMeltsEnabled = true; + + @ConfigProp(info = "Normal players can use soulstone on animals") + public static boolean SoulStoneAnimals = true; + + @ConfigProp(info = "Normal players can use soulstone on villagers") + public static boolean SoulStoneVillagers = false; + + @ConfigProp(info = "Normal players can use soulstone on all npcs") + public static boolean SoulStoneNPCs = false; + + @ConfigProp(info = "Normal players can use soulstone on friendly npcs") + public static boolean SoulStoneFriendlyNPCs = false; + + @ConfigProp(info = "When set to Minecraft it will use minecrafts font, when Default it will use OpenSans. Can only use fonts installed on your PC") + public static String FontType = "Default"; + + @ConfigProp(info = "Font size for custom fonts (doesn't work with minecrafts font)") + public static int FontSize = 18; + + @ConfigProp(info = "Enables Overlay Mixins for Conflicts relating to Optifine or other Skin Renderers. If crashes occur, please disable.") + public static boolean EntityRendererMixin = true; + + @ConfigProp(info = "Enables CustomNpcs startup update message") + public static boolean EnableUpdateChecker = true; + + public static LegacyLoader Config; + + public void init() { + MinecraftServer server = PlatformServiceHolder.get().getServer(); + String dir = ""; + if (server != null) { + dir = new File(".").getAbsolutePath(); + } else { + dir = Minecraft.getMinecraft().mcDataDir.getAbsolutePath(); + } + + Config = new LegacyLoader(this.getClass(), new File(dir, "config"), "CustomNpcs"); + Config.loadConfig(); + } +} diff --git a/core/src/main/java/noppes/npcs/config/legacy/LegacyLoader.java b/core/src/main/java/noppes/npcs/config/legacy/LegacyLoader.java new file mode 100644 index 000000000..8c1995b8a --- /dev/null +++ b/core/src/main/java/noppes/npcs/config/legacy/LegacyLoader.java @@ -0,0 +1,140 @@ +package noppes.npcs.config.legacy; + +import noppes.npcs.LogWriter; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.LinkedList; + +public class LegacyLoader { + private boolean updateFile = false; + private File dir; + private String fileName; + private Class configClass; + private LinkedList configFields; + + public LegacyLoader(Class clss, File dir, String fileName) { + if (!dir.exists()) + dir.mkdir(); + this.dir = dir; + configClass = clss; + configFields = new LinkedList(); + this.fileName = fileName + ".cfg"; + Field fields[] = configClass.getDeclaredFields(); + for (Field field : fields) { + if (field.isAnnotationPresent(ConfigProp.class)) { + configFields.add(field); + } + } + } + + public void loadConfig() { + try { + File configFile = new File(dir, fileName); + HashMap types = new HashMap(); + for (Field field : configFields) { + ConfigProp prop = (ConfigProp) field.getAnnotation(ConfigProp.class); + types.put(!prop.name().isEmpty() ? prop.name() : field.getName(), field); + } + if (configFile.exists()) { + HashMap properties = parseConfig(configFile, types); + for (String prop : properties.keySet()) { + Field field = types.get(prop); + Object obj = properties.get(prop); + if (!obj.equals(field.get(null))) { + field.set(null, obj); + } + } + for (String type : types.keySet()) + if (!properties.containsKey(type)) + updateFile = true; + } else { + updateFile = true; + } + } catch (Exception e) { + updateFile = true; + LogWriter.except(e); + } + if (updateFile) { + updateConfig(); + } + updateFile = false; + } + + private HashMap parseConfig(File file, HashMap types) throws Exception { + HashMap config = new HashMap(); + BufferedReader reader = new BufferedReader(new FileReader(file)); + String strLine; + while ((strLine = reader.readLine()) != null) { + if (strLine.startsWith("#") || strLine.length() == 0) + continue; + int index = strLine.indexOf("="); + if (index <= 0 || index == strLine.length()) { + updateFile = true; + continue; + } + String name = strLine.substring(0, index); + String prop = strLine.substring(index + 1); + if (!types.containsKey(name)) { + updateFile = true; + continue; + } + Object obj = null; + Class class2 = types.get(name).getType(); + if (class2.isAssignableFrom(java.lang.String.class)) { + obj = prop; + } else if (class2.isAssignableFrom(Integer.TYPE)) { + obj = Integer.valueOf(Integer.parseInt(prop)); + } else if (class2.isAssignableFrom(Short.TYPE)) { + obj = Short.valueOf(Short.parseShort(prop)); + } else if (class2.isAssignableFrom(Byte.TYPE)) { + obj = Byte.valueOf(Byte.parseByte(prop)); + } else if (class2.isAssignableFrom(Boolean.TYPE)) { + obj = Boolean.valueOf(Boolean.parseBoolean(prop)); + } else if (class2.isAssignableFrom(Float.TYPE)) { + obj = Float.valueOf(Float.parseFloat(prop)); + } else if (class2.isAssignableFrom(Double.TYPE)) { + obj = Double.valueOf(Double.parseDouble(prop)); + } + if (obj != null) + config.put(name, obj); + } + reader.close(); + return config; + } + + public void updateConfig() { + File file = new File(dir, fileName); + try { + if (!file.exists()) + file.createNewFile(); + BufferedWriter out = new BufferedWriter(new FileWriter(file)); + for (Field field : configFields) { + ConfigProp prop = (ConfigProp) field.getAnnotation(ConfigProp.class); + if (prop.info().length() != 0) + out.write("#" + prop.info() + System.getProperty("line.separator")); + String name = !prop.name().isEmpty() ? prop.name() : field.getName(); + try { + out.write(name + "=" + field.get(null).toString() + System.getProperty("line.separator")); + out.write(System.getProperty("line.separator")); + } catch (IllegalArgumentException e) { + + e.printStackTrace(); + } catch (IllegalAccessException e) { + + e.printStackTrace(); + } + } + out.close(); + } catch (IOException e) { + + e.printStackTrace(); + } + } +} diff --git a/core/src/main/java/noppes/npcs/constants/ClientOnly.java b/core/src/main/java/noppes/npcs/constants/ClientOnly.java new file mode 100644 index 000000000..eb811705e --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/ClientOnly.java @@ -0,0 +1,29 @@ +package noppes.npcs.constants; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import java.lang.annotation.*; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Marker annotation for client-only code. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR}) +public @interface ClientOnly { +} diff --git a/core/src/main/java/noppes/npcs/constants/Direction.java b/core/src/main/java/noppes/npcs/constants/Direction.java new file mode 100644 index 000000000..6f1969162 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/Direction.java @@ -0,0 +1,26 @@ +package noppes.npcs.constants; + +public enum Direction { + DOWN(0, -1, 0), + UP(0, 1, 0), + NORTH(0, 0, -1), + SOUTH(0, 0, 1), + WEST(-1, 0, 0), + EAST(1, 0, 0); + + private final int offsetX, offsetY, offsetZ; + + Direction(int x, int y, int z) { + this.offsetX = x; + this.offsetY = y; + this.offsetZ = z; + } + + public int getOffsetX() { return offsetX; } + public int getOffsetY() { return offsetY; } + public int getOffsetZ() { return offsetZ; } + + public static Direction byIndex(int index) { + return values()[index % values().length]; + } +} diff --git a/core/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java b/core/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java new file mode 100644 index 000000000..6c6fdce44 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java @@ -0,0 +1,44 @@ +package noppes.npcs.constants; + +import noppes.npcs.config.ConfigMarket; + +/** + * Enum for auction log action types. + * Used by AuctionController.logAuction() for type-safe logging. + */ +public enum EnumAuctionLogAction { + CREATED, + BID, + BUYOUT, + SOLD, + EXPIRED, + CANCELLED, + CLAIMED, + CLAIM_EXPIRED; + + /** + * Check if this action should be logged based on config settings + */ + public boolean shouldLog() { + if (!ConfigMarket.AuctionLoggingEnabled) return false; + switch (this) { + case CREATED: + return ConfigMarket.LogAuctionCreated; + case BID: + return ConfigMarket.LogAuctionBid; + case BUYOUT: + return ConfigMarket.LogAuctionBuyout; + case SOLD: + return ConfigMarket.LogAuctionSold; + case EXPIRED: + return ConfigMarket.LogAuctionExpired; + case CANCELLED: + return ConfigMarket.LogAuctionCancelled; + case CLAIMED: + case CLAIM_EXPIRED: + return ConfigMarket.LogAuctionClaimed; + default: + return false; + } + } +} diff --git a/core/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/core/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java new file mode 100644 index 000000000..20a909f3b --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java @@ -0,0 +1,45 @@ +package noppes.npcs.constants; + +public enum EnumAuctionStatus { + ACTIVE("auction.status.active"), + ENDED("auction.status.ended"), + CANCELLED("auction.status.cancelled"), + CLAIMED("auction.status.claimed"); + + private final String langKey; + + EnumAuctionStatus(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return PlatformServiceHolder.get().translateToLocal(langKey); + } + + public boolean isActive() { + return this == ACTIVE; + } + + public boolean isEnded() { + return this == ENDED || this == CANCELLED || this == CLAIMED; + } + + public boolean canBid() { + return this == ACTIVE; + } + + public boolean canCancel() { + return this == ACTIVE; + } + + public static EnumAuctionStatus fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return ACTIVE; + } +} diff --git a/core/src/main/java/noppes/npcs/constants/EnumClaimType.java b/core/src/main/java/noppes/npcs/constants/EnumClaimType.java new file mode 100644 index 000000000..34b8af0a4 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/EnumClaimType.java @@ -0,0 +1,36 @@ +package noppes.npcs.constants; + +public enum EnumClaimType { + ITEM("auction.claim.item"), + CURRENCY("auction.claim.currency"), + REFUND("auction.claim.refund"); + + private final String langKey; + + EnumClaimType(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return PlatformServiceHolder.get().translateToLocal(langKey); + } + + public boolean isItem() { + return this == ITEM; + } + + public boolean isCurrency() { + return this == CURRENCY || this == REFUND; + } + + public static EnumClaimType fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return ITEM; + } +} diff --git a/core/src/main/java/noppes/npcs/constants/EnumCompanionTalent.java b/core/src/main/java/noppes/npcs/constants/EnumCompanionTalent.java new file mode 100644 index 000000000..abf2927c4 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/EnumCompanionTalent.java @@ -0,0 +1,25 @@ +package noppes.npcs.constants; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.CustomItems; + +public enum EnumCompanionTalent { + INVENTORY(CustomItems.satchel), ARMOR(Items.iron_chestplate), + SWORD(Items.diamond_sword), RANGED(Items.bow), + ACROBATS(Items.leather_boots), INTEL(CustomItems.letter); + + public IItemStack item; + + private EnumCompanionTalent(Item item) { + this.item = new IItemStack(item); + } +} diff --git a/core/src/main/java/noppes/npcs/constants/EnumNotificationType.java b/core/src/main/java/noppes/npcs/constants/EnumNotificationType.java new file mode 100644 index 000000000..fab0953f2 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/EnumNotificationType.java @@ -0,0 +1,30 @@ +package noppes.npcs.constants; + +public enum EnumNotificationType { + AUCTION_WON("auction.notify.won"), + AUCTION_OUTBID("auction.notify.outbid"), + AUCTION_SOLD("auction.notify.sold"), + AUCTION_EXPIRED("auction.notify.expired"), + CLAIM_READY("auction.notify.claim"); + + private final String langKey; + + EnumNotificationType(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return PlatformServiceHolder.get().translateToLocal(langKey); + } + + public static EnumNotificationType fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return CLAIM_READY; + } +} diff --git a/core/src/main/java/noppes/npcs/constants/EnumNpcToolMaterial.java b/core/src/main/java/noppes/npcs/constants/EnumNpcToolMaterial.java index 065228b3d..33973bd88 100644 --- a/core/src/main/java/noppes/npcs/constants/EnumNpcToolMaterial.java +++ b/core/src/main/java/noppes/npcs/constants/EnumNpcToolMaterial.java @@ -1,5 +1,16 @@ package noppes.npcs.constants; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + public enum EnumNpcToolMaterial { WOOD(0, 59, 2.0F, 0, 15), STONE(1, 131, 4.0F, 1, 5), @@ -30,7 +41,7 @@ public enum EnumNpcToolMaterial { /** * Damage versus entities. */ - private final int damageVsEntity; + private final int damageVsIEntity; /** * Defines the natural enchantability factor of the material. @@ -41,7 +52,7 @@ public enum EnumNpcToolMaterial { this.harvestLevel = par3; this.maxUses = par4; this.efficiencyOnProperMaterial = par5; - this.damageVsEntity = par6; + this.damageVsIEntity = par6; this.enchantability = par7; } @@ -62,8 +73,8 @@ public enum EnumNpcToolMaterial { /** * Damage versus entities. */ - public int getDamageVsEntity() { - return this.damageVsEntity; + public int getDamageVsIEntity() { + return this.damageVsIEntity; } /** diff --git a/core/src/main/java/noppes/npcs/constants/EnumPotionType.java b/core/src/main/java/noppes/npcs/constants/EnumPotionType.java new file mode 100644 index 000000000..d53c992d3 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/EnumPotionType.java @@ -0,0 +1,92 @@ +package noppes.npcs.constants; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +public enum EnumPotionType { + None("gui.none", -1), + Fire("tile.fire.name", -1), + Poison("IPotionType.poison", IPotionType.poison.id), + Hunger("IPotionType.hunger", IPotionType.hunger.id), + Weakness("IPotionType.weakness", IPotionType.weakness.id), + Slowness("IPotionType.moveSlowdown", IPotionType.moveSlowdown.id), + Nausea("IPotionType.confusion", IPotionType.confusion.id), + Blindness("IPotionType.blindness", IPotionType.blindness.id), + Wither("IPotionType.wither", IPotionType.wither.id), + MiningFatigue("IPotionType.digSlowDown", IPotionType.digSlowdown.id), + Manual("effect.manual", -1); + + private final String langKey; + private final int IPotionTypeId; + + EnumPotionType(String langKey, int IPotionTypeId) { + this.langKey = langKey; + this.IPotionTypeId = IPotionTypeId; + } + + public String getLangKey() { + return langKey; + } + + public int getIPotionTypeId() { + return IPotionTypeId; + } + + /** + * Returns the actual IPotionType ID to apply. + * For Manual type, returns the provided manualId. + * For all others, returns the mapped IPotionTypeId. + */ + public int getResolvedIPotionTypeId(int manualId) { + if (this == Manual) return manualId; + return IPotionTypeId; + } + + public static String[] getLangKeys() { + EnumPotionType[] types = values(); + String[] keys = new String[types.length]; + for (int i = 0; i < types.length; i++) keys[i] = types[i].langKey; + return keys; + } + + /** + * Returns lang keys excluding None. For use in effect lists where None is not a valid choice. + */ + public static String[] getLangKeysNoNone() { + EnumPotionType[] types = values(); + String[] keys = new String[types.length - 1]; + for (int i = 1; i < types.length; i++) keys[i - 1] = types[i].langKey; + return keys; + } + + /** + * Maps a 0-based index (excluding None) back to the corresponding EnumPotionType. + * Index 0 = Fire, 1 = Poison, etc. + */ + public static EnumPotionType fromIndexNoNone(int index) { + return fromOrdinal(index + 1); + } + + public static EnumPotionType fromOrdinal(int ordinal) { + EnumPotionType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return None; + } + + /** + * Checks if the given IPotionType ID is valid and registered in the Minecraft IPotionType registry. + */ + public static boolean isValidIPotionTypeId(int id) { + return id >= 0 && id < IPotionType.IPotionTypeTypes.length && IPotionType.IPotionTypeTypes[id] != null; + } +} diff --git a/core/src/main/java/noppes/npcs/constants/EnumScriptType.java b/core/src/main/java/noppes/npcs/constants/EnumScriptType.java index f6d05d65b..c438d36b3 100644 --- a/core/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/core/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -1,5 +1,16 @@ package noppes.npcs.constants; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + public enum EnumScriptType { INIT("init"), TICK("tick"), @@ -40,7 +51,7 @@ public enum EnumScriptType { LOGIN("login"), LOGOUT("logout"), CHAT("chat"), - DAMAGED_ENTITY("damagedEntity"), + DAMAGED_IEntity("damagedIEntity"), SPAWN("spawn"), TOSSED("tossed"), PICKEDUP("pickedUp"), @@ -89,8 +100,8 @@ public enum EnumScriptType { BREAK_BLOCK("breakBlock"), FORGE_EVENT("forgeEvent"), FORGE_INIT("forgeInit"), - FORGE_ENTITY("forgeEntity"), - FORGE_WORLD("forgeWorld"), + FORGE_IEntity("forgeIEntity"), + FORGE_IWorld("forgeIWorld"), CNPC_NATURAL_SPAWN("onCNPCNaturalSpawn"), CUSTOM_ITEM_EVENT("customItemEvent"), @@ -136,7 +147,7 @@ public enum EnumScriptType { ENERGY_PROJECTILE_FIRED("energyProjectileFired"), ENERGY_PROJECTILE_TICK("energyProjectileTick"), - ENERGY_PROJECTILE_ENTITY_IMPACT("energyProjectileEntityImpact"), + ENERGY_PROJECTILE_IEntity_IMPACT("energyProjectileIEntityImpact"), ENERGY_PROJECTILE_BLOCK_IMPACT("energyProjectileBlockImpact"), ENERGY_PROJECTILE_EXPIRED("energyProjectileExpired"), diff --git a/core/src/main/java/noppes/npcs/constants/EnumStockReset.java b/core/src/main/java/noppes/npcs/constants/EnumStockReset.java index c66abdb78..dd1ac97ee 100644 --- a/core/src/main/java/noppes/npcs/constants/EnumStockReset.java +++ b/core/src/main/java/noppes/npcs/constants/EnumStockReset.java @@ -1,5 +1,16 @@ package noppes.npcs.constants; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + /** * Stock reset types for Trader NPCs. * Mirrors EnumQuestRepeat pattern for consistency. @@ -46,7 +57,7 @@ public enum EnumStockReset { } /** - * Check if this reset type uses real-world time + * Check if this reset type uses real-IWorld time */ public boolean isRealTime() { return this == RLDAILY || this == RLWEEKLY || this == RLCUSTOM; diff --git a/core/src/main/java/noppes/npcs/constants/ItemAction.java b/core/src/main/java/noppes/npcs/constants/ItemAction.java new file mode 100644 index 000000000..e179372f8 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/ItemAction.java @@ -0,0 +1,9 @@ +package noppes.npcs.constants; + +public enum ItemAction { + NONE, + EAT, + DRINK, + BLOCK, + BOW +} diff --git a/core/src/main/java/noppes/npcs/constants/ScriptContext.java b/core/src/main/java/noppes/npcs/constants/ScriptContext.java new file mode 100644 index 000000000..572b8e038 --- /dev/null +++ b/core/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -0,0 +1,326 @@ +package noppes.npcs.constants; + +import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IAuctionEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IEnergyBarrierEvent; +import noppes.npcs.api.event.IEnergyProjectileEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.ILinkedItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IChainEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; +import noppes.npcs.api.event.IRecipeEvent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Represents a script context that maps to one or more event interface namespaces. + *

+ * A script context can have multiple namespaces because script editors often + * support hooks from multiple event types. For example, Player scripts support: + * - IPlayerEvent (core player hooks) + * - IAnimationEvent (animation hooks) + * - IPartyEvent (party hooks) + * - ICustomGuiEvent (custom GUI hooks) + * - etc. + *

+ * Each context also has a hookContext string used by ScriptHookController for + * hook registration and lookup. + *

+ * This is a registerable system - any mod can register their own contexts: + *

+ * // Register a new context with multiple namespaces: + * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); + *

+ * // Or add namespaces to an existing context: + * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); + */ +public class ScriptContext { + + // Registry of all script contexts + private static final Map REGISTRY = new ConcurrentHashMap<>(); + + // ==================== BUILT-IN CONTEXTS ==================== + + public static final ScriptContext NPC = register("NPC", "npc", + INpcEvent.class, + IProjectileEvent.class, + IEnergyProjectileEvent.class, + IEnergyBarrierEvent.class, + IAbilityEvent.class, + IChainEvent.class, + IAnimationEvent.class + ); + + public static final ScriptContext PLAYER = register("PLAYER", "player", + IPlayerEvent.class, + IAnimationEvent.class, + IPartyEvent.class, + IDialogEvent.class, + IQuestEvent.class, + IFactionEvent.class, + ICustomGuiEvent.class, + IEnergyProjectileEvent.class, + IEnergyBarrierEvent.class, + IAbilityEvent.class, + IChainEvent.class, + IAuctionEvent.class + ); + + public static final ScriptContext BLOCK = register("BLOCK", "block", + IBlockEvent.class + ); + + public static final ScriptContext ITEM = register("ITEM", "item", + IItemEvent.class + ); + + public static final ScriptContext FORGE = register("FORGE", "forge", + IForgeEvent.class + ); + + public static final ScriptContext LINKED_ITEM = register("LINKED_ITEM", "linked_item", + ILinkedItemEvent.class, + IItemEvent.class + ); + + public static final ScriptContext RECIPE = register("RECIPE", "recipe", + IRecipeEvent.class + ); + + public static final ScriptContext EFFECT = register("EFFECT", "effect", + IPlayerEvent.class + ); + + public static final ScriptContext ABILITY = register("ABILITY", "ability", + IAbilityEvent.class + ); + + public static final ScriptContext CHAINED_ABILITY = register("CHAINED_ABILITY", "chained_ability", + IChainEvent.class + ); + + public static final ScriptContext GLOBAL = register("GLOBAL", "", + "Global" // Special case: Global namespace doesn't have a corresponding event class + ); + + // ==================== INSTANCE FIELDS ==================== + + /** + * Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") + */ + public final String id; + + /** + * Hook context identifier used by ScriptHookController (e.g., "npc", "player") + */ + public final String hookContext; + + /** + * The event interface namespaces this context supports + */ + private final List namespaces; + + /** + * Fully qualified names of the event interface classes this context supports. + * Only populated when registered via the Class overload. + */ + private final List namespaceFQNs = new ArrayList<>(); + + // ==================== CONSTRUCTOR ==================== + + private ScriptContext(String id, String hookContext, String... namespaces) { + this.id = id; + this.hookContext = hookContext != null ? hookContext : ""; + this.namespaces = new ArrayList<>(Arrays.asList(namespaces)); + } + + // ==================== NAMESPACE ACCESS ==================== + + /** + * Get all namespaces this context supports. + * + * @return Unmodifiable list of namespace strings + */ + public List getNamespaces() { + return Collections.unmodifiableList(namespaces); + } + + /** + * Get the fully qualified names of event interface classes this context supports. + * Only populated for contexts registered with Class references. + * + * @return Unmodifiable list of FQN strings (e.g., "noppes.npcs.api.event.INpcEvent") + */ + public List getNamespaceFQNs() { + return Collections.unmodifiableList(namespaceFQNs); + } + + /** + * Check if this context includes a specific namespace. + * + * @param namespace The namespace to check (e.g., "IPlayerEvent") + * @return true if this context supports hooks from that namespace + */ + public boolean hasNamespace(String namespace) { + return namespaces.contains(namespace); + } + + /** + * Add a namespace to this context. + * Mods can use this to extend built-in contexts with their own event types. + * + * @param namespace The namespace to add (e.g., "IDBCPlayerEvent") + */ + public void addNamespace(String namespace) { + if (!namespaces.contains(namespace)) { + namespaces.add(namespace); + } + } + + /** + * Get the primary namespace (first one registered). + * This is mainly for backward compatibility. + * + * @return The first namespace, or "Global" if none + */ + public String getPrimaryNamespace() { + return namespaces.isEmpty() ? "Global" : namespaces.get(0); + } + + // ==================== REGISTRATION API ==================== + + /** + * Register a new script context with a hook context and one or more namespaces. + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "dbc") + * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, String hookContext, String... namespaces) { + ScriptContext context = new ScriptContext(id, hookContext, namespaces); + REGISTRY.put(id, context); + return context; + } + + /** + * Register a new script context using event interface classes. + * Automatically extracts the simple name from each class. + *

+ * Example: + * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "npc") + * @param eventClasses The event interface classes (simple names will be extracted) + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, String hookContext, Class... eventClasses) { + String[] namespaces = new String[eventClasses.length]; + for (int i = 0; i < eventClasses.length; i++) { + namespaces[i] = eventClasses[i].getSimpleName(); + } + ScriptContext context = register(id, hookContext, namespaces); + for (Class clazz : eventClasses) { + context.namespaceFQNs.add(clazz.getName().replace('$', '.')); + } + return context; + } + + /** + * Get a script context by its ID. + * + * @param id The context ID (e.g., "NPC", "PLAYER", "DBC") + * @return The ScriptContext, or GLOBAL if not found + */ + public static ScriptContext byId(String id) { + return REGISTRY.getOrDefault(id, GLOBAL); + } + + /** + * Find a script context that contains a specific namespace. + * + * @param namespace The event interface namespace (e.g., "INpcEvent") + * @return The first ScriptContext that contains this namespace, or GLOBAL if not found + */ + public static ScriptContext byNamespace(String namespace) { + if (namespace == null) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (ctx.hasNamespace(namespace)) { + return ctx; + } + } + return GLOBAL; + } + + /** + * Find a script context by its hook context string. + * + * @param hookContext The hook context string (e.g., "npc", "player") + * @return The ScriptContext with that hook context, or GLOBAL if not found + */ + public static ScriptContext byHookContext(String hookContext) { + if (hookContext == null || hookContext.isEmpty()) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (hookContext.equals(ctx.hookContext)) { + return ctx; + } + } + return GLOBAL; + } + + /** + * Get all registered script contexts. + * + * @return Unmodifiable collection of all registered contexts + */ + public static Collection values() { + return Collections.unmodifiableCollection(REGISTRY.values()); + } + + /** + * Check if a context with the given ID exists. + * + * @param id The context ID to check + * @return true if registered + */ + public static boolean exists(String id) { + return REGISTRY.containsKey(id); + } + + // ==================== OBJECT METHODS ==================== + + @Override + public String toString() { + return id + " -> " + namespaces; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ScriptContext)) return false; + ScriptContext other = (ScriptContext) obj; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/core/src/main/java/noppes/npcs/containers/ContainerNPCBankInterface.java b/core/src/main/java/noppes/npcs/containers/ContainerNPCBankInterface.java new file mode 100644 index 000000000..474dbcfac --- /dev/null +++ b/core/src/main/java/noppes/npcs/containers/ContainerNPCBankInterface.java @@ -0,0 +1,11 @@ +package noppes.npcs.containers; + +import noppes.npcs.api.entity.IPlayer; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Bank container interface. + */ +public class ContainerNPCBankInterface { + public ContainerNPCBankInterface(IPlayer player, int bankId, int slot) {} +} diff --git a/core/src/main/java/noppes/npcs/controllers/AnimationController.java b/core/src/main/java/noppes/npcs/controllers/AnimationController.java new file mode 100644 index 000000000..058a7d553 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -0,0 +1,610 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.util.Register; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.handler.IAnimationHandler; +import noppes.npcs.api.handler.data.IAnimation; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; + +public class AnimationController implements IAnimationHandler { + public HashMap animations; + private HashMap bootOrder; + + // Built-in animations loaded from assets folder (read-only, keyed by name) + public HashMap builtInAnimations = new HashMap<>(); + private static final String BUILTIN_ANIMATIONS_PATH = "/assets/customnpcs/cnpc_animations"; + private static final String BUILTIN_ANIMATIONS_RESOURCE = "assets/customnpcs/cnpc_animations"; + + public static AnimationController Instance = new AnimationController(); + private int lastUsedID = 0; + public CategoryManager categoryManager = new CategoryManager(); + + public LinkedHashMap registeredAnimations = new LinkedHashMap<>(); + + public AnimationController() { + Instance = this; + bootOrder = new HashMap<>(); + animations = new HashMap<>(); + } + + public static AnimationController getInstance() { + return Instance; + } + + public void load() { + bootOrder = new HashMap<>(); + animations = new HashMap<>(); + builtInAnimations = new HashMap<>(); + LogWriter.info("Loading animations..."); + loadBuiltInAnimations(); + loadRegisteredBuiltIns(); + readAnimationMap(); + loadAnimations(); + LogWriter.info("Done loading animations."); + } + + /** + * Load built-in animations from assets/customnpcs/cnpc_animations/ folder. + * These animations are read-only and accessed by name only (no IDs). + * Scans the folder directly instead of using a manifest. + */ + private void loadBuiltInAnimations() { + try { + URL resourceUrl = CustomNpcs.class.getResource(BUILTIN_ANIMATIONS_PATH); + if (resourceUrl == null) { + LogWriter.info("Built-in animations folder not found: " + BUILTIN_ANIMATIONS_PATH); + return; + } + + URI uri = resourceUrl.toURI(); + Path animationsPath; + + if (uri.getScheme().equals("jar")) { + // Running from JAR - need to create a FileSystem to access it + FileSystem fileSystem = null; + try { + fileSystem = FileSystems.getFileSystem(uri); + } catch (Exception e) { + fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + } + animationsPath = fileSystem.getPath(BUILTIN_ANIMATIONS_PATH); + } else { + // Running from file system (dev environment) + animationsPath = Paths.get(uri); + } + + // Scan the directory for .json files + try (Stream paths = Files.walk(animationsPath, 1)) { + paths.filter(path -> path.toString().endsWith(".json")) + .forEach(path -> { + String fileName = path.getFileName().toString(); + String animName = fileName.substring(0, fileName.length() - 5); // Remove .json + try { + loadBuiltInAnimation(animName); + } catch (Exception e) { + LogWriter.error("Error loading built-in animation: " + animName, e); + } + }); + } + + LogWriter.info("Loaded " + builtInAnimations.size() + " built-in animations."); + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + + public void loadBuiltInAnimation(Class modClass, String path, String animName) throws Exception { + String resourcePath = path + "/" + animName + ".json"; + try (InputStream stream = modClass.getResourceAsStream(resourcePath)) { + if (stream == null) { + LogWriter.error("Animation not found: " + animName.toLowerCase()); + return; + } + + // Read JSON content + StringBuilder content = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + reader.close(); + + // Parse NBT from JSON + INbt nbt = NBTJsonUtil.Convert(content.toString()); + + BuiltInAnimation animation = new BuiltInAnimation(animName); + animation.readFromNBT(nbt); + + // Store with lowercase key for case-insensitive lookup + builtInAnimations.put(animName.toLowerCase(), animation); + LogWriter.info("Registered animation: " + animName.toLowerCase()); + } + } + + /** + * Load a single built-in animation by name from assets folder. + */ + private void loadBuiltInAnimation(String animName) throws Exception { + loadBuiltInAnimation(CustomNpcs.class, BUILTIN_ANIMATIONS_PATH, animName); + } + + private void loadAnimations() { + animations.clear(); + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdir(); + } + + categoryManager.loadCategories(dir); + + // Load uncategorized animations (root level .json files) + loadAnimationsFromDir(dir, CategoryManager.UNCATEGORIZED_ID); + + // Load categorized animations (subdirectories) + for (Map.Entry entry : categoryManager.getCategories().entrySet()) { + File catDir = categoryManager.getCategoryDir(entry.getKey()); + loadAnimationsFromDir(catDir, entry.getKey()); + } + + saveAnimationMap(); + } + + private void loadAnimationsFromDir(File dir, int catId) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + Animation animation = new Animation(); + animation.readFromNBT(NBTJsonUtil.LoadFile(file)); + animation.name = file.getName().substring(0, file.getName().length() - 5); + + if (animation.id == -1) { + animation.id = getUnusedId(); + } + + int originalID = animation.id; + int setID = animation.id; + while (bootOrder.containsKey(setID) || animations.containsKey(setID)) { + if (bootOrder.containsKey(setID)) + if (bootOrder.get(setID).equals(animation.name)) + break; + + setID++; + } + + animation.id = setID; + if (originalID != setID) { + LogWriter.info("Found Animation ID Mismatch: " + animation.name + ", New ID: " + setID); + animation.save(); + } + + animations.put(animation.id, animation); + categoryManager.registerItem(animation.id, catId); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + } + + public void addAnimationRegister(String namespace, Register.Animations register) { + registeredAnimations.put(namespace, register); + } + + private void loadRegisteredBuiltIns() { + for (Register.Animations register : registeredAnimations.values()) { + register.register(); + } + } + + /** + * Loads built-in animations for client-side usage. + * On dedicated servers, AnimationController.load() never runs on the client JVM, + * so built-in animations would be missing for client-only systems like ability preview. + * This method loads just the built-in animations (from assets and registered addons) + * without touching user animations or IWorld save data. + */ + public void loadClientBuiltIns() { + loadBuiltInAnimations(); + loadRegisteredBuiltIns(); + } + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "animations"); + } + + public int getUnusedId() { + if (lastUsedID == 0) { + for (int catid : animations.keySet()) { + if (catid > lastUsedID) + lastUsedID = catid; + } + + } + lastUsedID++; + return lastUsedID; + } + + public IAnimation saveAnimation(IAnimation animation) { + // Reject saving built-in animations + if (animation instanceof BuiltInAnimation) { + LogWriter.info("Cannot save built-in animation: " + animation.getName()); + return animation; + } + + if (animation.getID() < 0) { + animation.setID(getUnusedId()); + while (hasName(animation.getName())) + animation.setName(animation.getName() + "_"); + } else { + Animation existing = animations.get(animation.getID()); + if (existing != null && !existing.name.equals(animation.getName())) + while (hasName(animation.getName())) + animation.setName(animation.getName() + "_"); + } + + animations.remove(animation.getID()); + animations.put(animation.getID(), (Animation) animation); + + saveAnimationMap(); + + // Save Animation File + File dir = categoryManager.getItemDir(animation.getID()); + if (!dir.exists()) + dir.mkdirs(); + + File file = new File(dir, animation.getName() + ".json_new"); + File file2 = new File(dir, animation.getName() + ".json"); + + try { + NBTJsonUtil.SaveFile(file, ((Animation) animation).writeToNBT()); + if (file2.exists()) + file2.delete(); + file.renameTo(file2); + } catch (Exception e) { + LogWriter.except(e); + } + return animations.get(animation.getID()); + } + + public boolean hasName(String newName) { + if (newName.trim().isEmpty()) + return true; + for (Animation animation : animations.values()) + if (animation.name.equals(newName)) + return true; + return false; + } + + public Animation cloneAnimation(int originalId) { + Animation original = animations.get(originalId); + if (original == null || original instanceof BuiltInAnimation) return null; + + int originalCatId = categoryManager.getItemCategory(originalId); + + Animation clone = new Animation(); + clone.readFromNBT(original.writeToNBT()); + clone.id = getUnusedId(); + + String name = clone.name; + while (hasName(name)) name += "_"; + clone.name = name; + + if (originalCatId > CategoryManager.UNCATEGORIZED_ID) { + categoryManager.registerItem(clone.id, originalCatId); + } + + saveAnimation(clone); + return clone; + } + + public void delete(String name) { + Animation delete = getAnimationFromName(name); + if (delete != null) { + delete(delete.getID()); + } + } + + public void delete(int id) { + if (id < 0 || !this.animations.containsKey(id)) + return; + + // Reject deleting built-in animations + Animation anim = this.animations.get(id); + if (anim instanceof BuiltInAnimation) { + LogWriter.info("Cannot delete built-in animation: " + anim.name); + return; + } + + Animation foundAnimation = this.animations.remove(id); + if (foundAnimation != null && foundAnimation.name != null) { + File dir = categoryManager.getItemDir(id); + File file = new File(dir, foundAnimation.name + ".json"); + if (file.exists()) { + file.delete(); + } + categoryManager.removeItem(id); + saveAnimationMap(); + } + } + + public boolean has(String name) { + return getAnimationFromName(name) != null; + } + + public IAnimation get(String name) { + return get(name, false); + } + + public IAnimation get(String name, boolean builtIn) { + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entry : animations.entrySet()) { + if (entry.getValue().name.equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + // Then check built-in animations + if (builtIn) { + BuiltInAnimation builtInAnim = builtInAnimations.get(name.toLowerCase()); + if (builtInAnim != null) { + return builtInAnim; + } + } + + return null; + } + + /** + * Get a user animation by ID. + * Built-in animations have no IDs and cannot be retrieved this way. + */ + public IAnimation get(int id) { + // Only user animations have valid IDs + if (id < 0) { + return null; + } + return this.animations.get(id); + } + + /** + * Get a built-in animation by name. + * + * @param name The animation name (case-insensitive) + * @return The built-in animation, or null if not found + */ + public BuiltInAnimation getBuiltInAnimation(String name) { + return builtInAnimations.get(name.toLowerCase()); + } + + public IAnimation[] getAnimations() { + ArrayList animations = new ArrayList<>(this.animations.values()); + return animations.toArray(new IAnimation[0]); + } + + /** + * Get all built-in animations. + */ + public IAnimation[] getBuiltInAnimations() { + return builtInAnimations.values().toArray(new IAnimation[0]); + } + + /** + * Get all animations (both built-in and user-created). + */ + public IAnimation[] getAllAnimations() { + ArrayList all = new ArrayList<>(); + all.addAll(builtInAnimations.values()); + all.addAll(animations.values()); + return all.toArray(new IAnimation[0]); + } + + /** + * Check if an animation name is a built-in animation. + * + * @param name The animation name to check + * @return true if this is a built-in animation + */ + public boolean isBuiltIn(String name) { + return builtInAnimations.containsKey(name.toLowerCase()); + } + + /** + * @deprecated Built-in animations no longer use IDs. Use isBuiltIn(String name) instead. + */ + @Deprecated + public boolean isBuiltIn(int id) { + return false; // Built-in animations have no IDs + } + + public Animation getAnimationFromName(String animation) { + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entryAnimation : animations.entrySet()) { + if (entryAnimation.getValue().name.equalsIgnoreCase(animation)) { + return entryAnimation.getValue(); + } + } + // Then check built-in animations + BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); + if (builtIn != null) { + return builtIn; + } + return null; + } + + /** + * Get names of all built-in animations. + */ + public String[] getBuiltInAnimationNames() { + return builtInAnimations.keySet().toArray(new String[0]); + } + + public String[] getNames() { + String[] names = new String[animations.size()]; + int i = 0; + for (Animation animation : animations.values()) { + names[i] = animation.name.toLowerCase(); + i++; + } + return names; + } + + + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + // ANIMATION MAP + public File getMapDir() { + File dir = CustomNpcs.getWorldSaveDirectory(); + if (!dir.exists()) + dir.mkdir(); + return dir; + } + + public void readAnimationMap() { + bootOrder.clear(); + + try { + File file = new File(getMapDir(), "animations.dat"); + if (file.exists()) { + loadAnimationMapFile(file); + } + } catch (Exception e) { + try { + File file = new File(getMapDir(), "animations.dat_old"); + if (file.exists()) { + loadAnimationMapFile(file); + } + } catch (Exception ignored) { + } + } + } + + public INbt writeMapNBT() { + INbt nbt = new INbt(); + INbtList animationList = new INbtList(); + for (Integer key : animations.keySet()) { + Animation animation = animations.get(key); + if (!animation.getName().isEmpty()) { + INbt animationCompound = new INbt(); + animationCompound.setString("Name", animation.getName()); + animationCompound.setInteger("ID", key); + + animationList.appendTag(animationCompound); + } + } + nbt.setTag("Animations", animationList); + return nbt; + } + + public void readMapNBT(INbt compound) { + INbtList list = compound.getTagList("Animations", 10); + if (list != null) { + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + String animationName = INbt.getString("Name"); + Integer key = INbt.getInteger("ID"); + bootOrder.put(key, animationName); + } + } + } + + private void loadAnimationMapFile(File file) throws IOException { + DataInputStream var1 = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); + readAnimationMap(var1); + var1.close(); + } + + public void readAnimationMap(DataInputStream stream) throws IOException { + INbt nbtCompound = CompressedStreamTools.read(stream); + this.readMapNBT(nbtCompound); + } + + public void saveAnimationMap() { + try { + File saveDir = getMapDir(); + File file = new File(saveDir, "animations.dat_new"); + File file1 = new File(saveDir, "animations.dat_old"); + File file2 = new File(saveDir, "animations.dat"); + NBTIO.writeCompressed(this.writeMapNBT(), new FileOutputStream(file)); + if (file1.exists()) { + file1.delete(); + } + file2.renameTo(file1); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + LogWriter.except(e); + } + } + + //////////////////////////////////////////////////////// + // CATEGORY HELPERS + //////////////////////////////////////////////////////// + + public Map getCategoryScrollData() { + return categoryManager.getCategoryScrollData(); + } + + public void moveItemToCategory(int itemId, int catId) { + Animation animation = animations.get(itemId); + if (animation == null) return; + categoryManager.moveItem(itemId, animation.name + ".json", catId); + saveAnimationMap(); + } + + public Map getItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, animations.keySet()); + for (int itemId : itemIds) { + Animation animation = animations.get(itemId); + if (animation != null) { + map.put(animation.name, animation.id); + } + } + return map; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java b/core/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java new file mode 100644 index 000000000..5db87b885 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java @@ -0,0 +1,57 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.util.VaultUtil; +import noppes.npcs.config.ConfigMarket; + +/** + * Server-side utility for syncing auction IConfiguration to clients. + * Handles Vault integration for currency name resolution. + *

+ * This class is SERVER-SIDE ONLY. Client reads via AuctionClientConfig. + */ +public class AuctionConfigSync { + + /** + * Write auction config to NBT for sending to client. + * Called on server side when building login packet. + *

+ * Currency name is resolved from Vault if enabled and available, + * otherwise falls back to ConfigMarket.CurrencyName. + */ + public static INbt writeToNBT(INbt compound) { + compound.setBoolean("AuctionEnabled", ConfigMarket.AuctionEnabled); + compound.setLong("ListingFee", ConfigMarket.ListingFee); + compound.setString("CurrencyName", getEffectiveCurrencyName()); + compound.setInteger("AuctionDurationHours", ConfigMarket.AuctionDurationHours); + compound.setDouble("MinBidIncrement", ConfigMarket.MinBidIncrementPercent); + compound.setInteger("MaxActiveListings", ConfigMarket.DefaultMaxTrades); + compound.setInteger("ClaimExpirationDays", ConfigMarket.ClaimExpirationDays); + return compound; + } + + /** + * Get the effective currency name for display. + * Uses Vault currency name if Vault is enabled and available, + * otherwise uses ConfigMarket.CurrencyName. + * + * @return The currency name to display + */ + public static String getEffectiveCurrencyName() { + if (ConfigMarket.UseVault && VaultUtil.isEnabled()) { + String vaultName = VaultUtil.getCurrencyNamePlural(); + if (vaultName != null && !vaultName.isEmpty()) { + return vaultName; + } + } + return ConfigMarket.CurrencyName; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/AuctionController.java b/core/src/main/java/noppes/npcs/controllers/AuctionController.java new file mode 100644 index 000000000..daf838f9d --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -0,0 +1,1927 @@ +package noppes.npcs.controllers; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.handler.IAuctionHandler; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionLogAction; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.constants.EnumNotificationType; +import noppes.npcs.controllers.data.AuctionBlacklist; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerDataScript; +import noppes.npcs.controllers.data.PlayerTradeData; +import noppes.npcs.CustomNpcs; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.EventHooks; +import noppes.npcs.LogWriter; +import noppes.npcs.util.CustomNPCsThreader; + +/** + * Main controller for the Auction system. + *

+ * Thread Safety: + * - Uses ConcurrentHashMap for all data stores - no locking required + * - Claims are stored in PlayerData, not here - reduces contention + * - Saves are performed asynchronously on the CNPC+ thread + *

+ * Performance: + * - Auction processing runs every 30 seconds (600 ticks) + * - Save interval is every 30 seconds (600 ticks) + * - Player-specific queries use indexed maps for fast lookups + */ +public class AuctionController implements IAuctionHandler { + public static AuctionController Instance; + + // Main data stores - listings only, claims are stored in PlayerData + private final Map listings = new ConcurrentHashMap<>(); + // Shared global claims for admin-managed auction actions + private final List globalClaims = new CopyOnWriteArrayList<>(); + + // ========================================= + // Player Indices for O(1) Lookups + // ========================================= + + /** + * Listings by seller UUID - for quick "my listings" lookups + */ + private final Map> playerListingIds = new ConcurrentHashMap<>(); + + /** + * Listings where player is current high bidder + */ + private final Map> playerBidIds = new ConcurrentHashMap<>(); + + /** + * Cached max trade slots per player - computed on login, cleared on logout + */ + private final Map playerMaxTradesCache = new ConcurrentHashMap<>(); + + // ========================================= + // Threading & State + // ========================================= + + /** + * Flag indicating data needs to be saved + */ + private final AtomicBoolean dirty = new AtomicBoolean(false); + + /** + * Flag to prevent concurrent saves + */ + private final AtomicBoolean saving = new AtomicBoolean(false); + + private String filePath = ""; + private int tickCounter = 0; + + /** + * Process auctions and save every 30 seconds (600 ticks) + */ + private static final int TICK_INTERVAL = 600; + + public AuctionController() { + Instance = this; + AuctionBlacklist.reload(); + load(); + } + + public static AuctionController getInstance() { + if (Instance == null || needsNewInstance()) { + Instance = new AuctionController(); + } + return Instance; + } + + private static boolean needsNewInstance() { + if (Instance == null) return true; + File file = CustomNpcs.getWorldSaveDirectory(); + if (file == null) return false; + return !Instance.filePath.equals(file.getAbsolutePath()); + } + + // ========================================= + // Tick Processing + // ========================================= + + public void onServerTick() { + if (!ConfigMarket.AuctionEnabled) return; + + tickCounter++; + + // Process auctions and save every 30 seconds + if (tickCounter >= TICK_INTERVAL) { + tickCounter = 0; + processEndedAuctions(); + if (dirty.get()) { + saveAsync(); + } + } + } + + private void processEndedAuctions() { + boolean changed = false; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { + endAuction(listing); + changed = true; + } + } + if (changed) { + markDirty(); + } + } + + private void endAuction(AuctionListing listing) { + synchronized (listing) { + if (listing.status != EnumAuctionStatus.ACTIVE) return; + + listing.status = EnumAuctionStatus.ENDED; + boolean isGlobalListing = listing.isGlobalListing; + + // Remove from seller's active listings + if (!isGlobalListing && listing.sellerUUID != null) { + removeFromPlayerListings(listing.sellerUUID, listing.id); + } + + // Remove from bidder's active bids if any + if (listing.highBidderUUID != null) { + removeFromPlayerBids(listing.highBidderUUID, listing.id); + } + + // Get item display name safely + String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; + + if (listing.hasBids()) { + // Auction sold - create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); + addClaimToPlayer(listing.highBidderUUID, itemClaim); + + long saleAmount = listing.currentBid; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + itemDisplayName, listing.highBidderName); + if (isGlobalListing) { + addGlobalClaim(currencyClaim); + } else { + addClaimToPlayer(listing.sellerUUID, currencyClaim); + } + + // Notifications + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, + "You won the auction for " + itemDisplayName + "!"); + if (!isGlobalListing && listing.sellerUUID != null) { + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + } + + logAuction(EnumAuctionLogAction.SOLD, listing.sellerName, itemDisplayName, saleAmount, + "Winner: " + listing.highBidderName + ", Tax: " + tax + (isGlobalListing ? ", Global listing" : "")); + } else { + // No bids - return item to seller/global claims + AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + if (isGlobalListing) { + addGlobalClaim(returnClaim); + } else { + addClaimToPlayer(listing.sellerUUID, returnClaim); + if (listing.sellerUUID != null) { + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, + "Your auction for " + itemDisplayName + " expired with no bids."); + } + } + + logAuction(EnumAuctionLogAction.EXPIRED, listing.sellerName, itemDisplayName, listing.startingPrice, + "No bids" + (isGlobalListing ? ", Global listing" : "")); + } + } + } + + // ========================================= + // Index Management + // ========================================= + + private void addToPlayerListings(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; + playerListingIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); + } + + private void removeFromPlayerListings(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; + Set ids = playerListingIds.get(playerUUID); + if (ids != null) { + ids.remove(listingId); + if (ids.isEmpty()) { + playerListingIds.remove(playerUUID); + } + } + } + + private void addToPlayerBids(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; + playerBidIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); + } + + private void removeFromPlayerBids(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; + Set ids = playerBidIds.get(playerUUID); + if (ids != null) { + ids.remove(listingId); + if (ids.isEmpty()) { + playerBidIds.remove(playerUUID); + } + } + } + + /** + * Add a claim to a player's trade data. + * If player is online, adds directly. If offline, loads their data file and updates it. + */ + private void addClaimToPlayer(UUID playerUUID, AuctionClaim claim) { + if (playerUUID == null || claim == null) return; + + PlayerData data = PlayerDataController.Instance.getData(playerUUID); + if (data != null) { + data.tradeData.addClaim(claim); + data.updateClient = true; + data.save(); + } + } + + /** + * Add a claim to the global admin claims pool. + */ + private void addGlobalClaim(AuctionClaim claim) { + if (claim == null || claim.claimed) return; + + // Global claims are not tied to a single player. + claim.playerUUID = null; + claim.playerName = "Global"; + synchronized (globalClaims) { + // Prevent duplicate claim IDs + for (AuctionClaim existing : globalClaims) { + if (existing.id.equals(claim.id)) { + return; + } + } + globalClaims.add(claim); + } + markDirty(); + } + + private List getGlobalClaimsSnapshot() { + List result = new ArrayList<>(); + for (AuctionClaim claim : globalClaims) { + if (claim != null && !claim.claimed) { + result.add(claim); + } + } + return result; + } + + private AuctionClaim getGlobalClaimInternal(String claimId) { + if (claimId == null || claimId.isEmpty()) return null; + synchronized (globalClaims) { + for (AuctionClaim claim : globalClaims) { + if (claim != null && !claim.claimed && claimId.equals(claim.id)) { + return claim; + } + } + } + return null; + } + + private boolean claimAndRemoveGlobal(String claimId) { + synchronized (globalClaims) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) return false; + claim.claimed = true; + return globalClaims.remove(claim); + } + } + + /** + * Full list of unclaimed global claims sorted newest-first. + */ + public List getAllGlobalClaims() { + List snapshot = getGlobalClaimsSnapshot(); + snapshot.sort((a, b) -> Long.compare(b.createdTime, a.createdTime)); + return snapshot; + } + + /** + * Full list of active global listings sorted newest-first. + */ + public List getActiveGlobalListings() { + List result = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing == null) continue; + if (!listing.isGlobalListing) continue; + if (!listing.isActive()) continue; + result.add(listing); + } + result.sort((a, b) -> Long.compare(b.createdTime, a.createdTime)); + return result; + } + + // ========================================= + // Listing Management + // ========================================= + + public String createListing(IPlayer player, IItemStack item, long startingPrice, long buyoutPrice) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + + // Check blacklist (with bypass for admins) + if (AuctionBlacklist.isBlacklistedForPlayer(item, player)) { + return "This item cannot be listed on the Auction House."; + } + + // Validate prices + if (startingPrice <= 0) { + return "Starting price must be positive."; + } + if (startingPrice < ConfigMarket.MinimumListingPrice) { + return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; + } + if (startingPrice > ConfigMarket.MaxBalance) { + return "Starting price exceeds maximum allowed value."; + } + + // Validate buyout price if set + if (buyoutPrice < 0) { + return "Buyout price cannot be negative."; + } + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return "Buyout price must be higher than starting price."; + } + if (buyoutPrice > ConfigMarket.MaxBalance) { + return "Buyout price exceeds maximum allowed value."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + // Check trade slot limit (listings + bids + claims) + int currentTrades = getPlayerTradeSlotCount(player); + int maxTrades = getMaxTradesForPlayer(player); + if (currentTrades >= maxTrades) { + return "You have reached your maximum trade slots (" + maxTrades + ")."; + } + + // Check listing fee + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + long fee = ConfigMarket.ListingFee; + if (fee > 0 && !currency.canAfford(fee)) { + return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; + } + + // Fire auction create event (cancelable) + PlayerDataScript createHandler = ScriptController.Instance.getPlayerScripts(player); + IPlayer iCreator = (IPlayer) NpcAPI.Instance().getIEntity(player); + AuctionEvent.CreateEvent createEvent = new AuctionEvent.CreateEvent( + iCreator, NpcAPI.Instance().getIItemStack(item), startingPrice, buyoutPrice); + if (EventHooks.onAuctionCreate(createHandler, createEvent)) { + return "Auction creation was cancelled."; + } + + // Deduct fee + if (fee > 0 && !currency.withdraw(fee)) { + return "Failed to deduct listing fee."; + } + + // Calculate duration + long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L; + + // Create listing + AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs); + listings.put(listing.id, listing); + addToPlayerListings(playerUUID, listing.id); + + markDirty(); + + logAuction(EnumAuctionLogAction.CREATED, playerName, item.getDisplayName(), startingPrice, + "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); + + return null; // Success + } + + /** + * Create a fake/global listing managed through the global auction admin GUI. + * Global listings do not consume player currency and route proceeds to global claims. + */ + public String createGlobalListing(IPlayer admin, String sellerName, IItemStack item, long startingPrice, long buyoutPrice, int durationHours) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + if (admin == null) { + return "Admin is missing."; + } + if (item == null) { + return "No item provided."; + } + + // Validate prices + if (startingPrice <= 0) { + return "Starting price must be positive."; + } + if (startingPrice < ConfigMarket.MinimumListingPrice) { + return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; + } + if (buyoutPrice < 0) { + return "Buyout price cannot be negative."; + } + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return "Buyout price must be higher than starting price."; + } + + String cleanSeller = sellerName != null ? sellerName.trim() : ""; + if (cleanSeller.isEmpty()) { + cleanSeller = "Server"; + } + + int hours = durationHours > 0 ? durationHours : ConfigMarket.AuctionDurationHours; + long durationMs = hours * 60L * 60L * 1000L; + + // Global listings do not belong to a player UUID, only to the global pool. + AuctionListing listing = new AuctionListing(null, cleanSeller, item, startingPrice, buyoutPrice, durationMs); + listing.isGlobalListing = true; + listings.put(listing.id, listing); + markDirty(); + + logAuction(EnumAuctionLogAction.CREATED, admin.getCommandSenderName(), item.getDisplayName(), startingPrice, + "Global listing, Seller: " + cleanSeller + ", Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", DurationHours: " + hours); + return null; + } + + public String cancelListing(String listingId, IPlayer player, boolean isAdmin) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + if (isAdmin && listing.isGlobalListing) { + return adminStopListingToGlobal(listingId, player, true); + } + + synchronized (listing) { + if (!listing.status.canCancel()) { + return "This auction cannot be cancelled."; + } + + UUID playerUUID = player.getUniqueID(); + if (!isAdmin && !listing.isSeller(playerUUID)) { + return "You can only cancel your own listings."; + } + + // Fire auction cancel event (cancelable) + PlayerDataScript cancelHandler = ScriptController.Instance.getPlayerScripts(player); + IPlayer iCanceller = (IPlayer) NpcAPI.Instance().getIEntity(player); + AuctionEvent.CancelEvent cancelEvent = new AuctionEvent.CancelEvent(iCanceller, listing, isAdmin); + if (EventHooks.onAuctionCancel(cancelHandler, cancelEvent)) { + return "Auction cancellation was prevented."; + } + + // Handle penalty if there are bids + if (listing.hasBids()) { + long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); + + // Refund bidder (no item - can't rebid on cancelled auction) + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); + + // Return item to seller + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + // Deduct cancellation penalty from seller + if (penalty > 0) { + PlayerData sellerData = PlayerDataController.Instance.getData(listing.sellerUUID); + if (sellerData != null) { + if (!sellerData.tradeData.withdraw(penalty)) { + // Withdraw whatever they have if they can't cover the full penalty + long available = sellerData.tradeData.getBalance(); + if (available > 0) { + sellerData.tradeData.withdraw(available); + } + } + sellerData.save(); + } + } + + logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.currentBid, + "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } else { + // No bids - just return item + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, + "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.CANCELLED; + markDirty(); + } + + return null; // Success + } + + // ========================================= + // Bidding + // ========================================= + + public String placeBid(String listingId, IPlayer player, long bidAmount) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + + // Validate bid amount + if (bidAmount <= 0) { + return "Bid amount must be positive."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + synchronized (listing) { + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + // CRITICAL: Prevent bidding on own items + if (listing.isSeller(playerUUID)) { + return "You cannot bid on your own auction."; + } + + // Prevent rebidding if already highest bidder + if (listing.isHighBidder(playerUUID)) { + return "You are already the highest bidder. Use buyout if you want to purchase immediately."; + } + + // Check minimum bid + long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); + if (bidAmount < minBid) { + return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; + } + + // Prevent bidding at or above buyout price + if (listing.hasBuyout() && bidAmount >= listing.buyoutPrice) { + return "Your bid meets or exceeds the buyout price. Use Buy Now instead."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // Store previous bidder info + UUID previousBidder = listing.highBidderUUID; + long previousBid = listing.currentBid; + + // Full bid amount charged since we don't allow rebidding + long amountToCharge = bidAmount; + + if (!currency.canAfford(amountToCharge)) { + return "You cannot afford this bid."; + } + + // Fire auction bid event (cancelable) + PlayerDataScript bidHandler = ScriptController.Instance.getPlayerScripts(player); + IPlayer iBidder = (IPlayer) NpcAPI.Instance().getIEntity(player); + AuctionEvent.BidEvent bidEvent = new AuctionEvent.BidEvent(iBidder, listing, bidAmount); + if (EventHooks.onAuctionBid(bidHandler, bidEvent)) { + return "Bid was cancelled."; + } + + // Deduct bid from new bidder FIRST + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct bid amount."; + } + + // Refund previous bidder (include item for rebid option) + if (listing.hasBids() && previousBidder != null) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + previousBidder, listing.highBidderName, listing.id, previousBid, + listing.item.getDisplayName(), playerName, listing.item); + addClaimToPlayer(previousBidder, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(previousBidder, listing.id); + + sendNotificationToPlayer(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id, + "You were outbid on " + listing.item.getDisplayName() + "!"); + } + + // Update listing + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = bidAmount; + listing.bidCount++; + + // Add to new bidder's active bids + addToPlayerBids(playerUUID, listing.id); + + // Snipe protection (must be inside sync to prevent race with endAuction) + listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); + } + + markDirty(); + + logAuction(EnumAuctionLogAction.BID, playerName, listing.item.getDisplayName(), bidAmount, + "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); + + return null; // Success + } + + public String buyout(String listingId, IPlayer player) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + synchronized (listing) { + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + if (!listing.hasBuyout()) { + return "This auction does not have a buyout price."; + } + + // CRITICAL: Prevent buying own items + if (listing.isSeller(playerUUID)) { + return "You cannot buy your own auction."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // If buyer is already the high bidder, only charge the difference + boolean isCurrentBidder = listing.isHighBidder(playerUUID); + long amountToCharge = isCurrentBidder + ? (listing.buyoutPrice - listing.currentBid) + : listing.buyoutPrice; + + if (!currency.canAfford(amountToCharge)) { + if (isCurrentBidder) { + return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout."; + } + return "You cannot afford the buyout price."; + } + + // Fire auction buyout event (cancelable) + PlayerDataScript buyoutHandler = ScriptController.Instance.getPlayerScripts(player); + IPlayer iBuyer = (IPlayer) NpcAPI.Instance().getIEntity(player); + AuctionEvent.BuyoutEvent buyoutEvent = new AuctionEvent.BuyoutEvent(iBuyer, listing); + if (EventHooks.onAuctionBuyout(buyoutHandler, buyoutEvent)) { + return "Buyout was cancelled."; + } + + // Deduct buyout amount + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct buyout amount."; + } + + // Refund previous bidder if any AND they're not the buyer (no item - auction ended via buyout) + if (listing.hasBids() && listing.highBidderUUID != null && !isCurrentBidder) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), playerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded."); + } else if (isCurrentBidder) { + // Current bidder is buying out - just remove from their active bids + removeFromPlayerBids(playerUUID, listing.id); + } + + // Create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); + addClaimToPlayer(playerUUID, itemClaim); + + long saleAmount = listing.buyoutPrice; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + listing.item.getDisplayName(), playerName); + if (listing.isGlobalListing) { + addGlobalClaim(currencyClaim); + } else { + addClaimToPlayer(listing.sellerUUID, currencyClaim); + } + + // Notifications + if (!listing.isGlobalListing && listing.sellerUUID != null) { + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + } + + // Remove from seller's active listings + if (!listing.isGlobalListing && listing.sellerUUID != null) { + removeFromPlayerListings(listing.sellerUUID, listing.id); + } + + listing.status = EnumAuctionStatus.ENDED; + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = listing.buyoutPrice; + + markDirty(); + + logAuction(EnumAuctionLogAction.BUYOUT, playerName, listing.item.getDisplayName(), listing.buyoutPrice, + "Seller: " + listing.sellerName + ", Tax: " + tax + (listing.isGlobalListing ? ", Global listing" : "")); + } + + return null; // Success + } + + // ========================================= + // Claims - Now stored in PlayerData + // ========================================= + + /** + * Get player claims from PlayerData. + */ + public List getPlayerClaims(IPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return new ArrayList<>(); + } + return playerData.tradeData.getClaimsList(); + } + + /** + * Get player claims by UUID (for login notification). + */ + public List getPlayerClaims(UUID playerUUID) { + IPlayer onlinePlayer = (IPlayer) PlayerDataController.getPlayerFromUUID(playerUUID); + if (onlinePlayer != null) { + return getPlayerClaims(onlinePlayer); + } + return new ArrayList<>(); + } + + /** + * Paginated global claims used by the global auction admin GUI. + */ + public List getGlobalClaims(int page, int pageSize) { + List snapshot = getGlobalClaimsSnapshot(); + snapshot.sort((a, b) -> Long.compare(b.createdTime, a.createdTime)); + + int safePage = Math.max(0, page); + int safePageSize = Math.max(1, pageSize); + int start = safePage * safePageSize; + if (start >= snapshot.size()) { + return new ArrayList<>(); + } + int end = Math.min(start + safePageSize, snapshot.size()); + return new ArrayList<>(snapshot.subList(start, end)); + } + + public int getTotalGlobalClaims() { + return getGlobalClaimsSnapshot().size(); + } + + public boolean hasGlobalClaims() { + return getTotalGlobalClaims() > 0; + } + + public String claimGlobalItem(String claimId, IPlayer player) { + String listingId; + String itemName; + synchronized (globalClaims) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) { + return "Global claim not found."; + } + if (!claim.type.isItem()) { + return "This global claim is not an item claim."; + } + if (claim.item == null) { + return "Item data is missing."; + } + + // Check inventory space before adding (addItemStackToInventory mutates even on partial add) + if (!canFitInInventory(player, claim.item)) { + return "Not enough inventory space to claim this item."; + } + + // Add to inventory (safe - we verified space above) + player.inventory.addItemStackToInventory(claim.item.copy()); + + listingId = claim.listingId; + itemName = claim.item.getDisplayName(); + claimAndRemoveGlobal(claim.id); + } + cleanupListingIfComplete(listingId); + markDirty(); + + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), itemName, 0, + "Global item claim"); + return null; + } + + /** + * Global currency claims are acknowledged/cleared and do not alter player balances. + */ + public String claimGlobalCurrency(String claimId, IPlayer player) { + String listingId; + long currency; + synchronized (globalClaims) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) { + return "Global claim not found."; + } + if (!claim.type.isCurrency()) { + return "This global claim is not a currency claim."; + } + + listingId = claim.listingId; + currency = claim.currency; + claimAndRemoveGlobal(claim.id); + } + cleanupListingIfComplete(listingId); + markDirty(); + + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, currency, + "Global currency claim acknowledged (no balance change)"); + return null; + } + + public String claimItem(String claimId, IPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isItem()) { + return "This is not an item claim."; + } + + if (claim.item == null) { + return "Item data is missing."; + } + + // Fire auction claim event (cancelable) + PlayerDataScript itemClaimHandler = ScriptController.Instance.getPlayerScripts(player); + IPlayer iItemClaimer = (IPlayer) NpcAPI.Instance().getIEntity(player); + AuctionEvent.ClaimEvent itemClaimEvent = new AuctionEvent.ClaimEvent(iItemClaimer, claim); + if (EventHooks.onAuctionClaim(itemClaimHandler, itemClaimEvent)) { + return "Claim was prevented."; + } + + // Check inventory space before adding (addItemStackToInventory mutates even on partial add) + if (!canFitInInventory(player, claim.item)) { + return "Not enough inventory space to claim this item."; + } + + // Add to inventory (safe - we verified space above) + player.inventory.addItemStackToInventory(claim.item.copy()); + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); + + return null; // Success + } + + public String claimCurrency(String claimId, IPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isCurrency()) { + return "This is not a currency claim."; + } + + // Fire auction claim event (cancelable) + PlayerDataScript currencyClaimHandler = ScriptController.Instance.getPlayerScripts(player); + IPlayer iCurrencyClaimer = (IPlayer) NpcAPI.Instance().getIEntity(player); + AuctionEvent.ClaimEvent currencyClaimEvent = new AuctionEvent.ClaimEvent(iCurrencyClaimer, claim); + if (EventHooks.onAuctionClaim(currencyClaimHandler, currencyClaimEvent)) { + return "Claim was prevented."; + } + + if (!playerData.tradeData.deposit(claim.currency)) { + return "Failed to deposit currency."; + } + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); + + return null; // Success + } + + /** + * Check if a player's inventory can fit the entire IItemStack without partial adds. + * Unlike addItemStackToInventory, this does NOT mutate the inventory. + */ + private boolean canFitInInventory(IPlayer player, IItemStack stack) { + int remaining = stack.stackSize; + int maxStack = stack.getMaxStackSize(); + IItemStack[] mainInventory = player.inventory.mainInventory; + + for (int i = 0; i < mainInventory.length; i++) { + if (remaining <= 0) return true; + + IItemStack slot = mainInventory[i]; + if (slot == null) { + remaining -= Math.min(remaining, maxStack); + } else if (slot.getItem() == stack.getItem() + && slot.getItemDamage() == stack.getItemDamage() + && IItemStack.areItemStackTagsEqual(slot, stack)) { + int spaceInSlot = maxStack - slot.stackSize; + if (spaceInSlot > 0) { + remaining -= Math.min(remaining, spaceInSlot); + } + } + } + + return remaining <= 0; + } + + /** + * Clean up a listing if it's ended and no longer needed. + * Now just removes ended listings after some delay since claims are in PlayerData. + */ + private void cleanupListingIfComplete(String listingId) { + if (listingId == null || listingId.isEmpty()) return; + + AuctionListing listing = listings.get(listingId); + if (listing == null) return; + + // Only cleanup ended/cancelled listings + if (listing.status == EnumAuctionStatus.ACTIVE) return; + + // Remove ended listings (claims are now in PlayerData, not here) + listings.remove(listingId); + markDirty(); + } + + public int claimAll(IPlayer player) { + int claimedCount = 0; + List playerClaims = getPlayerClaims(player); + + for (AuctionClaim claim : playerClaims) { + String result; + if (claim.type.isItem()) { + result = claimItem(claim.id, player); + } else { + result = claimCurrency(claim.id, player); + } + if (result == null) { + claimedCount++; + } + } + + return claimedCount; + } + + // ========================================= + // Queries - Using Indexed Lookups + // ========================================= + + public List getActiveListings(AuctionFilter filter, int page, int pageSize) { + List result = new ArrayList<>(); + + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (listing.isExpired()) continue; + + // Apply search filter (searches item name AND seller name) + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; + } + + result.add(listing); + } + + // Sort + sortListings(result, filter.sortBy); + + // Paginate + int start = page * pageSize; + int end = Math.min(start + pageSize, result.size()); + if (start >= result.size()) { + return new ArrayList<>(); + } + return new ArrayList<>(result.subList(start, end)); + } + + private void sortListings(List list, EnumAuctionSort sortBy) { + Comparator comparator; + switch (sortBy) { + case ENDING_SOON: + comparator = Comparator.comparingLong(AuctionListing::getTimeRemaining); + break; + case PRICE_LOW: + comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice); + break; + case PRICE_HIGH: + comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice).reversed(); + break; + case MOST_BIDS: + comparator = Comparator.comparingInt(l -> -l.bidCount); + break; + case NEWEST: + default: + comparator = Comparator.comparingLong(l -> -l.createdTime); + break; + } + list.sort(comparator); + } + + public int getTotalActiveListings(AuctionFilter filter) { + int count = 0; + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (listing.isExpired()) continue; + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; + } + count++; + } + return count; + } + + @Override + public AuctionListing getListing(String listingId) { + return listings.get(listingId); + } + + /** + * Get a player's current currency balance. + */ + public long getPlayerBalance(IPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null || playerData.tradeData == null) { + return 0; + } + return playerData.tradeData.getBalance(); + } + + /** + * Get player listing count using index + */ + public int getPlayerListingCount(UUID playerUUID) { + Set ids = playerListingIds.get(playerUUID); + return ids != null ? ids.size() : 0; + } + + /** + * Get player's active listings using indexed lookup + */ + public List getPlayerActiveListings(UUID playerUUID) { + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds == null || listingIds.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (String listingId : listingIds) { + AuctionListing listing = listings.get(listingId); + if (listing != null && listing.status == EnumAuctionStatus.ACTIVE) { + result.add(listing); + } + } + return result; + } + + /** + * Get all active auctions where the player is currently the highest bidder. + * Uses indexed lookup for O(1) access to player's bid set. + */ + public List getPlayerActiveBids(UUID playerUUID) { + Set bidListingIds = playerBidIds.get(playerUUID); + if (bidListingIds == null || bidListingIds.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (String listingId : bidListingIds) { + AuctionListing listing = listings.get(listingId); + if (listing != null && listing.status == EnumAuctionStatus.ACTIVE && + listing.highBidderUUID != null && listing.highBidderUUID.equals(playerUUID)) { + result.add(listing); + } + } + return result; + } + + /** + * Get the total number of trade slots used by a player. + * Includes active listings, active bids, and pending claims. + */ + public int getPlayerTradeSlotCount(IPlayer player) { + UUID playerUUID = player.getUniqueID(); + int count = 0; + // Active listings + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds != null) count += listingIds.size(); + // Active bids + Set bidIds = playerBidIds.get(playerUUID); + if (bidIds != null) count += bidIds.size(); + // Pending claims (from PlayerData) + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + count += playerData.tradeData.getClaimCount(); + } + return count; + } + + /** + * Get the total number of trade slots used by a player (UUID version). + * For offline players, only returns listings + bids (claims are in their PlayerData). + */ + public int getPlayerTradeSlotCount(UUID playerUUID) { + int count = 0; + // Active listings + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds != null) count += listingIds.size(); + // Active bids + Set bidIds = playerBidIds.get(playerUUID); + if (bidIds != null) count += bidIds.size(); + // Can't get claims for offline player without loading their data + return count; + } + + /** + * Get the maximum trade slots for a player. + * Uses cached value if available, otherwise computes from permissions. + */ + public int getMaxTradesForPlayer(IPlayer player) { + UUID playerUUID = player.getUniqueID(); + + // Check cache first + Integer cached = playerMaxTradesCache.get(playerUUID); + if (cached != null) { + return cached; + } + + // Compute and cache + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + return maxTrades; + } + + /** + * Compute max trade slots from permissions. + * Checks for customnpcs.auction.trades.X permissions where X is the slot count. + * Uses the highest permission found, or DefaultMaxTrades if no permissions or lower. + * Capped at MAX_TRADE_SLOTS (45). + */ + private int computeMaxTradesForPlayer(IPlayer player) { + // Check for unlimited permission + if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.auction.trades.*")) { + return ConfigMarket.MAX_TRADE_SLOTS; + } + + // Find highest allowed slot count from permissions + int highestAllowed = 0; + for (int i = 1; i <= ConfigMarket.MAX_TRADE_SLOTS; i++) { + String perm = "customnpcs.auction.trades." + i; + if (CustomNpcsPermissions.hasCustomPermission(player, perm)) { + highestAllowed = i; + } + } + + // Use default if no permissions found or permission is lower than default + if (highestAllowed == 0 || highestAllowed < ConfigMarket.DefaultMaxTrades) { + highestAllowed = ConfigMarket.DefaultMaxTrades; + } + + // Cap at maximum + return Math.min(highestAllowed, ConfigMarket.MAX_TRADE_SLOTS); + } + + /** + * Refresh a player's cached max trades (call when permissions change). + */ + public void refreshPlayerMaxTrades(IPlayer player) { + UUID playerUUID = player.getUniqueID(); + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + } + + /** + * Clear a player's cached max trades (call on logout). + */ + public void clearPlayerMaxTradesCache(UUID playerUUID) { + playerMaxTradesCache.remove(playerUUID); + } + + // ========================================= + // Notifications + // ========================================= + + /** + * Send a notification to a player. + * If player is online, sends immediately. Otherwise, discards (claims will notify on login). + */ + private void sendNotificationToPlayer(UUID playerUUID, EnumNotificationType type, String listingId, String message) { + // Try to send immediately if player is online + IPlayer player = (IPlayer) PlayerDataController.getPlayerFromUUID(playerUUID); + if (player != null) { + sendNotificationMessage(player, type, message); + } + // If offline, no need to store - they'll get notified about claims on login + } + + /** + * Send a notification chat message to a player. + */ + private void sendNotificationMessage(IPlayer player, EnumNotificationType type, String message) { + EnumChatFormatting color; + switch (type) { + case AUCTION_WON: + case AUCTION_SOLD: + color = TextFormatting.GREEN; + break; + case AUCTION_OUTBID: + color = TextFormatting.YELLOW; + break; + case AUCTION_EXPIRED: + color = TextFormatting.RED; + break; + default: + color = TextFormatting.GRAY; + } + player.addChatMessage(new ITextComponent( + TextFormatting.GOLD + "[Auction] " + color + message)); + } + + public void onPlayerLogin(IPlayer player) { + // Cache player's max trades permission + refreshPlayerMaxTrades(player); + + // Process expired claims for this player + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + int expired = playerData.tradeData.processExpiredClaims(); + if (expired > 0) { + playerData.updateClient = true; + playerData.save(); + logAuction(EnumAuctionLogAction.CLAIM_EXPIRED, player.getCommandSenderName(), "Multiple", expired, + expired + " claims expired on login"); + } + } + + // Check for pending claims and notify + List claims = getPlayerClaims(player); + if (!claims.isEmpty()) { + int itemClaims = 0; + int currencyClaims = 0; + long totalCurrency = 0; + + for (AuctionClaim claim : claims) { + if (claim.type.isItem()) { + itemClaims++; + } else { + currencyClaims++; + totalCurrency += claim.currency; + } + } + + StringBuilder msg = new StringBuilder(); + msg.append(TextFormatting.GOLD).append("[Auction] "); + msg.append(TextFormatting.YELLOW).append("You have "); + + if (itemClaims > 0 && currencyClaims > 0) { + msg.append(itemClaims).append(" item(s) and "); + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); + } else if (itemClaims > 0) { + msg.append(itemClaims).append(" item(s)"); + } else { + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); + } + msg.append(" to claim!"); + + player.addChatMessage(new ITextComponent(msg.toString())); + } + + // Notify admins with permission about pending global claims. + if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.global.auction")) { + List global = getGlobalClaimsSnapshot(); + if (!global.isEmpty()) { + int itemClaims = 0; + int currencyClaims = 0; + for (AuctionClaim claim : global) { + if (claim.type.isItem()) { + itemClaims++; + } else { + currencyClaims++; + } + } + + StringBuilder msg = new StringBuilder(); + msg.append(TextFormatting.GOLD).append("[Auction] "); + msg.append(TextFormatting.AQUA).append("Global claims available: "); + msg.append(TextFormatting.GREEN).append(itemClaims).append(" item claim(s)"); + msg.append(TextFormatting.GRAY).append(", "); + msg.append(TextFormatting.YELLOW).append(currencyClaims).append(" currency claim(s)"); + player.addChatMessage(new ITextComponent(msg.toString())); + } + } + } + + public void onPlayerLogout(UUID playerUUID) { + // Clear cached permission + clearPlayerMaxTradesCache(playerUUID); + } + + // ========================================= + // Logging + // ========================================= + + private void logAuction(EnumAuctionLogAction action, String playerName, String itemName, long amount, String details) { + if (!action.shouldLog()) return; + + String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s", + action.name(), playerName, itemName, amount, details); + LogWriter.info(logMessage); + } + + // ========================================= + // Save / Load - Async on CNPC+ Thread + // ========================================= + + /** + * Mark data as dirty - will be saved on next save cycle + */ + private void markDirty() { + dirty.set(true); + } + + /** + * Save synchronously - used for server shutdown + */ + public void save() { + if (!dirty.compareAndSet(true, false)) { + return; // Nothing to save + } + saveInternal(); + } + + /** + * Save asynchronously on CNPC+ thread + */ + public void saveAsync() { + if (!dirty.compareAndSet(true, false)) { + return; // Nothing to save + } + + if (!saving.compareAndSet(false, true)) { + // Another save is in progress, mark dirty again + dirty.set(true); + return; + } + + // Take a snapshot of the data for async save + final INbt compound = writeToNBT(new INbt()); + + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + saveToFile(compound); + } catch (Exception e) { + LogWriter.error("Error saving auction data asynchronously", e); + // Mark dirty again so we retry + dirty.set(true); + } finally { + saving.set(false); + } + }); + } + + /** + * Internal synchronous save + */ + private void saveInternal() { + try { + INbt compound = writeToNBT(new INbt()); + saveToFile(compound); + } catch (Exception e) { + LogWriter.error("Error saving auction data", e); + } + } + + private void saveToFile(INbt compound) { + try { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) return; + + File fileNew = new File(saveDir, "auction.dat_new"); + File fileOld = new File(saveDir, "auction.dat_old"); + File fileCurrent = new File(saveDir, "auction.dat"); + + try (FileOutputStream fos = new FileOutputStream(fileNew)) { + NBTIO.writeCompressed(compound, fos); + } + + // Backup rotation + if (fileOld.exists()) fileOld.delete(); + if (fileCurrent.exists()) fileCurrent.renameTo(fileOld); + if (fileCurrent.exists()) fileCurrent.delete(); + fileNew.renameTo(fileCurrent); + if (fileNew.exists()) fileNew.delete(); + + } catch (Exception e) { + LogWriter.error("Error saving auction data to file", e); + } + } + + public void load() { + listings.clear(); + globalClaims.clear(); + playerListingIds.clear(); + playerBidIds.clear(); + + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) return; + filePath = saveDir.getAbsolutePath(); + + // Try current file + try { + File file = new File(saveDir, "auction.dat"); + if (file.exists()) { + loadFromFile(file); + rebuildIndices(); + return; + } + } catch (Exception e) { + LogWriter.error("Error loading auction.dat, trying backup", e); + } + + // Try backup file + try { + File file = new File(saveDir, "auction.dat_old"); + if (file.exists()) { + loadFromFile(file); + rebuildIndices(); + LogWriter.info("Loaded auction data from backup file"); + } + } catch (Exception e) { + LogWriter.error("Error loading auction.dat_old", e); + } + } + + /** + * Rebuild all indices from loaded data + */ + private void rebuildIndices() { + playerListingIds.clear(); + playerBidIds.clear(); + + // Index listings + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE) { + if (!listing.isGlobalListing && listing.sellerUUID != null) { + addToPlayerListings(listing.sellerUUID, listing.id); + } + if (listing.highBidderUUID != null) { + addToPlayerBids(listing.highBidderUUID, listing.id); + } + } + } + + // Claims are now stored in PlayerData, not indexed here + LogWriter.info("Rebuilt auction indices: " + playerListingIds.size() + " sellers, " + + playerBidIds.size() + " bidders"); + } + + private void loadFromFile(File file) throws Exception { + INbt compound; + try (FileInputStream fis = new FileInputStream(file)) { + compound = NBTIO.readCompressed(fis); + } + readFromNBT(compound); + } + + public INbt writeToNBT(INbt compound) { + // Listings + global claims (player claims are stored in PlayerData) + INbtList listingsList = new INbtList(); + for (AuctionListing listing : listings.values()) { + listingsList.appendTag(listing.writeToNBT(new INbt())); + } + compound.setTag("Listings", listingsList); + + INbtList globalClaimsList = new INbtList(); + for (AuctionClaim claim : globalClaims) { + if (claim != null && !claim.claimed) { + globalClaimsList.appendTag(claim.writeToNBT(new INbt())); + } + } + compound.setTag("GlobalClaims", globalClaimsList); + + return compound; + } + + public void readFromNBT(INbt compound) { + globalClaims.clear(); + + INbtList listingsList = compound.getTagList("Listings", 10); + for (int i = 0; i < listingsList.tagCount(); i++) { + AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i)); + listings.put(listing.id, listing); + } + + if (compound.hasKey("GlobalClaims")) { + INbtList claimsList = compound.getTagList("GlobalClaims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); + if (!claim.claimed) { + globalClaims.add(claim); + } + } + } + } + + // ========================================= + // IAuctionHandler Implementation + // ========================================= + + @Override + public boolean isEnabled() { + return ConfigMarket.AuctionEnabled; + } + + @Override + public IAuctionListing[] getActiveListings() { + List active = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + active.add(listing); + } + } + return active.toArray(new IAuctionListing[0]); + } + + @Override + public IAuctionListing[] getListingsBySeller(String sellerUUID) { + if (sellerUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(sellerUUID); + List result = getPlayerActiveListings(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public IAuctionListing[] getListingsByBidder(String bidderUUID) { + if (bidderUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(bidderUUID); + List result = getPlayerActiveBids(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public int getActiveListingCount() { + int count = 0; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + count++; + } + } + return count; + } + + @Override + public IAuctionListing createListing(IPlayer player, IItemStack item, long startingPrice, long buyoutPrice) { + if (player == null || item == null) return null; + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + IItemStack mcItem = item.getMCItemStack(); + String result = createListing(IPlayer, mcItem, startingPrice, buyoutPrice); + if (result != null) { + return null; // Failed + } + // Find the just-created listing + Set playerListings = playerListingIds.get(IPlayer.getUniqueID()); + if (playerListings != null && !playerListings.isEmpty()) { + // Get the most recent one + long latestTime = 0; + AuctionListing latestListing = null; + for (String id : playerListings) { + AuctionListing listing = listings.get(id); + if (listing != null && listing.createdTime > latestTime) { + latestTime = listing.createdTime; + latestListing = listing; + } + } + return latestListing; + } + return null; + } + + @Override + public String placeBid(String listingId, IPlayer player, long amount) { + if (player == null) return "Player is null"; + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + return placeBid(listingId, IPlayer, amount); + } + + @Override + public String buyout(String listingId, IPlayer player) { + if (player == null) return "Player is null"; + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + return buyout(listingId, IPlayer); + } + + @Override + public String cancelListing(String listingId, IPlayer player, boolean isAdmin) { + if (player == null) return "Player is null"; + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + return cancelListing(listingId, IPlayer, isAdmin); + } + + @Override + public long getListingFee() { + return ConfigMarket.ListingFee; + } + + @Override + public double getSalesTaxPercent() { + return ConfigMarket.SalesTaxPercent; + } + + @Override + public double getMinBidIncrementPercent() { + return ConfigMarket.MinBidIncrementPercent; + } + + @Override + public int getAuctionDurationHours() { + return ConfigMarket.AuctionDurationHours; + } + + @Override + public int getSnipeProtectionMinutes() { + return ConfigMarket.SnipeProtectionMinutes; + } + + @Override + public String getCurrencyName() { + return ConfigMarket.CurrencyName; + } + + @Override + public long getMinimumListingPrice() { + return ConfigMarket.MinimumListingPrice; + } + + @Override + public IAuctionListing[] searchListings(String searchText) { + if (searchText == null || searchText.trim().isEmpty()) { + return getActiveListings(); + } + String search = searchText.toLowerCase().trim(); + List result = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE || listing.isExpired()) continue; + String itemName = listing.item != null ? listing.item.getDisplayName().toLowerCase() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName.toLowerCase() : ""; + if (itemName.contains(search) || sellerName.contains(search)) { + result.add(listing); + } + } + return result.toArray(new IAuctionListing[0]); + } + + // ========================================= + // Admin Methods + // ========================================= + + public List getAllListings() { + return new ArrayList<>(listings.values()); + } + + /** + * Admin management flow for listings. + * cancelCompletely=false: stop listing and return assets to the original owners' claim pools. + * cancelCompletely=true: cancel listing and route assets to global claims. + */ + public String adminStopListingToGlobal(String listingId, IPlayer admin, boolean cancelCompletely) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + if (admin == null) { + return "Admin player is missing."; + } + + synchronized (listing) { + if (!listing.status.canCancel()) { + return "This auction cannot be managed."; + } + + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; + + // Return active highest bid - always refund to the actual bidder + if (listing.hasBids() && listing.highBidderUUID != null) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName, + listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + removeFromPlayerBids(listing.highBidderUUID, listing.id); + } + + // Return the listed item. + if (listing.item != null) { + AuctionClaim itemClaim; + if (cancelCompletely || listing.sellerUUID == null) { + itemClaim = AuctionClaim.createItemReturnedClaim( + null, "Global", listing.id, listing.item); + addGlobalClaim(itemClaim); + } else { + itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + } + } + + if (!listing.isGlobalListing && listing.sellerUUID != null) { + removeFromPlayerListings(listing.sellerUUID, listing.id); + } + + listing.status = cancelCompletely ? EnumAuctionStatus.CANCELLED : EnumAuctionStatus.ENDED; + markDirty(); + + logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, listing.currentBid, + (cancelCompletely ? "Admin cancelled to global claims" : "Admin stopped to owner claims") + + ", OriginalSeller: " + listing.sellerName); + } + + return null; + } + + public void adminCancelListing(String listingId, IPlayer admin, String reason) { + AuctionListing listing = listings.get(listingId); + String itemName = listing != null && listing.item != null ? listing.item.getDisplayName() : "Unknown"; + adminStopListingToGlobal(listingId, admin, true); + if (reason != null && !reason.isEmpty()) { + logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, 0, "Admin cancel - Reason: " + reason); + } + } + + public void clearEndedListings() { + // Remove ended/cancelled listings (claims are in PlayerData now) + listings.entrySet().removeIf(entry -> + entry.getValue().status == EnumAuctionStatus.CLAIMED || + entry.getValue().status == EnumAuctionStatus.ENDED || + entry.getValue().status == EnumAuctionStatus.CANCELLED); + + // Rebuild indices + rebuildIndices(); + markDirty(); + } + + /** + * Check if a player is the seller of a listing. + * Used for GUI visibility checks. + */ + public boolean isPlayerSeller(String listingId, UUID playerUUID) { + AuctionListing listing = listings.get(listingId); + return listing != null && listing.isSeller(playerUUID); + } + + /** + * Reload the item blacklist from config. + */ + public void reloadBlacklist() { + AuctionBlacklist.reload(); + } + + /** + * Check if an item is blacklisted. + */ + public boolean isItemBlacklisted(IItemStack item) { + return AuctionBlacklist.isBlacklisted(item); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/BankController.java b/core/src/main/java/noppes/npcs/controllers/BankController.java new file mode 100644 index 000000000..21d19acff --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/BankController.java @@ -0,0 +1,173 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.controllers.data.Bank; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; + +public class BankController { + public HashMap banks; + private String filePath = ""; + + private static BankController instance; + + public BankController() { + instance = this; + banks = new HashMap(); + loadBanks(); + + if (banks.isEmpty()) { + Bank bank = new Bank(); + bank.id = 0; + bank.name = "Default Bank"; + for (int i = 0; i < 6; i++) + bank.slotTypes.put(i, 0); + + banks.put(bank.id, bank); + } + } + + public static BankController getInstance() { + if (newInstance()) { + instance = new BankController(); + } + return instance; + } + + private static boolean newInstance() { + if (instance == null) + return true; + File file = CustomNpcs.getWorldSaveDirectory(); + if (file == null) + return false; + if (!instance.filePath.equals(file.getAbsolutePath())) { + return true; + } + + return false; + } + + private void loadBanks() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) + return; + + filePath = saveDir.getAbsolutePath(); + + try { + File file = new File(saveDir, "bank.dat"); + if (file.exists()) { + loadBanks(file); + } + } catch (Exception e) { + try { + File file = new File(saveDir, "bank.dat_old"); + if (file.exists()) { + loadBanks(file); + } + + } catch (Exception ee) { + } + } + } + + private void loadBanks(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + loadBanks(NBTIO.readCompressed(fis)); + } + } + + public void loadBanks(INbt nbttagcompound1) throws IOException { + HashMap banks = new HashMap(); + INbtList list = nbttagcompound1.getTagList("Data", 10); + if (list != null) { + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + Bank bank = new Bank(); + bank.readEntityFromNBT(INbt); + banks.put(bank.id, bank); + } + } + this.banks = banks; + } + + public INbt getNBT() { + + INbtList list = new INbtList(); + for (Bank bank : banks.values()) { + INbt nbtfactions = new INbt(); + bank.writeEntityToNBT(nbtfactions); + list.appendTag(nbtfactions); + } + INbt INbt = new INbt(); + INbt.setTag("Data", list); + return INbt; + } + + public Bank getBank(int bankId) { + Bank bank = banks.get(bankId); + if (bank != null) + return bank; + return banks.values().iterator().next(); + } + + public void saveBanks() { + try { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + File file = new File(saveDir, "bank.dat_new"); + File file1 = new File(saveDir, "bank.dat_old"); + File file2 = new File(saveDir, "bank.dat"); + NBTIO.writeCompressed(getNBT(), new FileOutputStream(file)); + if (file1.exists()) { + file1.delete(); + } + file2.renameTo(file1); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void saveBank(Bank bank) { + if (bank.id < 0) { + bank.id = getUnusedId(); + } + banks.put(bank.id, bank); + saveBanks(); + } + + public int getUnusedId() { + int id = 0; + while (true) { + if (!banks.containsKey(id)) + return id; + id++; + } + } + + public void removeBank(int bank) { + if (bank < 0 || banks.size() <= 1) + return; + banks.remove(bank); + saveBanks(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ChunkController.java b/core/src/main/java/noppes/npcs/controllers/ChunkController.java new file mode 100644 index 000000000..e97ba425b --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ChunkController.java @@ -0,0 +1,108 @@ +package noppes.npcs.controllers; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.constants.EnumJobType; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +public class ChunkController implements LoadingCallback { + public static ChunkController Instance; + + public HashMap tickets = new HashMap(); + + public ChunkController() { + Instance = this; + } + + public void clear() { + tickets = new HashMap(); + } + + public Ticket getTicket(EntityNPCInterface npc) { + Ticket ticket = tickets.get(npc); + if (ticket != null) + return ticket; + if (size() >= ConfigMain.ChunkLoaders) + return null; + ticket = ForgeChunkManager.requestTicket(CustomNpcs.instance, npc.worldObj, Type.IEntity); + if (ticket == null) + return null; + ticket.bindEntity(npc); + ticket.setChunkListDepth(6); + tickets.put(npc, ticket); + return null; + } + + public void deleteNPC(EntityNPCInterface npc) { + Ticket ticket = tickets.get(npc); + if (ticket != null) { + tickets.remove(npc); + ForgeChunkManager.releaseTicket(ticket); + } + } + + @Override + public void ticketsLoaded(List tickets, IWorld IWorld) { + for (Ticket ticket : tickets) { + if (!(ticket.getEntity() instanceof EntityNPCInterface)) + continue; + EntityNPCInterface npc = (EntityNPCInterface) ticket.getEntity(); + if (npc.advanced.job == EnumJobType.ChunkLoader && !tickets.contains(npc)) { + this.tickets.put(npc, ticket); + double x = npc.posX / 16; + double z = npc.posZ / 16; + + ForgeChunkManager.forceChunk(ticket, new ChunkCoordIntPair(ValueUtil.floorDouble(x), ValueUtil.floorDouble(z))); + ForgeChunkManager.forceChunk(ticket, new ChunkCoordIntPair(MathHelper.ceiling_double_int(x), MathHelper.ceiling_double_int(z))); + ForgeChunkManager.forceChunk(ticket, new ChunkCoordIntPair(ValueUtil.floorDouble(x), MathHelper.ceiling_double_int(z))); + ForgeChunkManager.forceChunk(ticket, new ChunkCoordIntPair(MathHelper.ceiling_double_int(x), ValueUtil.floorDouble(z))); + } + } + } + + public int size() { + return tickets.size(); + } + + public void unload(int toRemove) { + Iterator ite = tickets.keySet().iterator(); + int i = 0; + while (ite.hasNext()) { + if (i >= toRemove) + return; + IEntity IEntity = ite.next(); + ForgeChunkManager.releaseTicket(tickets.get(IEntity)); + ite.remove(); + i++; + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/CustomEffectController.java b/core/src/main/java/noppes/npcs/controllers/CustomEffectController.java new file mode 100644 index 000000000..10b6f8e8a --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/CustomEffectController.java @@ -0,0 +1,734 @@ +package noppes.npcs.controllers; + + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.zip.GZIPInputStream; +import kamkeel.npcs.controllers.SyncController; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.ICustomEffect; +import noppes.npcs.api.handler.ICustomEffectHandler; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.data.CustomEffect; +import noppes.npcs.controllers.data.EffectKey; +import noppes.npcs.controllers.data.EffectScript; +import noppes.npcs.controllers.data.PlayerEffect; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.util.NBTJsonUtil; + +public class CustomEffectController implements ICustomEffectHandler { + + public static CustomEffectController Instance = new CustomEffectController(); + public HashMap> indexMapper = new HashMap<>(); + private HashMap indexLabels = new HashMap<>(); + + public HashMap customEffectsSync = new HashMap<>(); + + public HashMap customEffectScriptHandlers = new HashMap<>(); + private HashMap bootOrder; + public CategoryManager categoryManager = new CategoryManager(); + + private int lastUsedID = 0; + public ConcurrentHashMap> playerEffects = new ConcurrentHashMap<>(); + + public CustomEffectController() { + HashMap customEffects = new HashMap<>(); + registerEffectMap(0, customEffects); + } + + public static CustomEffectController getInstance() { + return Instance; + } + + public void registerEffectMap(int index, HashMap effectHashMap) { + indexMapper.put(index, (HashMap) effectHashMap); + } + + public void registerEffectMapLabel(int index, String label) { + indexLabels.put(index, label); + } + + public HashMap getIndexLabels() { + return indexLabels; + } + + public HashMap getCustomEffects() { + return indexMapper.get(0); + } + + public void load() { + lastUsedID = 0; + playerEffects.clear(); + bootOrder = new HashMap<>(); + LogWriter.info("Loading custom effects..."); + readCustomEffectMap(); + loadCustomEffects(); + LogWriter.info("Done loading custom effects."); + } + + public void runEffects(IPlayer player) { + Map current = getPlayerEffects(player); + for (Map.Entry entry : current.entrySet()) { + int id = entry.getKey().getId(); + int index = entry.getKey().getIndex(); + CustomEffect effect = get(id, index); + if (effect != null) { + effect.runEffect(player, entry.getValue()); + } + } + } + + public void killEffects(IPlayer player) { + Map current = getPlayerEffects(player); + Iterator> iterator = current.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + int id = entry.getKey().getId(); + int index = entry.getKey().getIndex(); + CustomEffect effect = get(id, index); + if (effect != null) { + if (effect.lossOnDeath) { + effect.onRemoved(player, entry.getValue(), ExpirationType.DEATH); + iterator.remove(); + } + } else { + iterator.remove(); + } + } + } + + public void delete(int id) { + ICustomEffect effect = get(id); + if (effect != null) { + CustomEffect foundEffect = getCustomEffects().remove(effect.getID()); + customEffectScriptHandlers.remove(effect.getID()); + if (foundEffect != null && foundEffect.name != null) { + File dir = categoryManager.getItemDir(id); + File file = new File(dir, foundEffect.name + ".json"); + if (file.exists()) { + file.delete(); + } + categoryManager.removeItem(id); + SyncController.syncRemove(EnumSyncType.CUSTOM_EFFECTS, foundEffect.getID()); + saveEffectLoadMap(); + } + } + } + + public int getUnusedId() { + for (int catid : getCustomEffects().keySet()) { + if (catid > lastUsedID) + lastUsedID = catid; + } + lastUsedID++; + return lastUsedID; + } + + public CustomEffect get(int id, int index) { + HashMap effectMap = indexMapper.get(index); + return effectMap != null ? effectMap.get(id) : null; + } + + public CustomEffect get(int id) { + return get(id, 0); + } + + public boolean has(int id, int index) { + HashMap effectMap = indexMapper.get(index); + return effectMap != null && effectMap.containsKey(id); + } + + public boolean has(int id) { + return has(id, 0); + } + + public CustomEffect get(String name, int index) { + HashMap effectMap = indexMapper.get(index); + if (effectMap != null) { + for (CustomEffect effect : effectMap.values()) { + if (effect.getName().equalsIgnoreCase(name)) { + return effect; + } + } + } + return null; + } + + public CustomEffect get(String name) { + return get(name, 0); + } + + public boolean has(String name, int index) { + return get(name, index) != null; + } + + public boolean has(String name) { + return has(name, 0); + } + + public ConcurrentHashMap getPlayerEffects(IPlayer player) { + UUID playerId = NoppesUtilServer.getUUID(player); + ConcurrentHashMap effects = playerEffects.get(playerId); + if (effects == null) { + effects = new ConcurrentHashMap<>(); + playerEffects.put(playerId, effects); + } + return effects; + } + + public void removeEffect(IPlayer player, PlayerEffect effect, ExpirationType type) { + if (effect == null) + return; + + Map currentEffects = getPlayerEffects(player); + EffectKey key = new EffectKey(effect.id, effect.index); + if (currentEffects.containsKey(key)) { + CustomEffect parent = get(effect.id, effect.index); + if (parent != null) { + parent.onRemoved(player, effect, type); + } + currentEffects.remove(key); + } + } + + private boolean hasOther(String name, int id) { + for (CustomEffect effect : getCustomEffects().values()) { + if (effect.getID() != id && effect.getName().equalsIgnoreCase(name)) + return true; + } + return false; + } + + public void clearEffects(IPlayer player) { + Map effects = getPlayerEffects(player); + if (effects != null) { + effects.clear(); + } + } + + public void clearEffect(IPlayer player, int id) { + clearEffect(player, id, 0); + } + + public void clearEffect(IPlayer player, int id, int index) { + Map effects = getPlayerEffects(player); + if (effects != null) { + effects.remove(new EffectKey(id, index)); + } + } + + public boolean hasEffect(IPlayer player, int id) { + return hasEffect(player, id, 0); + } + + public boolean hasEffect(IPlayer player, int id, int index) { + return getPlayerEffects(player).containsKey(new EffectKey(id, index)); + } + + public int getEffectDuration(IPlayer player, int id) { + return getEffectDuration(player, id, 0); + } + + public int getEffectDuration(IPlayer player, int id, int index) { + PlayerEffect effect = getPlayerEffects(player).get(new EffectKey(id, index)); + return effect != null ? effect.duration : -1; + } + + public void applyEffect(IPlayer player, int id, int duration, byte level) { + applyEffect(player, id, duration, level, 0); + } + + public void applyEffect(IPlayer player, int id, int duration, byte level, int index) { + if (player == null || id <= 0) return; + Map currentEffects = getPlayerEffects(player); + CustomEffect parent = get(id, index); + if (parent != null) { + PlayerEffect playerEffect = new PlayerEffect(id, duration, level, index); + currentEffects.put(new EffectKey(id, index), playerEffect); + parent.onAdded(player, playerEffect); + } + } + + public void removeEffect(IPlayer player, int id) { + removeEffect(player, id, 0, ExpirationType.REMOVED); + } + + public void removeEffect(IPlayer player, int id, ExpirationType type) { + removeEffect(player, id, 0, type); + } + + public void removeEffect(IPlayer player, int id, int index) { + removeEffect(player, id, index, ExpirationType.REMOVED); + } + + public void removeEffect(IPlayer player, int id, int index, ExpirationType type) { + if (player == null || id <= 0) return; + Map currentEffects = getPlayerEffects(player); + EffectKey key = new EffectKey(id, index); + PlayerEffect effect = currentEffects.get(key); + if (effect != null) { + this.removeEffect(player, effect, type); + } + } + + public void decrementEffects(IPlayer player) { + Iterator iterator = getPlayerEffects(player).values().iterator(); + IPlayer iPlayer = NoppesUtilServer.getIPlayer(player); + while (iterator.hasNext()) { + PlayerEffect effect = iterator.next(); + if (effect == null) { + iterator.remove(); + continue; + } + if (effect.duration == -100) + continue; + if (effect.duration <= 0) { + CustomEffect parent = CustomEffectController.Instance.get(effect.id, effect.index); + if (parent != null) { + parent.onRemoved(player, effect, ExpirationType.RUN_OUT); + } + iterator.remove(); + continue; + } + effect.duration--; + } + } + + public File getMapDir() { + File dir = CustomNpcs.getWorldSaveDirectory(); + if (!dir.exists()) + dir.mkdir(); + return dir; + } + + private void loadCustomEffects() { + getCustomEffects().clear(); + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdir(); + } + + categoryManager.loadCategories(dir); + + // Load uncategorized effects (root level .json files) + loadEffectsFromDir(dir, CategoryManager.UNCATEGORIZED_ID); + + // Load categorized effects (subdirectories) + for (Map.Entry entry : categoryManager.getCategories().entrySet()) { + File catDir = categoryManager.getCategoryDir(entry.getKey()); + loadEffectsFromDir(catDir, entry.getKey()); + } + + this.registerEffectMap(0, getCustomEffects()); + saveEffectLoadMap(); + } + + private void loadEffectsFromDir(File dir, int catId) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(NBTJsonUtil.LoadFile(file)); + effect.name = file.getName().substring(0, file.getName().length() - 5); + if (effect.id == -1) { + effect.id = getUnusedId(); + } + int originalID = effect.id; + int setID = effect.id; + while (bootOrder.containsKey(setID) || getCustomEffects().containsKey(setID)) { + if (bootOrder.containsKey(setID)) + if (bootOrder.get(setID).equalsIgnoreCase(effect.name)) + break; + setID++; + } + effect.id = setID; + if (originalID != setID) { + LogWriter.info("Found Custom Effect ID Mismatch: " + effect.name + ", New ID: " + setID); + effect.save(); + } + getCustomEffects().put(effect.id, effect); + categoryManager.registerItem(effect.id, catId); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + } + + public HashMap getEffectMap(int index) { + return indexMapper.get(index); + } + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "customeffects"); + } + + public void saveEffectLoadMap() { + try { + File saveDir = getMapDir(); + File file = new File(saveDir, "customeffects.dat_new"); + File file1 = new File(saveDir, "customeffects.dat_old"); + File file2 = new File(saveDir, "customeffects.dat"); + NBTIO.writeCompressed(this.writeMapNBT(), new FileOutputStream(file)); + if (file1.exists()) { + file1.delete(); + } + file2.renameTo(file1); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + LogWriter.except(e); + } + } + + private INbt writeMapNBT() { + INbt nbt = new INbt(); + INbtList customEffectsList = new INbtList(); + for (Integer key : this.getCustomEffects().keySet()) { + CustomEffect customEffect = this.getCustomEffects().get(key); + if (!customEffect.getName().isEmpty()) { + INbt effectCompound = new INbt(); + effectCompound.setString("Name", customEffect.getName()); + effectCompound.setInteger("ID", key); + customEffectsList.appendTag(effectCompound); + } + } + nbt.setTag("CustomEffects", customEffectsList); + nbt.setInteger("lastID", lastUsedID); + return nbt; + } + + private void readCustomEffectMap() { + bootOrder.clear(); + try { + File file = new File(getMapDir(), "customeffects.dat"); + if (file.exists()) { + loadCustomEffectMap(file); + } + } catch (Exception e) { + try { + File file = new File(getMapDir(), "customeffects.dat_old"); + if (file.exists()) { + loadCustomEffectMap(file); + } + } catch (Exception ignored) { + } + } + } + + private void loadCustomEffectMap(File file) throws IOException { + DataInputStream dis = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); + readCustomEffectMap(dis); + dis.close(); + } + + private void readCustomEffectMap(DataInputStream stream) throws IOException { + INbt nbtCompound = CompressedStreamTools.read(stream); + this.readMapNBT(nbtCompound); + } + + private void readMapNBT(INbt compound) { + lastUsedID = compound.getInteger("lastID"); + INbtList list = compound.getTagList("CustomEffects", 10); + if (list != null) { + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + String effectName = INbt.getString("Name"); + Integer key = INbt.getInteger("ID"); + bootOrder.put(key, effectName); + } + } + } + + public void deleteEffectFile(String prevName) { + categoryManager.deleteFile(prevName + ".json"); + } + + // API Versions + @Override + public ICustomEffect createEffect(String name) { + if (has(name)) + return get(name); + + CustomEffect effect = new CustomEffect(); + effect.name = name; + + if (effect.id == -1) { + int id = getUnusedId(); + while (getCustomEffects().containsKey(id)) { + id = getUnusedId(); + } + effect.id = id; + } + getCustomEffects().put(effect.id, effect); + return effect; + } + + @Override + public ICustomEffect getEffect(String name) { + return get(name); + } + + @Override + public void deleteEffect(String name) { + ICustomEffect effect = getEffect(name); + if (effect != null) { + delete(effect.getID()); + } + } + + @Override + public boolean hasEffect(IPlayer player, int id) { + if (player == null || player.getMCEntity() == null) + return false; + return hasEffect((IPlayer) player.getMCEntity(), id); + } + + @Override + public boolean hasEffect(IPlayer player, ICustomEffect effect) { + if (player == null || player.getMCEntity() == null) + return false; + return hasEffect((IPlayer) player.getMCEntity(), effect.getID(), effect.getIndex()); + } + + @Override + public int getEffectDuration(IPlayer player, int id) { + if (player == null || player.getMCEntity() == null) + return -1; + return getEffectDuration((IPlayer) player.getMCEntity(), id); + } + + @Override + public int getEffectDuration(IPlayer player, ICustomEffect effect) { + if (effect == null) + return -1; + return getEffectDuration(player, effect.getID(), effect.getIndex()); + } + + @Override + public void applyEffect(IPlayer player, int id, int duration, byte level) { + if (player == null || player.getMCEntity() == null) + return; + applyEffect((IPlayer) player.getMCEntity(), id, duration, level); + } + + @Override + public void applyEffect(IPlayer player, ICustomEffect effect, int duration, byte level) { + applyEffect(player, effect.getID(), duration, level); + } + + @Override + public void removeEffect(IPlayer player, int id) { + if (player == null || player.getMCEntity() == null) + return; + removeEffect((IPlayer) player.getMCEntity(), id, ExpirationType.REMOVED); + } + + @Override + public void removeEffect(IPlayer player, ICustomEffect effect) { + removeEffect((IPlayer) player.getMCEntity(), (PlayerEffect) effect, ExpirationType.REMOVED); + } + + @Override + public void clearEffects(IPlayer player) { + if (player == null || player.getMCEntity() == null) + return; + clearEffects((IPlayer) player); + } + + @Override + public void applyEffect(IPlayer player, int id, int duration, byte level, int index) { + if (player == null || player.getMCEntity() == null) + return; + applyEffect((IPlayer) player.getMCEntity(), id, duration, level, index); + } + + @Override + public void applyEffect(IPlayer player, ICustomEffect effect, int duration, byte level, int index) { + applyEffect(player, effect.getID(), duration, level, index); + } + + @Override + public void removeEffect(IPlayer player, int id, int index) { + if (player == null || player.getMCEntity() == null) + return; + removeEffect((IPlayer) player.getMCEntity(), id, index, ExpirationType.REMOVED); + } + + @Override + public void removeEffect(IPlayer player, ICustomEffect effect, int index) { + removeEffect(player, effect.getID(), index); + } + + @Override + public void clearEffects(IPlayer player, int index) { + if (player == null || player.getMCEntity() == null) + return; + IPlayer IEntity = (IPlayer) player.getMCEntity(); + ConcurrentHashMap effects = getPlayerEffects(IEntity); + Iterator> iterator = effects.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getKey().getIndex() == index) { + CustomEffect parent = get(entry.getKey().getId(), index); + if (parent != null) { + parent.onRemoved(IEntity, entry.getValue(), ExpirationType.REMOVED); + } + iterator.remove(); + } + } + } + + @Override + public int getEffectDuration(IPlayer player, int id, int index) { + if (player == null || player.getMCEntity() == null) + return -1; + return getEffectDuration((IPlayer) player.getMCEntity(), id, index); + } + + @Override + public int getEffectDuration(IPlayer player, ICustomEffect effect, int index) { + return getEffectDuration(player, effect.getID(), index); + } + + @Override + public ICustomEffect getEffect(String name, int index) { + return get(name, index); + } + + @Override + public ICustomEffect getEffect(int id, int index) { + return get(id, index); + } + + //////////////////////////////////////////////////////// + // CATEGORY HELPERS + //////////////////////////////////////////////////////// + + public Map getCategoryScrollData() { + return categoryManager.getCategoryScrollData(); + } + + public void moveItemToCategory(int itemId, int catId) { + CustomEffect effect = getCustomEffects().get(itemId); + if (effect == null) return; + categoryManager.moveItem(itemId, effect.name + ".json", catId); + saveEffectLoadMap(); + } + + public Map getItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, getCustomEffects().keySet()); + for (int itemId : itemIds) { + CustomEffect effect = getCustomEffects().get(itemId); + if (effect != null) { + map.put(effect.name, effect.id); + } + } + return map; + } + + public HashMap> getItemTagMapForCategory(int catId) { + HashMap> tagMap = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, getCustomEffects().keySet()); + for (int itemId : itemIds) { + CustomEffect effect = getCustomEffects().get(itemId); + if (effect != null && !effect.tagUUIDs.isEmpty()) { + tagMap.put(effect.name, effect.tagUUIDs); + } + } + return tagMap; + } + + @Override + public ICustomEffect saveEffect(ICustomEffect customEffect) { + if (customEffect.getID() < 0) { + customEffect.setID(getUnusedId()); + while (has(customEffect.getName())) + customEffect.setName(customEffect.getName() + "_"); + } + + while (hasOther(customEffect.getName(), customEffect.getID())) + customEffect.setName(customEffect.getName() + "_"); + + TagController.validateTagUUIDs(((CustomEffect) customEffect).tagUUIDs); + getCustomEffects().remove(customEffect.getID()); + getCustomEffects().put(customEffect.getID(), (CustomEffect) customEffect); + + saveEffectLoadMap(); + + File dir = categoryManager.getItemDir(customEffect.getID()); + if (!dir.exists()) + dir.mkdirs(); + + File file = new File(dir, customEffect.getName() + ".json_new"); + File file2 = new File(dir, customEffect.getName() + ".json"); + + try { + INbt INbt = ((CustomEffect) customEffect).writeToNBT(true); + NBTJsonUtil.SaveFile(file, INbt); + if (file2.exists()) + file2.delete(); + file.renameTo(file2); + INbt.removeTag("ScriptData"); + SyncController.syncUpdate(EnumSyncType.CUSTOM_EFFECTS, -1, INbt); + } catch (Exception e) { + LogWriter.except(e); + } + return getCustomEffects().get(customEffect.getID()); + } + + public CustomEffect cloneEffect(int originalId) { + CustomEffect original = getCustomEffects().get(originalId); + if (original == null) return null; + + int originalCatId = categoryManager.getItemCategory(originalId); + + INbt nbt = original.writeToNBT(true); + int newId = getUnusedId(); + nbt.setInteger("ID", newId); + + CustomEffect clone = new CustomEffect(); + clone.readFromNBT(nbt); + + String name = clone.getName(); + while (has(name)) name += "_"; + clone.name = name; + + if (originalCatId > CategoryManager.UNCATEGORIZED_ID) { + categoryManager.registerItem(newId, originalCatId); + } + + saveEffect(clone); + return clone; + } + +} diff --git a/core/src/main/java/noppes/npcs/controllers/CustomGuiController.java b/core/src/main/java/noppes/npcs/controllers/CustomGuiController.java new file mode 100644 index 000000000..6ef91c5f8 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/CustomGuiController.java @@ -0,0 +1,156 @@ +package noppes.npcs.controllers; + + +import java.io.IOException; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.controllers.data.PlayerDataScript; +import noppes.npcs.CustomNpcs; + +public class CustomGuiController { + public CustomGuiController() { + } + + public static void openGui(IPlayer player, ScriptGui gui) { + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + IPlayer.openGui(CustomNpcs.instance, EnumGuiType.CustomGui.ordinal(), player.getWorld().getMCWorld(), gui.getSlots().size(), 0, 0); + ((ContainerCustomGui) ((IPlayer) player.getMCEntity()).openContainer).setGui(gui, (IPlayer) player.getMCEntity()); + GuiDataPacket.sendGuiData((IPlayer) player.getMCEntity(), gui.toNBT()); + } + + public static boolean updateGui(IPlayer player, ScriptGui gui) { + if (((IPlayer) player.getMCEntity()).openContainer instanceof ContainerCustomGui) { + ((ContainerCustomGui) ((IPlayer) player.getMCEntity()).openContainer).setGui(gui, (IPlayer) player.getMCEntity()); + GuiDataPacket.sendGuiData((IPlayer) player.getMCEntity(), gui.toNBT()); + return true; + } else { + return false; + } + } + + public static void openOverlay(IPlayer player, ScriptOverlay gui) { + PacketHandler.Instance.sendToPlayer(new ScriptOverlayDataPacket(gui.toNBT()), (IPlayer) player.getMCEntity()); + } + + public static boolean updateOverlay(IPlayer player, ScriptOverlay gui) { + PacketHandler.Instance.sendToPlayer(new ScriptOverlayDataPacket(gui.toNBT()), (IPlayer) player.getMCEntity()); + return true; + } + + static boolean checkGui(CustomGuiEvent event) { + IPlayer player = (IPlayer) event.player.getMCEntity(); + if (!(player.openContainer instanceof ContainerCustomGui)) { + return false; + } else { + return ((ContainerCustomGui) player.openContainer).customGui.getID() == event.gui.getID(); + } + } + + public static IItemStack[] getSlotContents(IPlayer player) { + IItemStack[] slotContents = new IItemStack[0]; + if (player.openContainer instanceof ContainerCustomGui) { + ContainerCustomGui container = (ContainerCustomGui) player.openContainer; + slotContents = new IItemStack[container.guiInventory.getSizeInventory()]; + + for (int i = 0; i < container.guiInventory.getSizeInventory(); ++i) { + slotContents[i] = NpcAPI.Instance().getIItemStack(container.guiInventory.getStackInSlot(i)); + } + } + + return slotContents; + } + + public static void onButton(CustomGuiEvent.ButtonEvent event) { + IPlayer player = (IPlayer) event.player.getMCEntity(); + if (player != null) { + ScriptGui gui = getOpenGui(player); + if (checkGui(event) && gui != null) { + gui.getScriptHandler(player).callScript(EnumScriptType.CUSTOM_GUI_BUTTON, event); + } + } + NpcAPI.EVENT_BUS.post(event); + } + + public static void onSlotChange(CustomGuiEvent.SlotEvent event) { + IPlayer player = (IPlayer) event.player.getMCEntity(); + if (player != null) { + ScriptGui gui = getOpenGui(player); + if (checkGui(event) && gui != null) { + gui.getScriptHandler(player).callScript(EnumScriptType.CUSTOM_GUI_SLOT, event); + } + } + NpcAPI.EVENT_BUS.post(event); + } + + public static boolean onSlotClick(CustomGuiEvent.SlotClickEvent event) { + IPlayer player = (IPlayer) event.player.getMCEntity(); + if (player != null) { + ScriptGui gui = getOpenGui(player); + if (checkGui(event) && gui != null) { + gui.getScriptHandler(player).callScript(EnumScriptType.CUSTOM_GUI_SLOT_CLICKED, event); + } + } + return NpcAPI.EVENT_BUS.post(event); + } + + public static void onCustomGuiUnfocused(CustomGuiEvent.UnfocusedEvent event) { + IPlayer player = (IPlayer) event.player.getMCEntity(); + if (player != null) { + ScriptGui gui = getOpenGui(player); + if (checkGui(event) && gui != null) { + gui.getScriptHandler(player).callScript(EnumScriptType.CUSTOM_GUI_TEXTFIELD, event); + } + } + NpcAPI.EVENT_BUS.post(event); + } + + public static void onScrollClick(CustomGuiEvent.ScrollEvent event) { + IPlayer player = (IPlayer) event.player.getMCEntity(); + if (player != null) { + ScriptGui gui = getOpenGui(player); + if (checkGui(event) && gui != null) { + gui.getScriptHandler(player).callScript(EnumScriptType.CUSTOM_GUI_SCROLL, event); + } + } + NpcAPI.EVENT_BUS.post(event); + } + + public static void onClose(CustomGuiEvent.CloseEvent event) { + IPlayer player = (IPlayer) event.player.getMCEntity(); + if (player != null) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + handler.callScript(EnumScriptType.CUSTOM_GUI_CLOSED, event); + } + NpcAPI.EVENT_BUS.post(event); + } + + public static ScriptGui getOpenGui(IPlayer player) { + return player.openContainer instanceof ContainerCustomGui ? ((ContainerCustomGui) player.openContainer).customGui : null; + } + + public static String[] readScrollSelection(ByteBuf buffer) { + try { + INbtList list = ByteBufUtils.readNBT(buffer).getTagList("selection", 8); + String[] selection = new String[list.tagCount()]; + + for (int i = 0; i < list.tagCount(); ++i) { + selection[i] = list.getStringTagAt(i); + } + + return selection; + } catch (IOException var4) { + var4.printStackTrace(); + return null; + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/DialogController.java b/core/src/main/java/noppes/npcs/controllers/DialogController.java new file mode 100644 index 000000000..e590932ea --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/DialogController.java @@ -0,0 +1,350 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.SyncController; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.NoppesStringUtils; +import noppes.npcs.api.handler.IDialogHandler; +import noppes.npcs.api.handler.data.IDialog; +import noppes.npcs.api.handler.data.IDialogCategory; +import noppes.npcs.constants.EnumOptionType; +import noppes.npcs.controllers.data.Dialog; +import noppes.npcs.controllers.data.DialogCategory; +import noppes.npcs.controllers.data.DialogOption; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class DialogController implements IDialogHandler { + public HashMap categoriesSync = new HashMap(); + public HashMap categories = new HashMap(); + public HashMap dialogs = new HashMap(); + public static DialogController Instance = new DialogController(); + ; + + private int lastUsedDialogID = 0; + private int lastUsedCatID = 0; + + public DialogController() { + Instance = this; + } + + public void load() { + LogWriter.info("Loading Dialogs"); + loadCategories(); + LogWriter.info("Done loading Dialogs"); + } + + private void loadCategories() { + categories.clear(); + dialogs.clear(); + + lastUsedCatID = 0; + lastUsedDialogID = 0; + + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "dialog.dat"); + if (file.exists()) { + loadCategoriesOld(file); + file.delete(); + file = new File(CustomNpcs.getWorldSaveDirectory(), "dialog.dat_old"); + if (file.exists()) + file.delete(); + return; + } + } catch (Exception e) { + LogWriter.except(e); + } + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdir(); + loadDefaultDialogs(); + } else { + for (File file : dir.listFiles()) { + if (!file.isDirectory()) + continue; + DialogCategory category = loadCategoryDir(file); + Iterator ite = category.dialogs.keySet().iterator(); + while (ite.hasNext()) { + int id = ite.next(); + if (id > lastUsedDialogID) + lastUsedDialogID = id; + Dialog dialog = category.dialogs.get(id); + if (dialogs.containsKey(id)) { + LogWriter.error("Duplicate id " + dialog.id + " from category " + category.title); + ite.remove(); + } else { + dialogs.put(id, dialog); + } + } + lastUsedCatID++; + category.id = lastUsedCatID; + categories.put(category.id, category); + } + } + } + + private DialogCategory loadCategoryDir(File dir) { + DialogCategory category = new DialogCategory(); + category.title = dir.getName(); + for (File file : dir.listFiles()) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + Dialog dialog = new Dialog(); + dialog.id = Integer.parseInt(file.getName().substring(0, file.getName().length() - 5)); + dialog.readNBTPartial(NBTJsonUtil.LoadFile(file)); + category.dialogs.put(dialog.id, dialog); + dialog.category = category; + + if (dialog.text != null && dialog.text.length() > 15000) { + LogWriter.error("Extremely big dialog loaded: [ID: " + dialog.id + "] Title: " + + dialog.title + " (Length: " + dialog.text.length() + " chars)"); + } + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + return category; + } + + private void loadCategoriesOld(File file) throws Exception { + File dir = getDir(); + if (!dir.exists()) { + dir.mkdir(); + } + INbt nbttagcompound1; + try (FileInputStream fis = new FileInputStream(file)) { + nbttagcompound1 = NBTIO.readCompressed(fis); + } + INbtList list = nbttagcompound1.getTagList("Data", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + DialogCategory category = new DialogCategory(); + category.readNBT(list.getCompoundTagAt(i)); + saveCategory(category); + Iterator> ita = category.dialogs.entrySet().iterator(); + while (ita.hasNext()) { + Map.Entry entry = ita.next(); + Dialog dialog = entry.getValue(); + dialog.id = entry.getKey(); + dialog.category = category; + if (dialogs.containsKey(dialog.id)) + ita.remove(); + else { + saveDialog(category.id, dialog); + } + } + + } + } + + private void loadDefaultDialogs() { + DialogCategory cat = new DialogCategory(); + cat.id = lastUsedCatID++; + cat.title = "Villager"; + + Dialog dia1 = new Dialog(); + dia1.id = 1; + dia1.category = cat; + dia1.title = "Start"; + dia1.text = "Hello {player}, " + '\n' + '\n' + "Welcome to our village. I hope you enjoy your stay"; + + Dialog dia2 = new Dialog(); + dia2.id = 2; + dia2.category = cat; + dia2.title = "Ask about village"; + dia2.text = "This village has been around for ages. Enjoy your stay here."; + + Dialog dia3 = new Dialog(); + dia3.id = 3; + dia3.category = cat; + dia3.title = "Who are you"; + dia3.text = "I'm a villager here. I have lived in this village my whole life."; + + cat.dialogs.put(dia1.id, dia1); + cat.dialogs.put(dia2.id, dia2); + cat.dialogs.put(dia3.id, dia3); + + + DialogOption option = new DialogOption(); + option.title = "Tell me something about this village"; + option.dialogId = 2; + option.optionType = EnumOptionType.DialogOption; + + DialogOption option2 = new DialogOption(); + option2.title = "Who are you?"; + option2.dialogId = 3; + option2.optionType = EnumOptionType.DialogOption; + + DialogOption option3 = new DialogOption(); + option3.title = "Goodbye"; + option3.optionType = EnumOptionType.QuitOption; + + dia1.options.put(0, option2); + dia1.options.put(1, option); + dia1.options.put(2, option3); + + + DialogOption option4 = new DialogOption(); + option4.title = "Back"; + option4.dialogId = 1; + + dia2.options.put(1, option4); + dia3.options.put(1, option4); + lastUsedDialogID = 3; + saveCategory(cat); + saveDialog(cat.id, dia1); + saveDialog(cat.id, dia2); + saveDialog(cat.id, dia3); + } + + public void saveCategory(DialogCategory category) { + category.title = NoppesStringUtils.cleanFileName(category.title); + if (categories.containsKey(category.id)) { + DialogCategory currentCategory = categories.get(category.id); + if (!currentCategory.title.equals(category.title)) { + while (containsCategoryName(category.title)) + category.title += "_"; + File newdir = new File(getDir(), category.title); + File olddir = new File(getDir(), currentCategory.title); + if (newdir.exists()) + return; + if (!olddir.renameTo(newdir)) + return; + } + category.dialogs = currentCategory.dialogs; + } else { + if (category.id < 0) { + lastUsedCatID++; + category.id = lastUsedCatID; + } + while (containsCategoryName(category.title)) + category.title += "_"; + File dir = new File(getDir(), category.title); + if (!dir.exists()) + dir.mkdirs(); + } + categories.put(category.id, category); + SyncController.syncUpdate(EnumSyncType.DIALOG_CATEGORY, -1, SyncController.updateDialogCat(category)); + } + + public void removeCategory(int category) { + DialogCategory cat = categories.get(category); + if (cat == null) + return; + File dir = new File(getDir(), cat.title); + if (!dir.delete()) + return; + for (int dia : cat.dialogs.keySet()) + dialogs.remove(dia); + categories.remove(category); + SyncController.syncRemove(EnumSyncType.DIALOG_CATEGORY, category); + } + + private boolean containsCategoryName(String name) { + name = name.toLowerCase(); + for (DialogCategory cat : categories.values()) { + if (cat.title.toLowerCase().equals(name)) + return true; + } + return false; + } + + private boolean containsDialogName(DialogCategory category, Dialog dialog) { + for (Dialog dia : category.dialogs.values()) { + if (dia.id != dialog.id && dia.title.equalsIgnoreCase(dialog.title)) + return true; + } + return false; + } + + public Dialog saveDialog(int categoryId, Dialog dialog) { + DialogCategory category = categories.get(categoryId); + if (category == null) + return dialog; + dialog.category = category; + + while (containsDialogName(dialog.category, dialog)) { + dialog.title = dialog.title + "_"; + } + if (dialog.id < 0) { + lastUsedDialogID++; + dialog.id = lastUsedDialogID; + } + + dialogs.put(dialog.id, dialog); + category.dialogs.put(dialog.id, dialog); + + File dir = new File(getDir(), category.title); + if (!dir.exists()) + dir.mkdirs(); + + File file = new File(dir, dialog.id + ".json_new"); + File file2 = new File(dir, dialog.id + ".json"); + + try { + INbt compound = dialog.writeToNBTPartial(new INbt()); + NBTJsonUtil.SaveFile(file, compound); + if (file2.exists()) + file2.delete(); + file.renameTo(file2); + SyncController.syncUpdate(EnumSyncType.DIALOG, category.id, dialog.writeToNBT(new INbt())); + } catch (Exception e) { + LogWriter.except(e); + } + return dialog; + } + + public void removeDialog(Dialog dialog) { + DialogCategory category = dialog.category; + File file = new File(new File(getDir(), category.title), dialog.id + ".json"); + if (!file.delete()) + return; + category.dialogs.remove(dialog.id); + dialogs.remove(dialog.id); + SyncController.syncRemove(EnumSyncType.DIALOG, dialog.id); + } + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "dialogs"); + } + + public boolean hasDialog(int dialogId) { + return dialogs.containsKey(dialogId); + } + + public Map getScroll() { + Map map = new HashMap(); + for (DialogCategory category : categories.values()) { + map.put(category.title, category.id); + } + return map; + } + + public List categories() { + return new ArrayList(this.categories.values()); + } + + public IDialog get(int id) { + return (IDialog) this.dialogs.get(id); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/FactionController.java b/core/src/main/java/noppes/npcs/controllers/FactionController.java index f472b9279..b45969fd3 100644 --- a/core/src/main/java/noppes/npcs/controllers/FactionController.java +++ b/core/src/main/java/noppes/npcs/controllers/FactionController.java @@ -1,16 +1,23 @@ package noppes.npcs.controllers; -import noppes.npcs.core.NBT; -import noppes.npcs.controllers.data.Faction; -import kamkeel.npcs.platform.PlatformServiceHolder; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import kamkeel.npcs.platform.PlatformServiceHolder; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.data.Faction; +import noppes.npcs.core.NBT; public class FactionController { public HashMap factionsSync = new HashMap(); @@ -40,7 +47,7 @@ public class FactionController { public void load() { factions = new HashMap(); lastUsedID = 0; - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); if (saveDir == null) { return; } @@ -71,20 +78,20 @@ public class FactionController { // OLD: DataInputStream var1 = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); // OLD: loadFactions(var1); // OLD: var1.close(); - INbt nbttagcompound1 = PlatformServiceHolder.get().readCompressedNBT(file); - loadFactions(nbttagcompound1); + INbt INbt1 = PlatformServiceHolder.get().readCompressedNBT(file); + loadFactions(INbt1); } - public void loadFactions(INbt nbttagcompound1) { + public void loadFactions(INbt INbt1) { HashMap factions = new HashMap(); - lastUsedID = nbttagcompound1.getInteger("lastID"); - INbtList list = nbttagcompound1.getTagList("NPCFactions", 10); + lastUsedID = INbt1.getInteger("lastID"); + INbtList list = INbt1.getTagList("NPCFactions", 10); if (list != null) { for (int i = 0; i < list.size(); i++) { - INbt nbttagcompound = list.getCompound(i); + INbt INbt = list.getCompound(i); Faction faction = new Faction(); - faction.readNBT(nbttagcompound); + faction.readNBT(INbt); factions.put(faction.id, faction); } } @@ -99,19 +106,19 @@ public class FactionController { faction.writeNBT(nbtfactions); list.addCompound(nbtfactions); } - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("lastID", lastUsedID); - nbttagcompound.setTagList("NPCFactions", list); - return nbttagcompound; + INbt INbt = NBT.compound(); + INbt.setInteger("lastID", lastUsedID); + INbt.setTagList("NPCFactions", list); + return INbt; } public void saveFactions() { try { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); File file = new File(saveDir, "factions.dat_new"); File file1 = new File(saveDir, "factions.dat_old"); File file2 = new File(saveDir, "factions.dat"); - // OLD: CompressedStreamTools.writeCompressed(getNBT(), new FileOutputStream(file)); + // OLD: NBTIO.writeCompressed(getNBT(), new FileOutputStream(file)); PlatformServiceHolder.get().writeCompressedNBT(getNBT(), file); if (file1.exists()) { file1.delete(); @@ -154,7 +161,7 @@ public class FactionController { factions.put(faction.id, faction); // TODO: mc1710 version also calls: - // OLD: NBTTagCompound facCompound = new NBTTagCompound(); + // OLD: INbt facCompound = new INbt(); // OLD: faction.writeNBT(facCompound); // OLD: SyncController.syncUpdate(EnumSyncType.FACTION, -1, facCompound); saveFactions(); diff --git a/core/src/main/java/noppes/npcs/controllers/GlobalDataController.java b/core/src/main/java/noppes/npcs/controllers/GlobalDataController.java index 5c70e4e44..8c92cea08 100644 --- a/core/src/main/java/noppes/npcs/controllers/GlobalDataController.java +++ b/core/src/main/java/noppes/npcs/controllers/GlobalDataController.java @@ -1,11 +1,18 @@ package noppes.npcs.controllers; -import noppes.npcs.core.NBT; -import kamkeel.npcs.platform.PlatformServiceHolder; -import noppes.npcs.api.INbt; import java.io.File; - +import kamkeel.npcs.platform.PlatformServiceHolder; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.core.NBT; import static noppes.npcs.util.CustomNPCsThreader.customNPCThread; public class GlobalDataController { @@ -18,7 +25,7 @@ public class GlobalDataController { } private void load() { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); try { File file = new File(saveDir, "global.dat"); if (file.exists()) { @@ -38,28 +45,28 @@ public class GlobalDataController { } private void loadData(File file) throws Exception { - // OLD: NBTTagCompound nbttagcompound1; + // OLD: INbt INbt1; // OLD: try (FileInputStream fis = new FileInputStream(file)) { - // OLD: nbttagcompound1 = CompressedStreamTools.readCompressed(fis); + // OLD: INbt1 = NBTIO.readCompressed(fis); // OLD: } - INbt nbttagcompound1 = PlatformServiceHolder.get().readCompressedNBT(file); - itemGiverId = nbttagcompound1.getInteger("itemGiverId"); + INbt INbt1 = PlatformServiceHolder.get().readCompressedNBT(file); + itemGiverId = INbt1.getInteger("itemGiverId"); } public void saveData() { customNPCThread.execute(() -> { try { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); - // OLD: NBTTagCompound nbttagcompound = new NBTTagCompound(); - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("itemGiverId", itemGiverId); + // OLD: INbt INbt = new INbt(); + INbt INbt = NBT.compound(); + INbt.setInteger("itemGiverId", itemGiverId); File file = new File(saveDir, "global.dat_new"); File file1 = new File(saveDir, "global.dat_old"); File file2 = new File(saveDir, "global.dat"); - // OLD: CompressedStreamTools.writeCompressed(nbttagcompound, new FileOutputStream(file)); - PlatformServiceHolder.get().writeCompressedNBT(nbttagcompound, file); + // OLD: NBTIO.writeCompressed(INbt, new FileOutputStream(file)); + PlatformServiceHolder.get().writeCompressedNBT(INbt, file); if (file1.exists()) { file1.delete(); } diff --git a/core/src/main/java/noppes/npcs/controllers/HookDefinition.java b/core/src/main/java/noppes/npcs/controllers/HookDefinition.java new file mode 100644 index 000000000..4ac324edc --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -0,0 +1,303 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.handler.IHookDefinition; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +/** + * Implementation of {@link IHookDefinition} with a fluent builder API. + * + *

Usage Examples

+ *
{@code
+ * // Recommended: auto-derives imports from event class
+ * HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class);
+ *
+ * // Full control with builder
+ * HookDefinition.builder("onDBCPowerUp")
+ *     .eventClass(IDBCEvent.PowerUpEvent.class)
+ *     .cancelable(true)
+ *     .build();
+ *
+ * // Simple hook (no event metadata)
+ * HookDefinition.simple("customHook");
+ * }
+ */ +public class HookDefinition implements IHookDefinition { + + private final String hookName; + private final String eventClassName; + private final String[] paramNames; + private final String[] requiredImports; + private final boolean cancelable; + + private transient Class cachedEventClass; + private transient boolean classResolutionAttempted; + + private HookDefinition(Builder builder) { + this.hookName = builder.hookName; + this.eventClassName = builder.eventClassName; + this.paramNames = builder.paramNames != null ? builder.paramNames : new String[]{"event"}; + this.requiredImports = builder.requiredImports != null ? builder.requiredImports : new String[0]; + this.cancelable = builder.cancelable; + } + + // ==================== IHookDefinition Implementation ==================== + + @Override + public String hookName() { + return hookName; + } + + @Override + public String eventClassName() { + return eventClassName; + } + + @Override + public String[] paramNames() { + return paramNames; + } + + @Override + public String[] requiredImports() { + return requiredImports; + } + + @Override + public boolean isCancelable() { + return cancelable; + } + + @Override + public Class getEventClass() { + if (classResolutionAttempted) { + return cachedEventClass; + } + + classResolutionAttempted = true; + if (eventClassName == null || eventClassName.isEmpty()) { + return null; + } + + try { + cachedEventClass = Class.forName(eventClassName); + } catch (ClassNotFoundException e) { + cachedEventClass = null; + } + + return cachedEventClass; + } + + // ==================== Factory Methods ==================== + + /** + * Create a hook definition from a hook name and event class. + * Automatically derives imports from the event class. + * + * @param hookName The hook function name + * @param eventClass The event class (e.g., INpcEvent.InitEvent.class) + * @return A hook definition with auto-derived imports + */ + public static HookDefinition of(String hookName, Class eventClass) { + return builder(hookName).eventClass(eventClass).build(); + } + + /** + * Create a simple hook definition with just a name (no event metadata). + * + * @param hookName The hook function name + * @return A minimal hook definition + */ + public static HookDefinition simple(String hookName) { + return builder(hookName).build(); + } + + /** + * Create a new builder for a hook definition. + * + * @param hookName The hook function name + * @return A new builder instance + */ + public static Builder builder(String hookName) { + return new Builder(hookName); + } + + /** + * Create a hook definition from an interface method. + * Automatically extracts metadata from the method signature and @ParamName annotations. + * This provides backward compatibility with the annotation-based hook definition approach. + * + * @param hookName The hook name (may differ from method name) + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(String hookName, Method method) { + Builder builder = builder(hookName); + + // Extract event type from first parameter + if (method.getParameterCount() > 0) { + Class eventType = method.getParameterTypes()[0]; + builder.eventClass(eventType); + + // Auto-detect cancelable from event class + if (eventType.isAnnotationPresent(Cancelable.class)) { + builder.cancelable(true); + } + + // Build required imports from event type + String[] importNames = getImportsForClass(eventType); + if (importNames.length > 0) { + builder.requiredImports(importNames); + } + } + + // Extract parameter names from @ParamName annotations + Parameter[] params = method.getParameters(); + if (params.length > 0) { + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + ParamName annotation = params[i].getAnnotation(ParamName.class); + if (annotation != null) { + names[i] = annotation.value(); + } else { + // Fallback: use lowercase simple type name + names[i] = Character.toLowerCase(params[i].getType().getSimpleName().charAt(0)) + + params[i].getType().getSimpleName().substring(1); + } + } + builder.paramNames(names); + } + + return builder.build(); + } + + /** + * Create a hook definition from a method using its name as the hook name. + * + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(Method method) { + return fromMethod(method.getName(), method); + } + + /** + * Get the import statements needed for a class. + * For nested classes, includes both enclosing and nested names. + */ + private static String[] getImportsForClass(Class clazz) { + if (clazz == null || clazz.isPrimitive()) { + return new String[0]; + } + + String fullName = clazz.getName(); + if (fullName.startsWith("java.lang.")) { + return new String[0]; + } + + Class enclosing = clazz; + while (enclosing.getEnclosingClass() != null) { + enclosing = enclosing.getEnclosingClass(); + } + + String enclosingName = enclosing.getName(); + String nestedName = fullName.replace('$', '.'); + if (!nestedName.equals(enclosingName)) { + return new String[]{enclosingName, nestedName}; + } + + return new String[]{enclosingName}; + } + + // ==================== Builder ==================== + + public static class Builder { + private final String hookName; + private String eventClassName; + private String[] paramNames; + private String[] requiredImports; + private boolean cancelable; + + private Builder(String hookName) { + if (hookName == null || hookName.isEmpty()) { + throw new IllegalArgumentException("hookName cannot be null or empty"); + } + this.hookName = hookName; + } + + /** + * Set the event class directly. + * Automatically derives: class name, required imports, and cancelable status. + */ + public Builder eventClass(Class clazz) { + if (clazz != null) { + this.eventClassName = clazz.getName(); + + String[] importNames = getImportsForClass(clazz); + if (importNames.length > 0) { + this.requiredImports = importNames; + } + + if (clazz.isAnnotationPresent(Cancelable.class)) { + this.cancelable = true; + } + } + return this; + } + + /** + * Set the event class by name (for addon classes that may not be loaded). + */ + public Builder eventClass(String className) { + this.eventClassName = className; + return this; + } + + /** + * Set parameter names for stub generation. + */ + public Builder paramNames(String... names) { + this.paramNames = names; + return this; + } + + /** + * Set required imports (only needed if using eventClass(String)). + */ + public Builder requiredImports(String... imports) { + this.requiredImports = imports; + return this; + } + + /** + * Set whether the event is cancelable. + */ + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + return this; + } + + public HookDefinition build() { + return new HookDefinition(this); + } + } + + // ==================== Object Methods ==================== + + @Override + public String toString() { + return "HookDefinition{hookName='" + hookName + "', eventClass='" + eventClassName + "'}"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof HookDefinition)) return false; + return hookName.equals(((HookDefinition) obj).hookName); + } + + @Override + public int hashCode() { + return hookName.hashCode(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/core/src/main/java/noppes/npcs/controllers/LinkedItemController.java new file mode 100644 index 000000000..2872b7477 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -0,0 +1,434 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.handler.ILinkedItemHandler; +import noppes.npcs.api.handler.data.ILinkedItem; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.LinkedItem; +import noppes.npcs.controllers.data.LinkedItemScript; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.zip.GZIPInputStream; + +public class LinkedItemController implements ILinkedItemHandler { + private static LinkedItemController Instance; + private int lastUsedID = 0; + + public HashMap linkedItems = new HashMap<>(); + public HashMap linkedItemsScripts = new HashMap<>(); + + private HashMap bootOrder; + public CategoryManager categoryManager = new CategoryManager(); + + private LinkedItemController() { + } + + public static LinkedItemController getInstance() { + if (Instance == null) { + Instance = new LinkedItemController(); + } + return Instance; + } + + @Override + public ILinkedItem createItem(String name) { + return new LinkedItem(name); + } + + @Override + public IItemStack createItemStack(int id) { + LinkedItem linkedItem = (LinkedItem) this.get(id); + if (linkedItem != null) { + return linkedItem.createStack(); + } + return null; + } + + @Override + public void add(ILinkedItem linkedItem) { + if (linkedItem != null) { + String name = linkedItem.getName(); + if (name != null && !name.isEmpty()) { + int linkedItemId = linkedItem.getId(); + int id = this.linkedItems.containsKey(linkedItemId) ? linkedItemId : this.getUnusedId(); + this.linkedItems.put(id, (LinkedItem) linkedItem); + linkedItem.setId(id); + this.addScript(id); + } + } + } + + private void addScript(int id) { + this.linkedItemsScripts.put(id, new LinkedItemScript()); + } + + @Override + public LinkedItem remove(int id) { + this.removeScript(id); + return this.linkedItems.remove(id); + } + + private void removeScript(int id) { + this.linkedItemsScripts.remove(id); + } + + @Override + public LinkedItem get(int id) { + return (LinkedItem) this.linkedItems.get(id); + } + + @Override + public boolean contains(int id) { + return this.linkedItems.containsKey(id); + } + + @Override + public boolean contains(ILinkedItem linkedItem) { + return this.linkedItems.containsValue((LinkedItem) linkedItem); + } + + public IScriptHandler getScriptHandler(int id) { + return this.linkedItemsScripts.get(id); + } + + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + + public void load() { + lastUsedID = 0; + bootOrder = new HashMap<>(); + linkedItems = new HashMap<>(); + LogWriter.info("Loading linked items..."); + readLinkedMap(); + loadLinkedItems(); + LogWriter.info("Done loading linked items."); + } + + private void loadLinkedItems() { + linkedItems.clear(); + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdir(); + } + + categoryManager.loadCategories(dir); + + // Load uncategorized items (root level .json files) + loadItemsFromDir(dir, CategoryManager.UNCATEGORIZED_ID); + + // Load categorized items (subdirectories) + for (Map.Entry entry : categoryManager.getCategories().entrySet()) { + File catDir = categoryManager.getCategoryDir(entry.getKey()); + loadItemsFromDir(catDir, entry.getKey()); + } + + saveLinkedItemsMap(); + } + + private void loadItemsFromDir(File dir, int catId) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + LinkedItem linkedItem = new LinkedItem(); + linkedItem.readFromNBT(NBTJsonUtil.LoadFile(file)); + linkedItem.name = file.getName().substring(0, file.getName().length() - 5); + + if (linkedItem.id == -1) { + linkedItem.id = getUnusedId(); + } + + int originalID = linkedItem.id; + int setID = linkedItem.id; + while (bootOrder.containsKey(setID) || linkedItems.containsKey(setID)) { + if (bootOrder.containsKey(setID)) + if (bootOrder.get(setID).equals(linkedItem.name)) + break; + + setID++; + } + + linkedItem.id = setID; + if (originalID != setID) { + LogWriter.info("Found Linked Item ID Mismatch: " + linkedItem.name + ", New ID: " + setID); + linkedItem.save(); + } + + linkedItems.put(linkedItem.id, linkedItem); + categoryManager.registerItem(linkedItem.id, catId); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + } + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "linkeditems"); + } + + public int getUnusedId() { + if (lastUsedID == 0) { + for (int catid : linkedItems.keySet()) { + if (catid > lastUsedID) + lastUsedID = catid; + } + + } + lastUsedID++; + return lastUsedID; + } + + public boolean hasName(String newName) { + if (newName.trim().isEmpty()) + return true; + for (LinkedItem linkedItem : linkedItems.values()) + if (linkedItem.name.equals(newName)) + return true; + return false; + } + + public ILinkedItem saveLinkedItem(ILinkedItem linkedItem) { + if (linkedItem.getId() < 0) { + linkedItem.setId(getUnusedId()); + while (hasName(linkedItem.getName())) + linkedItem.setName(linkedItem.getName() + "_"); + } + + while (hasOther(linkedItem.getName(), linkedItem.getId())) + linkedItem.setName(linkedItem.getName() + "_"); + + TagController.validateTagUUIDs(((LinkedItem) linkedItem).tagUUIDs); + linkedItems.remove(linkedItem.getId()); + linkedItems.put(linkedItem.getId(), (LinkedItem) linkedItem); + saveLinkedItemsMap(); + + // Save Linked Item File + File dir = categoryManager.getItemDir(linkedItem.getId()); + if (!dir.exists()) + dir.mkdirs(); + + File file = new File(dir, linkedItem.getName() + ".json_new"); + File file2 = new File(dir, linkedItem.getName() + ".json"); + + try { + NBTJsonUtil.SaveFile(file, ((LinkedItem) linkedItem).writeToNBT(true)); + if (file2.exists()) + file2.delete(); + file.renameTo(file2); + } catch (Exception e) { + LogWriter.except(e); + } + return linkedItems.get(linkedItem.getId()); + } + + public LinkedItem cloneLinkedItem(int originalId) { + LinkedItem original = linkedItems.get(originalId); + if (original == null) return null; + + int originalCatId = categoryManager.getItemCategory(originalId); + + INbt nbt = original.writeToNBT(true); + int newId = getUnusedId(); + nbt.setInteger("Id", newId); + + LinkedItem clone = new LinkedItem(); + clone.readFromNBT(nbt); + + String name = clone.getName(); + while (hasName(name)) name += "_"; + clone.name = name; + + if (originalCatId > CategoryManager.UNCATEGORIZED_ID) { + categoryManager.registerItem(newId, originalCatId); + } + + saveLinkedItem(clone); + return clone; + } + + private boolean hasOther(String name, int id) { + for (LinkedItem linkedItem : linkedItems.values()) { + if (linkedItem.getId() != id && linkedItem.getName().equalsIgnoreCase(name)) + return true; + } + return false; + } + + public void delete(int id) { + LinkedItem linkedItem = (LinkedItem) get(id); + if (linkedItem != null) { + LinkedItem foundItem = remove(id); + if (foundItem != null && foundItem.name != null) { + File dir = categoryManager.getItemDir(id); + File file = new File(dir, foundItem.name + ".json"); + if (file.exists()) { + file.delete(); + } + categoryManager.removeItem(id); + saveLinkedItemsMap(); + } + } + } + + public void deleteLinkedItemFile(String prevName) { + categoryManager.deleteFile(prevName + ".json"); + } + + + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + // LINKED ITEMS MAP + public File getMapDir() { + File dir = CustomNpcs.getWorldSaveDirectory(); + if (!dir.exists()) + dir.mkdir(); + return dir; + } + + public void readLinkedMap() { + bootOrder.clear(); + + try { + File file = new File(getMapDir(), "linkeditems.dat"); + if (file.exists()) { + loadLinkedMap(file); + } + } catch (Exception e) { + try { + File file = new File(getMapDir(), "linkeditems.dat_old"); + if (file.exists()) { + loadLinkedMap(file); + } + } catch (Exception ignored) { + } + } + } + + public INbt writeMapNBT() { + INbt nbt = new INbt(); + INbtList linkedList = new INbtList(); + for (Integer key : linkedItems.keySet()) { + LinkedItem linkedItem = linkedItems.get(key); + if (!linkedItem.getName().isEmpty()) { + INbt linkedCompound = new INbt(); + linkedCompound.setString("Name", linkedItem.getName()); + linkedCompound.setInteger("ID", key); + + linkedList.appendTag(linkedCompound); + } + } + nbt.setTag("LinkedItems", linkedList); + nbt.setInteger("lastID", lastUsedID); + return nbt; + } + + public void readMapNBT(INbt compound) { + lastUsedID = compound.getInteger("lastID"); + INbtList list = compound.getTagList("LinkedItems", 10); + if (list != null) { + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + String linkedName = INbt.getString("Name"); + Integer key = INbt.getInteger("ID"); + bootOrder.put(key, linkedName); + } + } + } + + private void loadLinkedMap(File file) throws IOException { + DataInputStream var1 = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); + readLinkedMap(var1); + var1.close(); + } + + public void readLinkedMap(DataInputStream stream) throws IOException { + INbt nbtCompound = CompressedStreamTools.read(stream); + this.readMapNBT(nbtCompound); + } + + public void saveLinkedItemsMap() { + try { + File saveDir = getMapDir(); + File file = new File(saveDir, "linkeditems.dat_new"); + File file1 = new File(saveDir, "linkeditems.dat_old"); + File file2 = new File(saveDir, "linkeditems.dat"); + NBTIO.writeCompressed(this.writeMapNBT(), new FileOutputStream(file)); + if (file1.exists()) { + file1.delete(); + } + file2.renameTo(file1); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + LogWriter.except(e); + } + } + + //////////////////////////////////////////////////////// + // CATEGORY HELPERS + //////////////////////////////////////////////////////// + + public Map getCategoryScrollData() { + return categoryManager.getCategoryScrollData(); + } + + public void moveItemToCategory(int itemId, int catId) { + LinkedItem item = linkedItems.get(itemId); + if (item == null) return; + categoryManager.moveItem(itemId, item.name + ".json", catId); + saveLinkedItemsMap(); + } + + public Map getItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, linkedItems.keySet()); + for (int itemId : itemIds) { + LinkedItem item = linkedItems.get(itemId); + if (item != null) { + map.put(item.name, item.id); + } + } + return map; + } + + public HashMap> getItemTagMapForCategory(int catId) { + HashMap> tagMap = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, linkedItems.keySet()); + for (int itemId : itemIds) { + LinkedItem item = linkedItems.get(itemId); + if (item != null && !item.tagUUIDs.isEmpty()) { + tagMap.put(item.name, item.tagUUIDs); + } + } + return tagMap; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/LinkedNpcController.java b/core/src/main/java/noppes/npcs/controllers/LinkedNpcController.java new file mode 100644 index 000000000..b226bd99d --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/LinkedNpcController.java @@ -0,0 +1,224 @@ +package noppes.npcs.controllers; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.NBTTags; +import noppes.npcs.util.JsonException; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class LinkedNpcController { + public static LinkedNpcController Instance; + public List list = new ArrayList(); + + public LinkedNpcController() { + Instance = this; + load(); + } + + private void load() { + try { + loadNpcs(); + } catch (Exception e) { + LogWriter.except(e); + } + } + + public File getDir() { + File dir = new File(CustomNpcs.getWorldSaveDirectory(), "linkednpcs"); + if (!dir.exists()) + dir.mkdir(); + return dir; + } + + private void loadNpcs() { + LogWriter.info("Loading Linked Npcs"); + File dir = getDir(); + if (dir.exists()) { + List list = new ArrayList(); + for (File file : dir.listFiles()) { + if (file.getName().endsWith(".json")) { + try { + INbt compound = NBTJsonUtil.LoadFile(file); + ; + LinkedData linked = new LinkedData(); + linked.setNBT(compound); + list.add(linked); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + } + + this.list = list; + } + LogWriter.info("Done loading Linked Npcs"); + } + + public void save() { + for (LinkedData npc : list) { + try { + saveNpc(npc); + } catch (IOException e) { + LogWriter.except(e); + } + } + } + + private void saveNpc(LinkedData npc) throws IOException { + File file = new File(getDir(), npc.name + ".json_new"); + File file1 = new File(getDir(), npc.name + ".json"); + try { + NBTJsonUtil.SaveFile(file, npc.getNBT()); + if (file1.exists()) { + file1.delete(); + } + file.renameTo(file1); + + } catch (JsonException e) { + LogWriter.except(e); + } + } + + public static class LinkedData { + public String name = "LinkedNpc"; + public long time = 0; + public INbt data = new INbt(); + + public LinkedData() { + time = System.currentTimeMillis(); + } + + public void setNBT(INbt compound) { + name = compound.getString("LinkedName"); + data = compound.getCompoundTag("NPCData"); + } + + public INbt getNBT() { + INbt compound = new INbt(); + compound.setString("LinkedName", name); + compound.setTag("NPCData", data); + return compound; + } + } + + public void loadNpcData(EntityNPCInterface npc) { + if (npc.linkedName.isEmpty()) + return; + LinkedData data = getData(npc.linkedName); + if (data == null) { + npc.linkedLast = 0; + npc.linkedName = ""; + npc.linkedData = null; + } else { + npc.linkedData = data; + if (npc.posX == 0 && npc.posY == 0 && npc.posZ == 0) + return; + npc.linkedLast = data.time; + List points = npc.ais.getMovingPath(); + + INbt compound = NBTTags.NBTMerge(readNpcData(npc), data.data); + + npc.display.readToNBT(compound); + npc.stats.readToNBT(compound); + npc.advanced.readToNBT(compound); + npc.inventory.readEntityFromNBT(compound); + if (compound.hasKey("ModelData")) + ((EntityCustomNpc) npc).modelData.readFromNBT(compound.getCompoundTag("ModelData")); + + npc.ais.readToNBT(compound); + npc.transform.readToNBT(compound); + npc.abilities.readFromNBT(compound); + npc.ais.setMovingPath(points); + npc.updateClient = true; + } + } + + private void cleanTags(INbt compound) { + compound.removeTag("MovingPathNew"); + } + + public LinkedData getData(String name) { + for (LinkedData data : list) { + if (data.name.equalsIgnoreCase(name)) + return data; + } + return null; + } + + private INbt readNpcData(EntityNPCInterface npc) { + INbt compound = new INbt(); + npc.display.writeToNBT(compound); + npc.inventory.writeEntityToNBT(compound); + npc.stats.writeToNBT(compound); + npc.ais.writeToNBT(compound); + npc.advanced.writeToNBT(compound); + npc.transform.writeToNBT(compound); + npc.abilities.writeToNBT(compound); + compound.setTag("ModelData", ((EntityCustomNpc) npc).modelData.writeToNBT()); + return compound; + } + + public void saveNpcData(EntityNPCInterface npc) { + INbt compound = readNpcData(npc); + cleanTags(compound); + + if (npc.linkedData.data.equals(compound)) + return; + + npc.linkedData.data = compound; + npc.linkedData.time = System.currentTimeMillis(); + save(); + } + + public void removeData(String name) { + Iterator ita = list.iterator(); + while (ita.hasNext()) { + if (ita.next().name.equalsIgnoreCase(name)) + ita.remove(); + } + save(); + } + + public void addData(String name) { + if (getData(name) != null || name.isEmpty()) + return; + + LinkedData data = new LinkedData(); + data.name = name; + list.add(data); + save(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/MagicController.java b/core/src/main/java/noppes/npcs/controllers/MagicController.java index 4cfa4edf4..15d9e17fc 100644 --- a/core/src/main/java/noppes/npcs/controllers/MagicController.java +++ b/core/src/main/java/noppes/npcs/controllers/MagicController.java @@ -1,16 +1,23 @@ package noppes.npcs.controllers; -import noppes.npcs.constants.EnumDiagramLayout; -import noppes.npcs.core.NBT; -import noppes.npcs.controllers.data.Magic; -import noppes.npcs.controllers.data.MagicAssociation; -import noppes.npcs.controllers.data.MagicCycle; -import kamkeel.npcs.platform.PlatformServiceHolder; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; import java.io.File; import java.util.HashMap; +import kamkeel.npcs.platform.PlatformServiceHolder; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumDiagramLayout; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicAssociation; +import noppes.npcs.controllers.data.MagicCycle; +import noppes.npcs.core.NBT; public class MagicController { public HashMap magics = new HashMap<>(); @@ -52,7 +59,7 @@ public class MagicController { lastUsedCycleID = 0; lastUsedMagicID = 0; - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); if (saveDir == null) return; try { File file = new File(saveDir, "magic.dat"); @@ -75,18 +82,18 @@ public class MagicController { } /** - * Creates default magic entries. Override in mc1710 to set ItemStack items. + * Creates default magic entries. Override in mc1710 to set IItemStack items. */ protected void createDefaults() { - // TODO: mc1710 version sets ItemStack items on each magic: - // OLD: earth.setItem(new ItemStack(CustomItems.earthElement)); - // OLD: water.setItem(new ItemStack(CustomItems.waterElement)); - // OLD: fire.setItem(new ItemStack(CustomItems.spellFire)); - // OLD: air.setItem(new ItemStack(CustomItems.airElement)); - // OLD: dark.setItem(new ItemStack(CustomItems.spellDark)); - // OLD: holy.setItem(new ItemStack(CustomItems.spellHoly)); - // OLD: nature.setItem(new ItemStack(CustomItems.spellNature)); - // OLD: arcane.setItem(new ItemStack(CustomItems.spellArcane)); + // TODO: mc1710 version sets IItemStack items on each magic: + // OLD: earth.setItem(new IItemStack(CustomItems.earthElement)); + // OLD: water.setItem(new IItemStack(CustomItems.waterElement)); + // OLD: fire.setItem(new IItemStack(CustomItems.spellFire)); + // OLD: air.setItem(new IItemStack(CustomItems.airElement)); + // OLD: dark.setItem(new IItemStack(CustomItems.spellDark)); + // OLD: holy.setItem(new IItemStack(CustomItems.spellHoly)); + // OLD: nature.setItem(new IItemStack(CustomItems.spellNature)); + // OLD: arcane.setItem(new IItemStack(CustomItems.spellArcane)); Magic earth = new Magic(getUnusedId(), "Earth", 0x00DD00); Magic water = new Magic(getUnusedId(), "Water", 0xF2DD00); Magic fire = new Magic(getUnusedId(), "Fire", 0xDD0000); @@ -235,11 +242,11 @@ public class MagicController { public void saveMagicData() { try { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); File fileNew = new File(saveDir, "magic.dat_new"); File fileOld = new File(saveDir, "magic.dat_old"); File fileCurrent = new File(saveDir, "magic.dat"); - // OLD: CompressedStreamTools.writeCompressed(getNBT(), new FileOutputStream(fileNew)); + // OLD: NBTIO.writeCompressed(getNBT(), new FileOutputStream(fileNew)); PlatformServiceHolder.get().writeCompressedNBT(getNBT(), fileNew); if (fileOld.exists()) fileOld.delete(); @@ -269,7 +276,7 @@ public class MagicController { magics.put(mag.id, mag); // TODO: mc1710 version also calls: - // OLD: NBTTagCompound magicCompound = new NBTTagCompound(); + // OLD: INbt magicCompound = new INbt(); // OLD: mag.writeNBT(magicCompound); // OLD: SyncController.syncUpdate(EnumSyncType.MAGIC, -1, magicCompound); saveMagicData(); @@ -337,7 +344,7 @@ public class MagicController { cycles.put(cycle.id, cycle); // TODO: mc1710 version also calls: - // OLD: NBTTagCompound cycleCompound = new NBTTagCompound(); + // OLD: INbt cycleCompound = new INbt(); // OLD: cycle.writeNBT(new NBTWrapper(cycleCompound)); // OLD: SyncController.syncUpdate(EnumSyncType.MAGIC_CYCLE, -1, cycleCompound); saveMagicData(); diff --git a/core/src/main/java/noppes/npcs/controllers/MarketRegistry.java b/core/src/main/java/noppes/npcs/controllers/MarketRegistry.java new file mode 100644 index 000000000..c0f2dde63 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/MarketRegistry.java @@ -0,0 +1,220 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import foxz.utils.Market; +import noppes.npcs.roles.RoleTrader; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Central registry for linked market synchronization. + * Tracks which RoleTrader instances use which markets, + * and which players are viewing each market. + *

+ * This enables immediate sync across all traders sharing + * the same market when a purchase occurs. + */ +public class MarketRegistry { + + // marketName -> Set of traders using this market (WeakHashMap for auto-cleanup) + private static final Map> marketTraders = new ConcurrentHashMap<>(); + + // marketName -> Set of players viewing this market (across all NPCs) + private static final Map> marketViewers = new ConcurrentHashMap<>(); + + // ==================== Trader Registration ==================== + + /** + * Register a trader as using a linked market. + * Should be called when NPC loads or marketName is set. + */ + public static void registerTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (traders) { + traders.add(trader); + } + } + + /** + * Unregister a trader from a linked market. + * Should be called when NPC unloads or marketName changes. + */ + public static void unregisterTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.get(marketName); + if (traders != null) { + synchronized (traders) { + traders.remove(trader); + if (traders.isEmpty()) { + marketTraders.remove(marketName); + } + } + } + } + + /** + * Change a trader's market registration. + * Handles unregistering from old market and registering to new. + */ + public static void updateTraderMarket(String oldMarket, String newMarket, RoleTrader trader) { + if (trader == null) return; + + if (oldMarket != null && !oldMarket.isEmpty()) { + unregisterTrader(oldMarket, trader); + } + if (newMarket != null && !newMarket.isEmpty()) { + registerTrader(newMarket, trader); + } + } + + // ==================== Viewer Registration ==================== + + /** + * Register a player as viewing a market (via any NPC). + * Should be called when player opens trader GUI with linked market. + */ + public static void registerViewer(String marketName, IPlayer player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (viewers) { + viewers.add(player); + } + } + + /** + * Unregister a player from viewing a market. + * Should be called when player closes trader GUI. + */ + public static void unregisterViewer(String marketName, IPlayer player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.get(marketName); + if (viewers != null) { + synchronized (viewers) { + viewers.remove(player); + if (viewers.isEmpty()) { + marketViewers.remove(marketName); + } + } + } + } + + // ==================== Synchronization ==================== + + /** + * Sync stock data to all traders and viewers of a market. + * Called after a purchase to update all linked traders and their viewers. + * + * @param marketName The market to sync + */ + public static void syncMarket(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + // Get latest data from cache + INbt marketData = Market.getMarketCache(marketName); + if (marketData == null) return; + + // Update all traders' internal stock data + Set traders = marketTraders.get(marketName); + if (traders != null) { + INbt stockData = marketData.getCompoundTag("Stock"); + + synchronized (traders) { + // Clean up any null references (WeakHashMap cleanup) + traders.removeIf(t -> t == null || t.npc == null); + + for (RoleTrader trader : traders) { + // Update stock data from cache + if (stockData != null && stockData.hasKey("EnableStock")) { + trader.stock.readFromNBT(new NBTWrapper(stockData)); + } + } + } + } + + // Sync all viewers (across all NPCs for this market) + syncMarketViewers(marketName); + } + + /** + * Sync stock data to all players viewing a specific market. + * Each viewer gets data relative to their own player (for per-player stock display). + */ + public static void syncMarketViewers(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + Set viewers = marketViewers.get(marketName); + if (viewers == null || viewers.isEmpty()) return; + + // Find any trader with this market to use for sync (they all have same data now) + Set traders = marketTraders.get(marketName); + if (traders == null || traders.isEmpty()) return; + + RoleTrader syncSource = null; + synchronized (traders) { + for (RoleTrader trader : traders) { + if (trader != null && trader.npc != null) { + syncSource = trader; + break; + } + } + } + + if (syncSource == null) return; + + // Sync to all viewers + synchronized (viewers) { + // Remove disconnected players + viewers.removeIf(p -> p == null || p.playerNetServerHandler == null); + + for (IPlayer viewer : viewers) { + syncSource.syncToPlayer(viewer); + } + } + } + + // ==================== Utility ==================== + + /** + * Get count of traders registered to a market (for debugging/info). + */ + public static int getTraderCount(String marketName) { + Set traders = marketTraders.get(marketName); + return traders != null ? traders.size() : 0; + } + + /** + * Get count of viewers for a market (for debugging/info). + */ + public static int getViewerCount(String marketName) { + Set viewers = marketViewers.get(marketName); + return viewers != null ? viewers.size() : 0; + } + + /** + * Clear all registrations (for server shutdown/reload). + */ + public static void clear() { + marketTraders.clear(); + marketViewers.clear(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/PartyController.java b/core/src/main/java/noppes/npcs/controllers/PartyController.java new file mode 100644 index 000000000..cbd332dc7 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/PartyController.java @@ -0,0 +1,266 @@ +package noppes.npcs.controllers; + + +import java.util.HashMap; +import java.util.UUID; +import java.util.Vector; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IParty; +import noppes.npcs.api.handler.IPartyHandler; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumQuestCompletion; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.Quest; +import noppes.npcs.controllers.data.QuestData; +import noppes.npcs.EventHooks; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.quests.QuestInterface; +import noppes.npcs.quests.QuestItem; + +public class PartyController implements IPartyHandler { + private static PartyController Instance; + + private HashMap parties = new HashMap<>(); + + private PartyController() { + } + + public static PartyController Instance() { + if (Instance == null) { + Instance = new PartyController(); + } + return Instance; + } + + public Party createParty() { + Party party = new Party(); + this.parties.put(party.getPartyUUID(), party); + return party; + } + + @Override + public IParty createParty(IPlayer player) { + if (player == null) + return null; + + if (player.getMCEntity() == null) + return null; + + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + PlayerData playerData = PlayerData.get(IPlayer); + if (playerData.partyUUID != null) + return getParty(playerData.partyUUID); + + Party party = new Party(); + party.addPlayer(IPlayer); + party.setLeader(IPlayer); + this.parties.put(party.getPartyUUID(), party); + return party; + } + + @Override + public void disbandParty(IPlayer player) { + if (player == null) + return; + + if (player.getMCEntity() == null) + return; + + IPlayer IPlayer = (IPlayer) player.getMCEntity(); + PlayerData playerData = PlayerData.get(IPlayer); + if (playerData.partyUUID != null) + disbandParty(playerData.partyUUID); + + return; + } + + public Party getParty(UUID partyUUID) { + return this.parties.get(partyUUID); + } + + public void disbandParty(UUID partyUUID) { + Party party = this.parties.get(partyUUID); + if (party != null) { + PartyEvent.PartyDisbandEvent partyEvent = new PartyEvent.PartyDisbandEvent(party, party.getQuest()); + EventHooks.onPartyDisband(party, partyEvent); + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer player = NoppesUtilServer.getPlayer(uuid); + PlayerData playerData; + if (player != null) { + playerData = PlayerData.get(player); + } else { + playerData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (playerData != null) { + playerData.partyUUID = null; + if (player != null) { + sendInviteData((IPlayer) player); + AchievementPacket.sendAchievement((IPlayer) player, true, "party.disbandAlert", ""); + ChatAlertPacket.sendChatAlert((IPlayer) player, "\u00A7c", "party.disbandMessage", "!"); + } + } + } + this.parties.remove(partyUUID); + } + } + + public void sendKickMessages(Party party, IPlayer kickPlayer, String kickPlayerName) { + if (party == null) + return; + + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP != null) { + AchievementPacket.sendAchievement((IPlayer) playerMP, true, "party.kickOtherAlert", kickPlayerName); + ChatAlertPacket.sendChatAlert((IPlayer) playerMP, "\u00A7e", kickPlayerName, " \u00A74", "party.kickOtherChat", "!"); + } + } + + if (kickPlayer != null) { + AchievementPacket.sendAchievement((IPlayer) kickPlayer, true, "party.kickYouAlert", ""); + ChatAlertPacket.sendChatAlert((IPlayer) kickPlayer, "\u00A74", "party.kickYouChat", "!"); + } + } + + public void sendLeavingMessages(Party party, IPlayer leavingPlayer) { + if (leavingPlayer == null || party == null) + return; + + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP != null) { + AchievementPacket.sendAchievement((IPlayer) playerMP, true, "party.leaveOtherAlert", leavingPlayer.getCommandSenderName()); + ChatAlertPacket.sendChatAlert((IPlayer) playerMP, "\u00A7e", leavingPlayer.getCommandSenderName(), " \u00A7c", "party.leaveOtherChat", "!"); + } + } + AchievementPacket.sendAchievement((IPlayer) leavingPlayer, true, "party.leaveYouAlert", ""); + ChatAlertPacket.sendChatAlert((IPlayer) leavingPlayer, "\u00A7c", "party.leaveYouChat", "!"); + } + + public void pingPartyUpdate(Party party) { + if (party == null) + return; + + INbt compound = party.writeToNBT(); + if (party.getQuest() != null) { + Quest quest = (Quest) party.getQuest(); + Vector vector = quest.questInterface.getPartyQuestLogStatus(party); + compound.setString("QuestName", quest.getCategory().getName() + ":" + quest.getName()); + INbtList list = new INbtList(); + for (String s : vector) { + list.appendTag(new NBTTagString(s)); + } + compound.setTag("QuestProgress", list); + if (quest.completion == EnumQuestCompletion.Npc && quest.questInterface.isPartyCompleted(party)) { + compound.setString("QuestCompleteWith", quest.completerNpc); + } + } + + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP != null) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData != null) { + PartyDataPacket.sendPartyData((IPlayer) playerMP, compound); + } + } + } + } + + public void pingPartyQuestObjectiveUpdate(Party party) { + if (party == null) + return; + + INbt compound = new INbt(); + compound.setInteger("QuestPing", 0); + if (party.getQuest() != null) { + Quest quest = (Quest) party.getQuest(); + Vector vector = quest.questInterface.getPartyQuestLogStatus(party); + INbtList list = new INbtList(); + compound.setString("QuestName", quest.getCategory().getName() + ":" + quest.getName()); + for (String s : vector) { + list.appendTag(new NBTTagString(s)); + } + compound.setTag("QuestProgress", list); + if (quest.completion == EnumQuestCompletion.Npc && quest.questInterface.isPartyCompleted(party)) { + compound.setString("QuestCompleteWith", quest.completerNpc); + } + } + + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP != null) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData != null) { + PartyDataPacket.sendPartyData((IPlayer) playerMP, compound); + } + } + } + } + + public void sendQuestChat(Party party, String... chatAlerts) { + if (party == null) + return; + + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP != null) { + Object[] args = new Object[2 + chatAlerts.length]; + args[0] = "\u00A7a"; + + System.arraycopy(chatAlerts, 0, args, 1, chatAlerts.length); + args[args.length - 1] = "!"; // Add "!" at the end + ChatAlertPacket.sendChatAlert((IPlayer) playerMP, args); + } + } + } + + public boolean checkQuestCompletion(Party party, EnumQuestType type) { + QuestData questData = party.getQuestData(); + if (questData != null) { + if (questData.quest.type == type || type == null) { + QuestInterface inter = questData.quest.questInterface; + if (inter.isPartyCompleted(party)) { + if ((!questData.isCompleted && questData.quest.completion == EnumQuestCompletion.Npc) || questData.quest.instantPartyComplete(party)) { + questData.isCompleted = true; + if (questData.quest.completion == EnumQuestCompletion.Npc) { + EventHooks.onPartyFinished(party, questData.quest); + PartyController.Instance().sendQuestChat(party, "party.turnedInChat"); + } else { + PartyController.Instance().sendQuestChat(party, "party.completeChat"); + } + PartyController.Instance().pingPartyUpdate(party); + } + } else { + questData.isCompleted = false; + } + + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP != null) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData != null) { + if (playerData.questData.getTrackedQuest() != null && questData.quest.getId() == playerData.questData.getTrackedQuest().getId()) { + NoppesUtilPlayer.sendPartyTrackedQuestData((IPlayer) playerMP, party); + } + } + } + } + } + } + + QuestItem.pickedUpParty = null; + QuestItem.pickedUpPlayer = null; + return true; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/PlayerDataController.java b/core/src/main/java/noppes/npcs/controllers/PlayerDataController.java new file mode 100644 index 000000000..cea2dcb40 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/PlayerDataController.java @@ -0,0 +1,601 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.controllers.data.Bank; +import noppes.npcs.controllers.data.PlayerBankData; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerDataScript; +import noppes.npcs.controllers.data.PlayerMail; +import noppes.npcs.util.CacheHashMap; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.zip.GZIPInputStream; + +import static noppes.npcs.util.CustomNPCsThreader.customNPCThread; + +public class PlayerDataController { + public static PlayerDataController Instance; + public HashMap nameUUIDs; + private final CacheHashMap> playerDataCache = new CacheHashMap<>(60 * 60 * 1000); + + public PlayerDataController() { + Instance = this; + File dir = getSaveDir(); + + LogWriter.info("Loading PlayerData..."); + // If no PlayerData MAP - Load PlayerData to create PlayerData Map + if (!getPlayerDataMap()) { + LogWriter.info("Generating PlayerData map file..."); + File[] files = dir.listFiles(); // Get an array of all files in the directory + HashMap map = new HashMap(); + if (files != null) { + int length = files.length; + if (length != 0) { + if (length > 100) { + LogWriter.info("Found " + length + " PlayerData files... This may take a few minutes"); + } + int tenPercent = (int) ((double) length * 0.1); + int progress = 0; + // Load the files in parallel using a stream + for (int i = 0; i < length; i++) { + File file = files[i]; + if (file.isDirectory() || (!file.getName().endsWith(".json") && !file.getName().endsWith(".dat"))) + continue; + try { + if (file.getName().endsWith(".json")) { + INbt compound = NBTJsonUtil.LoadFile(file); + if (compound.hasKey("PlayerName")) { + map.put(compound.getString("PlayerName"), file.getName().substring(0, file.getName().length() - 5)); + } + } + if (file.getName().endsWith(".dat")) { + INbt compound = NBTJsonUtil.loadNBTData(file); + if (compound.hasKey("PlayerName")) { + map.put(compound.getString("PlayerName"), file.getName().substring(0, file.getName().length() - 4)); + } + } + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + if (tenPercent != 0) { + if (progress != 100) { + if (i % tenPercent == 0) { + progress += 10; + LogWriter.info("Creating PlayerMap: Progress: " + progress + "%"); + } + } + } + } + } + } + nameUUIDs = map; + savePlayerDataMap(); + } + + LogWriter.info("Done loading PlayerData"); + } + + public boolean getPlayerDataMap() { + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "playerdatamap.dat"); + if (file.exists()) { + loadPlayerDataMap(file); + return true; + } + } catch (Exception e) { + LogWriter.except(e); + } + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "playerdatamap.dat_old"); + if (file.exists()) { + loadPlayerDataMap(file); + return true; + } + } catch (Exception e2) { + LogWriter.except(e2); + } + + return false; + } + + private void loadPlayerDataMap(File file) throws IOException { + DataInputStream var1 = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); + loadPlayerDatasMap(var1); + var1.close(); + } + + + public void loadPlayerDatasMap(DataInputStream stream) throws IOException { + INbt INbt = CompressedStreamTools.read(stream); + readNBT(INbt); + } + + public synchronized void savePlayerDataMap() { + customNPCThread.execute(() -> { + try { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + File file = new File(saveDir, "playerdatamap.dat_new"); + File file1 = new File(saveDir, "playerdatamap.dat_old"); + File file2 = new File(saveDir, "playerdatamap.dat"); + NBTIO.writeCompressed(writeNBT(), new FileOutputStream(file)); + if (file1.exists()) { + file1.delete(); + } + file2.renameTo(file1); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + LogWriter.except(e); + } + }); + } + + public File getSaveDir() { + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "playerdata"); + if (!file.exists()) + file.mkdir(); + return file; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public File getNewSaveDir() { + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "playerdata_new"); + if (file.exists()) { + return null; + } + file.mkdir(); + return file; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public INbt loadPlayerDataOld(String player) { + File saveDir = getSaveDir(); + String filename = player; + if (filename.isEmpty()) + filename = "noplayername"; + filename += ".dat"; + try { + File file = new File(saveDir, filename); + if (file.exists()) { + INbt comp; + try (FileInputStream fis = new FileInputStream(file)) { + comp = NBTIO.readCompressed(fis); + } + file.delete(); + file = new File(saveDir, filename + "_old"); + if (file.exists()) + file.delete(); + return comp; + } + } catch (Exception e) { + LogWriter.except(e); + } + try { + File file = new File(saveDir, filename + "_old"); + if (file.exists()) { + try (FileInputStream fis = new FileInputStream(file)) { + return NBTIO.readCompressed(fis); + } + } + + } catch (Exception e) { + LogWriter.except(e); + } + + return new INbt(); + } + + public INbt loadPlayerData(String player) { + File saveDir = getSaveDir(); + String filename = player; + if (filename.isEmpty()) + filename = "noplayername"; + + if (ConfigMain.DatFormat) { + filename += ".dat"; + } else { + filename += ".json"; + } + try { + File file = new File(saveDir, filename); + if (file.exists()) { + if (ConfigMain.DatFormat) { + return NBTJsonUtil.loadNBTData(file); + } else { + return NBTJsonUtil.LoadFile(file); + } + } + } catch (Exception e) { + LogWriter.error("Error loading: " + filename, e); + } + + return new INbt(); + } + + public void putPlayerDataCache(final String uuid, final PlayerData playerCompound) { + synchronized (playerDataCache) { + playerDataCache.put(uuid, new CacheHashMap.CachedObject<>(playerCompound)); + } + } + + public PlayerData getPlayerDataCache(final String uuid) { + synchronized (playerDataCache) { + if (!playerDataCache.containsKey(uuid)) { + return null; + } + return playerDataCache.get(uuid).getObject(); + } + } + + public void removePlayerDataCache(final String uuid) { + synchronized (playerDataCache) { + playerDataCache.remove(uuid); + } + } + + public void clearCache() { + synchronized (playerDataCache) { + playerDataCache.clear(); + } + } + + public PlayerBankData getBankData(IPlayer player, int bankId) { + Bank bank = BankController.getInstance().getBank(bankId); + PlayerBankData data = PlayerData.get(player).bankData; + if (!data.hasBank(bank.id)) { + data.loadNew(bank.id); + } + return data; + } + + public ArrayList getAllPlayerData() { + ArrayList playerDataList = new ArrayList<>(); + List list = PlatformServiceHolder.get().getServer().getConfigurationManager().playerEntityList; + for (Object o : list) { + if (o instanceof IPlayer) { + playerDataList.add(PlayerData.get((IPlayer) o)); + } + } + return playerDataList; + } + + public PlayerData getPlayerData(IPlayer player) { + PlayerData data = getPlayerDataCache(player.getUniqueID().toString()); + if (data != null) { + data.player = player; + return data; + } + + data = (PlayerData) player.getExtendedProperties("CustomNpcsData"); + if (data == null) { + player.registerExtendedProperties("CustomNpcsData", data = new PlayerData()); + data.player = player; + data.scriptData = new PlayerDataScript(player); + data.load(); + } + + data.player = player; + return data; + } + + public static IPlayer getPlayerFromUUID(UUID uuid) { + MinecraftServer server = PlatformServiceHolder.get().getServer(); + if (server != null) { + for (Object playerObj : server.getConfigurationManager().playerEntityList) { + if (playerObj instanceof IPlayer) { + IPlayer player = (IPlayer) playerObj; + if (player.getUniqueID().equals(uuid)) { + return player; + } + } + } + } + return null; + } + + public String hasPlayer(String username) { + for (String name : nameUUIDs.keySet()) { + if (name.equalsIgnoreCase(username)) + return name; + } + + return ""; + } + + public String getPlayerUUIDFromName(String username) { + for (String name : nameUUIDs.keySet()) { + if (name.equalsIgnoreCase(username)) + return nameUUIDs.get(name); + } + + return ""; + } + + public PlayerData getDataFromUsername(String username) { + IPlayer player = PlatformServiceHolder.get().getServer().getConfigurationManager().func_152612_a(username); + PlayerData data = null; + if (player == null) { + for (String name : nameUUIDs.keySet()) { + if (name.equalsIgnoreCase(username)) { + data = new PlayerData(); + data.setNBT(PlayerDataController.Instance.loadPlayerData(nameUUIDs.get(name))); + break; + } + } + } else + data = PlayerData.get(player); + + return data; + } + + public PlayerData getData(UUID uuid) { + IPlayer player = getPlayerFromUUID(uuid); + PlayerData data; + if (player == null) { + data = new PlayerData(); + data.setNBT(PlayerDataController.Instance.loadPlayerData(uuid.toString())); + } else + data = PlayerData.get(player); + return data; + } + + public void addPlayerMessage(String username, PlayerMail mail) { + mail.time = System.currentTimeMillis(); + + IPlayer player = PlatformServiceHolder.get().getServer().getConfigurationManager().func_152612_a(username); + PlayerData data = getDataFromUsername(username); + data.mailData.playermail.add(mail.copy()); + data.save(); + } + + public List getPlayersData(ICommandSender sender, String username) { + ArrayList list = new ArrayList(); + IPlayer[] players = PlayerSelector.matchPlayers(sender, username); + if (players == null || players.length == 0) { + PlayerData data = PlayerDataController.Instance.getDataFromUsername(username); + if (data != null) + list.add(data); + } else { + for (IPlayer player : players) { + list.add(PlayerData.get(player)); + } + } + + return list; + } + + public void putPlayerMap(String playerName, String uuid) { + nameUUIDs.put(playerName, uuid); + savePlayerDataMap(); + } + + public boolean hasMail(IPlayer player) { + return PlayerData.get(player).mailData.hasMail(); + } + + public void readNBT(INbt compound) { + this.nameUUIDs = new HashMap(); + INbtList list = compound.getTagList("PlayerDataMap", 10); + if (list != null) { + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + String playerName = INbt.getString("Name"); + String uuid = INbt.getString("UUID"); + nameUUIDs.put(playerName, uuid); + } + } + } + + public INbt writeNBT() { + INbt nbt = new INbt(); + INbtList playerList = new INbtList(); + for (String key : nameUUIDs.keySet()) { + INbt playerCompound = new INbt(); + playerCompound.setString("Name", key); + playerCompound.setString("UUID", nameUUIDs.get(key)); + playerList.appendTag(playerCompound); + + } + nbt.setTag("PlayerDataMap", playerList); + return nbt; + } + + public void generatePlayerMap(IPlayer sender) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + if (sender != null) { + LogWriter.info("PlayerMap regeneration queued by " + sender.getCommandSenderName()); + sender.addChatMessage(new ITextComponent("You have initiated PlayerMap regeneration")); + } + nameUUIDs.clear(); + + File dir = getSaveDir(); + LogWriter.info("Generating PlayerData map file..."); + File[] files = dir.listFiles(); // Get an array of all files in the directory + HashMap map = new HashMap(); + if (files != null) { + int length = files.length; + if (length != 0) { + if (length > 100) { + LogWriter.info("Found " + length + " PlayerData files... This may take a few minutes"); + } + int tenPercent = (int) ((double) length * 0.1); + int progress = 0; + // Load the files in parallel using a stream + for (int i = 0; i < length; i++) { + File file = files[i]; + if (file.isDirectory() || (!file.getName().endsWith(".json") && !file.getName().endsWith(".dat"))) + continue; + try { + if (file.getName().endsWith(".json")) { + INbt compound = NBTJsonUtil.LoadFile(file); + if (compound.hasKey("PlayerName")) { + map.put(compound.getString("PlayerName"), file.getName().substring(0, file.getName().length() - 5)); + } + } + if (file.getName().endsWith(".dat")) { + INbt compound = NBTJsonUtil.loadNBTData(file); + if (compound.hasKey("PlayerName")) { + map.put(compound.getString("PlayerName"), file.getName().substring(0, file.getName().length() - 4)); + } + } + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + if (tenPercent != 0) { + if (progress != 100) { + if (i % tenPercent == 0) { + progress += 10; + LogWriter.info("Creating PlayerMap: Progress: " + progress + "%"); + } + } + } + } + } + } + nameUUIDs = map; + savePlayerDataMap(); + if (sender != null) { + sender.addChatMessage(new ITextComponent("PlayerMap regeneration complete")); + } + }); + + executor.shutdown(); + } + + public void convertPlayerFiles(final IPlayer sender, final boolean type) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + String fileType; + if (type) { + fileType = ".dat"; + } else { + fileType = ".json"; + } + + if (sender != null) { + LogWriter.info("PlayerData Conversion queued by " + sender.getCommandSenderName()); + sender.addChatMessage(new ITextComponent("PlayerData Conversion to " + fileType + " format")); + } + + File dir = getSaveDir(); + LogWriter.info("Converting PlayerData to " + fileType + " format"); + File[] files = dir.listFiles(); // Get an array of all files in the directory + if (files != null) { + int length = files.length; + if (length != 0) { + if (length > 100) { + LogWriter.info("Found " + length + " PlayerData files... This may take a few minutes"); + } + int tenPercent = (int) ((double) length * 0.1); + int progress = 0; + File saveDir = PlayerDataController.Instance.getNewSaveDir(); + if (saveDir == null) { + if (sender != null) { + sender.addChatMessage(new ITextComponent("playerdata_new folder already exists please delete it or rename it")); + } + LogWriter.error("playerdata_new folder already exists please delete it or rename it"); + return; + } + // Load the files in parallel using a stream + for (int i = 0; i < length; i++) { + File file = files[i]; + if (file.isDirectory() || (!file.getName().endsWith(".json") && !file.getName().endsWith(".dat"))) + continue; + try { + String filename = "error"; + boolean valid = false; + INbt compound = new INbt(); + if (type) { + if (file.getName().endsWith(".json")) { + compound = NBTJsonUtil.LoadFile(file); + if (compound.hasKey("PlayerName")) { + filename = file.getName().substring(0, file.getName().length() - 5); + valid = true; + } + } + } else { + if (file.getName().endsWith(".dat")) { + compound = NBTJsonUtil.loadNBTData(file); + if (compound.hasKey("PlayerName")) { + filename = file.getName().substring(0, file.getName().length() - 4); + valid = true; + } + } + } + if (valid) { + try { + File newFile = new File(saveDir, filename + "_new" + fileType); + File oldFile = new File(saveDir, filename + fileType); + if (type) { + NBTIO.writeCompressed(compound, new FileOutputStream(newFile)); + } else { + NBTJsonUtil.SaveFile(newFile, compound); + } + if (oldFile.exists()) { + oldFile.delete(); + } + newFile.renameTo(oldFile); + } catch (Exception e) { + LogWriter.except(e); + } + } + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + if (tenPercent != 0) { + if (progress != 100) { + if (i % tenPercent == 0) { + progress += 10; + LogWriter.info("Converting PlayerData: Progress: " + progress + "%"); + } + } + } + } + } + } + if (sender != null) { + sender.addChatMessage(new ITextComponent("PlayerData Conversion complete")); + } + LogWriter.info("PlayerData Converted - Please rename the playerdata_new folder to playerdata"); + }); + + executor.shutdown(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/PlayerQuestController.java b/core/src/main/java/noppes/npcs/controllers/PlayerQuestController.java new file mode 100644 index 000000000..3ec020e71 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/PlayerQuestController.java @@ -0,0 +1,167 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.ProfileController; +import noppes.npcs.EventHooks; +import noppes.npcs.NoppesStringUtils; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.constants.EnumProfileSync; +import noppes.npcs.constants.EnumQuestRepeat; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerQuestData; +import noppes.npcs.controllers.data.Quest; +import noppes.npcs.controllers.data.QuestData; +import noppes.npcs.quests.QuestDialog; + +import java.util.Vector; + +public class PlayerQuestController { + + public static boolean hasActiveQuests(IPlayer player) { + PlayerQuestData data = PlayerData.get(player).questData; + return !data.activeQuests.isEmpty(); + } + + public static boolean isQuestActive(IPlayer player, int quest) { + PlayerQuestData data = PlayerData.get(player).questData; + return data.activeQuests.containsKey(quest); + } + + public static boolean isQuestFinished(IPlayer player, int questid) { + PlayerQuestData data = PlayerData.get(player).questData; + return data.finishedQuests.containsKey(questid); + } + + public static void addActiveQuest(QuestData questData, IPlayer player) { + PlayerData playerData = PlayerData.get(player); + PlayerQuestData data = playerData.questData; + if (canQuestBeAccepted(questData.quest, player)) { + if (EventHooks.onQuestStarted(player, questData.quest)) { + return; + } + + data.activeQuests.put(questData.quest.id, questData); + if (questData.sendAlerts) { + AchievementPacket.sendAchievement((IPlayer) player, false, "quest.newquest", questData.quest.title); + ChatAlertPacket.sendChatAlert((IPlayer) player, "quest.newquest", ": ", questData.quest.title); + } + playerData.updateClient = true; + } else { + long timeUntilRepeat = questData.quest.getTimeUntilRepeat(player); + if (timeUntilRepeat > 0 && questData.quest.getIsRepeatable() && questData.quest.repeat != EnumQuestRepeat.NONE && questData.quest.repeat != EnumQuestRepeat.REPEATABLE) { + String timeString = NoppesUtilServer.millisToTime(timeUntilRepeat); + String message = "You have " + timeString + " left until you can repeat this quest."; + player.addChatMessage(new ITextComponent(NoppesStringUtils.formatText(message, player))); + } + } + } + + public static void setQuestFinished(Quest quest, IPlayer player) { + PlayerData playerdata = PlayerData.get(player); + PlayerQuestData data = playerdata.questData; + QuestData questData = data.activeQuests.get(quest.id); + data.activeQuests.remove(quest.id); + setQuestFinishedUtil(player, quest, data); + if (quest.repeat != EnumQuestRepeat.NONE && quest.type == EnumQuestType.Dialog) { + QuestDialog questdialog = (QuestDialog) quest.questInterface; + for (int dialog : questdialog.dialogs.values()) { + playerdata.dialogData.dialogsRead.remove(dialog); + } + } + if (questData != null && questData.sendAlerts) { + AchievementPacket.sendAchievement((IPlayer) player, false, "quest.completed", questData.quest.title); + ChatAlertPacket.sendChatAlert((IPlayer) player, "quest.completed", ": ", questData.quest.title); + } + playerdata.updateClient = true; + } + + public static void setQuestFinishedUtil(IPlayer player, Quest quest, PlayerQuestData questData) { + long completeTime; + if (quest.repeat == EnumQuestRepeat.RLDAILY || quest.repeat == EnumQuestRepeat.RLWEEKLY || quest.repeat == EnumQuestRepeat.RLCUSTOM) { + completeTime = System.currentTimeMillis(); + questData.finishedQuests.put(quest.id, completeTime); + } else { + completeTime = player.worldObj.getTotalWorldTime(); + questData.finishedQuests.put(quest.id, completeTime); + } + + if (ConfigMain.ProfilesEnabled && quest.profileOptions.enableOptions + && (quest.profileOptions.completeControl == EnumProfileSync.Shared + || quest.profileOptions.cooldownControl == EnumProfileSync.Shared)) + ProfileController.Instance.shareQuestCompletion(player, quest.id, completeTime); + } + + public static void setQuestPartyFinished(Quest quest, IPlayer player, QuestData questData) { + PlayerData playerdata = PlayerData.get(player); + PlayerQuestData data = playerdata.questData; + data.activeQuests.remove(quest.id); + setQuestFinishedUtil(player, quest, data); + if (quest.repeat != EnumQuestRepeat.NONE && quest.type == EnumQuestType.Dialog) { + QuestDialog questdialog = (QuestDialog) quest.questInterface; + for (int dialog : questdialog.dialogs.values()) { + playerdata.dialogData.dialogsRead.remove(dialog); + } + } + if (questData != null && questData.sendAlerts) { + AchievementPacket.sendAchievement((IPlayer) player, false, "quest.completed", questData.quest.title); + ChatAlertPacket.sendChatAlert((IPlayer) player, "quest.completed", ": ", questData.quest.title); + } + playerdata.updateClient = true; + } + + public static boolean canQuestBeAccepted(Quest quest, IPlayer player) { + if (quest == null) + return false; + + PlayerData playerData = PlayerData.get(player); + if (playerData == null) + return false; + + PlayerQuestData data = playerData.questData; + if (data.activeQuests.containsKey(quest.id)) + return false; + + boolean finishedLocally = data.finishedQuests.containsKey(quest.id); + boolean completedShared = false; + boolean hasCooldownShared = false; + if (ProfileClientConfig.isProfilesEnabled() && quest.profileOptions.enableOptions + && ProfileClientConfig.hasSharedQuest(player, quest.id)) { + if (quest.profileOptions.completeControl == EnumProfileSync.Shared) + completedShared = true; + if (quest.profileOptions.cooldownControl == EnumProfileSync.Shared) + hasCooldownShared = true; + } + + boolean isFinished = finishedLocally || completedShared; + + if (!isFinished && !hasCooldownShared) + return true; + if (quest.repeat == EnumQuestRepeat.REPEATABLE) + return true; + if (quest.repeat == EnumQuestRepeat.NONE) + return !isFinished; + + return quest.getTimeUntilRepeat(player) <= 0; + } + + public static Vector getActiveQuests(IPlayer player) { + Vector quests = new Vector(); + PlayerQuestData data = PlayerData.get(player).questData; + for (QuestData questdata : data.activeQuests.values()) { + if (questdata.quest == null) + continue; + quests.add(questdata.quest); + } + return quests; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ProfileConfigSync.java b/core/src/main/java/noppes/npcs/controllers/ProfileConfigSync.java new file mode 100644 index 000000000..8a79b3a1c --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ProfileConfigSync.java @@ -0,0 +1,40 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.data.profile.Profile; +import noppes.npcs.config.ConfigMain; + +import java.util.Map; + +public class ProfileConfigSync { + public static INbt writeToNBT(IPlayer player) { + INbt compound = new INbt(); + if (!ConfigMain.ProfilesEnabled || ProfileController.Instance == null) { + return compound; + } + + Profile profile = ProfileController.Instance.getProfile(player); + if (profile == null) { + return compound; + } + + INbtList questList = new INbtList(); + for (Map.Entry entry : profile.sharedQuestTimestamps.entrySet()) { + INbt questEntry = new INbt(); + questEntry.setInteger("Quest", entry.getKey()); + questEntry.setLong("Date", entry.getValue()); + questList.appendTag(questEntry); + } + compound.setTag("SharedQuestTimestamps", questList); + return compound; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/QuestController.java b/core/src/main/java/noppes/npcs/controllers/QuestController.java new file mode 100644 index 000000000..4535b6a17 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/QuestController.java @@ -0,0 +1,278 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.SyncController; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.NoppesStringUtils; +import noppes.npcs.api.handler.IQuestHandler; +import noppes.npcs.api.handler.data.IQuest; +import noppes.npcs.api.handler.data.IQuestCategory; +import noppes.npcs.constants.EnumProfileSync; +import noppes.npcs.controllers.data.Quest; +import noppes.npcs.controllers.data.QuestCategory; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class QuestController implements IQuestHandler { + public HashMap categoriesSync = new HashMap(); + public HashMap categories = new HashMap(); + public HashMap quests = new HashMap(); + public HashMap sharedQuests = new HashMap<>(); + + public static QuestController Instance = new QuestController(); + ; + + private int lastUsedCatID = 0; + private int lastUsedQuestID = 0; + + public QuestController() { + Instance = this; + } + + public void load() { + categories.clear(); + quests.clear(); + + lastUsedCatID = 0; + lastUsedQuestID = 0; + + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "quests.dat"); + if (file.exists()) { + loadCategoriesOld(file); + file.delete(); + file = new File(CustomNpcs.getWorldSaveDirectory(), "quests.dat_old"); + if (file.exists()) + file.delete(); + return; + } + } catch (Exception e) { + + } + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdir(); + } else { + for (File file : dir.listFiles()) { + if (!file.isDirectory()) + continue; + QuestCategory category = loadCategoryDir(file); + Iterator ite = category.quests.keySet().iterator(); + while (ite.hasNext()) { + int id = ite.next(); + if (id > lastUsedQuestID) + lastUsedQuestID = id; + Quest quest = category.quests.get(id); + if (quests.containsKey(id)) { + LogWriter.error("Duplicate id " + quest.id + " from category " + category.title); + ite.remove(); + } else { + quests.put(id, quest); + if (quest.profileOptions.enableOptions + && (quest.profileOptions.completeControl == EnumProfileSync.Shared + || quest.profileOptions.cooldownControl == EnumProfileSync.Shared)) { + sharedQuests.put(id, quest); + } + } + } + lastUsedCatID++; + category.id = lastUsedCatID; + categories.put(category.id, category); + } + } + } + + private QuestCategory loadCategoryDir(File dir) { + QuestCategory category = new QuestCategory(); + category.title = dir.getName(); + for (File file : dir.listFiles()) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + Quest quest = new Quest(); + quest.id = Integer.parseInt(file.getName().substring(0, file.getName().length() - 5)); + quest.readNBTPartial(NBTJsonUtil.LoadFile(file)); + category.quests.put(quest.id, quest); + quest.category = category; + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + return category; + } + + private void loadCategoriesOld(File file) throws Exception { + File dir = getDir(); + if (!dir.exists()) { + dir.mkdir(); + } + INbt nbttagcompound1; + try (FileInputStream fis = new FileInputStream(file)) { + nbttagcompound1 = NBTIO.readCompressed(fis); + } + lastUsedCatID = nbttagcompound1.getInteger("lastID"); + lastUsedQuestID = nbttagcompound1.getInteger("lastQuestID"); + INbtList list = nbttagcompound1.getTagList("Data", 10); + if (list != null) { + for (int i = 0; i < list.tagCount(); i++) { + QuestCategory category = new QuestCategory(); + category.readNBT(list.getCompoundTagAt(i)); + categories.put(category.id, category); + saveCategory(category); + Iterator> ita = category.quests.entrySet().iterator(); + while (ita.hasNext()) { + Map.Entry entry = ita.next(); + Quest quest = entry.getValue(); + quest.id = entry.getKey(); + quest.category = category; + if (quests.containsKey(quest.id)) + ita.remove(); + else { + saveQuest(category.id, quest); + } + } + } + } + } + + + public void removeCategory(int category) { + QuestCategory cat = categories.get(category); + if (cat == null) + return; + File dir = new File(getDir(), cat.title); + if (!dir.delete()) + return; + for (int dia : cat.quests.keySet()) + quests.remove(dia); + categories.remove(category); + SyncController.syncRemove(EnumSyncType.QUEST_CATEGORY, category); + } + + public void saveCategory(QuestCategory category) { + category.title = NoppesStringUtils.cleanFileName(category.title); + if (categories.containsKey(category.id)) { + QuestCategory currentCategory = categories.get(category.id); + if (!currentCategory.title.equals(category.title)) { + while (containsCategoryName(category.title)) + category.title += "_"; + File newdir = new File(getDir(), category.title); + File olddir = new File(getDir(), currentCategory.title); + if (newdir.exists()) + return; + if (!olddir.renameTo(newdir)) + return; + } + category.quests = currentCategory.quests; + } else { + if (category.id < 0) { + lastUsedCatID++; + category.id = lastUsedCatID; + } + while (containsCategoryName(category.title)) + category.title += "_"; + File dir = new File(getDir(), category.title); + if (!dir.exists()) + dir.mkdirs(); + } + categories.put(category.id, category); + SyncController.syncUpdate(EnumSyncType.QUEST_CATEGORY, -1, SyncController.updateQuestCat(category)); + } + + private boolean containsCategoryName(String name) { + name = name.toLowerCase(); + for (QuestCategory cat : categories.values()) { + if (cat.title.toLowerCase().equals(name)) + return true; + } + return false; + } + + private boolean containsQuestName(QuestCategory category, Quest quest) { + for (Quest q : category.quests.values()) { + if (q.id != quest.id && q.title.equalsIgnoreCase(quest.title)) + return true; + } + return false; + } + + public void saveQuest(int categoryID, Quest quest) { + QuestCategory category = categories.get(categoryID); + if (category == null) + return; + quest.category = category; + + while (containsQuestName(quest.category, quest)) { + quest.title = quest.title + "_"; + } + + if (quest.id < 0) { + lastUsedQuestID++; + quest.id = lastUsedQuestID; + } + + quests.put(quest.id, quest); + category.quests.put(quest.id, quest); + + sharedQuests.remove(quest.id); + if (quest.profileOptions.enableOptions + && (quest.profileOptions.completeControl == EnumProfileSync.Shared + || quest.profileOptions.cooldownControl == EnumProfileSync.Shared)) { + sharedQuests.put(quest.id, quest); + } + + File dir = new File(getDir(), category.title); + if (!dir.exists()) + dir.mkdirs(); + + File file = new File(dir, quest.id + ".json_new"); + File file2 = new File(dir, quest.id + ".json"); + + try { + NBTJsonUtil.SaveFile(file, quest.writeToNBTPartial(new INbt())); + if (file2.exists()) + file2.delete(); + file.renameTo(file2); + SyncController.syncUpdate(EnumSyncType.QUEST, category.id, quest.writeToNBT(new INbt())); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void removeQuest(Quest quest) { + File file = new File(new File(getDir(), quest.category.title), quest.id + ".json"); + if (!file.delete()) + return; + quests.remove(quest.id); + quest.category.quests.remove(quest.id); + } + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "quests"); + } + + public List categories() { + return new ArrayList(this.categories.values()); + } + + public IQuest get(int id) { + return (IQuest) this.quests.get(id); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/RecipeController.java b/core/src/main/java/noppes/npcs/controllers/RecipeController.java new file mode 100644 index 000000000..377d841ec --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/RecipeController.java @@ -0,0 +1,390 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.SyncController; +import noppes.npcs.CustomNpcs; +import noppes.npcs.api.handler.IRecipeHandler; +import noppes.npcs.api.handler.data.IAnvilRecipe; +import noppes.npcs.api.handler.data.IRecipe; +import noppes.npcs.controllers.data.RecipeAnvil; +import noppes.npcs.controllers.data.RecipeCarpentry; +import noppes.npcs.controllers.data.RecipeScript; +import noppes.npcs.controllers.data.RecipesDefault; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +public class RecipeController implements IRecipeHandler { + private static Collection prevRecipes; + + public HashMap globalRecipes = new HashMap(); + public HashMap carpentryRecipes = new HashMap(); + public HashMap anvilRecipes = new HashMap(); + + public HashMap carpentryScripts = new HashMap<>(); + public HashMap anvilScripts = new HashMap<>(); + + public static RecipeController Instance; + + public static final int version = 1; + public int nextId = 1; + public int nextAnvilId = 1; + + public static HashMap syncRecipes = new HashMap(); + public static HashMap syncAnvilRecipes = new HashMap(); + + public RecipeController() { + Instance = this; + } + + public void load() { + loadCategories(); + reloadGlobalRecipes(globalRecipes); + } + + public static void reloadGlobalRecipes(HashMap globalRecipes) { + List list = CraftingManager.getInstance().getRecipeList(); + if (prevRecipes != null) { + list.removeAll(prevRecipes); + } + + prevRecipes = new HashSet(); + for (RecipeCarpentry recipe : globalRecipes.values()) { + if (recipe.isValid()) + prevRecipes.add(recipe); + } + list.addAll(prevRecipes); + } + + private void loadCategories() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + try { + File file = new File(saveDir, "recipes.dat"); + if (file.exists()) { + loadCategories(file); + } else { + globalRecipes.clear(); + carpentryRecipes.clear(); + anvilRecipes.clear(); + loadDefaultRecipes(-1); + } + } catch (Exception e) { + e.printStackTrace(); + try { + File file = new File(saveDir, "recipes.dat_old"); + if (file.exists()) { + loadCategories(file); + } + } catch (Exception ee) { + e.printStackTrace(); + } + } + } + + private void loadDefaultRecipes(int i) { + if (i == version) + return; + RecipesDefault.loadDefaultRecipes(i); + saveCategories(); + } + + private void loadCategories(File file) throws Exception { + INbt nbttagcompound1; + try (FileInputStream fis = new FileInputStream(file)) { + nbttagcompound1 = NBTIO.readCompressed(fis); + } + nextId = nbttagcompound1.getInteger("LastId"); + nextAnvilId = nbttagcompound1.getInteger("LastAnvilId"); + INbtList list = nbttagcompound1.getTagList("Data", 10); + HashMap globalRecipes = new HashMap(); + HashMap carpentryRecipes = new HashMap(); + HashMap anvilRecipes = new HashMap(); + if (list != null) { + for (int i = 0; i < list.tagCount(); i++) { + INbt compound = list.getCompoundTagAt(i); + if (compound.hasKey("IsAnvil")) { + RecipeAnvil anvil = new RecipeAnvil(); + anvil.readNBT(compound); + anvilRecipes.put(anvil.id, anvil); + if (anvil.id > nextAnvilId) + nextAnvilId = anvil.id; + } else { + RecipeCarpentry recipe = RecipeCarpentry.create(compound); + recipe.readNBT(compound); + if (recipe.isGlobal) + globalRecipes.put(recipe.id, recipe); + else + carpentryRecipes.put(recipe.id, recipe); + if (recipe.id > nextId) + nextId = recipe.id; + } + } + } + this.carpentryRecipes = carpentryRecipes; + this.globalRecipes = globalRecipes; + this.anvilRecipes = anvilRecipes; + loadDefaultRecipes(nbttagcompound1.getInteger("Version")); + } + + private void saveCategories() { + try { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + INbtList list = new INbtList(); + for (RecipeCarpentry recipe : globalRecipes.values()) { + list.appendTag(recipe.writeNBT(true)); + } + for (RecipeCarpentry recipe : carpentryRecipes.values()) { + list.appendTag(recipe.writeNBT(true)); + } + for (RecipeAnvil recipe : anvilRecipes.values()) { + list.appendTag(recipe.writeNBT(true)); + } + INbt INbt = new INbt(); + INbt.setTag("Data", list); + INbt.setInteger("LastId", nextId); + INbt.setInteger("LastAnvilId", nextAnvilId); + INbt.setInteger("Version", version); + File file = new File(saveDir, "recipes.dat_new"); + File file1 = new File(saveDir, "recipes.dat_old"); + File file2 = new File(saveDir, "recipes.dat"); + NBTIO.writeCompressed(INbt, new FileOutputStream(file)); + if (file1.exists()) { + file1.delete(); + } + file2.renameTo(file1); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public RecipeCarpentry findMatchingRecipe(InventoryCrafting par1InventoryCrafting) { + for (RecipeCarpentry recipe : carpentryRecipes.values()) { + if (recipe.isValid() && recipe.matches(par1InventoryCrafting, null)) + return recipe; + } + return null; + } + + public RecipeCarpentry getRecipe(int id) { + if (globalRecipes.containsKey(id)) + return globalRecipes.get(id); + if (carpentryRecipes.containsKey(id)) + return carpentryRecipes.get(id); + return null; + } + + public RecipeAnvil getAnvilRecipe(int id) { + if (anvilRecipes.containsKey(id)) + return anvilRecipes.get(id); + return null; + } + + public RecipeCarpentry saveRecipe(INbt compound) throws IOException { + RecipeCarpentry recipe = RecipeCarpentry.create(compound); + recipe.readNBT(compound); + + RecipeCarpentry current = getRecipe(recipe.id); + if (current != null && !current.name.equals(recipe.name)) { + while (containsRecipeName(recipe.name)) + recipe.name += "_"; + } + + if (recipe.id == -1) { + recipe.id = getUniqueId(); + while (containsRecipeName(recipe.name)) + recipe.name += "_"; + } + if (recipe.isGlobal) { + carpentryRecipes.remove(recipe.id); + globalRecipes.put(recipe.id, recipe); + } else { + globalRecipes.remove(recipe.id); + carpentryRecipes.put(recipe.id, recipe); + } + saveCategories(); + reloadGlobalRecipes(globalRecipes); + return recipe; + } + + public RecipeAnvil saveAnvilRecipe(INbt compound) throws IOException { + RecipeAnvil recipe = new RecipeAnvil(); + recipe.readNBT(compound); + + RecipeAnvil current = getAnvilRecipe(recipe.id); + if (current != null && !current.name.equals(recipe.name)) { + while (containsAnvilRecipeName(recipe.name)) + recipe.name += "_"; + } + + if (recipe.id == -1) { + recipe.id = getUniqueAnvilId(); + while (containsAnvilRecipeName(recipe.name)) + recipe.name += "_"; + } + + anvilRecipes.put(recipe.id, recipe); + saveCategories(); + return recipe; + } + + private int getUniqueId() { + return nextId++; + } + + private int getUniqueAnvilId() { + return nextAnvilId++; + } + + private boolean containsRecipeName(String name) { + name = name.toLowerCase(); + for (RecipeCarpentry recipe : globalRecipes.values()) { + if (recipe.name.toLowerCase().equals(name)) + return true; + } + for (RecipeCarpentry recipe : carpentryRecipes.values()) { + if (recipe.name.toLowerCase().equals(name)) + return true; + } + return false; + } + + private boolean containsAnvilRecipeName(String name) { + name = name.toLowerCase(); + for (RecipeAnvil recipe : anvilRecipes.values()) { + if (recipe.name.toLowerCase().equals(name)) + return true; + } + return false; + } + + public RecipeCarpentry delete(int id) { + RecipeCarpentry recipe = getRecipe(id); + if (recipe == null) + return null; + RecipeCarpentry globalRecipe = globalRecipes.remove(recipe.id); + if (globalRecipe != null) + SyncController.syncAllWorkbenchRecipes(); + + RecipeCarpentry carpentry = carpentryRecipes.remove(recipe.id); + if (carpentry != null) { + carpentryScripts.remove(recipe.id); + SyncController.syncAllCarpentryRecipes(); + } + + saveCategories(); + reloadGlobalRecipes(globalRecipes); + return recipe; + } + + public RecipeAnvil deleteAnvil(int id) { + RecipeAnvil recipe = getAnvilRecipe(id); + if (recipe == null) + return null; + + RecipeAnvil anvilRecipe = anvilRecipes.remove(recipe.id); + if (anvilRecipe != null) + SyncController.syncAllAnvilRecipes(); + + saveCategories(); + return recipe; + } + + public void addRecipe(RecipeCarpentry recipeCarpentry) { + recipeCarpentry.id = getUniqueId(); + if (!recipeCarpentry.isGlobal) + RecipeController.Instance.carpentryRecipes.put(recipeCarpentry.id, recipeCarpentry); + else { + RecipeController.Instance.globalRecipes.put(recipeCarpentry.id, recipeCarpentry); + } + } + + public void addAnvilRecipe(RecipeAnvil recipeAnvil) { + recipeAnvil.id = getUniqueAnvilId(); + anvilRecipes.put(recipeAnvil.id, recipeAnvil); + } + + @Override + public List getGlobalList() { + return new ArrayList(this.globalRecipes.values()); + } + + @Override + public List getCarpentryList() { + return new ArrayList(this.carpentryRecipes.values()); + } + + @Override + public List getAnvilList() { + List list = new ArrayList<>(); + list.addAll(anvilRecipes.values()); + return list; + } + + @Override + public void addRecipe(String name, boolean global, IItemStack result, Object... objects) { + RecipeCarpentry recipe = new RecipeCarpentry(name); + recipe.isGlobal = global; + recipe = RecipeCarpentry.saveRecipe(recipe, result, objects); + + try { + this.saveRecipe(recipe.writeNBT(true)); + } catch (Exception var7) { + var7.printStackTrace(); + } + } + + @Override + public void addRecipe(String name, boolean global, IItemStack result, int width, int height, IItemStack... objects) { + ArrayList list = new ArrayList<>(); + int var9 = objects.length; + + for (int var10 = 0; var10 < var9; ++var10) { + IItemStack item = objects[var10]; + if (item.stackSize > 0) { + list.add(item); + } + } + + RecipeCarpentry recipe = new RecipeCarpentry(width, height, list.toArray(new IItemStack[0]), result); + recipe.isGlobal = global; + recipe.name = name; + + try { + this.saveRecipe(recipe.writeNBT(true)); + } catch (IOException var12) { + var12.printStackTrace(); + } + } + + @Override + public void addAnvilRecipe(String name, boolean global, IItemStack itemToRepair, IItemStack repairMaterial, int xpCost, float repairPercentage) { + RecipeAnvil recipe = new RecipeAnvil(name, itemToRepair, repairMaterial, xpCost, repairPercentage); + try { + this.saveAnvilRecipe(recipe.writeNBT(true)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java b/core/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java new file mode 100644 index 000000000..a250117de --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java @@ -0,0 +1,33 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.config.ConfigScript; + +/** + * Server-side utility for syncing script IConfiguration to clients. + *

+ * This class is SERVER-SIDE ONLY. Client reads via ScriptClientConfig. + */ +public class ScriptConfigSync { + + /** + * Write script config to NBT for sending to client. + * Called on server side when building login packet. + */ + public static INbt writeToNBT(INbt compound) { + compound.setBoolean("ScriptingEnabled", ConfigScript.ScriptingEnabled); + compound.setBoolean("RunLoadedScriptsFirst", ConfigScript.RunLoadedScriptsFirst); + compound.setBoolean("GlobalPlayerScripts", ConfigScript.GlobalPlayerScripts); + compound.setBoolean("GlobalForgeScripts", ConfigScript.GlobalForgeScripts); + compound.setBoolean("GlobalNPCScripts", ConfigScript.GlobalNPCScripts); + return compound; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/core/src/main/java/noppes/npcs/controllers/ScriptContainer.java new file mode 100644 index 000000000..bba77a651 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -0,0 +1,436 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import jdk.nashorn.api.scripting.ScriptObjectMirror; +import noppes.npcs.CustomNpcs; +import noppes.npcs.NBTTags; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +public class ScriptContainer implements IScriptUnit { + private static final String lock = "lock"; + public static ScriptContainer Current; + private static String CurrentType; + public String fullscript = ""; + public String script = ""; + public TreeMap console = new TreeMap<>(); + public boolean errored = false; + public List scripts = new ArrayList<>(); + private HashSet unknownFunctions = new HashSet<>(); + public long lastCreated = 0L; + private String currentScriptLanguage = null; + public ScriptEngine engine = null; + private IScriptHandler handler = null; + private boolean evaluated = false; + private static Method luaCoerce; + private static Method luaCall; + private CompiledScript compScript = null; + private final HashMap cachedFunctions = new HashMap<>(); + private long lastGlobalsVersion = -1; + + /** + * Persisted per-container language. Null means inherit from handler default. + * Separate from currentScriptLanguage which is the engine cache. + */ + private String language = null; + + /** + * Per-instance bindings for variable isolation. When non-null, this container + * is an instance scope sharing a template's engine. All eval/lookup uses these + * bindings instead of the engine's default ENGINE_SCOPE. + */ + private Bindings instanceBindings = null; + + public ScriptContainer(IScriptHandler handler) { + this.handler = handler; + } + + /** + * Create a lightweight instance scope that shares this container's engine + * but has its own isolated Bindings for variable state. + * The template's engine is initialized if needed. + * + * @param instanceHandler the script handler for the instance + */ + @Override + public IScriptUnit createInstanceScope(IScriptHandler instanceHandler) { + if (this.engine == null) { + this.setEngine(this.getLanguage()); + } + ScriptContainer instance = new ScriptContainer(instanceHandler); + instance.script = this.script; + instance.scripts = new ArrayList<>(this.scripts); + instance.console = this.console; + instance.language = this.language; + instance.engine = this.engine; + instance.currentScriptLanguage = this.currentScriptLanguage; + instance.instanceBindings = this.engine.createBindings(); + return instance; + } + + public void readFromNBT(INbt compound) { + String prevScript = this.script; + this.script = compound.getString("Script"); + for (int i = 0; i < ConfigScript.ExpandedScriptLimit; i++) { + if (compound.hasKey("ExpandedScript" + i)) { + this.script += compound.getString("ExpandedScript" + i); + } else { + break; + } + } + + if (!this.script.equals(prevScript)) { + this.evaluated = false; + } + + this.console = NBTTags.GetLongStringMap(compound.getTagList("Console", 10)); + this.scripts = NBTTags.getStringList(compound.getTagList("ScriptList", 10)); + this.lastCreated = 0L; + + // Read per-container language (empty string or missing = inherit from handler) + if (compound.hasKey("ContainerLanguage")) { + String lang = compound.getString("ContainerLanguage"); + this.language = lang.isEmpty() ? null : lang; + } + } + + public INbt writeToNBT(INbt compound) { + compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_ECMASCRIPT); + if (this.script.length() < 65535) { + compound.setString("Script", this.script); + } else { + if (ConfigScript.ExpandedScriptLimit > 0) { + int i = 0; + int length = this.script.length(); + while (length > 0 && i <= ConfigScript.ExpandedScriptLimit) { + String str = ""; + if (i == 0) { + compound.setString("Script", this.script.substring(0, 65535)); + str = this.script.substring(0, 65535); + } else { + int end = (length - 65535) >= 0 ? 65535 * (i + 1) : 65535 * i + length; + str = this.script.substring(65535 * i, end); + compound.setString("ExpandedScript" + (i - 1), str); + } + i++; + length -= str.length(); + } + } else { + compound.setString("Script", this.script.substring(0, 65535)); + } + } + //compound.setString("Type", this.type); + compound.setTag("Console", NBTTags.NBTLongStringMap(this.console)); + compound.setTag("ScriptList", NBTTags.nbtStringList(this.scripts)); + + // Persist per-container language if explicitly set + if (this.language != null) { + compound.setString("ContainerLanguage", this.language); + } + return compound; + } + + private String getFullCode() { + if (!this.evaluated) { + // build includes first depending on config setting + StringBuilder sb = new StringBuilder(); + if (CustomNpcs.proxy.isRunLoadedScriptsFirst()) { + this.appendExternalScripts(sb); + } + + // then your per‐hook main script + if (this.script != null && !this.script.isEmpty()) { + sb.append(this.script).append("\n"); + } + + // build includes last if not built first + if (!CustomNpcs.proxy.isRunLoadedScriptsFirst()) { + this.appendExternalScripts(sb); + } + this.fullscript = sb.toString(); + this.unknownFunctions = new HashSet<>(); + } + return this.fullscript; + } + + private void appendExternalScripts(StringBuilder sb) { + for (String loc : this.scripts) { + String code = ScriptController.Instance.scripts.get(loc); + if (code != null && !code.isEmpty()) { + sb.append(code).append("\n"); + } + } + } + + public void run(ScriptEngine engine) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + this.engine.getContext().setWriter(pw); + this.engine.getContext().setErrorWriter(pw); + + try { + if (compScript == null && engine instanceof Compilable) + compScript = ((Compilable) engine).compile(getFullCode()); + + if (compScript != null) { + compScript.eval(engine.getContext()); + } else { + engine.eval(getFullCode()); + } + } catch (Throwable var14) { + this.errored = true; + var14.printStackTrace(pw); + } finally { + String errorString = sw.getBuffer().toString().trim(); + this.appendConsole(errorString); + pw.close(); + } + } + + public void run(EnumScriptType type, Event event) { + if (!CustomNpcs.proxy.isScriptingEnabled()) + return; + + this.run((String) type.function, (Object) event); + } + + public void run(String type, Object event) { + if (!CustomNpcs.proxy.isScriptingEnabled() || errored || !hasCode() || unknownFunctions.contains(type)) + return; + + this.setEngine(this.getLanguage()); + if (engine == null) + return; + + if (ScriptController.Instance.lastLoaded > this.lastCreated) { + this.lastCreated = ScriptController.Instance.lastLoaded; + evaluated = false; + lastGlobalsVersion = -1; + } + + synchronized (lock) { + Current = this; + CurrentType = type; + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + engine.getContext().setWriter(pw); + engine.getContext().setErrorWriter(pw); + + // Populate bindings with API objects (skip if globals unchanged) + long currentGlobalsVersion = NpcAPI.engineObjectsVersion; + boolean globalsChanged = lastGlobalsVersion != currentGlobalsVersion; + if (instanceBindings != null) { + if (globalsChanged) { + instanceBindings.put("API", NpcAPI.Instance()); + for (Map.Entry objectEntry : NpcAPI.engineObjects.entrySet()) { + instanceBindings.put(objectEntry.getKey(), objectEntry.getValue()); + } + lastGlobalsVersion = currentGlobalsVersion; + } + } else { + if (globalsChanged) { + engine.put("API", NpcAPI.Instance()); + for (Map.Entry objectEntry : NpcAPI.engineObjects.entrySet()) { + engine.put(objectEntry.getKey(), objectEntry.getValue()); + } + lastGlobalsVersion = currentGlobalsVersion; + } + } + + try { + if (!evaluated) { + this.cachedFunctions.clear(); + if (instanceBindings != null) { + engine.eval(getFullCode(), instanceBindings); + } else { + engine.eval(getFullCode()); + } + evaluated = true; + } + if (engine.getFactory().getLanguageName().equals("lua")) { + Object ob = instanceBindings != null ? instanceBindings.get(type) : engine.get(type); + if (ob != null) { + if (luaCoerce == null) { + luaCoerce = Class.forName("org.luaj.vm2.lib.jse.CoerceJavaToLua").getMethod("coerce", Object.class); + luaCall = ob.getClass().getMethod("call", Class.forName("org.luaj.vm2.LuaValue")); + } + luaCall.invoke(ob, luaCoerce.invoke(null, event)); + } else { + unknownFunctions.add(type); + } + } else { + if (!this.cachedFunctions.containsKey(type)) { + ScriptObjectMirror func; + if (instanceBindings != null) { + Object val = instanceBindings.get(type); + func = val instanceof ScriptObjectMirror ? (ScriptObjectMirror) val : null; + } else { + ScriptObjectMirror global = (ScriptObjectMirror) engine.getBindings(ScriptContext.ENGINE_SCOPE); + func = (ScriptObjectMirror) global.get(type); + } + this.cachedFunctions.put(type, func); + } + ScriptObjectMirror func = this.cachedFunctions.get(type); + if (func != null) { + func.call(null, event); + } + } + } catch (NoSuchMethodException e) { + unknownFunctions.add(type); + } catch (Throwable e) { + errored = true; + e.printStackTrace(pw); + } finally { + appendConsole(sw.getBuffer().toString().trim()); + pw.close(); + Current = null; + } + } + } + + public void appendConsole(String message) { + if (message != null && !message.isEmpty()) { + long time = System.currentTimeMillis(); + if (this.console.containsKey(time)) { + message = (String) this.console.get(time) + "\n" + message; + } + + this.console.put(time, message); + + while (this.console.size() > 40) { + this.console.remove(this.console.firstKey()); + } + } + } + + public boolean isValid() { + return evaluated && !errored; + } + + @Override + public boolean hasCode() { + if (!scripts.isEmpty()) + return true; + return !this.getFullCode().isEmpty(); + } + + @Override + public boolean isUnknownFunction(String type) { + return unknownFunctions.contains(type); + } + + public void setEngine(String scriptLanguage) { + if (!Objects.equals(scriptLanguage, this.currentScriptLanguage)) { + // Instance scopes share the template's engine — never create a new one + if (instanceBindings != null) return; + this.currentScriptLanguage = scriptLanguage; + if (ConfigScript.ScriptingECMA6 && scriptLanguage.equals("ECMAScript")) { + System.setProperty("nashorn.args", "--language=es6"); + } + this.engine = ScriptController.Instance.getEngineByName(scriptLanguage.toLowerCase()); + this.evaluated = false; + } + } + + // ==================== IScriptUnit IMPLEMENTATION ==================== + + @Override + public String getScript() { + return this.script; + } + + @Override + public void setScript(String script) { + this.script = script; + this.evaluated = false; + } + + @Override + public List getExternalScripts() { + return this.scripts; + } + + @Override + public void setExternalScripts(List scripts) { + this.scripts = scripts; + this.evaluated = false; + } + + @Override + public TreeMap getConsole() { + return this.console; + } + + @Override + public void clearConsole() { + this.console.clear(); + } + + @Override + public String getLanguage() { + // Return persisted per-container language, defaulting to ECMAScript. + // This is separate from currentScriptLanguage which is the engine cache. + return this.language != null ? this.language : "ECMAScript"; + } + + @Override + public void setLanguage(String language) { + if (!Objects.equals(this.language, language)) { + this.language = language; + this.evaluated = false; + } + } + + @Override + public String generateHookStub(String hookName, Object hookData) { + // ECMAScript function template + return "function " + hookName + "(event) {\n \n}\n"; + } + + @Override + public void ensureCompiled() { + } + + @Override + public boolean hasErrored() { + return this.errored; + } + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } + + @Override + public boolean isJanino() { + return false; + } +} + diff --git a/core/src/main/java/noppes/npcs/controllers/ScriptController.java b/core/src/main/java/noppes/npcs/controllers/ScriptController.java new file mode 100644 index 000000000..6bbd2ba89 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -0,0 +1,414 @@ +package noppes.npcs.controllers; + + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; +import jdk.nashorn.api.scripting.ClassFilter; +import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.data.ForgeDataScript; +import noppes.npcs.controllers.data.GlobalNPCDataScript; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerDataScript; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.util.JsonException; +import noppes.npcs.util.NBTJsonUtil; +import somehussar.janino.EntityUnloadListener; +import static noppes.npcs.util.CustomNPCsThreader.customNPCThread; + +public class ScriptController { + + public static ScriptController Instance; + public static boolean HasStart = false; + private final ScriptEngineManager manager; + private ScriptEngineFactory nashornFactory; + public final Map languages = new HashMap(); + public Map scripts = new HashMap(); + public long lastLoaded = 0; + public File dir; + public INbt compound = new INbt(); + + public boolean shouldSave = false; + public PlayerDataScript playerScripts = new PlayerDataScript((IPlayer) null); + public long lastPlayerUpdate = 0L; + + public ForgeDataScript forgeScripts = new ForgeDataScript(); + public long lastForgeUpdate = 0L; + + public GlobalNPCDataScript globalNpcScripts = new GlobalNPCDataScript((EntityNPCInterface) null); + public long lastGlobalNpcUpdate = 0L; + + /** + * Incremented whenever external scripts loaded. + */ + public int globalRevision; + + public ScriptController() { + Instance = this; + manager = new ScriptEngineManager(); + if (!CustomNpcs.proxy.isScriptingEnabled()) + return; + LogWriter.info("Script Engines Available:"); + + try { + this.nashornFactory = new NashornScriptEngineFactory(); + LogWriter.info("→ standalone Nashorn loaded"); + } catch (NoClassDefFoundError e) { + // fallback to built-in (Java 8–11) — also guarded + try { + ScriptEngine eng = manager.getEngineByName("nashorn"); + if (eng != null) { + this.nashornFactory = eng.getFactory(); + LogWriter.info("→ built-in Nashorn loaded"); + } + } catch (Throwable t) { + LogWriter.error("No Nashorn engine available"); + } + } + + for (ScriptEngineFactory fac : manager.getEngineFactories()) { + if (fac.getExtensions().isEmpty()) + continue; + + ScriptEngine scriptEngine = fac.getScriptEngine(); + try { + scriptEngine.put("$RunTest", null); + String ext = "." + fac.getExtensions().get(0).toLowerCase(); + LogWriter.info("Engine " + fac.getEngineName() + " running " + fac.getLanguageName() + " with extension: " + ext); + languages.put(fac.getLanguageName(), ext); + } catch (Exception ignored) { + } + } + languages.put("Java", ".java"); + } + + private File forgeScriptsFile() { + return new File(this.dir, "forge_scripts.json"); + } + + public boolean loadForgeScripts() { + this.forgeScripts.clear(); + File file = this.forgeScriptsFile(); + + try { + if (!file.exists()) { + return false; + } else { + this.forgeScripts.readFromNBT(NBTJsonUtil.LoadFile(file)); + return true; + } + } catch (Exception var3) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), var3); + return false; + } + } + + public void saveForgeScripts() { + File file = this.forgeScriptsFile(); + try { + NBTJsonUtil.SaveFile(file, this.forgeScripts.writeToNBT(new INbt())); + this.forgeScripts.resetLastInited(); + } catch (IOException | JsonException var4) { + var4.printStackTrace(); + } + } + + private File playerScriptsFile() { + return new File(dir, "player_scripts.json"); + } + + private File npcScriptsFile() { + return new File(dir, "npc_scripts.json"); + } + + public boolean loadPlayerScripts() { + this.playerScripts.clear(); + File file = this.playerScriptsFile(); + + try { + if (!file.exists()) { + return false; + } else { + this.playerScripts.readFromNBT(NBTJsonUtil.LoadFile(file)); + shouldSave = false; + return true; + } + } catch (Exception var3) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), var3); + return false; + } + } + + public void savePlayerScripts() { + File file = this.playerScriptsFile(); + try { + NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new INbt())); + this.playerScripts.resetLastInited(); + } catch (IOException | JsonException var4) { + var4.printStackTrace(); + } + } + + public PlayerDataScript getPlayerScripts(IPlayer player) { + if (ConfigScript.IndividualPlayerScripts) + return PlayerData.get(player).scriptData; + + return this.playerScripts; + } + + public PlayerDataScript getPlayerScripts(IPlayer player) { + if (ConfigScript.IndividualPlayerScripts) + return PlayerData.get((IPlayer) player.getMCEntity()).scriptData; + + return this.playerScripts; + } + + public boolean loadGlobalNPCScripts() { + this.globalNpcScripts.clear(); + File file = this.npcScriptsFile(); + + try { + if (!file.exists()) { + return false; + } else { + this.globalNpcScripts.readFromNBT(NBTJsonUtil.LoadFile(file)); + shouldSave = false; + return true; + } + } catch (Exception var3) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), var3); + return false; + } + } + + public void saveGlobalNpcScripts() { + File file = this.npcScriptsFile(); + try { + NBTJsonUtil.SaveFile(file, this.globalNpcScripts.writeToNBT(new INbt())); + this.globalNpcScripts.resetLastInited(); + } catch (IOException | JsonException var4) { + var4.printStackTrace(); + } + } + + public synchronized void saveForgeScriptsSync() { + customNPCThread.execute(this::saveForgeScripts); + } + + public synchronized void savePlayerScriptsSync() { + customNPCThread.execute(() -> { + File file = this.playerScriptsFile(); + try { + NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new INbt())); + this.playerScripts.resetLastInited(); + } catch (IOException | JsonException var4) { + var4.printStackTrace(); + } + }); + } + + public synchronized void saveGlobalScriptsSync() { + customNPCThread.execute(this::saveGlobalNpcScripts); + } + + public void syncClientScripts(IPlayer player) { + if (player != null) { + ScriptFilesPacket.sendToPlayer(player, "Java"); + } else { + ScriptFilesPacket.sendToAll("Java"); + } + } + + public void loadCategories() { + dir = new File(CustomNpcs.getWorldSaveDirectory(), "scripts"); + if (!dir.exists()) + dir.mkdir(); + if (!getSavedFile().exists()) + shouldSave = true; + new ScriptWorld(null).clearTempData(); + scripts.clear(); + for (String language : languages.keySet()) { + String ext = languages.get(language); + File scriptDir = new File(dir, language.toLowerCase()); + if (!scriptDir.exists()) + scriptDir.mkdir(); + else + loadDir(scriptDir, "", ext); + } + lastLoaded = System.currentTimeMillis(); + globalRevision++; + } + + private void loadDir(File dir, String name, String ext) { + for (File file : dir.listFiles()) { + String filename = name + file.getName().toLowerCase(); + if (file.isDirectory()) { + loadDir(file, filename + "/", ext); + continue; + } + if (!filename.endsWith(ext)) + continue; + try { + scripts.put(filename, readFile(file)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public boolean loadStoredData() { + loadCategories(); + File file = getSavedFile(); + try { + if (!file.exists()) + return false; + this.compound = NBTJsonUtil.LoadFile(file); + shouldSave = false; + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + return false; + } + return true; + } + + private File getSavedFile() { + return new File(dir, "world_data.json"); + } + + private String readFile(File file) throws IOException { + BufferedReader br = new BufferedReader(new FileReader(file)); + try { + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + + while (line != null) { + sb.append(line); + sb.append("\n"); + line = br.readLine(); + } + return sb.toString(); + } finally { + br.close(); + } + } + + private static final List nashornNames = immutableList("nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript"); + + public ScriptEngine getEngineByName(String language) { + if (nashornNames.contains(language) && this.nashornFactory != null) { + ScriptEngine scriptEngine; + if (ConfigScript.EnableBannedClasses) { + try { + ClassFilter filter = s -> { + for (String className : ConfigScript.BannedClasses) { + if (s.compareTo(className) == 0) { + return false; + } + } + return true; + }; + NashornScriptEngineFactory nashornScriptEngineFactory = (NashornScriptEngineFactory) this.nashornFactory; + scriptEngine = nashornScriptEngineFactory.getScriptEngine(filter); + } catch (Exception e) { + scriptEngine = this.nashornFactory.getScriptEngine(); + } + } else { + scriptEngine = this.nashornFactory.getScriptEngine(); + } + scriptEngine.setBindings(this.manager.getBindings(), ScriptContext.GLOBAL_SCOPE); + return scriptEngine; + } + return manager.getEngineByName(language); + } + + private static List immutableList(String... elements) { + return Collections.unmodifiableList(Arrays.asList(elements)); + } + + public INbtList nbtLanguages() { + INbtList list = new INbtList(); + for (String language : languages.keySet()) { + INbt compound = new INbt(); + INbtList scripts = new INbtList(); + for (String script : getScripts(language)) { + scripts.appendTag(new NBTTagString(script)); + } + compound.setTag("Scripts", scripts); + compound.setString("Language", language); + list.appendTag(compound); + } + return list; + } + + public List getScripts(String language) { + List list = new ArrayList(); + String ext = languages.get(language); + if (ext == null) + return list; + for (String script : scripts.keySet()) { + if (script.endsWith(ext)) { + list.add(script); + } + } + return list; + } + + @SubscribeEvent + public void addWorldAccess(WorldEvent.Load event) { + event.IWorld.addWorldAccess(new EntityUnloadListener()); + } + + @SubscribeEvent + public void invoke(WorldEvent.Save event) { + if (!shouldSave || event.IWorld.isRemote || event.IWorld != PlatformServiceHolder.get().getServer().worldServers[0]) + return; + + try { + NBTJsonUtil.SaveFile(getSavedFile(), compound); + } catch (Exception e) { + LogWriter.except(e); + } + + shouldSave = false; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ScriptEntityData.java b/core/src/main/java/noppes/npcs/controllers/ScriptEntityData.java new file mode 100644 index 000000000..0d2a3b2e0 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ScriptEntityData.java @@ -0,0 +1,32 @@ +package noppes.npcs.controllers; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; + +public class ScriptEntityData implements IExtendedEntityProperties { + public IEntity base; + + public ScriptEntityData(IEntity base) { + this.base = base; + } + + @Override + public void saveNBTData(INbt compound) { + } + + @Override + public void loadNBTData(INbt compound) { + } + + @Override + public void init(IEntity IEntity, IWorld IWorld) { + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/core/src/main/java/noppes/npcs/controllers/ScriptHookController.java new file mode 100644 index 000000000..4ce3f5fee --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -0,0 +1,548 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.IChainEvent; +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IAuctionEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.ICustomNPCsEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IEnergyBarrierEvent; +import noppes.npcs.api.event.IEnergyProjectileEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.ILinkedItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; +import noppes.npcs.api.event.IRecipeEvent; +import noppes.npcs.api.handler.IHookDefinition; +import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.data.RecipeScript; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static noppes.npcs.constants.EnumScriptType.*; +import static noppes.npcs.constants.ScriptContext.BLOCK; +import static noppes.npcs.constants.ScriptContext.EFFECT; +import static noppes.npcs.constants.ScriptContext.FORGE; +import static noppes.npcs.constants.ScriptContext.ITEM; +import static noppes.npcs.constants.ScriptContext.LINKED_ITEM; +import static noppes.npcs.constants.ScriptContext.NPC; +import static noppes.npcs.constants.ScriptContext.PLAYER; +import static noppes.npcs.constants.ScriptContext.ABILITY; +import static noppes.npcs.constants.ScriptContext.CHAINED_ABILITY; +import static noppes.npcs.constants.ScriptContext.RECIPE; + +/** + * Controller for managing script hooks. + * Allows addon mods to register custom hooks that appear in script editor GUIs. + * + *

Addon Hook Registration

+ *
{@code
+ * ScriptHookController.Instance.registerHook("player",
+ *     HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class));
+ * }
+ */ +public class ScriptHookController implements IScriptHookHandler { + + public static ScriptHookController Instance; + + private final Map> hookDefinitions = new HashMap<>(); + private int hookRevision = 0; + + public ScriptHookController() { + Instance = this; + initializeBuiltInHooks(); + } + + // ==================== INITIALIZATION ==================== + + private void initializeBuiltInHooks() { + for (ScriptContext context : ScriptContext.values()) { + if (!context.hookContext.isEmpty()) { + hookDefinitions.put(context.hookContext, new LinkedHashMap<>()); + } + } + + initializeNpcHooks(); + initializePlayerHooks(); + initializeBlockHooks(); + initializeItemHooks(); + initializeLinkedItemHooks(); + initializeRecipeHooks(); + initializeEffectHooks(); + initializeAbilityHooks(); + initializeForgeHooks(); + } + + private void initializeNpcHooks() { + // Core lifecycle + hook(NPC, INIT, INpcEvent.InitEvent.class); + hook(NPC, TICK, INpcEvent.UpdateEvent.class); + hook(NPC, TIMER, INpcEvent.TimerEvent.class); + + // Interaction + hook(NPC, INTERACT, INpcEvent.InteractEvent.class); + hook(NPC, DIALOG, INpcEvent.DialogEvent.class); + hook(NPC, DIALOG_CLOSE, INpcEvent.DialogClosedEvent.class); + hook(NPC, COLLIDE, INpcEvent.CollideEvent.class); + + // Combat + hook(NPC, DAMAGED, INpcEvent.DamagedEvent.class); + hook(NPC, KILLED, INpcEvent.DiedEvent.class); + hook(NPC, KILLS, INpcEvent.KilledEntityEvent.class); + hook(NPC, ATTACK_MELEE, INpcEvent.MeleeAttackEvent.class); + hook(NPC, ATTACK_SWING, INpcEvent.SwingEvent.class); + hook(NPC, RANGED_LAUNCHED, INpcEvent.RangedLaunchedEvent.class); + hook(NPC, TARGET, INpcEvent.TargetEvent.class); + hook(NPC, TARGET_LOST, INpcEvent.TargetLostEvent.class); + + // Projectile + hook(NPC, PROJECTILE_TICK, IProjectileEvent.UpdateEvent.class); + hook(NPC, PROJECTILE_IMPACT, IProjectileEvent.ImpactEvent.class); + + // Energy Projectile + hook(NPC, ENERGY_PROJECTILE_FIRED, IEnergyProjectileEvent.FiredEvent.class); + hook(NPC, ENERGY_PROJECTILE_TICK, IEnergyProjectileEvent.UpdateEvent.class); + hook(NPC, ENERGY_PROJECTILE_ENTITY_IMPACT, IEnergyProjectileEvent.EntityImpactEvent.class); + hook(NPC, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class); + hook(NPC, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class); + + // Ability + hook(NPC, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(NPC, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(NPC, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(NPC, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + hook(NPC, ABILITY_TOGGLE, IAbilityEvent.ToggleEvent.class); + hook(NPC, ABILITY_TOGGLE_TICK, IAbilityEvent.ToggleUpdateEvent.class); + + // Chain + hook(NPC, CHAIN_START, IChainEvent.StartEvent.class); + hook(NPC, CHAIN_NEXT, IChainEvent.NextEvent.class); + hook(NPC, CHAIN_COMPLETE, IChainEvent.CompleteEvent.class); + hook(NPC, CHAIN_INTERRUPT, IChainEvent.InterruptEvent.class); + + // Energy Barrier + hook(NPC, ENERGY_BARRIER_SPAWNED, IEnergyBarrierEvent.SpawnedEvent.class); + hook(NPC, ENERGY_BARRIER_TICK, IEnergyBarrierEvent.UpdateEvent.class); + hook(NPC, ENERGY_BARRIER_HIT, IEnergyBarrierEvent.HitEvent.class); + hook(NPC, ENERGY_BARRIER_DESTROYED, IEnergyBarrierEvent.DestroyedEvent.class); + + // Animation + hook(NPC, ANIMATION_START, IAnimationEvent.Started.class); + hook(NPC, ANIMATION_END, IAnimationEvent.Ended.class); + hook(NPC, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class); + hook(NPC, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class); + } + + private void initializePlayerHooks() { + // Core lifecycle + hook(PLAYER, INIT, IPlayerEvent.InitEvent.class); + hook(PLAYER, TICK, IPlayerEvent.UpdateEvent.class); + hook(PLAYER, TIMER, IPlayerEvent.TimerEvent.class); + + // Combat + hook(PLAYER, ATTACK, IPlayerEvent.AttackEvent.class); + hook(PLAYER, ATTACKED, IPlayerEvent.AttackedEvent.class); + hook(PLAYER, DAMAGED, IPlayerEvent.DamagedEvent.class); + hook(PLAYER, DAMAGED_ENTITY, IPlayerEvent.DamagedEntityEvent.class); + hook(PLAYER, KILLS, IPlayerEvent.KilledEntityEvent.class); + hook(PLAYER, KILLED, IPlayerEvent.DiedEvent.class); + + // Interaction + hook(PLAYER, INTERACT, IPlayerEvent.InteractEvent.class); + hook(PLAYER, RIGHT_CLICK, IPlayerEvent.RightClickEvent.class); + hook(PLAYER, BREAK_BLOCK, IPlayerEvent.BreakEvent.class); + hook(PLAYER, CHAT, IPlayerEvent.ChatEvent.class); + + // Connection + hook(PLAYER, LOGIN, IPlayerEvent.LoginEvent.class); + hook(PLAYER, LOGOUT, IPlayerEvent.LogoutEvent.class); + hook(PLAYER, RESPAWN, IPlayerEvent.RespawnEvent.class); + + // Input + hook(PLAYER, KEY_PRESSED, IPlayerEvent.KeyPressedEvent.class); + hook(PLAYER, MOUSE_CLICKED, IPlayerEvent.MouseClickedEvent.class); + + // Items + hook(PLAYER, PICKUP, IPlayerEvent.PickUpEvent.class); + hook(PLAYER, TOSS, IPlayerEvent.TossEvent.class); + hook(PLAYER, DROP, IPlayerEvent.DropEvent.class); + hook(PLAYER, PICKUP_XP, IPlayerEvent.PickupXPEvent.class); + hook(PLAYER, START_USING_ITEM, IPlayerEvent.StartUsingItem.class); + hook(PLAYER, USING_ITEM, IPlayerEvent.UsingItem.class); + hook(PLAYER, STOP_USING_ITEM, IPlayerEvent.StopUsingItem.class); + hook(PLAYER, FINISH_USING_ITEM, IPlayerEvent.FinishUsingItem.class); + hook(PLAYER, CONTAINER_OPEN, IPlayerEvent.ContainerOpen.class); + + // Movement + hook(PLAYER, JUMP, IPlayerEvent.JumpEvent.class); + hook(PLAYER, FALL, IPlayerEvent.FallEvent.class); + hook(PLAYER, CHANGED_DIM, IPlayerEvent.ChangedDimension.class); + + // IWorld + hook(PLAYER, RANGED_CHARGE, IPlayerEvent.RangedChargeEvent.class); + hook(PLAYER, RANGED_LAUNCHED, IPlayerEvent.RangedLaunchedEvent.class); + hook(PLAYER, USE_HOE, IPlayerEvent.UseHoeEvent.class); + hook(PLAYER, BONEMEAL, IPlayerEvent.BonemealEvent.class); + hook(PLAYER, FILL_BUCKET, IPlayerEvent.FillBucketEvent.class); + hook(PLAYER, WAKE_UP, IPlayerEvent.WakeUpEvent.class); + hook(PLAYER, SLEEP, IPlayerEvent.SleepEvent.class); + hook(PLAYER, PLAYSOUND, IPlayerEvent.SoundEvent.class); + hook(PLAYER, LIGHTNING, IPlayerEvent.LightningEvent.class); + hook(PLAYER, SCRIPT_COMMAND, ICustomNPCsEvent.ScriptedCommandEvent.class); + + // Quest + hook(PLAYER, QUEST_START, IQuestEvent.QuestStartEvent.class); + hook(PLAYER, QUEST_COMPLETED, IQuestEvent.QuestCompletedEvent.class); + hook(PLAYER, QUEST_TURNIN, IQuestEvent.QuestTurnedInEvent.class); + + // Dialog + hook(PLAYER, DIALOG_OPEN, IDialogEvent.DialogOpen.class); + hook(PLAYER, DIALOG_OPTION, IDialogEvent.DialogOption.class); + hook(PLAYER, DIALOG_CLOSE, IDialogEvent.DialogClosed.class); + + // Faction + hook(PLAYER, FACTION_POINTS, IFactionEvent.FactionPoints.class); + + // Custom GUI + hook(PLAYER, CUSTOM_GUI_CLOSED, ICustomGuiEvent.CloseEvent.class); + hook(PLAYER, CUSTOM_GUI_BUTTON, ICustomGuiEvent.ButtonEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT, ICustomGuiEvent.SlotEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT_CLICKED, ICustomGuiEvent.SlotClickEvent.class); + hook(PLAYER, CUSTOM_GUI_SCROLL, ICustomGuiEvent.ScrollEvent.class); + hook(PLAYER, CUSTOM_GUI_TEXTFIELD, ICustomGuiEvent.UnfocusedEvent.class); + + // Party + hook(PLAYER, PARTY_QUEST_COMPLETED, IPartyEvent.PartyQuestCompletedEvent.class); + hook(PLAYER, PARTY_QUEST_SET, IPartyEvent.PartyQuestSetEvent.class); + hook(PLAYER, PARTY_QUEST_TURNED_IN, IPartyEvent.PartyQuestTurnedInEvent.class); + hook(PLAYER, PARTY_INVITE, IPartyEvent.PartyInviteEvent.class); + hook(PLAYER, PARTY_KICK, IPartyEvent.PartyKickEvent.class); + hook(PLAYER, PARTY_LEAVE, IPartyEvent.PartyLeaveEvent.class); + hook(PLAYER, PARTY_DISBAND, IPartyEvent.PartyDisbandEvent.class); + + // Animation + hook(PLAYER, ANIMATION_START, IAnimationEvent.Started.class); + hook(PLAYER, ANIMATION_END, IAnimationEvent.Ended.class); + hook(PLAYER, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class); + hook(PLAYER, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class); + + // Energy Projectile + hook(PLAYER, ENERGY_PROJECTILE_FIRED, IEnergyProjectileEvent.FiredEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_TICK, IEnergyProjectileEvent.UpdateEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_ENTITY_IMPACT, IEnergyProjectileEvent.EntityImpactEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class); + + // Energy Barrier + hook(PLAYER, ENERGY_BARRIER_SPAWNED, IEnergyBarrierEvent.SpawnedEvent.class); + hook(PLAYER, ENERGY_BARRIER_TICK, IEnergyBarrierEvent.UpdateEvent.class); + hook(PLAYER, ENERGY_BARRIER_HIT, IEnergyBarrierEvent.HitEvent.class); + hook(PLAYER, ENERGY_BARRIER_DESTROYED, IEnergyBarrierEvent.DestroyedEvent.class); + + // Ability + hook(PLAYER, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(PLAYER, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(PLAYER, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(PLAYER, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(PLAYER, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(PLAYER, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + hook(PLAYER, ABILITY_TOGGLE, IAbilityEvent.ToggleEvent.class); + hook(PLAYER, ABILITY_TOGGLE_TICK, IAbilityEvent.ToggleUpdateEvent.class); + + // Chain + hook(PLAYER, CHAIN_START, IChainEvent.StartEvent.class); + hook(PLAYER, CHAIN_NEXT, IChainEvent.NextEvent.class); + hook(PLAYER, CHAIN_COMPLETE, IChainEvent.CompleteEvent.class); + hook(PLAYER, CHAIN_INTERRUPT, IChainEvent.InterruptEvent.class); + + // Auction + hook(PLAYER, AUCTION_CREATE, IAuctionEvent.CreateEvent.class); + hook(PLAYER, AUCTION_BID, IAuctionEvent.BidEvent.class); + hook(PLAYER, AUCTION_BUYOUT, IAuctionEvent.BuyoutEvent.class); + hook(PLAYER, AUCTION_CANCEL, IAuctionEvent.CancelEvent.class); + hook(PLAYER, AUCTION_CLAIM, IAuctionEvent.ClaimEvent.class); + + // Profile + hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class); + hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class); + hook(PLAYER, PROFILE_CREATE, IPlayerEvent.ProfileEvent.Create.class); + + // Effect (player context) + hook(PLAYER, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(PLAYER, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(PLAYER, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); + } + + private void initializeBlockHooks() { + hook(BLOCK, INIT, IBlockEvent.InitEvent.class); + hook(BLOCK, TICK, IBlockEvent.UpdateEvent.class); + hook(BLOCK, INTERACT, IBlockEvent.InteractEvent.class); + hook(BLOCK, FALLEN_UPON, IBlockEvent.EntityFallenUponEvent.class); + hook(BLOCK, REDSTONE, IBlockEvent.RedstoneEvent.class); + hook(BLOCK, BROKEN, IBlockEvent.BreakEvent.class); + hook(BLOCK, EXPLODED, IBlockEvent.ExplodedEvent.class); + hook(BLOCK, RAIN_FILLED, IBlockEvent.RainFillEvent.class); + hook(BLOCK, NEIGHBOR_CHANGED, IBlockEvent.NeighborChangedEvent.class); + hook(BLOCK, CLICKED, IBlockEvent.ClickedEvent.class); + hook(BLOCK, HARVESTED, IBlockEvent.HarvestedEvent.class); + hook(BLOCK, COLLIDE, IBlockEvent.CollidedEvent.class); + hook(BLOCK, TIMER, IBlockEvent.TimerEvent.class); + } + + private void initializeItemHooks() { + hook(ITEM, INIT, IItemEvent.InitEvent.class); + hook(ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); + } + + private void initializeLinkedItemHooks() { + // Linked item specific + hook(LINKED_ITEM, LINKED_ITEM_BUILD, ILinkedItemEvent.BuildEvent.class); + hook(LINKED_ITEM, LINKED_ITEM_VERSION, ILinkedItemEvent.VersionChangeEvent.class); + + // Standard item hooks + hook(LINKED_ITEM, INIT, IItemEvent.InitEvent.class); + hook(LINKED_ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(LINKED_ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(LINKED_ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(LINKED_ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(LINKED_ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(LINKED_ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(LINKED_ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(LINKED_ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(LINKED_ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(LINKED_ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(LINKED_ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); + } + + private void initializeRecipeHooks() { + hook(RECIPE, RecipeScript.ScriptType.PRE.function, IRecipeEvent.Pre.class); + hook(RECIPE, RecipeScript.ScriptType.POST.function, IRecipeEvent.Post.class); + } + + private void initializeEffectHooks() { + // Effect context uses IPlayerEvent.EffectEvent (same as player context) + hook(EFFECT, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(EFFECT, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(EFFECT, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); + } + + private void initializeAbilityHooks() { + // Ability context (internal per-ability scripts) + hook(ABILITY, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(ABILITY, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(ABILITY, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(ABILITY, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(ABILITY, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(ABILITY, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + hook(ABILITY, ABILITY_TOGGLE, IAbilityEvent.ToggleEvent.class); + hook(ABILITY, ABILITY_TOGGLE_TICK, IAbilityEvent.ToggleUpdateEvent.class); + + // Chained ability context (chain-level lifecycle hooks) + hook(CHAINED_ABILITY, CHAIN_START, IChainEvent.StartEvent.class); + hook(CHAINED_ABILITY, CHAIN_NEXT, IChainEvent.NextEvent.class); + hook(CHAINED_ABILITY, CHAIN_COMPLETE, IChainEvent.CompleteEvent.class); + hook(CHAINED_ABILITY, CHAIN_INTERRUPT, IChainEvent.InterruptEvent.class); + } + + private void initializeForgeHooks() { + hook(FORGE, INIT, IForgeEvent.InitEvent.class); + hook(FORGE, FORGE_WORLD, IForgeEvent.WorldEvent.class); + hook(FORGE, FORGE_ENTITY, IForgeEvent.EntityEvent.class); + hook(FORGE, CNPC_NATURAL_SPAWN, ICustomNPCsEvent.CNPCNaturalSpawnEvent.class); + } + + // ==================== INTERNAL REGISTRATION ==================== + + private void hook(ScriptContext context, EnumScriptType type, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.of(type.function, eventClass)); + } + + private void hook(ScriptContext context, EnumScriptType type) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.simple(type.function)); + } + + private void hook(ScriptContext context, String hookName) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.simple(hookName)); + } + + private void hook(ScriptContext context, String hookName, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.of(hookName, eventClass)); + } + + // ==================== PUBLIC API ==================== + + /** + * Register a hook with event class (auto-derives imports). + * For addon mods to register custom hooks. + * + * @param context Hook context (e.g., "npc", "player") + * @param hookName Function name in scripts + * @param eventClass The event class (imports auto-derived from enclosing class) + */ + public void registerHook(String context, String hookName, Class eventClass) { + if (context == null || hookName == null) + return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + contextDefs.put(hookName, HookDefinition.of(hookName, eventClass)); + hookRevision++; + } + + public void registerHook(ScriptContext context, String hookName, Class eventClass) { + registerHook(context.hookContext, hookName, eventClass); + } + + /** + * Register a simple hook with no event class metadata. + */ + public void registerHook(String context, String hookName) { + if (context == null || hookName == null) + return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + if (!contextDefs.containsKey(hookName)) { + contextDefs.put(hookName, HookDefinition.simple(hookName)); + hookRevision++; + } + } + + public void registerHook(ScriptContext context, String hookName) { + registerHook(context.hookContext, hookName); + } + + /** + * Register a hook definition for addon mods. + */ + public void registerHook(String context, HookDefinition definition) { + if (context == null || definition == null) + return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + contextDefs.put(definition.hookName(), definition); + hookRevision++; + } + + public void registerHook(ScriptContext context, HookDefinition definition) { + registerHook(context.hookContext, definition); + } + + @Override + public void registerHookDefinition(String context, IHookDefinition definition) { + if (definition instanceof HookDefinition) { + registerHook(context, (HookDefinition) definition); + } else if (definition != null) { + registerHook(context, HookDefinition.builder(definition.hookName()) + .eventClass(definition.eventClassName()) + .paramNames(definition.paramNames()) + .requiredImports(definition.requiredImports()) + .cancelable(definition.isCancelable()) + .build()); + } + } + + @Override + public IHookDefinition getHookDefinition(String context, String hookName) { + if (context == null || hookName == null) + return null; + + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null ? contextDefs.get(hookName) : null; + } + + @Override + public List getAllHookDefinitions(String context) { + if (context == null) + return Collections.emptyList(); + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) + return Collections.emptyList(); + + return new ArrayList<>(contextDefs.values()); + } + + @Override + public List getAllHooks(String context) { + if (context == null) + return Collections.emptyList(); + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) + return Collections.emptyList(); + + return new ArrayList<>(contextDefs.keySet()); + } + + @Override + public boolean hasHook(String context, String hookName) { + if (context == null || hookName == null) + return false; + + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null && contextDefs.containsKey(hookName); + } + + @Override + public int getHookRevision() { + return hookRevision; + } + + @Override + public String[] getContexts() { + return hookDefinitions.keySet().toArray(new String[0]); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ServerCloneController.java b/core/src/main/java/noppes/npcs/controllers/ServerCloneController.java new file mode 100644 index 000000000..4e025b594 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/ServerCloneController.java @@ -0,0 +1,754 @@ +package noppes.npcs.controllers; + + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.ICloneHandler; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.IPos; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.data.CloneFolder; +import noppes.npcs.controllers.data.TagMap; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.util.NBTJsonUtil; + +public class ServerCloneController implements ICloneHandler { + public static ServerCloneController Instance; + + protected Map folders = new LinkedHashMap<>(); + + public ServerCloneController() { + loadClones(); + loadFolders(); + } + + private void loadClones() { + try { + File dir = new File(getDir(), ".."); + File file = new File(dir, "clonednpcs.dat"); + if (file.exists()) { + Map> clones = loadOldClones(file); + file.delete(); + file = new File(dir, "clonednpcs.dat_old"); + if (file.exists()) + file.delete(); + + for (int tab : clones.keySet()) { + Map map = clones.get(tab); + for (String name : map.keySet()) { + saveClone(tab, name, map.get(name)); + } + } + } + } catch (Exception e) { + LogWriter.except(e); + } + } + + public File getDir() { + File dir = new File(CustomNpcs.getWorldSaveDirectory(), "clones"); + if (!dir.exists()) + dir.mkdir(); + return dir; + } + + private Map> loadOldClones(File file) throws Exception { + Map> clones = new HashMap>(); + INbt nbttagcompound1; + try (FileInputStream fis = new FileInputStream(file)) { + nbttagcompound1 = NBTIO.readCompressed(fis); + } + INbtList list = nbttagcompound1.getTagList("Data", 10); + if (list == null) { + return clones; + } + for (int i = 0; i < list.tagCount(); i++) { + INbt compound = list.getCompoundTagAt(i); + if (!compound.hasKey("ClonedTab")) { + compound.setInteger("ClonedTab", 1); + } + + Map tab = clones.get(compound.getInteger("ClonedTab")); + if (tab == null) + clones.put(compound.getInteger("ClonedTab"), tab = new HashMap()); + + String name = compound.getString("ClonedName"); + int number = 1; + while (tab.containsKey(name)) { + number++; + name = String.format("%s%s", compound.getString("ClonedName"), number); + } + compound.removeTag("ClonedName"); + compound.removeTag("ClonedTab"); + compound.removeTag("ClonedDate"); + cleanTags(compound); + tab.put(name, compound); + } + return clones; + } + + // ==================== Tab-Based Clone Operations ==================== + + public INbt getCloneData(ICommandSender player, String name, int tab) { + File file = new File(new File(getDir(), tab + ""), name + ".json"); + if (!file.exists()) { + if (player != null) + player.addChatMessage(new ITextComponent("Could not find clone file")); + return null; + } + try { + return NBTJsonUtil.LoadFile(file); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + if (player != null) + player.addChatMessage(new ITextComponent(e.getMessage())); + } + return null; + } + + public void saveClone(int tab, String name, INbt compound) { + try { + File dir = new File(getDir(), tab + ""); + if (!dir.exists()) + dir.mkdir(); + String filename = name + ".json"; + + File file = new File(dir, filename + "_new"); + File file2 = new File(dir, filename); + NBTJsonUtil.SaveFile(file, compound); + addToTagMap(compound, name, tab); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + } catch (Exception e) { + LogWriter.except(e); + } + } + + public List getClones(int tab) { + List list = new ArrayList(); + File dir = new File(getDir(), tab + ""); + if (!dir.exists() || !dir.isDirectory()) + return list; + for (String file : dir.list()) { + if (file.endsWith(".json")) + list.add(file.substring(0, file.length() - 5)); + } + return list; + } + + public List getClonesDate(int tab) { + List list = new ArrayList(); + File dir = new File(getDir(), tab + ""); + if (!dir.exists() || !dir.isDirectory()) + return list; + File[] files = dir.listFiles(); + Arrays.sort(files, new Comparator() { + public int compare(File f1, File f2) { + return Long.compare(f1.lastModified(), f2.lastModified()); + } + }); + + for (File file : files) { + String fileName = file.getName(); + if (fileName.endsWith(".json")) + list.add(fileName.substring(0, fileName.length() - 5)); + } + return list; + } + + public boolean removeClone(String name, int tab) { + File file = new File(new File(getDir(), tab + ""), name + ".json"); + if (!file.exists()) + return false; + file.delete(); + removeFromTagMap(name, tab); + return true; + } + + public String addClone(INbt INbt, String name, int tab) { + cleanTags(INbt); + saveClone(tab, name, INbt); + return name; + } + + public String addClone(INbt INbt, String name, int tab, INbt tempTags) { + cleanTagList(INbt, tempTags); + cleanTags(INbt); + saveClone(tab, name, INbt); + return name; + } + + // ==================== Folder Registry ==================== + + public void loadFolders() { + folders.clear(); + File dir = getDir(); + File[] files = dir.listFiles(); + if (files != null) { + List folderDirs = new ArrayList<>(); + for (File f : files) { + if (!f.isDirectory()) continue; + String name = f.getName(); + if (name.startsWith("___")) continue; + try { + Integer.parseInt(name); + continue; + } catch (NumberFormatException ignored) { + } + folderDirs.add(f); + } + folderDirs.sort(Comparator.comparing(File::getName)); + for (File f : folderDirs) { + CloneFolder folder = new CloneFolder(f.getName()); + folder.createdDate = f.lastModified(); + folders.put(f.getName(), folder); + } + } + } + + public List getFolderList() { + return new ArrayList<>(folders.values()); + } + + public List getFolderNames() { + return new ArrayList<>(folders.keySet()); + } + + public boolean hasFolder(String name) { + return folders.containsKey(name); + } + + // ==================== Folder CRUD ==================== + + public CloneFolder createFolder(String name) { + if (!CloneFolder.isValidName(name) || folders.containsKey(name)) { + return null; + } + CloneFolder folder = new CloneFolder(name); + File dir = getFolderDir(name); + if (!dir.exists()) { + dir.mkdir(); + } + folders.put(name, folder); + return folder; + } + + public boolean renameFolder(String oldName, String newName) { + if (!folders.containsKey(oldName) || !CloneFolder.isValidName(newName) || folders.containsKey(newName)) { + return false; + } + CloneFolder folder = folders.get(oldName); + File oldDir = getFolderDir(oldName); + File newDir = new File(getDir(), newName); + if (!oldDir.renameTo(newDir)) { + return false; + } + folders.remove(oldName); + folder.name = newName; + folders.put(newName, folder); + return true; + } + + public boolean deleteFolder(String name) { + if (!folders.containsKey(name)) { + return false; + } + List clones = getClones(name); + if (!clones.isEmpty()) { + return false; + } + File dir = getFolderDir(name); + if (dir.exists()) { + File tagMapFile = new File(dir, "___tagmap.dat"); + if (tagMapFile.exists()) tagMapFile.delete(); + File tagMapOld = new File(dir, "___tagmap.dat_old"); + if (tagMapOld.exists()) tagMapOld.delete(); + File tagMapNew = new File(dir, "___tagmap.dat_new"); + if (tagMapNew.exists()) tagMapNew.delete(); + dir.delete(); + } + folders.remove(name); + return true; + } + + // ==================== Folder-Based Clone Operations ==================== + + public File getFolderDir(String folderName) { + return new File(getDir(), folderName); + } + + public List getClones(String folderName) { + List list = new ArrayList<>(); + File dir = getFolderDir(folderName); + if (!dir.exists() || !dir.isDirectory()) + return list; + for (String file : dir.list()) { + if (file.endsWith(".json")) + list.add(file.substring(0, file.length() - 5)); + } + return list; + } + + public List getClonesDate(String folderName) { + List list = new ArrayList<>(); + File dir = getFolderDir(folderName); + if (!dir.exists() || !dir.isDirectory()) + return list; + File[] files = dir.listFiles(); + if (files == null) return list; + Arrays.sort(files, new Comparator() { + public int compare(File f1, File f2) { + return Long.compare(f1.lastModified(), f2.lastModified()); + } + }); + for (File file : files) { + String fileName = file.getName(); + if (fileName.endsWith(".json")) + list.add(fileName.substring(0, fileName.length() - 5)); + } + return list; + } + + public INbt getCloneData(ICommandSender player, String name, String folderName) { + File file = new File(getFolderDir(folderName), name + ".json"); + if (!file.exists()) { + if (player != null) + player.addChatMessage(new ITextComponent("Could not find clone file")); + return null; + } + try { + return NBTJsonUtil.LoadFile(file); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + if (player != null) + player.addChatMessage(new ITextComponent(e.getMessage())); + } + return null; + } + + public void saveClone(String folderName, String name, INbt compound) { + try { + File dir = getFolderDir(folderName); + if (!dir.exists()) dir.mkdir(); + String filename = name + ".json"; + File file = new File(dir, filename + "_new"); + File file2 = new File(dir, filename); + NBTJsonUtil.SaveFile(file, compound); + addToTagMap(compound, name, folderName); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + } catch (Exception e) { + LogWriter.except(e); + } + } + + public boolean removeClone(String name, String folderName) { + File file = new File(getFolderDir(folderName), name + ".json"); + if (!file.exists()) + return false; + file.delete(); + removeFromTagMap(name, folderName); + return true; + } + + public String addClone(INbt INbt, String name, String folderName) { + cleanTags(INbt); + saveClone(folderName, name, INbt); + return name; + } + + public String addClone(INbt INbt, String name, String folderName, INbt tempTags) { + cleanTagList(INbt, tempTags); + cleanTags(INbt); + saveClone(folderName, name, INbt); + return name; + } + + // ==================== Move Operations ==================== + + public boolean moveClone(String cloneName, int fromTab, String toFolder) { + INbt data = getCloneData(null, cloneName, fromTab); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromTab(cloneName, fromTab); + + saveClone(toFolder, cloneName, data); + setTagUUIDsForFolder(cloneName, toFolder, tagUUIDs); + + removeClone(cloneName, fromTab); + return true; + } + + public boolean moveClone(String cloneName, String fromFolder, int toTab) { + INbt data = getCloneData(null, cloneName, fromFolder); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromFolder(cloneName, fromFolder); + + saveClone(toTab, cloneName, data); + setTagUUIDsForTab(cloneName, toTab, tagUUIDs); + + removeClone(cloneName, fromFolder); + return true; + } + + public boolean moveClone(String cloneName, String fromFolder, String toFolder) { + INbt data = getCloneData(null, cloneName, fromFolder); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromFolder(cloneName, fromFolder); + + saveClone(toFolder, cloneName, data); + setTagUUIDsForFolder(cloneName, toFolder, tagUUIDs); + + removeClone(cloneName, fromFolder); + return true; + } + + public boolean moveClone(String cloneName, int fromTab, int toTab) { + INbt data = getCloneData(null, cloneName, fromTab); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromTab(cloneName, fromTab); + + saveClone(toTab, cloneName, data); + setTagUUIDsForTab(cloneName, toTab, tagUUIDs); + + removeClone(cloneName, fromTab); + return true; + } + + // ==================== Tag Utilities ==================== + + public INbt cleanTagList(INbt INbt, INbt tempTags) { + HashSet tagUUIDs = new HashSet(); + if (INbt.hasKey("TagUUIDs")) { + INbtList INbtList = INbt.getTagList("TagUUIDs", 8); + for (int i = 0; i < INbtList.tagCount(); i++) { + tagUUIDs.add(UUID.fromString(INbtList.getStringTagAt(i))); + } + + INbt.removeTag("TagUUIDs"); + } + if (tempTags.hasKey("TempTagUUIDs")) { + INbtList INbtList = tempTags.getTagList("TempTagUUIDs", 8); + for (int i = 0; i < INbtList.tagCount(); i++) { + tagUUIDs.add(UUID.fromString(INbtList.getStringTagAt(i))); + } + + tempTags.removeTag("TempTagUUIDs"); + } + + if (tagUUIDs.size() > 0) { + INbtList INbtList = new INbtList(); + for (UUID uuid : tagUUIDs) { + INbtList.appendTag(new NBTTagString(uuid.toString())); + } + INbt.setTag("TagUUIDs", INbtList); + } + + return INbt; + } + + public boolean addToTagMap(INbt INbt, String name, int tab) { + HashSet tagUUIDs = new HashSet(); + if (INbt.hasKey("TagUUIDs")) { + INbtList INbtList = INbt.getTagList("TagUUIDs", 8); + for (int i = 0; i < INbtList.tagCount(); i++) { + tagUUIDs.add(UUID.fromString(INbtList.getStringTagAt(i))); + } + } + + TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); + if (!tagUUIDs.isEmpty()) { + tagMap.putClone(name, tagUUIDs); + } else { + tagMap.removeClone(name); + } + ServerTagMapController.Instance.saveTagMap(tagMap); + return true; + } + + public boolean addToTagMap(INbt INbt, String name, String folderName) { + HashSet tagUUIDs = new HashSet<>(); + if (INbt.hasKey("TagUUIDs")) { + INbtList INbtList = INbt.getTagList("TagUUIDs", 8); + for (int i = 0; i < INbtList.tagCount(); i++) { + tagUUIDs.add(UUID.fromString(INbtList.getStringTagAt(i))); + } + } + + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + if (!tagUUIDs.isEmpty()) { + tagMap.putClone(name, tagUUIDs); + } else { + tagMap.removeClone(name); + } + ServerTagMapController.Instance.saveTagMap(tagMap); + return true; + } + + public boolean removeFromTagMap(String name, int tab) { + TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); + if (tagMap.removeClone(name)) { + ServerTagMapController.Instance.saveTagMap(tagMap); + return true; + } + return false; + } + + public boolean removeFromTagMap(String name, String folderName) { + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + if (tagMap.removeClone(name)) { + ServerTagMapController.Instance.saveTagMap(tagMap); + return true; + } + return false; + } + + protected HashSet getTagUUIDsFromTab(String cloneName, int tab) { + TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); + HashSet uuids = tagMap.getUUIDs(cloneName); + return uuids != null ? new HashSet<>(uuids) : new HashSet<>(); + } + + protected HashSet getTagUUIDsFromFolder(String cloneName, String folderName) { + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + HashSet uuids = tagMap.getUUIDs(cloneName); + return uuids != null ? new HashSet<>(uuids) : new HashSet<>(); + } + + protected void setTagUUIDsForTab(String cloneName, int tab, HashSet tagUUIDs) { + if (tagUUIDs == null || tagUUIDs.isEmpty()) return; + TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); + tagMap.putClone(cloneName, tagUUIDs); + ServerTagMapController.Instance.saveTagMap(tagMap); + } + + protected void setTagUUIDsForFolder(String cloneName, String folderName, HashSet tagUUIDs) { + if (tagUUIDs == null || tagUUIDs.isEmpty()) return; + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + tagMap.putClone(cloneName, tagUUIDs); + ServerTagMapController.Instance.saveTagMap(tagMap); + } + + public void cleanTags(INbt INbt) { + if (INbt.hasKey("ItemGiverId")) + INbt.setInteger("ItemGiverId", 0); + if (INbt.hasKey("TransporterId")) + INbt.setInteger("TransporterId", -1); + + INbt.removeTag("StartPosNew"); + INbt.removeTag("StartPos"); + INbt.removeTag("MovingPathNew"); + INbt.removeTag("Pos"); + INbt.removeTag("Riding"); + + if (!INbt.hasKey("ModRev")) + INbt.setInteger("ModRev", 1); + + if (INbt.hasKey("TransformRole")) { + INbt adv = INbt.getCompoundTag("TransformRole"); + adv.setInteger("TransporterId", -1); + INbt.setTag("TransformRole", adv); + } + + if (INbt.hasKey("TransformJob")) { + INbt adv = INbt.getCompoundTag("TransformJob"); + adv.setInteger("ItemGiverId", 0); + INbt.setTag("TransformJob", adv); + } + + if (INbt.hasKey("TransformAI")) { + INbt adv = INbt.getCompoundTag("TransformAI"); + adv.removeTag("StartPosNew"); + adv.removeTag("StartPos"); + adv.removeTag("MovingPathNew"); + INbt.setTag("TransformAI", adv); + } + } + + // ==================== API Methods (ICloneHandler) ==================== + + public IEntity spawn(double x, double y, double z, int tab, String name, IWorld IWorld, boolean ignoreProtection) { + INbt compound = this.getCloneData((ICommandSender) null, name, tab); + if (compound == null) { + throw new CustomNPCsException("Unknown clone tab:" + tab + " name:" + name, new Object[0]); + } else { + IEntity IEntity; + if (!ignoreProtection) { + IEntity = NoppesUtilServer.spawnCloneWithProtection(compound, (int) x, (int) y, (int) z, IWorld.getMCWorld()); + } else { + IEntity = NoppesUtilServer.spawnClone(compound, (int) x, (int) y, (int) z, IWorld.getMCWorld()); + } + return IEntity == null ? null : NpcAPI.Instance().getIEntity(IEntity); + } + } + + public IEntity spawn(IPos pos, int tab, String name, IWorld IWorld, boolean ignoreProtection) { + return this.spawn(pos.getX(), pos.getY(), pos.getZ(), tab, name, IWorld, ignoreProtection); + } + + public IEntity spawn(double x, double y, double z, int tab, String name, IWorld IWorld) { + return spawn(x, y, z, tab, name, IWorld, true); + } + + public IEntity spawn(IPos pos, int tab, String name, IWorld IWorld) { + return this.spawn(pos.getX(), pos.getY(), pos.getZ(), tab, name, IWorld); + } + + public IEntity[] getTab(int tab, IWorld IWorld) { + File dir = new File(getDir(), tab + ""); + if (!dir.exists() || !dir.isDirectory() || dir.listFiles() == null) { + return new IEntity[]{}; + } + + ArrayList arrayList = new ArrayList<>(); + + try { + for (File file : dir.listFiles()) { + if (file.getName().endsWith(".json")) { + INbt compound = NBTJsonUtil.LoadFile(file); + cleanTags(compound); + IEntity IEntity = EntityList.createEntityFromNBT(compound, IWorld.getMCWorld()); + arrayList.add(IEntity == null ? null : NpcAPI.Instance().getIEntity(IEntity)); + } + } + } catch (Exception ignored) { + } + + return arrayList.toArray(new IEntity[]{}); + } + + public IEntity get(int tab, String name, IWorld IWorld) { + INbt compound = this.getCloneData((ICommandSender) null, name, tab); + if (compound == null) { + throw new CustomNPCsException("Unknown clone tab:" + tab + " name:" + name, new Object[0]); + } else { + cleanTags(compound); + IEntity IEntity = EntityList.createEntityFromNBT(compound, IWorld.getMCWorld()); + return IEntity == null ? null : NpcAPI.Instance().getIEntity(IEntity); + } + } + + public boolean has(int tab, String name) { + INbt compound = this.getCloneData((ICommandSender) null, name, tab); + return compound != null; + } + + public void set(int tab, String name, IEntity IEntity) { + INbt compound = new INbt(); + if (!IEntity.getMCEntity().writeMountToNBT(compound)) + throw new CustomNPCsException("Cannot save dead entities", new Object[0]); + + this.cleanTags(compound); + this.saveClone(tab, name, compound); + } + + public void remove(int tab, String name) { + this.removeClone(name, tab); + } + + // --- Folder API Methods --- + + public String[] getFolders() { + return getFolderNames().toArray(new String[0]); + } + + public IEntity spawn(double x, double y, double z, String folderName, String name, IWorld IWorld, boolean ignoreProtection) { + INbt compound = getCloneData(null, name, folderName); + if (compound == null) { + throw new CustomNPCsException("Unknown clone folder:" + folderName + " name:" + name, new Object[0]); + } + IEntity IEntity; + if (!ignoreProtection) { + IEntity = NoppesUtilServer.spawnCloneWithProtection(compound, (int) x, (int) y, (int) z, IWorld.getMCWorld()); + } else { + IEntity = NoppesUtilServer.spawnClone(compound, (int) x, (int) y, (int) z, IWorld.getMCWorld()); + } + return IEntity == null ? null : NpcAPI.Instance().getIEntity(IEntity); + } + + public IEntity spawn(IPos pos, String folderName, String name, IWorld IWorld, boolean ignoreProtection) { + return this.spawn(pos.getX(), pos.getY(), pos.getZ(), folderName, name, IWorld, ignoreProtection); + } + + public IEntity spawn(double x, double y, double z, String folderName, String name, IWorld IWorld) { + return spawn(x, y, z, folderName, name, IWorld, true); + } + + public IEntity spawn(IPos pos, String folderName, String name, IWorld IWorld) { + return this.spawn(pos.getX(), pos.getY(), pos.getZ(), folderName, name, IWorld); + } + + public IEntity[] getFolder(String folderName, IWorld IWorld) { + File dir = getFolderDir(folderName); + if (!dir.exists() || !dir.isDirectory() || dir.listFiles() == null) { + return new IEntity[]{}; + } + + ArrayList arrayList = new ArrayList<>(); + try { + for (File file : dir.listFiles()) { + if (file.getName().endsWith(".json")) { + INbt compound = NBTJsonUtil.LoadFile(file); + cleanTags(compound); + IEntity IEntity = EntityList.createEntityFromNBT(compound, IWorld.getMCWorld()); + arrayList.add(IEntity == null ? null : NpcAPI.Instance().getIEntity(IEntity)); + } + } + } catch (Exception ignored) { + } + return arrayList.toArray(new IEntity[]{}); + } + + public IEntity get(String folderName, String name, IWorld IWorld) { + INbt compound = getCloneData(null, name, folderName); + if (compound == null) { + throw new CustomNPCsException("Unknown clone folder:" + folderName + " name:" + name, new Object[0]); + } + cleanTags(compound); + IEntity IEntity = EntityList.createEntityFromNBT(compound, IWorld.getMCWorld()); + return IEntity == null ? null : NpcAPI.Instance().getIEntity(IEntity); + } + + public boolean has(String folderName, String name) { + INbt compound = getCloneData(null, name, folderName); + return compound != null; + } + + public void set(String folderName, String name, IEntity IEntity) { + INbt compound = new INbt(); + if (!IEntity.getMCEntity().writeMountToNBT(compound)) + throw new CustomNPCsException("Cannot save dead entities", new Object[0]); + + this.cleanTags(compound); + this.saveClone(folderName, name, compound); + } + + public void remove(String folderName, String name) { + this.removeClone(name, folderName); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/ServerTagMapController.java b/core/src/main/java/noppes/npcs/controllers/ServerTagMapController.java index d25bb5f86..a90113bc9 100644 --- a/core/src/main/java/noppes/npcs/controllers/ServerTagMapController.java +++ b/core/src/main/java/noppes/npcs/controllers/ServerTagMapController.java @@ -1,10 +1,18 @@ package noppes.npcs.controllers; -import noppes.npcs.controllers.data.TagMap; -import kamkeel.npcs.platform.PlatformServiceHolder; -import noppes.npcs.api.INbt; import java.io.File; +import kamkeel.npcs.platform.PlatformServiceHolder; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.data.TagMap; public class ServerTagMapController { public static ServerTagMapController Instance; @@ -14,7 +22,7 @@ public class ServerTagMapController { } public File getDir() { - File dir = new File(PlatformServiceHolder.get().getWorldSaveDirectory(), "clones"); + File dir = new File(PlatformServiceHolder.get().getIWorldSaveDirectory(), "clones"); if (!dir.exists()) dir.mkdir(); return dir; @@ -55,7 +63,7 @@ public class ServerTagMapController { private void loadTagMapFile(File file) throws Exception { // OLD: DataInputStream var1 = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); - // OLD: NBTTagCompound nbtCompound = CompressedStreamTools.read(var1); + // OLD: INbt nbtCompound = CompressedStreamTools.read(var1); // OLD: this.tagMap.readNBT(new NBTWrapper(nbtCompound)); // OLD: var1.close(); INbt nbtCompound = PlatformServiceHolder.get().readCompressedNBT(file); @@ -92,7 +100,7 @@ public class ServerTagMapController { File file = new File(saveDir, "___tagmap.dat_new"); File file1 = new File(saveDir, "___tagmap.dat_old"); File file2 = new File(saveDir, "___tagmap.dat"); - // OLD: CompressedStreamTools.writeCompressed(((NBTWrapper) tagMap.writeNBT()).getMCTag(), new FileOutputStream(file)); + // OLD: NBTIO.writeCompressed(((NBTWrapper) tagMap.writeNBT()).getMCTag(), new FileOutputStream(file)); PlatformServiceHolder.get().writeCompressedNBT(tagMap.writeNBT(), file); if (file1.exists()) { file1.delete(); diff --git a/core/src/main/java/noppes/npcs/controllers/SpawnController.java b/core/src/main/java/noppes/npcs/controllers/SpawnController.java new file mode 100644 index 000000000..56492a979 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/SpawnController.java @@ -0,0 +1,249 @@ +package noppes.npcs.controllers; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.handler.INaturalSpawnsHandler; +import noppes.npcs.api.handler.data.INaturalSpawn; +import noppes.npcs.controllers.data.SpawnData; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.zip.GZIPInputStream; + +public class SpawnController implements INaturalSpawnsHandler { + public HashMap> biomes = new HashMap>(); + public ArrayList data = new ArrayList(); + public Random random = new Random(); + + public static SpawnController Instance; + + private int lastUsedID = 0; + + public SpawnController() { + Instance = this; + loadData(); + } + + private void loadData() { + + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + try { + File file = new File(saveDir, "spawns.dat"); + if (file.exists()) { + loadDataFile(file); + } + } catch (Exception e) { + try { + File file = new File(saveDir, "spawns.dat_old"); + if (file.exists()) { + loadDataFile(file); + } + + } catch (Exception ee) { + } + } + } + + private void loadDataFile(File file) throws IOException { + DataInputStream var1 = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); + loadData(var1); + var1.close(); + } + + public void loadData(DataInputStream stream) throws IOException { + ArrayList data = new ArrayList(); + INbt nbttagcompound1 = CompressedStreamTools.read(stream); + lastUsedID = nbttagcompound1.getInteger("lastID"); + INbtList nbtlist = nbttagcompound1.getTagList("NPCSpawnData", 10); + + if (nbtlist != null) { + for (int i = 0; i < nbtlist.tagCount(); i++) { + INbt INbt = nbtlist.getCompoundTagAt(i); + SpawnData spawn = new SpawnData(); + spawn.readNBT(INbt); + data.add(spawn); + } + } + this.data = data; + fillBiomeData(); + } + + public INbt getNBT() { + INbtList list = new INbtList(); + for (SpawnData spawn : data) { + INbt nbtfactions = new INbt(); + spawn.writeNBT(nbtfactions); + list.appendTag(nbtfactions); + } + INbt INbt = new INbt(); + INbt.setInteger("lastID", lastUsedID); + INbt.setTag("NPCSpawnData", list); + return INbt; + } + + public void saveData() { + try { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + File file = new File(saveDir, "spawns.dat_new"); + File file1 = new File(saveDir, "spawns.dat_old"); + File file2 = new File(saveDir, "spawns.dat"); + NBTIO.writeCompressed(getNBT(), new FileOutputStream(file)); + if (file1.exists()) { + file1.delete(); + } + file2.renameTo(file1); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + LogWriter.except(e); + } + } + + public SpawnData getSpawnData(int id) { + for (SpawnData spawn : data) + if (spawn.id == id) + return spawn; + return null; + } + + public void saveSpawnData(SpawnData spawn) { + if (spawn.id < 0) + spawn.id = getUnusedId(); + SpawnData original = getSpawnData(spawn.id); + if (original == null) + data.add(spawn); + else { + original.readNBT(spawn.writeNBT(new INbt())); + } + fillBiomeData(); + saveData(); + } + + private void fillBiomeData() { + HashMap> biomes = new HashMap>(); + for (SpawnData spawn : data) { + for (String s : spawn.biomes) { + List list = biomes.get(s); + if (list == null) + biomes.put(s, (list = new ArrayList())); + list.add(spawn); + } + } + this.biomes = biomes; + } + + public int getUnusedId() { + lastUsedID++; + return lastUsedID; + } + + public void removeSpawnData(int id) { + ArrayList data = new ArrayList(); + + for (SpawnData spawn : this.data) { + if (spawn.id == id) + continue; + data.add(spawn); + } + this.data = data; + + fillBiomeData(); + saveData(); + } + + public List getSpawnList(String biome) { + return biomes.get(biome); + } + + public SpawnData getRandomSpawnData(String biome, int dimensionId) { + List biomeList = getSpawnList(biome); + if (biomeList == null) + return null; + + ArrayList list = new ArrayList<>(); + for (SpawnData data : biomeList) { + if (data.dimensions.contains(dimensionId)) { + list.add(data); + } + } + if (list.isEmpty()) + return null; + + return (SpawnData) WeightedRandom.getRandomItem(this.random, list); + } + + public Map getScroll() { + Map map = new HashMap(); + for (SpawnData spawn : data) { + map.put(spawn.name, spawn.id); + } + return map; + } + + public void save() { + for (SpawnData spawn : this.data) { + if (spawn.id < 0) { + spawn.id = getUnusedId(); + } + SpawnData original = getSpawnData(spawn.id); + if (original == null) { + this.data.add(spawn); + } else { + original.readNBT(spawn.writeNBT(new INbt())); + } + } + + this.fillBiomeData(); + this.saveData(); + } + + public INaturalSpawn[] getSpawns() { + return data.toArray(new INaturalSpawn[]{}); + } + + public INaturalSpawn[] getSpawns(String biome) { + List biomeSpawns = getSpawnList(biome); + if (biomeSpawns == null) + return new INaturalSpawn[]{}; + return biomeSpawns.toArray(new INaturalSpawn[]{}); + } + + public void addSpawn(INaturalSpawn spawn) { + ((SpawnData) spawn).id = -1; + this.saveSpawnData((SpawnData) spawn); + } + + public void removeSpawn(INaturalSpawn spawn) { + this.removeSpawnData(((SpawnData) spawn).id); + } + + public INaturalSpawn createSpawn() { + return new SpawnData(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/SyncController.java b/core/src/main/java/noppes/npcs/controllers/SyncController.java new file mode 100644 index 000000000..e2dacc020 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/SyncController.java @@ -0,0 +1,10 @@ +package noppes.npcs.controllers; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class SyncController { + // STUB - sync stays in mc1710 +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/controllers/TagController.java b/core/src/main/java/noppes/npcs/controllers/TagController.java index ea443f38c..497cce0f8 100644 --- a/core/src/main/java/noppes/npcs/controllers/TagController.java +++ b/core/src/main/java/noppes/npcs/controllers/TagController.java @@ -1,10 +1,5 @@ package noppes.npcs.controllers; -import noppes.npcs.core.NBT; -import noppes.npcs.controllers.data.Tag; -import kamkeel.npcs.platform.PlatformServiceHolder; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; import java.io.File; import java.util.ArrayList; @@ -13,6 +8,18 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.UUID; +import kamkeel.npcs.platform.PlatformServiceHolder; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.data.Tag; +import noppes.npcs.core.NBT; public class TagController { public HashMap tags; @@ -23,7 +30,7 @@ public class TagController { // TODO: mc1710 version implements ITagHandler and adds: // OLD: public List list() // OLD: public ITag delete(int id) - // OLD: public static void sendCategoryTagMap(EntityPlayerMP player, HashMap> itemTags) — uses GuiDataPacket + // OLD: public static void sendCategoryTagMap(IPlayerMP player, HashMap> itemTags) — uses GuiDataPacket public TagController() { instance = this; tags = new HashMap(); @@ -35,7 +42,7 @@ public class TagController { } private void loadTags() { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); if (saveDir == null) { return; } @@ -60,21 +67,21 @@ public class TagController { // OLD: DataInputStream var1 = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)))); // OLD: loadTags(var1); // OLD: var1.close(); - INbt nbttagcompound1 = PlatformServiceHolder.get().readCompressedNBT(file); - loadTags(nbttagcompound1); + INbt INbt1 = PlatformServiceHolder.get().readCompressedNBT(file); + loadTags(INbt1); } - public void loadTags(INbt nbttagcompound1) { + public void loadTags(INbt INbt1) { HashMap tags = new HashMap(); - lastUsedID = nbttagcompound1.getInteger("lastID"); - INbtList list = nbttagcompound1.getTagList("NPCTags", 10); + lastUsedID = INbt1.getInteger("lastID"); + INbtList list = INbt1.getTagList("NPCTags", 10); if (list != null) { for (int i = 0; i < list.size(); i++) { - INbt nbttagcompound = list.getCompound(i); + INbt INbt = list.getCompound(i); Tag tag = new Tag(); - // OLD: tag.readNBT(new NBTWrapper(nbttagcompound)); - tag.readNBT(nbttagcompound); + // OLD: tag.readNBT(new NBTWrapper(INbt)); + tag.readNBT(INbt); tags.put(tag.id, tag); } } @@ -90,19 +97,19 @@ public class TagController { tag.writeNBT(nbtfactions); list.addCompound(nbtfactions); } - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("lastID", lastUsedID); - nbttagcompound.setTagList("NPCTags", list); - return nbttagcompound; + INbt INbt = NBT.compound(); + INbt.setInteger("lastID", lastUsedID); + INbt.setTagList("NPCTags", list); + return INbt; } public void saveTags() { try { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); File file = new File(saveDir, "tags.dat_new"); File file1 = new File(saveDir, "tags.dat_old"); File file2 = new File(saveDir, "tags.dat"); - // OLD: CompressedStreamTools.writeCompressed(getNBT(), new FileOutputStream(file)); + // OLD: NBTIO.writeCompressed(getNBT(), new FileOutputStream(file)); PlatformServiceHolder.get().writeCompressedNBT(getNBT(), file); if (file1.exists()) { file1.delete(); diff --git a/core/src/main/java/noppes/npcs/controllers/TransportController.java b/core/src/main/java/noppes/npcs/controllers/TransportController.java index a6cc25bfd..12e44193f 100644 --- a/core/src/main/java/noppes/npcs/controllers/TransportController.java +++ b/core/src/main/java/noppes/npcs/controllers/TransportController.java @@ -1,16 +1,23 @@ package noppes.npcs.controllers; -import noppes.npcs.core.NBT; -import noppes.npcs.controllers.data.TransportCategory; -import noppes.npcs.controllers.data.TransportLocation; -import kamkeel.npcs.platform.PlatformServiceHolder; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import kamkeel.npcs.platform.PlatformServiceHolder; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.data.TransportCategory; +import noppes.npcs.controllers.data.TransportLocation; +import noppes.npcs.core.NBT; public class TransportController { private HashMap locations = new HashMap(); @@ -19,8 +26,8 @@ public class TransportController { private int lastUsedID = 0; // TODO: mc1710 version implements ITransportHandler and adds: - // OLD: public TransportLocation saveLocation(int categoryId, NBTTagCompound compound, EntityNPCInterface npc) - // - This overload uses EntityNPCInterface, RoleTransporter, EnumRoleType + // OLD: public TransportLocation saveLocation(int categoryId, INbt compound, IEntityNPCInterface npc) + // - This overload uses IEntityNPCInterface, RoleTransporter, EnumRoleType // OLD: public ITransportCategory[] categories() // OLD: public void createCategory(String title) // OLD: public ITransportCategory getCategory(String title) @@ -39,7 +46,7 @@ public class TransportController { } private void loadCategories() { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); if (saveDir == null) return; try { @@ -64,13 +71,13 @@ public class TransportController { public void loadCategories(File file) throws Exception { HashMap locations = new HashMap(); HashMap categories = new HashMap(); - // OLD: NBTTagCompound nbttagcompound1; + // OLD: INbt INbt1; // OLD: try (FileInputStream fis = new FileInputStream(file)) { - // OLD: nbttagcompound1 = CompressedStreamTools.readCompressed(fis); + // OLD: INbt1 = NBTIO.readCompressed(fis); // OLD: } - INbt nbttagcompound1 = PlatformServiceHolder.get().readCompressedNBT(file); - lastUsedID = nbttagcompound1.getInteger("lastID"); - INbtList list = nbttagcompound1.getTagList("NPCTransportCategories", 10); + INbt INbt1 = PlatformServiceHolder.get().readCompressedNBT(file); + lastUsedID = INbt1.getInteger("lastID"); + INbtList list = INbt1.getTagList("NPCTransportCategories", 10); if (list == null) { return; } @@ -96,19 +103,19 @@ public class TransportController { category.writeNBT(compound); list.addCompound(compound); } - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("lastID", lastUsedID); - nbttagcompound.setTagList("NPCTransportCategories", list); - return nbttagcompound; + INbt INbt = NBT.compound(); + INbt.setInteger("lastID", lastUsedID); + INbt.setTagList("NPCTransportCategories", list); + return INbt; } public void saveCategories() { try { - File saveDir = PlatformServiceHolder.get().getWorldSaveDirectory(); + File saveDir = PlatformServiceHolder.get().getIWorldSaveDirectory(); File file = new File(saveDir, "transport.dat_new"); File file1 = new File(saveDir, "transport.dat_old"); File file2 = new File(saveDir, "transport.dat"); - // OLD: CompressedStreamTools.writeCompressed(getNBT(), new FileOutputStream(file)); + // OLD: NBTIO.writeCompressed(getNBT(), new FileOutputStream(file)); PlatformServiceHolder.get().writeCompressedNBT(getNBT(), file); if (file1.exists()) { file1.delete(); diff --git a/core/src/main/java/noppes/npcs/controllers/data/AbilityScript.java b/core/src/main/java/noppes/npcs/controllers/data/AbilityScript.java new file mode 100644 index 000000000..b213b1fe7 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/AbilityScript.java @@ -0,0 +1,65 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.ScriptContext; +/** + * Script handler for ability scripts. + * Manages a single script container for ability hooks (abilityStart, abilityTick, abilityComplete, etc.). + */ +public class AbilityScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** + * The ability ID (UUID) for packet communication. Empty if not bound. + */ + private String abilityId = ""; + + /** + * Create an unbound AbilityScript (for server-side use). + */ + public AbilityScript() { + } + + /** + * Create an AbilityScript bound to a specific ability (for GUI use). + * + * @param abilityId The UUID of the ability + */ + public AbilityScript(String abilityId) { + this.abilityId = abilityId != null ? abilityId : ""; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.ABILITY; + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.ABILITY); + } + + @Override + public String noticeString() { + return !abilityId.isEmpty() ? "Ability[" + abilityId + "]" : "Ability"; + } + + @Override + public void requestData() { + if (!abilityId.isEmpty()) + AbilityScriptPacket.Get(abilityId); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + if (!abilityId.isEmpty()) + AbilityScriptPacket.Save(abilityId, index, totalCount, nbt); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Animation.java b/core/src/main/java/noppes/npcs/controllers/data/Animation.java new file mode 100644 index 000000000..b96e074bc --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -0,0 +1,410 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.EventHooks; +import noppes.npcs.api.handler.data.IAnimation; +import noppes.npcs.api.handler.data.IAnimationData; +import noppes.npcs.api.handler.data.IFrame; +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; + +public class Animation implements IAnimation { + public AnimationData parent; //Client-sided only + public int id = -1; // Only for internal usage + + public ArrayList frames = new ArrayList<>(); + public int currentFrame = 0; + public int currentFrameTime = 0; + + public String name = ""; + public float speed = 1.0F; + public byte smooth = 0; + public int loop = -1; //If greater than 0 and less than the amount of frames, the animation will begin looping when it reaches this frame. + + public boolean whileStanding = true; + public boolean whileAttacking = true; + public boolean whileMoving = true; + + //Client-sided + public boolean paused; + + protected final Map dataStore = new HashMap<>(); + + public Consumer onAnimationStart; + public BiConsumer onAnimationFrame; + public Consumer onAnimationEnd; + + public Animation() { + } + + public Animation(int id, String name) { + this.name = name; + this.id = id; + } + + public Animation(int id, String name, float speed, byte smooth) { + this.name = name; + this.speed = speed; + this.smooth = smooth; + + this.id = id; + } + + public IAnimationData getParent() { + return this.parent; + } + + public IFrame currentFrame() { + return currentFrame < frames.size() ? frames.get(currentFrame) : null; + } + + public IFrame[] getFrames() { + return frames.toArray(new Frame[0]); + } + + public IAnimation setFrames(IFrame[] frames) { + this.clearFrames(); + for (IFrame frame : frames) { + this.frames.add((Frame) frame); + } + return this; + } + + public IAnimation clearFrames() { + this.frames.clear(); + return this; + } + + public IAnimation addFrame(IFrame frame) { + this.frames.add((Frame) frame); + return this; + } + + public IAnimation addFrame(int index, IFrame frame) { + this.frames.add(index, (Frame) frame); + return this; + } + + public IAnimation removeFrame(IFrame frame) { + this.frames.remove((Frame) frame); + return this; + } + + public IAnimation setName(String name) { + this.name = name; + return this; + } + + public String getName() { + return this.name; + } + + public IAnimation setSpeed(float speed) { + this.speed = speed; + return this; + } + + public float getSpeed() { + return this.speed; + } + + public IAnimation setSmooth(byte smooth) { + this.smooth = smooth; + return this; + } + + public byte isSmooth() { + return this.smooth; + } + + public IAnimation doWhileStanding(boolean whileStanding) { + this.whileStanding = whileStanding; + return this; + } + + public boolean doWhileStanding() { + return this.whileStanding; + } + + public IAnimation doWhileMoving(boolean whileMoving) { + this.whileMoving = whileMoving; + return this; + } + + public boolean doWhileMoving() { + return this.whileMoving; + } + + public IAnimation doWhileAttacking(boolean whileAttacking) { + this.whileAttacking = whileAttacking; + return this; + } + + public boolean doWhileAttacking() { + return this.whileAttacking; + } + + public IAnimation setLoop(int loopAtFrame) { + this.loop = loopAtFrame; + return this; + } + + public int loop() { + return this.loop; + } + + public IAnimation save() { + return AnimationController.Instance.saveAnimation(this); + } + + @Override + public int getID() { + return id; + } + + @Override + public void setID(int newID) { + id = newID; + } + + public long getTotalTime() { + long time = 0; + for (Frame frame : this.frames) { + time += frame.duration; + } + 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(INbt compound) { + if (compound.hasKey("ID")) { + id = compound.getInteger("ID"); + } else if (AnimationController.Instance != null) { + id = AnimationController.Instance.getUnusedId(); + } + + name = compound.getString("Name"); + speed = compound.getFloat("Speed"); + smooth = compound.getByte("Smooth"); + loop = compound.getInteger("Loop"); + + ArrayList frames = new ArrayList(); + INbtList list = compound.getTagList("Frames", 10); + for (int i = 0; i < list.tagCount(); i++) { + INbt item = list.getCompoundTagAt(i); + Frame frame = new Frame(); + frame.parentSpeed = this.speed; + frame.parentSmooth = this.smooth; + frame.readFromNBT(new NBTWrapper(item)); + frames.add(frame); + } + this.frames = frames; + + this.whileStanding = compound.getBoolean("WhileStanding"); + this.whileMoving = compound.getBoolean("WhileWalking"); + this.whileAttacking = compound.getBoolean("WhileAttacking"); + + this.currentFrame = compound.getInteger("CurrentFrame"); + this.currentFrameTime = compound.getInteger("CurrentFrameTime"); + } + + public INbt writeToNBT() { + INbt compound = new INbt(); + compound.setInteger("ID", id); + compound.setString("Name", name); + compound.setFloat("Speed", speed); + compound.setByte("Smooth", smooth); + compound.setInteger("Loop", loop); + + INbtList list = new INbtList(); + for (Frame frame : frames) { + INbt item = ((NBTWrapper) frame.writeToNBT()).getMCTag(); + list.appendTag(item); + } + compound.setTag("Frames", list); + + compound.setBoolean("WhileStanding", whileStanding); + compound.setBoolean("WhileWalking", whileMoving); + compound.setBoolean("WhileAttacking", whileAttacking); + + compound.setInteger("CurrentFrame", currentFrame); + compound.setInteger("CurrentFrameTime", currentFrameTime); + return compound; + } + + public boolean increaseTime() { + if (paused) + return false; + + if (this.parent != null && this.currentFrame < this.frames.size()) { + this.parent.finishedFrame = this.currentFrame; + } + + this.currentFrameTime++; + if (this.currentFrame() != null && this.currentFrameTime == this.currentFrame().getDuration()) { + EventHooks.onAnimationFrameExited(this, this.currentFrame()); + + Frame prevFrame = (Frame) this.currentFrame(); + Frame nextFrame = null; + this.currentFrameTime = 0; + this.currentFrame++; + if (this.currentFrame < this.frames.size()) { + nextFrame = this.frames.get(this.currentFrame); + } else if (this.loop >= 0 && this.loop < this.frames.size()) { + this.currentFrame = this.loop; + } + + if (this.currentFrame() != null) { + EventHooks.onAnimationFrameEntered(this, this.currentFrame()); + fireFrameTask(this.currentFrame); + } else { + EventHooks.onAnimationEnded(this); + fireEndTask(); + } + + if (nextFrame != null) { + for (EnumAnimationPart part : EnumAnimationPart.values()) { + if (prevFrame.frameParts.containsKey(part) && nextFrame.frameParts.containsKey(part)) { + nextFrame.frameParts.get(part).prevRotations = prevFrame.frameParts.get(part).prevRotations; + nextFrame.frameParts.get(part).prevPivots = prevFrame.frameParts.get(part).prevPivots; + } + } + } else if (this.parent != null && this.currentFrame > this.loop) { + this.parent.finishedTime = this.parent.getMCEntity().getAge(); + } + } + + return true; + } + + @ClientOnly + public void jumpToCurrentFrame() { + this.jumpToFrameAtTime(this.currentFrame, 0); + } + + @ClientOnly + public void jumpToFrameAtTime(int frameIndex, int time) { + this.currentFrame = frameIndex; + this.currentFrameTime = time; + + Frame frame = (Frame) this.currentFrame(); + if (frame != null) { + for (EnumAnimationPart part : EnumAnimationPart.values()) { + if (part != null) { + if (frame.frameParts.containsKey(part)) { + frame.frameParts.get(part).jumpToCurrentFrame(); + } + } + } + } + } + + @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; + } + + 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); + } + } + + /** + * Only called on setAnimation, to transfer tasks from + * this global instance stored in AnimationController + * to the new local instance on IEntity's AnimationData, + * and clear them from the global instance. + */ + public void moveFromGlobalToLocal(Animation animation) { + this.onAnimationStart = animation.onAnimationStart; + this.onAnimationFrame = animation.onAnimationFrame; + this.onAnimationEnd = animation.onAnimationEnd; + + animation.onAnimationEnd = null; + animation.onAnimationFrame = null; + animation.onAnimationStart = null; + + this.dataStore.putAll(animation.dataStore); + animation.dataStore.clear(); + } + + +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/AnimationData.java b/core/src/main/java/noppes/npcs/controllers/data/AnimationData.java new file mode 100644 index 000000000..3a3f1f9a0 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/AnimationData.java @@ -0,0 +1,326 @@ +package noppes.npcs.controllers.data; + + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import noppes.npcs.api.entity.IAnimatable; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IAnimation; +import noppes.npcs.api.handler.data.IAnimationData; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumAnimationPart; +import noppes.npcs.CustomNpcs; +import noppes.npcs.DataDisplay; +import noppes.npcs.EventHooks; + +public class AnimationData implements IAnimationData { + //Server-side: DataDisplay, PlayerData + //Client-side: DataDisplay, IPlayer + public Object parent; + + public Animation animation; + public boolean allowAnimation = false; + public long animatingTime = 0; + + public Animation currentClientAnimation; + private boolean isClientAnimating; + + //Client-side values + public int finishedTime = -1; + public int finishedFrame = -1; + + public AnimationData(Object parent) { + this.parent = parent; + } + + public static AnimationData getData(IEntity IEntity) { + if (IEntity == null || IEntity.worldObj == null) { + return null; + } + if (IEntity.worldObj.isRemote) { + return CustomNpcs.proxy.getClientAnimationData(IEntity); + } + + if (IEntity instanceof IPlayer) { + return PlayerData.get((IPlayer) IEntity).animationData; + } else if (IEntity instanceof EntityNPCInterface) { + return ((EntityNPCInterface) IEntity).display.animationData; + } else { + return null; + } + } + + public IAnimatable getEntity() { + IEntity IEntity = NpcAPI.Instance().getIEntity(this.getMCEntity()); + if (IEntity instanceof IAnimatable) { + return (IAnimatable) IEntity; + } + return null; + } + + public IEntityLivingBase getMCEntity() { + if (this.parent instanceof DataDisplay) { + return ((DataDisplay) this.parent).npc; + } else { + if (this.parent instanceof PlayerData) { + return ((PlayerData) this.parent).player; + } else { + return (IPlayer) this.parent; + } + } + } + + public void updateClient() { + this.updateClient(new IPlayer[0]); + } + + public void updateClient(IPlayer... excludedPlayers) { + IEntityLivingBase sendingEntity = parent instanceof PlayerData ? ((PlayerData) parent).player : parent instanceof DataDisplay ? ((DataDisplay) parent).npc : null; + float range = parent instanceof PlayerData ? 160 : 60; + if (sendingEntity != null && sendingEntity.worldObj != null) { + if (sendingEntity.dimension != sendingEntity.worldObj.provider.dimensionId) + sendingEntity.dimension = sendingEntity.worldObj.provider.dimensionId; + + this.animatingTime = 0; + + boolean prevIsClientAnimating = this.isClientAnimating && this.currentClientAnimation.currentFrame() != null; + 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(IPlayer.class, IBoundingBox.getBoundingBox( + sendingEntity.posX - range, sendingEntity.posY - range, sendingEntity.posZ - range, + sendingEntity.posX + range, sendingEntity.posY + range, sendingEntity.posZ + range)); + + entities.removeIf(player -> Arrays.stream(excludedPlayers).anyMatch(exp -> player == exp)); + + for (IPlayer player : entities) { + AnimationData animationData = PlayerData.get(player).animationData; + INbt animationNBT = this.animation != null ? this.animation.writeToNBT() : null; + animationData.viewAnimation(this.animation, this, animationNBT); + } + } + } + + public boolean isClientAnimating() { + return this.isClientAnimating; + } + + public boolean isActive() { + return this.isActive(this.animation); + } + + public boolean isActive(Animation animation) { + if (!this.allowAnimation || animation == null || animation.currentFrame == animation.frames.size() || animation.currentFrame() == null) + return false; + + if (this.parent instanceof DataDisplay) { + EntityNPCInterface npc = ((DataDisplay) this.parent).npc; + if (!npc.isEntityAlive()) + return false; + + return animation.whileAttacking && npc.isAttacking() || animation.whileMoving && npc.isWalking() || animation.whileStanding && !npc.isWalking(); + } else { + IPlayer player; + if (this.parent instanceof PlayerData) { + player = ((PlayerData) this.parent).player; + } else { + player = (IPlayer) this.parent; + } + if (!player.isEntityAlive()) + return false; + + boolean moving = Math.sqrt(player.motionX * player.motionX + player.motionY * player.motionY + player.motionZ * player.motionZ) != 0.0D; + + return animation.whileAttacking && player.getLastAttackerTime() - player.ticksExisted < 20 || animation.whileMoving && moving || animation.whileStanding && !moving; + } + } + + public void increaseTime() { + Animation updateAnimation = null; + if (this.animation != null && this.isActive(this.animation.parent.currentClientAnimation)) { + updateAnimation = this.currentClientAnimation; + } else { + this.isClientAnimating = false; + if (this.isActive()) { + updateAnimation = this.animation; + } + } + + if (updateAnimation != null && updateAnimation.increaseTime()) { + Frame frame = (Frame) updateAnimation.currentFrame(); + if (frame != null) { + this.animatingTime++; + } + } + } + + public void viewAnimation(Animation animation, AnimationData animationData, INbt animationNBT) { + this.viewAnimation(animation, animationData, animationNBT, animationData.allowAnimation, -1, -1); + } + + public boolean viewAnimation(Animation animation, AnimationData animationData, INbt animationNBT, boolean enabled, int currentFrame, int time) { + if (animation != null + && (currentFrame >= animation.frames.size() + || currentFrame >= 0 && animation.frames.get(currentFrame).getDuration() < time)) { + return false; + } + + boolean prevEnabled = animationData.allowAnimation; + animationData.allowAnimation = enabled; + INbt data = animationData.viewWriteNBT(new INbt()); + animationData.allowAnimation = prevEnabled; + + if (animation != null && currentFrame >= 0 && currentFrame < animation.frames.size()) { + data.setInteger("Frame", currentFrame); + data.setInteger("Time", time); + } + + if (animationNBT != null) { + data.setTag("Animation", animationNBT); + } else if (animation != null) { + data.setTag("Animation", animation.writeToNBT()); + } + + IAnimatable animatable = animationData.getEntity(); + IEntity IEntity = ((IEntity) animatable).getMCEntity(); + if (!(IEntity instanceof IPlayer)) { + data.setInteger("EntityId", IEntity.getEntityId()); + } + + PacketHandler.Instance.sendToPlayer(new UpdateAnimationsPacket(data, IEntity.getCommandSenderName()), (IPlayer) ((PlayerData) parent).player); + return true; + } + + public INbt viewWriteNBT(INbt compound) { + compound.setBoolean("AllowAnimation", allowAnimation); + return compound; + } + + public void viewReadFromNBT(INbt compound) { + this.setEnabled(compound.getBoolean("AllowAnimation")); + } + + public INbt writeToNBT(INbt compound) { + if (this.currentClientAnimation != null) { + compound.setBoolean("IsClientAnimating", isClientAnimating); + if (this.isClientAnimating) { + compound.setTag("CurrentAnimation", currentClientAnimation.writeToNBT()); + } + } + compound.setBoolean("AllowAnimation", allowAnimation); + return compound; + } + + public void readFromNBT(INbt compound) { + IEntity IEntity = this.getMCEntity(); + boolean isServer = IEntity != null && IEntity.worldObj != null && !IEntity.worldObj.isRemote; + if (compound.hasKey("IsClientAnimating") && isServer) { + this.isClientAnimating = compound.getBoolean("IsClientAnimating"); + if (this.isClientAnimating) { + this.currentClientAnimation = new Animation(); + this.currentClientAnimation.parent = this; + this.currentClientAnimation.readFromNBT(compound.getCompoundTag("CurrentAnimation")); + this.animation = this.currentClientAnimation; + } + } + this.setEnabled(compound.getBoolean("AllowAnimation")); + } + + public void setEnabled(boolean enabled) { + if (this.allowAnimation != enabled) { + this.allowAnimation = enabled; + if (this.parent instanceof IPlayer) { + this.finishedTime = enabled ? -1 : ((IPlayer) this.parent).getAge(); + } + } + } + + public boolean enabled() { + return this.allowAnimation; + } + + public void setAnimation(IAnimation animation) { + Animation newAnim = null; + if (animation != null) { + newAnim = new Animation(); + newAnim.readFromNBT(((Animation) animation).writeToNBT()); + newAnim.currentFrame = 0; + newAnim.currentFrameTime = 0; + newAnim.parent = this; + newAnim.moveFromGlobalToLocal((Animation) animation); + } + + if (this.getMCEntity() != null && this.getMCEntity().worldObj != null && this.getMCEntity().worldObj.isRemote && newAnim != null) { + this.animatingTime = 0; + } + + Animation prevAnim = this.animation; + this.animation = newAnim; + + if (this.getMCEntity() != null && this.getMCEntity().worldObj != null && this.getMCEntity().worldObj.isRemote && + this.isActive() && prevAnim != null && newAnim != null && !newAnim.frames.isEmpty()) { + Frame frame = (Frame) prevAnim.currentFrame(); + if (frame != null) { + Frame firstFrame = newAnim.frames.get(0); + for (Map.Entry entry : frame.frameParts.entrySet()) { + if (firstFrame.frameParts.containsKey(entry.getKey())) { + FramePart prevFramePart = entry.getValue(); + FramePart newFramePart = firstFrame.frameParts.get(entry.getKey()); + for (int i = 0; i < 3; i++) { + newFramePart.prevPivots[i] = prevFramePart.prevPivots[i]; + newFramePart.prevRotations[i] = prevFramePart.prevRotations[i]; + } + } + } + } + } + } + + public IAnimation getAnimation() { + return animation; + } + + public long getAnimatingTime() { + return this.animatingTime; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java b/core/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java new file mode 100644 index 000000000..0fb89f810 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java @@ -0,0 +1,279 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.util.AttributeItemUtil; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public class AuctionBlacklist { + private static volatile Set blacklistedItems = new HashSet<>(); + private static volatile Set wildcardPatterns = new HashSet<>(); + private static volatile Set blacklistedMods = new HashSet<>(); + private static volatile Set blacklistedNBTTags = new HashSet<>(); + + // Built-in bound item requirement keys (always blocked) + private static final String SOULBIND_KEY = "cnpc_soulbind"; + private static final String PROFILE_SLOT_KEY = "cnpc_profile_slot"; + + // Bypass permission + private static final String BYPASS_PERMISSION = "customnpcs.auction.blacklist.bypass"; + + public static void reload() { + Set newItems = new HashSet<>(); + Set newWildcards = new HashSet<>(); + Set newMods = new HashSet<>(); + Set newNBTTags = new HashSet<>(); + + // Load blacklisted items + for (String item : ConfigMarket.BlacklistedItems) { + if (item == null || item.isEmpty()) continue; + item = item.toLowerCase().trim(); + + if (item.contains("*")) { + String regex = "^" + item.replace(".", "\\.").replace("*", ".*") + "$"; + newWildcards.add(Pattern.compile(regex)); + } else { + newItems.add(item); + } + } + + // Load blacklisted mods + for (String mod : ConfigMarket.BlacklistedMods) { + if (mod == null || mod.isEmpty()) continue; + newMods.add(mod.toLowerCase().trim()); + } + + // Load blacklisted NBT tags + for (String tag : ConfigMarket.BlacklistedNBTTags) { + if (tag == null || tag.isEmpty()) continue; + newNBTTags.add(tag.trim()); + } + + // Atomic swap + blacklistedItems = newItems; + wildcardPatterns = newWildcards; + blacklistedMods = newMods; + blacklistedNBTTags = newNBTTags; + } + + /** + * Check if player can bypass the blacklist (OP or has permission) + */ + public static boolean canBypass(IPlayer player) { + if (player == null) return false; + + // Check OP status + if (player.canCommandSenderUseCommand(2, BYPASS_PERMISSION)) { + return true; + } + + // Check custom permission + return CustomNpcsPermissions.hasCustomPermission(player, BYPASS_PERMISSION); + } + + /** + * Check if item is blacklisted (without bypass check) + */ + public static boolean isBlacklisted(IItemStack item) { + if (item == null || item.getItem() == null) return true; + + // Check bound items (always blocked, even if blacklist is "disabled") + if (hasBoundRequirement(item)) { + return true; + } + + if (!ConfigMarket.BlacklistEnabled) return false; + + // Get registry name + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return true; + + String registryName = (uid.modId + ":" + uid.name).toLowerCase(); + String modId = uid.modId.toLowerCase(); + + // Check exact item match + if (blacklistedItems.contains(registryName)) { + return true; + } + + // Check wildcard patterns + for (Pattern pattern : wildcardPatterns) { + if (pattern.matcher(registryName).matches()) { + return true; + } + } + + // Check mod blacklist + if (blacklistedMods.contains(modId)) { + return true; + } + + // Check NBT tags + if (item.hasTagCompound() && !blacklistedNBTTags.isEmpty()) { + INbt tag = item.getTagCompound(); + for (String nbtKey : blacklistedNBTTags) { + if (tag.hasKey(nbtKey)) { + return true; + } + } + } + + return false; + } + + /** + * Check if item is blacklisted for a specific player (with bypass check) + */ + public static boolean isBlacklistedForPlayer(IItemStack item, IPlayer player) { + if (canBypass(player)) return false; + return isBlacklisted(item); + } + + /** + * Get the registry name of an item + */ + public static String getRegistryName(IItemStack item) { + if (item == null || item.getItem() == null) return null; + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return null; + return uid.modId + ":" + uid.name; + } + + // ========================================= + // Bound Item Checking (RPGCore/Requirements) + // ========================================= + + /** + * Check if item has soulbind or profile slot requirement. + * Requirements are stored under: item.stackTagCompound/RPGCore/Requirements + */ + private static boolean hasBoundRequirement(IItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + + INbt root = item.getTagCompound(); + if (!root.hasKey(AttributeItemUtil.TAG_RPGCORE)) return false; + + INbt rpgCore = root.getCompoundTag(AttributeItemUtil.TAG_RPGCORE); + if (!rpgCore.hasKey(AttributeItemUtil.TAG_REQUIREMENTS)) return false; + + INbt requirements = rpgCore.getCompoundTag(AttributeItemUtil.TAG_REQUIREMENTS); + return requirements.hasKey(SOULBIND_KEY) || requirements.hasKey(PROFILE_SLOT_KEY); + } + + // ========================================= + // Runtime Blacklist Management + // ========================================= + + public static boolean addItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + Set newWildcards = new HashSet<>(wildcardPatterns); + newWildcards.add(Pattern.compile(regex)); + wildcardPatterns = newWildcards; + } else { + Set newItems = new HashSet<>(blacklistedItems); + newItems.add(registryName); + blacklistedItems = newItems; + } + return true; + } + + public static boolean removeItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + Set newWildcards = new HashSet<>(wildcardPatterns); + boolean removed = false; + Iterator it = newWildcards.iterator(); + while (it.hasNext()) { + if (it.next().pattern().equals(regex)) { + it.remove(); + removed = true; + } + } + wildcardPatterns = newWildcards; + return removed; + } else { + Set newItems = new HashSet<>(blacklistedItems); + boolean removed = newItems.remove(registryName); + blacklistedItems = newItems; + return removed; + } + } + + public static boolean addMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + Set newMods = new HashSet<>(blacklistedMods); + boolean added = newMods.add(modId.toLowerCase().trim()); + blacklistedMods = newMods; + return added; + } + + public static boolean removeMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + Set newMods = new HashSet<>(blacklistedMods); + boolean removed = newMods.remove(modId.toLowerCase().trim()); + blacklistedMods = newMods; + return removed; + } + + public static boolean addNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + Set newTags = new HashSet<>(blacklistedNBTTags); + boolean added = newTags.add(tag.trim()); + blacklistedNBTTags = newTags; + return added; + } + + public static boolean removeNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + Set newTags = new HashSet<>(blacklistedNBTTags); + boolean removed = newTags.remove(tag.trim()); + blacklistedNBTTags = newTags; + return removed; + } + + // ========================================= + // Getters for listing + // ========================================= + + public static List getBlacklistedItems() { + List result = new ArrayList<>(blacklistedItems); + for (Pattern p : wildcardPatterns) { + // Convert regex back to wildcard format + String pattern = p.pattern(); + pattern = pattern.substring(1, pattern.length() - 1); // Remove ^ and $ + pattern = pattern.replace("\\.", ".").replace(".*", "*"); + result.add(pattern); + } + return result; + } + + public static List getBlacklistedMods() { + return new ArrayList<>(blacklistedMods); + } + + public static List getBlacklistedNBTTags() { + return new ArrayList<>(blacklistedNBTTags); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/core/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java new file mode 100644 index 000000000..488dfd43a --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -0,0 +1,260 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumClaimType; +import java.util.UUID; + +public class AuctionClaim implements IAuctionClaim { + public String id; + public UUID playerUUID; + public String playerName; + public String listingId; + public EnumClaimType type; + public IItemStack item; // The item (for ITEM claims only) + public String itemName; // Display name of item (for CURRENCY/REFUND tooltip) + public String otherPlayerName; // Buyer name (CURRENCY) or who outbid (REFUND) + public long currency; + public long createdTime; + public boolean claimed; + public boolean isReturned; // For ITEM claims: true if returned to seller, false if won by buyer + + public AuctionClaim() { + this.id = UUID.randomUUID().toString(); + this.createdTime = System.currentTimeMillis(); + this.claimed = false; + this.isReturned = false; + this.itemName = ""; + this.otherPlayerName = ""; + } + + /** + * Create claim for winning an item (buyer won auction or buyout) + */ + public static AuctionClaim createItemWonClaim(UUID playerUUID, String playerName, String listingId, IItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.ITEM; + claim.item = item != null ? item.copy() : null; + claim.currency = 0; + claim.isReturned = false; + return claim; + } + + /** + * Create claim for returned item (seller's expired/cancelled listing) + */ + public static AuctionClaim createItemReturnedClaim(UUID playerUUID, String playerName, String listingId, IItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.ITEM; + claim.item = item != null ? item.copy() : null; + claim.currency = 0; + claim.isReturned = true; + return claim; + } + + /** + * Create claim for seller's proceeds (stores item name for tooltip, not full item) + */ + public static AuctionClaim createCurrencyClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String buyerName) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.CURRENCY; + claim.item = null; // No item stored for currency claims + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = buyerName != null ? buyerName : ""; + claim.currency = currency; + claim.isReturned = false; + return claim; + } + + /** + * Create claim for outbid refund (stores item for display and rebid option) + */ + public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String outbidderName, IItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.REFUND; + claim.item = item != null ? item.copy() : null; // Store item for display and rebid + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = outbidderName != null ? outbidderName : ""; + claim.currency = currency; + claim.isReturned = false; + return claim; + } + + public boolean isExpired(int expirationDays) { + long expirationMs = expirationDays * 24L * 60L * 60L * 1000L; + return System.currentTimeMillis() - createdTime > expirationMs; + } + + public long getDaysUntilExpiration(int expirationDays) { + long expirationMs = expirationDays * 24L * 60L * 60L * 1000L; + long remaining = (createdTime + expirationMs) - System.currentTimeMillis(); + return Math.max(0, remaining / (24L * 60L * 60L * 1000L)); + } + + public boolean isForPlayer(UUID uuid) { + return playerUUID != null && playerUUID.equals(uuid); + } + + // ========================================= + // IAuctionClaim Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getPlayerUUID() { + return playerUUID != null ? playerUUID.toString() : null; + } + + @Override + public String getPlayerName() { + return playerName; + } + + @Override + public String getListingId() { + return listingId; + } + + @Override + public int getType() { + return type.ordinal(); + } + + @Override + public boolean isItemClaim() { + return type == EnumClaimType.ITEM; + } + + @Override + public boolean isCurrencyClaim() { + return type.isCurrency(); + } + + @Override + public boolean isRefundClaim() { + return type == EnumClaimType.REFUND; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public String getItemName() { + return itemName; + } + + @Override + public long getCurrency() { + return currency; + } + + @Override + public String getOtherPlayerName() { + return otherPlayerName; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public boolean isClaimed() { + return claimed; + } + + @Override + public boolean isReturnedItem() { + return isReturned; + } + + @Override + public long getDaysUntilExpiration() { + return getDaysUntilExpiration(ConfigMarket.ClaimExpirationDays); + } + + @Override + public boolean isExpired() { + return isExpired(ConfigMarket.ClaimExpirationDays); + } + + public INbt writeToNBT(INbt compound) { + compound.setString("ID", id); + compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); + compound.setString("PlayerName", playerName != null ? playerName : ""); + compound.setString("ListingID", listingId != null ? listingId : ""); + compound.setInteger("Type", type.ordinal()); + + if (item != null) { + INbt itemTag = new INbt(); + item.writeToNBT(itemTag); + compound.setTag("Item", itemTag); + } + + compound.setString("ItemName", itemName != null ? itemName : ""); + compound.setString("OtherPlayerName", otherPlayerName != null ? otherPlayerName : ""); + compound.setLong("Currency", currency); + compound.setLong("CreatedTime", createdTime); + compound.setBoolean("Claimed", claimed); + compound.setBoolean("IsReturned", isReturned); + + return compound; + } + + public void readFromNBT(INbt compound) { + id = compound.getString("ID"); + + String playerUUIDStr = compound.getString("PlayerUUID"); + playerUUID = playerUUIDStr.isEmpty() ? null : UUID.fromString(playerUUIDStr); + playerName = compound.getString("PlayerName"); + + listingId = compound.getString("ListingID"); + type = EnumClaimType.fromOrdinal(compound.getInteger("Type")); + + if (compound.hasKey("Item")) { + item = IItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } else { + item = null; + } + + itemName = compound.getString("ItemName"); + otherPlayerName = compound.getString("OtherPlayerName"); + currency = compound.getLong("Currency"); + createdTime = compound.getLong("CreatedTime"); + claimed = compound.getBoolean("Claimed"); + isReturned = compound.getBoolean("IsReturned"); + } + + public static AuctionClaim fromNBT(INbt compound) { + AuctionClaim claim = new AuctionClaim(); + claim.readFromNBT(compound); + return claim; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/core/src/main/java/noppes/npcs/controllers/data/AuctionListing.java new file mode 100644 index 000000000..4c7397193 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -0,0 +1,273 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionStatus; +import java.util.UUID; + +public class AuctionListing implements IAuctionListing { + public String id; + public UUID sellerUUID; + public String sellerName; + public IItemStack item; + public long startingPrice; + public long buyoutPrice; + public long currentBid; + public UUID highBidderUUID; + public String highBidderName; + public int bidCount; + public long createdTime; + public long endTime; + public EnumAuctionStatus status; + public boolean isGlobalListing; + + public AuctionListing() { + this.id = UUID.randomUUID().toString(); + this.status = EnumAuctionStatus.ACTIVE; + this.bidCount = 0; + this.currentBid = 0; + this.createdTime = System.currentTimeMillis(); + this.isGlobalListing = false; + } + + public AuctionListing(UUID sellerUUID, String sellerName, IItemStack item, long startingPrice, long buyoutPrice, long durationMs) { + this(); + this.sellerUUID = sellerUUID; + this.sellerName = sellerName; + this.item = item.copy(); + this.startingPrice = startingPrice; + this.buyoutPrice = buyoutPrice; + this.currentBid = 0; + this.endTime = this.createdTime + durationMs; + } + + public long getTimeRemaining() { + return Math.max(0, endTime - System.currentTimeMillis()); + } + + public long getEffectivePrice() { + return hasBids() ? currentBid : startingPrice; + } + + public long getMinimumBid(double minIncrementPercent) { + if (!hasBids()) { + return startingPrice; + } + long increment = (long) Math.ceil(currentBid * minIncrementPercent); + return currentBid + Math.max(1, increment); + } + + public boolean isSeller(UUID playerUUID) { + return sellerUUID != null && sellerUUID.equals(playerUUID); + } + + public boolean isHighBidder(UUID playerUUID) { + return highBidderUUID != null && highBidderUUID.equals(playerUUID); + } + + public void extendForSnipeProtection(int snipeProtectionMinutes) { + long snipeProtectionMs = snipeProtectionMinutes * 60 * 1000L; + long timeRemaining = getTimeRemaining(); + if (timeRemaining < snipeProtectionMs) { + endTime = System.currentTimeMillis() + snipeProtectionMs; + } + } + + // ========================================= + // IAuctionListing Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getSellerUUID() { + return sellerUUID != null ? sellerUUID.toString() : null; + } + + @Override + public String getSellerName() { + return sellerName; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public long getStartingPrice() { + return startingPrice; + } + + @Override + public long getBuyoutPrice() { + return buyoutPrice; + } + + @Override + public long getCurrentBid() { + return currentBid; + } + + @Override + public String getHighBidderUUID() { + return highBidderUUID != null ? highBidderUUID.toString() : null; + } + + @Override + public String getHighBidderName() { + return highBidderName; + } + + @Override + public int getBidCount() { + return bidCount; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public long getEndTime() { + return endTime; + } + + @Override + public long getRemainingTime() { + return getTimeRemaining(); + } + + @Override + public int getStatus() { + return status.ordinal(); + } + + @Override + public long getMinimumBid() { + return getMinimumBid(ConfigMarket.MinBidIncrementPercent); + } + + @Override + public boolean isSeller(String playerUUID) { + if (playerUUID == null || sellerUUID == null) return false; + return sellerUUID.toString().equals(playerUUID); + } + + @Override + public boolean isHighBidder(String playerUUID) { + if (playerUUID == null || highBidderUUID == null) return false; + return highBidderUUID.toString().equals(playerUUID); + } + + @Override + public String getRemainingTimeFormatted() { + long remaining = getTimeRemaining(); + if (remaining <= 0) return "Ended"; + + long hours = remaining / (60 * 60 * 1000); + long minutes = (remaining % (60 * 60 * 1000)) / (60 * 1000); + + if (hours > 0) { + return hours + "h " + minutes + "m"; + } else if (minutes > 0) { + return minutes + "m"; + } else { + long seconds = (remaining % (60 * 1000)) / 1000; + return seconds + "s"; + } + } + + // Override existing methods with @Override annotation + @Override + public boolean hasBids() { + return bidCount > 0 && highBidderUUID != null; + } + + @Override + public boolean hasBuyout() { + return buyoutPrice > 0; + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() >= endTime; + } + + @Override + public boolean isActive() { + return status == EnumAuctionStatus.ACTIVE && !isExpired(); + } + + public INbt writeToNBT(INbt compound) { + compound.setString("ID", id); + compound.setString("SellerUUID", sellerUUID != null ? sellerUUID.toString() : ""); + compound.setString("SellerName", sellerName != null ? sellerName : ""); + + if (item != null) { + INbt itemTag = new INbt(); + item.writeToNBT(itemTag); + compound.setTag("Item", itemTag); + } + + compound.setLong("StartingPrice", startingPrice); + compound.setLong("BuyoutPrice", buyoutPrice); + compound.setLong("CurrentBid", currentBid); + + compound.setString("HighBidderUUID", highBidderUUID != null ? highBidderUUID.toString() : ""); + compound.setString("HighBidderName", highBidderName != null ? highBidderName : ""); + + compound.setInteger("BidCount", bidCount); + compound.setLong("CreatedTime", createdTime); + compound.setLong("EndTime", endTime); + compound.setInteger("Status", status.ordinal()); + compound.setBoolean("IsGlobalListing", isGlobalListing); + + return compound; + } + + public void readFromNBT(INbt compound) { + id = compound.getString("ID"); + + String sellerUUIDStr = compound.getString("SellerUUID"); + sellerUUID = sellerUUIDStr.isEmpty() ? null : UUID.fromString(sellerUUIDStr); + sellerName = compound.getString("SellerName"); + + if (compound.hasKey("Item")) { + item = IItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } + + startingPrice = compound.getLong("StartingPrice"); + buyoutPrice = compound.getLong("BuyoutPrice"); + currentBid = compound.getLong("CurrentBid"); + + String highBidderUUIDStr = compound.getString("HighBidderUUID"); + highBidderUUID = highBidderUUIDStr.isEmpty() ? null : UUID.fromString(highBidderUUIDStr); + highBidderName = compound.getString("HighBidderName"); + + bidCount = compound.getInteger("BidCount"); + createdTime = compound.getLong("CreatedTime"); + endTime = compound.getLong("EndTime"); + status = EnumAuctionStatus.fromOrdinal(compound.getInteger("Status")); + isGlobalListing = compound.getBoolean("IsGlobalListing"); + } + + public static AuctionListing fromNBT(INbt compound) { + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(compound); + return listing; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Availability.java b/core/src/main/java/noppes/npcs/controllers/data/Availability.java new file mode 100644 index 000000000..58988abc6 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/Availability.java @@ -0,0 +1,529 @@ +package noppes.npcs.controllers.data; + + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import java.util.ArrayList; +import java.util.List; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IAvailability; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumAvailabilityDialog; +import noppes.npcs.constants.EnumAvailabilityFaction; +import noppes.npcs.constants.EnumAvailabilityFactionType; +import noppes.npcs.constants.EnumAvailabilityQuest; +import noppes.npcs.constants.EnumDayTime; +import noppes.npcs.controllers.FactionController; +import noppes.npcs.controllers.PlayerQuestController; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.ICompatibilty; +import noppes.npcs.VersionCompatibility; + +public class Availability implements ICompatibilty, IAvailability { + public int version = VersionCompatibility.ModRev; + + public EnumAvailabilityDialog dialogAvailable = EnumAvailabilityDialog.Always; + public EnumAvailabilityDialog dialog2Available = EnumAvailabilityDialog.Always; + public EnumAvailabilityDialog dialog3Available = EnumAvailabilityDialog.Always; + public EnumAvailabilityDialog dialog4Available = EnumAvailabilityDialog.Always; + public int dialogId = -1; + public int dialog2Id = -1; + public int dialog3Id = -1; + public int dialog4Id = -1; + + public EnumAvailabilityQuest questAvailable = EnumAvailabilityQuest.Always; + public EnumAvailabilityQuest quest2Available = EnumAvailabilityQuest.Always; + public EnumAvailabilityQuest quest3Available = EnumAvailabilityQuest.Always; + public EnumAvailabilityQuest quest4Available = EnumAvailabilityQuest.Always; + public int questId = -1; + public int quest2Id = -1; + public int quest3Id = -1; + public int quest4Id = -1; + + public EnumDayTime daytime = EnumDayTime.Always; + + public int factionId = -1; + public int faction2Id = -1; + + public EnumAvailabilityFactionType factionAvailable = EnumAvailabilityFactionType.Always; + public EnumAvailabilityFactionType faction2Available = EnumAvailabilityFactionType.Always; + + public EnumAvailabilityFaction factionStance = EnumAvailabilityFaction.Friendly; + public EnumAvailabilityFaction faction2Stance = EnumAvailabilityFaction.Friendly; + + public int minPlayerLevel = 0; + + public void readFromNBT(INbt compound) { + version = compound.getInteger("ModRev"); + VersionCompatibility.CheckAvailabilityCompatibility(this, compound); + + dialogAvailable = EnumAvailabilityDialog.values()[compound.getInteger("AvailabilityDialog")]; + dialog2Available = EnumAvailabilityDialog.values()[compound.getInteger("AvailabilityDialog2")]; + dialog3Available = EnumAvailabilityDialog.values()[compound.getInteger("AvailabilityDialog3")]; + dialog4Available = EnumAvailabilityDialog.values()[compound.getInteger("AvailabilityDialog4")]; + + dialogId = compound.getInteger("AvailabilityDialogId"); + dialog2Id = compound.getInteger("AvailabilityDialog2Id"); + dialog3Id = compound.getInteger("AvailabilityDialog3Id"); + dialog4Id = compound.getInteger("AvailabilityDialog4Id"); + + questAvailable = EnumAvailabilityQuest.values()[compound.getInteger("AvailabilityQuest")]; + quest2Available = EnumAvailabilityQuest.values()[compound.getInteger("AvailabilityQuest2")]; + quest3Available = EnumAvailabilityQuest.values()[compound.getInteger("AvailabilityQuest3")]; + quest4Available = EnumAvailabilityQuest.values()[compound.getInteger("AvailabilityQuest4")]; + + questId = compound.getInteger("AvailabilityQuestId"); + quest2Id = compound.getInteger("AvailabilityQuest2Id"); + quest3Id = compound.getInteger("AvailabilityQuest3Id"); + quest4Id = compound.getInteger("AvailabilityQuest4Id"); + + setFactionAvailability(compound.getInteger("AvailabilityFaction")); + setFactionAvailabilityStance(compound.getInteger("AvailabilityFactionStance")); + + setFaction2Availability(compound.getInteger("AvailabilityFaction2")); + setFaction2AvailabilityStance(compound.getInteger("AvailabilityFaction2Stance")); + + factionId = compound.getInteger("AvailabilityFactionId"); + faction2Id = compound.getInteger("AvailabilityFaction2Id"); + + daytime = EnumDayTime.values()[compound.getInteger("AvailabilityDayTime")]; + + minPlayerLevel = compound.getInteger("AvailabilityMinPlayerLevel"); + } + + public INbt writeToNBT(INbt compound) { + compound.setInteger("ModRev", version); + + compound.setInteger("AvailabilityDialog", dialogAvailable.ordinal()); + compound.setInteger("AvailabilityDialog2", dialog2Available.ordinal()); + compound.setInteger("AvailabilityDialog3", dialog3Available.ordinal()); + compound.setInteger("AvailabilityDialog4", dialog4Available.ordinal()); + + compound.setInteger("AvailabilityDialogId", dialogId); + compound.setInteger("AvailabilityDialog2Id", dialog2Id); + compound.setInteger("AvailabilityDialog3Id", dialog3Id); + compound.setInteger("AvailabilityDialog4Id", dialog4Id); + + compound.setInteger("AvailabilityQuest", questAvailable.ordinal()); + compound.setInteger("AvailabilityQuest2", quest2Available.ordinal()); + compound.setInteger("AvailabilityQuest3", quest3Available.ordinal()); + compound.setInteger("AvailabilityQuest4", quest4Available.ordinal()); + + compound.setInteger("AvailabilityQuestId", questId); + compound.setInteger("AvailabilityQuest2Id", quest2Id); + compound.setInteger("AvailabilityQuest3Id", quest3Id); + compound.setInteger("AvailabilityQuest4Id", quest4Id); + + compound.setInteger("AvailabilityFaction", factionAvailable.ordinal()); + compound.setInteger("AvailabilityFactionStance", factionStance.ordinal()); + + compound.setInteger("AvailabilityFactionId", factionId); + + compound.setInteger("AvailabilityFaction2", faction2Available.ordinal()); + compound.setInteger("AvailabilityFaction2Stance", faction2Stance.ordinal()); + + compound.setInteger("AvailabilityFaction2Id", faction2Id); + + compound.setInteger("AvailabilityDayTime", daytime.ordinal()); + compound.setInteger("AvailabilityMinPlayerLevel", minPlayerLevel); + return compound; + } + + public boolean isDefault() { + return dialogAvailable == EnumAvailabilityDialog.Always && + dialog2Available == EnumAvailabilityDialog.Always && + dialog3Available == EnumAvailabilityDialog.Always && + dialog4Available == EnumAvailabilityDialog.Always && + dialogId == -1 && + + questAvailable == EnumAvailabilityQuest.Always && + quest2Available == EnumAvailabilityQuest.Always && + quest3Available == EnumAvailabilityQuest.Always && + quest4Available == EnumAvailabilityQuest.Always && + questId == -1 && + + factionAvailable == EnumAvailabilityFactionType.Always && + faction2Available == EnumAvailabilityFactionType.Always && + factionId == -1 && + faction2Id == -1 && + factionStance == EnumAvailabilityFaction.Friendly && + faction2Stance == EnumAvailabilityFaction.Friendly && + + daytime == EnumDayTime.Always && + minPlayerLevel == 0; + } + + public void setFactionAvailability(int value) { + factionAvailable = EnumAvailabilityFactionType.values()[value]; + } + + public void setFaction2Availability(int value) { + faction2Available = EnumAvailabilityFactionType.values()[value]; + } + + public void setFactionAvailabilityStance(int integer) { + factionStance = EnumAvailabilityFaction.values()[integer]; + } + + public void setFaction2AvailabilityStance(int integer) { + faction2Stance = EnumAvailabilityFaction.values()[integer]; + } + + public boolean isAvailable(IPlayer player) { + if (daytime == EnumDayTime.Day) { + long time = player.worldObj.getWorldTime() % 24000; + if (time > 12000) + return false; + } + if (daytime == EnumDayTime.Night) { + long time = player.worldObj.getWorldTime() % 24000; + if (time < 12000) + return false; + } + + if (!dialogAvailable(dialogId, dialogAvailable, player)) + return false; + if (!dialogAvailable(dialog2Id, dialog2Available, player)) + return false; + if (!dialogAvailable(dialog3Id, dialog3Available, player)) + return false; + if (!dialogAvailable(dialog4Id, dialog4Available, player)) + return false; + + if (!questAvailable(questId, questAvailable, player)) + return false; + if (!questAvailable(quest2Id, quest2Available, player)) + return false; + if (!questAvailable(quest3Id, quest3Available, player)) + return false; + if (!questAvailable(quest4Id, quest4Available, player)) + return false; + + if (!factionAvailable(factionId, factionStance, factionAvailable, player)) + return false; + if (!factionAvailable(faction2Id, faction2Stance, faction2Available, player)) + return false; + + if (player.experienceLevel < minPlayerLevel) + return false; + + return true; + } + + @ClientOnly + public List isAvailableText(IPlayer player) { + List errors = new ArrayList(); + + // Check daytime conditions. + if (daytime == EnumDayTime.Day) { + long time = player.worldObj.getWorldTime() % 24000; + if (time > 12000) { + errors.add(PlatformServiceHolder.get().translateToLocal("availability.error.day")); + } + } else if (daytime == EnumDayTime.Night) { + long time = player.worldObj.getWorldTime() % 24000; + if (time < 12000) { + errors.add(PlatformServiceHolder.get().translateToLocal("availability.error.night")); + } + } + + // Check dialog conditions. + if (!dialogAvailable(dialogId, dialogAvailable, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.dialog", dialogAvailable.toString(), dialogId)); + } + if (!dialogAvailable(dialog2Id, dialog2Available, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.dialog", dialog2Available.toString(), dialog2Id)); + } + if (!dialogAvailable(dialog3Id, dialog3Available, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.dialog", dialog3Available.toString(), dialog3Id)); + } + if (!dialogAvailable(dialog4Id, dialog4Available, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.dialog", dialog4Available.toString(), dialog4Id)); + } + + // Check quest conditions. + if (!questAvailable(questId, questAvailable, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.quest", questAvailable.toString(), questId)); + } + if (!questAvailable(quest2Id, quest2Available, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.quest", quest2Available.toString(), quest2Id)); + } + if (!questAvailable(quest3Id, quest3Available, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.quest", quest3Available.toString(), quest3Id)); + } + if (!questAvailable(quest4Id, quest4Available, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.quest", quest4Available.toString(), quest4Id)); + } + + // Check faction conditions. + if (!factionAvailable(factionId, factionStance, factionAvailable, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.faction", factionAvailable.toString(), factionStance.toString(), factionId)); + } + if (!factionAvailable(faction2Id, faction2Stance, faction2Available, player)) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.faction", faction2Available.toString(), faction2Stance.toString(), faction2Id)); + } + if (player.experienceLevel < minPlayerLevel) { + errors.add(PlatformServiceHolder.get().translateToLocalFormatted("availability.error.xp", minPlayerLevel)); + } + return errors; + } + + private boolean factionAvailable(int id, EnumAvailabilityFaction stance, EnumAvailabilityFactionType available, IPlayer player) { + if (available == EnumAvailabilityFactionType.Always) + return true; + + Faction faction = FactionController.getInstance().get(id); + if (faction == null) + return true; + + PlayerFactionData data = PlayerData.get(player).factionData; + int points = data.getFactionPoints(id); + + EnumAvailabilityFaction current = EnumAvailabilityFaction.Neutral; + if (faction.neutralPoints >= points) + current = EnumAvailabilityFaction.Hostile; + if (faction.friendlyPoints < points) + current = EnumAvailabilityFaction.Friendly; + + if (available == EnumAvailabilityFactionType.Is && stance == current) { + return true; + } + if (available == EnumAvailabilityFactionType.IsNot && stance != current) { + return true; + } + + return false; + } + + public boolean dialogAvailable(int id, EnumAvailabilityDialog en, IPlayer player) { + if (player == null) + return false; + if (en == EnumAvailabilityDialog.Always) + return true; + PlayerData data = PlayerData.get(player); + if (data == null) + return false; + boolean hasRead = data.dialogData.dialogsRead.contains(id); + if (hasRead && en == EnumAvailabilityDialog.After) + return true; + else if (!hasRead && en == EnumAvailabilityDialog.Before) + return true; + return false; + } + + public boolean questAvailable(int id, EnumAvailabilityQuest en, IPlayer player) { + if (en == EnumAvailabilityQuest.Always) + return true; + else if (en == EnumAvailabilityQuest.After && PlayerQuestController.isQuestFinished(player, id)) + return true; + else if (en == EnumAvailabilityQuest.Before && !PlayerQuestController.isQuestFinished(player, id)) + return true; + else if (en == EnumAvailabilityQuest.Active && PlayerQuestController.isQuestActive(player, id)) + return true; + else if (en == EnumAvailabilityQuest.NotActive && !PlayerQuestController.isQuestActive(player, id)) + return true; + else if (en == EnumAvailabilityQuest.Acceptable || en == EnumAvailabilityQuest.NotAcceptable) { + Quest quest = QuestController.Instance.quests.get(id); + if (quest == null) + return true; + boolean result = PlayerQuestController.canQuestBeAccepted(quest, player); + return (result && en == EnumAvailabilityQuest.Acceptable) || (!result && en == EnumAvailabilityQuest.NotAcceptable); + } + return false; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public void setVersion(int version) { + this.version = version; + } + + + public int getDaytime() { + return this.daytime.ordinal(); + } + + public void setDaytime(int type) { + this.daytime = EnumDayTime.values()[ValueUtil.clampInt(type, 0, 2)]; + } + + public int getMinPlayerLevel() { + return this.minPlayerLevel; + } + + public void setMinPlayerLevel(int level) { + this.minPlayerLevel = level; + } + + public int getDialog(int i) { + if (i < 0 || i > 3) { + throw new CustomNPCsException(i + " isnt between 0 and 3", new Object[0]); + } else if (i == 0) { + return this.dialogId; + } else if (i == 1) { + return this.dialog2Id; + } else { + return i == 2 ? this.dialog3Id : this.dialog4Id; + } + } + + public void setDialog(int i, int id, int type) { + if (i < 0 || i > 3) { + throw new CustomNPCsException(i + " isnt between 0 and 3", new Object[0]); + } else { + EnumAvailabilityDialog e = EnumAvailabilityDialog.values()[ValueUtil.clampInt(type, 0, 2)]; + if (i == 0) { + this.dialogId = id; + this.dialogAvailable = e; + } else if (i == 1) { + this.dialog2Id = id; + this.dialog2Available = e; + } else if (i == 2) { + this.dialog3Id = id; + this.dialog3Available = e; + } else if (i == 3) { + this.dialog4Id = id; + this.dialog4Available = e; + } + + } + } + + public void removeDialog(int i) { + if (i < 0 || i > 3) { + throw new CustomNPCsException(i + " isnt between 0 and 3", new Object[0]); + } else { + if (i == 0) { + this.dialogId = -1; + this.dialogAvailable = EnumAvailabilityDialog.Always; + } else if (i == 1) { + this.dialog2Id = -1; + this.dialog2Available = EnumAvailabilityDialog.Always; + } else if (i == 2) { + this.dialog3Id = -1; + this.dialog3Available = EnumAvailabilityDialog.Always; + } else if (i == 3) { + this.dialog4Id = -1; + this.dialog4Available = EnumAvailabilityDialog.Always; + } + + } + } + + public int getQuest(int i) { + if (i < 0 || i > 3) { + throw new CustomNPCsException(i + " isnt between 0 and 3", new Object[0]); + } else if (i == 0) { + return this.questId; + } else if (i == 1) { + return this.quest2Id; + } else { + return i == 2 ? this.quest3Id : this.quest4Id; + } + } + + public void setQuest(int i, int id, int type) { + if (i < 0 || i > 3) { + throw new CustomNPCsException(i + " isnt between 0 and 3", new Object[0]); + } else { + EnumAvailabilityQuest e = EnumAvailabilityQuest.values()[ValueUtil.clampInt(type, 0, 5)]; + if (i == 0) { + this.questId = id; + this.questAvailable = e; + } else if (i == 1) { + this.quest2Id = id; + this.quest2Available = e; + } else if (i == 2) { + this.quest3Id = id; + this.quest3Available = e; + } else if (i == 3) { + this.quest4Id = id; + this.quest4Available = e; + } + + } + } + + public void removeQuest(int i) { + if (i < 0 || i > 3) { + throw new CustomNPCsException(i + " isnt between 0 and 3", new Object[0]); + } else { + if (i == 0) { + this.questId = -1; + this.questAvailable = EnumAvailabilityQuest.Always; + } else if (i == 1) { + this.quest2Id = -1; + this.quest2Available = EnumAvailabilityQuest.Always; + } else if (i == 2) { + this.quest3Id = -1; + this.quest3Available = EnumAvailabilityQuest.Always; + } else if (i == 3) { + this.quest4Id = -1; + this.quest4Available = EnumAvailabilityQuest.Always; + } + + } + } + + public void setFaction(int i, int id, int type, int stance) { + if (i < 0 || i > 1) { + throw new CustomNPCsException(i + " isnt between 0 and 1", new Object[0]); + } else { + EnumAvailabilityFactionType e = EnumAvailabilityFactionType.values()[ValueUtil.clampInt(type, 0, 2)]; + EnumAvailabilityFaction ee = EnumAvailabilityFaction.values()[ValueUtil.clampInt(stance, 0, 2)]; + if (i == 0) { + this.factionId = id; + this.factionAvailable = e; + this.factionStance = ee; + } else if (i == 1) { + this.faction2Id = id; + this.faction2Available = e; + this.faction2Stance = ee; + } + + } + } + + public void removeFaction(int i) { + if (i < 0 || i > 1) { + throw new CustomNPCsException(i + " isnt between 0 and 1", new Object[0]); + } else { + if (i == 0) { + this.factionId = -1; + this.factionAvailable = EnumAvailabilityFactionType.Always; + this.factionStance = EnumAvailabilityFaction.Friendly; + } else if (i == 1) { + this.faction2Id = -1; + this.faction2Available = EnumAvailabilityFactionType.Always; + this.faction2Stance = EnumAvailabilityFaction.Friendly; + } + + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Bank.java b/core/src/main/java/noppes/npcs/controllers/data/Bank.java new file mode 100644 index 000000000..ee0a4b7b8 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/Bank.java @@ -0,0 +1,72 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NpcMiscInventory; + +import java.util.HashMap; + + +public class Bank { + public int id = -1; + public String name = ""; + public HashMap slotTypes; + public int startSlots = 1; + public int maxSlots = 6; + public NpcMiscInventory currencyInventory; + public NpcMiscInventory upgradeInventory; + + public Bank() { + slotTypes = new HashMap(); + currencyInventory = new NpcMiscInventory(6); + upgradeInventory = new NpcMiscInventory(6); + for (int i = 0; i < 6; i++) + slotTypes.put(i, 0); + } + + public void writeEntityToNBT(INbt INbt) { + INbt.setInteger("BankID", id); + INbt.setTag("BankCurrency", currencyInventory.getToNBT()); + INbt.setTag("BankUpgrade", upgradeInventory.getToNBT()); + INbt.setString("Username", name); + INbt.setInteger("MaxSlots", maxSlots); + INbt.setInteger("StartSlots", startSlots); + INbt.setTag("BankTypes", NBTTags.nbtIntegerIntegerMap(slotTypes)); + } + + public void readEntityFromNBT(INbt INbt) { + id = INbt.getInteger("BankID"); + name = INbt.getString("Username"); + startSlots = INbt.getInteger("StartSlots"); + maxSlots = INbt.getInteger("MaxSlots"); + slotTypes = NBTTags.getIntegerIntegerMap(INbt.getTagList("BankTypes", 10)); + currencyInventory.setFromNBT(INbt.getCompoundTag("BankCurrency")); + upgradeInventory.setFromNBT(INbt.getCompoundTag("BankUpgrade")); + } + + public boolean isUpgraded(int slot) { + return slotTypes.get(slot) != null && slotTypes.get(slot) == 2; + } + + public boolean canBeUpgraded(int slot) { + if (upgradeInventory.getStackInSlot(slot) == null) + return false; + return slotTypes.get(slot) == null || slotTypes.get(slot) == 0; + } + + public int getMaxSlots() { + for (int i = 0; i < maxSlots; i++) { + if (currencyInventory.getStackInSlot(i) == null && i > startSlots - 1) + return i; + } + return maxSlots; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/BankData.java b/core/src/main/java/noppes/npcs/controllers/data/BankData.java new file mode 100644 index 000000000..3246580de --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/BankData.java @@ -0,0 +1,139 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.BankController; +import java.util.HashMap; + +public class BankData { + public HashMap itemSlots; + public HashMap upgradedSlots; + public int unlockedSlots = 0; + public int bankId = -1; + + public BankData() { + itemSlots = new HashMap(); + upgradedSlots = new HashMap(); + + for (int i = 0; i < 6; i++) { + itemSlots.put(i, new NpcMiscInventory(54)); + upgradedSlots.put(i, false); + } + } + + public void readNBT(INbt INbt) { + bankId = INbt.getInteger("DataBankId"); + unlockedSlots = INbt.getInteger("UnlockedSlots"); + itemSlots = getItemSlots(INbt.getTagList("BankInv", 10)); + upgradedSlots = NBTTags.getBooleanList(INbt.getTagList("UpdatedSlots", 10)); + } + + private HashMap getItemSlots(INbtList tagList) { + HashMap list = new HashMap(); + for (int i = 0; i < tagList.tagCount(); i++) { + INbt INbt = tagList.getCompoundTagAt(i); + int slot = INbt.getInteger("Slot"); + NpcMiscInventory inv = new NpcMiscInventory(54); + inv.setFromNBT(INbt.getCompoundTag("BankItems")); + list.put(slot, inv); + } + return list; + } + + public void writeNBT(INbt INbt) { + INbt.setInteger("DataBankId", bankId); + INbt.setInteger("UnlockedSlots", unlockedSlots); + INbt.setTag("UpdatedSlots", NBTTags.nbtBooleanList(upgradedSlots)); + INbt.setTag("BankInv", nbtItemSlots(itemSlots)); + } + + private INbtList nbtItemSlots(HashMap items) { + INbtList list = new INbtList(); + for (int slot : items.keySet()) { + INbt INbt = new INbt(); + INbt.setInteger("Slot", slot); + + INbt.setTag("BankItems", items.get(slot).getToNBT()); + list.appendTag(INbt); + } + return list; + } + + public boolean isUpgraded(Bank bank, int slot) { + if (bank.isUpgraded(slot)) + return true; + return bank.canBeUpgraded(slot) && upgradedSlots.get(slot); + } + + public void openBankGui(final IPlayer player, EntityNPCInterface npc, int bankId, int slot) { + final Bank bank = BankController.getInstance().getBank(bankId); + + if (bank.getMaxSlots() <= slot) + return; + + if (bank.startSlots > unlockedSlots) + unlockedSlots = bank.startSlots; + + IItemStack currency = null; + if (unlockedSlots <= slot) { + currency = bank.currencyInventory.getStackInSlot(slot); + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerBankUnlock, npc, slot, bank.id, 0); + } else if (isUpgraded(bank, slot)) { + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerBankLarge, npc, slot, bank.id, 0); + } else if (bank.canBeUpgraded(slot)) { + currency = bank.upgradeInventory.getStackInSlot(slot); + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerBankUprade, npc, slot, bank.id, 0); + } else { + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerBankSmall, npc, slot, bank.id, 0); + } + final IItemStack item = currency; + INbt compound = new INbt(); + compound.setInteger("MaxSlots", bank.getMaxSlots()); + compound.setInteger("UnlockedSlots", unlockedSlots); + if (item != null) { + compound.setTag("Currency", NoppesUtilServer.writeItem(item, new INbt())); + ContainerNPCBankInterface container = getContainer(player); + if (container != null) + container.setCurrency(item); + } + + GuiDataPacket.sendGuiData((IPlayer) player, compound); + } + + private ContainerNPCBankInterface getContainer(IPlayer player) { + Container con = player.openContainer; + if (con == null || !(con instanceof ContainerNPCBankInterface)) + return null; + + return (ContainerNPCBankInterface) con; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java b/core/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java new file mode 100644 index 000000000..a50da86bd --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java @@ -0,0 +1,105 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.data.IAnimation; +/** + * A read-only animation loaded from mod assets. + * Built-in animations are identified by name only, never by ID. + * They cannot be saved, deleted, or modified. + */ +public class BuiltInAnimation extends Animation { + + public BuiltInAnimation() { + super(); + this.id = -1; // No valid ID for built-in animations + } + + public BuiltInAnimation(String name) { + super(); + this.name = name; + this.id = -1; + } + + /** + * Built-in animations cannot be saved. + * + * @return this animation unchanged + */ + @Override + public IAnimation save() { + // Do nothing - built-in animations are read-only + return this; + } + + /** + * Built-in animations have no valid ID. + * + * @return always -1 + */ + @Override + public int getID() { + return -1; + } + + /** + * Built-in animations cannot have their ID changed. + */ + @Override + public void setID(int newID) { + // Ignore - built-in animations don't use IDs + } + + /** + * Check if this is a built-in animation. + * + * @return always true for BuiltInAnimation + */ + public boolean isBuiltIn() { + return true; + } + + /** + * Read animation data from NBT. + * Preserves the name set in constructor (from filename) rather than reading from NBT. + */ + @Override + public void readFromNBT(INbt compound) { + // Preserve the name set in constructor - don't read from NBT + // (filename is the source of truth for built-in animations) + String preservedName = this.name; + + speed = compound.getFloat("Speed"); + smooth = compound.getByte("Smooth"); + loop = compound.getInteger("Loop"); + + frames.clear(); + net.minecraft.nbt.INbtList list = compound.getTagList("Frames", 10); + for (int i = 0; i < list.tagCount(); i++) { + INbt item = list.getCompoundTagAt(i); + Frame frame = new Frame(); + frame.parentSpeed = this.speed; + frame.parentSmooth = this.smooth; + frame.readFromNBT(new NBTWrapper(item)); + frames.add(frame); + } + + this.whileStanding = compound.getBoolean("WhileStanding"); + this.whileMoving = compound.getBoolean("WhileWalking"); + this.whileAttacking = compound.getBoolean("WhileAttacking"); + + this.currentFrame = compound.getInteger("CurrentFrame"); + this.currentFrameTime = compound.getInteger("CurrentFrameTime"); + + // Ensure name and ID are preserved + this.name = preservedName; + this.id = -1; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/ChainedAbilityScript.java b/core/src/main/java/noppes/npcs/controllers/data/ChainedAbilityScript.java new file mode 100644 index 000000000..bc84bb4ff --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/ChainedAbilityScript.java @@ -0,0 +1,65 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.ScriptContext; +/** + * Script handler for chained ability scripts. + * Manages a single script container for chained ability hooks. + */ +public class ChainedAbilityScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** + * The chained ability ID (UUID) for packet communication. Empty if not bound. + */ + private String chainId = ""; + + /** + * Create an unbound ChainedAbilityScript (for server-side use). + */ + public ChainedAbilityScript() { + } + + /** + * Create a ChainedAbilityScript bound to a specific chained ability (for GUI use). + * + * @param chainId The UUID of the chained ability + */ + public ChainedAbilityScript(String chainId) { + this.chainId = chainId != null ? chainId : ""; + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.CHAINED_ABILITY); + } + + @Override + public ScriptContext getContext() { + return ScriptContext.CHAINED_ABILITY; + } + + @Override + public String noticeString() { + return !chainId.isEmpty() ? "ChainedAbility[" + chainId + "]" : "ChainedAbility"; + } + + @Override + public void requestData() { + if (!chainId.isEmpty()) + ChainedAbilityScriptPacket.Get(chainId); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + if (!chainId.isEmpty()) + ChainedAbilityScriptPacket.Save(chainId, index, totalCount, nbt); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/CustomEffect.java b/core/src/main/java/noppes/npcs/controllers/data/CustomEffect.java new file mode 100644 index 000000000..6ad8c7390 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/CustomEffect.java @@ -0,0 +1,359 @@ +package noppes.npcs.controllers.data; + + +import java.util.function.Consumer; +import java.util.HashSet; +import java.util.UUID; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.ICustomEffect; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.TagController; +import noppes.npcs.EventHooks; +import noppes.npcs.NoppesUtilServer; + +public class CustomEffect implements ICustomEffect { + public int id = -1; + + public String name = ""; + public boolean lossOnDeath = true; + public int length = 30; + + // Must be a multiple of 10 + public int everyXTick = 20; + + public String icon = ""; + public int iconX = 0, iconY = 0; + + public String menuName = "§aNEW EFFECT"; + public int width = 16, height = 16; + + // Animation + public boolean animated = false; + public int frameCount = 1; + public int frametime = 2; + + public int index = 0; + + public HashSet tagUUIDs = new HashSet<>(); + + public CustomEffect() { + } + + public CustomEffect(int id) { + this.id = id; + } + + public CustomEffect(int id, String name) { + this(id); + this.name = name; + } + + @Override + public int getID() { + return this.id; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setMenuName(String name) { + if (name != null && !name.isEmpty()) + this.menuName = name.replaceAll("&", "§"); + } + + @Override + public String getMenuName() { + return menuName; + } + + @Override + public void setName(String name) { + if (name != null && !name.isEmpty()) + this.name = name; + } + + @Override + public String getIcon() { + return icon; + } + + @Override + public void setIcon(String icon) { + this.icon = icon; + } + + @Override + public int getEveryXTick() { + return everyXTick; + } + + @Override + public void setEveryXTick(int everyXTick) { + if (everyXTick < 10) + everyXTick = 10; + int remainder = everyXTick % 10; + if (remainder >= 5) + everyXTick += 10 - remainder; + else + everyXTick -= remainder; + this.everyXTick = everyXTick; + } + + @Override + public int getIconX() { + return iconX; + } + + @Override + public void setIconX(int iconX) { + this.iconX = iconX; + } + + @Override + public int getIconY() { + return iconY; + } + + @Override + public void setIconY(int iconY) { + this.iconY = iconY; + } + + @Override + public int getWidth() { + return this.width; + } + + @Override + public int getHeight() { + return this.height; + } + + @Override + public void setWidth(int width) { + this.width = width; + } + + @Override + public void setHeight(int height) { + this.height = height; + } + + @Override + public boolean isAnimated() { + return animated; + } + + @Override + public void setAnimated(boolean animated) { + this.animated = animated; + } + + @Override + public int getFrameCount() { + return frameCount; + } + + @Override + public void setFrameCount(int frameCount) { + this.frameCount = Math.max(1, frameCount); + } + + @Override + public int getFrameTime() { + return frametime; + } + + @Override + public void setFrameTime(int frametime) { + this.frametime = Math.max(1, frametime); + } + + @Override + public boolean isLossOnDeath() { + return lossOnDeath; + } + + @Override + public void setLossOnDeath(boolean lossOnDeath) { + this.lossOnDeath = lossOnDeath; + } + + @Override + public ICustomEffect save() { + return CustomEffectController.getInstance().saveEffect(this); + } + + @Override + public void setID(int id) { + this.id = id; + } + + @Override + public int getIndex() { + return this.index; + } + + public void onAdded(IPlayer player, PlayerEffect playerEffect) { + IPlayer iPlayer = NoppesUtilServer.getIPlayer(player); + if (playerEffect.index == 0) { + PlayerEvent.EffectEvent.Added event = new PlayerEvent.EffectEvent.Added(iPlayer, playerEffect); + EffectScript script = getScriptHandler(); + if (script == null) { + return; + } + + script.callScript(EnumScriptType.ON_EFFECT_ADD, event); + } + + EventHooks.onEffectAdded(iPlayer, playerEffect); + } + + public void onTick(IPlayer player, PlayerEffect playerEffect) { + IPlayer iPlayer = NoppesUtilServer.getIPlayer(player); + if (playerEffect.index == 0) { + PlayerEvent.EffectEvent.Ticked event = new PlayerEvent.EffectEvent.Ticked(iPlayer, playerEffect); + EffectScript script = getScriptHandler(); + if (script == null) { + return; + } + + script.callScript(EnumScriptType.ON_EFFECT_TICK, event); + } + + EventHooks.onEffectTick(iPlayer, playerEffect); + } + + public void onRemoved(IPlayer player, PlayerEffect playerEffect, PlayerEvent.EffectEvent.ExpirationType type) { + IPlayer iPlayer = NoppesUtilServer.getIPlayer(player); + + if (playerEffect.index == 0) { + PlayerEvent.EffectEvent.Removed event = new PlayerEvent.EffectEvent.Removed(iPlayer, playerEffect, type); + EffectScript script = getScriptHandler(); + if (script == null) { + return; + } + + script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); + } + + EventHooks.onEffectRemove(iPlayer, playerEffect, type); + } + + public INbt writeToNBT(boolean saveScripts) { + INbt compound = new INbt(); + compound.setInteger("ID", id); + compound.setString("name", name); + compound.setString("menuName", menuName); + compound.setInteger("length", length); + compound.setInteger("everyXTick", everyXTick); + compound.setInteger("iconX", iconX); + compound.setInteger("iconY", iconY); + compound.setInteger("iconWidth", width); + compound.setInteger("iconHeight", height); + compound.setString("icon", icon); + compound.setBoolean("lossOnDeath", lossOnDeath); + compound.setBoolean("iconAnimated", animated); + compound.setInteger("iconFrameCount", frameCount); + compound.setInteger("iconFrameTime", frametime); + + TagController.writeTagUUIDs(compound, "TagUUIDs", tagUUIDs); + + if (saveScripts) { + INbt scriptData = new INbt(); + EffectScript handler = getScriptHandler(); + if (handler != null) + handler.writeToNBT(scriptData); + compound.setTag("ScriptData", scriptData); + } + + return compound; + } + + public void readFromNBT(INbt compound) { + if (compound.hasKey("ID")) + id = compound.getInteger("ID"); + else + id = CustomEffectController.Instance.getUnusedId(); + name = compound.getString("name"); + + if (compound.hasKey("menuName", NbtConstants.TAG_STRING)) + menuName = compound.getString("menuName"); + else + menuName = name; + + length = compound.getInteger("length"); + everyXTick = compound.getInteger("everyXTick"); + iconX = compound.getInteger("iconX"); + iconY = compound.getInteger("iconY"); + + if (compound.hasKey("iconWidth", NbtConstants.TAG_INT)) + width = compound.getInteger("iconWidth"); + else + width = 16; + + if (compound.hasKey("iconHeight", NbtConstants.TAG_INT)) + height = compound.getInteger("iconHeight"); + else + height = 16; + + icon = compound.getString("icon"); + lossOnDeath = compound.getBoolean("lossOnDeath"); + + if (compound.hasKey("iconAnimated")) + animated = compound.getBoolean("iconAnimated"); + if (compound.hasKey("iconFrameCount")) + frameCount = Math.max(1, compound.getInteger("iconFrameCount")); + if (compound.hasKey("iconFrameTime")) + frametime = Math.max(1, compound.getInteger("iconFrameTime")); + + tagUUIDs = TagController.readTagUUIDs(compound, "TagUUIDs"); + + if (compound.hasKey("ScriptData", NbtConstants.TAG_COMPOUND)) { + EffectScript handler = new EffectScript(); + handler.readFromNBT(compound.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } + + } + + public CustomEffect cloneEffect() { + CustomEffect newEffect = new CustomEffect(); + newEffect.readFromNBT(this.writeToNBT(true)); + newEffect.id = -1; + return newEffect; + } + + public EffectScript getScriptHandler() { + return CustomEffectController.getInstance().customEffectScriptHandlers.get(this.id); + } + + public void setScriptHandler(EffectScript handler) { + CustomEffectController.getInstance().customEffectScriptHandlers.put(this.id, handler); + } + + public EffectScript getOrCreateScriptHandler() { + EffectScript data = getScriptHandler(); + if (data == null) + setScriptHandler(data = new EffectScript()); + return data; + } + + public void runEffect(IPlayer player, PlayerEffect playerEffect) { + if (player.ticksExisted % everyXTick == 0) { + onTick(player, playerEffect); + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/DataScript.java b/core/src/main/java/noppes/npcs/controllers/data/DataScript.java new file mode 100644 index 000000000..e221f8eb9 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -0,0 +1,387 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.EventHooks; +import noppes.npcs.LogWriter; +import noppes.npcs.NBTTags; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.ICustomNpc; +import noppes.npcs.config.ConfigDebug; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; +import noppes.npcs.api.handler.IHookDefinition; + +import javax.script.ScriptEngine; +import java.util.*; + +public class DataScript implements IScriptHandlerPacket { + public List eventScripts = new ArrayList<>(); + + private HashMap scripts = new HashMap<>(); + private final static EntityType entities = new EntityType(); + private final static JobType jobs = new JobType(); + private final static RoleType roles = new RoleType(); + + public String scriptLanguage = "ECMAScript"; + private EntityNPCInterface npc; + public boolean enabled = false; + private long lastGlobalsVersion = -1; + + public ICustomNpc dummyNpc; + public IWorld dummyWorld; + public boolean clientNeedsUpdate = false; + public boolean aiNeedsUpdate = false; + public boolean hasInited = false; + + // Editor/runtime globals descriptor used for a single source of truth. + private static final class GlobalDefinition { + private final String typeName; + private final Object value; + + private GlobalDefinition(String typeName, Object value) { + this.typeName = typeName; + this.value = value; + } + } + + public DataScript(EntityNPCInterface npc) { + for (int i = 0; i < 15; i++) { + this.setNPCScript(i, new ScriptContainer(this)); + } + + this.npc = npc; + if (npc.wrappedNPC == null) { + npc.wrappedNPC = new ScriptNpc(this.npc); + } + dummyNpc = npc.wrappedNPC; + + if (npc.worldObj instanceof IWorld) + dummyWorld = NpcAPI.Instance().getIWorld((IWorld) npc.worldObj);//new ScriptWorld((IWorld) npc.worldObj); + } + + public void readFromNBT(INbt compound) { + scripts = readScript(compound.getTagList("ScriptsContainers", 10)); + this.scriptLanguage = compound.getString("ScriptLanguage"); + if (scriptLanguage == null || scriptLanguage.isEmpty()) { + if (!ScriptController.Instance.languages.isEmpty()) { + this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; + } else { + this.scriptLanguage = "ECMAScript"; + } + } + + enabled = compound.getBoolean("ScriptEnabled"); + } + + public void readEventsFromNBT(INbt compound) { + eventScripts = NBTTags.GetScript(compound, this); + } + + public INbt writeToNBT(INbt compound) { + compound.setTag("ScriptsContainers", writeScript(scripts)); + compound.setString("ScriptLanguage", scriptLanguage); + compound.setBoolean("ScriptEnabled", enabled); + return compound; + } + + public INbt writeEventsToNBT(INbt compound) { + compound.setInteger("TotalScripts", this.eventScripts.size()); + for (int i = 0; i < this.eventScripts.size(); i++) { + compound.setTag("Tab" + i, this.eventScripts.get(i).writeToNBT(new INbt())); + } + return compound; + } + + private HashMap readScript(INbtList list) { + HashMap scripts = new HashMap<>(); + for (int i = 0; i < 15; i++) { + scripts.put(EnumScriptType.values()[i], new ScriptContainer(this)); + } + + for (int i = 0; i < list.tagCount(); i++) { + INbt compoundd = list.getCompoundTagAt(i); + ScriptContainer script = new ScriptContainer(this); + script.readFromNBT(compoundd); + if (script.hasCode() || npc.isRemote()) { + scripts.put(EnumScriptType.values()[compoundd.getInteger("Type")], script); + } + } + return scripts; + } + + private INbtList writeScript(HashMap scripts) { + INbtList list = new INbtList(); + for (Map.Entry entry : scripts.entrySet()) { + INbt tagCompound = new INbt(); + ScriptContainer container = entry.getValue(); + if (container.hasCode()) { + tagCompound.setInteger("Type", entry.getKey().ordinal()); + container.writeToNBT(tagCompound); + list.appendTag(tagCompound); + } + } + return list; + } + + public boolean callScript(EnumScriptType type, Event event, Object... obs) { + if (aiNeedsUpdate) { + npc.updateAI = true; + aiNeedsUpdate = false; + } + if (clientNeedsUpdate) { + npc.updateClient = true; + clientNeedsUpdate = false; + } + if (!isEnabled()) + return false; + + if (!hasInited && !npc.isRemote() && type != EnumScriptType.INIT) { + hasInited = true; + for (IScriptUnit scriptUnit : this.eventScripts) { + scriptUnit.setErrored(false); + } + EventHooks.onNPCInit(this.npc); + } + + for (IScriptUnit script : this.eventScripts) { + script.run(type, event); + } + + ScriptContainer script = scripts.get(type); + if (script == null || script.errored || !script.hasCode()) + return false; + + script.setEngine(this.getLanguage()); + if (script.engine == null) + return false; + + for (int i = 0; i + 1 < obs.length; i += 2) { + Object ob = obs[i + 1]; + if (ob instanceof IEntity) + ob = NpcAPI.Instance().getIEntity((IEntity) ob); + script.engine.put(obs[i].toString(), ob); + } + if (ConfigDebug.ScriptLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { + if (obs.length > 1 && obs[1] == null) { + LogWriter.postScriptLog(npc.field_110179_h, type, String.format("[%s] NPC %s (%s, %s, %s)", ((String) type.function).toUpperCase(), npc.display.name, (int) npc.posX, (int) npc.posY, (int) npc.posZ)); + } else { + LogWriter.postScriptLog(npc.field_110179_h, type, String.format("[%s] NPC %s (%s, %s, %s) | Objects: %s", ((String) type.function).toUpperCase(), npc.display.name, (int) npc.posX, (int) npc.posY, (int) npc.posZ, Arrays.toString(obs))); + } + } + return callScript(script, type.function, event); + } + + private boolean callScript(ScriptContainer script, String hookName, Event event) { + ScriptEngine engine = script.engine; + applyGlobalsToEngine(engine, hookName, event); + script.run(engine); + + if (clientNeedsUpdate) { + npc.updateClient = true; + clientNeedsUpdate = false; + } + if (aiNeedsUpdate) { + npc.updateAI = true; + aiNeedsUpdate = false; + } + return event.isCanceled(); + } + + public boolean isEnabled() { + return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && CustomNpcs.proxy.isScriptingEnabled(); + } + + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; + } + + @Override + public void requestData() { + EventScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + EventScriptPacket.Save(index, totalCount, nbt); + } + + public Map getOldConsoleText() { + Map map = new TreeMap<>(); + + for (Map.Entry entry : scripts.entrySet()) { + for (Map.Entry consoleEntry : entry.getValue().console.entrySet()) { + map.put(consoleEntry.getKey(), " tab " + entry.getKey().ordinal() + ":\n" + consoleEntry.getValue()); + } + } + + return map; + } + + + @Override + public void callScript(EnumScriptType type, Event event) { + callScript(type, event, "$$IGNORED$$", null); + } + + @Override + public void callScript(String hookName, Event event) { + try { + EnumScriptType enumScriptType = EnumScriptType.valueOfIgnoreCase(hookName); + this.callScript(enumScriptType, event); + } catch (IllegalArgumentException ignored) { + } + } + + public Map getEditorGlobals(String hookName) { + Map definitions = getGlobalDefinitions(hookName, null); + Map globals = new LinkedHashMap<>(); + for (Map.Entry entry : definitions.entrySet()) { + String typeName = entry.getValue().typeName; + if (typeName != null && !typeName.isEmpty()) { + globals.put(entry.getKey(), typeName); + } + } + return globals; + } + + // Apply runtime globals plus engine-only bindings. + private void applyGlobalsToEngine(ScriptEngine engine, String hookName, Event event) { + Map definitions = getGlobalDefinitions(hookName, event); + for (Map.Entry entry : definitions.entrySet()) { + engine.put(entry.getKey(), entry.getValue().value); + } + + long currentGlobalsVersion = NpcAPI.engineObjectsVersion; + if (lastGlobalsVersion != currentGlobalsVersion) { + engine.put("API", NpcAPI.Instance()); + for (Map.Entry engineObjects : NpcAPI.engineObjects.entrySet()) { + engine.put(engineObjects.getKey(), engineObjects.getValue()); + } + lastGlobalsVersion = currentGlobalsVersion; + } + } + + // Build shared globals (editor types + runtime values). + private Map getGlobalDefinitions(String hookName, Event event) { + Map globals = new LinkedHashMap<>(); + globals.put("npc", new GlobalDefinition("ICustomNpc", dummyNpc)); + globals.put("IWorld", new GlobalDefinition("IWorld", dummyWorld)); + globals.put("event", new GlobalDefinition(resolveEditorEventTypeName(hookName), event)); + globals.put("EntityType", new GlobalDefinition("noppes.npcs.scripted.constants.EntityType", entities)); + globals.put("RoleType", new GlobalDefinition("noppes.npcs.scripted.constants.RoleType", roles)); + globals.put("JobType", new GlobalDefinition("noppes.npcs.scripted.constants.JobType", jobs)); + + return globals; + } + + // Resolve editor-only event type from hook name with a single fallback. + private String resolveEditorEventTypeName(String hookName) { + final String fallback = "INpcEvent"; + if (hookName == null || hookName.isEmpty()) + return fallback; + + IHookDefinition definition = ScriptHookController.Instance.getHookDefinition(getContext().hookContext, hookName); + if (definition == null) + return fallback; + + String typeName = definition.getUsableTypeName(); + if (typeName == null || typeName.isEmpty()) + return fallback; + + return typeName; + } + + public boolean isClient() { + return this.npc.isRemote(); + } + + public boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(boolean bo) { + this.enabled = bo; + } + + public String getLanguage() { + return this.scriptLanguage; + } + + public void setLanguage(String lang) { + this.scriptLanguage = lang; + } + + public void setScripts(List list) { + this.eventScripts = list; + } + + public List getScripts() { + return this.eventScripts; + } + + public String noticeString() { + //BlockPos pos = this.npc.func_180425_c(); + //return MoreObjects.toStringHelper(this.npc).add("x", pos.func_177958_n()).add("y", pos.func_177956_o()).add("z", pos.func_177952_p()).toString(); + return ""; + } + + public void setWorld(IWorld IWorld) { + if (IWorld instanceof IWorld) + dummyWorld = new ScriptWorld((IWorld) IWorld); + } + + public ScriptContainer getNPCScript(EnumScriptType scriptType) { + return this.scripts.get(scriptType); + } + + public ScriptContainer getNPCScript(int ordinal) { + return this.getNPCScript(EnumScriptType.values()[ordinal]); + } + + public void setNPCScript(EnumScriptType scriptType, ScriptContainer container) { + this.scripts.put(scriptType, container); + } + + public void setNPCScript(int ordinal, ScriptContainer container) { + this.setNPCScript(EnumScriptType.values()[ordinal], container); + } + + public Collection getNPCScripts() { + return this.scripts.values(); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.NPC); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/DataSkinOverlays.java b/core/src/main/java/noppes/npcs/controllers/data/DataSkinOverlays.java new file mode 100644 index 000000000..d76b9f1d5 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/DataSkinOverlays.java @@ -0,0 +1,9 @@ +package noppes.npcs.controllers.data; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Data skin overlays. + */ +public class DataSkinOverlays { + public DataSkinOverlays(Object parent) {} +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/DataTimers.java b/core/src/main/java/noppes/npcs/controllers/data/DataTimers.java new file mode 100644 index 000000000..6968874a2 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/DataTimers.java @@ -0,0 +1,9 @@ +package noppes.npcs.controllers.data; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Data timers for player tracking. + */ +public class DataTimers { + public DataTimers(Object parent) {} +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/DataTransform.java b/core/src/main/java/noppes/npcs/controllers/data/DataTransform.java new file mode 100644 index 000000000..19d57fb67 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/DataTransform.java @@ -0,0 +1,263 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.constants.EnumAnimation; +import noppes.npcs.constants.EnumJobType; +import noppes.npcs.constants.EnumRoleType; +import java.util.Set; + +public class DataTransform { + public INbt display; + public INbt ai; + public INbt advanced; + public INbt inv; + public INbt stats; + public INbt role; + public INbt job; + + public boolean hasDisplay, hasAi, hasAdvanced, hasInv, hasStats, hasRole, hasJob, isActive; + + private EntityNPCInterface npc; + + public boolean editingModus = false; + + public DataTransform(EntityNPCInterface npc) { + this.npc = npc; + } + + + public INbt writeToNBT(INbt compound) { + compound.setBoolean("TransformIsActive", isActive); + writeOptions(compound); + if (hasDisplay) + compound.setTag("TransformDisplay", display); + if (hasAi) + compound.setTag("TransformAI", ai); + if (hasAdvanced) + compound.setTag("TransformAdvanced", advanced); + if (hasInv) + compound.setTag("TransformInv", inv); + if (hasStats) + compound.setTag("TransformStats", stats); + if (hasRole) + compound.setTag("TransformRole", role); + if (hasJob) + compound.setTag("TransformJob", job); + + return compound; + } + + public INbt writeOptions(INbt compound) { + compound.setBoolean("TransformHasDisplay", hasDisplay); + compound.setBoolean("TransformHasAI", hasAi); + compound.setBoolean("TransformHasAdvanced", hasAdvanced); + compound.setBoolean("TransformHasInv", hasInv); + compound.setBoolean("TransformHasStats", hasStats); + compound.setBoolean("TransformHasRole", hasRole); + compound.setBoolean("TransformHasJob", hasJob); + compound.setBoolean("TransformEditingModus", editingModus); + return compound; + } + + + public void readToNBT(INbt compound) { + isActive = compound.getBoolean("TransformIsActive"); + readOptions(compound); + display = hasDisplay ? compound.getCompoundTag("TransformDisplay") : getDisplay(); + ai = hasAi ? compound.getCompoundTag("TransformAI") : npc.ais.writeToNBT(new INbt()); + advanced = hasAdvanced ? compound.getCompoundTag("TransformAdvanced") : getAdvanced(); + inv = hasInv ? compound.getCompoundTag("TransformInv") : npc.inventory.writeEntityToNBT(new INbt()); + stats = hasStats ? compound.getCompoundTag("TransformStats") : npc.stats.writeToNBT(new INbt()); + job = hasJob ? compound.getCompoundTag("TransformJob") : getJob(); + role = hasRole ? compound.getCompoundTag("TransformRole") : getRole(); + } + + public INbt getJob() { + INbt compound = new INbt(); + + compound.setInteger("NpcJob", npc.advanced.job.ordinal()); + if (npc.advanced.job != EnumJobType.None && npc.jobInterface != null) { + npc.jobInterface.writeToNBT(compound); + } + + return compound; + } + + public INbt getRole() { + INbt compound = new INbt(); + + compound.setInteger("Role", npc.advanced.role.ordinal()); + if (npc.advanced.role != EnumRoleType.None && npc.roleInterface != null) { + npc.roleInterface.writeToNBT(compound); + } + + return compound; + } + + public INbt getDisplay() { + INbt compound = npc.display.writeToNBT(new INbt()); + if (npc instanceof EntityCustomNpc) { + compound.setTag("ModelData", ((EntityCustomNpc) npc).modelData.writeToNBT()); + } + + return compound; + } + + public INbt getAdvanced() { + EnumJobType jopType = npc.advanced.job; + EnumRoleType roleType = npc.advanced.role; + + npc.advanced.job = EnumJobType.None; + npc.advanced.role = EnumRoleType.None; + + INbt compound = npc.advanced.writeToNBT(new INbt()); + compound.removeTag("Role"); + compound.removeTag("NpcJob"); + + npc.advanced.job = jopType; + npc.advanced.role = roleType; + + return compound; + } + + + public void readOptions(INbt compound) { + boolean hadDisplay = hasDisplay; + boolean hadAI = hasAi; + boolean hadAdvanced = hasAdvanced; + boolean hadInv = hasInv; + boolean hadStats = hasStats; + boolean hadRole = hasRole; + boolean hadJob = hasJob; + + hasDisplay = compound.getBoolean("TransformHasDisplay"); + hasAi = compound.getBoolean("TransformHasAI"); + hasAdvanced = compound.getBoolean("TransformHasAdvanced"); + hasInv = compound.getBoolean("TransformHasInv"); + hasStats = compound.getBoolean("TransformHasStats"); + hasRole = compound.getBoolean("TransformHasRole"); + hasJob = compound.getBoolean("TransformHasJob"); + editingModus = compound.getBoolean("TransformEditingModus"); + + if (hasDisplay && !hadDisplay) { + display = getDisplay(); + } + if (hasAi && !hadAI) + ai = npc.ais.writeToNBT(new INbt()); + if (hasStats && !hadStats) + stats = npc.stats.writeToNBT(new INbt()); + if (hasInv && !hadInv) + inv = npc.inventory.writeEntityToNBT(new INbt()); + if (hasAdvanced && !hadAdvanced) + advanced = getAdvanced(); + if (hasJob && !hadJob) + job = getJob(); + if (hasRole && !hadRole) + role = getRole(); + } + + public boolean isValid() { + return hasAdvanced || hasAi || hasDisplay || hasInv || hasStats || hasJob || hasRole; + } + + + public INbt processAdvanced(INbt compoundAdv, + INbt compoundRole, INbt compoundJob) { + + if (hasAdvanced) + compoundAdv = advanced; + if (hasRole) + compoundRole = role; + if (hasJob) + compoundJob = job; + + Set names = compoundRole.func_150296_c(); + for (String name : names) + compoundAdv.setTag(name, compoundRole.getTag(name)); + + names = compoundJob.func_150296_c(); + for (String name : names) + compoundAdv.setTag(name, compoundJob.getTag(name)); + + return compoundAdv; + } + + public void transform(boolean isActive) { + if (this.isActive == isActive) + return; + if (hasDisplay) { + INbt compound = getDisplay(); + npc.display.readToNBT(NBTTags.NBTMerge(compound, display)); + if (npc instanceof EntityCustomNpc) { + ((EntityCustomNpc) npc).modelData.readFromNBT(NBTTags.NBTMerge(compound.getCompoundTag("ModelData"), display.getCompoundTag("ModelData"))); + } + display = compound; + } + if (hasStats) { + INbt compound = npc.stats.writeToNBT(new INbt()); + npc.stats.readToNBT(NBTTags.NBTMerge(compound, stats)); + stats = compound; + } + if (hasAdvanced || hasJob || hasRole) { + INbt compoundAdv = getAdvanced(); + INbt compoundRole = getRole(); + INbt compoundJob = getJob(); + + INbt compound = processAdvanced(compoundAdv, compoundRole, compoundJob); + npc.advanced.readToNBT(compound); + if (npc.advanced.role != EnumRoleType.None && npc.roleInterface != null) + npc.roleInterface.readFromNBT(NBTTags.NBTMerge(compoundRole, compound)); + if (npc.advanced.job != EnumJobType.None && npc.jobInterface != null) + npc.jobInterface.readFromNBT(NBTTags.NBTMerge(compoundJob, compound)); + + if (hasAdvanced) + advanced = compoundAdv; + if (hasRole) + role = compoundRole; + if (hasJob) + job = compoundJob; + } + if (hasAi) { + INbt compound = npc.ais.writeToNBT(new INbt()); + npc.ais.readToNBT(NBTTags.NBTMerge(compound, ai)); + ai = compound; + npc.setCurrentAnimation(EnumAnimation.NONE); + } + if (hasInv) { + INbt compound = npc.inventory.writeEntityToNBT(new INbt()); + npc.inventory.readEntityFromNBT(NBTTags.NBTMerge(compound, inv)); + inv = compound; + } + npc.updateHitbox(); + npc.updateAI = true; + this.isActive = isActive; + npc.updateClient = true; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Dialog.java b/core/src/main/java/noppes/npcs/controllers/data/Dialog.java new file mode 100644 index 000000000..218f11a58 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/Dialog.java @@ -0,0 +1,612 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.ICompatibilty; +import noppes.npcs.VersionCompatibility; +import noppes.npcs.api.handler.data.IAvailability; +import noppes.npcs.api.handler.data.IDialog; +import noppes.npcs.api.handler.data.IDialogCategory; +import noppes.npcs.api.handler.data.IDialogImage; +import noppes.npcs.api.handler.data.IDialogOption; +import noppes.npcs.api.handler.data.IQuest; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.constants.EnumOptionType; +import noppes.npcs.controllers.DialogController; +import noppes.npcs.controllers.QuestController; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class Dialog implements ICompatibilty, IDialog { + public int version = VersionCompatibility.ModRev; + public int id = -1; + public String title = ""; + public String text = ""; + public int quest = -1; + public DialogCategory category; + public DialogColorData colorData = new DialogColorData(); + public HashMap options = new HashMap(); + public Availability availability = new Availability(); + public FactionOptions factionOptions = new FactionOptions(); + public String sound; + public String command = ""; + public PlayerMail mail = new PlayerMail(); + + public int color = 0xe0e0e0; + public int titleColor = 0xe0e0e0; + + public boolean hideNPC = false; + public boolean showWheel = false; + public boolean disableEsc = false; + public boolean darkenScreen = true; + public boolean showOptionLine = true; + + //0 - Bottom, 1 - Top, 2 - Center [Unimplemented] + public byte alignment = 0; + + public boolean renderGradual = false; + public boolean showPreviousBlocks = true; + + public String textSound = "minecraft:random.wood_click"; + public float textPitch = 1.0F; + + public int textWidth = 300; + public int textHeight = 400; + + public int titlePos; + public int textOffsetX, textOffsetY; + public int titleOffsetX, titleOffsetY; + + public int optionSpaceX, optionSpaceY; + public int optionOffsetX, optionOffsetY; + + public float npcScale = 1.0F; + public int npcOffsetX, npcOffsetY; + + public HashMap dialogImages = new HashMap<>(); + + public boolean hasDialogs(IPlayer player) { + for (DialogOption option : options.values()) + if (option != null && option.optionType == EnumOptionType.DialogOption && option.hasDialog() && option.isAvailable(player)) + return true; + return false; + } + + public void readNBT(INbt compound) { + id = compound.getInteger("DialogId"); + readNBTPartial(compound); + } + + public void readNBTPartial(INbt compound) { + version = compound.getInteger("ModRev"); + VersionCompatibility.CheckAvailabilityCompatibility(this, compound); + + title = compound.getString("DialogTitle"); + text = compound.getString("DialogText"); + quest = compound.getInteger("DialogQuest"); + sound = compound.getString("DialogSound"); + command = compound.getString("DialogCommand"); + mail.readNBT(compound.getCompoundTag("DialogMail")); + + hideNPC = compound.getBoolean("DialogHideNPC"); + if (compound.hasKey("DialogShowWheel")) + showWheel = compound.getBoolean("DialogShowWheel"); + else + showWheel = true; + disableEsc = compound.getBoolean("DialogDisableEsc"); + + if (compound.hasKey("DialogDarkScreen")) { + darkenScreen = compound.getBoolean("DialogDarkScreen"); + } else { + darkenScreen = true; + } + + alignment = compound.getByte("DialogAlignment"); + + + INbtList options = compound.getTagList("Options", 10); + HashMap newoptions = new HashMap(); + for (int iii = 0; iii < options.tagCount(); iii++) { + INbt option = options.getCompoundTagAt(iii); + int opslot = option.getInteger("OptionSlot"); + DialogOption dia = new DialogOption(); + dia.readNBT(option.getCompoundTag("Option")); + newoptions.put(opslot, dia); + } + this.options = newoptions; + + INbtList images = compound.getTagList("Images", 10); + HashMap newImages = new HashMap<>(); + for (int i = 0; i < images.tagCount(); i++) { + INbt imageCompound = images.getCompoundTagAt(i); + int id = imageCompound.getInteger("ID"); + DialogImage image = new DialogImage(id); + image.readNBT(new NBTWrapper(imageCompound)); + newImages.put(id, image); + } + this.dialogImages = newImages; + + color = compound.getInteger("Color"); + titleColor = compound.getInteger("TitleColor"); + renderGradual = compound.getBoolean("RenderGradual"); + showPreviousBlocks = compound.getBoolean("PreviousBlocks"); + showOptionLine = compound.getBoolean("ShowOptionLine"); + textSound = compound.getString("TextSound"); + textPitch = compound.getFloat("TextPitch"); + textWidth = compound.getInteger("TextWidth"); + textHeight = compound.getInteger("TextHeight"); + textOffsetX = compound.getInteger("TextOffsetX"); + textOffsetY = compound.getInteger("TextOffsetY"); + titlePos = compound.getInteger("TitlePos"); + titleOffsetX = compound.getInteger("TitleOffsetX"); + titleOffsetY = compound.getInteger("TitleOffsetY"); + optionOffsetX = compound.getInteger("OptionOffsetX"); + optionOffsetY = compound.getInteger("OptionOffsetY"); + optionSpaceX = compound.getInteger("OptionSpaceX"); + optionSpaceY = compound.getInteger("OptionSpaceY"); + npcScale = compound.getFloat("NPCScale"); + npcOffsetX = compound.getInteger("NPCOffsetX"); + npcOffsetY = compound.getInteger("NPCOffsetY"); + + if (!compound.hasKey("PreviousBlocks")) + showPreviousBlocks = true; + if (!compound.hasKey("TextSound")) + textSound = "minecraft:random.wood_click"; + if (!compound.hasKey("TextPitch")) + textPitch = 1.0F; + if (!compound.hasKey("TextWidth")) + textWidth = 300; + if (!compound.hasKey("TextHeight")) + textHeight = 400; + if (!compound.hasKey("ShowOptionLine")) + showOptionLine = true; + if (!compound.hasKey("NPCScale")) + npcScale = 1.0F; + if (!compound.hasKey("Color")) + color = 0xe0e0e0; + if (!compound.hasKey("TitleColor")) + titleColor = 0xe0e0e0; + + availability.readFromNBT(compound); + factionOptions.readFromNBT(compound); + colorData.readFromNBT(new NBTWrapper(compound)); + } + + + public INbt writeToNBT(INbt compound) { + compound.setInteger("DialogId", id); + return writeToNBTPartial(compound); + } + + public INbt writeToNBTPartial(INbt compound) { + compound.setString("DialogTitle", title); + compound.setString("DialogText", text); + compound.setInteger("DialogQuest", quest); + compound.setString("DialogCommand", command); + compound.setTag("DialogMail", mail.writeNBT()); + compound.setBoolean("DialogHideNPC", hideNPC); + compound.setBoolean("DialogShowWheel", showWheel); + compound.setBoolean("DialogDisableEsc", disableEsc); + + compound.setBoolean("DialogDarkScreen", darkenScreen); + compound.setByte("DialogAlignment", alignment); + + if (sound != null && !sound.isEmpty()) + compound.setString("DialogSound", sound); + + INbtList options = new INbtList(); + for (int opslot : this.options.keySet()) { + INbt listcompound = new INbt(); + listcompound.setInteger("OptionSlot", opslot); + listcompound.setTag("Option", this.options.get(opslot).writeNBT()); + options.appendTag(listcompound); + } + compound.setTag("Options", options); + + availability.writeToNBT(compound); + factionOptions.writeToNBT(compound); + colorData.writeToNBT(new NBTWrapper(compound)); + compound.setInteger("ModRev", version); + + compound.setInteger("Color", color); + compound.setInteger("TitleColor", titleColor); + compound.setBoolean("RenderGradual", renderGradual); + compound.setBoolean("PreviousBlocks", showPreviousBlocks); + compound.setBoolean("ShowOptionLine", showOptionLine); + compound.setString("TextSound", textSound); + compound.setFloat("TextPitch", textPitch); + compound.setInteger("TextWidth", textWidth); + compound.setInteger("TextHeight", textHeight); + compound.setInteger("TextOffsetX", textOffsetX); + compound.setInteger("TextOffsetY", textOffsetY); + compound.setInteger("TitlePos", titlePos); + compound.setInteger("TitleOffsetX", titleOffsetX); + compound.setInteger("TitleOffsetY", titleOffsetY); + compound.setInteger("OptionOffsetX", optionOffsetX); + compound.setInteger("OptionOffsetY", optionOffsetY); + compound.setInteger("OptionSpaceX", optionSpaceX); + compound.setInteger("OptionSpaceY", optionSpaceY); + compound.setFloat("NPCScale", npcScale); + compound.setInteger("NPCOffsetX", npcOffsetX); + compound.setInteger("NPCOffsetY", npcOffsetY); + + INbtList images = new INbtList(); + for (IDialogImage dialogImage : dialogImages.values()) { + INbt imageCompound = ((NBTWrapper) ((DialogImage) dialogImage).writeToNBT(new NBTWrapper(new INbt()))).getMCTag(); + images.appendTag(imageCompound); + } + compound.setTag("Images", images); + + return compound; + } + + public boolean hasQuest() { + return getQuest() != null; + } + + public boolean hasOtherOptions() { + for (DialogOption option : options.values()) + if (option != null && option.optionType != EnumOptionType.Disabled) + return true; + return false; + } + + public Dialog copy(IPlayer player) { + Dialog dialog = new Dialog(); + dialog.id = id; + dialog.text = text; + dialog.title = title; + dialog.category = category; + dialog.quest = quest; + dialog.sound = sound; + dialog.mail = mail; + dialog.command = command; + dialog.color = color; + dialog.titleColor = titleColor; + dialog.hideNPC = hideNPC; + dialog.showWheel = showWheel; + dialog.disableEsc = disableEsc; + dialog.darkenScreen = darkenScreen; + dialog.renderGradual = renderGradual; + dialog.showPreviousBlocks = showPreviousBlocks; + dialog.showOptionLine = showOptionLine; + dialog.alignment = alignment; + dialog.textSound = textSound; + dialog.textPitch = textPitch; + dialog.textWidth = textWidth; + dialog.textHeight = textHeight; + dialog.textOffsetX = textOffsetX; + dialog.textOffsetY = textOffsetY; + dialog.titlePos = titlePos; + dialog.titleOffsetX = titleOffsetX; + dialog.titleOffsetY = titleOffsetY; + dialog.optionOffsetX = optionOffsetX; + dialog.optionOffsetY = optionOffsetY; + dialog.optionSpaceX = optionSpaceX; + dialog.optionSpaceY = optionSpaceY; + dialog.npcScale = npcScale; + dialog.npcOffsetX = npcOffsetX; + dialog.npcOffsetY = npcOffsetY; + dialog.dialogImages = dialogImages; + dialog.colorData = colorData; + + for (int slot : options.keySet()) { + DialogOption option = options.get(slot); + if (option.optionType == EnumOptionType.DialogOption && (!option.hasDialog() || !option.isAvailable(player))) + continue; + dialog.options.put(slot, option); + } + return dialog; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public void setVersion(int version) { + this.version = version; + } + + public int getId() { + return this.id; + } + + public String getName() { + return this.title; + } + + public List getOptions() { + return new ArrayList(this.options.values()); + } + + public IDialogOption getOption(int slot) { + IDialogOption option = (IDialogOption) this.options.get(slot); + if (option == null) { + throw new CustomNPCsException("There is no DialogOption for slot: " + slot, new Object[0]); + } else { + return option; + } + } + + public IAvailability getAvailability() { + return this.availability; + } + + public IDialogCategory getCategory() { + return this.category; + } + + public void save() { + DialogController.Instance.saveDialog(this.category.id, this); + } + + public void setName(String name) { + this.title = name; + } + + public String getText() { + return this.text; + } + + public void setText(String text) { + this.text = text; + } + + public void setQuest(IQuest quest) { + if (quest == null) { + this.quest = -1; + } else { + if (quest.getId() < 0) { + throw new CustomNPCsException("Quest id is lower than 0", new Object[0]); + } + + this.quest = quest.getId(); + } + + } + + public Quest getQuest() { + return QuestController.Instance == null ? null : (Quest) QuestController.Instance.quests.get(this.quest); + } + + public String getCommand() { + return this.command; + } + + public void setCommand(String command) { + this.command = command; + } + + public void setDarkenScreen(boolean darkenScreen) { + this.darkenScreen = darkenScreen; + } + + public boolean getDarkenScreen() { + return this.darkenScreen; + } + + public void setDisableEsc(boolean disableEsc) { + this.disableEsc = disableEsc; + } + + public boolean getDisableEsc() { + return this.disableEsc; + } + + public void setShowWheel(boolean showWheel) { + this.showWheel = showWheel; + } + + public boolean getShowWheel() { + return this.showWheel; + } + + public void setHideNPC(boolean hideNPC) { + this.hideNPC = hideNPC; + } + + public boolean getHideNPC() { + return this.hideNPC; + } + + public void setSound(String sound) { + this.sound = sound; + } + + public String getSound() { + return this.sound; + } + + public void setColor(int color) { + this.color = color; + } + + public int getColor() { + return this.color; + } + + public void setTitleColor(int titleColor) { + this.titleColor = titleColor; + } + + public int getTitleColor() { + return titleColor; + } + + public void renderGradual(boolean gradual) { + this.renderGradual = gradual; + } + + public boolean renderGradual() { + return renderGradual; + } + + public void showPreviousBlocks(boolean show) { + this.showPreviousBlocks = show; + } + + public boolean showPreviousBlocks() { + return showPreviousBlocks; + } + + public void showOptionLine(boolean show) { + this.showOptionLine = show; + } + + public boolean showOptionLine() { + return showOptionLine; + } + + public void setTextSound(String textSound) { + this.textSound = textSound; + } + + public String getTextSound() { + return textSound; + } + + public void setTextPitch(float textPitch) { + this.textPitch = textPitch; + } + + public float getTextPitch() { + return textPitch; + } + + public void setTitlePos(int pos) { + this.titlePos = pos; + } + + public int getTitlePos() { + return this.titlePos; + } + + public void setNPCScale(float scale) { + this.npcScale = scale; + } + + public float getNpcScale() { + return npcScale; + } + + public void setNpcOffset(int offsetX, int offsetY) { + this.npcOffsetX = offsetX; + this.npcOffsetY = offsetY; + } + + public int getNpcOffsetX() { + return npcOffsetX; + } + + public int getNpcOffsetY() { + return npcOffsetY; + } + + public void textWidthHeight(int textWidth, int textHeight) { + this.textWidth = textWidth; + this.textHeight = textHeight; + } + + public int getTextWidth() { + return textWidth; + } + + public int setTextHeight() { + return textHeight; + } + + public void setTextOffset(int offsetX, int offsetY) { + this.textOffsetX = offsetX; + this.textOffsetY = offsetY; + } + + public int getTextOffsetX() { + return textOffsetX; + } + + public int getTextOffsetY() { + return textOffsetY; + } + + public void setTitleOffset(int offsetX, int offsetY) { + this.titleOffsetX = offsetX; + this.titleOffsetY = offsetY; + } + + public int getTitleOffsetX() { + return titleOffsetX; + } + + public int getTitleOffsetY() { + return titleOffsetY; + } + + public void setOptionOffset(int offsetX, int offsetY) { + this.optionOffsetX = offsetX; + this.optionOffsetY = offsetY; + } + + public int getOptionOffsetX() { + return optionOffsetX; + } + + public int getOptionOffsetY() { + return optionOffsetY; + } + + public void setOptionSpacing(int spaceX, int spaceY) { + this.optionSpaceX = spaceX; + this.optionSpaceY = spaceY; + } + + public int getOptionSpaceX() { + return optionSpaceX; + } + + public int getOptionSpaceY() { + return optionSpaceY; + } + + public void addImage(int id, IDialogImage image) { + if (dialogImages.size() >= ConfigMain.DialogImageLimit) { + return; + } + + ((DialogImage) image).id = id; + dialogImages.put(id, image); + } + + public IDialogImage getImage(int id) { + return dialogImages.get(id); + } + + public IDialogImage createImage() { + return new DialogImage(); + } + + public IDialogImage[] getImages() { + return (new ArrayList<>(dialogImages.values())).toArray(new IDialogImage[0]); + } + + public boolean hasImage(int id) { + return dialogImages.containsKey(id); + } + + public void removeImage(int id) { + dialogImages.remove(id); + } + + public void clearImages() { + dialogImages.clear(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/DialogCategory.java b/core/src/main/java/noppes/npcs/controllers/data/DialogCategory.java new file mode 100644 index 000000000..051affbc1 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/DialogCategory.java @@ -0,0 +1,86 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.data.IDialog; +import noppes.npcs.api.handler.data.IDialogCategory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class DialogCategory implements IDialogCategory { + + public int id = -1; + public String title = ""; + public HashMap dialogs; + + public DialogCategory() { + dialogs = new HashMap(); + } + + public void readNBT(INbt compound) { + id = compound.getInteger("Slot"); + title = compound.getString("Title"); + + INbtList dialogsList = compound.getTagList("Dialogs", 10); + if (dialogsList != null) { + for (int ii = 0; ii < dialogsList.tagCount(); ii++) { + Dialog dialog = new Dialog(); + dialog.category = this; + INbt comp = dialogsList.getCompoundTagAt(ii); + dialog.readNBT(comp); + dialog.id = comp.getInteger("DialogId"); + dialogs.put(dialog.id, dialog); + } + } + } + + public INbt writeNBT(INbt dialogCat) { + dialogCat.setInteger("Slot", id); + dialogCat.setString("Title", title); + INbtList dialogs = new INbtList(); + for (Dialog dialog : this.dialogs.values()) { + dialogs.appendTag(dialog.writeToNBT(new INbt())); + } + dialogCat.setTag("Dialogs", dialogs); + return dialogCat; + } + + public INbt writeSmallNBT(INbt dialogCat) { + dialogCat.setInteger("Slot", id); + dialogCat.setString("Title", title); + return dialogCat; + } + + public void readSmallNBT(INbt compound) { + id = compound.getInteger("Slot"); + title = compound.getString("Title"); + } + + public List dialogs() { + return new ArrayList(this.dialogs.values()); + } + + public String getName() { + return this.title; + } + + public IDialog create() { + Dialog dialog = new Dialog(); + dialog.category = this; + return dialog; + } + + @Override + public int getId() { + return this.id; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/DialogOption.java b/core/src/main/java/noppes/npcs/controllers/data/DialogOption.java new file mode 100644 index 000000000..19797b74a --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/DialogOption.java @@ -0,0 +1,69 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.EnumOptionType; +import noppes.npcs.controllers.DialogController; + +public class DialogOption { + public int dialogId = -1; + public String title = "Talk"; + public EnumOptionType optionType = EnumOptionType.DialogOption; + public int optionColor = 0xe0e0e0; + public String command = ""; + + public void readNBT(INbt compound) { + if (compound == null) + return; + title = compound.getString("Title"); + dialogId = compound.getInteger("Dialog"); + optionColor = compound.getInteger("DialogColor"); + optionType = EnumOptionType.values()[compound.getInteger("OptionType")]; + command = compound.getString("DialogCommand"); + if (optionColor == 0) { + optionColor = 0xe0e0e0; + } + } + + public INbt writeNBT() { + INbt compound = new INbt(); + compound.setString("Title", title); + compound.setInteger("OptionType", optionType.ordinal()); + compound.setInteger("Dialog", dialogId); + compound.setInteger("DialogColor", optionColor); + compound.setString("DialogCommand", command); + return compound; + } + + + public boolean hasDialog() { + if (dialogId <= 0) + return false; + if (!DialogController.Instance.hasDialog(dialogId)) { + dialogId = -1; + return false; + } + return true; + } + + public Dialog getDialog() { + if (!hasDialog()) + return null; + return DialogController.Instance.dialogs.get(dialogId); + } + + public boolean isAvailable(IPlayer player) { + Dialog dialog = getDialog(); + if (dialog == null) + return false; + + return dialog.availability.isAvailable(player); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/core/src/main/java/noppes/npcs/controllers/data/EffectScript.java new file mode 100644 index 000000000..5fabaeb02 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -0,0 +1,67 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.ScriptContext; +/** + * Script handler for CustomEffect scripts. + * Manages a single script container for effect hooks (onEffectAdd, onEffectTick, onEffectRemove). + */ +public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** + * The effect ID for packet communication. -1 if not bound to an effect. + */ + private int effectId = -1; + + /** + * Create an unbound EffectScript (for server-side use). + */ + public EffectScript() { + } + + /** + * Create an EffectScript bound to a specific effect (for GUI use). + * + * @param effectId The ID of the CustomEffect + */ + public EffectScript(int effectId) { + this.effectId = effectId; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.EFFECT; + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.EFFECT); + } + + @Override + public String noticeString() { + return effectId >= 0 ? "CustomEffect[" + effectId + "]" : "CustomEffect"; + } + + + @Override + public void requestData() { + if (effectId >= 0) + EffectScriptPacket.Get(effectId); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + if (effectId >= 0) + EffectScriptPacket.Save(effectId, index, totalCount, nbt); + } + +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Event.java b/core/src/main/java/noppes/npcs/controllers/data/Event.java new file mode 100644 index 000000000..af7ed3269 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/Event.java @@ -0,0 +1,10 @@ +package noppes.npcs.controllers.data; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class Event { + public Event() {} +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/controllers/data/Faction.java b/core/src/main/java/noppes/npcs/controllers/data/Faction.java index 8ee81f524..a09f2b216 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/Faction.java +++ b/core/src/main/java/noppes/npcs/controllers/data/Faction.java @@ -1,16 +1,22 @@ package noppes.npcs.controllers.data; -import noppes.npcs.NBTTags; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; -import noppes.npcs.api.entity.ICustomNpc; -import noppes.npcs.api.entity.IPlayer; -import noppes.npcs.api.handler.IPlayerFactionData; -import noppes.npcs.api.handler.data.IFaction; -import noppes.npcs.controllers.FactionController; import java.util.ArrayList; import java.util.HashSet; +import noppes.npcs.api.entity.ICustomNpc; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IFaction; +import noppes.npcs.api.handler.IPlayerFactionData; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.FactionController; +import noppes.npcs.NBTTags; public class Faction { public String name = ""; @@ -78,15 +84,15 @@ public class Faction { } // TODO: mc1710 version implements IFaction and adds: - // OLD: public boolean isFriendlyToPlayer(EntityPlayer player) — uses PlayerData.get(player).factionData - // OLD: public boolean isAggressiveToPlayer(EntityPlayer player) — uses PlayerData.get(player).factionData - // OLD: public boolean isNeutralToPlayer(EntityPlayer player) — uses PlayerData.get(player).factionData - // OLD: public boolean isAggressiveToNpc(EntityNPCInterface entity) — uses entity.faction.id + // OLD: public boolean isFriendlyToPlayer(IPlayer player) — uses PlayerData.get(player).factionData + // OLD: public boolean isAggressiveToPlayer(IPlayer player) — uses PlayerData.get(player).factionData + // OLD: public boolean isNeutralToPlayer(IPlayer player) — uses PlayerData.get(player).factionData + // OLD: public boolean isAggressiveToNpc(IEntityNPCInterface IEntity) — uses IEntity.faction.id // OLD: public int playerStatus(IPlayer player) — uses IPlayerFactionData // OLD: public boolean isAggressiveToNpc(ICustomNpc npc) — uses npc.getFaction().getId() - // OLD: public boolean isFriendlyToPlayer(IPlayer player) — delegates to EntityPlayer overload - // OLD: public boolean isNeutralToPlayer(IPlayer player) — delegates to EntityPlayer overload - // OLD: public boolean isAggressiveToPlayer(IPlayer player) — delegates to EntityPlayer overload + // OLD: public boolean isFriendlyToPlayer(IPlayer player) — delegates to IPlayer overload + // OLD: public boolean isNeutralToPlayer(IPlayer player) — delegates to IPlayer overload + // OLD: public boolean isAggressiveToPlayer(IPlayer player) — delegates to IPlayer overload public int getId() { return this.id; } diff --git a/core/src/main/java/noppes/npcs/controllers/data/FactionOptions.java b/core/src/main/java/noppes/npcs/controllers/data/FactionOptions.java new file mode 100644 index 000000000..25495de11 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/FactionOptions.java @@ -0,0 +1,76 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.FactionController; + +public class FactionOptions { + + public boolean decreaseFactionPoints = false; + public boolean decreaseFaction2Points = false; + + public int factionId = -1; + public int faction2Id = -1; + + public int factionPoints = 100; + public int faction2Points = 100; + + public void readFromNBT(INbt compound) { + factionId = compound.getInteger("OptionFactions1"); + faction2Id = compound.getInteger("OptionFactions2"); + + decreaseFactionPoints = compound.getBoolean("DecreaseFaction1Points"); + decreaseFaction2Points = compound.getBoolean("DecreaseFaction2Points"); + + factionPoints = compound.getInteger("OptionFaction1Points"); + faction2Points = compound.getInteger("OptionFaction2Points"); + } + + public INbt writeToNBT(INbt par1NBTTagCompound) { + par1NBTTagCompound.setInteger("OptionFactions1", factionId); + par1NBTTagCompound.setInteger("OptionFactions2", faction2Id); + + par1NBTTagCompound.setInteger("OptionFaction1Points", factionPoints); + par1NBTTagCompound.setInteger("OptionFaction2Points", faction2Points); + + par1NBTTagCompound.setBoolean("DecreaseFaction1Points", decreaseFactionPoints); + par1NBTTagCompound.setBoolean("DecreaseFaction2Points", decreaseFaction2Points); + return par1NBTTagCompound; + } + + public boolean hasFaction(int id) { + return factionId == id || faction2Id == id; + } + + public void addPoints(IPlayer player) { + if (factionId < 0 && faction2Id < 0) + return; + + PlayerFactionData data = PlayerData.get(player).factionData; + if (factionId >= 0 && factionPoints > 0) + addPoints(player, data, factionId, decreaseFactionPoints, factionPoints); + if (faction2Id >= 0 && faction2Points > 0) + addPoints(player, data, faction2Id, decreaseFaction2Points, faction2Points); + } + + private void addPoints(IPlayer player, PlayerFactionData data, int factionId, boolean decrease, int points) { + Faction faction = FactionController.getInstance().get(factionId); + if (faction == null) + return; + + if (!faction.hideFaction) { + String message = decrease ? "faction.decreasepoints" : "faction.increasepoints"; + player.addChatMessage(new ITextComponent(message, faction.name, points)); + } + + data.increasePoints(factionId, decrease ? -points : points, player); + + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/core/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java new file mode 100644 index 000000000..1917bc5d4 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -0,0 +1,189 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import com.google.common.reflect.ClassPath; +import noppes.npcs.CustomNpcs; +import noppes.npcs.EventHooks; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +public class ForgeDataScript extends MultiScriptHandler { + private static final Object HOOK_LOCK = new Object(); + private static List cachedHooks; + + private long lastForgeUpdate = -1L; + private final HashSet globalUnknownEvents = new HashSet<>(); + + public ForgeDataScript() { + } + + public boolean isEnabled() { + return this.enabled && CustomNpcs.proxy.isGlobalForgeScripts() && ScriptController.HasStart && this.scripts.size() > 0; + } + + @Override + protected boolean canRunScripts() { + return isEnabled(); + } + + @Override + public ScriptContext getContext() { + return ScriptContext.FORGE; + } + + @Override + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastForgeUpdate > lastForgeUpdate; + } + + @Override + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; + globalUnknownEvents.clear(); + + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; + } + } + + @Override + public void callScript(String type, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + if (!type.equals("init")) { + EventHooks.onForgeInit(this); + } + } + + // Skip events that no script tab handles + if (globalUnknownEvents.contains(type)) { + return; + } + + boolean anyHandled = false; + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + boolean wasUnknown = script.isUnknownFunction(type); + script.run(type, event); + if (!wasUnknown || !script.isUnknownFunction(type)) { + anyHandled = true; + } + } + + // If no script tab handled this event type, cache it globally + if (!anyHandled) { + globalUnknownEvents.add(type); + } + } + + @Override + public List getHooks() { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + synchronized (HOOK_LOCK) { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + // Start with built-in + addon hooks from ScriptHookController + List hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(ScriptContext.FORGE.hookContext)); + + // Dynamically discover Forge event classes + ArrayList list = new ArrayList<>(); + try { + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); + list.removeAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event.terraingen")); + } catch (IOException e) { + e.printStackTrace(); + } + + for (ClassPath.ClassInfo classInfo : list) { + Class infoClass = classInfo.load(); + List> classes = new ArrayList<>(Arrays.asList(infoClass.getDeclaredClasses())); + if (classes.isEmpty()) { + classes.add(infoClass); + } + + for (Class eventClass : classes) { + if (isValidForgeEvent(eventClass)) { + String eventName = eventClass.getName(); + int lastDot = eventName.lastIndexOf("."); + eventName = StringUtils.uncapitalize(eventName.substring(lastDot + 1).replace("$", "")); + + if (!hookList.contains(eventName)) { + hookList.add(eventName); + } + } + } + } + + cachedHooks = hookList; + } + + return new ArrayList<>(cachedHooks); + } + + /** + * Check if a class is a valid Forge event that should be exposed as a hook. + */ + private boolean isValidForgeEvent(Class eventClass) { + return Event.class.isAssignableFrom(eventClass) + && Modifier.isPublic(eventClass.getModifiers()) + && !Modifier.isAbstract(eventClass.getModifiers()) + && !EntityEvent.EntityConstructing.class.isAssignableFrom(eventClass) + && !WorldEvent.PotentialSpawns.class.isAssignableFrom(eventClass) + && !TickEvent.RenderTickEvent.class.isAssignableFrom(eventClass) + && !TickEvent.ClientTickEvent.class.isAssignableFrom(eventClass) + && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(eventClass) + && !ItemTooltipEvent.class.isAssignableFrom(eventClass) + && !ChunkEvent.class.isAssignableFrom(eventClass) + && !ChunkWatchEvent.class.isAssignableFrom(eventClass) + && !ChunkDataEvent.class.isAssignableFrom(eventClass); + } + + @Override + public void requestData() { + ForgeScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + ForgeScriptPacket.Save(index, totalCount, nbt); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.FORGE); + } + + @Override + public boolean isClient() { + return false; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/core/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java new file mode 100644 index 000000000..7210299ba --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -0,0 +1,108 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.api.entity.ICustomNpc; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.util.ScriptToStringHelper; + +public class GlobalNPCDataScript extends MultiScriptHandler { + private EntityNPCInterface npc; + private ICustomNpc npcAPI; + private long lastNpcUpdate = -1L; + + public GlobalNPCDataScript(EntityNPCInterface npc) { + if (npc != null) { + this.npc = npc; + } + } + + @Override + protected boolean canRunScripts() { + return isEnabled(); + } + + public boolean isEnabled() { + return CustomNpcs.proxy.isGlobalNPCScripts() && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; + } + + @Override + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastGlobalNpcUpdate > lastNpcUpdate; + } + + @Override + protected void reInitScripts() { + super.reInitScripts(); + lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.NPC); + } + + @Override + public void requestData() { + GlobalNPCScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + GlobalNPCScriptPacket.Save(index, totalCount, nbt); + } + + @Override + public boolean isClient() { + return this.npc != null && this.npc.isClientWorld(); + } + + @Override + public String noticeString() { + if (this.npc == null) { + return "Global script"; + } else { + BlockPos pos = new BlockPos(this.npc); + return ScriptToStringHelper.toStringHelper(this.npc).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); + } + } + + public ICustomNpc getNpc() { + if (this.npcAPI == null) { + this.npcAPI = (ICustomNpc) NpcAPI.Instance().getIEntity(this.npc); + } + return this.npcAPI; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/HitboxData.java b/core/src/main/java/noppes/npcs/controllers/data/HitboxData.java index 72c6224cb..496b11890 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/HitboxData.java +++ b/core/src/main/java/noppes/npcs/controllers/data/HitboxData.java @@ -1,7 +1,16 @@ package noppes.npcs.controllers.data; + import noppes.npcs.api.entity.data.IHitboxData; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; import noppes.npcs.core.CoreConfig; public class HitboxData implements IHitboxData { @@ -10,28 +19,28 @@ public class HitboxData implements IHitboxData { private float heightScale = 1f; private boolean hitboxEnabled = false; - public INbt writeToNBT(INbt nbttagcompound) { - nbttagcompound.setBoolean("HitboxEnabled", hitboxEnabled); + public INbt writeToNBT(INbt INbt) { + INbt.setBoolean("HitboxEnabled", hitboxEnabled); if (hitboxEnabled) { if (widthScale > CoreConfig.HitBoxScaleMax) widthScale = CoreConfig.HitBoxScaleMax; - nbttagcompound.setFloat("HitboxWidthScale", widthScale); + INbt.setFloat("HitboxWidthScale", widthScale); if (heightScale > CoreConfig.HitBoxScaleMax) heightScale = CoreConfig.HitBoxScaleMax; - nbttagcompound.setFloat("HitboxHeightScale", heightScale); + INbt.setFloat("HitboxHeightScale", heightScale); } - return nbttagcompound; + return INbt; } - public void readFromNBT(INbt nbttagcompound) { - hitboxEnabled = nbttagcompound.getBoolean("HitboxEnabled"); + public void readFromNBT(INbt INbt) { + hitboxEnabled = INbt.getBoolean("HitboxEnabled"); if (hitboxEnabled) { - widthScale = nbttagcompound.getFloat("HitboxWidthScale"); + widthScale = INbt.getFloat("HitboxWidthScale"); if (widthScale > CoreConfig.HitBoxScaleMax) widthScale = CoreConfig.HitBoxScaleMax; - heightScale = nbttagcompound.getFloat("HitboxHeightScale"); + heightScale = INbt.getFloat("HitboxHeightScale"); if (heightScale > CoreConfig.HitBoxScaleMax) heightScale = CoreConfig.HitBoxScaleMax; } diff --git a/core/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java b/core/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java new file mode 100644 index 000000000..82119a6d4 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java @@ -0,0 +1,7 @@ +package noppes.npcs.controllers.data; + +public interface INpcScriptHandler extends IScriptHandler { + // This interface is now empty - callScript methods moved to IScriptHandler + // Kept for backward compatibility + +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java b/core/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java new file mode 100644 index 000000000..f39b87112 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java @@ -0,0 +1,8 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.IBlock; + +public interface IScriptBlockHandler extends IScriptHandler { + + IBlock getBlock(); +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/core/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java new file mode 100644 index 000000000..9e7b43f99 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -0,0 +1,210 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public interface IScriptHandler { + default void callScript(EnumScriptType type, Event event) { + if (type != null) + callScript(type.function, event); + + } + + void callScript(String hookName, Event event); + + /** + * Call a function by name and return its result. + *

+ * Default implementation calls the function on each script unit (in order) and returns + * the first non-null value. + */ + default Object callFunction(String hookName, Object... args) { + if (hookName == null || hookName.isEmpty()) + return null; + + if (!getEnabled()) + return null; + + List scripts = getScripts(); + if (scripts == null || scripts.isEmpty()) + return null; + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + + Object result = script.callFunction(hookName, args); + if (result != null) + return result; + } + + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } + + default boolean isClient() { + return FMLCommonHandler.instance().getEffectiveSide().isClient(); + } + + boolean getEnabled(); + + void setEnabled(boolean enabled); + + String getLanguage(); + + void setLanguage(String language); + + void setScripts(List list); + + List getScripts(); + + default String noticeString() { + return ""; + } + + default Map getConsoleText() { + Map map = new TreeMap<>(); + int tab = 0; + + for (IScriptUnit script : getScripts()) { + ++tab; + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); + } + } + return map; + } + + default void clearConsole() { + for (IScriptUnit script : getScripts()) + script.clearConsole(); + + } + + /** + * Create a new Janino (Java) script unit for this handler. + * Handlers that don't support Janino scripts should return null. + * + * @return A new JaninoScript instance, or null if not supported + */ + default IScriptUnit createJaninoScriptUnit() { + return null; // Default: Janino not supported + } + + /** + * Check if this handler supports Janino (Java) scripts. + * + * @return true if createJaninoScriptUnit() can create valid Janino scripts + */ + default boolean supportsJanino() { + return createJaninoScriptUnit() != null; + } + + /** + * Get the script context for autocomplete and type resolution. + * + * @return The script context (default: GLOBAL) + */ + default ScriptContext getContext() { + return ScriptContext.GLOBAL; + } + + /** + * Get the hook context identifier for this handler. + * Used by ScriptHookController to look up available hooks. + *

+ * By default, derives from getContext().hookContext. + * Override if a different hook context is needed. + * + * @return The context string (e.g., "effect", "npc"), or empty if none + */ + default String getHookContext() { + ScriptContext ctx = getContext(); + return ctx != null ? ctx.hookContext : ""; + } + + /** + * Get the list of available hooks for this handler. + * Uses getHookContext() to look up hooks from ScriptHookController. + * + * @return List of hook names, or empty list if no context + */ + default List getHooks() { + String context = getHookContext(); + if (context == null || context.isEmpty()) + return Collections.emptyList(); + + return ScriptHookController.Instance.getAllHooks(context); + } + + /** + * Check if this handler manages only a single script container. + * Single-container handlers use simplified UI (no numbered tabs). + * + * @return true if this handler uses a single container + */ + default boolean isSingleContainer() { + return false; + } + + /** + * Get the single script unit for single-container handlers. + * Convenience method that returns the first script or null. + * + * @return The single script unit, or null if none exists + */ + default IScriptUnit getSingleScript() { + List scripts = getScripts(); + return (scripts != null && !scripts.isEmpty()) ? scripts.get(0) : null; + } + + /** + * Add a script unit to this handler. + * For single-container handlers, replaces the current unit. + */ + default void addScriptUnit(IScriptUnit unit) { + if (unit != null) + getScripts().add(unit); + } + + /** + * Replace a script unit at the given index. + * For single-container handlers, replaces the current unit. + */ + default void replaceScriptUnit(int index, IScriptUnit unit) { + List scripts = getScripts(); + if (index < 0 || index >= scripts.size()) + scripts.add(unit); + else + scripts.set(index, unit); + } + + /** + * Remove a script unit at the given index. + * For single-container handlers, clears the unit. + */ + default void removeScriptUnit(int index) { + List scripts = getScripts(); + if (index >= 0 && index < scripts.size()) + scripts.remove(index); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java b/core/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java new file mode 100644 index 000000000..377053ed4 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java @@ -0,0 +1,100 @@ +package noppes.npcs.controllers.data; + + +import java.util.List; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.NBTTags; + +public interface IScriptHandlerPacket extends IScriptHandler { + + /** + * Client-side: request initial script data from the server (typically a {@code ...Packet.Get(...)}). + */ + void requestData(); + + /** + * Client-side: send a script container packet to the server. + * + * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) + * @param totalCount Total number of scripts for this handler + * @param nbt The script container NBT (tab content or metadata) + */ + void sendSavePacket(int index, int totalCount, INbt nbt); + + enum GuiDataKind { + LOAD_COMPLETE, + METADATA, + TAB + } + + class GuiDataResult { + public final GuiDataKind kind; + public final int tabIndex; + + public GuiDataResult(GuiDataKind kind, int tabIndex) { + this.kind = kind; + this.tabIndex = tabIndex; + } + } + + /** + * Apply one incoming gui-data payload to this handler. + *

+ * Notes: + *

    + *
  • {@code Languages} / {@code ScriptConsole} are not consumed here; GuiScriptInterface owns those UI concerns.
  • + *
  • Tab payloads are converted to an {@link IScriptUnit} via {@link IScriptUnit#createFromNBT}.
  • + *
+ * + * @return A small classification result so GuiScriptInterface can update GUI state. + */ + default GuiDataResult setGuiData(INbt compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + // Ignore unrelated packets (e.g. ScriptInfoPacket config data) + // that don't contain any expected keys for this handler + if (!compound.hasKey("Tab") && !compound.hasKey("ScriptLanguage") && !compound.hasKey("ScriptEnabled")) { + return null; + } + + if (!compound.hasKey("Tab")) { + setLanguage(compound.getString("ScriptLanguage")); + setEnabled(compound.getBoolean("ScriptEnabled")); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + int tab = compound.getInteger("Tab"); + INbt scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit unit = IScriptUnit.createFromNBT(scriptCompound, this); + replaceScriptUnit(tab, unit); + return new GuiDataResult(GuiDataKind.TAB, tab); + } + + /** + * Emit save packets for all tabs plus one metadata packet. + * GuiScriptInterface is responsible for updating the active {@link IScriptUnit} with current editor text first. + */ + default void sync() { + List containers = getScripts(); + for (int i = 0; i < containers.size(); i++) { + IScriptUnit container = containers.get(i); + sendSavePacket(i, containers.size(), container.writeToNBT(new INbt())); + } + + INbt scriptData = new INbt(); + scriptData.setString("ScriptLanguage", getLanguage()); + scriptData.setBoolean("ScriptEnabled", getEnabled()); + scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(getConsoleText())); + sendSavePacket(-1, containers.size(), scriptData); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/core/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java new file mode 100644 index 000000000..40a255ada --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -0,0 +1,225 @@ +package noppes.npcs.controllers.data; + + +import java.util.List; +import java.util.TreeMap; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.controllers.ScriptContainer; + +/** + * Common interface for script units that can be edited in a script GUI. + * Implemented by both ScriptContainer (ECMAScript) and JaninoScript (Java/Janino). + * This allows GuiScriptInterface to work with both script types uniformly. + */ +public interface IScriptUnit { + + // NBT type identifiers + String NBT_TYPE_KEY = "ScriptUnitType"; + String TYPE_ECMASCRIPT = "ECMAScript"; + String TYPE_JANINO = "Janino"; + + /** + * Create an IScriptUnit from NBT data. + * Uses the "ScriptUnitType" tag to determine which implementation to create. + * + * @param compound The NBT data + * @param handler The script handler (used for ScriptContainer) + * @return A ScriptContainer or JaninoScript based on the NBT type + */ + static IScriptUnit createFromNBT(INbt compound, IScriptHandler handler) { + String type = compound.getString(NBT_TYPE_KEY); + IScriptUnit unit; + + if (TYPE_JANINO.equals(type)) { + // Create appropriate JaninoScript based on handler + unit = handler.createJaninoScriptUnit(); + if (unit == null) { + // Fallback to ScriptContainer if handler doesn't support Janino + unit = new ScriptContainer(handler); + } + } else { + // Default to ScriptContainer for ECMAScript or missing type + unit = new ScriptContainer(handler); + } + + unit.readFromNBT(compound); + return unit; + } + + // ==================== SCRIPT CONTENT ==================== + + /** + * Get the main script text content. + */ + String getScript(); + + /** + * Set the main script text content. + */ + void setScript(String script); + + /** + * Get the list of external script file names loaded for this unit. + */ + List getExternalScripts(); + + /** + * Set the list of external script file names to load. + */ + void setExternalScripts(List scripts); + + // ==================== CONSOLE ==================== + + /** + * Get the console output map (timestamp -> message). + */ + TreeMap getConsole(); + + /** + * Clear all console output. + */ + void clearConsole(); + + /** + * Append a message to the console. + */ + void appendConsole(String message); + + // ==================== LANGUAGE ==================== + + /** + * Get the scripting language for this unit. + * Returns "ECMAScript", "Java", etc. + */ + String getLanguage(); + + /** + * Set the scripting language for this unit. + * + * @param language The language name (e.g., "ECMAScript", "Java") + */ + void setLanguage(String language); + + /** + * Check if this script unit uses Janino (Java) compilation. + */ + default boolean isJanino() { + return "Java".equals(getLanguage()); + } + + // ==================== HOOK GENERATION ==================== + + /** + * Generate a stub/template for the given hook name. + * ECMAScript: function hookName(event) { } + * Janino: full Java method signature with return type + * + * @param hookName The hook name to generate a stub for + * @param hookData Optional data (e.g., Method object for Janino) + * @return The generated stub string + */ + String generateHookStub(String hookName, Object hookData); + + // ==================== NBT ==================== + + /** + * Write this script unit to NBT. + */ + INbt writeToNBT(INbt compound); + + /** + * Read this script unit from NBT. + */ + void readFromNBT(INbt compound); + + // ==================== STATE ==================== + + /** + * Ensure this script unit is compiled (for Janino). + * No-op for ScriptContainer. + */ + void ensureCompiled(); + + /** + * Check if this script unit has any code (script or external scripts). + */ + boolean hasCode(); + + /** + * Check if this script unit has encountered an error during execution. + */ + boolean hasErrored(); + + /** + * Set the error state of this script unit. + */ + void setErrored(boolean errored); + + // ==================== INSTANCE SCOPE ==================== + + /** + * Create an instance-scoped copy of this script unit for isolated execution. + * The instance shares compiled code with the template but has isolated state. + * + * @param instanceHandler the script handler for the new instance + * @return a new IScriptUnit with isolated variable state + */ + IScriptUnit createInstanceScope(IScriptHandler instanceHandler); + + /** + * Check if the given function name is known to be absent from this script unit. + */ + default boolean isUnknownFunction(String type) { + return false; + } + + // ==================== EXECUTION ==================== + + /** + * Execute this script unit for the given hook type and event. + * + * @param type The script hook type + * @param event The event object + */ + void run(EnumScriptType type, Event event); + + /** + * Execute this script unit for the given hook name and event. + * + * @param hookName The hook name (e.g., "init", "interact") + * @param event The event object + */ + void run(String hookName, Object event); + + /** + * Call a script function by name and return its result. + *

+ * Default implementation returns null (not supported). + */ + default Object callFunction(String hookName, Object... args) { + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/core/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java new file mode 100644 index 000000000..8aaed4486 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -0,0 +1,141 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.EnumScriptType; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * Reusable IScriptHandler implementation for objects that own an optional single JaninoScript unit. + *

+ * This is meant for "function provider" scripts (not EnumScriptType driven). Execution is typically + * initiated by the owning system calling script.call(...) directly. + *

+ * NBT storage is kept under the parent compound key "Script" (same convention as JaninoScriptable). + */ +public class JaninoScriptHandler> extends ScriptHandler { + + protected S script; + + private final Supplier factory; + private final Class scriptClass; + + public JaninoScriptHandler(Supplier factory, Class scriptClass) { + this.factory = factory; + this.scriptClass = scriptClass; + scriptLanguage = "Java"; + } + + // -------------------- Script ownership (JaninoScriptable) -------------------- + + public boolean hasScript() { + return script != null; + } + + public S getScript() { + return script; + } + + public JaninoScript createScript() { + if (script == null) + script = factory.get(); + + return script; + } + + public void deleteScript() { + script = null; + } + + // -------------------- NBT helpers -------------------- + + public void readFromNBT(INbt compound) { + // JaninoScriptable's helper generics assume JaninoScript, but most of our scripts are JaninoScript. + // Keep it simple and safe: treat it as raw. + script = (S) JaninoScript.readFromNBT(compound, (JaninoScript) script, (Supplier) factory); + } + + public void writeToNBT(INbt compound) { + JaninoScript.writeToNBT(compound, (JaninoScript) script); + } + + // -------------------- IScriptHandler -------------------- + + @Override + public void callScript(String hookName, Event event) { + // No-op: these scripts are not executed through the global hook dispatcher. + } + + @Override + public void callScript(EnumScriptType type, Event event) { + // No-op + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty() || list.get(0) == null) { + deleteScript(); + return; + } + + IScriptUnit unit = list.get(0); + if (scriptClass.isInstance(unit)) { + this.script = scriptClass.cast(unit); + return; + } + + // Fallback: copy code into a real instance of this script type. + S target = scriptClass.cast(createScript()); + target.setScript(unit.getScript()); + target.setExternalScripts(unit.getExternalScripts()); + } + + @Override + public List getScripts() { + if (script == null) + return Collections.emptyList(); + + return Collections.singletonList(script); + } + + @Override + public void addScriptUnit(IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void replaceScriptUnit(int index, IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void removeScriptUnit(int index) { + deleteScript(); + } + + public List getHooks() { + if (script != null) + return script.getHookList(); + + return Collections.emptyList(); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return factory.get(); + } + + @Override + public boolean supportsJanino() { + return true; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Lines.java b/core/src/main/java/noppes/npcs/controllers/data/Lines.java index 289f16ccb..9ed2ef8be 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/Lines.java +++ b/core/src/main/java/noppes/npcs/controllers/data/Lines.java @@ -1,15 +1,22 @@ package noppes.npcs.controllers.data; -import noppes.npcs.api.handler.data.ILine; -import noppes.npcs.api.handler.data.ILines; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; -import noppes.npcs.core.NBT; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Random; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.ILine; +import noppes.npcs.api.handler.data.ILines; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.core.NBT; public class Lines implements ILines { private static final Random random = new Random(); @@ -20,32 +27,32 @@ public class Lines implements ILines { public INbt writeToNBT() { INbt compound = NBT.compound(); - INbtList nbttaglist = NBT.list(); + INbtList INbtList = NBT.list(); for (int slot : lines.keySet()) { Line line = lines.get(slot); - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Slot", slot); - nbttagcompound.setString("Line", line.text); - nbttagcompound.setString("Song", line.sound); + INbt INbt = NBT.compound(); + INbt.setInteger("Slot", slot); + INbt.setString("Line", line.text); + INbt.setString("Song", line.sound); - nbttaglist.addCompound(nbttagcompound); + INbtList.addCompound(INbt); } - compound.setTagList("Lines", nbttaglist); + compound.setTagList("Lines", INbtList); return compound; } public void readNBT(INbt compound) { - INbtList nbttaglist = compound.getTagList("Lines", 10); + INbtList INbtList = compound.getTagList("Lines", 10); HashMap map = new HashMap(); - for (int i = 0; i < nbttaglist.size(); i++) { - INbt nbttagcompound = nbttaglist.getCompound(i); + for (int i = 0; i < INbtList.size(); i++) { + INbt INbt = INbtList.getCompound(i); Line line = new Line(); - line.text = nbttagcompound.getString("Line"); - line.sound = nbttagcompound.getString("Song"); + line.text = INbt.getString("Line"); + line.sound = INbt.getString("Song"); - map.put(nbttagcompound.getInteger("Slot"), line); + map.put(INbt.getInteger("Slot"), line); } lines = map; } diff --git a/core/src/main/java/noppes/npcs/controllers/data/LinkedItem.java b/core/src/main/java/noppes/npcs/controllers/data/LinkedItem.java new file mode 100644 index 000000000..0c3baa5f4 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/LinkedItem.java @@ -0,0 +1,283 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomItems; +import noppes.npcs.EventHooks; +import noppes.npcs.api.handler.data.ILinkedItem; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.controllers.LinkedItemController; +import noppes.npcs.controllers.TagController; +import java.util.HashSet; +import java.util.UUID; + +public class LinkedItem implements ILinkedItem { + public static final String LINKED_VERSION_VERSION_TAG = "LinkedVersion"; + public static final String LINKED_DATA_NBT_TAG = "LinkedData"; + + public int id = -1; + public int version = 0; + public String name; + public final ItemDisplayData display = new ItemDisplayData(); + + public double durabilityValue = 1.0D; + public int stackSize = 64; + + public int maxItemUseDuration = 20; + public int itemUseAction = 0; //0: none, 1: block, 2: bow, 3: eat, 4: drink + + public boolean isNormalItem = false; + public boolean isTool = false; + public int digSpeed = 1; + 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; + + public HashSet tagUUIDs = new HashSet<>(); + + public LinkedItem() { + this("New"); + } + + public LinkedItem(String name) { + this.name = name; + } + + public LinkedItemScript getScriptHandler() { + return (LinkedItemScript) LinkedItemController.getInstance().getScriptHandler(this.getId()); + } + + public INbt writeToNBT(boolean saveScripts) { + INbt compound = new INbt(); + compound.setInteger("Id", this.id); + compound.setInteger("Version", this.version); + compound.setString("Name", this.name); + + compound.setTag("Display", ((NBTWrapper) this.display.writeToNBT()).getMCTag()); + + compound.setDouble("DurabilityValue", this.durabilityValue); + compound.setInteger("MaxStackSize", this.stackSize); + + compound.setBoolean("IsTool", this.isTool); + compound.setBoolean("IsNormalItem", this.isNormalItem); + compound.setInteger("DigSpeed", this.digSpeed); + compound.setInteger("AttackSpeed", this.attackSpeed); + + compound.setInteger("ArmorType", this.armorType); + compound.setInteger("Enchantability", this.enchantability); + + compound.setInteger("MaxItemUseDuration", this.maxItemUseDuration); + compound.setInteger("ItemUseAction", this.itemUseAction); + + TagController.writeTagUUIDs(compound, "TagUUIDs", tagUUIDs); + + if (saveScripts) { + INbt scriptData = new INbt(); + LinkedItemScript handler = getScriptHandler(); + if (handler != null) + handler.writeToNBT(scriptData); + compound.setTag("ScriptData", scriptData); + } + return compound; + } + + public void readFromNBT(INbt compound) { + this.id = compound.hasKey("Id") ? compound.getInteger("Id") : LinkedItemController.getInstance().getUnusedId(); + this.name = compound.getString("Name"); + this.version = compound.getInteger("Version"); + + this.display.readFromNBT(new NBTWrapper(compound.getCompoundTag("Display"))); + + this.durabilityValue = compound.getDouble("DurabilityValue"); + this.stackSize = compound.getInteger("MaxStackSize"); + + this.isTool = compound.getBoolean("IsTool"); + this.isNormalItem = compound.getBoolean("IsNormalItem"); + this.digSpeed = compound.getInteger("DigSpeed"); + this.setAttackSpeed(compound.getInteger("AttackSpeed")); + this.armorType = compound.getInteger("ArmorType"); + this.enchantability = compound.getInteger("Enchantability"); + + this.maxItemUseDuration = compound.getInteger("MaxItemUseDuration"); + this.itemUseAction = compound.getInteger("ItemUseAction"); + + tagUUIDs = TagController.readTagUUIDs(compound, "TagUUIDs"); + + if (compound.hasKey("ScriptData", NbtConstants.TAG_COMPOUND)) { + LinkedItemScript handler = new LinkedItemScript(); + handler.readFromNBT(compound.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } + } + + @Override + public ILinkedItem save() { + return LinkedItemController.getInstance().saveLinkedItem(this); + } + + public void setScriptHandler(LinkedItemScript handler) { + LinkedItemController.getInstance().linkedItemsScripts.put(this.id, handler); + } + + @Override + public IItemStack createStack() { + LinkedItem copy = this.clone(); + IItemStack stack = new IItemStack(CustomItems.linked_item, 1); + ScriptLinkedItem scriptLinkedItem = new ScriptLinkedItem(stack, copy); + EventHooks.onLinkedItemBuild(scriptLinkedItem); + return NpcAPI.Instance().getIItemStack(scriptLinkedItem.item); + } + + @Override + public int getId() { + return id; + } + + @Override + public void setId(int id) { + this.id = id; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public void setVersion(int version) { + this.version = version; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public double getDurabilityValue() { + return durabilityValue; + } + + @Override + public void setDurabilityValue(double durabilityValue) { + this.durabilityValue = durabilityValue; + } + + @Override + public int getStackSize() { + return stackSize; + } + + @Override + public void setStackSize(int stackSize) { + this.stackSize = stackSize; + } + + @Override + public int getMaxItemUseDuration() { + return maxItemUseDuration; + } + + @Override + public void setMaxItemUseDuration(int maxItemUseDuration) { + this.maxItemUseDuration = maxItemUseDuration; + } + + @Override + public int getItemUseAction() { + return itemUseAction; + } + + @Override + public void setItemUseAction(int itemUseAction) { + this.itemUseAction = itemUseAction; + } + + @Override + public boolean isNormalItem() { + return isNormalItem; + } + + @Override + public void setNormalItem(boolean normalItem) { + isNormalItem = normalItem; + } + + @Override + public boolean isTool() { + return isTool; + } + + @Override + public void setTool(boolean tool) { + isTool = tool; + } + + @Override + public int getDigSpeed() { + return digSpeed; + } + + @Override + public void setDigSpeed(int digSpeed) { + this.digSpeed = digSpeed; + } + + @Override + public int getArmorType() { + return armorType; + } + + @Override + public void setArmorType(int armorType) { + this.armorType = armorType; + } + + @Override + public int getEnchantability() { + return enchantability; + } + + @Override + public void setEnchantability(int enchantability) { + 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() { + INbt nbt = this.writeToNBT(true); + LinkedItem clone = new LinkedItem(this.name); + clone.readFromNBT(nbt); + return clone; + } + + public LinkedItemScript getOrCreateScriptHandler() { + LinkedItemScript data = getScriptHandler(); + if (data == null) + setScriptHandler(data = new LinkedItemScript()); + return data; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/core/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java new file mode 100644 index 000000000..f62ec199d --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -0,0 +1,66 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.ScriptContext; +/** + * Script handler for LinkedItem scripts. + * Manages a single script container for linked item hooks. + */ +public class LinkedItemScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** + * The linked item ID for packet communication. -1 if not bound. + */ + private int linkedItemId = -1; + + /** + * Create an unbound LinkedItemScript (for server-side use). + */ + public LinkedItemScript() { + } + + /** + * Create a LinkedItemScript bound to a specific linked item (for GUI use). + * + * @param linkedItemId The ID of the LinkedItem + */ + public LinkedItemScript(int linkedItemId) { + this.linkedItemId = linkedItemId; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.LINKED_ITEM; + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.LINKED_ITEM); + } + + @Override + public String noticeString() { + return "LinkedItem"; + } + + @Override + public void requestData() { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Get(linkedItemId); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Save(linkedItemId, index, totalCount, nbt); + + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Magic.java b/core/src/main/java/noppes/npcs/controllers/data/Magic.java index 1d8086a7b..9e4728797 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/Magic.java +++ b/core/src/main/java/noppes/npcs/controllers/data/Magic.java @@ -1,13 +1,20 @@ package noppes.npcs.controllers.data; -import noppes.npcs.core.NBT; -import noppes.npcs.constants.EnumTextureType; -import noppes.npcs.controllers.MagicController; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; import java.util.HashMap; import java.util.Map; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumTextureType; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.core.NBT; public class Magic { public String name = ""; @@ -15,9 +22,8 @@ public class Magic { public int color = Integer.parseInt("FF00", 16); public int id = -1; - // TODO: mc1710 version has: public ItemStack item = null; - // OLD: import net.minecraft.item.ItemStack; - public EnumTextureType type = EnumTextureType.BASE; + // TODO: mc1710 version has: public IItemStack item = null; + // OLD: public EnumTextureType type = EnumTextureType.BASE; public String iconTexture = ""; public Map interactions = new HashMap<>(); @@ -44,11 +50,11 @@ public class Magic { type = EnumTextureType.values()[compound.getInteger("Type")]; iconTexture = compound.getString("IconTexture"); - // TODO: mc1710 version resolves ItemStack from GameRegistry here: + // TODO: mc1710 version resolves IItemStack from GameRegistry here: // OLD: if (type == EnumTextureType.ITEM && !iconTexture.isEmpty()) { // OLD: String[] parts = iconTexture.split(":"); // OLD: Item item = GameRegistry.findItem(parts[0], parts[1]); - // OLD: this.item = (item != null) ? new ItemStack(item) : null; + // OLD: this.item = (item != null) ? new IItemStack(item) : null; // OLD: } interactions.clear(); @@ -78,15 +84,15 @@ public class Magic { interactionsList.addCompound(interactionTag); } compound.setTagList("Interactions", interactionsList); - // TODO: mc1710 version resolves ItemStack from GameRegistry here: + // TODO: mc1710 version resolves IItemStack from GameRegistry here: // OLD: if (type == EnumTextureType.ITEM && !iconTexture.isEmpty()) { // OLD: Item item = GameRegistry.findItem(parts[0], parts[1]); - // OLD: this.item = (item != null) ? new ItemStack(item) : null; + // OLD: this.item = (item != null) ? new IItemStack(item) : null; // OLD: } } // TODO: mc1710 version implements IMagic and adds: - // OLD: public void setItem(ItemStack item) — uses GameRegistry.findUniqueIdentifierFor - // OLD: public ItemStack getItem() + // OLD: public void setItem(IItemStack item) — uses GameRegistry.findUniqueIdentifierFor + // OLD: public IItemStack getItem() public int getId() { return this.id; diff --git a/core/src/main/java/noppes/npcs/controllers/data/MarkData.java b/core/src/main/java/noppes/npcs/controllers/data/MarkData.java new file mode 100644 index 000000000..e646c018e --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/MarkData.java @@ -0,0 +1,162 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.entity.data.IMark; +import noppes.npcs.api.handler.data.IAvailability; +import noppes.npcs.constants.MarkType; +import java.util.ArrayList; +import java.util.List; + +public class MarkData implements IExtendedEntityProperties { + + private static final String CNPCMARK = "cnpcmark"; + private IEntityLivingBase IEntity; + + public List marks = new ArrayList(); + + public void setNBT(INbt compound) { + List marks = new ArrayList(); + INbtList list = compound.getTagList("marks", 10); + for (int i = 0; i < list.tagCount(); i++) { + INbt c = list.getCompoundTagAt(i); + Mark m = new Mark(); + m.type = c.getInteger("type"); + m.color = c.getInteger("color"); + m.availability.readFromNBT(c.getCompoundTag("availability")); + marks.add(m); + } + this.marks = marks; + } + + public INbt getNBT() { + INbt compound = new INbt(); + INbtList list = new INbtList(); + for (Mark m : marks) { + INbt c = new INbt(); + c.setInteger("type", m.type); + c.setInteger("color", m.color); + c.setTag("availability", m.availability.writeToNBT(new INbt())); + list.appendTag(c); + } + compound.setTag("marks", list); + return compound; + } + + public static boolean has(IEntityLivingBase IEntity) { + return IEntity.getExtendedProperties(CNPCMARK) != null; + } + + public static MarkData get(EntityNPCInterface npc) { + if (!MarkData.has(npc)) { + npc.registerExtendedProperties(CNPCMARK, new MarkData()); + } + + MarkData data = (MarkData) npc.getExtendedProperties(CNPCMARK); + if (data.IEntity == null) { + data.IEntity = npc; + data.setNBT(npc.getEntityData().getCompoundTag(CNPCMARK)); + } + return data; + } + + public IMark addMark(int type) { + Mark m = new Mark(); + m.type = type; + marks.add(m); + if (!IEntity.worldObj.isRemote) + syncClients(); + return m; + } + + public IMark addMark(int type, int color) { + Mark m = new Mark(); + m.type = type; + m.color = color; + marks.add(m); + if (!IEntity.worldObj.isRemote) + syncClients(); + return m; + } + + @Override + public void saveNBTData(INbt compound) { + if (!(this.IEntity instanceof EntityNPCInterface)) { + return; + } + this.IEntity.getEntityData().setTag(CNPCMARK, this.getNBT()); + } + + @Override + public void loadNBTData(INbt compound) { + } + + @Override + public void init(IEntity IEntity, IWorld IWorld) { + } + + public void syncClients() { + PacketHandler.Instance.sendToAll(new MarkDataPacket(IEntity.getEntityId(), getNBT())); + } + + public class Mark implements IMark { + public int type = MarkType.NONE; + public Availability availability = new Availability(); + public int color = 0xFFED51; + + @Override + public IAvailability getAvailability() { + return availability; + } + + @Override + public int getColor() { + return color; + } + + @Override + public void setColor(int color) { + this.color = color; + } + + @Override + public int getType() { + return type; + } + + @Override + public void setType(int type) { + this.type = type; + } + + @Override + public void update() { + MarkData.this.syncClients(); + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/ModelScalePart.java b/core/src/main/java/noppes/npcs/controllers/data/ModelScalePart.java new file mode 100644 index 000000000..100ed2e8b --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/ModelScalePart.java @@ -0,0 +1,9 @@ +package noppes.npcs.controllers.data; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Model scale part enum/class. + */ +public class ModelScalePart { + public float scaleX = 1, scaleY = 1, scaleZ = 1; +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/core/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java new file mode 100644 index 000000000..dff000795 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -0,0 +1,116 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; + +/** + * Base class for script handlers that manage multiple script containers (tabs). + * Provides shared NBT serialization and common script execution patterns. + *

+ * Subclasses should: + * - Override getContext() to return the appropriate ScriptContext + * - Override getHookContext() if not using ScriptContext's hookContext + * - Override requestData() and sendSavePacket() for packet communication + * - Override isEnabled() if custom enable logic is needed + */ +public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { + + // ==================== NBT SERIALIZATION ==================== + + /** + * Read script data from NBT using the Tab0/Tab1/etc. format. + */ + public void readFromNBT(INbt compound) { + if (compound.hasKey("Scripts")) { + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); + } else { + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); + } + + this.scriptLanguage = compound.getString("ScriptLanguage"); + normalizeLanguage(); + + this.enabled = compound.getBoolean("ScriptEnabled"); + } + + /** + * Write script data to NBT using the Tab0/Tab1/etc. format. + */ + public INbt writeToNBT(INbt compound) { + compound.setInteger("TotalScripts", this.scripts.size()); + for (int i = 0; i < this.scripts.size(); i++) { + compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new INbt())); + } + compound.setString("ScriptLanguage", this.scriptLanguage); + compound.setBoolean("ScriptEnabled", this.enabled); + return compound; + } + + /** + * Normalize the language to a valid one if the current language is not available. + */ + protected void normalizeLanguage() { + if (this.scriptLanguage == null || this.scriptLanguage.isEmpty()) { + if (!ScriptController.Instance.languages.isEmpty()) { + this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; + } else { + this.scriptLanguage = "ECMAScript"; + } + } + } + + // ==================== SCRIPT EXECUTION ==================== + + /** + * Check if scripts need to be re-initialized due to reload. + * Subclasses can override to add additional update tracking (e.g., lastPlayerUpdate). + * + * @return true if scripts should be re-initialized + */ + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited; + } + + /** + * Called when scripts need re-initialization. + * Clears error state on all script containers. + */ + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).errored = false; + } + } + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) { + continue; + } + script.run(hookName, event); + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/Party.java b/core/src/main/java/noppes/npcs/controllers/data/Party.java new file mode 100644 index 000000000..f10087e40 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/Party.java @@ -0,0 +1,480 @@ +package noppes.npcs.controllers.data; + + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IParty; +import noppes.npcs.api.handler.data.IQuest; +import noppes.npcs.api.handler.IPlayerQuestData; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumPartyObjectives; +import noppes.npcs.constants.EnumPartyRequirements; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.NoppesUtilServer; + +public class Party implements IParty { + private final UUID partyUUID; + private UUID partyLeader; + private QuestData questData; + + private final HashMap partyMembers = new HashMap<>(); + private final ArrayList partyOrder = new ArrayList<>(); + + private int currentQuestID = -1; + private String currentQuestName = ""; + + private boolean friendlyFire; + private boolean partyLocked = false; + + //Client-sided + private String partyLeaderName; + private Quest quest; + + public Party() { + this.partyUUID = UUID.randomUUID(); + } + + public Party(UUID uuid) { + this.partyUUID = uuid; + } + + public UUID getPartyUUID() { + return this.partyUUID; + } + + @Override + public String getPartyUUIDString() { + return this.partyUUID.toString(); + } + + @Override + public boolean getIsLocked() { + return this.partyLocked; + } + + @Override + public void setQuest(IQuest quest) { + if (quest != null) { + this.currentQuestID = quest.getId(); + this.currentQuestName = quest.getName(); + this.questData = new QuestData((Quest) quest); + } else { + this.currentQuestID = -1; + this.currentQuestName = ""; + this.questData = null; + } + this.partyLocked = quest != null; + } + + @Override + public IQuest getQuest() { + if (FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { + return QuestController.Instance.get(this.currentQuestID); + } else { + return this.quest; + } + } + + public QuestData getQuestData() { + return questData; + } + + public EnumPartyObjectives getObjectiveRequirement() { + if (getQuestData() != null) { + if (getQuestData().quest != null) { + return getQuestData().quest.partyOptions.objectiveRequirement; + } + } + return null; + } + + @Override + public int getCurrentQuestID() { + return currentQuestID; + } + + @Override + public String getCurrentQuestName() { + return this.currentQuestName; + } + + public boolean addPlayer(IPlayer player) { + if (player == null) return false; + + if (partyMembers.containsKey(player.getUniqueID())) { + return false; + } + + PlayerData playerData = PlayerData.get(player); + playerData.partyUUID = this.partyUUID; + + partyMembers.put(player.getUniqueID(), player.getCommandSenderName()); + partyOrder.add(player.getUniqueID()); + return true; + } + + public boolean removePlayer(IPlayer player) { + if (player == null) return false; + + UUID uuid = player.getUniqueID(); + if (partyMembers.containsKey(uuid)) { + partyMembers.remove(uuid); + partyOrder.remove(uuid); + + if (uuid.equals(partyLeader)) { + if (partyMembers.size() > 0) { + partyLeader = partyOrder.get(0); + } + } + + PlayerData playerData = PlayerData.get(player); + playerData.partyUUID = null; + + return true; + } + + return false; + } + + public boolean removePlayer(UUID uuid) { + if (uuid == null) return false; + if (partyMembers.containsKey(uuid)) { + partyMembers.remove(uuid); + partyOrder.remove(uuid); + if (uuid.equals(partyLeader)) { + if (!partyMembers.isEmpty()) { + partyLeader = partyOrder.get(0); + } + } + + PlayerData playerData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + playerData.partyUUID = null; + return true; + } + return false; + } + + @Override + public boolean addPlayer(String playerName) { + return playerName != null && this.addPlayer(NoppesUtilServer.getPlayerByName(playerName)); + } + + @Override + public boolean removePlayer(String playerName) { + return playerName != null && this.removePlayer(NoppesUtilServer.getPlayerByName(playerName)); + } + + + @Override + public boolean hasPlayer(String playerName) { + UUID uuid = getUUID(playerName); + if (uuid == null) { + return false; + } + return partyMembers.containsKey(uuid); + } + + public IPlayer getPartyLeader() { + return NoppesUtilServer.getPlayer(this.partyLeader); + } + + @Override + public String getPartyLeaderName() { + if (PlatformServiceHolder.get().isLogicalClient()) { + return this.partyLeaderName == null ? "" : this.partyLeaderName; + } else { + IPlayer leader = this.getPartyLeader(); + if (leader != null) + return leader.getCommandSenderName(); + String name = this.partyMembers.get(this.partyLeader); + return name == null ? "" : name; + } + } + + public boolean setLeader(IPlayer player) { + UUID uuid = player.getUniqueID(); + if (partyLeader != null && partyLeader.equals(uuid)) { + return false; + } + + if (!partyMembers.containsKey(uuid)) { + return false; + } + + // Remove New Leader + partyOrder.remove(uuid); + + // Add New Leader to Front + partyOrder.add(0, uuid); + + // Set New Leader + partyLeader = uuid; + return true; + } + + public UUID getLeaderUUID() { + return partyLeader; + } + + public Collection getPlayerNames() { + return this.getPlayerNames(false); + } + + @Override + public List getPlayerNamesList() { + return new ArrayList<>(this.getPlayerNames(false)); + } + + public Collection getPlayerNames(boolean lowercase) { + Collection names = this.partyMembers.values(); + if (!lowercase) { + return names; + } + + ArrayList lowerNames = new ArrayList<>(); + for (String s : names) { + lowerNames.add(s.toLowerCase()); + } + return lowerNames; + } + + public Collection getPlayerUUIDs() { + return this.partyMembers.keySet(); + } + + // Called when Set Quest is Called + @Override + public boolean validateQuest(int questID, boolean sendLeaderMessages) { + IQuest quest = QuestController.Instance.get(questID); + if (quest == null) { + return false; + } + + IPlayer leader = PlayerDataController.getPlayerFromUUID(partyLeader); + if (leader == null) { + return false; + } + + int partyReq = quest.getPartyOptions().getPartyRequirements(); + if (partyReq < 0 || partyReq >= EnumPartyRequirements.values().length) { + sendInfoMessage(leader, "Error in quest party requirements", sendLeaderMessages); + return false; + } + + if (partyMembers.size() < quest.getPartyOptions().getMinPartySize()) { + sendInfoMessage(leader, String.format("Party too small. Min %d members", quest.getPartyOptions().getMinPartySize()), sendLeaderMessages); + return false; + } + + if (partyMembers.size() > quest.getPartyOptions().getMaxPartySize()) { + sendInfoMessage(leader, String.format("Party too large. Max %d members", quest.getPartyOptions().getMaxPartySize()), sendLeaderMessages); + return false; + } + + EnumPartyRequirements partyRequirements = EnumPartyRequirements.values()[partyReq]; + if (partyRequirements == EnumPartyRequirements.Leader) { + boolean leaderBool = isValidLeaderQuest(leader, questID); + if (!leaderBool) { + sendInfoMessage(leader, "\u00A7cYou are invalid for this quest", sendLeaderMessages); + } + return leaderBool; + } + + return arePlayersValid(leader, partyRequirements, quest, sendLeaderMessages); + } + + private boolean isValidLeaderQuest(IPlayer leader, int questID) { + IPlayerQuestData questData = PlayerData.get(leader).getQuestData(); + return questData != null && questData.hasActiveQuest(questID); + } + + private boolean arePlayersValid(IPlayer leader, EnumPartyRequirements requirements, IQuest quest, boolean sendLeaderMessages) { + boolean allowQuest = true; + for (UUID playerUUID : partyMembers.keySet()) { + IPlayer player = PlayerDataController.getPlayerFromUUID(playerUUID); + if (player != null) { + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + IPlayerQuestData questData = playerData.getQuestData(); + if (questData != null) { + boolean hasActive = questData.hasActiveQuest(quest.getId()); + boolean hasFinished = questData.hasFinishedQuest(quest.getId()); + + if (requirements == EnumPartyRequirements.All && !hasActive) { + allowQuest = false; + sendInfoMessage(leader, String.format("%s does not have quest active", player.getCommandSenderName()), sendLeaderMessages); + } else if (requirements == EnumPartyRequirements.Valid && !hasActive && !hasFinished) { + allowQuest = false; + sendInfoMessage(leader, String.format("%s does not have the quest active or finished", player.getCommandSenderName()), sendLeaderMessages); + } + } else { + allowQuest = false; + sendInfoMessage(leader, String.format("%s has no quest data", player.getCommandSenderName()), sendLeaderMessages); + } + } else { + allowQuest = false; + sendInfoMessage(leader, String.format("%s has no player data", player.getCommandSenderName()), sendLeaderMessages); + } + } else { + allowQuest = false; + String playerName = partyMembers.get(playerUUID); + sendInfoMessage(leader, String.format("%s was not found", playerName), sendLeaderMessages); + } + } + return allowQuest; + } + + private void sendInfoMessage(IPlayer player, String message, boolean send) { + if (!send) + return; + + player.addChatMessage(new ITextComponent(String.format("\u00A7c%s", message))); + } + + @Override + public void toggleFriendlyFire() { + this.friendlyFire = !this.friendlyFire; + } + + @Override + public boolean friendlyFire() { + return this.friendlyFire; + } + + public static UUID getUUID(String name) { + UUID uuid = null; + try { + BufferedReader in = new BufferedReader(new InputStreamReader(new URL("https://api.mojang.com/users/profiles/minecraft/" + name).openStream())); + String uuidString; + uuidString = (((JsonObject) new JsonParser().parse(in)).get("id")).toString().replaceAll("\"", ""); + uuidString = uuidString.replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); + in.close(); + + uuid = UUID.fromString(uuidString); + } catch (Exception ignored) { + } + return uuid; + } + + public void readFromNBT(INbt compound) { + this.partyLeaderName = compound.getString("PartyLeader"); + this.currentQuestName = compound.getString("PartyQuestName"); + this.friendlyFire = compound.getBoolean("FriendlyFire"); + this.partyLocked = compound.getBoolean("PartyLocked"); + + this.currentQuestID = compound.getInteger("PartyQuestID"); + if (compound.hasKey("QuestNBT")) { + this.quest = new Quest(); + this.quest.readNBT(compound.getCompoundTag("QuestNBT")); + } + + INbtList list = compound.getTagList("PartyMembers", 10); + for (int i = 0; i < list.tagCount(); i++) { + INbt tagCompound = list.getCompoundTagAt(i); + UUID uuid = UUID.fromString(tagCompound.getString("UUID")); + String playerName = tagCompound.getString("PlayerName"); + this.partyOrder.add(uuid); + this.partyMembers.put(uuid, playerName); + } + } + + public INbt writeToNBT() { + INbt compound = new INbt(); + + compound.setString("PartyUUID", this.partyUUID.toString()); + compound.setString("PartyQuestName", this.currentQuestName); + compound.setBoolean("FriendlyFire", this.friendlyFire); + compound.setBoolean("PartyLocked", this.partyLocked); + + compound.setInteger("PartyQuestID", this.currentQuestID); + if (this.getQuest() != null) { + Quest quest = (Quest) this.getQuest(); + compound.setTag("QuestNBT", quest.writeToNBT(new INbt())); + } + + INbtList list = new INbtList(); + for (UUID uuid : this.partyOrder) { + INbt uuidCompound = new INbt(); + uuidCompound.setString("UUID", uuid.toString()); + String name = this.partyMembers.get(uuid); + uuidCompound.setString("PlayerName", name == null ? "" : name); + list.appendTag(uuidCompound); + } + compound.setTag("PartyMembers", list); + + IPlayer leader = getPartyLeader(); + compound.setString("PartyLeader", leader != null ? leader.getCommandSenderName() : ""); + Quest quest = (Quest) QuestController.Instance.get(this.getCurrentQuestID()); + if (quest != null) { + compound.setString("QuestName", quest.getName()); + compound.setString("QuestCategory", quest.getCategory().getName()); + } + + return compound; + } + + @Override + public void updateQuestObjectiveData() { + if (FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { + PartyController.Instance().pingPartyQuestObjectiveUpdate(this); + } + } + + @Override + public void updatePartyData() { + if (FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { + PartyController.Instance().pingPartyUpdate(this); + } + } + + public void readClientNBT(INbt compound) { + this.currentQuestID = compound.getInteger("PartyQuestID"); + this.currentQuestName = compound.getString("PartyQuestName"); + this.friendlyFire = compound.getBoolean("FriendlyFire"); + + INbtList list = compound.getTagList("PartyMembers", 10); + for (int i = 0; i < list.tagCount(); i++) { + INbt tagCompound = list.getCompoundTagAt(i); + UUID uuid = UUID.fromString(tagCompound.getString("UUID")); + String playerName = tagCompound.getString("PlayerName"); + this.partyOrder.add(uuid); + this.partyMembers.put(uuid, playerName); + } + } + + public INbt writeClientNBT() { + INbt compound = new INbt(); + compound.setInteger("PartyQuestID", this.currentQuestID); + compound.setString("PartyQuestName", this.currentQuestName); + compound.setBoolean("FriendlyFire", this.friendlyFire); + + INbtList list = new INbtList(); + for (UUID uuid : this.partyOrder) { + INbt uuidCompound = new INbt(); + uuidCompound.setString("UUID", uuid.toString()); + String name = this.partyMembers.get(uuid); + uuidCompound.setString("PlayerName", name == null ? "" : name); + list.appendTag(uuidCompound); + } + compound.setTag("PartyMembers", list); + return compound; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java new file mode 100644 index 000000000..81779cfa1 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -0,0 +1,1105 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; +import kamkeel.npcs.controllers.data.ability.data.entry.AbilityToggleEntry; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import noppes.npcs.AbstractDataAbilities; +import noppes.npcs.EventHooks; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IPlayerAbilityData; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Manages player ability data - unlocked abilities, selection, cooldowns, and execution. + * Players reference abilities by key (built-in keys or preset names), never customized copies. + * Extends AbstractDataAbilities for shared ability lifecycle logic. + *

+ * Key differences from NPC DataAbilities: + * - Universal cooldown (shared across all abilities) + * - Player manually selects ability (no weighted random) + * - Conditions requiring a target are skipped + * - Uses ability's base cooldownTicks only + */ +public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerAbilityData { + + private final PlayerData playerData; + + // ═══════════════════════════════════════════════════════════════════ + // IConfiguration (saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + /** + * List of unlocked ability keys (preset names or built-in keys). + * Order matters - determines selection index. + */ + private List unlockedAbilities = new ArrayList<>(); + + /** + * Currently selected ability index in the unlocked list. + */ + private int selectedIndex = 0; + + /** + * Tracks whether the current animation was started by an ability. + * Persisted to NBT so orphaned ability animations can be detected and + * cleared on login (ability state is transient, but animation state is not). + */ + private boolean playingAbilityAnimation = false; + + // ═══════════════════════════════════════════════════════════════════ + // PLAYER-SPECIFIC RUNTIME STATE (not saved) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Current ability's key (for cooldown tracking after completion). + */ + private transient String currentAbilityKey; + + /** + * Target for the current ability (can be null for self-targeting). + */ + private transient IEntityLivingBase currentTarget; + + /** + * Last synced state flags byte for change detection (avoids spamming packets). + */ + private transient byte lastSyncedFlags = 0; + + /** + * IWorld time when the last ability was activated (for double-press cancel detection). + */ + private transient long lastAbilityActivationTime = -1; + + /** + * Ticks after activation during which a second key press will cancel the ability. + */ + private static final int CANCEL_WINDOW_TICKS = 10; + + /** + * Position tracking for teleport detection during ability execution. + * If the player moves more than {@link #TELEPORT_THRESHOLD_SQ} in a single tick, + * the ability is interrupted as if the player changed dimensions. + */ + private transient double lastAbilityTickX, lastAbilityTickY, lastAbilityTickZ; + private transient boolean trackingAbilityPosition = false; + + /** + * Squared distance threshold for teleport detection (48 blocks). + * Normal sprint speed is ~5.6 blocks/tick; this threshold is well above + * any legitimate per-tick movement including ability-driven dashes. + */ + private static final double TELEPORT_THRESHOLD_SQ = 48.0 * 48.0; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTOR + // ═══════════════════════════════════════════════════════════════════ + + public PlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + } + + // ═══════════════════════════════════════════════════════════════════ + // ABSTRACT METHOD IMPLEMENTATIONS + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected IEntityLivingBase getEntity() { + return playerData.player; + } + + @Override + protected IEntityLivingBase getTarget() { + return currentTarget; + } + + @Override + protected long getWorldTime() { + IPlayer player = playerData.player; + return player != null ? player.worldObj.getTotalWorldTime() : 0; + } + + @Override + protected void spawnTelegraph(Ability ability, IEntityLivingBase target) { + IPlayer player = playerData.player; + List telegraphs = ability.createTelegraphs(player, target); + if (!telegraphs.isEmpty()) { + ability.setTelegraphInstances(telegraphs); + if (player instanceof IPlayer) { + for (TelegraphInstance telegraph : telegraphs) { + TelegraphSpawnPacket.sendToTracking(telegraph, player); + } + } + } + } + + @Override + protected void removeTelegraph(Ability ability) { + IPlayer player = playerData.player; + List telegraphs = ability.getTelegraphInstances(); + for (TelegraphInstance telegraph : telegraphs) { + if (player instanceof IPlayer) { + TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); + } + } + ability.setTelegraphInstances(null); + } + + @Override + protected void setAnimationData(Animation animation) { + playerData.animationData.setEnabled(true); + playerData.animationData.setAnimation(animation); + playerData.animationData.updateClient(); + playingAbilityAnimation = true; + } + + @Override + protected void clearAnimationData() { + playerData.animationData.setAnimation(null); + playerData.animationData.updateClient(); + + // Directly send animation clear to the owning player. updateClient() uses + // getEntitiesWithinAABB to find recipients, but during teleport the player + // IEntity hasn't migrated to the new chunk yet — so the AABB search centered + // on the new position won't find them, and the clear packet is never delivered. + // This guarantees the player's client always receives the clear. + IPlayer player = playerData.player; + if (player instanceof IPlayer) { + INbt data = new INbt(); + data.setBoolean("AllowAnimation", playerData.animationData.enabled()); + PacketHandler.Instance.sendToPlayer( + new UpdateAnimationsPacket(data, player.getCommandSenderName()), + (IPlayer) player + ); + } + + playingAbilityAnimation = false; + } + + @Override + protected void playAbilitySound(String sound) { + if (sound != null && !sound.isEmpty()) { + IPlayer player = playerData.player; + if (player != null) { + player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f); + } + } + } + + @Override + protected void captureLockedRotation() { + IPlayer player = playerData.player; + lockedYaw = player.rotationYaw; + lockedPitch = player.rotationPitch; + rotationLocked = true; + } + + @Override + protected void rollCooldown(Ability ability) { + if (ability.isIgnoreCooldown()) return; + + int duration = ability.getCooldownTicks(); + long endTime = getWorldTime() + duration; + + if (ability.isPerAbilityCooldown()) { + // Per-ability: only this ability goes on cooldown + if (currentAbilityKey != null) { + setPerAbilityCooldown(currentAbilityKey, endTime, duration); + } + } else { + // Global: all global-cooldown abilities share this cooldown + cooldownEndTime = endTime; + globalCooldownDuration = duration; + } + } + + @Override + protected void rollChainCooldown(ChainedAbility chain) { + if (chain.isIgnoreCooldown()) return; + + int duration = chain.getCooldownTicks(); + long endTime = getWorldTime() + duration; + + if (chain.isPerAbilityCooldown()) { + if (currentAbilityKey != null) { + setPerAbilityCooldown(currentAbilityKey, endTime, duration); + } + } else { + cooldownEndTime = endTime; + globalCooldownDuration = duration; + } + } + + @Override + protected void onAbilityComplete() { + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + lastAbilityActivationTime = -1; + syncAbilityStateClear(playerData.player); + } + + // ═══════════════════════════════════════════════════════════════════ + // HOOK OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void onInterruptComplete() { + // Player clears state immediately (unlike NPC which ticks through DAZED) + // Roll cooldown before clearing state — NPC rolls during handleAbilityCompletion + // after ticking through DAZED, but the player skips DAZED entirely. + if (!interruptCooldownRolled && currentAbility != null) { + rollCooldown(currentAbility); + } + interruptCooldownRolled = false; + + // Stop the dazed animation that was started in interruptCurrentAbility() + stopAbilityAnimation(); + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + lastAbilityActivationTime = -1; + syncAbilityStateClear(playerData.player); + } + + // ═══════════════════════════════════════════════════════════════════ + // TICK - Called from player tick handler + // ═══════════════════════════════════════════════════════════════════ + + /** + * Main tick method - handles ability execution. + * Should be called every server tick. + */ + public void tick(IPlayer player) { + if (player.worldObj.isRemote || player.isDead) { + return; + } + + // Tick active toggles (independent of currentAbility) + tickActiveToggles(); + + boolean hasActiveAbility = chainDelayRemaining > 0 || (currentAbility != null && currentAbility.isExecuting()); + + if (hasActiveAbility) { + // Detect large position jump (teleport) while ability is executing. + // Must run BEFORE tickCurrentAbility / applyPositionLock so the raw + // teleported position is visible before any lock snaps it back. + if (trackingAbilityPosition) { + double dx = player.posX - lastAbilityTickX; + double dy = player.posY - lastAbilityTickY; + double dz = player.posZ - lastAbilityTickZ; + if (dx * dx + dy * dy + dz * dz > TELEPORT_THRESHOLD_SQ) { + // Player was teleported — clear ability state (same as dimension change) + trackingAbilityPosition = false; + resetOnTeleport(); + return; + } + } + + // Update tracked position for next tick's comparison + lastAbilityTickX = player.posX; + lastAbilityTickY = player.posY; + lastAbilityTickZ = player.posZ; + trackingAbilityPosition = true; + + tickCurrentAbility(); + + // Apply rotation and position locks after ability tick + applyRotationControl(player); + applyPositionLock(player); + + // Sync lock state to client (only sends when flags change) + syncAbilityStateIfNeeded(player); + } else { + trackingAbilityPosition = false; + + // Safety: release orphaned locks if no ability is executing + if (rotationLocked || positionLocked) { + releaseRotationControl(); + releaseLockedPosition(); + syncAbilityStateClear(player); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY ACTIVATION - Called on Special Key press + // ═══════════════════════════════════════════════════════════════════ + + /** + * Activate the currently selected ability. + * Called when the player presses the Special Key. + * + * @param player The player + * @return true if the ability was started + */ + public boolean activateAbility(IPlayer player) { + if (player.worldObj.isRemote) return false; + if (unlockedAbilities.isEmpty()) return false; + if (selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) return false; + + String key = unlockedAbilities.get(selectedIndex); + return activateAbility(player, key); + } + + /** + * Prefix used to identify chained ability keys in the unlocked list. + */ + public static final String CHAIN_PREFIX = "chain:"; + + /** + * Resolve an action key to an IAbilityAction. + * Handles "chain:" prefix for chained abilities, otherwise resolves from AbilityController. + */ + private IAbilityAction resolveActionKey(String key) { + if (key.startsWith(CHAIN_PREFIX)) { + return AbilityController.Instance.resolveChainedAbility(key.substring(CHAIN_PREFIX.length())); + } + return AbilityController.Instance.resolveAbility(key); + } + + /** + * Activate a specific ability by key. Keys prefixed with "chain:" are + * treated as chained abilities and resolved from {@link AbilityController}. + * + * @param player The player + * @param key The ability key (built-in or preset name), or "chain:name" for chains + * @return true if the ability was started + */ + public boolean activateAbility(IPlayer player, String key) { + if (player.worldObj.isRemote || key == null || key.isEmpty()) return false; + + // Global activation check (e.g., addon-registered restrictions) + if (!AbilityController.Instance.canPlayerActivate(player)) return false; + + // Can't activate if already executing + if (currentAbility != null && currentAbility.isExecuting()) return false; + if (isExecutingChain()) return false; + + // Resolve via unified resolver + IAbilityAction action = resolveActionKey(key); + if (action == null) return false; + + // Check user type allows player + if (!action.getAllowedBy().allowsPlayer()) return false; + + // Check conditions (skip target-requiring ones) + if (!action.checkConditionsForPlayer(player)) return false; + + // Check player requirement (visibility/availability gate) + if (!action.isAvailableFor(player)) return false; + + // Dispatch: chain vs ability + if (action.isChain()) { + ChainedAbility chain = (ChainedAbility) action; + if (!chain.isIgnoreCooldown()) { + if (chain.isPerAbilityCooldown()) { + if (isOnPerAbilityCooldown(key)) return false; + } else { + if (isOnCooldown()) return false; + } + } + + currentAbilityKey = key; + currentTarget = null; + lastAbilityActivationTime = getWorldTime(); + return startChain((ChainedAbility) action, null); + } + + // Regular ability activation + Ability ability = (Ability) action; + + // Toggle abilities cycle state instead of executing + if (ability.isToggleable()) { + toggleAbility(key); + return true; + } + + // Check cooldown (per-ability cooldowns are independent from global) + if (!ability.isIgnoreCooldown()) { + if (ability.isPerAbilityCooldown()) { + if (isOnPerAbilityCooldown(key)) return false; + } else { + if (isOnCooldown()) return false; + } + } + + // Fire extender start hook (e.g., resource cost checks) + if (!AbilityController.Instance.fireOnAbilityStart(ability, player, null)) { + return false; + } + + // Fire start event (cancelable) + if (EventHooks.onAbilityStart(ability, player, null)) { + return false; // Cancelled + } + + // Start the ability + currentAbility = ability; + currentAbilityKey = key; + currentTarget = null; // Players don't have auto-targets + lastAbilityActivationTime = getWorldTime(); + ability.start(null); + + if (ability.getPhase() == AbilityPhase.ACTIVE) { + // Windup was 0 — capture locks for immediate active phase, then execute + if (ability.isRotationLockedDuringActive()) { + captureLockedRotation(); + } + if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) { + captureLockedPosition(); + } + executeImmediate(ability, currentTarget); + } else { + // Normal windup flow + if (ability.isRotationLockedDuringWindup()) { + captureLockedRotation(); + } + if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) { + captureLockedPosition(); + } + spawnTelegraph(ability, null); + playAbilitySound(ability.getWindUpSound()); + playAbilityAnimation(ability.getWindUpAnimation()); + } + + // Immediate sync on ability use + syncToClient(); + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // CLIENT SYNC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Full sync of all ability state to the client. + * Delegates to {@link SyncController#syncAbilities(IPlayer)}. + */ + public void syncToClient() { + IPlayer player = playerData.player; + if (player instanceof IPlayer) { + SyncController.syncAbilities((IPlayer) player); + } + } + + /** + * Lightweight sync of cooldown state only to the client. + * Delegates to {@link SyncController#syncAbilityCooldowns(IPlayer)}. + */ + public void syncCooldownToClient() { + IPlayer player = playerData.player; + if (player instanceof IPlayer) { + SyncController.syncAbilityCooldowns((IPlayer) player); + } + } + + @Override + protected void onToggleStateChanged(String key, boolean active, int state) { + } + + // ═══════════════════════════════════════════════════════════════════ + // SELECTION + // ═══════════════════════════════════════════════════════════════════ + + public int getSelectedIndex() { + return selectedIndex; + } + + public void setSelectedIndex(int index) { + if (index == -1 || (index >= 0 && index < unlockedAbilities.size())) { + this.selectedIndex = index; + } + } + + public void selectNext() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex + 1) % unlockedAbilities.size(); + } + + public void selectPrevious() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size(); + } + + public String getSelectedAbilityKey() { + if (unlockedAbilities.isEmpty() || selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) { + return null; + } + return unlockedAbilities.get(selectedIndex); + } + + // ═══════════════════════════════════════════════════════════════════ + // UNLOCKED ABILITIES + // ═══════════════════════════════════════════════════════════════════ + + public List getUnlockedAbilityList() { + return unlockedAbilities; + } + + @Override + public String[] getUnlockedAbilities() { + return unlockedAbilities.toArray(new String[0]); + } + + public void setUnlockedAbilities(List abilities) { + this.unlockedAbilities = abilities != null ? new ArrayList<>(abilities) : new ArrayList<>(); + // Clamp selected index + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + } + + public void unlockAbility(String key) { + if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) { + unlockedAbilities.add(key); + } + } + + public void lockAbility(String key) { + unlockedAbilities.remove(key); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + } + + public boolean hasUnlockedAbility(String key) { + return unlockedAbilities.contains(key); + } + + // ═══════════════════════════════════════════════════════════════════ + // COOLDOWNS (Player-specific overloads) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check if on cooldown using player reference. + */ + public boolean isOnCooldown(IPlayer player) { + return player.worldObj.getTotalWorldTime() < cooldownEndTime; + } + + /** + * Check if a specific ability key is on cooldown. + * Respects per-ability cooldown if the ability has it enabled. + */ + public boolean isOnCooldown(String key, IPlayer player) { + if (AbilityController.Instance != null) { + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability != null && ability.isPerAbilityCooldown()) { + return isOnPerAbilityCooldown(key); + } + } + return isOnCooldown(player); + } + + public long getRemainingCooldown(IPlayer player) { + long remaining = cooldownEndTime - player.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; + } + + /** + * Reset cooldown for a specific ability key. + * Resets per-ability cooldown if applicable, and global cooldown. + */ + public void resetCooldown(String key) { + resetPerAbilityCooldown(key); + cooldownEndTime = 0; + } + + /** + * Reset all cooldowns (global + all per-ability). + */ + public void resetAllCooldowns() { + cooldownEndTime = 0; + resetAllPerAbilityCooldowns(); + } + + // ═══════════════════════════════════════════════════════════════════ + // IPlayerAbilityData INTERFACE + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean activateAbility() { + return activateAbility(playerData.player); + } + + @Override + public boolean activateAbility(String key) { + return activateAbility(playerData.player, key); + } + + @Override + public boolean isOnCooldown() { + IPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(player); + } + + @Override + public boolean isOnCooldown(String key) { + IPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(key, player); + } + + @Override + public void resetCooldown() { + cooldownEndTime = 0; + resetAllPerAbilityCooldowns(); + } + + // ═══════════════════════════════════════════════════════════════════ + // INTERRUPTION (convenience overload) + // ═══════════════════════════════════════════════════════════════════ + + public void interruptCurrentAbility() { + interruptCurrentAbility(null, 0); + } + + /** + * Try to cancel the current ability via double-press. + * Only succeeds if within the cancel window and not in DAZED phase. + * Also handles cancellation during chain delay (between chain entries). + * + * @return true if the ability was cancelled + */ + public boolean tryCancelAbility() { + boolean hasExecutingAbility = currentAbility != null && currentAbility.isExecuting(); + boolean isInChainDelay = currentChain != null && chainDelayRemaining > 0; + + if (!hasExecutingAbility && !isInChainDelay) return false; + + // Only allow cancel during WINDUP phase + if (hasExecutingAbility) { + AbilityPhase phase = currentAbility.getPhase(); + if (phase != AbilityPhase.WINDUP) return false; + } + + long worldTime = getWorldTime(); + if (lastAbilityActivationTime < 0 || (worldTime - lastAbilityActivationTime) > CANCEL_WINDOW_TICKS) { + // Outside cancel window — update time so a quick follow-up press can cancel + lastAbilityActivationTime = worldTime; + return false; + } + + cancelCurrentAbility(); + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // IEntity RECONSTRUCTION RESET + // ═══════════════════════════════════════════════════════════════════ + + /** + * Handle IEntity reconstruction events (dimension change, death/respawn, etc.). + * Cleans up any executing ability state without firing script events (those were + * already fired on the original IEntity), clears transient runtime state, and + * re-syncs ability data to the client on the new IEntity. + *

+ * This should be called whenever the player's IEntity object is recreated by Forge, + * since the client-side IEntity loses all synced ability state. + * + * @param clearCooldowns true to fully reset cooldowns (dimension change), + * false to preserve cooldowns (death/respawn — interrupt already rolled them) + * @param clearToggles true to clear active toggles (death/respawn), + * false to keep them (dimension change) + */ + public void onEntityReconstructed(boolean clearCooldowns, boolean clearToggles) { + // Clean up any executing ability silently (no script events — already fired on old IEntity) + if (currentAbility != null && currentAbility.isExecuting()) { + removeTelegraph(currentAbility); + currentAbility.interrupt(); + stopAbilityAnimation(); + releaseRotationControl(); + releaseLockedPosition(); + } + + // Clear chain and concurrent state + if (currentChain != null) currentChain.clearInstanceScript(); + currentChain = null; + chainEntryIndex = -1; + chainDelayRemaining = -1; + interruptConcurrentSlots(); + + // Clear all transient state + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + lastAbilityActivationTime = -1; + + if (clearCooldowns) { + cooldownEndTime = 0; + resetAllPerAbilityCooldowns(); + } + interruptCooldownRolled = false; + + if (clearToggles) { + clearActiveToggles(); + } + + // Force-send cleared lock state to client. We must always send this + // (not gated by lastSyncedFlags != 0) because the NEW IEntity's client + // may have stale lock state from before the reconstruction. + lastSyncedFlags = 0; + IPlayer player = playerData.player; + if (player instanceof IPlayer) { + PlayerAbilityStatePacket.sendToPlayer((IPlayer) player, (byte) 0, 0, 0); + } + + // Re-sync full ability data (unlocked list, selection, toggles) to client + syncToClient(); + } + + /** + * Fully reset ability state when changing dimensions. + * Clears cooldowns so the player can immediately use abilities in the new dimension. + * Keeps active toggles (dimension change shouldn't reset ongoing effects). + */ + public void resetOnDimensionChange() { + trackingAbilityPosition = false; + onEntityReconstructed(true, false); + } + + /** + * Reset ability state when a large-distance teleport is detected during execution. + * Same behavior as dimension change — silent cleanup, clear cooldowns, keep toggles. + */ + public void resetOnTeleport() { + onEntityReconstructed(true, false); + } + + /** + * Reset ability state after death/respawn. + * Preserves cooldowns (the death handler interrupted the ability which rolled cooldown + * via onInterruptComplete) but clears active toggles (death should end ongoing effects). + */ + public void resetOnRespawn() { + onEntityReconstructed(false, true); + } + + /** + * Reset ability state on login. Clears all transient execution state (current ability, + * locks, chains, cooldowns) that may have leaked from a previous session via the + * PlayerData cache. Toggles are NOT cleared — they are restored from NBT by readFromNBT. + *

+ * Does NOT send packets — called before the client's sync state is initialized. + * The caller must trigger a full ability sync afterwards (e.g. via syncToClient). + */ + public void resetOnLogin() { + trackingAbilityPosition = false; + + // Silently clean up any stale executing ability + if (currentAbility != null && currentAbility.isExecuting()) { + removeTelegraph(currentAbility); + currentAbility.interrupt(); + stopAbilityAnimation(); + releaseRotationControl(); + releaseLockedPosition(); + } + + // Clear chain and concurrent state + if (currentChain != null) currentChain.clearInstanceScript(); + currentChain = null; + chainEntryIndex = -1; + chainDelayRemaining = -1; + interruptConcurrentSlots(); + + // Clear all transient state + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + lastAbilityActivationTime = -1; + + // Clear cooldowns (fresh start after relog) + cooldownEndTime = 0; + resetAllPerAbilityCooldowns(); + interruptCooldownRolled = false; + + // Reset sync tracking so next sync sends fresh state + lastSyncedFlags = 0; + } + + /** + * Clean up ability state on disconnect. Interrupts any executing ability (fires events, + * rolls cooldown) before the player IEntity goes away, then clears all transient state. + * This ensures scripts receive proper interrupt/complete events and prevents stale state + * from leaking into the next session via the PlayerData cache. + */ + public void resetOnDisconnect() { + // Interrupt with events so scripts know the ability ended + if (currentAbility != null && currentAbility.isExecuting()) { + interruptCurrentAbility(null, 0); + } + // Clear any remaining transient state (cooldowns, chains, locks) + // Don't clear toggles — they were already saved to NBT by this point + onEntityReconstructed(true, false); + } + + // ═══════════════════════════════════════════════════════════════════ + // DAMAGE HANDLING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Handle damage taken while an ability is executing. + * Called from LivingHurtEvent BEFORE damage is applied to health. + * Triggers Guard counter detection and handles ability interruption. + * + * @param source The damage source + * @param amount The damage amount (post-armor) + * @return The modified damage amount (reduced by guard, or 0 if counter absorbs) + */ + public float onDamage(IDamageSource source, float amount) { + if (currentAbility == null || !currentAbility.isExecuting()) { + return amount; + } + if (currentAbility.isInvulnerableForCurrentPhase()) { + return 0; + } + + if (currentAbility != null && currentAbility.canInterrupt(source)) { + interruptCurrentAbility(source, amount); + } + + return amount; + } + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES (Player-specific) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check if the player's movement should be locked due to an executing ability. + * Used by the tick handler to restrict player movement during ability phases. + */ + public boolean isMovementLocked() { + return currentAbility != null && currentAbility.isExecuting() + && currentAbility.isMovementLockedForCurrentPhase(); + } + + /** + * Returns true if an ability-driven animation is currently playing. + * Used by PlayerData.onLogin() to detect orphaned ability animations. + */ + public boolean isPlayingAbilityAnimation() { + return playingAbilityAnimation; + } + + /** + * Clears an orphaned ability animation (e.g. after relog when the ability is gone). + * Stops the animation and resets the tracking flag. + */ + public void clearOrphanedAbilityAnimation() { + stopAbilityAnimation(); + } + + // ═══════════════════════════════════════════════════════════════════ + // ROTATION CONTROL (Player-specific application) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Apply rotation control on the server side. + * Enforces locked rotation values on the player IEntity. + */ + private void applyRotationControl(IPlayer player) { + if (!rotationLocked || currentAbility == null) return; + + if (currentAbility.isRotationLockedForCurrentPhase()) { + player.rotationYaw = lockedYaw; + player.rotationPitch = lockedPitch; + player.prevRotationYaw = lockedYaw; + player.prevRotationPitch = lockedPitch; + if (player instanceof IPlayer) { + player.rotationYawHead = lockedYaw; + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // POSITION LOCKING (Player-specific application) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Apply position lock on the server side. + * Snaps player back to locked position and zeroes motion. + */ + private void applyPositionLock(IPlayer player) { + if (!positionLocked) return; + + player.setPosition(lockedPosX, lockedPosY, lockedPosZ); + player.prevPosX = lockedPosX; + player.prevPosY = lockedPosY; + player.prevPosZ = lockedPosZ; + player.motionX = 0; + player.motionY = 0; + player.motionZ = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY STATE SYNC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Build the current ability state flags byte. + */ + private byte getAbilityStateFlags() { + byte flags = 0; + if (currentAbility == null || !currentAbility.isExecuting()) return flags; + + if (currentAbility.isMovementLockedForCurrentPhase()) { + flags |= PlayerAbilityStatePacket.FLAG_MOVEMENT_LOCKED; + } + if (rotationLocked && currentAbility.isRotationLockedForCurrentPhase()) { + flags |= PlayerAbilityStatePacket.FLAG_ROTATION_LOCKED; + } + if (currentAbility.hasAbilityMovement() + && (currentAbility.getPhase() == AbilityPhase.ACTIVE || currentAbility.getPhase() == AbilityPhase.WINDUP)) { + flags |= PlayerAbilityStatePacket.FLAG_HAS_ABILITY_MOVEMENT; + } + if (positionLocked) { + flags |= PlayerAbilityStatePacket.FLAG_POSITION_LOCKED; + } + if (wasFlyingAtLock) { + flags |= PlayerAbilityStatePacket.FLAG_WAS_FLYING_AT_LOCK; + } + if (currentAbility.getPhase() == AbilityPhase.ACTIVE) { + flags |= PlayerAbilityStatePacket.FLAG_ACTIVE_PHASE; + } + return flags; + } + + /** + * Send ability state to client if flags changed since last sync. + */ + private void syncAbilityStateIfNeeded(IPlayer player) { + if (!(player instanceof IPlayer)) return; + byte flags = getAbilityStateFlags(); + if (flags != lastSyncedFlags) { + lastSyncedFlags = flags; + PlayerAbilityStatePacket.sendToPlayer((IPlayer) player, flags, lockedYaw, lockedPitch); + } + } + + /** + * Send cleared (all-zero) state to client. Called on ability completion/interrupt. + */ + private void syncAbilityStateClear(IPlayer player) { + if (!(player instanceof IPlayer)) return; + if (lastSyncedFlags != 0) { + lastSyncedFlags = 0; + PlayerAbilityStatePacket.sendToPlayer((IPlayer) player, (byte) 0, 0, 0); + } + } + + /** + * Periodic safety check: if no ability is executing but the client may have + * stale lock flags, send a clear. Prevents stuck clients from missed packets. + */ + public void ensureLockStateClear() { + if (isExecutingAbility() || isExecutingChain()) return; + IPlayer player = playerData.player; + if (!(player instanceof IPlayer)) return; + if (lastSyncedFlags != 0) { + lastSyncedFlags = 0; + PlayerAbilityStatePacket.sendToPlayer((IPlayer) player, (byte) 0, 0, 0); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + public void writeToNBT(INbt compound) { + INbtList list = new INbtList(); + for (String key : unlockedAbilities) { + list.appendTag(new NBTTagString(key)); + } + compound.setTag("PlayerAbilities", list); + compound.setInteger("PlayerAbilitySelected", selectedIndex); + compound.setBoolean("AbilityAnimating", playingAbilityAnimation); + + // Active toggles (compound format with state) + INbtList toggleList = new INbtList(); + for (Map.Entry entry : activeToggles.entrySet()) { + INbt toggleNbt = new INbt(); + toggleNbt.setString("Key", entry.getKey()); + toggleNbt.setInteger("State", entry.getValue().getState()); + toggleList.appendTag(toggleNbt); + } + compound.setTag("ActiveToggles", toggleList); + } + + public void readFromNBT(INbt compound) { + unlockedAbilities.clear(); + if (compound.hasKey("PlayerAbilities")) { + INbtList list = compound.getTagList("PlayerAbilities", 8); // 8 = string tag + for (int i = 0; i < list.tagCount(); i++) { + String key = list.getStringTagAt(i); + if (key != null && !key.isEmpty()) { + unlockedAbilities.add(key); + } + } + } + selectedIndex = compound.getInteger("PlayerAbilitySelected"); + + // Prune abilities that no longer exist (deleted custom abilities or removed built-ins) + validateUnlockedAbilities(); + + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + playingAbilityAnimation = compound.getBoolean("AbilityAnimating"); + + // Active toggles - restore state directly (no onToggle callback during load) + activeToggles.clear(); + if (compound.hasKey("ActiveToggles")) { + INbtList toggleNbt = compound.getTagList("ActiveToggles", 10); // 10 = TAG_COMPOUND + for (int i = 0; i < toggleNbt.tagCount(); i++) { + INbt entry = toggleNbt.getCompoundTagAt(i); + String key = entry.getString("Key"); + int state = entry.hasKey("State") ? entry.getInteger("State") : 1; + setToggleEntryDirect(key, state); + } + } + } + + /** + * Remove any unlocked ability keys that can no longer be resolved or + * are no longer allowed for players (e.g. NPC-only abilities). + * Called during load to clean up references to deleted/restricted abilities. + * Supports both regular ability keys and "chain:" prefixed chained ability keys. + */ + private void validateUnlockedAbilities() { + if (AbilityController.Instance == null) return; + if (playerData == null || !(playerData.player instanceof IPlayer)) return; + + Iterator it = unlockedAbilities.iterator(); + while (it.hasNext()) { + String key = it.next(); + IAbilityAction resolved = resolveActionKey(key); + if (resolved == null) { + it.remove(); + LogWriter.info("Removed invalid ability reference from player data: " + key); + } else if (!resolved.getAllowedBy().allowsPlayer()) { + it.remove(); + LogWriter.info("Removed non-player ability from player data: " + key); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java new file mode 100644 index 000000000..9cda159c7 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java @@ -0,0 +1,93 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +public class PlayerAbilityHotbarData { + public AbilityHotbarData[] slots = new AbilityHotbarData[AbilityHotbarData.TOTAL_SLOTS]; + + public PlayerAbilityHotbarData() { + for (int i = 0; i < slots.length; i++) { + slots[i] = new AbilityHotbarData(i); + } + } + + public AbilityHotbarData getSlot(int index) { + if (index < 0 || index >= slots.length) return null; + return slots[index]; + } + + public void setSlot(int index, String abilityKey) { + if (index < 0 || index >= slots.length) return; + slots[index].abilityKey = abilityKey != null ? abilityKey : ""; + } + + public void clearSlot(int index) { + if (index < 0 || index >= slots.length) return; + slots[index].reset(); + } + + public boolean hasAnyAbilities() { + for (AbilityHotbarData slot : slots) { + if (!slot.isEmpty()) return true; + } + return false; + } + + public void writeToNBT(INbt compound) { + for (int i = 0; i < slots.length; i++) { + slots[i].writeToNBT(new NBTWrapper(compound)); + } + } + + public void readFromNBT(INbt compound) { + for (int i = 0; i < slots.length; i++) { + INbt slotTag = compound.getCompoundTag("AbilityHotbar" + i); + slots[i].readFromNBT(new NBTWrapper(slotTag)); + } + validateSlots(); + } + + public void validateSlots() { + if (AbilityController.Instance == null) return; + // Only validate on the server - the client trusts server-sent data. + if (FMLCommonHandler.instance().getEffectiveSide().isClient()) return; + for (AbilityHotbarData slot : slots) { + if (slot.isEmpty()) continue; + boolean valid; + if (slot.isChainKey()) { + valid = AbilityController.Instance.canResolveChainedAbility(slot.getResolveKey()); + } else { + valid = AbilityController.Instance.canResolveAbility(slot.abilityKey); + // Also check that resolved ability allows player usage + if (valid) { + Ability ability = AbilityController.Instance.resolveAbility(slot.abilityKey); + if (ability != null && !ability.getAllowedBy().allowsPlayer()) { + valid = false; + } + } + } + if (!valid) { + slot.reset(); + } + } + } + + /** + * Sync hotbar data only to the client. + * Used after server confirms a client hotbar edit. + */ + public void syncToClient(IPlayer player) { + if (player instanceof IPlayer) { + AbilityHotbarSyncPacket.sendToPlayer((IPlayer) player); + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerBankData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerBankData.java new file mode 100644 index 000000000..22bddd4d1 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerBankData.java @@ -0,0 +1,74 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.IPlayerBankData; +import noppes.npcs.controllers.BankController; + +import java.util.HashMap; + +public class PlayerBankData implements IPlayerBankData { + private final PlayerData parent; + public HashMap banks; + + public PlayerBankData(PlayerData parent) { + banks = new HashMap(); + this.parent = parent; + } + + public void loadNBTData(INbt compound) { + HashMap banks = new HashMap(); + INbtList list = compound.getTagList("BankData", 10); + if (list == null) { + return; + } + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + BankData data = new BankData(); + data.readNBT(INbt); + banks.put(data.bankId, data); + } + this.banks = banks; + } + + public void saveNBTData(INbt playerData) { + INbtList list = new INbtList(); + for (BankData data : banks.values()) { + INbt INbt = new INbt(); + data.writeNBT(INbt); + list.appendTag(INbt); + } + playerData.setTag("BankData", list); + } + + + public BankData getBank(int bankId) { + return banks.get(bankId); + } + + public BankData getBankOrDefault(int bankId) { + BankData data = banks.get(bankId); + if (data != null) + return data; + Bank bank = BankController.getInstance().getBank(bankId); + return banks.get(bank.id); + } + + public boolean hasBank(int bank) { + return banks.containsKey(bank); + } + + public void loadNew(int bank) { + BankData data = new BankData(); + data.bankId = bank; + banks.put(bank, data); + + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerData.java new file mode 100644 index 000000000..8dde1f048 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -0,0 +1,506 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.data.profile.Profile; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.api.entity.ICustomNpc; +import noppes.npcs.api.handler.IPlayerBankData; +import noppes.npcs.api.handler.IPlayerData; +import noppes.npcs.api.handler.IPlayerDialogData; +import noppes.npcs.api.handler.IPlayerFactionData; +import noppes.npcs.api.handler.IPlayerItemGiverData; +import noppes.npcs.api.handler.IPlayerMailData; +import noppes.npcs.api.handler.IPlayerQuestData; +import noppes.npcs.api.handler.IPlayerTradeData; +import noppes.npcs.api.handler.IPlayerTransportData; +import noppes.npcs.config.ConfigMain; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.action.ActionManager; +import noppes.npcs.roles.RoleCompanion; +import noppes.npcs.util.CustomNPCsThreader; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.HashSet; +import java.util.UUID; + +public class PlayerData implements IExtendedEntityProperties, IPlayerData { + public PlayerDialogData dialogData = new PlayerDialogData(); + public PlayerBankData bankData = new PlayerBankData(this); + public PlayerQuestData questData = new PlayerQuestData(this); + public PlayerTransportData transportData = new PlayerTransportData(); + public PlayerFactionData factionData = new PlayerFactionData(this); + public PlayerItemGiverData itemgiverData = new PlayerItemGiverData(this); + public PlayerMailData mailData = new PlayerMailData(this); + public AnimationData animationData = new AnimationData(this); + public PlayerEffectData effectData = new PlayerEffectData(); + public DataTimers timers = new DataTimers(this); + public DataSkinOverlays skinOverlays = new DataSkinOverlays(this); + public MagicData magicData = new MagicData(); + + // Trade data (currency + auction claims) - shared across all profile slots + public PlayerTradeData tradeData = new PlayerTradeData(this); + + public PlayerAbilityData abilityData = new PlayerAbilityData(this); + public PlayerAbilityHotbarData hotbarData = new PlayerAbilityHotbarData(); + public ActionManager actionManager = new ActionManager(); + public PlayerDataScript scriptData; + + public EntityNPCInterface editingNpc; + public INbt cloned; + + public UUID partyUUID = null; + private final HashSet partyInvites = new HashSet<>(); + + public IPlayer player; + + public String playername = ""; + public String uuid = ""; + + private EntityNPCInterface activeCompanion = null; + public int companionID = 0; + + public boolean isGUIOpen = false; + public boolean hadInteract = true; + + public boolean updateClient = false; + + public ScreenSize screenSize = new ScreenSize(-1, -1); + + public int profileSlot = 0; + private boolean specialKeyDown = false; + + public void onLogin() { + // Clear any stale transient ability state that leaked via the PlayerData cache. + // Must happen before the animation check since it may clear ability animation state. + abilityData.resetOnLogin(); + + // Handle animation state from previous session + AnimationData animationData = this.animationData; + if (animationData != null && animationData.isClientAnimating()) { + if (abilityData.isPlayingAbilityAnimation()) { + // Ability animation was playing when player disconnected. + // Ability state is transient (lost on disconnect), so this animation + // is orphaned - clear it instead of replaying a stuck animation. + abilityData.clearOrphanedAbilityAnimation(); + } else { + // Non-ability animation (e.g. script-driven) - continue playing + Animation currentAnimation = animationData.currentClientAnimation; + INbt compound = currentAnimation.writeToNBT(); + animationData.viewAnimation(currentAnimation, animationData, compound, + animationData.isClientAnimating(), currentAnimation.currentFrame, currentAnimation.currentFrameTime); + } + } + + CustomEffectController controller = CustomEffectController.getInstance(); + UUID playerID = player.getPersistentID(); + // Only add if there are saved effects and none are registered yet. + if (!controller.playerEffects.containsKey(playerID)) { + controller.playerEffects.put(playerID, effectData.getEffects()); + } + } + + public void onLogout() { + // Interrupt executing ability (fires events, rolls cooldown), then clear + // all transient state so nothing leaks via the PlayerData cache. + abilityData.resetOnDisconnect(); + this.partyInvites.clear(); + this.actionManager.clear(); + } + + @Override + public void saveNBTData(INbt INbt) { + } + + @Override + public void loadNBTData(INbt compound) { + } + + public void setNBT(INbt data) { + dialogData.loadNBTData(new NBTWrapper(data)); + bankData.loadNBTData(data); + questData.loadNBTData(data); + transportData.loadNBTData(new NBTWrapper(data)); + factionData.loadNBTData(data); + itemgiverData.loadNBTData(data); + mailData.loadNBTData(data); + timers.readFromNBT(data); + skinOverlays.readFromNBT(data); + animationData.readFromNBT(data); + effectData.readFromNBT(new NBTWrapper(data)); + magicData.readToNBT(new NBTWrapper(data)); + tradeData.readFromNBT(data); + abilityData.readFromNBT(data); + hotbarData.readFromNBT(data); + + if (player != null) { + playername = player.getCommandSenderName(); + uuid = player.getPersistentID().toString(); + } else { + playername = data.getString("PlayerName"); + uuid = data.getString("UUID"); + } + companionID = data.getInteger("PlayerCompanionId"); + profileSlot = data.getInteger("ProfileSlot"); + if (data.hasKey("PlayerCompanion") && !hasCompanion()) { + EntityCustomNpc npc = new EntityCustomNpc(player.worldObj); + npc.readEntityFromNBT(data.getCompoundTag("PlayerCompanion")); + npc.setPosition(player.posX, player.posY, player.posZ); + if (npc.advanced.role == EnumRoleType.Companion) { + setCompanion(npc); + ((RoleCompanion) npc.roleInterface).setSitting(false); + player.worldObj.spawnEntityInWorld(npc); + } + } + isGUIOpen = data.getBoolean("isGUIOpen"); + DBCAddon.instance.readFromNBT(this, data); + } + + public INbt getNBT() { + if (player != null) { + playername = player.getCommandSenderName(); + uuid = player.getPersistentID().toString(); + } + INbt compound = new INbt(); + dialogData.saveNBTData(new NBTWrapper(compound)); + bankData.saveNBTData(compound); + questData.saveNBTData(compound); + transportData.saveNBTData(new NBTWrapper(compound)); + factionData.saveNBTData(compound); + itemgiverData.saveNBTData(compound); + mailData.saveNBTData(compound); + timers.writeToNBT(compound); + skinOverlays.writeToNBT(compound); + animationData.writeToNBT(compound); + effectData.writeToNBT(new NBTWrapper(compound)); + magicData.writeToNBT(new NBTWrapper(compound)); + tradeData.writeToNBT(compound); + abilityData.writeToNBT(compound); + hotbarData.writeToNBT(compound); + + compound.setString("PlayerName", playername); + compound.setString("UUID", uuid); + compound.setInteger("PlayerCompanionId", companionID); + compound.setBoolean("isGUIOpen", isGUIOpen); + compound.setInteger("ProfileSlot", profileSlot); + + if (hasCompanion()) { + INbt nbt = new INbt(); + if (activeCompanion.writeToNBTOptional(nbt)) + compound.setTag("PlayerCompanion", nbt); + } + DBCAddon.instance.writeToNBT(this, compound); + return compound; + } + + public INbt getSyncNBT() { + INbt compound = new INbt(); + dialogData.saveNBTData(new NBTWrapper(compound)); + questData.saveNBTData(compound); + factionData.saveNBTData(compound); + return compound; + } + + public void setSyncNBT(INbt data) { + dialogData.loadNBTData(new NBTWrapper(data)); + questData.loadNBTData(data); + factionData.loadNBTData(data); + } + + public INbt getPlayerEffects() { + INbt compound = new INbt(); + effectData.writeToNBT(new NBTWrapper(compound)); + return compound; + } + + public void setPlayerEffects(INbt data) { + effectData.readFromNBT(new NBTWrapper(data)); + } + + public INbt getSyncNBTFull() { + if (player != null) { + playername = player.getCommandSenderName(); + uuid = player.getPersistentID().toString(); + } + INbt compound = new INbt(); + dialogData.saveNBTData(new NBTWrapper(compound)); + bankData.saveNBTData(compound); + questData.saveNBTData(compound); + transportData.saveNBTData(new NBTWrapper(compound)); + factionData.saveNBTData(compound); + mailData.saveNBTData(compound); + tradeData.writeToNBT(compound); + abilityData.writeToNBT(compound); + hotbarData.writeToNBT(compound); + compound.setString("PlayerName", playername); + compound.setString("UUID", uuid); + DBCAddon.instance.writeToNBT(this, compound); + return compound; + } + + public void setSyncNBTFull(INbt data) { + dialogData.loadNBTData(new NBTWrapper(data)); + bankData.loadNBTData(data); + questData.loadNBTData(data); + transportData.loadNBTData(new NBTWrapper(data)); + factionData.loadNBTData(data); + mailData.loadNBTData(data); + tradeData.readFromNBT(data); + abilityData.readFromNBT(data); + hotbarData.readFromNBT(data); + if (player != null) { + playername = player.getCommandSenderName(); + uuid = player.getPersistentID().toString(); + } else { + playername = data.getString("PlayerName"); + uuid = data.getString("UUID"); + } + DBCAddon.instance.readFromNBT(this, data); + } + + public void getDBCSync(INbt compound) { + DBCAddon.instance.writeToNBT(this, compound); + } + + public void setDBCSync(INbt data) { + DBCAddon.instance.readFromNBT(this, data); + } + + @Override + public void init(IEntity IEntity, IWorld IWorld) { + + } + + public void setGUIOpen(boolean bool) { + this.isGUIOpen = bool; + } + + public boolean getGUIOpen() { + return this.isGUIOpen; + } + + public boolean isSpecialKeyDown() { + return specialKeyDown; + } + + public void setSpecialKeyDown(boolean specialKeyDown) { + this.specialKeyDown = specialKeyDown; + } + + public ScreenSize getScreenSize() { + return screenSize; + } + + public void setScreenSize(ScreenSize size) { + screenSize = size; + } + + public boolean hasCompanion() { + return activeCompanion != null && !activeCompanion.isDead; + } + + public void setCompanion(EntityNPCInterface npc) { + if (npc != null && npc.advanced.role != EnumRoleType.Companion)//shouldnt happen + return; + companionID++; + activeCompanion = npc; + if (npc != null) + ((RoleCompanion) npc.roleInterface).companionID = companionID; + save(); + } + + public void updateCompanion(IWorld IWorld) { + if (!hasCompanion() || IWorld == activeCompanion.worldObj) + return; + RoleCompanion role = (RoleCompanion) activeCompanion.roleInterface; + role.owner = player; + if (!role.isFollowing()) + return; + INbt nbt = new INbt(); + activeCompanion.writeToNBTOptional(nbt); + activeCompanion.isDead = true; + + EntityCustomNpc npc = new EntityCustomNpc(IWorld); + npc.readEntityFromNBT(nbt); + npc.setPosition(player.posX, player.posY, player.posZ); + setCompanion(npc); + ((RoleCompanion) npc.roleInterface).setSitting(false); + IWorld.spawnEntityInWorld(npc); + } + + public void inviteToParty(Party party) { + if (party != null && this.partyUUID == null && !this.partyInvites.contains(party.getPartyUUID())) { + this.partyInvites.add(party.getPartyUUID()); + + AchievementPacket.sendAchievement((IPlayer) player, true, "party.inviteAlert", party.getPartyLeader().getCommandSenderName()); + ChatAlertPacket.sendChatAlert((IPlayer) player, "\u00A7a", "party.inviteChat", " ", party.getPartyLeader().getCommandSenderName(), "!"); + } + } + + public void ignoreInvite(UUID uuid) { + if (uuid != null) { + this.partyInvites.remove(uuid); + PartyInvitePacket.sendInviteData((IPlayer) player); + } + } + + public void acceptInvite(UUID uuid) { + if (uuid != null) { + this.partyInvites.remove(uuid); + Party party = PartyController.Instance().getParty(uuid); + if (party != null) { + if (!party.getIsLocked()) { + party.addPlayer(player); + PartyController.Instance().pingPartyUpdate(party); + } + } + } + } + + public Party getPlayerParty() { + if (partyUUID != null) { + return PartyController.Instance().getParty(partyUUID); + } + return null; + } + + public HashSet getPartyInvites() { + return (HashSet) this.partyInvites.clone(); + } + + public void setCompanion(ICustomNpc npc) { + this.setCompanion((EntityNPCInterface) npc.getMCEntity()); + } + + public ICustomNpc getCompanion() { + return (ICustomNpc) NpcAPI.Instance().getIEntity(activeCompanion); + } + + public int getCompanionID() { + return companionID; + } + + public IPlayerDialogData getDialogData() { + return dialogData; + } + + public IPlayerBankData getBankData() { + return bankData; + } + + public IPlayerQuestData getQuestData() { + return questData; + } + + public IPlayerTransportData getTransportData() { + return transportData; + } + + public IPlayerFactionData getFactionData() { + return factionData; + } + + public IPlayerItemGiverData getItemGiverData() { + return itemgiverData; + } + + public IPlayerMailData getMailData() { + return mailData; + } + + public IPlayerTradeData getTradeData() { + return tradeData; + } + + public IPlayerAbilityData getAbilityData() { + return abilityData; + } + + public synchronized void save() { + // Don't Save this is a Modification of a Profile's PlayerData + Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid)); + if (profile != null && profile.currentSlotId != this.profileSlot) { + ProfileController.Instance.saveOffline(profile, UUID.fromString(uuid)); + return; + } + + final INbt compound = getNBT(); + final String filename; + if (ConfigMain.DatFormat) { + filename = uuid + ".dat"; + } else { + filename = uuid + ".json"; + } + PlayerDataController.Instance.putPlayerMap(playername, uuid); + PlayerDataController.Instance.putPlayerDataCache(uuid, this); + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + File saveDir = PlayerDataController.Instance.getSaveDir(); + File file = new File(saveDir, filename + "_new"); + File file1 = new File(saveDir, filename); + if (ConfigMain.DatFormat) { + NBTIO.writeCompressed(compound, new FileOutputStream(file)); + } else { + NBTJsonUtil.SaveFile(file, compound); + } + if (file1.exists()) { + file1.delete(); + } + file.renameTo(file1); + } catch (Exception e) { + LogWriter.except(e); + } + }); + } + + public void load() { + INbt data = PlayerDataController.Instance.loadPlayerData(player.getPersistentID().toString()); + if (data.hasNoTags()) { + data = PlayerDataController.Instance.loadPlayerDataOld(player.getCommandSenderName()); + } + if (data.hasNoTags()) { + data = getNBT(); + } + setNBT(data); + } + + public static PlayerData get(IPlayer player) { + if (player.worldObj.isRemote) + return CustomNpcs.proxy.getPlayerData(player); + + return PlayerDataController.Instance.getPlayerData(player); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java new file mode 100644 index 000000000..3424f34c7 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -0,0 +1,213 @@ +package noppes.npcs.controllers.data; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.CustomNpcs; +import noppes.npcs.EventHooks; +import noppes.npcs.NBTTags; +import noppes.npcs.util.ScriptToStringHelper; + +public class PlayerDataScript extends MultiScriptHandler { + private IPlayer player; + private IPlayer playerAPI; + private long lastPlayerUpdate = 0L; + + // Static console/errored for IndividualPlayerScripts mode + private static Map staticConsole = new TreeMap<>(); + private static List errored = new ArrayList<>(); + + public PlayerDataScript(IPlayer player) { + if (player != null) { + this.player = player; + } + } + + public void clear() { + super.clear(); + staticConsole = new TreeMap<>(); + errored = new ArrayList<>(); + } + + @Override + public void readFromNBT(INbt compound) { + super.readFromNBT(compound); + staticConsole = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); + } + + @Override + public INbt writeToNBT(INbt compound) { + super.writeToNBT(compound); + compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(staticConsole)); + return compound; + } + + public boolean isEnabled() { + return CustomNpcs.proxy.isGlobalPlayerScripts() && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); + } + + @Override + protected boolean canRunScripts() { + return isEnabled(); + } + + @Override + public ScriptContext getContext() { + return ScriptContext.PLAYER; + } + + @Override + public void requestData() { + PlayerScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + PlayerScriptPacket.Save(index, totalCount, nbt); + } + + @Override + public void callScript(String hookName, Event event) { + if (!isEnabled()) + return; + + if (ConfigScript.IndividualPlayerScripts) { + if (ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastPlayerUpdate > lastPlayerUpdate) { + lastInited = ScriptController.Instance.lastLoaded; + errored.clear(); + if (player != null) { + scripts.clear(); + for (IScriptUnit script : ScriptController.Instance.playerScripts.scripts) { + // Clone the script unit by writing to NBT and reading back with factory + INbt nbt = script.writeToNBT(new INbt()); + IScriptUnit cloned = IScriptUnit.createFromNBT(nbt, this); + scripts.add(cloned); + } + } + lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; + if (!Objects.equals(hookName, EnumScriptType.INIT.function) && event instanceof PlayerEvent) { + PlayerEvent playerEvent = (PlayerEvent) event; + EventHooks.onPlayerInit(this, playerEvent.player); + } + } + for (int i = 0; i < scripts.size(); i++) { + IScriptUnit script = scripts.get(i); + if (errored.contains(i)) + continue; + script.run(hookName, event); + if (script.hasErrored()) { + errored.add(i); + } + for (Entry entry : script.getConsole().entrySet()) { + if (!staticConsole.containsKey(entry.getKey())) + staticConsole.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + } + script.clearConsole(); + } + } else { + if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastPlayerUpdate > this.lastPlayerUpdate) { + this.lastInited = ScriptController.Instance.lastLoaded; + this.lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; + + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; + } + + if (!Objects.equals(hookName, EnumScriptType.INIT.function) && event instanceof PlayerEvent) { + PlayerEvent playerEvent = (PlayerEvent) event; + EventHooks.onPlayerInit(this, playerEvent.player); + } + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(hookName, event); + } + } + } + + @Override + public boolean isClient() { + return this.player != null && this.player.isClientWorld(); + } + + @Override + public boolean getEnabled() { + return ScriptController.Instance.playerScripts.enabled; + } + + @Override + public void setEnabled(boolean bo) { + ScriptController.Instance.playerScripts.enabled = bo; + this.enabled = bo; + } + + @Override + public String getLanguage() { + return ScriptController.Instance.playerScripts.scriptLanguage; + } + + @Override + public void setLanguage(String lang) { + ScriptController.Instance.playerScripts.scriptLanguage = lang; + this.scriptLanguage = lang; + } + + @Override + public String noticeString() { + if (this.player == null) { + return "Global script"; + } + BlockPos pos = new BlockPos(this.player); + return ScriptToStringHelper.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); + } + + public IPlayer getPlayer() { + if (this.playerAPI == null) { + this.playerAPI = (IPlayer) NpcAPI.Instance().getIEntity(this.player); + } + return this.playerAPI; + } + + @Override + public Map getConsoleText() { + if (ConfigScript.IndividualPlayerScripts) { + return staticConsole; + } + return super.getConsoleText(); + } + + @Override + public void clearConsole() { + if (ConfigScript.IndividualPlayerScripts) { + staticConsole.clear(); + return; + } + super.clearConsole(); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.PLAYER); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerDialogData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerDialogData.java index 46fd5720f..63573e3fe 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/PlayerDialogData.java +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerDialogData.java @@ -1,11 +1,18 @@ package noppes.npcs.controllers.data; -import noppes.npcs.api.handler.IPlayerDialogData; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; -import noppes.npcs.core.NBT; import java.util.HashSet; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.IPlayerDialogData; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.core.NBT; public class PlayerDialogData implements IPlayerDialogData { public HashSet dialogsRead = new HashSet(); @@ -23,8 +30,8 @@ public class PlayerDialogData implements IPlayerDialogData { } for (int i = 0; i < list.size(); i++) { - INbt nbttagcompound = list.getCompound(i); - dialogsRead.add(nbttagcompound.getInteger("Dialog")); + INbt INbt = list.getCompound(i); + dialogsRead.add(INbt.getInteger("Dialog")); } this.dialogsRead = dialogsRead; } @@ -32,9 +39,9 @@ public class PlayerDialogData implements IPlayerDialogData { public void saveNBTData(INbt compound) { INbtList list = NBT.list(); for (int dia : dialogsRead) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Dialog", dia); - list.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Dialog", dia); + list.addCompound(INbt); } compound.setTagList("DialogData", list); diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerEffect.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerEffect.java index 900a5737b..f35e2e8ed 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/PlayerEffect.java +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerEffect.java @@ -1,7 +1,16 @@ package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.handler.data.IPlayerEffect; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; public class PlayerEffect implements IPlayerEffect { public int id; @@ -59,7 +68,7 @@ public class PlayerEffect implements IPlayerEffect { /** * Applies this effect's tick logic to the given player. * This method is not part of the core IPlayerEffect interface because - * it depends on IPlayer which has MC entity dependencies. + * it depends on IPlayer which has MC IEntity dependencies. * Override in mc1710 to restore original behavior. */ public void performEffect(IPlayer player) { @@ -68,10 +77,10 @@ public class PlayerEffect implements IPlayerEffect { // TODO: Requires CustomEffectController + IPlayer - implement in mc1710 override // OLD CODE: - // if (player != null && player.getMCEntity() != null && player.getMCEntity() instanceof EntityPlayer) { + // if (player != null && player.getMCIEntity() != null && player.getMCIEntity() instanceof IPlayer) { // CustomEffect effect = CustomEffectController.getInstance().get(this.id, this.index); // if (effect != null) - // effect.onTick((EntityPlayer) player.getMCEntity(), this); + // effect.onTick((IPlayer) player.getMCIEntity(), this); // } } diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerEvent.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerEvent.java new file mode 100644 index 000000000..d7c7a81bf --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerEvent.java @@ -0,0 +1,15 @@ +package noppes.npcs.controllers.data; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Will be replaced when this class is properly migrated. + * DO NOT add logic here. + */ +public class PlayerEvent { + public static class EffectEvent { + public enum ExpirationType { EXPIRED, REMOVED, REPLACED } + public static class Removed extends Event { + public Removed(Object player, Object effect, ExpirationType type) { super(); } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerFactionData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerFactionData.java new file mode 100644 index 000000000..8d818529b --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerFactionData.java @@ -0,0 +1,110 @@ +package noppes.npcs.controllers.data; + + +import java.util.HashMap; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.IPlayerFactionData; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.controllers.FactionController; +import noppes.npcs.EventHooks; + +public class PlayerFactionData implements IPlayerFactionData { + private final PlayerData parent; + public HashMap factionData = new HashMap(); + + public PlayerFactionData() { + parent = null; + } + + public PlayerFactionData(PlayerData parent) { + this.parent = parent; + } + + public void loadNBTData(INbt compound) { + HashMap factionData = new HashMap(); + if (compound == null) + return; + INbtList list = compound.getTagList("FactionData", 10); + if (list == null) { + return; + } + + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + factionData.put(INbt.getInteger("Faction"), INbt.getInteger("Points")); + } + this.factionData = factionData; + } + + public void saveNBTData(INbt compound) { + INbtList list = new INbtList(); + for (int faction : factionData.keySet()) { + INbt INbt = new INbt(); + INbt.setInteger("Faction", faction); + INbt.setInteger("Points", factionData.get(faction)); + list.appendTag(INbt); + } + + compound.setTag("FactionData", list); + } + + public int getFactionPoints(int id) { + if (!factionData.containsKey(id)) { + Faction faction = FactionController.getInstance().get(id); + factionData.put(id, faction == null ? -1 : faction.defaultPoints); + } + return factionData.get(id); + } + + public void increasePoints(int factionId, int points, IPlayer player) { + if (EventHooks.onFactionPoints(player, new FactionEvent.FactionPoints((IPlayer) NpcAPI.Instance().getIEntity((IPlayer) player), FactionController.getInstance().get(factionId), points < 0, points))) + return; + + if (!factionData.containsKey(factionId)) { + Faction faction = FactionController.getInstance().get(factionId); + factionData.put(factionId, faction == null ? -1 : faction.defaultPoints); + } + factionData.put(factionId, factionData.get(factionId) + points); + } + + public INbt getPlayerGuiData() { + INbt compound = new INbt(); + saveNBTData(compound); + + INbtList list = new INbtList(); + for (int id : factionData.keySet()) { + Faction faction = FactionController.getInstance().get(id); + if (faction == null || faction.hideFaction) + continue; + INbt com = new INbt(); + faction.writeNBT(com); + list.appendTag(com); + } + compound.setTag("FactionList", list); + + return compound; + } + + public int getPoints(int id) { + return this.getFactionPoints(id); + } + + public void addPoints(int id, int points) { + if (parent != null) { + this.increasePoints(id, points, parent.player); + } + } + + public void setPoints(int id, int points) { + if (parent != null) { + increasePoints(id, points - getFactionPoints(id), parent.player); + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerItemGiverData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerItemGiverData.java new file mode 100644 index 000000000..95351b4bc --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerItemGiverData.java @@ -0,0 +1,71 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IPlayerItemGiverData; +import noppes.npcs.api.jobs.IJobItemGiver; +import noppes.npcs.roles.JobItemGiver; + +import java.util.HashMap; + +public class PlayerItemGiverData implements IPlayerItemGiverData { + private final PlayerData parent; + private HashMap itemgivers = new HashMap(); + private HashMap chained = new HashMap(); + + public PlayerItemGiverData(PlayerData parent) { + this.parent = parent; + } + + public void loadNBTData(INbt compound) { + chained = NBTTags.getIntegerIntegerMap(compound.getTagList("ItemGiverChained", 10)); + itemgivers = NBTTags.getIntegerLongMap(compound.getTagList("ItemGiversList", 10)); + } + + public void saveNBTData(INbt compound) { + compound.setTag("ItemGiverChained", NBTTags.nbtIntegerIntegerMap(chained)); + compound.setTag("ItemGiversList", NBTTags.nbtIntegerLongMap(itemgivers)); + } + + public boolean hasInteractedBefore(JobItemGiver jobItemGiver) { + return itemgivers.containsKey(jobItemGiver.itemGiverId); + } + + public long getTime(JobItemGiver jobItemGiver) { + return itemgivers.get(jobItemGiver.itemGiverId); + } + + public void setTime(JobItemGiver jobItemGiver, long day) { + itemgivers.put(jobItemGiver.itemGiverId, day); + } + + public int getItemIndex(JobItemGiver jobItemGiver) { + if (chained.containsKey(jobItemGiver.itemGiverId)) + return chained.get(jobItemGiver.itemGiverId); + return 0; + } + + public void setItemIndex(JobItemGiver jobItemGiver, int i) { + chained.put(jobItemGiver.itemGiverId, i); + } + + public long getTime(IJobItemGiver jobItemGiver) { + return itemgivers.get(((JobItemGiver) jobItemGiver).itemGiverId); + } + + public void setTime(IJobItemGiver jobItemGiver, long day) { + itemgivers.put(((JobItemGiver) jobItemGiver).itemGiverId, day); + } + + public boolean hasInteractedBefore(IJobItemGiver jobItemGiver) { + return itemgivers.containsKey(((JobItemGiver) jobItemGiver).itemGiverId); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerMail.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerMail.java new file mode 100644 index 000000000..0b2258223 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerMail.java @@ -0,0 +1,289 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.IPlayerMail; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.controllers.QuestController; +import java.util.ArrayList; + +public class PlayerMail implements IInventory, IPlayerMail { + public String subject = ""; + public String sender = ""; + public INbt message = new INbt(); + public long time = 0; + public boolean beenRead = false; + public int questId = -1; + public String questTitle = ""; + public IItemStack[] items = new IItemStack[4]; + + public long timePast; + + public void readNBT(INbt compound) { + subject = compound.getString("Subject"); + sender = compound.getString("Sender"); + time = compound.getLong("Time"); + beenRead = compound.getBoolean("BeenRead"); + message = compound.getCompoundTag("Message"); + timePast = compound.getLong("TimePast"); + if (compound.hasKey("MailQuest")) + questId = compound.getInteger("MailQuest"); + questTitle = compound.getString("MailQuestTitle"); + + this.items = new IItemStack[this.getSizeInventory()]; + + INbtList INbtList = compound.getTagList("MailItems", 10); + for (int i = 0; i < INbtList.tagCount(); ++i) { + INbt nbttagcompound1 = INbtList.getCompoundTagAt(i); + int j = nbttagcompound1.getByte("Slot") & 255; + + if (j >= 0 && j < this.items.length) { + this.items[j] = NoppesUtilServer.readItem(nbttagcompound1); + } + } + } + + public INbt writeNBT() { + INbt compound = new INbt(); + compound.setString("Subject", subject); + compound.setString("Sender", sender); + compound.setLong("Time", time); + compound.setBoolean("BeenRead", beenRead); + compound.setTag("Message", message); + compound.setLong("TimePast", System.currentTimeMillis() - time); + compound.setInteger("MailQuest", questId); + + if (hasQuest()) + compound.setString("MailQuestTitle", getQuest().title); + + INbtList INbtList = new INbtList(); + + for (int i = 0; i < this.items.length; ++i) { + if (this.items[i] != null) { + INbt nbttagcompound1 = new INbt(); + nbttagcompound1.setByte("Slot", (byte) i); + NoppesUtilServer.writeItem(this.items[i], nbttagcompound1); + INbtList.appendTag(nbttagcompound1); + } + } + + compound.setTag("MailItems", INbtList); + return compound; + } + + public boolean isValid() { + return !subject.isEmpty() && !message.hasNoTags() && !sender.isEmpty(); + } + + public boolean hasQuest() { + return getQuest() != null; + } + + public Quest getQuest() { + return QuestController.Instance != null ? QuestController.Instance.quests.get(questId) : null; + } + + @Override + public int getSizeInventory() { + return 4; + } + + @Override + public int getInventoryStackLimit() { + return 64; + } + + @Override + public IItemStack getStackInSlot(int var1) { + return this.items[var1]; + } + + @Override + public IItemStack decrStackSize(int par1, int par2) { + if (this.items[par1] != null) { + IItemStack IItemStack; + + if (this.items[par1].stackSize <= par2) { + IItemStack = this.items[par1]; + this.items[par1] = null; + this.markDirty(); + return IItemStack; + } else { + IItemStack = this.items[par1].splitStack(par2); + + if (this.items[par1].stackSize == 0) { + this.items[par1] = null; + } + + this.markDirty(); + return IItemStack; + } + } else { + return null; + } + } + + @Override + public IItemStack getStackInSlotOnClosing(int var1) { + if (this.items[var1] != null) { + IItemStack IItemStack = this.items[var1]; + this.items[var1] = null; + return IItemStack; + } else { + return null; + } + } + + @Override + public void setInventorySlotContents(int par1, IItemStack par2ItemStack) { + this.items[par1] = par2ItemStack; + + if (par2ItemStack != null && par2ItemStack.stackSize > this.getInventoryStackLimit()) { + par2ItemStack.stackSize = this.getInventoryStackLimit(); + } + + this.markDirty(); + } + + @Override + public String getInventoryName() { + + return null; + } + + @Override + public boolean hasCustomInventoryName() { + + return false; + } + + @Override + public void markDirty() { + + + } + + @Override + public boolean isUseableByPlayer(IPlayer var1) { + return true; + } + + @Override + public void openInventory() { + + } + + @Override + public void closeInventory() { + } + + @Override + public boolean isItemValidForSlot(int var1, IItemStack var2) { + return true; + } + + public PlayerMail copy() { + PlayerMail mail = new PlayerMail(); + mail.readNBT(writeNBT()); + return mail; + } + + public void setPageText(String[] pages) { + INbtList bookPages = new INbtList(); + for (String text : pages) { + bookPages.appendTag(new NBTTagString(text)); + } + + message.setTag("pages", bookPages); + } + + public String[] getPageText() { + INbtList bookPages = new INbtList(); + + if (message.hasKey("pages")) + bookPages = message.getTagList("pages", 8); + + if (bookPages != null) { + bookPages = (INbtList) bookPages.copy(); + ArrayList pageStrings = new ArrayList<>(); + + for (int i = 0; i < bookPages.tagCount(); i++) { + pageStrings.add(bookPages.getStringTagAt(i)); + } + + return pageStrings.toArray(new String[0]); + } else { + return new String[]{""}; + } + } + + public int getPageCount() { + INbtList bookPages = new INbtList(); + + if (message.hasKey("pages")) + bookPages = message.getTagList("pages", 8); + + int bookTotalPages = 0; + if (bookPages != null) { + bookPages = (INbtList) bookPages.copy(); + bookTotalPages = bookPages.tagCount(); + + if (bookTotalPages < 1) { + bookTotalPages = 1; + } + + return bookTotalPages; + } + + return 0; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public String getSender() { + return sender; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getSubject() { + return subject; + } + + public long getTimePast() { + return timePast; + } + + public long getTimeSent() { + return time; + } + + public IItemStack[] getItems() { + ArrayList list = new ArrayList<>(); + for (IItemStack IItemStack : items) { + list.add(NpcAPI.Instance().getIItemStack(IItemStack)); + } + + return list.toArray(new IItemStack[0]); + } + + public void setItems(IItemStack[] items) { + ArrayList list = new ArrayList<>(); + for (IItemStack IItemStack : items) { + list.add(IItemStack.getMCItemStack()); + } + + this.items = list.toArray(new IItemStack[0]); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerMailData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerMailData.java new file mode 100644 index 000000000..b7032ef3d --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerMailData.java @@ -0,0 +1,112 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.IPlayerMailData; +import noppes.npcs.api.handler.data.IPlayerMail; + +import java.util.ArrayList; + +public class PlayerMailData implements IPlayerMailData { + private final PlayerData parent; + public ArrayList playermail = new ArrayList(); + + public PlayerMailData() { + this.parent = null; + } + + public PlayerMailData(PlayerData parent) { + this.parent = parent; + } + + public void loadNBTData(INbt compound) { + ArrayList newmail = new ArrayList(); + INbtList list = compound.getTagList("MailData", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + PlayerMail mail = new PlayerMail(); + mail.readNBT(list.getCompoundTagAt(i)); + newmail.add(mail); + } + playermail = newmail; + } + + public INbt saveNBTData(INbt compound) { + INbtList list = new INbtList(); + + for (PlayerMail mail : playermail) { + list.appendTag(mail.writeNBT()); + } + + compound.setTag("MailData", list); + return compound; + } + + public boolean hasMail() { + for (PlayerMail mail : playermail) + if (!mail.beenRead) + return true; + return false; + } + + public void addMail(IPlayerMail mail) { + playermail.add((PlayerMail) mail); + } + + public void removeMail(IPlayerMail mail) { + playermail.remove((PlayerMail) mail); + } + + public boolean hasMail(IPlayerMail mail) { + return playermail.contains((PlayerMail) mail); + } + + public IPlayerMail[] getAllMail() { + return playermail.toArray(new IPlayerMail[0]); + } + + public IPlayerMail[] getUnreadMail() { + ArrayList mails = new ArrayList<>(); + + for (PlayerMail mail : playermail) { + if (!mail.beenRead) { + mails.add(mail); + } + } + + return mails.toArray(new IPlayerMail[0]); + } + + public IPlayerMail[] getReadMail() { + ArrayList mails = new ArrayList<>(); + + for (PlayerMail mail : playermail) { + if (mail.beenRead) { + mails.add(mail); + } + } + + return mails.toArray(new IPlayerMail[0]); + } + + public IPlayerMail[] getMailFrom(String sender) { + ArrayList mails = new ArrayList<>(); + + for (PlayerMail mail : playermail) { + if (mail.sender.equals(sender)) { + mails.add(mail); + } + } + + return mails.toArray(new IPlayerMail[0]); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerQuestData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerQuestData.java new file mode 100644 index 000000000..bd822e7ae --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerQuestData.java @@ -0,0 +1,315 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.EventHooks; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.api.handler.IPlayerQuestData; +import noppes.npcs.api.handler.data.IParty; +import noppes.npcs.api.handler.data.IQuest; +import noppes.npcs.constants.EnumQuestCompletion; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.PlayerQuestController; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.quests.QuestInterface; +import noppes.npcs.quests.QuestItem; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class PlayerQuestData implements IPlayerQuestData { + private final PlayerData parent; + private IQuest trackedQuest = null; + + public HashMap activeQuests = new HashMap(); + public HashMap finishedQuests = new HashMap(); + + public PlayerQuestData(PlayerData parent) { + this.parent = parent; + } + + public void loadNBTData(INbt mainCompound) { + if (mainCompound == null) + return; + INbt compound = mainCompound.getCompoundTag("QuestData"); + + INbtList list = compound.getTagList("CompletedQuests", 10); + if (list != null) { + HashMap finishedQuests = new HashMap(); + for (int i = 0; i < list.tagCount(); i++) { + INbt INbt = list.getCompoundTagAt(i); + finishedQuests.put(INbt.getInteger("Quest"), INbt.getLong("Date")); + } + this.finishedQuests = finishedQuests; + } + + INbtList list2 = compound.getTagList("ActiveQuests", 10); + if (list2 != null) { + HashMap activeQuests = new HashMap(); + for (int i = 0; i < list2.tagCount(); i++) { + INbt INbt = list2.getCompoundTagAt(i); + int id = INbt.getInteger("Quest"); + Quest quest = QuestController.Instance.quests.get(id); + if (quest == null) + continue; + QuestData data = new QuestData(quest); + data.readEntityFromNBT(INbt); + activeQuests.put(id, data); + } + this.activeQuests = activeQuests; + } + + this.trackedQuest = QuestController.Instance.get(mainCompound.getInteger("TrackedQuestID")); + } + + public void saveNBTData(INbt maincompound) { + INbt compound = new INbt(); + INbtList list = new INbtList(); + for (int quest : finishedQuests.keySet()) { + INbt INbt = new INbt(); + INbt.setInteger("Quest", quest); + INbt.setLong("Date", finishedQuests.get(quest)); + list.appendTag(INbt); + } + + compound.setTag("CompletedQuests", list); + + INbtList list2 = new INbtList(); + for (int quest : activeQuests.keySet()) { + INbt INbt = new INbt(); + INbt.setInteger("Quest", quest); + activeQuests.get(quest).writeEntityToNBT(INbt); + list2.appendTag(INbt); + } + + compound.setTag("ActiveQuests", list2); + + maincompound.setTag("QuestData", compound); + + if (this.trackedQuest != null) { + maincompound.setInteger("TrackedQuestID", this.trackedQuest.getId()); + } else { + maincompound.setInteger("TrackedQuestID", -1); + } + } + + public QuestData getQuestCompletion(IPlayer player, EntityNPCInterface npc) { + PlayerData playerData = PlayerData.get(player); + for (QuestData data : activeQuests.values()) { + Quest quest = data.quest; + if (quest != null && quest.completion == EnumQuestCompletion.Npc && quest.completerNpc.equals(npc.getCommandSenderName()) && quest.questInterface.isCompleted(playerData)) { + return data; + } + } + return null; + } + + public Party getPartyQuestCompletion(IPlayer player, EntityNPCInterface npc) { + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + Party party = playerData.getPlayerParty(); + if (party != null) { + QuestData data = party.getQuestData(); + if (data != null) { + Quest quest = data.quest; + if (quest != null && quest.completion == EnumQuestCompletion.Npc && quest.completerNpc.equals(npc.getCommandSenderName()) && quest.questInterface.isPartyCompleted(party)) { + return party; + } + } + } + } + return null; + } + + public boolean checkQuestCompletion(PlayerData playerData, EnumQuestType type) { + boolean bo = false; + IPlayer player = playerData.player; + + int partyQuestID = -1; + if (playerData.partyUUID != null) { + Party party = PartyController.Instance().getParty(playerData.partyUUID); + if (party != null) { + IQuest quest = party.getQuest(); + if (quest != null) { + partyQuestID = quest.getId(); + } + } + } + + ArrayList activeQuestValues = new ArrayList<>(this.activeQuests.values()); + for (QuestData data : activeQuestValues) { + if (data.quest.type != type && type != null) + continue; + + // Check if Active Quest = Party Quest + if (partyQuestID != -1 && data.quest.id == partyQuestID && data.quest.partyOptions.isAllowParty()) { + continue; + } + + if (data.quest.partyOptions.allowParty && data.quest.partyOptions.onlyParty) + continue; + + QuestInterface inter = data.quest.questInterface; + if (inter.isCompleted(playerData)) { + if ((!data.isCompleted && data.quest.completion == EnumQuestCompletion.Npc) || data.quest.instantComplete(player, data)) { + data.isCompleted = true; + if (data.quest.completion == EnumQuestCompletion.Npc) { + EventHooks.onQuestFinished(player, data.quest); + } + bo = true; + } + } else { + data.isCompleted = false; + } + + if (this.trackedQuest != null && data.quest.getId() == this.trackedQuest.getId()) { + NoppesUtilPlayer.sendTrackedQuestData((IPlayer) player); + } + } + QuestItem.pickedUp = null; + return bo; + } + + public void trackQuest(IQuest quest) { + if (this.trackedQuest == null || quest.getId() != this.trackedQuest.getId()) { + this.trackedQuest = quest; + NoppesUtilPlayer.sendTrackedQuestData((IPlayer) this.parent.player); + } + } + + public void trackParty(IParty party) { + if (party == null) + return; + + IQuest quest = party.getQuest(); + if (this.trackedQuest == null || quest.getId() != this.trackedQuest.getId()) { + this.trackedQuest = quest; + NoppesUtilPlayer.sendPartyTrackedQuestData((IPlayer) this.parent.player, (Party) party); + } + } + + public void untrackQuest() { + if (this.trackedQuest != null) { + this.trackedQuest = null; + PacketHandler.Instance.sendToPlayer(new OverlayQuestTrackingPacket(new INbt()), (IPlayer) this.parent.player); + } + } + + public IQuest getTrackedQuest() { + return this.trackedQuest; + } + + public void startQuest(int id) { + Quest quest = QuestController.Instance.quests.get(id); + if (quest == null) + return; + if (activeQuests.containsKey(id)) + return; + QuestData questdata = new QuestData(quest); + activeQuests.put(id, questdata); + + AchievementPacket.sendAchievement((IPlayer) parent.player, false, "quest.newquest", quest.title); + ChatAlertPacket.sendChatAlert((IPlayer) parent.player, "quest.newquest", ": ", quest.title); + parent.updateClient = true; + } + + public void finishQuest(int id) { + Quest quest = QuestController.Instance.quests.get(id); + if (quest == null) + return; + PlayerQuestController.setQuestFinished(quest, parent.player); + parent.updateClient = true; + } + + public void stopQuest(int id) { + Quest quest = QuestController.Instance.quests.get(id); + if (quest == null) + return; + activeQuests.remove(id); + parent.updateClient = true; + } + + public void removeQuest(int id) { + Quest quest = QuestController.Instance.quests.get(id); + if (quest == null) + return; + activeQuests.remove(id); + finishedQuests.remove(id); + parent.updateClient = true; + } + + public boolean hasFinishedQuest(int id) { + return finishedQuests.containsKey(id); + } + + public boolean hasActiveQuest(int id) { + return activeQuests.containsKey(id); + } + + public IQuest[] getActiveQuests() { + List quests = new ArrayList<>(); + + for (int id : activeQuests.keySet()) { + IQuest quest = (IQuest) QuestController.Instance.quests.get(id); + if (quest != null) { + quests.add(quest); + } + } + + return (IQuest[]) quests.toArray(new IQuest[0]); + } + + public IQuest[] getFinishedQuests() { + List quests = new ArrayList<>(); + + for (int id : finishedQuests.keySet()) { + IQuest quest = (IQuest) QuestController.Instance.quests.get(id); + if (quest != null) { + quests.add(quest); + } + } + + return (IQuest[]) quests.toArray(new IQuest[0]); + } + + public long getLastCompletedTime(int id) { + if (this.hasFinishedQuest(id)) { + return this.finishedQuests.get(id); + } + return 0; + } + + public void setLastCompletedTime(int id, long time) { + Quest quest = QuestController.Instance.quests.get(id); + if (quest == null) + return; + this.finishedQuests.put(id, time); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java new file mode 100644 index 000000000..4ca5c7393 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java @@ -0,0 +1,427 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.util.VaultUtil; +import noppes.npcs.api.handler.IPlayerTradeData; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Stores player's trade-related data: currency and auction claims. + * This data is shared across all profile slots. + * Uses CopyOnWriteArrayList for thread-safety when adding claims asynchronously. + */ +public class PlayerTradeData implements IPlayerTradeData { + // NBT key used by trade data - excluded from per-slot profile saving + public static final String NBT_KEY = "TradeData"; + + private final PlayerData playerData; + + // ========================================= + // Currency Data + // ========================================= + + // Built-in CNPC+ currency (always saved, used as fallback) + private long balance; + private long lifetimeEarned; + private long lifetimeSpent; + + // ========================================= + // Auction Claims Data + // ========================================= + + // Thread-safe list for claims - can be modified from CNPC+ thread + private final CopyOnWriteArrayList claims = new CopyOnWriteArrayList<>(); + + public PlayerTradeData(PlayerData playerData) { + this.playerData = playerData; + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + } + + // ========================================= + // Currency Methods + // ========================================= + + /** + * Check if Vault should be used for currency operations. + * Requires both config enabled AND Vault actually available. + */ + private boolean shouldUseVault() { + return ConfigMarket.UseVault && VaultUtil.isEnabled(); + } + + /** + * Get player name for Vault operations + */ + private String getPlayerName() { + if (playerData != null && playerData.player != null) { + return playerData.player.getCommandSenderName(); + } + return playerData != null ? playerData.playername : null; + } + + /** + * Get the current balance + */ + @Override + public long getBalance() { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return (long) VaultUtil.getBalance(playerName); + } + } + return balance; + } + + /** + * Set the balance directly (use with caution) + */ + @Override + public void setBalance(long balance) { + if (balance < 0) { + balance = 0; + } + if (balance > ConfigMarket.MaxBalance) { + balance = ConfigMarket.MaxBalance; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + VaultUtil.setBalance(playerName, balance); + return; + } + } + this.balance = balance; + } + + /** + * Add currency to balance (deposit) + * + * @param amount Amount to add + * @return true if successful, false if would exceed max balance + */ + @Override + public boolean deposit(long amount) { + if (amount <= 0) { + return false; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.addMoney(playerName, amount); + if (success) { + this.lifetimeEarned += amount; + } + return success; + } + } + + // Built-in currency + long newBalance = this.balance + amount; + // Overflow protection + if (newBalance < this.balance) { + newBalance = ConfigMarket.MaxBalance; + } + // Cap at MaxBalance instead of rejecting + if (newBalance > ConfigMarket.MaxBalance) { + newBalance = ConfigMarket.MaxBalance; + } + this.balance = newBalance; + this.lifetimeEarned += amount; + return true; + } + + /** + * Remove currency from balance (withdraw) + * + * @param amount Amount to remove + * @return true if successful, false if insufficient funds + */ + @Override + public boolean withdraw(long amount) { + if (amount <= 0) { + return false; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.withdrawMoney(playerName, amount); + if (success) { + this.lifetimeSpent += amount; + } + return success; + } + } + + // Built-in currency + if (this.balance < amount) { + return false; + } + this.balance -= amount; + this.lifetimeSpent += amount; + return true; + } + + /** + * Check if player can afford an amount + */ + @Override + public boolean canAfford(long amount) { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return VaultUtil.has(playerName, amount); + } + } + return this.balance >= amount; + } + + /** + * Get lifetime earned currency + */ + @Override + public long getLifetimeEarned() { + return lifetimeEarned; + } + + /** + * Get lifetime spent currency + */ + @Override + public long getLifetimeSpent() { + return lifetimeSpent; + } + + /** + * Format the balance for display + */ + @Override + public String formatBalance() { + if (shouldUseVault()) { + return VaultUtil.format(getBalance()); + } + return formatAmount(this.balance); + } + + /** + * Format an amount for display + */ + public static String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Check if Vault is being used for currency operations. + * Returns true only if config is enabled AND Vault is actually available. + */ + @Override + public boolean isUsingVault() { + return shouldUseVault(); + } + + /** + * Check if Vault is configured (regardless of availability) + */ + public boolean isVaultConfigured() { + return ConfigMarket.UseVault; + } + + // ========================================= + // Auction Claims Methods + // ========================================= + + /** + * Add a claim to this player's pending claims. + * Thread-safe - can be called from any thread. + */ + public synchronized void addClaim(AuctionClaim claim) { + if (claim != null && !claim.claimed) { + // Prevent duplicate claim IDs + for (AuctionClaim existing : claims) { + if (existing.id.equals(claim.id)) { + return; + } + } + claims.add(claim); + } + } + + /** + * Get all unclaimed claims for this player (internal use). + * Returns a copy of the list for safe iteration. + */ + public List getClaimsList() { + List result = new ArrayList<>(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + result.add(claim); + } + } + return result; + } + + /** + * Get all unclaimed claims for this player (API). + */ + @Override + public IAuctionClaim[] getClaims() { + List result = getClaimsList(); + return result.toArray(new IAuctionClaim[0]); + } + + /** + * Get a specific claim by ID (internal use). + * Synchronized to pair with claimAndRemove for thread safety. + */ + public synchronized AuctionClaim getClaimInternal(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + return claim; + } + } + return null; + } + + /** + * Get a specific claim by ID (API). + */ + @Override + public IAuctionClaim getClaim(String claimId) { + return getClaimInternal(claimId); + } + + /** + * Mark a claim as claimed and remove it. + * Synchronized to prevent double-claim from concurrent threads. + */ + public synchronized boolean claimAndRemove(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + claim.claimed = true; + claims.remove(claim); + return true; + } + } + return false; + } + + /** + * Get count of unclaimed claims. + */ + @Override + public int getClaimCount() { + int count = 0; + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + count++; + } + } + return count; + } + + /** + * Check if player has any unclaimed claims. + */ + @Override + public boolean hasClaims() { + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + return true; + } + } + return false; + } + + /** + * Process expired claims - removes claims older than expiration days. + * Returns number of claims removed. + */ + public int processExpiredClaims() { + int expirationDays = ConfigMarket.ClaimExpirationDays; + List expired = new ArrayList<>(); + for (AuctionClaim claim : claims) { + if (!claim.claimed && claim.isExpired(expirationDays)) { + expired.add(claim); + } + } + if (!expired.isEmpty()) { + claims.removeAll(expired); + } + return expired.size(); + } + + // ========================================= + // NBT Serialization + // ========================================= + + public void readFromNBT(INbt compound) { + INbt tradeData = compound.getCompoundTag(NBT_KEY); + if (tradeData == null || tradeData.hasNoTags()) { + // No trade data found, use defaults + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + claims.clear(); + return; + } + + // Currency + if (tradeData.hasKey("CurrencyBalance")) { + this.balance = tradeData.getLong("CurrencyBalance"); + } else { + this.balance = ConfigMarket.StartingBalance; + } + this.lifetimeEarned = tradeData.getLong("CurrencyLifetimeEarned"); + this.lifetimeSpent = tradeData.getLong("CurrencyLifetimeSpent"); + + // Claims + claims.clear(); + if (tradeData.hasKey("AuctionClaims")) { + INbtList claimsList = tradeData.getTagList("AuctionClaims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); + if (!claim.claimed) { + claims.add(claim); + } + } + } + } + + public void writeToNBT(INbt compound) { + INbt tradeData = new INbt(); + + // Currency - always save built-in currency data (preserved even when using Vault) + tradeData.setLong("CurrencyBalance", this.balance); + tradeData.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); + tradeData.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); + + // Claims + INbtList claimsList = new INbtList(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + claimsList.appendTag(claim.writeToNBT(new INbt())); + } + } + tradeData.setTag("AuctionClaims", claimsList); + + compound.setTag(NBT_KEY, tradeData); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/PlayerTransportData.java b/core/src/main/java/noppes/npcs/controllers/data/PlayerTransportData.java index b0011cb6b..6936bfd5b 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/PlayerTransportData.java +++ b/core/src/main/java/noppes/npcs/controllers/data/PlayerTransportData.java @@ -1,12 +1,19 @@ package noppes.npcs.controllers.data; -import noppes.npcs.api.handler.IPlayerTransportData; -import noppes.npcs.api.handler.data.ITransportLocation; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; -import noppes.npcs.core.NBT; import java.util.HashSet; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.ITransportLocation; +import noppes.npcs.api.handler.IPlayerTransportData; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.core.NBT; public class PlayerTransportData implements IPlayerTransportData { public HashSet transports = new HashSet(); @@ -24,8 +31,8 @@ public class PlayerTransportData implements IPlayerTransportData { } for (int i = 0; i < list.size(); i++) { - INbt nbttagcompound = list.getCompound(i); - dialogsRead.add(nbttagcompound.getInteger("Transport")); + INbt INbt = list.getCompound(i); + dialogsRead.add(INbt.getInteger("Transport")); } this.transports = dialogsRead; } @@ -33,9 +40,9 @@ public class PlayerTransportData implements IPlayerTransportData { public void saveNBTData(INbt compound) { INbtList list = NBT.list(); for (int dia : transports) { - INbt nbttagcompound = NBT.compound(); - nbttagcompound.setInteger("Transport", dia); - list.addCompound(nbttagcompound); + INbt INbt = NBT.compound(); + INbt.setInteger("Transport", dia); + list.addCompound(INbt); } compound.setTagList("TransportData", list); diff --git a/core/src/main/java/noppes/npcs/controllers/data/Quest.java b/core/src/main/java/noppes/npcs/controllers/data/Quest.java new file mode 100644 index 000000000..455159340 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/Quest.java @@ -0,0 +1,425 @@ +package noppes.npcs.controllers.data; + + +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.IPartyOptions; +import noppes.npcs.api.handler.data.IProfileOptions; +import noppes.npcs.api.handler.data.IQuest; +import noppes.npcs.api.handler.data.IQuestCategory; +import noppes.npcs.api.handler.data.IQuestInterface; +import noppes.npcs.api.handler.data.IQuestObjective; +import noppes.npcs.api.handler.data.ISlot; +import noppes.npcs.api.handler.IPlayerData; +import noppes.npcs.api.handler.IPlayerQuestData; +import noppes.npcs.api.IContainer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumProfileSync; +import noppes.npcs.constants.EnumQuestCompletion; +import noppes.npcs.constants.EnumQuestRepeat; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.ICompatibilty; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.quests.QuestDialog; +import noppes.npcs.quests.QuestInterface; +import noppes.npcs.quests.QuestItem; +import noppes.npcs.quests.QuestKill; +import noppes.npcs.quests.QuestLocation; +import noppes.npcs.quests.QuestManual; +import noppes.npcs.VersionCompatibility; + +public class Quest implements ICompatibilty, IQuest { + public int version = VersionCompatibility.ModRev; + public int id = -1; + public EnumQuestType type = EnumQuestType.Item; + public EnumQuestRepeat repeat = EnumQuestRepeat.NONE; + public EnumQuestCompletion completion = EnumQuestCompletion.Npc; + public String title = "default"; + public QuestCategory category; + public String logText = ""; + public String completeText = ""; + public String completerNpc = ""; + public int nextQuestid = -1; + public String nextQuestTitle = ""; + public PlayerMail mail = new PlayerMail(); + public String command = ""; + + public QuestInterface questInterface = new QuestItem(); + public long customCooldown = 0; + + public int rewardExp = 0; + public NpcMiscInventory rewardItems = new NpcMiscInventory(9); + public boolean randomReward = false; + public FactionOptions factionOptions = new FactionOptions(); + public PartyOptions partyOptions = new PartyOptions(); + public ProfileOptions profileOptions = new ProfileOptions(); + + public void readNBT(INbt compound) { + id = compound.getInteger("Id"); + readNBTPartial(compound); + } + + public void readNBTPartial(INbt compound) { + version = compound.getInteger("ModRev"); + VersionCompatibility.CheckAvailabilityCompatibility(this, compound); + + setType(EnumQuestType.values()[compound.getInteger("Type")]); + title = compound.getString("Title"); + logText = compound.getString("Text"); + completeText = compound.getString("CompleteText"); + completerNpc = compound.getString("CompleterNpc"); + command = compound.getString("QuestCommand"); + nextQuestid = compound.getInteger("NextQuestId"); + nextQuestTitle = compound.getString("NextQuestTitle"); + if (hasNewQuest()) + nextQuestTitle = getNextQuest().title; + else + nextQuestTitle = ""; + randomReward = compound.getBoolean("RandomReward"); + rewardExp = compound.getInteger("RewardExp"); + rewardItems.setFromNBT(compound.getCompoundTag("Rewards")); + + completion = EnumQuestCompletion.values()[compound.getInteger("QuestCompletion")]; + repeat = EnumQuestRepeat.values()[compound.getInteger("QuestRepeat")]; + customCooldown = compound.getLong("CustomCooldown"); + questInterface.readEntityFromNBT(compound); + + factionOptions.readFromNBT(compound.getCompoundTag("QuestFactionPoints")); + partyOptions.readFromNBT(new NBTWrapper(compound.getCompoundTag("PartyOptions"))); + profileOptions.readFromNBT(new NBTWrapper(compound.getCompoundTag("ProfileOptions"))); + + mail.readNBT(compound.getCompoundTag("QuestMail")); + } + + public void setType(EnumQuestType questType) { + type = questType; + if (type == EnumQuestType.Item) + questInterface = new QuestItem(); + else if (type == EnumQuestType.Dialog) + questInterface = new QuestDialog(); + else if (type == EnumQuestType.Kill || type == EnumQuestType.AreaKill) + questInterface = new QuestKill(); + else if (type == EnumQuestType.Location) + questInterface = new QuestLocation(); + else if (type == EnumQuestType.Manual) + questInterface = new QuestManual(); + + if (questInterface != null) + questInterface.questId = id; + } + + public INbt writeToNBT(INbt compound) { + compound.setInteger("Id", id); + return writeToNBTPartial(compound); + } + + public INbt writeToNBTPartial(INbt compound) { + compound.setInteger("ModRev", version); + compound.setInteger("Type", type.ordinal()); + compound.setString("Title", title); + compound.setString("Text", logText); + compound.setString("CompleteText", completeText); + compound.setString("CompleterNpc", completerNpc); + compound.setInteger("NextQuestId", nextQuestid); + compound.setString("NextQuestTitle", nextQuestTitle); + compound.setInteger("RewardExp", rewardExp); + compound.setTag("Rewards", rewardItems.getToNBT()); + compound.setString("QuestCommand", command); + compound.setBoolean("RandomReward", randomReward); + compound.setLong("CustomCooldown", customCooldown); + + compound.setInteger("QuestCompletion", completion.ordinal()); + compound.setInteger("QuestRepeat", repeat.ordinal()); + + this.questInterface.writeEntityToNBT(compound); + compound.setTag("QuestFactionPoints", factionOptions.writeToNBT(new INbt())); + compound.setTag("PartyOptions", ((NBTWrapper) partyOptions.writeToNBT()).getMCTag()); + compound.setTag("ProfileOptions", ((NBTWrapper) profileOptions.writeToNBT()).getMCTag()); + compound.setTag("QuestMail", mail.writeNBT()); + + return compound; + } + + public boolean hasNewQuest() { + return getNextQuest() != null; + } + + public Quest getNextQuest() { + return QuestController.Instance == null ? null : QuestController.Instance.quests.get(nextQuestid); + } + + public boolean instantComplete(IPlayer player, QuestData data) { + if (completion == EnumQuestCompletion.Instant && NoppesUtilPlayer.questCompletion((IPlayer) player, data.quest.id)) { + QuestCompletionPacket.sendQuestComplete((IPlayer) player, data.quest.writeToNBT(new INbt())); + return true; + } + return false; + } + + public boolean instantPartyComplete(Party party) { + return completion == EnumQuestCompletion.Instant && NoppesUtilPlayer.questPartyCompletion(party); + } + + public Quest copy() { + Quest quest = new Quest(); + quest.readNBT(this.writeToNBT(new INbt())); + return quest; + } + + public long getTimeUntilRepeat(IPlayer player) { + if (ProfileClientConfig.isProfilesEnabled() && profileOptions.enableOptions + && profileOptions.cooldownControl == EnumProfileSync.Shared) { + Long questTime = ProfileClientConfig.getSharedQuestTimestamp(player, this.id); + if (questTime != null) { + return calculateRemainingCooldown(player, questTime); + } + return 0L; + } + IPlayerQuestData questData = PlayerData.get(player).getQuestData(); + return getTimeUntilRepeatQuestData(player, questData); + } + + public long calculateRemainingCooldown(IPlayer player, long questTime) { + switch (repeat) { + case MCDAILY: { + long now = player.worldObj.getTotalWorldTime(); + long remainingTicks = questTime + 24000 - now; + return Math.max(0, remainingTicks * 50); + } + case MCWEEKLY: { + long now = player.worldObj.getTotalWorldTime(); + long remainingTicks = questTime + 168000 - now; + return Math.max(0, remainingTicks * 50); + } + case MCCUSTOM: { + long now = player.worldObj.getTotalWorldTime(); + long remainingTicks = questTime + this.customCooldown - now; + return Math.max(0, remainingTicks * 50); + } + case RLDAILY: { + long remainingMillis = questTime + 86400000 - System.currentTimeMillis(); + return Math.max(0, remainingMillis); + } + case RLWEEKLY: { + long remainingMillis = questTime + 604800000 - System.currentTimeMillis(); + return Math.max(0, remainingMillis); + } + case RLCUSTOM: { + long remainingMillis = questTime + this.customCooldown - System.currentTimeMillis(); + return Math.max(0, remainingMillis); + } + default: + return 0L; + } + } + + public long getTimeUntilRepeatQuestData(IPlayer player, IPlayerQuestData questData) { + long questTime = questData.getLastCompletedTime(this.id); + + switch (repeat) { + case MCDAILY: { + long now = player.worldObj.getTotalWorldTime(); + long allowedTicks = 24000; + // If questTime is in the future and the excess is greater than allowed, reset it. + if (questTime > now && (questTime - now) > allowedTicks) { + questTime = now; + questData.setLastCompletedTime(this.id, questTime); + } + long remainingTicks = questTime + allowedTicks - now; + return Math.max(0, remainingTicks * 50); + } + case MCWEEKLY: { + long now = player.worldObj.getTotalWorldTime(); + long allowedTicks = 168000; + if (questTime > now && (questTime - now) > allowedTicks) { + questTime = now; + questData.setLastCompletedTime(this.id, questTime); + } + long remainingTicks = questTime + allowedTicks - now; + return Math.max(0, remainingTicks * 50); + } + case RLDAILY: { + long now = System.currentTimeMillis(); + long allowedMillis = 86400000; + if (questTime > now && (questTime - now) > allowedMillis) { + questTime = now; + questData.setLastCompletedTime(this.id, questTime); + } + long remainingMillis = questTime + allowedMillis - now; + return Math.max(0, remainingMillis); + } + case RLWEEKLY: { + long now = System.currentTimeMillis(); + long allowedMillis = 604800000; + if (questTime > now && (questTime - now) > allowedMillis) { + questTime = now; + questData.setLastCompletedTime(this.id, questTime); + } + long remainingMillis = questTime + allowedMillis - now; + return Math.max(0, remainingMillis); + } + case MCCUSTOM: { + // For MCCUSTOM, customCooldown is assumed to be defined in ticks. + long now = player.worldObj.getTotalWorldTime(); + long allowedTicks = this.customCooldown; + if (questTime > now && (questTime - now) > allowedTicks) { + questTime = now; + questData.setLastCompletedTime(this.id, questTime); + } + long remainingTicks = questTime + allowedTicks - now; + return Math.max(0, remainingTicks * 50); + } + case RLCUSTOM: { + // For RLCUSTOM, customCooldown is assumed to be in milliseconds. + long now = System.currentTimeMillis(); + long allowedMillis = this.customCooldown; + if (questTime > now && (questTime - now) > allowedMillis) { + questTime = now; + questData.setLastCompletedTime(this.id, questTime); + } + long remainingMillis = questTime + allowedMillis - now; + return Math.max(0, remainingMillis); + } + default: + return 0L; + } + } + + @Override + public int getVersion() { + return version; + } + + @Override + public void setVersion(int version) { + this.version = version; + } + + public int getId() { + return this.id; + } + + public String getName() { + return this.title; + } + + public int getType() { + return this.type.ordinal(); + } + + public void setType(int questType) { + if (questType < 0 || questType >= EnumQuestType.values().length) + return; + + EnumQuestType type = EnumQuestType.values()[questType]; + setType(type); + } + + public IQuestCategory getCategory() { + return this.category; + } + + public void save() { + QuestController.Instance.saveQuest(this.category.id, this); + } + + public void setName(String name) { + this.title = name; + } + + public String getLogText() { + return this.logText; + } + + public void setLogText(String text) { + this.logText = text; + } + + public String getCompleteText() { + return this.completeText; + } + + public void setCompleteText(String text) { + this.completeText = text; + } + + public void setNextQuest(IQuest quest) { + if (quest == null) { + this.nextQuestid = -1; + this.nextQuestTitle = ""; + } else { + if (quest.getId() < 0) { + throw new CustomNPCsException("Quest id is lower than 0", new Object[0]); + } + + this.nextQuestid = quest.getId(); + this.nextQuestTitle = quest.getName(); + } + + } + + public String getNpcName() { + return this.completerNpc; + } + + public void setNpcName(String name) { + this.completerNpc = name; + } + + public IQuestObjective[] getObjectives(IPlayer player) { + if (!player.hasActiveQuest(this.id)) { + throw new CustomNPCsException("Player doesnt have this quest active.", new Object[0]); + } else { + return this.questInterface.getObjectives((IPlayer) player.getMCEntity()); + } + } + + public boolean getIsRepeatable() { + return this.repeat != EnumQuestRepeat.NONE; + } + + + public void setRepeatType(int type) { + if (type < 0 || type >= EnumQuestRepeat.values().length) { + return; + } + this.repeat = EnumQuestRepeat.values()[type]; + } + + public int getRepeatType() { + return this.repeat.ordinal(); + } + + public IContainer getRewards() { + return NpcAPI.Instance().getIContainer(this.rewardItems); + } + + public IQuestInterface getQuestInterface() { + return this.questInterface; + } + + public IPartyOptions getPartyOptions() { + return this.partyOptions; + } + + public IProfileOptions getProfileOptions() { + return this.profileOptions; + } + + public long getCustomCooldown() { + return this.customCooldown; + } + + public void setCustomCooldown(long newCooldown) { + this.customCooldown = newCooldown; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/QuestCategory.java b/core/src/main/java/noppes/npcs/controllers/data/QuestCategory.java new file mode 100644 index 000000000..b710056ce --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/QuestCategory.java @@ -0,0 +1,98 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.data.IQuest; +import noppes.npcs.api.handler.data.IQuestCategory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class QuestCategory implements IQuestCategory { + public HashMap quests; + public int id = -1; + public String title = ""; + + public QuestCategory() { + quests = new HashMap(); + } + + public void readNBT(INbt INbt) { + id = INbt.getInteger("Slot"); + title = INbt.getString("Title"); + INbtList questList = getFirstValidList(INbt, NbtConstants.TAG_COMPOUND, "Quests", "Dialogs"); + if (questList.tagCount() > 0) { + for (int ii = 0; ii < questList.tagCount(); ii++) { + INbt nbttagcompound2 = questList.getCompoundTagAt(ii); + Quest quest = new Quest(); + quest.readNBT(nbttagcompound2); + quest.category = this; + quests.put(quest.id, quest); + } + } + } + + private INbtList getFirstValidList(INbt nbt, int type, String... keys) { + for (String key : keys) { + if (nbt.hasKey(key, NbtConstants.TAG_LIST)) { + INbtList list = nbt.getTagList(key, type); + if (list.tagCount() > 0) { + return list; + } + } + } + return new INbtList(); + } + + public INbt writeNBT(INbt INbt) { + INbt.setInteger("Slot", id); + INbt.setString("Title", title); + INbtList quests = new INbtList(); + for (int questID : this.quests.keySet()) { + Quest quest = this.quests.get(questID); + quests.appendTag(quest.writeToNBT(new INbt())); + } + + INbt.setTag("Quests", quests); + + return INbt; + } + + public INbt writeSmallNBT(INbt INbt) { + INbt.setInteger("Slot", id); + INbt.setString("Title", title); + return INbt; + } + + public void readSmallNBT(INbt INbt) { + id = INbt.getInteger("Slot"); + title = INbt.getString("Title"); + } + + public List quests() { + return new ArrayList(this.quests.values()); + } + + public String getName() { + return this.title; + } + + public IQuest create() { + Quest quest = new Quest(); + quest.category = this; + return quest; + } + + @Override + public int getId() { + return this.id; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/QuestData.java b/core/src/main/java/noppes/npcs/controllers/data/QuestData.java new file mode 100644 index 000000000..62de09aa8 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/QuestData.java @@ -0,0 +1,33 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +public class QuestData { + public Quest quest; + public boolean isCompleted; + public boolean sendAlerts = true; + public INbt extraData = new INbt(); + + public QuestData(Quest quest) { + this.quest = quest; + } + + public void writeEntityToNBT(INbt INbt) { + INbt.setBoolean("QuestCompleted", isCompleted); + INbt.setBoolean("SendAlerts", sendAlerts); + INbt.setTag("ExtraData", extraData); + } + + public void readEntityFromNBT(INbt INbt) { + isCompleted = INbt.getBoolean("QuestCompleted"); + sendAlerts = INbt.getBoolean("SendAlerts"); + extraData = INbt.getCompoundTag("ExtraData"); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/RecipeAnvil.java b/core/src/main/java/noppes/npcs/controllers/data/RecipeAnvil.java new file mode 100644 index 000000000..03c7449d8 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/RecipeAnvil.java @@ -0,0 +1,188 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.IAnvilRecipe; +import noppes.npcs.controllers.RecipeController; + +public class RecipeAnvil implements IAnvilRecipe { + public int id = -1; + public String name = ""; + public Availability availability = new Availability(); + + public boolean ignoreRepairItemNBT = false; + public boolean ignoreRepairMaterialNBT = false; + public boolean ignoreRepairMaterialDamage = false; + + public IItemStack itemToRepair; + public IItemStack repairMaterial; + + public int xpCost; + public float repairPercentage; + + public RecipeAnvil() { + } + + public RecipeAnvil(String name, IItemStack itemToRepair, IItemStack repairMaterial, int xpCost, float repairPercentage) { + this.name = name; + this.itemToRepair = itemToRepair; + this.repairMaterial = repairMaterial; + this.xpCost = xpCost; + this.repairPercentage = repairPercentage; + } + + public void readNBT(INbt compound) { + this.id = compound.getInteger("ID"); + this.name = compound.getString("Name"); + this.availability.readFromNBT(compound.getCompoundTag("Availability")); + this.xpCost = compound.getInteger("XPCost"); + this.repairPercentage = compound.getFloat("RepairPercentage"); + this.itemToRepair = NoppesUtilServer.readItem(compound.getCompoundTag("ItemToRepair")); + this.repairMaterial = NoppesUtilServer.readItem(compound.getCompoundTag("RepairMaterial")); + this.ignoreRepairMaterialNBT = compound.getBoolean("IgnoreRepairMatNBT"); + this.ignoreRepairItemNBT = compound.getBoolean("IgnoreRepairItemNBT"); + this.ignoreRepairMaterialDamage = compound.getBoolean("IgnoreRepairMatDamage"); + + if (compound.hasKey("ScriptData", NbtConstants.TAG_COMPOUND)) { + RecipeScript handler = new RecipeScript(); + handler.readFromNBT(compound.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } + } + + public INbt writeNBT() { + return writeNBT(true); + } + + + public INbt writeNBT(boolean saveScripts) { + INbt compound = new INbt(); + compound.setInteger("ID", id); + compound.setString("Name", name); + compound.setTag("Availability", availability.writeToNBT(new INbt())); + compound.setInteger("XPCost", xpCost); + compound.setFloat("RepairPercentage", repairPercentage); + if (itemToRepair != null) { + compound.setTag("ItemToRepair", NoppesUtilServer.writeItem(itemToRepair, new INbt())); + } + if (repairMaterial != null) { + compound.setTag("RepairMaterial", NoppesUtilServer.writeItem(repairMaterial, new INbt())); + } + compound.setBoolean("IgnoreRepairMatNBT", ignoreRepairMaterialNBT); + compound.setBoolean("IgnoreRepairItemNBT", ignoreRepairItemNBT); + compound.setBoolean("IgnoreRepairMatDamage", ignoreRepairMaterialDamage); + compound.setBoolean("IsAnvil", true); + + if (saveScripts) { + INbt scriptData = new INbt(); + RecipeScript handler = getScriptHandler(); + if (handler != null) + handler.writeToNBT(scriptData); + compound.setTag("ScriptData", scriptData); + } + return compound; + } + + @Override + public int getXpCost() { + return xpCost; + } + + @Override + public float getRepairPercentage() { + return repairPercentage; + } + + @Override + public boolean matches(IItemStack inputItem, IItemStack inputRepairMaterial) { + if (inputItem == null || inputRepairMaterial == null) return false; + if (!NoppesUtilPlayer.compareItems(this.itemToRepair, inputItem, true, this.ignoreRepairItemNBT)) { + return false; + } + if (!NoppesUtilPlayer.compareItems(this.repairMaterial, inputRepairMaterial, this.ignoreRepairMaterialDamage, this.ignoreRepairMaterialNBT)) { + return false; + } + return true; + } + + @Override + public IItemStack getResult(IItemStack inputItem) { + if (inputItem == null) + return null; + IItemStack result = inputItem.copy(); + if (!result.isItemStackDamageable()) + return result; + int maxDamage = result.getMaxDamage(); + int repairAmount = (int) (maxDamage * repairPercentage); + int currentDamage = result.getItemDamage(); + int newDamage = currentDamage - repairAmount; + if (newDamage < 0) newDamage = 0; + result.setItemDamage(newDamage); + return result; + } + + @Override + public String getName() { + return name; + } + + @Override + public int getID() { + return this.id; + } + + public static RecipeAnvil saveRecipe(RecipeAnvil recipe, IItemStack output, IItemStack repairMaterial) { + if (output != null) { + recipe.itemToRepair = output.copy(); + if (recipe.itemToRepair.isItemStackDamageable()) { + recipe.itemToRepair.setItemDamage(0); + } + } else { + recipe.itemToRepair = null; + } + recipe.repairMaterial = (repairMaterial == null ? null : repairMaterial.copy()); + return recipe; + } + + public boolean isValid() { + return itemToRepair != null && repairMaterial != null; + } + + /** + * Creates and returns a deep copy of this RecipeAnvil. + */ + public void copy(RecipeAnvil recipe) { + this.id = recipe.id; + this.name = recipe.name; + this.availability = recipe.availability; + this.ignoreRepairMaterialDamage = recipe.ignoreRepairMaterialDamage; + this.ignoreRepairItemNBT = recipe.ignoreRepairItemNBT; + this.ignoreRepairMaterialNBT = recipe.ignoreRepairMaterialNBT; + this.repairPercentage = recipe.repairPercentage; + this.xpCost = recipe.xpCost; + } + + public RecipeScript getScriptHandler() { + return RecipeController.Instance.anvilScripts.get(this.id); + } + + public void setScriptHandler(RecipeScript handler) { + RecipeController.Instance.anvilScripts.put(this.id, handler); + } + + public RecipeScript getOrCreateScriptHandler() { + RecipeScript data = getScriptHandler(); + if (data == null) + setScriptHandler(data = new RecipeScript()); + return data; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/RecipeCarpentry.java b/core/src/main/java/noppes/npcs/controllers/data/RecipeCarpentry.java new file mode 100644 index 000000000..408290aec --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/RecipeCarpentry.java @@ -0,0 +1,309 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.IRecipe; +import noppes.npcs.controllers.RecipeController; + +import java.io.IOException; +import java.util.HashMap; + +public class RecipeCarpentry extends ShapedRecipes implements IRecipe { + public int id = -1; + public String name = ""; + public Availability availability = new Availability(); + public boolean isGlobal = false; + public boolean ignoreDamage = false; + public boolean ignoreNBT = false; + + public RecipeCarpentry(int width, int height, IItemStack[] recipe, IItemStack result) { + super(width, height, recipe, result); + } + + public RecipeCarpentry(String name) { + super(0, 0, new IItemStack[0], null); + this.name = name; + } + + public static RecipeCarpentry create(INbt compound) { + return new RecipeCarpentry(compound.getInteger("Width"), compound.getInteger("Height"), + NBTTags.getItemStackArray(compound.getTagList("Materials", 10)), NoppesUtilServer.readItem(compound.getCompoundTag("Item"))); + } + + public void readNBT(INbt compound) { + this.name = compound.getString("Name"); + this.id = compound.getInteger("ID"); + this.availability.readFromNBT(compound.getCompoundTag("Availability")); + this.ignoreDamage = compound.getBoolean("IgnoreDamage"); + this.ignoreNBT = compound.getBoolean("IgnoreNBT"); + this.isGlobal = compound.getBoolean("Global"); + + if (!isGlobal) { + if (compound.hasKey("ScriptData", NbtConstants.TAG_COMPOUND)) { + RecipeScript handler = new RecipeScript(); + handler.readFromNBT(compound.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } + } + } + + public INbt writeNBT() { + return writeNBT(true); + } + + public INbt writeNBT(boolean saveScripts) { + INbt compound = new INbt(); + compound.setInteger("ID", id); + compound.setInteger("Width", recipeWidth); + compound.setInteger("Height", recipeHeight); + if (getRecipeOutput() != null) + compound.setTag("Item", NoppesUtilServer.writeItem(getRecipeOutput(), new INbt())); + compound.setTag("Materials", NBTTags.nbtItemStackArray(recipeItems)); + compound.setTag("Availability", availability.writeToNBT(new INbt())); + compound.setString("Name", name); + compound.setBoolean("Global", isGlobal); + compound.setBoolean("IgnoreDamage", ignoreDamage); + compound.setBoolean("IgnoreNBT", ignoreNBT); + if (saveScripts) { + INbt scriptData = new INbt(); + RecipeScript handler = getScriptHandler(); + if (handler != null) + handler.writeToNBT(scriptData); + compound.setTag("ScriptData", scriptData); + } + return compound; + } + + @Override + public boolean matches(InventoryCrafting par1InventoryCrafting, IWorld IWorld) { + for (int i = 0; i <= 4 - this.recipeWidth; ++i) { + for (int j = 0; j <= 4 - this.recipeHeight; ++j) { + if (this.checkMatch(par1InventoryCrafting, i, j, true)) + return true; + + if (this.checkMatch(par1InventoryCrafting, i, j, false)) + return true; + } + } + return false; + } + + + /** + * Checks if the region of a crafting inventory is match for the recipe. + */ + private boolean checkMatch(InventoryCrafting par1InventoryCrafting, int par2, int par3, boolean par4) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + int var7 = i - par2; + int var8 = j - par3; + IItemStack var9 = null; + + if (var7 >= 0 && var8 >= 0 && var7 < this.recipeWidth && var8 < this.recipeHeight) { + if (par4) + var9 = this.recipeItems[this.recipeWidth - var7 - 1 + var8 * this.recipeWidth]; + else + var9 = this.recipeItems[var7 + var8 * this.recipeWidth]; + } + + IItemStack var10 = par1InventoryCrafting.getStackInRowAndColumn(i, j); + + if ((var10 != null || var9 != null) && !NoppesUtilPlayer.compareItems(var9, var10, ignoreDamage, ignoreNBT)) { + return false; + } + } + } + + return true; + } + + @Override + public IItemStack getCraftingResult(InventoryCrafting var1) { + if (getRecipeOutput() == null) + return null; + return getRecipeOutput().copy(); + } + + @Override + public int getRecipeSize() { + return 16; + } + + public static RecipeCarpentry saveRecipe(RecipeCarpentry recipe, IItemStack par1ItemStack, Object... par2ArrayOfObj) { + String var3 = ""; + int var4 = 0; + int var5 = 0; + int var6 = 0; + int var9; + + if (par2ArrayOfObj[var4] instanceof String[]) { + String[] var7 = (String[]) ((String[]) par2ArrayOfObj[var4++]); + String[] var8 = var7; + var9 = var7.length; + + for (int var10 = 0; var10 < var9; ++var10) { + String var11 = var8[var10]; + ++var6; + var5 = var11.length(); + var3 = var3 + var11; + } + } else { + while (par2ArrayOfObj[var4] instanceof String) { + String var13 = (String) par2ArrayOfObj[var4++]; + ++var6; + var5 = var13.length(); + var3 = var3 + var13; + } + } + + HashMap var14; + + for (var14 = new HashMap(); var4 < par2ArrayOfObj.length; var4 += 2) { + Character var16 = (Character) par2ArrayOfObj[var4]; + IItemStack var17 = null; + + if (par2ArrayOfObj[var4 + 1] instanceof Item) { + var17 = new IItemStack((Item) par2ArrayOfObj[var4 + 1]); + } else if (par2ArrayOfObj[var4 + 1] instanceof Block) { + var17 = new IItemStack((Block) par2ArrayOfObj[var4 + 1], 1, -1); + } else if (par2ArrayOfObj[var4 + 1] instanceof IItemStack) { + var17 = (IItemStack) par2ArrayOfObj[var4 + 1]; + } + + var14.put(var16, var17); + } + + IItemStack[] var15 = new IItemStack[var5 * var6]; + + for (var9 = 0; var9 < var5 * var6; ++var9) { + char var18 = var3.charAt(var9); + + if (var14.containsKey(Character.valueOf(var18))) { + var15[var9] = ((IItemStack) var14.get(Character.valueOf(var18))).copy(); + } else { + var15[var9] = null; + } + } + RecipeCarpentry newrecipe = new RecipeCarpentry(var5, var6, var15, par1ItemStack); + newrecipe.copy(recipe); + if (var5 == 4 || var6 == 4) + newrecipe.isGlobal = false; + + return newrecipe; + } + + public void copy(RecipeCarpentry recipe) { + this.id = recipe.id; + this.name = recipe.name; + this.availability = recipe.availability; + this.isGlobal = recipe.isGlobal; + this.ignoreDamage = recipe.ignoreDamage; + this.ignoreNBT = recipe.ignoreNBT; + } + + public IItemStack getCraftingItem(int i) { + if (recipeItems == null || i >= recipeItems.length) + return null; + return recipeItems[i]; + } + + public void setCraftingItem(int i, IItemStack item) { + if (i < recipeItems.length) + recipeItems[i] = item; + } + + public boolean isValid() { + if (recipeItems.length == 0 || getRecipeOutput() == null) + return false; + for (IItemStack item : recipeItems) { + if (item != null) + return true; + } + return false; + } + + public String getName() { + return this.name; + } + + public IItemStack getResult() { + return this.getRecipeOutput(); + } + + public boolean isGlobal() { + return this.isGlobal; + } + + public void setIsGlobal(boolean bo) { + this.isGlobal = bo; + } + + public boolean getIgnoreNBT() { + return this.ignoreNBT; + } + + public void setIgnoreNBT(boolean bo) { + this.ignoreNBT = bo; + } + + public boolean getIgnoreDamage() { + return this.ignoreDamage; + } + + public void setIgnoreDamage(boolean bo) { + this.ignoreDamage = bo; + } + + public int getWidth() { + return this.recipeWidth; + } + + public int getHeight() { + return this.recipeHeight; + } + + public void save() { + try { + RecipeController.Instance.saveRecipe(this.writeNBT(true)); + } catch (IOException var2) { + } + + } + + public void delete() { + RecipeController.Instance.delete(this.id); + } + + public IItemStack[] getRecipe() { + return this.recipeItems; + } + + public int getId() { + return this.id; + } + + public RecipeScript getScriptHandler() { + return RecipeController.Instance.carpentryScripts.get(this.id); + } + + public void setScriptHandler(RecipeScript handler) { + RecipeController.Instance.carpentryScripts.put(this.id, handler); + } + + public RecipeScript getOrCreateScriptHandler() { + RecipeScript data = getScriptHandler(); + if (data == null) + setScriptHandler(data = new RecipeScript()); + return data; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/core/src/main/java/noppes/npcs/controllers/data/RecipeScript.java new file mode 100644 index 000000000..409bdb25b --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -0,0 +1,82 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.constants.ScriptContext; +/** + * Script handler for Recipe scripts (both carpentry and anvil). + * Manages a single script container for recipe hooks (pre, post). + */ +public class RecipeScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** + * The recipe ID for packet communication. -1 if not bound. + */ + private int recipeId = -1; + + /** + * Whether this is an anvil recipe (vs carpentry). + */ + private boolean anvil = false; + + /** + * Create an unbound RecipeScript (for server-side use). + */ + public RecipeScript() { + } + + /** + * Create a RecipeScript bound to a specific recipe (for GUI use). + * + * @param recipeId The ID of the recipe + * @param anvil true for anvil recipe, false for carpentry recipe + */ + public RecipeScript(int recipeId, boolean anvil) { + this.recipeId = recipeId; + this.anvil = anvil; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.RECIPE; + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.RECIPE); + } + + @Override + public String noticeString() { + return "RecipeScript"; + } + + @Override + public void requestData() { + if (recipeId >= 0) + RecipeScriptPacket.Get(anvil, recipeId); + } + + @Override + public void sendSavePacket(int index, int totalCount, INbt nbt) { + if (recipeId >= 0) + RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, nbt); + } + + public enum ScriptType { + PRE("pre"), + POST("post"); + public final String function; + + ScriptType(String functionName) { + this.function = functionName; + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/RecipesDefault.java b/core/src/main/java/noppes/npcs/controllers/data/RecipesDefault.java new file mode 100644 index 000000000..dd4a14ebd --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/RecipesDefault.java @@ -0,0 +1,199 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomItems; +import noppes.npcs.config.ConfigItem; +import noppes.npcs.controllers.RecipeController; + +public class RecipesDefault { + public static void addRecipe(String name, Object ob, boolean isGlobal, Object... recipe) { + IItemStack item; + if (ob instanceof Item) + item = new IItemStack((Item) ob); + else if (ob instanceof Block) + item = new IItemStack((Block) ob); + else + item = (IItemStack) ob; + + RecipeCarpentry recipeCarpentry = new RecipeCarpentry(name); + recipeCarpentry.isGlobal = isGlobal; + recipeCarpentry = RecipeCarpentry.saveRecipe(recipeCarpentry, item, recipe); + RecipeController.Instance.addRecipe(recipeCarpentry); + } + + public static void loadDefaultRecipes(int i) { + if (i < 0) { + addRecipe("Npc Wand", CustomItems.wand, true, "XX", " Y", " Y", 'X', Items.bread, 'Y', Items.stick); + addRecipe("Mob Cloner", CustomItems.cloner, true, "XX", "XY", " Y", 'X', Items.bread, 'Y', Items.stick); + addRecipe("Carpentry Bench", CustomItems.carpentyBench, true, "XYX", "Z Z", "Z Z", 'X', Blocks.planks, 'Z', Items.stick, 'Y', Blocks.crafting_table); + + IItemStack anvil = new IItemStack(CustomItems.carpentyBench); + anvil.setItemDamage(1); + addRecipe("Anvil", anvil, true, "XXX", "Z Z", "Z Z", 'X', Blocks.iron_block, 'Z', Items.iron_ingot); + + if (!ConfigItem.DisableExtraItems) { + addRecipe("Mana", CustomItems.mana, true, "XY", 'X', Items.redstone, 'Y', Items.glowstone_dust); + addRecipe("Gun Wooden", CustomItems.gunWood, false, "XXXY", " ZM ", " M ", 'Y', Blocks.lever, 'M', Items.stick, 'Z', Blocks.stone_button, 'X', Blocks.planks); + addRecipe("Gun Stone", CustomItems.gunStone, false, "XXXY", " ZM ", " M ", 'Y', Blocks.lever, 'M', Items.stick, 'Z', Blocks.stone_button, 'X', Blocks.cobblestone); + addRecipe("Gun Iron", CustomItems.gunIron, false, "XXXY", " ZM ", " M ", 'Y', Blocks.lever, 'M', Items.stick, 'Z', Blocks.stone_button, 'X', Items.iron_ingot); + addRecipe("Gun Gold", CustomItems.gunGold, false, "XXXY", " ZM ", " M ", 'Y', Blocks.lever, 'M', Items.stick, 'Z', Blocks.stone_button, 'X', Items.gold_ingot); + addRecipe("Gun Diamond", CustomItems.gunDiamond, false, "XXXY", " ZM ", " M ", 'Y', Blocks.lever, 'M', Items.stick, 'Z', Blocks.stone_button, 'X', Items.diamond); + addRecipe("Gun Emerald", CustomItems.gunEmerald, false, "XXXY", " ZM ", " M ", 'Y', Blocks.lever, 'M', Items.stick, 'Z', Blocks.stone_button, 'X', Items.emerald); + addRecipe("Gun Bronze", CustomItems.gunBronze, false, "XXXY", " ZM ", " M ", 'Y', Blocks.lever, 'M', Items.stick, 'Z', Blocks.stone_button, 'X', CustomItems.bronze_ingot); + + addRecipe("Bullet Wooden", CustomItems.bulletWood, true, "X", 'X', Blocks.planks); + addRecipe("Bullet Stone", CustomItems.bulletStone, true, "X", 'X', Blocks.cobblestone); + addRecipe("Bullet Iron", CustomItems.bulletIron, true, "X", 'X', Items.iron_ingot); + addRecipe("Bullet Gold", CustomItems.bulletGold, true, "X", 'X', Items.gold_ingot); + addRecipe("Bullet Diamond", CustomItems.bulletDiamond, true, "X", 'X', Items.diamond); + addRecipe("Bullet Emerald", CustomItems.bulletEmerald, true, "X", 'X', Items.emerald); + addRecipe("Bullet Bronze", CustomItems.bulletBronze, true, "X", 'X', CustomItems.bronze_ingot); + } + } + if (i < 1) { + if (!ConfigItem.DisableExtraBlock) { + addRecipe("WallBanner Wooden", new IItemStack(CustomItems.wallBanner, 1, 0), false, "XXX", "ZZZ", "ZZZ", "Z Z", 'Z', Blocks.wool, 'X', Blocks.planks); + addRecipe("WallBanner Stone", new IItemStack(CustomItems.wallBanner, 1, 1), false, "XXX", "ZZZ", "ZZZ", "Z Z", 'Z', Blocks.wool, 'X', Blocks.cobblestone); + addRecipe("WallBanner Iron", new IItemStack(CustomItems.wallBanner, 1, 2), false, "XXX", "ZZZ", "ZZZ", "Z Z", 'Z', Blocks.wool, 'X', Items.iron_ingot); + addRecipe("WallBanner Gold", new IItemStack(CustomItems.wallBanner, 1, 3), false, "XXX", "ZZZ", "ZZZ", "Z Z", 'Z', Blocks.wool, 'X', Items.gold_ingot); + addRecipe("WallBanner Diamond", new IItemStack(CustomItems.wallBanner, 1, 4), false, "XXX", "ZZZ", "ZZZ", "Z Z", 'Z', Blocks.wool, 'X', Items.diamond); + + addRecipe("Banner Wooden", new IItemStack(CustomItems.banner, 1, 0), false, " X ", " Z ", " Z ", "ZZZ", 'X', new IItemStack(CustomItems.wallBanner, 1, 0), 'Z', Blocks.planks); + addRecipe("Banner Stone", new IItemStack(CustomItems.banner, 1, 1), false, " X ", " Z ", " Z ", "ZZZ", 'X', new IItemStack(CustomItems.wallBanner, 1, 1), 'Z', Blocks.cobblestone); + addRecipe("Banner Iron", new IItemStack(CustomItems.banner, 1, 2), false, " X ", " Z ", " Z ", "ZZZ", 'X', new IItemStack(CustomItems.wallBanner, 1, 2), 'Z', Items.iron_ingot); + addRecipe("Banner Gold", new IItemStack(CustomItems.banner, 1, 3), false, " X ", " Z ", " Z ", "ZZZ", 'X', new IItemStack(CustomItems.wallBanner, 1, 3), 'Z', Items.gold_ingot); + addRecipe("Banner Diamond", new IItemStack(CustomItems.banner, 1, 4), false, " X ", " Z ", " Z ", "ZZZ", 'X', new IItemStack(CustomItems.wallBanner, 1, 4), 'Z', Items.diamond); + + addRecipe("Lamp Wooden", new IItemStack(CustomItems.tallLamp, 1, 0), false, "YXY", " Z ", " Z ", "ZZZ", 'X', Blocks.torch, 'Y', Blocks.wool, 'Z', Blocks.planks); + addRecipe("Lamp Stone", new IItemStack(CustomItems.tallLamp, 1, 1), false, "YXY", " Z ", " Z ", "ZZZ", 'X', Blocks.torch, 'Y', Blocks.wool, 'Z', Blocks.cobblestone); + addRecipe("Lamp Iron", new IItemStack(CustomItems.tallLamp, 1, 2), false, "YXY", " Z ", " Z ", "ZZZ", 'X', Blocks.torch, 'Y', Blocks.wool, 'Z', Items.iron_ingot); + addRecipe("Lamp Gold", new IItemStack(CustomItems.tallLamp, 1, 3), false, "YXY", " Z ", " Z ", "ZZZ", 'X', Blocks.torch, 'Y', Blocks.wool, 'Z', Items.gold_ingot); + addRecipe("Lamp Diamond", new IItemStack(CustomItems.tallLamp, 1, 4), false, "YXY", " Z ", " Z ", "ZZZ", 'X', Blocks.torch, 'Y', Blocks.wool, 'Z', Items.diamond); + + addRecipe("Chair Wooden1", new IItemStack(CustomItems.chair, 1, 0), false, " X", " X", "XXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 0)); + addRecipe("Chair Wooden2", new IItemStack(CustomItems.chair, 1, 1), false, " X", " X", "XXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 1)); + addRecipe("Chair Wooden3", new IItemStack(CustomItems.chair, 1, 2), false, " X", " X", "XXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 2)); + addRecipe("Chair Wooden4", new IItemStack(CustomItems.chair, 1, 3), false, " X", " X", "XXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 3)); + addRecipe("Chair Wooden5", new IItemStack(CustomItems.chair, 1, 4), false, " X", " X", "XXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 4)); + addRecipe("Chair Wooden6", new IItemStack(CustomItems.chair, 1, 5), false, " X", " X", "XXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 5)); + + addRecipe("Crate Wooden1", new IItemStack(CustomItems.crate, 1, 0), false, "XXXX", "X X", "X X", "XXXX", 'X', new IItemStack(Blocks.planks, 1, 0)); + addRecipe("Crate Wooden2", new IItemStack(CustomItems.crate, 1, 1), false, "XXXX", "X X", "X X", "XXXX", 'X', new IItemStack(Blocks.planks, 1, 1)); + addRecipe("Crate Wooden3", new IItemStack(CustomItems.crate, 1, 2), false, "XXXX", "X X", "X X", "XXXX", 'X', new IItemStack(Blocks.planks, 1, 2)); + addRecipe("Crate Wooden4", new IItemStack(CustomItems.crate, 1, 3), false, "XXXX", "X X", "X X", "XXXX", 'X', new IItemStack(Blocks.planks, 1, 3)); + addRecipe("Crate Wooden5", new IItemStack(CustomItems.crate, 1, 4), false, "XXXX", "X X", "X X", "XXXX", 'X', new IItemStack(Blocks.planks, 1, 4)); + addRecipe("Crate Wooden6", new IItemStack(CustomItems.crate, 1, 5), false, "XXXX", "X X", "X X", "XXXX", 'X', new IItemStack(Blocks.planks, 1, 5)); + + addRecipe("WeaponRack Wooden1", new IItemStack(CustomItems.weaponsRack, 1, 0), false, "XXX", "XYX", "XYX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 0), 'Y', Items.stick); + addRecipe("WeaponRack Wooden2", new IItemStack(CustomItems.weaponsRack, 1, 1), false, "XXX", "XYX", "XYX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 1), 'Y', Items.stick); + addRecipe("WeaponRack Wooden3", new IItemStack(CustomItems.weaponsRack, 1, 2), false, "XXX", "XYX", "XYX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 2), 'Y', Items.stick); + addRecipe("WeaponRack Wooden4", new IItemStack(CustomItems.weaponsRack, 1, 3), false, "XXX", "XYX", "XYX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 3), 'Y', Items.stick); + addRecipe("WeaponRack Wooden5", new IItemStack(CustomItems.weaponsRack, 1, 4), false, "XXX", "XYX", "XYX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 4), 'Y', Items.stick); + addRecipe("WeaponRack Wooden6", new IItemStack(CustomItems.weaponsRack, 1, 5), false, "XXX", "XYX", "XYX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 5), 'Y', Items.stick); + + addRecipe("Couch Wooden1", new IItemStack(CustomItems.couchWood, 1, 0), false, " X", " X", "XXXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 0)); + addRecipe("Couch Wooden2", new IItemStack(CustomItems.couchWood, 1, 1), false, " X", " X", "XXXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 1)); + addRecipe("Couch Wooden3", new IItemStack(CustomItems.couchWood, 1, 2), false, " X", " X", "XXXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 2)); + addRecipe("Couch Wooden4", new IItemStack(CustomItems.couchWood, 1, 3), false, " X", " X", "XXXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 3)); + addRecipe("Couch Wooden5", new IItemStack(CustomItems.couchWood, 1, 4), false, " X", " X", "XXXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 4)); + addRecipe("Couch Wooden6", new IItemStack(CustomItems.couchWood, 1, 5), false, " X", " X", "XXXX", "X X", 'X', new IItemStack(Blocks.planks, 1, 5)); + + addRecipe("Couch Wool1", new IItemStack(CustomItems.couchWool, 1, 0), false, " Z", " Z", "ZZZZ", "X X", 'X', new IItemStack(Blocks.planks, 1, 0), 'Z', Blocks.wool); + addRecipe("Couch Wool2", new IItemStack(CustomItems.couchWool, 1, 1), false, " Z", " Z", "ZZZZ", "X X", 'X', new IItemStack(Blocks.planks, 1, 1), 'Z', Blocks.wool); + addRecipe("Couch Wool3", new IItemStack(CustomItems.couchWool, 1, 2), false, " Z", " Z", "ZZZZ", "X X", 'X', new IItemStack(Blocks.planks, 1, 2), 'Z', Blocks.wool); + addRecipe("Couch Wool4", new IItemStack(CustomItems.couchWool, 1, 3), false, " Z", " Z", "ZZZZ", "X X", 'X', new IItemStack(Blocks.planks, 1, 3), 'Z', Blocks.wool); + addRecipe("Couch Wool5", new IItemStack(CustomItems.couchWool, 1, 4), false, " Z", " Z", "ZZZZ", "X X", 'X', new IItemStack(Blocks.planks, 1, 4), 'Z', Blocks.wool); + addRecipe("Couch Wool6", new IItemStack(CustomItems.couchWool, 1, 5), false, " Z", " Z", "ZZZZ", "X X", 'X', new IItemStack(Blocks.planks, 1, 5), 'Z', Blocks.wool); + + addRecipe("Table Wood1", new IItemStack(CustomItems.table, 1, 0), false, "XXXX", "X X", "X X", 'X', new IItemStack(Blocks.planks, 1, 0), 'Z', Blocks.wool); + addRecipe("Table Wood2", new IItemStack(CustomItems.table, 1, 1), false, "XXXX", "X X", "X X", 'X', new IItemStack(Blocks.planks, 1, 1), 'Z', Blocks.wool); + addRecipe("Table Wood3", new IItemStack(CustomItems.table, 1, 2), false, "XXXX", "X X", "X X", 'X', new IItemStack(Blocks.planks, 1, 2), 'Z', Blocks.wool); + addRecipe("Table Wood4", new IItemStack(CustomItems.table, 1, 3), false, "XXXX", "X X", "X X", 'X', new IItemStack(Blocks.planks, 1, 3), 'Z', Blocks.wool); + addRecipe("Table Wood5", new IItemStack(CustomItems.table, 1, 4), false, "XXXX", "X X", "X X", 'X', new IItemStack(Blocks.planks, 1, 4), 'Z', Blocks.wool); + addRecipe("Table Wood6", new IItemStack(CustomItems.table, 1, 5), false, "XXXX", "X X", "X X", 'X', new IItemStack(Blocks.planks, 1, 5), 'Z', Blocks.wool); + + addRecipe("noppes.npcs.client.model.blocks.Stool Wood1", new IItemStack(CustomItems.stool, 1, 0), false, "XXX", " X ", "X X", 'X', new IItemStack(Blocks.planks, 1, 0)); + addRecipe("noppes.npcs.client.model.blocks.Stool Wood2", new IItemStack(CustomItems.stool, 1, 1), false, "XXX", " X ", "X X", 'X', new IItemStack(Blocks.planks, 1, 1)); + addRecipe("noppes.npcs.client.model.blocks.Stool Wood3", new IItemStack(CustomItems.stool, 1, 2), false, "XXX", " X ", "X X", 'X', new IItemStack(Blocks.planks, 1, 2)); + addRecipe("noppes.npcs.client.model.blocks.Stool Wood4", new IItemStack(CustomItems.stool, 1, 3), false, "XXX", " X ", "X X", 'X', new IItemStack(Blocks.planks, 1, 3)); + addRecipe("noppes.npcs.client.model.blocks.Stool Wood5", new IItemStack(CustomItems.stool, 1, 4), false, "XXX", " X ", "X X", 'X', new IItemStack(Blocks.planks, 1, 4)); + addRecipe("noppes.npcs.client.model.blocks.Stool Wood6", new IItemStack(CustomItems.stool, 1, 5), false, "XXX", " X ", "X X", 'X', new IItemStack(Blocks.planks, 1, 5)); + + addRecipe("Barrel Wood1", new IItemStack(CustomItems.barrel, 1, 0), false, "XXX", "X X", "X X", "XXX", 'X', new IItemStack(Blocks.planks, 1, 0)); + addRecipe("Barrel Wood2", new IItemStack(CustomItems.barrel, 1, 1), false, "XXX", "X X", "X X", "XXX", 'X', new IItemStack(Blocks.planks, 1, 1)); + addRecipe("Barrel Wood3", new IItemStack(CustomItems.barrel, 1, 2), false, "XXX", "X X", "X X", "XXX", 'X', new IItemStack(Blocks.planks, 1, 2)); + addRecipe("Barrel Wood4", new IItemStack(CustomItems.barrel, 1, 3), false, "XXX", "X X", "X X", "XXX", 'X', new IItemStack(Blocks.planks, 1, 3)); + addRecipe("Barrel Wood5", new IItemStack(CustomItems.barrel, 1, 4), false, "XXX", "X X", "X X", "XXX", 'X', new IItemStack(Blocks.planks, 1, 4)); + addRecipe("Barrel Wood6", new IItemStack(CustomItems.barrel, 1, 5), false, "XXX", "X X", "X X", "XXX", 'X', new IItemStack(Blocks.planks, 1, 5)); + + addRecipe("Shelf Wood1", new IItemStack(CustomItems.shelf, 2, 0), false, "XXX", "XY ", "X ", 'Y', Items.stick, 'X', new IItemStack(Blocks.planks, 1, 0)); + addRecipe("Shelf Wood2", new IItemStack(CustomItems.shelf, 2, 1), false, "XXX", "XY ", "X ", 'Y', Items.stick, 'X', new IItemStack(Blocks.planks, 1, 1)); + addRecipe("Shelf Wood3", new IItemStack(CustomItems.shelf, 2, 2), false, "XXX", "XY ", "X ", 'Y', Items.stick, 'X', new IItemStack(Blocks.planks, 1, 2)); + addRecipe("Shelf Wood4", new IItemStack(CustomItems.shelf, 2, 3), false, "XXX", "XY ", "X ", 'Y', Items.stick, 'X', new IItemStack(Blocks.planks, 1, 3)); + addRecipe("Shelf Wood5", new IItemStack(CustomItems.shelf, 2, 4), false, "XXX", "XY ", "X ", 'Y', Items.stick, 'X', new IItemStack(Blocks.planks, 1, 4)); + addRecipe("Shelf Wood6", new IItemStack(CustomItems.shelf, 2, 5), false, "XXX", "XY ", "X ", 'Y', Items.stick, 'X', new IItemStack(Blocks.planks, 1, 5)); + + addRecipe("Beam Wood1", new IItemStack(CustomItems.beam, 2, 0), false, "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 0)); + addRecipe("Beam Wood2", new IItemStack(CustomItems.beam, 2, 1), false, "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 1)); + addRecipe("Beam Wood3", new IItemStack(CustomItems.beam, 2, 2), false, "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 2)); + addRecipe("Beam Wood4", new IItemStack(CustomItems.beam, 2, 3), false, "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 3)); + addRecipe("Beam Wood5", new IItemStack(CustomItems.beam, 2, 4), false, "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 4)); + addRecipe("Beam Wood6", new IItemStack(CustomItems.beam, 2, 5), false, "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 5)); + + addRecipe("Sign Wood1", new IItemStack(CustomItems.sign, 1, 0), false, "YYY", "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 0), 'Y', Items.iron_ingot); + addRecipe("Sign Wood2", new IItemStack(CustomItems.sign, 1, 1), false, "YYY", "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 1), 'Y', Items.iron_ingot); + addRecipe("Sign Wood3", new IItemStack(CustomItems.sign, 1, 2), false, "YYY", "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 2), 'Y', Items.iron_ingot); + addRecipe("Sign Wood4", new IItemStack(CustomItems.sign, 1, 3), false, "YYY", "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 3), 'Y', Items.iron_ingot); + addRecipe("Sign Wood5", new IItemStack(CustomItems.sign, 1, 4), false, "YYY", "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 4), 'Y', Items.iron_ingot); + addRecipe("Sign Wood6", new IItemStack(CustomItems.sign, 1, 5), false, "YYY", "XXX", "XXX", 'X', new IItemStack(Blocks.planks, 1, 5), 'Y', Items.iron_ingot); + + addRecipe("Lamp", new IItemStack(CustomItems.lantern), false, "XXX", "YZY", "XXX", 'X', Items.iron_ingot, 'Y', Blocks.glass, 'Z', Blocks.torch); + addRecipe("Candle", new IItemStack(CustomItems.candle), false, "XZX", " X ", 'X', Items.iron_ingot, 'Z', Blocks.torch); + addRecipe("BigSign", new IItemStack(CustomItems.bigsign, 2), false, "XXX", "XXX", "XXX", 'X', Blocks.planks); + + } + if (!ConfigItem.DisableExtraItems) { + addRecipe("Battle Axe1", CustomItems.battleAxeWood, false, "XX", "XY", " Y", " Y", 'Y', Items.stick, 'X', Blocks.planks); + addRecipe("Battle Axe2", CustomItems.battleAxeStone, false, "XX", "XY", " Y", " Y", 'Y', Items.stick, 'X', Blocks.cobblestone); + addRecipe("Battle Axe3", CustomItems.battleAxeIron, false, "XX", "XY", " Y", " Y", 'Y', Items.stick, 'X', Items.iron_ingot); + addRecipe("Battle Axe4", CustomItems.battleAxeGold, false, "XX", "XY", " Y", " Y", 'Y', Items.stick, 'X', Items.gold_ingot); + addRecipe("Battle Axe5", CustomItems.battleAxeDiamond, false, "XX", "XY", " Y", " Y", 'Y', Items.stick, 'X', Items.diamond); + addRecipe("Battle Axe6", CustomItems.battleAxeBronze, false, "XX", "XY", " Y", " Y", 'Y', Items.stick, 'X', CustomItems.bronze_ingot); + addRecipe("Battle Axe7", CustomItems.battleAxeEmerald, false, "XX", "XY", " Y", " Y", 'Y', Items.stick, 'X', Items.emerald); + + addRecipe("Halberd1", CustomItems.halberdWood, false, " X ", "XYX", " Y ", " Y ", 'Y', Items.stick, 'X', Blocks.planks); + addRecipe("Halberd2", CustomItems.halberdStone, false, " X ", "XYX", " Y ", " Y ", 'Y', Items.stick, 'X', Blocks.cobblestone); + addRecipe("Halberd3", CustomItems.halberdIron, false, " X ", "XYX", " Y ", " Y ", 'Y', Items.stick, 'X', Items.iron_ingot); + addRecipe("Halberd4", CustomItems.halberdGold, false, " X ", "XYX", " Y ", " Y ", 'Y', Items.stick, 'X', Items.gold_ingot); + addRecipe("Halberd5", CustomItems.halberdDiamond, false, " X ", "XYX", " Y ", " Y ", 'Y', Items.stick, 'X', Items.diamond); + addRecipe("Halberd6", CustomItems.halberdBronze, false, " X ", "XYX", " Y ", " Y ", 'Y', Items.stick, 'X', CustomItems.bronze_ingot); + addRecipe("Halberd7", CustomItems.halberdEmerald, false, " X ", "XYX", " Y ", " Y ", 'Y', Items.stick, 'X', Items.emerald); + + addRecipe("Glaive1", CustomItems.glaiveWood, false, "X ", " Y ", " Y ", " X", 'Y', Items.stick, 'X', Blocks.planks); + addRecipe("Glaive2", CustomItems.glaiveStone, false, "X ", " Y ", " Y ", " X", 'Y', Items.stick, 'X', Blocks.cobblestone); + addRecipe("Glaive3", CustomItems.glaiveIron, false, "X ", " Y ", " Y ", " X", 'Y', Items.stick, 'X', Items.iron_ingot); + addRecipe("Glaive4", CustomItems.glaiveGold, false, "X ", " Y ", " Y ", " X", 'Y', Items.stick, 'X', Items.gold_ingot); + addRecipe("Glaive5", CustomItems.glaiveDiamond, false, "X ", " Y ", " Y ", " X", 'Y', Items.stick, 'X', Items.diamond); + addRecipe("Glaive6", CustomItems.glaiveBronze, false, "X ", " Y ", " Y ", " X", 'Y', Items.stick, 'X', CustomItems.bronze_ingot); + addRecipe("Glaive7", CustomItems.glaiveEmerald, false, "X ", " Y ", " Y ", " X", 'Y', Items.stick, 'X', Items.emerald); + + addRecipe("Trident1", CustomItems.tridentWood, false, "X X", " X ", " Y ", " Y ", 'Y', Items.stick, 'X', Blocks.planks); + addRecipe("Trident2", CustomItems.tridentStone, false, "X X", " X ", " Y ", " Y ", 'Y', Items.stick, 'X', Blocks.cobblestone); + addRecipe("Trident3", CustomItems.tridentIron, false, "X X", " X ", " Y ", " Y ", 'Y', Items.stick, 'X', Items.iron_ingot); + addRecipe("Trident4", CustomItems.tridentGold, false, "X X", " X ", " Y ", " Y ", 'Y', Items.stick, 'X', Items.gold_ingot); + addRecipe("Trident5", CustomItems.tridentDiamond, false, "X X", " X ", " Y ", " Y ", 'Y', Items.stick, 'X', Items.diamond); + addRecipe("Trident6", CustomItems.tridentBronze, false, "X X", " X ", " Y ", " Y ", 'Y', Items.stick, 'X', CustomItems.bronze_ingot); + addRecipe("Trident7", CustomItems.tridentEmerald, false, "X X", " X ", " Y ", " Y ", 'Y', Items.stick, 'X', Items.emerald); + + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/ScreenSize.java b/core/src/main/java/noppes/npcs/controllers/data/ScreenSize.java new file mode 100644 index 000000000..9b0d76959 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/ScreenSize.java @@ -0,0 +1,11 @@ +package noppes.npcs.controllers.data; + +/** + * TEMPORARY STUB -- auto-generated for compilation. + * Screen size data class. + */ +public class ScreenSize { + public int width; + public int height; + public ScreenSize(int w, int h) { this.width = w; this.height = h; } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/core/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java new file mode 100644 index 000000000..223e7e555 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -0,0 +1,77 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.CustomNpcs; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ScriptHandler implements IScriptHandler { + public List scripts = new ArrayList<>(); + protected String scriptLanguage = "ECMAScript"; + protected boolean enabled = false; + protected long lastInited = -1; + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && CustomNpcs.proxy.isScriptingEnabled() && scripts != null && !scripts.isEmpty(); + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) + return; + + if (ScriptController.Instance.lastLoaded > lastInited) { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : scripts) { + if (script != null) + script.setErrored(false); + } + } + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(hookName, event); + } + } + + @Override + public boolean getEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getLanguage() { + return scriptLanguage; + } + + @Override + public void setLanguage(String language) { + this.scriptLanguage = language; + } + + public void setScripts(List list) { + this.scripts = list; + } + + public List getScripts() { + return this.scripts; + } + + public void clear() { + this.scripts = new ArrayList<>(); + } + + /** + * Reset the lastInited timestamp to force re-initialization on next script call. + */ + public void resetLastInited() { + this.lastInited = -1; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/core/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java new file mode 100644 index 000000000..b53e250fb --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -0,0 +1,158 @@ +package noppes.npcs.controllers.data; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.CustomNpcs; +import noppes.npcs.controllers.ScriptController; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Base class for script handlers that manage a single script container. + * Provides shared NBT/ByteBuf serialization and packet handling. + *

+ * Subclasses should: + * - Store their parent object (e.g., CustomEffect, LinkedItem) + * - Override getHookContext() to return the appropriate context string + * - Override getContext() if needed (default is GLOBAL) + * - Override requestData() and sendSavePacket() for packet communication + */ +public abstract class SingleScriptHandler extends ScriptHandler { + public IScriptUnit container; + + @Override + public boolean isSingleContainer() { + return true; + } + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && CustomNpcs.proxy.isScriptingEnabled() && container != null; + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + container.run(hookName, event); + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty()) { + container = null; + this.scripts = new ArrayList<>(); + } else { + container = list.get(0); + this.scripts = new ArrayList<>(1); + this.scripts.add(container); + } + } + + @Override + public List getScripts() { + if (container == null) + return new ArrayList<>(); + + return Collections.singletonList(container); + } + + public void addScriptUnit(IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void replaceScriptUnit(int index, IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void removeScriptUnit(int index) { + this.container = null; + this.scripts.clear(); + } + + /** + * Write script data to NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public INbt writeToNBT(INbt compound) { + compound.setString("ScriptLanguage", scriptLanguage); + compound.setBoolean("ScriptEnabled", enabled); + + if (container != null) { + compound.setTag("ScriptContent", container.writeToNBT(new INbt())); + } + return compound; + } + + /** + * Read script data from NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public void readFromNBT(INbt compound) { + scriptLanguage = compound.getString("ScriptLanguage"); + enabled = compound.getBoolean("ScriptEnabled"); + + if (compound.hasKey("ScriptContent", NbtConstants.TAG_COMPOUND)) { + container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); + } + } + + /** + * Save script data from a ByteBuf packet. + * Handles the tab/totalScripts protocol used by script packets. + *

+ * Protocol: + * - tab == 0: Script content NBT follows + * - tab != 0 (typically -1): Script metadata (language, enabled) follows + */ + public void saveScript(ByteBuf buffer) throws IOException { + int tab = buffer.readInt(); + int totalScripts = buffer.readInt(); + + if (totalScripts == 0) { + this.container = null; + } + + if (tab == 0) { + // Script content + INbt tabCompound = ByteBufUtils.readNBT(buffer); + this.container = IScriptUnit.createFromNBT(tabCompound, this); + } else { + // Metadata (language, enabled) + INbt compound = ByteBufUtils.readNBT(buffer); + this.setLanguage(compound.getString("ScriptLanguage")); + normalizeLanguage(); + this.setEnabled(compound.getBoolean("ScriptEnabled")); + } + } + + /** + * Normalize the language to a valid one if the current language is not available. + */ + protected void normalizeLanguage() { + if (this.getLanguage() == null || this.getLanguage().isEmpty()) { + if (!ScriptController.Instance.languages.isEmpty()) { + this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); + } else { + this.setLanguage("ECMAScript"); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/SpawnData.java b/core/src/main/java/noppes/npcs/controllers/data/SpawnData.java new file mode 100644 index 000000000..51f2a8e81 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/SpawnData.java @@ -0,0 +1,276 @@ +package noppes.npcs.controllers.data; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.data.INaturalSpawn; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.NBTTags; + +public class SpawnData extends WeightedRandom.Item implements INaturalSpawn { + public static final int DESPAWN_FORCE_NATURAL = INaturalSpawn.DESPAWN_FORCE_NATURAL; + public static final int DESPAWN_PRESERVE_TEMPLATE = INaturalSpawn.DESPAWN_PRESERVE_TEMPLATE; + public static final int DESPAWN_FORCE_PERSISTENT = INaturalSpawn.DESPAWN_FORCE_PERSISTENT; + + public List biomes = new ArrayList<>(); + public HashSet dimensions = new HashSet<>(); + public int id = -1; + public String name = ""; + + public HashMap spawnCompounds = new HashMap<>(); + + public boolean animalSpawning = true; + public boolean monsterSpawning = false; + public boolean liquidSpawning = false; + public boolean airSpawning = false; + + public int spawnHeightMin; + public int spawnHeightMax = 100; + public int maxAlive = 0; + public int cooldownTicks = 0; + public int attemptsPerCycle = 1; + public int playerMinDistance = 24; + public int despawnMode = DESPAWN_FORCE_NATURAL; + + public SpawnData() { + super(10); + } + + public void readNBT(INbt compound) { + id = compound.getInteger("SpawnId"); + name = compound.getString("SpawnName"); + itemWeight = compound.getInteger("SpawnWeight"); + if (itemWeight == 0) + itemWeight = 1; + + biomes = NBTTags.getStringList(compound.getTagList("SpawnBiomes", 10)); + if (!compound.hasKey("SpawnDimensions")) { + dimensions.addAll(Arrays.asList(DimensionManager.getStaticDimensionIDs())); + } else { + dimensions = NBTTags.getIntegerSet(compound.getTagList("SpawnDimensions", 10)); + } + + this.spawnCompounds.clear(); + Set keys = compound.func_150296_c(); + for (Object key : keys) { + if (((String) key).startsWith("SpawnCompound")) { + int i = Integer.parseInt(((String) key).replace("SpawnCompound", "")); + this.spawnCompounds.put(i, compound.getCompoundTag("SpawnCompound" + i)); + } + } + + animalSpawning = compound.getBoolean("AnimalSpawning"); + monsterSpawning = compound.getBoolean("MonsterSpawning"); + liquidSpawning = compound.getBoolean("LiquidSpawning"); + airSpawning = compound.getBoolean("CaveSpawning"); + + spawnHeightMin = compound.getInteger("HeightMin"); + if (!compound.hasKey("HeightMax")) { + spawnHeightMax = 100; + } else { + spawnHeightMax = compound.getInteger("HeightMax"); + } + + maxAlive = compound.hasKey("MaxAlive") ? Math.max(0, compound.getInteger("MaxAlive")) : 0; + cooldownTicks = compound.hasKey("CooldownTicks") ? Math.max(0, compound.getInteger("CooldownTicks")) : 0; + attemptsPerCycle = compound.hasKey("AttemptsPerCycle") ? Math.max(1, compound.getInteger("AttemptsPerCycle")) : 1; + playerMinDistance = compound.hasKey("PlayerMinDistance") ? Math.max(0, compound.getInteger("PlayerMinDistance")) : 24; + if (compound.hasKey("DespawnMode")) { + despawnMode = compound.getInteger("DespawnMode"); + } else { + despawnMode = DESPAWN_FORCE_NATURAL; + } + if (despawnMode < DESPAWN_FORCE_NATURAL || despawnMode > DESPAWN_FORCE_PERSISTENT) { + despawnMode = DESPAWN_FORCE_NATURAL; + } + } + + public INbt writeNBT(INbt compound) { + compound.setInteger("SpawnId", id); + compound.setString("SpawnName", name); + compound.setInteger("SpawnWeight", itemWeight); + + compound.setTag("SpawnBiomes", NBTTags.nbtStringList(biomes)); + if (!this.dimensions.isEmpty()) { + compound.setTag("SpawnDimensions", NBTTags.nbtIntegerSet(dimensions)); + } + + Set> entries = this.spawnCompounds.entrySet(); + for (Map.Entry entry : entries) { + compound.setTag("SpawnCompound" + entry.getKey(), entry.getValue() != null ? entry.getValue() : new INbt()); + } + + compound.setBoolean("AnimalSpawning", animalSpawning); + compound.setBoolean("MonsterSpawning", monsterSpawning); + compound.setBoolean("LiquidSpawning", liquidSpawning); + compound.setBoolean("CaveSpawning", airSpawning); + + compound.setInteger("HeightMin", spawnHeightMin); + compound.setInteger("HeightMax", spawnHeightMax); + compound.setInteger("MaxAlive", maxAlive); + compound.setInteger("CooldownTicks", cooldownTicks); + compound.setInteger("AttemptsPerCycle", attemptsPerCycle); + compound.setInteger("PlayerMinDistance", playerMinDistance); + compound.setInteger("DespawnMode", despawnMode); + return compound; + } + + public void setName(String name) { + if (name != null && !name.isEmpty()) { + this.name = name; + } + } + + public String getName() { + return this.name; + } + + public void setEntity(IEntity IEntity, int slot) { + INbt compound = new INbt(); + if (IEntity != null && !IEntity.getMCEntity().writeToNBTOptional(compound)) { + throw new CustomNPCsException("IEntity could not be written to NBT"); + } else { + this.spawnCompounds.put(slot, compound); + } + } + + public IEntity getEntity(IWorld IWorld, int slot) { + if (!this.spawnCompounds.containsKey(slot)) { + return null; + } + try { + IEntity IEntity = EntityList.createEntityFromNBT(this.spawnCompounds.get(slot), IWorld.getMCWorld()); + return NpcAPI.Instance().getIEntity(IEntity); + } catch (Exception e) { + throw new CustomNPCsException("Error creating IEntity from spawn data:\n" + e.getMessage()); + } + } + + public Integer[] getSlots() { + return this.spawnCompounds.keySet().toArray(new Integer[0]); + } + + public void setWeight(int weight) { + if (weight < 1) + weight = 1; + if (weight > 100) + weight = 100; + + this.itemWeight = weight; + } + + public int getWeight() { + return this.itemWeight; + } + + public void setMinHeight(int height) { + this.spawnHeightMin = height; + } + + public int getMinHeight() { + return this.spawnHeightMin; + } + + public void setMaxHeight(int height) { + this.spawnHeightMax = height; + } + + public int getMaxHeight() { + return this.spawnHeightMax; + } + + public void spawnsLikeAnimal(boolean spawns) { + this.animalSpawning = spawns; + } + + public boolean spawnsLikeAnimal() { + return animalSpawning; + } + + public void spawnsLikeMonster(boolean spawns) { + this.monsterSpawning = spawns; + } + + public boolean spawnsLikeMonster() { + return monsterSpawning; + } + + public void spawnsInLiquid(boolean spawns) { + this.liquidSpawning = spawns; + } + + public boolean spawnsInLiquid() { + return this.liquidSpawning; + } + + public void spawnsInAir(boolean spawns) { + this.airSpawning = spawns; + } + + public boolean spawnsInAir() { + return this.airSpawning; + } + + public String[] getBiomes() { + return biomes.toArray(new String[]{}); + } + + public void setBiomes(String[] biomes) { + this.biomes = new ArrayList<>(Arrays.asList(biomes)); + } + + public void setMaxAlive(int maxAlive) { + this.maxAlive = Math.max(0, maxAlive); + } + + public int getMaxAlive() { + return this.maxAlive; + } + + public void setCooldownTicks(int ticks) { + this.cooldownTicks = Math.max(0, ticks); + } + + public int getCooldownTicks() { + return this.cooldownTicks; + } + + public void setAttemptsPerCycle(int attempts) { + this.attemptsPerCycle = Math.max(1, attempts); + } + + public int getAttemptsPerCycle() { + return this.attemptsPerCycle; + } + + public void setPlayerMinDistance(int distance) { + this.playerMinDistance = Math.max(0, distance); + } + + public int getPlayerMinDistance() { + return this.playerMinDistance; + } + + public void setDespawnMode(int mode) { + if (mode < DESPAWN_FORCE_NATURAL || mode > DESPAWN_FORCE_PERSISTENT) + mode = DESPAWN_FORCE_NATURAL; + this.despawnMode = mode; + } + + public int getDespawnMode() { + return this.despawnMode; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/TagMap.java b/core/src/main/java/noppes/npcs/controllers/data/TagMap.java index c915eec99..ff13dc125 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/TagMap.java +++ b/core/src/main/java/noppes/npcs/controllers/data/TagMap.java @@ -1,8 +1,5 @@ package noppes.npcs.controllers.data; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; -import noppes.npcs.core.NBT; import java.util.ArrayList; import java.util.Collections; @@ -10,6 +7,16 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.UUID; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.core.NBT; public class TagMap { public int cloneTab; @@ -33,13 +40,13 @@ public class TagMap { INbtList list = compound.getTagList("TagMap", 10); if (list != null) { for (int i = 0; i < list.size(); i++) { - INbt nbttagcompound = list.getCompound(i); - String cloneName = nbttagcompound.getString("Clone"); + INbt INbt = list.getCompound(i); + String cloneName = INbt.getString("Clone"); HashSet uuids = new HashSet(); - INbtList nbtTagList = nbttagcompound.getTagList("TagUUIDs", 8); - for (int j = 0; j < nbtTagList.size(); j++) { - String uuid = nbtTagList.getString(j); + INbtList INbtList = INbt.getTagList("TagUUIDs", 8); + for (int j = 0; j < INbtList.size(); j++) { + String uuid = INbtList.getString(j); if (!uuid.isEmpty()) { uuids.add(UUID.fromString(uuid)); } @@ -57,11 +64,11 @@ public class TagMap { if (uuidSet.size() > 0) { INbt cloneCompound = NBT.compound(); cloneCompound.setString("Clone", key); - INbtList nbtTagList = NBT.list(); + INbtList INbtList = NBT.list(); for (UUID uuid : uuidSet) { - nbtTagList.addString(uuid.toString()); + INbtList.addString(uuid.toString()); } - cloneCompound.setTagList("TagUUIDs", nbtTagList); + cloneCompound.setTagList("TagUUIDs", INbtList); cloneList.addCompound(cloneCompound); } } diff --git a/core/src/main/java/noppes/npcs/controllers/data/TintData.java b/core/src/main/java/noppes/npcs/controllers/data/TintData.java index c72dd164d..2134918fb 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/TintData.java +++ b/core/src/main/java/noppes/npcs/controllers/data/TintData.java @@ -1,7 +1,16 @@ package noppes.npcs.controllers.data; + import noppes.npcs.api.entity.data.ITintData; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; public class TintData implements ITintData { private boolean tintEnabled = false; @@ -11,26 +20,26 @@ public class TintData implements ITintData { private int generalTint = 0x000000; private int generalAlpha = 40; - public INbt writeToNBT(INbt nbttagcompound) { - nbttagcompound.setBoolean("TintEnabled", tintEnabled); + public INbt writeToNBT(INbt INbt) { + INbt.setBoolean("TintEnabled", tintEnabled); if (tintEnabled) { - nbttagcompound.setBoolean("HurtTintEnabled", hurtTintEnabled); - nbttagcompound.setBoolean("GeneralTintEnabled", generalTintEnabled); - nbttagcompound.setInteger("HurtTint", hurtTint); - nbttagcompound.setInteger("GeneralTint", generalTint); - nbttagcompound.setInteger("GeneralAlpha", generalAlpha); + INbt.setBoolean("HurtTintEnabled", hurtTintEnabled); + INbt.setBoolean("GeneralTintEnabled", generalTintEnabled); + INbt.setInteger("HurtTint", hurtTint); + INbt.setInteger("GeneralTint", generalTint); + INbt.setInteger("GeneralAlpha", generalAlpha); } - return nbttagcompound; + return INbt; } - public void readFromNBT(INbt nbttagcompound) { - tintEnabled = nbttagcompound.getBoolean("TintEnabled"); + public void readFromNBT(INbt INbt) { + tintEnabled = INbt.getBoolean("TintEnabled"); if (tintEnabled) { - hurtTintEnabled = nbttagcompound.getBoolean("HurtTintEnabled"); - generalTintEnabled = nbttagcompound.getBoolean("GeneralTintEnabled"); - hurtTint = nbttagcompound.getInteger("HurtTint"); - generalTint = nbttagcompound.getInteger("GeneralTint"); - generalAlpha = nbttagcompound.getInteger("GeneralAlpha"); + hurtTintEnabled = INbt.getBoolean("HurtTintEnabled"); + generalTintEnabled = INbt.getBoolean("GeneralTintEnabled"); + hurtTint = INbt.getInteger("HurtTint"); + generalTint = INbt.getInteger("GeneralTint"); + generalAlpha = INbt.getInteger("GeneralAlpha"); } } diff --git a/core/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/core/src/main/java/noppes/npcs/controllers/data/TraderStock.java index 922de856d..b3c4b3e4b 100644 --- a/core/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/core/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -1,12 +1,19 @@ package noppes.npcs.controllers.data; -import noppes.npcs.api.INbt; -import noppes.npcs.api.INbtList; -import noppes.npcs.core.NBT; -import noppes.npcs.constants.EnumStockReset; import java.util.HashMap; import java.util.Map; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.INbt; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.IWorld; +import noppes.npcs.constants.EnumStockReset; +import noppes.npcs.core.NBT; /** * Manages trader stock system including: @@ -15,11 +22,11 @@ import java.util.Map; * - Automatic stock reset timers */ public class TraderStock { - // Stock configuration + // Stock IConfiguration public boolean enableStock = false; public boolean perPlayer = false; // false = per-server, true = per-player - // Reset configuration (mirrors Quest cooldown system) + // Reset IConfiguration (mirrors Quest cooldown system) public EnumStockReset resetType = EnumStockReset.NONE; public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types (in ticks or ms) @@ -102,7 +109,7 @@ public class TraderStock { /** * Check if stock should reset based on current time * - * @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type + * @param currentTime Current MC IWorld time (ticks) or real time (ms) depending on reset type */ public boolean shouldReset(long currentTime) { if (resetType == EnumStockReset.NONE) { diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/Action.java b/core/src/main/java/noppes/npcs/controllers/data/action/Action.java new file mode 100644 index 000000000..babc4aafd --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/Action.java @@ -0,0 +1,718 @@ +package noppes.npcs.controllers.data.action; + +import noppes.npcs.api.handler.IActionManager; +import noppes.npcs.api.handler.data.IAction; +import noppes.npcs.api.handler.data.IActionQueue; +import noppes.npcs.api.handler.data.actions.IConditionalAction; +import noppes.npcs.controllers.ScriptContainer; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +public class Action implements IAction { + protected final ActionManager manager; + protected IActionQueue queue; + protected boolean isScheduled; + protected final String name; + protected int startAfterTicks; + protected int count, maxCount = -1; + protected int duration, maxDuration = -1; + protected int updateEveryXTick = 5; + protected Consumer task, onStart, onDone; + protected boolean done; + protected final Map dataStore = new HashMap<>(); + protected boolean isThreaded; + protected ActionThread actionThread; + protected ScriptContainer reportTo; + + //allows chaining .before() and .after() on unscheduled actions + protected ActionList unscheduledList; + + public Action(ActionManager manager, String name) { + this.manager = manager; + this.name = name; + + if (ScriptContainer.Current != null) + reportTo = ScriptContainer.Current; + } + + public Action(ActionManager manager, Consumer task) { + this(manager, Integer.toHexString(task.hashCode()), task); + } + + public Action(ActionManager manager, String name, Consumer task) { + this(manager, name); + this.task = task; + } + + public Action(ActionManager manager, int startAfterTicks, Consumer task) { + this(manager, task); + this.startAfterTicks = startAfterTicks; + } + + public Action(ActionManager manager, String name, int startAfterTicks, Consumer task) { + this(manager, name, task); + this.startAfterTicks = startAfterTicks; + } + + public Action(ActionManager manager, int maxDuration, int startAfterTicks, Consumer task) { + this(manager, startAfterTicks, task); + this.maxDuration = maxDuration; + } + + public Action(ActionManager manager, String name, int maxDuration, int startAfterTicks, Consumer task) { + this(manager, name, startAfterTicks, task); + this.maxDuration = maxDuration; + } + + @Override + public IActionManager getManager() { + return manager; + } + + @Override + public String getName() { + return name; + } + + @Override + public IActionQueue getQueue() { + return queue; + } + + public String getQueueName() { + return queue != null ? queue.getName() : "null"; + } + + @Override + public IAction setQueue(IActionQueue queue) { + if (this.queue == queue) + return this; + + if (this.queue != null) + this.queue.getQueue().remove(this); + + this.queue = queue; + queue.schedule(this); + return this; + } + + protected void schedule(IActionQueue queue) { + queue.getQueue().add(this); + + this.queue = queue; + this.isScheduled = true; + + if (manager.debug) + manager.LOGGER.log(String.format("Scheduled %s", getIdentifier()), queue); + } + + @Override + public boolean isScheduled() { + return isScheduled; + } + + @Override + public IAction setTask(Consumer task) { + this.task = task; + return this; + } + + @Override + public IAction onStart(Consumer task) { + this.onStart = task; + return this; + } + + @Override + public IAction onDone(Consumer task) { + this.onDone = task; + return this; + } + + @Override + public int getDuration() { + return duration; + } + + + @Override + public int getMaxDuration() { + return maxDuration; + } + + @Override + public IAction setMaxDuration(int ticks) { + this.maxDuration = Math.max(-1, ticks); + return this; + } + + @Override + public int getStartAfterTicks() { + return startAfterTicks; + } + + @Override + public int getUpdateEvery() { + return updateEveryXTick; + } + + @Override + public IAction updateEvery(int x) { + this.updateEveryXTick = Math.max(1, x); + return this; + } + + @Override + public IAction everyTick() { + this.updateEveryXTick = 1; + return this; + } + + @Override + public IAction everySecond() { + this.updateEveryXTick = 20; + return this; + } + + @Override + public int getCount() { + return count; + } + + @Override + public int getMaxCount() { + return maxCount; + } + + @Override + public IAction times(int n) { + this.maxCount = Math.max(-1, n); + return this; + } + + @Override + public IAction once() { + this.maxCount = 1; + return this; + } + + @Override + public Object getData(String key) { + return dataStore.get(key); + } + + @Override + public IAction setData(String key, Object v) { + dataStore.put(key, v); + return this; + } + + @Override + public IAction removeData(String key) { + dataStore.remove(key); + return this; + } + + @Override + public IAction copyDataTo(IAction to) { + ((Action) to).dataStore.putAll(dataStore); + return this; + } + + @Override + public String printData() { + if (dataStore.isEmpty()) { + return String.format("Action[name=%s] dataStore is empty", name); + } + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Action[name=%s] dataStore contents:\n", name)); + + for (Map.Entry entry : dataStore.entrySet()) { + sb.append(String.format(" %s = %s\n", entry.getKey(), entry.getValue())); + } + + return sb.toString(); + } + + @Override + public boolean hasData(String key) { + return dataStore.containsKey(key); + } + + @Override + public void markDone() { + done = true; + + if (manager.debug) + manager.LOGGER.log("Marked done!", this); + } + + @Override + public boolean isDone() { + return done; + } + + /////////////////////////////////////////////////// + ////////////////////////////////////////////////// + // Listener + +// public IAction listenTo(String hook, Object object) { +// return listenTo(hook, manager.getOrCreateListener(object)); +// } +// +// public IAction listenTo(String hook, IActionListener listener) { +// return listenTo(listener.getOrCreateHook(hook)); +// } +// +// public IAction listenTo(ActionListener.Hook hook) { +// hook.schedule(this); +// return this; +// } + + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// + // Handling + @Override + public IAction start() { + manager.start(); + return this; + } + + public void tick() { + if (done) + return; + + if (startAfterTicks > 0) { + manager.LOGGER.log(String.format("Starts after %s ticks...", startAfterTicks), this); + startAfterTicks--; + return; + } + + if (duration == 0 && onStart != null) + execute("onStart", this::executeOnStart); + + if (done) + return; + + duration++; + + inheritedTick(); + + if (maxDuration > -1 && duration >= maxDuration || maxCount == 0) { + if (manager.debug) + manager.LOGGER.log("Reached max duration", this); + + markDone(); + return; + } + + + if (maxCount > -1 && count >= maxCount) { + if (manager.debug) + manager.LOGGER.log("Reached max count", this); + + markDone(); + } + + } + + public void inheritedTick() { + if (manager.debug) + manager.LOGGER.log(String.format("Ticking... (duration = %s/%s, count = %s/%s)", duration, maxDuration, count, maxCount), this); + + + if (duration % updateEveryXTick == 0 && task != null) + execute("task", this::executeTask); + } + + public void execute(String taskName, Runnable task) { + if (isThreaded) + actionThread.execute(taskName, task); + else + task.run(); + } + + protected void executeOnStart() { + if (manager.debug) + manager.LOGGER.log("Executing onStart task...", this); + + try { + onStart.accept(this); + } catch (Throwable t) { + String err = "IAction" + getName() + " onStart threw an exception:"; + if (reportTo != null) + reportTo.appendConsole(err + "\n" + ExceptionUtils.getStackTrace(t)); + + manager.LOGGER.error("onStart Task of " + this + " threw an exception:", t); + } + + if (manager.debug) + manager.LOGGER.log("Finished executing onStart task", this); + } + + protected void executeTask() { + if (manager.debug) + manager.LOGGER.log("Executing task...", this); + + try { + task.accept(this); + count++; + } catch (Throwable t) { + String err = "IAction '" + name + "' threw an exception:"; + + if (reportTo != null) + reportTo.appendConsole(err + "\n" + ExceptionUtils.getStackTrace(t)); + + manager.LOGGER.error("Task of " + this + " threw an exception:", t); + markDone(); + } + + if (manager.debug) + manager.LOGGER.log(String.format("Finished executing task (count = %s/%s)", count, maxCount), this); + } + + protected void executeOnDone() { + if (manager.debug) + manager.LOGGER.log("Executing onDone task...", this); + + try { + onDone.accept(this); + } catch (Throwable t) { + String err = "IAction" + getName() + " onDone threw an exception:"; + if (reportTo != null) + reportTo.appendConsole(err + "\n" + ExceptionUtils.getStackTrace(t)); + + manager.LOGGER.error("onDone Task of " + this + " threw an exception:", t); + } + + if (manager.debug) + manager.LOGGER.log("Finished executing onDone task", this); + } + + + @Override + public void kill() { + if (manager.debug) + manager.LOGGER.log(String.format("Killing %s...", getIdentifier()), queue); + + if (actionThread != null) + actionThread.stop(); + + dataStore.clear(); + isScheduled = false; + done = true; + + } + + @Override + public String getIdentifier() { + return String.format("%s '%s'", getClass().getSimpleName(), name); + } + + public String toString() { + return String.format("%s [queue='%s', scheduled=%s, done=%s, paused=%s, updateEvery=%s, duration=%d/%d, count=%d/%d, threaded=%s]", getIdentifier(), getQueueName(), isScheduled, done, isPaused(), updateEveryXTick, duration, maxDuration, count, maxCount, isThreaded); + } + + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// + // Thread + @Override + public IAction threadify() { + if (!isThreaded) { + isThreaded = true; + actionThread = new ActionThread(this); + } + + return this; + } + + @Override + public void resume() { + if (isThreaded) + actionThread.resume(); + } + + @Override + public void pause() { + if (isThreaded) + actionThread.pause(); + else + throw new CustomNPCsException("Must threadify() IAction before pausing!"); + } + + @Override + public IAction pauseFor(int ticks) { + if (isThreaded) + actionThread.pauseFor(ticks); + else + this.startAfterTicks = ticks; + + return this; + } + + @Override + public IAction pauseFor(long millis) { + if (isThreaded) { + actionThread.pauseFor(millis); + return this; + } + + return pauseFor((int) (millis / 50)); + } + + @Override + public void pauseUntil(Function until) { + if (isThreaded) + actionThread.pauseUntil(until); + else + throw new CustomNPCsException("Must threadify() IAction before pausing!"); + } + + @Override + public boolean isPaused() { + if (isThreaded) + return actionThread.isPaused(); + + return startAfterTicks > 0; + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Sequential + @Override + public IAction getNext() { + if (queue == null) + return null; + + boolean seenMe = false; + for (IAction a : queue.getQueue()) { + if (seenMe) { + return a; + } + if (a == this) { + seenMe = true; + } + } + return null; + } + + @Override + public IAction getPrevious() { + if (queue == null) + return null; + + IAction prev = null; + for (IAction a : queue.getQueue()) { + if (a == this) { + return prev; + } + prev = a; + } + return null; + } + + @Override + public IAction after(IAction after) { + if (after instanceof IConditionalAction) + return conditional((IConditionalAction) after); + + int idx = queue == null ? -1 : queue.getIndex(this); + if (idx >= 0 && !queue.isParallel()) + queue.scheduleActionAt(idx + 1, after); + else { + if (unscheduledList == null) + unscheduledList = new ActionList(this); + + unscheduledList.after(this, (Action) after); + } + return after; + } + + @Override + public void after(IAction... actions) { + for (int i = 0; i < actions.length - 1; i++) + actions[i].after(actions[i + 1]); + } + + @Override + public void after(Consumer... tasks) { + IAction[] actions = new IAction[tasks.length]; + + for (int i = 0; i < tasks.length; i++) + actions[i] = manager.create(tasks[i]); + + after(actions); + } + + @Override + public IAction after(String name, int maxDuration, int delay, Consumer t) { + return after(manager.create(name, maxDuration, delay, t)); + } + + @Override + public IAction after(String name, int delay, Consumer t) { + return after(manager.create(name, delay, t)); + } + + @Override + public IAction after(int delay, Consumer t) { + return after(manager.create(delay, t)); + } + + @Override + public IAction after(String name, Consumer t) { + return after(manager.create(name, t)); + } + + @Override + public IAction after(Consumer t) { + return after(manager.create(t)); + } + + @Override + public IAction before(IAction before) { + if (before instanceof IConditionalAction) + return conditional((IConditionalAction) before); + + int idx = queue == null ? -1 : queue.getIndex(this); + if (idx >= 0 && !queue.isParallel()) + queue.scheduleActionAt(Math.max(0, idx), before); + else { + if (unscheduledList == null) + unscheduledList = new ActionList(this); + + unscheduledList.before(this, (Action) before); + } + return before; + } + + @Override + public IAction before(String name, int maxDuration, int delay, Consumer t) { + return before(manager.create(name, maxDuration, delay, t)); + } + + @Override + public IAction before(String name, int delay, Consumer t) { + return before(manager.create(name, delay, t)); + } + + @Override + public IAction before(int delay, Consumer t) { + return before(manager.create(delay, t)); + } + + @Override + public IAction before(String name, Consumer t) { + return before(manager.create(name, t)); + } + + @Override + public IAction before(Consumer t) { + return before(manager.create(t)); + } + + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Parallels + @Override + public IAction parallel(IAction act) { + if (unscheduledList != null) + ((Action) act).unscheduledList = unscheduledList; + + if (queue != null && queue.isParallel() && queue != manager.conditionalQueue) + return queue.schedule(act); + + return manager.scheduleParallel(act); + } + + @Override + public void parallel(IAction... actions) { + for (IAction act : actions) + parallel(act); + } + + @Override + public IAction parallel(Consumer task) { + return parallel(manager.create(task)); + } + + @Override + public void parallel(Consumer... tasks) { + for (Consumer task : tasks) + parallel(task); + } + + @Override + public IAction parallel(int delay, Consumer task) { + return parallel(manager.create(delay, task)); + } + + @Override + public IAction parallel(String name, Consumer task) { + return parallel(manager.create(name, task)); + } + + @Override + public IAction parallel(String name, int startAfterTicks, Consumer task) { + return parallel(manager.create(name, startAfterTicks, task)); + } + + @Override + public IAction parallel(String name, int maxDuration, int delay, Consumer t) { + return parallel(manager.create(name, maxDuration, delay, t)); + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Conditionals + @Override + public IConditionalAction conditional(IConditionalAction after) { + if (unscheduledList != null) + ((Action) after).unscheduledList = unscheduledList; + + return manager.schedule(after); + } + + @Override + public void conditional(IConditionalAction... actions) { + for (IConditionalAction act : actions) + conditional(act); + } + + @Override + public IConditionalAction conditional(Function condition, Consumer task) { + return conditional(manager.create(condition, task)); + } + + @Override + public IConditionalAction conditional(String name, Function condition, Consumer task) { + return conditional(manager.create(name, condition, task)); + } + + @Override + public IConditionalAction conditional(Function condition, Consumer task, Function terminateWhen) { + return conditional(manager.create(condition, task, terminateWhen)); + } + + @Override + public IConditionalAction conditional(String name, Function condition, Consumer task, Function terminateWhen) { + return conditional(manager.create(name, condition, task, terminateWhen)); + } + + @Override + public IConditionalAction conditional(Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + return conditional(manager.create(condition, task, terminateWhen, onTermination)); + } + + @Override + public IConditionalAction conditional(String name, Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + return conditional(manager.create(name, condition, task, terminateWhen, onTermination)); + } + +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/ActionList.java b/core/src/main/java/noppes/npcs/controllers/data/action/ActionList.java new file mode 100644 index 000000000..68a44ad43 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/ActionList.java @@ -0,0 +1,58 @@ +package noppes.npcs.controllers.data.action; + +import noppes.npcs.api.handler.data.IActionQueue; + +import java.util.LinkedList; +import java.util.function.Consumer; + +public class ActionList { + + protected final LinkedList list = new LinkedList<>(); + protected final Action first; + protected IActionQueue originalQueue; + + public ActionList(Action action) { + list.addFirst(first = action); + originalQueue = action.getQueue(); + } + + public Action after(Action current, Action aft) { + int currentIndex = list.indexOf(current); + + if (currentIndex == -1) + list.addLast(aft); + else + list.add(currentIndex + 1, aft); + + aft.unscheduledList = this; + return aft; + } + + public Action before(Action current, Action bef) { + int currentIndex = list.indexOf(current); + + if (currentIndex == -1) + list.addLast(bef); + else + list.add(Math.max(0, currentIndex), bef); + + bef.unscheduledList = this; + return bef; + } + + public ActionList scheduleAll(IActionQueue queue) { + list.forEach(act -> act.schedule(queue)); + + return this; + } + + public ActionList forEach(Consumer task) { + list.forEach(task); + return this; + } + + public ActionList kill() { + list.clear(); + return this; + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/ActionListener.java b/core/src/main/java/noppes/npcs/controllers/data/action/ActionListener.java new file mode 100644 index 000000000..01b557a5e --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/ActionListener.java @@ -0,0 +1,91 @@ +package noppes.npcs.controllers.data.action; + +import noppes.npcs.api.handler.data.IAction; +import noppes.npcs.api.handler.data.IActionListener; +import noppes.npcs.api.handler.data.IActionQueue; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ActionListener implements IActionListener { + private static final ActionManager manager = ActionManager.GLOBAL; + + private final Object object; //object listener is attached to + private final Map hooks = new ConcurrentHashMap<>(); + + public ActionListener(Object target) { + this.object = target; + } + + public void tick() { + Iterator> it = hooks.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + Hook hook = entry.getValue(); + ActionQueue hookQueue = (ActionQueue) hook.queue; + hookQueue.tick(); + + if (hookQueue.isDead()) { + hookQueue.clear(); + if (manager.debug) + manager.LOGGER.log(String.format("Removing queue '%s'", hookQueue.getName()), manager); + it.remove(); + } + } + } + + public Object getObject() { + return object; + } + + // @Override + public Hook getOrCreateHook(String hookName) { + return hooks.computeIfAbsent(hookName, Hook::new); + } + + // @Override + public Hook getHook(String hookName) { + return hooks.get(hookName); + } + + // @Override + public boolean fire(String hookName) { + Hook hook = hooks.get(hookName); + if (hook != null) { + hook.fire(); + return true; + } + + return false; + } + + public IAction schedule(String hookName, IAction action) { + return getOrCreateHook(hookName).schedule(action); + } + + public class Hook { + public final String name; + public final IActionQueue queue; + + public Hook(String hookName) { + this.name = hookName; + + String queueName = String.format("ActionListener#%s:%s", object, hookName); + this.queue = manager.createQueue(queueName).stopWhenEmpty(true).killWhenEmpty(false); + } + + public String getName() { + return name; + } + + public void fire() { + queue.getManager().start(); // just in case, ensures the manager is ticking + queue.start(); + } + + public IAction schedule(IAction action) { + return queue.schedule(action); + } + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/ActionLogger.java b/core/src/main/java/noppes/npcs/controllers/data/action/ActionLogger.java new file mode 100644 index 000000000..753419970 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/ActionLogger.java @@ -0,0 +1,204 @@ +package noppes.npcs.controllers.data.action; + +import noppes.npcs.LogWriter; +import noppes.npcs.api.handler.data.IAction; +import noppes.npcs.api.handler.data.IActionQueue; +import noppes.npcs.config.ConfigDebug; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Date; +import java.util.Deque; +import java.util.List; +import java.util.ListIterator; + +public class ActionLogger { + // private final Map> snapshot = new HashMap<>(); + private final ActionManager manager; + private final Deque stack = new ArrayDeque<>(); + private TreeNode root, currentNode; + + static class TreeNode { + public final Object component; //ActionManager, or ActionQueue, or Action, etc... + public final List children = new ArrayList<>(); + public TreeNode parent; + + public boolean isLastChild() { + if (parent == null) + return true; + List siblings = parent.children; + return siblings.get(siblings.size() - 1) == this; + } + + public TreeNode(Object component) { + this.component = component; + } + + public TreeNode addChild(Object component) { + TreeNode child = new TreeNode(component); + children.add(child); + child.parent = this; + return child; + } + } + + public ActionLogger(ActionManager manager) { + this.manager = manager; + } + + public ActionLogger beginTick(ActionManager manager) { + stack.clear(); + + root = currentNode = new TreeNode(manager); + for (IActionQueue queue : manager.getAllQueues()) { + if (!queue.hasActiveTasks()) + continue; + TreeNode queueNode = root.addChild(queue); + + for (IAction action : queue.getQueue()) + queueNode.addChild(action); + } + + return this; + } + + public ActionLogger push(Object component) { + for (TreeNode child : currentNode.children) { + if (child.component.equals(component)) { + currentNode = child; + break; + } + } + + stack.push(currentNode); + return this; + } + + public ActionLogger pop() { + if (!stack.isEmpty()) + currentNode = stack.pop(); + + return this; + } + + public ActionLogger finish(String finished, Object component) { + log(finished, component); + log(StringUtils.repeat("-", finished.length()), component, true); + return this; + } + + public ActionLogger log(String message, Object source) { + return log(message, source, false); + } + + public ActionLogger log(String message, Object source, boolean isLast) { + StringBuilder prefix = new StringBuilder(); + + ListIterator it = new ArrayList<>(stack).listIterator(stack.size()); + + while (it.hasPrevious()) { + TreeNode node = it.previous(); + if (it.hasPrevious()) + prefix.append("│ "); + else + prefix.append(isLast ? "└──── " : "├──── "); + } + + String header = getLogHeader(source); + println(prefix + header + message); + return this; + } + + public void error(String err, Throwable t) { + if (manager.reportTo != null) + manager.reportTo.appendConsole(err + (t != null ? "\n" + ExceptionUtils.getStackTrace(t) : "")); + + LogWriter.error(err); + if (t != null) + t.printStackTrace(); + } + + private String getLogHeader(Object obj) { + String type = obj.getClass().getSimpleName(); + String name = ""; + + if (obj instanceof ActionManager) { + name = ((ActionManager) obj).getName(); + } else if (obj instanceof IActionQueue) { + name = ((IActionQueue) obj).getName(); + } else if (obj instanceof IAction) { + name = ((IAction) obj).getName(); + } else if (obj instanceof ActionThread) { + name = ((ActionThread) obj).getAction().getName(); + } + + return String.format("[%s/'%s'] ", type, name); + } + + private static final String ANSI_CYAN = "\u001B[96m"; + private static final String ANSI_GOLD = "\u001B[93m"; + private static final String ANSI_RESET = "\u001B[0m"; + + public void println(String str) { + if (ConfigDebug.LogActionManager) + System.out.printf("[%tT] %s[%s]:%s %s%s%n", new Date(), ANSI_CYAN, Thread.currentThread().getName(), ANSI_GOLD, str, ANSI_RESET); + } + + // public ActionLogger beginTick1(ActionManager manager) { + // snapshot.clear(); + // stack.clear(); + // + // List queues = Arrays.stream(manager.getAllQueues()).filter(IActionQueue::hasActiveTasks).collect(Collectors.toList()); + // snapshot.put(manager, queues); + // + // for (IActionQueue queue : queues) { + // LinkedList contents = new LinkedList<>(); + // contents.add(queue); + // + // for (IAction a : queue.getQueue()) + // contents.add(a); + // + // // Actions only (not the queue itself) + // snapshot.put(queue, contents.subList(1, contents.size())); + // } + // + // return this; + // } + + // + // private Object getParent() { + // return stack.isEmpty() ? null : stack.peek().component; + // } + + // public ActionLogger push1(Object component) { + // List siblings = snapshot.get(getParent()); + // boolean isLast = siblings != null && !siblings.isEmpty() && siblings.get(siblings.size() - 1).equals(component); + // + // stack.push(new TreeNode(component)); + // return this; + // } + + // + // public void log(String message, Object source, int in) { + // StringBuilder prefix = new StringBuilder(); + // Deque branches = branchStack.get(); + // int depth = branches.size(); + // + // Iterator iter = branches.descendingIterator(); + // for (int i = 0; i < depth - 1; i++) { + // prefix.append(iter.next() ? " " : "│ "); + // } + // + // if (depth > 0) { + // prefix.append(branches.peek() ? "└── " : "├── "); + // } + // + // String header = getHeader(source); + // int pad = Math.max(0, maxHeaderLength - header.length()); + // String formatted = prefix + header + StringUtils.repeat(" ", pad) + message; + // + // output.accept(formatted); + // } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java b/core/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java new file mode 100644 index 000000000..9cdaf4e57 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java @@ -0,0 +1,551 @@ +package noppes.npcs.controllers.data.action; + +import noppes.npcs.api.handler.IActionManager; +import noppes.npcs.api.handler.data.IAction; +import noppes.npcs.api.handler.data.IActionChain; +import noppes.npcs.api.handler.data.IActionListener; +import noppes.npcs.api.handler.data.IActionQueue; +import noppes.npcs.api.handler.data.actions.IConditionalAction; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.action.action.ConditionalAction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Full-featured ActionManager: + * - delayed start, max-duration, tick-interval + * - chaining (getNext, getPrevious, scheduleAfter/Before) + * - per-action data + * - conditional actions + * - cancellation & clearing + */ +public class ActionManager implements IActionManager { + public static final ActionManager GLOBAL = new ActionManager("GLOBAL"); + private static final Map listeners = new ConcurrentHashMap<>(); + + protected boolean isWorking = false; + public ActionLogger LOGGER = new ActionLogger(this); + protected String name = ""; + + protected final ActionQueue sequentialQueue = new ActionQueue(this, "mainSequential"); + protected final ActionQueue parallelQueue = new ActionQueue(this, "mainParallel", true); + protected final ActionQueue conditionalQueue = new ActionQueue(this, "mainConditional", true); + protected final Map otherQueues = new HashMap<>(); + + protected boolean debug = false; + protected ScriptContainer reportTo; + + public ActionManager() { + } + + public ActionManager(String name) { + this.name = name; + } + + + @Override + public IActionManager start() { + isWorking = true; + return this; + } + + @Override + public IActionManager stop() { + isWorking = false; + return this; + } + + @Override + public String getName() { + return name; + } + + @Override + public IActionManager setName(String name) { + this.name = name; + return this; + } + + @Override + public boolean inDebugMode() { + return debug; + } + + @Override + public IActionManager setDebugMode(boolean debug) { + this.debug = debug; + + if (debug) + LOGGER.log("Enabled debug mode", this); + return this; + } + + /////////////////////////////////////////////////// + /////////////////////////////////////////////////// + // Listeners + + // IActionListener Comments +// public IActionListener getOrCreateListener(Object obj) { +// return listeners.computeIfAbsent(obj, o -> new ActionListener(o)); +// } + +// public IActionListener getListener(Object obj) { +// return listeners.get(obj); +// } + +// public boolean removeListener(Object obj) { +// if (listeners.containsKey(obj)) { +// listeners.remove(obj); +// return true; +// } +// +// return false; +// } + +// public boolean fireListener(String hookName, Object obj) { +// IActionListener listener = getListener(obj); +// if (listener != null) +// return listener.fire(hookName); +// +// return false; +// } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Creators + @Override + public IAction create(String name) { + return new Action(this, name); + } + + @Override + public IAction create(Consumer t) { + return new Action(this, t); + } + + @Override + public IAction create(int maxDuration, int delay, Consumer task) { + return new Action(this, maxDuration, delay, task); + } + + @Override + public IAction create(String name, int maxDuration, int delay, Consumer task) { + return new Action(this, name, maxDuration, delay, task); + } + + @Override + public IAction create(String name, int delay, Consumer t) { + return new Action(this, name, delay, t); + } + + @Override + public IAction create(int delay, Consumer t) { + return new Action(this, delay, t); + } + + @Override + public IAction create(String name, Consumer t) { + return new Action(this, name, t); + } + + @Override + public IConditionalAction create(Function condition, Consumer task) { + return new ConditionalAction(this, condition, task); + } + + @Override + public IConditionalAction create(String name, Function condition, Consumer task) { + return new ConditionalAction(this, name, condition, task); + } + + @Override + public IConditionalAction create(Function condition, Consumer task, Function terminateWhen) { + return new ConditionalAction(this, condition, task, terminateWhen); + } + + @Override + public IConditionalAction create(String name, Function condition, Consumer task, Function terminateWhen) { + return new ConditionalAction(this, name, condition, task, terminateWhen); + } + + @Override + public IConditionalAction create(Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + return new ConditionalAction(this, condition, task, terminateWhen, onTermination); + } + + @Override + public IConditionalAction create(String name, Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + return new ConditionalAction(this, name, condition, task, terminateWhen, onTermination); + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Queues + @Override + public IActionQueue createQueue(String name) { + return createQueue(name, false); + } + + @Override + public IActionQueue createQueue(String name, boolean isParallel) { + IActionQueue queue = new ActionQueue(this, name, isParallel).killWhenEmpty(true); + otherQueues.put(name, queue); + return queue; + } + + @Override + public IActionQueue getOrCreateQueue(String name) { + return getOrCreateQueue(name, false); + } + + @Override + public IActionQueue getOrCreateQueue(String name, boolean isParallel) { + IActionQueue queue = getQueue(name); + + if (queue == null) + queue = createQueue(name, isParallel); + return queue; + } + + @Override + public IActionQueue getQueue(String name) { + return otherQueues.get(name); + } + + @Override + public boolean hasQueue(String name) { + return otherQueues.containsKey(name); + } + + @Override + public boolean removeQueue(String name) { + IActionQueue queue = otherQueues.get(name); + + if (queue == null) + return false; + + queue.kill(); + return true; + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Sequential + @Override + public IActionQueue getSequentialQueue() { + return sequentialQueue; + } + + @Override + public IAction schedule(IAction action) { + if (action instanceof IConditionalAction) + return conditionalQueue.schedule(action); + + return sequentialQueue.schedule(action); + } + + @Override + public void schedule(IAction... actions) { + sequentialQueue.schedule(actions); + } + + @Override + public void schedule(Consumer... tasks) { + sequentialQueue.schedule(tasks); + } + + @Override + public IAction schedule(Consumer task) { + return sequentialQueue.schedule(task); + } + + @Override + public IAction schedule(int delay, Consumer task) { + return sequentialQueue.schedule(delay, task); + } + + @Override + public IAction schedule(int maxDuration, int delay, Consumer task) { + return sequentialQueue.schedule(maxDuration, delay, task); + } + + @Override + public IAction schedule(String name, Consumer task) { + return sequentialQueue.schedule(name, task); + } + + @Override + public IAction schedule(String name, int delay, Consumer task) { + return sequentialQueue.schedule(name, delay, task); + } + + @Override + public IAction schedule(String name, int maxDuration, int delay, Consumer task) { + return sequentialQueue.schedule(name, maxDuration, delay, task); + } + + @Override + public IAction scheduleActionAt(int index, IAction action) { + return sequentialQueue.scheduleActionAt(index, action); + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Parallels + @Override + public IActionQueue getParallelQueue() { + return parallelQueue; + } + + @Override + public IAction scheduleParallel(IAction action) { + return parallelQueue.schedule(action); + } + + @Override + public void scheduleParallel(IAction... actions) { + parallelQueue.schedule(actions); + } + + @Override + public void scheduleParallel(Consumer... tasks) { + parallelQueue.schedule(tasks); + } + + @Override + public IAction scheduleParallel(Consumer task) { + return parallelQueue.schedule(task); + } + + @Override + public IAction scheduleParallel(int delay, Consumer task) { + return parallelQueue.schedule(delay, task); + } + + @Override + public IAction scheduleParallel(int maxDuration, int delay, Consumer task) { + return parallelQueue.schedule(maxDuration, delay, task); + } + + @Override + public IAction scheduleParallel(String name, Consumer task) { + return parallelQueue.schedule(name, task); + } + + @Override + public IAction scheduleParallel(String name, int delay, Consumer task) { + return parallelQueue.schedule(name, delay, task); + } + + @Override + public IAction scheduleParallel(String name, int maxDuration, int delay, Consumer task) { + return parallelQueue.schedule(name, maxDuration, delay, task); + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Conditionals + @Override + public IActionQueue getConditionalQueue() { + return conditionalQueue; + } + + @Override + public IConditionalAction schedule(IConditionalAction action) { + return (IConditionalAction) conditionalQueue.schedule(action); + } + + @Override + public void schedule(IConditionalAction... actions) { + conditionalQueue.schedule(actions); + } + + @Override + public IConditionalAction schedule(Function condition, Consumer task) { + return schedule(new ConditionalAction(this, condition, task)); + } + + @Override + public IConditionalAction schedule(Function condition, Consumer task, Function terminateWhen) { + return schedule(new ConditionalAction(this, condition, task, terminateWhen)); + } + + @Override + public IConditionalAction schedule(Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + return schedule(new ConditionalAction(this, condition, task, terminateWhen, onTermination)); + } + + + @Override + public IConditionalAction schedule(String name, Function condition, Consumer task) { + return schedule(new ConditionalAction(this, name, condition, task)); + } + + @Override + public IConditionalAction schedule(String name, Function condition, Consumer task, Function terminateWhen) { + return schedule(new ConditionalAction(this, name, condition, task, terminateWhen)); + } + + @Override + public IConditionalAction schedule(String name, Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + return schedule(new ConditionalAction(this, name, condition, task, terminateWhen, onTermination)); + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Handling + @Override + public IActionQueue[] getAllQueues() { + ArrayList allQueues = new ArrayList<>(); + + allQueues.add(sequentialQueue); + allQueues.add(parallelQueue); + allQueues.add(conditionalQueue); + + allQueues.addAll(otherQueues.values()); + + return allQueues.toArray(new IActionQueue[0]); + } + + public boolean hasActiveTasks() { + return Arrays.stream(getAllQueues()).anyMatch(queue -> queue.hasActiveTasks()); + } + + @Override + public boolean hasAny(String name) { + return getAny(name) != null; + } + + @Override + public IAction getAny(String name) { + IAction act = sequentialQueue.get(name); + if (act != null) + return act; + + act = parallelQueue.get(name); + if (act != null) + return act; + + act = conditionalQueue.get(name); + if (act != null) + return act; + + for (IActionQueue q : otherQueues.values()) { + act = q.get(name); + if (act != null) + return act; + } + + return null; + } + + @Override + public boolean cancelAny(String name) { + boolean canceled = sequentialQueue.cancel(name); + if (canceled) + return true; + + canceled = parallelQueue.cancel(name); + if (canceled) + return true; + + canceled = conditionalQueue.cancel(name); + if (canceled) + return true; + + for (IActionQueue queue : otherQueues.values()) + if (queue.cancel(name)) + return true; + + return false; + } + + /** + * Call once per tick from your main loop. + */ + public void tick() { + if (!isWorking) return; + + boolean active = hasActiveTasks(); + if (debug && active) + LOGGER.beginTick(this).log("Started Tick Cycle", this); + + // ─── Sequential (head only) ───────────────────────────────── + sequentialQueue.tick(); + + // ─── Parallel (all) ─────────────────────────────────────── + parallelQueue.tick(); + + // ─── Conditionals ───────────────────────────────────────── + conditionalQueue.tick(); + + // ─── Other Queues ───────────────────────────────────────── + Iterator it = otherQueues.values().iterator(); + while (it.hasNext()) { + IActionQueue other = it.next(); + ((ActionQueue) other).tick(); + + if (other.isDead()) { + other.clear(); + if (debug) + LOGGER.log(String.format("Removing queue '%s' from '%s'", other.getName(), getInternalName()), this); + it.remove(); + } + } + // ─── All Listeners ───────────────────────────────────────── + // IActionListener Comments +// if (this == GLOBAL) { +// listeners.forEach((obj, listener) -> ((ActionListener) listener).tick()); +// } + + if (debug && active) + LOGGER.finish("Finished Tick Cycle", this); + } + + @Override + public void clear() { + sequentialQueue.clear(); + parallelQueue.clear(); + conditionalQueue.clear(); + otherQueues.forEach((name, queue) -> queue.clear()); + + if (debug) + LOGGER.log("Cleared all queues!", this); + } + + public String getInternalName() { + return name.isEmpty() ? "Action Manager" : name; + } + + @Override + public IActionChain chain() { + return sequentialQueue.chain(); + } + + @Override + public IActionChain parallelChain() { + return parallelQueue.chain(); + } + + @Override + public String printQueues() { + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("ActionManager [working=%s]\n", isWorking)); + + + int i = 0; + for (IActionQueue action : getAllQueues()) { + sb.append(String.format(" [%d] %s\n", i++, action.toString())); + } + + return sb.toString(); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java b/core/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java new file mode 100644 index 000000000..332adab39 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java @@ -0,0 +1,396 @@ +package noppes.npcs.controllers.data.action; + +import noppes.npcs.api.handler.IActionManager; +import noppes.npcs.api.handler.data.IAction; +import noppes.npcs.api.handler.data.IActionChain; +import noppes.npcs.api.handler.data.IActionQueue; +import noppes.npcs.controllers.data.action.chain.ActionChain; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.Consumer; + +public class ActionQueue implements IActionQueue { + protected final Deque queue = new ConcurrentLinkedDeque<>(); + protected final ActionManager manager; + protected final String name; + + protected boolean isWorking = true; + protected boolean isParallel; + + protected boolean isDead; + protected boolean stopWhenEmpty; + + protected boolean killWhenEmpty; + protected int killWhenEmptyAfter = 100; + + protected Action autoKill; + + public ActionQueue(ActionManager manager, String name) { + this.manager = manager; + this.name = name; + } + + public ActionQueue(ActionManager manager, String name, boolean isParallel) { + this(manager, name); + this.isParallel = isParallel; + } + + @Override + public IActionQueue start() { + isWorking = true; + return this; + } + + @Override + public IActionQueue stop() { + isWorking = false; + return this; + } + + @Override + public IActionManager getManager() { + return manager; + } + + @Override + public String getName() { + return name; + } + + @Override + public Queue getQueue() { + return queue; + } + + @Override + public boolean isParallel() { + return isParallel; + } + + @Override + public IActionQueue setParallel(boolean parallel) { + isParallel = parallel; + return this; + } + + @Override + public boolean isStoppedWhenEmpty() { + return stopWhenEmpty; + } + + @Override + public IActionQueue stopWhenEmpty(boolean stopWhenEmpty) { + this.stopWhenEmpty = stopWhenEmpty; + return this; + } + + @Override + public boolean isKilledWhenEmpty() { + return killWhenEmpty; + } + + @Override + public int getKillWhenEmptyAfter() { + return killWhenEmptyAfter; + } + + @Override + public IActionQueue killWhenEmpty(boolean killWhenEmpty) { + this.killWhenEmpty = killWhenEmpty; + return this; + } + + @Override + public IActionQueue killWhenEmptyAfter(int ticks) { + this.killWhenEmptyAfter = ticks; + return this; + } + + @Override + public boolean isDead() { + return isDead; + } + + @Override + public IActionQueue kill() { + this.isDead = true; + + if (manager.debug) + manager.LOGGER.log(String.format("Killing queue '%s' on '%s'", name, manager.getInternalName()), this); + return this; + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Schedulers + @Override + public IAction schedule(IAction action) { + Action act = (Action) action; + + if (!isParallel && act.unscheduledList != null) { + act.unscheduledList.scheduleAll(this).forEach((act1) -> act1.unscheduledList = null).kill(); + return action; + } + + act.schedule(this); + return action; + } + + @Override + public void schedule(IAction... actions) { + for (IAction act : actions) + schedule(act); + } + + @Override + public IAction schedule(Consumer task) { + return schedule(manager.create(task)); + } + + @Override + public void schedule(Consumer... tasks) { + for (Consumer task : tasks) + schedule(task); + } + + + @Override + public IAction schedule(int delay, Consumer task) { + return schedule(manager.create(delay, task)); + } + + @Override + public IAction schedule(int maxDuration, int delay, Consumer task) { + return schedule(manager.create(maxDuration, delay, task)); + } + + @Override + public IAction schedule(String name, Consumer task) { + return schedule(manager.create(name, task)); + } + + @Override + public IAction schedule(String name, int delay, Consumer task) { + return schedule(manager.create(name, delay, task)); + } + + @Override + public IAction schedule(String name, int maxDuration, int delay, Consumer task) { + return schedule(manager.create(name, maxDuration, delay, task)); + } + + @Override + public IAction scheduleActionAt(int index, IAction action) { + int size = queue.size(); + if (index <= 0) { + queue.addFirst(action); + } else if (index >= size) { + queue.addLast(action); + } else { + List tmp = new ArrayList<>(queue); + tmp.add(index, action); + queue.clear(); + queue.addAll(tmp); + } + ((Action) action).queue = this; + ((Action) action).isScheduled = true; + return action; + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Queue Data + @Override + public boolean hasActiveTasks() { + return !queue.isEmpty(); + } + + @Override + public int getIndex(IAction action) { + int i = 0; + for (IAction a : queue) { + if (a.equals(action)) + return i; + i++; + } + return -1; + } + + @Override + public IAction getCurrentAction() { + return queue.peekFirst(); + } + + @Override + public boolean has(IAction action) { + return has(action.getName()); + } + + @Override + public boolean has(String actionName) { + for (IAction act : queue) + if (act.getName().equals(actionName)) + return true; + + return false; + } + + @Override + public IAction get(String actionName) { + for (IAction action : queue) { + if (action.getName().equals(actionName)) + return action; + } + + return null; + } + + @Override + public boolean cancel(IAction action) { + if (queue.remove(action)) { + action.kill(); + return true; + } + + return false; + } + + @Override + public boolean cancel(String actionName) { + Iterator acts = queue.iterator(); + while (acts.hasNext()) { + IAction act = acts.next(); + if (act.getName().equals(actionName)) { + act.kill(); + acts.remove(); + return true; + } + } + return false; + } + + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// + // Handling + public boolean finish(Action a) { + if (a.isDone()) { + if (a.onDone != null) + a.execute("onDone", a::executeOnDone); + + a.kill(); + + if (manager.debug) + manager.LOGGER.finish(String.format("Removing %s from queue", a.getIdentifier()), this); + return true; + } + + return false; + } + + protected boolean tick(Action a) { + if (a == null) + return false; + + boolean wasDone = a.isDone(); + if (manager.debug && !wasDone) + manager.LOGGER.push(this).log(String.format("Started ticking %s", a.getIdentifier(), a.name), this).push(a); + + a.tick(); + + if (manager.debug && !wasDone) + manager.LOGGER.pop().finish(String.format("Finished ticking %s", a.getIdentifier(), a.name), this).pop(); + + return finish(a); + } + + protected void tick() { + if (!isWorking || isDead) + return; + + boolean active = hasActiveTasks(); + if (manager.debug && active) + manager.LOGGER.push(manager).log(String.format("Started ticking queue '%s' ", name), manager).push(this); + + if (!isParallel) { + if (tick((Action) getCurrentAction())) + queue.pollFirst(); + } else { + Iterator pit = queue.iterator(); + while (pit.hasNext()) + if (tick((Action) pit.next())) + pit.remove(); + } + + if (stopWhenEmpty && !hasActiveTasks()) + stop(); + + killWhenEmpty(); + + if (manager.debug && active) + manager.LOGGER.pop().finish(String.format("Finished ticking queue '%s' ", name), manager).pop(); + } + + protected void killWhenEmpty() { + if (killWhenEmpty && !hasActiveTasks() && autoKill == null) { + if (manager.debug) + manager.LOGGER.log(String.format("Queue is empty! Killing in %s ticks", killWhenEmptyAfter), this); + + autoKill = (Action) manager.create(String.format("Kill '%s'", name), killWhenEmptyAfter, (act) -> { + if (hasActiveTasks()) { + if (manager.debug) + manager.LOGGER.log("Scheduled Actions found! Aborted kill process", this); + return; + } + + manager.removeQueue(name); + }).everyTick().once(); + } + + if (autoKill != null) { + autoKill.tick(); + if (autoKill.isDone()) + autoKill = null; + } + } + + @Override + public void clear() { + queue.forEach((act) -> act.kill()); + queue.clear(); + + if (manager.debug) + manager.LOGGER.log("Cleared queue!", this); + } + + @Override + public IActionChain chain() { + return new ActionChain(manager, this, isParallel ? "parallel#" : "sequential#"); + } + + @Override + public String printQueue() { + if (queue.isEmpty()) { + return String.format("ActionQueue[name=%s] is empty", name); + } + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%s\n", this)); + + int i = 0; + for (IAction action : queue) { + sb.append(String.format(" [%d] %s\n", i++, action.toString())); + } + + return sb.toString(); + } + + public String toString() { + return String.format("ActionQueue '%s' [size=%d, parallel=%s, working=%s, dead=%s, killWhenEmpty=%s, killAfter=%d]", name, queue.size(), isParallel, isWorking, isDead, killWhenEmpty, killWhenEmptyAfter); + } + +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/ActionThread.java b/core/src/main/java/noppes/npcs/controllers/data/action/ActionThread.java new file mode 100644 index 000000000..37e06c982 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/ActionThread.java @@ -0,0 +1,168 @@ +package noppes.npcs.controllers.data.action; + +import noppes.npcs.api.handler.data.IAction; + +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +public class ActionThread { + private Action action; + + private final Object lock = new Object(); + private Thread thread; + ExecutorService executor; + + private final Queue> runningTasks = new ConcurrentLinkedQueue<>(); + private final Set runningKeys = ConcurrentHashMap.newKeySet(); + + private volatile boolean threadPaused, threadSleeping; + + private Function pauseUntil; + + public ActionThread(Action action) { + this.action = action; + executor = Executors.newSingleThreadExecutor(r -> { + thread = new Thread(r); + thread.setName(String.format("ActionThread (%s)", action.getName())); + return thread; + }); + + if (action.manager.debug) + action.manager.LOGGER.log("Created Action thread!", this); + } + + public void stop() { + runningTasks.forEach((task) -> { + if (!task.isDone()) + task.cancel(true); + }); + + executor.shutdown(); + + + if (action.manager.debug) + action.manager.LOGGER.log("Stopping thread", this); + } + + public Action getAction() { + return action; + } + + public void pauseFor(long millis) { + if (isPaused() || !inActionThread()) + return; + + try { + threadSleeping = true; + if (action.manager.debug) + action.manager.LOGGER.log(String.format("Sleeping thread for %s ticks", millis / 50), this); + + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + threadSleeping = false; + + if (action.manager.debug) + action.manager.LOGGER.log(String.format("Woken up thread after sleeping for %s ticks", millis / 50), this); + } + } + + public void pauseFor(int ticks) { + pauseFor(ticks * 50L); + } + + public void pause() { + if (!inActionThread() || isPaused()) + return; + + threadPaused = true; + synchronized (lock) { + while (threadPaused) { + try { + if (action.manager.debug) + action.manager.LOGGER.log(String.format("Pausing thread"), this); + + lock.wait(); // Wait until notified + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void pauseUntil(Function until) { + this.pauseUntil = until; + pause(); + } + + public void resume() { + synchronized (lock) { + if (threadPaused) { + threadPaused = false; + lock.notify(); + + if (action.manager.debug) + action.manager.LOGGER.log(String.format("Resumed thread"), this); + } + + if (threadSleeping) + thread.interrupt(); + } + } + + public boolean isTaskRunning(String taskKey) { + return runningKeys.contains(taskKey); + } + + public boolean inActionThread() { + return Thread.currentThread() == thread; + } + + public boolean isPaused() { + return threadPaused || threadSleeping; + } + + public boolean isThreadRunning() { + return runningTasks.stream().anyMatch(task -> !task.isDone()); + } + + public void execute(String taskKey, Runnable task) { + if (threadPaused && pauseUntil != null && pauseUntil.apply(action)) + resume(); + + if (!runningKeys.add(taskKey)) + return; + + if (action.manager.debug) + action.manager.LOGGER.log(String.format("Delegating task '%s' to Action thread...", taskKey), action); + + final AtomicReference> taskRef = new AtomicReference<>(); + taskRef.set(executor.submit(() -> { + if (action.manager.debug) + action.manager.LOGGER.log(String.format("Started executing task '%s' on Action thread...", taskKey), this); + + runningTasks.add(taskRef.get()); + try { + task.run(); + } finally { + runningTasks.remove(taskRef.get()); + runningKeys.remove(taskKey); + + if (action.manager.debug) + action.manager.LOGGER.log(String.format("Finished executing task '%s' on Action thread", taskKey), this); + + if (((ActionQueue) action.queue).finish(action)) + action.queue.getQueue().remove(action); + + } + })); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/action/ConditionalAction.java b/core/src/main/java/noppes/npcs/controllers/data/action/action/ConditionalAction.java new file mode 100644 index 000000000..035003cfe --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/action/ConditionalAction.java @@ -0,0 +1,208 @@ +package noppes.npcs.controllers.data.action.action; + +import noppes.npcs.api.handler.data.IAction; +import noppes.npcs.api.handler.data.actions.IConditionalAction; +import noppes.npcs.controllers.data.action.Action; +import noppes.npcs.controllers.data.action.ActionManager; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.util.function.Consumer; +import java.util.function.Function; + + +public class ConditionalAction extends Action implements IConditionalAction { + protected Function condition; + protected Function terminateWhen; + protected Consumer onTermination; + protected int maxChecks = -1; + protected int checkCount; + protected boolean taskExecuted; + + public ConditionalAction(ActionManager manager, Function condition, Consumer task) { + super(manager, task); + this.condition = condition; + } + + public ConditionalAction(ActionManager manager, String name, Function condition, Consumer task) { + super(manager, name, task); + this.condition = condition; + } + + public ConditionalAction(ActionManager manager, Function condition, Consumer task, Function terminateWhen) { + this(manager, condition, task); + this.terminateWhen = terminateWhen; + } + + public ConditionalAction(ActionManager manager, String name, Function condition, Consumer task, Function terminateWhen) { + this(manager, name, condition, task); + this.terminateWhen = terminateWhen; + } + + public ConditionalAction(ActionManager manager, Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + this(manager, condition, task, terminateWhen); + this.onTermination = onTermination; + } + + public ConditionalAction(ActionManager manager, String name, Function condition, Consumer task, Function terminateWhen, Consumer onTermination) { + this(manager, name, condition, task, terminateWhen); + this.onTermination = onTermination; + } + + @Override + public int getCheckCount() { + return checkCount; + } + + @Override + public int getMaxChecks() { + return maxChecks; + } + + @Override + public IConditionalAction setMaxChecks(int maxChecks) { + this.maxChecks = Math.max(-1, maxChecks); + return this; + } + + @Override + public IConditionalAction setCondition(Function condition) { + this.condition = condition; + return this; + } + + @Override + public IConditionalAction terminateWhen(Function terminateWhen) { + this.terminateWhen = terminateWhen; + return this; + } + + @Override + public IConditionalAction onTermination(Consumer onTermination) { + this.onTermination = onTermination; + return this; + } + + @Override + public boolean wasTaskExecuted() { + return taskExecuted; + } + + @Override + public boolean isTerminated() { + return checkTermination(); + } + + public void inheritedTick() { + if (manager.inDebugMode()) + manager.LOGGER.log(String.format("Ticking... (duration = %s/%s, count = %s/%s, checkCount = %s/%s)", duration, maxDuration, count, maxCount, checkCount, maxChecks), this); + + if (maxChecks > -1 && checkCount >= maxChecks) { + if (manager.inDebugMode()) + manager.LOGGER.log("Reached max check count", this); + + markDone(); + return; + } + + if (duration % updateEveryXTick == 0) { + checkCount++; + + Runnable execute = () -> { + if (evaluatePredicate(condition, "Condition")) + executeTask(); + + if (checkTermination()) { + if (onTermination != null) + executeOnTermination(); + markDone(); + } + }; + + execute("task", execute); + } + } + + protected void executeTask() { + if (manager.inDebugMode()) + manager.LOGGER.log("Executing task...", this); + + try { + task.accept(this); + count++; + taskExecuted = true; + } catch (Throwable t) { + String err = "IConditionalAction '" + getName() + "' threw an exception:"; + if (reportTo != null) + reportTo.appendConsole(err + "\n" + ExceptionUtils.getStackTrace(t)); + + manager.LOGGER.error("Task of " + this + " threw an exception:", t); + markDone(); + } + + if (manager.inDebugMode()) + manager.LOGGER.log(String.format("Finished executing task (count = %s/%s)", count, maxCount), this); + + } + + protected void executeOnTermination() { + if (manager.inDebugMode()) + manager.LOGGER.log("Executing onTermination task...", this); + + try { + onTermination.accept(this); + } catch (Throwable t) { + String err = "IConditionalAction" + getName() + " onTermination threw an exception:"; + if (reportTo != null) + reportTo.appendConsole(err + "\n" + ExceptionUtils.getStackTrace(t)); + + manager.LOGGER.error("Termination Task of " + this + " threw an exception:", t); + } + + if (manager.inDebugMode()) + manager.LOGGER.log("Finished executing onTermination task", this); + } + + private boolean checkTermination() { + return evaluatePredicate(terminateWhen, "Termination condition"); + } + + private boolean evaluatePredicate(Function predicate, String name) { + if (predicate == null) + return false; + + Boolean result; + try { + result = predicate.apply(this); + } catch (Throwable t) { + manager.LOGGER.error(name + " of " + this + " threw an exception:", t); + markDone(); + return false; + } + + if (result == null) { + if (manager.inDebugMode()) + manager.LOGGER.log(name + " returned null; treating as false", this); + return false; + } + + return result; + } + + + public String toString() { + return String.format("%s [queue='%s', scheduled=%s, done=%s, paused=%s, updateEvery=%s, duration=%d/%d, count=%d/%d, checks=%d/%d, taskExecuted=%s, threaded=%s]", getIdentifier(), getQueueName(), + isScheduled, + done, + isPaused(), + updateEveryXTick, + duration, + maxDuration, + count, + maxCount, + checkCount, + maxChecks, + taskExecuted, + isThreaded + ); + } +} diff --git a/core/src/main/java/noppes/npcs/controllers/data/action/chain/ActionChain.java b/core/src/main/java/noppes/npcs/controllers/data/action/chain/ActionChain.java new file mode 100644 index 000000000..c5940a2a6 --- /dev/null +++ b/core/src/main/java/noppes/npcs/controllers/data/action/chain/ActionChain.java @@ -0,0 +1,63 @@ +package noppes.npcs.controllers.data.action.chain; + +import noppes.npcs.api.handler.data.IAction; +import noppes.npcs.api.handler.data.IActionChain; +import noppes.npcs.api.handler.data.IActionQueue; +import noppes.npcs.controllers.data.action.ActionManager; + +import java.util.function.Consumer; + +/** + * helper to build a back‐to‐back chain of one‐shot actions + */ +public class ActionChain implements IActionChain { + protected final ActionManager manager; + protected final IActionQueue queue; + protected String name; + protected int index = 0, offset; + + public ActionChain(ActionManager manager, IActionQueue queue, String name) { + this.manager = manager; + this.queue = queue; + this.name = name; + } + + @Override + public IActionQueue getQueue() { + return queue; + } + + @Override + public String getName() { + return name; + } + + @Override + public IActionChain setName(String name) { + this.name = name; + return this; + } + + @Override + public IActionChain start() { + manager.start(); + return this; + } + + /** + * schedule the next task ‘delay’ ticks after the previous one + */ + @Override + public IActionChain after(int delay, String name, Consumer task) { + offset = queue.isParallel() ? offset + delay : delay; + queue.schedule(name, offset, task).everyTick().once(); + index++; + return this; + } + + @Override + public IActionChain after(int delay, Consumer task) { + return after(delay, name + index, task); + } + +} diff --git a/core/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/core/src/main/java/noppes/npcs/entity/EntityNPCInterface.java new file mode 100644 index 000000000..ad030efcf --- /dev/null +++ b/core/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -0,0 +1,25 @@ +package noppes.npcs.entity; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +/** + * TEMPORARY STUB -- auto-generated for compilation. + * NPC entity interface. Actual entity stays in mc1710. + */ +public class EntityNPCInterface { +} diff --git a/core/src/main/java/noppes/npcs/quests/QuestDialog.java b/core/src/main/java/noppes/npcs/quests/QuestDialog.java new file mode 100644 index 000000000..d0c9325fb --- /dev/null +++ b/core/src/main/java/noppes/npcs/quests/QuestDialog.java @@ -0,0 +1,547 @@ +package noppes.npcs.quests; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.IQuestDialog; +import noppes.npcs.api.handler.data.IQuestObjective; +import noppes.npcs.constants.EnumPartyObjectives; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.DialogController; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.Dialog; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.Vector; + +public class QuestDialog extends QuestInterface implements IQuestDialog { + + public HashMap dialogs = new HashMap(); + + @Override + public void readEntityFromNBT(INbt compound) { + dialogs = NBTTags.getIntegerIntegerMap(compound.getTagList("QuestDialogs", 10)); + } + + @Override + public void writeEntityToNBT(INbt compound) { + compound.setTag("QuestDialogs", NBTTags.nbtIntegerIntegerMap(dialogs)); + } + + @Override + public boolean isCompleted(PlayerData playerData) { + for (int dialogId : dialogs.values()) + if (!playerData.dialogData.dialogsRead.contains(dialogId)) + return false; + return true; + } + + @Override + public void handleComplete(IPlayer player) { + super.handleComplete(player); + } + + @Override + public Vector getQuestLogStatus(IPlayer player) { + Vector vec = new Vector(); + for (int dialogId : dialogs.values()) { + Dialog dialog = DialogController.Instance.dialogs.get(dialogId); + if (dialog == null) + continue; + String title = dialog.title; + if (PlayerData.get(player).dialogData.dialogsRead.contains(dialogId)) + title += " (read)"; + else + title += " (unread)"; + vec.add(title); + } + + return vec; + } + + @Override + public IQuestObjective[] getObjectives(IPlayer player) { + List list = new ArrayList(); + + for (int i = 0; i < 3; ++i) { + if (this.dialogs.containsKey(i)) { + Dialog dialog = (Dialog) DialogController.Instance.dialogs.get(this.dialogs.get(i)); + if (dialog != null) { + list.add(new noppes.npcs.quests.QuestDialog.QuestDialogObjective(this, player, dialog)); + } + } + } + + return (IQuestObjective[]) list.toArray(new IQuestObjective[list.size()]); + } + + @Override + public IQuestObjective[] getPartyObjectives(Party party) { + List list = new ArrayList(); + + for (int i = 0; i < 3; ++i) { + if (this.dialogs.containsKey(i)) { + Dialog dialog = (Dialog) DialogController.Instance.dialogs.get(this.dialogs.get(i)); + if (dialog != null) { + list.add(new noppes.npcs.quests.QuestDialog.QuestDialogObjective(this, party, dialog)); + } + } + } + + return (IQuestObjective[]) list.toArray(new IQuestObjective[list.size()]); + } + + @Override + public Vector getPartyQuestLogStatus(Party party) { + Vector vec = new Vector(); + if (party == null) + return vec; + + if (party.getQuestData() == null) + return vec; + + if (party.getQuestData().quest == null) + return vec; + + EnumPartyObjectives objectives = party.getQuestData().quest.partyOptions.objectiveRequirement; + if (objectives == EnumPartyObjectives.All) { + for (int dialogId : dialogs.values()) { + Dialog dialog = DialogController.Instance.dialogs.get(dialogId); + if (dialog == null) + continue; + + ArrayList unread = new ArrayList<>(); + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer player = NoppesUtilServer.getPlayer(uuid); + PlayerData playerData; + if (player != null) { + playerData = PlayerData.get(player); + } else { + playerData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (playerData != null) { + if (!playerData.dialogData.dialogsRead.contains(dialogId)) + unread.add(playerData.playername); + } + } + + String title = dialog.title; + if (!unread.isEmpty()) { + title += " (unread)"; + } else { + title += " (read)"; + } + vec.add(title); + + if (!unread.isEmpty()) { + StringBuilder unreaders = new StringBuilder(); + for (String name : unread) { + if (unreaders.length() > 0) { + unreaders.append(", "); + } + unreaders.append(name); + } + vec.add(unreaders.toString()); + } + } + return vec; + } else if (objectives == EnumPartyObjectives.Leader) { + IPlayer leader = NoppesUtilServer.getPlayer(party.getLeaderUUID()); + PlayerData playerData; + if (leader != null) { + playerData = PlayerData.get(leader); + } else { + playerData = PlayerDataController.Instance.getPlayerDataCache(party.getLeaderUUID().toString()); + } + if (playerData == null) + return vec; + for (int dialogId : dialogs.values()) { + Dialog dialog = DialogController.Instance.dialogs.get(dialogId); + if (dialog == null) + continue; + String title = dialog.title; + if (playerData.dialogData.dialogsRead.contains(dialogId)) + title += " (read)"; + else + title += " (unread)"; + vec.add(title); + } + return vec; + } else { + Set readValues = new HashSet<>(); + for (int dialogId : dialogs.values()) { + Dialog dialog = DialogController.Instance.dialogs.get(dialogId); + if (dialog == null) + continue; + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer player = NoppesUtilServer.getPlayer(uuid); + PlayerData sharedData; + if (player != null) { + sharedData = PlayerData.get(player); + } else { + sharedData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (sharedData != null) { + if (sharedData.dialogData.dialogsRead.contains(dialogId)) + readValues.add(dialogId); + } + } + } + for (int dialogId : dialogs.values()) { + Dialog dialog = DialogController.Instance.dialogs.get(dialogId); + if (dialog == null) + continue; + String title = dialog.title; + if (readValues.contains(dialogId)) + title += " (read)"; + else + title += " (unread)"; + vec.add(title); + } + return vec; + } + } + + @Override + public boolean isPartyCompleted(Party party) { + if (party == null) + return false; + + if (party.getQuestData() == null) + return false; + + if (party.getQuestData().quest == null) + return false; + + EnumPartyObjectives objectives = party.getQuestData().quest.partyOptions.objectiveRequirement; + if (objectives == EnumPartyObjectives.All) { + List incomplete = new ArrayList<>(); + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer individual = NoppesUtilServer.getPlayer(uuid); + PlayerData individualData; + if (individual != null) { + individualData = PlayerData.get(individual); + } else { + individualData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (individualData != null) { + for (int dialogId : dialogs.values()) { + if (!individualData.dialogData.dialogsRead.contains(dialogId)) { + incomplete.add(individualData.playername); + break; + } + } + } + } + return incomplete.isEmpty(); + } else if (objectives == EnumPartyObjectives.Leader) { + IPlayer leaderPlayer = NoppesUtilServer.getPlayer(party.getLeaderUUID()); + PlayerData leaderData; + if (leaderPlayer != null) { + leaderData = PlayerData.get(leaderPlayer); + } else { + leaderData = PlayerDataController.Instance.getPlayerDataCache(party.getLeaderUUID().toString()); + } + if (leaderData != null) { + for (int dialogId : dialogs.values()) { + if (!leaderData.dialogData.dialogsRead.contains(dialogId)) { + return false; + } + } + } else { + return false; + } + return true; + } else { + HashMap readValues = new HashMap<>(); + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer player = NoppesUtilServer.getPlayer(uuid); + PlayerData playerData; + if (player != null) { + playerData = PlayerData.get(player); + } else { + playerData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (playerData != null) { + for (int dialogId : dialogs.values()) { + if (playerData.dialogData.dialogsRead.contains(dialogId)) { + readValues.put(dialogId, true); + } + } + } + } + for (boolean result : readValues.values()) { + if (!result) + return false; + } + return true; + } + } + + class QuestDialogObjective implements IQuestObjective { + private final QuestDialog parent; + private final IPlayer player; + private final Party party; + private final Dialog dialog; + + public QuestDialogObjective(QuestDialog this$0, IPlayer player, Dialog dialog) { + this.parent = this$0; + this.player = player; + this.dialog = dialog; + this.party = null; + } + + public QuestDialogObjective(QuestDialog this$0, Party party, Dialog dialog) { + this.parent = this$0; + this.party = party; + this.dialog = dialog; + this.player = null; + } + + public int getProgress() { + return this.isCompleted() ? 1 : 0; + } + + public void setProgress(int progress) { + if (progress >= 0 && progress <= 1) { + if (player != null) { + PlayerData data = PlayerData.get(player); + boolean completed = data.dialogData.dialogsRead.contains(this.dialog.id); + if (progress == 0 && completed) { + data.dialogData.dialogsRead.remove(this.dialog.id); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[1]); + data.updateClient = true; + data.save(); + } + + if (progress == 1 && !completed) { + data.dialogData.dialogsRead.add(this.dialog.id); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[1]); + data.updateClient = true; + data.save(); + } + } else if (party != null) { + if (party.getObjectiveRequirement() != null) { + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + if (objectives == EnumPartyObjectives.Leader) { + IPlayer leaderPlayer = NoppesUtilServer.getPlayer(party.getLeaderUUID()); + PlayerData leaderData; + if (leaderPlayer != null) { + leaderData = PlayerData.get(leaderPlayer); + } else { + leaderData = PlayerDataController.Instance.getPlayerDataCache(party.getLeaderUUID().toString()); + } + if (leaderData == null) + return; + + boolean completed = leaderData.dialogData.dialogsRead.contains(this.dialog.id); + if (progress == 0 && completed) { + leaderData.dialogData.dialogsRead.remove(this.dialog.id); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[1]); + leaderData.updateClient = true; + leaderData.save(); + } + if (progress == 1 && !completed) { + leaderData.dialogData.dialogsRead.add(this.dialog.id); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[1]); + leaderData.updateClient = true; + leaderData.save(); + } + } else { + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer individual = NoppesUtilServer.getPlayer(uuid); + PlayerData individualData; + if (individual != null) { + individualData = PlayerData.get(individual); + } else { + individualData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (individualData != null) { + boolean completed = individualData.dialogData.dialogsRead.contains(this.dialog.id); + if (progress == 0 && completed) { + individualData.dialogData.dialogsRead.remove(this.dialog.id); + individualData.save(); + individualData.updateClient = true; + } + if (progress == 1 && !completed) { + individualData.dialogData.dialogsRead.add(this.dialog.id); + individualData.save(); + individualData.updateClient = true; + } + } + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[1]); + } + } + } + } else { + throw new CustomNPCsException("Progress has to be 0 or 1", new Object[0]); + } + } + + @Override + public void setPlayerProgress(String playerName, int progress) { + if (progress >= 0 && progress <= 1) { + IPlayer foundplayer = NoppesUtilServer.getPlayerByName(playerName); + if (foundplayer != null && party == null) { + PlayerData data = PlayerData.get(foundplayer); + boolean completed = data.dialogData.dialogsRead.contains(this.dialog.id); + if (progress == 0 && completed) { + data.dialogData.dialogsRead.remove(this.dialog.id); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[1]); + data.updateClient = true; + data.save(); + } + + if (progress == 1 && !completed) { + data.dialogData.dialogsRead.add(this.dialog.id); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[1]); + data.updateClient = true; + data.save(); + } + } else if (foundplayer != null) { + if (party.getObjectiveRequirement() != null) { + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + if (objectives == EnumPartyObjectives.Leader) { + IPlayer leaderPlayer = NoppesUtilServer.getPlayer(party.getLeaderUUID()); + PlayerData leaderData; + if (leaderPlayer != null) { + leaderData = PlayerData.get(leaderPlayer); + } else { + leaderData = PlayerDataController.Instance.getPlayerDataCache(party.getLeaderUUID().toString()); + } + if (leaderData == null) + return; + + boolean completed = leaderData.dialogData.dialogsRead.contains(this.dialog.id); + if (progress == 0 && completed) { + leaderData.dialogData.dialogsRead.remove(this.dialog.id); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[1]); + leaderData.updateClient = true; + leaderData.save(); + } + if (progress == 1 && !completed) { + leaderData.dialogData.dialogsRead.add(this.dialog.id); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[1]); + leaderData.updateClient = true; + leaderData.save(); + } + } else { + PlayerData individualData = PlayerData.get(foundplayer); + if (individualData != null) { + boolean completed = individualData.dialogData.dialogsRead.contains(this.dialog.id); + if (progress == 0 && completed) { + individualData.dialogData.dialogsRead.remove(this.dialog.id); + individualData.save(); + individualData.updateClient = true; + } + if (progress == 1 && !completed) { + individualData.dialogData.dialogsRead.add(this.dialog.id); + individualData.save(); + individualData.updateClient = true; + } + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[1]); + } + } + } + } else { + throw new CustomNPCsException("Progress has to be 0 or 1", new Object[0]); + } + } + + public int getMaxProgress() { + return 1; + } + + public boolean isCompleted() { + if (player != null) { + PlayerData data = PlayerData.get(player); + return data.dialogData.dialogsRead.contains(this.dialog.id); + } else if (party != null) { + if (party.getObjectiveRequirement() != null) { + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + if (objectives == EnumPartyObjectives.Leader) { + IPlayer leaderPlayer = NoppesUtilServer.getPlayer(party.getLeaderUUID()); + PlayerData leaderData; + if (leaderPlayer != null) { + leaderData = PlayerData.get(leaderPlayer); + } else { + leaderData = PlayerDataController.Instance.getPlayerDataCache(party.getLeaderUUID().toString()); + } + if (leaderData == null) + return false; + return leaderData.dialogData.dialogsRead.contains(this.dialog.id); + } else { + boolean requiresOneRead = objectives == EnumPartyObjectives.Shared; + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer individual = NoppesUtilServer.getPlayer(uuid); + PlayerData individualData; + if (individual != null) { + individualData = PlayerData.get(individual); + } else { + individualData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (individualData != null) { + boolean read = individualData.dialogData.dialogsRead.contains(this.dialog.id); + if (requiresOneRead && read) { + return true; + } else if (!requiresOneRead && !read) { + return false; + } + } + } + return !requiresOneRead; + } + } + } + return true; + } + + public String getText() { + return this.dialog.title + (this.isCompleted() ? " (read)" : " (unread)"); + } + + @Override + public String getAdditionalText() { + if (party != null && party.getObjectiveRequirement() == EnumPartyObjectives.All) { + List incomplete = new ArrayList<>(); + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer individual = NoppesUtilServer.getPlayer(uuid); + PlayerData individualData; + if (individual != null) { + individualData = PlayerData.get(individual); + } else { + individualData = PlayerDataController.Instance.getPlayerDataCache(uuid.toString()); + } + if (individualData != null) { + boolean read = individualData.dialogData.dialogsRead.contains(this.dialog.id); + if (!read) { + incomplete.add(individualData.playername); + } + } + } + if (!incomplete.isEmpty()) + return "[" + String.join(", ", incomplete) + "]"; + + } + return null; + } + } + +} diff --git a/core/src/main/java/noppes/npcs/quests/QuestInterface.java b/core/src/main/java/noppes/npcs/quests/QuestInterface.java new file mode 100644 index 000000000..35dbc2cb2 --- /dev/null +++ b/core/src/main/java/noppes/npcs/quests/QuestInterface.java @@ -0,0 +1,57 @@ +package noppes.npcs.quests; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.api.handler.data.IQuestInterface; +import noppes.npcs.api.handler.data.IQuestObjective; +import noppes.npcs.constants.EnumPartyObjectives; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerQuestData; + +import java.util.Vector; + +public abstract class QuestInterface implements IQuestInterface { + public int questId; + + public abstract void writeEntityToNBT(INbt compound); + + public abstract void readEntityFromNBT(INbt compound); + + public abstract boolean isCompleted(PlayerData player); + + public void handleComplete(IPlayer player) { + PlayerQuestData questData = PlayerData.get(player).questData; + if (questData != null && questData.getTrackedQuest() != null) { + if (this.questId == PlayerData.get(player).questData.getTrackedQuest().getId()) { + PlayerData.get(player).questData.untrackQuest(); + } + } + } + + public void handlePartyComplete(IPlayer player, Party party, boolean isLeader, EnumPartyObjectives objectives) { + handleComplete(player); + } + + public void removePartyItems(Party party) { + } + + public abstract Vector getQuestLogStatus(IPlayer player); + + public abstract IQuestObjective[] getObjectives(IPlayer var1); + + + // Party Related + public abstract IQuestObjective[] getPartyObjectives(Party party); + + public abstract Vector getPartyQuestLogStatus(Party party); + + public abstract boolean isPartyCompleted(Party party); +} diff --git a/core/src/main/java/noppes/npcs/quests/QuestItem.java b/core/src/main/java/noppes/npcs/quests/QuestItem.java new file mode 100644 index 000000000..b5c5a8648 --- /dev/null +++ b/core/src/main/java/noppes/npcs/quests/QuestItem.java @@ -0,0 +1,689 @@ +package noppes.npcs.quests; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.api.handler.data.IQuestItem; +import noppes.npcs.api.handler.data.IQuestObjective; +import noppes.npcs.constants.EnumPartyObjectives; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.util.ValueUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.Vector; + +public class QuestItem extends QuestInterface implements IQuestItem { + public NpcMiscInventory items = new NpcMiscInventory(3); + public static IPlayer pickedUpPlayerSolo; + public static IItemStack pickedUp; + + public static IItemStack pickedUpParty; + public static IPlayer pickedUpPlayer; + + public boolean leaveItems = false; + public boolean ignoreDamage = false; + public boolean ignoreNBT = false; + + @Override + public void readEntityFromNBT(INbt compound) { + items.setFromNBT(compound.getCompoundTag("Items")); + leaveItems = compound.getBoolean("LeaveItems"); + ignoreDamage = compound.getBoolean("IgnoreDamage"); + ignoreNBT = compound.getBoolean("IgnoreNBT"); + } + + @Override + public void writeEntityToNBT(INbt compound) { + compound.setTag("Items", items.getToNBT()); + compound.setBoolean("LeaveItems", leaveItems); + compound.setBoolean("IgnoreDamage", ignoreDamage); + compound.setBoolean("IgnoreNBT", ignoreNBT); + } + + @Override + public boolean isCompleted(PlayerData playerData) { + if (playerData == null || playerData.player == null) + return false; + + List requiredStacks = NoppesUtilPlayer.countStacks(this.items, this.ignoreDamage, this.ignoreNBT); + List availableStacks = aggregateAvailableStacks(collectAvailableStacks(playerData.player, pickedUp)); + + for (IItemStack required : requiredStacks) { + boolean done = false; + for (IItemStack available : availableStacks) { + if (NoppesUtilPlayer.compareItems(required, available, ignoreDamage, ignoreNBT) && available.stackSize >= required.stackSize) { + done = true; + break; + } + } + if (!done) + return false; + } + + return true; + } + + public HashMap getProcessSet(IPlayer player) { + return buildProgressMap(collectAvailableStacks(player, pickedUp)); + } + + @Override + public void handleComplete(IPlayer player) { + super.handleComplete(player); + if (leaveItems) + return; + removeItems(player); + } + + public void handlePartyComplete(IPlayer player, Party party, boolean isLeader, EnumPartyObjectives objectives) { + super.handlePartyComplete(player, party, isLeader, objectives); + if (leaveItems) + return; + + if (isLeader && objectives == EnumPartyObjectives.Leader) { + removeItems(player); + } else if (objectives == EnumPartyObjectives.All) { + removeItems(player); + } else if (objectives == EnumPartyObjectives.Shared) { + // Shared Case + // Do nothing, handled later in quest completion + } + } + + public void removePartyItems(Party party) { + // Iterate through each quest item in the list + for (IItemStack questItem : items.items.values()) { + int remainingItems = questItem.stackSize; + + // Iterate through all members of the party + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer player = NoppesUtilServer.getPlayer(uuid); + if (player != null) { + int removedItems = removeItemFromPlayer(player, questItem, remainingItems); + remainingItems -= removedItems; + + // If no remaining items are needed for this quest item, move to the next one + if (remainingItems <= 0) { + break; + } + } + } + } + } + + + private int removeItemFromPlayer(IPlayer player, IItemStack questItem, int numItemsToRemove) { + int itemsRemoved = 0; + + // Iterate through the player's inventory + for (int i = 0; i < player.inventory.mainInventory.length && itemsRemoved < numItemsToRemove; i++) { + IItemStack IItemStack = player.inventory.mainInventory[i]; + if (IItemStack == null) + continue; + if (NoppesUtilPlayer.compareItems(IItemStack, questItem, ignoreDamage, ignoreNBT)) { + int itemsToRemoveFromStack = Math.min(IItemStack.stackSize, numItemsToRemove - itemsRemoved); + int size = IItemStack.stackSize; + if (itemsToRemoveFromStack >= size) { + player.inventory.setInventorySlotContents(i, null); + IItemStack.splitStack(size); + } else { + IItemStack.splitStack(itemsToRemoveFromStack); + } + + itemsRemoved += size; + + // If there are no more remaining items to remove, break out of the loop + if (itemsRemoved >= numItemsToRemove) { + break; + } + } + } + return itemsRemoved; + } + + + public void removeItems(IPlayer player) { + for (IItemStack questitem : items.items.values()) { + int stacksize = questitem.stackSize; + for (int i = 0; i < player.inventory.mainInventory.length; i++) { + IItemStack item = player.inventory.mainInventory[i]; + if (item == null) + continue; + if (NoppesUtilPlayer.compareItems(item, questitem, ignoreDamage, ignoreNBT)) { + int size = item.stackSize; + if (stacksize - size >= 0) { + player.inventory.setInventorySlotContents(i, null); + item.splitStack(size); + } else { + item.splitStack(stacksize); + } + stacksize -= size; + if (stacksize <= 0) + break; + } + } + } + } + + @Override + public Vector getQuestLogStatus(IPlayer player) { + Vector vec = new Vector(); + + HashMap map = getProcessSet(player); + for (int slot : map.keySet()) { + IItemStack item = map.get(slot); + IItemStack quest = items.items.get(slot); + if (item == null) + continue; + String process = item.stackSize + ""; + if (item.stackSize > quest.stackSize) + process = quest.stackSize + ""; + process += "/" + quest.stackSize + ""; + if (item.hasDisplayName()) + vec.add(item.getDisplayName() + ": " + process); + else + vec.add(item.getUnlocalizedName() + ".name" + ": " + process); + } + return vec; + } + + public IQuestObjective[] getObjectives(IPlayer player) { + List list = new ArrayList<>(); + List questItems = NoppesUtilPlayer.countStacks(this.items, this.ignoreDamage, this.ignoreNBT); + + for (IItemStack stack : questItems) { + if (stack.stackSize > 0) { + list.add(new QuestItemObjective(this, player, stack)); + } + } + + return list.toArray(new IQuestObjective[0]); + } + + @Override + public IQuestObjective[] getPartyObjectives(Party party) { + List list = new ArrayList<>(); + List questItems = NoppesUtilPlayer.countStacks(this.items, this.ignoreDamage, this.ignoreNBT); + + for (IItemStack stack : questItems) { + if (stack.stackSize > 0) { + list.add(new QuestItemObjective(this, party, stack)); + } + } + + return list.toArray(new IQuestObjective[0]); + } + + @Override + public Vector getPartyQuestLogStatus(Party party) { + Vector vec = new Vector(); + if (party == null || party.getObjectiveRequirement() == null) + return vec; + + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + if (objectives == EnumPartyObjectives.All) { + // Slot, QuestItem Vector + HashMap questVector = new HashMap<>(); + HashMap> playerVector = new HashMap<>(); + for (int slot : items.items.keySet()) { + IItemStack questItem = items.items.get(slot); + if (questItem != null) { + if (questItem.hasDisplayName()) + questVector.put(slot, questItem.getDisplayName() + ": " + questItem.stackSize); + else + questVector.put(slot, questItem.getUnlocalizedName() + ".name" + ": " + questItem.stackSize); + + playerVector.put(slot, new ArrayList<>()); + } + } + + // Slot, List of Players and Their Item Counts + + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer player = NoppesUtilServer.getPlayer(uuid); + if (player != null) { + HashMap perPlayerMap = getProcessSetParty(player); + for (int slot : perPlayerMap.keySet()) { + List slotCurrent = playerVector.get(slot); + IItemStack item = perPlayerMap.get(slot); + IItemStack quest = items.items.get(slot); + if (item == null) + continue; + if (quest == null) + continue; + if (item.stackSize > quest.stackSize) + continue; + slotCurrent.add(player.getCommandSenderName() + ": " + item.stackSize); + playerVector.put(slot, slotCurrent); + } + } + } + + // Add entries to the vec vector + for (int slot : questVector.keySet()) { + String questEntry = questVector.get(slot); + vec.add(questEntry); + List playerItems = playerVector.get(slot); + if (playerItems != null && !playerItems.isEmpty()) { + StringBuilder playerNamesBuilder = new StringBuilder(); + for (String playerName : playerItems) { + if (playerNamesBuilder.length() > 0) { + playerNamesBuilder.append(", "); + } + playerNamesBuilder.append(playerName); + } + vec.add("[" + playerNamesBuilder.toString() + "]"); + } + } + } else if (objectives == EnumPartyObjectives.Shared || objectives == EnumPartyObjectives.Leader) { + HashMap totals = new HashMap(); + for (int slot : items.items.keySet()) { + IItemStack item = items.items.get(slot); + if (item != null) { + totals.put(slot, 0); + } + } + // Iterate over each player in the party + for (UUID uuid : party.getPlayerUUIDs()) { + if (uuid == null) + continue; + + if (objectives == EnumPartyObjectives.Leader && !party.getLeaderUUID().equals(uuid)) + continue; + + IPlayer player = NoppesUtilServer.getPlayer(uuid); + if (player != null) { + HashMap perPlayerMap = getProcessSetParty(player); + for (Map.Entry entry : items.items.entrySet()) { + int slot = entry.getKey(); + IItemStack reqItem = entry.getValue(); + if (reqItem == null) + continue; + IItemStack item = perPlayerMap.get(slot); + if (item != null && NoppesUtilPlayer.compareItems(reqItem, item, ignoreDamage, ignoreNBT)) { + int count = totals.get(slot); + count += item.stackSize; + totals.put(slot, count); + } + } + } + } + + // Check if the total count of required items collected by the party exceeds the required amount for each slot + for (int slot : items.items.keySet()) { + IItemStack reqItem = items.items.get(slot); + if (reqItem == null) + continue; + + int totalCount = totals.get(slot); + if (totalCount > reqItem.stackSize) { + totalCount = reqItem.stackSize; + } + String process = totalCount + "/" + reqItem.stackSize; + if (reqItem.hasDisplayName()) + vec.add(reqItem.getDisplayName() + ": " + process); + else + vec.add(reqItem.getUnlocalizedName() + ".name" + ": " + process); + } + } + return vec; + } + + @Override + public boolean isPartyCompleted(Party party) { + if (party == null || party.getObjectiveRequirement() == null) + return false; + + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + if (objectives == EnumPartyObjectives.All) { + for (UUID uuid : party.getPlayerUUIDs()) { + IPlayer player = NoppesUtilServer.getPlayer(uuid); + if (player != null) { + HashMap perPlayerMap = getProcessSetParty(player); + for (IItemStack reqItem : items.items.values()) { + boolean done = false; + for (IItemStack item : perPlayerMap.values()) { + if (NoppesUtilPlayer.compareItems(reqItem, item, ignoreDamage, ignoreNBT) && item.stackSize >= reqItem.stackSize) { + done = true; + break; + } + } + if (!done) + return false; + } + } + } + } else if (objectives == EnumPartyObjectives.Shared || objectives == EnumPartyObjectives.Leader) { + HashMap totals = new HashMap(); + for (int slot : items.items.keySet()) { + IItemStack item = items.items.get(slot); + if (item != null) { + totals.put(slot, 0); + } + } + + // Iterate over each player in the party + for (UUID uuid : party.getPlayerUUIDs()) { + if (uuid == null) + continue; + + if (objectives == EnumPartyObjectives.Leader && !party.getLeaderUUID().equals(uuid)) + continue; + + IPlayer player = NoppesUtilServer.getPlayer(uuid); + if (player != null) { + HashMap perPlayerMap = getProcessSetParty(player); + for (Map.Entry entry : items.items.entrySet()) { + int slot = entry.getKey(); + IItemStack reqItem = entry.getValue(); + if (reqItem == null) + continue; + IItemStack item = perPlayerMap.get(slot); + if (item != null && NoppesUtilPlayer.compareItems(reqItem, item, ignoreDamage, ignoreNBT)) { + int count = totals.get(slot); + count += item.stackSize; + totals.put(slot, count); + } + } + } + } + + // Check if the total count of required items collected by the party exceeds the required amount for each slot + for (int slot : items.items.keySet()) { + IItemStack reqItem = items.items.get(slot); + int totalCount = totals.get(slot); + if (totalCount < reqItem.stackSize) { + return false; // Party has not completed the objective + } + } + } + return true; + } + + public HashMap getProcessSetParty(IPlayer player) { + IItemStack extra = null; + if (pickedUpPlayer != null && player != null && player.getCommandSenderName().equals(pickedUpPlayer.getCommandSenderName())) + extra = pickedUpParty; + return buildProgressMap(collectAvailableStacks(player, extra)); + } + + private List collectAvailableStacks(IPlayer player, IItemStack extra) { + List available = new ArrayList<>(); + if (player == null) + return available; + + for (IItemStack stack : player.inventory.mainInventory) { + if (stack != null && stack.stackSize > 0) { + available.add(stack.copy()); + } + } + + if (extra != null && extra.stackSize > 0) { + available.add(extra.copy()); + } + + return available; + } + + private HashMap buildProgressMap(List available) { + HashMap map = new HashMap(); + List slots = new ArrayList<>(items.items.keySet()); + Collections.sort(slots); + + for (int slot : slots) { + IItemStack requirement = items.items.get(slot); + if (requirement == null) + continue; + + IItemStack progress = requirement.copy(); + progress.stackSize = 0; + int remaining = requirement.stackSize; + + for (IItemStack stack : available) { + if (stack == null || stack.stackSize <= 0) + continue; + + if (NoppesUtilPlayer.compareItems(requirement, stack, ignoreDamage, ignoreNBT)) { + int used = Math.min(stack.stackSize, remaining); + if (used > 0) { + progress.stackSize += used; + stack.stackSize -= used; + remaining -= used; + } + + if (remaining <= 0) + break; + } + } + + map.put(slot, progress); + } + + return map; + } + + private List aggregateAvailableStacks(List available) { + List aggregated = new ArrayList<>(); + for (IItemStack stack : available) { + if (stack == null || stack.stackSize <= 0) + continue; + + boolean found = false; + for (IItemStack existing : aggregated) { + if (NoppesUtilPlayer.compareItems(existing, stack, ignoreDamage, ignoreNBT)) { + existing.stackSize += stack.stackSize; + found = true; + break; + } + } + + if (!found) { + aggregated.add(stack.copy()); + } + } + return aggregated; + } + + public void setLeaveItems(boolean leaveItems) { + this.leaveItems = leaveItems; + } + + public boolean getLeaveItems() { + return this.leaveItems; + } + + public void setIgnoreDamage(boolean ignoreDamage) { + this.ignoreDamage = ignoreDamage; + } + + public boolean getIgnoreDamage() { + return this.ignoreDamage; + } + + public void setIgnoreNbt(boolean ignoreNbt) { + this.ignoreNBT = ignoreNbt; + } + + public boolean getIgnoreNbt() { + return this.ignoreNBT; + } + + class QuestItemObjective implements IQuestObjective { + private final QuestItem parent; + private final IPlayer player; + private final Party party; + private final IItemStack questItem; + + public QuestItemObjective(QuestItem this$0, IPlayer player, IItemStack item) { + this.parent = this$0; + this.player = player; + this.questItem = item; + this.party = null; + } + + public QuestItemObjective(QuestItem this$0, Party party, IItemStack item) { + this.parent = this$0; + this.player = null; + this.questItem = item; + this.party = party; + } + + public int getProgress() { + int count = 0; + if (player != null) { + IItemStack item = QuestItem.pickedUp; + if (!NoppesUtilServer.IsItemStackNull(item) && NoppesUtilPlayer.compareItems(this.questItem, item, this.parent.ignoreDamage, this.parent.ignoreNBT)) { + count += item.stackSize; + } + + for (int i = 0; i < this.player.inventory.getSizeInventory(); ++i) { + item = this.player.inventory.getStackInSlot(i); + if (!NoppesUtilServer.IsItemStackNull(item) && NoppesUtilPlayer.compareItems(this.questItem, item, this.parent.ignoreDamage, this.parent.ignoreNBT)) { + count += item.stackSize; + } + } + } else if (party != null && party.getObjectiveRequirement() != null) { + IItemStack item; + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP == null) + continue; + + if (!playerMP.getUniqueID().equals(party.getLeaderUUID()) && objectives == EnumPartyObjectives.Leader) + continue; + + int amount = 0; + for (int i = 0; i < playerMP.inventory.getSizeInventory(); ++i) { + item = playerMP.inventory.getStackInSlot(i); + if (!NoppesUtilServer.IsItemStackNull(item) && NoppesUtilPlayer.compareItems(this.questItem, item, this.parent.ignoreDamage, this.parent.ignoreNBT)) { + amount += item.stackSize; + } + } + + if (objectives == EnumPartyObjectives.All) { + count += ValueUtil.clamp(amount, 0, this.questItem.stackSize); + } else { + count += amount; + } + } + + if (objectives == EnumPartyObjectives.All) { + return ValueUtil.clamp(count, 0, this.questItem.stackSize * party.getPlayerNames().size()); + } + } + + return ValueUtil.clamp(count, 0, this.questItem.stackSize); + } + + public void setProgress(int progress) { + throw new CustomNPCsException("Cant set the progress of ItemQuests", new Object[0]); + } + + @Override + public void setPlayerProgress(String playerName, int progress) { + throw new CustomNPCsException("Cant set the progress of ItemQuests", new Object[0]); + } + + public int getMaxProgress() { + if (party != null && party.getObjectiveRequirement() != null) { + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + if (objectives == EnumPartyObjectives.All) { + return this.questItem.stackSize * party.getPlayerNames().size(); + } + } + + return this.questItem.stackSize; + } + + public boolean isCompleted() { + if (player != null) + return NoppesUtilPlayer.compareItems(this.player, this.questItem, this.parent.ignoreDamage, this.parent.ignoreNBT); + else if (party != null) + return comparePartyItems(this.party, this.questItem, this.parent.ignoreDamage, this.parent.ignoreNBT); + + return false; + } + + public boolean comparePartyItems(Party party, IItemStack item, boolean ignoreDamage, boolean ignoreNBT) { + int size = 0; + if (party.getObjectiveRequirement() == null) + return false; + + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP == null) + continue; + + if (objectives == EnumPartyObjectives.Leader && !playerMP.getUniqueID().equals(party.getLeaderUUID())) + continue; + + int amount = 0; + for (IItemStack is : playerMP.inventory.mainInventory) { + if (NoppesUtilPlayer.compareItems(item, is, ignoreDamage, ignoreNBT)) + amount += is.stackSize; + } + + if (objectives == EnumPartyObjectives.All) { + size += ValueUtil.clamp(amount, 0, this.questItem.stackSize); + } else { + size += amount; + } + } + + if (objectives == EnumPartyObjectives.All) { + return size >= item.stackSize * party.getPlayerNames().size(); + } + + return size >= item.stackSize; + } + + public String getText() { + return this.questItem.getDisplayName() + ": " + this.getProgress() + "/" + this.getMaxProgress(); + } + + @Override + public String getAdditionalText() { + if (party != null && party.getObjectiveRequirement() == EnumPartyObjectives.All) { + List incompletePlayers = new ArrayList<>(); + EnumPartyObjectives objectives = party.getObjectiveRequirement(); + for (String name : party.getPlayerNames()) { + IPlayer playerMP = NoppesUtilServer.getPlayerByName(name); + if (playerMP == null) { + incompletePlayers.add(name + ": " + "N/A"); + continue; + } + + int amount = 0; + for (IItemStack is : playerMP.inventory.mainInventory) { + if (NoppesUtilPlayer.compareItems(this.questItem, is, ignoreDamage, ignoreNBT)) + amount += is.stackSize; + } + + int completedSize = ValueUtil.clamp(amount, 0, this.questItem.stackSize); + if (completedSize < this.questItem.stackSize) + incompletePlayers.add(name + ": " + completedSize); + } + if (!incompletePlayers.isEmpty()) + return "[" + String.join(", ", incompletePlayers) + "]"; + } + return null; + } + } +} diff --git a/core/src/main/java/noppes/npcs/quests/QuestKill.java b/core/src/main/java/noppes/npcs/quests/QuestKill.java new file mode 100644 index 000000000..80725a787 --- /dev/null +++ b/core/src/main/java/noppes/npcs/quests/QuestKill.java @@ -0,0 +1,441 @@ +package noppes.npcs.quests; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.IQuestKill; +import noppes.npcs.api.handler.data.IQuestObjective; +import noppes.npcs.constants.EnumPartyObjectives; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerQuestData; +import noppes.npcs.controllers.data.QuestData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Vector; + +public class QuestKill extends QuestInterface implements IQuestKill { + public TreeMap targets = new TreeMap(); + public int targetType = 0; + public String customTargetType = "noppes.npcs.IEntity.EntityCustomNpc"; + + @Override + public void writeEntityToNBT(INbt compound) { + compound.setTag("QuestKills", NBTTags.nbtStringIntegerMap(targets)); + compound.setInteger("TargetType", targetType); + compound.setString("CustomTargetType", customTargetType); + } + + @Override + public void readEntityFromNBT(INbt compound) { + if (!compound.hasKey("QuestKills")) { + targets.clear(); + TreeMap oldTargets = new TreeMap(NBTTags.getStringIntegerMap(compound.getTagList("QuestDialogs", 10))); + targets.putAll(oldTargets); + } else { + targets = new TreeMap(NBTTags.getStringIntegerMap(compound.getTagList("QuestKills", 10))); + } + targetType = compound.getInteger("TargetType"); + customTargetType = compound.getString("CustomTargetType"); + } + + @Override + public boolean isCompleted(PlayerData playerData) { + PlayerQuestData playerdata = playerData.questData; + QuestData data = playerdata.activeQuests.get(questId); + if (data == null) + return false; + + HashMap killed = getKilled(data); + int completed = 0; + for (String entityName : targets.keySet()) { + int amount = 0; + if (killed.containsKey(entityName)) + amount = killed.get(entityName); + if (amount >= targets.get(entityName)) { + completed++; + } + } + if (completed >= targets.keySet().size()) + return true; + + if (killed.size() != targets.size()) + return false; + for (String IEntity : killed.keySet()) { + if (!targets.containsKey(IEntity) || targets.get(IEntity) > killed.get(IEntity)) + return false; + } + + return true; + } + + @Override + public void handleComplete(IPlayer player) { + super.handleComplete(player); + } + + @Override + public Vector getQuestLogStatus(IPlayer player) { + Vector vec = new Vector(); + PlayerQuestData playerdata = PlayerData.get(player).questData; + QuestData data = playerdata.activeQuests.get(questId); + if (data == null) + return vec; + HashMap killed = getKilled(data); + for (String entityName : targets.keySet()) { + //Class cls = (Class) EntityList.stringToClassMapping.get(entityName); + int amount = 0; + if (killed.containsKey(entityName)) + amount = killed.get(entityName); + String state = amount + "/" + targets.get(entityName); + + vec.add(entityName + ": " + state); + } + + return vec; + } + + public HashMap getKilled(QuestData data) { + return NBTTags.getStringIntegerMap(data.extraData.getTagList("Killed", 10)); + } + + public void setKilled(QuestData data, HashMap killed) { + data.extraData.setTag("Killed", NBTTags.nbtStringIntegerMap(killed)); + } + + public IQuestObjective[] getObjectives(IPlayer player) { + List list = new ArrayList(); + Iterator var3 = this.targets.entrySet().iterator(); + + while (var3.hasNext()) { + Map.Entry entry = (Map.Entry) var3.next(); + list.add(new noppes.npcs.quests.QuestKill.QuestKillObjective(this, player, (String) entry.getKey(), (Integer) entry.getValue())); + } + + return (IQuestObjective[]) list.toArray(new IQuestObjective[list.size()]); + } + + public HashMap getPlayerKilled(QuestData data, String playerName) { + return NBTTags.getStringIntegerMap(data.extraData.getTagList(playerName + "Killed", 10)); + } + + public void setPlayerKilled(QuestData data, HashMap killed, String playerName) { + data.extraData.setTag(playerName + "Killed", NBTTags.nbtStringIntegerMap(killed)); + } + + public IQuestObjective[] getPartyObjectives(Party party) { + List list = new ArrayList(); + Iterator var3 = this.targets.entrySet().iterator(); + + while (var3.hasNext()) { + Map.Entry entry = (Map.Entry) var3.next(); + list.add(new noppes.npcs.quests.QuestKill.QuestKillObjective(this, party, (String) entry.getKey(), (Integer) entry.getValue())); + } + + return (IQuestObjective[]) list.toArray(new IQuestObjective[list.size()]); + } + + @Override + public Vector getPartyQuestLogStatus(Party party) { + Vector vec = new Vector(); + QuestData data = party.getQuestData(); + if (data == null) + return vec; + + if (data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String entityName : targets.keySet()) { + String firstLine = entityName + ": " + targets.get(entityName); + List playerResults = new ArrayList<>(); + for (String player : party.getPlayerNames()) { + HashMap killed = getPlayerKilled(data, player); + int amount = 0; + if (killed.containsKey(entityName)) + amount = killed.get(entityName); + + if (amount < targets.get(entityName)) { + String state = player + ": " + amount; + playerResults.add(state); + } + } + + if (!playerResults.isEmpty()) { + vec.add(firstLine); + vec.add("[" + String.join(", ", playerResults) + "]"); + } else { + vec.add(firstLine + " (Done)"); + } + } + } else { + HashMap killed = getKilled(data); + for (String entityName : targets.keySet()) { + int amount = 0; + if (killed.containsKey(entityName)) + amount = killed.get(entityName); + String state = amount + "/" + targets.get(entityName); + + vec.add(entityName + ": " + state); + } + } + return vec; + } + + @Override + public boolean isPartyCompleted(Party party) { + if (party == null) + return false; + + QuestData data = party.getQuestData(); + if (data == null) + return false; + + if (data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String entityName : targets.keySet()) { + for (String player : party.getPlayerNames()) { + HashMap killed = getPlayerKilled(data, player); + int amount = killed.getOrDefault(entityName, 0); + if (amount < targets.get(entityName)) { + return false; + } + } + } + } else { + int completed = 0; + HashMap killed = getKilled(data); + for (String entityName : targets.keySet()) { + int amount = 0; + if (killed.containsKey(entityName)) + amount = killed.get(entityName); + if (amount >= targets.get(entityName)) { + completed++; + } + } + if (completed >= targets.keySet().size()) + return true; + + if (killed.size() != targets.size()) + return false; + for (String IEntity : killed.keySet()) { + if (!targets.containsKey(IEntity) || targets.get(IEntity) > killed.get(IEntity)) + return false; + } + } + + return true; + } + + public void setTargetType(int type) { + if (type < 0) + type = 0; + if (type > 2) + type = 2; + + this.targetType = type; + } + + public int getTargetType() { + return this.targetType; + } + + class QuestKillObjective implements IQuestObjective { + private final QuestKill parent; + private final IPlayer player; + private final Party party; + private final String IEntity; + private final int amount; + + public QuestKillObjective(QuestKill parent, IPlayer player, String IEntity, int amount) { + this.parent = parent; + this.player = player; + this.IEntity = IEntity; + this.amount = amount; + this.party = null; + } + + public QuestKillObjective(QuestKill parent, Party party, String IEntity, int amount) { + this.parent = parent; + this.party = party; + this.IEntity = IEntity; + this.amount = amount; + this.player = null; + } + + public int getProgress() { + if (player != null) { + PlayerData data = PlayerData.get(player); + PlayerQuestData playerdata = data.questData; + QuestData questdata = (QuestData) playerdata.activeQuests.get(this.parent.questId); + if (questdata != null) { + HashMap killed = this.parent.getKilled(questdata); + return !killed.containsKey(this.IEntity) ? 0 : (Integer) killed.get(this.IEntity); + } + } else if (party != null) { + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + int howManyDone = 0; + for (String player : party.getPlayerNames()) { + HashMap killed = this.parent.getPlayerKilled(questdata, player); + int currentProgress = !killed.containsKey(this.IEntity) ? 0 : (Integer) killed.get(this.IEntity); + if (currentProgress >= this.amount) + howManyDone += 1; + } + if (howManyDone == party.getPlayerNames().size()) + return getMaxProgress(); + + return 0; + } else { + HashMap killed = this.parent.getKilled(questdata); + return !killed.containsKey(this.IEntity) ? 0 : (Integer) killed.get(this.IEntity); + } + } + } + return 0; + } + + public void setProgress(int progress) { + if (progress >= 0 && progress <= this.amount) { + if (player != null) { + PlayerData data = PlayerData.get(player); + PlayerQuestData playerdata = data.questData; + QuestData questdata = (QuestData) playerdata.activeQuests.get(this.parent.questId); + if (questdata != null) { + HashMap killed = this.parent.getKilled(questdata); + if (!killed.containsKey(this.IEntity) || (Integer) killed.get(this.IEntity) != progress) { + killed.put(this.IEntity, progress); + this.parent.setKilled(questdata, killed); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[2]); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[4]); + data.updateClient = true; + data.save(); + } + } + } else if (party != null) { + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String player : party.getPlayerNames()) { + HashMap killed = this.parent.getPlayerKilled(questdata, player); + if (!killed.containsKey(this.IEntity) || (Integer) killed.get(this.IEntity) != progress) { + killed.put(this.IEntity, progress); + this.parent.setPlayerKilled(questdata, killed, player); + } + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[2]); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[4]); + } else { + HashMap killed = this.parent.getKilled(questdata); + if (!killed.containsKey(this.IEntity) || (Integer) killed.get(this.IEntity) != progress) { + killed.put(this.IEntity, progress); + this.parent.setKilled(questdata, killed); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[2]); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[4]); + } + } + } + } + } else { + throw new CustomNPCsException("Progress has to be between 0 and " + this.amount, new Object[0]); + } + } + + @Override + public void setPlayerProgress(String playerName, int progress) { + if (progress >= 0 && progress <= this.amount) { + IPlayer foundplayer = NoppesUtilServer.getPlayerByName(playerName); + if (foundplayer != null && party == null) { + PlayerData data = PlayerData.get(foundplayer); + PlayerQuestData playerdata = data.questData; + QuestData questdata = (QuestData) playerdata.activeQuests.get(this.parent.questId); + if (questdata != null) { + HashMap killed = this.parent.getKilled(questdata); + if (!killed.containsKey(this.IEntity) || (Integer) killed.get(this.IEntity) != progress) { + killed.put(this.IEntity, progress); + this.parent.setKilled(questdata, killed); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[2]); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[4]); + data.updateClient = true; + data.save(); + } + } + } else if (foundplayer != null) { + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + HashMap killed = this.parent.getPlayerKilled(questdata, playerName); + if (!killed.containsKey(this.IEntity) || (Integer) killed.get(this.IEntity) != progress) { + killed.put(this.IEntity, progress); + this.parent.setPlayerKilled(questdata, killed, playerName); + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[2]); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[4]); + } else { + HashMap killed = this.parent.getKilled(questdata); + if (!killed.containsKey(this.IEntity) || (Integer) killed.get(this.IEntity) != progress) { + killed.put(this.IEntity, progress); + this.parent.setKilled(questdata, killed); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[2]); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[4]); + } + } + } + } + } else { + throw new CustomNPCsException("Progress has to be between 0 and " + this.amount, new Object[0]); + } + } + + public int getMaxProgress() { + return this.amount; + } + + public boolean isCompleted() { + return this.getProgress() >= this.amount; + } + + public String getText() { + if (party != null) { + if (party.getObjectiveRequirement() == EnumPartyObjectives.All) { + return this.IEntity + ": " + this.getMaxProgress() + (isCompleted() ? " (Done)" : ""); + } + } + return this.IEntity + ": " + this.getProgress() + "/" + this.getMaxProgress(); + } + + @Override + public String getAdditionalText() { + if (party != null) { + List incompletePlayers = new ArrayList<>(); + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String player : party.getPlayerNames()) { + HashMap killed = this.parent.getPlayerKilled(questdata, player); + int currentProgress = !killed.containsKey(this.IEntity) ? 0 : (Integer) killed.get(this.IEntity); + if (currentProgress < this.amount) { + String state = player + ": " + currentProgress; + incompletePlayers.add(state); + } + } + if (!incompletePlayers.isEmpty()) + return "[" + String.join(", ", incompletePlayers) + "]"; + } + } + } + return null; + } + } +} diff --git a/core/src/main/java/noppes/npcs/quests/QuestLocation.java b/core/src/main/java/noppes/npcs/quests/QuestLocation.java new file mode 100644 index 000000000..1de3b3a24 --- /dev/null +++ b/core/src/main/java/noppes/npcs/quests/QuestLocation.java @@ -0,0 +1,448 @@ +package noppes.npcs.quests; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.IQuestLocation; +import noppes.npcs.api.handler.data.IQuestObjective; +import noppes.npcs.constants.EnumPartyObjectives; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerQuestData; +import noppes.npcs.controllers.data.QuestData; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +public class QuestLocation extends QuestInterface implements IQuestLocation { + public String location = ""; + public String location2 = ""; + public String location3 = ""; + + @Override + public void readEntityFromNBT(INbt compound) { + location = compound.getString("QuestLocation"); + location2 = compound.getString("QuestLocation2"); + location3 = compound.getString("QuestLocation3"); + } + + @Override + public void writeEntityToNBT(INbt compound) { + compound.setString("QuestLocation", location); + compound.setString("QuestLocation2", location2); + compound.setString("QuestLocation3", location3); + } + + @Override + public boolean isCompleted(PlayerData playerData) { + PlayerQuestData playerdata = playerData.questData; + QuestData data = playerdata.activeQuests.get(questId); + if (data == null) + return false; + return getFound(data, 0); + } + + @Override + public void handleComplete(IPlayer player) { + super.handleComplete(player); + } + + @Override + public Vector getQuestLogStatus(IPlayer player) { + Vector vec = new Vector(); + PlayerQuestData playerdata = PlayerData.get(player).questData; + QuestData data = playerdata.activeQuests.get(questId); + if (data == null) + return vec; + String found = PlatformServiceHolder.get().translateToLocal("quest.found"); + String notfound = PlatformServiceHolder.get().translateToLocal("quest.notfound"); + + if (!location.isEmpty()) + vec.add(location + ": " + (getFound(data, 1) ? found : notfound)); + if (!location2.isEmpty()) + vec.add(location2 + ": " + (getFound(data, 2) ? found : notfound)); + if (!location3.isEmpty()) + vec.add(location3 + ": " + (getFound(data, 3) ? found : notfound)); + + return vec; + } + + @Override + public IQuestObjective[] getObjectives(IPlayer player) { + List list = new ArrayList(); + if (!this.location.isEmpty()) { + list.add(new noppes.npcs.quests.QuestLocation.QuestLocationObjective(this, player, this.location, "LocationFound")); + } + + if (!this.location2.isEmpty()) { + list.add(new noppes.npcs.quests.QuestLocation.QuestLocationObjective(this, player, this.location2, "Location2Found")); + } + + if (!this.location3.isEmpty()) { + list.add(new noppes.npcs.quests.QuestLocation.QuestLocationObjective(this, player, this.location3, "Location3Found")); + } + + return (IQuestObjective[]) list.toArray(new IQuestObjective[list.size()]); + } + + public boolean getFound(QuestData data, int i) { + if (i == 1) + return data.extraData.getBoolean("LocationFound"); + if (i == 2) + return data.extraData.getBoolean("Location2Found"); + if (i == 3) + return data.extraData.getBoolean("Location3Found"); + + if (!location.isEmpty() && !data.extraData.getBoolean("LocationFound")) + return false; + if (!location2.isEmpty() && !data.extraData.getBoolean("Location2Found")) + return false; + if (!location3.isEmpty() && !data.extraData.getBoolean("Location3Found")) + return false; + return true; + } + + public boolean setFound(QuestData data, String location) { + if (location.equalsIgnoreCase(this.location) && !data.extraData.getBoolean("LocationFound")) { + data.extraData.setBoolean("LocationFound", true); + return true; + } + if (location.equalsIgnoreCase(location2) && !data.extraData.getBoolean("LocationFound2")) { + data.extraData.setBoolean("Location2Found", true); + return true; + } + if (location.equalsIgnoreCase(location3) && !data.extraData.getBoolean("LocationFound3")) { + data.extraData.setBoolean("Location3Found", true); + return true; + } + + return false; + } + + public boolean setFoundParty(Party party, IPlayer player, String location, boolean isLeader) { + QuestData data = party.getQuestData(); + if (data == null) + return false; + + if (data.quest == null) + return false; + + int memberCount = party.getPlayerNames().size(); + EnumPartyObjectives objectives = data.quest.partyOptions.objectiveRequirement; + if (objectives == EnumPartyObjectives.All) { + if (location.equalsIgnoreCase(this.location) && !data.extraData.getBoolean("LocationFound")) { + return setPartyPlayerFound(data, player, "LocationFound", memberCount); + } + if (location.equalsIgnoreCase(location2) && !data.extraData.getBoolean("LocationFound2")) { + return setPartyPlayerFound(data, player, "Location2Found", memberCount); + } + if (location.equalsIgnoreCase(location3) && !data.extraData.getBoolean("LocationFound3")) { + return setPartyPlayerFound(data, player, "Location3Found", memberCount); + } + } else if (objectives == EnumPartyObjectives.Leader && isLeader || objectives == EnumPartyObjectives.Shared) { + if (location.equalsIgnoreCase(this.location) && !data.extraData.getBoolean("LocationFound")) { + data.extraData.setBoolean("LocationFound", true); + return true; + } + if (location.equalsIgnoreCase(location2) && !data.extraData.getBoolean("LocationFound2")) { + data.extraData.setBoolean("Location2Found", true); + return true; + } + if (location.equalsIgnoreCase(location3) && !data.extraData.getBoolean("LocationFound3")) { + data.extraData.setBoolean("Location3Found", true); + return true; + } + } + return false; + } + + public List getPartyFound(Party party, String locationFoundKey) { + List foundPlayers = new ArrayList<>(); + if (party == null) + return foundPlayers; + + QuestData data = party.getQuestData(); + if (data == null) + return foundPlayers; + + String locationKey = "Players" + locationFoundKey; + if (data.extraData.hasKey(locationKey)) { + foundPlayers = NBTTags.getStringList(data.extraData.getTagList(locationKey, 10)); + } + + return foundPlayers; + } + + + public boolean setPartyPlayerFound(QuestData data, IPlayer player, String locationFoundKey, int partySize) { + boolean newFind = false; + List foundPlayers; + String locationKey = "Players" + locationFoundKey; + if (data.extraData.hasKey(locationKey)) { + foundPlayers = NBTTags.getStringList(data.extraData.getTagList(locationKey, 10)); + } else { + foundPlayers = new ArrayList<>(); + } + + if (!foundPlayers.contains(player.getCommandSenderName())) { + foundPlayers.add(player.getCommandSenderName()); + newFind = true; + } + + data.extraData.setTag(locationKey, NBTTags.nbtStringList(foundPlayers)); + if (foundPlayers.size() == partySize) { + data.extraData.setBoolean(locationFoundKey, true); + } + + return newFind; + } + + @Override + public IQuestObjective[] getPartyObjectives(Party party) { + List list = new ArrayList(); + if (!this.location.isEmpty()) { + list.add(new noppes.npcs.quests.QuestLocation.QuestLocationObjective(this, party, this.location, "LocationFound", getPartyFound(party, "LocationFound"))); + } + + if (!this.location2.isEmpty()) { + list.add(new noppes.npcs.quests.QuestLocation.QuestLocationObjective(this, party, this.location2, "Location2Found", getPartyFound(party, "Location2Found"))); + } + + if (!this.location3.isEmpty()) { + list.add(new noppes.npcs.quests.QuestLocation.QuestLocationObjective(this, party, this.location3, "Location3Found", getPartyFound(party, "Location3Found"))); + } + + return (IQuestObjective[]) list.toArray(new IQuestObjective[list.size()]); + } + + @Override + public Vector getPartyQuestLogStatus(Party party) { + Vector vec = new Vector(); + QuestData data = party.getQuestData(); + if (data == null) + return vec; + String found = PlatformServiceHolder.get().translateToLocal("quest.found"); + String notfound = PlatformServiceHolder.get().translateToLocal("quest.notfound"); + + if (data.quest == null) + return vec; + + boolean requireAll = data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All; + if (!location.isEmpty()) { + vec.add(location + ": " + (getFound(data, 1) ? found : notfound)); + if (requireAll) { + List playersFound = getPartyFound(party, "LocationFound"); + String playersFoundString = "Completed: " + String.join(", ", playersFound); + vec.add(playersFoundString); + } + } + if (!location2.isEmpty()) { + vec.add(location2 + ": " + (getFound(data, 2) ? found : notfound)); + if (requireAll) { + List playersFound = getPartyFound(party, "Location2Found"); + String playersFoundString = "Completed: " + String.join(", ", playersFound); + vec.add(playersFoundString); + } + } + if (!location3.isEmpty()) { + vec.add(location3 + ": " + (getFound(data, 3) ? found : notfound)); + if (requireAll) { + List playersFound = getPartyFound(party, "Location3Found"); + String playersFoundString = "Completed: " + String.join(", ", playersFound); + vec.add(playersFoundString); + } + } + return vec; + } + + public boolean isMultiQuest(Party party) { + QuestData data = party.getQuestData(); + if (data == null) + return false; + + if (data.quest == null) + return false; + + return data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All; + } + + @Override + public boolean isPartyCompleted(Party party) { + if (party == null) + return false; + + QuestData data = party.getQuestData(); + if (data == null) + return false; + return getFound(data, 0); + } + + public void setLocation1(String loc1) { + this.location = loc1; + } + + public String getLocation1() { + return location; + } + + public void setLocation2(String loc2) { + this.location2 = loc2; + } + + public String getLocation2() { + return location2; + } + + public void setLocation3(String loc3) { + this.location3 = loc3; + } + + public String getLocation3() { + return location3; + } + + class QuestLocationObjective implements IQuestObjective { + private final QuestLocation parent; + private final IPlayer player; + private final Party party; + private final String location; + private final String nbtName; + private final List completedPlayers; + + public QuestLocationObjective(QuestLocation this$0, IPlayer player, String location, String nbtName) { + this.parent = this$0; + this.player = player; + this.location = location; + this.nbtName = nbtName; + this.party = null; + this.completedPlayers = new ArrayList<>(); + } + + public QuestLocationObjective(QuestLocation this$0, Party party, String location, String nbtName, List completedPlayers) { + this.parent = this$0; + this.player = null; + this.location = location; + this.nbtName = nbtName; + this.party = party; + this.completedPlayers = new ArrayList<>(completedPlayers); + } + + public int getProgress() { + return this.isCompleted() ? 1 : 0; + } + + public void setProgress(int progress) { + if (progress >= 0 && progress <= 1) { + if (player != null) { + PlayerData data = PlayerData.get(player); + QuestData questData = (QuestData) data.questData.activeQuests.get(this.parent.questId); + boolean completed = questData.extraData.getBoolean(this.nbtName); + if ((!completed || progress != 1) && (completed || progress != 0)) { + questData.extraData.setBoolean(this.nbtName, progress == 1); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[3]); + data.save(); + data.updateClient = true; + } + } else if (party != null) { + QuestData questData = party.getQuestData(); + boolean completed = questData.extraData.getBoolean(this.nbtName); + if ((!completed || progress != 1) && (completed || progress != 0)) { + boolean setTo = progress == 1; + questData.extraData.setBoolean(this.nbtName, setTo); + if (questData.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + completedPlayers.clear(); + if (setTo) { + completedPlayers.addAll(party.getPlayerNames()); + } + String locationKey = "Players" + nbtName; + questData.extraData.setTag(locationKey, NBTTags.nbtStringList(completedPlayers)); + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[3]); + } + } + } else { + throw new CustomNPCsException("Progress has to be 0 or 1", new Object[0]); + } + } + + @Override + public void setPlayerProgress(String playerName, int progress) { + if (progress >= 0 && progress <= 1) { + IPlayer foundplayer = NoppesUtilServer.getPlayerByName(playerName); + if (foundplayer != null && party == null) { + PlayerData data = PlayerData.get(foundplayer); + QuestData questData = (QuestData) data.questData.activeQuests.get(this.parent.questId); + boolean completed = questData.extraData.getBoolean(this.nbtName); + if ((!completed || progress != 1) && (completed || progress != 0)) { + questData.extraData.setBoolean(this.nbtName, progress == 1); + data.questData.checkQuestCompletion(data, EnumQuestType.values()[3]); + data.save(); + data.updateClient = true; + } + } else if (foundplayer != null) { + QuestData questData = party.getQuestData(); + boolean completed = questData.extraData.getBoolean(this.nbtName); + if ((!completed || progress != 1) && (completed || progress != 0)) { + boolean setTo = progress == 1; + questData.extraData.setBoolean(this.nbtName, setTo); + if (questData.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + if (setTo) { + if (!completedPlayers.contains(foundplayer.getCommandSenderName())) + completedPlayers.add(foundplayer.getCommandSenderName()); + } else { + completedPlayers.remove(foundplayer.getCommandSenderName()); + } + String locationKey = "Players" + nbtName; + questData.extraData.setTag(locationKey, NBTTags.nbtStringList(completedPlayers)); + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.values()[3]); + } + } + } else { + throw new CustomNPCsException("Progress has to be 0 or 1", new Object[0]); + } + } + + public int getMaxProgress() { + return 1; + } + + public boolean isCompleted() { + QuestData questData = null; + if (player != null) { + PlayerData data = PlayerData.get(player); + PlayerQuestData playerQuestData = data.questData; + if (playerQuestData != null) { + questData = playerQuestData.activeQuests.get(this.parent.questId); + } + } else if (party != null) { + questData = party.getQuestData(); + } + + if (questData != null) { + return questData.extraData.getBoolean(this.nbtName); + } + return false; + } + + public String getText() { + return this.location + ": " + (this.isCompleted() ? "Found" : "Not Found"); + } + + public String getAdditionalText() { + if (party != null && parent.isMultiQuest(party) && completedPlayers.size() != party.getPlayerNames().size()) + return "Completed: " + String.join(", ", completedPlayers); + return null; + } + } +} diff --git a/core/src/main/java/noppes/npcs/quests/QuestManual.java b/core/src/main/java/noppes/npcs/quests/QuestManual.java new file mode 100644 index 000000000..876a490ed --- /dev/null +++ b/core/src/main/java/noppes/npcs/quests/QuestManual.java @@ -0,0 +1,389 @@ +package noppes.npcs.quests; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.IQuestObjective; +import noppes.npcs.constants.EnumPartyObjectives; +import noppes.npcs.constants.EnumQuestType; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerQuestData; +import noppes.npcs.controllers.data.QuestData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.Vector; + +public class QuestManual extends QuestInterface { + public TreeMap manuals = new TreeMap(); + + @Override + public void readEntityFromNBT(INbt compound) { + manuals = new TreeMap<>(NBTTags.getStringIntegerMap(compound.getTagList("QuestManual", 10))); + } + + @Override + public void writeEntityToNBT(INbt compound) { + compound.setTag("QuestManual", NBTTags.nbtStringIntegerMap(manuals)); + } + + @Override + public boolean isCompleted(PlayerData playerData) { + if (playerData == null) + return false; + + QuestData data = playerData.questData.activeQuests.get(questId); + if (data == null) + return false; + HashMap manual = getManual(data); + if (manuals.size() != manual.size()) + return false; + + for (String IEntity : manuals.keySet()) { + if (!manual.containsKey(IEntity) || manual.get(IEntity) < manuals.get(IEntity)) + return false; + } + + return true; + } + + @Override + public void handleComplete(IPlayer player) { + } + + @Override + public Vector getQuestLogStatus(IPlayer player) { + Vector vec = new Vector(); + PlayerData playerdata = PlayerData.get(player); + if (playerdata == null) + return vec; + + QuestData data = playerdata.questData.activeQuests.get(questId); + if (data == null) + return vec; + + HashMap manual = getManual(data); + for (String IEntity : manuals.keySet()) { + vec.add(IEntity + ": " + manual.getOrDefault(IEntity, 0) + "/" + manuals.get(IEntity)); + } + return vec; + } + + public HashMap getManual(QuestData data) { + return NBTTags.getStringIntegerMap(data.extraData.getTagList("Manual", 10)); + } + + public void setManual(QuestData data, HashMap manual) { + data.extraData.setTag("Manual", NBTTags.nbtStringIntegerMap(manual)); + } + + public HashMap getPlayerManual(QuestData data, String playerName) { + return NBTTags.getStringIntegerMap(data.extraData.getTagList(playerName + "Manual", 10)); + } + + public void setPlayerManual(QuestData data, HashMap playerManual, String playerName) { + data.extraData.setTag(playerName + "Manual", NBTTags.nbtStringIntegerMap(playerManual)); + } + + @Override + public IQuestObjective[] getObjectives(IPlayer player) { + List list = new ArrayList(); + for (Entry entry : manuals.entrySet()) { + list.add(new QuestManualObjective(player, entry.getKey(), entry.getValue())); + } + return list.toArray(new IQuestObjective[list.size()]); + } + + @Override + public IQuestObjective[] getPartyObjectives(Party party) { + List list = new ArrayList(); + for (Entry entry : manuals.entrySet()) { + list.add(new QuestManualObjective(party, entry.getKey(), entry.getValue())); + } + return (IQuestObjective[]) list.toArray(new IQuestObjective[list.size()]); + } + + @Override + public Vector getPartyQuestLogStatus(Party party) { + Vector vec = new Vector(); + QuestData data = party.getQuestData(); + if (data == null) + return vec; + + if (data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String entityName : manuals.keySet()) { + String firstLine = entityName + ": " + manuals.get(entityName); + List playerResults = new ArrayList<>(); + for (String player : party.getPlayerNames()) { + HashMap playerManual = getPlayerManual(data, player); + int amount = 0; + if (playerManual.containsKey(entityName)) + amount = playerManual.get(entityName); + + if (amount < manuals.get(entityName)) { + String state = player + ": " + amount; + playerResults.add(state); + } + } + + if (!playerResults.isEmpty()) { + vec.add(firstLine); + vec.add("[" + String.join(", ", playerResults) + "]"); + } else { + vec.add(firstLine + " (Done)"); + } + } + } else { + HashMap manual = getManual(data); + for (String IEntity : manuals.keySet()) { + int amount = 0; + if (manual.containsKey(IEntity)) + amount = manual.get(IEntity); + String state = amount + "/" + manuals.get(IEntity); + vec.add(IEntity + ": " + state); + } + } + return vec; + } + + @Override + public boolean isPartyCompleted(Party party) { + if (party == null) + return false; + + QuestData data = party.getQuestData(); + if (data == null) + return false; + + if (data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String entityName : manuals.keySet()) { + for (String player : party.getPlayerNames()) { + HashMap playerManual = getPlayerManual(data, player); + int amount = playerManual.getOrDefault(entityName, 0); + if (amount < manuals.get(entityName)) { + return false; + } + } + } + } else { + int completed = 0; + HashMap playerManual = getManual(data); + for (String entityName : manuals.keySet()) { + int amount = 0; + if (playerManual.containsKey(entityName)) + amount = playerManual.get(entityName); + if (amount >= manuals.get(entityName)) { + completed++; + } + } + if (completed >= manuals.keySet().size()) + return true; + + if (playerManual.size() != manuals.size()) + return false; + for (String IEntity : playerManual.keySet()) { + if (!manuals.containsKey(IEntity) || manuals.get(IEntity) > playerManual.get(IEntity)) + return false; + } + } + return true; + } + + class QuestManualObjective implements IQuestObjective { + private final IPlayer player; + private final String IEntity; + private final Party party; + private final int amount; + + public QuestManualObjective(IPlayer player, String IEntity, int amount) { + this.player = player; + this.IEntity = IEntity; + this.amount = amount; + this.party = null; + } + + public QuestManualObjective(Party party, String IEntity, int amount) { + this.party = party; + this.IEntity = IEntity; + this.amount = amount; + this.player = null; + } + + @Override + public int getProgress() { + if (player != null) { + PlayerData data = PlayerData.get(player); + PlayerQuestData playerdata = data.questData; + QuestData questdata = (QuestData) playerdata.activeQuests.get(questId); + if (questdata != null) { + HashMap playerManual = getManual(questdata); + return !playerManual.containsKey(this.IEntity) ? 0 : (Integer) playerManual.get(this.IEntity); + } + } else if (party != null) { + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + int howManyDone = 0; + for (String player : party.getPlayerNames()) { + HashMap playerManual = getPlayerManual(questdata, player); + int currentProgress = !playerManual.containsKey(this.IEntity) ? 0 : (Integer) playerManual.get(this.IEntity); + if (currentProgress >= this.amount) + howManyDone += 1; + } + if (howManyDone == party.getPlayerNames().size()) + return getMaxProgress(); + + return 0; + } else { + HashMap playerManual = getManual(questdata); + return !playerManual.containsKey(this.IEntity) ? 0 : (Integer) playerManual.get(this.IEntity); + } + } + } + return 0; + } + + @Override + public void setProgress(int progress) { + if (progress >= 0 && progress <= this.amount) { + if (player != null) { + PlayerData data = PlayerData.get(player); + PlayerQuestData playerdata = data.questData; + QuestData questdata = (QuestData) playerdata.activeQuests.get(questId); + if (questdata != null) { + HashMap playerManual = getManual(questdata); + if (!playerManual.containsKey(this.IEntity) || (Integer) playerManual.get(this.IEntity) != progress) { + playerManual.put(this.IEntity, progress); + setManual(questdata, playerManual); + data.questData.checkQuestCompletion(data, EnumQuestType.Manual); + data.updateClient = true; + data.save(); + } + } + } else if (party != null) { + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String player : party.getPlayerNames()) { + HashMap playerManual = getPlayerManual(questdata, player); + if (!playerManual.containsKey(this.IEntity) || (Integer) playerManual.get(this.IEntity) != progress) { + playerManual.put(this.IEntity, progress); + setPlayerManual(questdata, playerManual, player); + } + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.Manual); + } else { + HashMap playerManual = getManual(questdata); + if (!playerManual.containsKey(this.IEntity) || (Integer) playerManual.get(this.IEntity) != progress) { + playerManual.put(this.IEntity, progress); + setManual(questdata, playerManual); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.Manual); + } + } + } + } + } else { + throw new CustomNPCsException("Progress has to be between 0 and " + this.amount, new Object[0]); + } + } + + @Override + public void setPlayerProgress(String playerName, int progress) { + if (progress >= 0 && progress <= this.amount) { + IPlayer foundplayer = NoppesUtilServer.getPlayerByName(playerName); + if (foundplayer != null && party == null) { + PlayerData data = PlayerData.get(foundplayer); + PlayerQuestData playerdata = data.questData; + QuestData questdata = (QuestData) playerdata.activeQuests.get(questId); + if (questdata != null) { + HashMap playerManual = getManual(questdata); + if (!playerManual.containsKey(this.IEntity) || (Integer) playerManual.get(this.IEntity) != progress) { + playerManual.put(this.IEntity, progress); + setManual(questdata, playerManual); + data.questData.checkQuestCompletion(data, EnumQuestType.Manual); + data.updateClient = true; + data.save(); + } + } + } else if (foundplayer != null) { + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + HashMap playerManual = getPlayerManual(questdata, foundplayer.getCommandSenderName()); + if (!playerManual.containsKey(this.IEntity) || (Integer) playerManual.get(this.IEntity) != progress) { + playerManual.put(this.IEntity, progress); + setPlayerManual(questdata, playerManual, foundplayer.getCommandSenderName()); + } + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.Manual); + } else { + HashMap playerManual = getManual(questdata); + if (!playerManual.containsKey(this.IEntity) || (Integer) playerManual.get(this.IEntity) != progress) { + playerManual.put(this.IEntity, progress); + setManual(questdata, playerManual); + PartyController.Instance().checkQuestCompletion(party, EnumQuestType.Manual); + } + } + } + } + } else { + throw new CustomNPCsException("Progress has to be between 0 and " + this.amount, new Object[0]); + } + } + + @Override + public int getMaxProgress() { + return amount; + } + + @Override + public boolean isCompleted() { + return getProgress() >= amount; + } + + @Override + public String getText() { + if (party != null) { + if (party.getObjectiveRequirement() == EnumPartyObjectives.All) { + return this.IEntity + ": " + this.getMaxProgress() + (isCompleted() ? " (Done)" : ""); + } + } + return this.IEntity + ": " + this.getProgress() + "/" + this.getMaxProgress(); + } + + @Override + public String getAdditionalText() { + if (party != null) { + List incompletePlayers = new ArrayList<>(); + QuestData questdata = party.getQuestData(); + if (questdata != null) { + if (questdata.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.All) { + for (String player : party.getPlayerNames()) { + HashMap playerManual = getPlayerManual(questdata, player); + int currentProgress = !playerManual.containsKey(this.IEntity) ? 0 : (Integer) playerManual.get(this.IEntity); + if (currentProgress < this.amount) { + String state = player + ": " + currentProgress; + incompletePlayers.add(state); + } + } + if (!incompletePlayers.isEmpty()) + return "[" + String.join(", ", incompletePlayers) + "]"; + } + } + } + return null; + } + } + +} diff --git a/core/src/main/java/noppes/npcs/roles/JobBard.java b/core/src/main/java/noppes/npcs/roles/JobBard.java new file mode 100644 index 000000000..a333a1e02 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobBard.java @@ -0,0 +1,162 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomItems; +import noppes.npcs.CustomNpcs; +import noppes.npcs.constants.EnumBardInstrument; +import java.util.List; + +public class JobBard extends JobInterface { + public int minRange = 2; + public int maxRange = 64; + + public boolean isStreamer = true; + public boolean hasOffRange = true; + + public String song = ""; + + private EnumBardInstrument instrument = EnumBardInstrument.Banjo; + + public JobBard(EntityNPCInterface npc) { + super(npc); + if (CustomItems.banjo != null) { + mainhand = new IItemStack(CustomItems.banjo); + overrideMainHand = overrideOffHand = true; + } + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setString("BardSong", song); + INbt.setInteger("BardMinRange", minRange); + INbt.setInteger("BardMaxRange", maxRange); + INbt.setInteger("BardInstrument", instrument.ordinal()); + INbt.setBoolean("BardStreamer", isStreamer); + INbt.setBoolean("BardHasOff", hasOffRange); + + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + song = INbt.getString("BardSong"); + minRange = INbt.getInteger("BardMinRange"); + maxRange = INbt.getInteger("BardMaxRange"); + setInstrument(INbt.getInteger("BardInstrument")); + isStreamer = INbt.getBoolean("BardStreamer"); + hasOffRange = INbt.getBoolean("BardHasOff"); + } + + public void setInstrument(int i) { + if (CustomItems.banjo == null) + return; + instrument = EnumBardInstrument.values()[i]; + overrideMainHand = overrideOffHand = instrument != EnumBardInstrument.None; + switch (instrument) { + case None: + this.mainhand = null; + this.offhand = null; + break; + case Banjo: + this.mainhand = new IItemStack(CustomItems.banjo); + this.offhand = null; + break; + case Violin: + this.mainhand = new IItemStack(CustomItems.violin); + this.offhand = new IItemStack(CustomItems.violinbow); + break; + case Guitar: + this.mainhand = new IItemStack(CustomItems.guitar); + this.offhand = null; + break; + case Harp: + this.mainhand = new IItemStack(CustomItems.harp); + this.offhand = null; + break; + case FrenchHorn: + this.mainhand = new IItemStack(CustomItems.frenchHorn); + this.offhand = null; + break; + } + } + + public EnumBardInstrument getInstrument() { + return instrument; + } + + public void onLivingUpdate() { + if (!npc.isRemote() || song.isEmpty()) + return; + + if (!MusicController.Instance.isPlaying() && Minecraft.getMinecraft().currentScreen == null) { + if (!this.play()) return; + } else if (MusicController.Instance.getEntity() != npc) { + IPlayer player = CustomNpcs.proxy.getPlayer(); + double distanceToPlayer = npc.getDistanceToEntity(player); + double distanceToMusic = MusicController.Instance.getDistance(); + if (Math.ceil(distanceToPlayer) < Math.ceil(distanceToMusic)) { + if (!this.play()) return; + } + } else if (hasOffRange) { + List list = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(maxRange, maxRange, maxRange)); + if (!list.contains(CustomNpcs.proxy.getPlayer())) + MusicController.Instance.stopMusic(); + } + + if (MusicController.Instance.isPlaying(song)) { + Minecraft.getMinecraft().mcMusicTicker.field_147676_d = 12000; + } + } + + private boolean play() { + List list = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(minRange, minRange, minRange)); + if (!list.contains(CustomNpcs.proxy.getPlayer())) + return false; + + if (isStreamer) + MusicController.Instance.playMusicJukebox(song, npc, hasOffRange ? maxRange : 0); + else + MusicController.Instance.playMusicBackground(song, npc, hasOffRange ? maxRange : 0); + return true; + } + + @Override + public void killed() { + delete(); + } + + @Override + public void delete() { + if (npc.worldObj.isRemote && hasOffRange) { + if (MusicController.Instance.isPlaying(song)) { + MusicController.Instance.stopAllSounds(); + } + } + } +} diff --git a/core/src/main/java/noppes/npcs/roles/JobChunkLoader.java b/core/src/main/java/noppes/npcs/roles/JobChunkLoader.java new file mode 100644 index 000000000..a0e1ca280 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobChunkLoader.java @@ -0,0 +1,107 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.ChunkController; +import java.util.ArrayList; +import java.util.List; + +public class JobChunkLoader extends JobInterface { + + private List chunks = new ArrayList(); + private int ticks = 20; + private long playerLastSeen = 0; + + public JobChunkLoader(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt compound) { + compound.setLong("ChunkPlayerLastSeen", playerLastSeen); + return compound; + } + + @Override + public void readFromNBT(INbt compound) { + playerLastSeen = compound.getLong("ChunkPlayerLastSeen"); + } + + @Override + public boolean aiShouldExecute() { + ticks--; + if (ticks > 0) + return false; + ticks = 20; + + List players = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(48, 48, 48)); + if (!players.isEmpty()) + playerLastSeen = System.currentTimeMillis(); + + //unload after 10 min + if (System.currentTimeMillis() > playerLastSeen + 600000) { + ChunkController.Instance.deleteNPC(npc); + chunks.clear(); + return false; + } + Ticket ticket = ChunkController.Instance.getTicket(npc); + if (ticket == null) //Only null when too many active chunkloaders already + return false; + double x = npc.posX / 16; + double z = npc.posZ / 16; + + List list = new ArrayList(); + list.add(new ChunkCoordIntPair(ValueUtil.floorDouble(x), ValueUtil.floorDouble(z))); + list.add(new ChunkCoordIntPair(MathHelper.ceiling_double_int(x), MathHelper.ceiling_double_int(z))); + list.add(new ChunkCoordIntPair(ValueUtil.floorDouble(x), MathHelper.ceiling_double_int(z))); + list.add(new ChunkCoordIntPair(MathHelper.ceiling_double_int(x), ValueUtil.floorDouble(z))); + + for (ChunkCoordIntPair chunk : list) { + if (!chunks.contains(chunk)) { + ForgeChunkManager.forceChunk(ticket, chunk); + } else + chunks.remove(chunk); + } + + for (ChunkCoordIntPair chunk : chunks) + ForgeChunkManager.unforceChunk(ticket, chunk); + + this.chunks = list; + return false; + } + + @Override + public void reset() { + ChunkController.Instance.deleteNPC(npc); + chunks.clear(); + playerLastSeen = 0; + } + + public void delete() { + } +} diff --git a/core/src/main/java/noppes/npcs/roles/JobConversation.java b/core/src/main/java/noppes/npcs/roles/JobConversation.java new file mode 100644 index 000000000..5fddc124e --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobConversation.java @@ -0,0 +1,271 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.PlayerQuestController; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.data.Availability; +import noppes.npcs.controllers.data.Line; +import noppes.npcs.controllers.data.Quest; +import noppes.npcs.controllers.data.QuestData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class JobConversation extends JobInterface { + public Availability availability = new Availability(); + + private ArrayList names = new ArrayList(); + private HashMap npcs = new HashMap(); + + public HashMap lines = new HashMap(); + + public int quest = -1; + public String questTitle = ""; + public int generalDelay = 400; + public int ticks = 100; + public int range = 20; + + private ConversationLine nextLine; + + private boolean hasStarted = false; + private int startedTicks = 20; + public int mode = 0; //0:Always, 1:Player near + + public JobConversation(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt compound) { + compound.setTag("ConversationAvailability", availability.writeToNBT(new INbt())); + compound.setInteger("ConversationQuest", quest); + compound.setInteger("ConversationDelay", generalDelay); + compound.setInteger("ConversationRange", range); + compound.setInteger("ConversationMode", mode); + + INbtList INbtList = new INbtList(); + for (int slot : lines.keySet()) { + ConversationLine line = lines.get(slot); + INbt INbt = new INbt(); + INbt.setInteger("Slot", slot); + line.writeEntityToNBT(INbt); + + INbtList.appendTag(INbt); + } + + compound.setTag("ConversationLines", INbtList); + if (hasQuest()) + compound.setString("ConversationQuestTitle", getQuest().title); + + return compound; + } + + @Override + public void readFromNBT(INbt compound) { + names.clear(); + availability.readFromNBT(compound.getCompoundTag("ConversationAvailability")); + quest = compound.getInteger("ConversationQuest"); + generalDelay = compound.getInteger("ConversationDelay"); + questTitle = compound.getString("ConversationQuestTitle"); + range = compound.getInteger("ConversationRange"); + mode = compound.getInteger("ConversationMode"); + + INbtList INbtList = compound.getTagList("ConversationLines", 10); + HashMap map = new HashMap(); + for (int i = 0; i < INbtList.tagCount(); i++) { + INbt INbt = INbtList.getCompoundTagAt(i); + ConversationLine line = new ConversationLine(); + line.readEntityFromNBT(INbt); + if (!line.npc.isEmpty() && !names.contains(line.npc.toLowerCase())) + names.add(line.npc.toLowerCase()); + + map.put(INbt.getInteger("Slot"), line); + } + lines = map; + ticks = generalDelay; + } + + public boolean hasQuest() { + return getQuest() != null; + } + + public Quest getQuest() { + if (npc.isRemote()) + return null; + return QuestController.Instance.quests.get(quest); + } + + @Override + public void aiUpdateTask() { + ticks--; + if (ticks > 0 || nextLine == null) + return; + say(nextLine); + boolean seenNext = false; + ConversationLine compare = nextLine; + nextLine = null; + for (ConversationLine line : lines.values()) { + if (line.isEmpty()) + continue; + if (seenNext) { + nextLine = line; + break; + } + if (line == compare) { + seenNext = true; + } + } + if (nextLine != null) + ticks = nextLine.delay; + else if (hasQuest()) { + List inRange = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(range, range, range)); + + for (IPlayer player : inRange) { + if (availability.isAvailable(player)) + PlayerQuestController.addActiveQuest(new QuestData(getQuest()), player); + } + } + } + + + @Override + public boolean aiShouldExecute() { + if (lines.isEmpty() || npc.isKilled() || npc.isAttacking() || !shouldRun()) + return false; + if (!hasStarted && mode == 1) { + if (startedTicks-- > 0) + return false; + startedTicks = 10; + if (npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(range, range, range)).isEmpty()) { + return false; + } + } + + for (ConversationLine line : lines.values()) { + if (line == null || line.isEmpty()) + continue; + nextLine = line; + break; + } + return nextLine != null; + } + + private boolean shouldRun() { + ticks--; + if (ticks > 0) + return false; + npcs.clear(); + List list = npc.worldObj.getEntitiesWithinAABB(EntityNPCInterface.class, npc.boundingBox.expand(10, 10, 10)); + for (EntityNPCInterface npc : list) { + if (!npc.isKilled() && !npc.isAttacking() && names.contains(npc.getCommandSenderName().toLowerCase())) + npcs.put(npc.getCommandSenderName().toLowerCase(), npc); + } + boolean bo = names.size() == npcs.size(); + if (!bo) + ticks = 20; + return bo; + } + + @Override + public boolean aiContinueExecute() { + for (EntityNPCInterface npc : npcs.values()) { + if (npc.isKilled() || npc.isAttacking()) + return false; + } + return nextLine != null; + } + + @Override + public void resetTask() { + nextLine = null; + ticks = generalDelay; + hasStarted = false; + } + + @Override + public void aiStartExecuting() { + startedTicks = 20; + hasStarted = true; + } + + private void say(ConversationLine line) { + List inRange = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(range, range, range)); + + EntityNPCInterface npc = npcs.get(line.npc.toLowerCase()); + if (npc == null) + return; + for (IPlayer player : inRange) { + if (availability.isAvailable(player)) + npc.say(player, line); + } + } + + @Override + public void reset() { + hasStarted = false; + resetTask(); + ticks = 60; + } + + @Override + public void killed() { + reset(); + } + + public class ConversationLine extends Line { + public String npc = ""; + public int delay = 40; + + public void writeEntityToNBT(INbt compound) { + compound.setString("Line", text); + compound.setString("Npc", npc); + compound.setString("Sound", sound); + compound.setInteger("Delay", delay); + } + + public void readEntityFromNBT(INbt compound) { + text = compound.getString("Line"); + npc = compound.getString("Npc"); + sound = compound.getString("Sound"); + delay = compound.getInteger("Delay"); + } + + public boolean isEmpty() { + return npc.isEmpty() || text.isEmpty(); + } + } + + public ConversationLine getLine(int slot) { + if (lines.containsKey(slot)) + return lines.get(slot); + ConversationLine line = new ConversationLine(); + lines.put(slot, line); + return line; + } +} diff --git a/core/src/main/java/noppes/npcs/roles/JobFollower.java b/core/src/main/java/noppes/npcs/roles/JobFollower.java new file mode 100644 index 000000000..97dac0e5e --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobFollower.java @@ -0,0 +1,99 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.config.ConfigMain; +import java.util.List; + +public class JobFollower extends JobInterface { + public EntityNPCInterface following = null; + private int ticks = 40; + private int range = 20; + public String name = ""; + + public JobFollower(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt compound) { + compound.setString("FollowingEntityName", name); + return compound; + } + + @Override + public void readFromNBT(INbt compound) { + name = compound.getString("FollowingEntityName"); + + } + + @Override + public boolean aiShouldExecute() { + if (npc.isAttacking()) + return false; + + ticks--; + if (ticks > 0) + return false; + + ticks = 10; + following = null; + List list = npc.worldObj.getEntitiesWithinAABB(EntityNPCInterface.class, npc.boundingBox.expand(getRange(), getRange(), getRange())); + for (EntityNPCInterface IEntity : list) { + if (IEntity == npc || IEntity.isKilled()) + continue; + if (IEntity.display.name.equalsIgnoreCase(name)) { + following = IEntity; + break; + } + } + + return false; + } + + private int getRange() { + if (range > ConfigMain.NpcNavRange) + return ConfigMain.NpcNavRange; + return range; + } + + public boolean isFollowing() { + return following != null; + } + + public void reset() { + } + + public void resetTask() { + following = null; + } + + public boolean hasOwner() { + return !name.isEmpty(); + } +} diff --git a/core/src/main/java/noppes/npcs/roles/JobGuard.java b/core/src/main/java/noppes/npcs/roles/JobGuard.java new file mode 100644 index 000000000..1b8d9a27e --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobGuard.java @@ -0,0 +1,85 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import java.util.ArrayList; +import java.util.List; + +public class JobGuard extends JobInterface { + + public boolean attacksAnimals = false; + public boolean attackHostileMobs = true; + public boolean attackCreepers = false; + + public List targets = new ArrayList(); + public boolean specific = false; + + public JobGuard(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setBoolean("GuardAttackAnimals", attacksAnimals); + INbt.setBoolean("GuardAttackMobs", attackHostileMobs); + INbt.setBoolean("GuardAttackCreepers", attackCreepers); + INbt.setBoolean("GuardSpecific", specific); + + INbt.setTag("GuardTargets", NBTTags.nbtStringList(targets)); + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + attacksAnimals = INbt.getBoolean("GuardAttackAnimals"); + attackHostileMobs = INbt.getBoolean("GuardAttackMobs"); + attackCreepers = INbt.getBoolean("GuardAttackCreepers"); + specific = INbt.getBoolean("GuardSpecific"); + + targets = NBTTags.getStringList(INbt.getTagList("GuardTargets", 10)); + } + + public boolean isEntityApplicable(IEntity IEntity) { + if (IEntity instanceof IPlayer || IEntity instanceof EntityNPCInterface) + return false; + if (specific && targets.contains("IEntity." + EntityList.getEntityString(IEntity) + ".name")) + return true; + + if (IEntity instanceof EntityAnimal) { + if (!attacksAnimals || IEntity instanceof EntityTameable && ((EntityTameable) IEntity).getOwner() != null) + return false; + return true; + } else if (IEntity instanceof EntityCreeper) { + return attackCreepers; + } else if (IEntity instanceof IMob || IEntity instanceof EntityDragon) { + return attackHostileMobs; + } + return false; + } +} diff --git a/core/src/main/java/noppes/npcs/roles/JobHealer.java b/core/src/main/java/noppes/npcs/roles/JobHealer.java new file mode 100644 index 000000000..ae867a091 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobHealer.java @@ -0,0 +1,95 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilServer; +import java.util.ArrayList; +import java.util.List; + +public class JobHealer extends JobInterface { + private long healTicks = 0; + public int range = 5; + public int speed = 5; + + public JobHealer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setInteger("HealerRange", range); + INbt.setInteger("HealerSpeed", speed); + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + range = INbt.getInteger("HealerRange"); + speed = INbt.getInteger("HealerSpeed"); + } + + private List toHeal = new ArrayList(); + + public boolean aiShouldExecute() { + healTicks++; + if (healTicks < speed * 10) + return false; + + for (Object plObj : npc.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, npc.boundingBox.expand(range, range / 2.0, range))) { + IEntityLivingBase IEntity = (IEntityLivingBase) plObj; + + if (IEntity instanceof IPlayer) { + IPlayer player = (IPlayer) IEntity; + if (player.getHealth() < player.getMaxHealth() && !npc.faction.isAggressiveToPlayer(player)) + toHeal.add(player); + } + if (IEntity instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) IEntity; + if (npc.getHealth() < npc.getMaxHealth() && !this.npc.faction.isAggressiveToNpc(npc)) + toHeal.add(npc); + } + + } + + healTicks = 0; + return !toHeal.isEmpty(); + } + + public void aiStartExecuting() { + for (IEntityLivingBase IEntity : toHeal) { + float heal = IEntity.getMaxHealth() / 20; //heal 5% of max health + heal(IEntity, heal > 0 ? heal : 1); + } + toHeal.clear(); + } + + public void heal(IEntityLivingBase IEntity, float amount) { + IEntity.heal(amount); + NoppesUtilServer.spawnParticle(IEntity, "heal", IEntity.dimension); + } +} diff --git a/core/src/main/java/noppes/npcs/roles/JobInterface.java b/core/src/main/java/noppes/npcs/roles/JobInterface.java new file mode 100644 index 000000000..c9e99cc65 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobInterface.java @@ -0,0 +1,78 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +public abstract class JobInterface { + public EntityNPCInterface npc; + + public boolean overrideMainHand = false; + public boolean overrideOffHand = false; + + public IItemStack mainhand = null; + public IItemStack offhand = null; + + public JobInterface(EntityNPCInterface npc) { + this.npc = npc; + } + + public abstract INbt writeToNBT(INbt INbt); + + public abstract void readFromNBT(INbt INbt); + + public void killed() { + } + + ; + + public void delete() { + } + + ; + + public boolean aiShouldExecute() { + return false; + } + + public boolean aiContinueExecute() { + return aiShouldExecute(); + } + + public void aiStartExecuting() { + } + + public void aiUpdateTask() { + } + + public void reset() { + } + + public void resetTask() { + } + + +} diff --git a/core/src/main/java/noppes/npcs/roles/JobItemGiver.java b/core/src/main/java/noppes/npcs/roles/JobItemGiver.java new file mode 100644 index 000000000..f82383988 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobItemGiver.java @@ -0,0 +1,311 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.controllers.GlobalDataController; +import noppes.npcs.controllers.data.Availability; +import noppes.npcs.controllers.data.Line; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerItemGiverData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Vector; + +public class JobItemGiver extends JobInterface { + + public int cooldownType = 0; + public int givingMethod = 0; + public int cooldown = 10; + public NpcMiscInventory inventory; + public int itemGiverId = 0; + + public List lines = new ArrayList(); + + private int ticks = 10; + + private List recentlyChecked = new ArrayList(); + private List toCheck; + public Availability availability = new Availability(); + + public JobItemGiver() { + super(null); + inventory = new NpcMiscInventory(9); + } + + public JobItemGiver(EntityNPCInterface npc) { + super(npc); + inventory = new NpcMiscInventory(9); + lines.add("Have these items {player}"); + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setInteger("igCooldownType", cooldownType); + INbt.setInteger("igGivingMethod", givingMethod); + INbt.setInteger("igCooldown", cooldown); + INbt.setInteger("ItemGiverId", itemGiverId); + INbt.setTag("igLines", NBTTags.nbtStringList(lines)); + INbt.setTag("igJobInventory", inventory.getToNBT()); + INbt.setTag("igAvailability", availability.writeToNBT(new INbt())); + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + itemGiverId = INbt.getInteger("ItemGiverId"); + cooldownType = INbt.getInteger("igCooldownType"); + givingMethod = INbt.getInteger("igGivingMethod"); + cooldown = INbt.getInteger("igCooldown"); + lines = NBTTags.getStringList(INbt.getTagList("igLines", 10)); + inventory.setFromNBT(INbt.getCompoundTag("igJobInventory")); + + if (itemGiverId == 0 && GlobalDataController.Instance != null) + itemGiverId = GlobalDataController.Instance.incrementItemGiverId(); + + availability.readFromNBT(INbt.getCompoundTag("igAvailability")); + } + + public INbtList newHashMapNBTList(HashMap lines) { + INbtList INbtList = new INbtList(); + HashMap lines2 = lines; + for (String s : lines2.keySet()) { + INbt INbt = new INbt(); + INbt.setString("Line", s); + INbt.setLong("Time", lines.get(s)); + INbtList.appendTag(INbt); + } + return INbtList; + } + + public HashMap getNBTLines(INbtList tagList) { + HashMap map = new HashMap(); + for (int i = 0; i < tagList.tagCount(); i++) { + INbt INbt = tagList.getCompoundTagAt(i); + String line = INbt.getString("Line"); + long time = INbt.getLong("Time"); + map.put(line, time); + + } + return map; + } + + public boolean giveItems(IPlayer player) { + PlayerItemGiverData data = PlayerData.get(player).itemgiverData; + if (!canPlayerInteract(data)) { + return false; + } + + Vector items = new Vector(); + Vector toGive = new Vector(); + + for (IItemStack is : inventory.items.values()) + if (is != null) + items.add(is.copy()); + if (items.isEmpty()) + return false; + if (isAllGiver()) { + toGive = items; + } else if (isRemainingGiver()) { + for (IItemStack is : items) { + if (!playerHasItem(player, is.getItem())) + toGive.add(is); + } + } else if (isRandomGiver()) { + toGive.add(items.get(npc.worldObj.rand.nextInt(items.size())).copy()); + } else if (isGiverWhenNotOwnedAny()) { + boolean ownsItems = false; + for (IItemStack is : items) { + if (playerHasItem(player, is.getItem())) { + ownsItems = true; + break; + } + } + if (!ownsItems) { + toGive = items; + } else + return false; + } else if (isChainedGiver()) { + int itemIndex = data.getItemIndex(this); + int i = 0; + for (IItemStack item : inventory.items.values()) { + if (i == itemIndex) { + toGive.add(item); + break; + } + i++; + } + } + if (toGive.isEmpty()) + return false; + if (givePlayerItems(player, toGive)) { + if (!lines.isEmpty()) { + npc.say(player, new Line(lines.get(npc.getRNG().nextInt(lines.size())))); + } + if (isDaily()) + data.setTime(this, getDay()); + else + data.setTime(this, System.currentTimeMillis()); + if (isChainedGiver()) + data.setItemIndex(this, (data.getItemIndex(this) + 1) % inventory.items.size()); + return true; + } + return false; + } + + private int getDay() { + return (int) (npc.worldObj.getTotalWorldTime() / 24000L); + } + + public boolean canPlayerInteract(PlayerItemGiverData data) { + if (inventory.items.isEmpty()) + return false; + if (isOnTimer()) { + if (!data.hasInteractedBefore(this)) + return true; + return data.getTime(this) + (cooldown * 1000) < System.currentTimeMillis(); + } else if (isGiveOnce()) { + return !data.hasInteractedBefore(this); + } else if (isDaily()) { + if (!data.hasInteractedBefore(this)) + return true; + return getDay() > data.getTime(this); + } + return false; + } + + private boolean givePlayerItems(IPlayer player, + Vector toGive) { + if (toGive.isEmpty()) + return false; + if (freeInventorySlots(player) < toGive.size()) + return false; + for (IItemStack is : toGive) { + npc.givePlayerItem(player, is); + } + return true; + } + + private boolean playerHasItem(IPlayer player, Item item) { + for (IItemStack is : player.inventory.mainInventory) { + if (is != null && is.getItem() == item) + return true; + } + for (IItemStack is : player.inventory.armorInventory) { + if (is != null && is.getItem() == item) + return true; + } + return false; + } + + private int freeInventorySlots(IPlayer player) { + int i = 0; + for (IItemStack is : player.inventory.mainInventory) + if (is == null) + i++; + return i; + } + + private boolean isRandomGiver() { + return givingMethod == 0; + } + + private boolean isAllGiver() { + return givingMethod == 1; + } + + private boolean isRemainingGiver() { + return givingMethod == 2; + } + + private boolean isGiverWhenNotOwnedAny() { + return givingMethod == 3; + } + + private boolean isChainedGiver() { + return givingMethod == 4; + } + + public boolean isOnTimer() { + return cooldownType == 0; + } + + private boolean isGiveOnce() { + return cooldownType == 1; + } + + private boolean isDaily() { + return cooldownType == 2; + } + + public boolean aiShouldExecute() { + if (npc.isAttacking()) + return false; + ticks--; + if (ticks > 0) + return false; + ticks = 10; + + toCheck = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(3, 3, 3)); + toCheck.removeAll(recentlyChecked); + + List listMax = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(10, 10, 10)); + recentlyChecked.retainAll(listMax); + recentlyChecked.addAll(toCheck); + return toCheck.size() > 0; + } + + public void aiStartExecuting() { + for (IPlayer player : toCheck) { + if (npc.canSee(player) && availability.isAvailable(player)) { + recentlyChecked.add(player); + interact(player); + } + } + } + + @Override + public void killed() { + + + } + + private boolean interact(IPlayer player) { + if (!giveItems(player)) + npc.say(player, npc.advanced.getInteractLine()); + return true; + } + + @Override + public void delete() { + + + } +} diff --git a/core/src/main/java/noppes/npcs/roles/JobSpawner.java b/core/src/main/java/noppes/npcs/roles/JobSpawner.java new file mode 100644 index 000000000..c20a87603 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/JobSpawner.java @@ -0,0 +1,487 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.compat.PixelmonHelper; +import org.apache.commons.lang3.RandomStringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class JobSpawner extends JobInterface { + public INbt compound6; + public INbt compound5; + public INbt compound4; + public INbt compound3; + public INbt compound2; + public INbt compound1; + + private int number = 0; + + public List spawned = new ArrayList(); + + private Map cooldown = new HashMap(); + + private String id = RandomStringUtils.random(8, true, true); + + public boolean doesntDie = false; + public boolean despawnOnSummonerDeath = false; + public boolean despawnOnTargetLost = true; + + // 0 - One By One, 1 - All at Once, 2 - Random, 3 - When Summoner Dies + public int spawnType = 0; + + public int xOffset = 0; + public int yOffset = 0; + public int zOffset = 0; + + private IEntityLivingBase target; + + private boolean isResetting = false; + + public JobSpawner(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt compound) { + saveCompound(compound1, "SpawnerNBT1", compound); + saveCompound(compound2, "SpawnerNBT2", compound); + saveCompound(compound3, "SpawnerNBT3", compound); + saveCompound(compound4, "SpawnerNBT4", compound); + saveCompound(compound5, "SpawnerNBT5", compound); + saveCompound(compound6, "SpawnerNBT6", compound); + + compound.setString("SpawnerId", id); + compound.setBoolean("SpawnerDoesntDie", doesntDie); + compound.setInteger("SpawnerType", spawnType); + compound.setInteger("SpawnerXOffset", xOffset); + compound.setInteger("SpawnerYOffset", yOffset); + compound.setInteger("SpawnerZOffset", zOffset); + + compound.setBoolean("DespawnOnTargetLost", despawnOnTargetLost); + compound.setBoolean("DespawnOnSummmoner", despawnOnSummonerDeath); + return compound; + } + + public INbt getTitles() { + INbt compound = new INbt(); + compound.setString("Title1", getTitle(compound1)); + compound.setString("Title2", getTitle(compound2)); + compound.setString("Title3", getTitle(compound3)); + compound.setString("Title4", getTitle(compound4)); + compound.setString("Title5", getTitle(compound5)); + compound.setString("Title6", getTitle(compound6)); + return compound; + } + + private String getTitle(INbt compound) { + if (compound != null && compound.hasKey("ClonedName")) + return compound.getString("ClonedName"); + + return "gui.selectnpc"; + } + + private void saveCompound(INbt save, String name, INbt compound) { + if (save != null) + compound.setTag(name, save); + } + + + @Override + public void readFromNBT(INbt compound) { + compound1 = compound.getCompoundTag("SpawnerNBT1"); + compound2 = compound.getCompoundTag("SpawnerNBT2"); + compound3 = compound.getCompoundTag("SpawnerNBT3"); + compound4 = compound.getCompoundTag("SpawnerNBT4"); + compound5 = compound.getCompoundTag("SpawnerNBT5"); + compound6 = compound.getCompoundTag("SpawnerNBT6"); + + id = compound.getString("SpawnerId"); + doesntDie = compound.getBoolean("SpawnerDoesntDie"); + spawnType = compound.getInteger("SpawnerType"); + xOffset = compound.getInteger("SpawnerXOffset"); + yOffset = compound.getInteger("SpawnerYOffset"); + zOffset = compound.getInteger("SpawnerZOffset"); + + despawnOnTargetLost = compound.getBoolean("DespawnOnTargetLost"); + despawnOnSummonerDeath = compound.getBoolean("DespawnOnSummmoner"); + } + + + public void cleanCompound(INbt compound) { + compound.removeTag("SpawnerNBT1"); + compound.removeTag("SpawnerNBT2"); + compound.removeTag("SpawnerNBT3"); + compound.removeTag("SpawnerNBT4"); + compound.removeTag("SpawnerNBT5"); + compound.removeTag("SpawnerNBT6"); + } + + public void setJobCompound(int i, INbt compound) { + if (i == 1) + compound1 = compound; + if (i == 2) + compound2 = compound; + if (i == 3) + compound3 = compound; + if (i == 4) + compound4 = compound; + if (i == 5) + compound5 = compound; + if (i == 6) + compound6 = compound; + } + + @Override + public void aiUpdateTask() { + if (spawned.isEmpty()) { + if (spawnType == 0) { + if (spawnEntity(number + 1) == null && !doesntDie) + npc.setDead(); + } + if (spawnType == 1) { + if (number >= 6 && !doesntDie) + npc.setDead(); + else { + spawnEntity(compound1); + spawnEntity(compound2); + spawnEntity(compound3); + spawnEntity(compound4); + spawnEntity(compound5); + spawnEntity(compound6); + number = 6; + } + } + if (spawnType == 2) { + ArrayList list = new ArrayList(); + if (compound1 != null && compound1.hasKey("id")) + list.add(compound1); + if (compound2 != null && compound2.hasKey("id")) + list.add(compound2); + if (compound3 != null && compound3.hasKey("id")) + list.add(compound3); + if (compound4 != null && compound4.hasKey("id")) + list.add(compound4); + if (compound5 != null && compound5.hasKey("id")) + list.add(compound5); + if (compound6 != null && compound6.hasKey("id")) + list.add(compound6); + + if (!list.isEmpty()) { + INbt compound = list.get(npc.getRNG().nextInt(list.size())); + spawnEntity(compound); + } else if (!doesntDie) + npc.setDead(); + } + } else { + checkSpawns(); + } + + } + + public void checkSpawns() { + Iterator iterator = spawned.iterator(); + while (iterator.hasNext()) { + IEntityLivingBase spawn = iterator.next(); + if (shouldDelete(spawn)) { + spawn.isDead = true; + iterator.remove(); + } else { + checkTarget(spawn); + } + } + } + + public void checkTarget(IEntityLivingBase IEntity) { + if (IEntity instanceof IEntityLiving) { + IEntityLiving liv = (IEntityLiving) IEntity; + if (liv.getAttackTarget() == null || npc.getRNG().nextInt(100) == 1) + liv.setAttackTarget(target); + } else if (IEntity.getAITarget() == null || npc.getRNG().nextInt(100) == 1) { + IEntity.setRevengeTarget(target); + } + } + + public boolean shouldDelete(IEntityLivingBase IEntity) { + return npc.getDistanceToEntity(IEntity) > 60 || IEntity.isDead || IEntity.getHealth() <= 0 || + PixelmonHelper.Enabled && hasPixelmon() && !PixelmonHelper.isBattling(IEntity) || (despawnOnSummonerDeath && npc.isDead) || despawnOnTargetLost && target == null; + } + + private IEntityLivingBase getTarget() { + IEntityLivingBase target = getTarget(npc); + if (target != null) + return target; + + for (IEntityLivingBase IEntity : spawned) { + target = getTarget(IEntity); + if (target != null) + return target; + } + return null; + } + + private IEntityLivingBase getTarget(IEntityLivingBase IEntity) { + if (IEntity instanceof IEntityLiving) { + target = ((IEntityLiving) IEntity).getAttackTarget(); + if (target != null && !target.isDead && target.getHealth() > 0) + return target; + } + target = IEntity.getAITarget(); + if (target != null && !target.isDead && target.getHealth() > 0) + return target; + return null; + } + + public boolean isEmpty() { + if (compound1 != null && compound1.hasKey("id")) + return false; + if (compound2 != null && compound2.hasKey("id")) + return false; + if (compound3 != null && compound3.hasKey("id")) + return false; + if (compound4 != null && compound4.hasKey("id")) + return false; + if (compound5 != null && compound5.hasKey("id")) + return false; + if (compound6 != null && compound6.hasKey("id")) + return false; + + return true; + } + + private void setTarget(IEntityLivingBase base, IEntityLivingBase target) { + if (PixelmonHelper.isTrainer(base) && target instanceof IPlayer) { + IPlayer player = (IPlayer) target; + if (!PixelmonHelper.canBattle(player, npc)) + return; + cooldown.put(player.getCommandSenderName(), System.currentTimeMillis()); + + Iterator> ita = cooldown.entrySet().iterator(); + + while (ita.hasNext()) { + Entry entry = ita.next(); + if (!isOnCooldown(entry.getKey())) + ita.remove(); + } + } else if (base instanceof IEntityLiving) + ((IEntityLiving) base).setAttackTarget(target); + else + base.setRevengeTarget(target); + } + + @Override + public boolean aiShouldExecute() { + if (isEmpty() || npc.isKilled()) + return false; + + target = getTarget(); + if (npc.getRNG().nextInt(30) == 1) { + if (spawned.isEmpty()) + spawned = getNearbySpawned(); + } + if (!spawned.isEmpty()) + checkSpawns(); + return target != null; + } + + public boolean aiContinueExecute() { + return aiShouldExecute(); + } + + public void resetTask() { + reset(); + } + + public void aiStartExecuting() { + number = 0; + for (IEntityLivingBase IEntity : spawned) { + int i = IEntity.getEntityData().getInteger("NpcSpawnerNr"); + if (i > number) + number = i; + setTarget(IEntity, npc.getAttackTarget()); + } + } + + @Override + public void reset() { + if (isResetting) + return; + isResetting = true; + try { + number = 0; + if (spawned.isEmpty()) + spawned = getNearbySpawned(); + + target = null; + checkSpawns(); + } finally { + isResetting = false; + } + } + + @Override + public void delete() { + if (isResetting) + return; + if (spawnType == 3 && npc.stats.spawnCycle == 3) { + spawnEntity(compound1); + spawnEntity(compound2); + spawnEntity(compound3); + spawnEntity(compound4); + spawnEntity(compound5); + spawnEntity(compound6); + number = 6; + } + reset(); + } + + + @Override + public void killed() { + if (spawnType == 3 && npc.stats.spawnCycle != 3) { + spawnEntity(compound1); + spawnEntity(compound2); + spawnEntity(compound3); + spawnEntity(compound4); + spawnEntity(compound5); + spawnEntity(compound6); + number = 6; + } + reset(); + } + + public IEntityLivingBase spawnEntity(int i) { + INbt compound = getCompound(i); + if (compound == null) { + return null; + } + return spawnEntity(compound); + } + + private IEntityLivingBase spawnEntity(INbt compound) { + if (compound == null || !compound.hasKey("id")) + return null; + double x = this.npc.posX + xOffset - 0.5 + this.npc.getRNG().nextFloat(); + double y = this.npc.posY + yOffset; + double z = this.npc.posZ + zOffset - 0.5 + this.npc.getRNG().nextFloat(); + IEntity IEntity = NoppesUtilServer.getEntityFromNBT(compound, ValueUtil.floorDouble(x), ValueUtil.floorDouble(y), ValueUtil.floorDouble(z), npc.worldObj); + if (IEntity == null) { + return null; + } + IEntity.dimension = npc.worldObj.provider.dimensionId; + if (!IEntity.forceSpawn && !npc.worldObj.checkChunksExist( + (int) IEntity.posX, (int) IEntity.posY, (int) IEntity.posZ, (int) IEntity.posX, (int) IEntity.posY, (int) IEntity.posZ + )) { + return null; + } else { + if (!(IEntity instanceof IEntityLivingBase)) + return null; + + IEntityLivingBase living = (IEntityLivingBase) IEntity; + living.getEntityData().setString("NpcSpawnerId", id); + living.getEntityData().setInteger("NpcSpawnerNr", number); + setTarget(living, this.npc.getAttackTarget()); + living.setPosition(x + 0.5, y + 1 + 0.2F, z + 0.5); + + if (living instanceof EntityNPCInterface) { + EntityNPCInterface snpc = (EntityNPCInterface) living; + snpc.stats.spawnCycle = 3; + snpc.ais.returnToStart = false; + snpc.stats.canDespawn = true; + snpc.stats.playerSetCanDespawn = true; + } + + int i = ValueUtil.floorDouble(living.posX / 16.0D); + int j = ValueUtil.floorDouble(living.posZ / 16.0D); + + npc.worldObj.getChunkFromChunkCoords(i, j).addEntity(living); + npc.worldObj.loadedEntityList.add(living); + npc.worldObj.onEntityAdded(living); + spawned.add(living); + return living; + } + } + + public INbt getCompound(int i) { + if (i <= 1 && compound1 != null && compound1.hasKey("id")) { + number = 1; + return compound1; + } + if (i <= 2 && compound2 != null && compound2.hasKey("id")) { + number = 2; + return compound2; + } + if (i <= 3 && compound3 != null && compound3.hasKey("id")) { + number = 3; + return compound3; + } + if (i <= 4 && compound4 != null && compound4.hasKey("id")) { + number = 4; + return compound4; + } + if (i <= 5 && compound5 != null && compound5.hasKey("id")) { + number = 5; + return compound5; + } + if (i <= 6 && compound6 != null && compound6.hasKey("id")) { + number = 6; + return compound6; + } + return null; + } + + + public List getNearbySpawned() { + List spawnList = new ArrayList(); + List list = npc.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, npc.boundingBox.expand(40, 40, 40)); + for (IEntityLivingBase IEntity : list) { + if (IEntity.getEntityData().getString("NpcSpawnerId").equals(id) && !IEntity.isDead) + spawnList.add(IEntity); + } + return spawnList; + } + + public boolean isOnCooldown(String name) { + if (!cooldown.containsKey(name)) + return false; + + long time = cooldown.get(name); + return System.currentTimeMillis() < time + 1200000; //20 minutes cooldown + } + + + public boolean hasPixelmon() { + return compound1 != null && compound1.getString("id").equals("pixelmontainer"); + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/core/src/main/java/noppes/npcs/roles/RoleAuctioneer.java new file mode 100644 index 000000000..41f33673e --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleAuctioneer.java @@ -0,0 +1,64 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +public class RoleAuctioneer extends RoleInterface { + + public RoleAuctioneer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt compound) { + // Simple role - no custom data needed + return compound; + } + + @Override + public void readFromNBT(INbt compound) { + // Simple role - no custom data needed + } + + @Override + public void interact(IPlayer player) { + if (!ConfigMarket.AuctionEnabled) { + return; + } + + // Open the auction house GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); + } + + @Override + public void delete() { + // No cleanup needed + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleBank.java b/core/src/main/java/noppes/npcs/roles/RoleBank.java new file mode 100644 index 000000000..4252ddbf3 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleBank.java @@ -0,0 +1,66 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.BankController; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.Bank; +import noppes.npcs.controllers.data.BankData; +public class RoleBank extends RoleInterface { + + public int bankId = -1; + + public RoleBank(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setInteger("RoleBankID", bankId); + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + bankId = INbt.getInteger("RoleBankID"); + } + + @Override + public void interact(IPlayer player) { + BankData data = PlayerDataController.Instance.getBankData(player, bankId).getBankOrDefault(bankId); + data.openBankGui(player, npc, bankId, 0); + npc.say(player, npc.advanced.getInteractLine()); + } + + public Bank getBank() { + Bank bank = BankController.getInstance().banks.get(bankId); + if (bank != null) + return bank; + return BankController.getInstance().banks.values().iterator().next(); + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleCompanion.java b/core/src/main/java/noppes/npcs/roles/RoleCompanion.java new file mode 100644 index 000000000..bee0665e0 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleCompanion.java @@ -0,0 +1,696 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import com.google.common.collect.HashMultimap; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.constants.EnumAnimation; +import noppes.npcs.constants.EnumCompanionJobs; +import noppes.npcs.constants.EnumCompanionStage; +import noppes.npcs.constants.EnumCompanionTalent; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.roles.companion.CompanionFarmer; +import noppes.npcs.roles.companion.CompanionFoodStats; +import noppes.npcs.roles.companion.CompanionGuard; +import noppes.npcs.roles.companion.CompanionJobInterface; +import noppes.npcs.roles.companion.CompanionTrader; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.TreeMap; +import java.util.UUID; + +public class RoleCompanion extends RoleInterface { + public NpcMiscInventory inventory; + public String uuid = ""; + public String ownerName = ""; + public Map talents = new TreeMap(); + + public boolean canAge = true; + public long ticksActive = 0; + public EnumCompanionStage stage = EnumCompanionStage.FULLGROWN; + + public IPlayer owner = null; + public int companionID; + + public EnumCompanionJobs job = EnumCompanionJobs.NONE; + public CompanionJobInterface jobInterface = null; + + public boolean hasInv = true; + public boolean defendOwner = true; + + public CompanionFoodStats foodstats = new CompanionFoodStats(); + private int eatingTicks = 20; + private IItemStack eating = null; + private int eatingDelay = 00; + + public int currentExp = 0; + + public RoleCompanion(EntityNPCInterface npc) { + super(npc); + inventory = new NpcMiscInventory(12); + } + + @Override + public boolean aiShouldExecute() { + IPlayer prev = owner; + owner = getOwner(); + if (jobInterface != null && jobInterface.isSelfSufficient()) + return true; + + if (owner == null && !uuid.isEmpty()) { + npc.isDead = true; + } else if (prev != owner && owner != null) { + ownerName = owner.getDisplayName(); + PlayerData data = PlayerData.get(owner); + if (data.companionID != companionID) { + npc.isDead = true; + } + } + return owner != null; + } + + @Override + public void aiUpdateTask() { + if (owner != null && (jobInterface == null || !jobInterface.isSelfSufficient())) + foodstats.onUpdate(npc); + if (foodstats.getFoodLevel() >= 18) { + npc.stats.healthRegen = 0; + npc.stats.combatRegen = 0; + } + if (foodstats.needFood() && isSitting()) { + if (eatingDelay > 0) { + eatingDelay--; + return; + } + + IItemStack prev = eating; + eating = getFood(); + + if (prev != null && eating == null) + npc.setRoleDataWatcher(""); + + if (prev == null && eating != null) { + npc.setRoleDataWatcher("eating"); + eatingTicks = 20; + } + + if (isEating()) { + doEating(); + } + + } else if (eating != null && !isSitting()) { + eating = null; + eatingDelay = 20; + npc.setRoleDataWatcher(""); + } + + ticksActive++; + if (canAge && stage != EnumCompanionStage.FULLGROWN) { + if (stage == EnumCompanionStage.BABY && ticksActive > EnumCompanionStage.CHILD.matureAge) { + matureTo(EnumCompanionStage.CHILD); + } else if (stage == EnumCompanionStage.CHILD && ticksActive > EnumCompanionStage.TEEN.matureAge) { + matureTo(EnumCompanionStage.TEEN); + } else if (stage == EnumCompanionStage.TEEN && ticksActive > EnumCompanionStage.ADULT.matureAge) { + matureTo(EnumCompanionStage.ADULT); + } else if (stage == EnumCompanionStage.ADULT && ticksActive > EnumCompanionStage.FULLGROWN.matureAge) { + matureTo(EnumCompanionStage.FULLGROWN); + } + } + } + + public void clientUpdate() { + if (npc.getRoleDataWatcher().equals("eating")) { + eating = getFood(); + + if (isEating()) { + doEating(); + } + } else if (eating != null) { + eating = null; + } + + } + + private void doEating() { + if (npc.worldObj.isRemote) { + Random rand = npc.getRNG(); + for (int j = 0; j < 2; ++j) { + IVector3 IVector3 = IVector3.createVectorHelper(((double) rand.nextFloat() - 0.5D) * 0.1D, Math.random() * 0.1D + 0.1D, 0.0D); + IVector3.rotateAroundX(-npc.rotationPitch * (float) Math.PI / 180.0F); + IVector3.rotateAroundY(-npc.renderYawOffset * (float) Math.PI / 180.0F); + IVector3 vec31 = IVector3.createVectorHelper(((double) rand.nextFloat() - 0.5D) * 0.3D, (double) (-rand.nextFloat()) * 0.6D - 0.3D, npc.width / 2 + 0.1); + vec31.rotateAroundX(-npc.rotationPitch * (float) Math.PI / 180.0F); + vec31.rotateAroundY(-npc.renderYawOffset * (float) Math.PI / 180.0F); + vec31 = vec31.addVector(npc.posX, npc.posY + (double) npc.height + 0.1, npc.posZ); + String s = "iconcrack_" + Item.getIdFromItem(eating.getItem()); + + if (eating.getHasSubtypes()) { + s = s + "_" + eating.getItemDamage(); + } + + npc.worldObj.spawnParticle(s, vec31.xCoord, vec31.yCoord, vec31.zCoord, IVector3.xCoord, IVector3.yCoord + 0.0D, IVector3.zCoord); + } + } else { + eatingTicks--; + + if (eatingTicks <= 0) { + if (inventory.decrStackSize(eating, 1)) { + ItemFood food = (ItemFood) eating.getItem(); + foodstats.onFoodEaten(food, eating); + npc.playSound("random.burp", 0.5F, npc.getRNG().nextFloat() * 0.1F + 0.9F); + } + eatingDelay = 20; + npc.setRoleDataWatcher(""); + eating = null; + } else if (eatingTicks > 3 && eatingTicks % 2 == 0) { + Random rand = npc.getRNG(); + npc.playSound("random.eat", 0.5F + 0.5F * rand.nextInt(2), (rand.nextFloat() - rand.nextFloat()) * 0.2F + 1.0F); + } + } + } + + public void matureTo(EnumCompanionStage stage) { + this.stage = stage; + EntityCustomNpc npc = (EntityCustomNpc) this.npc; + npc.ais.animationType = stage.animation; + if (stage == EnumCompanionStage.BABY) { + npc.modelData.modelScale.arms.setScale(0.5f, 0.5f, 0.5f); + npc.modelData.modelScale.legs.setScale(0.5f, 0.5f, 0.5f); + npc.modelData.modelScale.body.setScale(0.5f, 0.5f, 0.5f); + npc.modelData.modelScale.head.setScale(0.7f, 0.7f, 0.7f); + + npc.ais.onAttack = 1; + npc.ais.setWalkingSpeed(3); + if (!talents.containsKey(EnumCompanionTalent.INVENTORY)) + talents.put(EnumCompanionTalent.INVENTORY, 0); + } + if (stage == EnumCompanionStage.CHILD) { + npc.modelData.modelScale.arms.setScale(0.6f, 0.6f, 0.6f); + npc.modelData.modelScale.legs.setScale(0.6f, 0.6f, 0.6f); + npc.modelData.modelScale.body.setScale(0.6f, 0.6f, 0.6f); + npc.modelData.modelScale.head.setScale(0.8f, 0.8f, 0.8f); + + npc.ais.onAttack = 0; + npc.ais.setWalkingSpeed(4); + if (!talents.containsKey(EnumCompanionTalent.SWORD)) + talents.put(EnumCompanionTalent.SWORD, 0); + } + if (stage == EnumCompanionStage.TEEN) { + npc.modelData.modelScale.arms.setScale(0.8f, 0.8f, 0.8f); + npc.modelData.modelScale.legs.setScale(0.8f, 0.8f, 0.8f); + npc.modelData.modelScale.body.setScale(0.8f, 0.8f, 0.8f); + npc.modelData.modelScale.head.setScale(0.9f, 0.9f, 0.9f); + + npc.ais.onAttack = 0; + npc.ais.setWalkingSpeed(5); + if (!talents.containsKey(EnumCompanionTalent.ARMOR)) + talents.put(EnumCompanionTalent.ARMOR, 0); + } + if (stage == EnumCompanionStage.ADULT || stage == EnumCompanionStage.FULLGROWN) { + npc.modelData.modelScale.arms.setScale(1f, 1f, 1f); + npc.modelData.modelScale.legs.setScale(1f, 1f, 1f); + npc.modelData.modelScale.body.setScale(1f, 1f, 1f); + npc.modelData.modelScale.head.setScale(1f, 1f, 1f); + + npc.ais.onAttack = 0; + npc.ais.setWalkingSpeed(5); + } + } + + @Override + public INbt writeToNBT(INbt compound) { + compound.setTag("CompanionInventory", inventory.getToNBT()); + compound.setString("CompanionOwner", uuid); + compound.setString("CompanionOwnerName", ownerName); + compound.setInteger("CompanionID", companionID); + + compound.setInteger("CompanionStage", stage.ordinal()); + compound.setInteger("CompanionExp", currentExp); + compound.setBoolean("CompanionCanAge", canAge); + compound.setLong("CompanionAge", ticksActive); + + compound.setBoolean("CompanionHasInv", hasInv); + compound.setBoolean("CompanionDefendOwner", defendOwner); + + foodstats.writeNBT(compound); + + compound.setInteger("CompanionJob", job.ordinal()); + if (jobInterface != null) + compound.setTag("CompanionJobData", jobInterface.getNBT()); + + INbtList list = new INbtList(); + for (EnumCompanionTalent talent : talents.keySet()) { + INbt c = new INbt(); + c.setInteger("Talent", talent.ordinal()); + c.setInteger("Exp", talents.get(talent)); + list.appendTag(c); + } + compound.setTag("CompanionTalents", list); + return compound; + } + + @Override + public void readFromNBT(INbt compound) { + inventory.setFromNBT(compound.getCompoundTag("CompanionInventory")); + uuid = compound.getString("CompanionOwner"); + ownerName = compound.getString("CompanionOwnerName"); + companionID = compound.getInteger("CompanionID"); + + stage = EnumCompanionStage.values()[compound.getInteger("CompanionStage")]; + currentExp = compound.getInteger("CompanionExp"); + canAge = compound.getBoolean("CompanionCanAge"); + ticksActive = compound.getLong("CompanionAge"); + + hasInv = compound.getBoolean("CompanionHasInv"); + defendOwner = compound.getBoolean("CompanionDefendOwner"); + + foodstats.readNBT(compound); + + INbtList list = compound.getTagList("CompanionTalents", 10); + Map talents = new TreeMap(); + for (int i = 0; i < list.tagCount(); i++) { + INbt c = list.getCompoundTagAt(i); + EnumCompanionTalent talent = EnumCompanionTalent.values()[c.getInteger("Talent")]; + talents.put(talent, c.getInteger("Exp")); + } + this.talents = talents; + + setJob(compound.getInteger("CompanionJob")); + if (jobInterface != null) + jobInterface.setNBT(compound.getCompoundTag("CompanionJobData")); + setStats(); + } + + private void setJob(int i) { + job = EnumCompanionJobs.values()[i]; + if (job == EnumCompanionJobs.SHOP) + jobInterface = new CompanionTrader(); + else if (job == EnumCompanionJobs.FARMER) + jobInterface = new CompanionFarmer(); + else if (job == EnumCompanionJobs.GUARD) + jobInterface = new CompanionGuard(); + else + jobInterface = null; + + if (jobInterface != null) + jobInterface.npc = npc; + } + + @Override + public void interact(IPlayer player) { + if (player != null && job == EnumCompanionJobs.SHOP) + ((CompanionTrader) jobInterface).interact(player); + if (player != owner || !npc.isEntityAlive() || npc.isAttacking()) + return; + if (player.isSneaking()) { + openGui(player); + } else { + setSitting(!isSitting()); + } + } + + public int getTotalLevel() { + int level = 0; + for (EnumCompanionTalent talent : talents.keySet()) + level += this.getTalentLevel(talent); + return level; + } + + public int getMaxExp() { + return 500 + getTotalLevel() * 200; + } + + public void addExp(int exp) { + if (canAddExp(exp)) + this.currentExp += exp; + } + + public boolean canAddExp(int exp) { + int newExp = this.currentExp + exp; + return newExp >= 0 && newExp < getMaxExp(); + } + + public void gainExp(int chance) { + if (npc.getRNG().nextInt(chance) == 0) + addExp(1); + } + + private void openGui(IPlayer player) { + NoppesUtilServer.sendOpenGui(player, EnumGuiType.Companion, npc); + } + + public IPlayer getOwner() { + if (uuid == null || uuid.isEmpty()) + return null; + try { + UUID id = UUID.fromString(uuid); + if (id != null) { + return NoppesUtilServer.getPlayer(id); + } + } catch (IllegalArgumentException ex) { + + } + return null; + } + + + public void setOwner(IPlayer player) { + uuid = player.getUniqueID().toString(); + } + + + public boolean hasTalent(EnumCompanionTalent talent) { + return getTalentLevel(talent) > 0; + } + + public int getTalentLevel(EnumCompanionTalent talent) { + if (!talents.containsKey(talent)) + return 0; + + int exp = talents.get(talent); + if (exp >= 5000) + return 5; + if (exp >= 3000) + return 4; + if (exp >= 1700) + return 3; + if (exp >= 1000) + return 2; + if (exp >= 400) + return 1; + return 0; + } + + public Integer getNextLevel(EnumCompanionTalent talent) { + if (!talents.containsKey(talent)) + return 0; + int exp = talents.get(talent); + if (exp < 400) + return 400; + if (exp < 1000) + return 700; + if (exp < 1700) + return 1700; + if (exp < 3000) + return 3000; + return 5000; + } + + public void levelSword() { + if (!talents.containsKey(EnumCompanionTalent.SWORD)) + return; + } + + public void levelTalent(EnumCompanionTalent talent, int exp) { + if (!talents.containsKey(EnumCompanionTalent.SWORD)) + return; + talents.put(talent, exp + talents.get(talent)); + } + + public int getExp(EnumCompanionTalent talent) { + if (talents.containsKey(talent)) + return talents.get(talent); + return -1; + } + + public void setExp(EnumCompanionTalent talent, int exp) { + talents.put(talent, exp); + } + + private boolean isWeapon(IItemStack item) { + if (item == null || item.getItem() == null) + return false; + return item.getItem() instanceof ItemSword || + item.getItem() instanceof ItemBow || + item.getItem() == Item.getItemFromBlock(Blocks.cobblestone); + } + + public boolean canWearWeapon(IItemStack item) { + if (item == null || item.getItem() == null) + return false; + + if (item.getItem() instanceof ItemSword) + return canWearSword(item); + + if (item.getItem() instanceof ItemBow) + return this.getTalentLevel(EnumCompanionTalent.RANGED) > 2; + + if (item.getItem() == Item.getItemFromBlock(Blocks.cobblestone)) + return this.getTalentLevel(EnumCompanionTalent.RANGED) > 1; + + return false; + } + + public boolean canWearArmor(IItemStack item) { + int level = getTalentLevel(EnumCompanionTalent.ARMOR); + if (item == null || !(item.getItem() instanceof ItemArmor) || level <= 0) + return false; + + if (level >= 5) + return true; + + ItemArmor armor = (ItemArmor) item.getItem(); + int reduction = ObfuscationReflectionHelper.getPrivateValue(ArmorMaterial.class, armor.getArmorMaterial(), 5); + if (reduction <= 5 && level >= 1) + return true; + if (reduction <= 7 && level >= 2) + return true; + if (reduction <= 15 && level >= 3) + return true; + if (reduction <= 33 && level >= 4) + return true; + return false; + } + + public boolean canWearSword(IItemStack item) { + int level = getTalentLevel(EnumCompanionTalent.SWORD); + if (item == null || !(item.getItem() instanceof ItemSword) || level <= 0) + return false; + if (level >= 5) + return true; + return getSwordDamage(item) - level < 4; + } + + private double getSwordDamage(IItemStack item) { + if (item == null || !(item.getItem() instanceof ItemSword)) + return 0; + HashMultimap map = (HashMultimap) item.getAttributeModifiers(); + Iterator iterator = map.entries().iterator(); + while (iterator.hasNext()) { + Entry entry = (Entry) iterator.next(); + if (entry.getKey().equals(Attributes.ATTACK_DAMAGE.getAttributeUnlocalizedName())) { + AttributeModifier mod = (AttributeModifier) entry.getValue(); + return mod.getAmount(); + } + } + return 0; + } + + public void setStats() { + IItemStack weapon = npc.inventory.getWeapon(); + npc.stats.setAttackStrength((float) (1 + getSwordDamage(weapon))); + npc.stats.healthRegen = 0; + npc.stats.combatRegen = 0; + int ranged = getTalentLevel(EnumCompanionTalent.RANGED); + if (ranged > 0 && weapon != null) { + if (ranged > 0 && weapon.getItem() == Item.getItemFromBlock(Blocks.cobblestone)) { + npc.inventory.setProjectile(weapon); + } + if (ranged > 0 && weapon.getItem() instanceof ItemBow) { + npc.inventory.setProjectile(new IItemStack(Items.arrow)); + } + } + + inventory.setSize(2 + getTalentLevel(EnumCompanionTalent.INVENTORY) * 2); + } + + public void setSelfsuficient(boolean bo) { + if (owner == null || jobInterface != null && bo == jobInterface.isSelfSufficient()) + return; + PlayerData data = PlayerData.get(owner); + if (!bo && data.hasCompanion()) + return; + data.setCompanion(bo ? null : npc); + if (job == EnumCompanionJobs.GUARD) + ((CompanionGuard) jobInterface).isStanding = bo; + else if (job == EnumCompanionJobs.FARMER) + ((CompanionFarmer) jobInterface).isStanding = bo; + + } + + public void setSitting(boolean sit) { + if (sit) { + npc.ais.animationType = EnumAnimation.SITTING; + npc.ais.onAttack = 3; + npc.ais.startPos = new int[]{ValueUtil.floorDouble(npc.posX), + ValueUtil.floorDouble(npc.posY), ValueUtil.floorDouble(npc.posZ)}; + npc.getNavigator().clearPathEntity(); + npc.setPositionAndUpdate(npc.ais.startPos[0] + 0.5, npc.posY, npc.ais.startPos[2] + 0.5); + } else { + npc.ais.animationType = stage.animation; + npc.ais.onAttack = 0; + } + npc.setResponse(); + } + + public boolean isSitting() { + return npc.ais.animationType == EnumAnimation.SITTING; + } + + public float applyArmorCalculations(IDamageSource source, float damage) { + if (!hasInv || getTalentLevel(EnumCompanionTalent.ARMOR) <= 0) + return damage; + if (!source.isUnblockable()) { + damageArmor(damage); + int i = 25 - getTotalArmorValue(); + float f1 = damage * (float) i; + damage = f1 / 25.0F; + } + return damage; + } + + private void damageArmor(float damage) { + damage /= 4.0F; + + if (damage < 1.0F) { + damage = 1.0F; + } + boolean hasArmor = false; + Iterator> ita = npc.inventory.armor.entrySet().iterator(); + while (ita.hasNext()) { + Entry entry = ita.next(); + IItemStack item = entry.getValue(); + if (item == null || !(item.getItem() instanceof ItemArmor)) + continue; + hasArmor = true; + item.damageItem((int) damage, npc); + if (item.stackSize <= 0) + ita.remove(); + } + this.gainExp(hasArmor ? 4 : 8); + } + + public int getTotalArmorValue() { + int armorValue = 0; + for (IItemStack armor : npc.inventory.getArmor().values()) { + if (armor != null && armor.getItem() instanceof ItemArmor) + armorValue += ((ItemArmor) armor.getItem()).damageReduceAmount; + } + return armorValue; + } + + public boolean isFollowing() { + if (jobInterface != null && jobInterface.isSelfSufficient()) + return false; + return owner != null && !isSitting(); + } + + @Override + public boolean defendOwner() { + if (!defendOwner || owner == null || stage == EnumCompanionStage.BABY || jobInterface != null && jobInterface.isSelfSufficient()) + return false; + return true; + } + + public int followRange() { + return 9 + 12 * stage.ordinal(); + + } + + public boolean hasOwner() { + return !uuid.isEmpty(); + } + + public void addMovementStat(double x, double y, double z) { + int i = Math.round(Math.sqrt(x * x + y * y + z * z) * 100.0F); + if (npc.isAttacking()) + foodstats.addExhaustion(0.04F * (float) i * 0.01F); + else + foodstats.addExhaustion(0.02F * (float) i * 0.01F); + } + + private IItemStack getFood() { + List food = new ArrayList(inventory.items.values()); + Iterator ite = food.iterator(); + int i = -1; + while (ite.hasNext()) { + IItemStack is = ite.next(); + if (is == null || !(is.getItem() instanceof ItemFood)) { + ite.remove(); + continue; + } + int amount = ((ItemFood) is.getItem()).func_150905_g(is); + if (i == -1 || amount < i) + i = amount; + } + for (IItemStack is : food) { + if (((ItemFood) is.getItem()).func_150905_g(is) == i) + return is; + } + return null; + } + + public IItemStack getHeldItem() { + if (eating != null) + return eating; + return npc.inventory.getWeapon(); + } + + public boolean isEating() { + return eating != null; + } + + public boolean hasInv() { + if (!hasInv) + return false; + return hasTalent(EnumCompanionTalent.INVENTORY) || hasTalent(EnumCompanionTalent.ARMOR) || hasTalent(EnumCompanionTalent.SWORD); + } + + public void attackedEntity(IEntity IEntity) { + IItemStack weapon = npc.inventory.getWeapon(); + gainExp(weapon == null ? 8 : 4); + if (weapon == null) + return; + weapon.damageItem(1, npc); + if (weapon.stackSize <= 0) + npc.inventory.setWeapon(null); + } + + public void addTalentExp(EnumCompanionTalent talent, int exp) { + if (talents.containsKey(talent)) + exp += talents.get(talent); + talents.put(talent, exp); + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleFollower.java b/core/src/main/java/noppes/npcs/roles/RoleFollower.java new file mode 100644 index 000000000..5d91a510c --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleFollower.java @@ -0,0 +1,213 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesStringUtils; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.constants.EnumJobType; +import java.util.HashMap; +import java.util.UUID; + +public class RoleFollower extends RoleInterface { + + private String ownerUUID; + public boolean isFollowing = true; + public HashMap rates; + public NpcMiscInventory inventory; + public String dialogHire = PlatformServiceHolder.get().translateToLocal("follower.hireText") + " {days} " + PlatformServiceHolder.get().translateToLocal("follower.days"); + public String dialogFarewell = PlatformServiceHolder.get().translateToLocal("follower.farewellText") + " {player}"; + public int daysHired; + public long hiredTime; + public boolean disableGui = false; + public boolean infiniteDays = false; + public boolean refuseSoulStone = false; + + public IPlayer owner = null; + + public RoleFollower(EntityNPCInterface npc) { + super(npc); + inventory = new NpcMiscInventory(3); + rates = new HashMap(); + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setInteger("MercenaryDaysHired", daysHired); + INbt.setLong("MercenaryHiredTime", hiredTime); + INbt.setString("MercenaryDialogHired", dialogHire); + INbt.setString("MercenaryDialogFarewell", dialogFarewell); + if (hasOwner()) + INbt.setString("MercenaryOwner", ownerUUID); + INbt.setTag("MercenaryDayRates", NBTTags.nbtIntegerIntegerMap(rates)); + INbt.setTag("MercenaryInv", inventory.getToNBT()); + INbt.setBoolean("MercenaryIsFollowing", isFollowing); + INbt.setBoolean("MercenaryDisableGui", disableGui); + INbt.setBoolean("MercenaryInfiniteDays", infiniteDays); + INbt.setBoolean("MercenaryRefuseSoulstone", refuseSoulStone); + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + ownerUUID = INbt.getString("MercenaryOwner"); + daysHired = INbt.getInteger("MercenaryDaysHired"); + hiredTime = INbt.getLong("MercenaryHiredTime"); + dialogHire = INbt.getString("MercenaryDialogHired"); + dialogFarewell = INbt.getString("MercenaryDialogFarewell"); + rates = NBTTags.getIntegerIntegerMap(INbt.getTagList("MercenaryDayRates", 10)); + inventory.setFromNBT(INbt.getCompoundTag("MercenaryInv")); + isFollowing = INbt.getBoolean("MercenaryIsFollowing"); + disableGui = INbt.getBoolean("MercenaryDisableGui"); + infiniteDays = INbt.getBoolean("MercenaryInfiniteDays"); + refuseSoulStone = INbt.getBoolean("MercenaryRefuseSoulstone"); + } + + @Override + public boolean aiShouldExecute() { + owner = getOwner(); + if (!infiniteDays && owner != null && getDaysLeft() <= 0) { + owner.addChatMessage(new ITextComponent(NoppesStringUtils.formatText(dialogFarewell, owner, npc))); + killed(); + } + return false; + } + + public IPlayer getOwner() { + if (ownerUUID == null || ownerUUID.isEmpty()) + return null; + try { + UUID uuid = UUID.fromString(ownerUUID); + if (uuid != null) + return npc.worldObj.func_152378_a(uuid); + } catch (IllegalArgumentException ex) { + + } + + return npc.worldObj.getPlayerEntityByName(ownerUUID); + } + + public boolean hasOwner() { + if (!infiniteDays && daysHired <= 0) + return false; + return ownerUUID != null && !ownerUUID.isEmpty(); + } + + @Override + public void killed() { + ownerUUID = null; + daysHired = 0; + hiredTime = 0; + isFollowing = true; + npc.stats.canDespawn = npc.stats.playerSetCanDespawn; + } + + public int getDaysLeft() { + if (infiniteDays) + return 100; + if (daysHired <= 0) + return 0; + int days = (int) ((npc.worldObj.getTotalWorldTime() - hiredTime) / 24000); + return daysHired - days; + } + + public void addDays(int days) { + daysHired = days + getDaysLeft(); + hiredTime = npc.worldObj.getTotalWorldTime(); + } + + @Override + public void interact(IPlayer player) { + if (ownerUUID == null || ownerUUID.isEmpty()) { + npc.say(player, npc.advanced.getInteractLine()); + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerFollowerHire, npc); + } else if (player == owner && !disableGui) { + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerFollower, npc); + } + } + + @Override + public boolean defendOwner() { + return isFollowing() && npc.advanced.job == EnumJobType.Guard; + } + + @Override + public void delete() { + + } + + public boolean isFollowing() { + return owner != null && isFollowing && getDaysLeft() > 0; + } + + public void setOwner(IPlayer player) { + UUID id = player.getUniqueID(); + if (ownerUUID == null || id == null || !ownerUUID.equals(id)) + killed(); + ownerUUID = id.toString(); + npc.stats.canDespawn = false; + } + + public void setRate(int index, int amount) { + if (index > 2 || index < 0) + return; + + if (amount < 1) + return; + + rates.put(index, amount); + } + + public int getRate(int index) { + if (index > 2 || index < 0) + return -1; + + if (!rates.containsKey(index)) + return -1; + + return rates.get(index); + } + + public void setDialogHire(String dialogHire) { + this.dialogHire = dialogHire; + } + + public String getDialogHire() { + return dialogHire; + } + + public void setDialogFarewell(String dialogFarewell) { + this.dialogFarewell = dialogFarewell; + } + + public String getDialogFarewell() { + return this.dialogFarewell; + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleInnkeeper.java b/core/src/main/java/noppes/npcs/roles/RoleInnkeeper.java new file mode 100644 index 000000000..70a950386 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleInnkeeper.java @@ -0,0 +1,98 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.controllers.data.InnDoorData; +import java.util.HashMap; + +public class RoleInnkeeper extends RoleInterface { + + private String innName = "Inn"; + private HashMap doors = new HashMap(); + + public RoleInnkeeper(EntityNPCInterface npc) { + super(npc); + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setString("InnName", innName); + INbt.setTag("InnDoors", nbtInnDoors(doors)); + return INbt; + } + + private INbtBase nbtInnDoors(HashMap doors1) { + INbtList INbtList = new INbtList(); + if (doors1 == null) + return INbtList; + HashMap doors2 = doors1; + for (String name : doors2.keySet()) { + InnDoorData door = doors2.get(name); + if (door == null) + continue; + INbt INbt = new INbt(); + INbt.setString("Name", name); + INbt.setInteger("posX", door.x); + INbt.setInteger("posY", door.y); + INbt.setInteger("posZ", door.z); + + INbtList.appendTag(INbt); + } + return INbtList; + } + + @Override + public void readFromNBT(INbt INbt) { + innName = INbt.getString("InnName"); + doors = getInnDoors(INbt.getTagList("InnDoors", 10)); + } + + private HashMap getInnDoors(INbtList tagList) { + HashMap list = new HashMap(); + for (int i = 0; i < tagList.tagCount(); i++) { + INbt INbt = tagList.getCompoundTagAt(i); + String name = INbt.getString("Name"); + + InnDoorData door = new InnDoorData(); + door.x = INbt.getInteger("posX"); + door.y = INbt.getInteger("posY"); + door.z = INbt.getInteger("posZ"); + list.put(name, door); + } + return list; + } + + @Override + public void interact(IPlayer player) { + npc.say(player, npc.advanced.getInteractLine()); + if (doors.isEmpty()) { + player.addChatMessage(new ITextComponent("No Rooms available")); + } + } + +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleInterface.java b/core/src/main/java/noppes/npcs/roles/RoleInterface.java new file mode 100644 index 000000000..9db659d6c --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleInterface.java @@ -0,0 +1,77 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import java.util.HashMap; + +public abstract class RoleInterface { + public EntityNPCInterface npc; + public HashMap dataString = new HashMap(); + + public RoleInterface(EntityNPCInterface npc) { + this.npc = npc; + } + + public abstract INbt writeToNBT(INbt compound); + + public abstract void readFromNBT(INbt compound); + + public abstract void interact(IPlayer player); + + public void killed() { + } + + ; + + public void delete() { + } + + ; + + public boolean aiShouldExecute() { + return false; + } + + public boolean aiContinueExecute() { + return false; + } + + public void aiStartExecuting() { + } + + public void aiUpdateTask() { + } + + public boolean defendOwner() { + return false; + } + + public void clientUpdate() { + + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleMount.java b/core/src/main/java/noppes/npcs/roles/RoleMount.java new file mode 100644 index 000000000..c3a14480d --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleMount.java @@ -0,0 +1,230 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.util.ValueUtil; + +public class RoleMount extends RoleInterface { + + private float offsetX; + private float offsetY; + private float offsetZ; + 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); + this.storedReturnToStart = npc.ais.returnToStart; + npc.ais.returnToStart = false; + } + + @Override + public INbt writeToNBT(INbt compound) { + compound.setFloat("MountOffsetX", offsetX); + compound.setFloat("MountOffsetY", offsetY); + compound.setFloat("MountOffsetZ", offsetZ); + 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; + } + + @Override + public void readFromNBT(INbt compound) { + setOffsetX(compound.getFloat("MountOffsetX")); + setOffsetY(compound.getFloat("MountOffsetY")); + setOffsetZ(compound.getFloat("MountOffsetZ")); + if (compound.hasKey("MountReturnFlag")) { + storedReturnToStart = compound.getBoolean("MountReturnFlag"); + } else { + storedReturnToStart = npc.ais.returnToStart; + } + if (compound.hasKey("MountJumpStrength")) { + setJumpStrength(compound.getFloat("MountJumpStrength")); + } else { + 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; + } + + @Override + public void interact(IPlayer player) { + if (player == null || npc.worldObj.isRemote) { + return; + } + if (npc.riddenByEntity != null && npc.riddenByEntity != player) { + return; + } + if (player.ridingEntity != null && player.ridingEntity != npc) { + player.mountEntity(null); + } + player.fallDistance = 0.0F; + npc.fallDistance = 0.0F; + player.mountEntity(npc); + npc.getNavigator().clearPathEntity(); + npc.setAttackTarget(null); + npc.setRevengeTarget(null); + } + + @Override + public boolean aiShouldExecute() { + if (npc.ais.returnToStart) { + storedReturnToStart = true; + npc.ais.returnToStart = false; + } + return false; + } + + public void setOffsetX(float value) { + this.offsetX = clamp(value); + } + + public void setOffsetY(float value) { + this.offsetY = clamp(value); + } + + public void setOffsetZ(float value) { + this.offsetZ = clamp(value); + } + + public float getOffsetX() { + return offsetX; + } + + public float getOffsetY() { + return offsetY; + } + + public float getOffsetZ() { + return offsetZ; + } + + public void setJumpStrength(float value) { + this.jumpStrength = ValueUtil.clamp(value, 0.1F, 3.0F); + } + + public float getJumpStrength() { + return jumpStrength; + } + + public void setSprintAllowed(boolean allowSprint) { + this.allowSprint = allowSprint; + } + + public boolean isSprintAllowed() { + 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; + } + + public boolean getReturnToStartPreference() { + return storedReturnToStart; + } + + private float clamp(float value) { + return ValueUtil.clamp(value, -5.0F, 5.0F); + } + + public void resetOffsets() { + offsetX = 0.0F; + offsetY = 0.0F; + offsetZ = 0.0F; + } + + public void onDisable() { + // Dismount any rider when mount role is disabled + if (npc.riddenByEntity != null) { + npc.riddenByEntity.mountEntity(null); + } + npc.ais.returnToStart = storedReturnToStart; + } + + @Override + public void delete() { + onDisable(); + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RolePostman.java b/core/src/main/java/noppes/npcs/roles/RolePostman.java new file mode 100644 index 000000000..dbaf82405 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RolePostman.java @@ -0,0 +1,82 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.CustomNpcs; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.PlayerDataController; +import java.util.ArrayList; +import java.util.List; + +public class RolePostman extends RoleInterface { + + public NpcMiscInventory inventory = new NpcMiscInventory(1); + private List recentlyChecked = new ArrayList(); + private List toCheck; + + public RolePostman(EntityNPCInterface npc) { + super(npc); + } + + public boolean aiShouldExecute() { + if (npc.ticksExisted % 20 != 0) + return false; + + toCheck = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(10, 10, 10)); + toCheck.removeAll(recentlyChecked); + + List listMax = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(20, 20, 20)); + recentlyChecked.retainAll(listMax); + recentlyChecked.addAll(toCheck); + + for (IPlayer player : toCheck) { + if (PlayerDataController.Instance.hasMail(player)) + player.addChatMessage(new ITextComponent("You've got mail")); + } + return false; + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setTag("PostInv", inventory.getToNBT()); + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + inventory.setFromNBT(INbt.getCompoundTag("PostInv")); + } + + + @Override + public void interact(IPlayer player) { + player.openGui(CustomNpcs.instance, EnumGuiType.PlayerMailman.ordinal(), player.worldObj, 1, 1, 0); + } + +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleTrader.java b/core/src/main/java/noppes/npcs/roles/RoleTrader.java new file mode 100644 index 000000000..0e3f44399 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -0,0 +1,475 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import foxz.utils.Market; +import noppes.npcs.NBTTags; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.NpcMiscInventory; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.MarketRegistry; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.TraderStock; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RoleTrader extends RoleInterface { + + public String marketName = ""; + public NpcMiscInventory inventoryCurrency; + public NpcMiscInventory inventorySold; + + public boolean ignoreDamage = false; + public boolean ignoreNBT = false; + public boolean recordHistory = false; + + public int[] purchases; + public int[] disableSlot; + public HashMap playerPurchases; + public HashMap playerDisableSlot; + + // Stock system + public TraderStock stock = new TraderStock(); + + // Currency cost per trade slot (ADDITIVE to item costs, 0 = no currency required) + public long[] currencyCost = new long[18]; + + // Local viewer tracking for non-linked traders (per-NPC sync) + private final Set localViewers = new HashSet<>(); + + public RoleTrader(EntityNPCInterface npc) { + super(npc); + inventoryCurrency = new NpcMiscInventory(36); + inventorySold = new NpcMiscInventory(18); + purchases = new int[18]; + disableSlot = new int[18]; + playerPurchases = new HashMap(); + playerDisableSlot = new HashMap(); + } + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setString("TraderMarket", marketName); + writeNBT(INbt); + return INbt; + } + + public INbt writeNBT(INbt INbt) { + INbt.setTag("TraderCurrency", inventoryCurrency.getToNBT()); + INbt.setTag("TraderSold", inventorySold.getToNBT()); + INbt.setBoolean("TraderIgnoreDamage", ignoreDamage); + INbt.setBoolean("TraderIgnoreNBT", ignoreNBT); + INbt.setBoolean("RecordHistory", recordHistory); + INbt.setIntArray("DisableSlot", disableSlot); + INbt.setTag("PlayerDisableSlot", NBTTags.nbtStringIntegerArrayMap(playerDisableSlot)); + if (recordHistory) { + INbt.setIntArray("Purchases", purchases); + INbt.setTag("PlayerPurchases", NBTTags.nbtStringIntegerArrayMap(playerPurchases)); + } + + // Stock system + INbt.setTag("Stock", ((NBTWrapper) stock.writeToNBT(new NBTWrapper(new INbt()))).getMCTag()); + + // Currency cost per slot (additive to item costs) + INbtList currencyList = new INbtList(); + for (long cost : currencyCost) { + INbt costTag = new INbt(); + costTag.setLong("Cost", cost); + currencyList.appendTag(costTag); + } + INbt.setTag("CurrencyCost", currencyList); + + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + String oldMarket = marketName; + marketName = INbt.getString("TraderMarket"); + readNBT(INbt); + + // Register with MarketRegistry for linked market sync + MarketRegistry.updateTraderMarket(oldMarket, marketName, this); + + try { + Market.getMarket(this, marketName); + } catch (Exception ex) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); + } + } + + public void readNBT(INbt INbt) { + inventoryCurrency.setFromNBT(INbt.getCompoundTag("TraderCurrency")); + inventorySold.setFromNBT(INbt.getCompoundTag("TraderSold")); + ignoreDamage = INbt.getBoolean("TraderIgnoreDamage"); + ignoreNBT = INbt.getBoolean("TraderIgnoreNBT"); + recordHistory = INbt.getBoolean("RecordHistory"); + disableSlot = INbt.getIntArray("DisableSlot"); + playerDisableSlot = NBTTags.getStringIntegerArrayMap(INbt.getTagList("PlayerDisableSlot", 10), 18); + if (recordHistory) { + purchases = INbt.getIntArray("Purchases"); + playerPurchases = NBTTags.getStringIntegerArrayMap(INbt.getTagList("PlayerPurchases", 10), 18); + } + if (purchases == null || purchases.length != 18) { + purchases = new int[18]; + for (int i = 0; i < purchases.length; ++i) purchases[i] = 0; + } + if (disableSlot == null || disableSlot.length != 18) { + disableSlot = new int[18]; + for (int i = 0; i < disableSlot.length; ++i) disableSlot[i] = 0; + } + + // Stock system + if (INbt.hasKey("Stock")) { + stock.readFromNBT(new NBTWrapper(INbt.getCompoundTag("Stock"))); + } + + // Currency cost per slot (additive to item costs) + if (INbt.hasKey("CurrencyCost")) { + INbtList currencyList = INbt.getTagList("CurrencyCost", 10); + for (int i = 0; i < currencyList.tagCount() && i < 18; i++) { + currencyCost[i] = currencyList.getCompoundTagAt(i).getLong("Cost"); + } + } + } + + @Override + public void interact(IPlayer player) { + npc.say(player, npc.advanced.getInteractLine()); + + // Load from market FIRST to get latest shared data + try { + Market.getMarket(this, marketName); + } catch (Exception ex) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); + } + + // THEN check for stock reset (after loading latest market data) + if (stock.enableStock) { + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + boolean needsSave = false; + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + needsSave = true; + } + + // Validate stock levels (clamp current to max if exceeded) + if (stock.validateStock()) { + needsSave = true; + } + + // Save to market if using shared market and changes were made + if (needsSave && !marketName.isEmpty()) { + Market.save(this, marketName); + } + } + + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); + } + + public boolean hasCurrency(IItemStack IItemStack) { + if (IItemStack == null) + return false; + for (IItemStack item : inventoryCurrency.items.values()) { + if (item != null && NoppesUtilPlayer.compareItems(item, IItemStack, ignoreDamage, ignoreNBT)) { + return true; + } + } + return false; + } + + public void addPurchase(int slot, String playerName) { + if (slot >= 18 || slot < 0) return; + ++purchases[slot]; + ++getArrayByName(playerName, playerPurchases)[slot]; + + if (recordHistory) + Market.save(this, marketName); + } + + public boolean isSlotEnabled(int slot, String playerName) { + if (slot >= 18 || slot < 0) return false; + if (disableSlot[slot] > 0) return false; + return getArrayByName(playerName, playerDisableSlot)[slot] <= 0; + } + + public int[] getArrayByName(String name, HashMap map) { + map.computeIfAbsent(name, k -> new int[18]); + return map.get(name); + } + + // ==================== Stock System Methods ==================== + + /** + * Check if there is stock available for a slot + */ + public boolean hasStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled, always available + } + return stock.hasStock(slot, playerName, amount); + } + + /** + * Get available stock for a slot + */ + public int getAvailableStock(int slot, String playerName) { + if (!stock.enableStock) { + return Integer.MAX_VALUE; // Unlimited + } + return stock.getAvailableStock(slot, playerName); + } + + /** + * Consume stock after purchase. + * For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise). + * + * @param slot The slot purchased from + * @param playerName The player making the purchase + * @param amount Amount to consume + * @return true if stock was consumed successfully + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled + } + boolean consumed = stock.consumeStock(slot, playerName, amount); + if (consumed) { + // Save to market if linked + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + + // Sync viewers for shared stock (not per-player) + if (!stock.perPlayer) { + if (!marketName.isEmpty()) { + // Linked market: sync all traders and viewers market-wide + MarketRegistry.syncMarket(marketName); + } else { + // No linked market: sync only this NPC's viewers + syncLocalViewers(); + } + } + } + return consumed; + } + + /** + * Get time until stock reset formatted for display + */ + public String getTimeUntilResetFormatted() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return ""; + } + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + return stock.getTimeUntilResetFormatted(currentTime); + } + + /** + * Get time until stock reset in real-time milliseconds. + * Converts MC ticks to approximate real-time for client countdown display. + * + * @return Remaining time in milliseconds, or -1 if no reset scheduled + */ + public long getResetTimeRemainingMillis() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return -1; + } + + long currentTime; + long remaining; + + if (stock.resetType.isRealTime()) { + // Real time - already in milliseconds + currentTime = System.currentTimeMillis(); + remaining = stock.getTimeUntilReset(currentTime); + } else { + // MC time - convert ticks to real milliseconds (1 tick = 50ms) + currentTime = (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + long remainingTicks = stock.getTimeUntilReset(currentTime); + remaining = remainingTicks * 50L; + } + + return remaining; + } + + /** + * Reset the stock cooldown timer (for admin use) + */ + public void resetCooldown() { + stock.lastResetTime = 0; + } + + // ==================== Currency Cost Methods ==================== + + /** + * Get the currency cost for a slot (additive to item costs) + */ + public long getCurrencyCost(int slot) { + if (slot < 0 || slot >= 18) return 0; + return currencyCost[slot]; + } + + /** + * Set the currency cost for a slot (additive to item costs) + */ + public void setCurrencyCost(int slot, long cost) { + if (slot >= 0 && slot < 18) { + currencyCost[slot] = Math.max(0, cost); + } + } + + /** + * Check if a slot has a currency cost (greater than 0) + */ + public boolean hasCurrencyCost(int slot) { + return slot >= 0 && slot < 18 && currencyCost[slot] > 0; + } + + // ==================== Viewer Tracking Methods ==================== + + /** + * Register a player as viewing this trader's GUI. + * - For linked markets: uses MarketRegistry (market-wide sync) + * - For normal traders: uses local viewer set (per-NPC sync) + */ + public void registerViewer(IPlayer player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + // Linked market: use MarketRegistry for market-wide tracking + MarketRegistry.registerViewer(marketName, player); + } else { + // No linked market: use local viewer set + localViewers.add(player); + } + } + + /** + * Unregister a player from viewing this trader's GUI. + * - For linked markets: uses MarketRegistry + * - For normal traders: uses local viewer set + */ + public void unregisterViewer(IPlayer player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + MarketRegistry.unregisterViewer(marketName, player); + } else { + localViewers.remove(player); + } + } + + /** + * Sync trader data to all players viewing this trader. + * - For linked markets: syncs ALL viewers across ALL NPCs sharing the market + * - For normal traders: syncs only viewers of THIS specific NPC + */ + public void syncAllViewers() { + if (!marketName.isEmpty()) { + // Linked market: sync all viewers across all NPCs with this market + MarketRegistry.syncMarketViewers(marketName); + } else { + // No linked market: sync only local viewers + syncLocalViewers(); + } + } + + /** + * Sync to all players viewing this specific NPC (for non-linked traders). + */ + private void syncLocalViewers() { + // Remove disconnected players + localViewers.removeIf(player -> player.playerNetServerHandler == null); + + for (IPlayer viewer : localViewers) { + syncToPlayer(viewer); + } + } + + /** + * Called when this trader is unloaded (NPC death, chunk unload, etc.) + * Cleans up MarketRegistry registration. + */ + public void onUnload() { + if (!marketName.isEmpty()) { + MarketRegistry.unregisterTrader(marketName, this); + } + localViewers.clear(); + } + + /** + * Send trader data to a specific player + */ + public void syncToPlayer(IPlayer player) { + if (player == null || player.playerNetServerHandler == null) { + return; + } + + INbt compound = new INbt(); + + // Player balance + PlayerData data = PlayerData.get(player); + compound.setLong("Balance", data.tradeData.getBalance()); + + // Stock info + compound.setBoolean("StockEnabled", stock.enableStock); + compound.setString("ResetTime", getTimeUntilResetFormatted()); + + // Available stock per slot (player-specific if perPlayer mode) + String playerName = player.getCommandSenderName(); + int[] stockArr = new int[18]; + for (int i = 0; i < 18; i++) { + stockArr[i] = getAvailableStock(i, playerName); + } + compound.setIntArray("Stock", stockArr); + + // Currency cost per slot + for (int i = 0; i < 18; i++) { + compound.setLong("Cost" + i, getCurrencyCost(i)); + } + + GuiDataPacket.sendGuiData(player, compound); + } + + @Override + public void delete() { + onUnload(); + super.delete(); + } +} diff --git a/core/src/main/java/noppes/npcs/roles/RoleTransporter.java b/core/src/main/java/noppes/npcs/roles/RoleTransporter.java new file mode 100644 index 000000000..b95bd38b2 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/RoleTransporter.java @@ -0,0 +1,130 @@ +package noppes.npcs.roles; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.api.handler.data.ITransportLocation; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.TransportController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTransportData; +import noppes.npcs.controllers.data.TransportLocation; +import java.util.List; + +public class RoleTransporter extends RoleInterface { + + public int transportId = -1; + public String name; + + public RoleTransporter(EntityNPCInterface npc) { + super(npc); + } + + + @Override + public INbt writeToNBT(INbt INbt) { + INbt.setInteger("TransporterId", transportId); + return INbt; + } + + @Override + public void readFromNBT(INbt INbt) { + transportId = INbt.getInteger("TransporterId"); + TransportLocation loc = getLocation(); + if (loc != null) { + name = loc.name; + } + } + + private int ticks = 10; + + @Override + public boolean aiShouldExecute() { + ticks--; + if (ticks > 0) + return false; + ticks = 10; + + if (!hasTransport()) + return false; + + TransportLocation loc = getLocation(); + if (loc.type != 0) + return false; + + List inRange = npc.worldObj.getEntitiesWithinAABB(IPlayer.class, npc.boundingBox.expand(6D, 6D, 6D)); + for (IPlayer player : inRange) { + if (!npc.canSee(player)) + continue; + unlock(player, loc); + } + return false; + + } + + @Override + public void aiStartExecuting() { + + } + + @Override + public void interact(IPlayer player) { + if (hasTransport()) { + TransportLocation loc = getLocation(); + if (loc.type == 2) { + unlock(player, loc); + } + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTransporter, npc); + } + } + + public void unlock(IPlayer player, ITransportLocation loc) { + PlayerTransportData data = PlayerData.get(player).transportData; + if (data.transports.contains(transportId)) + return; + data.transports.add(transportId); + player.addChatMessage(new ITextComponent("transporter.unlock", loc.getName())); + } + + public TransportLocation getLocation() { + if (npc.isRemote()) + return null; + return TransportController.getInstance().getTransport(transportId); + } + + public boolean hasTransport() { + TransportLocation loc = getLocation(); + return loc != null && loc.id == transportId; + } + + public void setTransport(ITransportLocation location) { + transportId = location.getId(); + name = location.getName(); + } + +} diff --git a/core/src/main/java/noppes/npcs/roles/companion/CompanionFarmer.java b/core/src/main/java/noppes/npcs/roles/companion/CompanionFarmer.java new file mode 100644 index 000000000..caf4bc30f --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/companion/CompanionFarmer.java @@ -0,0 +1,36 @@ +package noppes.npcs.roles.companion; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +public class CompanionFarmer extends CompanionJobInterface { + public boolean isStanding = false; + + @Override + public INbt getNBT() { + INbt compound = new INbt(); + compound.setBoolean("CompanionFarmerStanding", isStanding); + return compound; + } + + @Override + public void setNBT(INbt compound) { + isStanding = compound.getBoolean("CompanionFarmerStanding"); + } + + @Override + public boolean isSelfSufficient() { + return isStanding; + } + + @Override + public void onUpdate() { + + } +} diff --git a/core/src/main/java/noppes/npcs/roles/companion/CompanionFoodStats.java b/core/src/main/java/noppes/npcs/roles/companion/CompanionFoodStats.java new file mode 100644 index 000000000..f14132d8c --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/companion/CompanionFoodStats.java @@ -0,0 +1,165 @@ +package noppes.npcs.roles.companion; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +public class CompanionFoodStats { + /** + * The player's food level. + */ + private int foodLevel = 20; + /** + * The player's food saturation. + */ + private float foodSaturationLevel = 5.0F; + /** + * The player's food exhaustion. + */ + private float foodExhaustionLevel; + /** + * The player's food timer value. + */ + private int foodTimer; + private int prevFoodLevel = 20; + + /** + * Args: int foodLevel, float foodSaturationModifier + */ + private void addStats(int p_75122_1_, float p_75122_2_) { + this.foodLevel = Math.min(p_75122_1_ + this.foodLevel, 20); + this.foodSaturationLevel = Math.min(this.foodSaturationLevel + (float) p_75122_1_ * p_75122_2_ * 2.0F, (float) this.foodLevel); + } + + public void onFoodEaten(ItemFood food, IItemStack IItemStack) { + this.addStats(food.func_150905_g(IItemStack), food.func_150906_h(IItemStack)); + } + + /** + * Handles the food game logic. + */ + public void onUpdate(EntityNPCInterface npc) { + EnumDifficulty enumdifficulty = npc.worldObj.difficultySetting; + this.prevFoodLevel = this.foodLevel; + + if (this.foodExhaustionLevel > 4.0F) { + this.foodExhaustionLevel -= 4.0F; + + if (this.foodSaturationLevel > 0.0F) { + this.foodSaturationLevel = Math.max(this.foodSaturationLevel - 1.0F, 0.0F); + } else if (enumdifficulty != EnumDifficulty.PEACEFUL) { + this.foodLevel = Math.max(this.foodLevel - 1, 0); + } + } + + if (npc.worldObj.getGameRules().getGameRuleBooleanValue("naturalRegeneration") && this.foodLevel >= 18 && npc.getHealth() > 0.0F && npc.getHealth() < npc.getMaxHealth()) { + ++this.foodTimer; + + if (this.foodTimer >= 80) { + npc.heal(1.0F); + this.addExhaustion(3.0F); + this.foodTimer = 0; + } + } else if (this.foodLevel <= 0) { + ++this.foodTimer; + + if (this.foodTimer >= 80) { + if (npc.getHealth() > 10.0F || enumdifficulty == EnumDifficulty.HARD || npc.getHealth() > 1.0F && enumdifficulty == EnumDifficulty.NORMAL) { + npc.attackEntityFrom(IDamageSource.starve, 1.0F); + } + + this.foodTimer = 0; + } + } else { + this.foodTimer = 0; + } + } + + /** + * Reads food stats from an NBT object. + */ + public void readNBT(INbt p_75112_1_) { + if (p_75112_1_.hasKey("foodLevel", 99)) { + this.foodLevel = p_75112_1_.getInteger("foodLevel"); + this.foodTimer = p_75112_1_.getInteger("foodTickTimer"); + this.foodSaturationLevel = p_75112_1_.getFloat("foodSaturationLevel"); + this.foodExhaustionLevel = p_75112_1_.getFloat("foodExhaustionLevel"); + } + } + + /** + * Writes food stats to an NBT object. + */ + public void writeNBT(INbt p_75117_1_) { + p_75117_1_.setInteger("foodLevel", this.foodLevel); + p_75117_1_.setInteger("foodTickTimer", this.foodTimer); + p_75117_1_.setFloat("foodSaturationLevel", this.foodSaturationLevel); + p_75117_1_.setFloat("foodExhaustionLevel", this.foodExhaustionLevel); + } + + /** + * Get the player's food level. + */ + public int getFoodLevel() { + return this.foodLevel; + } + + @ClientOnly + public int getPrevFoodLevel() { + return this.prevFoodLevel; + } + + /** + * If foodLevel is not max. + */ + public boolean needFood() { + return this.foodLevel < 20; + } + + /** + * adds input to foodExhaustionLevel to a max of 40 + */ + public void addExhaustion(float p_75113_1_) { + this.foodExhaustionLevel = Math.min(this.foodExhaustionLevel + p_75113_1_, 40.0F); + } + + /** + * Get the player's food saturation level. + */ + public float getSaturationLevel() { + return this.foodSaturationLevel; + } + + @ClientOnly + public void setFoodLevel(int p_75114_1_) { + this.foodLevel = p_75114_1_; + } + + @ClientOnly + public void setFoodSaturationLevel(float p_75119_1_) { + this.foodSaturationLevel = p_75119_1_; + } +} diff --git a/core/src/main/java/noppes/npcs/roles/companion/CompanionGuard.java b/core/src/main/java/noppes/npcs/roles/companion/CompanionGuard.java new file mode 100644 index 000000000..0a39e1867 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/companion/CompanionGuard.java @@ -0,0 +1,61 @@ +package noppes.npcs.roles.companion; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +public class CompanionGuard extends CompanionJobInterface { + public boolean isStanding = false; + + @Override + public INbt getNBT() { + INbt compound = new INbt(); + compound.setBoolean("CompanionGuardStanding", isStanding); + return compound; + } + + @Override + public void setNBT(INbt compound) { + isStanding = compound.getBoolean("CompanionGuardStanding"); + } + + public boolean isEntityApplicable(IEntity IEntity) { + + if (IEntity instanceof IPlayer || IEntity instanceof EntityNPCInterface) + return false; + + else if (IEntity instanceof EntityCreeper) { + return false; + } else if (IEntity instanceof IMob) { + return true; + } + return false; + } + + public boolean isSelfSufficient() { + return isStanding; + } +} diff --git a/core/src/main/java/noppes/npcs/roles/companion/CompanionJobInterface.java b/core/src/main/java/noppes/npcs/roles/companion/CompanionJobInterface.java new file mode 100644 index 000000000..dea6687aa --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/companion/CompanionJobInterface.java @@ -0,0 +1,43 @@ +package noppes.npcs.roles.companion; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +public abstract class CompanionJobInterface { + public EntityNPCInterface npc; + + public abstract INbt getNBT(); + + public abstract void setNBT(INbt compound); + + public void onUpdate() { + } + + public boolean isSelfSufficient() { + return false; + } +} diff --git a/core/src/main/java/noppes/npcs/roles/companion/CompanionTrader.java b/core/src/main/java/noppes/npcs/roles/companion/CompanionTrader.java new file mode 100644 index 000000000..1b7dfc2c0 --- /dev/null +++ b/core/src/main/java/noppes/npcs/roles/companion/CompanionTrader.java @@ -0,0 +1,31 @@ +package noppes.npcs.roles.companion; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.constants.EnumGuiType; + +public class CompanionTrader extends CompanionJobInterface { + + @Override + public INbt getNBT() { + INbt compound = new INbt(); + return compound; + } + + @Override + public void setNBT(INbt compound) { + + } + + public void interact(IPlayer player) { + NoppesUtilServer.sendOpenGui(player, EnumGuiType.CompanionTrader, npc); + } +} diff --git a/core/src/main/java/noppes/npcs/scripted/constants/EntityType.java b/core/src/main/java/noppes/npcs/scripted/constants/EntityType.java index e92babb24..23609eb58 100644 --- a/core/src/main/java/noppes/npcs/scripted/constants/EntityType.java +++ b/core/src/main/java/noppes/npcs/scripted/constants/EntityType.java @@ -19,9 +19,5 @@ public class EntityType { public static final int ENERGY_BEAM = 15; public static final int ENERGY_DISC = 16; public static final int ENERGY_LASER = 17; - public static final int ENERGY_SLICER = 18; - public static final int ENERGY_BARRIER = 19; - public static final int ENERGY_DOME = 20; - public static final int ENERGY_PANEL = 21; - public static final int ENERGY_EXPLOSION = 22; + public static final int ENERGY_BARRIER = 18; } diff --git a/core/src/main/java/noppes/npcs/util/GameProfileAlt.java b/core/src/main/java/noppes/npcs/util/GameProfileAlt.java new file mode 100644 index 000000000..435054c29 --- /dev/null +++ b/core/src/main/java/noppes/npcs/util/GameProfileAlt.java @@ -0,0 +1,44 @@ +package noppes.npcs.util; + +import noppes.npcs.constants.ClientOnly; +import kamkeel.npcs.util.IVector3; +import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler; +import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; +import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.entity.EntityEnergyDome; +import kamkeel.npcs.entity.EntityEnergyBarrier; +import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.util.ByteBufUtils; +import java.util.UUID; + +public class GameProfileAlt extends GameProfile { + private static final UUID id = UUID.randomUUID(); + public EntityNPCInterface npc; + + public GameProfileAlt() { + super(null, "customnpc"); + } + + @Override + public String getName() { + if (npc == null) + return super.getName(); + return npc.getCommandSenderName(); + } + + @Override + public UUID getId() { + if (npc == null) + return id; + return npc.getPersistentID(); + } +} diff --git a/core/src/main/java/noppes/npcs/util/JsonException.java b/core/src/main/java/noppes/npcs/util/JsonException.java new file mode 100644 index 000000000..f9d6583ca --- /dev/null +++ b/core/src/main/java/noppes/npcs/util/JsonException.java @@ -0,0 +1,9 @@ +package noppes.npcs.util; + +import noppes.npcs.util.NBTJsonUtil.JsonFile; + +public class JsonException extends Exception { + public JsonException(String message, JsonFile json) { + super(message + ": " + json.getCurrentPos()); + } +} diff --git a/core/src/main/java/noppes/npcs/util/NBTJsonUtil.java b/core/src/main/java/noppes/npcs/util/NBTJsonUtil.java new file mode 100644 index 000000000..0eb292d68 --- /dev/null +++ b/core/src/main/java/noppes/npcs/util/NBTJsonUtil.java @@ -0,0 +1,391 @@ +package noppes.npcs.util; + +import noppes.npcs.api.entity.IEntityLiving; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.INbtList; +import noppes.npcs.api.INbt; +import com.google.common.io.Files; +import noppes.npcs.LogWriter; +import org.apache.commons.io.Charsets; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; + +public class NBTJsonUtil { + public static String Convert(INbt compound) { + List list = new ArrayList(); + JsonLine line = ReadTag("", compound, list); + line.removeComma(); + return ConvertList(list); + } + + public static INbt Convert(String json) throws JsonException { + json = json.trim(); + JsonFile file = new JsonFile(json); + if (!json.startsWith("{") || !json.endsWith("}")) + throw new JsonException("Not properly incapsulated between { }", file); + + INbt compound = new INbt(); + FillCompound(compound, file); + return compound; + } + + public static void FillCompound(INbt compound, JsonFile json) throws JsonException { + if (json.startsWith("{") || json.startsWith(",")) + json.cut(1); + if (json.startsWith("}")) + return; + int index = json.indexOf(":"); + if (index < 1) + throw new JsonException("Expected key after ,", json); + + String key = json.substring(0, index); + json.cut(index + 1); + + INbtBase base = ReadValue(json); + + if (base == null) + base = new NBTTagString(); + + if (key.startsWith("\"")) + key = key.substring(1); + if (key.endsWith("\"")) + key = key.substring(0, key.length() - 1); + + compound.setTag(key, base); + if (json.startsWith(",")) + FillCompound(compound, json); + } + + public static INbtBase ReadValue(JsonFile json) throws JsonException { + if (json.startsWith("{")) { + INbt compound = new INbt(); + FillCompound(compound, json); + if (!json.startsWith("}")) { + throw new JsonException("Expected }", json); + } + json.cut(1); + return compound; + } + if (json.startsWith("[")) { + json.cut(1); + INbtList list = new INbtList(); + INbtBase value = ReadValue(json); + while (value != null) { + list.appendTag(value); + if (!json.startsWith(",")) + break; + json.cut(1); + value = ReadValue(json); + } + if (!json.startsWith("]")) { + throw new JsonException("Expected ]", json); + } + json.cut(1); + if (list.func_150303_d() == 3) { + int[] arr = new int[list.tagCount()]; + for (int i = 0; list.tagCount() > 0; i++) { + arr[i] = ((NBTTagInt) list.removeTag(0)).func_150287_d(); + } + return new NBTTagIntArray(arr); + } + if (list.func_150303_d() == 1) { + byte[] arr = new byte[list.tagCount()]; + for (int i = 0; list.tagCount() > 0; i++) { + arr[i] = ((NBTTagByte) list.removeTag(0)).func_150290_f(); + } + return new NBTTagByteArray(arr); + } + return list; + } + if (json.startsWith("\"")) { + json.cut(1); + String s = ""; + boolean ignore = false; + while (!json.startsWith("\"") || ignore) { + String cut = json.cutDirty(1); + ignore = cut.equals("\\"); + s += cut; + } + json.cut(1); + return new NBTTagString(s.replace("\\\"", "\"")); + } + String s = ""; + while (!json.startsWith(",", "]", "}")) { + s += json.cut(1); + } + s = s.trim().toLowerCase(); + if (s.isEmpty() || s.contains("bytes]")) + return null; + try { + if (s.endsWith("d")) { + return new NBTTagDouble(Double.parseDouble(s.substring(0, s.length() - 1))); + } + if (s.endsWith("f")) { + return new NBTTagFloat(Float.parseFloat(s.substring(0, s.length() - 1))); + } + if (s.endsWith("b")) { + return new NBTTagByte(Byte.parseByte(s.substring(0, s.length() - 1))); + } + if (s.endsWith("s")) { + return new NBTTagShort(Short.parseShort(s.substring(0, s.length() - 1))); + } + if (s.endsWith("l")) { + return new NBTTagLong(Long.parseLong(s.substring(0, s.length() - 1))); + } + if (s.contains(".")) + return new NBTTagDouble(Double.parseDouble(s)); + else + return new NBTTagInt(Integer.parseInt(s)); + } catch (NumberFormatException ex) { + throw new JsonException("Unable to convert: " + s + " to a number", json); + } + } + + private static List getListData(INbtList list) { + return ObfuscationReflectionHelper.getPrivateValue(INbtList.class, list, 0); + } + + private static JsonLine ReadTag(String name, INbtBase base, List list) { + if (!name.isEmpty()) + name = "\"" + name + "\": "; + if (base.getId() == 8) { // NBTTagString + String data = ((NBTTagString) base).func_150285_a_(); + data = data.replace("\"", "\\\""); // Escape quotes + list.add(new JsonLine(name + "\"" + data + "\"")); + } else if (base.getId() == 7) { // NBTTagByteArray + byte[] arr = ((NBTTagByteArray) base).func_150292_c(); + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0; i < arr.length; i++) { + sb.append(arr[i]).append("b"); + if (i < arr.length - 1) { + sb.append(","); + } + } + sb.append("]"); + list.add(new JsonLine(name + sb.toString())); + JsonLine line = list.get(list.size() - 1); + line.line += ","; + return line; + } else if (base.getId() == 9) { // INbtList + list.add(new JsonLine(name + "[")); + INbtList tags = (INbtList) base; + JsonLine line = null; + List data = getListData(tags); + for (INbtBase b : data) + line = ReadTag("", b, list); + if (line != null) + line.removeComma(); + list.add(new JsonLine("]")); + } else if (base.getId() == 10) { // INbt + list.add(new JsonLine(name + "{")); + INbt compound = (INbt) base; + JsonLine line = null; + for (Object key : compound.func_150296_c()) + line = ReadTag(key.toString(), compound.getTag(key.toString()), list); + if (line != null) + line.removeComma(); + list.add(new JsonLine("}")); + } else if (base.getId() == 11) { // NBTTagIntArray + list.add(new JsonLine(name + base.toString().replaceFirst(",]", "]"))); + } else { + list.add(new JsonLine(name + base)); + } + JsonLine line = list.get(list.size() - 1); + line.line += ","; + return line; + } + + private static String ConvertList(List list) { + String json = ""; + int tab = 0; + for (JsonLine tag : list) { + if (tag.reduceTab()) + tab--; + for (int i = 0; i < tab; i++) { + json += " "; + } + json += tag + "\n"; + if (tag.increaseTab()) + tab++; + } + return json; + } + + public static INbt loadNBTData(File file) { + try (FileInputStream fis = new FileInputStream(file)) { + return NBTIO.readCompressed(fis); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getName(), e); + } + return new INbt(); + } + + static class JsonLine { + private String line; + + public JsonLine(String line) { + this.line = line; + } + + public void removeComma() { + if (line.endsWith(",")) + line = line.substring(0, line.length() - 1); + } + + public boolean reduceTab() { + int length = line.length(); + return length == 1 && (line.endsWith("}") || line.endsWith("]")) + || length == 2 && (line.endsWith("},") || line.endsWith("],")); + } + + public boolean increaseTab() { + return line.endsWith("{") || line.endsWith("["); + } + + @Override + public String toString() { + return line; + } + } + + static class JsonFile { + private String original; + private String text; + + public JsonFile(String text) { + this.text = text; + this.original = text; + } + + public String cutDirty(int i) { + String s = text.substring(0, i); + text = text.substring(i); + return s; + } + + public String cut(int i) { + String s = text.substring(0, i); + text = text.substring(i).trim(); + return s; + } + + public String substring(int beginIndex, int endIndex) { + return text.substring(beginIndex, endIndex); + } + + public int indexOf(String s) { + return text.indexOf(s); + } + + public String getCurrentPos() { + int lengthOr = original.length(); + int lengthCur = text.length(); + int currentPos = lengthOr - lengthCur; + String done = original.substring(0, currentPos); + String[] lines = done.split("\r\n|\r|\n"); + int pos = 0; + String line = ""; + if (lines.length > 0) { + pos = lines[lines.length - 1].length(); + line = original.split("\r\n|\r|\n")[lines.length - 1].trim(); + } + return "Line: " + lines.length + ", Pos: " + pos + ", Text: " + line; + } + + public boolean startsWith(String... ss) { + for (String s : ss) + if (text.startsWith(s)) + return true; + return false; + } + + public boolean endsWith(String s) { + return text.endsWith(s); + } + } + + public static INbt LoadFile(File file) throws IOException, JsonException { + return Convert(Files.toString(file, Charsets.UTF_8)); + } + + public static void SaveFile(File file, INbt compound) throws IOException, JsonException { + String json = Convert(compound); + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(new FileOutputStream(file), Charsets.UTF_8); + writer.write(json); + } finally { + if (writer != null) + writer.close(); + } + } + + public static void main(String[] args) { + try { + // Create a test compound with various NBT types + INbt comp = new INbt(); + INbt comp2 = new INbt(); + + // ByteArray and IntArray + comp2.setByteArray("test", new byte[]{0, 0, 1, 1, 0}); + comp2.setIntArray("intArray", new int[]{0, 0, 1, 1, 0}); + + // Primitive types + comp2.setByte("byte", (byte) 7); + comp2.setShort("short", (short) 123); + comp2.setInteger("int", 456); + comp2.setLong("long", 789L); + comp2.setFloat("float", 3.14F); + comp2.setDouble("double", 2.71828); + comp2.setString("string", "Testing"); + + // A list containing strings + INbtList list = new INbtList(); + list.appendTag(new NBTTagString("jim")); + list.appendTag(new NBTTagString("foo")); + comp2.setTag("list", list); + + // A nested compound + INbt innerComp = new INbt(); + innerComp.setString("innerString", "innerValue"); + innerComp.setInteger("innerInt", 10); + innerComp.setByte("innerByte", (byte) 11); + comp2.setTag("innerComp", innerComp); + + // Add our compound to the root compound. + comp.setTag("comp", comp2); + + // Convert NBT to JSON string (writing) + String json = Convert(comp); + System.out.println("Generated JSON:"); + System.out.println(json); + + // Read back from JSON string (reading) + INbt compFromJson = Convert(json); + String jsonAfterRead = Convert(compFromJson); + System.out.println("\nRe-converted JSON from read NBT:"); + System.out.println(jsonAfterRead); + + // Basic check: compare the JSON outputs to verify consistency. + if (json.equals(jsonAfterRead)) { + System.out.println("\nSUCCESS: Read and write operations are consistent for all types."); + } else { + System.out.println("\nWARNING: There is a mismatch in read and write results for some types."); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/core/src/main/java/noppes/npcs/util/ScriptToStringHelper.java b/core/src/main/java/noppes/npcs/util/ScriptToStringHelper.java new file mode 100644 index 000000000..d34e5c1cd --- /dev/null +++ b/core/src/main/java/noppes/npcs/util/ScriptToStringHelper.java @@ -0,0 +1,210 @@ +package noppes.npcs.util; + +import com.google.common.base.Preconditions; + +import javax.annotation.CheckForNull; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +/** + * Utility class for building toString representations of script handlers. + * Used by GlobalNPCDataScript and PlayerDataScript for notice strings. + */ +public final class ScriptToStringHelper { + private final String className; + private final ValueHolder holderHead; + private ValueHolder holderTail; + private boolean omitNullValues; + private boolean omitEmptyValues; + + private ScriptToStringHelper(String className) { + this.holderHead = new ValueHolder(); + this.holderTail = this.holderHead; + this.omitNullValues = false; + this.omitEmptyValues = false; + this.className = Preconditions.checkNotNull(className); + } + + public static ScriptToStringHelper toStringHelper(Object self) { + return new ScriptToStringHelper(self.getClass().getSimpleName()); + } + + public ScriptToStringHelper omitNullValues() { + this.omitNullValues = true; + return this; + } + + public ScriptToStringHelper add(String name, @CheckForNull Object value) { + return this.addHolder(name, value); + } + + public ScriptToStringHelper add(String name, boolean value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, char value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, double value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, float value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, int value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, long value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper addValue(@CheckForNull Object value) { + return this.addHolder(value); + } + + public ScriptToStringHelper addValue(boolean value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(char value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(double value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(float value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(int value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(long value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + private static boolean isEmpty(Object value) { + if (value instanceof CharSequence) { + return ((CharSequence) value).length() == 0; + } else if (value instanceof Collection) { + return ((Collection) value).isEmpty(); + } else if (value instanceof Map) { + return ((Map) value).isEmpty(); + } else if (value instanceof Optional) { + return !((Optional) value).isPresent(); + } else if (value instanceof OptionalInt) { + return !((OptionalInt) value).isPresent(); + } else if (value instanceof OptionalLong) { + return !((OptionalLong) value).isPresent(); + } else if (value instanceof OptionalDouble) { + return !((OptionalDouble) value).isPresent(); + } else if (value instanceof com.google.common.base.Optional) { + return !((com.google.common.base.Optional) value).isPresent(); + } else if (value.getClass().isArray()) { + return Array.getLength(value) == 0; + } else { + return false; + } + } + + public String toString() { + boolean omitNullValuesSnapshot = this.omitNullValues; + boolean omitEmptyValuesSnapshot = this.omitEmptyValues; + String nextSeparator = ""; + StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); + for (ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { + Object value = valueHolder.value; + if (!(valueHolder instanceof UnconditionalValueHolder)) { + if (value == null) { + if (omitNullValuesSnapshot) { + continue; + } + } else if (omitEmptyValuesSnapshot && isEmpty(value)) { + continue; + } + } + builder.append(nextSeparator); + nextSeparator = ", "; + if (valueHolder.name != null) { + builder.append(valueHolder.name).append('='); + } + if (value != null && value.getClass().isArray()) { + Object[] objectArray = new Object[]{value}; + String arrayString = Arrays.deepToString(objectArray); + builder.append(arrayString, 1, arrayString.length() - 1); + } else { + builder.append(value); + } + } + return builder.append('}').toString(); + } + + private ValueHolder addHolder() { + ValueHolder valueHolder = new ValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addHolder(@CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addHolder(String name, @CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private UnconditionalValueHolder addUnconditionalHolder() { + UnconditionalValueHolder valueHolder = new UnconditionalValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addUnconditionalHolder(Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addUnconditionalHolder(String name, Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private static final class UnconditionalValueHolder extends ValueHolder { + private UnconditionalValueHolder() { + super(); + } + } + + private static class ValueHolder { + @CheckForNull + String name; + @CheckForNull + Object value; + @CheckForNull + ValueHolder next; + + private ValueHolder() { + } + } +} diff --git a/platform-api/src/main/java/common/minecraft/command/ICommandSender.java b/platform-api/src/main/java/common/minecraft/command/ICommandSender.java new file mode 100644 index 000000000..0c51ceac7 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/command/ICommandSender.java @@ -0,0 +1,16 @@ +package common.minecraft.command; + +import common.minecraft.util.ITextComponent; +import noppes.npcs.api.IWorld; + +/** + * Platform abstraction for Minecraft's command sender. + * MC 1.7.10: net.minecraft.command.ICommandSender + * MC 1.12+: ICommandSender / CommandSource (1.13+) + */ +public interface ICommandSender { + String getCommandSenderName(); + IWorld getEntityWorld(); + void addChatMessage(ITextComponent component); + boolean canCommandSenderUseCommand(int permLevel, String commandName); +} diff --git a/platform-api/src/main/java/common/minecraft/nbt/INbtBase.java b/platform-api/src/main/java/common/minecraft/nbt/INbtBase.java new file mode 100644 index 000000000..a606f00c5 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/nbt/INbtBase.java @@ -0,0 +1,12 @@ +package common.minecraft.nbt; + +/** + * Platform abstraction for Minecraft's NBT base tag type. + * MC 1.7.10: NBTBase + * MC 1.12+: NBTBase / INBT (1.16+) + */ +public interface INbtBase { + byte getId(); + INbtBase copy(); + String toString(); +} diff --git a/platform-api/src/main/java/common/minecraft/potion/IPotionEffect.java b/platform-api/src/main/java/common/minecraft/potion/IPotionEffect.java new file mode 100644 index 000000000..afa76db33 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/potion/IPotionEffect.java @@ -0,0 +1,13 @@ +package common.minecraft.potion; + +/** + * Platform abstraction for Minecraft's active potion effect instance. + * MC 1.7.10: PotionEffect + * MC 1.12+: PotionEffect / EffectInstance (1.14+) + */ +public interface IPotionEffect { + int getPotionID(); + int getDuration(); + int getAmplifier(); + boolean getIsAmbient(); +} diff --git a/platform-api/src/main/java/common/minecraft/potion/IPotionType.java b/platform-api/src/main/java/common/minecraft/potion/IPotionType.java new file mode 100644 index 000000000..75cd98fc9 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/potion/IPotionType.java @@ -0,0 +1,12 @@ +package common.minecraft.potion; + +/** + * Platform abstraction for Minecraft's potion type (the potion definition, not the effect instance). + * MC 1.7.10: Potion + * MC 1.12+: Potion (registry-based) + */ +public interface IPotionType { + int getId(); + String getName(); + boolean isBadEffect(); +} diff --git a/platform-api/src/main/java/common/minecraft/util/IBoundingBox.java b/platform-api/src/main/java/common/minecraft/util/IBoundingBox.java new file mode 100644 index 000000000..901d40cc2 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/util/IBoundingBox.java @@ -0,0 +1,18 @@ +package common.minecraft.util; + +/** + * Platform abstraction for Minecraft's axis-aligned bounding box. + * MC 1.7.10: AxisAlignedBB + * MC 1.12+: AxisAlignedBB + */ +public interface IBoundingBox { + double getMinX(); + double getMinY(); + double getMinZ(); + double getMaxX(); + double getMaxY(); + double getMaxZ(); + IBoundingBox expand(double x, double y, double z); + IBoundingBox offset(double x, double y, double z); + boolean isVecInside(double x, double y, double z); +} diff --git a/platform-api/src/main/java/common/minecraft/util/IRayTraceResult.java b/platform-api/src/main/java/common/minecraft/util/IRayTraceResult.java new file mode 100644 index 000000000..a5327eb96 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/util/IRayTraceResult.java @@ -0,0 +1,19 @@ +package common.minecraft.util; + +import noppes.npcs.api.entity.IEntity; + +/** + * Platform abstraction for Minecraft's ray trace / moving object position. + * MC 1.7.10: MovingObjectPosition + * MC 1.12+: RayTraceResult + */ +public interface IRayTraceResult { + /** 0=MISS, 1=BLOCK, 2=ENTITY */ + int getTypeOfHit(); + int getBlockX(); + int getBlockY(); + int getBlockZ(); + int getSideHit(); + IEntity getEntityHit(); + IVector3 getHitVec(); +} diff --git a/platform-api/src/main/java/common/minecraft/util/IResourceLocation.java b/platform-api/src/main/java/common/minecraft/util/IResourceLocation.java new file mode 100644 index 000000000..30b111121 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/util/IResourceLocation.java @@ -0,0 +1,12 @@ +package common.minecraft.util; + +/** + * Platform abstraction for Minecraft's ResourceLocation. + * MC 1.7.10: net.minecraft.util.ResourceLocation + * MC 1.12+: net.minecraft.util.ResourceLocation + */ +public interface IResourceLocation { + String getResourceDomain(); + String getResourcePath(); + String toString(); +} diff --git a/platform-api/src/main/java/common/minecraft/util/ITextComponent.java b/platform-api/src/main/java/common/minecraft/util/ITextComponent.java new file mode 100644 index 000000000..8744576dd --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/util/ITextComponent.java @@ -0,0 +1,13 @@ +package common.minecraft.util; + +/** + * Platform abstraction for Minecraft's chat component hierarchy. + * MC 1.7.10: ChatComponentText, ChatComponentTranslation, IChatComponent + * MC 1.12+: ITextComponent, TextComponentString, TextComponentTranslation + */ +public interface ITextComponent { + String getUnformattedText(); + String getFormattedText(); + ITextComponent appendText(String text); + ITextComponent appendSibling(ITextComponent sibling); +} diff --git a/platform-api/src/main/java/common/minecraft/util/ITextStyle.java b/platform-api/src/main/java/common/minecraft/util/ITextStyle.java new file mode 100644 index 000000000..8a2b2aaa7 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/util/ITextStyle.java @@ -0,0 +1,15 @@ +package common.minecraft.util; + +/** + * Platform abstraction for Minecraft's chat formatting style. + * MC 1.7.10: ChatStyle + * MC 1.12+: Style + */ +public interface ITextStyle { + boolean getBold(); + boolean getItalic(); + boolean getUnderlined(); + boolean getStrikethrough(); + boolean getObfuscated(); + String getColor(); +} diff --git a/platform-api/src/main/java/common/minecraft/util/IVector3.java b/platform-api/src/main/java/common/minecraft/util/IVector3.java new file mode 100644 index 000000000..00f8ddb27 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/util/IVector3.java @@ -0,0 +1,19 @@ +package common.minecraft.util; + +/** + * Platform abstraction for Minecraft's 3D vector. + * MC 1.7.10: Vec3 + * MC 1.12+: Vec3d + */ +public interface IVector3 { + double getX(); + double getY(); + double getZ(); + IVector3 normalize(); + double lengthVector(); + double distanceTo(IVector3 other); + IVector3 addVector(double x, double y, double z); + IVector3 subtract(IVector3 other); + double dotProduct(IVector3 other); + IVector3 crossProduct(IVector3 other); +} diff --git a/platform-api/src/main/java/common/minecraft/world/IExplosion.java b/platform-api/src/main/java/common/minecraft/world/IExplosion.java new file mode 100644 index 000000000..d563220e1 --- /dev/null +++ b/platform-api/src/main/java/common/minecraft/world/IExplosion.java @@ -0,0 +1,13 @@ +package common.minecraft.world; + +/** + * Platform abstraction for Minecraft's explosion. + * MC 1.7.10: Explosion + * MC 1.12+: Explosion + */ +public interface IExplosion { + double getX(); + double getY(); + double getZ(); + float getSize(); +} diff --git a/platform-api/src/main/java/common/minecraftforge/common/config/IConfigProperty.java b/platform-api/src/main/java/common/minecraftforge/common/config/IConfigProperty.java new file mode 100644 index 000000000..0c11afc05 --- /dev/null +++ b/platform-api/src/main/java/common/minecraftforge/common/config/IConfigProperty.java @@ -0,0 +1,21 @@ +package common.minecraftforge.common.config; + +/** + * Platform abstraction for Forge's Configuration property. + * MC 1.7.10: net.minecraftforge.common.config.Property + * MC 1.12+: net.minecraftforge.common.config.Property + * MC 1.14+: ForgeConfigSpec.ConfigValue (adapter needed) + */ +public interface IConfigProperty { + String getString(); + int getInt(); + boolean getBoolean(); + double getDouble(); + String[] getStringList(); + void set(String value); + void set(int value); + void set(boolean value); + void set(double value); + String getComment(); + String getName(); +} diff --git a/platform-api/src/main/java/common/minecraftforge/common/config/IConfiguration.java b/platform-api/src/main/java/common/minecraftforge/common/config/IConfiguration.java new file mode 100644 index 000000000..57689cc7e --- /dev/null +++ b/platform-api/src/main/java/common/minecraftforge/common/config/IConfiguration.java @@ -0,0 +1,22 @@ +package common.minecraftforge.common.config; + +/** + * Platform abstraction for Forge's Configuration file handler. + * MC 1.7.10: net.minecraftforge.common.config.Configuration + * MC 1.12+: net.minecraftforge.common.config.Configuration + * MC 1.14+: ForgeConfigSpec (different pattern, adapter needed) + */ +public interface IConfiguration { + IConfigProperty get(String category, String key, String defaultValue, String comment); + IConfigProperty get(String category, String key, int defaultValue, String comment); + IConfigProperty get(String category, String key, boolean defaultValue, String comment); + IConfigProperty get(String category, String key, double defaultValue, String comment); + IConfigProperty get(String category, String key, String defaultValue, String comment, String[] validValues); + IConfigProperty get(String category, String key, int defaultValue, String comment, int minValue, int maxValue); + IConfigProperty get(String category, String key, double defaultValue, String comment, double minValue, double maxValue); + void save(); + void load(); + boolean hasKey(String category, String key); + boolean hasCategory(String category); + boolean hasChanged(); +} diff --git a/platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java b/platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java index 8b6097aee..d3545f1e5 100644 --- a/platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java +++ b/platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java @@ -1,5 +1,7 @@ package kamkeel.npcs.platform; +import common.minecraft.util.IResourceLocation; +import common.minecraft.util.ITextComponent; import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.IWorld; @@ -11,6 +13,10 @@ import noppes.npcs.platform.nbt.NBTFactory; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.UUID; /** * Central platform abstraction that bridges CORE logic with MC-version-specific code. @@ -93,4 +99,54 @@ public interface PlatformService { // --- Scheduling --- void runOnMainThread(Runnable task); + + // --- Translation --- + + String translateToLocal(String key); + + String translateToLocalFormatted(String key, Object... args); + + // --- Server --- + + boolean isLogicalClient(); + + boolean isModLoaded(String modId); + + boolean isDedicatedServer(); + + List getOnlinePlayers(); + + IPlayer getPlayerByName(String name); + + IPlayer getPlayerByUUID(UUID uuid); + + IWorld getWorldForDimension(int dimensionId); + + // --- Entity Registry --- + + String getEntityRegistryName(IEntity entity); + + // --- Item Registry --- + + IItemStack getItemById(int id); + + int getIdFromItem(IItemStack item); + + // --- Text Components --- + + ITextComponent createTextComponent(String text); + + ITextComponent createTranslatedComponent(String key, Object... args); + + // --- Resource --- + + IResourceLocation createResourceLocation(String resourceName); + + IResourceLocation createResourceLocation(String domain, String path); + + // --- NBT Stream I/O --- + + INbt readCompressedNBT(InputStream stream) throws IOException; + + void writeCompressedNBT(INbt compound, OutputStream stream) throws IOException; } diff --git a/tools/migration/Symbol-Swap.ps1 b/tools/migration/Symbol-Swap.ps1 index 2d6b3e633..759b914c3 100644 --- a/tools/migration/Symbol-Swap.ps1 +++ b/tools/migration/Symbol-Swap.ps1 @@ -88,11 +88,12 @@ function Symbol-Swap { } } - # PHASE 2: Replace symbols in code body + # PHASE 2: Replace symbols in code body (whole-word, CASE-SENSITIVE via -creplace) foreach ($swap in $Swaps) { $oldSym = $swap[0]; $newSym = $swap[1] - if ($content -match [regex]::Escape($oldSym)) { - $content = $content -replace [regex]::Escape($oldSym), $newSym + $pattern = '\b' + [regex]::Escape($oldSym) + '\b' + if ($content -cmatch $pattern) { + $content = $content -creplace $pattern, $newSym $fileSwapped = $true } }