From 002176cff6b383bc0d0aeea7582959458dc3ed87 Mon Sep 17 00:00:00 2001 From: voidstarr <3334013+voidstarr@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:28:09 -0800 Subject: [PATCH 1/4] fix QueueTask.produceItemBox as the cs2 script has been updated in rev 235 --- .../kotlin/org/alter/api/ext/QueueTaskExt.kt | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/game-api/src/main/kotlin/org/alter/api/ext/QueueTaskExt.kt b/game-api/src/main/kotlin/org/alter/api/ext/QueueTaskExt.kt index ff664ca..fbaeb32 100644 --- a/game-api/src/main/kotlin/org/alter/api/ext/QueueTaskExt.kt +++ b/game-api/src/main/kotlin/org/alter/api/ext/QueueTaskExt.kt @@ -20,6 +20,7 @@ import org.alter.game.pluginnew.event.impl.DialogMessageOption import org.alter.game.pluginnew.event.impl.DialogNpcOpen import org.alter.game.pluginnew.event.impl.DialogPlayerOpen import org.alter.game.pluginnew.event.impl.DialogSkillMulti +import org.alter.game.pluginnew.event.EventManager import org.alter.rscm.RSCM.asRSCM @@ -273,22 +274,44 @@ suspend fun QueueTask.doubleItemMessageBox( * Prompts the player with skill menu for making things. * * @param items - * The possible [Item] products the menu presents as options. - * Note| max is 10 + * The possible product item ids the menu presents as options. + * This UI supports up to 18 items (components a..r). Extra items are ignored. * * @param title - * Title String to display atop the prompt. + * Title string to display atop the prompt. * * @param maxProducable * The possible number of products which could be made from the available input mats. * Note| defaults to full inventory as being possible * * @param logic - * The logic to be executed upon response using selected [Item] from prompt - * and quantity as indicated by response slot message. + * Callback invoked with `(selectedItemId, quantity)` after the player responds. + * + * ## ClientScript contract + * This function is a server-side wrapper around the clientscript `skillmulti_setup`. + * The reference dump for the script (signature + behaviour) is: + * https://github.com/Joshua-F/osrs-dumps/blob/ef7ba91167f84b05792373056ad4c9d374041394/script/%5Bclientscript%2Cskillmulti_setup%5D.cs2 + * + * The script expects arguments in this shape: + * - `int0`: mode/type (we pass `0`) + * - `string0`: payload formatted as `Title|Name1|Name2|...` + * - `int1`: max producible (used for quantity button setup/clamping) + * - `obj2..obj19`: up to 18 item ids; unused obj slots should be sent as `-1` (treated as null obj) + * - `int20`: suggested quantity + * + * If the argument count/order is wrong, the interface can open but appear blank. + * + * ## Ordering / race avoidance + * The interface must be opened client-side before `skillmulti_setup` runs. Since dialog open events + * are posted through an async event bus, we open the interface synchronously (`postAndWait`) to + * avoid races where the script runs before the modal exists. + * + * ## Selection mapping + * Mapping from the incoming resume packet (`ResumePauseButton.componentId`) to an item index is + * revision-dependent. The current implementation assumes item components start at child id `15`. * * @return - * The id of the option chosen. The id can range from [1] inclusive to [9] inclusive. + * The selected item id and quantity are delivered to [logic]. */ suspend fun QueueTask.produceItemBox( player: Player, @@ -298,24 +321,49 @@ suspend fun QueueTask.produceItemBox( logic: Player.(Int, Int) -> Unit, ) { - val itemDefs = items.map { getItem(it) } + // `skillmulti_setup` (CS2) takes 18 obj slots (obj2..obj19) and a trailing suggested quantity (int20). + // If we call it with the wrong argument count/order, the clientscript can fail and the interface appears blank. + val maxSelectable = 18 + val scriptSlots = 18 + + val displayedItems = items.take(maxSelectable) + val displayedDefs = displayedItems.mapNotNull { itemId -> getItem(itemId)?.let { def -> itemId to def } } + if (displayedDefs.isEmpty()) { + player.message("You can't think of any options.") + return + } val baseChild = 15 - val itemArray = Array(15) { -1 } - val nameArray = Array(15) { "|" } + val itemArray = Array(scriptSlots) { -1 } + displayedDefs.withIndex().forEach { (index, pair) -> + itemArray[index] = pair.second.id + } - itemDefs.withIndex().forEach { - val def = it.value - itemArray[it.index] = def!!.id - nameArray[it.index] = "|${def.name}" + val nameString = buildString { + append(title) + displayedDefs.forEach { (_, def) -> + append("|") + append(def.name) + } } + val suggestedQuantity = 1 + player.runClientScript(CommonClientScripts.CHATBOX_RESET_BACKGROUND) player.sendTempVarbit("varbits.chatmodal_unclamp", 1) - DialogSkillMulti(player).post() + // Open the interface synchronously; the async event bus can otherwise race and + // run the clientscript before the interface exists client-side. + EventManager.postAndWait(DialogSkillMulti(player)) - player.runClientScript(CommonClientScripts.SKILL_MULTI_SETUP, 0, "$title${nameArray.joinToString("")}", maxProducable, *itemArray) + player.runClientScript( + CommonClientScripts.SKILL_MULTI_SETUP, + 0, + nameString, + maxProducable, + *itemArray, + suggestedQuantity + ) terminateAction = closeDialog(player) waitReturnValue() @@ -325,11 +373,11 @@ suspend fun QueueTask.produceItemBox( val child = msg.componentId - if (child < baseChild || child >= baseChild + items.size) { + if (child < baseChild || child >= baseChild + displayedDefs.size) { return } - val item = items[child - baseChild] + val item = displayedDefs[child - baseChild].first val qty = msg.sub logic(player, item, qty) From c7ea64eb4e91bd436db6a071aa3c1aba12147d81 Mon Sep 17 00:00:00 2001 From: voidstarr <3334013+voidstarr@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:29:00 -0800 Subject: [PATCH 2/4] ignore .vscode/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7f6d689..8455513 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.gradle/ +.vscode/ build/ g=out/ gen/ From 57232ea923b531a5b4de85f692de6b9ffc2e1ee9 Mon Sep 17 00:00:00 2001 From: voidstarr <3334013+voidstarr@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:29:42 -0800 Subject: [PATCH 3/4] allow for the settings interface to close --- .../interfaces/settings/SettingsSideEvents.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/content/src/main/kotlin/org/alter/interfaces/settings/SettingsSideEvents.kt b/content/src/main/kotlin/org/alter/interfaces/settings/SettingsSideEvents.kt index 35b0a3f..b2018db 100644 --- a/content/src/main/kotlin/org/alter/interfaces/settings/SettingsSideEvents.kt +++ b/content/src/main/kotlin/org/alter/interfaces/settings/SettingsSideEvents.kt @@ -6,6 +6,7 @@ import org.alter.game.model.entity.Player import org.alter.game.pluginnew.PluginEvent import org.alter.game.pluginnew.event.impl.onButton import org.alter.game.pluginnew.event.impl.onIfOpen +import org.alter.interfaces.ifCloseOverlay import org.alter.interfaces.ifOpenOverlay import org.alter.interfaces.ifSetEvents import org.alter.interfaces.settings.configs.setting_components @@ -16,6 +17,11 @@ class SettingsSideScript : PluginEvent() { override fun init() { onIfOpen("interfaces.settings_side") { player.updateIfEvents() } + onIfOpen("interfaces.settings") { + player.ifSetEvents("components.settings:close", 0..0, IfEvent.Op1) + player.ifSetEvents("components.settings:dropdown_close", 0..0, IfEvent.Op1) + } + SettingsTabView.entries.forEach { onButton(it.component) { player.setVarbit("varbits.settings_side_panel_tab",it.varValue) @@ -25,6 +31,14 @@ class SettingsSideScript : PluginEvent() { onButton(setting_components.settings_open) { player.ifOpenOverlay("interfaces.settings") } + + onButton("components.settings:close") { + player.ifCloseOverlay("interfaces.settings") + } + + onButton("components.settings:dropdown_close") { + player.ifCloseOverlay("interfaces.settings") + } } private fun Player.updateIfEvents() { From e8f253c85991cf109ac8605ae8a5e43d0a71689c Mon Sep 17 00:00:00 2001 From: voidstarr <3334013+voidstarr@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:31:08 -0800 Subject: [PATCH 4/4] cooking: start initial skill implementation - Add cooking recipe db table and pack it into cache tables - Implement cooking interactions for ranges/fires (cook click + item-on-object) - Show Make-X selection and process cooking loop (burn chance + xp) - Add simple recipe registry (lookup by raw/cooked) - Add DEV-only ::cooktest presets for quick testing - Define gamevals for cooking table + initial dbrows --- cache/src/main/kotlin/org/alter/CacheTools.kt | 2 + .../kotlin/org/alter/impl/skills/Cooking.kt | 147 ++++++++++++ .../developer/CookingTestCommandsPlugin.kt | 126 ++++++++++ .../org/alter/skills/cooking/CookingEvents.kt | 223 ++++++++++++++++++ .../alter/skills/cooking/CookingRecipes.kt | 11 + .../org/alter/skills/cooking/gamevals.toml | 16 ++ 6 files changed, 525 insertions(+) create mode 100644 cache/src/main/kotlin/org/alter/impl/skills/Cooking.kt create mode 100644 content/src/main/kotlin/org/alter/commands/developer/CookingTestCommandsPlugin.kt create mode 100644 content/src/main/kotlin/org/alter/skills/cooking/CookingEvents.kt create mode 100644 content/src/main/kotlin/org/alter/skills/cooking/CookingRecipes.kt create mode 100644 content/src/main/resources/org/alter/skills/cooking/gamevals.toml diff --git a/cache/src/main/kotlin/org/alter/CacheTools.kt b/cache/src/main/kotlin/org/alter/CacheTools.kt index 2af03a5..46307e3 100644 --- a/cache/src/main/kotlin/org/alter/CacheTools.kt +++ b/cache/src/main/kotlin/org/alter/CacheTools.kt @@ -18,6 +18,7 @@ import org.alter.codegen.startGeneration import org.alter.gamevals.GameValProvider import org.alter.gamevals.GamevalDumper import org.alter.impl.GameframeTable +import org.alter.impl.skills.Cooking import org.alter.impl.skills.Firemaking import org.alter.impl.misc.FoodTable import org.alter.impl.skills.PrayerTable @@ -43,6 +44,7 @@ fun tablesToPack() = listOf( TeleTabs.teleTabs(), StatComponents.statsComponents(), FoodTable.consumableFood(), + Cooking.recipes(), Firemaking.logs(), Woodcutting.trees(), Woodcutting.axes(), diff --git a/cache/src/main/kotlin/org/alter/impl/skills/Cooking.kt b/cache/src/main/kotlin/org/alter/impl/skills/Cooking.kt new file mode 100644 index 0000000..a1feab7 --- /dev/null +++ b/cache/src/main/kotlin/org/alter/impl/skills/Cooking.kt @@ -0,0 +1,147 @@ +package org.alter.impl.skills + +import dev.openrune.definition.dbtables.dbTable +import dev.openrune.definition.util.VarType + +object Cooking { + + const val COL_RAW = 0 + const val COL_COOKED = 1 + const val COL_BURNT = 2 + const val COL_LEVEL = 3 + const val COL_XP = 4 + const val COL_STOP_BURN_FIRE = 5 + const val COL_STOP_BURN_RANGE = 6 + + fun recipes() = dbTable("tables.cooking_recipes") { + + column("raw", COL_RAW, VarType.OBJ) + column("cooked", COL_COOKED, VarType.OBJ) + column("burnt", COL_BURNT, VarType.OBJ) + column("level", COL_LEVEL, VarType.INT) + column("xp", COL_XP, VarType.INT) + column("stop_burn_fire", COL_STOP_BURN_FIRE, VarType.INT) + column("stop_burn_range", COL_STOP_BURN_RANGE, VarType.INT) + + // Basic fish & meat (initial skill implementation) + row("dbrows.cooking_shrimps") { + columnRSCM(COL_RAW, "items.raw_shrimp") + columnRSCM(COL_COOKED, "items.shrimp") + columnRSCM(COL_BURNT, "items.burnt_shrimp") + column(COL_LEVEL, 1) + column(COL_XP, 30) + column(COL_STOP_BURN_FIRE, 34) + column(COL_STOP_BURN_RANGE, 33) + } + + row("dbrows.cooking_anchovies") { + columnRSCM(COL_RAW, "items.raw_anchovies") + columnRSCM(COL_COOKED, "items.anchovies") + columnRSCM(COL_BURNT, "items.burntfish1") + column(COL_LEVEL, 1) + column(COL_XP, 30) + column(COL_STOP_BURN_FIRE, 34) + column(COL_STOP_BURN_RANGE, 33) + } + + row("dbrows.cooking_sardine") { + columnRSCM(COL_RAW, "items.raw_sardine") + columnRSCM(COL_COOKED, "items.sardine") + columnRSCM(COL_BURNT, "items.burntfish5") + column(COL_LEVEL, 1) + column(COL_XP, 40) + column(COL_STOP_BURN_FIRE, 38) + column(COL_STOP_BURN_RANGE, 37) + } + + row("dbrows.cooking_herring") { + columnRSCM(COL_RAW, "items.raw_herring") + columnRSCM(COL_COOKED, "items.herring") + columnRSCM(COL_BURNT, "items.burntfish3") + column(COL_LEVEL, 5) + column(COL_XP, 50) + column(COL_STOP_BURN_FIRE, 41) + column(COL_STOP_BURN_RANGE, 40) + } + + row("dbrows.cooking_mackerel") { + columnRSCM(COL_RAW, "items.raw_mackerel") + columnRSCM(COL_COOKED, "items.mackerel") + columnRSCM(COL_BURNT, "items.burntfish3") + column(COL_LEVEL, 10) + column(COL_XP, 60) + column(COL_STOP_BURN_FIRE, 44) + column(COL_STOP_BURN_RANGE, 43) + } + + row("dbrows.cooking_trout") { + columnRSCM(COL_RAW, "items.raw_trout") + columnRSCM(COL_COOKED, "items.trout") + columnRSCM(COL_BURNT, "items.burntfish2") + column(COL_LEVEL, 15) + column(COL_XP, 70) + column(COL_STOP_BURN_FIRE, 50) + column(COL_STOP_BURN_RANGE, 49) + } + + row("dbrows.cooking_salmon") { + columnRSCM(COL_RAW, "items.raw_salmon") + columnRSCM(COL_COOKED, "items.salmon") + columnRSCM(COL_BURNT, "items.burntfish2") + column(COL_LEVEL, 25) + column(COL_XP, 90) + column(COL_STOP_BURN_FIRE, 58) + column(COL_STOP_BURN_RANGE, 57) + } + + row("dbrows.cooking_lobster") { + columnRSCM(COL_RAW, "items.raw_lobster") + columnRSCM(COL_COOKED, "items.lobster") + columnRSCM(COL_BURNT, "items.burnt_lobster") + column(COL_LEVEL, 40) + column(COL_XP, 120) + column(COL_STOP_BURN_FIRE, 74) + column(COL_STOP_BURN_RANGE, 64) + } + + row("dbrows.cooking_swordfish") { + columnRSCM(COL_RAW, "items.raw_swordfish") + columnRSCM(COL_COOKED, "items.swordfish") + columnRSCM(COL_BURNT, "items.burnt_swordfish") + column(COL_LEVEL, 45) + column(COL_XP, 140) + column(COL_STOP_BURN_FIRE, 86) + column(COL_STOP_BURN_RANGE, 80) + } + + row("dbrows.cooking_shark") { + columnRSCM(COL_RAW, "items.raw_shark") + columnRSCM(COL_COOKED, "items.shark") + columnRSCM(COL_BURNT, "items.burnt_shark") + column(COL_LEVEL, 80) + column(COL_XP, 210) + column(COL_STOP_BURN_FIRE, 99) + column(COL_STOP_BURN_RANGE, 94) + } + + row("dbrows.cooking_beef") { + columnRSCM(COL_RAW, "items.raw_beef") + columnRSCM(COL_COOKED, "items.cooked_meat") + columnRSCM(COL_BURNT, "items.burnt_meat") + column(COL_LEVEL, 1) + column(COL_XP, 30) + column(COL_STOP_BURN_FIRE, 33) + column(COL_STOP_BURN_RANGE, 32) + } + + row("dbrows.cooking_chicken") { + columnRSCM(COL_RAW, "items.raw_chicken") + columnRSCM(COL_COOKED, "items.cooked_chicken") + columnRSCM(COL_BURNT, "items.burnt_chicken") + column(COL_LEVEL, 1) + column(COL_XP, 30) + column(COL_STOP_BURN_FIRE, 33) + column(COL_STOP_BURN_RANGE, 32) + } + } +} diff --git a/content/src/main/kotlin/org/alter/commands/developer/CookingTestCommandsPlugin.kt b/content/src/main/kotlin/org/alter/commands/developer/CookingTestCommandsPlugin.kt new file mode 100644 index 0000000..199b929 --- /dev/null +++ b/content/src/main/kotlin/org/alter/commands/developer/CookingTestCommandsPlugin.kt @@ -0,0 +1,126 @@ +package org.alter.commands.developer + +import org.alter.api.Skills +import org.alter.api.ext.message +import org.alter.game.model.priv.Privilege +import org.alter.game.model.Tile +import org.alter.game.model.move.moveTo +import org.alter.game.pluginnew.PluginEvent +import org.alter.game.pluginnew.event.impl.CommandEvent +import org.alter.rscm.RSCM.asRSCM + +class CookingTestCommandsPlugin : PluginEvent() { + + private enum class CookTestSet(val key: String) { + LOW("low"), + MID("mid"), + HIGH("high"), + MEAT("meat"), + ALL("all") + } + + private data class CookTestPreset( + val key: String, + val set: CookTestSet, + val level: Int, + val qty: Int, + ) + + private val presets = listOf( + CookTestPreset(key = "low", set = CookTestSet.LOW, level = 1, qty = 5), + CookTestPreset(key = "mid", set = CookTestSet.MID, level = 25, qty = 5), + CookTestPreset(key = "high", set = CookTestSet.HIGH, level = 80, qty = 3), + CookTestPreset(key = "meat", set = CookTestSet.MEAT, level = 1, qty = 10), + CookTestPreset(key = "all", set = CookTestSet.ALL, level = 80, qty = 1), + ) + + override fun init() { + on { + where { + command.equals("cooktest", ignoreCase = true) && + player.world.privileges.isEligible(player.privilege, Privilege.DEV_POWER) + } + then { + val args = args.orEmpty().map { it.trim() }.filter { it.isNotEmpty() } + + val preset = parseCookTestPreset(player, args) + if (preset == null) { + sendCookTestHelp(player) + return@then + } + + player.getSkills().setBaseLevel(Skills.COOKING, preset.level) + + val keys = keysFor(preset.set) + if (keys.isEmpty()) { + player.message("No items configured for preset '${preset.key}'.") + return@then + } + + var addedAny = false + var failed = 0 + for (key in keys) { + val id = runCatching { key.asRSCM() }.getOrNull() ?: continue + val result = player.inventory.add(id, preset.qty) + if (result.hasSucceeded()) { + addedAny = true + } else { + failed++ + } + } + + if (!addedAny) { + player.message("Couldn't add any items (inventory full?).") + } else if (failed > 0) { + player.message("Added cooking test items (some didn't fit: $failed).") + } else { + player.message("Applied cooktest preset '${preset.key}' (x${preset.qty} each, Cooking=${preset.level}).") + } + } + } + } + + private fun parseCookTestPreset(player: org.alter.game.model.entity.Player, args: List): CookTestPreset? { + if (args.isEmpty() || args.any { it.equals("help", ignoreCase = true) || it == "-h" || it == "--help" }) { + return null + } + + val token = args.first().removePrefix("--").trim().lowercase() + return presets.firstOrNull { it.key == token } + } + + private fun keysFor(set: CookTestSet): List = + when (set) { + CookTestSet.LOW -> listOf( + "items.raw_shrimp", + "items.raw_anchovies", + "items.raw_sardine", + "items.raw_herring" + ) + + CookTestSet.MID -> listOf( + "items.raw_mackerel", + "items.raw_trout", + "items.raw_salmon" + ) + + CookTestSet.HIGH -> listOf( + "items.raw_lobster", + "items.raw_swordfish", + "items.raw_shark" + ) + + CookTestSet.MEAT -> listOf( + "items.raw_beef", + "items.raw_chicken" + ) + + CookTestSet.ALL -> keysFor(CookTestSet.LOW) + keysFor(CookTestSet.MID) + keysFor(CookTestSet.HIGH) + keysFor(CookTestSet.MEAT) + } + + private fun sendCookTestHelp(player: org.alter.game.model.entity.Player) { + player.message("Usage: ::cooktest ") + player.message("Presets: ${presets.joinToString(", ") { it.key }}") + player.message("Examples: ::cooktest low | ::cooktest mid | ::cooktest high | ::cooktest meat | ::cooktest all") + } +} diff --git a/content/src/main/kotlin/org/alter/skills/cooking/CookingEvents.kt b/content/src/main/kotlin/org/alter/skills/cooking/CookingEvents.kt new file mode 100644 index 0000000..d946a9b --- /dev/null +++ b/content/src/main/kotlin/org/alter/skills/cooking/CookingEvents.kt @@ -0,0 +1,223 @@ +package org.alter.skills.cooking + +import dev.openrune.ServerCacheManager.getItem +import org.alter.api.Skills +import org.alter.api.computeSkillingSuccess +import org.alter.api.ext.filterableMessage +import org.alter.api.ext.message +import org.alter.api.ext.produceItemBox +import org.alter.game.model.entity.GameObject +import org.alter.game.model.entity.Player +import org.alter.game.pluginnew.MenuOption +import org.alter.game.pluginnew.PluginEvent +import org.alter.game.pluginnew.event.impl.ItemOnObject +import org.alter.game.pluginnew.event.impl.ObjectClickEvent +import org.alter.rscm.RSCM +import org.alter.rscm.RSCM.asRSCM +import org.alter.rscm.RSCMType +import org.alter.skills.firemaking.ColoredLogs +import org.generated.tables.cooking.CookingRecipesRow +import kotlin.random.Random + +class CookingEvents : PluginEvent() { + + private enum class CookStation { + FIRE, + RANGE + } + + override fun init() { + + // Click range/fire -> open cooking menu + on { + where { optionName.equals("cook", ignoreCase = true) } + then { + val station = stationFor(gameObject) + openCookingMenu(player, station, gameObject) + } + } + + // Use raw food on range/fire -> open cooking menu for that item + on { + where { + option.equals("cook", ignoreCase = true) && + CookingRecipes.byRaw.containsKey(item.id) + } + then { + val recipe = CookingRecipes.byRaw[item.id] ?: return@then + val station = stationFor(gameObject) + openCookingMenu(player, station, gameObject, listOf(recipe)) + } + } + } + + private fun stationFor(gameObject: GameObject): CookStation { + val fireIds = buildFireObjectIds() + return if (fireIds.contains(gameObject.internalID)) CookStation.FIRE else CookStation.RANGE + } + + private fun buildFireObjectIds(): Set { + val fireKeys = ColoredLogs.COLOURED_LOGS.values.map { it.second } + + "objects.fire" + + "objects.forestry_fire" + + val fireInts = fireKeys.mapNotNull { key -> + runCatching { key.asRSCM() }.getOrNull() + } + + return fireInts.toSet() + } + + private fun openCookingMenu( + player: Player, + station: CookStation, + gameObject: GameObject, + only: List? = null + ) { + val cookingLevel = player.getSkills().getCurrentLevel(Skills.COOKING) + + val candidates = (only ?: CookingRecipes.all) + .filter { recipe -> + player.inventory.contains(recipe.raw) && cookingLevel >= recipe.level + } + + if (candidates.isEmpty()) { + player.message("You have nothing you can cook.") + return + } + + // The SKILL_MULTI chatbox UI supports up to 18 item options. + val maxOptions = 18 + val menuCandidates = candidates + .sortedWith( + compareBy { it.level } + .thenBy { getItem(it.cooked)?.name ?: "" } + ) + .take(maxOptions) + + val maxProducible = menuCandidates.maxOf { recipe -> player.inventory.getItemCount(recipe.raw) } + + player.queue { + val cookedItems = menuCandidates.map { it.cooked }.toIntArray() + + produceItemBox( + player, + *cookedItems, + title = "What would you like to cook?", + maxProducable = maxProducible + ) { cookedItemId, qty -> + val recipe = menuCandidates.firstOrNull { it.cooked == cookedItemId } ?: return@produceItemBox + + cook(player, station, gameObject, recipe, qty) + } + } + } + + private fun cook( + player: Player, + station: CookStation, + gameObject: GameObject, + recipe: CookingRecipesRow, + quantity: Int + ) { + val cookingLevel = player.getSkills().getCurrentLevel(Skills.COOKING) + if (cookingLevel < recipe.level) { + player.filterableMessage("You need a Cooking level of ${recipe.level} to cook this.") + return + } + + val objectTile = gameObject.tile + val objectId = gameObject.internalID + + player.queue { + var cooked = 0 + + player.filterableMessage("You begin cooking...") + + repeatWhile(delay = 4, immediate = true, canRepeat = { + cooked < quantity && + player.inventory.contains(recipe.raw) && + isStillAtStation(player, objectTile, objectId) + }) { + + // Optional animation (safe fallback if missing) + runCatching { player.animate("sequences.human_cooking", interruptable = true) } + .onFailure { player.animate(RSCM.NONE) } + + val removed = player.inventory.remove(recipe.raw, 1) + if (!removed.hasSucceeded()) { + stop() + return@repeatWhile + } + + val success = rollCookSuccess(player, station, recipe) + val outputItem = if (success) recipe.cooked else recipe.burnt + + val addResult = player.inventory.add(outputItem, 1) + if (!addResult.hasSucceeded()) { + // restore raw + player.inventory.add(recipe.raw, 1) + player.filterableMessage("You don't have enough inventory space.") + stop() + return@repeatWhile + } + + if (success) { + player.addXp(Skills.COOKING, recipe.xp.toDouble()) + val cookedName = getItem(recipe.cooked)?.name?.lowercase() ?: "food" + player.filterableMessage("You cook the $cookedName.") + } else { + val burntName = getItem(recipe.burnt)?.name?.lowercase() ?: "food" + player.filterableMessage("You accidentally burn the $burntName.") + } + + cooked++ + wait(1) + } + + player.animate(RSCM.NONE) + } + } + + private fun rollCookSuccess( + player: Player, + station: CookStation, + recipe: CookingRecipesRow + ): Boolean { + val cookingLevel = player.getSkills().getCurrentLevel(Skills.COOKING) + + val stopBurn = when (station) { + CookStation.FIRE -> recipe.stopBurnFire + CookStation.RANGE -> recipe.stopBurnRange + }.coerceAtLeast(recipe.level) + + if (cookingLevel >= stopBurn) { + return true + } + + val baseSuccessAtReq = when (station) { + CookStation.FIRE -> 0.30 + CookStation.RANGE -> 0.45 + } + + val progress = ((cookingLevel - recipe.level).toDouble() / (stopBurn - recipe.level).toDouble()) + .coerceIn(0.0, 1.0) + + val chance = (baseSuccessAtReq + (1.0 - baseSuccessAtReq) * progress).coerceIn(0.0, 1.0) + + // Blend with the shared OSRS skilling success curve for nicer scaling. + // This keeps early levels from feeling too punishing while still rewarding levels. + val curve = computeSkillingSuccess(low = 64, high = 256, level = (cookingLevel - recipe.level + 1).coerceIn(1, 99)) + val finalChance = (chance * 0.70 + curve * 0.30).coerceIn(0.0, 1.0) + + return Random.nextDouble() < finalChance + } + + private fun isStillAtStation(player: Player, objectTile: org.alter.game.model.Tile, objectId: Int): Boolean { + if (player.tile.getDistance(objectTile) > 1) return false + + val world = player.world + val obj = world.getObject(objectTile, type = 10) ?: world.getObject(objectTile, type = 11) + return obj != null && obj.internalID == objectId + } +} diff --git a/content/src/main/kotlin/org/alter/skills/cooking/CookingRecipes.kt b/content/src/main/kotlin/org/alter/skills/cooking/CookingRecipes.kt new file mode 100644 index 0000000..58d45d2 --- /dev/null +++ b/content/src/main/kotlin/org/alter/skills/cooking/CookingRecipes.kt @@ -0,0 +1,11 @@ +package org.alter.skills.cooking + +import org.generated.tables.cooking.CookingRecipesRow + +object CookingRecipes { + val all: List = CookingRecipesRow.all() + + val byRaw: Map = all.associateBy { it.raw } + + val byCooked: Map = all.associateBy { it.cooked } +} diff --git a/content/src/main/resources/org/alter/skills/cooking/gamevals.toml b/content/src/main/resources/org/alter/skills/cooking/gamevals.toml new file mode 100644 index 0000000..0d6ca13 --- /dev/null +++ b/content/src/main/resources/org/alter/skills/cooking/gamevals.toml @@ -0,0 +1,16 @@ +[gamevals.tables] +cooking_recipes=1236 + +[gamevals.dbrows] +cooking_shrimps=982014 +cooking_anchovies=982015 +cooking_sardine=982016 +cooking_herring=982017 +cooking_mackerel=982018 +cooking_trout=982019 +cooking_salmon=982020 +cooking_lobster=982021 +cooking_swordfish=982022 +cooking_shark=982023 +cooking_beef=982024 +cooking_chicken=982025