diff --git a/content/skills/fletching/build.gradle.kts b/content/skills/fletching/build.gradle.kts new file mode 100644 index 000000000..a5db8fcad --- /dev/null +++ b/content/skills/fletching/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("base-conventions") +} + +dependencies { + implementation(projects.api.pluginCommons) + implementation(projects.api.registry) + implementation(projects.content.skills.utils) +} diff --git a/content/skills/fletching/pack/build.gradle.kts b/content/skills/fletching/pack/build.gradle.kts new file mode 100644 index 000000000..92f804a7e --- /dev/null +++ b/content/skills/fletching/pack/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("base-conventions") +} diff --git a/content/skills/fletching/pack/src/main/kotlin/org/rsmod/content/skills/fletching/pack/FletchingPluginPack.kt b/content/skills/fletching/pack/src/main/kotlin/org/rsmod/content/skills/fletching/pack/FletchingPluginPack.kt new file mode 100644 index 000000000..41a38e0e3 --- /dev/null +++ b/content/skills/fletching/pack/src/main/kotlin/org/rsmod/content/skills/fletching/pack/FletchingPluginPack.kt @@ -0,0 +1,15 @@ +package org.rsmod.content.skills.fletching.pack + +import dev.openrune.definition.dbtables.DBTable +import dev.openrune.pack.PluginPack + +class FletchingPluginPack : PluginPack() { + override fun dbTables(): List = + listOf( + FletchingTables.cutting(), + FletchingTables.stringing(), + FletchingTables.attaching(), + FletchingTables.gemTips(), + FletchingTables.assembly(), + ) +} diff --git a/content/skills/fletching/pack/src/main/kotlin/org/rsmod/content/skills/fletching/pack/FletchingTables.kt b/content/skills/fletching/pack/src/main/kotlin/org/rsmod/content/skills/fletching/pack/FletchingTables.kt new file mode 100644 index 000000000..780b31cd2 --- /dev/null +++ b/content/skills/fletching/pack/src/main/kotlin/org/rsmod/content/skills/fletching/pack/FletchingTables.kt @@ -0,0 +1,2050 @@ +package org.rsmod.content.skills.fletching.pack + +import dev.openrune.definition.util.VarType +import dev.openrune.tables.production.productionTable + +/** + * Fletching recipes, sourced from the wiki Recipe bucket. + * + * XP is stored multiplied by ten. 46 of these recipes have fractional XP — a magic shortbow is 83.3 + * — and the xp column is an int. This matches CombinationRune, which stores tenths the same way. + * Content divides by ten once, in FletchingDefinitions. + * + * Any feather fletches bolts, darts and headless arrows. Only obj.feather appears here; the other + * seven feather types are substituted in the content layer rather than duplicated as rows. + * + * The `tool` column is held, never consumed, so it is not an input. For cutting and gem tips it is + * also half the trigger: the tool is used on the first input and the remaining materials are taken + * from the inventory, which is how the wiki describes the interaction — "the wielder will still + * need a knife in their inventory to use on logs to initiate the cutting" + * (https://oldschool.runescape.wiki/w/Fletching_knife) — and matches the vanilla cache's own + * dbtable.fletching_blowpipe_crafting, which splits its inputs into log_resource and + * secondary_resource. Stringing and attaching are plain item pairs and declare no tool column at + * all; assembly pairs stock with limbs and holds a hammer. + * + * Recipe accounting: 411 raw wiki rows for uses_skill=Fletching reduce to the 204 below. + * - 33 need a facility Forestry doesn't implement: 32 Vale-totem offerings, 1 repair bench. + * - 1 has no output; 1 is an exact duplicate; 154 are coloured-feather duplicates that collapse + * into these obj.feather rows (any feather fletches bolts, darts and headless arrows). + * - 18 are deferred one-offs, each a multi-stage assembly or a boss/quest/Forestry-gated build: the + * 4-stage ballista chain, 6 Greenman mask variants, mith grapple + (unf), lyre, scorching bow, + * webweaver bow (u), bone shortbow. + * - 9 pair a finished arrow with a Seeking headless arrow (e.g. Adamant arrow x150 + Seeking + * headless arrow x150). These are included, not dropped: it's the documented bulk-conversion + * route that turns 150 finished arrows into their seeking variant in one action. + * https://oldschool.runescape.wiki/w/Seeking_headless_arrow + */ +object FletchingTables { + + const val COL_TICKS = 7 + const val COL_TOOL = 8 + + private data class Recipe( + val row: String, + val inputs: List>, + val output: String, + val outputAmount: Int, + val level: Int, + val xp: Double, + val ticks: Int, + val crafting: Int? = null, + /** + * Held, never consumed, so it is not one of [inputs]. For the knife and chisel families it + * is also half the trigger: the tool is used on `inputs[0]` and the rest of the materials + * come from the inventory. Null for the families that need no tool. + */ + val tool: String? = null, + ) + + private fun productionRows(tableId: String, category: String, recipes: List) = + productionTable( + tableId, + serverOnly = true, + defaultCategory = category, + extraColumns = { + column("ticks", COL_TICKS, VarType.INT) + // Declared only where it is used, so the two toolless families don't generate a + // column that no row of theirs could ever fill. + if (recipes.any { it.tool != null }) column("tool", COL_TOOL, VarType.OBJ) + }, + ) { + recipes.forEach { recipe -> + row(recipe.row) { + production { + recipe.inputs.forEach { (item, amount) -> input(item, amount) } + statReq("stat.fletching", recipe.level) + recipe.crafting?.let { statReq("stat.crafting", it) } + xp((recipe.xp * 10).toInt()) + output(recipe.output, recipe.outputAmount) + } + column(COL_TICKS, recipe.ticks) + recipe.tool?.let { columnRSCM(COL_TOOL, it) } + } + } + } + + /** + * Cutting and gem tips are single-tool families by construction — a recipe belongs to them + * *because* the wiki lists a knife or a chisel — so the tool is stated once here rather than + * repeated on all 76 rows. A row that already names a tool keeps it. + */ + private fun List.withTool(tool: String) = map { it.copy(tool = it.tool ?: tool) } + + fun cutting() = + productionRows("dbtable.fletching_cutting", "Cut", CUTTING.withTool("obj.knife")) + + fun stringing() = productionRows("dbtable.fletching_stringing", "String", STRINGING) + + fun attaching() = productionRows("dbtable.fletching_attaching", "Make", ATTACHING) + + fun gemTips() = + productionRows("dbtable.fletching_gem_tips", "Cut", GEM_TIPS.withTool("obj.chisel")) + + // Assembly is mixed: every crossbow and brutal-arrow row needs a hammer, but attaching + // wolfbone arrowtips to flighted ogre arrows needs none, so the hammer is stated per row. + fun assembly() = productionRows("dbtable.fletching_assembly", "Make", ASSEMBLY) + + private val CUTTING = + listOf( + Recipe( + "dbrow.fletching_arrow_shaft_from_logs", + listOf("obj.logs" to 1), + "obj.arrow_shaft", + 15, + level = 1, + xp = 5.0, + ticks = 2, + ), + // Wiki reports 0 ticks for this row - inherited data, not a defaulted blank - so this + // knife action plays no animation. + Recipe( + "dbrow.fletching_brew_scrapey_bark_from_brew_scrapey_logs", + listOf("obj.brew_scrapey_logs" to 1), + "obj.brew_scrapey_bark", + 1, + level = 1, + xp = 50.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_seeking_arrow_shaft_from_sap_treated_logs", + listOf("obj.sap_treated_logs" to 1), + "obj.seeking_arrow_shaft", + 15, + level = 1, + xp = 5.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_javelin_shaft_from_logs", + listOf("obj.logs" to 1), + "obj.javelin_shaft", + 15, + level = 3, + xp = 5.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_ogre_arrow_shaft_from_achey_tree_logs", + listOf("obj.achey_tree_logs" to 1), + "obj.ogre_arrow_shaft", + 4, + level = 5, + xp = 6.4, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_shortbow_from_logs", + listOf("obj.logs" to 1), + "obj.unstrung_shortbow", + 1, + level = 5, + xp = 5.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_wood_from_logs", + listOf("obj.logs" to 1), + "obj.xbows_crossbow_stock_wood", + 1, + level = 9, + xp = 6.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_longbow_from_logs", + listOf("obj.logs" to 1), + "obj.unstrung_longbow", + 1, + level = 10, + xp = 10.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_arrow_shaft_from_oak_logs", + listOf("obj.oak_logs" to 1), + "obj.arrow_shaft", + 30, + level = 15, + xp = 10.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_arrow_shaft_from_sap_treated_oak_logs", + listOf("obj.sap_treated_oak_logs" to 1), + "obj.seeking_arrow_shaft", + 30, + level = 15, + xp = 10.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_unstrung_oak_shortbow_from_oak_logs", + listOf("obj.oak_logs" to 1), + "obj.unstrung_oak_shortbow", + 1, + level = 20, + xp = 16.5, + ticks = 3, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_oak_from_oak_logs", + listOf("obj.oak_logs" to 1), + "obj.xbows_crossbow_stock_oak", + 1, + level = 24, + xp = 16.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_oak_longbow_from_oak_logs", + listOf("obj.oak_logs" to 1), + "obj.unstrung_oak_longbow", + 1, + level = 25, + xp = 25.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_oak_shield_from_oak_logs", + listOf("obj.oak_logs" to 2), + "obj.oak_shield", + 1, + level = 27, + xp = 50.0, + ticks = 7, + ), + Recipe( + "dbrow.fletching_arrow_shaft_from_willow_logs", + listOf("obj.willow_logs" to 1), + "obj.arrow_shaft", + 45, + level = 30, + xp = 15.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_arrow_shaft_from_sap_treated_willow_logs", + listOf("obj.sap_treated_willow_logs" to 1), + "obj.seeking_arrow_shaft", + 45, + level = 30, + xp = 15.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_unstrung_willow_shortbow_from_willow_logs", + listOf("obj.willow_logs" to 1), + "obj.unstrung_willow_shortbow", + 1, + level = 35, + xp = 33.3, + ticks = 3, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_willow_from_willow_logs", + listOf("obj.willow_logs" to 1), + "obj.xbows_crossbow_stock_willow", + 1, + level = 39, + xp = 22.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_willow_longbow_from_willow_logs", + listOf("obj.willow_logs" to 1), + "obj.unstrung_willow_longbow", + 1, + level = 40, + xp = 41.5, + ticks = 3, + ), + Recipe( + "dbrow.fletching_willow_shield_from_willow_logs", + listOf("obj.willow_logs" to 2), + "obj.willow_shield", + 1, + level = 42, + xp = 83.0, + ticks = 7, + ), + Recipe( + "dbrow.fletching_arrow_shaft_from_maple_logs", + listOf("obj.maple_logs" to 1), + "obj.arrow_shaft", + 60, + level = 45, + xp = 20.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_arrow_shaft_from_sap_treated_maple_logs", + listOf("obj.sap_treated_maple_logs" to 1), + "obj.seeking_arrow_shaft", + 60, + level = 45, + xp = 20.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_teak_from_teak_logs", + listOf("obj.teak_logs" to 1), + "obj.xbows_crossbow_stock_teak", + 1, + level = 46, + xp = 27.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_maple_shortbow_from_maple_logs", + listOf("obj.maple_logs" to 1), + "obj.unstrung_maple_shortbow", + 1, + level = 50, + xp = 50.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_maple_from_maple_logs", + listOf("obj.maple_logs" to 1), + "obj.xbows_crossbow_stock_maple", + 1, + level = 54, + xp = 32.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_maple_longbow_from_maple_logs", + listOf("obj.maple_logs" to 1), + "obj.unstrung_maple_longbow", + 1, + level = 55, + xp = 58.3, + ticks = 3, + ), + Recipe( + "dbrow.fletching_maple_shield_from_maple_logs", + listOf("obj.maple_logs" to 2), + "obj.maple_shield", + 1, + level = 57, + xp = 116.5, + ticks = 7, + ), + Recipe( + "dbrow.fletching_arrow_shaft_from_yew_logs", + listOf("obj.yew_logs" to 1), + "obj.arrow_shaft", + 75, + level = 60, + xp = 25.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_arrow_shaft_from_sap_treated_yew_logs", + listOf("obj.sap_treated_yew_logs" to 1), + "obj.seeking_arrow_shaft", + 75, + level = 60, + xp = 25.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_mahogany_from_mahogany_logs", + listOf("obj.mahogany_logs" to 1), + "obj.xbows_crossbow_stock_mahogany", + 1, + level = 61, + xp = 41.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_yew_shortbow_from_yew_logs", + listOf("obj.yew_logs" to 1), + "obj.unstrung_yew_shortbow", + 1, + level = 65, + xp = 67.5, + ticks = 3, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_yew_from_yew_logs", + listOf("obj.yew_logs" to 1), + "obj.xbows_crossbow_stock_yew", + 1, + level = 69, + xp = 50.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_blisterwood_stake_from_blisterwood_logs", + listOf("obj.blisterwood_logs" to 1), + "obj.blisterwood_stake", + 5, + level = 70, + xp = 0.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_yew_longbow_from_yew_logs", + listOf("obj.yew_logs" to 1), + "obj.unstrung_yew_longbow", + 1, + level = 70, + xp = 75.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_yew_shield_from_yew_logs", + listOf("obj.yew_logs" to 2), + "obj.yew_shield", + 1, + level = 72, + xp = 150.0, + ticks = 7, + ), + Recipe( + "dbrow.fletching_atlatl_dart_shaft_from_ent_branch", + listOf("obj.ent_branch" to 1), + "obj.atlatl_dart_shaft", + 100, + level = 74, + xp = 30.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_arrow_shaft_from_magic_logs", + listOf("obj.magic_logs" to 1), + "obj.arrow_shaft", + 90, + level = 75, + xp = 30.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_arrow_shaft_from_sap_treated_magic_logs", + listOf("obj.sap_treated_magic_logs" to 1), + "obj.seeking_arrow_shaft", + 90, + level = 75, + xp = 30.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_stock_magic_from_magic_logs", + listOf("obj.magic_logs" to 1), + "obj.xbows_crossbow_stock_magic", + 1, + level = 78, + xp = 70.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_magic_shortbow_from_magic_logs", + listOf("obj.magic_logs" to 1), + "obj.unstrung_magic_shortbow", + 1, + level = 80, + xp = 83.3, + ticks = 3, + ), + Recipe( + "dbrow.fletching_unstrung_magic_longbow_from_magic_logs", + listOf("obj.magic_logs" to 1), + "obj.unstrung_magic_longbow", + 1, + level = 85, + xp = 91.5, + ticks = 3, + ), + Recipe( + "dbrow.fletching_magic_shield_from_magic_logs", + listOf("obj.magic_logs" to 2), + "obj.magic_shield", + 1, + level = 87, + xp = 183.0, + ticks = 7, + ), + Recipe( + "dbrow.fletching_arrow_shaft_from_redwood_logs", + listOf("obj.redwood_logs" to 1), + "obj.arrow_shaft", + 105, + level = 90, + xp = 35.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_redwood_hiking_staff_from_redwood_logs", + listOf("obj.redwood_logs" to 1), + "obj.redwood_hiking_staff", + 1, + level = 90, + xp = 10.5, + ticks = 3, + ), + Recipe( + "dbrow.fletching_seeking_arrow_shaft_from_sap_treated_redwood_logs", + listOf("obj.sap_treated_redwood_logs" to 1), + "obj.seeking_arrow_shaft", + 105, + level = 90, + xp = 35.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_redwood_shield_from_redwood_logs", + listOf("obj.redwood_logs" to 2), + "obj.redwood_shield", + 1, + level = 92, + xp = 216.0, + ticks = 7, + ), + // Unlocked by the tool column: the knife is used on the log named first, and any + // further material is taken from the inventory. Battlestaff and Root kindling need no + // second material and were only ever held back alongside these. + Recipe( + "dbrow.fletching_unstrung_zogre_bow_from_achey_tree_logs", + listOf("obj.achey_tree_logs" to 1, "obj.wolf_bones" to 1), + "obj.unstrung_zogre_bow", + 1, + level = 30, + xp = 45.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_battlestaff_from_celastrus_wood", + listOf("obj.celastrus_wood" to 1), + "obj.battlestaff", + 1, + level = 40, + xp = 80.0, + ticks = 4, + ), + Recipe( + "dbrow.fletching_vmq4_root_kindling_from_vmq4_roots", + listOf("obj.vmq4_roots" to 1), + "obj.vmq4_root_kindling", + 4, + level = 52, + xp = 10.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_greenman_statue_from_maple_logs", + listOf("obj.maple_logs" to 1, "obj.ent_branch" to 5, "obj.greenman_mask" to 1), + "obj.greenman_statue", + 1, + level = 53, + xp = 55.0, + ticks = 4, + ), + Recipe( + "dbrow.fletching_camphor_blowpipe_empty_from_camphor_logs", + listOf("obj.camphor_logs" to 2, "obj.squid_beak" to 1), + "obj.camphor_blowpipe_empty", + 1, + level = 58, + xp = 140.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_blisterwood_sickle_from_blisterwood_logs", + listOf("obj.blisterwood_logs" to 1, "obj.silver_sickle_ruby_enchanted" to 1), + "obj.blisterwood_sickle", + 1, + level = 60, + xp = 82.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_hg_hunter_spear_from_teak_logs", + listOf( + "obj.teak_logs" to 1, + "obj.hg_speartip" to 5, + "obj.hunting_jerboa_tail" to 5, + ), + "obj.hg_hunter_spear", + 5, + level = 60, + xp = 9.5, + ticks = 3, + ), + Recipe( + "dbrow.fletching_blisterwood_sickle_enhanced_from_blisterwood_logs", + listOf("obj.blisterwood_logs" to 1, "obj.silver_sickle_diamond_enchanted" to 1), + "obj.blisterwood_sickle_enhanced", + 1, + level = 70, + xp = 94.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_ironwood_blowpipe_empty_from_ironwood_logs", + listOf("obj.ironwood_logs" to 2, "obj.squid_beak" to 1), + "obj.ironwood_blowpipe_empty", + 1, + level = 72, + xp = 170.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_greenman_wall_decoration_from_yew_logs", + listOf("obj.yew_logs" to 1, "obj.ent_branch" to 5, "obj.greenman_mask" to 1), + "obj.greenman_wall_decoration", + 1, + level = 79, + xp = 70.0, + ticks = 2, + ), // wiki ticks blank + Recipe( + "dbrow.fletching_rosewood_blowpipe_empty_from_rosewood_logs", + listOf("obj.rosewood_logs" to 2, "obj.squid_beak" to 1), + "obj.rosewood_blowpipe_empty", + 1, + level = 84, + xp = 200.0, + ticks = 3, + ), + ) + + private val STRINGING = + listOf( + Recipe( + "dbrow.fletching_shortbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_shortbow" to 1), + "obj.shortbow", + 1, + level = 5, + xp = 5.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bronze_from_xbows_crossbow_unstrung_bronze", + listOf("obj.xbows_crossbow_unstrung_bronze" to 1, "obj.xbows_crossbow_string" to 1), + "obj.xbows_crossbow_bronze", + 1, + level = 9, + xp = 6.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_longbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_longbow" to 1), + "obj.longbow", + 1, + level = 10, + xp = 10.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_oak_shortbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_oak_shortbow" to 1), + "obj.oak_shortbow", + 1, + level = 20, + xp = 16.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_blurite_from_xbows_crossbow_unstrung_blurite", + listOf( + "obj.xbows_crossbow_unstrung_blurite" to 1, + "obj.xbows_crossbow_string" to 1, + ), + "obj.xbows_crossbow_blurite", + 1, + level = 24, + xp = 16.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_oak_longbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_oak_longbow" to 1), + "obj.oak_longbow", + 1, + level = 25, + xp = 25.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_zogre_bow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_zogre_bow" to 1), + "obj.zogre_bow", + 1, + level = 30, + xp = 45.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_willow_shortbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_willow_shortbow" to 1), + "obj.willow_shortbow", + 1, + level = 35, + xp = 33.2, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_iron_from_xbows_crossbow_string", + listOf("obj.xbows_crossbow_string" to 1, "obj.xbows_crossbow_unstrung_iron" to 1), + "obj.xbows_crossbow_iron", + 1, + level = 39, + xp = 22.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_willow_longbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_willow_longbow" to 1), + "obj.willow_longbow", + 1, + level = 40, + xp = 41.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_steel_from_xbows_crossbow_string", + listOf("obj.xbows_crossbow_string" to 1, "obj.xbows_crossbow_unstrung_steel" to 1), + "obj.xbows_crossbow_steel", + 1, + level = 46, + xp = 27.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_devious_bowsword_from_bow_string", + listOf("obj.bow_string" to 1, "obj.devious_slenderblade" to 1), + "obj.devious_bowsword", + 1, + level = 50, + xp = 50.0, + ticks = 2, + ), // wiki ticks blank + Recipe( + "dbrow.fletching_maple_shortbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_maple_shortbow" to 1), + "obj.maple_shortbow", + 1, + level = 50, + xp = 50.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_mithril_from_xbows_crossbow_string", + listOf( + "obj.xbows_crossbow_string" to 1, + "obj.xbows_crossbow_unstrung_mithril" to 1, + ), + "obj.xbows_crossbow_mithril", + 1, + level = 54, + xp = 32.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_maple_longbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_maple_longbow" to 1), + "obj.maple_longbow", + 1, + level = 55, + xp = 58.2, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_adamantite_from_xbows_crossbow_unstrung_adamantite", + listOf( + "obj.xbows_crossbow_unstrung_adamantite" to 1, + "obj.xbows_crossbow_string" to 1, + ), + "obj.xbows_crossbow_adamantite", + 1, + level = 61, + xp = 41.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_yew_shortbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_yew_shortbow" to 1), + "obj.yew_shortbow", + 1, + level = 65, + xp = 67.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_runite_from_xbows_crossbow_string", + listOf("obj.xbows_crossbow_string" to 1, "obj.xbows_crossbow_unstrung_runite" to 1), + "obj.xbows_crossbow_runite", + 1, + level = 69, + xp = 50.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_yew_longbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_yew_longbow" to 1), + "obj.yew_longbow", + 1, + level = 70, + xp = 75.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_dragon_from_xbows_crossbow_string", + listOf("obj.xbows_crossbow_string" to 1, "obj.xbows_crossbow_unstrung_dragon" to 1), + "obj.xbows_crossbow_dragon", + 1, + level = 78, + xp = 70.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_magic_shortbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_magic_shortbow" to 1), + "obj.magic_shortbow", + 1, + level = 80, + xp = 83.3, + ticks = 2, + ), + Recipe( + "dbrow.fletching_magic_longbow_from_bow_string", + listOf("obj.bow_string" to 1, "obj.unstrung_magic_longbow" to 1), + "obj.magic_longbow", + 1, + level = 85, + xp = 91.5, + ticks = 2, + ), + ) + + private val ATTACHING = + listOf( + Recipe( + "dbrow.fletching_bronze_arrow_from_bronze_arrowheads", + listOf("obj.bronze_arrowheads" to 15, "obj.headless_arrow" to 15), + "obj.bronze_arrow", + 15, + level = 1, + xp = 19.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_headless_arrow_from_arrow_shaft", + listOf("obj.arrow_shaft" to 15, "obj.feather" to 15), + "obj.headless_arrow", + 15, + level = 1, + xp = 15.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_adamant_arrow_from_adamant_arrow", + listOf("obj.adamant_arrow" to 15, "obj.bucket_of_bloodwood_sap" to 1), + "obj.seeking_adamant_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_seeking_amethyst_arrow_from_amethyst_arrow", + listOf("obj.amethyst_arrow" to 15, "obj.bucket_of_bloodwood_sap" to 1), + "obj.seeking_amethyst_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + // Broad arrows/bolts below need an unlocked Slayer reward; that gate does not exist + // in the codebase yet, so only the stat.fletching level check is enforced. + Recipe( + "dbrow.fletching_seeking_slayer_broad_arrows_from_slayer_broad_arrows", + listOf("obj.slayer_broad_arrows" to 15, "obj.bucket_of_bloodwood_sap" to 1), + "obj.seeking_slayer_broad_arrows", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_seeking_bronze_arrow_from_bronze_arrowheads", + listOf("obj.bronze_arrowheads" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_bronze_arrow", + 15, + level = 1, + xp = 19.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_bronze_arrow_from_bronze_arrow", + listOf("obj.bronze_arrow" to 15, "obj.bucket_of_bloodwood_sap" to 1), + "obj.seeking_bronze_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_seeking_dragon_arrow_from_bucket_of_bloodwood_sap", + listOf("obj.bucket_of_bloodwood_sap" to 1, "obj.dragon_arrow" to 15), + "obj.seeking_dragon_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_seeking_headless_arrow_from_feather", + listOf("obj.feather" to 15, "obj.seeking_arrow_shaft" to 15), + "obj.seeking_headless_arrow", + 15, + level = 1, + xp = 15.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_iron_arrow_from_bucket_of_bloodwood_sap", + listOf("obj.bucket_of_bloodwood_sap" to 1, "obj.iron_arrow" to 15), + "obj.seeking_iron_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_seeking_mithril_arrow_from_bucket_of_bloodwood_sap", + listOf("obj.bucket_of_bloodwood_sap" to 1, "obj.mithril_arrow" to 15), + "obj.seeking_mithril_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_seeking_rune_arrow_from_bucket_of_bloodwood_sap", + listOf("obj.bucket_of_bloodwood_sap" to 1, "obj.rune_arrow" to 15), + "obj.seeking_rune_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_seeking_steel_arrow_from_bucket_of_bloodwood_sap", + listOf("obj.bucket_of_bloodwood_sap" to 1, "obj.steel_arrow" to 15), + "obj.seeking_steel_arrow", + 15, + level = 1, + xp = 0.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_bronze_javelin_from_bronze_javelin_head", + listOf("obj.bronze_javelin_head" to 15, "obj.javelin_shaft" to 15), + "obj.bronze_javelin", + 15, + level = 3, + xp = 15.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_ogre_headless_arrow_from_feather", + listOf("obj.feather" to 24, "obj.ogre_arrow_shaft" to 6), + "obj.ogre_headless_arrow", + 6, + level = 5, + xp = 5.4, + ticks = 2, + ), + // The wiki Recipe bucket lists ticks=0 for the ten feather + bolts(unf) -> bolts + // rows in this family, but its own mechanics page disagrees: "Fletching arrows (all + // steps) or feathering bolts: 2 ticks per set of 15 arrows/10 bolts" - + // https://oldschool.runescape.wiki/w/Game_tick/Action_lengths. Corrected to 2 below; + // the dart rows here are unaffected and correctly stay at 0 (up to 10 dart sets can + // queue per tick, which is why darts are the fastest fletching xp in the game). + Recipe( + "dbrow.fletching_bolt_from_xbows_crossbow_bolts_bronze_unfeathered", + listOf("obj.xbows_crossbow_bolts_bronze_unfeathered" to 10, "obj.feather" to 10), + "obj.bolt", + 10, + level = 9, + xp = 5.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_bronze_dart_from_bronze_dart_tip", + listOf("obj.bronze_dart_tip" to 10, "obj.feather" to 10), + "obj.bronze_dart", + 10, + level = 10, + xp = 18.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_thprotodart_from_feather", + listOf("obj.feather" to 10, "obj.thprotodarttip" to 1), + "obj.thprotodart", + 1, + level = 10, + xp = 10.0, + ticks = 2, + ), // wiki ticks blank + Recipe( + "dbrow.fletching_opal_bolt_from_bolt", + listOf("obj.bolt" to 10, "obj.opal_bolttips" to 10), + "obj.opal_bolt", + 10, + level = 11, + xp = 16.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_iron_arrow_from_headless_arrow", + listOf("obj.headless_arrow" to 15, "obj.iron_arrowheads" to 15), + "obj.iron_arrow", + 15, + level = 15, + xp = 37.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_iron_arrow_from_iron_arrowheads", + listOf("obj.iron_arrowheads" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_iron_arrow", + 15, + level = 15, + xp = 37.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_steel_arrow_from_seeking_headless_arrow", + listOf("obj.seeking_headless_arrow" to 15, "obj.steel_arrowheads" to 15), + "obj.seeking_steel_arrow", + 15, + level = 15, + xp = 37.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_iron_javelin_from_iron_javelin_head", + listOf("obj.iron_javelin_head" to 15, "obj.javelin_shaft" to 15), + "obj.iron_javelin", + 15, + level = 17, + xp = 30.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_iron_dart_from_feather", + listOf("obj.feather" to 10, "obj.iron_dart_tip" to 10), + "obj.iron_dart", + 10, + level = 22, + xp = 38.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_blurite_from_xbows_crossbow_bolts_blurite_unfeathered", + listOf("obj.xbows_crossbow_bolts_blurite_unfeathered" to 10, "obj.feather" to 10), + "obj.xbows_crossbow_bolts_blurite", + 10, + level = 24, + xp = 10.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_blurite_tipped_jade_from_xbows_crossbow_bolts_blurite", + listOf("obj.xbows_crossbow_bolts_blurite" to 10, "obj.xbows_bolt_tips_jade" to 10), + "obj.xbows_crossbow_bolts_blurite_tipped_jade", + 10, + level = 26, + xp = 24.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_steel_arrow_from_headless_arrow", + listOf("obj.headless_arrow" to 15, "obj.steel_arrowheads" to 15), + "obj.steel_arrow", + 15, + level = 30, + xp = 75.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_steel_javelin_from_javelin_shaft", + listOf("obj.javelin_shaft" to 15, "obj.steel_javelin_head" to 15), + "obj.steel_javelin", + 15, + level = 32, + xp = 75.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_steel_dart_from_feather", + listOf("obj.feather" to 10, "obj.steel_dart_tip" to 10), + "obj.steel_dart", + 10, + level = 37, + xp = 75.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_iron_from_feather", + listOf("obj.feather" to 10, "obj.xbows_crossbow_bolts_iron_unfeathered" to 10), + "obj.xbows_crossbow_bolts_iron", + 10, + level = 39, + xp = 15.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_pearl_bolt_from_xbows_crossbow_bolts_iron", + listOf("obj.xbows_crossbow_bolts_iron" to 10, "obj.pearl_bolttips" to 10), + "obj.pearl_bolt", + 10, + level = 41, + xp = 32.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_silver_from_feather", + listOf("obj.feather" to 10, "obj.xbows_crossbow_bolts_silver_unfeathered" to 10), + "obj.xbows_crossbow_bolts_silver", + 10, + level = 43, + xp = 25.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_mithril_arrow_from_headless_arrow", + listOf("obj.headless_arrow" to 15, "obj.mithril_arrowheads" to 15), + "obj.mithril_arrow", + 15, + level = 45, + xp = 112.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_mithril_arrow_from_mithril_arrowheads", + listOf("obj.mithril_arrowheads" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_mithril_arrow", + 15, + level = 45, + xp = 112.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_steel_from_feather", + listOf("obj.feather" to 10, "obj.xbows_crossbow_bolts_steel_unfeathered" to 10), + "obj.xbows_crossbow_bolts_steel", + 10, + level = 46, + xp = 35.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_mithril_javelin_from_javelin_shaft", + listOf("obj.javelin_shaft" to 15, "obj.mithril_javelin_head" to 15), + "obj.mithril_javelin", + 15, + level = 47, + xp = 120.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_steel_tipped_redtopaz_from_xbows_crossbow_bolts_steel", + listOf( + "obj.xbows_crossbow_bolts_steel" to 10, + "obj.xbows_bolt_tips_redtopaz" to 10, + ), + "obj.xbows_crossbow_bolts_steel_tipped_redtopaz", + 10, + level = 48, + xp = 39.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_barbed_bolt_from_barbed_bolttips", + listOf("obj.barbed_bolttips" to 10, "obj.bolt" to 10), + "obj.barbed_bolt", + 10, + level = 51, + xp = 95.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_slayer_broad_arrows_from_slayer_broad_arrowhead", + listOf("obj.slayer_broad_arrowhead" to 15, "obj.headless_arrow" to 15), + "obj.slayer_broad_arrows", + 15, + level = 52, + xp = 150.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_mithril_dart_from_feather", + listOf("obj.feather" to 10, "obj.mithril_dart_tip" to 10), + "obj.mithril_dart", + 10, + level = 52, + xp = 112.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_seeking_slayer_broad_arrows_from_slayer_broad_arrowhead", + listOf("obj.slayer_broad_arrowhead" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_slayer_broad_arrows", + 15, + level = 52, + xp = 150.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_mithril_from_feather", + listOf("obj.feather" to 10, "obj.xbows_crossbow_bolts_mithril_unfeathered" to 10), + "obj.xbows_crossbow_bolts_mithril", + 10, + level = 54, + xp = 50.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_slayer_broad_bolt_from_feather", + listOf("obj.feather" to 10, "obj.slayer_broad_bolt_unfinished" to 10), + "obj.slayer_broad_bolt", + 10, + level = 55, + xp = 30.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_mithril_tipped_sapphire_from_xbows_crossbow_bolts_mithril", + listOf( + "obj.xbows_crossbow_bolts_mithril" to 10, + "obj.xbows_bolt_tips_sapphire" to 10, + ), + "obj.xbows_crossbow_bolts_mithril_tipped_sapphire", + 10, + level = 56, + xp = 47.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_mithril_tipped_emerald_from_xbows_bolt_tips_emerald", + listOf( + "obj.xbows_bolt_tips_emerald" to 10, + "obj.xbows_crossbow_bolts_mithril" to 10, + ), + "obj.xbows_crossbow_bolts_mithril_tipped_emerald", + 10, + level = 58, + xp = 55.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_adamant_arrow_from_adamant_arrowheads", + listOf("obj.adamant_arrowheads" to 15, "obj.headless_arrow" to 15), + "obj.adamant_arrow", + 15, + level = 60, + xp = 150.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_adamant_arrow_from_adamant_arrowheads", + listOf("obj.adamant_arrowheads" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_adamant_arrow", + 15, + level = 60, + xp = 150.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_adamantite_from_xbows_crossbow_bolts_adamantite_unfeathered", + listOf( + "obj.xbows_crossbow_bolts_adamantite_unfeathered" to 10, + "obj.feather" to 10, + ), + "obj.xbows_crossbow_bolts_adamantite", + 10, + level = 61, + xp = 70.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_adamant_javelin_from_adamant_javelin_head", + listOf("obj.adamant_javelin_head" to 15, "obj.javelin_shaft" to 15), + "obj.adamant_javelin", + 15, + level = 62, + xp = 150.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_adamantite_tipped_ruby_from_xbows_crossbow_bolts_adamantite", + listOf( + "obj.xbows_crossbow_bolts_adamantite" to 10, + "obj.xbows_bolt_tips_ruby" to 10, + ), + "obj.xbows_crossbow_bolts_adamantite_tipped_ruby", + 10, + level = 63, + xp = 63.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_adamantite_tipped_diamond_from_xbows_crossbow_bolts_adamantite", + listOf( + "obj.xbows_crossbow_bolts_adamantite" to 10, + "obj.xbows_bolt_tips_diamond" to 10, + ), + "obj.xbows_crossbow_bolts_adamantite_tipped_diamond", + 10, + level = 65, + xp = 70.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_adamant_dart_from_adamant_dart_tip", + listOf("obj.adamant_dart_tip" to 10, "obj.feather" to 10), + "obj.adamant_dart", + 10, + level = 67, + xp = 150.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_runite_from_feather", + listOf("obj.feather" to 10, "obj.xbows_crossbow_bolts_runite_unfeathered" to 10), + "obj.xbows_crossbow_bolts_runite", + 10, + level = 69, + xp = 100.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_runite_tipped_dragonstone_from_xbows_bolt_tips_dragonstone", + listOf( + "obj.xbows_bolt_tips_dragonstone" to 10, + "obj.xbows_crossbow_bolts_runite" to 10, + ), + "obj.xbows_crossbow_bolts_runite_tipped_dragonstone", + 10, + level = 71, + xp = 82.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_crossbow_bolts_runite_tipped_onyx_from_xbows_bolt_tips_onyx", + listOf("obj.xbows_bolt_tips_onyx" to 10, "obj.xbows_crossbow_bolts_runite" to 10), + "obj.xbows_crossbow_bolts_runite_tipped_onyx", + 10, + level = 73, + xp = 94.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_atlatl_dart_from_atlatl_dart_tips", + listOf("obj.atlatl_dart_tips" to 20, "obj.headless_atlatl_dart" to 20), + "obj.atlatl_dart", + 20, + level = 74, + xp = 30.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_headless_atlatl_dart_from_atlatl_dart_shaft", + listOf("obj.atlatl_dart_shaft" to 20, "obj.feather" to 20), + "obj.headless_atlatl_dart", + 20, + level = 74, + xp = 20.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_rune_arrow_from_headless_arrow", + listOf("obj.headless_arrow" to 15, "obj.rune_arrowheads" to 15), + "obj.rune_arrow", + 15, + level = 75, + xp = 187.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_rune_arrow_from_rune_arrowheads", + listOf("obj.rune_arrowheads" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_rune_arrow", + 15, + level = 75, + xp = 187.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_slayer_broad_bolt_amethyst_from_xbows_bolt_tips_amethyst", + listOf("obj.xbows_bolt_tips_amethyst" to 10, "obj.slayer_broad_bolt" to 10), + "obj.slayer_broad_bolt_amethyst", + 10, + level = 76, + xp = 106.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_rune_javelin_from_javelin_shaft", + listOf("obj.javelin_shaft" to 15, "obj.rune_javelin_head" to 15), + "obj.rune_javelin", + 15, + level = 77, + xp = 187.5, + ticks = 1, + ), + Recipe( + "dbrow.fletching_rune_dart_from_feather", + listOf("obj.feather" to 10, "obj.rune_dart_tip" to 10), + "obj.rune_dart", + 10, + level = 81, + xp = 188.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_amethyst_arrow_from_amethyst_arrowheads", + listOf("obj.amethyst_arrowheads" to 15, "obj.headless_arrow" to 15), + "obj.amethyst_arrow", + 15, + level = 82, + xp = 202.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_amethyst_arrow_from_amethyst_arrowheads", + listOf("obj.amethyst_arrowheads" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_amethyst_arrow", + 15, + level = 82, + xp = 202.5, + ticks = 2, + ), + Recipe( + "dbrow.fletching_amethyst_javelin_from_amethyst_javelin_head", + listOf("obj.amethyst_javelin_head" to 15, "obj.javelin_shaft" to 15), + "obj.amethyst_javelin", + 15, + level = 84, + xp = 202.5, + ticks = 1, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_diamond_from_xbows_bolt_tips_diamond", + listOf("obj.xbows_bolt_tips_diamond" to 10, "obj.dragon_bolts" to 10), + "obj.dragon_bolts_unenchanted_diamond", + 10, + level = 84, + xp = 70.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_from_dragon_bolts_unfeathered", + listOf("obj.dragon_bolts_unfeathered" to 10, "obj.feather" to 10), + "obj.dragon_bolts", + 10, + level = 84, + xp = 120.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_dragonstone_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.xbows_bolt_tips_dragonstone" to 10), + "obj.dragon_bolts_unenchanted_dragonstone", + 10, + level = 84, + xp = 82.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_emerald_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.xbows_bolt_tips_emerald" to 10), + "obj.dragon_bolts_unenchanted_emerald", + 10, + level = 84, + xp = 55.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_jade_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.xbows_bolt_tips_jade" to 10), + "obj.dragon_bolts_unenchanted_jade", + 10, + level = 84, + xp = 24.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_onyx_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.xbows_bolt_tips_onyx" to 10), + "obj.dragon_bolts_unenchanted_onyx", + 10, + level = 84, + xp = 94.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_opal_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.opal_bolttips" to 10), + "obj.dragon_bolts_unenchanted_opal", + 10, + level = 84, + xp = 16.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_pearl_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.pearl_bolttips" to 10), + "obj.dragon_bolts_unenchanted_pearl", + 10, + level = 84, + xp = 32.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_ruby_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.xbows_bolt_tips_ruby" to 10), + "obj.dragon_bolts_unenchanted_ruby", + 10, + level = 84, + xp = 63.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_sapphire_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.xbows_bolt_tips_sapphire" to 10), + "obj.dragon_bolts_unenchanted_sapphire", + 10, + level = 84, + xp = 47.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_bolts_unenchanted_topaz_from_dragon_bolts", + listOf("obj.dragon_bolts" to 10, "obj.xbows_bolt_tips_redtopaz" to 10), + "obj.dragon_bolts_unenchanted_topaz", + 10, + level = 84, + xp = 39.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_amethyst_dart_from_amethyst_dart_tip", + listOf("obj.amethyst_dart_tip" to 10, "obj.feather" to 10), + "obj.amethyst_dart", + 10, + level = 90, + xp = 210.0, + ticks = 0, + ), + Recipe( + "dbrow.fletching_dragon_arrow_from_dragon_arrowheads", + listOf("obj.dragon_arrowheads" to 15, "obj.headless_arrow" to 15), + "obj.dragon_arrow", + 15, + level = 90, + xp = 225.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_dragon_arrow_from_dragon_arrowheads", + listOf("obj.dragon_arrowheads" to 15, "obj.seeking_headless_arrow" to 15), + "obj.seeking_dragon_arrow", + 15, + level = 90, + xp = 225.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_dragon_javelin_from_dragon_javelin_head", + listOf("obj.dragon_javelin_head" to 15, "obj.javelin_shaft" to 15), + "obj.dragon_javelin", + 15, + level = 92, + xp = 225.0, + ticks = 1, + ), + Recipe( + "dbrow.fletching_dragon_dart_from_dragon_dart_tip", + listOf("obj.dragon_dart_tip" to 10, "obj.feather" to 10), + "obj.dragon_dart", + 10, + level = 95, + xp = 250.0, + ticks = 0, + ), + // Seeking headless arrows also bulk-convert already-finished arrows to their seeking + // variant, in sets of 150 - a documented mechanic, not a data error: + // https://oldschool.runescape.wiki/w/Seeking_headless_arrow + Recipe( + "dbrow.fletching_seeking_adamant_arrow_from_adamant_arrow_x150", + listOf("obj.adamant_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_adamant_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_amethyst_arrow_from_amethyst_arrow_x150", + listOf("obj.amethyst_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_amethyst_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_slayer_broad_arrows_from_slayer_broad_arrows_x150", + listOf("obj.slayer_broad_arrows" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_slayer_broad_arrows", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_bronze_arrow_from_bronze_arrow_x150", + listOf("obj.bronze_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_bronze_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_dragon_arrow_from_dragon_arrow_x150", + listOf("obj.dragon_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_dragon_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_iron_arrow_from_iron_arrow_x150", + listOf("obj.iron_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_iron_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_mithril_arrow_from_mithril_arrow_x150", + listOf("obj.mithril_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_mithril_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_rune_arrow_from_rune_arrow_x150", + listOf("obj.rune_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_rune_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_seeking_steel_arrow_from_steel_arrow_x150", + listOf("obj.steel_arrow" to 150, "obj.seeking_headless_arrow" to 150), + "obj.seeking_steel_arrow", + 150, + level = 1, + xp = 0.0, + ticks = 2, + ), + ) + + private val GEM_TIPS = + listOf( + Recipe( + "dbrow.fletching_wolfbone_arrowheads_from_wolf_bones", + listOf("obj.wolf_bones" to 1), + "obj.wolfbone_arrowheads", + 4, + level = 5, + xp = 10.0, + ticks = 2, + crafting = 1, + ), + Recipe( + "dbrow.fletching_opal_bolttips_from_opal", + listOf("obj.opal" to 1), + "obj.opal_bolttips", + 12, + level = 11, + xp = 1.5, + ticks = 5, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_jade_from_jade", + listOf("obj.jade" to 1), + "obj.xbows_bolt_tips_jade", + 12, + level = 26, + xp = 2.0, + ticks = 5, + ), + Recipe( + "dbrow.fletching_huntingbow_bolts_from_huntingbeast_spike", + listOf("obj.huntingbeast_spike" to 1), + "obj.huntingbow_bolts", + 12, + level = 32, + xp = 5.8, + ticks = 3, + ), + Recipe( + "dbrow.fletching_pearl_bolttips_from_smalloysterpearls", + listOf("obj.smalloysterpearls" to 1), + "obj.pearl_bolttips", + 6, + level = 41, + xp = 3.2, + ticks = 5, + ), + Recipe( + "dbrow.fletching_pearl_bolttips_from_bigoysterpearls", + listOf("obj.bigoysterpearls" to 1), + "obj.pearl_bolttips", + 24, + level = 41, + xp = 3.2, + ticks = 5, + ), + Recipe( + "dbrow.fletching_huntingbow_bigbolts_from_huntingbeast_bigspike", + listOf("obj.huntingbeast_bigspike" to 1), + "obj.huntingbow_bigbolts", + 12, + level = 42, + xp = 7.9, + ticks = 2, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_redtopaz_from_red_topaz", + listOf("obj.red_topaz" to 1), + "obj.xbows_bolt_tips_redtopaz", + 12, + level = 48, + xp = 3.9, + ticks = 5, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_sapphire_from_sapphire", + listOf("obj.sapphire" to 1), + "obj.xbows_bolt_tips_sapphire", + 12, + level = 56, + xp = 4.0, + ticks = 5, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_emerald_from_emerald", + listOf("obj.emerald" to 1), + "obj.xbows_bolt_tips_emerald", + 12, + level = 58, + xp = 5.5, + ticks = 5, + ), + Recipe( + "dbrow.fletching_sunlight_antelope_bolt_from_hunting_antelopesun_horn", + listOf("obj.hunting_antelopesun_horn" to 1), + "obj.sunlight_antelope_bolt", + 12, + level = 62, + xp = 10.0, + ticks = 3, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_ruby_from_ruby", + listOf("obj.ruby" to 1), + "obj.xbows_bolt_tips_ruby", + 12, + level = 63, + xp = 6.3, + ticks = 5, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_diamond_from_diamond", + listOf("obj.diamond" to 1), + "obj.xbows_bolt_tips_diamond", + 12, + level = 65, + xp = 7.0, + ticks = 5, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_dragonstone_from_dragonstone", + listOf("obj.dragonstone" to 1), + "obj.xbows_bolt_tips_dragonstone", + 12, + level = 71, + xp = 8.2, + ticks = 5, + ), + Recipe( + "dbrow.fletching_moonlight_antelope_bolt_from_hunting_antelopemoon_horn", + listOf("obj.hunting_antelopemoon_horn" to 1), + "obj.moonlight_antelope_bolt", + 12, + level = 72, + xp = 12.1, + ticks = 3, + ), + Recipe( + "dbrow.fletching_xbows_bolt_tips_onyx_from_onyx", + listOf("obj.onyx" to 1), + "obj.xbows_bolt_tips_onyx", + 24, + level = 73, + xp = 9.4, + ticks = 5, + ), + Recipe( + "dbrow.fletching_atlatl_dart_tips_from_custodian_broken_antler", + listOf("obj.custodian_broken_antler" to 1), + "obj.atlatl_dart_tips", + 100, + level = 74, + xp = 10.0, + ticks = 4, + ), + // Wiki reports 0 ticks for this row - inherited data, not a defaulted blank - so this + // chisel action plays no animation. + Recipe( + "dbrow.fletching_toxic_blowpipe_from_blowpipe_fang", + listOf("obj.blowpipe_fang" to 1), + "obj.toxic_blowpipe", + 1, + level = 78, + xp = 120.0, + ticks = 0, + ), + // Chiselling the antler with a Hunters' crossbow held upgrades the crossbow, so this + // shares its trigger - and its make-X menu - with the antler's bolt-tip row above. + Recipe( + "dbrow.fletching_hunting_crossbow_sunlight_from_hunting_antelopesun_horn", + listOf("obj.hunting_antelopesun_horn" to 1, "obj.hunting_crossbow" to 1), + "obj.hunting_crossbow_sunlight", + 1, + level = 74, + xp = 0.0, + ticks = 0, + ), + ) + + private val ASSEMBLY = + listOf( + Recipe( + "dbrow.fletching_ogre_arrow_from_ogre_headless_arrow", + listOf("obj.ogre_headless_arrow" to 6, "obj.wolfbone_arrowheads" to 6), + "obj.ogre_arrow", + 6, + level = 5, + xp = 6.0, + ticks = 2, + ), + Recipe( + "dbrow.fletching_zogre_brutal_bronze_from_nails_bronze", + listOf("obj.nails_bronze" to 6, "obj.ogre_headless_arrow" to 6), + "obj.zogre_brutal_bronze", + 6, + level = 7, + xp = 8.4, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_bronze_from_xbows_crossbow_limbs_bronze", + listOf( + "obj.xbows_crossbow_limbs_bronze" to 1, + "obj.xbows_crossbow_stock_wood" to 1, + ), + "obj.xbows_crossbow_unstrung_bronze", + 1, + level = 9, + xp = 12.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_zogre_brutal_iron_from_ogre_headless_arrow", + listOf("obj.ogre_headless_arrow" to 6, "obj.nails_iron" to 6), + "obj.zogre_brutal_iron", + 6, + level = 18, + xp = 15.6, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_blurite_from_xbows_crossbow_limbs_blurite", + listOf( + "obj.xbows_crossbow_limbs_blurite" to 1, + "obj.xbows_crossbow_stock_oak" to 1, + ), + "obj.xbows_crossbow_unstrung_blurite", + 1, + level = 24, + xp = 32.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_zogre_brutal_steel_from_ogre_headless_arrow", + listOf("obj.ogre_headless_arrow" to 6, "obj.nails" to 6), + "obj.zogre_brutal_steel", + 6, + level = 33, + xp = 30.6, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_zogre_brutal_black_from_nails_black", + listOf("obj.nails_black" to 6, "obj.ogre_headless_arrow" to 6), + "obj.zogre_brutal_black", + 6, + level = 38, + xp = 39.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_iron_from_xbows_crossbow_limbs_iron", + listOf( + "obj.xbows_crossbow_limbs_iron" to 1, + "obj.xbows_crossbow_stock_willow" to 1, + ), + "obj.xbows_crossbow_unstrung_iron", + 1, + level = 39, + xp = 44.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_steel_from_xbows_crossbow_limbs_steel", + listOf("obj.xbows_crossbow_limbs_steel" to 1, "obj.xbows_crossbow_stock_teak" to 1), + "obj.xbows_crossbow_unstrung_steel", + 1, + level = 46, + xp = 54.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_zogre_brutal_mithril_from_ogre_headless_arrow", + listOf("obj.ogre_headless_arrow" to 6, "obj.nails_mithril" to 6), + "obj.zogre_brutal_mithril", + 6, + level = 49, + xp = 45.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_mithril_from_xbows_crossbow_stock_maple", + listOf( + "obj.xbows_crossbow_stock_maple" to 1, + "obj.xbows_crossbow_limbs_mithril" to 1, + ), + "obj.xbows_crossbow_unstrung_mithril", + 1, + level = 54, + xp = 64.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_adamantite_from_xbows_crossbow_limbs_adamantite", + listOf( + "obj.xbows_crossbow_limbs_adamantite" to 1, + "obj.xbows_crossbow_stock_mahogany" to 1, + ), + "obj.xbows_crossbow_unstrung_adamantite", + 1, + level = 61, + xp = 82.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_zogre_brutal_adamant_from_nails_adamant", + listOf("obj.nails_adamant" to 6, "obj.ogre_headless_arrow" to 6), + "obj.zogre_brutal_adamant", + 6, + level = 62, + xp = 61.2, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_runite_from_xbows_crossbow_limbs_runite", + listOf("obj.xbows_crossbow_limbs_runite" to 1, "obj.xbows_crossbow_stock_yew" to 1), + "obj.xbows_crossbow_unstrung_runite", + 1, + level = 69, + xp = 100.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_zogre_brutal_rune_from_ogre_headless_arrow", + listOf("obj.ogre_headless_arrow" to 6, "obj.nails_rune" to 6), + "obj.zogre_brutal_rune", + 6, + level = 77, + xp = 75.0, + ticks = 2, + tool = "obj.hammer", + ), + Recipe( + "dbrow.fletching_xbows_crossbow_unstrung_dragon_from_xbows_crossbow_limbs_dragon", + listOf( + "obj.xbows_crossbow_limbs_dragon" to 1, + "obj.xbows_crossbow_stock_magic" to 1, + ), + "obj.xbows_crossbow_unstrung_dragon", + 1, + level = 78, + xp = 135.0, + ticks = 2, + tool = "obj.hammer", + ), + ) +} diff --git a/content/skills/fletching/pack/src/main/resources/pack/configs/fletching_anim.toml b/content/skills/fletching/pack/src/main/resources/pack/configs/fletching_anim.toml new file mode 100644 index 000000000..9b6f9117c --- /dev/null +++ b/content/skills/fletching/pack/src/main/resources/pack/configs/fletching_anim.toml @@ -0,0 +1,87 @@ +[[enum]] +isServerOnly = true +id = "enum.fletching_anims" +keyType = "OBJ" +valueType = "SEQ" + +[enum.values] +"obj.adamant_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.adamant_dart" = "seq.human_fletching_add_dart_feathers_adamant" +"obj.amethyst_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.amethyst_dart" = "seq.human_fletching_add_dart_feathers_amethyst" +"obj.barbed_bolt" = "seq.human_fletching_add_bolt_tips_bronze" +"obj.bolt" = "seq.human_fletching_add_bolt_feathers_bronze" +"obj.bronze_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.bronze_dart" = "seq.human_fletching_add_dart_feathers_bronze" +"obj.dragon_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.dragon_bolts" = "seq.human_fletching_add_bolt_feathers_dragon" +"obj.dragon_bolts_unenchanted_diamond" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_dragonstone" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_emerald" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_jade" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_onyx" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_opal" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_pearl" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_ruby" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_sapphire" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_bolts_unenchanted_topaz" = "seq.human_fletching_add_bolt_tips_dragon" +"obj.dragon_dart" = "seq.human_fletching_add_dart_feathers_dragon" +"obj.headless_arrow" = "seq.human_fletching_add_feather" +"obj.headless_atlatl_dart" = "seq.human_fletching_add_feather" +"obj.iron_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.iron_dart" = "seq.human_fletching_add_dart_feathers_iron" +"obj.longbow" = "seq.stringing_longbow" +"obj.magic_longbow" = "seq.stringing_magic_longbow" +"obj.magic_shortbow" = "seq.stringing_magic_shortbow" +"obj.maple_longbow" = "seq.stringing_maple_longbow" +"obj.maple_shortbow" = "seq.stringing_maple_shortbow" +"obj.mithril_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.mithril_dart" = "seq.human_fletching_add_dart_feathers_mithril" +"obj.oak_longbow" = "seq.stringing_oak_longbow" +"obj.oak_shortbow" = "seq.stringing_oak_shortbow" +"obj.ogre_headless_arrow" = "seq.human_fletching_add_feather" +"obj.opal_bolt" = "seq.human_fletching_add_bolt_tips_bronze" +"obj.pearl_bolt" = "seq.human_fletching_add_bolt_tips_iron" +"obj.rune_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.rune_dart" = "seq.human_fletching_add_dart_feathers_rune" +"obj.seeking_adamant_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_amethyst_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_bronze_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_dragon_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_headless_arrow" = "seq.human_fletching_add_feather" +"obj.seeking_iron_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_mithril_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_rune_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_slayer_broad_arrows" = "seq.human_fletching_add_arrow_tips" +"obj.seeking_steel_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.shortbow" = "seq.stringing_shortbow" +"obj.slayer_broad_arrows" = "seq.human_fletching_add_arrow_tips" +"obj.steel_arrow" = "seq.human_fletching_add_arrow_tips" +"obj.steel_dart" = "seq.human_fletching_add_dart_feathers_steel" +"obj.willow_longbow" = "seq.stringing_willow_longbow" +"obj.willow_shortbow" = "seq.stringing_willow_shortbow" +"obj.xbows_crossbow_adamantite" = "seq.xbows_stringing_crossbow_adamantite" +"obj.xbows_crossbow_blurite" = "seq.xbows_stringing_crossbow_blurite" +"obj.xbows_crossbow_bolts_adamantite" = "seq.human_fletching_add_bolt_feathers_adamant" +"obj.xbows_crossbow_bolts_adamantite_tipped_diamond" = "seq.human_fletching_add_bolt_tips_adamant" +"obj.xbows_crossbow_bolts_adamantite_tipped_ruby" = "seq.human_fletching_add_bolt_tips_adamant" +"obj.xbows_crossbow_bolts_blurite" = "seq.human_fletching_add_bolt_feathers_blurite" +"obj.xbows_crossbow_bolts_blurite_tipped_jade" = "seq.human_fletching_add_bolt_tips_blurite" +"obj.xbows_crossbow_bolts_iron" = "seq.human_fletching_add_bolt_feathers_iron" +"obj.xbows_crossbow_bolts_mithril" = "seq.human_fletching_add_bolt_feathers_mithril" +"obj.xbows_crossbow_bolts_mithril_tipped_emerald" = "seq.human_fletching_add_bolt_tips_mithril" +"obj.xbows_crossbow_bolts_mithril_tipped_sapphire" = "seq.human_fletching_add_bolt_tips_mithril" +"obj.xbows_crossbow_bolts_runite" = "seq.human_fletching_add_bolt_feathers_rune" +"obj.xbows_crossbow_bolts_runite_tipped_dragonstone" = "seq.human_fletching_add_bolt_tips_rune" +"obj.xbows_crossbow_bolts_runite_tipped_onyx" = "seq.human_fletching_add_bolt_tips_rune" +"obj.xbows_crossbow_bolts_silver" = "seq.human_fletching_add_bolt_feathers_silver" +"obj.xbows_crossbow_bolts_steel" = "seq.human_fletching_add_bolt_feathers_steel" +"obj.xbows_crossbow_bolts_steel_tipped_redtopaz" = "seq.human_fletching_add_bolt_tips_steel" +"obj.xbows_crossbow_bronze" = "seq.xbows_stringing_crossbow_bronze" +"obj.xbows_crossbow_dragon" = "seq.xbows_stringing_crossbow_dragon" +"obj.xbows_crossbow_iron" = "seq.xbows_stringing_crossbow_iron" +"obj.xbows_crossbow_mithril" = "seq.xbows_stringing_crossbow_mithril" +"obj.xbows_crossbow_runite" = "seq.xbows_stringing_crossbow_runite" +"obj.xbows_crossbow_steel" = "seq.xbows_stringing_crossbow_steel" +"obj.yew_longbow" = "seq.stringing_yew_longbow" +"obj.yew_shortbow" = "seq.stringing_yew_shortbow" diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/AssemblyEvents.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/AssemblyEvents.kt new file mode 100644 index 000000000..f316622a1 --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/AssemblyEvents.kt @@ -0,0 +1,17 @@ +package org.rsmod.content.skills.fletching + +import org.rsmod.api.script.onOpHeldU +import org.rsmod.content.skills.SkillingActionType +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +class AssemblyEvents : PluginScript() { + + override fun ScriptContext.startup() { + for ((pair, recipes) in pairsFor(FletchingDefinitions.assembly)) { + onOpHeldU(pair.first, pair.second) { + openFletchMenu(recipes, SkillingActionType.MAKE_SETS) + } + } + } +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/AttachingEvents.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/AttachingEvents.kt new file mode 100644 index 000000000..3372d198a --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/AttachingEvents.kt @@ -0,0 +1,19 @@ +package org.rsmod.content.skills.fletching + +import org.rsmod.api.script.onOpHeldU +import org.rsmod.content.skills.SkillingActionType +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +class AttachingEvents : PluginScript() { + + override fun ScriptContext.startup() { + for ((pair, recipes) in pairsFor(FletchingDefinitions.attaching)) { + val usedFeather = + listOf(pair.first, pair.second).firstOrNull { it in FletchingDefinitions.feathers } + onOpHeldU(pair.first, pair.second) { + openFletchMenu(recipes, SkillingActionType.MAKE_SETS, usedFeather) + } + } + } +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/CuttingEvents.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/CuttingEvents.kt new file mode 100644 index 000000000..2b105d225 --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/CuttingEvents.kt @@ -0,0 +1,15 @@ +package org.rsmod.content.skills.fletching + +import org.rsmod.api.script.onOpHeldU +import org.rsmod.content.skills.SkillingActionType +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +class CuttingEvents : PluginScript() { + + override fun ScriptContext.startup() { + for ((pair, recipes) in byToolAndPrimary(FletchingDefinitions.cutting)) { + onOpHeldU(pair.first, pair.second) { openFletchMenu(recipes, SkillingActionType.CUT) } + } + } +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingAction.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingAction.kt new file mode 100644 index 000000000..c83b21d9c --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingAction.kt @@ -0,0 +1,166 @@ +package org.rsmod.content.skills.fletching + +import dev.openrune.types.StatType +import org.rsmod.api.player.protect.ProtectedAccess +import org.rsmod.api.script.onPlayerQueueWithArgs +import org.rsmod.api.table.Tuple2 +import org.rsmod.content.skills.Material +import org.rsmod.content.skills.SkillMultiConfig +import org.rsmod.content.skills.SkillMultiEntry +import org.rsmod.content.skills.SkillingActionType +import org.rsmod.content.skills.openSkillMulti +import org.rsmod.plugin.scripts.ScriptContext + +private const val FLETCH_QUEUE = "queue.fletching_produce" + +data class FletchingTask(val recipe: FletchingRecipe, val amount: Int, val created: Int) + +fun ScriptContext.registerFletchingQueue() { + onPlayerQueueWithArgs(FLETCH_QUEUE) { processFletchTick(it.args) } +} + +/** + * Silent stat-requirement check used to build the candidate list for the make-X menu. + * [meetsStatReqs] below shows a message box on failure, which is only appropriate for the single + * recipe the player actually chose - running it over every candidate here would fire a blocking + * dialog for each recipe the player doesn't yet qualify for. + */ +private fun ProtectedAccess.hasStatReqs(reqs: List>): Boolean = + reqs.all { statBase(it.t0.internalName) >= it.t1 } + +private suspend fun ProtectedAccess.meetsStatReqs(reqs: List>): Boolean { + for (req in reqs) { + val stat = req.t0 + val level = req.t1 + if (statBase(stat.internalName) < level) { + val name = stat.displayName.ifEmpty { stat.internalName.removePrefix("stat.") } + mesbox("You need a $name level of $level.") + return false + } + } + return true +} + +/** "You need a hammer to make that." The tool is named by its cache display name. */ +private fun missingToolMessage(tool: String): String = + "You need a ${Material(tool).obj.name.lowercase()} to make that." + +/** Some gem tip rows also carry a crafting requirement, so the fletching one is picked by name. */ +private fun FletchingRecipe.fletchingLevel(): Int = + statReq.firstOrNull { it.t0.internalName == "stat.fletching" }?.t1 ?: 0 + +suspend fun ProtectedAccess.startFletching(recipe: FletchingRecipe, amount: Int) { + if (!meetsStatReqs(recipe.statReq)) return + val tool = recipe.tool + if (tool != null && !recipe.hasTool(inv)) { + mes(missingToolMessage(tool)) + return + } + if (!recipe.hasMaterials(inv)) { + mes("You don't have the materials needed to make that.") + return + } + weakQueue(FLETCH_QUEUE, 1, FletchingTask(recipe, amount, 0)) +} + +/** + * Opens the standard make-X menu for whichever recipes the player can currently make, and starts + * the chosen one. Every fletching flow funnels through here; the only thing that varies between + * them is the action type the client uses to label the menu. + */ +suspend fun ProtectedAccess.openFletchMenu( + recipes: List, + actionType: SkillingActionType, + preferredFeather: String? = null, +) { + // Sorted by level because a trigger can now gather recipes from more than one place in the + // table - a knife on teak logs reaches both the teak stock and the hunter's spear - and the + // make-X menu should still read low level to high. + val affordable = + recipes + .map { it.withHeldFeather(inv, preferredFeather) } + .filter { hasStatReqs(it.statReq) && it.maxProducible(inv) > 0 } + .sortedBy { it.fletchingLevel() } + val candidates = affordable.filter { it.hasTool(inv) } + if (candidates.isEmpty()) { + // Only complain when the tool is the sole thing standing in the way; an empty menu for any + // other reason stays silent, as it did before. + affordable.firstNotNullOfOrNull { it.tool }?.let { mes(missingToolMessage(it)) } + return + } + + // Two recipes can share an output name (e.g. multiple routes to obj.arrow_shaft), so the + // entry itself - internal name plus materials - is the disambiguating key back to the recipe + // that produced it, not the output name alone. + val entries = candidates.map { SkillMultiEntry(it.output.internalName, it.inputs) } + val recipeByEntry = entries.zip(candidates).toMap() + + openSkillMulti( + SkillMultiConfig( + actionType = actionType, + verb = "make", + entries = entries, + maxCountProvider = { inventory, entry -> + recipeByEntry[entry]?.maxProducible(inventory) ?: 0 + }, + ) + ) { selection -> + val recipe = recipeByEntry[selection.entry] ?: return@openSkillMulti + startFletching(recipe, selection.amount) + } +} + +private suspend fun ProtectedAccess.processFletchTick(task: FletchingTask) { + val recipe = task.recipe + + if (!meetsStatReqs(recipe.statReq) || !recipe.hasTool(inv) || !recipe.hasMaterials(inv)) { + resetAnim() + return + } + + // The shared animation loops, so it suits a run of actions and a lone one takes the one-shot + // variant. Its tick gate covers the rows that animate nothing; a row with an animation of its + // own is exempt, the dart rows being 0-tick yet animated. + val specific = FletchingAnims.forOutput(recipe.output) + val looping = specific == null && recipe.ticks > 0 && task.amount > 1 + if (specific != null) { + anim(specific) + } else if (recipe.ticks > 0) { + anim(if (looping) FletchingAnims.GENERIC else FletchingAnims.GENERIC_SINGLE) + } + + val consumed = mutableListOf>() + for (material in recipe.inputs) { + val name = material.obj.internalName + if (invDel(inv, name, material.count).success) { + consumed += name to material.count + } else { + consumed.forEach { (restore, count) -> invAdd(inv, restore, count) } + resetAnim() + return + } + } + + if (invAdd(inv, recipe.output.internalName, recipe.outputAmount).failure) { + consumed.forEach { (restore, count) -> invAdd(inv, restore, count) } + mes("You don't have enough inventory space to make that.") + resetAnim() + return + } + + statAdvance("stat.fletching", recipe.xp) + + val created = task.created + 1 + if (created < task.amount) { + // A queue submitted while the queue list is being processed is decremented on that same + // cycle, so a re-queue arrives a cycle sooner than the opening one in startFletching. + weakQueue( + FLETCH_QUEUE, + if (recipe.ticks > 0) recipe.ticks + 1 else 1, + FletchingTask(recipe, task.amount, created), + ) + } else if (looping) { + // Only the looping animation needs stopping; a one-shot would be cut short instead. + resetAnim() + } +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingAnims.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingAnims.kt new file mode 100644 index 000000000..772b1d0b3 --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingAnims.kt @@ -0,0 +1,34 @@ +package org.rsmod.content.skills.fletching + +import dev.openrune.rscm.RSCM +import dev.openrune.rscm.RSCMType +import dev.openrune.types.ItemServerType +import org.rsmod.api.enums.NamedEnums.fletching_anims + +/** + * Per-recipe fletching animations, read from `enum.fletching_anims`. + * + * Cutting shares one animation; stringing and attaching each have one per bow tier and per + * bolt/dart metal. An output with no entry resolves to null and leaves the caller on [GENERIC], so + * a row can only ever be unanimated, never wrongly animated. + */ +internal object FletchingAnims { + /** + * Loops (`loops=15`, `maxloops=2`), so it covers a run of actions and has to be cancelled when + * the run ends. [GENERIC_SINGLE] is the same frames without the looping; every animation in the + * enum is likewise a one-shot. + */ + const val GENERIC: String = "seq.human_fletching" + + const val GENERIC_SINGLE: String = "seq.human_fletching_single" + + private val byOutputId: Map by lazy { + fletching_anims + .mapNotNull { (obj, seq) -> + seq?.let { obj.id to RSCM.getReverseMapping(RSCMType.SEQ, it.id) } + } + .toMap() + } + + fun forOutput(output: ItemServerType): String? = byOutputId[output.id] +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingDefinitions.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingDefinitions.kt new file mode 100644 index 000000000..b3e1d1d4d --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingDefinitions.kt @@ -0,0 +1,189 @@ +package org.rsmod.content.skills.fletching + +import dev.openrune.types.ItemServerType +import dev.openrune.types.StatType +import org.rsmod.api.table.Tuple2 +import org.rsmod.api.table.fletching.FletchingAssemblyRow +import org.rsmod.api.table.fletching.FletchingAttachingRow +import org.rsmod.api.table.fletching.FletchingCuttingRow +import org.rsmod.api.table.fletching.FletchingGemTipsRow +import org.rsmod.api.table.fletching.FletchingStringingRow +import org.rsmod.content.skills.Material +import org.rsmod.game.inv.Inventory + +/** + * The five generated row types have no common supertype, so each gets a one-line adapter into this + * shared builder rather than repeating the body five times. + * + * The tables store XP in tenths — see the header comment on Fletching.kt. This is the only place + * that divides; no script should ever read a row's xp property directly. + */ +private fun recipe( + input: List, + inputAmount: List, + output: ItemServerType, + outputAmount: Int, + statReq: List>, + xpTenths: Int, + ticks: Int, + category: String, + tool: ItemServerType?, +) = + FletchingRecipe( + inputs = + input.mapIndexed { i, item -> + Material(item.internalName, inputAmount.getOrElse(i) { 1 }) + }, + output = output, + outputAmount = outputAmount, + statReq = statReq, + xp = xpTenths / 10.0, + ticks = ticks, + category = category, + tool = tool?.internalName, + ) + +private fun FletchingCuttingRow.toRecipe() = + recipe(input, inputAmount, output, outputAmount, statReq, xp, ticks, category, tool) + +private fun FletchingGemTipsRow.toRecipe() = + recipe(input, inputAmount, output, outputAmount, statReq, xp, ticks, category, tool) + +private fun FletchingStringingRow.toRecipe() = + recipe(input, inputAmount, output, outputAmount, statReq, xp, ticks, category, tool = null) + +private fun FletchingAttachingRow.toRecipe() = + recipe(input, inputAmount, output, outputAmount, statReq, xp, ticks, category, tool = null) + +private fun FletchingAssemblyRow.toRecipe() = + recipe(input, inputAmount, output, outputAmount, statReq, xp, ticks, category, tool) + +object FletchingDefinitions { + val cutting: List = FletchingCuttingRow.all().map { it.toRecipe() } + val stringing: List = FletchingStringingRow.all().map { it.toRecipe() } + val attaching: List = FletchingAttachingRow.all().map { it.toRecipe() } + val gemTips: List = FletchingGemTipsRow.all().map { it.toRecipe() } + val assembly: List = FletchingAssemblyRow.all().map { it.toRecipe() } + + /** + * Any feather fletches bolts, darts and headless arrows; the table only carries obj.feather. + * The other seven were resolved by display-name lookup against the packed cache: the + * `hunting_*_feather` keys are named by biome, not by colour, so the mapping below is not + * derivable from spelling. + * - Feather -> id=314 -> obj.feather + * - Yellow feather -> id=10090 -> obj.hunting_desert_feather + * - Orange feather -> id=10091 -> obj.hunting_woodland_feather + * - Red feather -> id=10088 -> obj.hunting_jungle_feather + * - Blue feather -> id=10089 -> obj.hunting_polar_feather + * - Stripy feather -> id=10087 -> obj.hunting_stripy_bird_feather + * - Gryphon feather -> id=31235 -> obj.gryphon_feather + * - Stymphike feather -> id=33651 -> obj.stymphike_feather + */ + val feathers: Set = + setOf( + "obj.feather", + "obj.hunting_desert_feather", + "obj.hunting_woodland_feather", + "obj.hunting_jungle_feather", + "obj.hunting_polar_feather", + "obj.hunting_stripy_bird_feather", + "obj.gryphon_feather", + "obj.stymphike_feather", + ) + + /** + * Items that stand in for the tool a row names. Only the hammer has any, matching + * SmithingUtils.hasHammer - without them, requiring a hammer for crossbow assembly would lock + * out players carrying an Imcando one. + * + * The Forestry fletching knife is deliberately absent. It speeds fletching up but does not + * replace the knife: "the wielder will still need a knife in their inventory to use on logs to + * initiate the cutting". https://oldschool.runescape.wiki/w/Fletching_knife + */ + private val TOOL_VARIANTS: Map> = + mapOf( + "obj.hammer" to setOf("obj.hammer", "obj.imcando_hammer", "obj.imcando_hammer_offhand") + ) + + fun toolVariants(tool: String): Set = TOOL_VARIANTS[tool] ?: setOf(tool) +} + +/** + * Groups two-input recipes by the ordered item pair that triggers them. + * + * onOpHeldU is keyed per ordered pair and throws at boot on a duplicate or reversed registration, + * so callers must register one handler per key of this map and never per recipe. + * + * A feather input expands to all eight feather types, since any feather fletches bolts, darts and + * headless arrows and the table only carries obj.feather. + */ +fun pairsFor(recipes: List): Map, List> { + val byPair = mutableMapOf, MutableList>() + for (recipe in recipes) { + val names = recipe.inputs.map { it.obj.internalName } + if (names.size != 2) continue + for (first in variantsOf(names[0])) { + for (second in variantsOf(names[1])) { + byPair.getOrPut(first to second) { mutableListOf() }.add(recipe) + } + } + } + return byPair +} + +private fun variantsOf(item: String): Set = + if (item in FletchingDefinitions.feathers) FletchingDefinitions.feathers else setOf(item) + +/** + * Groups tool-driven recipes by the ordered pair that triggers them: the tool used on the recipe's + * first material. Any further materials are ordinary inventory requirements, which is what lets a + * blowpipe (two logs plus a squid beak) or a hunter's spear (three materials) share the same + * trigger shape as a plain log. + * + * Note this deliberately keys on the tool rather than on a material pair. A knife on teak logs + * therefore offers the teak stock and the hunter's spear from one menu, and a chisel on a sunlight + * antelope antler offers both its bolt tips and the crossbow upgrade, which is how those recipes + * are reached in game. + */ +fun byToolAndPrimary( + recipes: List +): Map, List> = + recipes + .mapNotNull { recipe -> + val tool = recipe.tool ?: return@mapNotNull null + (recipe.inputs.first().obj.internalName to tool) to recipe + } + .groupBy({ it.first }, { it.second }) + +/** + * Rewrites a canonical obj.feather input to whichever feather type the player actually holds. + * Recipes without a feather input are returned unchanged, so this is safe to apply to every recipe + * regardless of family. + * + * [preferred], when given, is the feather the player actually clicked to trigger this recipe (see + * `pairsFor`); if the player holds enough of it, it is used regardless of the fallback order below. + * This keeps the menu's candidate/max-count pass and the consume step substituting the same + * feather. + */ +fun FletchingRecipe.withHeldFeather(inv: Inventory, preferred: String? = null): FletchingRecipe { + val feather = inputs.firstOrNull { it.obj.internalName == "obj.feather" } ?: return this + if (preferred != null && inv.count(preferred) >= feather.count) { + return copy( + inputs = + inputs.map { + if (it.obj.internalName == "obj.feather") Material(preferred, it.count) else it + } + ) + } + // Plain feathers take priority whenever the player holds enough of them; this is checked + // explicitly rather than relying on "obj.feather" being iterated first in the set below. + if (inv.count("obj.feather") >= feather.count) return this + val held = + FletchingDefinitions.feathers.firstOrNull { inv.count(it) >= feather.count } ?: return this + return copy( + inputs = + inputs.map { + if (it.obj.internalName == "obj.feather") Material(held, it.count) else it + } + ) +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingQueues.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingQueues.kt new file mode 100644 index 000000000..ceb9321cc --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingQueues.kt @@ -0,0 +1,15 @@ +package org.rsmod.content.skills.fletching + +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +/** + * Owns the shared fletching production queue. Cutting, stringing, attaching, gem tips and assembly + * all queue onto [registerFletchingQueue] - it lives in its own script so no single family's + * removal or refactor can silently take the other four down with it. + */ +class FletchingQueues : PluginScript() { + override fun ScriptContext.startup() { + registerFletchingQueue() + } +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingRecipe.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingRecipe.kt new file mode 100644 index 000000000..34e2c5f8d --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/FletchingRecipe.kt @@ -0,0 +1,40 @@ +package org.rsmod.content.skills.fletching + +import dev.openrune.types.ItemServerType +import dev.openrune.types.StatType +import org.rsmod.api.table.Tuple2 +import org.rsmod.content.skills.Material +import org.rsmod.game.inv.Inventory + +/** + * One fletching recipe, normalised from whichever generated row type produced it. + * + * All five tables share this shape, which is what lets a single queued action drive every flow + * instead of one loop per family. + */ +data class FletchingRecipe( + val inputs: List, + val output: ItemServerType, + val outputAmount: Int, + val statReq: List>, + val xp: Double, + val ticks: Int, + val category: String, + /** Held, never consumed, so deliberately not one of [inputs]. Null when none is needed. */ + val tool: String? = null, +) + +fun FletchingRecipe.hasMaterials(inv: Inventory): Boolean = + inputs.all { inv.count(it.obj.internalName) >= it.count } + +fun FletchingRecipe.maxProducible(inv: Inventory): Int = + inputs.minOfOrNull { inv.count(it.obj.internalName) / it.count } ?: 0 + +/** + * The tool is held rather than consumed, so it is a presence check and never limits how many can be + * made - which is why it is not folded into [maxProducible]. + */ +fun FletchingRecipe.hasTool(inv: Inventory): Boolean { + val tool = tool ?: return true + return FletchingDefinitions.toolVariants(tool).any { inv.count(it) > 0 } +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/GemTipEvents.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/GemTipEvents.kt new file mode 100644 index 000000000..a93424002 --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/GemTipEvents.kt @@ -0,0 +1,15 @@ +package org.rsmod.content.skills.fletching + +import org.rsmod.api.script.onOpHeldU +import org.rsmod.content.skills.SkillingActionType +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +class GemTipEvents : PluginScript() { + + override fun ScriptContext.startup() { + for ((pair, recipes) in byToolAndPrimary(FletchingDefinitions.gemTips)) { + onOpHeldU(pair.first, pair.second) { openFletchMenu(recipes, SkillingActionType.CUT) } + } + } +} diff --git a/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/StringingEvents.kt b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/StringingEvents.kt new file mode 100644 index 000000000..3b8e015db --- /dev/null +++ b/content/skills/fletching/src/main/kotlin/org/rsmod/content/skills/fletching/StringingEvents.kt @@ -0,0 +1,17 @@ +package org.rsmod.content.skills.fletching + +import org.rsmod.api.script.onOpHeldU +import org.rsmod.content.skills.SkillingActionType +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +class StringingEvents : PluginScript() { + + override fun ScriptContext.startup() { + for ((pair, recipes) in pairsFor(FletchingDefinitions.stringing)) { + onOpHeldU(pair.first, pair.second) { + openFletchMenu(recipes, SkillingActionType.STRING) + } + } + } +} diff --git a/content/skills/fletching/src/main/resources/gamevals.toml b/content/skills/fletching/src/main/resources/gamevals.toml new file mode 100644 index 000000000..a3367c69b --- /dev/null +++ b/content/skills/fletching/src/main/resources/gamevals.toml @@ -0,0 +1,218 @@ +[gamevals.enum] +fletching_anims=65470 + +[gamevals.dbtable] +fletching_cutting=65501 +fletching_stringing=65500 +fletching_attaching=65499 +fletching_gem_tips=65498 +fletching_assembly=65497 + +[gamevals.queue] +fletching_produce = 61 + +[gamevals.dbrow] +fletching_arrow_shaft_from_logs=63893 +fletching_brew_scrapey_bark_from_brew_scrapey_logs=63892 +fletching_seeking_arrow_shaft_from_sap_treated_logs=63891 +fletching_javelin_shaft_from_logs=63890 +fletching_ogre_arrow_shaft_from_achey_tree_logs=63889 +fletching_unstrung_shortbow_from_logs=63888 +fletching_xbows_crossbow_stock_wood_from_logs=63887 +fletching_unstrung_longbow_from_logs=63886 +fletching_arrow_shaft_from_oak_logs=63885 +fletching_seeking_arrow_shaft_from_sap_treated_oak_logs=63884 +fletching_unstrung_oak_shortbow_from_oak_logs=63883 +fletching_xbows_crossbow_stock_oak_from_oak_logs=63882 +fletching_unstrung_oak_longbow_from_oak_logs=63881 +fletching_oak_shield_from_oak_logs=63880 +fletching_arrow_shaft_from_willow_logs=63879 +fletching_seeking_arrow_shaft_from_sap_treated_willow_logs=63878 +fletching_unstrung_willow_shortbow_from_willow_logs=63877 +fletching_xbows_crossbow_stock_willow_from_willow_logs=63876 +fletching_unstrung_willow_longbow_from_willow_logs=63875 +fletching_willow_shield_from_willow_logs=63874 +fletching_arrow_shaft_from_maple_logs=63873 +fletching_seeking_arrow_shaft_from_sap_treated_maple_logs=63872 +fletching_xbows_crossbow_stock_teak_from_teak_logs=63871 +fletching_unstrung_maple_shortbow_from_maple_logs=63870 +fletching_xbows_crossbow_stock_maple_from_maple_logs=63869 +fletching_unstrung_maple_longbow_from_maple_logs=63868 +fletching_maple_shield_from_maple_logs=63867 +fletching_arrow_shaft_from_yew_logs=63866 +fletching_seeking_arrow_shaft_from_sap_treated_yew_logs=63865 +fletching_xbows_crossbow_stock_mahogany_from_mahogany_logs=63864 +fletching_unstrung_yew_shortbow_from_yew_logs=63863 +fletching_xbows_crossbow_stock_yew_from_yew_logs=63862 +fletching_blisterwood_stake_from_blisterwood_logs=63861 +fletching_unstrung_yew_longbow_from_yew_logs=63860 +fletching_yew_shield_from_yew_logs=63859 +fletching_atlatl_dart_shaft_from_ent_branch=63858 +fletching_arrow_shaft_from_magic_logs=63857 +fletching_seeking_arrow_shaft_from_sap_treated_magic_logs=63856 +fletching_xbows_crossbow_stock_magic_from_magic_logs=63855 +fletching_unstrung_magic_shortbow_from_magic_logs=63854 +fletching_unstrung_magic_longbow_from_magic_logs=63853 +fletching_magic_shield_from_magic_logs=63852 +fletching_arrow_shaft_from_redwood_logs=63851 +fletching_redwood_hiking_staff_from_redwood_logs=63850 +fletching_seeking_arrow_shaft_from_sap_treated_redwood_logs=63849 +fletching_redwood_shield_from_redwood_logs=63848 +fletching_shortbow_from_bow_string=63847 +fletching_xbows_crossbow_bronze_from_xbows_crossbow_unstrung_bronze=63846 +fletching_longbow_from_bow_string=63845 +fletching_oak_shortbow_from_bow_string=63844 +fletching_xbows_crossbow_blurite_from_xbows_crossbow_unstrung_blurite=63843 +fletching_oak_longbow_from_bow_string=63842 +fletching_zogre_bow_from_bow_string=63841 +fletching_willow_shortbow_from_bow_string=63840 +fletching_xbows_crossbow_iron_from_xbows_crossbow_string=63839 +fletching_willow_longbow_from_bow_string=63838 +fletching_xbows_crossbow_steel_from_xbows_crossbow_string=63837 +fletching_devious_bowsword_from_bow_string=63836 +fletching_maple_shortbow_from_bow_string=63835 +fletching_xbows_crossbow_mithril_from_xbows_crossbow_string=63834 +fletching_maple_longbow_from_bow_string=63833 +fletching_xbows_crossbow_adamantite_from_xbows_crossbow_unstrung_adamantite=63832 +fletching_yew_shortbow_from_bow_string=63831 +fletching_xbows_crossbow_runite_from_xbows_crossbow_string=63830 +fletching_yew_longbow_from_bow_string=63829 +fletching_xbows_crossbow_dragon_from_xbows_crossbow_string=63828 +fletching_magic_shortbow_from_bow_string=63827 +fletching_magic_longbow_from_bow_string=63826 +fletching_bronze_arrow_from_bronze_arrowheads=63825 +fletching_headless_arrow_from_arrow_shaft=63824 +fletching_seeking_adamant_arrow_from_adamant_arrow=63823 +fletching_seeking_amethyst_arrow_from_amethyst_arrow=63822 +fletching_seeking_slayer_broad_arrows_from_slayer_broad_arrows=63821 +fletching_seeking_bronze_arrow_from_bronze_arrowheads=63820 +fletching_seeking_bronze_arrow_from_bronze_arrow=63819 +fletching_seeking_dragon_arrow_from_bucket_of_bloodwood_sap=63818 +fletching_seeking_headless_arrow_from_feather=63817 +fletching_seeking_iron_arrow_from_bucket_of_bloodwood_sap=63816 +fletching_seeking_mithril_arrow_from_bucket_of_bloodwood_sap=63815 +fletching_seeking_rune_arrow_from_bucket_of_bloodwood_sap=63814 +fletching_seeking_steel_arrow_from_bucket_of_bloodwood_sap=63813 +fletching_bronze_javelin_from_bronze_javelin_head=63812 +fletching_ogre_headless_arrow_from_feather=63811 +fletching_bolt_from_xbows_crossbow_bolts_bronze_unfeathered=63810 +fletching_bronze_dart_from_bronze_dart_tip=63809 +fletching_thprotodart_from_feather=63808 +fletching_opal_bolt_from_bolt=63807 +fletching_iron_arrow_from_headless_arrow=63806 +fletching_seeking_iron_arrow_from_iron_arrowheads=63805 +fletching_seeking_steel_arrow_from_seeking_headless_arrow=63804 +fletching_iron_javelin_from_iron_javelin_head=63803 +fletching_iron_dart_from_feather=63802 +fletching_xbows_crossbow_bolts_blurite_from_xbows_crossbow_bolts_blurite_unfeathered=63801 +fletching_xbows_crossbow_bolts_blurite_tipped_jade_from_xbows_crossbow_bolts_blurite=63800 +fletching_steel_arrow_from_headless_arrow=63799 +fletching_steel_javelin_from_javelin_shaft=63798 +fletching_steel_dart_from_feather=63797 +fletching_xbows_crossbow_bolts_iron_from_feather=63796 +fletching_pearl_bolt_from_xbows_crossbow_bolts_iron=63795 +fletching_xbows_crossbow_bolts_silver_from_feather=63794 +fletching_mithril_arrow_from_headless_arrow=63793 +fletching_seeking_mithril_arrow_from_mithril_arrowheads=63792 +fletching_xbows_crossbow_bolts_steel_from_feather=63791 +fletching_mithril_javelin_from_javelin_shaft=63790 +fletching_xbows_crossbow_bolts_steel_tipped_redtopaz_from_xbows_crossbow_bolts_steel=63789 +fletching_barbed_bolt_from_barbed_bolttips=63788 +fletching_slayer_broad_arrows_from_slayer_broad_arrowhead=63787 +fletching_mithril_dart_from_feather=63786 +fletching_seeking_slayer_broad_arrows_from_slayer_broad_arrowhead=63785 +fletching_xbows_crossbow_bolts_mithril_from_feather=63784 +fletching_slayer_broad_bolt_from_feather=63783 +fletching_xbows_crossbow_bolts_mithril_tipped_sapphire_from_xbows_crossbow_bolts_mithril=63782 +fletching_xbows_crossbow_bolts_mithril_tipped_emerald_from_xbows_bolt_tips_emerald=63781 +fletching_adamant_arrow_from_adamant_arrowheads=63780 +fletching_seeking_adamant_arrow_from_adamant_arrowheads=63779 +fletching_xbows_crossbow_bolts_adamantite_from_xbows_crossbow_bolts_adamantite_unfeathered=63778 +fletching_adamant_javelin_from_adamant_javelin_head=63777 +fletching_xbows_crossbow_bolts_adamantite_tipped_ruby_from_xbows_crossbow_bolts_adamantite=63776 +fletching_xbows_crossbow_bolts_adamantite_tipped_diamond_from_xbows_crossbow_bolts_adamantite=63775 +fletching_adamant_dart_from_adamant_dart_tip=63774 +fletching_xbows_crossbow_bolts_runite_from_feather=63773 +fletching_xbows_crossbow_bolts_runite_tipped_dragonstone_from_xbows_bolt_tips_dragonstone=63772 +fletching_xbows_crossbow_bolts_runite_tipped_onyx_from_xbows_bolt_tips_onyx=63771 +fletching_atlatl_dart_from_atlatl_dart_tips=63770 +fletching_headless_atlatl_dart_from_atlatl_dart_shaft=63769 +fletching_rune_arrow_from_headless_arrow=63768 +fletching_seeking_rune_arrow_from_rune_arrowheads=63767 +fletching_slayer_broad_bolt_amethyst_from_xbows_bolt_tips_amethyst=63766 +fletching_rune_javelin_from_javelin_shaft=63765 +fletching_rune_dart_from_feather=63764 +fletching_amethyst_arrow_from_amethyst_arrowheads=63763 +fletching_seeking_amethyst_arrow_from_amethyst_arrowheads=63762 +fletching_amethyst_javelin_from_amethyst_javelin_head=63761 +fletching_dragon_bolts_unenchanted_diamond_from_xbows_bolt_tips_diamond=63760 +fletching_dragon_bolts_from_dragon_bolts_unfeathered=63759 +fletching_dragon_bolts_unenchanted_dragonstone_from_dragon_bolts=63758 +fletching_dragon_bolts_unenchanted_emerald_from_dragon_bolts=63757 +fletching_dragon_bolts_unenchanted_jade_from_dragon_bolts=63756 +fletching_dragon_bolts_unenchanted_onyx_from_dragon_bolts=63755 +fletching_dragon_bolts_unenchanted_opal_from_dragon_bolts=63754 +fletching_dragon_bolts_unenchanted_pearl_from_dragon_bolts=63753 +fletching_dragon_bolts_unenchanted_ruby_from_dragon_bolts=63752 +fletching_dragon_bolts_unenchanted_sapphire_from_dragon_bolts=63751 +fletching_dragon_bolts_unenchanted_topaz_from_dragon_bolts=63750 +fletching_amethyst_dart_from_amethyst_dart_tip=63749 +fletching_dragon_arrow_from_dragon_arrowheads=63748 +fletching_seeking_dragon_arrow_from_dragon_arrowheads=63747 +fletching_dragon_javelin_from_dragon_javelin_head=63746 +fletching_dragon_dart_from_dragon_dart_tip=63745 +fletching_wolfbone_arrowheads_from_wolf_bones=63744 +fletching_opal_bolttips_from_opal=63743 +fletching_xbows_bolt_tips_jade_from_jade=63742 +fletching_huntingbow_bolts_from_huntingbeast_spike=63741 +fletching_pearl_bolttips_from_smalloysterpearls=63740 +fletching_pearl_bolttips_from_bigoysterpearls=63739 +fletching_huntingbow_bigbolts_from_huntingbeast_bigspike=63738 +fletching_xbows_bolt_tips_redtopaz_from_red_topaz=63737 +fletching_xbows_bolt_tips_sapphire_from_sapphire=63736 +fletching_xbows_bolt_tips_emerald_from_emerald=63735 +fletching_sunlight_antelope_bolt_from_hunting_antelopesun_horn=63734 +fletching_xbows_bolt_tips_ruby_from_ruby=63733 +fletching_xbows_bolt_tips_diamond_from_diamond=63732 +fletching_xbows_bolt_tips_dragonstone_from_dragonstone=63731 +fletching_moonlight_antelope_bolt_from_hunting_antelopemoon_horn=63730 +fletching_xbows_bolt_tips_onyx_from_onyx=63729 +fletching_atlatl_dart_tips_from_custodian_broken_antler=63728 +fletching_toxic_blowpipe_from_blowpipe_fang=63727 +fletching_ogre_arrow_from_ogre_headless_arrow=63726 +fletching_zogre_brutal_bronze_from_nails_bronze=63725 +fletching_xbows_crossbow_unstrung_bronze_from_xbows_crossbow_limbs_bronze=63724 +fletching_zogre_brutal_iron_from_ogre_headless_arrow=63723 +fletching_xbows_crossbow_unstrung_blurite_from_xbows_crossbow_limbs_blurite=63722 +fletching_zogre_brutal_steel_from_ogre_headless_arrow=63721 +fletching_zogre_brutal_black_from_nails_black=63720 +fletching_xbows_crossbow_unstrung_iron_from_xbows_crossbow_limbs_iron=63719 +fletching_xbows_crossbow_unstrung_steel_from_xbows_crossbow_limbs_steel=63718 +fletching_zogre_brutal_mithril_from_ogre_headless_arrow=63717 +fletching_xbows_crossbow_unstrung_mithril_from_xbows_crossbow_stock_maple=63716 +fletching_xbows_crossbow_unstrung_adamantite_from_xbows_crossbow_limbs_adamantite=63715 +fletching_zogre_brutal_adamant_from_nails_adamant=63714 +fletching_xbows_crossbow_unstrung_runite_from_xbows_crossbow_limbs_runite=63713 +fletching_zogre_brutal_rune_from_ogre_headless_arrow=63712 +fletching_xbows_crossbow_unstrung_dragon_from_xbows_crossbow_limbs_dragon=63711 +fletching_seeking_adamant_arrow_from_adamant_arrow_x150=63710 +fletching_seeking_amethyst_arrow_from_amethyst_arrow_x150=63709 +fletching_seeking_slayer_broad_arrows_from_slayer_broad_arrows_x150=63708 +fletching_seeking_bronze_arrow_from_bronze_arrow_x150=63707 +fletching_seeking_dragon_arrow_from_dragon_arrow_x150=63706 +fletching_seeking_iron_arrow_from_iron_arrow_x150=63705 +fletching_seeking_mithril_arrow_from_mithril_arrow_x150=63704 +fletching_seeking_rune_arrow_from_rune_arrow_x150=63703 +fletching_seeking_steel_arrow_from_steel_arrow_x150=63702 +fletching_unstrung_zogre_bow_from_achey_tree_logs=63701 +fletching_battlestaff_from_celastrus_wood=63700 +fletching_vmq4_root_kindling_from_vmq4_roots=63699 +fletching_greenman_statue_from_maple_logs=63698 +fletching_camphor_blowpipe_empty_from_camphor_logs=63697 +fletching_blisterwood_sickle_from_blisterwood_logs=63696 +fletching_hg_hunter_spear_from_teak_logs=63695 +fletching_blisterwood_sickle_enhanced_from_blisterwood_logs=63694 +fletching_ironwood_blowpipe_empty_from_ironwood_logs=63693 +fletching_greenman_wall_decoration_from_yew_logs=63692 +fletching_rosewood_blowpipe_empty_from_rosewood_logs=63691 +fletching_hunting_crossbow_sunlight_from_hunting_antelopesun_horn=63690