Revenants have been reworked to feel much more present, more alive and more dangerous - Beware!

Revenant spawns, patrols and areas of exploration are now based on era-authentic maps
New revenant command for admins ::setrevcap to set max number of revenants
New server config world option revenant_population
This commit is contained in:
Ceikry 2022-06-13 12:47:15 +00:00 committed by Ryan
parent 053edc3f34
commit 0e74cf0495
7 changed files with 264 additions and 107 deletions

View file

@ -15,15 +15,18 @@ import core.game.world.map.path.Pathfinder;
import core.game.world.map.zone.ZoneBorders;
import core.game.world.map.zone.impl.WildernessZone;
import core.game.world.update.flag.context.Animation;
import core.plugin.Initializable;
import core.tools.RandomFunction;
import rs09.game.content.zone.wilderness.RevenantController;
import rs09.game.node.entity.combat.CombatSwingHandler;
import rs09.game.system.config.NPCConfigParser;
import rs09.game.world.GameWorld;
/**
* Handles a revenant NPC.
* @author Vexia
* @author Ceikry-ish (mostly Vexia code still)
*/
@Initializable
public class RevenantNPC extends AbstractNPC {
/**
@ -74,21 +77,21 @@ public class RevenantNPC extends AbstractNPC {
setWalks(true);
setRespawn(false);
setAggressive(true);
setRenderable(true);
setDefaultBehavior();
getAggressiveHandler().setRadius(64 * 2);
getAggressiveHandler().setChanceRatio(9);
getAggressiveHandler().setAllowTolerance(false);
getProperties().setCombatTimeOut(120);
configureRoute();
super.configure();
configureBonuses();
super.configure();
this.swingHandler = new RevenantCombatHandler(getProperties().getAttackAnimation(), getProperties().getMagicAnimation(), getProperties().getRangeAnimation());
}
@Override
public void init() {
super.init();
RevenantPlugin.getRevenants().add(this);
RevenantController.registerRevenant(this);
int spawnAnim = getDefinition().getConfiguration(NPCConfigParser.SPAWN_ANIMATION, -1);
if (spawnAnim != -1) {
animate(new Animation(spawnAnim));
@ -98,8 +101,7 @@ public class RevenantNPC extends AbstractNPC {
@Override
public void clear() {
super.clear();
RevenantPlugin.getRevenants().remove(this);
RevenantPlugin.spawn();
RevenantController.unregisterRevenant(this);
}
@Override
@ -111,7 +113,11 @@ public class RevenantNPC extends AbstractNPC {
}
@Override
public void handleTickActions() {
public void tick() {
skills.pulse();
getWalkingQueue().update();
if (this.getViewport().getRegion().isActive())
getUpdateMasks().prepare(this);
if (!DeathTask.isDead(this) && getSkills().getLifepoints() <= (getSkills().getStaticLevel(Skills.HITPOINTS) / 2) && getAttribute("eat-delay", 0) < GameWorld.getTicks()) {
lock(3);
getProperties().getCombatPulse().delayNextAttack(3);
@ -121,15 +127,6 @@ public class RevenantNPC extends AbstractNPC {
}
setAttribute("eat-delay", GameWorld.getTicks() + 6);
}
if (!getLocks().isMovementLocked()) {
if (!getPulseManager().hasPulseRunning() && !getProperties().getCombatPulse().isAttacking() && !getProperties().getCombatPulse().isInCombat() && nextWalk < GameWorld.getTicks()) {
setNextWalk();
Location l = getMovementDestination();
if (canMove(l)) {
Pathfinder.find(this, l, true, Pathfinder.SMART).walk(this);
}
}
}
if (aggressiveHandler != null && aggressiveHandler.selectTarget()) {
return;
}

View file

@ -1,90 +0,0 @@
package core.game.node.entity.npc.revenant;
import java.util.ArrayList;
import java.util.List;
import core.game.node.entity.npc.NPC;
import core.game.world.map.Location;
import core.plugin.Initializable;
import core.plugin.Plugin;
import rs09.plugin.ClassScanner;
import core.tools.RandomFunction;
/**
* Handles the revenants.
* @author Vexia
*/
@Initializable
public class RevenantPlugin implements Plugin<Object> {
/**
* The revenants npc.
*/
private static final List<NPC> REVENANTS = new ArrayList<>(20);
/**
* The spawning locations.
*/
private static final Location[] SPAWN_LOCATIONS = new Location[] { new Location(2968, 3695), new Location(2956, 3716), new Location(2969, 3753), new Location(2967, 3817), new Location(2976, 3849), new Location(2976, 3725), new Location(2973, 3576), new Location(2982, 3850), new Location(3348, 3822), new Location(3362, 3808), new Location(3026, 3734), new Location(2961, 3792), new Location(2984, 3801), new Location(2974, 3744), new Location(2984, 3718), new Location(2990, 3695), new Location(2960, 3590), new Location(3020, 3674), new Location(3019, 3723), new Location(3019, 3822), Location.create(3123, 3567, 0), Location.create(3123, 3567, 0), Location.create(3201, 3678, 0), Location.create(3220, 3755, 0), Location.create(3249, 3882, 0), Location.create(3283, 3893, 0), Location.create(3034, 3938, 0), Location.create(2964, 3617, 0), Location.create(3253, 3922, 0), Location.create(3205, 3907, 0), Location.create(3194, 3895, 0), Location.create(3168, 3788, 0), Location.create(3287, 3557, 0), Location.create(3327, 3558, 0), Location.create(3364, 3536, 0), Location.create(3262, 3595, 0), Location.create(3099, 3957, 0), Location.create(3028, 3915, 0), Location.create(3285, 3922, 0), Location.create(2980, 3855, 0), Location.create(3243, 3917, 0), Location.create(3190, 3630, 0), Location.create(3188, 3585, 0), Location.create(3117, 3590, 0), Location.create(3136, 3624, 0), Location.create(3356, 3701, 0) };
/**
* The maximum amount of revenants spawned.
*/
private static final int MAX = 20;
@Override
public Plugin<Object> newInstance(Object arg) throws Throwable {
ClassScanner.definePlugin(new RevenantNPC());
//CorruptEquipment.init();
//PVPEquipment.init();
spawn();
return this;
}
/**
* Spawns the revenants.
*/
public static void spawn() {
int size = REVENANTS.size();
int left = MAX - size;
List<Location> taken = new ArrayList<>(20);
for (NPC n : REVENANTS) {
taken.add(n.getProperties().getSpawnLocation());
}
if (left > 0) {
int spawnAmount = RandomFunction.random(1, left);
if (size == 0) {
spawnAmount = MAX;
}
for (int i = 0; i < spawnAmount; i++) {
Location loc = null;
while (loc == null) {
Location l = RandomFunction.getRandomElement(SPAWN_LOCATIONS);
if (taken.contains(l)) {
continue;
}
loc = l;
}
taken.add(loc);
RevenantType type = RandomFunction.getRandomElement(RevenantType.values());
int id = type.getIds()[0];
NPC revenant = NPC.create(id, loc);
revenant.init();
}
}
}
@Override
public Object fireEvent(String identifier, Object... args) {
return null;
}
/**
* Gets the revenants.
* @return the revenants
*/
public static List<NPC> getRevenants() {
return REVENANTS;
}
}

View file

@ -293,4 +293,8 @@ public class RandomFunction {
}
return RANDOM.nextInt(value);
}
public static boolean nextBool() {
return RANDOM.nextBoolean();
}
}

