Timers can be flagged for removal on death (fixes deep wilderness threats attacking for brief period after respawn)

Fixed an issue with the skill restore timer that caused overheals to be reset to normal hp
This commit is contained in:
Ceikry
2023-08-14 13:22:53 +00:00
committed by Ryan
parent a3e6df109d
commit 678d25dacd
20 changed files with 159 additions and 35 deletions
@@ -99,7 +99,7 @@ class RevGuardianBehavior : NPCBehavior() {
override fun tick(self: NPC): Boolean {
val target = getAttribute<Player?>(self, "dw-threat-target", null) ?: return true
if (!target.isActive) {
if (!target.isActive || DeathTask.isDead(target)) {
self.clear()
return true
}
@@ -33,8 +33,6 @@ import core.game.system.timer.TimerRegistry;
import java.util.*;
import static core.api.ContentAPIKt.isStunned;
/**
* An entity is a movable node, such as players and NPCs.
* @author Emperor
@@ -278,6 +276,7 @@ public abstract class Entity extends Node {
skills.rechargePrayerPoints();
impactHandler.getImpactQueue().clear();
impactHandler.setDisabledTicks(10);
timers.onEntityDeath();
removeAttribute("combat-time");
face(null);
//Check if it's a Loar shade and transform back into the shadow version.
@@ -700,8 +700,6 @@ public class Player extends Entity {
getPrayer().reset();
super.finalizeDeath(killer);
appearance.sync();
timers.removeTimer("poison");
timers.removeTimer("poison:immunity");
if (!getSavedData().getGlobalData().isDeathScreenDisabled()) {
getInterfaceManager().open(new Component(153));
}
@@ -8,7 +8,7 @@ import kotlin.reflect.full.createInstance
/**
* A timer implementation with support for saving and loading arbitrary data. See `RSTimer` for more info on timers themselves.
**/
abstract class PersistTimer (runInterval: Int, identifier: String, isSoft: Boolean = false, isAuto: Boolean = false) : RSTimer (runInterval, identifier, isSoft, isAuto) {
abstract class PersistTimer (runInterval: Int, identifier: String, isSoft: Boolean = false, isAuto: Boolean = false, flags: Array<TimerFlag> = arrayOf()) : RSTimer (runInterval, identifier, isSoft, isAuto, flags) {
open fun save (root: JSONObject, entity: Entity) {
root["ticksLeft"] = (nextExecution - getWorldTicks()).toString()
}
@@ -9,7 +9,8 @@ import kotlin.reflect.full.createInstance
* default PersistTimer behavior, which automatically starts the timer only if there's saved data for that timer present. In truth, there's very few
* timers that should have isAuto true.
**/
abstract class RSTimer (var runInterval: Int, val identifier: String = "generictimer", val isSoft: Boolean = false, val isAuto: Boolean = false) {
abstract class RSTimer (var runInterval: Int, val identifier: String = "generictimer", val isSoft: Boolean = false, val isAuto: Boolean = false, val flags: Array<TimerFlag> = arrayOf()) {
/**
* Executed every time the run interval of the timer elapses.
* Execution will be delayed if this timer has `isSoft` set to false (which 99% of timers should) if the entity has a modal open or is otherwise stalled.
@@ -28,6 +29,11 @@ abstract class RSTimer (var runInterval: Int, val identifier: String = "generict
**/
open fun onRegister (entity: Entity) {}
/**
* Called by core code when the timer is being removed.
*/
open fun onRemoval (entity: Entity) {}
var lastExecution: Int = 0
var nextExecution: Int = 0
@@ -0,0 +1,5 @@
package core.game.system.timer
enum class TimerFlag {
ClearOnDeath
}
@@ -27,11 +27,14 @@ class TimerManager (val entity: Entity) {
if (timer.nextExecution > getWorldTicks()) continue
if (!canRunNormalTimers && !timer.isSoft) continue
if (timer.run(entity)) {
timer.nextExecution = getWorldTicks() + timer.runInterval
} else {
timer.nextExecution = Int.MAX_VALUE
toRemoveTimers.add(timer)
try {
if (timer.run(entity)) {
timer.nextExecution = getWorldTicks() + timer.runInterval
} else removeTimer(timer)
} catch (e: Exception) {
log (this::class.java, Log.ERR, "Prematurely removing timer ${timer::class.java.simpleName} from ${entity.name} because it threw an exception when ran. Exception follows:")
e.printStackTrace()
removeTimer(timer)
}
}
@@ -49,6 +52,13 @@ class TimerManager (val entity: Entity) {
toRemoveTimers.clear()
}
fun onEntityDeath() {
for (timer in activeTimers) {
if (timer.flags.contains(TimerFlag.ClearOnDeath))
removeTimer(timer)
}
}
fun saveTimers (root: JSONObject) {
for (timer in activeTimers) {
if (timer !is PersistTimer) continue
@@ -82,10 +92,10 @@ class TimerManager (val entity: Entity) {
inline fun <reified T: RSTimer> removeTimer () {
for (timer in activeTimers)
if (timer is T)
toRemoveTimers.add(timer)
removeTimer(timer)
for (timer in newTimers)
if (timer is T)
toRemoveTimers.add(timer)
removeTimer(timer)
}
inline fun <reified T: RSTimer> getTimer () : T? {
@@ -118,13 +128,15 @@ class TimerManager (val entity: Entity) {
fun removeTimer (identifier: String) {
for (timer in activeTimers)
if (timer.identifier == identifier)
toRemoveTimers.add(timer)
removeTimer(timer)
for (timer in newTimers)
if (timer.identifier == identifier)
toRemoveTimers.add(timer)
removeTimer(timer)
}
fun removeTimer (timer: RSTimer) {
timer.nextExecution = Int.MAX_VALUE
toRemoveTimers.add(timer)
try { timer.onRemoval(entity) } catch (e: Exception) { e.printStackTrace() }
}
}
@@ -9,7 +9,7 @@ import core.game.node.entity.combat.ImpactHandler
import core.tools.RandomFunction
import org.json.simple.*
class Disease : PersistTimer (30, "disease") {
class Disease : PersistTimer (30, "disease", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var hitsLeft = 25
override fun save (root: JSONObject, entity: Entity) {
@@ -7,7 +7,7 @@ import core.game.node.entity.player.Player
import core.game.world.repository.Repository
import org.json.simple.*
class Frozen : PersistTimer (1, "frozen") {
class Frozen : PersistTimer (1, "frozen", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var shouldApplyImmunity = false
override fun save (root: JSONObject, entity: Entity) {
@@ -7,7 +7,7 @@ import core.game.node.entity.player.Player
import core.game.world.repository.Repository
import org.json.simple.*
class FrozenImmunity : PersistTimer (1, "frozen:immunity") {
class FrozenImmunity : PersistTimer (1, "frozen:immunity", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var ticksRemaining = 0
override fun save (root: JSONObject, entity: Entity) {
@@ -7,7 +7,7 @@ import core.game.node.entity.player.Player
import org.json.simple.*
import kotlin.math.min
class HealOverTime : PersistTimer (1, "healovertime") {
class HealOverTime : PersistTimer (1, "healovertime", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var healRemaining = 0
var healPerTick = 0
@@ -1,12 +1,15 @@
package core.game.system.timer.impl
import core.game.system.timer.*
import core.api.*
import core.api.hasTimerActive
import core.api.registerTimer
import core.api.removeTimer
import core.api.spawnTimer
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import org.json.simple.*
import core.game.system.timer.PersistTimer
import core.game.system.timer.RSTimer
import core.game.system.timer.TimerFlag
class Miasmic : PersistTimer (1, "miasmic") {
class Miasmic : PersistTimer (1, "miasmic", flags = arrayOf(TimerFlag.ClearOnDeath)) {
override fun run (entity: Entity) : Boolean {
registerTimer (entity, spawnTimer<MiasmicImmunity>(entity, 7))
return false
@@ -1,12 +1,13 @@
package core.game.system.timer.impl
import core.game.system.timer.*
import core.api.*
import core.api.hasTimerActive
import core.api.removeTimer
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import org.json.simple.*
import core.game.system.timer.PersistTimer
import core.game.system.timer.RSTimer
import core.game.system.timer.TimerFlag
class MiasmicImmunity : PersistTimer (1, "miasmic:immunity") {
class MiasmicImmunity : PersistTimer (1, "miasmic:immunity", flags = arrayOf(TimerFlag.ClearOnDeath)) {
override fun run (entity: Entity) : Boolean {
return false
}
@@ -16,7 +16,7 @@ import org.json.simple.*
* Every time the damage is applied, the severity decreases by 1. Poison ends when severity reaches 0.
* Example: 30 Severity. Deals 6 damage 5 times, then 5 damage 5 times, and so on.
**/
class Poison : PersistTimer (30, "poison") {
class Poison : PersistTimer (30, "poison", flags = arrayOf(TimerFlag.ClearOnDeath)) {
lateinit var damageSource: Entity
var severity = 0
@@ -15,7 +15,7 @@ import org.json.simple.*
* Will notify the player of various levels of remaining poison immunity, and then remove itself once it has run out.
* This timer is a "soft" timer, meaning it will tick down even while other timers would normally stall (e.g. during entity delays or when the entity has a modal open.)
**/
class PoisonImmunity : PersistTimer (1, "poison:immunity", isSoft = true) {
class PoisonImmunity : PersistTimer (1, "poison:immunity", isSoft = true, flags = arrayOf(TimerFlag.ClearOnDeath)) {
var ticksRemaining = 0
override fun save (root: JSONObject, entity: Entity) {
@@ -21,7 +21,7 @@ class SkillRestore : RSTimer (1, "skillrestore", isAuto = true, isSoft = true) {
for (i in 0 until 24) {
if (i == Skills.PRAYER) continue
if (ticksSinceLastRestore[i]++ >= restoreTicks[i]) {
if (i == Skills.HITPOINTS) {
if (i == Skills.HITPOINTS && entity.skills.lifepoints < entity.skills.maximumLifepoints) {
skills.heal (getHealAmount(entity))
} else {
val max = getStatLevel (entity, i)
@@ -6,7 +6,7 @@ import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import org.json.simple.*
class Skulled : PersistTimer (1, "skulled") {
class Skulled : PersistTimer (1, "skulled", flags = arrayOf(TimerFlag.ClearOnDeath)) {
override fun onRegister (entity: Entity) {
if (entity !is Player) return
entity.skullManager.setSkullIcon(0)
@@ -6,7 +6,7 @@ import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import org.json.simple.*
class Teleblock : PersistTimer (1, "teleblock") {
class Teleblock : PersistTimer (1, "teleblock", flags = arrayOf(TimerFlag.ClearOnDeath)) {
override fun run (entity: Entity) : Boolean {
return false
}
+1
View File
@@ -43,6 +43,7 @@ import java.nio.ByteBuffer
object TestUtils {
var uidCounter = 0
const val PLAYER_DEATH_TICKS = 14
fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE, rights: Rights = Rights.ADMINISTRATOR): MockPlayer {
val p = MockPlayer(name)
+99
View File
@@ -0,0 +1,99 @@
package core
import TestUtils
import core.api.*
import core.game.node.entity.Entity
import core.game.node.entity.skill.Skills
import core.game.system.timer.RSTimer
import core.game.system.timer.TimerFlag
import core.game.system.timer.impl.SkillRestore
import core.tools.Log
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class TimerTests {
init { TestUtils.preTestSetup() }
@Test fun timerWithNoFlagsShouldNotBeClearedOnDeath() {
TestUtils.getMockPlayer("noflagnoclear").use { p ->
var incrementer = 0
val timer = object : RSTimer(1) {
override fun run(entity: Entity): Boolean {
incrementer++
return true
}
}
registerTimer(p, timer)
impact(p, p.skills.lifepoints)
TestUtils.advanceTicks(TestUtils.PLAYER_DEATH_TICKS, false)
closeInterface(p) //close the interface that opens after death - as it would pause the timer
TestUtils.advanceTicks(18, false)
Assertions.assertEquals(20, incrementer)
}
}
@Test fun timerWithClearOnDeathFlagShouldClearOnDeath() {
TestUtils.getMockPlayer("clearflagtimer").use { p ->
var incrementer = 0
val timer = object : RSTimer(1, flags = arrayOf(TimerFlag.ClearOnDeath)) {
override fun run(entity: Entity): Boolean {
incrementer++
return true
}
}
registerTimer(p, timer)
impact(p, p.skills.lifepoints)
TestUtils.advanceTicks(TestUtils.PLAYER_DEATH_TICKS, false)
closeInterface(p) //close the interface that opens after death - as it would pause the timer
TestUtils.advanceTicks(18, false)
Assertions.assertEquals(2, incrementer)
}
}
@Test fun skillRestoreTimerShouldSlowlyRaiseLoweredStats() {
TestUtils.getMockPlayer("statrestore-slowrestore").use { p ->
val timer = SkillRestore()
registerTimer(p, timer)
p.skills.staticLevels[Skills.FARMING] = 20
setTempLevel(p, Skills.FARMING, 10)
TestUtils.advanceTicks(timer.restoreTicks[Skills.FARMING] + 3, false)
Assertions.assertEquals(11, getDynLevel(p, Skills.FARMING))
}
}
@Test fun skillRestoreTimerShouldSlowlyLowerBoostedStats() {
TestUtils.getMockPlayer("statrestore-slowdrain").use { p ->
val timer = SkillRestore()
p.timers.registerTimer(timer)
setTempLevel(p, Skills.FARMING, 6)
TestUtils.advanceTicks(timer.restoreTicks[Skills.FARMING] + 3, false)
Assertions.assertEquals(5, getDynLevel(p, Skills.FARMING))
}
}
@Test fun skillRestoreTimerShouldRaiseLoweredHp() {
TestUtils.getMockPlayer("statrestore-raiseloweredhp").use { p ->
val timer = SkillRestore()
registerTimer(p, timer)
p.skills.lifepoints /= 2
TestUtils.advanceTicks(600, false)
Assertions.assertEquals(10, p.skills.lifepoints)
}
}
@Test fun skillRestoreTimerShouldNeverLowerBoostedHp() {
TestUtils.getMockPlayer("statrestore-neverlowerhp").use { p ->
val timer = SkillRestore()
registerTimer(p, timer)
p.skills.lifepoints = 50
TestUtils.advanceTicks(500, false)
Assertions.assertEquals(50, p.skills.lifepoints)
}
}
}