diff --git a/Server/src/main/content/data/consumables/Consumables.java b/Server/src/main/content/data/consumables/Consumables.java index 40c52e87e..3f27c800f 100644 --- a/Server/src/main/content/data/consumables/Consumables.java +++ b/Server/src/main/content/data/consumables/Consumables.java @@ -42,7 +42,7 @@ public enum Consumables { SALMON(new Food(new int[] {329}, new HealingEffect(9))), SLIMY_EEL(new Food(new int[] {3381}, new HealingEffect(6))), TUNA(new Food(new int[] {361}, new HealingEffect(10))), - COOKED_KARAMBWAN(new Food(new int[] {3144}, new HealingEffect(18))), + COOKED_KARAMBWAN(new Food(new int[] {3144}, new HealingEffect(18)), true), COOKED_CHOMPY(new Food(new int[] {2878}, new HealingEffect(10))), RAINBOW_FISH(new Food(new int[] {10136}, new HealingEffect(11))), CAVE_EEL(new Food(new int[] {5003}, new HealingEffect(7))), @@ -364,24 +364,26 @@ public enum Consumables { SC_MAGIC(new Potion(new int[] {14267, 14269, 14271, 14273, 14275}, new SkillEffect(Skills.MAGIC, 3, 0.1))), SC_SUMMONING(new Potion(new int[] {14277, 14279, 14281, 14283, 14285}, new SummoningEffect(7, 0.25))); - public static HashMap consumables = new HashMap<>(); + public static HashMap consumables = new HashMap<>(); private final Consumable consumable; + public boolean isIgnoreMainClock = false; Consumables(Consumable consumable) { this.consumable = consumable; } + Consumables(Consumable consumable, boolean isIgnoreMainClock) {this.consumable = consumable; this.isIgnoreMainClock = isIgnoreMainClock;} public Consumable getConsumable() { return consumable; } - public static Consumable getConsumableById(final int itemId) { + public static Consumables getConsumableById(final int itemId) { return consumables.get(itemId); } - public static void add(final Consumable consumable) { - for (int id : consumable.getIds()) { + public static void add(final Consumables consumable) { + for (int id : consumable.consumable.getIds()) { consumables.putIfAbsent(id, consumable); } } @@ -391,7 +393,7 @@ public enum Consumables { */ static { for (Consumables consumable : Consumables.values()) { - add(consumable.consumable); + add(consumable); } } } diff --git a/Server/src/main/content/global/handlers/item/BirdNestPlugin.java b/Server/src/main/content/global/handlers/item/BirdNestPlugin.java index 30d4e64de..5499ba690 100644 --- a/Server/src/main/content/global/handlers/item/BirdNestPlugin.java +++ b/Server/src/main/content/global/handlers/item/BirdNestPlugin.java @@ -12,7 +12,6 @@ import core.plugin.Plugin; * Handles the searching of a bird nest item. * @author Vexia */ -@Initializable public final class BirdNestPlugin extends OptionHandler { @Override diff --git a/Server/src/main/content/global/handlers/item/BirdNestScript.kt b/Server/src/main/content/global/handlers/item/BirdNestScript.kt new file mode 100644 index 000000000..fb34fb63a --- /dev/null +++ b/Server/src/main/content/global/handlers/item/BirdNestScript.kt @@ -0,0 +1,22 @@ +package content.global.handlers.item + +import content.data.tables.BirdNest +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.Node +import core.game.node.entity.player.Player +import core.game.node.item.Item + +class BirdNestScript : InteractionListener { + val nestIds = BirdNest.values().map { it.nest.id }.toIntArray() + + override fun defineListeners() { + on(nestIds, IntType.ITEM, "search", handler = ::handleNest) + } + + private fun handleNest(player: Player, node: Node) : Boolean { + val nest = BirdNest.forNest(node as? Item ?: return false) + nest.search(player, node as? Item ?: return false) + return true + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/item/ConsumableListener.kt b/Server/src/main/content/global/handlers/item/ConsumableListener.kt new file mode 100644 index 000000000..7a9074f66 --- /dev/null +++ b/Server/src/main/content/global/handlers/item/ConsumableListener.kt @@ -0,0 +1,51 @@ +package content.global.handlers.item + +import content.data.consumables.Consumables +import core.api.delayAttack +import core.api.getUsedOption +import core.api.getWorldTicks +import core.api.stopExecuting +import core.game.interaction.Clocks +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.Node +import core.game.node.entity.player.Player +import core.game.node.item.Item +import core.game.world.GameWorld + +class ConsumableListener : InteractionListener { + override fun defineListeners() { + on(IntType.ITEM, "eat", "drink", handler = ::handleConsumable) + } + + private fun handleConsumable(player: Player, node: Node) : Boolean { + val consumable = Consumables.getConsumableById(node.id) ?: return stopExecuting(player) + + val food = getUsedOption(player) == "eat" + val isIgnoreMainClock = consumable.isIgnoreMainClock + + if (food) { + if (isIgnoreMainClock && player.clocks[Clocks.NEXT_CONSUME] < GameWorld.ticks) { + consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player) + player.clocks[Clocks.NEXT_CONSUME] = getWorldTicks() + 2 + player.clocks[Clocks.NEXT_EAT] = getWorldTicks() + 2 + delayAttack(player, 3) + } else if (player.clocks[Clocks.NEXT_CONSUME] < getWorldTicks() && player.clocks[Clocks.NEXT_EAT] < getWorldTicks()) { + consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player) + player.clocks[Clocks.NEXT_EAT] = getWorldTicks() + 2 + delayAttack(player, 3) + } + } else { + if (isIgnoreMainClock && player.clocks[Clocks.NEXT_CONSUME] < getWorldTicks()) { + consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player) + player.clocks[Clocks.NEXT_CONSUME] = getWorldTicks() + 3 + player.clocks[Clocks.NEXT_DRINK] = getWorldTicks() + 3 + } else if (player.clocks[Clocks.NEXT_CONSUME] < getWorldTicks() && player.clocks[Clocks.NEXT_DRINK] < getWorldTicks()) { + consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player) + player.clocks[Clocks.NEXT_DRINK] = getWorldTicks() + 3 + } + } + + return stopExecuting(player) + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/item/ConsumableOptionPlugin.java b/Server/src/main/content/global/handlers/item/ConsumableOptionPlugin.java index 1eff2635b..3b74cd867 100644 --- a/Server/src/main/content/global/handlers/item/ConsumableOptionPlugin.java +++ b/Server/src/main/content/global/handlers/item/ConsumableOptionPlugin.java @@ -17,7 +17,6 @@ import core.plugin.Plugin; * @author Emperor * @version 1.0 */ -@Initializable public final class ConsumableOptionPlugin extends OptionHandler { @Override @@ -35,7 +34,7 @@ public final class ConsumableOptionPlugin extends OptionHandler { @Override public boolean handle(final Player player, final Node node, final String option) { - if (player.getLocks().isLocked(option)) { +/* if (player.getLocks().isLocked(option)) { return true; } boolean food = option.equals("eat"); @@ -61,7 +60,7 @@ public final class ConsumableOptionPlugin extends OptionHandler { if (food) { player.getProperties().getCombatPulse().delayNextAttack(3); } - lastEaten = node.asItem().getId(); + lastEaten = node.asItem().getId();*/ return true; } } diff --git a/Server/src/main/content/global/handlers/item/GrandSeedPodHandler.kt b/Server/src/main/content/global/handlers/item/GrandSeedPodHandler.kt index 6d360b832..087ce60e6 100644 --- a/Server/src/main/content/global/handlers/item/GrandSeedPodHandler.kt +++ b/Server/src/main/content/global/handlers/item/GrandSeedPodHandler.kt @@ -2,14 +2,18 @@ package content.global.handlers.item import content.global.travel.glider.GliderPulse import content.global.travel.glider.Gliders +import core.api.* +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.interaction.QueueStrength import core.game.node.entity.player.Player import core.game.node.entity.skill.Skills import core.game.system.task.Pulse import core.game.world.map.Location +import core.net.packet.PacketRepository +import core.net.packet.context.MinimapStateContext +import core.net.packet.out.MinimapState import org.rs09.consts.Items -import core.game.interaction.InteractionListener -import core.game.interaction.IntType -import core.api.* private const val SQUASH_GRAPHICS_BEGIN = 767 private const val SQUASH_GRAPHICS_END = 769 @@ -25,41 +29,68 @@ private const val LAUNCH_ANIMATION = 4547 class GrandSeedPodHandler : InteractionListener { override fun defineListeners() { - on(Items.GRAND_SEED_POD_9469, IntType.ITEM, "squash", "launch"){ player, _ -> - when(getUsedOption(player)){ - "launch" -> submitWorldPulse(LaunchPulse(player)) - "squash" -> submitWorldPulse(SquashPulse(player)) + on(intArrayOf(Items.GRAND_SEED_POD_9469), IntType.ITEM, "squash", "launch") { player, _ -> + val opt = getUsedOption(player) + if (!removeItem(player, Items.GRAND_SEED_POD_9469)) return@on false + if (opt == "launch") { + visualize(player, LAUNCH_ANIMATION, LAUNCH_GRAPHICS) + delayEntity(player, 7) + queueScript(player, 3, QueueStrength.SOFT) {stage: Int -> + if (stage == 0) { + rewardXP(player, Skills.FARMING, 100.0) + openOverlay(player, 115) + return@queueScript keepRunning(player) + } + + if (stage == 1) { + PacketRepository.send(MinimapState::class.java, MinimapStateContext(player, 2)) + return@queueScript delayScript(player, 3) + } + + if (stage == 2) { + teleport(player, Gliders.TA_QUIR_PRIW.location) + return@queueScript delayScript(player, 2) + } + + if (stage == 3) { + closeOverlay(player) + PacketRepository.send(MinimapState::class.java, MinimapStateContext(player, 0)) + } + + return@queueScript stopExecuting(player) + } } - removeItem(player, Items.GRAND_SEED_POD_9469, Container.INVENTORY) - lock(player, 50) + + if (opt == "squash") { + visualize(player, SQUASH_ANIM_BEGIN, SQUASH_GRAPHICS_BEGIN) + delayEntity(player, 12) + queueScript(player, 3, QueueStrength.SOFT) {stage: Int -> + if (stage == 0) { + animate(player, 1241, true) + return@queueScript keepRunning(player) + } + + if (stage == 1) { + teleport(player, Location.create(2464, 3494, 0)) + return@queueScript keepRunning(player) + } + + if (stage == 2) { + visualize(player, 1241, SQUASH_GRAPHICS_END) + return@queueScript delayScript(player, 2) + } + + if (stage == 3) { + animate(player, SQUASH_ANIM_END, true) + adjustLevel(player, Skills.FARMING, -5) + return@queueScript keepRunning(player) + } + + return@queueScript stopExecuting(player) + } + } + return@on true } } - - class LaunchPulse(val player: Player): Pulse(){ - var counter = 0 - override fun pulse(): Boolean { - when(counter++){ - 1 -> visualize(player, LAUNCH_ANIMATION, LAUNCH_GRAPHICS) - 3 -> rewardXP(player, Skills.FARMING, 100.0) - 4 -> submitWorldPulse(GliderPulse(2, player, Gliders.TA_QUIR_PRIW)).also { return true } - } - return false - } - } - - class SquashPulse(val player: Player) : Pulse(){ - var counter = 0 - override fun pulse(): Boolean { - when(counter++){ - 1 -> visualize(player, SQUASH_ANIM_BEGIN, SQUASH_GRAPHICS_BEGIN) - 4 -> animate(player, 1241, true) - 5 -> teleport(player, Location.create(2464, 3494, 0)) - 6 -> visualize(player, anim = 1241, gfx = SQUASH_GRAPHICS_END) - 8 -> animate(player, SQUASH_ANIM_END, true).also { adjustLevel(player, Skills.FARMING, -5) } - 9 -> unlock(player).also { return true } - } - return false - } - } } \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/item/PlayerPeltables.kt b/Server/src/main/content/global/handlers/item/PlayerPeltables.kt index fc1a63790..931a39ef2 100644 --- a/Server/src/main/content/global/handlers/item/PlayerPeltables.kt +++ b/Server/src/main/content/global/handlers/item/PlayerPeltables.kt @@ -1,8 +1,7 @@ package content.global.handlers.item import core.api.* -import core.game.interaction.Interaction -import core.game.interaction.Option +import core.game.interaction.* import core.game.node.Node import core.game.node.entity.impl.Projectile import core.game.node.entity.player.Player @@ -11,8 +10,6 @@ import core.game.system.task.Pulse import core.game.world.map.path.Pathfinder import core.game.world.update.flag.context.Graphics import org.rs09.consts.Items -import core.game.interaction.InteractionListener -import core.game.interaction.IntType class PlayerPeltables : InteractionListener { @@ -40,7 +37,7 @@ class PlayerPeltables : InteractionListener { } private fun removePlayerOps(player: Player, _node: Node) : Boolean { - Interaction.sendOption(player, 0, "null") + InteractPlugin.sendOption(player, 0, "null") return true } diff --git a/Server/src/main/content/global/handlers/item/equipment/special/ShoveSpecialHandler.java b/Server/src/main/content/global/handlers/item/equipment/special/ShoveSpecialHandler.java index 22bea6e0d..104a14cf5 100644 --- a/Server/src/main/content/global/handlers/item/equipment/special/ShoveSpecialHandler.java +++ b/Server/src/main/content/global/handlers/item/equipment/special/ShoveSpecialHandler.java @@ -16,6 +16,8 @@ import core.game.world.update.flag.context.Graphics; import core.plugin.Initializable; import core.plugin.Plugin; +import static core.api.ContentAPIKt.stun; + /** * Handles the dragon spear special attack. * @author Emperor @@ -95,8 +97,7 @@ public final class ShoveSpecialHandler extends MeleeSwingHandler implements Plug } victim.getWalkingQueue().reset(); victim.getPulseManager().clear(); - victim.animate(STUN_ANIM); - victim.getStateManager().set(EntityState.STUNNED, 5); + stun(victim, 5); if (dir != null) { Point p = Direction.getWalkPoint(dir); Location dest = victim.getLocation().transform(p.getX(), p.getY(), 0); diff --git a/Server/src/main/content/global/handlers/scenery/BankBoothListener.kt b/Server/src/main/content/global/handlers/scenery/BankBoothListener.kt index e624fa7ad..c5dc0ac8f 100644 --- a/Server/src/main/content/global/handlers/scenery/BankBoothListener.kt +++ b/Server/src/main/content/global/handlers/scenery/BankBoothListener.kt @@ -93,7 +93,7 @@ class BankBoothListener : InteractionListener { } } - private fun quickBankBoothUse(player: Player, node: Node): Boolean { + private fun quickBankBoothUse(player: Player, node: Node, state: Int): Boolean { if (player.ironmanManager.checkRestriction(IronmanMode.ULTIMATE)) { return true } @@ -107,20 +107,20 @@ class BankBoothListener : InteractionListener { return true } - private fun regularBankBoothUse(player: Player, node: Node): Boolean { + private fun regularBankBoothUse(player: Player, node: Node, state: Int): Boolean { if (player.ironmanManager.checkRestriction(IronmanMode.ULTIMATE)) { return true } if (ServerConstants.BANK_BOOTH_QUICK_OPEN) { - return quickBankBoothUse(player, node) + return quickBankBoothUse(player, node, state) } tryInvokeBankerDialogue(player, node) return true } - private fun collectBankBoothUse(player: Player, node: Node): Boolean { + private fun collectBankBoothUse(player: Player, node: Node, state: Int): Boolean { if (BankerNPC.checkLunarIsleRestriction(player, node)) { tryInvokeBankerDialogue(player, node) return true @@ -176,9 +176,9 @@ class BankBoothListener : InteractionListener { } override fun defineListeners() { - on(BANK_BOOTHS, IntType.SCENERY, "use-quickly", "bank", handler = ::quickBankBoothUse) - on(BANK_BOOTHS, IntType.SCENERY, "use", handler = ::regularBankBoothUse) - on(BANK_BOOTHS, IntType.SCENERY, "collect", handler = ::collectBankBoothUse) + defineInteraction(IntType.SCENERY, BANK_BOOTHS, "use-quickly", "bank", handler = ::quickBankBoothUse) + defineInteraction(IntType.SCENERY, BANK_BOOTHS, "use", handler = ::regularBankBoothUse) + defineInteraction(IntType.SCENERY, BANK_BOOTHS, "collect", handler = ::collectBankBoothUse) if (ServerConstants.BANK_BOOTH_NOTE_ENABLED) { onUseAnyWith(IntType.SCENERY, *BANK_BOOTHS, handler = ::attemptToConvertItems) diff --git a/Server/src/main/content/global/handlers/scenery/BankChestListener.kt b/Server/src/main/content/global/handlers/scenery/BankChestListener.kt index e6e093d34..9fb26831b 100644 --- a/Server/src/main/content/global/handlers/scenery/BankChestListener.kt +++ b/Server/src/main/content/global/handlers/scenery/BankChestListener.kt @@ -20,9 +20,9 @@ private val BANK_CHESTS = intArrayOf( */ class BankChestListener : InteractionListener { override fun defineListeners() { - on(BANK_CHESTS, IntType.SCENERY, "bank", "use") { player, node -> + defineInteraction(IntType.SCENERY, BANK_CHESTS, "bank", "use") {player, node, state -> openBankAccount(player) - return@on true + return@defineInteraction true } } } \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/scenery/BankDepositBoxListener.kt b/Server/src/main/content/global/handlers/scenery/BankDepositBoxListener.kt index 5455c60c1..68579bfb6 100644 --- a/Server/src/main/content/global/handlers/scenery/BankDepositBoxListener.kt +++ b/Server/src/main/content/global/handlers/scenery/BankDepositBoxListener.kt @@ -29,7 +29,7 @@ private val BANK_DEPOSIT_BOXES = intArrayOf( */ class BankDepositBoxListener : InteractionListener { - private fun openDepositBox(player: Player, node: Node) : Boolean { + private fun openDepositBox(player: Player, node: Node, state: Int) : Boolean { restrictForIronman(player, IronmanMode.ULTIMATE) { player.interfaceManager.open(Component(Components.BANK_DEPOSIT_BOX_11)).closeEvent = CloseEvent { p, _ -> p.interfaceManager.openDefaultTabs() @@ -55,6 +55,6 @@ class BankDepositBoxListener : InteractionListener { } override fun defineListeners() { - on(BANK_DEPOSIT_BOXES, IntType.SCENERY, "deposit", handler = ::openDepositBox) + defineInteraction(IntType.SCENERY, BANK_DEPOSIT_BOXES, "deposit", handler = ::openDepositBox) } } \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/scenery/DoogleLeafInteraction.kt b/Server/src/main/content/global/handlers/scenery/DoogleLeafInteraction.kt new file mode 100644 index 000000000..fdf7d9891 --- /dev/null +++ b/Server/src/main/content/global/handlers/scenery/DoogleLeafInteraction.kt @@ -0,0 +1,24 @@ +package content.global.handlers.scenery + +import core.api.addItem +import core.api.sendMessage +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.Node +import core.game.node.entity.player.Player +import org.rs09.consts.Items + +class DoogleLeafInteraction : InteractionListener { + override fun defineListeners() { + defineInteraction(IntType.SCENERY, intArrayOf(31155), "pick-leaf", handler = ::handleDoogle) + } + + fun handleDoogle(player: Player, node: Node, state: Int) : Boolean { + if (!addItem(player, Items.DOOGLE_LEAVES_1573)) { + sendMessage(player, "You don't have enough space in your inventory.") + return true + } + sendMessage(player, "You pick some doogle leaves.") + return true + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/scenery/DoogleLeafPlugin.java b/Server/src/main/content/global/handlers/scenery/DoogleLeafPlugin.java index 5006404c0..99da19d9c 100644 --- a/Server/src/main/content/global/handlers/scenery/DoogleLeafPlugin.java +++ b/Server/src/main/content/global/handlers/scenery/DoogleLeafPlugin.java @@ -13,7 +13,6 @@ import core.plugin.Plugin; * @author 'Vexia * @version 1.0 */ -@Initializable public class DoogleLeafPlugin extends OptionHandler { /** diff --git a/Server/src/main/content/global/handlers/scenery/MillingListener.kt b/Server/src/main/content/global/handlers/scenery/MillingListener.kt index 077cc40ea..77e2abf0e 100644 --- a/Server/src/main/content/global/handlers/scenery/MillingListener.kt +++ b/Server/src/main/content/global/handlers/scenery/MillingListener.kt @@ -1,6 +1,7 @@ package content.global.handlers.scenery import core.api.* +import core.game.interaction.IntType import core.game.interaction.InteractionListener import core.game.node.entity.player.Player import core.game.node.entity.player.link.audio.Audio @@ -27,13 +28,13 @@ private val SOUND = Audio(3189) */ class MillingListener : InteractionListener { override fun defineListeners() { - on(HOPPER_CONTROLS, SCENERY, "operate", "pull") { player, _ -> + defineInteraction(IntType.SCENERY, HOPPER_CONTROLS, "operate", "pull") { player, _, _ -> useHopperControl(player) - return@on true + return@defineInteraction true } - on(FLOUR_BINS, SCENERY, "empty") { player, _ -> + defineInteraction(IntType.SCENERY, FLOUR_BINS, "empty") {player, _, _ -> fillPot(player) - return@on true + return@defineInteraction true } onUseWith(SCENERY, intArrayOf(GRAIN, SWEETCORN), *HOPPERS) { player, used, _ -> fillHopper(player, used.asItem()) diff --git a/Server/src/main/content/global/skill/gather/GatheringSkillOptionListeners.kt b/Server/src/main/content/global/skill/gather/GatheringSkillOptionListeners.kt index e29aa194e..d40ed5d0f 100644 --- a/Server/src/main/content/global/skill/gather/GatheringSkillOptionListeners.kt +++ b/Server/src/main/content/global/skill/gather/GatheringSkillOptionListeners.kt @@ -1,42 +1,20 @@ package content.global.skill.gather -import core.game.node.Node -import core.game.node.entity.npc.NPC -import core.game.node.entity.player.Player import content.global.skill.fishing.FishingSpot import content.global.skill.gather.fishing.FishingPulse import content.global.skill.gather.mining.MiningSkillPulse -import content.global.skill.gather.woodcutting.WoodcuttingSkillPulse -import org.rs09.consts.NPCs -import content.region.misc.miscellania.dialogue.KjallakOnChopDialogue -import core.game.interaction.InteractionListener import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.Node +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player class GatheringSkillOptionListeners : InteractionListener { val ETCETERIA_REGION = 10300 override fun defineListeners() { - on(IntType.SCENERY,"chop-down","chop down","cut down","chop"){ player, node -> - if(player.location.regionId == ETCETERIA_REGION){ - player.dialogueInterpreter.open(KjallakOnChopDialogue(), NPC(NPCs.CARPENTER_KJALLAK_3916)) - return@on true - } - player.pulseManager.run(WoodcuttingSkillPulse(player, node.asScenery())) - return@on true - } - on(IntType.SCENERY,"mine"){ player, node -> - player.pulseManager.run(MiningSkillPulse(player, node.asScenery())) - return@on true - } - - on(IntType.NPC,"net"){ player, node -> return@on fish(player,node,"net")} - on(IntType.NPC,"lure"){ player, node -> return@on fish(player,node,"lure")} - on(IntType.NPC,"bait"){ player, node -> return@on fish(player,node,"bait")} - on(IntType.NPC,"harpoon"){ player, node -> return@on fish(player,node,"harpoon")} - on(IntType.NPC,"cage"){ player, node -> return@on fish(player,node,"cage")} - on(IntType.NPC,"fish"){ player, node -> return@on fish(player,node,"fish") } } fun fish(player: Player, node: Node, opt: String): Boolean{ diff --git a/Server/src/main/content/global/skill/gather/fishing/FishingListener.kt b/Server/src/main/content/global/skill/gather/fishing/FishingListener.kt new file mode 100644 index 000000000..60dc2c404 --- /dev/null +++ b/Server/src/main/content/global/skill/gather/fishing/FishingListener.kt @@ -0,0 +1,118 @@ +package content.global.skill.gather.fishing + +import content.global.skill.fishing.Fish +import content.global.skill.fishing.FishingOption +import content.global.skill.fishing.FishingSpot +import content.global.skill.skillcapeperks.SkillcapePerks +import content.global.skill.skillcapeperks.SkillcapePerks.Companion.isActive +import content.global.skill.summoning.familiar.Forager +import core.api.* +import core.game.event.ResourceProducedEvent +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.Node +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player +import core.game.node.entity.skill.Skills +import core.game.system.command.sets.STATS_BASE +import core.game.system.command.sets.STATS_FISH +import core.game.world.map.path.Pathfinder +import core.tools.RandomFunction +import core.tools.colorize + +class FishingListener : InteractionListener{ + override fun defineListeners() { + val SPOT_IDS = FishingSpot.values().flatMap { it.ids.toList() }.toIntArray() + defineInteraction( + IntType.NPC, + SPOT_IDS, + "net", "lure", "bait", "harpoon", "cage", "fish", + persistent = true, + allowedDistance = 1, + handler = ::handleFishing + ) + } + + private fun handleFishing(player: Player, node: Node, state: Int) : Boolean { + val npc = node as? NPC ?: return clearScripts(player) + val spot = FishingSpot.forId(npc.id) ?: return clearScripts(player) + val op = spot.getOptionByName(getUsedOption(player)) + var forager: Forager? = null + + if (player.familiarManager.hasFamiliar() && player.familiarManager.familiar is Forager) { + forager = player.familiarManager.familiar as Forager + } + + if (!finishedMoving(player)) + return restartScript(player) + + if (state == 0) { + if (!checkRequirements(player, op, node)) + return clearScripts(player) + forager?.let { + val dest = player.location.transform(player.direction) + Pathfinder.find(it, dest).walk(it) + } + anim(player, op) + return delayScript(player, 5) + } + + anim(player, op) + forager?.handlePassiveAction() + + val fish = op.rollFish(player) ?: return delayScript(player, 5) + if (!hasSpaceFor(player, fish.item)) return restartScript(player) + if (!op.removeBait(player.inventory)) return restartScript(player) + player.dispatch(ResourceProducedEvent(fish.item.id, fish.item.amount, node)) + + val item = fish.item + if (isActive(SkillcapePerks.GREAT_AIM, player) && RandomFunction.roll(20)) { + addItem(player, item.id, item.amount) + sendMessage(player, colorize("%RYour expert aim catches you a second fish.")) + } + addItemOrDrop(player, item.id, item.amount) + player.incrementAttribute("$STATS_BASE:$STATS_FISH") + rewardXP(player, Skills.FISHING, fish.experience) + + return restartScript(player) + } + + private fun anim(player: Player, option: FishingOption) { + if (animationFinished(player)) + animate(player, option.animation) + } + + private fun checkRequirements(player: Player, option: FishingOption, node: Node) : Boolean { + if (!player.inventory.containsItem(option.tool) && !hasBarbTail(player, option)) { + player.dialogueInterpreter.sendDialogue("You need a " + option.tool.name.toLowerCase() + " to catch these fish.") + return false + } + if (!option.hasBait(player.inventory)) { + player.dialogueInterpreter.sendDialogue("You don't have any " + option.getBaitName().toLowerCase() + "s left.") + return false + } + if (player.skills.getLevel(Skills.FISHING) < option!!.level) { + val f = option!!.fish[option!!.fish.size - 1] + player.dialogueInterpreter.sendDialogue("You need a fishing level of " + f.level + " to catch " + (if (f == Fish.SHRIMP || f == Fish.ANCHOVIE) "" else "a") + " " + f.item.name.toLowerCase() + ".".trim { it <= ' ' }) + return false + } + if (player.inventory.freeSlots() == 0) { + player.dialogueInterpreter.sendDialogue("You don't have enough space in your inventory.") + return false + } + return node.isActive && node.location.withinDistance(player.location, 1) + } + + + private fun hasBarbTail(player: Player, option: FishingOption): Boolean { + if (option == FishingOption.HARPOON || option == FishingOption.N_HARPOON) { + if (player.inventory.containsItem(FishingOption.BARB_HARPOON.tool) || player.equipment.containsItem( + FishingOption.BARB_HARPOON.tool + ) + ) { + return true + } + } + return false + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/gather/mining/MiningListener.kt b/Server/src/main/content/global/skill/gather/mining/MiningListener.kt new file mode 100644 index 000000000..9f743c7e5 --- /dev/null +++ b/Server/src/main/content/global/skill/gather/mining/MiningListener.kt @@ -0,0 +1,225 @@ +package content.global.skill.gather.mining + +import content.data.skill.SkillingPets +import content.data.skill.SkillingTool +import content.global.skill.skillcapeperks.SkillcapePerks +import core.api.* +import core.cache.def.impl.ItemDefinition +import core.game.event.ResourceProducedEvent +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.Node +import core.game.node.entity.npc.drop.DropFrequency +import core.game.node.entity.player.Player +import core.game.node.entity.player.link.diary.DiaryType +import core.game.node.entity.skill.Skills +import core.game.node.item.ChanceItem +import core.game.node.scenery.Scenery +import core.game.node.scenery.SceneryBuilder +import core.game.system.command.sets.STATS_BASE +import core.game.system.command.sets.STATS_ROCKS +import core.tools.RandomFunction +import core.tools.prependArticle +import org.rs09.consts.Items + +class MiningListener : InteractionListener { + override fun defineListeners() { + defineInteraction( + IntType.SCENERY, + MiningNode.values().map { it.id }.toIntArray(), + "mine", + persistent = true, allowedDistance = 1, + handler = ::handleMining + ) + } + private val GEM_REWARDS = arrayOf(ChanceItem(1623, 1, DropFrequency.COMMON), ChanceItem(1621, 1, DropFrequency.COMMON), ChanceItem(1619, 1, DropFrequency.UNCOMMON), ChanceItem(1617, 1, DropFrequency.RARE)) + + private fun handleMining(player: Player, node: Node, state: Int) : Boolean { + val resource = MiningNode.forId(node.id) + val tool = SkillingTool.getPickaxe(player) + val isEssence = resource.id == 2491 + val isGems = resource.identifier == MiningNode.GEM_ROCK_0.identifier + + if (!finishedMoving(player)) + return true + + if (state == 0) { + if (!checkRequirements(player, resource, node)) { + player.scripts.reset() + return true + } + anim(player, tool) + sendMessage(player, "You swing your pickaxe at the rock...") + return delayScript(player, getDelay(resource)) + } + + anim(player, tool) + if (!checkReward(player, resource, tool)) + return delayScript(player, getDelay(resource)) + + // Reward logic + var reward = resource!!.reward + var rewardAmount : Int + if (reward > 0) { + reward = calculateReward(player, resource, isEssence, isGems, reward) // calculate rewards + rewardAmount = calculateRewardAmount(player, isEssence, reward) // calculate amount + + player.dispatch(ResourceProducedEvent(reward, rewardAmount, node)) + SkillingPets.checkPetDrop(player, SkillingPets.GOLEM) // roll for pet + + // Reward mining experience + val experience = resource!!.experience * rewardAmount + rewardXP(player, Skills.MINING, experience) + + // If player is wearing Bracelet of Clay, soften + if(reward == Items.CLAY_434){ + val bracelet = getItemFromEquipment(player, EquipmentSlot.HANDS) + if(bracelet != null && bracelet.id == Items.BRACELET_OF_CLAY_11074){ + var charges = player.getAttribute("jewellery-charges:bracelet-of-clay", 28); + charges-- + reward = Items.SOFT_CLAY_1761 + sendMessage(player, "Your bracelet of clay softens the clay for you.") + if(charges <= 0) { + if(removeItem(player, bracelet, Container.EQUIPMENT)) { + sendMessage(player, "Your bracelet of clay crumbles to dust.") + charges = 28 + } + } + player.setAttribute("/save:jewellery-charges:bracelet-of-clay", charges) + } + } + val rewardName = getItemName(reward).lowercase() + + // Send the message for the resource reward + if (isGems) { + sendMessage(player, "You get ${prependArticle(rewardName)}.") + } else { + sendMessage(player, "You get some ${rewardName.lowercase()}.") + } + + // Give the mining reward, increment 'rocks mined' attribute + if(addItem(player, reward, rewardAmount)) { + var rocksMined = getAttribute(player, "$STATS_BASE:$STATS_ROCKS", 0) + setAttribute(player, "/save:$STATS_BASE:$STATS_ROCKS", ++rocksMined) + } + + // Calculate bonus gem chance while mining + if (!isEssence) { + var chance = 282 + var altered = false + val ring = getItemFromEquipment(player, EquipmentSlot.RING) + if (ring != null && ring.name.lowercase().contains("ring of wealth") || inEquipment(player, Items.RING_OF_THE_STAR_SPRITE_14652)) { + chance = (chance / 1.5).toInt() + altered = true + } + val necklace = getItemFromEquipment(player, EquipmentSlot.AMULET) + if (necklace != null && necklace.id in 1705..1713) { + chance = (chance / 1.5).toInt() + altered = true + } + if (RandomFunction.roll(chance)) { + val gem = GEM_REWARDS.random() + sendMessage(player,"You find a ${gem.name}!") + if (freeSlots(player) == 0) { + sendMessage(player,"You do not have enough space in your inventory, so you drop the gem on the floor.") + } + addItemOrDrop(player, gem.id) + } + } + + // Transform ore to depleted version + if (!isEssence && resource!!.respawnRate != 0) { + SceneryBuilder.replace(node as Scenery, Scenery(resource!!.emptyId, node.getLocation(), node.type, node.rotation), resource!!.respawnDuration) + node.setActive(false) + return true + } + } + return true + } + + private fun calculateRewardAmount(player: Player, isMiningEssence: Boolean, reward: Int): Int { + var amount = 1 + + // If player is wearing Varrock armour from diary, roll chance at extra ore + if (!isMiningEssence && player.achievementDiaryManager.getDiary(DiaryType.VARROCK).level != -1) { + when (reward) { + Items.CLAY_434, Items.COPPER_ORE_436, Items.TIN_ORE_438, Items.LIMESTONE_3211, Items.BLURITE_ORE_668, Items.IRON_ORE_440, Items.ELEMENTAL_ORE_2892, Items.SILVER_ORE_442, Items.COAL_453 -> if (player.achievementDiaryManager.armour >= 0 && RandomFunction.random(100) <= 10) { + amount += 1 + sendMessage(player,"The Varrock armour allows you to mine an additional ore.") + } + Items.GOLD_ORE_444, Items.GRANITE_500G_6979, Items.GRANITE_2KG_6981, Items.GRANITE_5KG_6983, Items.MITHRIL_ORE_447 -> if (player.achievementDiaryManager.armour >= 1 && RandomFunction.random(100) <= 10) { + amount += 1 + sendMessage(player, "The Varrock armour allows you to mine an additional ore.") + } + Items.ADAMANTITE_ORE_449 -> if (player.achievementDiaryManager.armour >= 2 && RandomFunction.random(100) <= 10) { + amount += 1 + sendMessage(player, "The Varrock armour allows you to mine an additional ore.") + } + } + } + + // If player has mining boost from Shooting Star, roll chance at extra ore + if (player.hasActiveState("shooting-star")) { + if (RandomFunction.getRandom(5) == 3) { + sendMessage(player, "...you manage to mine a second ore thanks to the Star Sprite.") + amount += 1 + } + } + return amount + } + + private fun calculateReward(player: Player, resource: MiningNode, isMiningEssence: Boolean, isMiningGems: Boolean, reward: Int): Int { + // If the player is mining sandstone or granite, then get size of sandstone/granite and xp reward for that size + var reward = reward + if (resource == MiningNode.SANDSTONE || resource == MiningNode.GRANITE) { + val value = RandomFunction.randomize(if (resource == MiningNode.GRANITE) 3 else 4) + reward += value shl 1 + rewardXP(player, Skills.MINING, value * 10.toDouble()) + } else if (isMiningEssence && getDynLevel(player, Skills.MINING) >= 30) { + reward = 7936 + } else if (isMiningGems) { + reward = RandomFunction.rollWeightedChanceTable(MiningNode.gemRockGems).id + } + return reward + } + + private fun checkReward(player: Player, resource: MiningNode?, tool: SkillingTool): Boolean { + val level = 1 + getDynLevel(player, Skills.MINING) + getFamiliarBoost(player, Skills.MINING) + val hostRatio = Math.random() * (100.0 * resource!!.rate) + var toolRatio = tool.ratio + if(SkillcapePerks.isActive(SkillcapePerks.PRECISION_MINER,player)){ + toolRatio += 0.075 + } + val clientRatio = Math.random() * ((level - resource.level) * (1.0 + toolRatio)) + return hostRatio < clientRatio + } + + fun getDelay(resource: MiningNode) : Int { + return if (resource.id == 2491) 3 else 4 + } + + fun anim(player: Player, tool: SkillingTool) { + if (animationFinished(player)) + animate(player, tool.animation) + } + + fun checkRequirements(player: Player, resource: MiningNode, node: Node): Boolean { + if (getDynLevel(player, Skills.MINING) < resource.level) { + sendMessage(player, "You need a mining level of ${resource.level} to mine this rock.") + return false + } + if (SkillingTool.getPickaxe(player) == null) { + sendMessage(player, "You do not have a pickaxe to use.") + return false + } + if (freeSlots(player) == 0) { + if(resource.identifier == 13.toByte()) { + sendDialogue(player,"Your inventory is too full to hold any more gems.") + return false + } + sendDialogue(player,"Your inventory is too full to hold any more ${ItemDefinition.forId(resource!!.reward).name.lowercase()}.") + return false + } + return node.isActive + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingListener.kt b/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingListener.kt new file mode 100644 index 000000000..f21b8d336 --- /dev/null +++ b/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingListener.kt @@ -0,0 +1,247 @@ +package content.global.skill.gather.woodcutting + +import content.data.skill.SkillingPets +import content.data.skill.SkillingTool +import content.data.tables.BirdNest +import content.global.skill.farming.FarmingPatch.Companion.forObject +import content.global.skill.skillcapeperks.SkillcapePerks +import content.global.skill.skillcapeperks.SkillcapePerks.Companion.isActive +import core.api.delayScript +import core.api.finishedMoving +import core.api.sendMessage +import core.cache.def.impl.ItemDefinition +import core.game.container.impl.EquipmentContainer +import core.game.event.ResourceProducedEvent +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.Node +import core.game.node.entity.impl.Projectile +import core.game.node.entity.player.Player +import core.game.node.entity.player.link.audio.Audio +import core.game.node.entity.player.link.diary.DiaryType +import core.game.node.entity.skill.Skills +import core.game.node.item.Item +import core.game.node.scenery.Scenery +import core.game.node.scenery.SceneryBuilder +import core.game.system.command.sets.STATS_BASE +import core.game.system.command.sets.STATS_LOGS +import core.game.world.map.RegionManager +import core.tools.RandomFunction +import org.rs09.consts.Items +import org.rs09.consts.Sounds +import org.rs09.consts.Sounds.TREE_FALLING_2734 +import java.util.* +import kotlin.streams.toList + +class WoodcuttingListener : InteractionListener { + private val woodcuttingSounds = intArrayOf( + Sounds.WOODCUTTING_HIT_3038, + Sounds.WOODCUTTING_HIT_3039, + Sounds.WOODCUTTING_HIT_3040, + Sounds.WOODCUTTING_HIT_3041, + Sounds.WOODCUTTING_HIT_3042 + ) + + override fun defineListeners() { + defineInteraction( + IntType.SCENERY, + ids = WoodcuttingNode.values().map { it.id }.toIntArray(), + "chop-down", "chop", "chop down", "cut down", + persistent = true, + allowedDistance = 1, + handler = ::handleWoodcutting + ) + } + + private fun handleWoodcutting(player: Player, node: Node, state: Int) : Boolean { + val resource = WoodcuttingNode.forId(node.id) + val tool = SkillingTool.getHatchet(player) + + if (!finishedMoving(player)) + return true + + if (state == 0) { + if (!checkWoodcuttingRequirements(player, resource, node)) { + player.scripts.reset() + return true + } + animateWoodcutting(player) + sendMessage(player, "You swing your axe at the tree...") + return delayScript(player, 3) + } + + animateWoodcutting(player) + if (!checkReward(player, resource, tool)) + return delayScript(player, 3) + + if (tool.id == Items.INFERNO_ADZE_13661 && RandomFunction.roll(4)) { + sendMessage(player, "You chop some logs. The heat of the inferno adze incinerates them.") + Projectile.create( + player, null, + 1776, + 35, 30, + 20, 25 + ).transform( + player, + player.location.transform(2, 0, 0), + true, + 25, 25 + ).send() + delayScript(player, 3) + return rollDepletion(player, node.asScenery(), resource) + } + + val reward = resource.getReward() + val rewardAmount: Int + if (reward > 0) { + rewardAmount = calculateRewardAmount(player, reward) // calculate amount + SkillingPets.checkPetDrop(player, SkillingPets.BEAVER) // roll for pet + + //add experience + val experience: Double = calculateExperience(player, resource, rewardAmount) + player.getSkills().addExperience(Skills.WOODCUTTING, experience, true) + + //send the message for the resource reward + if (resource == WoodcuttingNode.DRAMEN_TREE) { + player.packetDispatch.sendMessage("You cut a branch from the Dramen tree.") + } else { + player.packetDispatch.sendMessage("You get some " + ItemDefinition.forId(reward).name.lowercase(Locale.getDefault()) + ".") + } + + //give the reward + player.inventory.add(Item(reward, rewardAmount)) + player.dispatch(ResourceProducedEvent(reward, rewardAmount, node, -1)) + var cutLogs = player.getAttribute("$STATS_BASE:$STATS_LOGS", 0) + player.setAttribute("/save:$STATS_BASE:$STATS_LOGS", ++cutLogs) + + //calculate bonus bird nest for mining + val chance = 282 + if (RandomFunction.random(chance) == chance / 2) { + if (isActive(SkillcapePerks.NEST_HUNTER, player)) { + if (!player.inventory.add(BirdNest.getRandomNest(false).nest)) { + BirdNest.drop(player) + } + } else { + BirdNest.drop(player) + } + } + } + + delayScript(player, 3) + rollDepletion(player, node.asScenery(), resource) + return true + } + + private fun rollDepletion(player: Player, node: Scenery, resource: WoodcuttingNode): Boolean { + //transform to depleted version + //OSRS and RS3 Wikis both agree: All trees present in 2009 are a 1/8 fell chance, aside from normal trees/dead trees which are 100% + //OSRS: https://oldschool.runescape.wiki/w/Woodcutting scroll down to the mechanics section + //RS3 : https://runescape.wiki/w/Woodcutting scroll down to the mechanics section, and expand the tree felling chances table + if (resource.getRespawnRate() > 0) { + if (RandomFunction.roll(8) || resource.identifier.toInt() == 1 || resource.identifier.toInt() == 2 || resource.identifier.toInt() == 3 || resource.identifier.toInt() == 6) { + if (resource.isFarming()) { + val fPatch = forObject(node.asScenery()) + if (fPatch != null) { + val patch = fPatch.getPatchFor(player) + patch.setCurrentState(patch.getCurrentState() + 1) + } + return true + } + if (resource.getEmptyId() > -1) { + SceneryBuilder.replace(node, node.transform(resource.getEmptyId()), resource.getRespawnDuration()) + } else { + SceneryBuilder.replace(node, node.transform(0), resource.getRespawnDuration()) + } + node.setActive(false) + player.getAudioManager().send(TREE_FALLING_2734) + return true + } + } + return false + } + + private fun checkReward(player: Player, resource: WoodcuttingNode, tool: SkillingTool): Boolean { + val skill = Skills.WOODCUTTING + val level: Int = player.getSkills().getLevel(skill) + player.getFamiliarManager().getBoost(skill) + val hostRatio = RandomFunction.randomDouble(100.0) + val lowMod: Double = if (tool == SkillingTool.BLACK_AXE) resource.tierModLow / 2 else resource.tierModLow + val low: Double = resource.baseLow + tool.ordinal * lowMod + val highMod: Double = if (tool == SkillingTool.BLACK_AXE) resource.tierModHigh / 2 else resource.tierModHigh + val high: Double = resource.baseHigh + tool.ordinal * highMod + val clientRatio = RandomFunction.getSkillSuccessChance(low, high, level) + return hostRatio < clientRatio + } + + fun animateWoodcutting(player: Player) { + if (!player.animator.isAnimating) { + player.animate(SkillingTool.getHatchet(player).animation) + val playersAroundMe: List = RegionManager.getLocalPlayers(player, 2) + .stream() + .filter { p: Player -> p.username != player.username } + .toList() + val soundIndex = RandomFunction.random(0, woodcuttingSounds.size) + player.audioManager.send( + Audio(woodcuttingSounds[soundIndex]), + playersAroundMe, + player.location + ) + } + } + + fun checkWoodcuttingRequirements(player: Player, resource: WoodcuttingNode, node: Node): Boolean { + if (player.getSkills().getLevel(Skills.WOODCUTTING) < resource.getLevel()) { + player.getPacketDispatch().sendMessage("You need a woodcutting level of " + resource.getLevel() + " to chop this tree.") + return false + } + if (SkillingTool.getHatchet(player) == null) { + player.packetDispatch.sendMessage("You do not have an axe to use.") + return false + } + if (player.inventory.freeSlots() < 1) { + player.dialogueInterpreter.sendDialogue("Your inventory is too full to hold any more " + ItemDefinition.forId(resource.getReward()).name.lowercase(Locale.getDefault()) + ".") + return false + } + return node.isActive + } + + + private fun calculateRewardAmount(player: Player, reward: Int): Int { + var amount = 1 + + // 3239: Hollow tree (bark) 10% chance of obtaining + if (reward == 3239 && RandomFunction.random(100) >= 10) { + amount = 0 + } + + // Seers village medium reward - extra normal log while in seer's village + if (reward == 1511 && player.getAchievementDiaryManager().getDiary(DiaryType.SEERS_VILLAGE).isComplete(1) && player.getViewport().getRegion().getId() == 10806) { + amount = 2 + } + return amount + } + + private fun calculateExperience(player: Player, resource: WoodcuttingNode, amount: Int): Double { + var amount = amount + var experience: Double = resource.getExperience() + val reward = resource.reward + if (player.getLocation().getRegionId() == 10300) { + return 1.0 + } + + // Bark + if (reward == 3239) { + // If we receive the item, give the full experience points otherwise give the base amount + if (amount >= 1) { + experience = 275.2 + } else { + amount = 1 + } + } + + // Seers village medium reward - extra 10% xp from maples while wearing headband + if (reward == 1517 && player.getAchievementDiaryManager().getDiary(DiaryType.SEERS_VILLAGE).isComplete(1) && player.getEquipment().get(EquipmentContainer.SLOT_HAT) != null && player.getEquipment().get(EquipmentContainer.SLOT_HAT).getId() == 14631) { + experience *= 1.10 + } + return experience * amount + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingNode.java b/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingNode.java index 9415b4b2c..520821bd3 100644 --- a/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingNode.java +++ b/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingNode.java @@ -141,10 +141,10 @@ public enum WoodcuttingNode { double experience,rate; public byte identifier; boolean farming; - double baseLow = 64; - double baseHigh = 200; - double tierModLow = 32; - double tierModHigh = 100; + public double baseLow = 64; + public double baseHigh = 200; + public double tierModLow = 32; + public double tierModHigh = 100; WoodcuttingNode(int full, int empty,byte identifier){ this.full = full; this.empty = empty; diff --git a/Server/src/main/content/global/skill/magic/lunar/StatBoostSpell.java b/Server/src/main/content/global/skill/magic/lunar/StatBoostSpell.java index 2877d1a1e..7e6d6d842 100644 --- a/Server/src/main/content/global/skill/magic/lunar/StatBoostSpell.java +++ b/Server/src/main/content/global/skill/magic/lunar/StatBoostSpell.java @@ -40,7 +40,7 @@ public final class StatBoostSpell extends MagicSpell { public boolean cast(Entity entity, Node target) { final Player player = ((Player) entity); Item item = ((Item) target); - final Potion potion = (Potion) Consumables.getConsumableById(item.getId()); + final Potion potion = (Potion) Consumables.getConsumableById(item.getId()).getConsumable(); player.getInterfaceManager().setViewedTab(6); if (potion == null) { player.getPacketDispatch().sendMessage("You can only cast this spell on a potion."); diff --git a/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java b/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java index e702f9942..c56122ca3 100644 --- a/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java +++ b/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java @@ -42,7 +42,7 @@ public class StatRestoreSpell extends MagicSpell { public boolean cast(Entity entity, Node target) { final Player player = ((Player) entity); Item item = ((Item) target); - final Potion potion = (Potion) Consumables.getConsumableById(item.getId()); + final Potion potion = (Potion) Consumables.getConsumableById(item.getId()).getConsumable(); player.getInterfaceManager().setViewedTab(6); if (potion == null) { player.getPacketDispatch().sendMessage("You can only cast this spell on a potion."); diff --git a/Server/src/main/content/global/skill/runecrafting/RunePouch.java b/Server/src/main/content/global/skill/runecrafting/RunePouch.java index 98b5a6caa..b2fbf892a 100644 --- a/Server/src/main/content/global/skill/runecrafting/RunePouch.java +++ b/Server/src/main/content/global/skill/runecrafting/RunePouch.java @@ -1,6 +1,6 @@ package content.global.skill.runecrafting; -import core.game.global.action.DropItemHandler; +import core.game.global.action.DropListener; import core.game.node.entity.skill.Skills; import core.game.node.entity.player.Player; import core.game.node.item.Item; @@ -195,7 +195,7 @@ public enum RunePouch { */ private void drop(Player player, Item item) { onDrop(player, item); - DropItemHandler.drop(player, item); + DropListener.drop(player, item); } /** diff --git a/Server/src/main/content/global/skill/summoning/familiar/BunyipNPC.java b/Server/src/main/content/global/skill/summoning/familiar/BunyipNPC.java index dab0ee56b..df1c54e37 100644 --- a/Server/src/main/content/global/skill/summoning/familiar/BunyipNPC.java +++ b/Server/src/main/content/global/skill/summoning/familiar/BunyipNPC.java @@ -94,7 +94,7 @@ public class BunyipNPC extends Familiar { player.sendMessage("You can't use this special on an object like that."); return false; } - Consumable consumable = Consumables.getConsumableById(special.getItem().getId() + 2); + Consumable consumable = Consumables.getConsumableById(special.getItem().getId() + 2).getConsumable(); if (consumable == null) { player.sendMessage("Error: Report to admin."); return false; @@ -138,7 +138,7 @@ public class BunyipNPC extends Familiar { public boolean handle(NodeUsageEvent event) { Player player = event.getPlayer(); Fish fish = Fish.forItem(event.getUsedItem()); - Consumable consumable = Consumables.getConsumableById(fish.getItem().getId() + 2); + Consumable consumable = Consumables.getConsumableById(fish.getItem().getId() + 2).getConsumable(); if (consumable == null) { return true; } diff --git a/Server/src/main/content/global/skill/summoning/familiar/MinotaurFamiliarNPC.java b/Server/src/main/content/global/skill/summoning/familiar/MinotaurFamiliarNPC.java index 33e8f9ba4..f5c4d0a07 100644 --- a/Server/src/main/content/global/skill/summoning/familiar/MinotaurFamiliarNPC.java +++ b/Server/src/main/content/global/skill/summoning/familiar/MinotaurFamiliarNPC.java @@ -15,6 +15,8 @@ import core.plugin.ClassScanner; import core.plugin.Initializable; import core.tools.RandomFunction; +import static core.api.ContentAPIKt.stun; + /** * The plugin used to load the minotaur familiar npcs. * @author Vexia @@ -58,7 +60,7 @@ public final class MinotaurFamiliarNPC implements Plugin { GameWorld.getPulser().submit(new Pulse(ticks) { @Override public boolean pulse() { - target.getStateManager().set(EntityState.STUNNED, 4); + stun(target, 4); return true; } }); diff --git a/Server/src/main/content/global/skill/summoning/familiar/RavenousLocustNPC.java b/Server/src/main/content/global/skill/summoning/familiar/RavenousLocustNPC.java index bd1cb72c4..ad6d5180b 100644 --- a/Server/src/main/content/global/skill/summoning/familiar/RavenousLocustNPC.java +++ b/Server/src/main/content/global/skill/summoning/familiar/RavenousLocustNPC.java @@ -52,7 +52,7 @@ public class RavenousLocustNPC extends Familiar { if (item == null) { continue; } - Consumable consumable = Consumables.getConsumableById(item.getId()); + Consumable consumable = Consumables.getConsumableById(item.getId()).getConsumable(); if (consumable != null) { p.getInventory().remove(item); break; diff --git a/Server/src/main/content/global/skill/thieving/ThievingListeners.kt b/Server/src/main/content/global/skill/thieving/ThievingListeners.kt index 1722902e4..276b5556c 100644 --- a/Server/src/main/content/global/skill/thieving/ThievingListeners.kt +++ b/Server/src/main/content/global/skill/thieving/ThievingListeners.kt @@ -1,5 +1,6 @@ package content.global.skill.thieving +import core.api.stun import core.game.node.entity.combat.ImpactHandler import core.game.node.entity.impl.Animator import core.game.node.entity.player.Player @@ -59,8 +60,7 @@ class ThievingListeners : InteractionListener { val hitSoundId = 518 + RandomFunction.random(4) // choose 1 of 4 possible hit noises player.audioManager.send(hitSoundId, 1, 20) // OSRS defines a delay of 20 - player.stateManager.set(EntityState.STUNNED, secondsToTicks(pickpocketData.stunTime)) - player.lock(secondsToTicks(pickpocketData.stunTime)) + stun(player, pickpocketData.stunTime) player.impactHandler.manualHit(node.asNpc(),RandomFunction.random(pickpocketData.stunDamageMin,pickpocketData.stunDamageMax),ImpactHandler.HitsplatType.NORMAL) diff --git a/Server/src/main/content/minigame/bountyhunter/BountyLocateSpell.java b/Server/src/main/content/minigame/bountyhunter/BountyLocateSpell.java index 7e24571e5..672c2d8bf 100644 --- a/Server/src/main/content/minigame/bountyhunter/BountyLocateSpell.java +++ b/Server/src/main/content/minigame/bountyhunter/BountyLocateSpell.java @@ -17,6 +17,8 @@ import core.game.world.map.Location; import core.game.world.map.RegionManager; import core.plugin.Plugin; +import static core.api.ContentAPIKt.isStunned; + /** * Handles the bounty target locate spell. * @author Emperor @@ -43,8 +45,8 @@ public final class BountyLocateSpell extends MagicSpell { player.getPacketDispatch().sendMessage("You don't have a target to teleport to."); return true; } - if (player.getStateManager().hasState(EntityState.FROZEN) || player.getStateManager().hasState(EntityState.STUNNED)) { - player.getPacketDispatch().sendMessage("You can't use this when " + (player.getStateManager().hasState(EntityState.STUNNED) ? "stunned." : "frozen.")); + if (player.getStateManager().hasState(EntityState.FROZEN) || isStunned(player)) { + player.getPacketDispatch().sendMessage("You can't use this when " + (isStunned(player) ? "stunned." : "frozen.")); return true; } boolean combat = player.inCombat(); diff --git a/Server/src/main/content/region/kandarin/seers/quest/merlinsquest/MerlinCrystalPlugin.java b/Server/src/main/content/region/kandarin/seers/quest/merlinsquest/MerlinCrystalPlugin.java index d6a07c71a..2f339872d 100644 --- a/Server/src/main/content/region/kandarin/seers/quest/merlinsquest/MerlinCrystalPlugin.java +++ b/Server/src/main/content/region/kandarin/seers/quest/merlinsquest/MerlinCrystalPlugin.java @@ -9,7 +9,7 @@ import core.game.dialogue.DialoguePlugin; import core.game.dialogue.FacialExpression; import core.game.global.action.ClimbActionHandler; import core.game.global.action.DoorActionHandler; -import core.game.global.action.DropItemHandler; +import core.game.global.action.DropListener; import core.game.interaction.NodeUsageEvent; import core.game.interaction.OptionHandler; import core.game.interaction.UseWithHandler; @@ -143,7 +143,7 @@ public final class MerlinCrystalPlugin extends OptionHandler { } return true; } else { - DropItemHandler.drop(player, node.asItem()); + DropListener.drop(player, node.asItem()); } return true; case 40026: diff --git a/Server/src/main/content/region/morytania/handlers/MortMyreGhastNPC.kt b/Server/src/main/content/region/morytania/handlers/MortMyreGhastNPC.kt index 88a891810..69f2b029a 100644 --- a/Server/src/main/content/region/morytania/handlers/MortMyreGhastNPC.kt +++ b/Server/src/main/content/region/morytania/handlers/MortMyreGhastNPC.kt @@ -67,7 +67,7 @@ class MortMyreGhastNPC : AbstractNPC { for(i in player.inventory.toArray()){ if(i == null) continue val consumable = Consumables.getConsumableById(i.id) - if(consumable != null && consumable is Food) { + if(consumable != null && consumable.consumable is Food) { hasFood = true removeItem(player, i, Container.INVENTORY) addItem(player, Items.ROTTEN_FOOD_2959) diff --git a/Server/src/main/content/region/wilderness/handlers/DarkEnergyCoreNPC.java b/Server/src/main/content/region/wilderness/handlers/DarkEnergyCoreNPC.java index 3b35a1e36..2e8c5fbf8 100644 --- a/Server/src/main/content/region/wilderness/handlers/DarkEnergyCoreNPC.java +++ b/Server/src/main/content/region/wilderness/handlers/DarkEnergyCoreNPC.java @@ -13,6 +13,8 @@ import core.game.world.map.Location; import core.plugin.Initializable; import core.tools.RandomFunction; +import static core.api.ContentAPIKt.isStunned; + /** * Handles the Dark Energy Core NPC. * @author Emperor @@ -70,7 +72,7 @@ public final class DarkEnergyCoreNPC extends AbstractNPC { public void handleTickActions() { ticks++; boolean poisoned = getStateManager().hasState(EntityState.POISONED); - if (getStateManager().hasState(EntityState.STUNNED) || isInvisible()) { + if (isStunned(this) || isInvisible()) { return; } if (fails == 0 && poisoned && (ticks % 100) != 0) { diff --git a/Server/src/main/core/api/ContentAPI.kt b/Server/src/main/core/api/ContentAPI.kt index ba8b61c15..ab3b94db5 100644 --- a/Server/src/main/core/api/ContentAPI.kt +++ b/Server/src/main/core/api/ContentAPI.kt @@ -53,6 +53,9 @@ import core.game.interaction.InteractionListeners import content.global.handlers.iface.ge.StockMarket import content.global.skill.slayer.SlayerManager import core.game.activity.Cutscene +import core.game.interaction.Clocks +import core.game.interaction.QueueStrength +import core.game.interaction.QueuedScript import core.game.node.entity.player.info.LogType import core.game.node.entity.player.info.PlayerMonitor import core.tools.SystemLogger @@ -60,7 +63,9 @@ import core.game.system.config.ItemConfigParser import core.game.system.config.ServerConfigParser import core.game.world.GameWorld import core.game.world.GameWorld.Pulser +import core.game.world.map.path.ProjectilePathfinder import core.game.world.repository.Repository +import core.tools.tick import kotlin.math.absoluteValue /** @@ -2261,6 +2266,97 @@ fun addDialogueAction(player: Player, action: core.game.dialogue.DialogueAction) player.dialogueInterpreter.addAction(action) } +/** + * Used by content handlers to check if the entity is done moving yet + */ +fun finishedMoving(entity: Entity) : Boolean { + return entity.clocks[Clocks.MOVEMENT] < GameWorld.ticks +} + + +/** + * Delay the execution of the currently running script + */ +fun delayScript(entity: Entity, ticks: Int): Boolean { + entity.scripts.getActiveScript()?.let { it.nextExecution = GameWorld.ticks + ticks } + return false +} + +/** + * Set the global delay for the entity, pausing execution of all queues/scripts until passed. + */ +fun delayEntity(entity: Entity, ticks: Int) { + entity.scripts.delay = GameWorld.ticks + ticks + lock(entity, 5) //TODO: REMOVE WHEN EVERYTHING IMPORTANT USES PROPER QUEUES - THIS IS INCORRECT BEHAVIOR +} + +fun apRange(entity: Entity, apRange: Int) { + entity.scripts.apRange = apRange + entity.scripts.apRangeCalled = true +} + +fun hasLineOfSight(entity: Entity, target: Node) : Boolean { + return ProjectilePathfinder.find(entity, target).isSuccessful +} + +fun animationFinished(entity: Entity) : Boolean { + return entity.clocks[Clocks.ANIMATION_END] < GameWorld.ticks +} + +fun clearScripts(entity: Entity) : Boolean { + entity.scripts.reset() + return true +} + +fun restartScript(entity: Entity) : Boolean { + if (entity.scripts.getActiveScript()?.persist != true) { + SystemLogger.logErr(entity.scripts.getActiveScript()!!::class.java, "Tried to call restartScript on a non-persistent script! Either use stopExecuting() or make the script persistent.") + return clearScripts(entity) + } + return true +} + +fun keepRunning(entity: Entity) : Boolean { + entity.scripts.getActiveScript()?.nextExecution = getWorldTicks() + 1 + return false +} + +fun stopExecuting(entity: Entity) : Boolean { + if (entity.scripts.getActiveScript()?.persist == true) { + SystemLogger.logErr(entity.scripts.getActiveScript()!!::class.java, "Tried to call stopExecuting() on a persistent script! To halt execution of a persistent script, you MUST call clearScripts()!") + return clearScripts(entity) + } + return true +} + +fun queueScript(entity: Entity, delay: Int = 1, strength: QueueStrength = QueueStrength.WEAK, persist: Boolean = false, script: (stage: Int) -> Boolean) { + val s = QueuedScript(script, strength, persist) + s.nextExecution = getWorldTicks() + delay + entity.scripts.addToQueue(s, strength) +} + +fun delayAttack(entity: Entity, ticks: Int) { + entity.properties.combatPulse.delayNextAttack(3) + entity.clocks[Clocks.NEXT_ATTACK] = getWorldTicks() + ticks +} + +fun stun(entity: Entity, ticks: Int) { + entity.walkingQueue.reset() + entity.pulseManager.clear() + entity.locks.lockMovement(ticks) + entity.clocks[Clocks.STUN] = getWorldTicks() + ticks + entity.graphics(Graphics(80, 96)) + if (entity is Player) { + entity.audioManager.send(Audio(2727, 1, 0)) + entity.animate(Animation(424, Animator.Priority.VERY_HIGH)) + sendMessage(entity, "You have been stunned!") + } +} + +fun isStunned(entity: Entity) : Boolean { + return entity.clocks[Clocks.STUN] >= getWorldTicks() +} + /** * Modifies prayer points by value * @param player the player to modify prayer points diff --git a/Server/src/main/core/cache/def/impl/ItemDefinition.java b/Server/src/main/core/cache/def/impl/ItemDefinition.java index 36fddfdb6..877fbbce4 100644 --- a/Server/src/main/core/cache/def/impl/ItemDefinition.java +++ b/Server/src/main/core/cache/def/impl/ItemDefinition.java @@ -5,16 +5,13 @@ import core.cache.Cache; import core.cache.def.Definition; import core.cache.misc.buffer.ByteBufferUtils; import core.game.container.Container; -import core.game.global.action.DropItemHandler; import core.game.interaction.OptionHandler; -import core.game.node.Node; import core.game.node.entity.player.Player; import core.game.node.entity.skill.Skills; import core.game.node.item.Item; import core.game.node.item.ItemPlugin; import core.net.packet.PacketRepository; import core.net.packet.out.WeightUpdate; -import core.plugin.Plugin; import core.tools.StringUtils; import core.tools.SystemLogger; import core.game.system.config.ItemConfigParser; @@ -259,32 +256,6 @@ public class ItemDefinition extends Definition { options = new String[] { null, null, null, null, "drop" }; } - /** - * Initialize the default option handlers. - */ - static { - // TODO: Move this crap in a plugin. - OptionHandler handler = new OptionHandler() { - @Override - public Plugin newInstance(Object arg) throws Throwable { - return this; - } - - @Override - public boolean handle(final Player player, Node node, String option) { - return DropItemHandler.handle(player, node, option); - } - - @Override - public boolean isWalk() { - return false; - } - }; - setOptionHandler("destroy", handler); - setOptionHandler("dissolve", handler); - setOptionHandler("drop", handler); - } - /** * Parses the item definitions. */ diff --git a/Server/src/main/core/game/bots/CombatBot.kt b/Server/src/main/core/game/bots/CombatBot.kt index bf2c35bdd..a5b5c6780 100644 --- a/Server/src/main/core/game/bots/CombatBot.kt +++ b/Server/src/main/core/game/bots/CombatBot.kt @@ -43,7 +43,7 @@ class CombatBot(location: Location) : AIPlayer(location) { this.lock(3) //this.animate(new Animation(829)); val food = inventory.getItem(foodItem) - var consumable: Consumable? = Consumables.getConsumableById(food.id) + var consumable: Consumable? = Consumables.getConsumableById(food.id)?.consumable if (consumable == null) { consumable = Food(IntArray(food.id), HealingEffect(1)) } diff --git a/Server/src/main/core/game/bots/PvMBots.java b/Server/src/main/core/game/bots/PvMBots.java index 7f7a87f47..6aef6e01e 100644 --- a/Server/src/main/core/game/bots/PvMBots.java +++ b/Server/src/main/core/game/bots/PvMBots.java @@ -133,10 +133,10 @@ public class PvMBots extends AIPlayer { //this.animate(new Animation(829)); Item food = this.getInventory().getItem(foodItem); - Consumable consumable = Consumables.getConsumableById(food.getId()); + Consumable consumable = Consumables.getConsumableById(food.getId()).getConsumable(); if (consumable == null) { - consumable = new Food(new int[] {food.getId()}, new HealingEffect(1)); + return; } consumable.consume(food, this); diff --git a/Server/src/main/core/game/bots/ScriptAPI.kt b/Server/src/main/core/game/bots/ScriptAPI.kt index 3a70ef9f7..5cd9979a9 100644 --- a/Server/src/main/core/game/bots/ScriptAPI.kt +++ b/Server/src/main/core/game/bots/ScriptAPI.kt @@ -653,7 +653,7 @@ class ScriptAPI(private val bot: Player) { bot.lock(3) //this.animate(new Animation(829)); val food = bot.inventory.getItem(foodItem) - var consumable: Consumable? = Consumables.getConsumableById(foodId) + var consumable: Consumable? = Consumables.getConsumableById(foodId)?.consumable if (consumable == null) { consumable = Food(intArrayOf(food.id), HealingEffect(1)) } @@ -673,7 +673,7 @@ class ScriptAPI(private val bot: Player) { bot.lock(3) //this.animate(new Animation(829)); val food = bot.inventory.getItem(foodItem) - var consumable: Consumable? = Consumables.getConsumableById(foodId) + var consumable: Consumable? = Consumables.getConsumableById(foodId)?.consumable if (consumable == null) { consumable = Food(intArrayOf(foodId), HealingEffect(1)) } diff --git a/Server/src/main/core/game/consumable/Cake.java b/Server/src/main/core/game/consumable/Cake.java index 5f8bedc55..bd7286240 100644 --- a/Server/src/main/core/game/consumable/Cake.java +++ b/Server/src/main/core/game/consumable/Cake.java @@ -23,7 +23,7 @@ public class Cake extends Food { player.getInventory().remove(item); } final int initialLifePoints = player.getSkills().getLifepoints(); - Consumables.getConsumableById(item.getId()).effect.activate(player); + Consumables.getConsumableById(item.getId()).getConsumable().effect.activate(player); sendMessages(player, initialLifePoints, item, messages); } diff --git a/Server/src/main/core/game/consumable/Consumable.java b/Server/src/main/core/game/consumable/Consumable.java index 970ddb1c7..0d50dc3cd 100644 --- a/Server/src/main/core/game/consumable/Consumable.java +++ b/Server/src/main/core/game/consumable/Consumable.java @@ -11,7 +11,7 @@ import core.plugin.Plugin; /** * Represents any item that has a consumption option such as 'Eat' or 'Drink'. */ -public abstract class Consumable implements Plugin { +public abstract class Consumable { /** * Represents the item IDs of all the variants of a consumable where the last one is often the empty container, if it has any. @@ -58,7 +58,7 @@ public abstract class Consumable implements Plugin { addItem(player, nextItemId, 1, Container.INVENTORY); } final int initialLifePoints = player.getSkills().getLifepoints(); - Consumables.getConsumableById(item.getId()).effect.activate(player); + Consumables.getConsumableById(item.getId()).getConsumable().effect.activate(player); sendMessages(player, initialLifePoints, item, messages); } @@ -100,17 +100,6 @@ public abstract class Consumable implements Plugin { return item.getName().replace("(4)", "").replace("(3)", "").replace("(2)", "").replace("(1)", "").trim().toLowerCase(); } - @Override - public Plugin newInstance(Object arg) throws Throwable { - Consumables.add(this); - return this; - } - - @Override - public Object fireEvent(String identifier, Object... args) { - return null; - } - public int getHealthEffectValue(Player player) { return effect.getHealthEffectValue(player); } diff --git a/Server/src/main/core/game/consumable/Potion.java b/Server/src/main/core/game/consumable/Potion.java index e5b3cdc61..a9f9506ad 100644 --- a/Server/src/main/core/game/consumable/Potion.java +++ b/Server/src/main/core/game/consumable/Potion.java @@ -28,7 +28,7 @@ public class Potion extends Drink { } final int initialLifePoints = player.getSkills().getLifepoints(); - Consumables.getConsumableById(item.getId()).effect.activate(player); + Consumables.getConsumableById(item.getId()).getConsumable().effect.activate(player); if (messages.length == 0) { sendDefaultMessages(player, item); } else { diff --git a/Server/src/main/core/game/global/action/DropItemHandler.java b/Server/src/main/core/game/global/action/DropItemHandler.java deleted file mode 100644 index 64d29a7f4..000000000 --- a/Server/src/main/core/game/global/action/DropItemHandler.java +++ /dev/null @@ -1,75 +0,0 @@ -package core.game.global.action; - -import core.game.node.Node; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.info.login.PlayerParser; -import core.game.node.entity.player.link.audio.Audio; -import core.game.node.item.GroundItemManager; -import core.game.node.item.Item; -import core.game.node.entity.combat.graves.GraveController; -import core.tools.SystemLogger; -import core.game.system.config.ItemConfigParser; -import core.game.world.GameWorld; - -/** - * Handles the dropping of an item. - * @author Vexia - */ -public final class DropItemHandler { - - /** - * Handles the droping of an item. - * @param player the player. - * @param node the node. - * @param option the option. - * @return {@code True} if so. - */ - public static boolean handle(final Player player, Node node, String option) { - Item item = (Item) node; - if (item.getSlot() == -1) { - player.getPacketDispatch().sendMessage("Invalid slot!"); - return false; - } - switch (option) { - case "drop": - case "destroy": - case "dissolve": - if (!player.getInterfaceManager().close()) { - return true; - } - player.getDialogueInterpreter().close(); - player.getPulseManager().clear(); - if (option.equalsIgnoreCase("destroy") || option.equalsIgnoreCase("dissolve") || (boolean) item.getDefinition().getHandlers().getOrDefault(ItemConfigParser.DESTROY,false)) { - player.getDialogueInterpreter().open(9878, item); - return true; - } - if (GraveController.hasGraveAt(player.getLocation())) { - player.sendMessage("You cannot drop items on top of graves!"); - return false; - } - if (player.getAttribute("equipLock:" + item.getId(), 0) > GameWorld.getTicks()) { - SystemLogger.logAlert(DropItemHandler.class, player + ", tried to do the drop & equip dupe."); - return true; - } - if (player.getInventory().replace(null, item.getSlot()) == item) { - item = item.getDropItem(); - player.getAudioManager().send(new Audio(item.getId() == 995 ? 10 : 2739, 1, 0));//2739 ACTUAL DROP SOUND - GroundItemManager.create(item, player.getLocation(), player); - PlayerParser.save(player); - } - player.setAttribute("droppedItem:" + item.getId(), GameWorld.getTicks() + 2); - return true; - } - return false; - } - - /** - * Drops an item. - * @param player the player. - * @param item the item. - * @return - */ - public static boolean drop(Player player, Item item) { - return handle(player, item, item.getDefinition().hasDestroyAction() ? "destroy" : "drop"); - } -} diff --git a/Server/src/main/core/game/global/action/DropListener.kt b/Server/src/main/core/game/global/action/DropListener.kt new file mode 100644 index 000000000..60d7f9029 --- /dev/null +++ b/Server/src/main/core/game/global/action/DropListener.kt @@ -0,0 +1,51 @@ +package core.game.global.action + +import core.api.* +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.interaction.QueueStrength +import core.game.node.Node +import core.game.node.entity.combat.graves.GraveController +import core.game.node.entity.player.Player +import core.game.node.entity.player.info.login.PlayerParser +import core.game.node.entity.player.link.audio.Audio +import core.game.node.item.GroundItemManager +import core.game.node.item.Item +import core.game.system.config.ItemConfigParser + +class DropListener : InteractionListener { + override fun defineListeners() { + on(IntType.ITEM, "drop", "destroy", "dissolve", handler = ::handleDropAction) + } + + companion object { + @JvmStatic fun drop(player: Player, item: Item) : Boolean { + return handleDropAction(player, item) + } + private fun handleDropAction(player: Player, node: Node) : Boolean { + val option = getUsedOption(player) + var item = node as? Item ?: return false + if (option == "drop") { + if (GraveController.hasGraveAt(player.location)) { + sendMessage(player, "You cannot drop items on top of graves!") + return false + } + if (getAttribute(player, "equipLock:${node.id}", 0 ) > getWorldTicks()) + return false + + queueScript (player, strength = QueueStrength.SOFT) { + if (player.inventory.replace(null, item.slot) != item) return@queueScript stopExecuting(player) + item = item.dropItem + player.audioManager.send(Audio(if (item.id == 995) 10 else 2739, 1, 0)) + GroundItemManager.create(item, player.location, player) + setAttribute(player, "droppedItem:${item.id}", getWorldTicks() + 2) + PlayerParser.save(player) + return@queueScript stopExecuting(player) + } + } else if (option == "destroy" || option == "dissolve" || item.definition.handlers.getOrDefault(ItemConfigParser.DESTROY, false) as Boolean) { + player.dialogueInterpreter.open(9878, item) + } + return true + } + } +} \ No newline at end of file diff --git a/Server/src/main/core/game/interaction/Clocks.kt b/Server/src/main/core/game/interaction/Clocks.kt new file mode 100644 index 000000000..712c8adba --- /dev/null +++ b/Server/src/main/core/game/interaction/Clocks.kt @@ -0,0 +1,11 @@ +package core.game.interaction + +object Clocks { + @JvmStatic val MOVEMENT = 0 + @JvmStatic val ANIMATION_END = 1 + @JvmStatic val NEXT_EAT = 2 + @JvmStatic val NEXT_CONSUME = 3 + @JvmStatic val NEXT_DRINK = 4 + @JvmStatic val NEXT_ATTACK = 5 + @JvmStatic val STUN = 6 +} \ No newline at end of file diff --git a/Server/src/main/core/game/interaction/Interaction.java b/Server/src/main/core/game/interaction/InteractPlugin.java similarity index 99% rename from Server/src/main/core/game/interaction/Interaction.java rename to Server/src/main/core/game/interaction/InteractPlugin.java index c96de2c10..7092ea372 100644 --- a/Server/src/main/core/game/interaction/Interaction.java +++ b/Server/src/main/core/game/interaction/InteractPlugin.java @@ -20,7 +20,7 @@ import core.game.world.GameWorld; * Handles interaction between nodes. * @author Emperor */ -public class Interaction { +public class InteractPlugin { /** * The current options. @@ -41,7 +41,7 @@ public class Interaction { * Constructs a new {@code Interaction} {@code Object}. * @param node The node reference. */ - public Interaction(Node node) { + public InteractPlugin(Node node) { this.node = node; } diff --git a/Server/src/main/core/game/interaction/InteractionListener.kt b/Server/src/main/core/game/interaction/InteractionListener.kt index ef7963650..fff396633 100644 --- a/Server/src/main/core/game/interaction/InteractionListener.kt +++ b/Server/src/main/core/game/interaction/InteractionListener.kt @@ -23,7 +23,7 @@ interface InteractionListener : ContentInterface{ fun on(ids: IntArray, type: IntType, vararg option: String, handler: (player: Player, node: Node) -> Boolean){ InteractionListeners.add(ids, type.ordinal, option, handler) } - fun on(option: String, type: IntType, handler: (player: Player, node: Node) -> Boolean){ + @Deprecated("Don't use") fun on(option: String, type: IntType, handler: (player: Player, node: Node) -> Boolean){ InteractionListeners.add(option, type.ordinal, handler) } fun on(type: IntType, vararg option: String, handler: (player: Player, node: Node) -> Boolean){ @@ -82,5 +82,16 @@ interface InteractionListener : ContentInterface{ InteractionListeners.instantClasses.add(name) } + fun defineInteraction(type: IntType, ids: IntArray, vararg options: String, persistent: Boolean = false, allowedDistance: Int = 1, handler: (player: Player, node: Node, state: Int) -> Boolean) { + InteractionListeners.addMetadata(ids, type, options, InteractionMetadata(handler, allowedDistance, persistent)) + } + + fun defineInteraction(type: IntType, vararg options: String, persist: Boolean = false, allowedDistance: Int = 1, handler: (player: Player, node: Node, state: Int) -> Boolean) { + InteractionListeners.addGenericMetadata(options, type, InteractionMetadata(handler, allowedDistance, persist)) + } + + data class InteractionMetadata(val handler: (player: Player, node: Node, state: Int) -> Boolean, val distance: Int, val persist: Boolean) + data class UseWithMetadata(val handler: (player: Player, used: Node, with: Node, state: Int) -> Boolean, val distance: Int, val persist: Boolean) + fun defineListeners() } diff --git a/Server/src/main/core/game/interaction/InteractionListeners.kt b/Server/src/main/core/game/interaction/InteractionListeners.kt index 0d65d40c5..c85e0a61b 100644 --- a/Server/src/main/core/game/interaction/InteractionListeners.kt +++ b/Server/src/main/core/game/interaction/InteractionListeners.kt @@ -1,5 +1,7 @@ package core.game.interaction +import core.api.forceWalk +import core.api.queueScript import core.game.event.InteractionEvent import core.game.event.UseWithEvent import core.game.node.Node @@ -14,6 +16,8 @@ object InteractionListeners { private val useWithWildcardListeners = HashMap Boolean, (Player, Node, Node) -> Boolean>>>(10) private val destinationOverrides = HashMap Location>(100) private val equipListeners = HashMap Boolean>(10) + private val interactions = HashMap() + private val useWithInteractions = HashMap() val instantClasses = HashSet() @JvmStatic @@ -232,10 +236,25 @@ object InteractionListeners { if(player.locks.isInteractionLocked) return false - val method = get(id,type.ordinal,option) ?: get(option,type.ordinal) ?: return false + val method = get(id,type.ordinal,option) ?: get(option,type.ordinal) + + player.setAttribute("interact:option", option.lowercase()) + player.dispatch(InteractionEvent(node, option.toLowerCase())) + + if (method == null) { + val inter = interactions["${type.ordinal}:$id:${option.lowercase()}"] ?: interactions["${type.ordinal}:${option.lowercase()}"] ?: return false + val script = Interaction(inter.handler, inter.distance, inter.persist) + player.scripts.setInteractionScript(node, script) + player.pulseManager.run(object : MovementPulse(player, node, flag) { + override fun pulse(): Boolean { + return true + } + }) + return true + } + val destOverride = getOverride(type.ordinal, id, option) ?: getOverride(type.ordinal,node.id) ?: getOverride(type.ordinal,option.toLowerCase()) - player.setAttribute("interact:option", option) if(option.toLowerCase() == "attack") //Attack needs special handling >.> { @@ -250,14 +269,12 @@ object InteractionListeners { override fun pulse(): Boolean { if(player.zoneMonitor.interact(node, Option(option, 0))) return true player.faceLocation(node.location) - player.dispatch(InteractionEvent(node, option.toLowerCase())) method.invoke(player,node) return true } }) } else { method.invoke(player,node) - player.dispatch(InteractionEvent(node, option.toLowerCase())) } return true } @@ -285,4 +302,29 @@ object InteractionListeners { val className = handler.javaClass.name.substringBefore("$") return instantClasses.contains(className) } + + fun addMetadata (ids: IntArray, type: IntType, options: Array, metadata: InteractionListener.InteractionMetadata) { + for (id in ids) + for (opt in options) + interactions["${type.ordinal}:$id:${opt.lowercase()}"] = metadata + } + + fun addMetadata (id: Int, type: IntType, options: Array, metadata: InteractionListener.InteractionMetadata) { + for (opt in options) + interactions["${type.ordinal}:$id:${opt.lowercase()}"] = metadata + } + + fun addGenericMetadata (options: Array, type: IntType, metadata: InteractionListener.InteractionMetadata) { + for (opt in options) + interactions["${type.ordinal}:$opt"] = metadata + } + + fun addMetadata (used: Int, with: IntArray, type: IntType, metadata: InteractionListener.UseWithMetadata) { + for (id in with) + useWithInteractions["${type.ordinal}:$used:$with"] = metadata + } + + fun addMetadata (used: Int, with: Int, type: IntType, metadata: InteractionListener.UseWithMetadata) { + useWithInteractions["${type.ordinal}:$used:$with"] = metadata + } } diff --git a/Server/src/main/core/game/interaction/Script.kt b/Server/src/main/core/game/interaction/Script.kt new file mode 100644 index 000000000..d266c1e34 --- /dev/null +++ b/Server/src/main/core/game/interaction/Script.kt @@ -0,0 +1,27 @@ +package core.game.interaction + +import core.game.node.Node +import core.game.node.entity.Entity +import core.game.node.entity.player.Player +import core.game.world.GameWorld + +typealias UseWithExecutor = (Player, Node, Node, Int) -> Boolean +typealias InteractExecutor = (Player, Node, Int) -> Boolean +typealias VoidExecutor = (Int) -> Boolean + +enum class QueueStrength { + WEAK, + NORMAL, + STRONG, + SOFT +} + +open class Script (val execution: T, val persist: Boolean) { + var state: Int = 0 + var nextExecution = 0 +} + +class Interaction(execution: InteractExecutor, val distance: Int, persist: Boolean) : Script(execution, persist) +class UseWithInteraction(execution: UseWithExecutor, val distance: Int, persist: Boolean, val used: Node, val with: Node) : Script(execution, persist) +class QueuedScript(executor: VoidExecutor, val strength: QueueStrength, persist: Boolean) : Script(executor, persist) +class QueuedUseWith(executor: UseWithExecutor, val strength: QueueStrength, persist: Boolean, val used: Node, val with: Node) : Script(executor, persist) \ No newline at end of file diff --git a/Server/src/main/core/game/interaction/ScriptProcessor.kt b/Server/src/main/core/game/interaction/ScriptProcessor.kt new file mode 100644 index 000000000..ede813aa1 --- /dev/null +++ b/Server/src/main/core/game/interaction/ScriptProcessor.kt @@ -0,0 +1,295 @@ +package core.game.interaction + +import core.api.* +import core.game.node.Node +import core.game.node.entity.Entity +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player +import core.game.node.item.GroundItem +import core.game.node.scenery.Scenery +import core.game.world.GameWorld +import core.game.world.map.Location +import core.game.world.map.path.Pathfinder +import core.tools.SystemLogger +import java.lang.Integer.max + +class ScriptProcessor(val entity: Entity) { + private var apScript: Script<*>? = null + private var opScript: Script<*>? = null + private var interactTarget: Node? = null + private var currentScript: Script<*>? = null + private val queue = ArrayList>() + + var delay = 0 + var interacted = false + var apRangeCalled = false + var apRange = 10 + var persistent = false + var targetDestination: Location? = null + + fun preMovement() { + var allSkipped = false + while (!allSkipped) { + allSkipped = processQueue() + } + + if (isStunned(entity)) return + if (entity.delayed()) return + + var canProcess = !entity.delayed() + if (entity is Player) + canProcess = canProcess && !entity.interfaceManager.isOpened && !entity.interfaceManager.hasChatbox() + + if (entity !is Player) return + if (!entity.delayed() && canProcess && interactTarget != null) { + if (opScript != null && inOperableDistance()) { + face(entity, interactTarget?.centerLocation ?: return) + processInteractScript(opScript ?: return) + } + else if (apScript != null && inApproachDistance(apScript ?: return)) { + face(entity, interactTarget?.centerLocation ?: return) + processInteractScript(apScript ?: return) + } + else if (apScript == null && opScript == null && inOperableDistance()) { + sendMessage(entity, "Nothing interesting happens.") + } + } + } + + fun postMovement(didMove: Boolean) { + if (didMove) + entity.clocks[Clocks.MOVEMENT] = GameWorld.ticks + 1 + var canProcess = !entity.delayed() + if (entity is Player) + canProcess = canProcess && !entity.interfaceManager.isOpened && !entity.interfaceManager.hasChatbox() + + if (entity !is Player) return + if (!entity.delayed() && canProcess && interactTarget != null && !interacted) { + if (opScript != null && inOperableDistance()) { + face(entity, interactTarget?.centerLocation ?: return) + processInteractScript(opScript ?: return) + } + else if (apScript != null && inApproachDistance(apScript ?: return)) { + face(entity, interactTarget?.centerLocation ?: return) + processInteractScript(apScript ?: return) + } + else if (apScript == null && opScript == null && inOperableDistance()) { + sendMessage(entity, "Nothing interesting happens.") + } + } + if (canProcess && (apScript != null || opScript != null)) { + if (!interacted && !didMove) { + sendMessage(entity, "I can't reach that!") + reset() + } + } + if (interacted && !apRangeCalled && !persistent) reset() + if (interactTarget != null && interactTarget?.isActive != true) reset() + } + + fun processQueue() : Boolean { + var strongInQueue = false + var softInQueue = false + var anyExecuted = false + for (i in 0 until queue.size) { + val script = queue[i] + if (script is QueuedScript && script.strength == QueueStrength.STRONG) + strongInQueue = true + if (script is QueuedUseWith && script.strength == QueueStrength.STRONG) + strongInQueue = true + if (script is QueuedScript && script.strength == QueueStrength.SOFT) + softInQueue = true + if (script is QueuedUseWith && script.strength == QueueStrength.SOFT) + softInQueue = true + } + + if (softInQueue) { + removeWeakScripts() + removeNormalScripts() + if (entity is Player) { + entity.interfaceManager.close() + entity.interfaceManager.closeChatbox() + entity.dialogueInterpreter.close() + } + } + + if (strongInQueue) { + removeWeakScripts() + if (entity is Player) { + entity.interfaceManager.close() + entity.interfaceManager.closeChatbox() + entity.dialogueInterpreter.close() + } + } + + val toRemove = ArrayList>() + + for (i in 0 until queue.size) { + when (val script = queue[i]) { + is QueuedScript -> { + if (entity.delayed() && script.strength != QueueStrength.SOFT) + continue + if (script.nextExecution > GameWorld.ticks) + continue + if ((script.strength == QueueStrength.STRONG || script.strength == QueueStrength.SOFT) && entity is Player) { + entity.interfaceManager.close() + entity.interfaceManager.closeChatbox() + entity.dialogueInterpreter.close() + } + script.nextExecution = GameWorld.ticks + 1 + val finished = executeScript(script) + script.state++ + if (finished && !script.persist) + toRemove.add(script) + else if (finished) + script.state = 0 + anyExecuted = true + } + is QueuedUseWith -> { + if (entity.delayed() && script.strength != QueueStrength.SOFT) + continue + if (entity !is Player) { + toRemove.add(script) + SystemLogger.logErr(this::class.java, "Tried to queue an item UseWith interaction for a non-player!") + continue + } + if (script.nextExecution > GameWorld.ticks) + continue + if ((script.strength == QueueStrength.STRONG || script.strength == QueueStrength.SOFT)) { + entity.interfaceManager.close() + entity.interfaceManager.closeChatbox() + entity.dialogueInterpreter.close() + } + script.nextExecution = GameWorld.ticks + 1 + val finished = executeScript(script) + script.state++ + if (finished && !script.persist) + toRemove.add(script) + else if (finished) + script.state = 0 + anyExecuted = true + } + } + } + + queue.removeAll(toRemove.toSet()) + return !anyExecuted + } + + fun isPersist (script: Script<*>) : Boolean { + return script.persist + } + + fun processInteractScript(script: Script<*>) { + if (script.nextExecution < GameWorld.ticks) { + val finished = executeScript(script) + script.state++ + if (finished && isPersist(script)) + script.state = 0 + interacted = true + } + } + + fun executeScript(script: Script<*>) : Boolean { + currentScript = script + when (script) { + is Interaction -> return script.execution.invoke(entity as? Player ?: return true, interactTarget ?: return true, script.state) + is UseWithInteraction -> return script.execution.invoke(entity as? Player ?: return true, script.used, script.with, script.state) + is QueuedScript -> return script.execution.invoke(script.state) + is QueuedUseWith -> return script.execution.invoke(entity as? Player ?: return true, script.used, script.with, script.state) + } + currentScript = null + return true + } + + fun removeWeakScripts() { + queue.removeAll(queue.filter { it is QueuedScript && it.strength == QueueStrength.WEAK || it is QueuedUseWith && it.strength == QueueStrength.WEAK }.toSet()) + } + + fun removeNormalScripts() { + queue.removeAll(queue.filter { it is QueuedScript && it.strength == QueueStrength.NORMAL || it is QueuedUseWith && it.strength == QueueStrength.NORMAL }.toSet()) + } + + fun inApproachDistance(script: Script<*>) : Boolean { + val distance = when (script) { + is Interaction -> script.distance + is UseWithInteraction -> script.distance + else -> 10 + } + targetDestination?.let { + return it.location.getDistance(entity.location) <= distance && hasLineOfSight(entity, it) + } + return false + } + + fun inOperableDistance() : Boolean { + targetDestination?.let { + return it.cardinalTiles.any {loc -> loc == entity.location} && hasLineOfSight(entity, it) + } + return false + } + + fun reset() { + apScript = null + opScript = null + apRangeCalled = false + interacted = false + apRange = 10 + interactTarget = null + persistent = false + targetDestination = null + resetAnimator(entity as? Player ?: return) + } + + fun setInteractionScript(target: Node, script: Script<*>?) { + reset() + interactTarget = target + if (script != null) { + apRange = when(script) { + is Interaction -> script.distance + is UseWithInteraction -> script.distance + else -> 10 + } + persistent = script.persist + if (apRange == -1) + opScript = script + else + apScript = script + targetDestination = when (interactTarget) { + is NPC -> DestinationFlag.ENTITY.getDestination(entity, interactTarget) + is Scenery -> { + val path = Pathfinder.find(entity, interactTarget).points.lastOrNull() + if (path == null) { + clearScripts(entity) + return + } + Location.create(path.x, path.y, entity.location.z) + } + is GroundItem -> DestinationFlag.ITEM.getDestination(entity, interactTarget) + else -> target.location + } + } + } + + fun addToQueue(script: Script<*>, strength: QueueStrength) { + if (script !is QueuedScript && script !is QueuedUseWith) { + SystemLogger.logErr(this::class.java, "Tried to queue ${script::class.java.simpleName} as a queueable script but it's not!") + return + } + if (strength == QueueStrength.STRONG && entity is Player) { + entity.interfaceManager.close() + entity.interfaceManager.closeChatbox() + entity.dialogueInterpreter.close() + } + script.nextExecution = max(GameWorld.ticks + 1, script.nextExecution) + queue.add(script) + } + + fun getActiveScript() : Script<*>? { + return currentScript ?: getActiveInteraction() + } + + private fun getActiveInteraction() : Script<*>? { + return opScript ?: apScript + } +} \ No newline at end of file diff --git a/Server/src/main/core/game/node/Node.java b/Server/src/main/core/game/node/Node.java index 493ec9239..4eb3676d1 100644 --- a/Server/src/main/core/game/node/Node.java +++ b/Server/src/main/core/game/node/Node.java @@ -1,7 +1,7 @@ package core.game.node; import core.game.interaction.DestinationFlag; -import core.game.interaction.Interaction; +import core.game.interaction.InteractPlugin; import core.game.node.entity.npc.NPC; import core.game.node.entity.player.Player; import core.game.node.item.Item; @@ -49,7 +49,7 @@ public abstract class Node { /** * The interaction instance. */ - protected Interaction interaction; + protected InteractPlugin interactPlugin; /** * The destination flag. @@ -220,19 +220,19 @@ public abstract class Node { * Gets the interaction. * @return The interaction. */ - public Interaction getInteraction() { - if (interaction != null && !interaction.isInitialized()) { - interaction.setDefault(); + public InteractPlugin getInteraction() { + if (interactPlugin != null && !interactPlugin.isInitialized()) { + interactPlugin.setDefault(); } - return interaction; + return interactPlugin; } /** * Sets the interaction. - * @param interaction The interaction to set. + * @param interactPlugin The interaction to set. */ - public void setInteraction(Interaction interaction) { - this.interaction = interaction; + public void setInteraction(InteractPlugin interactPlugin) { + this.interactPlugin = interactPlugin; } /** diff --git a/Server/src/main/core/game/node/entity/Entity.java b/Server/src/main/core/game/node/entity/Entity.java index db9a0d417..ede85aec5 100644 --- a/Server/src/main/core/game/node/entity/Entity.java +++ b/Server/src/main/core/game/node/entity/Entity.java @@ -1,7 +1,7 @@ package core.game.node.entity; import core.game.event.*; -import core.game.interaction.DestinationFlag; +import core.game.interaction.*; import core.game.node.Node; import core.game.node.entity.combat.BattleState; import core.game.node.entity.combat.CombatStyle; @@ -9,6 +9,7 @@ import core.game.node.entity.combat.DeathTask; import core.game.node.entity.combat.ImpactHandler; import core.game.node.entity.combat.equipment.ArmourSet; import core.game.node.entity.impl.*; +import core.game.node.entity.impl.Properties; import core.game.node.entity.lock.ActionLocks; import core.game.node.entity.npc.NPC; import core.game.node.entity.player.Player; @@ -29,10 +30,9 @@ import core.game.world.update.flag.context.Graphics; import core.game.node.entity.combat.CombatSwingHandler; import core.game.world.update.UpdateMasks; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; + +import static core.api.ContentAPIKt.isStunned; /** * An entity is a movable node, such as players and NPCs. @@ -109,6 +109,8 @@ public abstract class Entity extends Node { * The reward locks. */ private final ActionLocks locks = new ActionLocks(); + public final ScriptProcessor scripts = new ScriptProcessor(this); + public final int[] clocks = new int[10]; /** @@ -122,6 +124,8 @@ public abstract class Entity extends Node { */ private boolean invisible; + + /** * Constructs a new {@code Entity} {@code Object}. * @param name The name of the entity. @@ -209,9 +213,12 @@ public abstract class Entity extends Node { * This methods gets called before the {@link #update()} method. */ public void tick() { + scripts.preMovement(); dispatch(new TickEvent(GameWorld.getTicks())); skills.pulse(); + Location old = location != null ? location.transform(0, 0, 0) : Location.create(0,0,0); walkingQueue.update(); + scripts.postMovement(!Objects.equals(location, old)); updateMasks.prepare(this); } @@ -953,4 +960,8 @@ public abstract class Entity extends Node { } return occupied; } + + public boolean delayed() { + return scripts.getDelay() > GameWorld.getTicks(); + } } diff --git a/Server/src/main/core/game/node/entity/impl/Animator.java b/Server/src/main/core/game/node/entity/impl/Animator.java index f52c8dc14..42e201f63 100644 --- a/Server/src/main/core/game/node/entity/impl/Animator.java +++ b/Server/src/main/core/game/node/entity/impl/Animator.java @@ -1,5 +1,6 @@ package core.game.node.entity.impl; +import core.game.interaction.Clocks; import core.game.node.entity.Entity; import core.game.node.entity.npc.NPC; import core.game.world.GameWorld; @@ -121,7 +122,12 @@ public final class Animator { animation.setId(-1); } this.animation = animation; - ticks = GameWorld.getTicks() + animation.getDuration(); + if (animation.getId() != -1) { + ticks = GameWorld.getTicks() + animation.getDuration(); + } else { + ticks = 0; + } + entity.clocks[Clocks.getANIMATION_END()] = ticks; entity.getUpdateMasks().register(entity instanceof NPC ? new NPCAnimation(animation) : new AnimationFlag(animation)); priority = animation.getPriority(); } @@ -151,6 +157,8 @@ public final class Animator { */ public void reset() { animate(RESET_A); + entity.clocks[Clocks.getANIMATION_END()] = 0; + ticks = 0; } /** @@ -158,7 +166,7 @@ public final class Animator { * @return {@code True} if so. */ public boolean isAnimating() { - return animation != null && ticks > GameWorld.getTicks(); + return animation != null && animation.getId() != -1 && ticks > GameWorld.getTicks(); } /** diff --git a/Server/src/main/core/game/node/entity/npc/NPC.java b/Server/src/main/core/game/node/entity/npc/NPC.java index 82d9fd4c9..25117a9b9 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -3,7 +3,7 @@ package core.game.node.entity.npc; import core.game.event.NPCKillEvent; import core.cache.def.impl.NPCDefinition; import core.game.dialogue.DialoguePlugin; -import core.game.interaction.Interaction; +import core.game.interaction.InteractPlugin; import core.game.interaction.MovementPulse; import core.game.node.entity.Entity; import core.game.node.entity.combat.BattleState; @@ -162,7 +162,7 @@ public class NPC extends Entity { this.definition = NPCDefinition.forId(id); super.size = definition.size; super.direction = direction; - super.interaction = new Interaction(this); + super.interactPlugin = new InteractPlugin(this); } /** @@ -213,14 +213,14 @@ public class NPC extends Entity { if (getViewport().getRegion().isActive()) { Repository.addRenderableNPC(this); } - interaction.setDefault(); + interactPlugin.setDefault(); configure(); setDefaultBehavior(); if (definition.childNPCIds != null) { children = new NPC[definition.childNPCIds.length]; for (int i = 0; i < children.length; i++) { NPC npc = children[i] = new NPC(definition.childNPCIds[i]); - npc.interaction.setDefault(); + npc.interactPlugin.setDefault(); npc.index = index; npc.size = size; } @@ -671,10 +671,10 @@ public class NPC extends Entity { this.definition = NPCDefinition.forId(id); super.name = definition.getName(); super.size = definition.size; - super.interaction = new Interaction(this); + super.interactPlugin = new InteractPlugin(this); initConfig(); configure(); - interaction.setDefault(); + interactPlugin.setDefault(); if (id == originalId) { getUpdateMasks().unregisterSynced(NPCSwitchId.getOrdinal()); } diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index 57a779f25..b20f86de1 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -9,7 +9,7 @@ import core.game.container.impl.BankContainer; import core.game.container.impl.EquipmentContainer; import core.game.container.impl.InventoryListener; import core.game.dialogue.DialogueInterpreter; -import core.game.interaction.Interaction; +import core.game.interaction.InteractPlugin; import core.game.node.entity.Entity; import core.game.node.entity.combat.BattleState; import core.game.node.entity.combat.CombatStyle; @@ -19,6 +19,7 @@ import content.global.handlers.item.equipment.special.ChinchompaSwingHandler; import core.game.node.entity.npc.NPC; import core.game.node.entity.player.info.*; import core.game.node.entity.player.info.login.LoginConfiguration; +import core.game.node.entity.player.info.login.PlayerParser; import core.game.node.entity.player.link.*; import core.game.node.entity.player.link.appearance.Appearance; import core.game.node.entity.player.link.audio.AudioManager; @@ -324,7 +325,7 @@ public class Player extends Entity { public Player(PlayerDetails details) { super(details.getUsername(), ServerConstants.START_LOCATION); super.active = false; - super.interaction = new Interaction(this); + super.interactPlugin = new InteractPlugin(this); this.details = details; this.direction = Direction.SOUTH; } @@ -523,6 +524,10 @@ public class Player extends Entity { PacketRepository.send(SkillLevel.class, new SkillContext(this, Skills.HITPOINTS)); getSkills().setLifepointsUpdate(false); } + if (getAttribute("flagged-for-save", false)) { + PlayerParser.saveImmediately(this); + removeAttribute("flagged-for-save"); + } } @Override diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerParser.java b/Server/src/main/core/game/node/entity/player/info/login/PlayerParser.java index bb19f1b31..a0fd37aae 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerParser.java +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerParser.java @@ -31,6 +31,10 @@ public final class PlayerParser { * @param player The player. */ public static void save(Player player) { + player.setAttribute("flagged-for-save", true); + } + + public static void saveImmediately(Player player) { new PlayerSaver(player).save(); } diff --git a/Server/src/main/core/game/node/item/GroundItem.java b/Server/src/main/core/game/node/item/GroundItem.java index 01ad07313..af9054ee8 100644 --- a/Server/src/main/core/game/node/item/GroundItem.java +++ b/Server/src/main/core/game/node/item/GroundItem.java @@ -80,7 +80,7 @@ public class GroundItem extends Item { super(item.getId(), item.getAmount(), item.getCharge()); super.location = location; super.index = -1; - super.interaction.setDefault(); + super.interactPlugin.setDefault(); this.dropper = player; this.dropperUid = player != null ? player.getDetails().getUid() : -1; this.ticks = GameWorld.getTicks(); diff --git a/Server/src/main/core/game/node/item/Item.java b/Server/src/main/core/game/node/item/Item.java index 9f0a034cc..23e45d818 100644 --- a/Server/src/main/core/game/node/item/Item.java +++ b/Server/src/main/core/game/node/item/Item.java @@ -2,7 +2,7 @@ package core.game.node.item; import core.cache.def.impl.ItemDefinition; import core.game.interaction.DestinationFlag; -import core.game.interaction.Interaction; +import core.game.interaction.InteractPlugin; import core.game.interaction.OptionHandler; import core.game.node.Node; import core.game.node.entity.combat.equipment.DegradableEquipment; @@ -34,7 +34,7 @@ public class Item extends Node{ */ public Item() { super("null", null); - super.interaction = new Interaction(this); + super.interactPlugin = new InteractPlugin(this); this.idHash = -1 << 16 | 1000; } @@ -65,7 +65,7 @@ public class Item extends Node{ super(ItemDefinition.forId(id).getName(), null); super.destinationFlag = DestinationFlag.ITEM; super.index = -1; // Item slot. - super.interaction = new Interaction(this); + super.interactPlugin = new InteractPlugin(this); this.idHash = id << 16 | charge; this.amount = amount; this.definition = ItemDefinition.forId(id); diff --git a/Server/src/main/core/game/node/scenery/Scenery.java b/Server/src/main/core/game/node/scenery/Scenery.java index 27e2e0ea9..9cf2ef8e7 100644 --- a/Server/src/main/core/game/node/scenery/Scenery.java +++ b/Server/src/main/core/game/node/scenery/Scenery.java @@ -3,7 +3,7 @@ package core.game.node.scenery; import core.cache.def.impl.VarbitDefinition; import core.cache.def.impl.SceneryDefinition; import core.game.interaction.DestinationFlag; -import core.game.interaction.Interaction; +import core.game.interaction.InteractPlugin; import core.game.node.Node; import core.game.node.entity.impl.GameAttributes; import core.game.node.entity.player.Player; @@ -145,7 +145,7 @@ public class Scenery extends Node { } super.destinationFlag = DestinationFlag.OBJECT; super.direction = Direction.get(rotation); - super.interaction = new Interaction(this); + super.interactPlugin = new InteractPlugin(this); this.rotation = rotation; this.id = id; this.location = location; diff --git a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt index 563748a1c..2c5df9a9d 100644 --- a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt @@ -2,6 +2,8 @@ package core.game.system.command.sets import content.global.activity.jobs.JobManager import content.global.skill.slayer.Master +import core.api.removeAttribute +import core.api.getItemName import core.api.sendMessage import core.cache.Cache import core.cache.def.impl.DataMap @@ -197,5 +199,14 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) { } } } + + define("itemsearch") {player, args -> + val itemName = args.copyOfRange(1, args.size).joinToString(" ").lowercase() + for (i in 0 until 15000) { + val name = getItemName(i).lowercase() + if (name.contains(itemName) || itemName.contains(name)) + notify(player, "$i: $name") + } + } } } \ No newline at end of file diff --git a/Server/src/main/core/game/world/map/Location.java b/Server/src/main/core/game/world/map/Location.java index 53c5562a7..cfeb61f6a 100644 --- a/Server/src/main/core/game/world/map/Location.java +++ b/Server/src/main/core/game/world/map/Location.java @@ -289,6 +289,16 @@ public final class Location extends Node { return locs; } + public ArrayList getCardinalTiles() { + ArrayList locs = new ArrayList<>(); + + locs.add(transform(0, 1, 0)); + locs.add(transform(0, -1, 0)); + locs.add(transform(-1, 0, 0)); + locs.add(transform(1, 0, 0)); + return locs; + } + /** * Gets a square of 3 x 3 tiles as an ArrayList */ diff --git a/Server/src/main/core/game/world/repository/DisconnectionQueue.kt b/Server/src/main/core/game/world/repository/DisconnectionQueue.kt index 4448cbd16..e5e3ebef5 100644 --- a/Server/src/main/core/game/world/repository/DisconnectionQueue.kt +++ b/Server/src/main/core/game/world/repository/DisconnectionQueue.kt @@ -191,8 +191,7 @@ class DisconnectionQueue { */ fun save(player: Player, sql: Boolean): Boolean { try { - PlayerParser.save(player) - return true + PlayerParser.saveImmediately(player) } catch (t: Throwable) { t.printStackTrace() } diff --git a/Server/src/main/core/net/packet/PacketProcessor.kt b/Server/src/main/core/net/packet/PacketProcessor.kt index 0ba9d3494..59d67c9b3 100644 --- a/Server/src/main/core/net/packet/PacketProcessor.kt +++ b/Server/src/main/core/net/packet/PacketProcessor.kt @@ -9,12 +9,6 @@ import core.cache.def.impl.NPCDefinition import core.cache.def.impl.SceneryDefinition import core.game.container.Container import core.game.container.impl.BankContainer -import core.game.interaction.PluginInteractionManager -import core.game.interaction.Interaction -import core.game.interaction.MovementPulse -import core.game.interaction.NodeUsageEvent -import core.game.interaction.Option -import core.game.interaction.UseWithHandler import core.game.node.Node import core.game.node.entity.player.Player import core.game.node.entity.player.info.Rights @@ -44,13 +38,11 @@ import core.game.ge.GrandExchange.Companion.getOfferStats import core.game.ge.GrandExchange.Companion.getRecommendedPrice import core.game.ge.GrandExchangeOffer import core.game.ge.PriceIndex -import core.game.interaction.IntType -import core.game.interaction.InteractionListeners -import core.game.interaction.InterfaceListeners import content.global.handlers.iface.ge.StockMarket import content.global.skill.magic.SpellListener import content.global.skill.magic.SpellListeners import content.global.skill.magic.SpellUtils +import core.game.interaction.* import core.game.node.entity.player.info.LogType import core.game.node.entity.player.info.PlayerMonitor import core.tools.SystemLogger @@ -78,6 +70,10 @@ object PacketProcessor { var pkt: Packet while (countThisCycle-- > 0) { pkt = queue.tryPop(Packet.NoProcess()) + if (pkt is Packet.NoProcess) { + queue.clear() + return + } try { process(pkt) } catch (e: Exception) { @@ -457,6 +453,7 @@ object PacketProcessor { player.face(null) player.faceLocation(null) + player.scripts.reset() player.pulseManager.run(object : MovementPulse(player, Location.create(x,y,player.location.z), isRunning) { override fun pulse(): Boolean { @@ -569,6 +566,7 @@ object PacketProcessor { if (node.id != nodeId) return sendClearMinimap(player) + player.scripts.reset() if (player.zoneMonitor.useWith(item, node)) return if (InteractionListeners.run(item, node, type, player)) @@ -590,13 +588,14 @@ object PacketProcessor { private fun processGroundItemAction(pkt: Packet.GroundItemAction) { val item = GroundItemManager.get(pkt.id, Location.create(pkt.x, pkt.y, pkt.player.location.z), pkt.player) val player = pkt.player + player.scripts.reset() if (item == null) { return sendClearMinimap(player) } val option = item.interaction[pkt.optIndex] if (option == null) { - Interaction.handleInvalidInteraction(player, item, Option.NULL) + InteractPlugin.handleInvalidInteraction(player, item, Option.NULL) return sendClearMinimap(player) } if (PluginInteractionManager.handle(player, item, option)) @@ -613,6 +612,7 @@ object PacketProcessor { if (pkt.otherIndex !in 1 until ServerConstants.MAX_PLAYERS) { return sendClearMinimap(player) } + player.scripts.reset() val other = Repository.players[pkt.otherIndex] if (other == null || !other.isActive) return sendClearMinimap(player) @@ -642,7 +642,7 @@ object PacketProcessor { if (scenery == null || scenery.id != objId || !scenery.isActive) { player.debug("[SCENERY INTERACT] NULL OR MISMATCH OR INACTIVE") - Interaction.handleInvalidInteraction(player, scenery, Option.NULL) + InteractPlugin.handleInvalidInteraction(player, scenery, Option.NULL) return sendClearMinimap(player) } @@ -651,7 +651,7 @@ object PacketProcessor { if (option == null) { player.debug("[SCENERY INTERACT] NULL OPTION") - Interaction.handleInvalidInteraction(player, scenery, Option.NULL) + InteractPlugin.handleInvalidInteraction(player, scenery, Option.NULL) return sendClearMinimap(player) } @@ -665,6 +665,7 @@ object PacketProcessor { } player.debug("------------------------------------------------") + player.scripts.reset() if (InteractionListeners.run(wrapperChild.id, IntType.SCENERY, option.name, player, wrapperChild)) return if (PluginInteractionManager.handle(player, wrapperChild)) @@ -681,7 +682,7 @@ object PacketProcessor { val option = wrapperChild.interaction[pkt.optIndex] if (option == null) { - Interaction.handleInvalidInteraction(pkt.player, npc, Option.NULL) + InteractPlugin.handleInvalidInteraction(pkt.player, npc, Option.NULL) return sendClearMinimap(pkt.player) } @@ -696,6 +697,7 @@ object PacketProcessor { } pkt.player.debug("---------------------------------") + pkt.player.scripts.reset() if (InteractionListeners.run(wrapperChild.id, IntType.NPC,option.name,pkt.player,npc)) return if (PluginInteractionManager.handle(pkt.player, wrapperChild, option)) @@ -714,6 +716,7 @@ object PacketProcessor { if (pkt.player.locks.isInteractionLocked) return item.interaction.handleItemOption(pkt.player, option, container) + pkt.player.scripts.reset() pkt.player.debug("[ITEM INTERACT] ID: ${item.id}, Slot: ${pkt.slot}, Opt: ${option.name}") } diff --git a/Server/src/test/kotlin/content/DeathTests.kt b/Server/src/test/kotlin/content/DeathTests.kt index 5fcca6641..58299514e 100644 --- a/Server/src/test/kotlin/content/DeathTests.kt +++ b/Server/src/test/kotlin/content/DeathTests.kt @@ -2,6 +2,8 @@ package content import TestUtils import core.api.asItem +import core.api.setAttribute +import core.game.global.action.DropListener import core.game.node.entity.player.info.Rights import core.game.node.entity.player.link.IronmanMode import core.game.world.map.Location @@ -301,7 +303,8 @@ class DeathTests { Assertions.assertNotNull(g) Assertions.assertEquals(p.location, g?.location) - val canDrop = core.game.global.action.DropItemHandler.drop(p, p.inventory[0]) + setAttribute(p, "interact:option", "drop") + val canDrop = DropListener.drop(p, p.inventory[0]) Assertions.assertEquals(false, canDrop) } } \ No newline at end of file diff --git a/Server/src/test/kotlin/core/PathfinderTests.kt b/Server/src/test/kotlin/core/PathfinderTests.kt index b2da7a30c..910ee6c23 100644 --- a/Server/src/test/kotlin/core/PathfinderTests.kt +++ b/Server/src/test/kotlin/core/PathfinderTests.kt @@ -2,6 +2,7 @@ package core import TestUtils import content.global.skill.gather.GatheringSkillOptionListeners +import content.global.skill.gather.woodcutting.WoodcuttingListener import core.game.node.scenery.Scenery import core.game.world.map.Location import core.game.world.map.RegionManager @@ -11,7 +12,7 @@ import core.game.interaction.IntType import core.game.interaction.InteractionListeners class PathfinderTests { - companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners() }} + companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners(); WoodcuttingListener().defineListeners() }} @Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() { //clay fireplace - 13609 - sizex: 1, sizey: 2