Implemented timer subsystem to eventually replace pulses

Authentic subsystem supports saving/loading arbitrary data and resuming timer countdowns
Lots of documented CAPI functions for working with these new timers
Converted poison to new timers
Converted disease to new timers
Converted farming to new timers (CropGrowth and Compost)
Farming now syncs with 5 minute intervals on realtime clocks (authentic)
Converted seedling growth to new timers
Converted shooting star mining bonus to new timers
Converted entity freezing to new timers
Converted incubation to new timers
Incubation now supports using both incubators (so 2 eggs at once)
Converted miasmic states to the new timers
Converted god spell charged state to new timers
Converted teleblock to new timers
Converted skulled state to new timers
Converted Enhanced Excalibur special attack effect to new timers
Converted passive stat restoration to timers
Converted multicannon firing and decay to a timer
This commit is contained in:
Ceikry
2023-07-29 04:51:06 +00:00
committed by Ryan
parent bda000f220
commit 661390e66f
114 changed files with 1718 additions and 2112 deletions
@@ -54,6 +54,10 @@ class CompostBin(val player: Player, val bin: CompostBins) {
return Item(item)
}
fun isDefaultState() : Boolean {
return (isFinished == false && finishedTime == 0L && items.size == 0)
}
fun isReady(): Boolean {
return System.currentTimeMillis() > finishedTime && finishedTime != 0L
}
@@ -1,9 +1,11 @@
package content.global.skill.farming
import core.api.*
import core.cache.def.impl.SceneryDefinition
import core.cache.def.impl.VarbitDefinition
import core.game.node.scenery.Scenery
import core.game.node.entity.player.Player
import content.global.skill.farming.timers.*
enum class CompostBins(val varbit: Int) {
FALADOR_COMPOST(740),
@@ -28,11 +30,7 @@ enum class CompostBins(val varbit: Int) {
}
fun getBinForPlayer(player: Player) : CompostBin {
var state: FarmingState? = player.states.get("farming") as FarmingState?
return if(state == null){
state = player.registerState("farming") as FarmingState
state.getBin(this).also { state.init() }
} else
state.getBin(this)
val bins = getOrStartTimer <Compost> (player)
return bins.getBin (this)
}
}
@@ -1,9 +1,11 @@
package content.global.skill.farming
import core.api.*
import core.cache.def.impl.SceneryDefinition
import core.cache.def.impl.VarbitDefinition
import core.game.node.scenery.Scenery
import core.game.node.entity.player.Player
import content.global.skill.farming.timers.CropGrowth
enum class FarmingPatch(val varbit: Int, val type: PatchType) {
//Allotments
@@ -86,11 +88,7 @@ enum class FarmingPatch(val varbit: Int, val type: PatchType) {
}
fun getPatchFor(player: Player): Patch{
var state: FarmingState? = player.states.get("farming") as FarmingState?
return if(state == null){
state = player.registerState("farming") as FarmingState
state.getPatch(this).also { state.init() }
} else
state.getPatch(this)
var crops = getOrStartTimer <CropGrowth> (player)!!
return crops.getPatch(this)
}
}
@@ -1,5 +1,6 @@
package content.global.skill.farming
import core.api.*
import core.Util.clamp
import core.game.node.entity.player.Player
import core.game.system.task.Pulse
@@ -11,125 +12,26 @@ import core.game.node.entity.state.PlayerState
import core.game.node.entity.state.State
import core.tools.SystemLogger
import java.util.concurrent.TimeUnit
import content.global.skill.farming.timers.*
@PlayerState("farming")
/**
* Kept around solely for the purpose of porting save data from this old system to the new one.
* //TODO REMOVE BY END OF 2023
**/
class FarmingState(player: Player? = null) : State(player) {
private val patchMap = HashMap<FarmingPatch, Patch>()
private val binMap = HashMap<CompostBins, CompostBin>()
fun getPatch(patch: FarmingPatch): Patch {
return patchMap[patch] ?: (Patch(player!!,patch).also { patchMap[patch] = it })
}
fun getBin(bin: CompostBins) : CompostBin{
return binMap[bin] ?: (CompostBin(player!!,bin).also { binMap[bin] = it })
}
fun getPatches(): MutableCollection<Patch>{
return patchMap.values
}
fun getBins(): MutableCollection<CompostBin>{
return binMap.values
}
override fun save(root: JSONObject) {
val patches = JSONArray()
for((key,patch) in patchMap){
val p = JSONObject()
p.put("patch-ordinal",key.ordinal)
p.put("patch-plantable-ordinal",patch.plantable?.ordinal ?: -1)
p.put("patch-watered",patch.isWatered)
p.put("patch-diseased",patch.isDiseased)
p.put("patch-dead",patch.isDead)
p.put("patch-stage",patch.currentGrowthStage)
p.put("patch-state",patch.getCurrentState())
p.put("patch-nextGrowth",patch.nextGrowth)
p.put("patch-harvestAmt",patch.harvestAmt)
p.put("patch-checkHealth",patch.isCheckHealth)
p.put("patch-compost",patch.compost.ordinal)
p.put("patch-paidprot",patch.protectionPaid)
p.put("patch-croplives", patch.cropLives)
patches.add(p)
}
val bins = JSONArray()
for((key,bin) in binMap){
val b = JSONObject()
b.put("bin-ordinal",key.ordinal)
bin.save(b)
bins.add(b)
}
root.put("farming-patches",patches)
root.put("farming-bins",bins)
}
override fun save(root: JSONObject) {}
override fun parse(_data: JSONObject) {
player ?: return
if(_data.containsKey("farming-bins")){
(_data["farming-bins"] as JSONArray).forEach {
val bin = it as JSONObject
val binOrdinal = bin["bin-ordinal"].toString().toInt()
val cBin = CompostBins.values()[binOrdinal]
val b = cBin.getBinForPlayer(player)
b.parse(bin["binData"] as JSONObject)
}
_data["bins"] = _data["farming-bins"]
val timer = getOrStartTimer <Compost> (player)
timer.parse (_data, player)
}
if(_data.containsKey("farming-patches")){
val data = _data["farming-patches"] as JSONArray
for(d in data){
val p = d as JSONObject
val patchOrdinal = p["patch-ordinal"].toString().toInt()
val patchPlantableOrdinal = p["patch-plantable-ordinal"].toString().toInt()
val patchWatered = p["patch-watered"] as Boolean
val patchDiseased = p["patch-diseased"] as Boolean
val patchDead = p["patch-dead"] as Boolean
val patchStage = p["patch-stage"].toString().toInt()
val nextGrowth = p["patch-nextGrowth"].toString().toLong()
val harvestAmt = (p["patch-harvestAmt"] ?: 0).toString().toInt()
val checkHealth = p["patch-checkHealth"] as Boolean
val savedState = p["patch-state"].toString().toInt()
val compostOrdinal = p["patch-compost"].toString().toInt()
val protectionPaid = p["patch-paidprot"] as Boolean
val cropLives = if(p["patch-croplives"] != null) p["patch-croplives"].toString().toInt() else 3
val fPatch = FarmingPatch.values()[patchOrdinal]
val plantable = if(patchPlantableOrdinal != -1) Plantable.values()[patchPlantableOrdinal] else null
val patch = Patch(player,fPatch,plantable,patchStage,patchDiseased,patchDead,patchWatered,nextGrowth,harvestAmt,checkHealth)
patch.cropLives = cropLives
patch.compost = CompostType.values()[compostOrdinal]
patch.protectionPaid = protectionPaid
patch.setCurrentState(savedState)
if((savedState - (patch?.plantable?.value ?: 0)) > patch.currentGrowthStage){
patch.setCurrentState(savedState)
} else {
patch.setCurrentState((patch.plantable?.value ?: 0) + patch.currentGrowthStage)
}
val type = patch.patch.type
val shouldPlayCatchup = !patch.isGrown() || (type == PatchType.BUSH && patch.getFruitOrBerryCount() < 4) || (type == PatchType.FRUIT_TREE && patch.getFruitOrBerryCount() < 6)
if(shouldPlayCatchup && patch.plantable != null && !patchDead){
var stagesToSimulate = if (!patch.isGrown()) patch.plantable!!.stages - patch.currentGrowthStage else 0
if (type == PatchType.BUSH)
stagesToSimulate += Math.min(4, 4 - patch.getFruitOrBerryCount())
if (type == PatchType.FRUIT_TREE)
stagesToSimulate += Math.min(6, 6 - patch.getFruitOrBerryCount())
val nowTime = System.currentTimeMillis()
var simulatedTime = patch.nextGrowth
while (simulatedTime < nowTime && stagesToSimulate-- > 0 && !patch.isDead) {
val timeToIncrement = TimeUnit.MINUTES.toMillis(patch.getStageGrowthMinutes().toLong())
patch.update()
simulatedTime += timeToIncrement
}
}
if(patchMap[fPatch] == null) {
patchMap[fPatch] = patch
}
}
_data["patches"] = _data["farming-patches"]
val timer = getOrStartTimer <CropGrowth> (player)
timer.parse(_data, player)
}
}
@@ -137,35 +39,5 @@ class FarmingState(player: Player? = null) : State(player) {
return FarmingState(player)
}
override fun createPulse() {
pulse = object : Pulse(3){
override fun pulse(): Boolean {
GlobalScope.launch {
var removeList = ArrayList<FarmingPatch>()
for((_,patch) in patchMap){
if(patch.getCurrentState() in 1..3 && patch.nextGrowth == 0L){
patch.nextGrowth = System.currentTimeMillis() + 60000
continue
}
if(patch.nextGrowth < System.currentTimeMillis() && !patch.isDead){
patch.update()
patch.nextGrowth = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(patch.getStageGrowthMinutes().toLong())
}
}
for((_,bin) in binMap){
if(bin.isReady() && !bin.isFinished){
bin.finish()
}
}
}
return false
}
}
}
override fun createPulse() {}
}
@@ -8,7 +8,7 @@ enum class PatchType(val stageGrowthTime: Int) {
BUSH(20),
FLOWER(5),
HERB(20),
SPIRIT_TREE(293),
SPIRIT_TREE(295),
MUSHROOM(30),
BELLADONNA(80),
CACTUS(60),
@@ -1,13 +1,11 @@
package content.global.skill.farming
import core.api.addItem
import core.api.inInventory
import core.api.removeItem
import core.api.sendDialogue
import core.api.*
import core.game.node.Node
import core.game.node.entity.player.Player
import org.rs09.consts.Items
import core.game.interaction.IntType
import core.game.node.entity.player.Player
import content.global.skill.farming.timers.*
import core.game.interaction.InteractionListener
class SeedlingListener : InteractionListener {
@@ -43,17 +41,8 @@ class SeedlingListener : InteractionListener {
addItem(player, wateredSeedling)
addItem(player, nextCan)
var state = player.states["seedling"] as SeedlingState?
if (state != null) {
state.addSeedling(wateredSeedling)
return true
}
state = player.registerState("seedling") as SeedlingState?
state?.addSeedling(wateredSeedling)
state?.init()
var seedlings = getOrStartTimer <SeedlingGrowth> (player)
seedlings.addSeedling(wateredSeedling)
return true
}
@@ -114,4 +103,4 @@ class SeedlingListener : InteractionListener {
)
private val WATERING_CANS = intArrayOf(Items.WATERING_CAN8_5340,Items.WATERING_CAN7_5339,Items.WATERING_CAN6_5338,Items.WATERING_CAN5_5337,Items.WATERING_CAN4_5336,Items.WATERING_CAN3_5335,Items.WATERING_CAN2_5334,Items.WATERING_CAN1_5333)
}
}
@@ -1,83 +0,0 @@
package content.global.skill.farming
import core.game.node.entity.player.Player
import core.game.node.item.Item
import core.game.system.task.Pulse
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import core.game.node.entity.state.PlayerState
import core.game.node.entity.state.State
import java.util.concurrent.TimeUnit
@PlayerState("seedling")
class SeedlingState(player: Player? = null) : State(player) {
val seedlings = ArrayList<Seedling>()
fun addSeedling(seedling: Int){
seedlings.add(Seedling(seedling, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5),seedling + if(seedling > 5400) 8 else 6))
}
override fun save(root: JSONObject) {
val seedArray = JSONArray()
for(s in seedlings){
val seed = JSONObject()
seed.put("id",s.id)
seed.put("ttl",s.TTL)
seed.put("sapling",s.sapling)
seedArray.add(seed)
}
root.put("seedlings",seedArray)
}
override fun parse(_data: JSONObject) {
if(_data.containsKey("seedlings")){
(_data["seedlings"] as JSONArray).forEach {
val s = it as JSONObject
val id = s["id"].toString().toInt()
val ttl = s["ttl"].toString().toLong()
val sapling = s["sapling"].toString().toInt()
seedlings.add(Seedling(id,ttl,sapling))
}
}
}
override fun newInstance(player: Player?): State {
return SeedlingState(player)
}
override fun createPulse() {
if(seedlings.isEmpty()) return
player ?: return
pulse = object : Pulse(5){
override fun pulse(): Boolean {
val removeList = ArrayList<Seedling>()
GlobalScope.launch {
for (seed in seedlings) {
if (System.currentTimeMillis() > seed.TTL) {
val inInventory = player.inventory.get(Item(seed.id))
if (inInventory != null) {
player.inventory.replace(Item(seed.sapling), inInventory.slot)
removeList.add(seed)
} else {
val inBank = player.bank.get(Item(seed.id))
if(inBank == null) removeList.add(seed)
else {
player.bank.remove(Item(inBank.id,1))
player.bank.add(Item(seed.sapling))
removeList.add(seed)
}
}
}
}
seedlings.removeAll(removeList)
}
return false
}
}
}
}
@@ -0,0 +1,65 @@
package content.global.skill.farming.timers
import core.api.*
import core.game.system.timer.*
import org.json.simple.*
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import content.global.skill.farming.*
class Compost : PersistTimer (500, "farming:compost", isSoft = true) {
private val binMap = HashMap<CompostBins, CompostBin>()
lateinit var player: Player
override fun onRegister (entity: Entity) {
player = (entity as? Player)!!
}
override fun getInitialRunDelay() : Int {
return 1 //run once immediately after log in to complete any pending-but-enough-time-has-passed bins.
}
override fun run (entity: Entity) : Boolean {
val removeList = ArrayList <CompostBins> ()
for((cBin,bin) in binMap){
if(bin.isReady() && !bin.isFinished){
bin.finish()
}
else if (bin.isDefaultState())
removeList.add(cBin)
}
removeList.forEach { binMap.remove(it) }
removeList.clear()
return binMap.isNotEmpty()
}
fun getBin (bin: CompostBins) : CompostBin{
return binMap[bin] ?: (CompostBin (player, bin).also { binMap[bin] = it })
}
fun getBins(): MutableCollection<CompostBin>{
return binMap.values
}
override fun save (root: JSONObject, entity: Entity) {
val bins = JSONArray()
for((key,bin) in binMap){
val b = JSONObject()
b.put("bin-ordinal",key.ordinal)
bin.save(b)
bins.add(b)
}
root.put("bins", bins)
}
override fun parse (root: JSONObject, entity: Entity) {
(root["bins"] as JSONArray).forEach {
val bin = it as JSONObject
val binOrdinal = bin["bin-ordinal"].toString().toInt()
val cBin = CompostBins.values()[binOrdinal]
val b = CompostBin ((entity as? Player)!!, cBin).also { binMap[cBin] = it }
b.parse(bin["binData"] as JSONObject)
}
}
}
@@ -0,0 +1,148 @@
package content.global.skill.farming.timers
import core.api.*
import core.tools.*
import core.game.system.timer.*
import org.json.simple.*
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import content.global.skill.farming.*
import java.util.concurrent.TimeUnit
import java.time.*
class CropGrowth : PersistTimer (500, "farming:crops", isSoft = true) {
private val patchMap = HashMap<FarmingPatch, Patch>()
lateinit var player: Player
override fun onRegister (entity: Entity) {
player = (entity as? Player)!!
runOfflineCatchupLogic()
}
//Sync the 5 minute run cycles with :05 on realtime clocks - authentic
override fun getInitialRunDelay() : Int {
val now = LocalTime.now();
val minsUntil5MinSync = 5 - (now.getMinute() % 5)
val ticks = secondsToTicks (minsUntil5MinSync * 60)
player.debug("[CropGrowth] Scheduled first growth cycle for $ticks ticks from now.")
return ticks
}
override fun run (entity: Entity) : Boolean {
var removeList = ArrayList<FarmingPatch>()
for((fp,patch) in patchMap){
if(patch.getCurrentState() in 1..3 && patch.nextGrowth == 0L){
patch.nextGrowth = System.currentTimeMillis() + 60000
continue
}
//Go ahead and grow anything within 4 minutes of the 5-minute-synced growth cycles, bringing out-of-sync patches into sync.
//This seems to be authentic as well, with the RS wiki sometimes stating 20-minute patches can grow in as little as 7 minutes depending on timing of planting
//It also makes sense, as otherwise if you e.g. planted something at 10:34 that takes 5 minutes to grow, it would take 6 minutes in reality instead of 5.
//Another more extreme example is if you planted something at 10:31 that takes 5 minutes to grow. 10:35 comes around, it hasn't been 5 minutes, so it doesn't grow, meaning
//it actually grows at 10:40, an extra 4 minutes.
//this code makes it so crops planted both at 10:31 and 10:34 grow at 10:35 if they are supposed to take 5 minutes for each stage.
if(patch.nextGrowth < (System.currentTimeMillis() + 240_000L) && !patch.isDead){
patch.nextGrowth = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(patch.getStageGrowthMinutes().toLong())
patch.update()
}
if (patch.getCurrentState() == 0)
removeList.add(fp)
}
removeList.forEach { patchMap.remove(it) }
removeList.clear()
return patchMap.isNotEmpty()
}
private fun runOfflineCatchupLogic() {
for ((_, patch) in patchMap) {
val type = patch.patch.type
val shouldPlayCatchup = !patch.isGrown() || (type == PatchType.BUSH && patch.getFruitOrBerryCount() < 4) || (type == PatchType.FRUIT_TREE && patch.getFruitOrBerryCount() < 6)
if(shouldPlayCatchup && patch.plantable != null && !patch.isDead){
var stagesToSimulate = if (!patch.isGrown()) patch.plantable!!.stages - patch.currentGrowthStage else 0
if (type == PatchType.BUSH)
stagesToSimulate += Math.min(4, 4 - patch.getFruitOrBerryCount())
if (type == PatchType.FRUIT_TREE)
stagesToSimulate += Math.min(6, 6 - patch.getFruitOrBerryCount())
val nowTime = System.currentTimeMillis()
var simulatedTime = patch.nextGrowth
while (simulatedTime < nowTime && stagesToSimulate-- > 0 && !patch.isDead) {
val timeToIncrement = TimeUnit.MINUTES.toMillis(patch.getStageGrowthMinutes().toLong())
patch.update()
simulatedTime += timeToIncrement
}
}
}
}
fun getPatch(patch: FarmingPatch): Patch {
return patchMap[patch] ?: (Patch(player,patch).also { patchMap[patch] = it })
}
fun getPatches(): MutableCollection<Patch>{
return patchMap.values
}
override fun save (root: JSONObject, entity: Entity) {
val patches = JSONArray()
for((key,patch) in patchMap){
val p = JSONObject()
p.put("patch-ordinal",key.ordinal)
p.put("patch-plantable-ordinal",patch.plantable?.ordinal ?: -1)
p.put("patch-watered",patch.isWatered)
p.put("patch-diseased",patch.isDiseased)
p.put("patch-dead",patch.isDead)
p.put("patch-stage",patch.currentGrowthStage)
p.put("patch-state",patch.getCurrentState())
p.put("patch-nextGrowth",patch.nextGrowth)
p.put("patch-harvestAmt",patch.harvestAmt)
p.put("patch-checkHealth",patch.isCheckHealth)
p.put("patch-compost",patch.compost.ordinal)
p.put("patch-paidprot",patch.protectionPaid)
p.put("patch-croplives", patch.cropLives)
patches.add(p)
}
root["patches"] = patches
}
override fun parse (root: JSONObject, entity: Entity) {
val data = root["patches"] as JSONArray
for(d in data){
val p = d as JSONObject
val patchOrdinal = p["patch-ordinal"].toString().toInt()
val patchPlantableOrdinal = p["patch-plantable-ordinal"].toString().toInt()
val patchWatered = p["patch-watered"] as Boolean
val patchDiseased = p["patch-diseased"] as Boolean
val patchDead = p["patch-dead"] as Boolean
val patchStage = p["patch-stage"].toString().toInt()
val nextGrowth = p["patch-nextGrowth"].toString().toLong()
val harvestAmt = (p["patch-harvestAmt"] ?: 0).toString().toInt()
val checkHealth = p["patch-checkHealth"] as Boolean
val savedState = p["patch-state"].toString().toInt()
val compostOrdinal = p["patch-compost"].toString().toInt()
val protectionPaid = p["patch-paidprot"] as Boolean
val cropLives = if(p["patch-croplives"] != null) p["patch-croplives"].toString().toInt() else 3
val fPatch = FarmingPatch.values()[patchOrdinal]
val plantable = if(patchPlantableOrdinal != -1) Plantable.values()[patchPlantableOrdinal] else null
val patch = Patch((entity as? Player)!!,fPatch,plantable,patchStage,patchDiseased,patchDead,patchWatered,nextGrowth,harvestAmt,checkHealth)
patch.cropLives = cropLives
patch.compost = CompostType.values()[compostOrdinal]
patch.protectionPaid = protectionPaid
patch.setCurrentState(savedState)
if((savedState - (patch?.plantable?.value ?: 0)) > patch.currentGrowthStage){
patch.setCurrentState(savedState)
} else {
patch.setCurrentState((patch.plantable?.value ?: 0) + patch.currentGrowthStage)
}
if(patchMap[fPatch] == null) {
patchMap[fPatch] = patch
}
}
}
}
@@ -0,0 +1,74 @@
package content.global.skill.farming.timers
import core.game.node.entity.Entity
import core.game.node.item.Item
import core.game.system.timer.*
import core.game.node.entity.player.Player
import content.global.skill.farming.*
import java.util.concurrent.TimeUnit
import org.json.simple.*
import java.time.*
class SeedlingGrowth : PersistTimer (1, "farming:seedling", isSoft = true) {
val seedlings = ArrayList<Seedling>()
lateinit var player: Player
fun addSeedling(seedling: Int){
seedlings.add(
Seedling(
seedling,
System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5),
seedling + if(seedling > 5400) 8 else 6
)
)
}
override fun onRegister (entity: Entity) {
player = (entity as? Player)!!
}
override fun run (entity: Entity) : Boolean {
val removeList = ArrayList<Seedling>()
for (seed in seedlings) {
if (System.currentTimeMillis() > seed.TTL) {
val inInventory = player.inventory.get(Item(seed.id))
if (inInventory != null) {
player.inventory.replace(Item(seed.sapling), inInventory.slot)
removeList.add(seed)
} else {
val inBank = player.bank.get(Item(seed.id))
if(inBank == null) removeList.add(seed)
else {
player.bank.remove(Item(inBank.id,1))
player.bank.add(Item(seed.sapling))
removeList.add(seed)
}
}
}
}
seedlings.removeAll(removeList)
return seedlings.isNotEmpty()
}
override fun save(root: JSONObject, entity: Entity) {
val seedArray = JSONArray()
for(s in seedlings){
val seed = JSONObject()
seed.put("id",s.id)
seed.put("ttl",s.TTL)
seed.put("sapling",s.sapling)
seedArray.add(seed)
}
root.put("seedlings",seedArray)
}
override fun parse(root: JSONObject, entity: Entity) {
(root["seedlings"] as JSONArray).forEach {
val s = it as JSONObject
val id = s["id"].toString().toInt()
val ttl = s["ttl"].toString().toLong()
val sapling = s["sapling"].toString().toInt()
seedlings.add(Seedling(id,ttl,sapling))
}
}
}
@@ -3,6 +3,7 @@ package content.global.skill.gather.mining
import content.data.skill.SkillingPets
import content.data.skill.SkillingTool
import content.global.skill.skillcapeperks.SkillcapePerks
import content.global.activity.shootingstar.StarBonus
import core.api.*
import core.cache.def.impl.ItemDefinition
import core.game.event.ResourceProducedEvent
@@ -159,7 +160,7 @@ class MiningListener : InteractionListener {
}
// If player has mining boost from Shooting Star, roll chance at extra ore
if (player.hasActiveState("shooting-star")) {
if (hasTimerActive<StarBonus>(player)) {
if (RandomFunction.getRandom(5) == 3) {
sendMessage(player, "...you manage to mine a second ore thanks to the Star Sprite.")
amount += 1
@@ -222,4 +223,4 @@ class MiningListener : InteractionListener {
}
return node.isActive
}
}
}
@@ -12,7 +12,6 @@ import core.game.node.entity.impl.Projectile;
import core.game.node.entity.impl.Animator.Priority;
import core.game.node.entity.player.link.SpellBookManager.SpellBook;
import core.game.node.entity.player.link.audio.Audio;
import core.game.node.entity.state.EntityState;
import core.game.node.item.Item;
import core.game.world.GameWorld;
import core.game.world.update.flag.context.Animation;
@@ -20,6 +19,8 @@ import core.game.world.update.flag.context.Graphics;
import core.plugin.Initializable;
import core.plugin.Plugin;
import static core.api.ContentAPIKt.*;
/**
* Handles the Ice spells from the Ancient spellbook.
* @author Emperor
@@ -133,8 +134,8 @@ public final class IceSpells extends CombatSpell {
}
int ticks = (1 + (type.ordinal() - SpellType.RUSH.ordinal())) * 8;
if (state.getEstimatedHit() > -1) {
if (victim.getAttribute("freeze_immunity", -1) < GameWorld.getTicks()) {
victim.getStateManager().set(EntityState.FROZEN, ticks);
if (!hasTimerActive(victim, "frozen:immunity")) {
registerTimer(victim, spawnTimer("frozen", ticks, true));
} else if (type == SpellType.BARRAGE) {
state.setFrozen(true);
}
@@ -14,13 +14,14 @@ import core.game.node.entity.impl.Projectile;
import core.game.node.entity.impl.Animator.Priority;
import core.game.node.entity.player.Player;
import core.game.node.entity.player.link.SpellBookManager.SpellBook;
import core.game.node.entity.state.EntityState;
import core.game.node.item.Item;
import core.game.world.GameWorld;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
import core.plugin.Plugin;
import static core.api.ContentAPIKt.*;
/**
* Handles the Miasmic spells that are a part of the Ancient spellbook.
* @author Splinter
@@ -119,8 +120,8 @@ public final class MiasmicSpells extends CombatSpell {
@Override
public void fireEffect(Entity entity, Entity victim, BattleState state) {
if (victim.getAttribute("miasmic_immunity", -1) < GameWorld.getTicks()) {
victim.getStateManager().register(EntityState.MIASMIC, true, (getSpellId() - 15) * 20);
if (!hasTimerActive(victim, "miasmic:immunity")) {
registerTimer(victim, spawnTimer("miasmic", (getSpellId() - 15) * 20));
}
}
@@ -11,13 +11,14 @@ import core.game.node.entity.combat.spell.SpellType;
import core.game.node.entity.impl.Projectile;
import core.game.node.entity.impl.Animator.Priority;
import core.game.node.entity.player.link.SpellBookManager.SpellBook;
import core.game.node.entity.state.EntityState;
import core.game.node.item.Item;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
import core.plugin.Initializable;
import core.plugin.Plugin;
import static core.api.ContentAPIKt.*;
/**
* Handles the Smoke spells from the Ancient spellbook.
* @author Emperor
@@ -111,7 +112,7 @@ public final class SmokeSpells extends CombatSpell {
@Override
public void fireEffect(Entity entity, Entity victim, BattleState state) {
if (state.getEstimatedHit() > -1) {
victim.getStateManager().register(EntityState.POISONED, false, type.ordinal() >= SpellType.BLITZ.ordinal() ? 48 : 28, entity);
applyPoison(victim, entity, type.ordinal() >= SpellType.BLITZ.ordinal() ? 4 : 2);
}
}
@@ -15,7 +15,6 @@ import core.game.node.entity.player.Player
import core.game.node.entity.player.link.TeleportManager
import core.game.node.entity.player.link.audio.Audio
import core.game.node.entity.skill.Skills
import core.game.node.entity.state.EntityState
import core.game.node.item.Item
import core.game.node.scenery.Scenery
import core.game.system.command.Privilege
@@ -206,7 +205,7 @@ class LunarListeners : SpellListener("lunar"), Commands {
return@define
}
if(dmg != null) {
p?.let { addState(it, EntityState.POISONED, false, (dmg * 10 + 8), player) }
p?.let { applyPoison(it, it, dmg) }
} else {
sendMessage(player, "Damage must be an integer. Format:")
sendMessage(player, "::poison username damage")
@@ -480,14 +479,14 @@ class LunarListeners : SpellListener("lunar"), Commands {
}
private fun cureMe(player: Player) {
if(!hasState(player, EntityState.POISONED)) {
if(!isPoisoned(player)) {
sendMessage(player, "You are not poisoned.")
return
}
requires(player, 71, arrayOf(Item(Items.ASTRAL_RUNE_9075, 2), Item(Items.LAW_RUNE_563, 1), Item(Items.COSMIC_RUNE_564, 2)))
removeRunes(player, true)
visualizeSpell(player, CURE_ME_ANIM, CURE_ME_GFX, 2880)
removeState(player, EntityState.POISONED)
curePoison(player)
addXP(player, 69.0)
playAudio(player, Audio(2900))
sendMessage(player, "You have been cured of poison.")
@@ -497,7 +496,7 @@ class LunarListeners : SpellListener("lunar"), Commands {
requires(player, 74, arrayOf(Item(Items.ASTRAL_RUNE_9075, 2), Item(Items.LAW_RUNE_563, 2), Item(Items.COSMIC_RUNE_564, 2)))
removeRunes(player, true)
visualizeSpell(player, CURE_GROUP_ANIM, CURE_GROUP_GFX, 2882)
removeState(player, EntityState.POISONED)
curePoison(player)
for(acct in RegionManager.getLocalPlayers(player, 1)) {
if(!acct.isActive || acct.locks.isInteractionLocked) {
continue
@@ -505,7 +504,7 @@ class LunarListeners : SpellListener("lunar"), Commands {
if(!acct.settings.isAcceptAid) {
continue
}
removeState(acct, EntityState.POISONED)
curePoison(acct)
sendMessage(acct, "You have been cured of poison.")
playAudio(acct, Audio(2889), true)
visualize(acct, -1, CURE_GROUP_GFX)
@@ -527,7 +526,7 @@ class LunarListeners : SpellListener("lunar"), Commands {
sendMessage(player, "This player is not accepting any aid.")
return
}
if(!hasState(p, EntityState.POISONED)) {
if(!isPoisoned(p)) {
sendMessage(player, "This player is not poisoned.")
return
}
@@ -537,7 +536,7 @@ class LunarListeners : SpellListener("lunar"), Commands {
visualize(p, -1, CURE_OTHER_GFX)
playAudio(p, Audio(2889), true)
removeRunes(player, true)
removeState(p, EntityState.POISONED)
curePoison(p)
sendMessage(p, "You have been cured of poison.")
addXP(player, 65.0)
}
@@ -15,6 +15,8 @@ import core.plugin.Initializable;
import core.plugin.Plugin;
import org.rs09.consts.Sounds;
import static core.api.ContentAPIKt.*;
/**
* Represents the charge spell magic spell.
* @author Emperor
@@ -49,8 +51,8 @@ public final class ChargeSpell extends MagicSpell {
p.getLocks().lock("charge_cast", 100);
visualize(entity, target);
// Remove the previous copy of the state in order to refresh the duration if recast before 7 minutes
p.clearState("godcharge");
p.registerState("godcharge").init();
removeTimer (p, "magic:spellcharge");
registerTimer (p, spawnTimer("magic:spellcharge"));
p.getPacketDispatch().sendMessage("You feel charged with magic power.");
return true;
}
@@ -1,46 +0,0 @@
package content.global.skill.magic.modern
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.audio.Audio
import core.game.system.task.Pulse
import org.json.simple.JSONObject
import core.game.node.entity.state.PlayerState
import core.game.node.entity.state.State
import core.game.world.GameWorld;
@PlayerState("godcharge")
class GodspellChargedState(player: Player? = null) : State(player) {
val DURATION = 700
var startTick: Int = 0
override fun save(root: JSONObject) {
root.put("ticks_elapsed", GameWorld.ticks - startTick)
}
override fun parse(_data: JSONObject) {
if(_data.containsKey("ticks_elapsed")){
startTick = GameWorld.ticks - _data["ticks_elapsed"].toString().toInt()
}
}
override fun newInstance(player: Player?): State {
var ret = GodspellChargedState(player)
ret.startTick = GameWorld.ticks
return ret
}
override fun createPulse() {
player ?: return
if(GameWorld.ticks - startTick >= DURATION) return
pulse = object : Pulse(DURATION) {
override fun pulse(): Boolean {
player.sendMessage("Your magical charge fades away.")
player.clearState("godcharge")
player.audioManager.send(Audio(1650))
pulse = null
return true
}
}
}
}
@@ -0,0 +1,16 @@
package content.global.skill.magic.modern
import core.api.*
import org.json.simple.*
import core.game.system.timer.*
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
class SpellCharge : PersistTimer (700, "magic:spellcharge") {
override fun run (entity: Entity) : Boolean {
if (entity !is Player) return false
sendMessage(entity, "Your magical charge fades away.")
playAudio(entity, getAudio(1650))
return false
}
}
@@ -11,13 +11,14 @@ import core.game.node.entity.impl.Projectile;
import core.game.node.entity.player.Player;
import core.game.node.entity.player.link.SpellBookManager.SpellBook;
import core.game.node.entity.player.link.prayer.PrayerType;
import core.game.node.entity.state.EntityState;
import core.game.node.item.Item;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
import core.plugin.Initializable;
import core.plugin.Plugin;
import static core.api.ContentAPIKt.*;
/**
* Handles the teleportation block spell in the modern spellbook.
* @author Splinter
@@ -117,7 +118,7 @@ public final class TeleblockSpell extends CombatSpell {
if(((Player) victim).getPrayer().get(PrayerType.PROTECT_FROM_MAGIC)){
ticks /= 2;
}
victim.getStateManager().set(EntityState.TELEBLOCK, ticks);
registerTimer(victim, spawnTimer("teleblock", ticks));
} else if(victim.isTeleBlocked()){
entity.asPlayer().sendMessage("Your target is already blocked from teleporting.");
}
@@ -4,7 +4,6 @@ import core.cache.def.impl.SceneryDefinition;
import core.game.component.Component;
import core.game.node.entity.player.link.prayer.PrayerType;
import core.plugin.Initializable;
import core.game.node.entity.skill.SkillRestoration;
import core.game.node.entity.skill.Skills;
import core.game.interaction.OptionHandler;
import core.game.node.Node;
@@ -5,9 +5,10 @@ import core.game.node.entity.skill.Skills;
import core.game.node.entity.combat.ImpactHandler.HitsplatType;
import core.game.node.entity.combat.equipment.WeaponInterface;
import core.game.node.entity.player.Player;
import core.game.node.entity.state.EntityState;
import core.tools.RandomFunction;
import static core.api.ContentAPIKt.*;
/**
* Represents the Bloated Leech familiar.
* @author Aero
@@ -38,7 +39,7 @@ public class BloatedLeechNPC extends Familiar {
@Override
protected boolean specialMove(FamiliarSpecial special) {
owner.getStateManager().remove(EntityState.POISONED);
curePoison(owner);
for (int i = 0; i < Skills.SKILL_NAME.length; i++) {
if (owner.getSkills().getLevel(i) < owner.getSkills().getStaticLevel(i)) {
owner.getSkills().setLevel(i, owner.getSkills().getStaticLevel(i));
@@ -5,7 +5,6 @@ import core.game.node.entity.combat.CombatStyle;
import core.game.node.entity.combat.equipment.WeaponInterface;
import core.game.node.entity.impl.Projectile;
import core.game.node.entity.player.Player;
import core.game.node.entity.state.EntityState;
import core.game.system.task.Pulse;
import core.game.world.GameWorld;
import core.game.world.update.flag.context.Animation;
@@ -8,11 +8,12 @@ import core.game.node.entity.combat.equipment.Weapon;
import core.game.node.entity.combat.equipment.WeaponInterface;
import core.game.node.entity.impl.Projectile;
import core.game.node.entity.player.Player;
import core.game.node.entity.state.EntityState;
import core.game.node.item.Item;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
import static core.api.ContentAPIKt.*;
/**
* Represents the Spirit Scorpion familiar.
* @author Vexia
@@ -49,7 +50,7 @@ public class SpiritScorpionNPC extends Familiar {
if (isCharged() && new Item(weapon.getId() + 6).getName().startsWith(weapon.getName())) {
final Entity victim = state.getVictim();
setCharged(false);
victim.getStateManager().register(EntityState.POISONED, false, 10, owner);
applyPoison(victim, owner, 1);
}
}
}
@@ -4,12 +4,13 @@ import core.plugin.Initializable;
import core.game.node.entity.Entity;
import core.game.node.entity.impl.Projectile;
import core.game.node.entity.player.Player;
import core.game.node.entity.state.EntityState;
import core.game.node.item.Item;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
import core.tools.RandomFunction;
import static core.api.ContentAPIKt.*;
/**
* Represents the Stranger Plant familiar.
* @author Aero
@@ -45,7 +46,7 @@ public class StrangerPlantNPC extends Forager {
}
Entity target = special.getTarget();
if (RandomFunction.random(2) == 1) {
target.getStateManager().register(EntityState.POISONED, false, 40, target);
applyPoison(target, owner, 20);
}
animate(Animation.create(8211));
Projectile.ranged(this, target, 1508, 50, 40, 1, 45).send();
@@ -7,12 +7,13 @@ import core.game.interaction.OptionHandler;
import core.game.node.Node;
import core.game.node.entity.combat.equipment.WeaponInterface;
import core.game.node.entity.player.Player;
import core.game.node.entity.state.EntityState;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
import core.plugin.Plugin;
import core.plugin.ClassScanner;
import static core.api.ContentAPIKt.*;
/**
* Represents the Unicorn Stallion familiar.
* @author Aero
@@ -72,13 +73,13 @@ public class UnicornStallionNPC extends Familiar {
player.sendMessage("You don't have enough summoning points left");
return true;
}
if (!owner.getStateManager().hasState(EntityState.POISONED)) {
if (!isPoisoned(owner)) {
player.sendMessage("You are not poisoned.");
return true;
}
player.getAudioManager().send(4372);
familiar.visualize(Animation.create(8267), Graphics.create(1356));
player.getStateManager().remove(EntityState.POISONED);
curePoison(player);
player.getSkills().updateLevel(Skills.SUMMONING, -2, 0);
return true;
}
@@ -0,0 +1,76 @@
package content.global.skill.summoning.pet
import core.api.*
import core.cache.def.impl.SceneryDefinition
import core.game.node.Node
import core.game.node.entity.player.Player
import core.game.node.entity.skill.Skills
import core.game.node.item.GroundItemManager
import core.game.interaction.*
import core.tools.StringUtils
class IncubatorHandler : InteractionListener {
val eggIds = IncubatorEgg.values().map { it.egg.id }.toIntArray()
val incubators = intArrayOf(28550, 28352, 28359)
override fun defineListeners() {
on (incubators, IntType.SCENERY, "inspect", handler = ::handleInspectOption)
on (incubators, IntType.SCENERY, "take-egg", handler = ::handleTakeOption)
onUseWith (IntType.SCENERY, eggIds, *incubators, handler = ::handleEggOnIncubator)
}
fun handleEggOnIncubator (player: Player, used: Node, with: Node) : Boolean {
val egg = IncubatorEgg.forItem (used.asItem()) ?: return false
val activeEgg = IncubatorTimer.getEggFor (player, player.location.regionId)
if (activeEgg != null) {
sendMessage (player, "You already have an egg in this incubator.")
return true
}
if (removeItem(player, used.asItem()))
IncubatorTimer.registerEgg (player, player.location.regionId, egg)
return true
}
fun handleInspectOption (player: Player, node: Node) : Boolean {
val activeEgg = IncubatorTimer.getEggFor (player, player.location.regionId)
if (activeEgg == null) {
sendMessage (player, "The incubator is currently empty.")
return true
}
if (activeEgg.finished) {
sendMessage (player, "The egg inside has finished incubating.")
return true
}
val creatureName = activeEgg.egg.product.name.lowercase()
sendMessage (player, "There is currently ${if (StringUtils.isPlusN(creatureName)) "an" else "a"} $creatureName egg incubating.")
return true
}
fun handleTakeOption (player: Player, node: Node) : Boolean {
val region = player.location.regionId
val activeEgg = IncubatorTimer.getEggFor (player, region) ?: return false
if (!activeEgg.finished) {
sendMessage (player, "That egg hasn't finished incubating!")
return true
}
if (freeSlots(player) < 1) {
sendMessage (player, "You do not have enough inventory space to do that.")
return true
}
val egg = IncubatorTimer.removeEgg (player, region) ?: return false
val product = egg.product
val name = product.name.lowercase()
sendMessage(player, "You take your $name out of the incubator.")
addItem(player, product.id)
return true
}
}
@@ -1,65 +0,0 @@
package content.global.skill.summoning.pet
import core.game.node.entity.player.Player
import core.game.node.entity.state.PlayerState
import core.game.node.entity.state.State
import core.game.system.task.Pulse
import org.json.simple.JSONObject
import core.api.*
import core.tools.*
@PlayerState("incubator")
class IncubatorState(player: Player? = null) : State(player) {
var egg: IncubatorEgg? = null
var completionTimeMs = 0L
fun setTicksLeft (ticks: Int) {
completionTimeMs = System.currentTimeMillis() + (ticksToSeconds(ticks) * 1000)
}
override fun newInstance(player: Player?): State {
return IncubatorState(player)
}
override fun save(root: JSONObject) {
if(pulse == null) return
val data = JSONObject()
data.put("eggOrdinal",(egg?.ordinal ?: 0).toString())
data.put("endTime", completionTimeMs.toString())
root.put("eggdata",data)
}
override fun parse(_data: JSONObject) {
if(_data.containsKey("eggdata")){
val data = _data["eggdata"] as JSONObject
egg = IncubatorEgg.values()[data["eggOrdinal"].toString().toInt()]
if (data.containsKey("ticksLeft"))
completionTimeMs = System.currentTimeMillis() + ticksToSeconds(data["ticksLeft"].toString().toInt() * 1000)
else
completionTimeMs = data["endTime"].toString().toLong()
}
}
override fun createPulse() {
player ?: return
egg ?: return
setVarbit(player, 4277, 1)
pulse = object : Pulse(){
override fun pulse(): Boolean {
if(System.currentTimeMillis() >= completionTimeMs){
player.setAttribute(ATTR_INCUBATOR_PRODUCT, egg!!.ordinal)
player.sendMessage("Your " + egg!!.product.name.toLowerCase() + " has finished hatching.")
pulse = null
return true
}
return false
}
}
}
companion object {
const val ATTR_INCUBATOR_PRODUCT = "/save:incubator:egg-product"
}
}
@@ -0,0 +1,98 @@
package content.global.skill.summoning.pet
import core.api.*
import core.tools.*
import core.game.system.timer.*
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import java.util.*
import org.json.simple.*
class IncubatorTimer : PersistTimer (500, "incubation") {
val incubatingEggs = HashMap<Int, IncubatingEgg>()
override fun getInitialRunDelay() : Int {
return 50
}
override fun parse (root: JSONObject, entity: Entity) {
val eggs = root["eggs"] as? JSONArray ?: return
for (eggData in eggs) {
val eggInfo = eggData as JSONArray
val egg = IncubatingEgg (
eggInfo[0].toString().toInt(),
IncubatorEgg.values()[eggInfo[1].toString().toInt()],
eggInfo[2].toString().toLong(),
eggInfo[3].toString().toBoolean()
)
incubatingEggs[egg.region] = egg
}
}
override fun save (root: JSONObject, entity: Entity) {
val arr = JSONArray()
for ((_, eggInfo) in incubatingEggs) {
val eggArr = JSONArray()
eggArr.add(eggInfo.region.toString())
eggArr.add(eggInfo.egg.ordinal.toString())
eggArr.add(eggInfo.endTime.toString())
eggArr.add(eggInfo.finished)
arr.add(eggArr)
}
root["eggs"] = arr
}
override fun run (entity: Entity) : Boolean {
if (entity !is Player) return false
for ((_, egg) in incubatingEggs) {
if (egg.finished) continue
if (egg.isDone()) {
sendMessage(entity, colorize("%RYour ${egg.egg.product.name.lowercase()} egg has finished hatching."))
egg.finished = true
}
}
return !incubatingEggs.isEmpty()
}
data class IncubatingEgg (val region: Int, val egg: IncubatorEgg, var endTime: Long, var finished: Boolean = false)
fun IncubatingEgg.isDone() : Boolean {
return endTime < System.currentTimeMillis()
}
companion object {
val TAVERLY_REGION = 11573
val TAVERLY_VARBIT = 4277
val YANILLE_REGION = 10288
val YANILLE_VARBIT = 4221
fun varbitForRegion (region: Int) : Int {
return when (region) {
TAVERLY_REGION -> TAVERLY_VARBIT
YANILLE_REGION -> YANILLE_VARBIT
else -> -1
}
}
fun getEggFor (player: Player, region: Int) : IncubatingEgg? {
val playerTimer = getTimer<IncubatorTimer>(player) ?: return null
return playerTimer.incubatingEggs[region]
}
fun registerEgg (player: Player, region: Int, egg: IncubatorEgg) {
val timer = getTimer<IncubatorTimer>(player) ?: IncubatorTimer()
timer.incubatingEggs [region] = IncubatingEgg (region, egg, System.currentTimeMillis() + (ticksToSeconds(egg.inucbationTime * 100) * 1000))
if (!hasTimerActive<IncubatorTimer>(player))
registerTimer(player, timer)
setVarbit(player, varbitForRegion(region), 1, true)
}
fun removeEgg (player: Player, region: Int) : IncubatorEgg? {
val egg = getEggFor (player, region) ?: return null
val timer = getTimer<IncubatorTimer>(player) ?: return null
timer.incubatingEggs.remove(region)
setVarbit(player, varbitForRegion(region), 0, true)
return egg.egg
}
}
}
@@ -7,7 +7,6 @@ import core.game.node.entity.impl.Animator
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.audio.Audio
import core.game.node.entity.skill.Skills
import core.game.node.entity.state.EntityState
import core.game.world.update.flag.context.Animation
import core.tools.RandomFunction
import org.rs09.consts.Items
@@ -75,4 +74,4 @@ class ThievingListeners : InteractionListener {
return@on true
}
}
}
}