mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2025-12-10 10:20:41 -07:00
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:
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
|
||||
|
||||
|
|
|
|||
5
Server/src/main/core/game/system/timer/TimerFlag.kt
Normal file
5
Server/src/main/core/game/system/timer/TimerFlag.kt
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Server/src/test/kotlin/core/TimerTests.kt
Normal file
99
Server/src/test/kotlin/core/TimerTests.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue