From 7efa7690abb227653da2d927317bab45a9abb270 Mon Sep 17 00:00:00 2001 From: Major- Date: Sat, 15 Feb 2014 16:01:04 +0000 Subject: [PATCH] Add Herblore skill. --- data/plugins/skill-herblore/herb.rb | 90 ++++++ data/plugins/skill-herblore/herblore.rb | 101 ++++++ data/plugins/skill-herblore/ingredient.rb | 258 +++++++++++++++ data/plugins/skill-herblore/plugin.xml | 18 ++ data/plugins/skill-herblore/potion.rb | 364 ++++++++++++++++++++++ 5 files changed, 831 insertions(+) create mode 100644 data/plugins/skill-herblore/herb.rb create mode 100644 data/plugins/skill-herblore/herblore.rb create mode 100644 data/plugins/skill-herblore/ingredient.rb create mode 100644 data/plugins/skill-herblore/plugin.xml create mode 100644 data/plugins/skill-herblore/potion.rb diff --git a/data/plugins/skill-herblore/herb.rb b/data/plugins/skill-herblore/herb.rb new file mode 100644 index 00000000..0f01fb49 --- /dev/null +++ b/data/plugins/skill-herblore/herb.rb @@ -0,0 +1,90 @@ +require 'java' + +java_import 'org.apollo.game.action.Action' + +# A herb is an ingredient that requires identification before being used. +class Herb < Ingredient + include HerbloreMethod + + attr_reader :unidentified, :level, :experience + + def initialize(item_id, unidentified, level, experience) + super item_id + + @unidentified = unidentified + @level = level + @experience = experience + end + + def invoke(player, id, slot) + item = player.inventory.get(slot) + player.start_action(HerbIdentificationAction.new(player, self, slot, item)) + end +end + +# An action that makes a player identify a herb. +class HerbIdentificationAction < Action + attr_reader :herb, :slot, :item, :pulses + + def initialize(player, herb, slot, item) + super(0, true, player) + + @herb = herb + @slot = slot + @item = item + @pulses = 0 + end + + def execute + if @pulses == 0 + unless check_skill(mob, @herb.level, "identify this herb") + stop + return + end + end + execute_action + @pulses += 1 + end + + def execute_action + player = mob + inventory = player.inventory + + if inventory.remove_slot(@slot, 1) == 1 + identified = @herb.item + + inventory.add(identified) + player.skill_set.add_experience(HERBLORE_ID, @herb.experience) + player.send_message("You identify the herb as a #{identified.definition.name}.", true) + end + stop + end + + def equals(other) + return (get_class == other.get_class and slot == other.slot and herb == other.herb) + end +end + +# Appends a herb to the InventoryItemEvent interception. +def append_herb(item_id, unidentified, level, experience) + herb = Herb.new(item_id, unidentified, level, experience) + append_herblore_item(herb, unidentified) + return herb +end + +# Herbs + +GUAM_LEAF = append_herb(249, 199, 1, 2.5) # 3, 2.5 +MARRENTILL = append_herb(251, 201, 5, 3.8) +TARROMIN = append_herb(253, 203, 11, 5) +HARRALANDER = append_herb(255, 205, 20, 6.3) +RANARR = append_herb(257, 207, 25, 7.5) +TOADFLAX = append_herb(2998, 3049, 30, 8) +IRIT_LEAF = append_herb(259, 209, 40, 8.8) +AVANTOE = append_herb(261, 211, 48, 10) +KWUARM = append_herb(263, 213, 54, 11.3) +SNAPDRAGON = append_herb(3000, 3051, 59, 11.8) +CADANTINE = append_herb(265, 215, 65, 12.5) +LANTADYME = append_herb(2481, 2485, 67, 13.1) +DWARF_WEED = append_herb(267, 217, 70, 13.8) +TORSTOL = append_herb(269, 219, 75, 15) \ No newline at end of file diff --git a/data/plugins/skill-herblore/herblore.rb b/data/plugins/skill-herblore/herblore.rb new file mode 100644 index 00000000..5773c9eb --- /dev/null +++ b/data/plugins/skill-herblore/herblore.rb @@ -0,0 +1,101 @@ +# Thanks to Sillhouette for posting +# a large amount of Herblore skill data which has been thankfully used in this plugin. + +require 'java' + +java_import 'org.apollo.game.event.impl.SetWidgetItemModelEvent' +java_import 'org.apollo.game.model.Skill' + +HERBLORE_ID = Skill::HERBLORE +HERBLORE_DIALOGUE = 4429 + +HERBLORE_ITEM = {} +HERBLORE_ITEM_ITEM = {} + +DRINK_ITEM = {} + +# A module which describes an invocable method of the Herblore skill. +module HerbloreMethod + def self.new + raise 'You cannot instantiate this module!' + end + + def invoke(player, primary, secondary) + raise NotImplementedError.new('You must implement the invocation of HerbloreMethod!') + end +end + +# The ItemOnItemEvent handler for all Herblore-related functions. +on :event, :item_on_item do |ctx, player, event| + primary = event.id + secondary = event.target_id + hash = HERBLORE_ITEM_ITEM[primary] + + if hash == nil + secondary = event.id + primary = event.target_id + hash = HERBLORE_ITEM_ITEM[primary] + end + + if hash != nil + method = hash[secondary] + if method != nil + method.invoke(player, primary, secondary) + ctx.break_handler_chain + end + end +end + +# The ItemOptionEvent handler for all Herblore-related functions. +on :event, :item_option do |ctx, player, event| + if event.option == 1 + id = event.id + method = HERBLORE_ITEM[id] + + if method != nil + method.invoke(player, id, event.slot) + ctx.break_handler_chain + end + method = DRINK_ITEM[id] + + if method != nil + method.invoke(player, id, event.slot) + ctx.break_handler_chain + end + end +end + +# Utility for adding the various Herblore methods to the handled constant arrays. +def append_herblore_item(method, key, secondary = -1) + if secondary == -1 + HERBLORE_ITEM[key] = method + else + hash = HERBLORE_ITEM_ITEM[key] + hash = {} if hash == nil + + hash[secondary] = method + HERBLORE_ITEM_ITEM[key] = hash + end +end + +# Utility method for checking if a player's inventory has an item of the specified id, with optionally the specified amount (1 by default), at the specified slot. +def check_slot(player, slot, id, amount = 1) + item = player.inventory.get(slot) + return (item != nil and item.id == id and item.amount >= amount) +end + +# Utility method for checking if a player's Herblore (maximum) level is at a required height. Also informs the player if this is not the case with use of the action +# variable, like so: "You need a Herblore level of at least #{required.to_s} to #{action}." +def check_skill(player, required, action) + if required > player.skill_set.skill(HERBLORE_ID).current_level + player.send_message("You need a Herblore level of at least #{required} to #{action}.") + return false + end + return true +end + +# Opens a 'make' dialogue for the specified player, displaying the specified item. Optionally, a listener can be used for the dialogue. +def open_dialogue(player, item, listener = nil) + player.send(SetWidgetItemModelEvent.new(1746, item, 170)) + player.interface_set.open_dialogue(listener, HERBLORE_DIALOGUE) +end \ No newline at end of file diff --git a/data/plugins/skill-herblore/ingredient.rb b/data/plugins/skill-herblore/ingredient.rb new file mode 100644 index 00000000..2f2759a1 --- /dev/null +++ b/data/plugins/skill-herblore/ingredient.rb @@ -0,0 +1,258 @@ +require 'java' + +java_import 'org.apollo.game.action.Action' +java_import 'org.apollo.game.model.Animation' +java_import 'org.apollo.game.model.Item' +java_import 'org.apollo.game.model.def.ItemDefinition' +java_import 'org.apollo.game.model.inter.EnterAmountListener' +java_import 'org.apollo.game.model.inter.dialogue.DialogueAdapter' + +GRINDING_ANIM = Animation.new(364) +PESTLE_MORTAR = 233 + +# An ingredient which can be used for making (unfinished) potions. +class Ingredient + attr_reader :item_id, :item + + def initialize(item) + @item_id = item + @item = Item.new(item) # Share item instances. + end + + # Checks if the specified player has the specified amount of this ingredient. Optionally, they can immediately be removed if that + # amount was indeed found. + def check_remove(player, amount, remove) + inventory = player.inventory + counter = 0 + + inventory.items.each do |inv_item| + break unless counter < amount + + next if inv_item == nil + + id = inv_item.id + inventory_amount = inv_item.amount + + if id == @item_id + if inventory_amount >= amount + inventory.remove(@item_id, amount) if remove + return true + else + counter += inventory_amount + end + end + end + + if counter >= amount + inventory.remove(@item_id, amount) if remove + return true + end + + return false + end +end + +# An ingredient which needs to be grinded before being usable for Herblore. +class GrindedIngredient < Ingredient + include HerbloreMethod + + attr_reader :raw + + def initialize(item_id, raw) + super(item_id) + + @raw = raw + end + + def invoke(player, pestle_mortar, ingredient) + action = GrindingAction.new(player, self) + listener = GrindingDialogueListener.new(player, action) + + open_dialogue(player, @item_id, listener) + end +end + +# A DialogueAdapter used for grinding ingredients. It is also used as an EnterAmountListener for the amount of grinding actions. +class GrindingDialogueListener < DialogueAdapter + include EnterAmountListener + + attr_reader :player, :action + + def initialize(player, action) + super() + + @player = player + @action = action + end + + # Called when a button has been clicked whilst the dialogue was opened. + def buttonClicked(button) + amount = get_amount(button) + if amount == 0 + return false + end + + interfaces = @player.interface_set + interfaces.close + + if amount == -1 + interfaces.open_enter_amount_dialogue(self) + return true + end + + amount = player.inventory.get_amount(@action.ingredient.raw) if amount == -2 + execute(amount) + end + + # Called when an amount of grinding actions has been entered. + def amountEntered(amount) + if amount <= 0 then return else execute(amount) end + end + + # Called to set the action(s) in motion. + def execute(amount) + @action.set_amount(amount) + @player.start_action(@action) + end + + # Gets the amount of actions based on the specified button id. + def get_amount(button) + case button + when 2799 then return 1 + when 2798 then return 5 + when 1748 then return -1 + when 1747 then return -2 + else return 0 + end + end +end + +# An action which makes the player grind one or more GrindedIngredients from their 'raw' form. +class GrindingAction < Action + attr_reader :ingredient, :amount, :pulses, :slot, :listener + + def initialize(player, ingredient) + super 0, true, player + + @ingredient = ingredient + @pulses = 0 + end + + def execute + grind + @pulses += 1 + end + + # Performs the grinding action once the materials have been checked. + def grind + if @pulses == 0 + mob.play_animation GRINDING_ANIM + elsif @pulses == 1 + if not gather_materials + stop + return + end + + player = mob + inventory = player.inventory + item = inventory.get(@slot) + + name = item.definition.name.downcase + player.send_message("You grind the #{name} to dust.", true) + + inventory.reset(@slot) + inventory.add(@ingredient.item) + + set_delay(1) + elsif @pulses == 2 + mob.stop_animation + continue() + end + end + + # Checks if the player has the required materials to perform the (next) action. + def gather_materials + items = mob.inventory.items + + pst_mrt = false + ingr = false + raw = @ingredient.raw + (0...items.length).each do |slot| + item = items[slot] + next if item == nil + + id = item.id + if id == PESTLE_MORTAR and !pst_mrt + pst_mrt = true + elsif id == raw and !ingr + ingr = true + @slot = slot + end + + return true if pst_mrt and ingr + end + + ingr = ItemDefinition.lookup(raw).name.downcase + mob.send_message("You do not have any more #{ingr}s.") + return false + end + + # Either invokes the stop() method in Action to shut it down + # or continues to the next ingredient. + def continue + @amount -= 1 + + if @amount > 0 + set_delay(0) + @pulses = -1 + else + stop + end + end + + # Sets the amount of actions. + def set_amount(amount) + @amount = amount + end + + def stop + super + mob.inventory.remove_listener(@listener) unless listener == nil + end + + def equals(other) + return (get_class == other.get_class and @ingredient == other.ingredient) + end +end + +# Appends a grinded ingredient to the ItemOnItemEvent handler interception. +def append_grinded(id, raw) + grinded = GrindedIngredient.new(id, raw) + append_herblore_item(grinded, PESTLE_MORTAR, raw) + return grinded +end + +# Normal ingredients +EYE_NEWT = Ingredient.new(221) +RED_SPIDERS_EGGS = Ingredient.new(223) +LIMPWURT_ROOT = Ingredient.new(225) +SNAPE_GRASS = Ingredient.new(231) +WHITE_BERRIES = Ingredient.new(239) +WINE_ZAMORAK = Ingredient.new(245) +JANGERBERRIES = Ingredient.new(247) +TOADS_LEGS = Ingredient.new(2152) +MORT_MYRE_FUNGI = Ingredient.new(2970) +POTATO_CACTUS = Ingredient.new(3138) +PHOENIX_FEATHER = Ingredient.new(4621) +FROG_SPAWN = Ingredient.new(5004) +PAPAYA_FRUIT = Ingredient.new(5972) +POISON_IVY_BERRIES = Ingredient.new(6018) +YEW_ROOTS = Ingredient.new(6049) +MAGIC_ROOTS = Ingredient.new(6051) + +# Grinded ingredients +UNICORN_HORN_DUST = append_grinded(235, 237) +DRAGON_SCALE_DUST = append_grinded(241, 243) +CHOCOLATE_DUST = append_grinded(1975, 1973) +CRUSHED_NEST = append_grinded(6693, 5075) +GROUND_MUD_RUNE = append_grinded(9594, 4698) \ No newline at end of file diff --git a/data/plugins/skill-herblore/plugin.xml b/data/plugins/skill-herblore/plugin.xml new file mode 100644 index 00000000..a4481e6b --- /dev/null +++ b/data/plugins/skill-herblore/plugin.xml @@ -0,0 +1,18 @@ + + + skill-herblore + 1 + Herblore + Adds the Herblore skill. + + Chris Fletcher + Major + + + + + + + + + \ No newline at end of file diff --git a/data/plugins/skill-herblore/potion.rb b/data/plugins/skill-herblore/potion.rb new file mode 100644 index 00000000..a8d82a75 --- /dev/null +++ b/data/plugins/skill-herblore/potion.rb @@ -0,0 +1,364 @@ +require 'java' + +java_import 'org.apollo.game.action.Action' +java_import 'org.apollo.game.model.Animation' +java_import 'org.apollo.game.model.Item' +java_import 'org.apollo.game.model.def.ItemDefinition' +java_import 'org.apollo.game.model.inter.EnterAmountListener' +java_import 'org.apollo.game.model.inter.dialogue.DialogueAdapter' + +WATER_VIAL_ID = 227 +EMPTY_VIAL_ID = 229 + +MIXING_ANIM = Animation.new(363) + +# Represents an unfinished potion which can be invoked as a HerbloreMethod and used as an ingredient. +class UnfinishedPotion < Ingredient + include HerbloreMethod + + attr_reader :herb, :level + + def initialize(item_id, herb, level) + super item_id + + @herb = herb + @level = level + end + + def invoke(player, primary, secondary) + action = UnfinishedMixingAction.new(player, self) + listener = UnfinishedMixingDialogueListener.new(player, action) + + open_dialogue(player, @item_id, listener) + end +end + +# Represents a finished potion which can be invoked as a HerbloreMethod. +class FinishedPotion + include HerbloreMethod + + attr_reader :item, :ingredients, :level, :experience + + def initialize(item, ingredients, level, experience) + @item = Item.new(item) + @ingredients = ingredients + @level = level + @experience = experience + end + + def invoke(player, primary, secondary) + action = FinishedMixingAction.new(player, primary, secondary, self) + listener = FinishedMixingDialogueListener.new(player, action) + + open_dialogue(player, @item.id, listener) end +end + +# A DialogueAdapter used for mixing potions. It is also used as an EnterAmountListener for the amount of mixing actions. +class MixingDialogueListener < DialogueAdapter + include EnterAmountListener + + attr_reader :player, :action + + def initialize(player, action) + super() + + @player = player + @action = action + end + + # Called when a button has been clicked whilst the dialogue was opened. + def buttonClicked(button) + amount = get_amount(button) + + return false if amount == 0 + + interfaces = @player.interface_set + interfaces.close + + if amount == -1 + interfaces.open_enter_amount_dialogue(self) + return true + end + + amount = calculate_maximum if amount == -2 + + execute(amount) + return true + end + + # Called when an amount of mixing actions has been entered. + def amountEntered(amount) + if amount <= 0 then return else execute(amount) end + end + + # Called to set the action(s) in motion. + def execute(amount) + @action.set_amount(amount) + @player.start_action(@action) + end + + def calculate_maximum(code) + # Override for potion-specific amount calculation. + end + + # Gets the amount of actions based on the specified button id. + def get_amount(button) + case button + when 2799 then return 1 + when 2798 then return 5 + when 1748 then return -1 + when 1747 then return -2 + else return 0 + end + end + +end + +# A MixingDialogueListener used for mixing unfinished potions. +class UnfinishedMixingDialogueListener < MixingDialogueListener + def calculate_maximum + inventory = @player.inventory + + amount = inventory.get_amount(WATER_VIAL_ID) + + return 0 if amount <= 0 + + herbs = inventory.get_amount(@action.potion.herb.item.id) + amount = herbs if amount > herbs + + return amount + end +end + +# A MixingDialogueListener used for mixing finished potions. +class FinishedMixingDialogueListener < MixingDialogueListener + + def calculate_maximum + inventory = @player.inventory + + amount = inventory.capacity + @action.potion.ingredients.each do |ingredient| + item_amount = inventory.get_amount(ingredient.item.id) + amount = item_amount if amount > item_amount + end + + return amount + end + +end + +# An Action which handles the none-finished-dependent mixing. +class MixingAction < Action + attr_reader :potion, :amount, :started, :pulses, :action, :listener + + def initialize(player, potion, action) + super(1, true, player) + + @potion = potion + @started = false + @pulses = 0 + @action = action + @action.freeze + end + + def execute + if @pulses == 0 + unless @started + unless check_skill(mob, @potion.level, @action) + stop + return + end + @started = true + end + + unless gather_materials + stop + return + end + end + mob.play_animation(MIXING_ANIM) + execute_action + + if (@amount -= 1) > 0 then @pulses = 0 else stop end + end + + def stop + super() + mob.inventory.remove_listener(@listener) unless @listener == nil + end + + def execute_action + # Override for action execution. + end + + def gather_materials + # Override for ingredient checking and gathering + return false + end + + # Sets the amount of actions. + def set_amount(amount) + @amount = amount + end + + def equals(other) + return (get_class == other.get_class and @potion == other.potion) + end +end + +# A MixingAction which handles the execution of making UnfinishedPotions. +class UnfinishedMixingAction < MixingAction + attr_reader :slots + + def initialize(player, potion) + super(player, potion, "use this herb.") + end + + def execute_action + name = @potion.herb.item.definition.name + player = mob + inventory = player.inventory + + player.send_message("You put the #{name} in the water to make an unfinished #{name.sub(/ leaf$/, "")} potion.", true) + + @slots.each do |slot, amount| + unless inventory.remove_slot(slot, amount) + stop + return + end + end + + inventory.add(@potion.item) + end + + def gather_materials + @slots = {} + inventory = mob.inventory + + vial_slot = inventory.slot_of(WATER_VIAL_ID) + if vial_slot == -1 + mob.send_message('You do not have any more vials of water.') + return false + end + + item = @potion.herb.item + herb_slot = inventory.slot_of(item.id) + if herb_slot == -1 + mob.send_message("You do not have any more #{item.definition.name}.") + return false + end + + @slots[vial_slot] = 1 + @slots[herb_slot] = 1 + + return true + end +end + +# A MixingAction which handles the execution of making FinishedPotions. +class FinishedMixingAction < MixingAction + attr_reader :unfinished, :ingredient, :slots + + def initialize(player, unfinished, ingredient, potion) + super(player, potion, "mix this potion") + + @unfinished = unfinished + @ingredient = ingredient + end + + def execute_action + player = mob + ingredient = ItemDefinition.lookup(@ingredient).name.downcase + name = @potion.item.definition.name.sub('(3)', '') + + player.send_message("You add the #{ingredient} to the mixture to make an #{name}.", true) + player.skill_set.add_experience(@potion.experience) + + inventory = player.inventory + + @slots.each do |slot, amount| + if not inventory.remove_slot(slot, amount) + stop + return + end + end + + inventory.add(@potion.item) + end + + def gather_materials + @slots = {} + inventory = mob.inventory + + vial_slot = inventory.slot_of(@unfinished) + if vial_slot == -1 + mob.send_message('You do not have enough unfinished potions.') + return false + end + + ingredient_slot = inventory.slot_of(@ingredient) + if ingredient_slot == -1 + mob.send_message('You do not have enough ingredients.') + return false + end + + @slots[vial_slot] = 1 + @slots[ingredient_slot] = 1 + + return true + end +end + +# Appends a finished potion to the ItemOnItemEvent handling interception. +def append_finished_potion(item, unfinished, ingredient, level, experience) + potion = FinishedPotion.new(item, [ unfinished, ingredient ], level, experience) + append_herblore_item(potion, unfinished.item_id, ingredient.item_id) + return potion +end + +# Appends an unfinished potion to the ItemOnItemEvent handling interception. +def append_unfinished_potion(item, herb, level) + potion = UnfinishedPotion.new(item, herb, level) + append_herblore_item(potion, herb.item_id, WATER_VIAL_ID) + return potion +end + + +# Unfinished potions +UNF_GUAM = append_unfinished_potion(91, GUAM_LEAF, 1) # 3 +UNF_MARRENTILL = append_unfinished_potion(93, MARRENTILL, 5) +UNF_TARROMIN = append_unfinished_potion(95, TARROMIN, 12) +UNF_HARRALANDER = append_unfinished_potion(97, HARRALANDER, 22) +UNF_RANARR = append_unfinished_potion(99, RANARR, 30) +UNF_TOADFLAX = append_unfinished_potion(3002, TOADFLAX, 34) +UNF_IRIT = append_unfinished_potion(101, IRIT_LEAF, 45) +UNF_AVANTOE = append_unfinished_potion(103, AVANTOE, 50) +UNF_KWUARM = append_unfinished_potion(105, KWUARM, 55) +UNF_SNAPDRAGON = append_unfinished_potion(3004, SNAPDRAGON, 63) +UNF_CADANTINE = append_unfinished_potion(107, CADANTINE, 66) +UNF_LANTADYME = append_unfinished_potion(2483, LANTADYME, 69) +UNF_DWARF_WEED = append_unfinished_potion(109, DWARF_WEED, 72) +UNF_TORSTOL = append_unfinished_potion(111, TORSTOL, 78) + + +# Finished potions +ATTACK_POT = append_finished_potion(121, UNF_GUAM, EYE_NEWT, 1, 25) # 3, 25 +ANTIPOISON_POT = append_finished_potion(175, UNF_MARRENTILL, UNICORN_HORN_DUST, 5, 37.5) +STRENGTH_POT = append_finished_potion(115, UNF_TARROMIN, LIMPWURT_ROOT, 12, 50) +RESTORE_POT = append_finished_potion(127, UNF_HARRALANDER, RED_SPIDERS_EGGS, 18, 62.5) +ENERGY_POT = append_finished_potion(3010, UNF_HARRALANDER, CHOCOLATE_DUST, 26, 67.5) +DEFENCE_POT = append_finished_potion(133, UNF_RANARR, WHITE_BERRIES, 30, 75) +AGILITY_POT = append_finished_potion(3034, UNF_TOADFLAX, TOADS_LEGS, 34, 80) +PRAYER_POT = append_finished_potion(139, UNF_RANARR, SNAPE_GRASS, 38, 87.5) +SUPER_ATTACK_POT = append_finished_potion(145, UNF_IRIT, EYE_NEWT, 45, 100) +SUPER_ANTIPOISON_POT = append_finished_potion(181, UNF_IRIT, UNICORN_HORN_DUST, 48, 106.3) +FISHING_POT = append_finished_potion(151, UNF_AVANTOE, SNAPE_GRASS, 50, 112.5) +SUPER_ENERGY_POT = append_finished_potion(3018, UNF_AVANTOE, MORT_MYRE_FUNGI, 52, 117.5) +SUPER_STRENGTH_POT = append_finished_potion(157, UNF_KWUARM, LIMPWURT_ROOT, 55, 125) +WEAPON_POISON = append_finished_potion(187, UNF_KWUARM, DRAGON_SCALE_DUST, 60, 137.5) +SUPER_RESTORE_POT = append_finished_potion(3026, UNF_SNAPDRAGON, RED_SPIDERS_EGGS, 63, 142.5) +SUPER_DEFENCE_POT = append_finished_potion(163, UNF_CADANTINE, WHITE_BERRIES, 66, 150) +ANTIFIRE_POT = append_finished_potion(2428, UNF_LANTADYME, DRAGON_SCALE_DUST, 69, 157.5) +RANGING_POT = append_finished_potion(169, UNF_DWARF_WEED, WINE_ZAMORAK, 72, 162.5) +MAGIC_POT = append_finished_potion(3042, UNF_LANTADYME, POTATO_CACTUS, 76, 172.5) +ZAMORAK_BREW = append_finished_potion(189, UNF_TORSTOL, JANGERBERRIES, 78, 175) \ No newline at end of file