Merge pull request #386 from Major-/kotlin-experiments

Improve woodcutting, fishing, mining plugin code
This commit is contained in:
Major
2018-03-28 17:52:25 +01:00
committed by GitHub
13 changed files with 491 additions and 487 deletions
+3 -1
View File
@@ -6,6 +6,8 @@ plugin {
"tlf30" "tlf30"
] ]
dependencies = [ dependencies = [
"util:lookup", "entity:spawn", "api" "api",
"entity:spawn",
"util:lookup"
] ]
} }
+52 -60
View File
@@ -1,50 +1,54 @@
package org.apollo.game.plugin.skills.fishing package org.apollo.game.plugin.skills.fishing
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.Animation import org.apollo.game.model.Animation
import org.apollo.game.plugin.api.Definitions
import org.apollo.game.plugin.api.rand
import org.apollo.game.plugin.skills.fishing.Fish.* import org.apollo.game.plugin.skills.fishing.Fish.*
import org.apollo.game.plugin.skills.fishing.FishingTool.* import org.apollo.game.plugin.skills.fishing.FishingTool.*
import java.util.Random
/** /**
* A fish that can be gathered using the fishing skill. * A fish that can be gathered using the fishing skill.
*/ */
enum class Fish(val id: Int, val level: Int, val experience: Double) { enum class Fish(val id: Int, val level: Int, val experience: Double) {
SHRIMP(317, 1, 10.0), SHRIMP(id = 317, level = 1, experience = 10.0),
SARDINE(327, 5, 20.0), SARDINE(id = 327, level = 5, experience = 20.0),
MACKEREL(353, 16, 20.0), MACKEREL(id = 353, level = 16, experience = 20.0),
HERRING(345, 10, 30.0), HERRING(id = 345, level = 10, experience = 30.0),
ANCHOVY(321, 15, 40.0), ANCHOVY(id = 321, level = 15, experience = 40.0),
TROUT(335, 20, 50.0), TROUT(id = 335, level = 20, experience = 50.0),
COD(341, 23, 45.0), COD(id = 341, level = 23, experience = 45.0),
PIKE(349, 25, 60.0), PIKE(id = 349, level = 25, experience = 60.0),
SALMON(331, 30, 70.0), SALMON(id = 331, level = 30, experience = 70.0),
TUNA(359, 35, 80.0), TUNA(id = 359, level = 35, experience = 80.0),
LOBSTER(377, 40, 90.0), LOBSTER(id = 377, level = 40, experience = 90.0),
BASS(363, 46, 100.0), BASS(id = 363, level = 46, experience = 100.0),
SWORDFISH(371, 50, 100.0), SWORDFISH(id = 371, level = 50, experience = 100.0),
SHARK(383, 76, 110.0); SHARK(id = 383, level = 76, experience = 110.0);
/** /**
* The name of this fish, formatted so it can be inserted into a message. * The name of this fish, formatted so it can be inserted into a message.
*/ */
val formattedName = ItemDefinition.lookup(id).name.toLowerCase() val formattedName = Definitions.item(id)!!.name.toLowerCase()
// TODO this leads to incorrect messages, e.g. 'You catch a raw shrimps'.
} }
/** /**
* A tool used to gather [Fish] from a [FishingSpot]. * A tool used to gather [Fish] from a [FishingSpot].
*/ */
enum class FishingTool(val id: Int, animation: Int, val message: String, val bait: Int, val baitName: String?) { enum class FishingTool(
LOBSTER_CAGE(301, 619, "You attempt to catch a lobster..."), val message: String,
SMALL_NET(303, 620, "You cast out your net..."), val id: Int,
BIG_NET(305, 620, "You cast out your net..."), animation: Int,
HARPOON(311, 618, "You start harpooning fish..."), val bait: Int = -1,
FISHING_ROD(307, 622, "You attempt to catch a fish...", 313, "feathers"), val baitName: String? = null
FLY_FISHING_ROD(309, 622, "You attempt to catch a fish...", 314, "fishing bait"); ) {
LOBSTER_CAGE("You attempt to catch a lobster...", id = 301, animation = 619),
@Suppress("unused") // IntelliJ bug, doesn't detect that this constructor is used SMALL_NET("You cast out your net...", id = 303, animation = 620),
constructor(id: Int, animation: Int, message: String) : this(id, animation, message, -1, null) BIG_NET("You cast out your net...", id = 305, animation = 620),
HARPOON("You start harpooning fish...", id = 311, animation = 618),
FISHING_ROD("You attempt to catch a fish...", id = 307, animation = 622, bait = 313, baitName = "feathers"),
FLY_FISHING_ROD("You attempt to catch a fish...", id = 309, animation = 622, bait = 314, baitName = "fishing bait");
/** /**
* The [Animation] played when fishing with this tool. * The [Animation] played when fishing with this tool.
@@ -54,7 +58,7 @@ enum class FishingTool(val id: Int, animation: Int, val message: String, val bai
/** /**
* The name of this tool, formatted so it can be inserted into a message. * The name of this tool, formatted so it can be inserted into a message.
*/ */
val formattedName = ItemDefinition.lookup(id).name.toLowerCase() val formattedName = Definitions.item(id)!!.name.toLowerCase()
} }
@@ -68,14 +72,12 @@ enum class FishingSpot(val npc: Int, private val first: Option, private val seco
NET_ROD(316, Option.of(SMALL_NET, SHRIMP, ANCHOVY), Option.of(FISHING_ROD, SARDINE, HERRING)); NET_ROD(316, Option.of(SMALL_NET, SHRIMP, ANCHOVY), Option.of(FISHING_ROD, SARDINE, HERRING));
companion object { companion object {
private val FISHING_SPOTS = FishingSpot.values().associateBy({ it.npc }, { it }) private val FISHING_SPOTS = FishingSpot.values().associateBy({ it.npc }, { it })
/** /**
* Returns the [FishingSpot] with the specified [id], or `null` if the spot does not exist. * Returns the [FishingSpot] with the specified [id], or `null` if the spot does not exist.
*/ */
fun lookup(id: Int): FishingSpot? = FISHING_SPOTS[id] fun lookup(id: Int): FishingSpot? = FISHING_SPOTS[id]
} }
/** /**
@@ -94,19 +96,6 @@ enum class FishingSpot(val npc: Int, private val first: Option, private val seco
*/ */
sealed class Option { sealed class Option {
companion object {
fun of(tool: FishingTool, primary: Fish): Option = Single(tool, primary)
fun of(tool: FishingTool, primary: Fish, secondary: Fish): Option {
return when {
primary.level < secondary.level -> Pair(tool, primary, secondary)
else -> Pair(tool, secondary, primary)
}
}
}
/** /**
* The tool used to obtain fish * The tool used to obtain fish
*/ */
@@ -131,35 +120,38 @@ enum class FishingSpot(val npc: Int, private val first: Option, private val seco
override val level = primary.level override val level = primary.level
override fun sample(level: Int): Fish = primary override fun sample(level: Int): Fish = primary
} }
/** /**
* A [FishingSpot] [Option] that can provide a two different types of fish. * A [FishingSpot] [Option] that can provide a two different types of fish.
*/ */
private data class Pair(override val tool: FishingTool, val primary: Fish, val secondary: Fish) : Option() { private data class Pair(override val tool: FishingTool, val primary: Fish, val secondary: Fish) : Option() {
companion object {
val random = Random()
/**
* The weighting factor that causes the lower-level fish to be returned more frequently.
*/
const val WEIGHTING = 70
}
override val level = Math.min(primary.level, secondary.level) override val level = Math.min(primary.level, secondary.level)
override fun sample(level: Int): Fish { override fun sample(level: Int): Fish {
if (secondary.level > level) { return if (level < secondary.level || rand(100) < WEIGHTING) {
return primary primary
} else {
secondary
}
} }
private companion object {
/**
* The weighting factor that causes the lower-level fish to be returned more frequently.
*/
private const val WEIGHTING = 70
}
}
companion object {
fun of(tool: FishingTool, primary: Fish): Option = Single(tool, primary)
fun of(tool: FishingTool, primary: Fish, secondary: Fish): Option {
return when { return when {
random.nextInt(100) < WEIGHTING -> primary primary.level < secondary.level -> Pair(tool, primary, secondary)
else -> secondary else -> Pair(tool, secondary, primary)
} }
} }
@@ -6,44 +6,31 @@ import org.apollo.game.message.impl.NpcActionMessage
import org.apollo.game.model.Position import org.apollo.game.model.Position
import org.apollo.game.model.entity.Player import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.api.fishing import org.apollo.game.plugin.api.fishing
import org.apollo.game.plugin.api.rand
import org.apollo.game.plugin.skills.fishing.FishingSpot import org.apollo.game.plugin.skills.fishing.FishingSpot
import org.apollo.game.plugin.skills.fishing.FishingTool import org.apollo.game.plugin.skills.fishing.FishingTool
import java.util.Objects import java.util.Objects
import java.util.Random
// TODO: moving fishing spots, seaweed and caskets, evil bob // TODO: moving fishing spots, seaweed and caskets, evil bob
/**
* Intercepts the [NpcActionMessage] and starts a [FishingAction] if the npc
*/
on { NpcActionMessage::class }
.where { option == 1 || option == 3 }
.then { player ->
val entity = player.world.npcRepository[index]
val spot = FishingSpot.lookup(entity.id) ?: return@then
val option = spot.option(option)
player.startAction(FishingAction(player, entity.position, option))
terminate()
}
class FishingAction(player: Player, position: Position, val option: FishingSpot.Option) : class FishingAction(player: Player, position: Position, val option: FishingSpot.Option) :
AsyncDistancedAction<Player>(0, true, player, position, SPOT_DISTANCE) { AsyncDistancedAction<Player>(0, true, player, position, SPOT_DISTANCE) {
companion object {
private const val SPOT_DISTANCE = 1
private const val FISHING_DELAY = 4
/**
* The random number generator used by the fishing plugin.
*/
private val random = Random()
/**
* Returns whether or not the catch was successful.
* TODO: We need to identify the correct algorithm for this
*/
private fun successfulCatch(level: Int, req: Int): Boolean = minOf(level - req + 5, 40) > random.nextInt(100)
/**
* Returns whether or not the [Player] has (or does not need) bait.
*/
private fun hasBait(player: Player, bait: Int): Boolean = bait == -1 || player.inventory.contains(bait)
/**
* @return if the player has the needed tool to fish at the spot.
*/
private fun hasTool(player: Player, tool: FishingTool): Boolean = player.equipment.contains(tool.id) ||
player.inventory.contains(tool.id)
}
/** /**
* The [FishingTool] used for the fishing spot. * The [FishingTool] used for the fishing spot.
*/ */
@@ -75,10 +62,12 @@ class FishingAction(player: Player, position: Position, val option: FishingSpot.
if (mob.inventory.freeSlots() == 0) { if (mob.inventory.freeSlots() == 0) {
mob.inventory.forceCapacityExceeded() mob.inventory.forceCapacityExceeded()
mob.stopAnimation() mob.stopAnimation()
stop() stop()
} else if (!hasBait(mob, tool.bait)) { } else if (!hasBait(mob, tool.bait)) {
mob.sendMessage("You need more ${tool.baitName} to fish at this spot.") mob.sendMessage("You need more ${tool.baitName} to fish at this spot.")
mob.stopAnimation() mob.stopAnimation()
stop() stop()
} }
@@ -90,21 +79,17 @@ class FishingAction(player: Player, position: Position, val option: FishingSpot.
* Verifies that the player can gather fish from the [FishingSpot] they clicked. * Verifies that the player can gather fish from the [FishingSpot] they clicked.
*/ */
private fun verify(): Boolean { private fun verify(): Boolean {
if (mob.fishing.current < option.level) { val current = mob.fishing.current
mob.sendMessage("You need a fishing level of ${option.level} to fish at this spot.")
return false when {
} else if (!hasTool(mob, tool)) { current < option.level -> mob.sendMessage("You need a fishing level of ${option.level} to fish at this spot.")
mob.sendMessage("You need a ${tool.formattedName} to fish at this spot.") !hasTool(mob, tool) -> mob.sendMessage("You need a ${tool.formattedName} to fish at this spot.")
return false !hasBait(mob, tool.bait) -> mob.sendMessage("You need some ${tool.baitName} to fish at this spot.")
} else if (!hasBait(mob, tool.bait)) { mob.inventory.freeSlots() == 0 -> mob.inventory.forceCapacityExceeded()
mob.sendMessage("You need some ${tool.baitName} to fish at this spot.") else -> return true
return false
} else if (mob.inventory.freeSlots() == 0) {
mob.inventory.forceCapacityExceeded()
return false
} }
return true return false
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -117,19 +102,27 @@ class FishingAction(player: Player, position: Position, val option: FishingSpot.
override fun hashCode(): Int = Objects.hash(option, position, mob) override fun hashCode(): Int = Objects.hash(option, position, mob)
} private companion object {
private const val SPOT_DISTANCE = 1
private const val FISHING_DELAY = 4
/** /**
* Intercepts the [NpcActionMessage] and starts a [FishingAction] if the npc * Returns whether or not the catch was successful.
* TODO: We need to identify the correct algorithm for this
*/ */
on { NpcActionMessage::class } private fun successfulCatch(level: Int, req: Int): Boolean = minOf(level - req + 5, 40) > rand(100)
.where { option == 1 || option == 3 }
.then {
val entity = it.world.npcRepository[index]
val spot = FishingSpot.lookup(entity.id) ?: return@then
val option = spot.option(option) /**
it.startAction(FishingAction(it, entity.position, option)) * Returns whether or not the [Player] has (or does not need) bait.
*/
private fun hasBait(player: Player, bait: Int): Boolean = bait == -1 || player.inventory.contains(bait)
/**
* Returns whether or not the player has the required tool to fish at the spot.
*/
private fun hasTool(player: Player, tool: FishingTool): Boolean = player.equipment.contains(tool.id) ||
player.inventory.contains(tool.id)
}
terminate()
} }
-5
View File
@@ -125,10 +125,6 @@ register(ROD, x = 2385, y = 3422)
register(ROD, x = 2384, y = 3419) register(ROD, x = 2384, y = 3419)
register(ROD, x = 2383, y = 3417) register(ROD, x = 2383, y = 3417)
// Gunnarsgrunn
register(ROD, x = 3101, y = 3092)
register(ROD, x = 3103, y = 3092)
// Karamja // Karamja
register(NET_ROD, x = 2921, y = 3178) register(NET_ROD, x = 2921, y = 3178)
register(CAGE_HARPOON, x = 2923, y = 3179) register(CAGE_HARPOON, x = 2923, y = 3179)
@@ -163,7 +159,6 @@ register(NET_ROD, x = 2990, y = 3169)
register(NET_ROD, x = 2986, y = 3176) register(NET_ROD, x = 2986, y = 3176)
// Shilo Village // Shilo Village
register(ROD, x = 2855, y = 2974) register(ROD, x = 2855, y = 2974)
register(ROD, x = 2865, y = 2972) register(ROD, x = 2865, y = 2972)
register(ROD, x = 2860, y = 2972) register(ROD, x = 2860, y = 2972)
+1
View File
@@ -3,6 +3,7 @@ plugin {
authors = [ authors = [
"Graham", "Graham",
"Mikey`", "Mikey`",
"Major",
"WH:II:DOW", "WH:II:DOW",
"Requa", "Requa",
"Clifton", "Clifton",
+10 -9
View File
@@ -1,12 +1,13 @@
package org.apollo.game.plugin.skills.mining package org.apollo.game.plugin.skills.mining
enum class Gem(val id: Int, val chance: Int) { enum class Gem(val id: Int) { // TODO add gem drop chances
UNCUT_SAPPHIRE(1623, 0), UNCUT_SAPPHIRE(1623),
UNCUT_EMERALD(1605, 0), UNCUT_EMERALD(1605),
UNCUT_RUBY(1619, 0), UNCUT_RUBY(1619),
UNCUT_DIAMOND(1617, 0) UNCUT_DIAMOND(1617);
companion object {
private val GEMS = Gem.values().associateBy({ it.id }, { it })
fun lookup(id: Int): Gem? = GEMS[id]
}
} }
val GEMS = Gem.values()
fun lookupGem(id: Int): Gem? = GEMS.find { it.id == id }
+51 -94
View File
@@ -1,6 +1,5 @@
import org.apollo.game.action.ActionBlock import org.apollo.game.action.ActionBlock
import org.apollo.game.action.AsyncDistancedAction import org.apollo.game.action.AsyncDistancedAction
import org.apollo.game.action.DistancedAction
import org.apollo.game.message.impl.ObjectActionMessage import org.apollo.game.message.impl.ObjectActionMessage
import org.apollo.game.model.Position import org.apollo.game.model.Position
import org.apollo.game.model.World import org.apollo.game.model.World
@@ -12,26 +11,28 @@ import org.apollo.game.plugin.api.findObject
import org.apollo.game.plugin.api.mining import org.apollo.game.plugin.api.mining
import org.apollo.game.plugin.api.rand import org.apollo.game.plugin.api.rand
import org.apollo.game.plugin.skills.mining.Ore import org.apollo.game.plugin.skills.mining.Ore
import org.apollo.game.plugin.skills.mining.PICKAXES
import org.apollo.game.plugin.skills.mining.Pickaxe import org.apollo.game.plugin.skills.mining.Pickaxe
import org.apollo.game.plugin.skills.mining.lookupOreRock
import org.apollo.net.message.Message import org.apollo.net.message.Message
import java.util.Optional import java.util.Objects
class MiningTarget(val objectId: Int, val position: Position, val ore: Ore) { on { ObjectActionMessage::class }
.where { option == Actions.MINING }
fun getObject(world: World): Optional<GameObject> { .then { player ->
val region = world.regionRepository.fromPosition(position) Ore.fromRock(id)?.let { ore ->
return region.findObject(position, objectId) MiningAction.start(this, player, ore)
}
} }
fun isSuccessful(mob: Player): Boolean { on { ObjectActionMessage::class }
val offset = if (ore.chanceOffset) 1 else 0 .where { option == Actions.PROSPECTING }
val percent = (ore.chance * mob.mining.current + offset) * 100 .then { player ->
val ore = Ore.fromRock(id)
return rand(100) < percent if (ore != null) {
ProspectingAction.start(this, player, ore)
} else if (Ore.fromExpiredRock(id) != null) {
ExpiredProspectingAction.start(this, player)
} }
} }
class MiningAction( class MiningAction(
@@ -41,27 +42,22 @@ class MiningAction(
) : AsyncDistancedAction<Player>(PULSES, true, player, target.position, ORE_SIZE) { ) : AsyncDistancedAction<Player>(PULSES, true, player, target.position, ORE_SIZE) {
companion object { companion object {
private val PULSES = 0 private const val PULSES = 0
private val ORE_SIZE = 1 private const val ORE_SIZE = 1
fun pickaxeFor(player: Player): Pickaxe? {
return PICKAXES
.filter { it.level <= player.mining.current }
.filter { player.equipment.contains(it.id) || player.inventory.contains(it.id) }
.sortedByDescending { it.level }
.firstOrNull()
}
/** /**
* Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it. * Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it.
*/ */
fun start(message: ObjectActionMessage, player: Player, ore: Ore) { fun start(message: ObjectActionMessage, player: Player, ore: Ore) {
val pickaxe = pickaxeFor(player) val pickaxe = Pickaxe.bestFor(player)
if (pickaxe != null) {
val action = MiningAction(player, pickaxe, MiningTarget(message.id, message.position, ore)) if (pickaxe == null) {
player.startAction(action)
} else {
player.sendMessage("You do not have a pickaxe for which you have the level to use.") player.sendMessage("You do not have a pickaxe for which you have the level to use.")
} else {
val target = MiningTarget(message.id, message.position, ore)
val action = MiningAction(player, pickaxe, target)
player.startAction(action)
} }
message.terminate() message.terminate()
@@ -83,7 +79,7 @@ class MiningAction(
wait(tool.pulses) wait(tool.pulses)
val obj = target.getObject(mob.world) val obj = target.getObject(mob.world)
if (!obj.isPresent) { if (obj == null) {
stop() stop()
} }
@@ -94,84 +90,45 @@ class MiningAction(
} }
if (mob.inventory.add(target.ore.id)) { if (mob.inventory.add(target.ore.id)) {
val oreName = Definitions.item(target.ore.id)?.name?.toLowerCase() val oreName = Definitions.item(target.ore.id)!!.name.toLowerCase()
mob.sendMessage("You manage to mine some $oreName") mob.sendMessage("You manage to mine some $oreName")
mob.mining.experience += target.ore.exp mob.mining.experience += target.ore.exp
mob.world.expireObject(obj.get(), target.ore.objects[target.objectId]!!, target.ore.respawn) mob.world.expireObject(obj!!, target.ore.objects[target.objectId]!!, target.ore.respawn)
stop() stop()
} }
} }
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MiningAction
return mob == other.mob && target == other.target
} }
class ExpiredProspectingAction( override fun hashCode(): Int = Objects.hash(mob, target)
mob: Player,
position: Position
) : DistancedAction<Player>(PROSPECT_PULSES, true, mob, position, ORE_SIZE) {
companion object {
private val PROSPECT_PULSES = 0
private val ORE_SIZE = 1
} }
override fun executeAction() { data class MiningTarget(val objectId: Int, val position: Position, val ore: Ore) {
mob.sendMessage("There is currently no ore available in this rock.")
stop() fun getObject(world: World): GameObject? {
val region = world.regionRepository.fromPosition(position)
return region.findObject(position, objectId).orElse(null)
}
fun isSuccessful(mob: Player): Boolean {
val offset = if (ore.chanceOffset) 1 else 0
val percent = (ore.chance * mob.mining.current + offset) * 100
return rand(100) < percent
} }
} }
class ProspectingAction( private object Actions {
player: Player, const val MINING = 1
position: Position, const val PROSPECTING = 2
private val ore: Ore
) : AsyncDistancedAction<Player>(PROSPECT_PULSES, true, player, position, ORE_SIZE) {
companion object {
private val PROSPECT_PULSES = 3
private val ORE_SIZE = 1
/**
* Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it.
*/
fun start(message: ObjectActionMessage, player: Player, ore: Ore) {
val action = ProspectingAction(player, message.position, ore)
player.startAction(action)
message.terminate()
}
}
override fun action(): ActionBlock = {
mob.sendMessage("You examine the rock for ores...")
mob.turnTo(position)
wait()
val oreName = Definitions.item(ore.id)?.name?.toLowerCase()
mob.sendMessage("This rock contains $oreName")
stop()
}
}
on { ObjectActionMessage::class }
.where { option == 1 }
.then {
val ore = lookupOreRock(id)
if (ore != null) {
MiningAction.start(this, it, ore)
}
}
on { ObjectActionMessage::class }
.where { option == 2 }
.then {
val ore = lookupOreRock(id)
if (ore != null) { // TODO send expired action if rock is expired
ProspectingAction.start(this, it, ore)
}
} }
+35 -36
View File
@@ -1,13 +1,46 @@
package org.apollo.game.plugin.skills.mining package org.apollo.game.plugin.skills.mining
import com.google.common.collect.Maps.asMap
/* /*
Thanks to Mikey` <http://www.rune-server.org/members/mikey%60/> for helping Thanks to Mikey` <http://www.rune-server.org/members/mikey%60/> for helping
to find some of the item/object IDs, minimum levels and experiences. to find some of the item/object IDs, minimum levels and experiences.
Thanks to Clifton <http://www.rune-server.org/members/clifton/> for helping Thanks to Clifton <http://www.rune-server.org/members/clifton/> for helping
to find some of the expired object IDs. to find some of the expired object IDs.
*/ */
/**
* Chance values thanks to: http://runescape.wikia.com/wiki/Talk:Mining#Mining_success_rate_formula
* Respawn times and xp thanks to: http://oldschoolrunescape.wikia.com/wiki/
*/
enum class Ore(
val objects: Map<Int, Int>,
val id: Int,
val level: Int,
val exp: Double,
val respawn: Int,
val chance: Double,
val chanceOffset: Boolean = false
) {
CLAY(CLAY_OBJECTS, id = 434, level = 1, exp = 5.0, respawn = 1, chance = 0.0085, chanceOffset = true),
COPPER(COPPER_OBJECTS, id = 436, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = true),
TIN(TIN_OBJECTS, id = 438, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = true),
IRON(IRON_OBJECTS, id = 440, level = 15, exp = 35.0, respawn = 9, chance = 0.0085, chanceOffset = true),
COAL(COAL_OBJECTS, id = 453, level = 30, exp = 50.0, respawn = 50, chance = 0.004),
GOLD(GOLD_OBJECTS, id = 444, level = 40, exp = 65.0, respawn = 100, chance = 0.003),
SILVER(SILVER_OBJECTS, id = 442, level = 20, exp = 40.0, respawn = 100, chance = 0.0085),
MITHRIL(MITHRIL_OBJECTS, id = 447, level = 55, exp = 80.0, respawn = 200, chance = 0.002),
ADAMANT(ADAMANT_OBJECTS, id = 449, level = 70, exp = 95.0, respawn = 800, chance = 0.001),
RUNITE(RUNITE_OBJECTS, id = 451, level = 85, exp = 125.0, respawn = 1_200, chance = 0.0008);
companion object {
private val ORE_ROCKS = Ore.values().flatMap { ore -> ore.objects.map { Pair(it.key, ore) } }.toMap()
private val EXPIRED_ORE = Ore.values().flatMap { ore -> ore.objects.map { Pair(it.value, ore) } }.toMap()
fun fromRock(id: Int): Ore? = ORE_ROCKS[id]
fun fromExpiredRock(id: Int): Ore? = EXPIRED_ORE[id]
}
}
// Maps from regular rock id to expired rock id.
val CLAY_OBJECTS = mapOf( val CLAY_OBJECTS = mapOf(
2108 to 450, 2108 to 450,
@@ -119,37 +152,3 @@ val RUNITE_OBJECTS = mapOf(
14860 to 14833, 14860 to 14833,
14861 to 14834 14861 to 14834
) )
/**
* Chance values thanks to: http://runescape.wikia.com/wiki/Talk:Mining#Mining_success_rate_formula
* Respawn times and xp thanks to: http://oldschoolrunescape.wikia.com/wiki/
*/
enum class Ore(val id: Int, val objects: Map<Int, Int>, val level: Int, val exp: Double, val respawn: Int, val chance: Double, val chanceOffset: Boolean) {
CLAY(434, CLAY_OBJECTS, 1, 5.0, 1, 0.0085, true),
COPPER(436, COPPER_OBJECTS, 1, 17.5, 4, 0.0085, true),
TIN(438, TIN_OBJECTS, 1, 17.5, 4, 0.0085, true),
IRON(440, IRON_OBJECTS, 15, 35.0, 9, 0.0085, true),
COAL(453, COAL_OBJECTS, 30, 50.0, 50, 0.004, false),
GOLD(444, GOLD_OBJECTS, 40, 65.0, 100, 0.003, false),
SILVER(442, SILVER_OBJECTS, 20, 40.0, 100, 0.0085, false),
MITHRIL(447, MITHRIL_OBJECTS, 55, 80.0, 200, 0.002, false),
ADAMANT(449, ADAMANT_OBJECTS, 70, 95.0, 800, 0.001, false),
RUNITE(451, RUNITE_OBJECTS, 85, 125.0, 1200, 0.0008, false)
}
val ORES = enumValues<Ore>()
fun lookupOre(id: Int): Ore? = ORES.find { it.id == id }
fun lookupOreRock(id: Int): Ore? {
for (ore in enumValues<Ore>()) {
for (rock in ore.objects) {
if (rock.key == id) {
return ore
}
}
}
return null
}
+22 -18
View File
@@ -1,23 +1,27 @@
package org.apollo.game.plugin.skills.mining package org.apollo.game.plugin.skills.mining
import org.apollo.game.model.Animation; import org.apollo.game.model.Animation
import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.api.mining
enum class Pickaxe(val id: Int, val level: Int, val animation: Animation, val pulses: Int) { enum class Pickaxe(val id: Int, val level: Int, animation: Int, val pulses: Int) {
RUNE(1275, 41, Animation(624), 3), BRONZE(id = 1265, level = 1, animation = 625, pulses = 8),
ADAMANT(1271, 31, Animation(628), 4), ITRON(id = 1267, level = 1, animation = 626, pulses = 7),
MITHRIL(1273, 21, Animation(629), 5), STEEL(id = 1269, level = 1, animation = 627, pulses = 6),
STEEL(1269, 1, Animation(627), 6), MITHRIL(id = 1273, level = 21, animation = 629, pulses = 5),
ITRON(1267, 1, Animation(626), 7), ADAMANT(id = 1271, level = 31, animation = 628, pulses = 4),
BRONZE(1265, 1, Animation(625), 8) RUNE(id = 1275, level = 41, animation = 624, pulses = 3);
val animation = Animation(animation)
companion object {
private val PICKAXES = Pickaxe.values().sortedByDescending { it.level }
fun bestFor(player: Player): Pickaxe? {
return PICKAXES.asSequence()
.filter { it.level <= player.mining.current }
.filter { player.equipment.contains(it.id) || player.inventory.contains(it.id) }
.firstOrNull()
}
} }
val PICKAXES = Pickaxe.values()
fun getPickaxes(): Array<Pickaxe> {
return PICKAXES
} }
fun lookupPickaxe(id: Int): Pickaxe? = PICKAXES.find { it.id == id }
@@ -0,0 +1,92 @@
import org.apollo.game.action.ActionBlock
import org.apollo.game.action.AsyncDistancedAction
import org.apollo.game.action.DistancedAction
import org.apollo.game.message.impl.ObjectActionMessage
import org.apollo.game.model.Position
import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.api.Definitions
import org.apollo.game.plugin.skills.mining.Ore
import org.apollo.net.message.Message
import java.util.Objects
class ProspectingAction(
player: Player,
position: Position,
private val ore: Ore
) : AsyncDistancedAction<Player>(DELAY, true, player, position, ORE_SIZE) {
companion object {
private const val DELAY = 3
private const val ORE_SIZE = 1
/**
* Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it.
*/
fun start(message: ObjectActionMessage, player: Player, ore: Ore) {
val action = ProspectingAction(player, message.position, ore)
player.startAction(action)
message.terminate()
}
}
override fun action(): ActionBlock = {
mob.sendMessage("You examine the rock for ores...")
mob.turnTo(position)
wait()
val oreName = Definitions.item(ore.id)?.name?.toLowerCase()
mob.sendMessage("This rock contains $oreName.")
stop()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ProspectingAction
return mob == other.mob && position == other.position && ore == other.ore
}
override fun hashCode(): Int = Objects.hash(mob, position, ore)
}
class ExpiredProspectingAction(
mob: Player,
position: Position
) : DistancedAction<Player>(DELAY, true, mob, position, ORE_SIZE) {
companion object {
private const val DELAY = 0
private const val ORE_SIZE = 1
/**
* Starts a [ExpiredProspectingAction] for the specified [Player], terminating the [Message] that triggered it.
*/
fun start(message: ObjectActionMessage, player: Player) {
val action = ExpiredProspectingAction(player, message.position)
player.startAction(action)
message.terminate()
}
}
override fun executeAction() {
mob.sendMessage("There is currently no ore available in this rock.")
stop()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ExpiredProspectingAction
return mob == other.mob && position == other.position
}
override fun hashCode(): Int = Objects.hash(mob, position)
}
+22 -17
View File
@@ -1,23 +1,28 @@
package org.apollo.game.plugin.skills.woodcutting package org.apollo.game.plugin.skills.woodcutting
import org.apollo.game.model.Animation; import org.apollo.game.model.Animation
import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.api.woodcutting
//Animation IDs thanks to Deadly A G S at rune-server.ee enum class Axe(val id: Int, val level: Int, animation: Int, val pulses: Int) {
enum class Axe(val id: Int, val level: Int, val animation: Animation, val pulses: Int) { BRONZE(id = 1351, level = 1, animation = 879, pulses = 8),
RUNE(1359, 41, Animation(867), 3), IRON(id = 1349, level = 1, animation = 877, pulses = 7),
ADAMANT(1357, 31, Animation(869), 4), STEEL(id = 1353, level = 6, animation = 875, pulses = 6),
MITHRIL(1355, 21, Animation(871), 5), BLACK(id = 1361, level = 11, animation = 873, pulses = 6),
BLACK(1361, 11, Animation(873), 6), MITHRIL(id = 1355, level = 21, animation = 871, pulses = 5),
STEEL(1353, 6, Animation(875), 6), ADAMANT(id = 1357, level = 31, animation = 869, pulses = 4),
IRON(1349, 1, Animation(877), 7), RUNE(id = 1359, level = 41, animation = 867, pulses = 3);
BRONZE(1351, 1, Animation(879), 8);
val animation = Animation(animation)
companion object { companion object {
private val AXES = Axe.values() private val AXES = Axe.values().sortedByDescending { it.level }
fun getAxes(): Array<Axe> {
return AXES
}
}
}
fun bestFor(player: Player): Axe? {
return AXES.asSequence()
.filter { it.level <= player.woodcutting.current }
.filter { player.equipment.contains(it.id) || player.inventory.contains(it.id) }
.firstOrNull()
}
}
}
+34 -61
View File
@@ -1,67 +1,27 @@
package org.apollo.game.plugin.skills.woodcutting package org.apollo.game.plugin.skills.woodcutting
private val NORMAL_STUMP = 1342 /*
private val ACHEY_STUMP = 3371 * Values thanks to: http://oldschoolrunescape.wikia.com/wiki/Woodcutting
private val OAK_STUMP = 1342 * https://twitter.com/JagexKieren/status/713409506273787904
private val WILLOW_STUMP = 1342
private val TEAK_STUMP = 1342
private val MAPLE_STUMP = 1342
private val MAHOGANY_STUMP = 1342
private val YEW_STUMP = 1342
private val MAGIC_STUMP = 1324
private val NORMAL_OBJECTS = hashSetOf(
1276, 1277, 1278, 1279, 1280, 1282, 1283, 1284, 1285, 1285, 1286, 1289, 1290, 1291, 1315,
1316, 1318, 1330, 1331, 1332, 1365, 1383, 1384, 2409, 3033, 3034, 3035, 3036, 3881, 3882,
3883, 5902, 5903, 5904, 10041
)
private val ACHEY_OBJECTS = hashSetOf(
2023
)
private val OAK_OBJECTS = hashSetOf(
1281, 3037
)
private val WILLOW_OBJECTS = hashSetOf(
5551, 5552, 5553
)
private val TEAK_OBJECTS = hashSetOf(
9036
)
private val MAPLE_OBJECTS = hashSetOf(
1307, 4674
)
private val MAHOGANY_OBJECTS = hashSetOf(
9034
)
private val YEW_OBJECTS = hashSetOf(
1309
)
private val MAGIC_OBJECTS = hashSetOf(
1292, 1306
)
/**
* values thanks to: http://oldschoolrunescape.wikia.com/wiki/Woodcutting
*/ */
enum class Tree(val id: Int, val objects: HashSet<Int>, val stump: Int, val level: Int, val exp: Double, val chance: Double) {
NORMAL(1511, NORMAL_OBJECTS, NORMAL_STUMP, 1, 25.0, 100.0), enum class Tree(
ACHEY(2862, ACHEY_OBJECTS, ACHEY_STUMP, 1, 25.0, 100.0), val objects: Set<Int>,
OAK(1521, OAK_OBJECTS, OAK_STUMP, 15, 37.5, 0.125), val id: Int,
WILLOW(1519, WILLOW_OBJECTS, WILLOW_STUMP, 30, 67.5, 0.125), val stump: Int,
TEAK(6333, TEAK_OBJECTS, TEAK_STUMP, 35, 85.0, 0.125), val level: Int,
MAPLE(1517, MAPLE_OBJECTS, MAPLE_STUMP, 45, 100.0, 0.125), val exp: Double,
MAHOGANY(6332, MAHOGANY_OBJECTS, MAHOGANY_STUMP, 50, 125.0, 0.125), val chance: Double
YEW(1515, YEW_OBJECTS, YEW_STUMP, 60, 175.0, 0.125), ) {
MAGIC(1513, MAGIC_OBJECTS, MAGIC_STUMP, 75, 250.0, 0.125); NORMAL(NORMAL_OBJECTS, id = 1511, stump = 1342, level = 1, exp = 25.0, chance = 100.0),
ACHEY(ACHEY_OBJECTS, id = 2862, stump = 3371, level = 1, exp = 25.0, chance = 100.0),
OAK(OAK_OBJECTS, id = 1521, stump = 1342, level = 15, exp = 37.5, chance = 12.5),
WILLOW(WILLOW_OBJECTS, id = 1519, stump = 1342, level = 30, exp = 67.5, chance = 12.5),
TEAK(TEAK_OBJECTS, id = 6333, stump = 1342, level = 35, exp = 85.0, chance = 12.5),
MAPLE(MAPLE_OBJECTS, id = 1517, stump = 1342, level = 45, exp = 100.0, chance = 12.5),
MAHOGANY(MAHOGANY_OBJECTS, id = 6332, stump = 1342, level = 50, exp = 125.0, chance = 12.5),
YEW(YEW_OBJECTS, id = 1515, stump = 1342, level = 60, exp = 175.0, chance = 12.5),
MAGIC(MAGIC_OBJECTS, id = 1513, stump = 1324, level = 75, exp = 250.0, chance = 12.5);
companion object { companion object {
private val TREES = Tree.values().flatMap { tree -> tree.objects.map { Pair(it, tree) } }.toMap() private val TREES = Tree.values().flatMap { tree -> tree.objects.map { Pair(it, tree) } }.toMap()
@@ -69,4 +29,17 @@ enum class Tree(val id: Int, val objects: HashSet<Int>, val stump: Int, val leve
} }
} }
private val NORMAL_OBJECTS = hashSetOf(
1276, 1277, 1278, 1279, 1280, 1282, 1283, 1284, 1285, 1285, 1286, 1289, 1290, 1291, 1315,
1316, 1318, 1330, 1331, 1332, 1365, 1383, 1384, 2409, 3033, 3034, 3035, 3036, 3881, 3882,
3883, 5902, 5903, 5904, 10041
)
private val ACHEY_OBJECTS = hashSetOf(2023)
private val OAK_OBJECTS = hashSetOf(1281, 3037)
private val WILLOW_OBJECTS = hashSetOf(5551, 5552, 5553)
private val TEAK_OBJECTS = hashSetOf(9036)
private val MAPLE_OBJECTS = hashSetOf(1307, 4674)
private val MAHOGANY_OBJECTS = hashSetOf(9034)
private val YEW_OBJECTS = hashSetOf(1309)
private val MAGIC_OBJECTS = hashSetOf(1292, 1306)
@@ -1,3 +1,4 @@
import org.apollo.game.GameConstants import org.apollo.game.GameConstants
import org.apollo.game.action.ActionBlock import org.apollo.game.action.ActionBlock
import org.apollo.game.action.AsyncDistancedAction import org.apollo.game.action.AsyncDistancedAction
@@ -13,17 +14,24 @@ import org.apollo.game.plugin.api.rand
import org.apollo.game.plugin.api.woodcutting import org.apollo.game.plugin.api.woodcutting
import org.apollo.game.plugin.skills.woodcutting.Axe import org.apollo.game.plugin.skills.woodcutting.Axe
import org.apollo.game.plugin.skills.woodcutting.Tree import org.apollo.game.plugin.skills.woodcutting.Tree
import java.util.Optional
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
// TODO Accurate chopping rates, e.g. https://twitter.com/JagexKieren/status/713403124464107520
on { ObjectActionMessage::class }
.where { option == 1 }
.then { player ->
Tree.lookup(id)?.let { WoodcuttingAction.start(this, player, it) }
}
class WoodcuttingTarget(private val objectId: Int, val position: Position, val tree: Tree) { class WoodcuttingTarget(private val objectId: Int, val position: Position, val tree: Tree) {
/** /**
* Get the tree object in the world * Get the tree object in the world
*/ */
fun getObject(world: World): Optional<GameObject> { fun getObject(world: World): GameObject? {
val region = world.regionRepository.fromPosition(position) val region = world.regionRepository.fromPosition(position)
return region.findObject(position, objectId) return region.findObject(position, objectId).orElse(null)
} }
/** /**
@@ -44,23 +52,12 @@ class WoodcuttingAction(
private const val TREE_SIZE = 2 private const val TREE_SIZE = 2
private const val MINIMUM_RESPAWN_TIME = 30L // In seconds private const val MINIMUM_RESPAWN_TIME = 30L // In seconds
/**
* Find the highest level axe the player has
*/
private fun axeFor(player: Player): Axe? {
return Axe.getAxes()
.filter { it.level <= player.woodcutting.current }
.filter { player.equipment.contains(it.id) || player.inventory.contains(it.id) }
.sortedByDescending { it.level }
.firstOrNull()
}
/** /**
* Starts a [WoodcuttingAction] for the specified [Player], terminating the [ObjectActionMessage] that triggered * Starts a [WoodcuttingAction] for the specified [Player], terminating the [ObjectActionMessage] that triggered
* it. * it.
*/ */
fun start(message: ObjectActionMessage, player: Player, wood: Tree) { fun start(message: ObjectActionMessage, player: Player, wood: Tree) {
val axe = axeFor(player) val axe = Axe.bestFor(player)
if (axe != null) { if (axe != null) {
if (player.inventory.freeSlots() == 0) { if (player.inventory.freeSlots() == 0) {
player.inventory.forceCapacityExceeded() player.inventory.forceCapacityExceeded()
@@ -94,12 +91,12 @@ class WoodcuttingAction(
// Check that the object exists in the world // Check that the object exists in the world
val obj = target.getObject(mob.world) val obj = target.getObject(mob.world)
if (!obj.isPresent) { if (obj == null) {
stop() stop()
} }
if (mob.inventory.add(target.tree.id)) { if (mob.inventory.add(target.tree.id)) {
val logName = Definitions.item(target.tree.id)?.name?.toLowerCase() val logName = Definitions.item(target.tree.id)!!.name.toLowerCase()
mob.sendMessage("You managed to cut some $logName.") mob.sendMessage("You managed to cut some $logName.")
mob.woodcutting.experience += target.tree.exp mob.woodcutting.experience += target.tree.exp
} }
@@ -107,18 +104,11 @@ class WoodcuttingAction(
if (target.isCutDown()) { if (target.isCutDown()) {
// respawn time: http://runescape.wikia.com/wiki/Trees // respawn time: http://runescape.wikia.com/wiki/Trees
val respawn = TimeUnit.SECONDS.toMillis(MINIMUM_RESPAWN_TIME + rand(150)) / GameConstants.PULSE_DELAY val respawn = TimeUnit.SECONDS.toMillis(MINIMUM_RESPAWN_TIME + rand(150)) / GameConstants.PULSE_DELAY
mob.world.expireObject(obj.get(), target.tree.stump, respawn.toInt())
mob.world.expireObject(obj!!, target.tree.stump, respawn.toInt())
stop() stop()
} }
} }
} }
}
on { ObjectActionMessage::class }
.where { option == 1 }
.then {
val tree = Tree.lookup(id)
if (tree != null) {
WoodcuttingAction.start(this, it, tree)
}
} }