diff --git a/Server/src/main/java/core/game/content/quest/members/merlinscrystal/ArheinShopDialogue.java b/Server/src/main/java/core/game/content/quest/members/merlinscrystal/ArheinShopDialogue.java deleted file mode 100644 index 4787892ea..000000000 --- a/Server/src/main/java/core/game/content/quest/members/merlinscrystal/ArheinShopDialogue.java +++ /dev/null @@ -1,185 +0,0 @@ -package core.game.content.quest.members.merlinscrystal; - -import core.game.content.dialogue.DialoguePlugin; -import core.game.content.dialogue.FacialExpression; -import core.game.node.entity.npc.NPC; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.link.quest.Quest; - -/** - * Represents the dialogue plugin used for the arhein npc. - * @author 'Vexia - * @version 1.0 - */ -public final class ArheinShopDialogue extends DialoguePlugin { - - /** - * Constructs a new {@code ArheinShopDialogue} {@code Object}. - */ - public ArheinShopDialogue() { - /** - * empty. - */ - } - - /** - * Constructs a new {@code ArheinShopDialogue} {@code Object}. - * @param player the player. - */ - public ArheinShopDialogue(Player player) { - super(player); - } - - @Override - public DialoguePlugin newInstance(Player player) { - return new ArheinShopDialogue(player); - } - - @Override - public boolean open(Object... args) { - npc = (NPC) args[0]; - if (args.length > 1) { - interpreter.sendDialogues(npc, null, "Hey buddy! Get away from my ship alright?"); - stage = 2000; - return true; - } - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Hello! Would you like to trade? I've a variety of wares", "for sale!"); - stage = 0; - return true; - } - - @Override - public boolean handle(int interfaceId, int buttonId) { - Quest quest = player.getQuestRepository().getQuest("Merlin's Crystal"); - switch (stage) { - case 0: - interpreter.sendOptions("Select an Option", "Let's trade.", "No thank you.", "Is that your ship?"); - stage = 1; - break; - case 1: - switch (buttonId) { - case 1: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Let's trade."); - stage = 100; - break; - case 2: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "No thanks."); - stage = 200; - break; - case 3: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Is that your ship?"); - stage = 300; - break; - } - break; - case 100: - end(); - npc.openShop(player); - break; - case 300: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Yes, I use it to make deliveries to my customers up and", "down the coast. These crates here are all ready for my", "next trip."); - stage = 301; - break; - case 301: - interpreter.sendOptions("Select an Option", "Where do you deliver to?", "Are you rich then?", "Do you deliver to the fort just down the coast?"); - stage = 303; - break; - case 303: - switch (buttonId) { - case 1: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Where do you deliver to?"); - stage = 1000; - break; - case 2: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Are you rich then?"); - stage = 2500; - break; - case 3: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Do you deliver to the fort just down the coast?"); - stage = 3000; - break; - - } - break; - case 1000: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Various places up and down the coast. Mostly Karamja", "and Port Sarim."); - stage = 1001; - break; - case 1001: - interpreter.sendOptions("Select an Option", "I don't suppose I could get a lift anywhere?", "Well, good luck with your business."); - stage = 1002; - break; - case 1002: - switch (buttonId) { - case 1: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "I don't suppose I could get a lift anywhere?"); - stage = 1003; - break; - case 2: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Well, good luck with your business."); - stage = 2700; - break; - - } - break; - case 1003: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Sorry pal, but I'm afraid I'm not quite ready to sail yet."); - stage = 1004; - break; - case 1004: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "I'm waiting on a big delivery of candles which I need to", "deliver further along the coast."); - stage = 1005; - break; - case 1005: - end(); - break; - case 2500: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Business is going reasonably well... I wouldn't say I was the", "richest of merchants every, but I'm doing fairly well all", "things considered."); - stage = 2501; - break; - case 2501: - end(); - break; - case 2700: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Thanks buddy!"); - stage = 2701; - break; - case 2701: - end(); - break; - case 200: - end(); - break; - case 2000: - interpreter.sendDialogues(player, null, "Yeah... uh... sorry..."); - stage = 2001; - break; - case 2001: - end(); - break; - case 3000: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Yes, I do have orders to deliver there from time to", "time. I think I may have some bits and pieces for them", "when I leave here next actually."); - if (quest.getStage(player) == 30 || quest.getStage(player) == 40) { - stage = 3001; - } else { - stage = 2701; - } - break; - case 3001: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Can you drop me off on the way down please?"); - stage = 3002; - break; - case 3002: - interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "I don't think Sir Mordred would like that. He wants as", "few outsiders visiting as possible. I wouldn't want to lose", "his business."); - quest.setStage(player, 40); - stage = 2701; - break; - } - return true; - } - - @Override - public int[] getIds() { - return new int[] { 563 }; - } -} \ No newline at end of file diff --git a/Server/src/main/java/core/game/content/quest/members/merlinscrystal/MerlinCrystal.java b/Server/src/main/java/core/game/content/quest/members/merlinscrystal/MerlinCrystal.java index 42b7d4fd9..0fbac8411 100644 --- a/Server/src/main/java/core/game/content/quest/members/merlinscrystal/MerlinCrystal.java +++ b/Server/src/main/java/core/game/content/quest/members/merlinscrystal/MerlinCrystal.java @@ -79,7 +79,6 @@ public final class MerlinCrystal extends Quest { public Quest newInstance(Object object) { ClassScanner.definePlugins(new CrateCutscenePlugin(), new MerlinCrystalPlugin(), - new ArheinShopDialogue(), new BeggarDialogue(), new CandleMakerDialogue(), new KingArthurDialogue(), diff --git a/Server/src/main/kotlin/rs09/ServerStore.kt b/Server/src/main/kotlin/rs09/ServerStore.kt index 317fb9e6b..0bbe3cc77 100644 --- a/Server/src/main/kotlin/rs09/ServerStore.kt +++ b/Server/src/main/kotlin/rs09/ServerStore.kt @@ -3,6 +3,9 @@ package rs09 import api.PersistWorld import api.ShutdownListener import api.StartupListener +import api.getItemName +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player import org.json.simple.JSONArray import org.json.simple.JSONObject import org.json.simple.parser.JSONParser @@ -131,5 +134,71 @@ class ServerStore : PersistWorld { } return jArray } + + /** NPCItemMemory + * These next methods handle the NPCItemMemory database. these are server-stored JSON objects, + * which are based on an npc and an item. + * each NPC can have a JSON object for itself for any item. + * the NPC Item object can store integer values for individual players, using their names as keys. + * the first methods handle the serverstore filename and accessing the JSON object. + * these methods are wrapped by more convenient ones that allow access for a particular player, see below. + */ + fun NPCItemFilename(npc: Int, item: Int, period: String = "daily"): String { + val itemName = getItemName(item).lowercase().replace(" ","-") + val npcName = NPC(npc).name.lowercase() + return "$period-$npcName-$itemName" + } + fun NPCItemMemory(npc: Int, item: Int, period: String = "daily"): JSONObject { + return getArchive(NPCItemFilename(npc,item,period)) + } + + /** NPCItemMemory Player Access + * These next functions handle individual player access to the NPC Item Memory database. + * each of the functions has at least 3 mandatory arguments: npc, item, player. + * These functions can be used to allow players to access a daily- or weekly-reset stock of a particular item overseen by a particular NPC. + * + * note that none of these functions perform tangible actions on the player. + * whoever calls these functions is responsible for handling tangible actions like putting items in an inventory. + * these functions simply return and update the NPCItemMemory database numbers in useful ways. + */ + + /** getNPCItemStock: + * gets the available stock of a particular item at a particular NPC for a particular player. + */ + fun getNPCItemStock(npc: Int, item: Int, limit: Int, player: Player, period: String = "daily"): Int { + val itemMemory = NPCItemMemory(npc,item) + val key = player.name + var stock = limit-itemMemory.getInt(key) + stock = maxOf(stock,0) + return stock + } + /** getNPCItemAmount: + * gets the usable amount of a particular item from a particular npc for a particular player. + * this is essentially just a requested amount cut to conform to stock and nonnegative requirements. + * what this function does is it takes a requested amount, cuts it down to be equal to available stock if + * available stock is less than the requested amount, and then additionally sets a minimum of 0. + */ + fun getNPCItemAmount(npc: Int, item: Int, limit: Int, player: Player, amount: Int, period: String = "daily"): Int { + val stock = getNPCItemStock(npc,item,limit,player,period) + var realamount = minOf(amount,stock) + realamount = maxOf(realamount,0) + return realamount + } + /** addNPCItemAmount: + * this function updates the NPC Item Memory database entry for a particular player. + * it is intended to be called after all the actions that use the number have been successfully completed. + * this function will *add* the amount argument to the current npc item memory entry for that player. this means that + * the amount argument is the amount to add, not the absolute final amount. + * this function will handle correctly setting the limit and any other required semantics. + */ + fun addNPCItemAmount(npc: Int, item: Int, limit: Int, player: Player, amount: Int, period: String = "daily") { + val itemMemory = NPCItemMemory(npc,item,period) + val key = player.name + var realamount = itemMemory.getInt(key) + amount + realamount = minOf(realamount,limit) + realamount = maxOf(realamount,0) + itemMemory[key] = realamount + } } -} \ No newline at end of file +} + diff --git a/Server/src/main/kotlin/rs09/game/content/dialogue/region/catherby/ArheinDialogue.kt b/Server/src/main/kotlin/rs09/game/content/dialogue/region/catherby/ArheinDialogue.kt new file mode 100644 index 000000000..9ca851907 --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/content/dialogue/region/catherby/ArheinDialogue.kt @@ -0,0 +1,189 @@ +package rs09.game.content.dialogue.region.catherby +import api.* +import core.game.content.dialogue.DialoguePlugin +import core.game.content.dialogue.FacialExpression +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player +import core.game.node.item.Item +import core.plugin.Initializable +import core.tools.StringUtils +import org.rs09.consts.NPCs +import rs09.ServerStore +import rs09.game.content.dialogue.Topic +import rs09.tools.END_DIALOGUE +import org.rs09.consts.Items +import rs09.game.content.quest.members.merlinsquest.ArheinMCDialogue + + +/** + * @author lila + */ + +/** + * Arhein dialogue + * This class handles all dialogue from the Catherby dock NPC Arhein. + * + * Arhein is relevant to: + * Merlin's Crystal quest + * crafting/glassmaking - via selling seaweed + * farming/supercompost - via selling pineapples + */ + +@Initializable +class ArheinDialogue(player: Player? = null) : DialoguePlugin(player) { + + val limits = mapOf( + Items.PINEAPPLE_2114 to 40, + Items.SEAWEED_401 to 80 + ) + val period = "daily" + + // this function handles Arhein's specialty stock (pineapple and seaweed) transactions. + fun getGoods(requestedItem: Int, requestedAmount: Int): Int { + val price = 2 + val afford = player.getInventory().getAmount(Items.COINS_995) / price + var realamount = minOf(requestedAmount, afford) + val exactafford = (realamount == afford) && (afford == freeSlots(player) + 1) + realamount = minOf(realamount,if(exactafford) realamount else freeSlots(player)) + realamount = ServerStore.getNPCItemAmount(NPCs.ARHEIN_563, requestedItem, limits.getOrDefault(requestedItem,0), player, realamount, period) + if (removeItem(player, Item(Items.COINS_995, realamount * price), Container.INVENTORY)) { + if (addItem(player, requestedItem, realamount, Container.INVENTORY)) { + ServerStore.addNPCItemAmount(NPCs.ARHEIN_563, requestedItem, limits.getOrDefault(requestedItem,0), player, realamount, period) + return realamount + } + } + return 0 + } + // this function handles selecting which of the specialty stock items (pineapple or seaweed) to use. + fun selectGoods(requestedItem: Int) { + this.goods = requestedItem + this.stock = ServerStore.getNPCItemStock(NPCs.ARHEIN_563,this.goods,limits.getOrDefault(this.goods,0),player,period) + } + + // some class variables that handle the special stock (pineapples and seaweed) + private var goods = Items.PINEAPPLE_2114 + private var goodsMessage = "" + private var goodsName = "" + private var stock = 0 + + /** Arhein Dialogue Opener + * If the opener is called with nontrivial arguments, then this indicates that the player tried to board the ship without permission. + * In that case, Arhein tells the player to bugger off. + * Otherwise, start the normal dialogue. + * Before all this, we check and initialize some important information for Arhein's specialty stock. + */ + override fun open(vararg args: Any?): Boolean { + npc = args[0] as NPC + + if (args.size > 1) { + npcl(FacialExpression.ANGRY, "Hey buddy! Get away from my ship alright?") + stage = 701 + } else { + npcl(FacialExpression.HAPPY, "Hello! Would you like to trade? I've a variety of wares for sale!") + stage = 1 + } + return true + } + + /** Arhein Dialogue Handler + * Handler for the dialogue. the handler determines dialogue content and flow, + * as well as some specialty operations to handle special things like quest progress and specialty stock. + */ + override fun handle(interfaceId: Int, buttonId: Int): Boolean { + when(stage) { + 1 -> showTopics( + Topic("Let's Trade.", 7), + Topic("I hear that you sell pineapples.",800), + Topic("I hear that you sell seaweed.",900), + Topic("No thank you.", END_DIALOGUE), + Topic("Is that your ship?", 100) + ) + 7 -> npc.openShop(player).also { stage = END_DIALOGUE } + 100 -> npcl(FacialExpression.NEUTRAL, "Yes, I use it to make deliveries to my customers up and down the coast. These crates here are all ready for my next trip.").also { stage++ } + 101 -> showTopics( + Topic("Where do you deliver to?",120), + Topic("Are you rich then?", 110), + Topic("Do you deliver to the fort just down the coast?",500) + ) + 110 -> npcl(FacialExpression.NEUTRAL, "Business is going reasonably well... I wouldn't say I was the richest of merchants ever, but I'm doing fairly well all things considered.").also { stage = END_DIALOGUE } + 120 -> npcl(FacialExpression.NEUTRAL, "Various places up and down the coast. Mostly Karamja and Port Sarim.").also { stage++ } + 121 -> showTopics( + Topic("I don't suppose I could get a lift anywhere?",140), + Topic("Well, good luck with your business.",130) + ) + 130 -> npcl(FacialExpression.HAPPY,"Thanks buddy!").also { stage = END_DIALOGUE } + 140 -> npcl(FacialExpression.GUILTY,"Sorry pal, but I'm afraid I'm not quite ready to sail yet.").also { stage++ } + 141 -> npcl(FacialExpression.NEUTRAL,"I'm waiting on a big delivery of candles which I need to deliver further along the coast.").also { stage = END_DIALOGUE } + 500 -> npcl(FacialExpression.HALF_THINKING, "Yes, I do have orders to deliver there from time to time. I think I may have some bits and pieces for them when I leave here next actually.").also { + val queststage = player.questRepository.getStage("Merlin's Crystal") + if(queststage == 30 || queststage == 40) { + loadFile(ArheinMCDialogue(queststage)) + } else { + stage = END_DIALOGUE + } + } + 701 -> playerl(FacialExpression.GUILTY,"Yeah... uh... sorry...").also{ stage = END_DIALOGUE } + 800 -> { + // Pineapples + selectGoods(Items.PINEAPPLE_2114) + // and now the conditional block to handle cases + if(stock==0) { + npcl(FacialExpression.SAD,"Actually, I've run out. Come back tomorrow and I should have some more.") + stage = END_DIALOGUE + } else { + // approximately authentic dialogue prompt for buying pineapples from arhein. (closest known dialogues do not make grammatical sense when spliced.) + this.goodsMessage = "I certainly do! I've got ${stock} in stock, going for 2 coins each. How many would you like?" + npcl(FacialExpression.HAPPY,this.goodsMessage) + this.goodsName = if(stock==1) getItemName(this.goods) else StringUtils.plusS(getItemName(this.goods)) + stage = 1200 + } + } + 900 -> { + // Seaweed + selectGoods(Items.SEAWEED_401) + // and now the conditional block to handle cases + if(stock==0) { + npcl(FacialExpression.SAD,"Actually, I've run out. Come back tomorrow and I should have some more.") + stage = END_DIALOGUE + } else { + // approximately authentic dialogue prompt for buying seaweed from arhein. (closest known dialogues do not make grammatical sense when spliced.) + this.goodsMessage = "I certainly do! I've ${stock} at the moment and they cost 2 coins each. How many would you like?" + npcl(FacialExpression.HAPPY,this.goodsMessage) + this.goodsName = getItemName(this.goods) + stage = 1200 + } + } + 1200 -> { + // arhein specialty items sales + val goodsPrompt = "Arhein has ${this.stock} ${this.goodsName}. How many would you like to buy?" + sendInputDialogue(player,InputType.AMOUNT, goodsPrompt) { value -> + val amountReceived = getGoods(this.goods,Integer.parseInt(value.toString())) + var exitMsg = "" + if(amountReceived==this.stock) { + exitMsg = "Here you go! I've run out for now. Come again tomorrow and I should have more." + } else if (amountReceived>0) { + // inauthentic exit dialogue for arhein's pineapple/seaweed sales. documented dialogue that i found indicates that there is no exit message. + // i kept the spirit of arhein's personality, using "buddy!" that is known from another line. + exitMsg = "Here you go, buddy!" + } else { + // same deal here, inauthentic exit dialogue based on arhein's authentic personality. + exitMsg = "Take care, buddy!" + } + npcl(FacialExpression.HAPPY,exitMsg) + } + stage = END_DIALOGUE + } + } + return true + } + + + // finale funz - newInstance, getIds + override fun newInstance(player: Player?): DialoguePlugin { + return ArheinDialogue(player) + } + override fun getIds(): IntArray { + return intArrayOf(NPCs.ARHEIN_563) + } +} + diff --git a/Server/src/main/kotlin/rs09/game/content/quest/members/merlinsquest/ArheinMCDialogue.kt b/Server/src/main/kotlin/rs09/game/content/quest/members/merlinsquest/ArheinMCDialogue.kt new file mode 100644 index 000000000..86358d38c --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/content/quest/members/merlinsquest/ArheinMCDialogue.kt @@ -0,0 +1,29 @@ +package rs09.game.content.quest.members.merlinsquest + +import core.game.content.dialogue.FacialExpression +import rs09.game.content.dialogue.DialogueFile +import rs09.tools.END_DIALOGUE +import rs09.tools.START_DIALOGUE + +/** + * @author lila + */ + +/** + * This class handles dialogue for Arhein for the Merlin's Crystal quest + */ + +class ArheinMCDialogue (val questStage: Int) : DialogueFile() { + override fun handle(componentID: Int, buttonID: Int) { + when (stage) { + START_DIALOGUE -> playerl(FacialExpression.NEUTRAL, "Can you drop me off on the way down please?").also { stage++ } + 1 -> { + npcl(FacialExpression.ANNOYED,"I don't think Sir Mordred would like that. He wants as few outsiders visiting as possible. I wouldn't want to lose his business.") + val quest = player!!.questRepository.getQuest("Merlin's Crystal") + player!!.questRepository.setStage(quest, 40) + stage = END_DIALOGUE + } + } + } + +} \ No newline at end of file