Implement Authentic Interaction Subsystem

Implemented authentic script/interaction queues
This does now mean we have a total of 3 interaction systems, but this additional system is necessary to fix certain categories of bug and implement some authentic features
Converted mining to new system
Converted fishing to new system
Converted woodcutting to new system
Provided an example of soft-queued scripts with GrandTreePodListener
Implemented tick-eating (it is now possible to eat a shark, drink a potion, and eat a karambwan all on the same tick)
Can now eat and drop items while stunned
This commit is contained in:
Ceikry
2023-02-28 23:41:14 +00:00
committed by Ryan
parent 5206c99151
commit 124eeab893
61 changed files with 1466 additions and 293 deletions
@@ -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<Integer,Consumable> consumables = new HashMap<>();
public static HashMap<Integer,Consumables> 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);
}
}
}
@@ -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
@@ -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
}
}
@@ -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)
}
}
@@ -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;
}
}
@@ -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
}
}
}
@@ -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
}
@@ -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);
@@ -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)
@@ -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
}
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -13,7 +13,6 @@ import core.plugin.Plugin;
* @author 'Vexia
* @version 1.0
*/
@Initializable
public class DoogleLeafPlugin extends OptionHandler {
/**
@@ -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())
@@ -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{
@@ -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
}
}
@@ -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
}
}
@@ -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<Player> = 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
}
}
@@ -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;
@@ -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.");
@@ -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.");
@@ -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);
}
/**
@@ -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;
}
@@ -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<Object> {
GameWorld.getPulser().submit(new Pulse(ticks) {
@Override
public boolean pulse() {
target.getStateManager().set(EntityState.STUNNED, 4);
stun(target, 4);
return true;
}
});
@@ -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;
@@ -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)
@@ -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();
@@ -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:
@@ -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)
@@ -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) {