From d91396d0ea828c61411bdc30a826929d6b09a415 Mon Sep 17 00:00:00 2001 From: Ryan <2804894-ryannathans@users.noreply.gitlab.com> Date: Fri, 6 May 2022 22:54:32 +0000 Subject: [PATCH] adjusted price calculation formula used when selling items to shops --- .../rs09/game/content/global/shops/Shop.kt | 49 ++--- Server/src/test/kotlin/ShopTests.kt | 167 ++++++++++++++++-- Server/src/test/kotlin/TestUtils.kt | 9 +- 3 files changed, 185 insertions(+), 40 deletions(-) diff --git a/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt b/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt index 279b5f9ff..31e13de4f 100644 --- a/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt +++ b/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt @@ -15,11 +15,11 @@ import org.rs09.consts.Components import org.rs09.consts.Items import rs09.ServerConstants import rs09.game.content.global.shops.Shops.Companion.logShop -import rs09.game.system.SystemLogger import rs09.game.world.GameWorld import java.lang.Integer.max import java.lang.Integer.min import kotlin.math.ceil +import kotlin.math.roundToInt data class ShopItem(var itemId: Int, var amount: Int, val restockRate: Int = 100) @@ -168,7 +168,14 @@ class Shop(val title: String, val stock: Array, val general: Boolean = { val shopCont = getAttribute(player, "shop-cont", null) ?: return Pair(null, Item(-1,-1)) val item = player.inventory[slot] - var (isPlayerStock, shopSlot) = getStockSlot(item.id) + val shopItemId = if (item.definition.isUnnoted) { + item.id + } + else + { + item.noteChange + } + var (isPlayerStock, shopSlot) = getStockSlot(shopItemId) val stockAmt = if(isPlayerStock) @@ -178,7 +185,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = else 0 } val currentAmt = - if(isPlayerStock) playerStock.getAmount(item.id) + if(isPlayerStock) playerStock.getAmount(shopItemId) else { if(shopSlot != -1) shopCont[shopSlot].amount else { @@ -191,7 +198,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = { Items.TOKKUL_6529 -> item.definition.getConfiguration("tokkul_price", 1) Items.ARCHERY_TICKET_1464 -> item.definition.getConfiguration("archery_ticket_price", 1) - else -> getGPSell(Item(item.id, 1), stockAmt, currentAmt) + else -> getGPSell(Item(shopItemId, 1), stockAmt, currentAmt) } if(!general && stockAmt == 0 && shopSlot == -1) @@ -202,7 +209,8 @@ class Shop(val title: String, val stock: Array, val general: Boolean = return Pair(if (isPlayerStock) playerStock else shopCont, Item(currency, price)) } - private fun getGPCost(item: Item, stockAmount: Int, currentAmt: Int): Int{ + private fun getGPCost(item: Item, stockAmount: Int, currentAmt: Int): Int + { var mod: Int mod = if(stockAmount == 0) 100 else if(currentAmt == 0) 130 @@ -219,22 +227,21 @@ class Shop(val title: String, val stock: Array, val general: Boolean = return max(price, 1) } - private fun getGPSell(item: Item, stockAmount: Int, currentAmt: Int): Int{ - if(!item.definition.isUnnoted) - item.id = item.noteChange - var mod: Int - mod = if(stockAmount == 0) 70 - else if(currentAmt == 0) 100 - else if(currentAmt >= stockAmount) 70 - else 100 - (100 - 70) * currentAmt / stockAmount - if(mod < 1) mod = 1 - mod = max(70, min(100, mod)) - - var base = if (highAlch) item.definition.getAlchemyValue(true) else item.definition.value - base = max(base, item.definition.value) - - val price: Int = ceil(base * mod.toDouble() / 100.0).toInt() - return max(price, 1) + private fun getGPSell(item: Item, stockAmount: Int, currentAmt: Int): Int + { + val base = item.definition.getAlchemyValue(highAlch) + var overstock = currentAmt - stockAmount + if (overstock < 0) { + return base + } + if (overstock > 10) { + overstock = 10 + } + val price = (base - (item.definition.value * 0.03 * overstock)).roundToInt() + if (price < 1) { + return 1 + } + return price } fun buy(player: Player, slot: Int, amount: Int) : TransactionStatus diff --git a/Server/src/test/kotlin/ShopTests.kt b/Server/src/test/kotlin/ShopTests.kt index e705b05b1..3a207e361 100644 --- a/Server/src/test/kotlin/ShopTests.kt +++ b/Server/src/test/kotlin/ShopTests.kt @@ -1,23 +1,45 @@ import core.game.node.entity.player.link.IronmanMode import core.game.node.item.Item -import org.junit.Assert import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.rs09.consts.Items import rs09.game.content.global.shops.Shop import rs09.game.content.global.shops.Shops -import rs09.game.system.config.ShopParser +import kotlin.math.min +import kotlin.math.roundToInt class ShopTests { - val testPlayer = TestUtils.getMockPlayer("test") - val testIronman = TestUtils.getMockPlayer("test2", IronmanMode.STANDARD) - val nonGeneral = TestUtils.getMockShop("Not General", false, Item(4151, 1)) - val general = TestUtils.getMockShop("General", true, Item(4151, 1)) + companion object { + init {TestUtils.preTestSetup()} + } + + private val testPlayer = TestUtils.getMockPlayer("test") + private val testIronman = TestUtils.getMockPlayer("test2", IronmanMode.STANDARD) + var nonGeneral = TestUtils.getMockShop("Not General", false, false, Item(4151, 1)) + var general = TestUtils.getMockShop("General", true, false, Item(4151, 1)) + var highAlch = TestUtils.getMockShop("High(af) Alch", true, true, Item(4151, 1)) + + @BeforeEach fun beforeEach() { + val testPlayers = arrayOf(testPlayer, testIronman) + for (player in testPlayers) { + player.inventory.clear() + player.attributes.remove("shop-cont") + } + nonGeneral.playerStock.clear() + general.playerStock.clear() + highAlch.playerStock.clear() + } + + private fun assertTransactionSuccess(status: Shop.TransactionStatus) { + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } @Test fun shouldSellItemToStore() { testPlayer.inventory.add(Item(4151, 1)) testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) val status = general.sell(testPlayer, 0, 1) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failed: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) } @Test fun shouldNotSellUnstockedItemToStandardStore() { @@ -31,28 +53,139 @@ class ShopTests { testPlayer.inventory.add(Item(1, 1)) testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) val status = general.sell(testPlayer, 0, 1) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) + } + + @Test fun shouldSellUnstockedItemToGeneralStoreUsingLowAlchBaseValue() { + //starts at 40% value - a.k.a. low alch price + //drops 3% value per item stocked + //bottoms out at 10% value (25% of the low alch price) after 10 items stocked + val saleItemId = Items.RUNE_MED_HELM_1147 + val shopContainer = general.getContainer(testPlayer) + Assertions.assertFalse(shopContainer.containItems(saleItemId), "Pre-assertion, shop container should not have the unstocked item to begin with.") + testPlayer.setAttribute("shop-cont", shopContainer) + val playerStock = general.playerStock + Assertions.assertFalse(playerStock.containItems(saleItemId), "Pre-assertion, player stock should not have the unstocked item to begin with.") + val saleItem = Item(saleItemId, 1) + val alchValue = saleItem.definition.getAlchemyValue(false) + val value = saleItem.definition.value + Assertions.assertEquals((value * 0.4).roundToInt(), alchValue, "Pre-assertion, low alch value should be 40% value.") + + for (i in 0..14) { + val expectedCoins = (alchValue.toDouble() - value * min(0.03 * i, 0.30)).roundToInt() + testPlayer.inventory.add(saleItem.copy()) + Assertions.assertEquals(saleItemId, testPlayer.inventory.getId(0), "Pre-assertion, should have item in inventory slot 0.") + + val status = general.sell(testPlayer, 0, 1) + + assertTransactionSuccess(status) + val coinItem = testPlayer.inventory[0] + Assertions.assertEquals(Items.COINS_995, coinItem.id) + Assertions.assertEquals( + expectedCoins, + coinItem.amount, + "Selling item $i should yield the expected price." + ) + testPlayer.inventory.clear() + } + } + + @Test fun shouldSellUnstockedItemToHighAlchStoreUsingHighAlchBaseValue() { + //starts at 60% value - a.k.a. high alch price + //drops 3% value per item stocked + //bottoms out at 30% value (50% of the high alch price) after 10 items stocked + val saleItemId = Items.RUNE_MED_HELM_1147 + val shopContainer = highAlch.getContainer(testPlayer) + Assertions.assertFalse(shopContainer.containItems(saleItemId), "Pre-assertion, shop container should not have the unstocked item to begin with.") + testPlayer.setAttribute("shop-cont", shopContainer) + val playerStock = highAlch.playerStock + Assertions.assertFalse(playerStock.containItems(saleItemId), "Pre-assertion, player stock should not have the unstocked item to begin with.") + val saleItem = Item(saleItemId, 1) + val alchValue = saleItem.definition.getAlchemyValue(true) + val value = saleItem.definition.value + Assertions.assertEquals((value * 0.6).roundToInt(), alchValue, "Pre-assertion, high alch value should be 60% value.") + + for (i in 0..14) { + val expectedCoins = (alchValue.toDouble() - value * min(0.03 * i, 0.30)).roundToInt() + testPlayer.inventory.add(saleItem.copy()) + Assertions.assertEquals(saleItemId, testPlayer.inventory.getId(0), "Pre-assertion, should have item in inventory slot 0.") + + val status = highAlch.sell(testPlayer, 0, 1) + + assertTransactionSuccess(status) + val coinItem = testPlayer.inventory[0] + Assertions.assertEquals(Items.COINS_995, coinItem.id) + Assertions.assertEquals( + expectedCoins, + coinItem.amount, + "Selling item $i should yield the expected price." + ) + testPlayer.inventory.clear() + } + } + + @Test fun shouldSellNotedUnstockedItemForSamePriceAsUnnoted() { + val saleItemId = Items.RUNE_MED_HELM_1147 + var notedSaleItemId = Items.RUNE_MED_HELM_1148 + val shopContainer = highAlch.getContainer(testPlayer) + Assertions.assertFalse(shopContainer.containItems(saleItemId), "Pre-assertion, shop container should not have the unstocked item to begin with.") + testPlayer.setAttribute("shop-cont", shopContainer) + val playerStock = highAlch.playerStock + Assertions.assertFalse(playerStock.containItems(saleItemId), "Pre-assertion, player stock should not have the unstocked item to begin with.") + testPlayer.inventory.add(Item(saleItemId, 1)) + var status = highAlch.sell(testPlayer, 0, 1) + assertTransactionSuccess(status) + testPlayer.inventory.clear() + val notedSaleItem = Item(notedSaleItemId, 1) + testPlayer.inventory.add(notedSaleItem) + val alchValue = notedSaleItem.definition.getAlchemyValue(true) + val value = notedSaleItem.definition.value + status = highAlch.sell(testPlayer, 0, 1) + assertTransactionSuccess(status) + val expectedCoins = (alchValue.toDouble() - value * 0.03).roundToInt() + + val coinItem = testPlayer.inventory[0] + Assertions.assertEquals(Items.COINS_995, coinItem.id) + Assertions.assertEquals( + expectedCoins, + coinItem.amount, + "Selling noted item should yield the same devalued price as base item." + ) + } + + @Test fun minimumSellPriceShouldBe1Coin() { + testPlayer.inventory.add(Item(Items.EMPTY_POT_1931, 1)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + val status = general.sell(testPlayer, 0, 1) + assertTransactionSuccess(status) + val coinItem = testPlayer.inventory[0] + Assertions.assertEquals(Items.COINS_995, coinItem.id) + Assertions.assertEquals( + 1, + coinItem.amount, + "1 coin should be the minimum selling price." + ) } @Test fun shouldSellUnstockedItemToGeneralStoreAsIronman() { testIronman.inventory.add(Item(1, 1)) - testIronman.setAttribute("shop-cont", general.getContainer(testPlayer)) + testIronman.setAttribute("shop-cont", general.getContainer(testIronman)) val status = general.sell(testIronman, 0, 1) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) } @Test fun shouldSellStackOfUnstockedItemsToPlayerStock() { testPlayer.inventory.add(Item(1, 20)) testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) val status = general.sell(testPlayer, 0, 20) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) } @Test fun shouldPutSoldUnstockedItemsInPlayerStock() { testPlayer.inventory.add(Item(2,1)) testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) val status = general.sell(testPlayer, 0, 1) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) Assertions.assertEquals(1, general.playerStock.getAmount(2)) Assertions.assertEquals(0, general.getContainer(testPlayer).getAmount(2)) } @@ -62,7 +195,7 @@ class ShopTests { testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) testPlayer.setAttribute("shop-main", true) val status = general.buy(testPlayer, 0, 1) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) } @Test fun shouldAllowStandardPlayerToBuyOverstock() { @@ -71,7 +204,7 @@ class ShopTests { testPlayer.setAttribute("shop-main", true) general.getContainer(testPlayer).add(Item(4151, 100)) val status = general.buy(testPlayer, 0, 1) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) } @Test fun shouldAllowStandardPlayerToBuyPlayerStock() { @@ -80,7 +213,7 @@ class ShopTests { testPlayer.setAttribute("shop-main", false) general.playerStock.add(Item(4151, 100)) val status = general.buy(testPlayer, 0, 1) - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + assertTransactionSuccess(status) } @Test fun shouldNotAllowIronmanToBuyOverstock() { @@ -128,11 +261,11 @@ class ShopTests { testPlayer.inventory.add(Item(995, 100000)) testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) testPlayer.setAttribute("shop-main", false) - var status: Shop.TransactionStatus = Shop.TransactionStatus.Success() + var status: Shop.TransactionStatus = Shop.TransactionStatus.Failure("Test did not assign transaction status.") Assertions.assertDoesNotThrow({ status = general.buy(testPlayer, 36, 1) }, "Error selling to shop: ${general.playerStock}") - Assertions.assertEquals(true, status is Shop.TransactionStatus.Success) + assertTransactionSuccess(status) } @Test fun invalidStockJsonShouldNotCauseItemShift() { diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt index 79ccddbcc..4b64e950e 100644 --- a/Server/src/test/kotlin/TestUtils.kt +++ b/Server/src/test/kotlin/TestUtils.kt @@ -22,8 +22,13 @@ object TestUtils { return p } - fun getMockShop(name: String, general: Boolean, vararg stock: Item) : Shop { - return Shop(name, stock.map { ShopItem(it.id, it.amount, 100) }.toTypedArray(), general) + fun getMockShop(name: String, general: Boolean, highAlch: Boolean, vararg stock: Item) : Shop { + return Shop( + name, + stock.map { ShopItem(it.id, it.amount, 100) }.toTypedArray(), + general, + highAlch = highAlch + ) } fun preTestSetup() {