From 01f2bdda58d7a3e4e7c47d63b033382f85d9850a Mon Sep 17 00:00:00 2001 From: Trevor Flynn Date: Sat, 23 Sep 2017 18:00:38 -0700 Subject: [PATCH] Port Fishing plugin to kotlin (#347) This also fixes a bunch of bugs. --- game/plugin/skills/fishing/build.gradle | 12 ++ game/plugin/skills/fishing/src/fishing.kt | 170 ++++++++++++++++ .../skills/fishing/src/fishing.plugin.kts | 137 +++++++++++++ game/plugin/skills/fishing/src/spots.kts | 187 ++++++++++++++++++ 4 files changed, 506 insertions(+) create mode 100644 game/plugin/skills/fishing/build.gradle create mode 100644 game/plugin/skills/fishing/src/fishing.kt create mode 100644 game/plugin/skills/fishing/src/fishing.plugin.kts create mode 100644 game/plugin/skills/fishing/src/spots.kts diff --git a/game/plugin/skills/fishing/build.gradle b/game/plugin/skills/fishing/build.gradle new file mode 100644 index 00000000..4e13f439 --- /dev/null +++ b/game/plugin/skills/fishing/build.gradle @@ -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" + ] +} diff --git a/game/plugin/skills/fishing/src/fishing.kt b/game/plugin/skills/fishing/src/fishing.kt new file mode 100644 index 00000000..e045a445 --- /dev/null +++ b/game/plugin/skills/fishing/src/fishing.kt @@ -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 + } + } + + } + + } + +} diff --git a/game/plugin/skills/fishing/src/fishing.plugin.kts b/game/plugin/skills/fishing/src/fishing.plugin.kts new file mode 100644 index 00000000..cc53c99c --- /dev/null +++ b/game/plugin/skills/fishing/src/fishing.plugin.kts @@ -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(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() + } diff --git a/game/plugin/skills/fishing/src/spots.kts b/game/plugin/skills/fishing/src/spots.kts new file mode 100644 index 00000000..83080e29 --- /dev/null +++ b/game/plugin/skills/fishing/src/spots.kts @@ -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)) +} \ No newline at end of file