diff --git a/Server/src/main/java/core/game/content/dialogue/DialogueInterpreter.java b/Server/src/main/java/core/game/content/dialogue/DialogueInterpreter.java index 80556cc23..577ab2d8c 100644 --- a/Server/src/main/java/core/game/content/dialogue/DialogueInterpreter.java +++ b/Server/src/main/java/core/game/content/dialogue/DialogueInterpreter.java @@ -204,6 +204,7 @@ public final class DialogueInterpreter { public boolean close() { if (dialogue != null || dialogueStage != null) { actions.clear(); + if (player.getInterfaceManager().getChatbox() != null && player.getInterfaceManager().getChatbox().getCloseEvent() != null) { return true; } @@ -211,8 +212,10 @@ public final class DialogueInterpreter { dialogueStage = null; player.getInterfaceManager().closeChatbox(); } - if (dialogue != null && dialogue.close()) { + if (dialogue != null) { + DialoguePlugin d = dialogue; dialogue = null; + d.close(); } } return dialogue == null && dialogueStage == null; diff --git a/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java b/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java index 962164acf..b814afc94 100644 --- a/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java +++ b/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java @@ -122,6 +122,7 @@ public abstract class DialoguePlugin implements Plugin { public boolean close() { player.getInterfaceManager().closeChatbox(); player.getInterfaceManager().openChatbox(137); + if(file != null) file.end(); finished = true; return true; } diff --git a/Server/src/main/kotlin/api/ContentAPI.kt b/Server/src/main/kotlin/api/ContentAPI.kt index 1bbbb2b78..b47200e24 100644 --- a/Server/src/main/kotlin/api/ContentAPI.kt +++ b/Server/src/main/kotlin/api/ContentAPI.kt @@ -167,6 +167,26 @@ object ContentAPI { if(!player.inventory.add(item)) GroundItemManager.create(item,player) } + /** + * Clears an NPC with the "poof" smoke graphics commonly seen with random event NPCs. + * @param npc the NPC object to initialize + */ + @JvmStatic + fun poofClear(npc: NPC){ + submitWorldPulse(object : Pulse(){ + var counter = 0 + override fun pulse(): Boolean { + when(counter++){ + 2 -> { + npc.isInvisible = true; Graphics.send(Graphics(86), npc.location) + } + 3 -> npc.clear().also { return true } + } + return false + } + }) + } + /** * Check if an item exists in a player's bank * @param player the player whose bank to check @@ -546,8 +566,8 @@ object ContentAPI { @JvmStatic fun openDialogue(player: Player, dialogue: Any, vararg args: Any) { when (dialogue) { - is Int -> player.dialogueInterpreter.open(dialogue, args) - is DialogueFile -> player.dialogueInterpreter.open(dialogue, args) + is Int -> player.dialogueInterpreter.open(dialogue, *args) + is DialogueFile -> player.dialogueInterpreter.open(dialogue, *args) else -> SystemLogger.logErr("Invalid object type passed to openDialogue() -> ${dialogue.javaClass.simpleName}") } } diff --git a/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt b/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt index 175c67199..7ceb1cf3b 100644 --- a/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt +++ b/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt @@ -1,5 +1,6 @@ package rs09.game.content.ame +import api.ContentAPI import core.game.interaction.MovementPulse import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player @@ -36,18 +37,7 @@ abstract class RandomEventNPC(id: Int) : NPC(id) { open fun terminate(){ player.antiMacroHandler.event = null if(initialized){ - Pulser.submit(object : Pulse(){ - var counter = 0 - override fun pulse(): Boolean { - when(counter++){ - 2 -> { - isInvisible = true; Graphics.send(SMOKE_GRAPHICS, location) - } - 3 -> clear().also { return true } - } - return false - } - }) + ContentAPI.poofClear(this) } } diff --git a/Server/src/main/kotlin/rs09/game/content/dialogue/DialogueFile.kt b/Server/src/main/kotlin/rs09/game/content/dialogue/DialogueFile.kt index 9e0cdffa6..f120909d7 100644 --- a/Server/src/main/kotlin/rs09/game/content/dialogue/DialogueFile.kt +++ b/Server/src/main/kotlin/rs09/game/content/dialogue/DialogueFile.kt @@ -75,7 +75,7 @@ abstract class DialogueFile { return player(expr, *splitLines(msg!!)) } - fun end(){ + open fun end(){ if(interpreter != null) interpreter!!.close() } diff --git a/Server/src/main/kotlin/rs09/game/content/zone/FarmingPatchZone.kt b/Server/src/main/kotlin/rs09/game/content/zone/FarmingPatchZone.kt new file mode 100644 index 000000000..68145204b --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/content/zone/FarmingPatchZone.kt @@ -0,0 +1,98 @@ +package rs09.game.content.zone + +import api.ContentAPI +import core.game.content.dialogue.FacialExpression +import core.game.node.entity.Entity +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player +import core.game.node.entity.skill.Skills +import core.game.system.task.Pulse +import core.game.world.map.zone.MapZone +import core.game.world.map.zone.ZoneBorders +import core.game.world.map.zone.ZoneBuilder +import core.plugin.Initializable +import core.plugin.Plugin +import org.rs09.consts.NPCs +import rs09.game.content.dialogue.DialogueFile +import rs09.tools.secondsToTicks +import java.util.concurrent.TimeUnit + +@Initializable +class FarmingPatchZone : MapZone("farming patch", true), Plugin { + + val playersInZone = hashMapOf() + + override fun configure() { + registerRegion(12083) + registerRegion(10548) + register(ZoneBorders(3594,3521,3608,3532)) + ContentAPI.submitWorldPulse(zonePulse) + } + + override fun newInstance(arg: Any?): Plugin { + ZoneBuilder.configure(this) + return this + } + + override fun fireEvent(identifier: String?, vararg args: Any?): Any { + return Unit + } + + override fun enter(e: Entity?): Boolean { + if(e is Player && playersInZone[e] == null && ContentAPI.getStatLevel(e, Skills.FARMING) <= 15) { + playersInZone[e] = 0 + } + return super.enter(e) + } + + override fun leave(e: Entity?, logout: Boolean): Boolean { + if(e is Player){ + playersInZone.remove(e) + } + return super.enter(e) + } + + val zonePulse = object : Pulse(){ + override fun pulse(): Boolean { + playersInZone.toList().forEach { (player, ticks) -> + if(ticks == secondsToTicks(TimeUnit.MINUTES.toSeconds(5).toInt())){ + val npc = NPC(NPCs.GITHAN_7122) + npc.location = player.location + npc.init() + npc.moveStep() + npc.face(player) + ContentAPI.openDialogue(player, SpiritDialogue(true), npc) + } else if (ticks == secondsToTicks(TimeUnit.MINUTES.toSeconds(10).toInt())){ + val npc = NPC(NPCs.GITHAN_7122) + npc.location = player.location + npc.init() + npc.moveStep() + npc.face(player) + ContentAPI.openDialogue(player, SpiritDialogue(false), npc) + playersInZone.remove(player) + } + + playersInZone[player] = ticks + 1 + } + return false + } + } + + internal class SpiritDialogue(val is5: Boolean) : DialogueFile(){ + override fun handle(componentID: Int, buttonID: Int) { + when(stage){ + 0 -> { + if(is5) npcl(FacialExpression.NEUTRAL, "In case you didn't know, you don't have to stand by your Farming patch. Your crops will grow even if you're not around.").also { stage++ } + else npcl(FacialExpression.NEUTRAL, " Did you know that if your Farming patch has fully grown, it will never catch disease or die? It will be perfectly safe until you choose to harvest it.").also { stage++ } + } + 1 -> end() + } + } + + override fun end(){ + super.end() + ContentAPI.poofClear(npc ?: return) + } + } + +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/CropHarvester.kt b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/CropHarvester.kt index 3698bd93d..892394d6b 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/CropHarvester.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/CropHarvester.kt @@ -1,5 +1,6 @@ package rs09.game.node.entity.skill.farming +import api.ContentAPI import core.cache.def.impl.SceneryDefinition import core.game.interaction.OptionHandler import core.game.node.Node @@ -66,12 +67,8 @@ class CropHarvester : OptionHandler() { delay = 2 player.inventory.add(Item(plantable.harvestItem,1)) player.skills.addExperience(Skills.FARMING,plantable.harvestXP) - patch.harvestAmt-- - - if(patch.harvestAmt <= 0){ - patch.clear() - } - return patch.harvestAmt <= 0 + patch.rollLivesDecrement(ContentAPI.getDynLevel(player, Skills.FARMING), requiredItem == Items.MAGIC_SECATEURS_7409) + return patch.cropLives <= 0 } }) diff --git a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Patch.kt b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Patch.kt index 19aae22ab..e178d8ae8 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Patch.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Patch.kt @@ -4,6 +4,8 @@ import core.game.node.entity.player.Player import core.tools.RandomFunction import rs09.game.system.SystemLogger import java.util.concurrent.TimeUnit +import kotlin.math.ceil +import kotlin.math.min class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantable?, var currentGrowthStage: Int, var isDiseased: Boolean, var isDead: Boolean, var isWatered: Boolean, var nextGrowth: Long, var harvestAmt: Int, var isCheckHealth: Boolean) { constructor(player: Player, patch: FarmingPatch) : this(player,patch,null,0,false,false,false,0L,0,false) @@ -11,22 +13,58 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl var diseaseMod = 0 var compost = CompostType.NONE var protectionPaid = false + var cropLives = 3 fun setNewHarvestAmount() { - if(patch.type == PatchType.ALLOTMENT){ - harvestAmt = RandomFunction.random(4,17) - } else if(patch.type == PatchType.FLOWER || patch.type == PatchType.EVIL_TURNIP) { - harvestAmt = when (plantable) { - Plantable.LIMPWURT_SEED, Plantable.WOAD_SEED -> 3 - else -> 1 + cropLives = 3 + } + + fun rollLivesDecrement(farmingLevel: Int, magicSecateurs: Boolean){ + if(patch.type == PatchType.HERB){ + //authentic formula thanks to released data. + var herbSaveLow = when(plantable){ + Plantable.GUAM_SEED -> min(24 + farmingLevel, 80) + Plantable.MARRENTILL_SEED -> min(28 + farmingLevel, 80) + Plantable.TARROMIN_SEED -> min(31 + farmingLevel, 80) + Plantable.HARRALANDER_SEED -> min(36 + farmingLevel, 80) + Plantable.GOUT_TUBER -> min(39 + farmingLevel, 80) + Plantable.RANARR_SEED -> min(39 + farmingLevel, 80) + Plantable.SPIRIT_WEED_SEED -> min(43 + farmingLevel, 80) + Plantable.TOADFLAX_SEED -> min(43 + farmingLevel, 80) + Plantable.IRIT_SEED -> min(46 + farmingLevel, 80) + Plantable.AVANTOE_SEED -> min(50 + farmingLevel, 80) + Plantable.KWUARM_SEED -> min(54 + farmingLevel, 80) + Plantable.SNAPDRAGON_SEED -> min(57 + farmingLevel, 80) + Plantable.CADANTINE_SEED -> min(60 + farmingLevel, 80) + Plantable.LANTADYME_SEED -> min(64 + farmingLevel, 80) + Plantable.DWARF_WEED_SEED -> min(67 + farmingLevel, 80) + Plantable.TORSTOL_SEED -> min(71 + farmingLevel, 80) + else -> -1 + } + + if(magicSecateurs) herbSaveLow = ceil(1.10 * herbSaveLow).toInt() + + val rand = RandomFunction.random(256) + + if(rand > herbSaveLow){ + cropLives -= 1 } - } else if(patch.type == PatchType.HOPS){ - harvestAmt = RandomFunction.random(3,35) } else { - harvestAmt = RandomFunction.random(3,5) + //inauthentic formulae based on reported averages due to lack of formula + var chance = when(patch.type){ + PatchType.ALLOTMENT -> 8 //average of 8 per life times 3 lives = average 24 + PatchType.HOPS -> 6 //average of 6 per life times 3 lives = 18 + PatchType.BELLADONNA -> 2 //average of 2 per life times 3 lives = 6 + PatchType.CACTUS -> 3 //average of 3 per life times 3 lives = 9 + else -> return + } + + if(magicSecateurs) chance += ceil(1.10 * chance).toInt() //will increase average yield by roughly 3. + + if(RandomFunction.roll(chance)) cropLives -= 1 } - if(compost == CompostType.NORMAL) harvestAmt += 1 - if(compost == CompostType.SUPER) harvestAmt += 2 + + if(cropLives <= 0) clear() } fun isWeedy(): Boolean { @@ -82,6 +120,11 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl if(isDead) player.varpManager.get(patch.varpIndex).setVarbit(patch.varpOffset, getCactusDeathValue()) else if(isDiseased && !isDead) player.varpManager.get(patch.varpIndex).setVarbit(patch.varpOffset, getCactusDiseaseValue()) } + PatchType.HERB -> { + if(isDead) player.varpManager.get(patch.varpIndex).setVarbit(patch.varpOffset, getHerbDeathValue()) + else if(isDiseased && !isDead) player.varpManager.get(patch.varpIndex).setVarbit(patch.varpOffset, getHerbDiseaseValue()) + else player.varpManager.get(patch.varpIndex).setVarbit(patch.varpOffset, (plantable?.value ?: 0) + currentGrowthStage) + } else -> {} } } @@ -144,6 +187,22 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl return (plantable?.value ?: 0) + currentGrowthStage + 16 } + private fun getHerbDiseaseValue(): Int { + return if (plantable?.value ?: -1 <= 103) { + 128 + (((plantable?.ordinal ?: 0) - Plantable.GUAM_SEED.ordinal) * 3) + currentGrowthStage - 1 + } else if (plantable == Plantable.SPIRIT_WEED_SEED) { + 211 + currentGrowthStage - 1 + } else { + 198 + currentGrowthStage -1 + } + } + + private fun getHerbDeathValue(): Int { + return if(plantable == Plantable.GOUT_TUBER){ + 201 + currentGrowthStage - 1 + } else 170 + currentGrowthStage - 1 + } + private fun grow(){ if(isWeedy() && getCurrentState() > 0) { nextGrowth = System.currentTimeMillis() + 60000 @@ -164,8 +223,8 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl } if(RandomFunction.random(128) <= (17 - diseaseMod) && !isWatered && !isGrown() && !protectionPaid && !isFlowerProtected() && patch.type != PatchType.EVIL_TURNIP){ - //bush, tree, fruit tree and cactus can not disease on stage 1(0) of growth. - if(!((patch.type == PatchType.BUSH || patch.type == PatchType.TREE || patch.type == PatchType.FRUIT_TREE || patch.type == PatchType.CACTUS) && currentGrowthStage == 0)) { + //bush, tree, fruit tree, herb and cactus can not disease on stage 1(0) of growth. + if(!((patch.type == PatchType.BUSH || patch.type == PatchType.TREE || patch.type == PatchType.FRUIT_TREE || patch.type == PatchType.CACTUS || patch.type == PatchType.HERB) && currentGrowthStage == 0)) { isDiseased = true return } diff --git a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Plantable.kt b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Plantable.kt index 8cf594791..8921590d3 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Plantable.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/Plantable.kt @@ -63,17 +63,18 @@ enum class Plantable(val itemID: Int, val value: Int, val stages: Int, val plant MARRENTILL_SEED(5292,11,4,13.5,15.0,0.0,14,PatchType.HERB,Items.GRIMY_MARRENTILL_201), TARROMIN_SEED(5293,18,4,16.0,18.0,0.0,19,PatchType.HERB,Items.GRIMY_TARROMIN_203), HARRALANDER_SEED(5294,25,4,21.5,24.0,0.0,26,PatchType.HERB,Items.GRIMY_HARRALANDER_205), - GOUT_TUBER(6311,32,4,105.0,45.0,0.0,29,PatchType.HERB,Items.GOUTWEED_3261), - RANARR_SEED(5295,39,4,27.0,30.5,0.0,32,PatchType.HERB,Items.GRIMY_RANARR_207), - TOADFLAX_SEED(5296,47,4,34.0,38.5,0.0,38,PatchType.HERB,Items.GRIMY_TOADFLAX_3049), - IRIT_SEED(5297,54,4,43.0,48.5,0.0,44,PatchType.HERB,Items.GRIMY_IRIT_209), - AVANTOE_SEED(5298,204,4,54.5,61.5,0.0,50,PatchType.HERB,Items.GRIMY_AVANTOE_211), + RANARR_SEED(5295,32,4,27.0,30.5,0.0,32,PatchType.HERB,Items.GRIMY_RANARR_207), + AVANTOE_SEED(5298,39,4,54.5,61.5,0.0,50,PatchType.HERB,Items.GRIMY_AVANTOE_211), + TOADFLAX_SEED(5296,46,4,34.0,38.5,0.0,38,PatchType.HERB,Items.GRIMY_TOADFLAX_3049), + IRIT_SEED(5297,53,4,43.0,48.5,0.0,44,PatchType.HERB,Items.GRIMY_IRIT_209), KWUARM_SEED(5299,68,4,69.0,78.0,0.0,56,PatchType.HERB,Items.GRIMY_KWUARM_213), SNAPDRAGON_SEED(5300,75,4,87.5,98.5,0.0,62,PatchType.HERB,Items.GRIMY_SNAPDRAGON_3051), CADANTINE_SEED(5301,82,4,106.5,120.0,0.0,67,PatchType.HERB,Items.GRIMY_CADANTINE_215), LANTADYME_SEED(5302,89,4,134.5,151.5,0.0,73,PatchType.HERB,Items.GRIMY_LANTADYME_2485), DWARF_WEED_SEED(5303,96,4,170.5,192.0,0.0,79,PatchType.HERB,Items.GRIMY_DWARF_WEED_217), TORSTOL_SEED(5304,103,4,199.5,224.5,0.0,85,PatchType.HERB,Items.GRIMY_TORSTOL_219), + GOUT_TUBER(6311,192,4,105.0,45.0,0.0,29,PatchType.HERB,Items.GOUTWEED_3261), + SPIRIT_WEED_SEED(12176, 204, 4, 32.0, 36.0, 0.0, 36, PatchType.HERB, Items.GRIMY_SPIRIT_WEED_12174), //Other BELLADONNA_SEED(5281, 4, 4, 91.0, 128.0, 0.0, 63, PatchType.BELLADONNA, Items.CAVE_NIGHTSHADE_2398), diff --git a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/UseWithPatchHandler.kt b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/UseWithPatchHandler.kt index 1d45b2b70..22d160a93 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/UseWithPatchHandler.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/skill/farming/UseWithPatchHandler.kt @@ -128,6 +128,7 @@ object UseWithPatchHandler{ if(p.plantable != null){ p.harvestAmt += if(p.compost == CompostType.NORMAL) 1 else if(p.compost == CompostType.SUPER) 2 else 0 } + p.cropLives += if(p.compost == CompostType.SUPER) 2 else 1 player.inventory.add(Item(Items.BUCKET_1925)) } return true diff --git a/Server/src/main/kotlin/rs09/game/node/entity/state/newsys/states/FarmingState.kt b/Server/src/main/kotlin/rs09/game/node/entity/state/newsys/states/FarmingState.kt index e5ae69574..2024508a9 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/state/newsys/states/FarmingState.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/state/newsys/states/FarmingState.kt @@ -50,6 +50,7 @@ class FarmingState(player: Player? = null) : State(player) { 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() @@ -91,10 +92,12 @@ class FarmingState(player: Player? = null) : State(player) { 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)