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 { override fun tick(self: NPC): Boolean {
val target = getAttribute<Player?>(self, "dw-threat-target", null) ?: return true val target = getAttribute<Player?>(self, "dw-threat-target", null) ?: return true
if (!target.isActive) { if (!target.isActive || DeathTask.isDead(target)) {
self.clear() self.clear()
return true return true
} }
@@ -33,8 +33,6 @@ import core.game.system.timer.TimerRegistry;
import java.util.*; import java.util.*;
import static core.api.ContentAPIKt.isStunned;
/** /**
* An entity is a movable node, such as players and NPCs. * An entity is a movable node, such as players and NPCs.
* @author Emperor * @author Emperor
@@ -278,6 +276,7 @@ public abstract class Entity extends Node {
skills.rechargePrayerPoints(); skills.rechargePrayerPoints();
impactHandler.getImpactQueue().clear(); impactHandler.getImpactQueue().clear();
impactHandler.setDisabledTicks(10); impactHandler.setDisabledTicks(10);
timers.onEntityDeath();
removeAttribute("combat-time"); removeAttribute("combat-time");
face(null); face(null);
//Check if it's a Loar shade and transform back into the shadow version. //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(); getPrayer().reset();
super.finalizeDeath(killer); super.finalizeDeath(killer);
appearance.sync(); appearance.sync();
timers.removeTimer("poison");
timers.removeTimer("poison:immunity");
if (!getSavedData().getGlobalData().isDeathScreenDisabled()) { if (!getSavedData().getGlobalData().isDeathScreenDisabled()) {
getInterfaceManager().open(new Component(153)); 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. * 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) { open fun save (root: JSONObject, entity: Entity) {
root["ticksLeft"] = (nextExecution - getWorldTicks()).toString() 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 * 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. * 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. * 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. * 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) {} 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 lastExecution: Int = 0
var nextExecution: 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 (timer.nextExecution > getWorldTicks()) continue
if (!canRunNormalTimers && !timer.isSoft) continue if (!canRunNormalTimers && !timer.isSoft) continue
if (timer.run(entity)) { try {
timer.nextExecution = getWorldTicks() + timer.runInterval if (timer.run(entity)) {
} else { timer.nextExecution = getWorldTicks() + timer.runInterval
timer.nextExecution = Int.MAX_VALUE } else removeTimer(timer)
toRemoveTimers.add(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() toRemoveTimers.clear()
} }
fun onEntityDeath() {
for (timer in activeTimers) {
if (timer.flags.contains(TimerFlag.ClearOnDeath))
removeTimer(timer)
}
}
fun saveTimers (root: JSONObject) { fun saveTimers (root: JSONObject) {
for (timer in activeTimers) { for (timer in activeTimers) {
if (timer !is PersistTimer) continue if (timer !is PersistTimer) continue
@@ -82,10 +92,10 @@ class TimerManager (val entity: Entity) {
inline fun <reified T: RSTimer> removeTimer () { inline fun <reified T: RSTimer> removeTimer () {
for (timer in activeTimers) for (timer in activeTimers)
if (timer is T) if (timer is T)
toRemoveTimers.add(timer) removeTimer(timer)
for (timer in newTimers) for (timer in newTimers)
if (timer is T) if (timer is T)
toRemoveTimers.add(timer) removeTimer(timer)
} }
inline fun <reified T: RSTimer> getTimer () : T? { inline fun <reified T: RSTimer> getTimer () : T? {
@@ -118,13 +128,15 @@ class TimerManager (val entity: Entity) {
fun removeTimer (identifier: String) { fun removeTimer (identifier: String) {
for (timer in activeTimers) for (timer in activeTimers)
if (timer.identifier == identifier) if (timer.identifier == identifier)
toRemoveTimers.add(timer) removeTimer(timer)
for (timer in newTimers) for (timer in newTimers)
if (timer.identifier == identifier) if (timer.identifier == identifier)
toRemoveTimers.add(timer) removeTimer(timer)
} }
fun removeTimer (timer: RSTimer) { fun removeTimer (timer: RSTimer) {
timer.nextExecution = Int.MAX_VALUE
toRemoveTimers.add(timer) 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 core.tools.RandomFunction
import org.json.simple.* import org.json.simple.*
class Disease : PersistTimer (30, "disease") { class Disease : PersistTimer (30, "disease", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var hitsLeft = 25 var hitsLeft = 25
override fun save (root: JSONObject, entity: Entity) { 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 core.game.world.repository.Repository
import org.json.simple.* import org.json.simple.*
class Frozen : PersistTimer (1, "frozen") { class Frozen : PersistTimer (1, "frozen", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var shouldApplyImmunity = false var shouldApplyImmunity = false
override fun save (root: JSONObject, entity: Entity) { 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 core.game.world.repository.Repository
import org.json.simple.* import org.json.simple.*
class FrozenImmunity : PersistTimer (1, "frozen:immunity") { class FrozenImmunity : PersistTimer (1, "frozen:immunity", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var ticksRemaining = 0 var ticksRemaining = 0
override fun save (root: JSONObject, entity: Entity) { override fun save (root: JSONObject, entity: Entity) {
@@ -7,7 +7,7 @@ import core.game.node.entity.player.Player
import org.json.simple.* import org.json.simple.*
import kotlin.math.min import kotlin.math.min
class HealOverTime : PersistTimer (1, "healovertime") { class HealOverTime : PersistTimer (1, "healovertime", flags = arrayOf(TimerFlag.ClearOnDeath)) {
var healRemaining = 0 var healRemaining = 0
var healPerTick = 0 var healPerTick = 0
@@ -1,12 +1,15 @@
package core.game.system.timer.impl package core.game.system.timer.impl
import core.game.system.timer.* import core.api.hasTimerActive
import core.api.* import core.api.registerTimer
import core.api.removeTimer
import core.api.spawnTimer
import core.game.node.entity.Entity import core.game.node.entity.Entity
import core.game.node.entity.player.Player import core.game.system.timer.PersistTimer
import org.json.simple.* 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 { override fun run (entity: Entity) : Boolean {
registerTimer (entity, spawnTimer<MiasmicImmunity>(entity, 7)) registerTimer (entity, spawnTimer<MiasmicImmunity>(entity, 7))
return false return false
@@ -1,12 +1,13 @@
package core.game.system.timer.impl package core.game.system.timer.impl
import core.game.system.timer.* import core.api.hasTimerActive
import core.api.* import core.api.removeTimer
import core.game.node.entity.Entity import core.game.node.entity.Entity
import core.game.node.entity.player.Player import core.game.system.timer.PersistTimer
import org.json.simple.* 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 { override fun run (entity: Entity) : Boolean {
return false 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. * 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. * 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 lateinit var damageSource: Entity
var severity = 0 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. * 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.) * 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 var ticksRemaining = 0
override fun save (root: JSONObject, entity: Entity) { 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) { for (i in 0 until 24) {
if (i == Skills.PRAYER) continue if (i == Skills.PRAYER) continue
if (ticksSinceLastRestore[i]++ >= restoreTicks[i]) { if (ticksSinceLastRestore[i]++ >= restoreTicks[i]) {
if (i == Skills.HITPOINTS) { if (i == Skills.HITPOINTS && entity.skills.lifepoints < entity.skills.maximumLifepoints) {
skills.heal (getHealAmount(entity)) skills.heal (getHealAmount(entity))
} else { } else {
val max = getStatLevel (entity, i) val max = getStatLevel (entity, i)
@@ -6,7 +6,7 @@ import core.game.node.entity.Entity
import core.game.node.entity.player.Player import core.game.node.entity.player.Player
import org.json.simple.* import org.json.simple.*
class Skulled : PersistTimer (1, "skulled") { class Skulled : PersistTimer (1, "skulled", flags = arrayOf(TimerFlag.ClearOnDeath)) {
override fun onRegister (entity: Entity) { override fun onRegister (entity: Entity) {
if (entity !is Player) return if (entity !is Player) return
entity.skullManager.setSkullIcon(0) entity.skullManager.setSkullIcon(0)
@@ -6,7 +6,7 @@ import core.game.node.entity.Entity
import core.game.node.entity.player.Player import core.game.node.entity.player.Player
import org.json.simple.* import org.json.simple.*
class Teleblock : PersistTimer (1, "teleblock") { class Teleblock : PersistTimer (1, "teleblock", flags = arrayOf(TimerFlag.ClearOnDeath)) {
override fun run (entity: Entity) : Boolean { override fun run (entity: Entity) : Boolean {
return false return false
} }
+1
View File
@@ -43,6 +43,7 @@ import java.nio.ByteBuffer
object TestUtils { object TestUtils {
var uidCounter = 0 var uidCounter = 0
const val PLAYER_DEATH_TICKS = 14
fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE, rights: Rights = Rights.ADMINISTRATOR): MockPlayer { fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE, rights: Rights = Rights.ADMINISTRATOR): MockPlayer {
val p = MockPlayer(name) 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)
}
}
}