View file

@ -13,6 +13,9 @@ import java.math.BigInteger
class ServerConstants {
companion object {
@JvmField
var REVENANT_POPULATION: Int = 30
@JvmField
var BOTS_INFLUENCE_PRICE_INDEX = true
@JvmField

View file

@ -0,0 +1,239 @@
package rs09.game.content.zone.wilderness
import api.Commands
import api.TickListener
import api.poofClear
import api.teleport
import core.game.interaction.MovementPulse
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.revenant.RevenantNPC
import core.game.node.entity.npc.revenant.RevenantType
import core.game.node.entity.player.link.TeleportManager
import core.game.system.task.Pulse
import core.game.world.map.Location
import core.game.world.map.zone.ZoneBorders
import core.game.world.update.flag.context.Graphics
import core.tools.RandomFunction
import rs09.ServerConstants
import rs09.game.system.SystemLogger
import rs09.game.system.command.Privilege
import rs09.game.world.GameWorld
import rs09.game.world.repository.Repository
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
class RevenantController : TickListener, Commands {
companion object {
private val trackedRevenants = ArrayList<RevenantNPC>()
private val taskTimeRemaining = HashMap<RevenantNPC, Int>()
private val currentTask = HashMap<RevenantNPC, RevenantTask>()
private var expectedRevAmount: Int = ServerConstants.REVENANT_POPULATION
private var groupPatrolQueue = ArrayList<RevenantNPC>()
@JvmStatic fun registerRevenant(revenantNPC: RevenantNPC) {
trackedRevenants.add(revenantNPC)
taskTimeRemaining[revenantNPC] = 0
currentTask[revenantNPC] = RevenantTask.NONE
Repository.RENDERABLE_NPCS.add(revenantNPC)
}
@JvmStatic fun unregisterRevenant(revenantNPC: RevenantNPC) {
trackedRevenants.remove(revenantNPC)
taskTimeRemaining.remove(revenantNPC)
currentTask.remove(revenantNPC)
Repository.RENDERABLE_NPCS.remove(revenantNPC)
}
val routes = listOf(
arrayOf(Location.create(3070, 3651), Location.create(3083, 3640), Location.create(3106, 3645), Location.create(3133, 3647), Location.create(3149, 3642), Location.create(3160, 3654), Location.create(3171, 3665, 0), Location.create(3189, 3663, 0), Location.create(3202, 3675, 0), Location.create(3217, 3660, 0), Location.create(3235, 3661, 0), Location.create(3235, 3661, 0), Location.create(3279, 3650, 0), Location.create(3269, 3636, 0), Location.create(3253, 3632, 0), Location.create(3236, 3638, 0), Location.create(3220, 3637, 0), Location.create(3203, 3634, 0), Location.create(3187, 3631, 0), Location.create(3166, 3633, 0), Location.create(3160, 3616, 0), Location.create(3148, 3604, 0), Location.create(3134, 3596, 0), Location.create(3118, 3590, 0), Location.create(3104, 3597, 0)),
arrayOf(Location.create(3077, 3565, 0), Location.create(3093, 3559, 0), Location.create(3110, 3566, 0), Location.create(3127, 3574, 0), Location.create(3146, 3571, 0), Location.create(3164, 3575, 0), Location.create(3183, 3573, 0), Location.create(3197, 3587, 0), Location.create(3215, 3584, 0), Location.create(3233, 3576, 0), Location.create(3251, 3573, 0), Location.create(3269, 3577, 0), Location.create(3287, 3569, 0), Location.create(3305, 3568, 0), Location.create(3321, 3576, 0), Location.create(3338, 3584, 0), Location.create(3352, 3573, 0), Location.create(3354, 3554, 0), Location.create(3342, 3541, 0), Location.create(3324, 3536, 0), Location.create(3306, 3543, 0), Location.create(3290, 3544, 0), Location.create(3272, 3545, 0), Location.create(3255, 3546, 0), Location.create(3239, 3539, 0), Location.create(3222, 3543, 0), Location.create(3206, 3548, 0), Location.create(3189, 3549, 0), Location.create(3173, 3552, 0), Location.create(3157, 3549, 0), Location.create(3140, 3548, 0), Location.create(3122, 3548, 0), Location.create(3110, 3555, 0)),
arrayOf(Location.create(3318, 3691, 0), Location.create(3307, 3700, 0), Location.create(3290, 3696, 0), Location.create(3277, 3706, 0), Location.create(3260, 3706, 0), Location.create(3250, 3707, 0), Location.create(3245, 3723, 0), Location.create(3254, 3735, 0), Location.create(3251, 3754, 0), Location.create(3243, 3768, 0), Location.create(3253, 3780, 0), Location.create(3238, 3783, 0), Location.create(3224, 3793, 0), Location.create(3206, 3786, 0), Location.create(3192, 3780, 0), Location.create(3170, 3787, 0), Location.create(3156, 3800, 0), Location.create(3148, 3814, 0), Location.create(3148, 3814, 0), Location.create(3127, 3840, 0), Location.create(3124, 3856, 0), Location.create(3124, 3872, 0), Location.create(3116, 3892, 0)),
arrayOf(Location.create(2949, 3890, 0), Location.create(2965, 3899, 0), Location.create(2984, 3900, 0), Location.create(2998, 3895, 0), Location.create(3016, 3898, 0), Location.create(3032, 3893, 0), Location.create(3048, 3897, 0), Location.create(3068, 3894, 0), Location.create(3084, 3898, 0), Location.create(3101, 3895, 0), Location.create(3118, 3897, 0), Location.create(3136, 3893, 0), Location.create(3154, 3900, 0), Location.create(3172, 3895, 0), Location.create(3189, 3892, 0), Location.create(3206, 3897, 0), Location.create(3222, 3890, 0), Location.create(3240, 3897, 0), Location.create(3259, 3892, 0), Location.create(3278, 3895, 0), Location.create(3296, 3892, 0), Location.create(3313, 3899, 0), Location.create(3331, 3888, 0), Location.create(3345, 3880, 0)),
arrayOf(Location.create(3308, 3941, 0), Location.create(3301, 3925, 0), Location.create(3287, 3915, 0), Location.create(3276, 3922, 0), Location.create(3266, 3938, 0), Location.create(3267, 3952, 0), Location.create(3250, 3949, 0), Location.create(3235, 3944, 0), Location.create(3219, 3944, 0), Location.create(3206, 3938, 0), Location.create(3194, 3929, 0), Location.create(3182, 3921, 0), Location.create(3174, 3936, 0), Location.create(3180, 3952, 0), Location.create(3167, 3960, 0), Location.create(3155, 3959, 0), Location.create(3141, 3953, 0), Location.create(3126, 3954, 0), Location.create(3110, 3961, 0), Location.create(3093, 3962, 0), Location.create(3078, 3953, 0), Location.create(3066, 3942, 0), Location.create(3059, 3929, 0), Location.create(3049, 3916, 0), Location.create(3033, 3924, 0), Location.create(3020, 3921, 0), Location.create(3010, 3913, 0), Location.create(2993, 3906, 0), Location.create(2977, 3911, 0), Location.create(2970, 3928, 0))
)
val spawnLocations = listOf(Location.create(3075, 3553, 0), Location.create(3077, 3563, 0), Location.create(3077, 3578, 0), Location.create(3093, 3581, 0), Location.create(3103, 3570, 0), Location.create(3101, 3564, 0), Location.create(3030, 3596, 0), Location.create(3015, 3598, 0), Location.create(3000, 3593, 0), Location.create(2986, 3588, 0), Location.create(2969, 3701, 0), Location.create(2982, 3689, 0), Location.create(2967, 3689, 0), Location.create(2953, 3711, 0), Location.create(2966, 3759, 0), Location.create(2989, 3759, 0), Location.create(2986, 3741, 0), Location.create(2961, 3763, 0), Location.create(2969, 3808, 0), Location.create(3004, 3816, 0))
}
override fun tick() {
taskTimeRemaining.replaceAll { _, t -> t - 1 }
currentTask.entries.forEach { entry ->
if (entry.value == RevenantTask.NONE) {
entry.setValue(assignRandomTask(entry.key))
} else {
entry.value.execute(entry.key)
}
}
spawnMissingRevenants()
}
private fun spawnMissingRevenants() {
val amountToSpawn = expectedRevAmount - trackedRevenants.size
if (amountToSpawn <= 0) return
for (i in 0 until amountToSpawn) {
val type = RevenantType.values().random()
val npc = NPC.create(type.ids[0], getRandomSpawnLocation())
npc.init()
}
}
private fun getRandomSpawnLocation(): Location {
return spawnLocations.random()
}
private fun assignRandomTask(npc: RevenantNPC): RevenantTask {
return RevenantTask.values().random().also { it.assign(npc) }
}
override fun defineCommands() {
define("setrevcap", Privilege.ADMIN) {player, strings ->
val amt = strings[1].toInt()
expectedRevAmount = amt
}
define("listrevs", Privilege.ADMIN) {player, strings ->
for (rev in trackedRevenants) {
SystemLogger.logInfo("REV ${rev.id}-${rev.name} @ ${rev.location.toString()}")
}
SystemLogger.logInfo("Total of ${trackedRevenants.size} revenants spawned.")
}
}
//The current task for any given revenant - execute is called every tick.
enum class RevenantTask {
NONE {
override fun execute(revenantNPC: RevenantNPC) {}
},
INTENTIONAL_IDLE {
private val MAX_IDLE_TIME: Int = 50
override fun execute(revenantNPC: RevenantNPC) {
if (taskTimeRemaining[revenantNPC] == 0) currentTask[revenantNPC] = NONE
}
override fun assign(revenantNPC: RevenantNPC) {
taskTimeRemaining[revenantNPC] = RandomFunction.random(MAX_IDLE_TIME)
}
},
RANDOM_ROAM {
private val MAX_ROAM_TICKS: Int = 250
override fun execute(revenantNPC: RevenantNPC) {
if (!canMove(revenantNPC)) return
revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, getNextLocation(revenantNPC)) {
override fun pulse(): Boolean {
if (taskTimeRemaining[revenantNPC]!! <= 0) currentTask[revenantNPC] = NONE
return true
}
})
}
override fun assign(revenantNPC: RevenantNPC) {
taskTimeRemaining[revenantNPC] = RandomFunction.random(MAX_ROAM_TICKS)
}
fun canMove(revenantNPC: RevenantNPC) : Boolean {
return !revenantNPC.walkingQueue.isMoving
&& !revenantNPC.properties.combatPulse.isAttacking
&& !revenantNPC.properties.combatPulse.isInCombat
}
fun getNextLocation(revenantNPC: RevenantNPC) : Location {
val nextX = RandomFunction.random(-revenantNPC.walkRadius, revenantNPC.walkRadius)
val nextY = RandomFunction.random(-revenantNPC.walkRadius, revenantNPC.walkRadius)
return revenantNPC.location.transform(nextX, nextY, 0)
}
},
PATROLLING_ROUTE {
private val MAXIMUM_GROUP_PATROL_LEVEL = 105
override fun assign(revenantNPC: RevenantNPC) {
if (canGroup(revenantNPC)) {
addToPatrolGroup(revenantNPC)
} else {
revenantNPC.setAttribute("route", routes.random())
revenantNPC.setAttribute("routeidx", -1)
}
}
private fun addToPatrolGroup(revenantNPC: RevenantNPC) {
revenantNPC.setAttribute("group", true)
groupPatrolQueue.add(revenantNPC)
if (groupPatrolQueue.size == 3) {
val groupRoute = routes.random()
for (rev in groupPatrolQueue) {
rev.setAttribute("route", groupRoute)
rev.setAttribute("routeidx", -1)
}
groupPatrolQueue.clear()
}
}
private fun canGroup(revenantNPC: RevenantNPC) =
revenantNPC.properties.currentCombatLevel <= MAXIMUM_GROUP_PATROL_LEVEL && RandomFunction.nextBool()
override fun execute(revenantNPC: RevenantNPC) {
val isGroup = revenantNPC.getAttribute("group", false)
val route = revenantNPC.getAttribute<Array<Location>>("route", null)
val routeIdx = revenantNPC.getAttribute("routeidx", -1)
if (!canMove(revenantNPC)) return
if (isGroup && route == null) { //if this is a grouped rev and we are waiting on more revs still
taskTimeRemaining[revenantNPC] = 50 //just to make sure it doesn't time out roaming...
RANDOM_ROAM.execute(revenantNPC)
return
}
if (routeIdx == -1) {
GameWorld.Pulser.submit(object : Pulse() {
override fun pulse(): Boolean {
Graphics.send(Graphics(86), revenantNPC.location)
return true
}
})
teleport(revenantNPC, route[0], TeleportManager.TeleportType.INSTANT)
revenantNPC.setAttribute("routeidx", 1)
} else {
if (routeIdx == route.size) {
poofClear(revenantNPC)
revenantNPC.setAttribute("done", true)
return
}
val pathVariance = if (isGroup) 4 else 10
val nextLoc = route[routeIdx].transform(
RandomFunction.random(-pathVariance, pathVariance),
RandomFunction.random(-pathVariance, pathVariance),
0
)
revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, nextLoc) {
override fun pulse(): Boolean {
return true
}
})
revenantNPC.setAttribute("routeidx", routeIdx + 1)
}
}
fun canMove(revenantNPC: RevenantNPC) : Boolean {
return !revenantNPC.walkingQueue.isMoving
&& !revenantNPC.pulseManager.hasPulseRunning()
&& !revenantNPC.properties.combatPulse.isAttacking
&& !revenantNPC.properties.combatPulse.isInCombat
&& revenantNPC.properties.teleportLocation == null
&& !revenantNPC.getAttribute("done", false)
}
}
;
abstract fun execute(revenantNPC: RevenantNPC)
open fun assign(revenantNPC: RevenantNPC) {}
}
}

