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

View file

@ -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
}

View file

@ -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.

View file

@ -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));
}

View file

@ -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()
}

View file

@ -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

View file

@ -0,0 +1,5 @@
package core.game.system.timer
enum class TimerFlag {
ClearOnDeath
}

View file

@ -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() }
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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) {

View file

@ -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)

View file

@ -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)

View file

@ -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
}

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)

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)
}
}
}