mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-28 05:45:10 -06:00
Merge branch 'combat-rewrites' into 'master'
Movement rewrite Closes #1137, #1954, #1959, #2078, #2437, and #2501 See merge request 2009scape/2009scape!2362
This commit is contained in:
commit
1739b39d16
38 changed files with 7831 additions and 2465 deletions
|
|
@ -63,6 +63,12 @@
|
|||
<version>[1.4.0,)</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.rsmod</groupId>
|
||||
<artifactId>rsmod-pathfinder</artifactId>
|
||||
<version>4.2.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import core.game.node.entity.impl.Projectile
|
|||
import core.game.node.entity.player.Player
|
||||
import core.game.node.item.Item
|
||||
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
|
||||
|
||||
|
|
@ -47,7 +46,7 @@ class PlayerPeltables : InteractionListener {
|
|||
|
||||
val other = node.asPlayer()
|
||||
|
||||
if (!Pathfinder.find(player, other, false, Pathfinder.PROJECTILE).isSuccessful) {
|
||||
if (!hasLineOfSight(player, other)) {
|
||||
sendDialogue(player, "You can't reach them!")
|
||||
return true
|
||||
}
|
||||
|
|
@ -112,4 +111,4 @@ class PlayerPeltables : InteractionListener {
|
|||
|
||||
return equipped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package content.global.skill.thieving;
|
||||
|
||||
import core.game.event.ResourceProducedEvent;
|
||||
import core.game.node.entity.combat.CombatPulse;
|
||||
import core.game.node.entity.combat.CombatReach;
|
||||
import core.game.node.entity.combat.ImpactHandler;
|
||||
import core.game.node.entity.skill.SkillPulse;
|
||||
import core.game.node.entity.skill.Skills;
|
||||
|
|
@ -11,6 +13,8 @@ import core.game.node.scenery.Scenery;
|
|||
import core.game.node.scenery.SceneryBuilder;
|
||||
import core.game.world.GameWorld;
|
||||
import core.game.world.map.RegionManager;
|
||||
import core.game.world.map.path.Path;
|
||||
import core.game.world.map.path.Pathfinder;
|
||||
import core.game.world.update.flag.context.Animation;
|
||||
import core.tools.RandomFunction;
|
||||
import core.tools.StringUtils;
|
||||
|
|
@ -156,15 +160,85 @@ public final class StallThiefPulse extends SkillPulse<Scenery> {
|
|||
player.sendMessage("A higher power smites you");
|
||||
return false;
|
||||
}
|
||||
for (NPC npc : RegionManager.getLocalNpcs(player.getLocation(), 8)) {
|
||||
if (!npc.getProperties().getCombatPulse().isAttacking() && (npc.getId() == 32 || npc.getId() == 2236)) {
|
||||
npc.sendChat("Hey! Get your hands off there!");
|
||||
npc.getProperties().getCombatPulse().attack(player);
|
||||
return false;
|
||||
}
|
||||
NPC guard = findGuardForFailedSteal(player);
|
||||
if (guard != null) {
|
||||
guard.sendChat("Hey! Get your hands off there!");
|
||||
guard.getProperties().getCombatPulse().attack(player);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the guard that busts a failed steal. Guards whose attack can actually
|
||||
* connect are preferred: NPC combat chase uses dumb pathing, so a guard on the far
|
||||
* side of a stall (or inside the guardhouse next to the Ardougne market) would
|
||||
* shout and then silently never reach the player. If no guard can reach, any guard
|
||||
* within detection range still catches the thief - the steal must not succeed just
|
||||
* because the witness is boxed in.
|
||||
* @param player the thieving player.
|
||||
* @return The catching guard, or {@code null} if no guard is in range.
|
||||
*/
|
||||
public static NPC findGuardForFailedSteal(Player player) {
|
||||
NPC catcher = findCatchingGuard(player);
|
||||
return catcher != null ? catcher : findWitnessGuard(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a guard within detection range whose attack can actually connect.
|
||||
* @param player the thieving player.
|
||||
* @return The catching guard, or {@code null} if no guard can reach the player.
|
||||
*/
|
||||
private static NPC findCatchingGuard(Player player) {
|
||||
for (NPC npc : RegionManager.getLocalNpcs(player.getLocation(), 8)) {
|
||||
if (npc.getProperties().getCombatPulse().isAttacking() || (npc.getId() != 32 && npc.getId() != 2236)) {
|
||||
continue;
|
||||
}
|
||||
if (canReachThief(npc, player)) {
|
||||
return npc;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds any guard within detection range that witnesses the steal, even if it
|
||||
* cannot reach the player. A guard already fighting someone else is too busy to
|
||||
* notice, but one already (fruitlessly) chasing this player keeps catching them.
|
||||
* @param player the thieving player.
|
||||
* @return The witnessing guard, or {@code null} if none is in range.
|
||||
*/
|
||||
private static NPC findWitnessGuard(Player player) {
|
||||
for (NPC npc : RegionManager.getLocalNpcs(player.getLocation(), 8)) {
|
||||
if (npc.getId() != 32 && npc.getId() != 2236) {
|
||||
continue;
|
||||
}
|
||||
CombatPulse pulse = npc.getProperties().getCombatPulse();
|
||||
if (pulse.isAttacking() && pulse.getVictim() != player) {
|
||||
continue;
|
||||
}
|
||||
return npc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the guard's attack can actually connect: either it is already in
|
||||
* melee reach, or its (dumb) chase route reaches the player.
|
||||
* @param npc the guard.
|
||||
* @param player the thieving player.
|
||||
* @return {@code True} if the guard can reach the player.
|
||||
*/
|
||||
private static boolean canReachThief(NPC npc, Player player) {
|
||||
if (CombatReach.canMelee(npc, player, 1)) {
|
||||
return true;
|
||||
}
|
||||
if (npc.getLocks().isMovementLocked()) {
|
||||
return false;
|
||||
}
|
||||
Path path = Pathfinder.find(npc, player, true, Pathfinder.DUMB);
|
||||
return path.isSuccessful() && !path.isMoveNear();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ class ServerConstants {
|
|||
arrayOf(Location.create(2722, 4886, 0), "quest the golem 1"),
|
||||
arrayOf(Location.create(2704, 5349, 0), "dorgeshuun", "dorg"),
|
||||
arrayOf(Location.create(2711, 10132, 0), "brine rats"),
|
||||
arrayOf(Location.create(2591, 4320, 0), "puro puro", "puro-puro", "puropuro", "puro", "impling maze"),
|
||||
arrayOf(Location.create(2328, 3677, 0), "piscatoris"),
|
||||
arrayOf(Location.create(2660, 3158, 0), "fishing trawler", "trawler"),
|
||||
arrayOf(Location.create(2800, 3667, 0), "mountain camp"),
|
||||
|
|
|
|||
|
|
@ -162,6 +162,29 @@ object InteractionListeners {
|
|||
return destinationOverrides["$type:$option"]
|
||||
}
|
||||
|
||||
private fun getOptionHandlerDestination(id: Int, option: String, node: Node): ((Entity, Node) -> Location?)? {
|
||||
val handlers = ArrayList<OptionHandler>(2)
|
||||
Option.defaultHandler(node, id, option)?.let { handlers.add(it) }
|
||||
node.interaction?.options
|
||||
?.firstOrNull { it != null && it.name.equals(option, ignoreCase = true) }
|
||||
?.handler
|
||||
?.takeIf { it !in handlers }
|
||||
?.let { handlers.add(it) }
|
||||
if (handlers.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return { entity, target ->
|
||||
handlers.firstNotNullOfOrNull { it.getDestination(entity, target) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDestinationOverride(type: Int, id: Int, option: String, node: Node): ((Entity, Node) -> Location?)? {
|
||||
return getOverride(type, id, option)
|
||||
?: getOverride(type, node.id)
|
||||
?: getOverride(type, option.toLowerCase())
|
||||
?: getOptionHandlerDestination(id, option, node)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun run(id: Int, player: Player, node: Node, isEquip: Boolean): Boolean{
|
||||
player.scripts.removeWeakScripts()
|
||||
|
|
@ -255,7 +278,7 @@ object InteractionListeners {
|
|||
return true
|
||||
}
|
||||
|
||||
val destOverride = getOverride(type.ordinal, id, option) ?: getOverride(type.ordinal,node.id) ?: getOverride(type.ordinal,option.toLowerCase())
|
||||
val destOverride = getDestinationOverride(type.ordinal, id, option, node)
|
||||
|
||||
if(type != IntType.ITEM && !isInstant(method)) {
|
||||
if(player.locks.isMovementLocked) return false
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -253,15 +253,14 @@ class ScriptProcessor(val entity: Entity) {
|
|||
is Scenery -> {
|
||||
val basicPath = Pathfinder.find(entity, interactTarget)
|
||||
val path = basicPath.points.lastOrNull()
|
||||
if (basicPath.isMoveNear) {
|
||||
target.location
|
||||
return
|
||||
}
|
||||
if (path == null) {
|
||||
if (!basicPath.isSuccessful) {
|
||||
clearScripts(entity)
|
||||
return
|
||||
}
|
||||
Location.create(path.x, path.y, entity.location.z)
|
||||
if (basicPath.isMoveNear) {
|
||||
return
|
||||
}
|
||||
path?.let { Location.create(it.x, it.y, entity.location.z) } ?: entity.location
|
||||
}
|
||||
is GroundItem -> DestinationFlag.ITEM.getDestination(entity, interactTarget)
|
||||
else -> target.location
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ public abstract class Entity extends Node {
|
|||
impactHandler.getImpactQueue().clear();
|
||||
impactHandler.setDisabledTicks(10);
|
||||
timers.onEntityDeath();
|
||||
removeAttribute("combat-time");
|
||||
clearCombatDeathState(killer);
|
||||
face(null);
|
||||
//Check if it's a Loar shade and transform back into the shadow version.
|
||||
if(this.getId() == 1240 || this.getId() == 1241){
|
||||
|
|
@ -288,6 +288,37 @@ public abstract class Entity extends Node {
|
|||
}
|
||||
}
|
||||
|
||||
private void clearCombatDeathState(Entity killer) {
|
||||
Object attacker = getAttribute("combat-attacker");
|
||||
Object aggressor = getAttribute("aggressor");
|
||||
properties.getCombatPulse().stop();
|
||||
removeAttribute("combat-time");
|
||||
removeAttribute("combat-attacker");
|
||||
removeAttribute("aggressor");
|
||||
clearCombatReference(killer);
|
||||
if (attacker instanceof Entity) {
|
||||
clearCombatReference((Entity) attacker);
|
||||
}
|
||||
if (aggressor instanceof Entity) {
|
||||
clearCombatReference((Entity) aggressor);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearCombatReference(Entity entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (entity.getAttribute("combat-attacker") == this) {
|
||||
entity.removeAttribute("combat-attacker");
|
||||
}
|
||||
if (entity.getAttribute("aggressor") == this) {
|
||||
entity.removeAttribute("aggressor");
|
||||
}
|
||||
if (entity.getProperties().getCombatPulse().getVictim() == this) {
|
||||
entity.getProperties().getCombatPulse().stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the location of an entity.
|
||||
* @param last the last location.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,240 @@
|
|||
package core.game.node.entity.combat
|
||||
|
||||
import core.ServerConstants
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.Point
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.world.map.path.Pathfinder
|
||||
|
||||
/** Plans combat-specific chase targets without mutating walking queues. */
|
||||
object CombatMovementPlanner {
|
||||
data class Plan(
|
||||
val targetLocation: Location,
|
||||
val attackTile: Location?,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun plan(attacker: Entity, target: Entity): Plan {
|
||||
val targetLocation = predictedTargetLocation(target)
|
||||
return Plan(
|
||||
targetLocation = targetLocation,
|
||||
attackTile = chooseTargetBorderTile(attacker, target, targetLocation),
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun exceedsCombatChaseDistance(attacker: Entity, target: Entity): Boolean {
|
||||
if (attacker.location.z != target.location.z) {
|
||||
return true
|
||||
}
|
||||
val targetTile = target.getClosestOccupiedTile(attacker.location)
|
||||
return attacker.location.getDistance(targetTile) >
|
||||
ServerConstants.MAX_PATHFIND_DISTANCE * 2.0
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictedTargetLocation(target: Entity): Location {
|
||||
return predictedTargetLocation(target, 2)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictedTargetLocation(target: Entity, maxSteps: Int): Location {
|
||||
return predictedMovementLocation(target, maxSteps) ?: target.location
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictTargetLocations(target: Entity): List<Location> {
|
||||
return predictTargetLocations(target, movementStepsThisTick(target))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictTargetLocations(target: Entity, maxSteps: Int): List<Location> {
|
||||
if (maxSteps <= 0) {
|
||||
return emptyList()
|
||||
}
|
||||
var first: Point? = null
|
||||
var second: Point? = null
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction == null) {
|
||||
continue
|
||||
}
|
||||
if (first == null) {
|
||||
first = point
|
||||
} else {
|
||||
second = point
|
||||
break
|
||||
}
|
||||
}
|
||||
val firstPoint = first ?: return emptyList()
|
||||
val steps = movementStepsThisTick(target, firstPoint, second).coerceAtMost(maxSteps)
|
||||
val predicted = ArrayList<Location>(steps)
|
||||
predicted.add(Location.create(firstPoint.x, firstPoint.y, target.location.z))
|
||||
if (steps > 1 && second != null) {
|
||||
predicted.add(Location.create(second.x, second.y, target.location.z))
|
||||
}
|
||||
return predicted
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun movementStepsThisTick(target: Entity): Int {
|
||||
var first: Point? = null
|
||||
var second: Point? = null
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction == null) {
|
||||
continue
|
||||
}
|
||||
if (first == null) {
|
||||
first = point
|
||||
} else {
|
||||
second = point
|
||||
break
|
||||
}
|
||||
}
|
||||
return movementStepsThisTick(target, first ?: return 0, second)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasMovementStepThisTick(target: Entity): Boolean {
|
||||
return firstMovementPoint(target) != null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictedMovementLocation(target: Entity): Location? {
|
||||
return predictedMovementLocation(target, 2)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun chooseTargetBorderTile(attacker: Entity, target: Entity): Location? {
|
||||
return chooseTargetBorderTile(attacker, target, predictedTargetLocation(target))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun chooseTargetBorderTile(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
targetLocation: Location,
|
||||
): Location? {
|
||||
return candidateAttackTiles(attacker, target, targetLocation).firstOrNull()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun candidateAttackTiles(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
targetLocation: Location,
|
||||
): List<Location> {
|
||||
val candidates = borderTiles(target, targetLocation, attacker.size())
|
||||
val walkable = candidates.filter { RegionManager.isTeleportPermitted(it) }
|
||||
val attackable = walkable.filter { canInteractFrom(attacker, it, target, targetLocation) }
|
||||
return (attackable.ifEmpty { walkable.ifEmpty { candidates } }).sortedWith(
|
||||
compareBy<Location> {
|
||||
it.getDistance(attacker.location)
|
||||
}
|
||||
.thenBy { it.x }
|
||||
.thenBy { it.y }
|
||||
)
|
||||
}
|
||||
|
||||
private fun canInteractFrom(
|
||||
attacker: Entity,
|
||||
location: Location,
|
||||
target: Entity,
|
||||
targetLocation: Location,
|
||||
): Boolean {
|
||||
if (attacker.size() == 1 && target.size() == 1) {
|
||||
val direction = Direction.getDirection(location, targetLocation) ?: return false
|
||||
return direction.canMoveFrom(
|
||||
location.z,
|
||||
location.x,
|
||||
location.y,
|
||||
RegionManager::getClippingFlag,
|
||||
)
|
||||
}
|
||||
return Pathfinder.canInteract(
|
||||
location.x,
|
||||
location.y,
|
||||
attacker.size(),
|
||||
targetLocation.x,
|
||||
targetLocation.y,
|
||||
target.size(),
|
||||
target.size(),
|
||||
0,
|
||||
targetLocation.z,
|
||||
) { z, x, y ->
|
||||
RegionManager.getClippingFlag(z, x, y)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun borderTiles(target: Entity, targetLocation: Location, attackerSize: Int): List<Location> {
|
||||
val targetSize = target.size()
|
||||
val border = LinkedHashSet<Location>()
|
||||
val plane = targetLocation.z
|
||||
val minOffset = -attackerSize + 1
|
||||
val maxOffset = targetSize - 1
|
||||
|
||||
for (offset in minOffset..maxOffset) {
|
||||
border.add(
|
||||
Location.create(targetLocation.x - attackerSize, targetLocation.y + offset, plane)
|
||||
)
|
||||
border.add(
|
||||
Location.create(targetLocation.x + targetSize, targetLocation.y + offset, plane)
|
||||
)
|
||||
border.add(
|
||||
Location.create(targetLocation.x + offset, targetLocation.y - attackerSize, plane)
|
||||
)
|
||||
border.add(
|
||||
Location.create(targetLocation.x + offset, targetLocation.y + targetSize, plane)
|
||||
)
|
||||
}
|
||||
|
||||
return border.toList()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictedMovementLocation(target: Entity, maxSteps: Int): Location? {
|
||||
if (maxSteps <= 0) {
|
||||
return null
|
||||
}
|
||||
var first: Point? = null
|
||||
var second: Point? = null
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction == null) {
|
||||
continue
|
||||
}
|
||||
if (first == null) {
|
||||
first = point
|
||||
} else {
|
||||
second = point
|
||||
break
|
||||
}
|
||||
}
|
||||
val firstPoint = first ?: return null
|
||||
val steps = movementStepsThisTick(target, firstPoint, second).coerceAtMost(maxSteps)
|
||||
val point = if (steps > 1 && second != null) second else firstPoint
|
||||
return Location.create(point.x, point.y, target.location.z)
|
||||
}
|
||||
|
||||
private fun firstMovementPoint(target: Entity): Point? {
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction != null) {
|
||||
return point
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun movementStepsThisTick(target: Entity, first: Point, second: Point?): Int {
|
||||
return if (canMoveTwoStepsThisTick(target, first, second)) 2 else 1
|
||||
}
|
||||
|
||||
private fun canMoveTwoStepsThisTick(target: Entity, first: Point, second: Point?): Boolean {
|
||||
if (second == null || first.isRunDisabled || !target.walkingQueue.isRunningBoth) {
|
||||
return false
|
||||
}
|
||||
return target !is Player || target.settings.runEnergy >= 1.0
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package core.game.node.entity.combat
|
|||
import content.global.ame.RandomEventNPC
|
||||
import content.global.handlers.item.equipment.special.SalamanderSwingHandler
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
import core.game.interaction.MovementPulse
|
||||
import core.game.node.Node
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.combat.equipment.WeaponInterface
|
||||
|
|
@ -17,7 +16,6 @@ import core.game.world.GameWorld
|
|||
import core.game.world.update.flag.context.Animation
|
||||
import core.tools.RandomFunction
|
||||
import core.api.*
|
||||
import core.game.interaction.DestinationFlag
|
||||
import core.game.system.timer.impl.*
|
||||
|
||||
/**
|
||||
|
|
@ -93,11 +91,6 @@ class CombatPulse(
|
|||
*/
|
||||
private var combatTimeOut = 0
|
||||
|
||||
/**
|
||||
* The movement handling pulse.
|
||||
*/
|
||||
private val movement: MovementPulse
|
||||
|
||||
/**
|
||||
* The last attack sent.
|
||||
*/
|
||||
|
|
@ -120,9 +113,15 @@ class CombatPulse(
|
|||
return true
|
||||
}
|
||||
if (!interactable()) {
|
||||
return if (entity.walkingQueue.isMoving) {
|
||||
return if (entity.walkingQueue.isMoving || entity.walkingQueue.hasPath()) {
|
||||
false
|
||||
} else combatTimeOut++ > entity.properties.combatTimeOut
|
||||
} else {
|
||||
val timedOut = combatTimeOut++ > entity.properties.combatTimeOut
|
||||
if (timedOut && entity is Player && !CombatMovementPlanner.hasMovementStepThisTick(victim!!)) {
|
||||
CombatMovementIntents.stopUnreachableCombat(entity)
|
||||
}
|
||||
timedOut
|
||||
}
|
||||
}
|
||||
combatTimeOut = 0
|
||||
entity.face(victim)
|
||||
|
|
@ -195,29 +194,44 @@ class CombatPulse(
|
|||
* @return `True` if so.
|
||||
*/
|
||||
private fun interactable(): Boolean {
|
||||
if (victim == null) {
|
||||
return false
|
||||
}
|
||||
if (entity is NPC && victim is Player && entity.isHidden(victim as Player?)) {
|
||||
val attacker = entity ?: return false
|
||||
val target = victim ?: return false
|
||||
if (attacker is NPC && target is Player && attacker.isHidden(target)) {
|
||||
stop()
|
||||
return false
|
||||
}
|
||||
if (victim is NPC && entity is Player && (victim as NPC).isHidden(entity as Player?)) {
|
||||
if (target is NPC && attacker is Player && target.isHidden(attacker)) {
|
||||
stop()
|
||||
return false
|
||||
}
|
||||
if (entity is NPC && !entity.asNpc().canStartCombat(victim)) {
|
||||
if (attacker is NPC && !attacker.asNpc().canStartCombat(target)) {
|
||||
stop()
|
||||
return false
|
||||
}
|
||||
if (CombatMovementPlanner.exceedsCombatChaseDistance(attacker, target)) {
|
||||
if (attacker is Player && !CombatMovementPlanner.hasMovementStepThisTick(target)) {
|
||||
CombatMovementIntents.stopUnreachableCombat(attacker)
|
||||
} else {
|
||||
stop()
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (style == CombatStyle.MELEE) {
|
||||
CombatMovementIntents.trackActiveMelee(attacker, target)
|
||||
}
|
||||
val type = canInteract()
|
||||
if (type == InteractionType.STILL_INTERACT) {
|
||||
if (style == CombatStyle.MELEE && !attacker.locks.isMovementLocked &&
|
||||
CombatMovementIntents.shouldMaintainMeleePressure(attacker, target)
|
||||
) {
|
||||
CombatMovementIntents.request(attacker, target)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (entity == null || victim == null || entity.locks.isMovementLocked) {
|
||||
if (attacker.locks.isMovementLocked) {
|
||||
return false
|
||||
}
|
||||
movement.updatePath()
|
||||
CombatMovementIntents.request(attacker, target)
|
||||
return type == InteractionType.MOVE_INTERACT
|
||||
}
|
||||
|
||||
|
|
@ -300,8 +314,10 @@ class CombatPulse(
|
|||
victim.scripts.removeWeakScripts()
|
||||
}
|
||||
|
||||
if (!isAttacking) {
|
||||
if (!isAttacking)
|
||||
entity.pulseManager.run(this)
|
||||
if (style == CombatStyle.MELEE) {
|
||||
CombatMovementIntents.trackActiveMelee(entity, victim)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -311,8 +327,6 @@ class CombatPulse(
|
|||
*/
|
||||
fun setVictim(victim: Node?) {
|
||||
super.addNodeCheck(1, victim)
|
||||
movement.setLast(null)
|
||||
movement.setDestination(victim)
|
||||
this.victim = victim as Entity?
|
||||
combatTimeOut = 0
|
||||
}
|
||||
|
|
@ -362,6 +376,7 @@ class CombatPulse(
|
|||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
CombatMovementIntents.untrack(entity)
|
||||
entity!!.setAttribute("combat-stop", GameWorld.ticks)
|
||||
if (victim != null) {
|
||||
lastVictim = victim
|
||||
|
|
@ -472,11 +487,4 @@ class CombatPulse(
|
|||
}
|
||||
}
|
||||
|
||||
init {
|
||||
movement = object : MovementPulse(entity, null) {
|
||||
override fun pulse(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
217
Server/src/main/core/game/node/entity/combat/CombatReach.kt
Normal file
217
Server/src/main/core/game/node/entity/combat/CombatReach.kt
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
package core.game.node.entity.combat
|
||||
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager.getClippingFlag
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import core.game.world.map.path.Pathfinder.*
|
||||
import core.game.world.map.path.RsmodPathfinder
|
||||
|
||||
/** Shared combat reach calculations. */
|
||||
object CombatReach {
|
||||
private const val BORK_LEGION_ID = 7135
|
||||
|
||||
@JvmStatic
|
||||
fun isUsingHalberd(entity: Entity): Boolean {
|
||||
if (entity is Player) {
|
||||
val weapon = entity.equipment[EquipmentContainer.SLOT_WEAPON]
|
||||
if (weapon != null) {
|
||||
return weapon.id in 3190..3204 || weapon.id == 6599
|
||||
}
|
||||
} else if (entity is NPC) {
|
||||
return entity.id == 8612
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun meleeDistance(entity: Entity): Int {
|
||||
return if (hasExtendedMeleeReach(entity)) 2 else 1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasExtendedMeleeReach(entity: Entity): Boolean {
|
||||
return isUsingHalberd(entity) || entity is NPC && entity.id == BORK_LEGION_ID
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun canMelee(entity: Entity, victim: Entity?, distance: Int): Boolean {
|
||||
val e = entity.location
|
||||
if (victim == null) {
|
||||
return false
|
||||
}
|
||||
if (entity.id == BORK_LEGION_ID && entity.location.withinDistance(victim.location, 2)) {
|
||||
return true
|
||||
}
|
||||
if (occupiedAreasOverlap(entity, victim)) {
|
||||
return false
|
||||
}
|
||||
val x = victim.location.x
|
||||
val y = victim.location.y
|
||||
val size = entity.size()
|
||||
if (distance == 1) {
|
||||
val victimSize = victim.size()
|
||||
fun adjacent(ex: Int, ey: Int): Boolean =
|
||||
Pathfinder.isStandingIn(ex, ey, 1, 1, x, y, victimSize, victimSize)
|
||||
for (i in 0 until size) {
|
||||
if (adjacent(e.x - 1, e.y + i)) return true
|
||||
if (adjacent(e.x + size, e.y + i)) return true
|
||||
if (adjacent(e.x + i, e.y - 1)) return true
|
||||
if (adjacent(e.x + i, e.y + size)) return true
|
||||
}
|
||||
return victim.getSwingHandler(false).type == CombatStyle.MELEE &&
|
||||
e.withinDistance(
|
||||
victim.location,
|
||||
1,
|
||||
) &&
|
||||
victim.properties.combatPulse.getVictim() === entity &&
|
||||
entity.index < victim.index
|
||||
}
|
||||
return entity.centerLocation.withinDistance(
|
||||
victim.centerLocation,
|
||||
distance + (size shr 1) + (victim.size() shr 1),
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasMeleeReach(
|
||||
attackerLocation: Location,
|
||||
attackerSize: Int,
|
||||
targetLocation: Location,
|
||||
targetSize: Int,
|
||||
): Boolean {
|
||||
return RsmodPathfinder.canReach(
|
||||
attackerLocation.x,
|
||||
attackerLocation.y,
|
||||
attackerSize,
|
||||
targetLocation.x,
|
||||
targetLocation.y,
|
||||
targetSize,
|
||||
targetSize,
|
||||
0,
|
||||
-1,
|
||||
0,
|
||||
attackerLocation.z,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun occupiedAreasOverlap(first: Entity, second: Entity): Boolean {
|
||||
return Pathfinder.isStandingIn(
|
||||
first.location.x,
|
||||
first.location.y,
|
||||
first.size(),
|
||||
first.size(),
|
||||
second.location.x,
|
||||
second.location.y,
|
||||
second.size(),
|
||||
second.size(),
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun combatDistance(entity: Entity, victim: Entity, rawDistance: Int): Int {
|
||||
var distance = rawDistance
|
||||
if (entity is NPC && entity.definition.combatDistance > 0) {
|
||||
distance = entity.definition.combatDistance
|
||||
}
|
||||
return (entity.size() shr 1) + (victim.size() shr 1) + distance
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun canReach(entity: Entity, victim: Entity, distance: Int): Boolean {
|
||||
return victim.centerLocation.withinDistance(entity.centerLocation, distance)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun canStepTowards(entity: Entity, victim: Entity): InteractionType {
|
||||
val closestVictimTile = victim.getClosestOccupiedTile(entity.location)
|
||||
val closestEntityTile = entity.getClosestOccupiedTile(closestVictimTile)
|
||||
val dir =
|
||||
closestEntityTile.deriveDirection(closestVictimTile)
|
||||
?: return InteractionType.STILL_INTERACT
|
||||
var next = closestEntityTile
|
||||
|
||||
// A fixed-direction walk can pass beside an oblique target without converging, so
|
||||
// limit the skipped gap to the number of steps available from the starting distance.
|
||||
val maxSkipSteps = next.getDistance(closestVictimTile).toInt() + 1
|
||||
for (i in 0 until maxSkipSteps) {
|
||||
if (next.getDistance(closestVictimTile) <= 3) {
|
||||
break
|
||||
}
|
||||
next = next.transform(dir)
|
||||
}
|
||||
if (next.getDistance(closestVictimTile) > 3) {
|
||||
return InteractionType.STILL_INTERACT
|
||||
}
|
||||
|
||||
var result = InteractionType.STILL_INTERACT
|
||||
val maxIterations = next.getDistance(closestVictimTile).toInt()
|
||||
for (i in 0 until maxIterations) {
|
||||
next = next.transform(dir)
|
||||
result = checkStepInterval(dir, next)
|
||||
if (result == InteractionType.NO_INTERACT) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun checkStepInterval(dir: Direction, next: Location): InteractionType {
|
||||
val components = next.getStepComponents(dir)
|
||||
|
||||
when (dir) {
|
||||
Direction.NORTH ->
|
||||
if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.EAST ->
|
||||
if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT
|
||||
Direction.SOUTH ->
|
||||
if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.WEST ->
|
||||
if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT
|
||||
|
||||
Direction.NORTH_EAST -> {
|
||||
if (
|
||||
getClippingFlag(components[0]) and PREVENT_EAST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_NORTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_NORTHEAST != 0
|
||||
)
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.NORTH_WEST -> {
|
||||
if (
|
||||
getClippingFlag(components[0]) and PREVENT_WEST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_NORTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_NORTHWEST != 0
|
||||
)
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_EAST -> {
|
||||
if (
|
||||
getClippingFlag(components[0]) and PREVENT_EAST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_SOUTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_SOUTHEAST != 0
|
||||
)
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_WEST -> {
|
||||
if (
|
||||
getClippingFlag(components[0]) and PREVENT_WEST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_SOUTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_SOUTHWEST != 0
|
||||
)
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
}
|
||||
|
||||
return InteractionType.STILL_INTERACT
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +1,47 @@
|
|||
package core.game.node.entity.combat
|
||||
|
||||
import content.global.skill.summoning.familiar.Familiar
|
||||
import core.api.log
|
||||
import core.api.playGlobalAudio
|
||||
import core.game.component.Component
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
import core.game.node.Node
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.combat.equipment.*
|
||||
import core.game.node.entity.combat.equipment.ArmourSet
|
||||
import core.game.node.entity.combat.equipment.DegradableEquipment
|
||||
import core.game.node.entity.combat.equipment.WeaponInterface
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.player.link.audio.Audio
|
||||
import core.game.node.entity.player.link.prayer.PrayerType
|
||||
import core.game.node.entity.skill.Skills
|
||||
import content.global.skill.summoning.familiar.Familiar
|
||||
import core.api.log
|
||||
import core.api.playGlobalAudio
|
||||
import core.game.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.world.map.RegionManager.getClippingFlag
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import core.game.world.map.path.Pathfinder.*
|
||||
import core.game.world.update.flag.context.Animation
|
||||
import core.tools.RandomFunction
|
||||
import core.game.system.config.ItemConfigParser
|
||||
import core.game.world.map.path.RsmodPathfinder
|
||||
import core.game.world.update.flag.context.Animation
|
||||
import core.tools.Log
|
||||
import core.tools.RandomFunction
|
||||
import org.rs09.consts.Sounds
|
||||
import java.util.*
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Handles a combat swing.
|
||||
*
|
||||
* @author Emperor
|
||||
* @author Ceikry - Kotlin refactoring, general cleanup
|
||||
* @author Player Name - converted `flags` to ArrayList
|
||||
*/
|
||||
abstract class CombatSwingHandler(var type: CombatStyle?) {
|
||||
var flags: ArrayList<SwingHandlerFlag> = ArrayList(SwingHandlerFlag.values().size)
|
||||
|
||||
constructor(type: CombatStyle?, vararg flags: SwingHandlerFlag) : this(type) {
|
||||
this.flags = arrayListOf(*flags)
|
||||
}
|
||||
|
||||
/**
|
||||
* The mapping of the special attack handlers.
|
||||
*/
|
||||
/** The mapping of the special attack handlers. */
|
||||
private var specialHandlers: MutableMap<Int, CombatSwingHandler?>? = null
|
||||
|
||||
/**
|
||||
* Starts the combat swing.
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state instance.
|
||||
|
|
@ -55,6 +51,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Handles the impact of the combat swing (victim getting hit).
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state instance.
|
||||
|
|
@ -63,6 +60,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Visualizes the impact itself (end animation, end GFX, ...)
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state instance.
|
||||
|
|
@ -71,6 +69,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Calculates the maximum accuracy of the entity.
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @return The maximum accuracy value.
|
||||
*/
|
||||
|
|
@ -78,6 +77,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Calculates the maximum strength of the entity.
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @param victim The victim.
|
||||
* @param modifier The modifier.
|
||||
|
|
@ -87,6 +87,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Calculates the maximum defence of the entity.
|
||||
*
|
||||
* @param victim The entity.
|
||||
* @param attacker The entity to defend against.
|
||||
* @return The maximum defence value.
|
||||
|
|
@ -95,6 +96,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Gets the void set multiplier.
|
||||
*
|
||||
* @param e The entity.
|
||||
* @param skillId The skill id.
|
||||
* @return The multiplier.
|
||||
|
|
@ -103,6 +105,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Visualizes the combat swing (start animation, GFX, projectile, ...)
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state instance.
|
||||
|
|
@ -113,6 +116,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Method called when the impact method got called.
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state.
|
||||
|
|
@ -139,6 +143,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Gets the currently worn armour set, if any.
|
||||
*
|
||||
* @param e The entity.
|
||||
* @return The armour set worn.
|
||||
*/
|
||||
|
|
@ -148,6 +153,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Checks if the hit will be accurate.
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @param victim The victim.
|
||||
* @return `True` if the hit is accurate.
|
||||
|
|
@ -158,6 +164,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Checks if the hit will be accurate.
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @param victim The victim.
|
||||
* @param style The combat style used.
|
||||
|
|
@ -169,6 +176,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Checks if the hit will be accurate.
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @param victim The victim.
|
||||
* @param style The combat style (null to ignore prayers).
|
||||
|
|
@ -176,26 +184,38 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
* @param defenceMod The defence modifier.
|
||||
* @return `True` if the hit is accurate.
|
||||
*/
|
||||
fun isAccurateImpact(entity: Entity?, victim: Entity?, style: CombatStyle?, accuracyMod: Double, defenceMod: Double): Boolean {
|
||||
fun isAccurateImpact(
|
||||
entity: Entity?,
|
||||
victim: Entity?,
|
||||
style: CombatStyle?,
|
||||
accuracyMod: Double,
|
||||
defenceMod: Double,
|
||||
): Boolean {
|
||||
var mod = 1.0
|
||||
if (victim == null || style == null) {
|
||||
return false
|
||||
}
|
||||
if (victim is Player && entity is Familiar && victim.prayer[PrayerType.PROTECT_FROM_SUMMONING]) {
|
||||
if (
|
||||
victim is Player &&
|
||||
entity is Familiar &&
|
||||
victim.prayer[PrayerType.PROTECT_FROM_SUMMONING]
|
||||
) {
|
||||
mod = 0.0
|
||||
}
|
||||
val attack = calculateAccuracy(entity) * accuracyMod * mod
|
||||
val defence = calculateDefence(victim, entity) * defenceMod
|
||||
val chance: Double = if (attack > defence) {
|
||||
1 - ((defence + 2) / (2 * (attack + 1)))
|
||||
} else {
|
||||
attack / (2 * (defence + 1))
|
||||
}
|
||||
val chance: Double =
|
||||
if (attack > defence) {
|
||||
1 - ((defence + 2) / (2 * (attack + 1)))
|
||||
} else {
|
||||
attack / (2 * (defence + 1))
|
||||
}
|
||||
return Math.random() < chance
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entity can execute a combat swing at the victim.
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @param victim The victim.
|
||||
* @return `True` if so.
|
||||
|
|
@ -206,6 +226,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Checks if the victim can be attacked by the entity.
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The entity being attacked.
|
||||
* @return `True` if so.
|
||||
|
|
@ -215,15 +236,20 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
if (type == CombatStyle.MELEE) {
|
||||
if (type == CombatStyle.MELEE && !CombatReach.hasExtendedMeleeReach(entity)) {
|
||||
val stepType = canStepTowards(entity, victim)
|
||||
if (stepType != InteractionType.STILL_INTERACT) return stepType
|
||||
}
|
||||
|
||||
val comp = entity.getAttribute("autocast_component",null) as Component?
|
||||
if((comp != null || type == CombatStyle.MAGIC) && (entity.properties.autocastSpell == null || entity.properties.autocastSpell.spellId == 0) && entity is Player){
|
||||
val comp = entity.getAttribute("autocast_component", null) as Component?
|
||||
if (
|
||||
(comp != null || type == CombatStyle.MAGIC) &&
|
||||
(entity.properties.autocastSpell == null ||
|
||||
entity.properties.autocastSpell.spellId == 0) &&
|
||||
entity is Player
|
||||
) {
|
||||
val weapEx = entity.getExtension<Any>(WeaponInterface::class.java) as WeaponInterface?
|
||||
if(comp != null){
|
||||
if (comp != null) {
|
||||
entity.interfaceManager.close(comp)
|
||||
entity.interfaceManager.openTab(weapEx)
|
||||
entity.properties.combatPulse.stop()
|
||||
|
|
@ -234,7 +260,12 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
entity.debug("Adjusting attack style")
|
||||
}
|
||||
if (entity.location == victim.location) {
|
||||
return if (entity is Player && victim is Player && entity.clientIndex < victim.clientIndex && victim.properties.combatPulse.getVictim() === entity) {
|
||||
return if (
|
||||
entity is Player &&
|
||||
victim is Player &&
|
||||
entity.clientIndex < victim.clientIndex &&
|
||||
victim.properties.combatPulse.getVictim() === entity
|
||||
) {
|
||||
InteractionType.STILL_INTERACT
|
||||
} else InteractionType.NO_INTERACT
|
||||
}
|
||||
|
|
@ -248,81 +279,12 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
}
|
||||
|
||||
protected fun canStepTowards(entity: Entity, victim: Entity): InteractionType {
|
||||
val closestVictimTile = victim.getClosestOccupiedTile(entity.location)
|
||||
val closestEntityTile = entity.getClosestOccupiedTile(closestVictimTile)
|
||||
val dir = closestEntityTile.deriveDirection(closestVictimTile)
|
||||
?: return InteractionType.STILL_INTERACT //if we cannot derive a direction, it's because both tiles are the same, so hand off control to the main logic which already handles this case
|
||||
var next = closestEntityTile
|
||||
|
||||
//Skip the initial gap in distance if it exists, because standard pathfinding would already stop us before this point if something was between us and the NPC or vice versa.
|
||||
//A fixed-direction walk can only arrive within its starting distance in steps; on oblique approaches it
|
||||
//passes beside the target and never converges, so the walk is hard-capped at that many steps.
|
||||
val maxSkipSteps = next.getDistance(closestVictimTile).toInt() + 1
|
||||
for (i in 0 until maxSkipSteps) {
|
||||
if (next.getDistance(closestVictimTile) <= 3) break
|
||||
next = next.transform(dir)
|
||||
}
|
||||
if (next.getDistance(closestVictimTile) > 3) return InteractionType.STILL_INTERACT //never converged (oblique approach), so defer to the range checks instead
|
||||
|
||||
var result: InteractionType = InteractionType.STILL_INTERACT
|
||||
val maxIterations = next.getDistance(closestVictimTile).toInt()
|
||||
for (i in 0 until maxIterations) { //step towards the target tile, checking if anything would obstruct us on the way, and immediately breaking + returning if it does.
|
||||
next = next.transform(dir)
|
||||
result = checkStepInterval(dir, next)
|
||||
if (result == InteractionType.NO_INTERACT) break
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun checkStepInterval(
|
||||
dir: Direction,
|
||||
next: Location
|
||||
): InteractionType {
|
||||
val components = next.getStepComponents(dir)
|
||||
|
||||
when (dir) {
|
||||
Direction.NORTH -> if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.EAST -> if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT
|
||||
Direction.SOUTH -> if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.WEST -> if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT
|
||||
|
||||
Direction.NORTH_EAST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_EAST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_NORTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_NORTHEAST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.NORTH_WEST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_WEST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_NORTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_NORTHWEST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_EAST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_EAST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_SOUTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_SOUTHEAST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_WEST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_WEST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_SOUTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_SOUTHWEST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return InteractionType.STILL_INTERACT
|
||||
return CombatReach.canStepTowards(entity, victim)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dragonfire message.
|
||||
*
|
||||
* @param protection The protection value.
|
||||
* @param fireName The fire breath name.
|
||||
* @return The message to send.
|
||||
|
|
@ -345,6 +307,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Visualizes the audio.
|
||||
*
|
||||
* @param entity the entity.
|
||||
* @param victim the victim.
|
||||
* @param state the state.
|
||||
|
|
@ -354,7 +317,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
val styleIndex = entity.settings.attackStyleIndex
|
||||
if (state.weapon != null && state.weapon.item != null) {
|
||||
val weapon = state.weapon.item
|
||||
val audios = weapon.definition.getConfiguration<Array<Audio>>(ItemConfigParser.ATTACK_AUDIO, null)
|
||||
val audios =
|
||||
weapon.definition.getConfiguration<Array<Audio>>(
|
||||
ItemConfigParser.ATTACK_AUDIO,
|
||||
null,
|
||||
)
|
||||
if (audios != null) {
|
||||
var audio: Audio? = null
|
||||
if (styleIndex < audios.size) {
|
||||
|
|
@ -366,7 +333,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
playGlobalAudio(entity.location, audio.id)
|
||||
}
|
||||
} else if (type == CombatStyle.MELEE) {
|
||||
//plays a punching sound when no weapon is equipped
|
||||
// plays a punching sound when no weapon is equipped
|
||||
playGlobalAudio(entity.location, Sounds.HUMAN_ATTACK_2564)
|
||||
}
|
||||
} else if (entity is NPC) {
|
||||
|
|
@ -378,24 +345,20 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Gets the combat distance.
|
||||
*
|
||||
* @param e The entity.
|
||||
* @param v The victim.
|
||||
* @param rawDistance The distance.
|
||||
* @return The actual distance used for combat.
|
||||
*/
|
||||
open fun getCombatDistance(e: Entity, v: Entity, rawDistance: Int): Int {
|
||||
var distance = rawDistance
|
||||
if (e is NPC) {
|
||||
if (e.definition.combatDistance > 0) {
|
||||
distance = e.definition.combatDistance
|
||||
}
|
||||
}
|
||||
return (e.size() shr 1) + (v.size() shr 1) + distance
|
||||
return CombatReach.combatDistance(e, v, rawDistance)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the hit for the victim. (called as
|
||||
* victim.getSwingHandler(false).formatHit(victim, hit))
|
||||
* Formats the hit for the victim. (called as victim.getSwingHandler(false).formatHit(victim,
|
||||
* hit))
|
||||
*
|
||||
* @param victim The entity receiving the hit.
|
||||
* @param rawHit The hit to format.
|
||||
* @return The formatted hit.
|
||||
|
|
@ -413,6 +376,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Adjusts the battle state object for this combat swing.
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state.
|
||||
|
|
@ -425,8 +389,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
}
|
||||
entity.sendImpact(state)
|
||||
victim.checkImpact(state)
|
||||
//Prevents lumbridge dummies from dying (true to how rs3 / 2009scape in 2009 does it)
|
||||
if (victim.id == 4474 && type == CombatStyle.MAGIC || victim.id == 7891 && type == CombatStyle.MELEE) {
|
||||
// Prevents lumbridge dummies from dying (true to how rs3 / 2009scape in 2009 does it)
|
||||
if (
|
||||
victim.id == 4474 && type == CombatStyle.MAGIC ||
|
||||
victim.id == 7891 && type == CombatStyle.MELEE
|
||||
) {
|
||||
EXPERIENCE_MOD = 0.1
|
||||
victim.fullRestore()
|
||||
if (state.estimatedHit >= 15) {
|
||||
|
|
@ -439,7 +406,8 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
if (victim.id == 757) {
|
||||
EXPERIENCE_MOD = 0.01
|
||||
}
|
||||
// Recursively adjustBattleState targets so that multi-target attacks have protection prayers applied.
|
||||
// Recursively adjustBattleState targets so that multi-target attacks have protection
|
||||
// prayers applied.
|
||||
if (state.targets != null && state.targets.isNotEmpty()) {
|
||||
if (!(state.targets.size == 1 && state.targets[0] == state)) {
|
||||
for (s in state.targets) {
|
||||
|
|
@ -467,7 +435,10 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
}
|
||||
}
|
||||
if (victim is NPC) {
|
||||
if (victim.properties.protectStyle != null && state.style == victim.properties.protectStyle) {
|
||||
if (
|
||||
victim.properties.protectStyle != null &&
|
||||
state.style == victim.properties.protectStyle
|
||||
) {
|
||||
state.neutralizeHits()
|
||||
}
|
||||
}
|
||||
|
|
@ -475,21 +446,31 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Adds the experience for the current combat swing.
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state.
|
||||
*/
|
||||
open fun addExperience(entity: Entity?, victim: Entity?, state: BattleState?) {
|
||||
if (entity == null || (victim is Player && entity is Player && entity.asPlayer().ironmanManager.isIronman)) {
|
||||
if (
|
||||
entity == null ||
|
||||
(victim is Player && entity is Player && entity.asPlayer().ironmanManager.isIronman)
|
||||
) {
|
||||
return
|
||||
}
|
||||
var player: Player
|
||||
var attStyle: Int
|
||||
|
||||
when(entity)
|
||||
{
|
||||
is Familiar -> {player = entity.owner; attStyle = entity.attackStyle}
|
||||
is Player -> {player = entity; attStyle = entity.properties.attackStyle.style}
|
||||
when (entity) {
|
||||
is Familiar -> {
|
||||
player = entity.owner
|
||||
attStyle = entity.attackStyle
|
||||
}
|
||||
|
||||
is Player -> {
|
||||
player = entity
|
||||
attStyle = entity.properties.attackStyle.style
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
if (victim is NPC) EXPERIENCE_MOD *= victim.behavior.getXpMultiplier(victim, player)
|
||||
|
|
@ -538,20 +519,26 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook for operations that conceptually happen during swing but could mess with experience granting logic if they
|
||||
* happened earlier.
|
||||
* Hook for operations that conceptually happen during swing but could mess with experience
|
||||
* granting logic if they happened earlier.
|
||||
*/
|
||||
open fun postSwing(entity: Entity?, victim: Entity?, state: BattleState?) {}
|
||||
|
||||
/**
|
||||
* Gets the formated hit.
|
||||
*
|
||||
* @param attacker The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @param state The battle state.
|
||||
* @param rawHit The hit to format.
|
||||
* @return The formated hit.
|
||||
*/
|
||||
protected open fun getFormattedHit(attacker: Entity, victim: Entity, state: BattleState, rawHit: Int): Int {
|
||||
protected open fun getFormattedHit(
|
||||
attacker: Entity,
|
||||
victim: Entity,
|
||||
state: BattleState,
|
||||
rawHit: Int,
|
||||
): Int {
|
||||
var hit = rawHit
|
||||
hit = attacker.getFormattedHit(state, hit).toInt()
|
||||
if (victim is Player) {
|
||||
|
|
@ -582,7 +569,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
}
|
||||
if (attacker is Player) {
|
||||
val player = attacker.asPlayer()
|
||||
if (player.equipment[3] != null && player.equipment[3].id == 14726 && state.style == CombatStyle.MAGIC) {
|
||||
if (
|
||||
player.equipment[3] != null &&
|
||||
player.equipment[3].id == 14726 &&
|
||||
state.style == CombatStyle.MAGIC
|
||||
) {
|
||||
hit += (hit.toDouble() * 0.15).toInt()
|
||||
}
|
||||
}
|
||||
|
|
@ -596,6 +587,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Gets the default animation of the entity.
|
||||
*
|
||||
* @param e The entity.
|
||||
* @param style The combat style.
|
||||
* @return The attack animation.
|
||||
|
|
@ -610,6 +602,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Registers a special attack handler.
|
||||
*
|
||||
* @param itemId The item id.
|
||||
* @param handler The combat swing handler.
|
||||
* @return `True` if succesful.
|
||||
|
|
@ -619,7 +612,17 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
specialHandlers = HashMap()
|
||||
}
|
||||
if (specialHandlers!!.containsKey(itemId)) {
|
||||
log(this::class.java, Log.ERR, "Already contained special attack handler for item " + itemId + " - [old=" + specialHandlers!![itemId]!!::class.java.simpleName + ", new=" + handler.javaClass.simpleName + "].")
|
||||
log(
|
||||
this::class.java,
|
||||
Log.ERR,
|
||||
"Already contained special attack handler for item " +
|
||||
itemId +
|
||||
" - [old=" +
|
||||
specialHandlers!![itemId]!!::class.java.simpleName +
|
||||
", new=" +
|
||||
handler.javaClass.simpleName +
|
||||
"].",
|
||||
)
|
||||
return false
|
||||
}
|
||||
return specialHandlers!!.put(itemId, handler) == null
|
||||
|
|
@ -627,9 +630,9 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
|
||||
/**
|
||||
* Gets the special attack handler for the given item id.
|
||||
*
|
||||
* @param itemId The item id.
|
||||
* @return The special attack handler, or `null` if this item has no
|
||||
* special attack handler.
|
||||
* @return The special attack handler, or `null` if this item has no special attack handler.
|
||||
*/
|
||||
fun getSpecial(itemId: Int): CombatSwingHandler? {
|
||||
if (specialHandlers == null) {
|
||||
|
|
@ -639,41 +642,30 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The amount of experience to get per hit.
|
||||
*/
|
||||
@JvmField
|
||||
var EXPERIENCE_MOD = 4.0
|
||||
/** The amount of experience to get per hit. */
|
||||
@JvmField
|
||||
var EXPERIENCE_MOD = 4.0
|
||||
|
||||
/**
|
||||
* Checks if a projectile can be fired from the node location to the victim
|
||||
* location.
|
||||
* Checks if a projectile can be fired from the node location to the victim location.
|
||||
*
|
||||
* @param entity The node.
|
||||
* @param victim The victim.
|
||||
* @param checkClose If we are checking for a melee attack rather than a
|
||||
* projectile.
|
||||
* @param checkClose If we are checking for a melee attack rather than a projectile.
|
||||
* @return `True` if so.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isProjectileClipped(entity: Node, victim: Node?, checkClose: Boolean): Boolean {
|
||||
for(x1 in 0 until entity.size()) {
|
||||
for(y1 in 0 until entity.size()) {
|
||||
val src = entity.location.transform(x1, y1, 0)
|
||||
for(x2 in 0 until victim!!.size()) {
|
||||
for(y2 in 0 until victim!!.size()) {
|
||||
val dst = victim!!.location.transform(x2, y2, 0)
|
||||
val path = PROJECTILE.find(src, 1, dst, 1, 1, 0, 0, 0, false, RegionManager::getClippingFlag)
|
||||
if(path.isSuccessful && (!checkClose || path.points.size <= 1)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
@JvmStatic
|
||||
fun isProjectileClipped(entity: Node, victim: Node?, checkClose: Boolean): Boolean {
|
||||
val target = requireNotNull(victim)
|
||||
return RsmodPathfinder.hasLineOfSightBetween(
|
||||
entity.location,
|
||||
entity.size(),
|
||||
target.location,
|
||||
target.size(),
|
||||
maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class SwingHandlerFlag {
|
||||
|
|
@ -681,5 +673,5 @@ enum class SwingHandlerFlag {
|
|||
IGNORE_STAT_BOOSTS_ACCURACY,
|
||||
IGNORE_PRAYER_BOOSTS_DAMAGE,
|
||||
IGNORE_PRAYER_BOOSTS_ACCURACY,
|
||||
IGNORE_STAT_REDUCTION
|
||||
IGNORE_STAT_REDUCTION,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ open class MagicSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
}
|
||||
var distance = 10
|
||||
var type = InteractionType.STILL_INTERACT
|
||||
var goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, distance))
|
||||
var goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, distance))
|
||||
if (victim.walkingQueue.isMoving && !goodRange) {
|
||||
goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, ++distance))
|
||||
goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, ++distance))
|
||||
type = InteractionType.MOVE_INTERACT
|
||||
}
|
||||
if (goodRange && isAttackable(entity, victim) != InteractionType.NO_INTERACT) {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ package core.game.node.entity.combat
|
|||
|
||||
import content.global.skill.skillcapeperks.SkillcapePerks
|
||||
import content.global.skill.slayer.SlayerEquipmentFlags
|
||||
import content.global.skill.slayer.SlayerManager
|
||||
import content.global.skill.slayer.Tasks
|
||||
import content.global.skill.summoning.SummoningPouch
|
||||
import core.api.*
|
||||
import core.api.EquipmentSlot
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.combat.equipment.ArmourSet
|
||||
import core.game.node.entity.combat.equipment.Weapon
|
||||
|
|
@ -15,50 +12,165 @@ import core.game.node.entity.combat.equipment.WeaponInterface
|
|||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import core.game.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import core.tools.RandomFunction
|
||||
import org.rs09.consts.Items
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Handles a melee combat swing.
|
||||
*
|
||||
* @author Emperor
|
||||
* @author Ceikry, Kotlin conversion + cleanup
|
||||
*/
|
||||
open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
||||
/**
|
||||
* Constructs a new `MeleeSwingHandler` {@Code Object}.
|
||||
*/
|
||||
open class MeleeSwingHandler(vararg flags: SwingHandlerFlag)
|
||||
/** Constructs a new `MeleeSwingHandler` {@Code Object}. */
|
||||
: CombatSwingHandler(CombatStyle.MELEE, *flags) {
|
||||
override fun canSwing(entity : Entity, victim : Entity) : InteractionType? {
|
||||
//Credits wolfenzi, https://www.rune-server.ee/2009scape-development/rs2-server/snippets/608720-arios-hybridding-improve.html
|
||||
var distance = if (usingHalberd(entity)) 2 else 1
|
||||
var type = InteractionType.STILL_INTERACT
|
||||
var goodRange = canMelee(entity, victim, distance)
|
||||
if (!goodRange && victim.properties.combatPulse.getVictim() !== entity && victim.walkingQueue.isMoving && entity.size() == 1) {
|
||||
type = InteractionType.MOVE_INTERACT
|
||||
distance += if (entity.walkingQueue.isRunningBoth) 2 else 1
|
||||
goodRange = canMelee(entity, victim, distance)
|
||||
}
|
||||
if (!isProjectileClipped(entity, victim, !usingHalberd(entity))) {
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
val isRunning = entity.walkingQueue.runDir != -1
|
||||
val enemyRunning = victim.walkingQueue.runDir != -1
|
||||
// THX 4 fix tom <333.
|
||||
if (super.canSwing(entity, victim) != InteractionType.NO_INTERACT) {
|
||||
val maxDistance = if (isRunning) if (enemyRunning) 3 else 4 else 2
|
||||
if (entity.walkingQueue.isMoving && entity.location.getDistance(victim.location) <= maxDistance && goodRange) {
|
||||
return type
|
||||
} else if (goodRange) {
|
||||
if (canStepTowards(entity, victim) == InteractionType.NO_INTERACT) return InteractionType.NO_INTERACT
|
||||
if (type == InteractionType.STILL_INTERACT) entity.walkingQueue.reset()
|
||||
return type
|
||||
}
|
||||
}
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
override fun canSwing(entity: Entity, victim: Entity): InteractionType? {
|
||||
// Credits wolfenzi,
|
||||
// https://www.rune-server.ee/2009scape-development/rs2-server/snippets/608720-arios-hybridding-improve.html
|
||||
var distance = CombatReach.meleeDistance(entity)
|
||||
var type = InteractionType.STILL_INTERACT
|
||||
var goodRange = CombatReach.canMelee(entity, victim, distance)
|
||||
if (
|
||||
!goodRange &&
|
||||
victim.properties.combatPulse.getVictim() !== entity &&
|
||||
victim.walkingQueue.isMoving &&
|
||||
entity.size() == 1
|
||||
) {
|
||||
type = InteractionType.MOVE_INTERACT
|
||||
distance += if (entity.walkingQueue.isRunningBoth) 2 else 1
|
||||
goodRange = CombatReach.canMelee(entity, victim, distance)
|
||||
}
|
||||
if (!hasMeleeLineOfSight(entity, victim, type)) {
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
val isRunning = entity.walkingQueue.runDir != -1
|
||||
val enemyRunning = victim.walkingQueue.runDir != -1
|
||||
// THX 4 fix tom <333.
|
||||
if (super.canSwing(entity, victim) != InteractionType.NO_INTERACT) {
|
||||
val maxDistance = if (isRunning) if (enemyRunning) 3 else 4 else 2
|
||||
if (
|
||||
entity.walkingQueue.isMoving &&
|
||||
entity.location.getDistance(victim.location) <= maxDistance &&
|
||||
goodRange
|
||||
) {
|
||||
return type
|
||||
} else if (goodRange) {
|
||||
if (
|
||||
!CombatReach.hasExtendedMeleeReach(entity) &&
|
||||
canStepTowards(entity, victim) == InteractionType.NO_INTERACT
|
||||
)
|
||||
return InteractionType.NO_INTERACT
|
||||
if (type == InteractionType.STILL_INTERACT) entity.walkingQueue.reset()
|
||||
return type
|
||||
}
|
||||
}
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
private fun hasMeleeLineOfSight(
|
||||
entity: Entity,
|
||||
victim: Entity,
|
||||
type: InteractionType,
|
||||
): Boolean {
|
||||
if (CombatReach.hasExtendedMeleeReach(entity)) {
|
||||
return isProjectileClipped(entity, victim, false)
|
||||
}
|
||||
if (
|
||||
CombatReach.hasMeleeReach(
|
||||
entity.location,
|
||||
entity.size(),
|
||||
victim.location,
|
||||
victim.size(),
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (type != InteractionType.MOVE_INTERACT) {
|
||||
return false
|
||||
}
|
||||
val predictedVictimLocation =
|
||||
CombatMovementPlanner.predictedMovementLocation(victim) ?: return false
|
||||
val projectedEntityLocation =
|
||||
projectedMeleeChaseLocation(entity, victim, predictedVictimLocation) ?: return false
|
||||
if (!isAdjacentTo(entity, projectedEntityLocation, victim, predictedVictimLocation)) {
|
||||
return false
|
||||
}
|
||||
return CombatReach.hasMeleeReach(
|
||||
projectedEntityLocation,
|
||||
entity.size(),
|
||||
predictedVictimLocation,
|
||||
victim.size(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun projectedMeleeChaseLocation(
|
||||
entity: Entity,
|
||||
victim: Entity,
|
||||
victimLocation: Location,
|
||||
): Location? {
|
||||
if (entity.size() != 1 || entity.location.z != victimLocation.z) {
|
||||
return null
|
||||
}
|
||||
val maxSteps =
|
||||
if (
|
||||
entity is Player &&
|
||||
entity.walkingQueue.isRunningBoth &&
|
||||
entity.settings.runEnergy >= 1.0
|
||||
)
|
||||
2
|
||||
else 1
|
||||
var current = entity.location
|
||||
var steps = 0
|
||||
while (steps < maxSteps && !isAdjacentTo(entity, current, victim, victimLocation)) {
|
||||
val direction = Direction.getDirection(current, victimLocation) ?: return null
|
||||
if (
|
||||
!direction.canMoveFrom(
|
||||
current.z,
|
||||
current.x,
|
||||
current.y,
|
||||
RegionManager::getClippingFlag,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
val next = current.transform(direction)
|
||||
if (!RegionManager.isTeleportPermitted(next)) {
|
||||
return null
|
||||
}
|
||||
current = next
|
||||
steps++
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
private fun isAdjacentTo(
|
||||
entity: Entity,
|
||||
entityLocation: Location,
|
||||
victim: Entity,
|
||||
victimLocation: Location,
|
||||
): Boolean {
|
||||
val entityMinX = entityLocation.x
|
||||
val entityMaxX = entityLocation.x + entity.size()
|
||||
val entityMinY = entityLocation.y
|
||||
val entityMaxY = entityLocation.y + entity.size()
|
||||
val victimMinX = victimLocation.x
|
||||
val victimMaxX = victimLocation.x + victim.size()
|
||||
val victimMinY = victimLocation.y
|
||||
val victimMaxY = victimLocation.y + victim.size()
|
||||
|
||||
val xOverlaps = entityMinX < victimMaxX && entityMaxX > victimMinX
|
||||
val yOverlaps = entityMinY < victimMaxY && entityMaxY > victimMinY
|
||||
if (xOverlaps && yOverlaps) {
|
||||
return false
|
||||
}
|
||||
val xTouches = entityMaxX == victimMinX || victimMaxX == entityMinX
|
||||
val yTouches = entityMaxY == victimMinY || victimMaxY == entityMinY
|
||||
return (xTouches && yOverlaps) || (yTouches && xOverlaps)
|
||||
}
|
||||
|
||||
override fun swing(entity: Entity?, victim: Entity?, state: BattleState?): Int {
|
||||
var hit = 0
|
||||
|
|
@ -69,18 +181,29 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
if (entity!!.properties.armourSet == ArmourSet.VERAC && RandomFunction.roll(4)) {
|
||||
state.armourEffect = ArmourSet.VERAC
|
||||
}
|
||||
if (state.armourEffect == ArmourSet.VERAC || isAccurateImpact(entity, victim, CombatStyle.MELEE)) {
|
||||
if (
|
||||
state.armourEffect == ArmourSet.VERAC ||
|
||||
isAccurateImpact(entity, victim, CombatStyle.MELEE)
|
||||
) {
|
||||
var max = calculateHit(entity, victim, 1.0)
|
||||
if (victim != null) {
|
||||
if (entity is NPC && state.armourEffect == ArmourSet.VERAC && victim.hasProtectionPrayer(CombatStyle.MELEE)) max = max * 2 / 3
|
||||
if (
|
||||
entity is NPC &&
|
||||
state.armourEffect == ArmourSet.VERAC &&
|
||||
victim.hasProtectionPrayer(CombatStyle.MELEE)
|
||||
)
|
||||
max = max * 2 / 3
|
||||
}
|
||||
state.maximumHit = max
|
||||
hit = RandomFunction.random(max + 1)
|
||||
}
|
||||
state.estimatedHit = hit
|
||||
if(victim != null) {
|
||||
if (state.estimatedHit > victim.skills.lifepoints) state.estimatedHit = victim.skills.lifepoints
|
||||
if (state.estimatedHit + state.secondaryHit > victim.skills.lifepoints) state.secondaryHit -= ((state.estimatedHit + state.secondaryHit) - victim.skills.lifepoints)
|
||||
if (victim != null) {
|
||||
if (state.estimatedHit > victim.skills.lifepoints)
|
||||
state.estimatedHit = victim.skills.lifepoints
|
||||
if (state.estimatedHit + state.secondaryHit > victim.skills.lifepoints)
|
||||
state.secondaryHit -=
|
||||
((state.estimatedHit + state.secondaryHit) - victim.skills.lifepoints)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
|
@ -123,7 +246,7 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
damage = 20
|
||||
}
|
||||
if (damage > -1 && RandomFunction.random(10) < 4) {
|
||||
applyPoison (victim, entity, damage)
|
||||
applyPoison(victim, entity, damage)
|
||||
}
|
||||
}
|
||||
} else if (entity is NPC) {
|
||||
|
|
@ -131,26 +254,36 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
val damage = entity.poisonSeverity()
|
||||
|
||||
if (poisonous && damage > -1 && RandomFunction.random(10) < 4) {
|
||||
applyPoison (victim, entity, damage)
|
||||
applyPoison(victim, entity, damage)
|
||||
}
|
||||
}
|
||||
super.adjustBattleState(entity, victim, state)
|
||||
}
|
||||
|
||||
override fun calculateAccuracy(entity: Entity?): Int {
|
||||
//formula taken from wiki: https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_six:_Calculate_the_hit_chance Yes I know it's old school. It's the best resource we have for potentially authentic formulae.
|
||||
// formula taken from wiki:
|
||||
// https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_six:_Calculate_the_hit_chance Yes I know it's old school. It's the best resource we have for potentially authentic formulae.
|
||||
entity ?: return 0
|
||||
|
||||
val styleAttackBonus = entity.properties.bonuses[entity.properties.attackStyle.bonusType] + 64
|
||||
val styleAttackBonus =
|
||||
entity.properties.bonuses[entity.properties.attackStyle.bonusType] + 64
|
||||
when (entity) {
|
||||
is Player -> {
|
||||
var effectiveAttackLevel = entity.skills.getLevel(Skills.ATTACK).toDouble()
|
||||
if(!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_ACCURACY))
|
||||
effectiveAttackLevel = floor(effectiveAttackLevel + (entity.prayer.getSkillBonus(Skills.ATTACK) * effectiveAttackLevel))
|
||||
if(entity.properties.attackStyle.style == WeaponInterface.STYLE_ACCURATE) effectiveAttackLevel += 3
|
||||
else if(entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveAttackLevel += 1
|
||||
if (!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_ACCURACY))
|
||||
effectiveAttackLevel =
|
||||
floor(
|
||||
effectiveAttackLevel +
|
||||
(entity.prayer.getSkillBonus(Skills.ATTACK) * effectiveAttackLevel)
|
||||
)
|
||||
if (entity.properties.attackStyle.style == WeaponInterface.STYLE_ACCURATE)
|
||||
effectiveAttackLevel += 3
|
||||
else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED)
|
||||
effectiveAttackLevel += 1
|
||||
effectiveAttackLevel += 8
|
||||
if(SkillcapePerks.isActive(SkillcapePerks.PRECISION_STRIKES, entity)){ //Attack skillcape perk
|
||||
if (
|
||||
SkillcapePerks.isActive(SkillcapePerks.PRECISION_STRIKES, entity)
|
||||
) { // Attack skillcape perk
|
||||
effectiveAttackLevel += 6
|
||||
}
|
||||
effectiveAttackLevel *= getSetMultiplier(entity, Skills.ATTACK)
|
||||
|
|
@ -163,14 +296,27 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
|
||||
// attack bonus for specialized equipments (salve amulets, slayer equips)
|
||||
val amuletId = getItemFromEquipment(entity, EquipmentSlot.NECK)?.id ?: 0
|
||||
if ((amuletId == Items.SALVE_AMULET_4081 || amuletId == Items.SALVE_AMULETE_10588) && checkUndead(victimName)) {
|
||||
if (
|
||||
(amuletId == Items.SALVE_AMULET_4081 ||
|
||||
amuletId == Items.SALVE_AMULETE_10588) && checkUndead(victimName)
|
||||
) {
|
||||
effectiveAttackLevel *= if (amuletId == Items.SALVE_AMULET_4081) 1.15 else 1.2
|
||||
} else if (getSlayerTask(entity)?.let { task ->
|
||||
} else if (
|
||||
getSlayerTask(entity)?.let { task ->
|
||||
val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0
|
||||
task.ids.contains(victimId) || (task == Tasks.KALPHITES && (victimId == 1158)) // Kalphite Queen phase 1
|
||||
} == true) {
|
||||
effectiveAttackLevel *= SlayerEquipmentFlags.getDamAccBonus(entity) //Slayer Helm/ Black Mask/ Slayer cape
|
||||
if (getSlayerTask(entity)?.dragon == true && inEquipment(entity, Items.DRAGON_SLAYER_GLOVES_12862))
|
||||
task.ids.contains(victimId) ||
|
||||
(task == Tasks.KALPHITES &&
|
||||
(victimId == 1158)) // Kalphite Queen phase 1
|
||||
} == true
|
||||
) {
|
||||
effectiveAttackLevel *=
|
||||
SlayerEquipmentFlags.getDamAccBonus(
|
||||
entity
|
||||
) // Slayer Helm/ Black Mask/ Slayer cape
|
||||
if (
|
||||
getSlayerTask(entity)?.dragon == true &&
|
||||
inEquipment(entity, Items.DRAGON_SLAYER_GLOVES_12862)
|
||||
)
|
||||
effectiveAttackLevel *= 1.1
|
||||
}
|
||||
|
||||
|
|
@ -183,8 +329,6 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
}
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun calculateHit(entity: Entity?, victim: Entity?, modifier: Double): Int {
|
||||
|
|
@ -194,28 +338,43 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
when (entity) {
|
||||
is Player -> {
|
||||
var effectiveStrengthLevel = entity.skills.getLevel(Skills.STRENGTH).toDouble()
|
||||
if(!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_DAMAGE))
|
||||
effectiveStrengthLevel = floor(effectiveStrengthLevel + (entity.prayer.getSkillBonus(Skills.STRENGTH) * effectiveStrengthLevel))
|
||||
if(entity.properties.attackStyle.style == WeaponInterface.STYLE_AGGRESSIVE) effectiveStrengthLevel += 3
|
||||
else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveStrengthLevel += 1
|
||||
if (!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_DAMAGE))
|
||||
effectiveStrengthLevel =
|
||||
floor(
|
||||
effectiveStrengthLevel +
|
||||
(entity.prayer.getSkillBonus(Skills.STRENGTH) *
|
||||
effectiveStrengthLevel)
|
||||
)
|
||||
if (entity.properties.attackStyle.style == WeaponInterface.STYLE_AGGRESSIVE)
|
||||
effectiveStrengthLevel += 3
|
||||
else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED)
|
||||
effectiveStrengthLevel += 1
|
||||
effectiveStrengthLevel += 8
|
||||
effectiveStrengthLevel *= getSetMultiplier(entity, Skills.STRENGTH)
|
||||
effectiveStrengthLevel = floor(effectiveStrengthLevel)
|
||||
if (!flags.contains(SwingHandlerFlag.IGNORE_STAT_BOOSTS_DAMAGE))
|
||||
effectiveStrengthLevel *= styleStrengthBonus
|
||||
else effectiveStrengthLevel *= 64
|
||||
if (getSlayerTask(entity)?.let { task ->
|
||||
val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0
|
||||
task.ids.contains(victimId) || (task == Tasks.KALPHITES && (victimId == 1158)) // Kalphite Queen phase 1
|
||||
} == true) {
|
||||
effectiveStrengthLevel *= SlayerEquipmentFlags.getDamAccBonus(entity) //Slayer Helm/ Black Mask/ Slayer cape
|
||||
if (
|
||||
getSlayerTask(entity)?.let { task ->
|
||||
val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0
|
||||
task.ids.contains(victimId) ||
|
||||
(task == Tasks.KALPHITES &&
|
||||
(victimId == 1158)) // Kalphite Queen phase 1
|
||||
} == true
|
||||
) {
|
||||
effectiveStrengthLevel *=
|
||||
SlayerEquipmentFlags.getDamAccBonus(
|
||||
entity
|
||||
) // Slayer Helm/ Black Mask/ Slayer cape
|
||||
}
|
||||
|
||||
return (floor((0.5 + (effectiveStrengthLevel / 640.0))) * modifier).toInt()
|
||||
}
|
||||
is NPC -> {
|
||||
val strengthLevel = entity.skills.getLevel(Skills.STRENGTH) + 9
|
||||
return (floor((0.5 + (strengthLevel * styleStrengthBonus / 640.0))) * modifier).toInt()
|
||||
return (floor((0.5 + (strengthLevel * styleStrengthBonus / 640.0))) * modifier)
|
||||
.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,17 +382,28 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
}
|
||||
|
||||
override fun calculateDefence(victim: Entity?, attacker: Entity?): Int {
|
||||
//authentic formula, taken from OSRS wiki: https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_five:_Calculate_the_Defence_roll
|
||||
// authentic formula, taken from OSRS wiki:
|
||||
// https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_five:_Calculate_the_Defence_roll
|
||||
victim ?: return 0
|
||||
attacker ?: return 0
|
||||
|
||||
val styleDefenceBonus = victim.properties.bonuses[attacker.properties.attackStyle.bonusType + 5] + 64
|
||||
val styleDefenceBonus =
|
||||
victim.properties.bonuses[attacker.properties.attackStyle.bonusType + 5] + 64
|
||||
when (victim) {
|
||||
is Player -> {
|
||||
var effectiveDefenceLevel = victim.skills.getLevel(Skills.DEFENCE).toDouble()
|
||||
effectiveDefenceLevel = floor(effectiveDefenceLevel + (victim.prayer.getSkillBonus(Skills.DEFENCE) * effectiveDefenceLevel))
|
||||
if (victim.properties.attackStyle.style == WeaponInterface.STYLE_DEFENSIVE || victim.properties.attackStyle.style == WeaponInterface.STYLE_LONG_RANGE) effectiveDefenceLevel += 3
|
||||
else if (victim.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveDefenceLevel += 1
|
||||
effectiveDefenceLevel =
|
||||
floor(
|
||||
effectiveDefenceLevel +
|
||||
(victim.prayer.getSkillBonus(Skills.DEFENCE) * effectiveDefenceLevel)
|
||||
)
|
||||
if (
|
||||
victim.properties.attackStyle.style == WeaponInterface.STYLE_DEFENSIVE ||
|
||||
victim.properties.attackStyle.style == WeaponInterface.STYLE_LONG_RANGE
|
||||
)
|
||||
effectiveDefenceLevel += 3
|
||||
else if (victim.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED)
|
||||
effectiveDefenceLevel += 1
|
||||
effectiveDefenceLevel += 8
|
||||
effectiveDefenceLevel *= getSetMultiplier(victim, Skills.DEFENCE)
|
||||
effectiveDefenceLevel *= familiarDefenceBonus(victim)
|
||||
|
|
@ -283,12 +453,23 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
|
||||
/**
|
||||
* Check to see whether an NPC is classified as undead.
|
||||
*
|
||||
* @param name
|
||||
* @return true if so
|
||||
*/
|
||||
private fun checkUndead(name: String): Boolean {
|
||||
return (name == "Zombie" || name.contains("rmoured") || name == "Ankou" || name == "Crawling Hand" || name == "Banshee" || name == "Ghost" || name == "Ghast" || name == "Mummy" || name.contains("Revenant")
|
||||
|| name == "Skeleton" || name == "Zogre" || name == "Spiritual Mage")
|
||||
return (name == "Zombie" ||
|
||||
name.contains("rmoured") ||
|
||||
name == "Ankou" ||
|
||||
name == "Crawling Hand" ||
|
||||
name == "Banshee" ||
|
||||
name == "Ghost" ||
|
||||
name == "Ghast" ||
|
||||
name == "Mummy" ||
|
||||
name.contains("Revenant") ||
|
||||
name == "Skeleton" ||
|
||||
name == "Zogre" ||
|
||||
name == "Spiritual Mage")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -298,11 +479,12 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
*/
|
||||
private fun familiarDefenceBonus(e: Entity?): Double {
|
||||
if (e !is Player) return 1.0
|
||||
val fam = try {
|
||||
e.familiarManager?.familiar
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
} ?: return 1.0
|
||||
val fam =
|
||||
try {
|
||||
e.familiarManager?.familiar
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
} ?: return 1.0
|
||||
return when (fam.pouchId) {
|
||||
SummoningPouch.IRON_TITAN_POUCH.pouchId -> 1.10
|
||||
SummoningPouch.STEEL_TITAN_POUCH.pouchId -> 1.15
|
||||
|
|
@ -311,61 +493,15 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Checks if the entity is using a halberd.
|
||||
* @param entity The entity.
|
||||
* @return `True` if so.
|
||||
*/
|
||||
private fun usingHalberd(entity: Entity): Boolean {
|
||||
if (entity is Player) {
|
||||
val weapon = entity.equipment[EquipmentContainer.SLOT_WEAPON]
|
||||
if (weapon != null) {
|
||||
return weapon.id in 3190..3204 || weapon.id == 6599
|
||||
}
|
||||
} else if (entity is NPC) {
|
||||
return entity.id == 8612
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entity can execute a melee swing from its current location.
|
||||
*
|
||||
* @param entity The attacking entity.
|
||||
* @param victim The victim.
|
||||
* @return `True` if so.
|
||||
*/
|
||||
fun canMelee(entity: Entity, victim: Entity?, distance: Int): Boolean {
|
||||
val e = entity.location
|
||||
if (victim == null) {
|
||||
return false
|
||||
}
|
||||
if (entity.id == 7135 && entity.location.withinDistance(victim.location, 2)) {
|
||||
return true
|
||||
}
|
||||
val x = victim.location.x
|
||||
val y = victim.location.y
|
||||
val size = entity.size()
|
||||
if (distance == 1) {
|
||||
for (i in 0 until size) {
|
||||
if (Pathfinder.isStandingIn(e.x - 1, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + size, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + i, e.y - 1, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + i, e.y + size, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (e == victim.location) {
|
||||
return true
|
||||
}
|
||||
return victim.getSwingHandler(false).type == CombatStyle.MELEE && e.withinDistance(victim.location, 1) && victim.properties.combatPulse.getVictim() === entity && entity.index < victim.index
|
||||
}
|
||||
return entity.centerLocation.withinDistance(victim.centerLocation, distance + (size shr 1) + (victim.size() shr 1))
|
||||
return CombatReach.canMelee(entity, victim, distance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ open class RangeSwingHandler (vararg flags: SwingHandlerFlag) : CombatSwingHandl
|
|||
distance = 10
|
||||
}
|
||||
}
|
||||
var goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, distance))
|
||||
var goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, distance))
|
||||
var type = InteractionType.STILL_INTERACT
|
||||
if (victim.walkingQueue.isMoving && !goodRange) {
|
||||
goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, ++distance))
|
||||
goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, ++distance))
|
||||
type = InteractionType.MOVE_INTERACT
|
||||
}
|
||||
if (goodRange && super.canSwing(entity, victim) != InteractionType.NO_INTERACT) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,21 @@ public final class WalkingQueue {
|
|||
|
||||
public ArrayList<GroundItem> routeItems = new ArrayList<GroundItem>();
|
||||
|
||||
/**
|
||||
* Clears the route markers created by ::drawroute.
|
||||
*/
|
||||
private void clearRouteItems() {
|
||||
if (!(entity instanceof Player) || routeItems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (GroundItem item : routeItems) {
|
||||
if (item != null) {
|
||||
RegionManager.getRegionPlane(item.getLocation()).remove(item);
|
||||
}
|
||||
}
|
||||
routeItems.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code WalkingQueue} {@code Object}.
|
||||
* @param entity The entity.
|
||||
|
|
@ -92,17 +107,9 @@ public final class WalkingQueue {
|
|||
if (hasTimerActive(entity, "frozen"))
|
||||
return;
|
||||
Point point = walkingQueue.poll();
|
||||
boolean drawPath = entity.getAttribute("routedraw", false);
|
||||
if (point == null) {
|
||||
updateRunEnergy(false);
|
||||
if (isPlayer && drawPath) {
|
||||
for (GroundItem item : routeItems) {
|
||||
if (item != null) {
|
||||
RegionManager.getRegionPlane(item.getLocation()).remove(item);
|
||||
}
|
||||
}
|
||||
routeItems.clear();
|
||||
}
|
||||
clearRouteItems();
|
||||
return;
|
||||
}
|
||||
if (isPlayer && ((Player) entity).getSettings().getRunEnergy() < 1.0) {
|
||||
|
|
@ -236,6 +243,7 @@ public final class WalkingQueue {
|
|||
*/
|
||||
public boolean updateTeleport() {
|
||||
if (entity.getProperties().getTeleportLocation() != null) {
|
||||
entity.getProperties().getCombatPulse().stop();
|
||||
reset(false);
|
||||
entity.setLocation(entity.getProperties().getTeleportLocation());
|
||||
entity.getProperties().setTeleportLocation(null);
|
||||
|
|
@ -374,13 +382,19 @@ public final class WalkingQueue {
|
|||
}
|
||||
return running;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity has a path to walk.
|
||||
*
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public boolean hasPath() {
|
||||
return !walkingQueue.isEmpty();
|
||||
for (Point point : walkingQueue) {
|
||||
if (point.getDirection() != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -412,6 +426,7 @@ public final class WalkingQueue {
|
|||
);
|
||||
}
|
||||
|
||||
clearRouteItems();
|
||||
walkingQueue.clear();
|
||||
walkingQueue.add(new Point(loc.getX(), loc.getY()));
|
||||
this.running = running;
|
||||
|
|
|
|||
|
|
@ -21,8 +21,11 @@ import content.global.skill.slayer.Tasks;
|
|||
import content.global.skill.summoning.familiar.Familiar;
|
||||
import core.game.world.map.Direction;
|
||||
import core.game.world.map.Location;
|
||||
import core.game.world.map.Point;
|
||||
import core.game.world.map.RegionManager;
|
||||
import core.game.world.map.build.DynamicRegion;
|
||||
import core.game.world.map.path.ClipMaskSupplier;
|
||||
import core.game.world.map.path.Path;
|
||||
import core.game.world.map.path.Pathfinder;
|
||||
import core.game.world.update.flag.context.Animation;
|
||||
import core.game.world.update.flag.context.Graphics;
|
||||
|
|
@ -466,25 +469,22 @@ public class NPC extends Entity {
|
|||
return;
|
||||
if (!getLocks().isInteractionLocked()) {
|
||||
if (!getLocks().isMovementLocked()) {
|
||||
int effectiveWalkRadius = getWalkRadius();
|
||||
if (
|
||||
!pathBoundMovement
|
||||
&& walkRadius > 0
|
||||
&& walkRadius <= 20
|
||||
&& !getLocation().withinDistance(getProperties().getSpawnLocation(), (int)(walkRadius * 1.5))
|
||||
&& effectiveWalkRadius > 0
|
||||
&& effectiveWalkRadius <= 20
|
||||
&& !getLocation().withinDistance(getProperties().getSpawnLocation(), getSpawnReturnDistance(effectiveWalkRadius))
|
||||
&& !getAttribute("no-spawn-return", false)
|
||||
)
|
||||
{
|
||||
MovementPulse current = getAttribute("return-to-spawn-pulse");
|
||||
if (current != null && current.isRunning()) return;
|
||||
|
||||
if(!isNeverWalks()){
|
||||
if(walkRadius == 0)
|
||||
walkRadius = 3;
|
||||
}
|
||||
if (aggressiveHandler != null) {
|
||||
aggressiveHandler.setPauseTicks(walkRadius + 1);
|
||||
aggressiveHandler.setPauseTicks(effectiveWalkRadius + 1);
|
||||
}
|
||||
nextWalk = GameWorld.getTicks() + walkRadius + 1;
|
||||
nextWalk = GameWorld.getTicks() + effectiveWalkRadius + 1;
|
||||
getLocks().lockMovement(100);
|
||||
getImpactHandler().setDisabledTicks(100);
|
||||
setAttribute("return-to-spawn", true);
|
||||
|
|
@ -530,15 +530,130 @@ public class NPC extends Entity {
|
|||
setNextWalk();
|
||||
Location l = getMovementDestination();
|
||||
if (canMove(l)) {
|
||||
if((Boolean) definition.getHandlers().getOrDefault("water_npc",false)){
|
||||
Pathfinder.findWater(this,l,true,Pathfinder.DUMB).walk(this);
|
||||
} else {
|
||||
Pathfinder.find(this, l, true, Pathfinder.DUMB).walk(this);
|
||||
}
|
||||
Path path = pathBoundMovement ? findMovementPath(l) : findRandomMovementPath(l);
|
||||
path.walk(this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Path findMovementPath(Location destination) {
|
||||
if (isWaterNPC()) {
|
||||
return Pathfinder.findWater(this, destination, true, Pathfinder.DUMB);
|
||||
}
|
||||
return Pathfinder.find(this, destination, true, Pathfinder.DUMB);
|
||||
}
|
||||
|
||||
private Path findRandomMovementPath(Location destination) {
|
||||
if (isWaterNPC() || size() != 1) {
|
||||
Path path = findMovementPath(destination);
|
||||
return isRandomMovementPathWithinBounds(path) ? path : new Path();
|
||||
}
|
||||
return findDirectRandomMovementPath(destination);
|
||||
}
|
||||
|
||||
private Path findDirectRandomMovementPath(Location destination) {
|
||||
Path path = new Path();
|
||||
path.setSuccesful(true);
|
||||
Location current = getLocation();
|
||||
int maxSteps = Math.min(14, Math.max(1, getSpawnReturnDistance(Math.max(1, getWalkRadius()))));
|
||||
boolean usedSidestep = false;
|
||||
for (int steps = 0; !current.equals(destination) && steps < maxSteps; steps++) {
|
||||
Direction direction = Direction.getDirection(current, destination);
|
||||
Direction stepDirection = chooseRandomMovementStep(current, direction, usedSidestep);
|
||||
if (stepDirection == null) {
|
||||
path.setSuccesful(false);
|
||||
path.setMoveNear(!path.getPoints().isEmpty());
|
||||
break;
|
||||
}
|
||||
Location next = current.transform(stepDirection);
|
||||
if (!isWithinRandomMovementBounds(next)) {
|
||||
path.setSuccesful(false);
|
||||
path.setMoveNear(!path.getPoints().isEmpty());
|
||||
break;
|
||||
}
|
||||
if (stepDirection != direction) {
|
||||
usedSidestep = true;
|
||||
}
|
||||
path.getPoints().add(new Point(next.getX(), next.getY(), stepDirection, stepDirection.getStepX(), stepDirection.getStepY()));
|
||||
current = next;
|
||||
}
|
||||
if (!current.equals(destination) && !path.getPoints().isEmpty()) {
|
||||
path.setMoveNear(true);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private Direction chooseRandomMovementStep(Location current, Direction direction, boolean usedSidestep) {
|
||||
if (direction == null) {
|
||||
return null;
|
||||
}
|
||||
Location next = current.transform(direction);
|
||||
if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, direction)) {
|
||||
return direction;
|
||||
}
|
||||
if (usedSidestep) {
|
||||
return null;
|
||||
}
|
||||
for (Direction sidestep : sidestepDirections(direction)) {
|
||||
next = current.transform(sidestep);
|
||||
if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, sidestep)) {
|
||||
return sidestep;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Direction[] sidestepDirections(Direction direction) {
|
||||
switch (direction) {
|
||||
case NORTH:
|
||||
case SOUTH:
|
||||
return new Direction[] { Direction.EAST, Direction.WEST };
|
||||
case EAST:
|
||||
case WEST:
|
||||
return new Direction[] { Direction.NORTH, Direction.SOUTH };
|
||||
case NORTH_EAST:
|
||||
return new Direction[] { Direction.EAST, Direction.NORTH };
|
||||
case SOUTH_EAST:
|
||||
return new Direction[] { Direction.EAST, Direction.SOUTH };
|
||||
case SOUTH_WEST:
|
||||
return new Direction[] { Direction.WEST, Direction.SOUTH };
|
||||
case NORTH_WEST:
|
||||
return new Direction[] { Direction.WEST, Direction.NORTH };
|
||||
default:
|
||||
return new Direction[0];
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canTakeRandomMovementStep(Location current, Direction direction) {
|
||||
ClipMaskSupplier clipMaskSupplier = behavior != null ? behavior.getClippingSupplier(this) : null;
|
||||
if (clipMaskSupplier == null) {
|
||||
clipMaskSupplier = RegionManager::getClippingFlag;
|
||||
}
|
||||
return direction.canMoveFrom(current.getZ(), current.getX(), current.getY(), clipMaskSupplier);
|
||||
}
|
||||
|
||||
private boolean isRandomMovementPathWithinBounds(Path path) {
|
||||
for (Point point : path.getPoints()) {
|
||||
if (!isWithinRandomMovementBounds(Location.create(point.getX(), point.getY(), getLocation().getZ()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isWithinRandomMovementBounds(Location location) {
|
||||
int walkRadius = getWalkRadius();
|
||||
return walkRadius <= 0 || location.withinDistance(getProperties().getSpawnLocation(), getSpawnReturnDistance(walkRadius));
|
||||
}
|
||||
|
||||
private int getSpawnReturnDistance(int walkRadius) {
|
||||
return (int) (walkRadius * 1.5);
|
||||
}
|
||||
|
||||
private boolean isWaterNPC() {
|
||||
return (Boolean) definition.getHandlers().getOrDefault("water_npc", false);
|
||||
}
|
||||
|
||||
public int getNextWalk() {
|
||||
return nextWalk;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package core.game.node.entity.npc
|
|||
import core.game.node.item.Item
|
||||
import core.api.ContentInterface
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.combat.BattleState
|
||||
import core.game.node.entity.combat.CombatStyle
|
||||
|
|
@ -24,12 +23,6 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
|
|||
}
|
||||
}
|
||||
|
||||
object StandardClipMaskSupplier : ClipMaskSupplier {
|
||||
override fun getClippingFlag (z: Int, x: Int, y: Int) : Int {
|
||||
return RegionManager.getClippingFlag(z,x,y)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every tick, before the base NPC tick() method.
|
||||
* @param self the NPC instance this behavior belongs to
|
||||
|
|
@ -144,7 +137,7 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
|
|||
* Called by pathfinding code to determine the clipping mask supplier this NPC should use. You can use this to ignore water, etc.
|
||||
*/
|
||||
open fun getClippingSupplier(self: NPC) : ClipMaskSupplier? {
|
||||
return StandardClipMaskSupplier
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ object ServerConfigParser {
|
|||
wild_pvp_enabled = data.getBoolean("world.wild_pvp_enabled"),
|
||||
jad_practice_enabled = data.getBoolean("world.jad_practice_enabled"),
|
||||
ge_announcement_limit = data.getLong("world.ge_announcement_limit", 500L).toInt(),
|
||||
smartpathfinder_bfs = data.getBoolean("world.smartpathfinder_bfs", false),
|
||||
enable_castle_wars = data.getBoolean("world.enable_castle_wars", false),
|
||||
message_model = data.getString("world.motw_identifier").toInt(),
|
||||
message_string = data.getString("world.motw_text").replace("@name", ServerConstants.SERVER_NAME)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ class GameSettings
|
|||
var wild_pvp_enabled: Boolean,
|
||||
var jad_practice_enabled: Boolean,
|
||||
var ge_announcement_limit: Int,
|
||||
var smartpathfinder_bfs: Boolean,
|
||||
var enable_castle_wars: Boolean,
|
||||
|
||||
/**"Lobby" interface
|
||||
|
|
@ -135,7 +134,6 @@ class GameSettings
|
|||
val wild_pvp_enabled = if(data.containsKey("wild_pvp_enabled")) data["wild_pvp_enabled"] as Boolean else true
|
||||
val jad_practice_enabled = if(data.containsKey("jad_practice_enabled")) data["jad_practice_enabled"] as Boolean else true
|
||||
val ge_announcement_limit = data["ge_announcement_limit"].toString().toInt()
|
||||
val smartpathfinder_bfs = if(data.containsKey("smartpathfinder_bfs")) data["smartpathfinder_bfs"] as Boolean else false
|
||||
val enable_castle_wars = if(data.containsKey("enable_castle_wars")) data["enable_castle_wars"] as Boolean else false
|
||||
val allow_token_purchase = data["allow_token_purchase"] as Boolean
|
||||
val message_of_the_week_identifier = data["message_of_the_week_identifier"].toString().toInt()
|
||||
|
|
@ -165,7 +163,6 @@ class GameSettings
|
|||
wild_pvp_enabled,
|
||||
jad_practice_enabled,
|
||||
ge_announcement_limit,
|
||||
smartpathfinder_bfs,
|
||||
enable_castle_wars,
|
||||
message_of_the_week_identifier,
|
||||
message_of_the_week_text
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import core.game.node.entity.Entity
|
|||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.scenery.Scenery
|
||||
import core.game.world.GameWorld
|
||||
import core.game.world.map.zone.ZoneBorders
|
||||
import core.tools.Log
|
||||
import core.tools.RandomFunction
|
||||
import core.tools.SystemLogger
|
||||
import org.rsmod.game.pathfinder.collision.CollisionFlagMap
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
|
@ -24,8 +26,11 @@ object RegionManager {
|
|||
* The region cache mapping.
|
||||
*/
|
||||
private val REGION_CACHE: MutableMap<Int, Region> = HashMap()
|
||||
private val RSMOD_PROJECTILE_REGIONS = HashSet<Int>()
|
||||
@JvmStatic val CLIPPING_FLAGS = HashMap<Int, Array<Int>>()
|
||||
@JvmStatic val PROJECTILE_FLAGS = HashMap<Int, Array<Int>>()
|
||||
@JvmStatic val RSMOD_CLIPPING_FLAGS = CollisionFlagMap()
|
||||
@JvmStatic val RSMOD_PROJECTILE_FLAGS = CollisionFlagMap()
|
||||
|
||||
public val LOCK = ReentrantLock()
|
||||
|
||||
|
|
@ -125,16 +130,89 @@ object RegionManager {
|
|||
|
||||
@JvmStatic
|
||||
fun getFlags(regionId: Int, projectile: Boolean) : Array<Int> {
|
||||
return if (projectile)
|
||||
PROJECTILE_FLAGS.getOrPut (regionId) {Array(16384){0}}
|
||||
else
|
||||
return if (projectile) {
|
||||
initialiseRsmodProjectileRegion(regionId)
|
||||
PROJECTILE_FLAGS.getOrPut(regionId) { Array(16384) { 0 } }
|
||||
} else {
|
||||
CLIPPING_FLAGS.getOrPut (regionId) {Array(16384){-1}}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun resetFlags(regionId: Int) {
|
||||
PROJECTILE_FLAGS.put (regionId, Array(16384){0})
|
||||
CLIPPING_FLAGS.put (regionId, Array(16384){-1})
|
||||
resetRsmodFlags(regionId)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setRsmodFlag(z: Int, x: Int, y: Int, projectile: Boolean, flag: Int) {
|
||||
val flags = if (projectile) RSMOD_PROJECTILE_FLAGS else RSMOD_CLIPPING_FLAGS
|
||||
flags[x, y, z] = flag
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadClippingWindow(center: Location, size: Int) {
|
||||
val ensured = ENSURED_WINDOW_REGIONS.get()
|
||||
if (ensured.tick != GameWorld.ticks) {
|
||||
ensured.regions.clear()
|
||||
ensured.tick = GameWorld.ticks
|
||||
}
|
||||
val minX = center.x - (size / 2)
|
||||
val minY = center.y - (size / 2)
|
||||
val maxX = minX + size - 1
|
||||
val maxY = minY + size - 1
|
||||
for (regionX in (minX shr 6)..(maxX shr 6)) {
|
||||
for (regionY in (minY shr 6)..(maxY shr 6)) {
|
||||
val regionId = (regionX shl 8) or regionY
|
||||
if (!ensured.regions.add(regionId)) {
|
||||
continue
|
||||
}
|
||||
Region.load(forId(regionId))
|
||||
initialiseRsmodProjectileRegion(regionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pathfinding probes load the same clipping window many times per tick; regions
|
||||
* cannot transition to unloaded between probes within one tick, so each thread only
|
||||
* needs to ensure a region once per tick instead of taking the region lock per probe.
|
||||
*/
|
||||
private class EnsuredWindowRegions {
|
||||
var tick = -1
|
||||
val regions = HashSet<Int>()
|
||||
}
|
||||
|
||||
private val ENSURED_WINDOW_REGIONS = ThreadLocal.withInitial { EnsuredWindowRegions() }
|
||||
|
||||
private fun resetRsmodFlags(regionId: Int) {
|
||||
val baseX = (regionId shr 8) shl 6
|
||||
val baseY = (regionId and 0xFF) shl 6
|
||||
for (z in 0 until 4) {
|
||||
for (x in baseX until baseX + 64 step 8) {
|
||||
for (y in baseY until baseY + 64 step 8) {
|
||||
RSMOD_CLIPPING_FLAGS.deallocateIfPresent(x, y, z)
|
||||
val projectileFlags = RSMOD_PROJECTILE_FLAGS.allocateIfAbsent(x, y, z)
|
||||
projectileFlags.fill(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialiseRsmodProjectileRegion(regionId: Int) {
|
||||
if (!RSMOD_PROJECTILE_REGIONS.add(regionId)) {
|
||||
return
|
||||
}
|
||||
val baseX = (regionId shr 8) shl 6
|
||||
val baseY = (regionId and 0xFF) shl 6
|
||||
for (z in 0 until 4) {
|
||||
for (x in baseX until baseX + 64 step 8) {
|
||||
for (y in baseY until baseY + 64 step 8) {
|
||||
RSMOD_PROJECTILE_FLAGS.allocateIfAbsent(x, y, z).fill(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ public final class RegionFlags {
|
|||
public void addFlag(int x, int y, int clipdata) {
|
||||
int current = getFlag(x, y);
|
||||
Pair<Integer, Integer> indices = getFlagIndex(x, y);
|
||||
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = max(0, current) | clipdata;
|
||||
int updated = max(0, current) | clipdata;
|
||||
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = updated;
|
||||
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, updated);
|
||||
}
|
||||
|
||||
public void removeFlag(int x, int y, int clipdata) {
|
||||
|
|
@ -189,16 +191,19 @@ public final class RegionFlags {
|
|||
current = max(0, current) & ~clipdata;
|
||||
|
||||
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = current;
|
||||
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, current);
|
||||
}
|
||||
|
||||
public void clearFlag(int x, int y) {
|
||||
Pair<Integer, Integer> indices = getFlagIndex(x, y);
|
||||
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = 0;
|
||||
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, 0);
|
||||
}
|
||||
|
||||
public void invalidateFlag(int x, int y) {
|
||||
Pair<Integer, Integer> indices = getFlagIndex(x, y);
|
||||
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = -1;
|
||||
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -534,4 +539,4 @@ public final class RegionFlags {
|
|||
public void setLandscape(boolean[][] landscape) {
|
||||
this.landscape = landscape;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,39 +3,35 @@ package core.game.world.map.path;
|
|||
import core.game.world.map.Direction;
|
||||
import core.game.world.map.Location;
|
||||
import core.game.world.map.Point;
|
||||
import core.game.world.map.RegionManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A pathfinder implementation used for an easy path, where the pathfinder won't
|
||||
* find a way around clipped objects.. <br> This is used for NPC combat
|
||||
* following, NPC random movement, etc.
|
||||
* @author Emperor
|
||||
* Pathfinder for simple local movement. It walks directly toward the
|
||||
* destination and only tries the horizontal/vertical alternatives of a blocked
|
||||
* diagonal step; it does not search around obstacles.
|
||||
*/
|
||||
public final class DumbPathfinder extends Pathfinder {
|
||||
/**
|
||||
* If a path can be found.
|
||||
*/
|
||||
|
||||
private boolean found;
|
||||
|
||||
/**
|
||||
* The plane.
|
||||
*/
|
||||
private int z;
|
||||
|
||||
/**
|
||||
* The x-coordinate.
|
||||
*/
|
||||
private int x;
|
||||
|
||||
/**
|
||||
* The y-coordinate.
|
||||
*/
|
||||
private int y;
|
||||
|
||||
|
||||
@Override
|
||||
public Path find(Location start, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier) {
|
||||
public Path find(Location start,
|
||||
int size,
|
||||
Location end,
|
||||
int sizeX,
|
||||
int sizeY,
|
||||
int rotation,
|
||||
int type,
|
||||
int walkingFlag,
|
||||
boolean near,
|
||||
ClipMaskSupplier clipMaskSupplier) {
|
||||
ClipMaskSupplier supplier = clipMaskSupplier != null ? clipMaskSupplier : RegionManager::getClippingFlag;
|
||||
Path path = new Path();
|
||||
z = start.getZ();
|
||||
x = start.getX();
|
||||
|
|
@ -44,20 +40,19 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
path.setSuccesful(true);
|
||||
while (x != end.getX() || y != end.getY()) {
|
||||
Direction[] directions = getDirection(x, y, end);
|
||||
if (type != 0) {
|
||||
if ((type < 5 || type == 10) && canDoorInteract(x, y, size, end.getX(), end.getY(), type - 1, rotation, z, clipMaskSupplier)) {
|
||||
if (type >= 0) {
|
||||
if ((type < 5 || type == 9) && canDoorInteract(x, y, size, end.getX(), end.getY(), type, rotation, z, supplier)) {
|
||||
break;
|
||||
}
|
||||
if (type < 10 && canDecorationInteract(x, y, size, end.getX(), end.getY(), type - 1, rotation, z, clipMaskSupplier)) {
|
||||
if (type < 10 && canDecorationInteract(x, y, size, end.getX(), end.getY(), rotation, type, z, supplier)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sizeX != 0 && sizeY != 0) {
|
||||
if (canInteract(x, y, size, end.getX(), end.getY(), sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
|
||||
if (canInteract(x, y, size, end.getX(), end.getY(), sizeX, sizeY, walkingFlag, z, supplier)) {
|
||||
break;
|
||||
}
|
||||
if (directions.length > 1) { // Ensures we approach the location
|
||||
// correctly (non-diagonal).
|
||||
if (directions.length > 1) {
|
||||
Direction dir = directions[0];
|
||||
if (x + dir.getStepX() == end.getX() && y + dir.getStepY() == end.getY()) {
|
||||
directions[0] = directions[directions.length - 1];
|
||||
|
|
@ -67,11 +62,11 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
}
|
||||
found = true;
|
||||
if (size < 2) {
|
||||
checkSingleTraversal(points, clipMaskSupplier, directions);
|
||||
checkSingleTraversal(points, supplier, directions);
|
||||
} else if (size == 2) {
|
||||
checkDoubleTraversal(points, clipMaskSupplier, directions);
|
||||
checkDoubleTraversal(points, supplier, directions);
|
||||
} else {
|
||||
checkVariableTraversal(points, directions, size, clipMaskSupplier);
|
||||
checkVariableTraversal(points, directions, size, supplier);
|
||||
}
|
||||
if (!found) {
|
||||
path.setMoveNear(x != start.getX() || y != start.getY());
|
||||
|
|
@ -79,22 +74,10 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (!points.isEmpty()) {
|
||||
Direction last = null;
|
||||
for (int i = 0; i < points.size() - 1; i++) {
|
||||
Point p = points.get(i);
|
||||
path.getPoints().add(p);
|
||||
}
|
||||
path.getPoints().add(points.get(points.size() - 1));
|
||||
}
|
||||
path.getPoints().addAll(points);
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks traversal for a size 1 entity.
|
||||
* @param points The points list.
|
||||
* @param directions The directions.
|
||||
*/
|
||||
|
||||
private void checkSingleTraversal(List<Point> points, ClipMaskSupplier clipMaskSupplier, Direction... directions) {
|
||||
for (Direction dir : directions) {
|
||||
found = true;
|
||||
|
|
@ -108,7 +91,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y++;
|
||||
break;
|
||||
case NORTH_EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y + 1) & PREVENT_NORTHEAST) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x,
|
||||
y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x + 1,
|
||||
y + 1) & PREVENT_NORTHEAST) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -125,7 +113,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
x++;
|
||||
break;
|
||||
case SOUTH_EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & PREVENT_SOUTHEAST) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x,
|
||||
y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x + 1,
|
||||
y - 1) & PREVENT_SOUTHEAST) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -142,7 +135,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y--;
|
||||
break;
|
||||
case SOUTH_WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & PREVENT_SOUTHWEST) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x,
|
||||
y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x - 1,
|
||||
y - 1) & PREVENT_SOUTHWEST) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -159,7 +157,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
x--;
|
||||
break;
|
||||
case NORTH_WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & PREVENT_NORTHWEST) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x,
|
||||
y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x - 1,
|
||||
y + 1) & PREVENT_NORTHWEST) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -173,18 +176,15 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks traversal for a size 1 entity.
|
||||
* @param points The points list.
|
||||
* @param directions The directions.
|
||||
*/
|
||||
|
||||
private void checkDoubleTraversal(List<Point> points, ClipMaskSupplier clipMaskSupplier, Direction... directions) {
|
||||
for (Direction dir : directions) {
|
||||
found = true;
|
||||
switch (dir) {
|
||||
case NORTH:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c01e0) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + 1,
|
||||
y + 2) & 0x12c01e0) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -192,7 +192,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y++;
|
||||
break;
|
||||
case NORTH_EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y + 2) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y + 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + 2,
|
||||
y + 2) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x + 2,
|
||||
y + 1) & 0x12c01e3) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -201,7 +206,9 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y++;
|
||||
break;
|
||||
case EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y + 1) & 0x12c01e0) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + 2,
|
||||
y + 1) & 0x12c01e0) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -209,7 +216,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
x++;
|
||||
break;
|
||||
case SOUTH_EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y - 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c018f) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + 2,
|
||||
y) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x + 2,
|
||||
y - 1) & 0x12c0183) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -218,7 +230,9 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y--;
|
||||
break;
|
||||
case SOUTH:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + 1,
|
||||
y - 1) & 0x12c0183) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -226,7 +240,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y--;
|
||||
break;
|
||||
case SOUTH_WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x - 1,
|
||||
y) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x,
|
||||
y - 1) & 0x12c018f) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -235,7 +254,9 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y--;
|
||||
break;
|
||||
case WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c0138) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x - 1,
|
||||
y + 1) & 0x12c0138) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -243,7 +264,12 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
x--;
|
||||
break;
|
||||
case NORTH_WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c01e0) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x - 1,
|
||||
y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x,
|
||||
y + 2) & 0x12c01f8) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -257,19 +283,16 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks traversal for variable size entities.
|
||||
* @param points The points list.
|
||||
* @param directions The directions to check.
|
||||
* @param size The mover size.
|
||||
*/
|
||||
|
||||
private void checkVariableTraversal(List<Point> points, Direction[] directions, int size, ClipMaskSupplier clipMaskSupplier) {
|
||||
for (Direction dir : directions) {
|
||||
found = true;
|
||||
roar: switch (dir) {
|
||||
roar:
|
||||
switch (dir) {
|
||||
case NORTH:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (size - 1), y + size) & 0x12c01e0) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + (size - 1),
|
||||
y + size) & 0x12c01e0) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -283,12 +306,19 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y++;
|
||||
break;
|
||||
case NORTH_EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + size) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + size,
|
||||
y + size) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x + size,
|
||||
y + 1) & 0x12c01e3) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
for (int i = 1; i < size - 1; i++) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + (i + 1), y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + (i + 1)) & 0x12c01e3) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + (i + 1), y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + size,
|
||||
y + (i + 1)) & 0x12c01e3) != 0) {
|
||||
found = false;
|
||||
break roar;
|
||||
}
|
||||
|
|
@ -298,7 +328,9 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y++;
|
||||
break;
|
||||
case EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + size, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + (size - 1)) & 0x12c01e0) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + size, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + size,
|
||||
y + (size - 1)) & 0x12c01e0) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -312,12 +344,19 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
x++;
|
||||
break;
|
||||
case SOUTH_EAST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + (size - 2)) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y - 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c018f) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + size,
|
||||
y + (size - 2)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x + size,
|
||||
y - 1) & 0x12c0183) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
for (int i = 1; i < size - 1; i++) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + size, y + (i - 1)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (i + 1), y - 1) & 0x12c018f) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x + size, y + (i - 1)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + (i + 1),
|
||||
y - 1) & 0x12c018f) != 0) {
|
||||
found = false;
|
||||
break roar;
|
||||
}
|
||||
|
|
@ -327,7 +366,9 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y--;
|
||||
break;
|
||||
case SOUTH:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (size - 1), y - 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + (size - 1),
|
||||
y - 1) & 0x12c0183) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -341,12 +382,19 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y--;
|
||||
break;
|
||||
case SOUTH_WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 2)) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (size - 2), y - 1) & 0x12c0183) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 2)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x - 1,
|
||||
y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x + (size - 2),
|
||||
y - 1) & 0x12c018f) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
for (int i = 1; i < size - 1; i++) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i - 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (i - 1), y - 1) & 0x12c018f) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i - 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + (i - 1),
|
||||
y - 1) & 0x12c018f) != 0) {
|
||||
found = false;
|
||||
break roar;
|
||||
}
|
||||
|
|
@ -356,7 +404,9 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
y--;
|
||||
break;
|
||||
case WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 1)) & 0x12c0138) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x - 1,
|
||||
y + (size - 1)) & 0x12c0138) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -370,12 +420,19 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
x--;
|
||||
break;
|
||||
case NORTH_WEST:
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c01e0) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x - 1,
|
||||
y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(
|
||||
z,
|
||||
x,
|
||||
y + size) & 0x12c01f8) != 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
for (int i = 1; i < size - 1; i++) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i + 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (i - 1), y + size) & 0x12c01f8) != 0) {
|
||||
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i + 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
|
||||
x + i,
|
||||
y + size) & 0x12c01f8) != 0) {
|
||||
found = false;
|
||||
break roar;
|
||||
}
|
||||
|
|
@ -390,38 +447,32 @@ public final class DumbPathfinder extends Pathfinder {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the direction.
|
||||
* @param end The end direction.
|
||||
* @return The direction.
|
||||
*/
|
||||
|
||||
private static Direction[] getDirection(int startX, int startY, Location end) {
|
||||
int endX = end.getX();
|
||||
int endY = end.getY();
|
||||
if (startX == endX) {
|
||||
if (startY > endY) {
|
||||
return new Direction[] { Direction.SOUTH };
|
||||
return new Direction[]{Direction.SOUTH};
|
||||
} else if (startY < endY) {
|
||||
return new Direction[] { Direction.NORTH };
|
||||
return new Direction[]{Direction.NORTH};
|
||||
}
|
||||
} else if (startY == endY) {
|
||||
if (startX > endX) {
|
||||
return new Direction[] { Direction.WEST };
|
||||
return new Direction[]{Direction.WEST};
|
||||
}
|
||||
return new Direction[] { Direction.EAST };
|
||||
return new Direction[]{Direction.EAST};
|
||||
} else {
|
||||
if (startX < endX && startY < endY) {
|
||||
return new Direction[] { Direction.NORTH_EAST, Direction.EAST, Direction.NORTH };
|
||||
return new Direction[]{Direction.NORTH_EAST, Direction.EAST, Direction.NORTH};
|
||||
} else if (startX < endX && startY > endY) {
|
||||
return new Direction[] { Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH };
|
||||
return new Direction[]{Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH};
|
||||
} else if (startX > endX && startY < endY) {
|
||||
return new Direction[] { Direction.NORTH_WEST, Direction.WEST, Direction.NORTH };
|
||||
return new Direction[]{Direction.NORTH_WEST, Direction.WEST, Direction.NORTH};
|
||||
} else if (startX > endX && startY > endY) {
|
||||
return new Direction[] { Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH };
|
||||
return new Direction[]{Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH};
|
||||
}
|
||||
}
|
||||
return new Direction[0];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import core.game.node.entity.Entity;
|
|||
import core.game.node.entity.npc.NPC;
|
||||
import core.game.node.item.GroundItem;
|
||||
import core.game.node.scenery.Scenery;
|
||||
import core.game.world.map.Direction;
|
||||
import core.game.world.map.Location;
|
||||
import core.game.world.map.RegionManager;
|
||||
|
||||
public abstract class Pathfinder {
|
||||
|
||||
public static final int PREVENT_NORTH = 0x12c0120;
|
||||
public static final int PREVENT_EAST = 0x12c0180;
|
||||
public static final int PREVENT_NORTHEAST = 0x12c01e0;
|
||||
|
|
@ -18,182 +18,140 @@ public abstract class Pathfinder {
|
|||
public static final int PREVENT_WEST = 0x12c0108;
|
||||
public static final int PREVENT_SOUTHWEST = 0x12c010e;
|
||||
public static final int PREVENT_NORTHWEST = 0x12c0138;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The smart path finder.
|
||||
*/
|
||||
public static final SmartPathfinder SMART = new SmartPathfinder();
|
||||
|
||||
public static final Pathfinder SMART = new RsmodPathfinder();
|
||||
|
||||
/**
|
||||
* The dumb path finder.
|
||||
*/
|
||||
public static final DumbPathfinder DUMB = new DumbPathfinder();
|
||||
|
||||
public static final Pathfinder DUMB = new DumbPathfinder();
|
||||
|
||||
/**
|
||||
* The projectile path finder.
|
||||
*/
|
||||
public static final ProjectilePathfinder PROJECTILE = new ProjectilePathfinder();
|
||||
|
||||
/**
|
||||
* The south direction flag.
|
||||
*/
|
||||
public static final int SOUTH_FLAG = 0x1;
|
||||
|
||||
/**
|
||||
* The west direction flag.
|
||||
*/
|
||||
public static final int WEST_FLAG = 0x2;
|
||||
|
||||
/**
|
||||
* The north direction flag.
|
||||
*/
|
||||
public static final int NORTH_FLAG = 0x4;
|
||||
|
||||
/**
|
||||
* The east direction flag.
|
||||
*/
|
||||
public static final int EAST_FLAG = 0x8;
|
||||
|
||||
/**
|
||||
* The south-west direction flag.
|
||||
*/
|
||||
public static final int SOUTH_WEST_FLAG = SOUTH_FLAG | WEST_FLAG;
|
||||
|
||||
/**
|
||||
* The north-west direction flag.
|
||||
*/
|
||||
public static final int NORTH_WEST_FLAG = NORTH_FLAG | WEST_FLAG;
|
||||
|
||||
/**
|
||||
* The south-east direction flag.
|
||||
*/
|
||||
public static final int SOUTH_EAST_FLAG = SOUTH_FLAG | EAST_FLAG;
|
||||
|
||||
/**
|
||||
* The north-east direction flag.
|
||||
*/
|
||||
public static final int NORTH_EAST_FLAG = NORTH_FLAG | EAST_FLAG;
|
||||
|
||||
public static int flagForDirection(Direction d) {
|
||||
switch(d) {
|
||||
case NORTH_WEST: return NORTH_WEST_FLAG;
|
||||
case NORTH: return NORTH_FLAG;
|
||||
case NORTH_EAST: return NORTH_EAST_FLAG;
|
||||
case WEST: return WEST_FLAG;
|
||||
case EAST: return EAST_FLAG;
|
||||
case SOUTH_WEST: return SOUTH_WEST_FLAG;
|
||||
case SOUTH: return SOUTH_FLAG;
|
||||
case SOUTH_EAST: return SOUTH_EAST_FLAG;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Pathfinder PROJECTILE = new RsmodProjectilePathfinder();
|
||||
|
||||
/**
|
||||
* Finds a path from the location to the end location.
|
||||
* @param location The start location.
|
||||
* @param size The mover size.
|
||||
* @param end The end location.
|
||||
* @param sizeX The x-size of the destination node.
|
||||
* @param sizeY The y-size of the destination node.
|
||||
* @param rotation The object rotation.
|
||||
* @param type The object type.
|
||||
*
|
||||
* @param location The start location.
|
||||
* @param size The mover size.
|
||||
* @param end The end location.
|
||||
* @param sizeX The x-size of the destination node.
|
||||
* @param sizeY The y-size of the destination node.
|
||||
* @param rotation The object rotation.
|
||||
* @param type The object type.
|
||||
* @param walkingFlag The object walking flag.
|
||||
* @param near If we should find the nearest location if a path can't be
|
||||
* found.
|
||||
* @param near If we should find the nearest location if a path can't be
|
||||
* found.
|
||||
* @return The path.
|
||||
*/
|
||||
public abstract Path find(Location location, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier);
|
||||
|
||||
public abstract Path find(Location location,
|
||||
int size,
|
||||
Location end,
|
||||
int sizeX,
|
||||
int sizeY,
|
||||
int rotation,
|
||||
int type,
|
||||
int walkingFlag,
|
||||
boolean near,
|
||||
ClipMaskSupplier clipMaskSupplier);
|
||||
|
||||
/**
|
||||
* Finds a path from the start location to the end location.
|
||||
* @param mover The moving entity.
|
||||
*
|
||||
* @param mover The moving entity.
|
||||
* @param destination The destination node.
|
||||
* @return The path.
|
||||
*/
|
||||
public static Path find(Entity mover, Node destination) {
|
||||
return find(mover, destination, true, SMART);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds a path from the start location to the end location.
|
||||
* @param mover The moving entity.
|
||||
*
|
||||
* @param mover The moving entity.
|
||||
* @param destination The destination node.
|
||||
* @param near If we should move near the end location, if we can't reach
|
||||
* it.
|
||||
* @param finder The pathfinder to use.
|
||||
* @param near If we should move near the end location, if we can't reach
|
||||
* it.
|
||||
* @param finder The pathfinder to use.
|
||||
* @return The path.
|
||||
*/
|
||||
public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder) {
|
||||
ClipMaskSupplier cms = null;
|
||||
if (mover instanceof NPC) {
|
||||
cms = ((NPC) mover).behavior.getClippingSupplier(((NPC) mover));
|
||||
}
|
||||
if (cms == null)
|
||||
cms = RegionManager::getClippingFlag;
|
||||
ClipMaskSupplier cms = null;
|
||||
if (mover instanceof NPC) {
|
||||
NPC npc = (NPC) mover;
|
||||
cms = npc.behavior != null ? npc.behavior.getClippingSupplier(npc) : null;
|
||||
}
|
||||
return find(mover.getLocation(), mover.size(), destination, near, finder, cms);
|
||||
}
|
||||
|
||||
public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder){
|
||||
return find(mover.getLocation(),mover.size(),destination,near,finder, RegionManager::getWaterClipFlag);
|
||||
|
||||
public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder) {
|
||||
return find(mover.getLocation(), mover.size(), destination, near, finder, RegionManager::getWaterClipFlag);
|
||||
}
|
||||
|
||||
|
||||
public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) {
|
||||
return find(mover.getLocation(), mover.size(), destination, near, finder, clipMaskSupplier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds a path from the start location to the end location.
|
||||
*
|
||||
* @param destination The destination node.
|
||||
* @return The path.
|
||||
*/
|
||||
public static Path find(Location start, Node destination) {
|
||||
return find(start, destination, true, SMART);
|
||||
}
|
||||
|
||||
public static Path find(Location start, Node destination, int moverSize) {
|
||||
return find(start, moverSize, destination, true, SMART, RegionManager::getClippingFlag);
|
||||
}
|
||||
|
||||
|
||||
public static Path find(Location start, Node destination, int moverSize) {
|
||||
return find(start, moverSize, destination, true, SMART, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a path from the start location to the end location.
|
||||
*
|
||||
* @param destination The destination node.
|
||||
* @param near If we should move near the end location, if we can't reach
|
||||
* it.
|
||||
* @param finder The pathfinder to use.
|
||||
* @param near If we should move near the end location, if we can't reach
|
||||
* it.
|
||||
* @param finder The pathfinder to use.
|
||||
* @return The path.
|
||||
*/
|
||||
public static Path find(Location start, Node destination, boolean near, Pathfinder finder) {
|
||||
return find(start, 1, destination, near, finder, RegionManager::getClippingFlag);
|
||||
return find(start, 1, destination, near, finder, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds a path from the start location to the end location.
|
||||
*
|
||||
* @param destination The destination node.
|
||||
* @param near If we should move near the end location, if we can't reach
|
||||
* it.
|
||||
* @param finder The pathfinder to use.
|
||||
* @param near If we should move near the end location, if we can't reach
|
||||
* it.
|
||||
* @param finder The pathfinder to use.
|
||||
* @return The path.
|
||||
*/
|
||||
public static Path find(Location start, int moverSize, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) {
|
||||
if (destination instanceof Scenery) {
|
||||
Scenery object = (Scenery) destination;
|
||||
Scenery object = getRouteScenery((Scenery) destination);
|
||||
int type = object.getType();
|
||||
int rotation = object.getRotation();
|
||||
if (type == 10 || type == 11 || type == 22) {
|
||||
int sizeX = object.getDefinition().sizeX;
|
||||
int sizeY = object.getDefinition().sizeY;
|
||||
if (rotation % 2 != 0) {
|
||||
sizeX = object.getDefinition().sizeY;
|
||||
sizeY = object.getDefinition().sizeX;
|
||||
}
|
||||
int walkingFlag = object.getDefinition().getWalkingFlag();
|
||||
if (rotation != 0) {
|
||||
walkingFlag = (walkingFlag << rotation & 0xf) + (walkingFlag >> 4 - rotation);
|
||||
}
|
||||
return finder.find(start, moverSize, destination.getLocation(), sizeX, sizeY, 0, 0, walkingFlag, near, clipMaskSupplier);
|
||||
return finder.find(start,
|
||||
moverSize,
|
||||
object.getLocation(),
|
||||
object.getDefinition().sizeX,
|
||||
object.getDefinition().sizeY,
|
||||
rotation,
|
||||
type,
|
||||
object.getDefinition().getWalkingFlag(),
|
||||
near,
|
||||
clipMaskSupplier);
|
||||
}
|
||||
return finder.find(start, moverSize, destination.getLocation(), 0, 0, rotation, 1 + type, 0, near, clipMaskSupplier);
|
||||
return finder.find(start, moverSize, object.getLocation(), 0, 0, rotation, type, 0, near, clipMaskSupplier);
|
||||
}
|
||||
int size = 0;
|
||||
if (destination instanceof Entity) {
|
||||
|
|
@ -201,393 +159,83 @@ public abstract class Pathfinder {
|
|||
} else if (destination instanceof GroundItem && !RegionManager.isTeleportPermitted(destination.getLocation())) {
|
||||
size = 1;
|
||||
}
|
||||
return finder.find(start, moverSize, destination.getLocation(), size, size, 0, 0, 0, near, clipMaskSupplier);
|
||||
return finder.find(start, moverSize, destination.getLocation(), size, size, 0, -1, 0, near, clipMaskSupplier);
|
||||
}
|
||||
|
||||
|
||||
private static Scenery getRouteScenery(Scenery object) {
|
||||
Scenery wrapper = object.getWrapper();
|
||||
if (wrapper == object) {
|
||||
return object;
|
||||
}
|
||||
if (getFootprintArea(wrapper) <= getFootprintArea(object)) {
|
||||
return object;
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private static int getFootprintArea(Scenery object) {
|
||||
return object.getDefinition().sizeX * object.getDefinition().sizeY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if interaction with decoration is possible.
|
||||
* @param curX The current x-coordinate in viewport.
|
||||
* @param curY The current y-coordinate in viewport.
|
||||
* @param size The mover size.
|
||||
* @param destX The destination x-coordinate in viewport.
|
||||
* @param destY The destination y-coordinate in viewport.
|
||||
* @param type The object type.
|
||||
*
|
||||
* @param curX The current x-coordinate in viewport.
|
||||
* @param curY The current y-coordinate in viewport.
|
||||
* @param size The mover size.
|
||||
* @param destX The destination x-coordinate in viewport.
|
||||
* @param destY The destination y-coordinate in viewport.
|
||||
* @param type The object type.
|
||||
* @param rotation The object rotation.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public static boolean canDecorationInteract(int curX, int curY, int size, int destX, int destY, int rotation, int type, int z, ClipMaskSupplier clipMaskSupplier) {
|
||||
if (size != 1) {
|
||||
if (destX >= curX && destX <= (curX + size) - 1 && destY <= (destY + size) - 1) {
|
||||
return true;
|
||||
}
|
||||
} else if (destX == curX && curY == destY) {
|
||||
return true;
|
||||
}
|
||||
if (size == 1) {
|
||||
int flag = clipMaskSupplier.getClippingFlag(z, curX, curY);
|
||||
if (type == 6 || type == 7) {
|
||||
if (type == 7) {
|
||||
rotation = rotation + 2 & 0x3;
|
||||
}
|
||||
if (rotation == 0) {
|
||||
if (curX == 1 + destX && curY == destY && (0x80 & flag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && curY == destY - 1 && (flag & 0x2) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 1) {
|
||||
if (curX == destX - 1 && curY == destY && (0x8 & flag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && curY == destY - 1 && (flag & 0x2) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 2) {
|
||||
if (destX - 1 == curX && destY == curY && (flag & 0x8) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && destY + 1 == curY && (0x20 & flag) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 3) {
|
||||
if (destX + 1 == curX && curY == destY && (0x80 & flag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && curY == destY + 1 && (0x20 & flag) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == 8) {
|
||||
if (destX == curX && curY == destY + 1 && (flag & 0x20) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && -1 + destY == curY && (0x2 & flag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX - 1 && curY == destY && (0x8 & flag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX + 1 && curY == destY && (flag & 0x80) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int cornerX = curX + size - 1;
|
||||
int cornerY = curY + size - 1;
|
||||
if (type == 6 || type == 7) {
|
||||
if (type == 7) {
|
||||
rotation = 0x3 & 2 + rotation;
|
||||
}
|
||||
if (rotation == 0) {
|
||||
if (destX + 1 == curX && destY >= curY && destY <= cornerY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x80) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && destX <= cornerX && destY - size == curY && (0x2 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 1) {
|
||||
if (-size + destX == curX && destY >= curY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x8) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && cornerX >= destX && -size + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, cornerY) & 0x2) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 2) {
|
||||
if (curX == destX - size && curY <= destY && destY <= cornerY && (0x8 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && cornerX >= destX && destY + 1 == curY && (0x20 & clipMaskSupplier.getClippingFlag(z, destX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 3) {
|
||||
if (1 + destX == curX && curY <= destY && destY <= cornerY && (0x80 & clipMaskSupplier.getClippingFlag(z, curX, destY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && destX <= cornerX && 1 + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x20) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == 8) {
|
||||
if (curX <= destX && destX <= cornerX && 1 + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x20) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && destX <= cornerX && curY == -size + destY && (0x2 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == -size + destX && destY >= curY && destY <= cornerY && (0x8 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (1 + destX == curX && curY <= destY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x80) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public static boolean canDecorationInteract(int curX,
|
||||
int curY,
|
||||
int size,
|
||||
int destX,
|
||||
int destY,
|
||||
int rotation,
|
||||
int type,
|
||||
int z,
|
||||
ClipMaskSupplier clipMaskSupplier) {
|
||||
return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if interaction with a door is possible.
|
||||
* @param curX The current x-coordinate in viewport.
|
||||
* @param curY The current y-coordinate in viewport.
|
||||
* @param size The mover size.
|
||||
* @param destX The destination x-coordinate in viewport.
|
||||
* @param destY The destination y-coordinate in viewport.
|
||||
* @param type The object type.
|
||||
*
|
||||
* @param curX The current x-coordinate in viewport.
|
||||
* @param curY The current y-coordinate in viewport.
|
||||
* @param size The mover size.
|
||||
* @param destX The destination x-coordinate in viewport.
|
||||
* @param destY The destination y-coordinate in viewport.
|
||||
* @param type The object type.
|
||||
* @param rotation The object rotation.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public static boolean canDoorInteract(int curX, int curY, int size, int destX, int destY, int type, int rotation, int z, ClipMaskSupplier clipMaskSupplier) {
|
||||
if (size != 1) {
|
||||
if (destX >= curX && destX <= size + curX - 1 && destY <= destY + size - 1) {
|
||||
return true;
|
||||
}
|
||||
} else if (curX == destX && destY == curY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (size == 1) {
|
||||
if (type == 0) {
|
||||
if (rotation == 0) {
|
||||
if (curX == destX - 1 && destY == curY) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && destY - 1 == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 1) {
|
||||
if (curX == destX && destY + 1 == curY) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX - 1 && curY == destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == 1 + destX && destY == curY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 2) {
|
||||
if (1 + destX == curX && destY == curY) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && curY == destY - 1 && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 3) {
|
||||
if (curX == destX && -1 + destY == curY) {
|
||||
return true;
|
||||
}
|
||||
if (curX == -1 + destX && destY == curY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == 1 + destX && destY == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0180) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (type == 2) {
|
||||
if (rotation == 0) {
|
||||
if (destX - 1 == curX && curY == destY) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && curY == 1 + destY) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX + 1 && curY == destY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && destY - 1 == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 1) {
|
||||
if (curX == destX - 1 && curY == destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && curY == 1 + destY) {
|
||||
return true;
|
||||
}
|
||||
if (1 + destX == curX && curY == destY) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && destY - 1 == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 2) {
|
||||
if (destX - 1 == curX && destY == curY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (1 + destX == curX && curY == destY) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && curY == destY - 1) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 3) {
|
||||
if (destX - 1 == curX && curY == destY) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && curY == destY + 1 && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == 1 + destX && curY == destY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0180) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX == curX && destY - 1 == curY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (type == 9) {
|
||||
if (curX == destX && curY == destY + 1 && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x20) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX && curY == destY - 1 && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x2) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX - 1 && curY == destY && (0x8 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX + 1 == curX && curY == destY && (0x80 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int cornerX = curX - (1 - size);
|
||||
int cornerY = -1 + curY + size;
|
||||
if (type == 0) {
|
||||
if (rotation == 0) {
|
||||
if (destX - size == curX && destY >= curY && destY <= cornerY) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && cornerX >= destX && curY == 1 + destY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x12c0120) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && cornerX >= destX && destY - size == curY && (clipMaskSupplier.getClippingFlag(z, destX, cornerY) & 0x12c0102) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 1) {
|
||||
if (destX >= curX && cornerX >= destX && destY + 1 == curY) {
|
||||
return true;
|
||||
}
|
||||
if (curX == -size + destX && destY >= curY && cornerY >= destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == 1 + destX && destY >= curY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x12c0180) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 2) {
|
||||
if (curX == 1 + destX && curY <= destY && destY <= cornerY) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && cornerX >= destX && destY + 1 == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, destX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && destX <= cornerX && destY - size == curY && (0x12c0102 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 3) {
|
||||
if (curX <= destX && destX <= cornerX && curY == -size + destY) {
|
||||
return true;
|
||||
}
|
||||
if (-size + destX == curX && curY <= destY && destY <= cornerY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x12c0108) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (1 + destX == curX && curY <= destY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x12c0180) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == 2) {
|
||||
if (rotation == 0) {
|
||||
if (destX - size == curX && curY <= destY && destY <= cornerY) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && destX <= cornerX && curY == 1 + destY) {
|
||||
return true;
|
||||
}
|
||||
if (curX == 1 + destX && curY <= destY && destY <= cornerY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, destY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && cornerX >= destX && -size + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, cornerY) & 0x12c0102) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 1) {
|
||||
if (-size + destX == curX && destY >= curY && destY <= cornerY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x12c0108) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && cornerX >= destX && curY == 1 + destY) {
|
||||
return true;
|
||||
}
|
||||
if (destX + 1 == curX && curY <= destY && destY <= cornerY) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && cornerX >= destX && destY + -size == curY && (0x12c0102 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 2) {
|
||||
if (curX == destX - size && curY <= destY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x12c0108) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && destX <= cornerX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, destX, curY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (1 + destX == curX && destY >= curY && cornerY >= destY) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && destX <= cornerX && curY == -size + destY) {
|
||||
return true;
|
||||
}
|
||||
} else if (rotation == 3) {
|
||||
if (destX + -size == curX && destY >= curY && destY <= cornerY) {
|
||||
return true;
|
||||
}
|
||||
if (curX <= destX && cornerX >= destX && curY == 1 + destY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x12c0120) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (1 + destX == curX && destY >= curY && cornerY >= destY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, destY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && destX <= cornerX && curY == -size + destY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == 9) {
|
||||
if (destX >= curX && destX <= cornerX && curY == 1 + destY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x12c0120) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (destX >= curX && cornerX >= destX && curY == -size + destY && (0x12c0102 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (-size + destX == curX && destY >= curY && cornerY >= destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (curX == destX + 1 && destY >= curY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x12c0180) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public static boolean canDoorInteract(int curX,
|
||||
int curY,
|
||||
int size,
|
||||
int destX,
|
||||
int destY,
|
||||
int type,
|
||||
int rotation,
|
||||
int z,
|
||||
ClipMaskSupplier clipMaskSupplier) {
|
||||
return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the mover is standing on the destination.
|
||||
* @param x The current x-location (in viewport).
|
||||
* @param y The current y-location (in viewport).
|
||||
*
|
||||
* @param x The current x-location (in viewport).
|
||||
* @param y The current y-location (in viewport).
|
||||
* @param moverSizeX The mover x size.
|
||||
* @param moverSizeY The mover y size.
|
||||
* @param destX The destination x-location in viewport.
|
||||
* @param destY The destination y-location in viewport.
|
||||
* @param sizeX The destination node x-size.
|
||||
* @param sizeY The destination node y-size.
|
||||
* @param destX The destination x-location in viewport.
|
||||
* @param destY The destination y-location in viewport.
|
||||
* @param sizeX The destination node x-size.
|
||||
* @param sizeY The destination node y-size.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public static boolean isStandingIn(int x, int y, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY) {
|
||||
|
|
@ -599,120 +247,52 @@ public abstract class Pathfinder {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if interaction is possible from the current location.
|
||||
* @param x The current x-location (in viewport).
|
||||
* @param y The current y-location (in viewport).
|
||||
*
|
||||
* @param x The current x-location (in viewport).
|
||||
* @param y The current y-location (in viewport).
|
||||
* @param moverSize The mover size.
|
||||
* @param destX The destination x-location in viewport.
|
||||
* @param destY The destination y-location in viewport.
|
||||
* @param sizeX The destination node x-size.
|
||||
* @param sizeY The destination node y-size.
|
||||
* @param walkFlag The walking flag.
|
||||
* @param destX The destination x-location in viewport.
|
||||
* @param destY The destination y-location in viewport.
|
||||
* @param sizeX The destination node x-size.
|
||||
* @param sizeY The destination node y-size.
|
||||
* @param walkFlag The walking flag.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public static boolean canInteract(int x, int y, int moverSize, int destX, int destY, int sizeX, int sizeY, int walkFlag, int z, ClipMaskSupplier clipMaskSupplier) {
|
||||
if (moverSize > 1) {
|
||||
return isStandingIn(x, y, moverSize, moverSize, destX, destY, sizeX, sizeY) || canInteractSized(x, y, moverSize, moverSize, destX, destY, sizeX, sizeY, walkFlag, z);
|
||||
}
|
||||
int flag = clipMaskSupplier.getClippingFlag(z, x, y);
|
||||
int cornerX = destX + sizeX - 1;
|
||||
int cornerY = destY + sizeY - 1;
|
||||
if (destX <= x && cornerX >= x && y >= destY && y <= cornerY) {
|
||||
return true;
|
||||
}
|
||||
if (x == destX - 1 && destY <= y && y <= cornerY && (0x8 & flag) == 0 && (0x8 & walkFlag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (x == cornerX + 1 && destY <= y && cornerY >= y && (flag & 0x80) == 0 && (0x2 & walkFlag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (y == destY - 1 && destX <= x && cornerX >= x && (0x2 & flag) == 0 && (0x4 & walkFlag) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (y == cornerY + 1 && destX <= x && cornerX >= x && (flag & 0x20) == 0 && (0x1 & walkFlag) == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public static boolean canInteract(int x,
|
||||
int y,
|
||||
int moverSize,
|
||||
int destX,
|
||||
int destY,
|
||||
int sizeX,
|
||||
int sizeY,
|
||||
int walkFlag,
|
||||
int z,
|
||||
ClipMaskSupplier clipMaskSupplier) {
|
||||
return RsmodPathfinder.canReach(x, y, moverSize, destX, destY, sizeX, sizeY, 0, -1, walkFlag, z, clipMaskSupplier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if interaction is possible from the current location.
|
||||
*
|
||||
* @param destX The destination x-location in viewport.
|
||||
* @param destY The destination y-location in viewport.
|
||||
* @param sizeX The destination node x-size.
|
||||
* @param sizeY The destination node y-size.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public static boolean canInteractSized(int curX, int curY, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY, int walkingFlag, int z) {
|
||||
int fromCornerY = curY + moverSizeY;
|
||||
int fromCornerX = curX + moverSizeX;
|
||||
int toCornerX = sizeX + destX;
|
||||
int toCornerY = sizeY + destY;
|
||||
if (destX <= curX && curX < toCornerX) {
|
||||
if (destY == fromCornerY && (walkingFlag & 0x4) == 0) {
|
||||
int x = curX;
|
||||
for (int endX = toCornerX < fromCornerX ? toCornerX : fromCornerX; endX > x; x++) {
|
||||
if ((RegionManager.getClippingFlag(z, x, -1 + fromCornerY) & 0x2) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (toCornerY == curY && (walkingFlag & 0x1) == 0) {
|
||||
int x = curX;
|
||||
for (int endX = fromCornerX <= toCornerX ? fromCornerX : toCornerX; x < endX; x++) {
|
||||
if ((RegionManager.getClippingFlag(z, x, curY) & 0x20) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (destX < fromCornerX && toCornerX >= fromCornerX) {
|
||||
if (fromCornerY == destY && (0x4 & walkingFlag) == 0) {
|
||||
for (int x = destX; fromCornerX > x; x++) {
|
||||
if ((RegionManager.getClippingFlag(z, x, -1 + (fromCornerY)) & 0x2) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (toCornerY == curY && (0x1 & walkingFlag) == 0) {
|
||||
for (int x = destX; fromCornerX > x; x++) {
|
||||
if ((RegionManager.getClippingFlag(z, x, curY) & 0x20) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (curY < destY || curY >= toCornerY) {
|
||||
if (fromCornerY > destY && toCornerY >= fromCornerY) {
|
||||
if (fromCornerX == destX && (walkingFlag & 0x8) == 0) {
|
||||
for (int y = destY; y < fromCornerY; y++) {
|
||||
if ((RegionManager.getClippingFlag(z, -1 + fromCornerX, y) & 0x8) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (curX == toCornerX && (0x2 & walkingFlag) == 0) {
|
||||
for (int y = destY; fromCornerY > y; y++) {
|
||||
if ((RegionManager.getClippingFlag(z, curX, y) & 0x80) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (destX != fromCornerX || (0x8 & walkingFlag) != 0) {
|
||||
if (curX == toCornerX && (walkingFlag & 0x2) == 0) {
|
||||
int y = curY;
|
||||
for (int endY = fromCornerY <= toCornerY ? fromCornerY : toCornerY; y < endY; y++) {
|
||||
if ((0x80 & RegionManager.getClippingFlag(z, curX, y)) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int y = curY;
|
||||
for (int endY = fromCornerY > toCornerY ? toCornerY : fromCornerY; endY > y; y++) {
|
||||
if ((RegionManager.getClippingFlag(z, fromCornerX - 1, y) & 0x8) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public static boolean canInteractSized(int curX,
|
||||
int curY,
|
||||
int moverSizeX,
|
||||
int moverSizeY,
|
||||
int destX,
|
||||
int destY,
|
||||
int sizeX,
|
||||
int sizeY,
|
||||
int walkingFlag,
|
||||
int z) {
|
||||
return RsmodPathfinder.canReach(curX, curY, moverSizeX, destX, destY, sizeX, sizeY, 0, -1, walkingFlag, z, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,200 +0,0 @@
|
|||
package core.game.world.map.path;
|
||||
|
||||
import core.game.world.map.Direction;
|
||||
import core.game.world.map.Location;
|
||||
import core.game.world.map.Point;
|
||||
import core.game.world.map.RegionManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A pathfinder implementation used for checking projectile paths.
|
||||
* @author Emperor
|
||||
*/
|
||||
public final class ProjectilePathfinder extends Pathfinder {
|
||||
|
||||
/**
|
||||
* If a path can be found.
|
||||
*/
|
||||
private boolean found;
|
||||
|
||||
/**
|
||||
* The plane.
|
||||
*/
|
||||
private int z;
|
||||
|
||||
/**
|
||||
* The x-coordinate.
|
||||
*/
|
||||
private int x;
|
||||
|
||||
/**
|
||||
* The y-coordinate.
|
||||
*/
|
||||
private int y;
|
||||
|
||||
@Override
|
||||
public Path find(Location start, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier) {
|
||||
Path path = new Path();
|
||||
z = start.getZ();
|
||||
x = start.getX();
|
||||
y = start.getY();
|
||||
List<Point> points = new ArrayList<>(20);
|
||||
path.setSuccesful(true);
|
||||
while (x != end.getX() || y != end.getY()) {
|
||||
Direction[] directions = getDirection(x, y, end);
|
||||
found = true;
|
||||
checkSingleTraversal(points, directions);
|
||||
if (!found) {
|
||||
path.setMoveNear(x != start.getX() || y != start.getY());
|
||||
path.setSuccesful(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!points.isEmpty()) {
|
||||
for (int i = 0; i < points.size() - 1; i++) {
|
||||
Point p = points.get(i);
|
||||
if (p.getDirection() != null) {
|
||||
path.getPoints().add(p);
|
||||
}
|
||||
}
|
||||
path.getPoints().add(points.get(points.size() - 1));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks traversal for a size 1 entity.
|
||||
*
|
||||
* @param points The points list.
|
||||
* @param directions The directions.
|
||||
*/
|
||||
private void checkSingleTraversal(List<Point> points, Direction... directions) {
|
||||
dir:
|
||||
for (Direction dir : directions) {
|
||||
found = true;
|
||||
switch (dir) {
|
||||
case NORTH:
|
||||
if (flagged(z, x, y + 1, 0x12c0120)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x, y + 1, dir));
|
||||
y++;
|
||||
break;
|
||||
case NORTH_EAST:
|
||||
if (flagged(z, x + 1, y, 0x12c0180)
|
||||
|| flagged(z, x, y + 1, 0x12c0120)
|
||||
|| flagged(z, x + 1, y + 1, 0x12c01e0)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x + 1, y + 1, dir));
|
||||
x++;
|
||||
y++;
|
||||
break;
|
||||
case EAST:
|
||||
if (flagged(z, x + 1, y, 0x12c0180)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x + 1, y, dir));
|
||||
x++;
|
||||
break;
|
||||
case SOUTH_EAST:
|
||||
if (flagged(z, x + 1, y, 0x12c0180)
|
||||
|| flagged(z, x, y - 1, 0x12c0102)
|
||||
|| flagged(z, x + 1, y - 1, 0x12c0183)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x + 1, y - 1, dir));
|
||||
x++;
|
||||
y--;
|
||||
break;
|
||||
case SOUTH:
|
||||
if (flagged(z, x, y - 1, 0x12c0102)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x, y - 1, dir));
|
||||
y--;
|
||||
break;
|
||||
case SOUTH_WEST:
|
||||
if (flagged(z, x - 1, y, 0x12c0108)
|
||||
|| flagged(z, x, y - 1, 0x12c0102)
|
||||
|| flagged(z, x - 1, y - 1, 0x12c010e)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x - 1, y - 1, dir));
|
||||
x--;
|
||||
y--;
|
||||
break;
|
||||
case WEST:
|
||||
if (flagged(z, x - 1, y, 0x12c0108)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x - 1, y, dir));
|
||||
x--;
|
||||
break;
|
||||
case NORTH_WEST:
|
||||
if (flagged(z, x - 1, y, 0x12c0108)
|
||||
|| flagged(z, x, y + 1, 0x12c0120)
|
||||
|| flagged(z, x - 1, y + 1, 0x12c0138)) {
|
||||
found = false;
|
||||
break dir;
|
||||
}
|
||||
points.add(new Point(x - 1, y + 1, dir));
|
||||
x--;
|
||||
y++;
|
||||
break;
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean flagged(int z, int x, int y, int pFlagMask) {
|
||||
int pFlag = RegionManager.getProjectileFlag(z, x, y);
|
||||
return (pFlag & pFlagMask) != 0 || (pFlag & 0x20000) != 0 || (RegionManager.getClippingFlag(z, x, y) & 0x20000) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the direction.
|
||||
* @param startX The startX.
|
||||
* @param startY The startY.
|
||||
* @param end The end direction.
|
||||
* @return The direction.
|
||||
*/
|
||||
private static Direction[] getDirection(int startX, int startY, Location end) {
|
||||
int endX = end.getX();
|
||||
int endY = end.getY();
|
||||
if (startX == endX) {
|
||||
if (startY > endY) {
|
||||
return new Direction[] { Direction.SOUTH };
|
||||
} else if (startY < endY) {
|
||||
return new Direction[] { Direction.NORTH };
|
||||
}
|
||||
} else if (startY == endY) {
|
||||
if (startX > endX) {
|
||||
return new Direction[] { Direction.WEST };
|
||||
}
|
||||
return new Direction[] { Direction.EAST };
|
||||
} else {
|
||||
if (startX < endX && startY < endY) {
|
||||
return new Direction[] { Direction.NORTH_EAST, Direction.EAST, Direction.NORTH };
|
||||
} else if (startX < endX && startY > endY) {
|
||||
return new Direction[] { Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH };
|
||||
} else if (startX > endX && startY < endY) {
|
||||
return new Direction[] { Direction.NORTH_WEST, Direction.WEST, Direction.NORTH };
|
||||
} else if (startX > endX && startY > endY) {
|
||||
return new Direction[] { Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH };
|
||||
}
|
||||
}
|
||||
return new Direction[0];
|
||||
}
|
||||
}
|
||||
361
Server/src/main/core/game/world/map/path/RsmodPathfinder.kt
Normal file
361
Server/src/main/core/game/world/map/path/RsmodPathfinder.kt
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
package core.game.world.map.path
|
||||
|
||||
import core.ServerConstants
|
||||
import core.api.utils.Vector
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.Point
|
||||
import core.game.world.map.RegionManager
|
||||
import org.rsmod.game.pathfinder.LinePathFinder
|
||||
import org.rsmod.game.pathfinder.LineValidator
|
||||
import org.rsmod.game.pathfinder.PathFinder
|
||||
import org.rsmod.game.pathfinder.RayCast
|
||||
import org.rsmod.game.pathfinder.collision.CollisionFlagMap
|
||||
import org.rsmod.game.pathfinder.reach.ReachStrategy
|
||||
import kotlin.math.floor
|
||||
|
||||
private const val SEARCH_MAP_SIZE = 128
|
||||
private const val RING_BUFFER_SIZE = 4096
|
||||
|
||||
class RsmodPathfinder(private val maxWaypoints: Int = 25) : Pathfinder() {
|
||||
|
||||
private val defaultFinder = ThreadLocal.withInitial {
|
||||
PathFinder(RegionManager.RSMOD_CLIPPING_FLAGS, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
|
||||
}
|
||||
private val suppliedFinder = ThreadLocal.withInitial { RouteFinderState() }
|
||||
|
||||
override fun find(
|
||||
start: Location?,
|
||||
moverSize: Int,
|
||||
dest: Location?,
|
||||
sizeX: Int,
|
||||
sizeY: Int,
|
||||
rotation: Int,
|
||||
type: Int,
|
||||
walkingFlag: Int,
|
||||
near: Boolean,
|
||||
clipMaskSupplier: ClipMaskSupplier?,
|
||||
): Path {
|
||||
val source = requireNotNull(start)
|
||||
val destination = requireNotNull(dest)
|
||||
val path = Path()
|
||||
var end = destination
|
||||
val vector = Vector.betweenLocs(source, destination)
|
||||
val magnitude = floor(vector.magnitude())
|
||||
|
||||
if (magnitude > ServerConstants.MAX_PATHFIND_DISTANCE) {
|
||||
if (canAttempt(source, destination)) {
|
||||
end =
|
||||
source.transform(
|
||||
vector.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1)
|
||||
)
|
||||
} else {
|
||||
path.isMoveNear = true
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
val shape = routeShape(type, sizeX, sizeY)
|
||||
val finder =
|
||||
if (clipMaskSupplier == null) {
|
||||
RegionManager.loadClippingWindow(source, SEARCH_MAP_SIZE)
|
||||
defaultFinder.get()
|
||||
} else {
|
||||
val state = suppliedFinder.get()
|
||||
state.loadCollisionWindow(source, clipMaskSupplier)
|
||||
state.finder
|
||||
}
|
||||
val route =
|
||||
finder.findPath(
|
||||
level = source.z,
|
||||
srcX = source.x,
|
||||
srcZ = source.y,
|
||||
destX = end.x,
|
||||
destZ = end.y,
|
||||
srcSize = moverSize,
|
||||
destWidth = if (sizeX == 0) 1 else sizeX,
|
||||
destHeight = if (sizeY == 0) 1 else sizeY,
|
||||
objRot = rotation,
|
||||
objShape = shape,
|
||||
moveNear = near,
|
||||
blockAccessFlags = walkingFlag,
|
||||
maxWaypoints = maxWaypoints,
|
||||
)
|
||||
|
||||
if (route.failed) {
|
||||
return path
|
||||
}
|
||||
|
||||
var currentX = source.x
|
||||
var currentY = source.y
|
||||
path.points.add(Point(currentX, currentY))
|
||||
for (waypoint in route.waypoints) {
|
||||
while (currentX != waypoint.x || currentY != waypoint.z) {
|
||||
currentX += waypoint.x.compareTo(currentX)
|
||||
currentY += waypoint.z.compareTo(currentY)
|
||||
path.points.add(Point(currentX, currentY))
|
||||
}
|
||||
}
|
||||
path.setSuccesful(true)
|
||||
path.isMoveNear = route.alternative || end != destination
|
||||
return path
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun canAttempt(start: Location, dest: Location): Boolean {
|
||||
val distance = floor(Vector.betweenLocs(start, dest).magnitude())
|
||||
return distance < ServerConstants.MAX_PATHFIND_DISTANCE * 2.0
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun canReach(
|
||||
srcX: Int,
|
||||
srcY: Int,
|
||||
moverSize: Int,
|
||||
destX: Int,
|
||||
destY: Int,
|
||||
destWidth: Int,
|
||||
destHeight: Int,
|
||||
rotation: Int,
|
||||
type: Int,
|
||||
walkingFlag: Int,
|
||||
z: Int,
|
||||
clipMaskSupplier: ClipMaskSupplier?,
|
||||
): Boolean {
|
||||
if (clipMaskSupplier == null) {
|
||||
RegionManager.loadClippingWindow(Location.create(srcX, srcY, z), SEARCH_MAP_SIZE)
|
||||
return ReachStrategy.reached(
|
||||
flags = RegionManager.RSMOD_CLIPPING_FLAGS,
|
||||
level = z,
|
||||
srcX = srcX,
|
||||
srcZ = srcY,
|
||||
destX = destX,
|
||||
destZ = destY,
|
||||
destWidth = if (destWidth == 0) 1 else destWidth,
|
||||
destHeight = if (destHeight == 0) 1 else destHeight,
|
||||
srcSize = moverSize,
|
||||
objRot = rotation,
|
||||
objShape = routeShape(type, destWidth, destHeight),
|
||||
blockAccessFlags = walkingFlag,
|
||||
)
|
||||
}
|
||||
// Reach checks only read tiles within the source/destination rectangles and
|
||||
// their cross-axis combinations, so loading just their padded bounding box
|
||||
// into a reused map produces the same reads as a freshly allocated full map.
|
||||
val flags = suppliedReachFlags.get()
|
||||
val destSize =
|
||||
maxOf(if (destWidth == 0) 1 else destWidth, if (destHeight == 0) 1 else destHeight)
|
||||
val minX = maxOf(0, minOf(srcX, destX) - 1)
|
||||
val minY = maxOf(0, minOf(srcY, destY) - 1)
|
||||
val maxX = maxOf(srcX + moverSize, destX + destSize) + 1
|
||||
val maxY = maxOf(srcY + moverSize, destY + destSize) + 1
|
||||
try {
|
||||
for (x in minX..maxX) {
|
||||
for (y in minY..maxY) {
|
||||
flags[x, y, z] = clipMaskSupplier.getClippingFlag(z, x, y)
|
||||
}
|
||||
}
|
||||
return ReachStrategy.reached(
|
||||
flags = flags,
|
||||
level = z,
|
||||
srcX = srcX,
|
||||
srcZ = srcY,
|
||||
destX = destX,
|
||||
destZ = destY,
|
||||
destWidth = if (destWidth == 0) 1 else destWidth,
|
||||
destHeight = if (destHeight == 0) 1 else destHeight,
|
||||
srcSize = moverSize,
|
||||
objRot = rotation,
|
||||
objShape = routeShape(type, destWidth, destHeight),
|
||||
blockAccessFlags = walkingFlag,
|
||||
)
|
||||
} finally {
|
||||
for (zoneX in (minX shr 3)..(maxX shr 3)) {
|
||||
for (zoneY in (minY shr 3)..(maxY shr 3)) {
|
||||
flags.deallocateIfPresent(zoneX shl 3, zoneY shl 3, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun lineOfSight(
|
||||
start: Location,
|
||||
dest: Location,
|
||||
moverSize: Int,
|
||||
destWidth: Int,
|
||||
destHeight: Int,
|
||||
): RayCast {
|
||||
RegionManager.loadClippingWindow(start, SEARCH_MAP_SIZE)
|
||||
return lineOfSightLoaded(start, dest, moverSize, destWidth, destHeight)
|
||||
}
|
||||
|
||||
private fun lineOfSightLoaded(
|
||||
start: Location,
|
||||
dest: Location,
|
||||
moverSize: Int,
|
||||
destWidth: Int,
|
||||
destHeight: Int,
|
||||
): RayCast {
|
||||
return projectileLineFinder
|
||||
.get()
|
||||
.lineOfSight(
|
||||
level = start.z,
|
||||
srcX = start.x,
|
||||
srcZ = start.y,
|
||||
destX = dest.x,
|
||||
destZ = dest.y,
|
||||
srcSize = moverSize,
|
||||
destWidth = destWidth.coerceAtLeast(1),
|
||||
destHeight = destHeight.coerceAtLeast(1),
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasLineOfSight(
|
||||
start: Location,
|
||||
dest: Location,
|
||||
moverSize: Int,
|
||||
destWidth: Int,
|
||||
destHeight: Int,
|
||||
maxRaySteps: Int = Int.MAX_VALUE,
|
||||
): Boolean {
|
||||
val rayCast = lineOfSight(start, dest, moverSize, destWidth, destHeight)
|
||||
return rayCast.success && rayCast.coordinates.size <= maxRaySteps
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasLineOfSightBetween(
|
||||
sourceLocation: Location,
|
||||
sourceSize: Int,
|
||||
targetLocation: Location,
|
||||
targetSize: Int,
|
||||
maxRaySteps: Int = Int.MAX_VALUE,
|
||||
): Boolean {
|
||||
RegionManager.loadClippingWindow(sourceLocation, SEARCH_MAP_SIZE)
|
||||
return hasLineOfSightBetweenLoaded(
|
||||
sourceLocation,
|
||||
sourceSize,
|
||||
targetLocation,
|
||||
targetSize,
|
||||
maxRaySteps,
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadLineOfSightWindow(center: Location) {
|
||||
RegionManager.loadClippingWindow(center, SEARCH_MAP_SIZE)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasLineOfSightBetweenLoaded(
|
||||
sourceLocation: Location,
|
||||
sourceSize: Int,
|
||||
targetLocation: Location,
|
||||
targetSize: Int,
|
||||
maxRaySteps: Int = Int.MAX_VALUE,
|
||||
): Boolean {
|
||||
if (sourceSize == 1 && targetSize == 1) {
|
||||
if (maxRaySteps == Int.MAX_VALUE) {
|
||||
return hasSingleTileLineOfSight(sourceLocation, targetLocation)
|
||||
}
|
||||
if (maxRaySteps == 1) {
|
||||
if (manhattanDistance(sourceLocation, targetLocation) > 1) {
|
||||
return false
|
||||
}
|
||||
return hasSingleTileLineOfSight(sourceLocation, targetLocation)
|
||||
}
|
||||
}
|
||||
for (sourceX in 0 until sourceSize) {
|
||||
for (sourceY in 0 until sourceSize) {
|
||||
val source = sourceLocation.transform(sourceX, sourceY, 0)
|
||||
for (targetX in 0 until targetSize) {
|
||||
for (targetY in 0 until targetSize) {
|
||||
if (maxRaySteps == 1) {
|
||||
// A ray between distinct tiles emits a coordinate per axis
|
||||
// step, so tiles further than one orthogonal step apart can
|
||||
// never satisfy a single-step ray.
|
||||
val manhattan =
|
||||
kotlin.math.abs(source.x - (targetLocation.x + targetX)) +
|
||||
kotlin.math.abs(source.y - (targetLocation.y + targetY))
|
||||
if (manhattan > 1) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
val destination = targetLocation.transform(targetX, targetY, 0)
|
||||
val rayCast = lineOfSightLoaded(source, destination, 1, 1, 1)
|
||||
if (rayCast.success && rayCast.coordinates.size <= maxRaySteps) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasSingleTileLineOfSight(
|
||||
sourceLocation: Location,
|
||||
targetLocation: Location,
|
||||
): Boolean {
|
||||
return projectileLineValidator
|
||||
.get()
|
||||
.hasLineOfSight(
|
||||
level = sourceLocation.z,
|
||||
srcX = sourceLocation.x,
|
||||
srcZ = sourceLocation.y,
|
||||
destX = targetLocation.x,
|
||||
destZ = targetLocation.y,
|
||||
srcSize = 1,
|
||||
destWidth = 1,
|
||||
destHeight = 1,
|
||||
)
|
||||
}
|
||||
|
||||
private fun manhattanDistance(first: Location, second: Location): Int {
|
||||
val dx = kotlin.math.abs(first.x - second.x)
|
||||
val dy = kotlin.math.abs(first.y - second.y)
|
||||
return dx + dy
|
||||
}
|
||||
|
||||
private fun routeShape(type: Int, sizeX: Int, sizeY: Int): Int =
|
||||
when {
|
||||
type >= 0 -> type
|
||||
sizeX != 0 && sizeY != 0 -> 10
|
||||
else -> -1
|
||||
}
|
||||
|
||||
private val suppliedReachFlags = ThreadLocal.withInitial { CollisionFlagMap() }
|
||||
|
||||
private val projectileLineValidator = ThreadLocal.withInitial {
|
||||
LineValidator(RegionManager.RSMOD_PROJECTILE_FLAGS)
|
||||
}
|
||||
|
||||
private val projectileLineFinder = ThreadLocal.withInitial {
|
||||
LinePathFinder(RegionManager.RSMOD_PROJECTILE_FLAGS)
|
||||
}
|
||||
|
||||
private fun loadCollisionWindow(
|
||||
flags: CollisionFlagMap,
|
||||
start: Location,
|
||||
supplier: ClipMaskSupplier,
|
||||
) {
|
||||
val baseX = start.x - (SEARCH_MAP_SIZE / 2)
|
||||
val baseY = start.y - (SEARCH_MAP_SIZE / 2)
|
||||
for (x in baseX until baseX + SEARCH_MAP_SIZE) {
|
||||
for (y in baseY until baseY + SEARCH_MAP_SIZE) {
|
||||
flags[x, y, start.z] = supplier.getClippingFlag(start.z, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RouteFinderState {
|
||||
val flags = CollisionFlagMap()
|
||||
val finder = PathFinder(flags, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
|
||||
|
||||
fun loadCollisionWindow(start: Location, supplier: ClipMaskSupplier) {
|
||||
loadCollisionWindow(flags, start, supplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package core.game.world.map.path
|
||||
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.Point
|
||||
|
||||
class RsmodProjectilePathfinder : Pathfinder() {
|
||||
override fun find(
|
||||
start: Location?,
|
||||
size: Int,
|
||||
end: Location?,
|
||||
sizeX: Int,
|
||||
sizeY: Int,
|
||||
rotation: Int,
|
||||
type: Int,
|
||||
walkingFlag: Int,
|
||||
near: Boolean,
|
||||
clipMaskSupplier: ClipMaskSupplier?,
|
||||
): Path {
|
||||
val source = requireNotNull(start)
|
||||
val destination = requireNotNull(end)
|
||||
val rayCast =
|
||||
RsmodPathfinder.lineOfSight(
|
||||
start = source,
|
||||
dest = destination,
|
||||
moverSize = size,
|
||||
destWidth = sizeX,
|
||||
destHeight = sizeY,
|
||||
)
|
||||
val path = Path()
|
||||
for (coordinate in rayCast.coordinates) {
|
||||
path.points.add(Point(coordinate.x, coordinate.z))
|
||||
}
|
||||
if (!rayCast.success) {
|
||||
path.isMoveNear = rayCast.alternative
|
||||
return path
|
||||
}
|
||||
path.setSuccesful(true)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
|
@ -1,591 +0,0 @@
|
|||
package core.game.world.map.path
|
||||
|
||||
import core.game.world.GameWorld
|
||||
import core.game.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.Point
|
||||
import core.tools.*
|
||||
import core.api.*
|
||||
import core.api.utils.Vector
|
||||
import core.ServerConstants
|
||||
|
||||
import java.util.Comparator
|
||||
import java.util.PriorityQueue
|
||||
|
||||
import java.io.*
|
||||
import javax.imageio.ImageIO
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
class SmartPathfinder
|
||||
/**
|
||||
* Constructs a new `SmartPathfinder` `Object`.
|
||||
*/
|
||||
internal constructor() : Pathfinder() {
|
||||
/**
|
||||
* The x-queue.
|
||||
*/
|
||||
private var queueX: IntArray = intArrayOf(0)
|
||||
|
||||
/**
|
||||
* The y-queue.
|
||||
*/
|
||||
private var queueY: IntArray = intArrayOf(0)
|
||||
|
||||
/**
|
||||
* The "via" array.
|
||||
*/
|
||||
private var via: Array<IntArray> = Array(104) { IntArray(104) }
|
||||
|
||||
/**
|
||||
* The cost array.
|
||||
*/
|
||||
private var cost: Array<IntArray> = Array(104) { IntArray(104) }
|
||||
|
||||
/**
|
||||
* The current writing position.
|
||||
*/
|
||||
private var writePathPosition = 0
|
||||
|
||||
/**
|
||||
* The current x-coordinate.
|
||||
*/
|
||||
private var curX = 0
|
||||
|
||||
/**
|
||||
* The current y-coordinate.
|
||||
*/
|
||||
private var curY = 0
|
||||
|
||||
/**
|
||||
* The destination x-coordinate.
|
||||
*/
|
||||
private var dstX = 0
|
||||
|
||||
/**
|
||||
* The destination y-coordinate.
|
||||
*/
|
||||
private var dstY = 0
|
||||
|
||||
/**
|
||||
* If a path was found.
|
||||
*/
|
||||
private var foundPath = false
|
||||
|
||||
/**
|
||||
* Resets the pathfinder.
|
||||
*/
|
||||
fun reset() {
|
||||
queueX = IntArray(4096)
|
||||
queueY = IntArray(4096)
|
||||
via = Array(104) { IntArray(104) }
|
||||
cost = Array(104) { IntArray(104) }
|
||||
writePathPosition = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a tile.
|
||||
* @param x The x-coordinate.
|
||||
* @param y The y-coordinate.
|
||||
* @param dir The direction.
|
||||
* @param currentCost The current cost.
|
||||
*/
|
||||
fun check(x: Int, y: Int, dir: Int, currentCost: Int, diagonalPenalty: Int = 0) {
|
||||
if(cost[x][y] > currentCost + diagonalPenalty) {
|
||||
queueX[writePathPosition] = x
|
||||
queueY[writePathPosition] = y
|
||||
via[x][y] = dir
|
||||
cost[x][y] = currentCost + diagonalPenalty
|
||||
writePathPosition = writePathPosition + 1 and 0xfff
|
||||
}
|
||||
}
|
||||
|
||||
override fun find(start: Location?, moverSize: Int, dest: Location?, sizeX: Int, sizeY: Int, rotation: Int, type: Int, walkingFlag: Int, near: Boolean, clipMaskSupplier: ClipMaskSupplier?): Path {
|
||||
reset()
|
||||
assert(start != null && dest != null)
|
||||
var vec = Vector.betweenLocs(start!!, dest!!)
|
||||
var mag = kotlin.math.floor(vec.magnitude())
|
||||
var end = dest!!
|
||||
if (mag > ServerConstants.MAX_PATHFIND_DISTANCE) {
|
||||
try {
|
||||
if (mag < 50.0) { //truncate the path if it's realistically long
|
||||
vec = vec.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1)
|
||||
end = start!!.transform(vec)
|
||||
} else throw Exception("Pathfinding distance exceeds server max! -> " + mag.toString() + " {" + start + "->" + end + "}")
|
||||
} catch (e: Exception) {
|
||||
val sw = StringWriter()
|
||||
val pw = PrintWriter(sw)
|
||||
e.printStackTrace(pw)
|
||||
log(this::class.java, Log.FINE, sw.toString())
|
||||
val p = Path()
|
||||
p.isMoveNear = true
|
||||
return p
|
||||
}
|
||||
}
|
||||
val path = Path()
|
||||
foundPath = false
|
||||
for (x in 0..103) {
|
||||
for (y in 0..103) {
|
||||
via[x][y] = 0
|
||||
cost[x][y] = 99999999
|
||||
}
|
||||
}
|
||||
val z = start!!.z
|
||||
val location = Location.create(start.regionX - 6 shl 3, start.regionY - 6 shl 3, z)
|
||||
curX = start.sceneX
|
||||
curY = start.sceneY
|
||||
dstX = end!!.getSceneX(start)
|
||||
dstY = end.getSceneY(start)
|
||||
var attempts: Int
|
||||
var readPosition: Int
|
||||
check(curX, curY, 99, 0)
|
||||
try {
|
||||
if (moverSize < 2) {
|
||||
if(GameWorld.settings?.smartpathfinder_bfs ?: false) {
|
||||
checkSingleTraversal(end, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
|
||||
} else {
|
||||
checkSingleTraversalAstar(end, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
|
||||
}
|
||||
} else if (moverSize == 2) {
|
||||
checkDoubleTraversal(end, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
|
||||
} else {
|
||||
checkVariableTraversal(end, moverSize, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
|
||||
}
|
||||
} catch (e: Exception) {}
|
||||
var debugImg = if(false) { BufferedImage(4*104+2, 104, BufferedImage.TYPE_INT_RGB) } else { null }
|
||||
if(debugImg != null) {
|
||||
for(y in 0 until 104) {
|
||||
for(x in 0 until 104) {
|
||||
debugImg.setRGB(x, 103-y, via[x][y] * (((1 shl 24)-1)/12))
|
||||
val c = Math.min(4*Math.min(cost[x][y], 64), 255)
|
||||
debugImg.setRGB(105+x, 103-y, (c shl 16) or (c shl 8) or c)
|
||||
debugImg.setRGB(2*105+x, 103-y, clipMaskSupplier!!.getClippingFlag(location.z, location.x + x, location.y + y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPath) {
|
||||
if (near) {
|
||||
var fullCost = 1000
|
||||
var thisCost = 100
|
||||
val depth = 10
|
||||
for (x in dstX - depth..dstX + depth) {
|
||||
for (y in dstY - depth..dstY + depth) {
|
||||
if (x >= 0 && y >= 0 && x < 104 && y < 104 && cost[x][y] < 100) {
|
||||
var diffX = 0
|
||||
if (x < dstX) {
|
||||
diffX = dstX - x
|
||||
} else if (x > dstX + sizeX - 1) {
|
||||
diffX = x - (dstX + sizeX - 1)
|
||||
}
|
||||
var diffY = 0
|
||||
if (y < dstY) {
|
||||
diffY = dstY - y
|
||||
} else if (y > dstY + sizeY - 1) {
|
||||
diffY = y - (dstY + sizeY - 1)
|
||||
}
|
||||
val totalCost = diffX * diffX + diffY * diffY
|
||||
if (totalCost < fullCost || totalCost == fullCost && cost[x][y] < thisCost) {
|
||||
fullCost = totalCost
|
||||
thisCost = cost[x][y]
|
||||
curX = x
|
||||
curY = y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fullCost == 1000) {
|
||||
return path
|
||||
}
|
||||
path.isMoveNear = true
|
||||
}
|
||||
}
|
||||
readPosition = 0
|
||||
queueX[readPosition] = curX
|
||||
queueY[readPosition++] = curY
|
||||
var previousDirection: Int
|
||||
attempts = 0
|
||||
var directionFlag = via[curX][curY].also { previousDirection = it }
|
||||
while (curX != start.sceneX || curY != start.sceneY) {
|
||||
if (++attempts > queueX.size) {
|
||||
return path
|
||||
}
|
||||
previousDirection = directionFlag
|
||||
queueX[readPosition] = curX
|
||||
queueY[readPosition++] = curY
|
||||
if (directionFlag and WEST_FLAG != 0) {
|
||||
curX++
|
||||
} else if (directionFlag and EAST_FLAG != 0) {
|
||||
curX--
|
||||
}
|
||||
if (directionFlag and SOUTH_FLAG != 0) {
|
||||
curY++
|
||||
} else if (directionFlag and NORTH_FLAG != 0) {
|
||||
curY--
|
||||
}
|
||||
if(debugImg != null) {
|
||||
debugImg.setRGB(3*105+curX, 103-curY, 0x0000ff)
|
||||
}
|
||||
directionFlag = via[curX][curY]
|
||||
}
|
||||
if(debugImg != null) {
|
||||
debugImg.setRGB(3*105+start.sceneX, 103-start.sceneY, 0xff0000)
|
||||
debugImg.setRGB(3*105+dstX, 103-dstY, 0x00ff00)
|
||||
if(GameWorld.settings?.smartpathfinder_bfs ?: false) {
|
||||
ImageIO.write(debugImg, "png", File(String.format("bfs_%04d_%04d_%04d_%04d.png", start.x, start.y, end.x, end.y)))
|
||||
} else {
|
||||
ImageIO.write(debugImg, "png", File(String.format("astar_%04d_%04d_%04d_%04d.png", start.x, start.y, end.x, end.y)))
|
||||
}
|
||||
}
|
||||
val size = readPosition--
|
||||
var absX = location.x + queueX[readPosition]
|
||||
var absY = location.y + queueY[readPosition]
|
||||
path.points.add(Point(absX, absY))
|
||||
for (i in 1 until size) {
|
||||
readPosition--
|
||||
absX = location.x + queueX[readPosition]
|
||||
absY = location.y + queueY[readPosition]
|
||||
path.points.add(Point(absX, absY))
|
||||
}
|
||||
path.setSuccesful(true)
|
||||
if (end != dest)
|
||||
path.isMoveNear = true
|
||||
return path
|
||||
}
|
||||
|
||||
class UIntAsPointComparator(val end: Location) : Comparator<UInt> {
|
||||
override fun compare(p: UInt, q: UInt): Int {
|
||||
val pc: UInt = (p and 0x00ff0000u) shr 16
|
||||
val px: UInt = (p and 0x0000ff00u) shr 8
|
||||
val py: UInt = (p and 0x000000ffu)
|
||||
val qc: UInt = (q and 0x00ff0000u) shr 16
|
||||
val qx: UInt = (q and 0x0000ff00u) shr 8
|
||||
val qy: UInt = (q and 0x000000ffu)
|
||||
//val dp = pc.toInt() + Math.abs(end.sceneX - (px.toInt())) + Math.abs(end.sceneY - (py.toInt()))
|
||||
//val dq = qc.toInt() + Math.abs(end.sceneX - (qx.toInt())) + Math.abs(end.sceneY - (qy.toInt()))
|
||||
val dp = pc.toDouble() + Math.max(Math.abs(end.sceneX - px.toInt()), Math.abs(end.sceneY - py.toInt())).toDouble()
|
||||
val dq = qc.toDouble() + Math.max(Math.abs(end.sceneX - qx.toInt()), Math.abs(end.sceneY - qy.toInt())).toDouble()
|
||||
if(dp < dq) {
|
||||
return -1
|
||||
} else if(dq < dp) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if(other is UIntAsPointComparator) {
|
||||
return end == other.end
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
override fun hashCode(): Int {
|
||||
return end.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSingleTraversalAstar(end: Location, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
|
||||
val z = location.z
|
||||
var queue = PriorityQueue(4096, UIntAsPointComparator(end))
|
||||
queue.add(((curX.toUInt()) shl 8) or (curY.toUInt()))
|
||||
while(!foundPath && !queue.isEmpty()) {
|
||||
val point = queue.poll()
|
||||
val curCost = ((point and 0xff0000u) shr 16).toInt()
|
||||
curX = ((point and 0x0000ff00u) shr 8).toInt()
|
||||
curY = (point and 0x000000ffu).toInt()
|
||||
val absX = location.x + curX
|
||||
val absY = location.y + curY
|
||||
if (curX == dstX && curY == dstY) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
if (type != 0) {
|
||||
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
if (type < 10 && canDecorationInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, 1, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
val newCost = curCost + 1
|
||||
//val orthogonalsFirst = arrayOf(Direction.EAST, Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.NORTH_EAST, Direction.NORTH_WEST, Direction.SOUTH_WEST, Direction.SOUTH_EAST)
|
||||
val orthogonalsFirst = arrayOf(Direction.SOUTH, Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH_WEST, Direction.NORTH_WEST, Direction.SOUTH_EAST, Direction.NORTH_EAST)
|
||||
//val orthogonalsFirst = arrayOf(Direction.SOUTH, Direction.WEST, Direction.NORTH, Direction.EAST)
|
||||
//for(dir in Direction.values()) {
|
||||
for(dir in orthogonalsFirst) {
|
||||
val newSceneX: Int = curX + dir.stepX
|
||||
val newSceneY: Int = curY + dir.stepY
|
||||
if(0 <= newSceneX && newSceneX < 104 && 0 <= newSceneY && newSceneY < 104 && via[newSceneX][newSceneY] == 0) {
|
||||
if(dir.canMoveFrom(z, absX, absY, clipMaskSupplier)) {
|
||||
val diagonalPenalty = Math.abs(dir.stepX) + Math.abs(dir.stepY) - 1
|
||||
val flag = flagForDirection(dir)
|
||||
check(newSceneX, newSceneY, flag, newCost, diagonalPenalty)
|
||||
if(via[newSceneX][newSceneY] == flag) {
|
||||
queue.add(((newCost + diagonalPenalty).toUInt() shl 16) or (newSceneX.toUInt() shl 8) or newSceneY.toUInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks possible traversal for a size 1 entity.
|
||||
* @param end The destination location.
|
||||
* @param sizeX The x-size of the destination.
|
||||
* @param sizeY The y-size of the destination.
|
||||
* @param type The object type.
|
||||
* @param rotation The object rotation.
|
||||
* @param walkingFlag The walking flag.
|
||||
* @param location The viewport location.
|
||||
*/
|
||||
private fun checkSingleTraversal(end: Location, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
|
||||
var readPosition = 0
|
||||
val z = location.z
|
||||
while (writePathPosition != readPosition) {
|
||||
curX = queueX[readPosition]
|
||||
curY = queueY[readPosition]
|
||||
readPosition = readPosition + 1 and 0xfff
|
||||
if (curX == dstX && curY == dstY) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
try {
|
||||
val absX = location.x + curX
|
||||
val absY = location.y + curY
|
||||
if (type != 0) {
|
||||
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
if (type < 10 && canDecorationInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, 1, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
val thisCost = cost[curX][curY] + 1
|
||||
if (curY > 0 && via[curX][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0102 == 0) {
|
||||
check(curX, curY - 1, SOUTH_FLAG, thisCost)
|
||||
}
|
||||
if (curX > 0 && via[curX - 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0108 == 0) {
|
||||
check(curX - 1, curY, WEST_FLAG, thisCost)
|
||||
}
|
||||
if (curY < 103 && via[curX][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 1) and 0x12c0120 == 0) {
|
||||
check(curX, curY + 1, NORTH_FLAG, thisCost)
|
||||
}
|
||||
if (curX < 103 && via[curX + 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY) and 0x12c0180 == 0) {
|
||||
check(curX + 1, curY, EAST_FLAG, thisCost)
|
||||
}
|
||||
if (curX > 0 && curY > 0 && via[curX - 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0108 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0102 == 0) {
|
||||
check(curX - 1, curY - 1, SOUTH_WEST_FLAG, thisCost)
|
||||
}
|
||||
if (curX > 0 && curY < 103 && via[curX - 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0108 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 1) and 0x12c0120 == 0) {
|
||||
check(curX - 1, curY + 1, NORTH_WEST_FLAG, thisCost)
|
||||
}
|
||||
if (curX < 103 && curY > 0 && via[curX + 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY) and 0x12c0180 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0102 == 0) {
|
||||
check(curX + 1, curY - 1, SOUTH_EAST_FLAG, thisCost)
|
||||
}
|
||||
if (curX < 103 && curY < 103 && via[curX + 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + 1) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY) and 0x12c0180 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 1) and 0x12c0120 == 0) {
|
||||
check(curX + 1, curY + 1, NORTH_EAST_FLAG, thisCost)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// e.printStackTrace()println("curX " + curX + " curY" + curY + " via " + via[curX + 1] + via[curY + 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks possible traversal for a size 2 entity.
|
||||
* @param end The destination location.
|
||||
* @param sizeX The x-size of the destination.
|
||||
* @param sizeY The y-size of the destination.
|
||||
* @param type The object type.
|
||||
* @param rotation The object rotation.
|
||||
* @param walkingFlag The walking flag.
|
||||
* @param location The viewport location.
|
||||
*/
|
||||
private fun checkDoubleTraversal(end: Location, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
|
||||
var readPosition = 0
|
||||
val z = location.z
|
||||
while (writePathPosition != readPosition) {
|
||||
curX = queueX[readPosition]
|
||||
curY = queueY[readPosition]
|
||||
readPosition = readPosition + 1 and 0xfff
|
||||
if (curX == dstX && curY == dstY) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
val absX = location.x + curX
|
||||
val absY = location.y + curY
|
||||
if (type != 0) {
|
||||
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, 2, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
if (type < 10 && canDecorationInteract(absX, absY, 2, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, 2, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
val thisCost = cost[curX][curY] + 1
|
||||
if (curY > 0 && via[curX][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c0183 == 0) {
|
||||
check(curX, curY - 1, SOUTH_FLAG, thisCost)
|
||||
}
|
||||
if (curX > 0 && via[curX - 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c0138 == 0) {
|
||||
check(curX - 1, curY, WEST_FLAG, thisCost)
|
||||
}
|
||||
if (curY < 102 && via[curX][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 2) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + 2) and 0x12c01e0 == 0) {
|
||||
check(curX, curY + 1, NORTH_FLAG, thisCost)
|
||||
}
|
||||
if (curX < 102 && via[curX + 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY + 1) and 0x12c01e0 == 0) {
|
||||
check(curX + 1, curY, EAST_FLAG, thisCost)
|
||||
}
|
||||
if (curX > 0 && curY > 0 && via[curX - 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0183 == 0) {
|
||||
check(curX - 1, curY - 1, SOUTH_WEST_FLAG, thisCost)
|
||||
}
|
||||
if (curX > 0 && curY < 102 && via[curX - 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 2) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 2) and 0x12c01e0 == 0) {
|
||||
check(curX - 1, curY + 1, NORTH_WEST_FLAG, thisCost)
|
||||
}
|
||||
if (curX < 102 && curY > 0 && via[curX + 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY - 1) and 0x12c0183 == 0) {
|
||||
check(curX + 1, curY - 1, SOUTH_EAST_FLAG, thisCost)
|
||||
}
|
||||
if (curX < 102 && curY < 102 && via[curX + 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + 2) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY + 2) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY + 1) and 0x12c0183 == 0) {
|
||||
check(curX + 1, curY + 1, NORTH_EAST_FLAG, thisCost)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks possible traversal for any sized entity.
|
||||
* @param end The destination location.
|
||||
* @param size The mover size.
|
||||
* @param sizeX The x-size of the destination.
|
||||
* @param sizeY The y-size of the destination.
|
||||
* @param type The object type.
|
||||
* @param rotation The object rotation.
|
||||
* @param walkingFlag The walking flag.
|
||||
* @param location The viewport location.
|
||||
*/
|
||||
private fun checkVariableTraversal(end: Location, size: Int, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
|
||||
var readPosition = 0
|
||||
val z = location.z
|
||||
main@ while (writePathPosition != readPosition) {
|
||||
curX = queueX[readPosition]
|
||||
curY = queueY[readPosition]
|
||||
readPosition = readPosition + 1 and 0xfff
|
||||
if (curX == dstX && curY == dstY) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
val absX = location.x + curX
|
||||
val absY = location.y + curY
|
||||
if (type != 0) {
|
||||
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, size, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
if (type < 10 && canDecorationInteract(absX, absY, size, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, size, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
|
||||
foundPath = true
|
||||
break
|
||||
}
|
||||
val thisCost = cost[curX][curY] + 1
|
||||
south@ do {
|
||||
if (curY > 0 && via[curX][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + (size - 1), absY - 1) and 0x12c0183 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX + i, absY - 1) and 0x12c018f != 0) {
|
||||
break@south
|
||||
}
|
||||
}
|
||||
check(curX, curY - 1, SOUTH_FLAG, thisCost)
|
||||
}
|
||||
} while (false)
|
||||
west@ do {
|
||||
if (curX > 0 && via[curX - 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (size - 1)) and 0x12c0138 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX - 1, absY + i) and 0x12c013e != 0) {
|
||||
break@west
|
||||
}
|
||||
}
|
||||
check(curX - 1, curY, WEST_FLAG, thisCost)
|
||||
}
|
||||
} while (false)
|
||||
north@ do {
|
||||
if (curY < 102 && via[curX][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + size) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + (size - 1), absY + size) and 0x12c01e0 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX + i, absY + size) and 0x12c01f8 != 0) {
|
||||
break@north
|
||||
}
|
||||
}
|
||||
check(curX, curY + 1, NORTH_FLAG, thisCost)
|
||||
}
|
||||
} while (false)
|
||||
east@ do {
|
||||
if (curX < 102 && via[curX + 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + (size - 1)) and 0x12c01e0 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX + size, absY + i) and 0x12c01e3 != 0) {
|
||||
break@east
|
||||
}
|
||||
}
|
||||
check(curX + 1, curY, EAST_FLAG, thisCost)
|
||||
}
|
||||
} while (false)
|
||||
southWest@ do {
|
||||
if (curX > 0 && curY > 0 && via[curX - 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (size - 2)) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + (size - 2), absY - 1) and 0x12c0183 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (i - 1)) and 0x12c013e != 0 || clipMaskSupplier.getClippingFlag(z, absX + (i - 1), absY - 1) and 0x12c018f != 0) {
|
||||
break@southWest
|
||||
}
|
||||
}
|
||||
check(curX - 1, curY - 1, SOUTH_WEST_FLAG, thisCost)
|
||||
}
|
||||
} while (false)
|
||||
northWest@ do {
|
||||
if (curX > 0 && curY < 102 && via[curX - 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + size) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + size) and 0x12c01e0 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (i + 1)) and 0x12c013e != 0 || clipMaskSupplier.getClippingFlag(z, absX + (i - 1), absY + size) and 0x12c01f8 != 0) {
|
||||
break@northWest
|
||||
}
|
||||
}
|
||||
check(curX - 1, curY + 1, NORTH_WEST_FLAG, thisCost)
|
||||
}
|
||||
} while (false)
|
||||
southEast@ do {
|
||||
if (curX < 102 && curY > 0 && via[curX + 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY - 1) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + (size - 2)) and 0x12c01e0 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX + size, absY + (i - 1)) and 0x12c01e3 != 0 || clipMaskSupplier.getClippingFlag(z, absX + (i + 1), absY - 1) and 0x12c018f != 0) {
|
||||
break@southEast
|
||||
}
|
||||
}
|
||||
check(curX + 1, curY - 1, SOUTH_EAST_FLAG, thisCost)
|
||||
}
|
||||
} while (false)
|
||||
if (curX < 102 && curY < 102 && via[curX + 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + size) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + size) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + 1) and 0x12c0183 == 0) {
|
||||
for (i in 1 until size - 1) {
|
||||
if (clipMaskSupplier.getClippingFlag(z, absX + (i + 1), absY + size) and 0x12c01f8 != 0 || clipMaskSupplier.getClippingFlag(z, absX + size, absY + (i + 1)) and 0x12c01e3 != 0) {
|
||||
continue@main
|
||||
}
|
||||
}
|
||||
check(curX + 1, curY + 1, NORTH_EAST_FLAG, thisCost)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -296,7 +296,9 @@ class EasterEvent : WorldEvent("easter"), TickListener, InteractionListener, Log
|
|||
val dir = dirs[RandomFunction.random(dirs.size)]
|
||||
var loc = player.location.transform(dir, 3)
|
||||
val path = Pathfinder.find(player, loc)
|
||||
loc = Location.create(path.points.last.x, path.points.last.y, loc.z)
|
||||
path.points.lastOrNull()?.let {
|
||||
loc = Location.create(it.x, it.y, loc.z)
|
||||
}
|
||||
GroundItemManager.create(Item(eggs.random()), loc, player)
|
||||
sendMessage(player, colorize("%RAn egg has appeared nearby."))
|
||||
}
|
||||
|
|
@ -317,4 +319,4 @@ class EasterEvent : WorldEvent("easter"), TickListener, InteractionListener, Log
|
|||
WeightedItem(Items.DRAGON_IMPLING_JAR_11256, 1, 1, 0.005)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@ object PacketProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun clearQueue() {
|
||||
synchronized(queueLock) {
|
||||
queue.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun processQueue() {
|
||||
synchronized(queueLock) {
|
||||
if (queue.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import core.ServerConstants
|
|||
import core.ServerStore
|
||||
import core.api.log
|
||||
import core.api.submitWorldPulse
|
||||
import core.game.node.entity.combat.CombatMovementIntents
|
||||
import core.game.system.task.Pulse
|
||||
import core.game.world.GameWorld
|
||||
import core.game.world.repository.Repository
|
||||
|
|
@ -20,6 +21,7 @@ import java.lang.Long.max
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/**
|
||||
* Handles the running of pulses and writing of masks, etc
|
||||
|
|
@ -118,6 +120,13 @@ class MajorUpdateWorker {
|
|||
GameWorld.Pulser.updateAll()
|
||||
}
|
||||
GameWorld.tickListeners.forEach { it.tick() }
|
||||
val meleePressureTime = measureTimeMillis {
|
||||
CombatMovementIntents.requestActiveMeleePressure()
|
||||
}
|
||||
val movementResolveTime = measureTimeMillis {
|
||||
CombatMovementIntents.resolve()
|
||||
}
|
||||
notifyIfCombatMovementTooLong(meleePressureTime, movementResolveTime)
|
||||
|
||||
sequence.start()
|
||||
sequence.run()
|
||||
|
|
@ -138,6 +147,29 @@ class MajorUpdateWorker {
|
|||
}
|
||||
}
|
||||
|
||||
private fun notifyIfCombatMovementTooLong(meleePressureTime: Long, movementResolveTime: Long) {
|
||||
val totalTime = meleePressureTime + movementResolveTime
|
||||
val resolveSummary = CombatMovementIntents.lastResolveSummary()
|
||||
if (totalTime >= CRITICAL_COMBAT_MOVEMENT_MS) {
|
||||
log(
|
||||
this::class.java,
|
||||
Log.WARN,
|
||||
"CRITICALLY long combat movement update - requestActiveMeleePressure took $meleePressureTime ms, resolve took $movementResolveTime ms, total $totalTime ms, $resolveSummary"
|
||||
)
|
||||
} else if (totalTime >= LONG_COMBAT_MOVEMENT_MS) {
|
||||
log(
|
||||
this::class.java,
|
||||
Log.WARN,
|
||||
"Long combat movement update - requestActiveMeleePressure took $meleePressureTime ms, resolve took $movementResolveTime ms, total $totalTime ms, $resolveSummary"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LONG_COMBAT_MOVEMENT_MS = 50L
|
||||
private const val CRITICAL_COMBAT_MOVEMENT_MS = 100L
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (!started) {
|
||||
running = true
|
||||
|
|
|
|||
2149
Server/src/test/kotlin/content/CombatMovementTests.kt
Normal file
2149
Server/src/test/kotlin/content/CombatMovementTests.kt
Normal file
File diff suppressed because it is too large
Load diff
209
Server/src/test/kotlin/content/CombatPerformanceTests.kt
Normal file
209
Server/src/test/kotlin/content/CombatPerformanceTests.kt
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package content
|
||||
|
||||
import TestUtils
|
||||
import core.ServerConstants
|
||||
import core.game.node.entity.combat.CombatMovementIntents
|
||||
import core.game.node.entity.combat.equipment.WeaponInterface
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.world.GameWorld
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.repository.Repository
|
||||
import core.game.world.update.UpdateSequence
|
||||
import core.net.packet.PacketProcessor
|
||||
import core.tools.LogLevel
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Disabled // ENABLE LOCALLY ONLY
|
||||
class CombatPerformanceTests {
|
||||
init {
|
||||
TestUtils.preTestSetup()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serverTickStaysWithinBudgetWithLiveSizedCombatLoad() {
|
||||
withQuietPerformanceLogs {
|
||||
var load: CombatLoad? = null
|
||||
try {
|
||||
load = createCombatLoad()
|
||||
assertEquals(REAL_PLAYER_COUNT, load.players.count { !it.isArtificial })
|
||||
assertEquals(BOT_PLAYER_COUNT, load.players.count { it.isArtificial })
|
||||
|
||||
repeat(WARMUP_TICKS) {
|
||||
measureCombatTick(load, it)
|
||||
}
|
||||
|
||||
val durations =
|
||||
LongArray(MEASURED_TICKS) { tick ->
|
||||
measureCombatTick(load, tick + WARMUP_TICKS)
|
||||
}
|
||||
val sorted = durations.sorted()
|
||||
val p90Index = ((sorted.size * 9 + 9) / 10 - 1).coerceIn(0, sorted.lastIndex)
|
||||
val p90 = sorted[p90Index]
|
||||
val max = sorted.last()
|
||||
val durationText = durations.joinToString(prefix = "[", postfix = "]")
|
||||
|
||||
assertTrue(
|
||||
p90 <= HEADROOM_TICK_BUDGET_MILLIS,
|
||||
"650-player combat p90 tick time should leave room for slower live hardware. " +
|
||||
"durations=${durationText}ms, p90=${p90}ms, " +
|
||||
"budget=${HEADROOM_TICK_BUDGET_MILLIS}ms",
|
||||
)
|
||||
assertTrue(
|
||||
max <= LIVE_TICK_BUDGET_MILLIS,
|
||||
"650-player combat tick should remain under the live 600ms server tick budget. " +
|
||||
"durations=${durationText}ms, max=${max}ms",
|
||||
)
|
||||
} finally {
|
||||
load?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> withQuietPerformanceLogs(action: () -> T): T {
|
||||
val previousLogLevel = ServerConstants.LOG_LEVEL
|
||||
ServerConstants.LOG_LEVEL = LogLevel.CAUTIOUS
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
ServerConstants.LOG_LEVEL = previousLogLevel
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCombatLoad(): CombatLoad {
|
||||
val players = ArrayList<Player>(TOTAL_PLAYER_COUNT)
|
||||
val previousWildPvp = GameWorld.settings!!.wild_pvp_enabled
|
||||
|
||||
for (i in 0 until TOTAL_PLAYER_COUNT) {
|
||||
val player = TestUtils.getMockPlayer("combat_perf_$i", isBot = i >= REAL_PLAYER_COUNT)
|
||||
players.add(player)
|
||||
configureMeleePlayer(player)
|
||||
}
|
||||
|
||||
val pairs =
|
||||
players.chunked(2).mapIndexed { index, pair ->
|
||||
CombatPair(pair[0], pair[1], pairOrigin(index))
|
||||
}
|
||||
val load = CombatLoad(players, pairs, previousWildPvp)
|
||||
|
||||
GameWorld.settings!!.wild_pvp_enabled = true
|
||||
load.resetPairPositionsAndMovement(0)
|
||||
for (pair in pairs) {
|
||||
pair.first.attack(pair.second)
|
||||
pair.second.attack(pair.first)
|
||||
}
|
||||
return load
|
||||
}
|
||||
|
||||
private fun measureCombatTick(load: CombatLoad, tick: Int): Long {
|
||||
PacketProcessor.clearQueue()
|
||||
load.resetPairPositionsAndMovement(tick)
|
||||
load.requestAllCombatMovement()
|
||||
|
||||
assertEquals(
|
||||
TOTAL_PLAYER_COUNT,
|
||||
CombatMovementIntents.pendingCount(),
|
||||
"The performance fixture should exercise one combat movement intent per loaded player.",
|
||||
)
|
||||
|
||||
val start = System.nanoTime()
|
||||
GameWorld.majorUpdateWorker.handleTickActions(false)
|
||||
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)
|
||||
}
|
||||
|
||||
private fun configureMeleePlayer(player: Player) {
|
||||
player.properties.attackStyle =
|
||||
WeaponInterface.AttackStyle(
|
||||
WeaponInterface.STYLE_AGGRESSIVE,
|
||||
WeaponInterface.BONUS_CRUSH,
|
||||
)
|
||||
player.properties.combatPulse.updateStyle()
|
||||
player.properties.combatLevel = 126
|
||||
player.skills.setStaticLevel(Skills.HITPOINTS, 10_000)
|
||||
player.skills.lifepoints = 10_000
|
||||
player.settings.runEnergy = 100.0
|
||||
player.settings.setRunToggled(true)
|
||||
player.skullManager.isWilderness = true
|
||||
player.skullManager.level = 50
|
||||
}
|
||||
|
||||
private data class CombatPair(
|
||||
val first: Player,
|
||||
val second: Player,
|
||||
val origin: Location,
|
||||
)
|
||||
|
||||
private class CombatLoad(
|
||||
val players: List<Player>,
|
||||
private val pairs: List<CombatPair>,
|
||||
private val previousWildPvp: Boolean,
|
||||
) : AutoCloseable {
|
||||
|
||||
fun resetPairPositionsAndMovement(tick: Int) {
|
||||
val direction = if (tick % 2 == 0) 1 else -1
|
||||
for (pair in pairs) {
|
||||
val firstLocation = pair.origin
|
||||
val secondLocation = pair.origin.transform(1, 0, 0)
|
||||
place(pair.first, firstLocation)
|
||||
place(pair.second, secondLocation)
|
||||
queueRun(pair.first, firstLocation.transform(-8 * direction, 0, 0))
|
||||
queueRun(pair.second, secondLocation.transform(8 * direction, 0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAllCombatMovement() {
|
||||
CombatMovementIntents.clear()
|
||||
for (pair in pairs) {
|
||||
CombatMovementIntents.request(pair.first, pair.second)
|
||||
CombatMovementIntents.request(pair.second, pair.first)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
CombatMovementIntents.clear()
|
||||
for (player in players.asReversed()) {
|
||||
player.pulseManager.clear()
|
||||
player.walkingQueue.reset()
|
||||
player.isActive = false
|
||||
player.setPlaying(false)
|
||||
Repository.removePlayer(player)
|
||||
UpdateSequence.renderablePlayers.remove(player)
|
||||
}
|
||||
GameWorld.Pulser.updateAll()
|
||||
UpdateSequence.renderablePlayers.sync()
|
||||
PacketProcessor.clearQueue()
|
||||
GameWorld.settings!!.wild_pvp_enabled = previousWildPvp
|
||||
}
|
||||
|
||||
private fun place(player: Player, location: Location) {
|
||||
player.location = location
|
||||
player.walkingQueue.reset()
|
||||
}
|
||||
|
||||
private fun queueRun(player: Player, destination: Location) {
|
||||
player.walkingQueue.reset(true)
|
||||
player.walkingQueue.addPath(destination.x, destination.y)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val REAL_PLAYER_COUNT = 150
|
||||
const val BOT_PLAYER_COUNT = 500
|
||||
const val TOTAL_PLAYER_COUNT = REAL_PLAYER_COUNT + BOT_PLAYER_COUNT
|
||||
const val WARMUP_TICKS = 3
|
||||
const val MEASURED_TICKS = 10
|
||||
const val LIVE_TICK_BUDGET_MILLIS = 600L
|
||||
const val HEADROOM_TICK_BUDGET_MILLIS = 350L
|
||||
|
||||
fun pairOrigin(index: Int): Location {
|
||||
val columns = 25
|
||||
val column = index % columns
|
||||
val row = index / columns
|
||||
return Location.create(3200 + column * 12, 3600 + row * 6, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,20 @@ package content
|
|||
|
||||
import TestUtils
|
||||
import content.global.handlers.item.equipment.special.ChinchompaSwingHandler
|
||||
import core.ServerConstants
|
||||
import core.api.EquipmentSlot
|
||||
import core.game.container.impl.EquipmentContainer.updateBonuses
|
||||
import core.game.interaction.IntType
|
||||
import core.game.interaction.InteractionListeners
|
||||
import core.game.node.entity.combat.CombatMovementIntents
|
||||
import core.game.node.entity.combat.CombatMovementPlanner
|
||||
import core.game.node.entity.combat.CombatReach
|
||||
import core.game.node.entity.combat.MagicSwingHandler
|
||||
import core.game.node.entity.combat.MeleeSwingHandler
|
||||
import core.game.node.entity.combat.RangeSwingHandler
|
||||
import core.game.node.entity.combat.SwingHandlerFlag
|
||||
import core.game.node.entity.combat.equipment.WeaponInterface
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.link.prayer.PrayerType
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.node.item.Item
|
||||
|
|
@ -135,4 +140,89 @@ class CombatTests {
|
|||
Assertions.assertEquals(damageBaseline, handler.calculateHit(p, p, 1.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatReachUsesOccupiedTilesForLargeMeleeTargets() {
|
||||
TestUtils.getMockPlayer("combatReachLargeTarget").use { attacker ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
val victim = NPC.create(100, origin.transform(1, 0, 0))
|
||||
victim.setSize(2)
|
||||
attacker.location = origin
|
||||
|
||||
Assertions.assertTrue(CombatReach.canMelee(attacker, victim, 1))
|
||||
Assertions.assertTrue(MeleeSwingHandler.canMelee(attacker, victim, 1))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatMovementPlannerPredictsRunningTargetSteps() {
|
||||
TestUtils.getMockPlayer("combatPlannerRunner").use { target ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
target.location = origin
|
||||
target.walkingQueue.reset(true)
|
||||
target.walkingQueue.addPath(origin.x + 3, origin.y)
|
||||
|
||||
Assertions.assertEquals(
|
||||
listOf(origin.transform(1, 0, 0), origin.transform(2, 0, 0)),
|
||||
CombatMovementPlanner.predictTargetLocations(target)
|
||||
)
|
||||
Assertions.assertEquals(origin.transform(2, 0, 0), CombatMovementPlanner.predictedTargetLocation(target))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatMovementPlannerTreatsZeroEnergyRunningPlayerAsWalking() {
|
||||
TestUtils.getMockPlayer("combatPlannerNoEnergyRunner").use { target ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
target.location = origin
|
||||
target.settings.runEnergy = 0.0
|
||||
target.walkingQueue.reset(true)
|
||||
target.walkingQueue.addPath(origin.x + 3, origin.y)
|
||||
|
||||
Assertions.assertEquals(1, CombatMovementPlanner.movementStepsThisTick(target))
|
||||
Assertions.assertEquals(
|
||||
listOf(origin.transform(1, 0, 0)),
|
||||
CombatMovementPlanner.predictTargetLocations(target)
|
||||
)
|
||||
Assertions.assertEquals(origin.transform(1, 0, 0), CombatMovementPlanner.predictedTargetLocation(target))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatMovementPlannerChoosesClosestTargetBorderTile() {
|
||||
TestUtils.getMockPlayer("combatPlannerAttacker").use { attacker ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
val victim = NPC.create(100, origin.transform(4, 0, 0))
|
||||
victim.setSize(2)
|
||||
attacker.location = origin
|
||||
|
||||
Assertions.assertEquals(
|
||||
origin.transform(3, 0, 0),
|
||||
CombatMovementPlanner.chooseTargetBorderTile(attacker, victim, victim.location)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatMovementIntentResolverAppliesPendingMeleePathBeforeEntityMovement() {
|
||||
TestUtils.getMockPlayer("combatIntentAttacker").use { attacker ->
|
||||
TestUtils.getMockPlayer("combatIntentVictim").use { victim ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
attacker.location = origin
|
||||
victim.location = origin.transform(4, 0, 0)
|
||||
attacker.properties.attackStyle = WeaponInterface.AttackStyle(
|
||||
WeaponInterface.STYLE_AGGRESSIVE,
|
||||
WeaponInterface.BONUS_CRUSH
|
||||
)
|
||||
attacker.properties.combatPulse.updateStyle()
|
||||
attacker.attack(victim)
|
||||
|
||||
CombatMovementIntents.request(attacker, victim)
|
||||
CombatMovementIntents.resolve()
|
||||
|
||||
Assertions.assertTrue(attacker.walkingQueue.hasPath())
|
||||
Assertions.assertEquals(0, CombatMovementIntents.pendingCount())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
266
Server/src/test/kotlin/content/StallGuardReactionTests.kt
Normal file
266
Server/src/test/kotlin/content/StallGuardReactionTests.kt
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
package content
|
||||
|
||||
import MockSession
|
||||
import TestUtils
|
||||
import content.global.skill.thieving.StallThiefPulse
|
||||
import content.global.skill.thieving.ThievingOptionPlugin
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.combat.CombatMovementIntents
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Regression tests for the Ardougne market stall mechanic: a failed steal must never
|
||||
* fail silently. Any guard in range busts the steal; a guard whose attack can actually
|
||||
* connect is preferred over one boxed in behind a stall or inside the guardhouse, so
|
||||
* the shout is followed by a real attack whenever possible.
|
||||
*/
|
||||
class StallGuardReactionTests {
|
||||
init {
|
||||
TestUtils.preTestSetup()
|
||||
}
|
||||
|
||||
private fun findBakersStall(): core.game.node.scenery.Scenery {
|
||||
core.game.world.map.Region.load(RegionManager.forId(10547))
|
||||
val bakerIds = setOf(2561, 6163, 34384)
|
||||
for (x in 2650..2680) {
|
||||
for (y in 3295..3325) {
|
||||
val obj = RegionManager.getObject(0, x, y) ?: continue
|
||||
if (obj.id in bakerIds) {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
throw AssertionError("No baker's stall found in the Ardougne market square.")
|
||||
}
|
||||
|
||||
private fun adjacentWalkableTile(stall: core.game.node.scenery.Scenery): Location {
|
||||
val base = stall.location
|
||||
val candidates = ArrayList<Location>()
|
||||
for (dx in -1..2) {
|
||||
for (dy in -1..2) {
|
||||
if (dx in 0..1 && dy in 0..1) continue
|
||||
candidates.add(base.transform(dx, dy, 0))
|
||||
}
|
||||
}
|
||||
return candidates.firstOrNull { RegionManager.isTeleportPermitted(it) }
|
||||
?: throw AssertionError("No walkable tile adjacent to the stall at $base.")
|
||||
}
|
||||
|
||||
private fun place(entity: Entity, location: Location) {
|
||||
entity.location = location
|
||||
RegionManager.move(entity)
|
||||
entity.walkingQueue.reset()
|
||||
}
|
||||
|
||||
private fun assertGuardAttacks(guard: NPC, player: Player, scenario: String) {
|
||||
TestUtils.advanceTicks(30, false)
|
||||
assertTrue(
|
||||
player.inCombat(),
|
||||
"[$scenario] Player should have been hit by the guard. " +
|
||||
"guard=${guard.location}, attacking=${guard.properties.combatPulse.isAttacking}, " +
|
||||
"player=${player.location}, ${CombatMovementIntents.lastResolveSummary()}",
|
||||
)
|
||||
}
|
||||
|
||||
private val movementBlockFlag =
|
||||
Pathfinder.PREVENT_NORTH or
|
||||
Pathfinder.PREVENT_EAST or
|
||||
Pathfinder.PREVENT_SOUTH or
|
||||
Pathfinder.PREVENT_WEST
|
||||
|
||||
/** Finds an origin whose east and north arms (4 tiles each) are fully walkable. */
|
||||
private fun openCrossOrigin(): Location {
|
||||
val start = Location.create(3200, 3600, 0)
|
||||
for (dy in -16..16) {
|
||||
for (dx in -16..16) {
|
||||
val candidate = start.transform(dx, dy, 0)
|
||||
val armsOpen = (0..4).all {
|
||||
RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) &&
|
||||
RegionManager.isTeleportPermitted(candidate.transform(0, it, 0))
|
||||
}
|
||||
if (armsOpen) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
throw AssertionError("No open cross-shaped area found near $start.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blockedGuardStillBustsTheStealButReachableGuardIsPreferred() {
|
||||
TestUtils.getMockPlayer("stall_guard_selection").use { player ->
|
||||
val origin = openCrossOrigin()
|
||||
place(player, origin)
|
||||
// Wall column two tiles east of the player, between them and the guard.
|
||||
val wall = (-2..2).map { origin.transform(2, it, 0) }
|
||||
val blockedGuard = NPC.create(32, origin.transform(4, 0, 0))
|
||||
try {
|
||||
wall.forEach { RegionManager.addClippingFlag(it.z, it.x, it.y, false, movementBlockFlag) }
|
||||
blockedGuard.init()
|
||||
place(blockedGuard, origin.transform(4, 0, 0))
|
||||
|
||||
assertSame(
|
||||
blockedGuard,
|
||||
StallThiefPulse.findGuardForFailedSteal(player),
|
||||
"A guard whose chase route is blocked must still bust the steal.",
|
||||
)
|
||||
|
||||
val reachableGuard = NPC.create(32, origin.transform(0, 4, 0))
|
||||
try {
|
||||
reachableGuard.init()
|
||||
place(reachableGuard, origin.transform(0, 4, 0))
|
||||
assertSame(
|
||||
reachableGuard,
|
||||
StallThiefPulse.findGuardForFailedSteal(player),
|
||||
"The reachable guard should be picked over the blocked one.",
|
||||
)
|
||||
} finally {
|
||||
reachableGuard.clear()
|
||||
}
|
||||
} finally {
|
||||
wall.forEach { RegionManager.removeClippingFlag(it.z, it.x, it.y, false, movementBlockFlag) }
|
||||
blockedGuard.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guardBusyFightingSomeoneElseDoesNotBustTheSteal() {
|
||||
TestUtils.getMockPlayer("stall_guard_busy").use { player ->
|
||||
TestUtils.getMockPlayer("stall_guard_other_victim").use { other ->
|
||||
val origin = openCrossOrigin()
|
||||
place(player, origin)
|
||||
place(other, origin.transform(4, 1, 0))
|
||||
val guard = NPC.create(32, origin.transform(4, 0, 0))
|
||||
try {
|
||||
guard.init()
|
||||
place(guard, origin.transform(4, 0, 0))
|
||||
guard.properties.combatPulse.attack(other)
|
||||
assertNull(
|
||||
StallThiefPulse.findGuardForFailedSteal(player),
|
||||
"A guard busy fighting someone else should not notice the thief.",
|
||||
)
|
||||
guard.properties.combatPulse.stop()
|
||||
assertSame(
|
||||
guard,
|
||||
StallThiefPulse.findGuardForFailedSteal(player),
|
||||
"Once free, the same guard should bust the thief again.",
|
||||
)
|
||||
} finally {
|
||||
guard.clear()
|
||||
CombatMovementIntents.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ardougneGuardShouldAttackPlayerCaughtStealing() {
|
||||
TestUtils.getMockPlayer("stall_thief").use { player ->
|
||||
val stall = findBakersStall()
|
||||
place(player, adjacentWalkableTile(stall))
|
||||
player.skills.setStaticLevel(Skills.HITPOINTS, 99)
|
||||
player.skills.lifepoints = 5000
|
||||
player.properties.isRetaliating = false
|
||||
val guardSpawn = Location.create(2661, 3309, 0)
|
||||
val guard = NPC.create(32, guardSpawn)
|
||||
guard.init()
|
||||
try {
|
||||
place(guard, guardSpawn)
|
||||
val caughtBy = StallThiefPulse.findGuardForFailedSteal(player)
|
||||
assertNotNull(caughtBy, "A guard should be found near the stall.")
|
||||
caughtBy!!.sendChat("Hey! Get your hands off there!")
|
||||
caughtBy.properties.combatPulse.attack(player)
|
||||
assertGuardAttacks(caughtBy, player, "open-path stall=${stall.location}")
|
||||
} finally {
|
||||
guard.clear()
|
||||
CombatMovementIntents.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun walkingGuardShouldStillAttackWhenCaught() {
|
||||
TestUtils.getMockPlayer("stall_thief_walker").use { player ->
|
||||
val stall = findBakersStall()
|
||||
place(player, adjacentWalkableTile(stall))
|
||||
player.skills.setStaticLevel(Skills.HITPOINTS, 99)
|
||||
player.skills.lifepoints = 5000
|
||||
player.properties.isRetaliating = false
|
||||
val guardSpawn = Location.create(2661, 3309, 0)
|
||||
val guard = NPC.create(32, guardSpawn)
|
||||
guard.init()
|
||||
try {
|
||||
// Guard is mid-wander when the player gets caught.
|
||||
guard.walkingQueue.reset(false)
|
||||
guard.walkingQueue.addPath(guardSpawn.x - 3, guardSpawn.y)
|
||||
TestUtils.advanceTicks(1, false)
|
||||
guard.sendChat("Hey! Get your hands off there!")
|
||||
guard.properties.combatPulse.attack(player)
|
||||
assertGuardAttacks(guard, player, "walking-guard stall=${stall.location}")
|
||||
} finally {
|
||||
guard.clear()
|
||||
CombatMovementIntents.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End-to-end: clicking Steal-from must start the thieving attempt from every
|
||||
* walkable tile around the stall. A tile from which the click silently does
|
||||
* nothing (no attempt, no message) reproduces the reported bug.
|
||||
*/
|
||||
@Test
|
||||
fun stealFromClickShouldStartAttemptFromEveryNearbyTile() {
|
||||
ThievingOptionPlugin().newInstance(null)
|
||||
TestUtils.getMockPlayer("stall_click_thief").use { player ->
|
||||
val stall = findBakersStall()
|
||||
player.skills.setStaticLevel(Skills.THIEVING, 99)
|
||||
player.skills.setLevel(Skills.THIEVING, 99)
|
||||
player.skills.setStaticLevel(Skills.HITPOINTS, 99)
|
||||
player.properties.isRetaliating = false
|
||||
val optIndex = stall.interaction.options.indexOfFirst {
|
||||
it != null && it.name.equals("steal-from", ignoreCase = true)
|
||||
}
|
||||
assertTrue(optIndex >= 0, "Stall has no Steal-from option: ${stall.interaction.options?.map { it?.name }}")
|
||||
|
||||
val base = stall.location
|
||||
val silent = ArrayList<Location>()
|
||||
var attempts = 0
|
||||
for (dx in -2..3) {
|
||||
for (dy in -2..3) {
|
||||
if (dx in 0..1 && dy in 0..1) continue // stall footprint
|
||||
val tile = base.transform(dx, dy, 0)
|
||||
if (!RegionManager.isTeleportPermitted(tile)) continue
|
||||
place(player, tile)
|
||||
player.removeAttribute("thieveDelay")
|
||||
player.removeAttribute("combat-time")
|
||||
player.properties.combatPulse.stop()
|
||||
player.inventory.clear()
|
||||
player.skills.lifepoints = 5000
|
||||
attempts++
|
||||
val session = player.session as MockSession
|
||||
session.receivedPackets.clear()
|
||||
TestUtils.simulateInteraction(player, stall, optIndex)
|
||||
TestUtils.advanceTicks(10, false)
|
||||
if (player.getAttribute<Any?>("thieveDelay", null) == null) {
|
||||
silent.add(tile)
|
||||
}
|
||||
TestUtils.advanceTicks(10, false) // let locks/stall respawn settle
|
||||
}
|
||||
}
|
||||
assertTrue(attempts > 0, "No walkable tiles found around the stall.")
|
||||
assertTrue(
|
||||
silent.isEmpty(),
|
||||
"Steal-from click did nothing from ${silent.size}/$attempts tiles: $silent (stall=$base)",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +1,653 @@
|
|||
package core
|
||||
|
||||
import TestUtils
|
||||
import content.global.handlers.npc.NPCTalkListener
|
||||
import content.global.handlers.scenery.BankBoothListener
|
||||
import content.global.skill.gather.GatheringSkillOptionListeners
|
||||
import content.global.skill.gather.woodcutting.WoodcuttingListener
|
||||
import content.region.misthalin.varrock.dialogue.GrandExchangeClerk
|
||||
import content.region.misthalin.varrock.handlers.GrandExchangePlugin
|
||||
import core.api.log
|
||||
import core.cache.def.impl.NPCDefinition
|
||||
import core.game.dialogue.DialogueInterpreter
|
||||
import core.game.interaction.*
|
||||
import core.game.node.scenery.Scenery
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import core.game.node.Node
|
||||
import core.game.node.entity.impl.PulseType
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.scenery.Scenery
|
||||
import core.game.world.GameWorld
|
||||
import core.game.world.map.Region
|
||||
import core.net.packet.PacketProcessor
|
||||
import core.plugin.ClassScanner
|
||||
import core.game.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.world.map.path.ClipMaskSupplier
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import core.game.world.map.path.RsmodPathfinder
|
||||
import core.plugin.Plugin
|
||||
import core.tools.Log
|
||||
import org.rs09.consts.NPCs
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.rs09.consts.Scenery as SceneryIds
|
||||
|
||||
class PathfinderTests {
|
||||
companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners(); WoodcuttingListener().defineListeners() }; val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)}
|
||||
companion object {
|
||||
init {
|
||||
TestUtils.preTestSetup()
|
||||
GatheringSkillOptionListeners().defineListeners()
|
||||
WoodcuttingListener().defineListeners()
|
||||
BankBoothListener().defineListeners()
|
||||
}
|
||||
|
||||
@Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() {
|
||||
val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rsmodPathfinderShouldRejectDestinationsAtTheTruncationLimit() {
|
||||
val start = Location.create(3165, 3218, 0)
|
||||
|
||||
Assertions.assertTrue(RsmodPathfinder.canAttempt(start, Location.create(3203, 3186, 0)))
|
||||
Assertions.assertFalse(RsmodPathfinder.canAttempt(start, Location.create(3203, 3185, 0)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() {
|
||||
//clay fireplace - 13609 - sizex: 1, sizey: 2
|
||||
val scenery = Scenery(13609, Location.create(50, 50, 0))
|
||||
|
||||
scenery.rotation = 0
|
||||
val occupiedAt0 = scenery.occupiedTiles.toTypedArray()
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50,51)), occupiedAt0)
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 51)), occupiedAt0)
|
||||
|
||||
scenery.rotation = 1
|
||||
val occupiedAt1 = scenery.occupiedTiles.toTypedArray()
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(51,50)), occupiedAt1)
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(51, 50)), occupiedAt1)
|
||||
|
||||
scenery.rotation = 2
|
||||
val occupiedAt2 = scenery.occupiedTiles.toTypedArray()
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(50,49)), occupiedAt2)
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 49)), occupiedAt2)
|
||||
|
||||
scenery.rotation = 3
|
||||
val occupiedAt3 = scenery.occupiedTiles.toTypedArray()
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(49,50)), occupiedAt3)
|
||||
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(49, 50)), occupiedAt3)
|
||||
}
|
||||
|
||||
@Test fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() {
|
||||
@Test
|
||||
fun smartPathfinderShouldRespectSuppliedClipMask() {
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
val dest = Location.create(3202, 3200, 0)
|
||||
val blockedDestination = ClipMaskSupplier { _, x, y ->
|
||||
if (x == dest.x && y == dest.y) 0x100 else 0
|
||||
}
|
||||
|
||||
val path = Pathfinder.SMART.find(start, 1, dest, 1, 1, 0, 0, 0, true, blockedDestination)
|
||||
|
||||
Assertions.assertTrue(path.isSuccessful)
|
||||
Assertions.assertEquals(
|
||||
Location.create(3201, 3200, 0), Location.create(path.points.last.x, path.points.last.y, 0)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dumbPathfinderShouldNotRouteAroundBlockedCardinalStep() {
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
val dest = Location.create(3202, 3200, 0)
|
||||
val blockedMiddle = ClipMaskSupplier { _, x, y ->
|
||||
if (x == 3201 && y == 3200) movementBlockFlag else 0
|
||||
}
|
||||
|
||||
val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedMiddle)
|
||||
|
||||
Assertions.assertFalse(path.isSuccessful)
|
||||
Assertions.assertTrue(path.points.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dumbPathfinderShouldUseAxisFallbackForBlockedDiagonalStep() {
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
val dest = Location.create(3202, 3202, 0)
|
||||
val blockedNorth = ClipMaskSupplier { _, x, y ->
|
||||
if (x == 3200 && y == 3201) movementBlockFlag else 0
|
||||
}
|
||||
|
||||
val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedNorth)
|
||||
|
||||
Assertions.assertTrue(path.isSuccessful)
|
||||
Assertions.assertTrue(path.points.isNotEmpty())
|
||||
Assertions.assertEquals(3201, path.points.first.x)
|
||||
Assertions.assertEquals(3200, path.points.first.y)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun walkingQueueHasPathShouldIgnoreResetAnchor() {
|
||||
TestUtils.getMockPlayer("walkingQueueAnchor").use { player ->
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
player.location = start
|
||||
player.walkingQueue.reset()
|
||||
|
||||
Assertions.assertFalse(player.walkingQueue.hasPath())
|
||||
|
||||
player.walkingQueue.addPath(start.x + 1, start.y)
|
||||
|
||||
Assertions.assertTrue(player.walkingQueue.hasPath())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun projectilePathfinderShouldUseRsmodLineOfSightFlags() {
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
val dest = Location.create(3202, 3200, 0)
|
||||
RegionManager.loadClippingWindow(start, 128)
|
||||
try {
|
||||
RegionManager.setRsmodFlag(0, 3201, 3200, true, 0x20000)
|
||||
|
||||
val blocked = Pathfinder.PROJECTILE.find(start, 1, dest, 0, 0, 0, -1, 0, false, null)
|
||||
|
||||
Assertions.assertFalse(blocked.isSuccessful)
|
||||
Assertions.assertFalse(RsmodPathfinder.hasLineOfSightBetween(start, 1, dest, 1))
|
||||
RsmodPathfinder.loadLineOfSightWindow(start)
|
||||
Assertions.assertFalse(RsmodPathfinder.hasLineOfSightBetweenLoaded(start, 1, dest, 1))
|
||||
} finally {
|
||||
RegionManager.setRsmodFlag(0, 3201, 3200, true, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun projectilePathfinderShouldTreatZeroSizedLocationDestinationsAsOneTile() {
|
||||
val start = Location.create(3204, 3204, 0)
|
||||
val destinations =
|
||||
listOf(
|
||||
Location.create(3202, 3204, 0),
|
||||
Location.create(3204, 3202, 0),
|
||||
)
|
||||
val fixtureTiles =
|
||||
(3201..3204).flatMap { x ->
|
||||
(3201..3204).map { y -> Location.create(x, y, 0) }
|
||||
}
|
||||
RegionManager.loadClippingWindow(start, 128)
|
||||
val originalFlags = fixtureTiles.associateWith {
|
||||
RegionManager.getProjectileFlag(it.z, it.x, it.y)
|
||||
}
|
||||
try {
|
||||
for (tile in fixtureTiles) {
|
||||
RegionManager.setRsmodFlag(tile.z, tile.x, tile.y, true, 0)
|
||||
}
|
||||
|
||||
for (destination in destinations) {
|
||||
val path =
|
||||
Pathfinder.PROJECTILE.find(
|
||||
start,
|
||||
1,
|
||||
destination,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
)
|
||||
|
||||
Assertions.assertTrue(path.isSuccessful)
|
||||
Assertions.assertEquals(
|
||||
destination,
|
||||
Location.create(path.points.last.x, path.points.last.y, start.z),
|
||||
"A west/south projectile ray must stop on its zero-sized Location destination.",
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
for ((tile, flag) in originalFlags) {
|
||||
RegionManager.setRsmodFlag(tile.z, tile.x, tile.y, true, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun metadataSceneryInteractionShouldTriggerWhenAlreadyAtRsmodApproachTile() {
|
||||
TestUtils.getMockPlayer("bankBoothApproach").use { p ->
|
||||
val (booth, approach) = findReachableBankBoothFixture()
|
||||
p.location = approach
|
||||
val alreadyAtPath = Pathfinder.find(p, booth)
|
||||
Assertions.assertTrue(alreadyAtPath.isSuccessful)
|
||||
Assertions.assertFalse(alreadyAtPath.isMoveNear)
|
||||
|
||||
Assertions.assertTrue(InteractionListeners.run(booth.id, IntType.SCENERY, "bank", p, booth))
|
||||
TestUtils.advanceTicks(10, false)
|
||||
|
||||
Assertions.assertTrue(p.bank.isOpen)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun metadataSceneryCollectShouldTriggerWhenAlreadyAtRsmodApproachTile() {
|
||||
TestUtils.getMockPlayer("bankBoothCollectApproach").use { p ->
|
||||
val (booth, approach) = findReachableBankBoothFixture()
|
||||
p.location = approach
|
||||
var collected = false
|
||||
InteractionListeners.addMetadata(
|
||||
booth.id, IntType.SCENERY, arrayOf("collect"), InteractionListener.InteractionMetadata({ _, _, _ ->
|
||||
collected = true
|
||||
true
|
||||
}, 1, false)
|
||||
)
|
||||
|
||||
try {
|
||||
Assertions.assertTrue(InteractionListeners.run(booth.id, IntType.SCENERY, "collect", p, booth))
|
||||
TestUtils.advanceTicks(10, false)
|
||||
|
||||
Assertions.assertTrue(collected)
|
||||
} finally {
|
||||
BankBoothListener().defineListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directObjectMovementPulseShouldTriggerWhenAlreadyAtRsmodApproachTile() {
|
||||
TestUtils.getMockPlayer("objectPulseApproach").use { p ->
|
||||
val tree =
|
||||
RegionManager.getObject(0, 2720, 3475, 1307) ?: throw AssertionError("Expected test tree object.")
|
||||
val approach = findReachableApproachTile(tree)
|
||||
var pulsed = false
|
||||
p.location = approach
|
||||
|
||||
GameWorld.Pulser.submit(object : MovementPulse(p, tree) {
|
||||
override fun pulse(): Boolean {
|
||||
pulsed = true
|
||||
return true
|
||||
}
|
||||
})
|
||||
TestUtils.advanceTicks(3, false)
|
||||
|
||||
Assertions.assertTrue(pulsed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pathfinderShouldUseWrapperFootprintWhenSceneryChildIsSmallerThanWrapper() {
|
||||
TestUtils.getMockPlayer("taverleyPatchChildPath").use { p ->
|
||||
val wrapper = RegionManager.getObject(0, 2935, 3437, 8388)
|
||||
?: throw AssertionError("Expected Taverley tree patch wrapper.")
|
||||
val child = wrapper.getChild(p)
|
||||
val start = Location.create(2936, 3440, 0)
|
||||
|
||||
Assertions.assertEquals(8395, child.id)
|
||||
Assertions.assertTrue(wrapper.definition.sizeX * wrapper.definition.sizeY > child.definition.sizeX * child.definition.sizeY)
|
||||
|
||||
val path = Pathfinder.find(start, child)
|
||||
val last = path.points.lastOrNull() ?: throw AssertionError("Expected a path point.")
|
||||
val approach = Location.create(last.x, last.y, start.z)
|
||||
|
||||
Assertions.assertTrue(path.isSuccessful)
|
||||
Assertions.assertFalse(path.isMoveNear)
|
||||
Assertions.assertEquals(start, approach)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun entityMovementPulseShouldTriggerWhenDestinationOverrideIsAlreadyReached() {
|
||||
TestUtils.getMockPlayer("bankerOverrideApproach").use { p ->
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.isNeverWalks = true
|
||||
npc.init()
|
||||
p.location = ServerConstants.HOME_LOCATION
|
||||
var pulsed = false
|
||||
|
||||
GameWorld.Pulser.submit(object : MovementPulse(p, npc, DestinationFlag.ENTITY, { _, _ -> p.location }) {
|
||||
override fun pulse(): Boolean {
|
||||
pulsed = true
|
||||
return true
|
||||
}
|
||||
})
|
||||
TestUtils.advanceTicks(3, false)
|
||||
|
||||
Assertions.assertTrue(pulsed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingEntityMovementPulseShouldNotInteractFromDiagonalTile() {
|
||||
TestUtils.getMockPlayer("movingNpcDiagonalApproach").use { p ->
|
||||
val origin = Location.create(3200, 3600, 0)
|
||||
val npc = NPC.create(0, origin.transform(1, 1, 0))
|
||||
npc.init()
|
||||
p.location = origin
|
||||
p.settings.runEnergy = 100.0
|
||||
p.settings.setRunToggled(true)
|
||||
npc.walkingQueue.reset(false)
|
||||
npc.walkingQueue.addPath(origin.transform(4, 1, 0).x, origin.transform(4, 1, 0).y)
|
||||
|
||||
var pulseLocation: Location? = null
|
||||
var pulseTargetLocation: Location? = null
|
||||
try {
|
||||
GameWorld.Pulser.submit(object : MovementPulse(p, npc) {
|
||||
override fun pulse(): Boolean {
|
||||
pulseLocation = p.location
|
||||
pulseTargetLocation = npc.location
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
TestUtils.advanceTicks(1, false)
|
||||
Assertions.assertNull(
|
||||
pulseLocation, "A moving entity interaction must not trigger from a diagonal non-interaction tile."
|
||||
)
|
||||
|
||||
repeat(8) {
|
||||
if (pulseLocation == null) {
|
||||
TestUtils.advanceTicks(1, false)
|
||||
}
|
||||
}
|
||||
|
||||
val actualPulseLocation = pulseLocation
|
||||
?: throw AssertionError("Expected the movement pulse to eventually reach the moving NPC.")
|
||||
val actualTargetLocation = pulseTargetLocation
|
||||
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
|
||||
Assertions.assertTrue(
|
||||
Pathfinder.canInteract(
|
||||
actualPulseLocation.x,
|
||||
actualPulseLocation.y,
|
||||
p.size(),
|
||||
actualTargetLocation.x,
|
||||
actualTargetLocation.y,
|
||||
npc.size(),
|
||||
npc.size(),
|
||||
0,
|
||||
actualPulseLocation.z,
|
||||
null
|
||||
), "Entity interaction must fire only from a currently valid interaction tile."
|
||||
)
|
||||
} finally {
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun entityOptionHandlerShouldNotInteractFromNpcQueuedDestination() {
|
||||
TestUtils.getMockPlayer("queuedNpcPredictionGuard").use { p ->
|
||||
val origin = Location.create(3200, 3600, 0)
|
||||
val npc = NPC.create(0, origin.transform(0, 1, 0))
|
||||
npc.init()
|
||||
p.location = origin.transform(1, 0, 0)
|
||||
npc.walkingQueue.reset(false)
|
||||
npc.walkingQueue.addPath(origin.transform(4, 1, 0).x, origin.transform(4, 1, 0).y)
|
||||
|
||||
val optionHandler = object : OptionHandler() {
|
||||
override fun newInstance(_arg: Any?): Plugin<Any> {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun handle(_player: Player?, _node: Node?, _option: String?): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
var pulseLocation: Location? = null
|
||||
var pulseTargetLocation: Location? = null
|
||||
try {
|
||||
GameWorld.Pulser.submit(object : MovementPulse(p, npc, optionHandler) {
|
||||
override fun pulse(): Boolean {
|
||||
pulseLocation = p.location
|
||||
pulseTargetLocation = npc.location
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
TestUtils.advanceTicks(1, false)
|
||||
Assertions.assertNull(
|
||||
pulseLocation,
|
||||
"Option-handler entity interaction must not fire from a tile that only reaches the NPC's queued destination."
|
||||
)
|
||||
|
||||
repeat(8) {
|
||||
if (pulseLocation == null) {
|
||||
TestUtils.advanceTicks(1, false)
|
||||
}
|
||||
}
|
||||
|
||||
val actualPulseLocation = pulseLocation
|
||||
?: throw AssertionError("Expected the movement pulse to eventually reach the moving NPC.")
|
||||
val actualTargetLocation = pulseTargetLocation
|
||||
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
|
||||
Assertions.assertTrue(
|
||||
Pathfinder.canInteract(
|
||||
actualPulseLocation.x,
|
||||
actualPulseLocation.y,
|
||||
p.size(),
|
||||
actualTargetLocation.x,
|
||||
actualTargetLocation.y,
|
||||
npc.size(),
|
||||
npc.size(),
|
||||
0,
|
||||
actualPulseLocation.z,
|
||||
null
|
||||
), "Entity interaction must fire only from a currently valid interaction tile."
|
||||
)
|
||||
} finally {
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun entityMovementPulseShouldIgnoreMissingTargetLocation() {
|
||||
TestUtils.getMockPlayer("missingTargetLocationGuard").use { p ->
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.init()
|
||||
val originalNpcLocation = npc.location
|
||||
var pulsed = false
|
||||
try {
|
||||
npc.location = null
|
||||
val pulse = object : MovementPulse(p, npc) {
|
||||
override fun pulse(): Boolean {
|
||||
pulsed = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Assertions.assertFalse(pulse.update())
|
||||
Assertions.assertFalse(pulsed)
|
||||
} finally {
|
||||
npc.location = originalNpcLocation
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun entityMovementPulseShouldIgnoreMissingMoverLocation() {
|
||||
TestUtils.getMockPlayer("missingMoverLocationGuard").use { p ->
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.init()
|
||||
val locationField = Node::class.java.getDeclaredField("location")
|
||||
locationField.isAccessible = true
|
||||
val originalPlayerLocation = p.location
|
||||
var pulsed = false
|
||||
try {
|
||||
locationField.set(p, null)
|
||||
val pulse = object : MovementPulse(p, npc) {
|
||||
override fun pulse(): Boolean {
|
||||
pulsed = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Assertions.assertFalse(pulse.update())
|
||||
Assertions.assertFalse(pulsed)
|
||||
} finally {
|
||||
locationField.set(p, originalPlayerLocation)
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runEnabledEntityMovementPulseShouldCatchWalkingNpcMovingDirectlyAway() {
|
||||
TestUtils.getMockPlayer("runNpcInteractionChaser").use { p ->
|
||||
val origin = openHorizontalInteractionOrigin()
|
||||
val npc = NPC.create(0, origin.transform(4, 0, 0))
|
||||
npc.init()
|
||||
p.location = origin
|
||||
p.settings.runEnergy = 100.0
|
||||
p.settings.setRunToggled(true)
|
||||
npc.walkingQueue.reset(false)
|
||||
npc.walkingQueue.addPath(origin.transform(12, 0, 0).x, origin.transform(12, 0, 0).y)
|
||||
|
||||
var pulseLocation: Location? = null
|
||||
var pulseTargetLocation: Location? = null
|
||||
try {
|
||||
GameWorld.Pulser.submit(object : MovementPulse(p, npc) {
|
||||
override fun pulse(): Boolean {
|
||||
pulseLocation = p.location
|
||||
pulseTargetLocation = npc.location
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
repeat(6) {
|
||||
if (pulseLocation == null) {
|
||||
TestUtils.advanceTicks(1, false)
|
||||
}
|
||||
}
|
||||
|
||||
val actualPulseLocation = pulseLocation ?: throw AssertionError(
|
||||
"Run-enabled player should catch the walking NPC before it stops. " + "player=${p.location}, npc=${npc.location}, queue=${p.walkingQueue.queue}"
|
||||
)
|
||||
val actualTargetLocation = pulseTargetLocation
|
||||
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
|
||||
Assertions.assertNotEquals(
|
||||
origin.transform(12, 0, 0),
|
||||
actualTargetLocation,
|
||||
"The interaction should not wait until the NPC finishes walking away."
|
||||
)
|
||||
Assertions.assertTrue(
|
||||
Pathfinder.canInteract(
|
||||
actualPulseLocation.x,
|
||||
actualPulseLocation.y,
|
||||
p.size(),
|
||||
actualTargetLocation.x,
|
||||
actualTargetLocation.y,
|
||||
npc.size(),
|
||||
npc.size(),
|
||||
0,
|
||||
actualPulseLocation.z,
|
||||
null
|
||||
), "Entity interaction must fire from a currently valid interaction tile."
|
||||
)
|
||||
} finally {
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun interactionListenerShouldUseOptionHandlerDestinationWhenNoListenerDestinationOverride() {
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.isNeverWalks = true
|
||||
npc.init()
|
||||
|
||||
var listenerRan = false
|
||||
var optionHandlerRan = false
|
||||
val optionName = "listener-custom-destination"
|
||||
val option = Option(optionName, 4)
|
||||
val destinationHandler = object : OptionHandler() {
|
||||
override fun newInstance(arg: Any?): Plugin<Any> {
|
||||
NPCDefinition.forId(0).handlers["option:$optionName"] = this
|
||||
return this
|
||||
}
|
||||
|
||||
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
|
||||
optionHandlerRan = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getDestination(n: Node, node: Node): Location {
|
||||
return n.location
|
||||
}
|
||||
}
|
||||
destinationHandler.newInstance(null)
|
||||
option.handler = destinationHandler
|
||||
npc.interaction.set(option)
|
||||
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf(optionName)) { _, _ ->
|
||||
listenerRan = true
|
||||
true
|
||||
}
|
||||
|
||||
TestUtils.getMockPlayer("listenerOptionDestination").use { p ->
|
||||
p.location = ServerConstants.HOME_LOCATION
|
||||
TestUtils.simulateInteraction(p, npc, 4)
|
||||
TestUtils.advanceTicks(3, false)
|
||||
|
||||
Assertions.assertTrue(listenerRan)
|
||||
Assertions.assertFalse(optionHandlerRan)
|
||||
Assertions.assertEquals(ServerConstants.HOME_LOCATION, p.location)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun genericTalkToShouldOpenGrandExchangeClerkDialogueFromCounterApproachTile() {
|
||||
GrandExchangePlugin().newInstance(null)
|
||||
if (!DialogueInterpreter.contains(6528)) {
|
||||
GrandExchangeClerk().init()
|
||||
}
|
||||
if (InteractionListeners.get("talk-to", IntType.NPC.ordinal) == null) {
|
||||
NPCTalkListener().defineListeners()
|
||||
}
|
||||
|
||||
val clerk = NPC.create(6528, Location.create(3165, 3491, 0), Direction.NORTH)
|
||||
clerk.isNeverWalks = true
|
||||
clerk.init()
|
||||
|
||||
try {
|
||||
TestUtils.getMockPlayer("geClerkTalk").use { p ->
|
||||
p.location = Location.create(3165, 3492, 0)
|
||||
|
||||
TestUtils.simulateInteraction(p, clerk, 0)
|
||||
TestUtils.advanceTicks(3, false)
|
||||
|
||||
Assertions.assertNotNull(p.dialogueInterpreter.dialogue)
|
||||
Assertions.assertEquals(GrandExchangeClerk::class.java, p.dialogueInterpreter.dialogue.javaClass)
|
||||
Assertions.assertEquals(Location.create(3165, 3492, 0), p.location)
|
||||
}
|
||||
} finally {
|
||||
clerk.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findReachableBankBoothFixture(): Pair<Scenery, Location> {
|
||||
val base = ServerConstants.HOME_LOCATION!!.transform(8, 8, 0)
|
||||
for (rotation in 0..3) {
|
||||
val booth = Scenery(SceneryIds.BANK_BOOTH_2213, base, 10, rotation)
|
||||
runCatching { findReachableApproachTile(booth) }.getOrNull()?.let { return booth to it }
|
||||
}
|
||||
throw AssertionError("Could not find a reachable synthetic bank booth fixture.")
|
||||
}
|
||||
|
||||
private fun findReachableApproachTile(scenery: Scenery): Location {
|
||||
for (radius in 1..8) {
|
||||
for (x in scenery.location.x - radius..scenery.location.x + radius) {
|
||||
for (y in scenery.location.y - radius..scenery.location.y + radius) {
|
||||
val start = Location.create(x, y, scenery.location.z)
|
||||
if (!RegionManager.isTeleportPermitted(start)) {
|
||||
continue
|
||||
}
|
||||
val path = Pathfinder.find(start, scenery)
|
||||
if (!path.isSuccessful || path.isMoveNear) {
|
||||
continue
|
||||
}
|
||||
val point = path.points.lastOrNull()
|
||||
val approach = Location.create(point?.x ?: start.x, point?.y ?: start.y, start.z)
|
||||
val check = Pathfinder.find(approach, scenery)
|
||||
if (check.isSuccessful && !check.isMoveNear) {
|
||||
return approach
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw AssertionError("Could not find a reachable approach tile for $scenery.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() {
|
||||
val start = Location.create(2731, 3481)
|
||||
val dest = RegionManager.getObject(0, 2720, 3475, 1307)
|
||||
val p = TestUtils.getMockPlayer("treefindtest")
|
||||
|
|
@ -59,15 +659,17 @@ class PathfinderTests {
|
|||
Assertions.assertEquals(Location.create(2722, 3475, 0), p.location)
|
||||
}
|
||||
|
||||
@Test fun movementInteractionShouldTrigger() {
|
||||
@Test
|
||||
fun movementInteractionShouldTrigger() {
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.init()
|
||||
|
||||
var intListenerRan = false
|
||||
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = {player: Player, node: Node ->
|
||||
intListenerRan = true
|
||||
return@add true
|
||||
})
|
||||
InteractionListeners.add(
|
||||
0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = { player: Player, node: Node ->
|
||||
intListenerRan = true
|
||||
return@add true
|
||||
})
|
||||
|
||||
var pluginRan = false
|
||||
val option = Option("testoption", 4)
|
||||
|
|
@ -77,6 +679,7 @@ class PathfinderTests {
|
|||
NPCDefinition.forId(0).handlers["option:testoption"] = this
|
||||
return this
|
||||
}
|
||||
|
||||
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
|
||||
pluginRan = true
|
||||
return true
|
||||
|
|
@ -87,7 +690,7 @@ class PathfinderTests {
|
|||
npc.interaction.set(option2)
|
||||
option.handler = testHandler
|
||||
|
||||
TestUtils.getMockPlayer("interactionTest").use {p ->
|
||||
TestUtils.getMockPlayer("interactionTest").use { p ->
|
||||
p.location = ServerConstants.HOME_LOCATION
|
||||
TestUtils.simulateInteraction(p, npc, 0)
|
||||
TestUtils.advanceTicks(10, false)
|
||||
|
|
@ -99,8 +702,9 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() {
|
||||
TestUtils.getMockPlayer("idlenpcdest").use {p ->
|
||||
@Test
|
||||
fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() {
|
||||
TestUtils.getMockPlayer("idlenpcdest").use { p ->
|
||||
val startLoc = ServerConstants.HOME_LOCATION
|
||||
p.location = startLoc
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
|
|
@ -116,8 +720,9 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() {
|
||||
TestUtils.getMockPlayer("entitystoptest").use {p ->
|
||||
@Test
|
||||
fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() {
|
||||
TestUtils.getMockPlayer("entitystoptest").use { p ->
|
||||
p.location = ServerConstants.HOME_LOCATION
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.isNeverWalks = true
|
||||
|
|
@ -133,7 +738,8 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() {
|
||||
@Test
|
||||
fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() {
|
||||
TestUtils.getMockPlayer("entitystoptest2").use { p ->
|
||||
p.location = ServerConstants.HOME_LOCATION
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
|
|
@ -150,7 +756,8 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun doubleMovementPulseToEntityShouldNotStopOnSameTile() {
|
||||
@Test
|
||||
fun doubleMovementPulseToEntityShouldNotStopOnSameTile() {
|
||||
TestUtils.getMockPlayer("entitystoptest3").use { p ->
|
||||
p.location = ServerConstants.HOME_LOCATION
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
|
|
@ -173,12 +780,14 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() {
|
||||
@Test
|
||||
fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() {
|
||||
val testHandler = object : OptionHandler() {
|
||||
override fun newInstance(arg: Any?): Plugin<Any> {
|
||||
NPCDefinition.forId(0).handlers["option:testoption"] = this
|
||||
return this
|
||||
}
|
||||
|
||||
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
|
||||
log(this::class.java, Log.ERR, "Interaction triggered")
|
||||
return true
|
||||
|
|
@ -202,14 +811,16 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() {
|
||||
@Test
|
||||
fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() {
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.isNeverWalks = true
|
||||
npc.init()
|
||||
|
||||
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = {player: Player, node: Node ->
|
||||
return@add true
|
||||
})
|
||||
InteractionListeners.add(
|
||||
0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = { player: Player, node: Node ->
|
||||
return@add true
|
||||
})
|
||||
val opt = Option("testoptlistener2", 1)
|
||||
npc.interaction.set(opt)
|
||||
|
||||
|
|
@ -223,7 +834,8 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun npcShouldReliablyReturnToSpawnLocationIfTooFar() {
|
||||
@Test
|
||||
fun npcShouldReliablyReturnToSpawnLocationIfTooFar() {
|
||||
//spawn a player into the area just to make sure it ticks...
|
||||
TestUtils.getMockPlayer("areatest").use { p ->
|
||||
val npc = NPC(1, Location.create(3240, 3226, 0))
|
||||
|
|
@ -239,7 +851,106 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() {
|
||||
@Test
|
||||
fun npcReturnToSpawnShouldUseOverriddenWalkRadius() {
|
||||
TestUtils.getMockPlayer("overriddenRadiusReturn").use {
|
||||
val spawn = ServerConstants.HOME_LOCATION!!
|
||||
val npc = object : NPC(1, spawn.transform(5, 0, 0)) {
|
||||
override fun getWalkRadius(): Int {
|
||||
return 3
|
||||
}
|
||||
}
|
||||
npc.isWalks = true
|
||||
npc.isNeverWalks = false
|
||||
npc.init()
|
||||
npc.properties.spawnLocation = spawn
|
||||
try {
|
||||
npc.handleTickActions()
|
||||
|
||||
Assertions.assertEquals(true, npc.getAttribute("return-to-spawn", false))
|
||||
} finally {
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun randomWalkingNpcShouldUseSideStepWhenDirectNorthTileIsBlocked() {
|
||||
val origin = Location.create(3200, 3600, 0)
|
||||
val blocked = origin.transform(0, 1, 0)
|
||||
val destination = origin.transform(0, 2, 0)
|
||||
val sidesteps = setOf(
|
||||
origin.transform(-1, 0, 0), origin.transform(1, 0, 0)
|
||||
)
|
||||
val npc = FixedDestinationNPC(origin, destination, 3)
|
||||
npc.isWalks = true
|
||||
npc.isNeverWalks = false
|
||||
npc.init()
|
||||
npc.properties.spawnLocation = origin
|
||||
RegionManager.addClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
|
||||
try {
|
||||
npc.resetWalk()
|
||||
repeat(20) {
|
||||
if (!npc.walkingQueue.hasPath()) {
|
||||
npc.handleTickActions()
|
||||
}
|
||||
}
|
||||
|
||||
Assertions.assertTrue(
|
||||
npc.walkingQueue.hasPath(),
|
||||
"Random-walking NPCs should use an open east/west sidestep when the direct north tile is blocked."
|
||||
)
|
||||
npc.walkingQueue.update()
|
||||
Assertions.assertTrue(
|
||||
npc.location in sidesteps,
|
||||
"Random-walking NPC should only take a local sidestep, not route through the blocked north tile. " + "npc=${npc.location}"
|
||||
)
|
||||
} finally {
|
||||
RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun randomWalkingNpcShouldNotFullyRouteAroundBlockedLocalDestination() {
|
||||
val origin = Location.create(3200, 3600, 0)
|
||||
val blocked = origin.transform(1, 0, 0)
|
||||
val destination = origin.transform(2, 0, 0)
|
||||
val sidesteps = setOf(
|
||||
origin.transform(0, -1, 0), origin.transform(0, 1, 0)
|
||||
)
|
||||
val npc = FixedDestinationNPC(origin, destination, 3)
|
||||
npc.isWalks = true
|
||||
npc.isNeverWalks = false
|
||||
npc.init()
|
||||
npc.properties.spawnLocation = origin
|
||||
RegionManager.addClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
|
||||
try {
|
||||
npc.resetWalk()
|
||||
repeat(20) {
|
||||
if (!npc.walkingQueue.hasPath()) {
|
||||
npc.handleTickActions()
|
||||
}
|
||||
}
|
||||
|
||||
Assertions.assertTrue(
|
||||
npc.walkingQueue.hasPath(),
|
||||
"Random-walking NPCs should use a local sidestep instead of taking an RSMOD detour."
|
||||
)
|
||||
npc.walkingQueue.update()
|
||||
Assertions.assertTrue(
|
||||
npc.location in sidesteps,
|
||||
"Random-walking NPC should not fully route around a clipped boundary tile. npc=${npc.location}"
|
||||
)
|
||||
Assertions.assertNotEquals(destination, npc.location)
|
||||
} finally {
|
||||
RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() {
|
||||
//spawn a player into the area just to make sure it ticks...
|
||||
TestUtils.getMockPlayer("areaunloadtest").use { p ->
|
||||
val npc = NPC(1, Location.create(3240, 3226, 0))
|
||||
|
|
@ -257,4 +968,45 @@ class PathfinderTests {
|
|||
Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION!!) <= 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FixedDestinationNPC(
|
||||
location: Location, private val destination: Location, private val radius: Int
|
||||
) : NPC(1, location) {
|
||||
override fun getMovementDestination(): Location {
|
||||
return destination
|
||||
}
|
||||
|
||||
override fun getWalkRadius(): Int {
|
||||
return radius
|
||||
}
|
||||
}
|
||||
|
||||
private val movementBlockFlag =
|
||||
Pathfinder.PREVENT_NORTH or Pathfinder.PREVENT_EAST or Pathfinder.PREVENT_SOUTH or Pathfinder.PREVENT_WEST
|
||||
|
||||
private fun openHorizontalInteractionOrigin(): Location {
|
||||
val start = Location.create(3200, 3600, 0)
|
||||
for (dy in -16..16) {
|
||||
for (dx in -16..16) {
|
||||
val candidate = start.transform(dx, dy, 0)
|
||||
if ((0..12).all { RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) } && (0..11).all {
|
||||
Pathfinder.canInteract(
|
||||
candidate.x + it,
|
||||
candidate.y,
|
||||
1,
|
||||
candidate.x + it + 1,
|
||||
candidate.y,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
candidate.z,
|
||||
null
|
||||
)
|
||||
}) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
throw AssertionError("No open horizontal interaction test line found near $start.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
Server/src/test/kotlin/core/WalkingQueueTests.kt
Normal file
59
Server/src/test/kotlin/core/WalkingQueueTests.kt
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package core
|
||||
|
||||
import TestUtils
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class WalkingQueueTests {
|
||||
companion object {
|
||||
init {
|
||||
TestUtils.preTestSetup()
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun resetClearsDrawRouteMarkersBeforeQueuingNewDestination() {
|
||||
TestUtils.getMockPlayer("drawRouteReset").use { player ->
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
player.location = start
|
||||
player.setAttribute("routedraw", true)
|
||||
|
||||
val queue = player.walkingQueue
|
||||
queue.reset()
|
||||
queue.addPath(start.x + 3, start.y)
|
||||
|
||||
val firstRouteItemLocation = queue.routeItems.firstOrNull()?.location
|
||||
?: throw AssertionError("Expected the first route to draw route markers.")
|
||||
Assertions.assertNotNull(
|
||||
RegionManager.getRegionPlane(firstRouteItemLocation).getItem(
|
||||
DRAW_ROUTE_ITEM_ID,
|
||||
firstRouteItemLocation,
|
||||
player
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
queue.reset()
|
||||
queue.addPath(start.x, start.y + 3)
|
||||
|
||||
Assertions.assertNull(
|
||||
RegionManager.getRegionPlane(firstRouteItemLocation).getItem(
|
||||
DRAW_ROUTE_ITEM_ID,
|
||||
firstRouteItemLocation,
|
||||
player
|
||||
),
|
||||
"The first route marker should be removed when a new movement destination resets the queue."
|
||||
)
|
||||
Assertions.assertTrue(
|
||||
queue.routeItems.any { it.location != firstRouteItemLocation },
|
||||
"Expected the second route to draw its own markers."
|
||||
)
|
||||
} finally {
|
||||
queue.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DRAW_ROUTE_ITEM_ID = 13444
|
||||
Loading…
Add table
Add a link
Reference in a new issue