Farming improvements

White Berry Bushes can now be protected
Giant Ent properly increases Belladonna yield
Mushrooms now disease properly
Fixed various bush bugs
Fruit Trees can now be chopped
Fixed Scarecrow needing to grow to work
Mushrooms should now visually update as each mushroom is picked from the patch
Poison Ivy Bushes are now disease immune
Poison Ivy Berries picked in the Champion's Guild Patch now finishes a Varrock Diary task
This commit is contained in:
Syndromeramo 2025-06-18 13:35:50 +00:00 committed by Ryan
parent 91f3a70f75
commit e8c3f31786
8 changed files with 137 additions and 23 deletions

View file

@ -15,7 +15,7 @@ import core.plugin.Plugin
import org.rs09.consts.Items
import org.rs09.consts.Sounds
val livesBased = arrayOf(PatchType.HERB_PATCH, PatchType.CACTUS_PATCH, PatchType.BELLADONNA_PATCH, PatchType.HOPS_PATCH, PatchType.ALLOTMENT, PatchType.EVIL_TURNIP_PATCH)
val livesBased = arrayOf(PatchType.HERB_PATCH, PatchType.CACTUS_PATCH, PatchType.HOPS_PATCH, PatchType.ALLOTMENT)
@Initializable
class CropHarvester : OptionHandler() {
@ -43,14 +43,16 @@ class CropHarvester : OptionHandler() {
override fun pulse(): Boolean {
var reward = Item(crop)
val familiar = player.familiarManager.familiar
if (familiar != null && familiar is GiantEntNPC) {
familiar.modifyFarmingReward(fPatch, reward)
}
if (!hasSpaceFor(player, reward)) {
sendMessage(player, "You have run out of inventory space.")
return true
}
val familiar = player.familiarManager.familiar
if (familiar != null && familiar is GiantEntNPC) {
familiar.modifyFarmingReward(fPatch, reward)
}
var requiredItem = when (fPatch.type) {
PatchType.TREE_PATCH -> Items.SECATEURS_5329
else -> Items.SPADE_952
@ -92,7 +94,7 @@ class CropHarvester : OptionHandler() {
// TODO: If a flower patch is being harvested, delay the clearing of the
// patch until after the animation has played - https://youtu.be/lg4GktlVNUY?t=75
delay = 2
addItem(player, reward.id)
addItemOrDrop(player, reward.id,reward.amount)
rewardXP(player, Skills.FARMING, plantable.harvestXP)
if (patch.patch.type in livesBased) {
patch.rollLivesDecrement(
@ -104,6 +106,9 @@ class CropHarvester : OptionHandler() {
if (patch.harvestAmt <= 0 && crop == plantable.harvestItem) {
patch.clear()
}
else if (fPatch.type == PatchType.MUSHROOM_PATCH){
patch.setCurrentState(patch.getCurrentState() + 1)
}
}
if (sendHarvestMessages && (patch.cropLives <= 0 || patch.harvestAmt <= 0)) {
sendMessage(player, "The $patchName is now empty.")

View file

@ -30,6 +30,14 @@ class DigUpPatchDialogue(player: Player? = null) : DialoguePlugin(player) {
return true
}
}
if (patch?.patch?.type == PatchType.FRUIT_TREE_PATCH) {
val isTreeStump = patch?.getCurrentState() == patch?.plantable!!.value + 25
if (patch!!.isGrown() && !isTreeStump) {
sendMessage(player, "You need to chop this tree down first.") // this message is not authentic
stage = 1000
return true
}
}
sendDialogueOptions(player, "Are you sure you want to dig up this patch?", "Yes, I want to clear it for new crops.", "No, I want to leave it as it is.")
stage = 0
return true

View file

@ -6,6 +6,7 @@ import core.game.interaction.OptionHandler
import core.game.node.Node
import content.global.skill.summoning.familiar.GiantEntNPC
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.diary.DiaryType
import core.game.node.entity.skill.Skills
import core.game.node.item.Item
import core.game.system.task.Pulse
@ -86,6 +87,10 @@ class FruitAndBerryPicker : OptionHandler() {
sendMessage(player, "You pick $determiner ${reward.name.lowercase()}.")
}
if (plantable == Plantable.POISON_IVY_SEED && patch.patch == FarmingPatch.CHAMPIONS_GUILD_BUSH){
player.achievementDiaryManager.finishTask(player, DiaryType.VARROCK, 2, 0)
}
return patch.getFruitOrBerryCount() == 0
}
})

View file

@ -0,0 +1,57 @@
package content.global.skill.farming
import content.data.skill.SkillingTool
import core.api.*
import core.cache.def.impl.SceneryDefinition
import core.game.interaction.OptionHandler
import core.game.node.Node
import core.game.node.entity.player.Player
import core.game.system.task.Pulse
import core.plugin.Initializable
import core.plugin.Plugin
import core.tools.RandomFunction
import org.rs09.consts.Sounds
@Initializable
class FruitTreeChopper : OptionHandler() {
override fun newInstance(arg: Any?): Plugin<Any> {
SceneryDefinition.setOptionHandler("chop-down",this)
SceneryDefinition.setOptionHandler("chop down",this)
return this
}
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
player ?: return false
node ?: return false
val fPatch = FarmingPatch.forObject(node.asScenery())
fPatch ?: return false
val patch = fPatch.getPatchFor(player)
val plantable = patch.plantable
plantable ?: return false
val animation = SkillingTool.getHatchet(player).animation
submitIndividualPulse(player, object : Pulse(animation.duration) {
override fun pulse(): Boolean {
animate(player, animation)
val soundIndex = RandomFunction.random(0, woodcuttingSounds.size)
playAudio(player, woodcuttingSounds[soundIndex])
patch.setCurrentState(patch.getCurrentState() + 19)
sendMessage(player, "You chop down the ${plantable.displayName.lowercase().removeSuffix(" sapling")}.")
return true
}
})
return true
}
private val woodcuttingSounds = intArrayOf(
Sounds.WOODCUTTING_HIT_3038,
Sounds.WOODCUTTING_HIT_3039,
Sounds.WOODCUTTING_HIT_3040,
Sounds.WOODCUTTING_HIT_3041,
Sounds.WOODCUTTING_HIT_3042
)
}

View file

@ -29,7 +29,11 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
Plantable.WILLOW_SAPLING -> 0
else -> 1
}
if(plantable != null && plantable?.applicablePatch != PatchType.FLOWER_PATCH) {
if(plantable != null
&& plantable?.applicablePatch != PatchType.FLOWER_PATCH
&& plantable?.applicablePatch != PatchType.BELLADONNA_PATCH
&& plantable?.applicablePatch != PatchType.MUSHROOM_PATCH
&& plantable?.applicablePatch != PatchType.EVIL_TURNIP_PATCH) {
harvestAmt += compostMod
}
cropLives = 3 + compostMod
@ -70,8 +74,6 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
var chance = when(patch.type){
PatchType.ALLOTMENT -> 8 //average of 8 per life times 3 lives = average 24
PatchType.HOPS_PATCH -> 6 //average of 6 per life times 3 lives = 18
PatchType.BELLADONNA_PATCH -> 2 //average of 2 per life times 3 lives = 6
PatchType.EVIL_TURNIP_PATCH -> 2 //average 2 per, same as BELLADONNA
PatchType.CACTUS_PATCH -> 3 //average of 3 per life times 3 lives = 9
else -> 0 // nothing should go here, but if it does, do not give extra crops amd decrement cropLives
}
@ -88,6 +90,11 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
return getCurrentState() in 0..2
}
fun isChoppedFruitTree(): Boolean {
return (patch.type == PatchType.FRUIT_TREE_PATCH)
&& getCurrentState() == (plantable?.value ?: 0) + 25
}
fun isEmptyAndWeeded(): Boolean {
return getCurrentState() == 3
}
@ -198,6 +205,10 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
else if(isDiseased && !isDead) setVisualState(getHerbDiseaseValue())
else setVisualState((plantable?.value ?: 0) + currentGrowthStage)
}
PatchType.MUSHROOM_PATCH -> {
if(isDead) setVisualState(getMushroomDeathValue())
else if(isDiseased && !isDead) setVisualState(getMushroomDiseaseValue())
}
else -> {}
}
}
@ -221,16 +232,27 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
private fun getBushDiseaseValue(): Int{
if(plantable == Plantable.POISON_IVY_SEED){
return (plantable?.value ?: 0) + currentGrowthStage + 12
} else {
}
else if (plantable == Plantable.REDBERRY_SEED
|| plantable == Plantable.CADAVABERRY_SEED){
return (plantable?.value ?: 0) + currentGrowthStage + 65
}
else {
return (plantable?.value ?: 0) + currentGrowthStage + 64
}
}
private fun getBushDeathValue(): Int{
if(plantable == Plantable.POISON_IVY_SEED){
return (plantable?.value ?: 0) + currentGrowthStage + 22
} else {
return (plantable?.value ?: 0) + currentGrowthStage + 126
return (plantable?.value ?: 0) + currentGrowthStage + 20
}
else if (plantable == Plantable.REDBERRY_SEED
|| plantable == Plantable.CADAVABERRY_SEED
|| plantable == Plantable.WHITEBERRY_SEED){
return (plantable?.value ?: 0) + currentGrowthStage + 129
}
else {
return (plantable?.value ?: 0) + currentGrowthStage + 128
}
}
@ -258,6 +280,14 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
return (plantable?.value ?: 0) + currentGrowthStage + 16
}
private fun getMushroomDiseaseValue(): Int {
return (plantable?.value ?: 0) + currentGrowthStage + 11
}
private fun getMushroomDeathValue(): Int {
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
@ -275,7 +305,7 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
}
private fun grow(){
if((isWeedy() || isEmptyAndWeeded()) && getCurrentState() > 0) {
if((isWeedy() || isEmptyAndWeeded() || (plantable == Plantable.SCARECROW && !isGrown())) && getCurrentState() > 0) {
nextGrowth = System.currentTimeMillis() + 60000
setCurrentState(getCurrentState() - 1)
currentGrowthStage--
@ -294,7 +324,14 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
CompostType.SUPERCOMPOST -> 13
}
if(patch != FarmingPatch.TROLL_STRONGHOLD_HERB && RandomFunction.random(128) <= (17 - diseaseMod) && !isWatered && !isGrown() && !protectionPaid && !isFlowerProtected() && patch.type != PatchType.EVIL_TURNIP_PATCH && currentGrowthStage != 0){
if(patch != FarmingPatch.TROLL_STRONGHOLD_HERB
&& RandomFunction.random(128) <= (17 - diseaseMod)
&& !isWatered && !isGrown()
&& !protectionPaid
&& !isFlowerProtected()
&& patch.type != PatchType.EVIL_TURNIP_PATCH
&& plantable != Plantable.POISON_IVY_SEED
&& currentGrowthStage != 0){
isDiseased = true
// If we manually set disease mod reset it back to 0 so that crops can naturally grow after being treated/accidentally attempted to disease when they cannot be
if (diseaseMod < 0) diseaseMod = 0
@ -389,7 +426,9 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl
else -> return false
}.getPatchFor(player, false)
return (fpatch.plantable != null &&
if (fpatch.plantable == Plantable.SCARECROW && fpatch.plantable == plantable?.protectionFlower){
return true
} else return (fpatch.plantable != null &&
(fpatch.plantable == plantable?.protectionFlower || fpatch.plantable == Plantable.forItemID(Items.WHITE_LILY_SEED_14589))
&& fpatch.isGrown())
}

View file

@ -14,7 +14,7 @@ enum class Plantable(val itemID: Int, val displayName: String, val value: Int, v
WHITE_LILY_SEED(Items.WHITE_LILY_SEED_14589,"white lily seed",37,4,42.0,250.0,0.0,52,PatchType.FLOWER_PATCH,Items.WHITE_LILY_14583),
// Flower (technically)
SCARECROW(Items.SCARECROW_6059,"scarecrow",33,3,0.0,0.0,0.0,23,PatchType.FLOWER_PATCH,Items.SCARECROW_6059),
SCARECROW(Items.SCARECROW_6059,"scarecrow",36,-3,0.0,0.0,0.0,23,PatchType.FLOWER_PATCH,Items.SCARECROW_6059),
// Allotments
POTATO_SEED(Items.POTATO_SEED_5318, "potato seed", 6, 4, 8.0, 9.0, 0.0, 1, PatchType.ALLOTMENT, Items.POTATO_1942,Item(Items.COMPOST_6032,2),MARIGOLD_SEED),
@ -53,9 +53,9 @@ enum class Plantable(val itemID: Int, val displayName: String, val value: Int, v
// Bushes
REDBERRY_SEED(Items.REDBERRY_SEED_5101,"redberry bush seed",5,5,11.5,4.5,64.0,10,PatchType.BUSH_PATCH,Items.REDBERRIES_1951,Item(Items.CABBAGES10_5478,4)),
CADAVABERRY_SEED(Items.CADAVABERRY_SEED_5102,"cadavaberry bush seed",15,6,18.0,7.0,102.5,22,PatchType.BUSH_PATCH,Items.CADAVA_BERRIES_753,Item(Items.TOMATOES5_5968,3)),
DWELLBERRY_SEED(Items.DWELLBERRY_SEED_5103,"dwellberry bush seed",26,27,31.5,12.0,177.5,36,PatchType.BUSH_PATCH,Items.DWELLBERRIES_2126,Item(Items.STRAWBERRIES5_5406,3)),
DWELLBERRY_SEED(Items.DWELLBERRY_SEED_5103,"dwellberry bush seed",26,7,31.5,12.0,177.5,36,PatchType.BUSH_PATCH,Items.DWELLBERRIES_2126,Item(Items.STRAWBERRIES5_5406,3)),
JANGERBERRY_SEED(Items.JANGERBERRY_SEED_5104,"jangerberry bush seed",38,8,50.5,19.0,284.5,48,PatchType.BUSH_PATCH,Items.JANGERBERRIES_247,Item(Items.WATERMELON_5982,6)),
WHITEBERRY_SEED(Items.WHITEBERRY_SEED_5105,"whiteberry bush seed",51,8,78.0,29.0,437.5,59,PatchType.BUSH_PATCH,Items.WHITE_BERRIES_239,null),
WHITEBERRY_SEED(Items.WHITEBERRY_SEED_5105,"whiteberry bush seed",51,8,78.0,29.0,437.5,59,PatchType.BUSH_PATCH,Items.WHITE_BERRIES_239,Item(Items.MUSHROOM_6004,8)),
POISON_IVY_SEED(Items.POISON_IVY_SEED_5106,"poison ivy bush seed",197,8,120.0,45.0,675.0,70,PatchType.BUSH_PATCH,Items.POISON_IVY_BERRIES_6018,null),
// Herbs
@ -78,7 +78,7 @@ enum class Plantable(val itemID: Int, val displayName: String, val value: Int, v
// Special
BELLADONNA_SEED(Items.BELLADONNA_SEED_5281, "belladonna seed", 4, 4, 91.0, 128.0, 0.0, 63, PatchType.BELLADONNA_PATCH, Items.CAVE_NIGHTSHADE_2398),
MUSHROOM_SPORE(Items.MUSHROOM_SPORE_5282, "mushroom spore", 6, 7, 61.5, 57.7, 0.0, 53, PatchType.MUSHROOM_PATCH, Items.MUSHROOM_6004),
MUSHROOM_SPORE(Items.MUSHROOM_SPORE_5282, "mushroom spore", 4, 6, 61.5, 57.7, 0.0, 53, PatchType.MUSHROOM_PATCH, Items.MUSHROOM_6004),
CACTUS_SEED(Items.CACTUS_SEED_5280, "cactus seed", 8, 7, 66.5, 25.0, 374.0, 55, PatchType.CACTUS_PATCH, Items.CACTUS_SPINE_6016),
EVIL_TURNIP_SEED(Items.EVIL_TURNIP_SEED_12148, "evil turnip seed", 4, 1, 41.0, 46.0, 0.0, 42, PatchType.EVIL_TURNIP_PATCH, Items.EVIL_TURNIP_12134)
;

View file

@ -235,7 +235,7 @@ class UseWithPatchHandler : InteractionListener {
}
val p = patch.getPatchFor(player)
if (p.getCurrentState() < 3 && p.isWeedy() && plantable != Plantable.SCARECROW) {
if (p.getCurrentState() < 3 && p.isWeedy()) {
sendMessage(player, "This patch needs weeding first.")
return@onUseWith true
} else if (p.getCurrentState() > 3) {

View file

@ -41,7 +41,7 @@ class CropGrowth : PersistTimer (500, "farming:crops", isSoft = true) {
//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){
if(patch.nextGrowth < (System.currentTimeMillis() + 240_000L) && !patch.isDead && !patch.isChoppedFruitTree()){
patch.nextGrowth = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(patch.getStageGrowthMinutes().toLong())
patch.update()
}
@ -58,7 +58,7 @@ class CropGrowth : PersistTimer (500, "farming:crops", isSoft = true) {
for ((_, patch) in patchMap) {
val type = patch.patch.type
val shouldPlayCatchup = !patch.isGrown() || (type == PatchType.BUSH_PATCH && patch.getFruitOrBerryCount() < 4) || (type == PatchType.FRUIT_TREE_PATCH && patch.getFruitOrBerryCount() < 6)
if (shouldPlayCatchup && !patch.isDead) {
if (shouldPlayCatchup && !patch.isDead && !patch.isChoppedFruitTree()) {
var stagesToSimulate = if (!patch.isGrown()) {
if (patch.isWeedy() || patch.isEmptyAndWeeded()) patch.currentGrowthStage % 4
else patch.plantable!!.stages - patch.currentGrowthStage