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:
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user