diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTest.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTest.kt index fce9fc41..08fecc64 100644 --- a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTest.kt +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTest.kt @@ -20,7 +20,7 @@ abstract class KotlinPluginTest: KotlinPluginTestHelpers() { override lateinit var messageHandlers: MessageHandlerChainSet @Before - fun setup() { + open fun setup() { messageHandlers = MessageHandlerChainSet() world = PowerMockito.spy(World()) diff --git a/game/src/plugins/consumables/meta.toml b/game/src/plugins/consumables/meta.toml new file mode 100644 index 00000000..e31a38c3 --- /dev/null +++ b/game/src/plugins/consumables/meta.toml @@ -0,0 +1,8 @@ +name = "consumables" +package = "org.apollo.game.plugin.consumables" +authors = [ "Gary Tierney" ] +dependencies = [] + +[config] +srcDir = "src/" +testDir = "test/" \ No newline at end of file diff --git a/game/src/plugins/consumables/src/consumables.kt b/game/src/plugins/consumables/src/consumables.kt new file mode 100644 index 00000000..3fec3eaf --- /dev/null +++ b/game/src/plugins/consumables/src/consumables.kt @@ -0,0 +1,82 @@ +package org.apollo.plugin.consumables + +import org.apollo.game.model.entity.Player +import org.apollo.game.model.entity.Skill + +/** + * An item that can be consumed to restore or buff stats. + */ +abstract class Consumable(val name: String, val id: Int, val sound: Int, val delay: Int, val replacement: Int?) { + + abstract fun addEffect(player: Player) + + fun consume(player: Player, slot: Int) { + addEffect(player) + player.inventory.reset(slot) + + if (replacement != null) { + player.inventory.add(replacement) + } + } + +} + +private val consumables = mutableMapOf() + +fun isConsumable(itemId: Int) = consumables.containsKey(itemId) +fun lookupConsumable(itemId: Int): Consumable = consumables.get(itemId)!! +fun consumable(consumable: Consumable) = consumables.put(consumable.id, consumable) + +enum class FoodOrDrinkType(val action: String) { + FOOD("eat"), DRINK("drink") +} + +class FoodOrDrink : Consumable { + + companion object { + const val EAT_FOOD_SOUND = 317 + } + + val restoration: Int + val type: FoodOrDrinkType + + constructor( + name: String, + id: Int, + delay: Int, + type: FoodOrDrinkType, + restoration: Int, + replacement: Int? = null + ) : super(name, id, EAT_FOOD_SOUND, delay, replacement) { + this.type = type + this.restoration = restoration + } + + override fun addEffect(player: Player) { + val hitpoints = player.skillSet.getSkill(Skill.HITPOINTS) + val hitpointsLevel = hitpoints.currentLevel + val newHitpointsLevel = Math.min(hitpointsLevel + restoration, hitpoints.maximumLevel) + + player.sendMessage("You ${type.action} the $name.") + if (newHitpointsLevel > hitpointsLevel) { + player.sendMessage("It heals some health.") + } + + player.skillSet.setCurrentLevel(Skill.HITPOINTS, newHitpointsLevel) + } + +} + +/** + * Define a new type of [Consumable] food. + */ +fun food(name: String, id: Int, restoration: Int, replacement: Int? = null, delay: Int = 3) { + consumable(FoodOrDrink(name, id, delay, FoodOrDrinkType.FOOD, restoration, replacement)) +} + +/** + * Define a new type of [Consumable] drink. + */ +fun drink(name: String, id: Int, restoration: Int, replacement: Int? = null, delay: Int = 3) { + consumable(FoodOrDrink(name, id, delay, FoodOrDrinkType.DRINK, restoration, replacement)) +} \ No newline at end of file diff --git a/game/src/plugins/consumables/src/consumables.plugin.kts b/game/src/plugins/consumables/src/consumables.plugin.kts new file mode 100644 index 00000000..49c64312 --- /dev/null +++ b/game/src/plugins/consumables/src/consumables.plugin.kts @@ -0,0 +1,36 @@ +import org.apollo.game.action.AsyncAction +import org.apollo.game.message.impl.ItemOptionMessage +import org.apollo.game.model.Animation +import org.apollo.game.model.entity.Player +import org.apollo.net.message.Message +import org.apollo.plugin.consumables.* + +on { ItemOptionMessage::class } + .where { option == 1 && isConsumable(id) } + .then { + ConsumeAction.start(this, it, lookupConsumable(id), slot) + } + +class ConsumeAction(val consumable: Consumable, player: Player, val slot: Int) : + AsyncAction(CONSUME_STARTUP_DELAY, true, player) { + + companion object { + const val CONSUME_ANIMATION_ID = 829 + const val CONSUME_STARTUP_DELAY = 2 + + /** + * Starts a [ConsumeAction] for the specified [Player], terminating the [Message] that triggered it. + */ + fun start(message: Message, player: Player, consumable: Consumable, slot: Int) { + player.startAction(ConsumeAction(consumable, player, slot)) + message.terminate() + } + } + + suspend override fun executeActionAsync() { + consumable.consume(mob, slot) + mob.playAnimation(Animation(CONSUME_ANIMATION_ID)) + wait(consumable.delay) + } + +} diff --git a/game/src/plugins/consumables/src/drinks.plugin.kts b/game/src/plugins/consumables/src/drinks.plugin.kts new file mode 100644 index 00000000..f3c8075a --- /dev/null +++ b/game/src/plugins/consumables/src/drinks.plugin.kts @@ -0,0 +1,24 @@ +import org.apollo.plugin.consumables.drink + +//# Wine +drink(name = "jug_of_wine", id = 1993, restoration = 11) + +//# Hot Drinks +drink(name = "nettle_tea", id = 4239, restoration = 3) +drink(name = "nettle_tea", id = 4240, restoration = 3) + +//# Gnome Cocktails +drink(name = "fruit_blast", id = 2034, restoration = 9) +drink(name = "fruit_blast", id = 2084, restoration = 9) +drink(name = "pineapple_punch", id = 2036, restoration = 9) +drink(name = "pineapple_punch", id = 2048, restoration = 9) +drink(name = "wizard_blizzard", id = 2040, restoration = 5) // -4 attack, +5 strength also +drink(name = "wizard_blizzard", id = 2054, restoration = 5) // -4 attack, +5 strength also +drink(name = "short_green_guy", id = 2038, restoration = 5) // -4 attack, +5 strength also +drink(name = "short_green_guy", id = 2080, restoration = 5) // -4 attack, +5 strength also +drink(name = "drunk_dragon", id = 2032, restoration = 5) // -4 attack, +6 strength also +drink(name = "drunk_dragon", id = 2092, restoration = 5) // -4 attack, +6 strength also +drink(name = "chocolate_saturday", id = 2030, restoration = 7) // -4 attack, +6 strength also +drink(name = "chocolate_saturday", id = 2074, restoration = 7) // -4 attack, +6 strength also +drink(name = "blurberry_special", id = 2028, restoration = 7) // -4 attack, +6 strength also +drink(name = "blurberry_special", id = 2064, restoration = 7) // -4 attack, +6 strength also diff --git a/game/src/plugins/consumables/src/foods.plugin.kts b/game/src/plugins/consumables/src/foods.plugin.kts new file mode 100644 index 00000000..7762969b --- /dev/null +++ b/game/src/plugins/consumables/src/foods.plugin.kts @@ -0,0 +1,159 @@ +import org.apollo.plugin.consumables.food + +food(name = "anchovies", id = 319, restoration = 1) +food(name = "crab_meat", id = 7521, restoration = 2, replacement = 7523) +food(name = "crab_meat", id = 7523, restoration = 2, replacement = 7524) +food(name = "crab_meat", id = 7524, restoration = 2, replacement = 7525) +food(name = "crab_meat", id = 7525, restoration = 2, replacement = 7526) +food(name = "crab_meat", id = 7526, restoration = 2) +food(name = "shrimp", id = 315, restoration = 3) +food(name = "sardine", id = 325, restoration = 3) +food(name = "cooked_meat", id = 2142, restoration = 3) +food(name = "cooked_chicken", id = 2140, restoration = 3) +food(name = "ugthanki_meat", id = 1861, restoration = 3) +food(name = "karambwanji", id = 3151, restoration = 3) +food(name = "cooked_rabbit", id = 3228, restoration = 5) +food(name = "herring", id = 347, restoration = 6) +food(name = "trout", id = 333, restoration = 7) +food(name = "cod", id = 339, restoration = 7) +food(name = "mackeral", id = 355, restoration = 7) +food(name = "roast_rabbit", id = 7223, restoration = 7) +food(name = "pike", id = 351, restoration = 8) +food(name = "lean_snail_meat", id = 3371, restoration = 8) +food(name = "salmon", id = 329, restoration = 9) +food(name = "tuna", id = 361, restoration = 10) +food(name = "lobster", id = 379, restoration = 12) +food(name = "bass", id = 365, restoration = 13) +food(name = "swordfish", id = 373, restoration = 14) +food(name = "cooked_jubbly", id = 7568, restoration = 15) +food(name = "monkfish", id = 7946, restoration = 16) +food(name = "cooked_karambwan", id = 3144, restoration = 18, delay = 0) +food(name = "shark", id = 385, restoration = 20) +food(name = "sea_turtle", id = 397, restoration = 21) +food(name = "manta_ray", id = 391, restoration = 22) + +//# Breads/Wraps +food(name = "bread", id = 2309, restoration = 5) +food(name = "oomlie_wrap", id = 2343, restoration = 14) +food(name = "ugthanki_kebab", id = 1883, restoration = 19) + +//# Fruits +food(name = "banana", id = 1963, restoration = 2) +food(name = "sliced_banana", id = 3162, restoration = 2) +food(name = "lemon", id = 2102, restoration = 2) +food(name = "lemon_chunks", id = 2104, restoration = 2) +food(name = "lemon_slices", id = 2106, restoration = 2) +food(name = "lime", id = 2120, restoration = 2) +food(name = "lime_chunks", id = 2122, restoration = 2) +food(name = "lime_slices", id = 2124, restoration = 2) +food(name = "strawberry", id = 5504, restoration = 5) +food(name = "papaya_fruit", id = 5972, restoration = 8) +food(name = "pineapple_chunks", id = 2116, restoration = 2) +food(name = "pineapple_ring", id = 2118, restoration = 2) +food(name = "orange", id = 2108, restoration = 2) +food(name = "orange_rings", id = 2110, restoration = 2) +food(name = "orange_slices", id = 2112, restoration = 2) + +//# Pies +//# TODO: pie special effects (e.g. fish pie raises fishing level) +food(name = "redberry_pie", id = 2325, restoration = 5, replacement = 2333, delay = 1) +food(name = "redberry_pie", id = 2333, restoration = 5, delay = 1) + +food(name = "meat_pie", id = 2327, restoration = 6, replacement = 2331, delay = 1) +food(name = "meat_pie", id = 2331, restoration = 6, delay = 1) + +food(name = "apple_pie", id = 2323, restoration = 7, replacement = 2335, delay = 1) +food(name = "apple_pie", id = 2335, restoration = 7, delay = 1) + +food(name = "fish_pie", id = 7188, restoration = 6, replacement = 7190, delay = 1) +food(name = "fish_pie", id = 7190, restoration = 6, delay = 1) + +food(name = "admiral_pie", id = 7198, restoration = 8, replacement = 7200, delay = 1) +food(name = "admiral_pie", id = 7200, restoration = 8, delay = 1) + +food(name = "wild_pie", id = 7208, restoration = 11, replacement = 7210, delay = 1) +food(name = "wild_pie", id = 7210, restoration = 11, delay = 1) + +food(name = "summer_pie", id = 7218, restoration = 11, replacement = 7220, delay = 1) +food(name = "summer_pie", id = 7220, restoration = 11, delay = 1) + +//# Stews +food(name = "stew", id = 2003, restoration = 11) +food(name = "banana_stew", id = 4016, restoration = 11) +food(name = "curry", id = 2011, restoration = 19) + +//# Pizzas +food(name = "plain_pizza", id = 2289, restoration = 7, replacement = 2291) +food(name = "plain_pizza", id = 2291, restoration = 7) + +food(name = "meat_pizza", id = 2293, restoration = 8, replacement = 2295) +food(name = "meat_pizza", id = 2295, restoration = 8) + +food(name = "anchovy_pizza", id = 2297, restoration = 9, replacement = 2299) +food(name = "anchovy_pizza", id = 2299, restoration = 9) + +food(name = "pineapple_pizza", id = 2301, restoration = 11, replacement = 2303) +food(name = "pineapple_pizza", id = 2303, restoration = 11) + +//# Cakes +food(name = "fishcake", id = 7530, restoration = 11) + +food(name = "cake", id = 1891, restoration = 4, replacement = 1893) +food(name = "cake", id = 1893, restoration = 4, replacement = 1895) +food(name = "cake", id = 1895, restoration = 4) + +food(name = "chocolate_cake", id = 1897, restoration = 5, replacement = 1899) +food(name = "chocolate_cake", id = 1899, restoration = 5, replacement = 1901) +food(name = "chocolate_cake", id = 1901, restoration = 5) + +//# Vegetables +food(name = "potato", id = 1942, restoration = 1) +food(name = "spinach_roll", id = 1969, restoration = 2) +food(name = "baked_potato", id = 6701, restoration = 4) +food(name = "sweetcorn", id = 5988, restoration = 10) +food(name = "sweetcorn_bowl", id = 7088, restoration = 13) +food(name = "potato_with_butter", id = 6703, restoration = 14) +food(name = "chili_potato", id = 7054, restoration = 14) +food(name = "potato_with_cheese", id = 6705, restoration = 16) +food(name = "egg_potato", id = 7056, restoration = 16) +food(name = "mushroom_potato", id = 7058, restoration = 20) +food(name = "tuna_potato", id = 7060, restoration = 22) + +//# Dairy +food(name = "cheese", id = 1985, restoration = 2) +food(name = "pot_of_cream", id = 2130, restoration = 1) + +//# Gnome Food +food(name = "toads_legs", id = 2152, restoration = 3) + +//# Gnome Bowls +food(name = "worm_hole", id = 2191, restoration = 12) +food(name = "worm_hole", id = 2233, restoration = 12) +food(name = "vegetable_ball", id = 2195, restoration = 12) +food(name = "vegetable_ball", id = 2235, restoration = 12) +food(name = "tangled_toads_legs", id = 2187, restoration = 15) +food(name = "tangled_toads_legs", id = 2231, restoration = 15) +food(name = "chocolate_bomb", id = 2185, restoration = 15) +food(name = "chocolate_bomb", id = 2229, restoration = 15) + +//# Gnome Crunchies +food(name = "toad_crunchies", id = 2217, restoration = 7) +food(name = "toad_crunchies", id = 2243, restoration = 7) +food(name = "spicy_crunchies", id = 2213, restoration = 7) +food(name = "spicy_crunchies", id = 2241, restoration = 7) +food(name = "worm_crunchies", id = 2205, restoration = 8) +food(name = "worm_crunchies", id = 2237, restoration = 8) +food(name = "chocchip_crunchies", id = 2209, restoration = 7) +food(name = "chocchip_crunchies", id = 2239, restoration = 7) + +//# Gnome Battas +food(name = "fruit_batta", id = 2225, restoration = 11) +food(name = "fruit_batta", id = 2277, restoration = 11) +food(name = "toad_batta", id = 2221, restoration = 11) +food(name = "toad_batta", id = 2255, restoration = 11) +food(name = "worm_batta", id = 2219, restoration = 11) +food(name = "worm_batta", id = 2253, restoration = 11) +food(name = "vegetable_batta", id = 2227, restoration = 11) +food(name = "vegetable_batta", id = 2281, restoration = 11) +food(name = "cheese_tom_batta", id = 2223, restoration = 11) +food(name = "cheese_tom_batta", id = 2259, restoration = 11) diff --git a/game/src/plugins/consumables/test/FoodOrDrinkTests.kt b/game/src/plugins/consumables/test/FoodOrDrinkTests.kt new file mode 100644 index 00000000..1394e64b --- /dev/null +++ b/game/src/plugins/consumables/test/FoodOrDrinkTests.kt @@ -0,0 +1,86 @@ +package org.apollo.plugin.consumables + +import org.apollo.game.message.impl.ItemOptionMessage +import org.apollo.game.model.entity.Skill +import org.apollo.game.plugin.testing.KotlinPluginTest +import org.apollo.game.plugin.testing.mockito.KotlinMockitoExtensions.matches +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +class FoodOrDrinkTests : KotlinPluginTest() { + + companion object { + const val TEST_FOOD_NAME = "test_food" + const val TEST_FOOD_ID = 2000 + const val TEST_FOOD_RESTORATION = 5 + + const val TEST_DRINK_NAME = "test_drink" + const val TEST_DRINK_ID = 2001 + const val TEST_DRINK_RESTORATION = 5 + + const val HP_LEVEL = 5 + const val MAX_HP_LEVEL = 10 + } + + @Before override fun setup() { + super.setup() + + val skills = player.skillSet + skills.setCurrentLevel(Skill.HITPOINTS, HP_LEVEL) + skills.setMaximumLevel(Skill.HITPOINTS, MAX_HP_LEVEL) + + food("test_food", TEST_FOOD_ID, TEST_FOOD_RESTORATION) + drink("test_drink", TEST_DRINK_ID, TEST_DRINK_RESTORATION) + } + + @Test fun `Consuming food or drink should restore the players hitpoints`() { + val expectedHpLevel = TEST_FOOD_RESTORATION + HP_LEVEL + + player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1)) + player.waitForActionCompletion() + + val currentHpLevel = player.skillSet.getCurrentLevel(Skill.HITPOINTS) + assertThat(currentHpLevel).isEqualTo(expectedHpLevel) + } + + @Test fun `A message should be sent notifying the player if the item restored hitpoints`() { + player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1)) + player.waitForActionCompletion() + + verify(player).sendMessage(matches { + assertThat(this).contains("heals some health") + }) + } + + @Test fun `A message should not be sent to the player if the item did not restore hitpoints`() { + player.skillSet.setCurrentLevel(Skill.HITPOINTS, MAX_HP_LEVEL) + player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1)) + player.waitForActionCompletion() + + verify(player, never()).sendMessage(matches { + assertThat(this).contains("heals some health") + }) + } + + @Test fun `A message should be sent saying the player has drank an item when consuming a drink`() { + player.notify(ItemOptionMessage(1, -1, TEST_DRINK_ID, 1)) + player.waitForActionCompletion() + + verify(player).sendMessage(matches { + assertThat(this).contains("You drink the ${TEST_DRINK_NAME}") + }) + } + + @Test fun `A message should be sent saying the player has eaten an item when consuming food`() { + player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1)) + player.waitForActionCompletion() + + verify(player).sendMessage(matches { + assertThat(this).contains("You eat the ${TEST_FOOD_NAME}") + }) + } + +} \ No newline at end of file