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

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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;
}
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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);

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -13,7 +13,6 @@ import core.plugin.Plugin;
* @author 'Vexia
* @version 1.0
*/
@Initializable
public class DoogleLeafPlugin extends OptionHandler {
/**

View file

@ -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())

View file

@ -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{

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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;

View file

@ -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.");

View file

@ -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.");

View file

@ -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);
}
/**

View file

@ -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;
}

View file

@ -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;
}
});

View file

@ -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;

View file

@ -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)

View file

@ -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();

View file

@ -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:

View file

@ -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)

View file

@ -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) {

View file

@ -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

View file

@ -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<Item> {
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<Object> 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.
*/

View file

@ -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))
}

View file

@ -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);

View file

@ -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))
}

View file

@ -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);
}

View file

@ -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<Object> {
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<Object> {
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<Object> {
return item.getName().replace("(4)", "").replace("(3)", "").replace("(2)", "").replace("(1)", "").trim().toLowerCase();
}
@Override
public Plugin<Object> 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);
}

View file

@ -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 {

View file

@ -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");
}
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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;
}

View file

@ -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()
}

View file

@ -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<Int, ArrayList<Pair<(Int, Int) -> Boolean, (Player, Node, Node) -> Boolean>>>(10)
private val destinationOverrides = HashMap<String,(Entity, Node) -> Location>(100)
private val equipListeners = HashMap<String,(Player,Node) -> Boolean>(10)
private val interactions = HashMap<String, InteractionListener.InteractionMetadata>()
private val useWithInteractions = HashMap<String, InteractionListener.UseWithMetadata>()
val instantClasses = HashSet<String>()
@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<out String>, 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<out String>, metadata: InteractionListener.InteractionMetadata) {
for (opt in options)
interactions["${type.ordinal}:$id:${opt.lowercase()}"] = metadata
}
fun addGenericMetadata (options: Array<out String>, 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
}
}

View file

@ -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<T> (val execution: T, val persist: Boolean) {
var state: Int = 0
var nextExecution = 0
}
class Interaction(execution: InteractExecutor, val distance: Int, persist: Boolean) : Script<InteractExecutor>(execution, persist)
class UseWithInteraction(execution: UseWithExecutor, val distance: Int, persist: Boolean, val used: Node, val with: Node) : Script<UseWithExecutor>(execution, persist)
class QueuedScript(executor: VoidExecutor, val strength: QueueStrength, persist: Boolean) : Script<VoidExecutor>(executor, persist)
class QueuedUseWith(executor: UseWithExecutor, val strength: QueueStrength, persist: Boolean, val used: Node, val with: Node) : Script<UseWithExecutor>(executor, persist)

View file

@ -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<Script<*>>()
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<Script<*>>()
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
}
}

View file

@ -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;
}
/**

View file

@ -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();
}
}

View file

@ -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();
}
/**

View file

@ -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());
}

View file

@ -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

View file

@ -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();
}

View file

@ -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();

View file

@ -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);

View file

@ -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;

View file

@ -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")
}
}
}
}

View file

@ -289,6 +289,16 @@ public final class Location extends Node {
return locs;
}
public ArrayList<Location> getCardinalTiles() {
ArrayList<Location> 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<Location>
*/

View file

@ -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()
}

View file

@ -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}")
}

View file

@ -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)
}
}

View file

@ -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