View file

@ -115,6 +115,7 @@ object ServerConfigParser {
ServerConstants.SERVER_GE_NAME = data.getString("world.name_ge") ?: ServerConstants.SERVER_NAME
ServerConstants.RULES_AND_INFO_ENABLED = data.getBoolean("world.show_rules", true)
ServerConstants.BOTS_INFLUENCE_PRICE_INDEX = data.getBoolean("world.bots_influence_ge_price", true)
ServerConstants.REVENANT_POPULATION = data.getLong("world.revenant_population", 30L).toInt()
}

View file

@ -1,6 +1,7 @@
package rs09.game.world.repository
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.revenant.RevenantNPC
import core.game.node.entity.player.Player
import core.game.world.map.Location
import core.game.world.map.RegionManager
@ -37,7 +38,7 @@ object Repository {
/**
* The renderable NPCs.
*/
private val RENDERABLE_NPCS: MutableList<NPC> = CopyOnWriteArrayList()
public val RENDERABLE_NPCS: MutableList<NPC> = CopyOnWriteArrayList()
/**
* A mapping holding the players sorted by their names.
@ -100,6 +101,7 @@ object Repository {
*/
@JvmStatic
fun addRenderableNPC(npc: NPC) {
if (npc is RevenantNPC) return // hack to make sure we can update revenants every tick.
if (!RENDERABLE_NPCS.contains(npc)) {
RENDERABLE_NPCS.add(npc)
npc.isRenderable = true
@ -112,6 +114,7 @@ object Repository {
*/
@JvmStatic
fun removeRenderableNPC(npc: NPC) {
if (npc is RevenantNPC) return // hack to make sure we can update revenants every tick.
RENDERABLE_NPCS.remove(npc)
npc.isRenderable = false
}