Port Fishing plugin to kotlin (#347)

This also fixes a bunch of bugs.
This commit is contained in:
Trevor Flynn
2017-09-23 18:00:38 -07:00
committed by Major
parent 64ee653423
commit 01f2bdda58
4 changed files with 506 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
plugin {
name = "fishing_skill"
packageName = "org.apollo.game.plugin.skills.fishing"
authors = [
"Linux",
"Major",
"tlf30"
]
dependencies = [
"util:lookup", "entity:spawn", "api"
]
}
+170
View File
@@ -0,0 +1,170 @@
package org.apollo.game.plugin.skills.fishing
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.Animation
import org.apollo.game.plugin.skills.fishing.Fish.*
import org.apollo.game.plugin.skills.fishing.FishingTool.*
import java.util.Random
/**
* A fish that can be gathered using the fishing skill.
*/
enum class Fish(val id: Int, val level: Int, val experience: Double) {
SHRIMP(317, 1, 10.0),
SARDINE(327, 5, 20.0),
MACKEREL(353, 16, 20.0),
HERRING(345, 10, 30.0),
ANCHOVY(321, 15, 40.0),
TROUT(335, 20, 50.0),
COD(341, 23, 45.0),
PIKE(349, 25, 60.0),
SALMON(331, 30, 70.0),
TUNA(359, 35, 80.0),
LOBSTER(377, 40, 90.0),
BASS(363, 46, 100.0),
SWORDFISH(371, 50, 100.0),
SHARK(383, 76, 110.0);
/**
* The name of this fish, formatted so it can be inserted into a message.
*/
val formattedName = ItemDefinition.lookup(id).name.toLowerCase()
}
/**
* 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?) {
LOBSTER_CAGE(301, 619, "You attempt to catch a lobster..."),
SMALL_NET(303, 620, "You cast out your net..."),
BIG_NET(305, 620, "You cast out your net..."),
HARPOON(311, 618, "You start harpooning fish..."),
FISHING_ROD(307, 622, "You attempt to catch a fish...", 313, "feathers"),
FLY_FISHING_ROD(309, 622, "You attempt to catch a fish...", 314, "fishing bait");
@Suppress("unused") // IntelliJ bug, doesn't detect that this constructor is used
constructor(id: Int, animation: Int, message: String) : this(id, animation, message, -1, null)
/**
* The [Animation] played when fishing with this tool.
*/
val animation: Animation = Animation(animation)
/**
* The name of this tool, formatted so it can be inserted into a message.
*/
val formattedName = ItemDefinition.lookup(id).name.toLowerCase()
}
/**
* A spot that can be fished from.
*/
enum class FishingSpot(val npc: Int, private val first: Option, private val second: Option) {
ROD(309, Option.of(FLY_FISHING_ROD, TROUT, SALMON), Option.of(FISHING_ROD, PIKE)),
CAGE_HARPOON(312, Option.of(LOBSTER_CAGE, LOBSTER), Option.of(HARPOON, TUNA, SWORDFISH)),
NET_HARPOON(313, Option.of(BIG_NET, MACKEREL, COD), Option.of(HARPOON, BASS, SHARK)),
NET_ROD(316, Option.of(SMALL_NET, SHRIMP, ANCHOVY), Option.of(FISHING_ROD, SARDINE, HERRING));
companion object {
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.
*/
fun lookup(id: Int): FishingSpot? = FISHING_SPOTS[id]
}
/**
* Returns the [FishingSpot.Option] associated with the specified action id.
*/
fun option(action: Int): Option {
return when (action) {
1 -> first
3 -> second
else -> throw UnsupportedOperationException("Unexpected fishing spot option $action.")
}
}
/**
* An option at a [FishingSpot] (e.g. either "rod fishing" or "net fishing").
*/
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
*/
abstract val tool: FishingTool
/**
* The minimum level required to obtain fish.
*/
abstract val level: Int
/**
* Samples a [Fish], randomly (with weighting) returning one (that can be fished by the player).
*
* @param level The fishing level of the player.
*/
abstract fun sample(level: Int): Fish
/**
* A [FishingSpot] [Option] that can only provide a single type of fish.
*/
private data class Single(override val tool: FishingTool, val primary: Fish) : Option() {
override val level = primary.level
override fun sample(level: Int): Fish = primary
}
/**
* 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() {
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 fun sample(level: Int): Fish {
if (secondary.level > level) {
return primary
}
return when {
random.nextInt(100) < WEIGHTING -> primary
else -> secondary
}
}
}
}
}
@@ -0,0 +1,137 @@
import Fishing_plugin.FishingAction
import org.apollo.game.action.ActionBlock
import org.apollo.game.action.AsyncDistancedAction
import org.apollo.game.message.impl.NpcActionMessage
import org.apollo.game.model.Position
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.skills.fishing.FishingSpot
import org.apollo.game.plugin.skills.fishing.FishingTool
import org.apollo.game.plugins.api.fishing
import org.apollo.game.plugins.api.skills
import java.util.Objects
import java.util.Random
// TODO: moving fishing spots, seaweed and caskets, evil bob
class FishingAction(player: Player, position: Position, val option: FishingSpot.Option) :
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.
*/
private val tool = option.tool
override fun action(): ActionBlock = {
if (!verify()) {
stop()
}
mob.turnTo(position)
mob.sendMessage(tool.message)
while (isRunning) {
mob.playAnimation(tool.animation)
wait(FISHING_DELAY)
val level = mob.skills.fishing.currentLevel
val fish = option.sample(level)
if (successfulCatch(level, fish.level)) {
if (tool.bait != -1) {
mob.inventory.remove(tool.bait)
}
mob.inventory.add(fish.id)
mob.sendMessage("You catch a ${fish.formattedName}.")
mob.skills.addExperience(Skill.FISHING, fish.experience)
if (mob.inventory.freeSlots() == 0) {
mob.inventory.forceCapacityExceeded()
mob.stopAnimation()
stop()
} else if (!hasBait(mob, tool.bait)) {
mob.sendMessage("You need more ${tool.baitName} to fish at this spot.")
mob.stopAnimation()
stop()
}
}
}
}
/**
* Verifies that the player can gather fish from the [FishingSpot] they clicked.
*/
private fun verify(): Boolean {
if (mob.skills.fishing.currentLevel < option.level) {
mob.sendMessage("You need a fishing level of ${option.level} to fish at this spot.")
return false
} else if (!hasTool(mob, tool)) {
mob.sendMessage("You need a ${tool.formattedName} to fish at this spot.")
return false
} else if (!hasBait(mob, tool.bait)) {
mob.sendMessage("You need some ${tool.baitName} to fish at this spot.")
return false
} else if (mob.inventory.freeSlots() == 0) {
mob.inventory.forceCapacityExceeded()
return false
}
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FishingAction
return option == other.option && position == other.position && mob == other.mob
}
override fun hashCode(): Int = Objects.hash(option, position, mob)
}
/**
* Intercepts the [NpcActionMessage] and starts a [FishingAction] if the npc
*/
on { NpcActionMessage::class }
.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))
terminate()
}
+187
View File
@@ -0,0 +1,187 @@
import org.apollo.game.model.Direction
import org.apollo.game.model.Position
import org.apollo.game.plugin.skills.fishing.FishingSpot
import org.apollo.game.plugin.skills.fishing.FishingSpot.CAGE_HARPOON
import org.apollo.game.plugin.skills.fishing.FishingSpot.NET_HARPOON
import org.apollo.game.plugin.skills.fishing.FishingSpot.NET_ROD
import org.apollo.game.plugin.skills.fishing.FishingSpot.ROD
// Al-Kharid
register(NET_ROD, x = 3267, y = 3148)
register(NET_ROD, x = 3268, y = 3147)
register(NET_ROD, x = 3277, y = 3139)
register(CAGE_HARPOON, x = 3350, y = 3817)
register(CAGE_HARPOON, x = 3347, y = 3814)
register(CAGE_HARPOON, x = 3363, y = 3816)
register(CAGE_HARPOON, x = 3368, y = 3811)
// Ardougne
register(ROD, x = 2561, y = 3374)
register(ROD, x = 2562, y = 3374)
register(ROD, x = 2568, y = 3365)
// Bandit camp
register(NET_ROD, x = 3047, y = 3703)
register(NET_ROD, x = 3045, y = 3702)
// Baxtorian falls
register(ROD, x = 2527, y = 3412)
register(ROD, x = 2530, y = 3412)
register(ROD, x = 2533, y = 3410)
// Burgh de Rott
register(NET_HARPOON, x = 3497, y = 3175)
register(NET_HARPOON, x = 3496, y = 3178)
register(NET_HARPOON, x = 3499, y = 3178)
register(NET_HARPOON, x = 3489, y = 3184)
register(NET_HARPOON, x = 3496, y = 3176)
register(NET_HARPOON, x = 3486, y = 3184)
register(NET_HARPOON, x = 3479, y = 3189)
register(NET_HARPOON, x = 3476, y = 3191)
register(NET_HARPOON, x = 3472, y = 3196)
register(NET_HARPOON, x = 3496, y = 3180)
register(NET_HARPOON, x = 3512, y = 3178)
register(NET_HARPOON, x = 3515, y = 3180)
register(NET_HARPOON, x = 3518, y = 3177)
register(NET_HARPOON, x = 3528, y = 3172)
register(NET_HARPOON, x = 3531, y = 3169)
register(NET_HARPOON, x = 3531, y = 3172)
register(NET_HARPOON, x = 3531, y = 3167)
// Camelot
register(ROD, x = 2726, y = 3524)
register(ROD, x = 2727, y = 3524)
// Castle wars
register(ROD, x = 2461, y = 3151)
register(ROD, x = 2461, y = 3150)
register(ROD, x = 2462, y = 3145)
register(ROD, x = 2472, y = 3156)
// Catherby 1
register(NET_ROD, x = 2838, y = 3431)
register(CAGE_HARPOON, x = 2837, y = 3431)
register(CAGE_HARPOON, x = 2836, y = 3431)
register(NET_ROD, x = 2846, y = 3429)
register(NET_ROD, x = 2844, y = 3429)
register(CAGE_HARPOON, x = 2845, y = 3429)
register(NET_HARPOON, x = 2853, y = 3423)
register(NET_HARPOON, x = 2855, y = 3423)
register(NET_HARPOON, x = 2859, y = 3426)
// Draynor village
register(NET_ROD, x = 3085, y = 3230)
register(NET_ROD, x = 3085, y = 3231)
register(NET_ROD, x = 3086, y = 3227)
// Elf camp
register(ROD, x = 2210, y = 3243)
register(ROD, x = 2216, y = 3236)
register(ROD, x = 2222, y = 3241)
// Entrana
register(NET_ROD, x = 2843, y = 3359)
register(NET_ROD, x = 2842, y = 3359)
register(NET_ROD, x = 2847, y = 3361)
register(NET_ROD, x = 2848, y = 3361)
register(NET_ROD, x = 2840, y = 3356)
register(NET_ROD, x = 2845, y = 3356)
register(NET_ROD, x = 2875, y = 3342)
register(NET_ROD, x = 2876, y = 3342)
register(NET_ROD, x = 2877, y = 3342)
// Fishing guild
register(CAGE_HARPOON, x = 2612, y = 3411)
register(CAGE_HARPOON, x = 2607, y = 3410)
register(NET_HARPOON, x = 2612, y = 3414)
register(NET_HARPOON, x = 2612, y = 3415)
register(NET_HARPOON, x = 2609, y = 3416)
register(CAGE_HARPOON, x = 2604, y = 3417)
register(NET_HARPOON, x = 2605, y = 3416)
register(NET_HARPOON, x = 2602, y = 3411)
register(NET_HARPOON, x = 2602, y = 3412)
register(CAGE_HARPOON, x = 2602, y = 3414)
register(NET_HARPOON, x = 2603, y = 3417)
register(NET_HARPOON, x = 2599, y = 3419)
register(NET_HARPOON, x = 2601, y = 3422)
register(NET_HARPOON, x = 2605, y = 3421)
register(CAGE_HARPOON, x = 2602, y = 3426)
register(NET_HARPOON, x = 2604, y = 3426)
register(CAGE_HARPOON, x = 2605, y = 3425)
// Fishing platform
register(NET_ROD, x = 2791, y = 3279)
register(NET_ROD, x = 2795, y = 3279)
register(NET_ROD, x = 2790, y = 3273)
// Grand Tree
register(ROD, x = 2393, y = 3419)
register(ROD, x = 2391, y = 3421)
register(ROD, x = 2389, y = 3423)
register(ROD, x = 2388, y = 3423)
register(ROD, x = 2385, y = 3422)
register(ROD, x = 2384, y = 3419)
register(ROD, x = 2383, y = 3417)
// Gunnarsgrunn
register(ROD, x = 3101, y = 3092)
register(ROD, x = 3103, y = 3092)
// Karamja
register(NET_ROD, x = 2921, y = 3178)
register(CAGE_HARPOON, x = 2923, y = 3179)
register(CAGE_HARPOON, x = 2923, y = 3180)
register(NET_ROD, x = 2924, y = 3181)
register(NET_ROD, x = 2926, y = 3180)
register(CAGE_HARPOON, x = 2926, y = 3179)
// Lumbridge
register(ROD, x = 3239, y = 3244)
register(NET_ROD, x = 3238, y = 3252)
// Miscellenia
register(CAGE_HARPOON, x = 2580, y = 3851)
register(CAGE_HARPOON, x = 2581, y = 3851)
register(CAGE_HARPOON, x = 2582, y = 3851)
register(CAGE_HARPOON, x = 2583, y = 3852)
register(CAGE_HARPOON, x = 2583, y = 3853)
// Rellekka
register(NET_ROD, x = 2633, y = 3691)
register(NET_ROD, x = 2633, y = 3689)
register(CAGE_HARPOON, x = 2639, y = 3698)
register(CAGE_HARPOON, x = 2639, y = 3697)
register(CAGE_HARPOON, x = 2639, y = 3695)
register(NET_HARPOON, x = 2642, y = 3694)
register(NET_HARPOON, x = 2642, y = 3697)
register(NET_HARPOON, x = 2644, y = 3709)
// Rimmington
register(NET_ROD, x = 2990, y = 3169)
register(NET_ROD, x = 2986, y = 3176)
// Shilo Village
register(ROD, x = 2855, y = 2974)
register(ROD, x = 2865, y = 2972)
register(ROD, x = 2860, y = 2972)
register(ROD, x = 2835, y = 2974)
register(ROD, x = 2859, y = 2976)
// Tirannwn
register(ROD, x = 2266, y = 3253)
register(ROD, x = 2265, y = 3258)
register(ROD, x = 2264, y = 3258)
// Tutorial island
register(NET_ROD, x = 3101, y = 3092)
register(NET_ROD, x = 3103, y = 3092)
/**
* Registers the [FishingSpot] at the specified position.
*/
fun register(spot: FishingSpot, x: Int, y: Int, z: Int = 0) {
val position = Position(x, y, z)
Spawns.list.add(Spawn(spot.npc, "", position, Direction.NORTH))
}