diff --git a/quests_without_completions.txt b/quests_without_completions.txt new file mode 100644 index 00000000000..50e13ad1f46 --- /dev/null +++ b/quests_without_completions.txt @@ -0,0 +1,23 @@ + +The following quests do not support tracking number of completions: +- adventure_island (Adventure Island) +- dragon_lair (Dragon Lair) +- easter_gifts_[year] (Easter Gifts for Children) +- find_rat_kids (Find Rat Children) +- fishsoup_for_hughie (Fish Soup for Hughie) +- fishsoup_maker (Fish Soup) +- fix_emerald_ring (The Ring Maker) +- fruits_coralia (Fruits for Coralia) +- goodies_rudolph (Goodies for Rudolph) +- guess_kills (The Guessing Game) +- kill_all_spiders (Kill Spiders) +- kill_dhohr_nuggetcutter (Kill Dhohr Nuggetcutter) +- koboldish_torcibud (Koboldish Torcibud) +- meet_bunny_[year] (Meet Easter Bunny) +- meet_santa_[seasonyear] (Meet Santa) +- paper_chase_20[year] (Paper Chase) ??? +- reverse_arrow (Reverse Arrow) +- snowballs (Snowballs for Mr. Yeti) +- soup_maker (Soup) +- the_pied_piper (The Pied Piper) +- zoo_food (Zoo Food) diff --git a/src/games/stendhal/server/entity/npc/RatKidsNPCBase.java b/src/games/stendhal/server/entity/npc/RatKidsNPCBase.java index a4a3517de9a..dfadc8374a7 100644 --- a/src/games/stendhal/server/entity/npc/RatKidsNPCBase.java +++ b/src/games/stendhal/server/entity/npc/RatKidsNPCBase.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -12,12 +12,13 @@ ***************************************************************************/ package games.stendhal.server.entity.npc; -import java.util.Arrays; import java.util.List; +import java.util.Locale; import games.stendhal.common.parser.Sentence; import games.stendhal.server.entity.npc.condition.GreetingMatchesNameCondition; import games.stendhal.server.entity.player.Player; +import games.stendhal.server.maps.quests.FindRatChildren; // import org.apache.log4j.Logger; @@ -58,30 +59,20 @@ protected void createDialog() { private static class RatKidGreetingAction implements ChatAction { @Override public void fire(final Player player, final Sentence sentence, final EventRaiser npc) { - if (!player.hasQuest(QUEST_SLOT) || player.isQuestInState(QUEST_SLOT, "rejected")) { + if (!player.hasQuest(QUEST_SLOT) || player.isQuestInState(QUEST_SLOT, 0, "rejected")) { npc.say("Mother says I mustn't talk to strangers."); } else { - final String npcQuestText = player.getQuest(QUEST_SLOT); - final String[] npcDoneText = npcQuestText.split(":"); - - final String lookStr; - final String saidStr; - if (npcDoneText.length > 1) { - lookStr = npcDoneText[0].toLowerCase(); - saidStr = npcDoneText[1].toLowerCase(); - - final List list = Arrays.asList(lookStr.split(";")); - String npcName = npc.getName().toLowerCase(); - if (list.contains(npcName) || player.isQuestCompleted(QUEST_SLOT)) { - npc.say("Oh hello again."); - } else if ( npcDoneText.length > 1) { - player.setQuest(QUEST_SLOT, lookStr + ";" + npcName - + ":" + saidStr);// - npc.say("Hello my name is " + npc.getName() + ". Please tell mother that I am ok."); - player.addXP(500); - } else { - npc.say("Mother says I mustn't talk to strangers."); - } + final List found = FindRatChildren.getFoundNames(player); + String npcName = npc.getName().toLowerCase(Locale.ENGLISH); + final boolean questCompleted = player.isQuestCompleted(QUEST_SLOT); + if (questCompleted || found.contains(npcName)) { + npc.say("Oh hello again."); + } else if (!questCompleted) { + FindRatChildren.addFoundName(player, npcName); + npc.say("Hello my name is " + npc.getName() + ". Please tell mother that I am ok."); + player.addXP(500); + } else { + npc.say("Mother says I mustn't talk to strangers."); } } } diff --git a/src/games/stendhal/server/entity/player/Player.java b/src/games/stendhal/server/entity/player/Player.java index 5850c7eb979..4bdd5210a32 100644 --- a/src/games/stendhal/server/entity/player/Player.java +++ b/src/games/stendhal/server/entity/player/Player.java @@ -3024,20 +3024,6 @@ protected void handleLeaveZone(int nx, final int ny) { } } - /** - * Gets the number of repetitions in a substate of quest slot - * - * @param questname - * The quest's name - * @param index - * the index of the sub state to get (separated by ";") - * @return the integer value in the index of the quest slot, used to - * represent a number of repetitions - */ - public int getNumberOfRepetitions(final String questname, final int index) { - return quests.getNumberOfRepetitions(questname, index); - } - /** * gets the timestmap this client sent the last action * diff --git a/src/games/stendhal/server/entity/player/PlayerQuests.java b/src/games/stendhal/server/entity/player/PlayerQuests.java index 33c615f8902..3498992b528 100644 --- a/src/games/stendhal/server/entity/player/PlayerQuests.java +++ b/src/games/stendhal/server/entity/player/PlayerQuests.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2020 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -277,23 +277,4 @@ public int getRequiredItemQuantity(final String name, final int index) { return amount; } - - /** - * Gets the number of repetitions in a substate of quest slot - * - * @param name - * The quest's name - * @param index - * the index of the sub state to get (separated by ";") - * @return the integer value in the index of the quest slot, used to represent a number of repetitions - */ - public int getNumberOfRepetitions(final String name, final int index) { - if (!player.hasQuest(name)) { - logger.error(player.getName() + " does not have quest " + name); - return 0; - } - String questState = player.getQuest(name, index); - return MathHelper.parseIntDefault(questState, 0); - } - } diff --git a/src/games/stendhal/server/entity/player/UpdateConverter.java b/src/games/stendhal/server/entity/player/UpdateConverter.java index e6b7c706711..207f0bb785b 100644 --- a/src/games/stendhal/server/entity/player/UpdateConverter.java +++ b/src/games/stendhal/server/entity/player/UpdateConverter.java @@ -31,6 +31,7 @@ import games.stendhal.server.entity.slot.EntitySlot; import games.stendhal.server.entity.slot.KeyedSlot; import games.stendhal.server.entity.slot.PlayerSlot; +import games.stendhal.server.maps.quests.FindRatChildren; import games.stendhal.server.util.TimeUtil; import marauroa.common.Pair; import marauroa.common.game.RPObject; @@ -673,6 +674,19 @@ public static void updateQuests(final Player player) { player.setQuest(questSlot, 1, Long.toString(System.currentTimeMillis() - (TimeUtil.MINUTES_IN_HALF_YEAR / 2 * TimeUtil.MILLISECONDS_IN_MINUTE))); } + + // 1.48: support completions tracking in Chocolate for Elisabeth & Ice Cream for Annie + for (String slot: Arrays.asList("chocolate_for_elisabeth", "icecream_for_annie")) { + if (!player.hasQuest(slot)) { + continue; + } + if ("eating".equals(player.getQuest(slot, 0)) && "".equals(player.getQuest(slot, 2))) { + player.setQuest(slot, 2, "1"); + } + } + + // update Find Rat Children + FindRatChildren.checkPlayerUpdate(player); } diff --git a/src/games/stendhal/server/maps/quests/AbstractQuest.java b/src/games/stendhal/server/maps/quests/AbstractQuest.java index e60dd0f2b73..fc57d498e58 100644 --- a/src/games/stendhal/server/maps/quests/AbstractQuest.java +++ b/src/games/stendhal/server/maps/quests/AbstractQuest.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.List; -import games.stendhal.common.MathHelper; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.entity.npc.NPCList; import games.stendhal.server.entity.player.Player; @@ -146,25 +145,37 @@ public boolean isCompleted(final Player player) { && player.isQuestCompleted(getSlotName()); } + @Override + public boolean hasCompleted(final Player player) { + return getCompletions(player) > 0; + } + + /** + * Retrieves number of times player has completed quest. + * + * @param player + * Player for whom quest is being checked. + * @return + * Number of completions. + */ + @Override + public int getCompletions(final Player player) { + return isCompleted(player) ? 1 : 0; + } + + /** + * Retrieves number of times player has completed quest. + * + * @param player + * Player for whom quest is being checked. + * @return + * Number of completions. + * @deprecated + */ + @Deprecated @Override public int getCompletedCount(final Player player) { - final String questSlot = getSlotName(); - final boolean completed = isCompleted(player); - if (player.hasQuest(questSlot)) { - final String[] state = player.getQuest(questSlot).split(";"); - final Pair completionsIndexes = questInfo.getCompletionsIndexes(); - Integer stateIndex = null; - if (completed) { - stateIndex = completionsIndexes.second(); - } else { - stateIndex = completionsIndexes.first(); - } - if (stateIndex != null && state.length > stateIndex && !"".equals(state[stateIndex])) { - return MathHelper.parseIntDefault(state[stateIndex], completed ? 1 : 0); - } - } - // default is to return 1 if quest is in complete state and 0 otherwise - return completed ? 1 : 0; + return getCompletions(player); } @Override diff --git a/src/games/stendhal/server/maps/quests/ChocolateForElisabeth.java b/src/games/stendhal/server/maps/quests/ChocolateForElisabeth.java index 6a6fe66aac9..f461df00e30 100644 --- a/src/games/stendhal/server/maps/quests/ChocolateForElisabeth.java +++ b/src/games/stendhal/server/maps/quests/ChocolateForElisabeth.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -86,7 +86,7 @@ *
  • Every 60 minutes
  • * */ -public class ChocolateForElisabeth extends AbstractQuest { +public class ChocolateForElisabeth extends CompletionsTrackingQuest { // constants private static final String QUEST_SLOT = "chocolate_for_elisabeth"; @@ -104,7 +104,7 @@ private void chocolateStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestNotStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, "rejected")), + new QuestNotStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, 0, "rejected")), ConversationStates.ATTENDING, "I can't remember when I smelt the good taste of #chocolate the last time...", null); @@ -117,7 +117,7 @@ private void chocolateStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), new PlayerHasItemWithHimCondition("chocolate bar")), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new PlayerHasItemWithHimCondition("chocolate bar")), ConversationStates.IDLE, "My mum wants to know who I was asking for chocolate from now :(", null); @@ -126,7 +126,7 @@ private void chocolateStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), new NotCondition(new PlayerHasItemWithHimCondition("chocolate bar"))), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new NotCondition(new PlayerHasItemWithHimCondition("chocolate bar"))), ConversationStates.ATTENDING, "I hope that someone will bring me some chocolate soon...:(", null); @@ -135,7 +135,7 @@ private void chocolateStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "mummy"), new PlayerHasItemWithHimCondition("chocolate bar")), + new QuestInStateCondition(QUEST_SLOT, 0, "mummy"), new PlayerHasItemWithHimCondition("chocolate bar")), ConversationStates.QUESTION_1, "Awesome! Is that chocolate for me?", null); @@ -144,7 +144,7 @@ private void chocolateStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "mummy"), new NotCondition(new PlayerHasItemWithHimCondition("chocolate bar"))), + new QuestInStateCondition(QUEST_SLOT, 0, "mummy"), new NotCondition(new PlayerHasItemWithHimCondition("chocolate bar"))), ConversationStates.ATTENDING, "I hope that someone will bring me some chocolate soon...:(", null); @@ -153,7 +153,7 @@ private void chocolateStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, "start"), new QuestNotInStateCondition(QUEST_SLOT, "mummy")), + new QuestStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new QuestNotInStateCondition(QUEST_SLOT, 0, "mummy")), ConversationStates.ATTENDING, "Hello.", null); @@ -162,7 +162,7 @@ private void chocolateStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "rejected")), + new QuestInStateCondition(QUEST_SLOT, 0, "rejected")), ConversationStates.ATTENDING, "Hello.", null); @@ -213,7 +213,7 @@ private void chocolateStep() { null, ConversationStates.ATTENDING, "Thank you!", - new SetQuestAction(QUEST_SLOT, "start")); + new SetQuestAction(QUEST_SLOT, 0, "start")); // Player says no, they've lost karma npc.add(ConversationStates.QUEST_OFFERED, @@ -221,7 +221,7 @@ private void chocolateStep() { null, ConversationStates.IDLE, "Ok, I'll wait till mommy finds some helpers...", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -5.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -5.0)); // Player has got chocolate bar and spoken to mummy final List reward = new LinkedList(); @@ -239,8 +239,9 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser } }); reward.add(new IncreaseXPAction(500)); - reward.add(new SetQuestAction(QUEST_SLOT, "eating;")); - reward.add(new SetQuestToTimeStampAction(QUEST_SLOT,1)); + reward.add(new SetQuestAction(QUEST_SLOT, 0, "eating")); + reward.add(new SetQuestToTimeStampAction(QUEST_SLOT, 1)); + reward.add(incrementCompletionsAction()); reward.add(new IncreaseKarmaAction(10.0)); reward.add(new InflictStatusOnNPCAction("chocolate bar")); @@ -284,10 +285,10 @@ private void meetMummyStep() { mummyNPC.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(mummyNPC.getName()), - new QuestInStateCondition(QUEST_SLOT, "start")), + new QuestInStateCondition(QUEST_SLOT, 0, "start")), ConversationStates.ATTENDING, "Oh you met my daughter Elisabeth already. You seem like a nice person so it would be really kind, if you can bring her a chocolate bar because I'm not #strong enough for that.", - new SetQuestAction(QUEST_SLOT, "mummy")); + new SetQuestAction(QUEST_SLOT, 0, "mummy")); mummyNPC.addReply("strong", "I tried to get some chocolate for Elisabeth a few times, but I couldn't make my way through the assassins and bandits running around #there."); @@ -305,7 +306,7 @@ public void addToWorld() { fillQuestInfo( "Chocolate for Elisabeth", "Sweet, sweet chocolate! No one can live without it! And Elisabeth loooves to have some...", - true); + true, 2); chocolateStep(); meetMummyStep(); } @@ -318,14 +319,14 @@ public List getHistory(final Player player) { return res; } res.add("Elisabeth is a sweet little girl who lives in Kirdneh together with her family."); - final String questState = player.getQuest(QUEST_SLOT); + final String questState = player.getQuest(QUEST_SLOT, 0); if ("rejected".equals(questState)) { res.add("I don't like sweet little girls."); } - if (player.isQuestInState(QUEST_SLOT, "start","mummy") || isCompleted(player)) { + if (player.isQuestInState(QUEST_SLOT, 0, "start","mummy") || isCompleted(player)) { res.add("Little Elisabeth wants a chocolate bar."); } - if (player.isQuestInState(QUEST_SLOT, "start","mummy") && player.isEquipped("chocolate bar") || isCompleted(player)) { + if (player.isQuestInState(QUEST_SLOT, 0, "start","mummy") && player.isEquipped("chocolate bar") || isCompleted(player)) { res.add("I found a tasty chocolate bar for Elisabeth."); } if ("mummy".equals(questState) || isCompleted(player)) { diff --git a/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java b/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java new file mode 100644 index 00000000000..e4190093e5e --- /dev/null +++ b/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java @@ -0,0 +1,167 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +package games.stendhal.server.maps.quests; + +import games.stendhal.common.MathHelper; +import games.stendhal.common.parser.Sentence; +import games.stendhal.server.entity.npc.ChatAction; +import games.stendhal.server.entity.npc.EventRaiser; +import games.stendhal.server.entity.npc.action.IncrementQuestAction; +import games.stendhal.server.entity.player.Player; +import marauroa.common.Pair; + + +/** + * A quest designed to keep track of number of completions. + */ +public abstract class CompletionsTrackingQuest extends AbstractQuest { + + /** + * Retrieves number of times player has completed quest. + * + * FIXME: perhaps definition should be moved to super class (AbstractQuest) + * + * @param player + * Player for whom quest is being checked. + * @return + * Number of completions. + */ + @Override + public int getCompletions(final Player player) { + final String questSlot = getSlotName(); + final boolean completed = isCompleted(player); + if (player.hasQuest(questSlot)) { + final String[] state = player.getQuest(questSlot).split(";"); + final Pair completionsIndexes = questInfo.getCompletionsIndexes(); + Integer stateIndex = null; + if (completed) { + stateIndex = completionsIndexes.second(); + } else { + stateIndex = completionsIndexes.first(); + } + if (stateIndex != null && state.length > stateIndex && !"".equals(state[stateIndex])) { + return MathHelper.parseIntDefault(state[stateIndex], completed ? 1 : 0); + } + } + // default is to return 1 if quest is in complete state and 0 otherwise + return completed ? 1 : 0; + } + + /** + * Increments number of completions. + * + * @param player + * Player being updated. + * @param complete + * {@code true} to use "done" state index, {@code false} to use "active" state index. + */ + protected void incrementCompletions(final Player player, final boolean complete) { + incrementCompletionsAction(complete).fire(player, null, null); + } + + /** + * Increments number of completions. + * + * @param player + * Player being updated. + */ + protected void incrementCompletions(final Player player) { + incrementCompletions(player, true); + } + + /** + * Retrieves action to execute when completions count should be incremented. + * + * NOTE: quest completions indexes should be set to use this method + * + * @param complete + * {@code true} to use "done" state index, {@code false} to use "active" state index. + * @return + * {@code IncrementQuestAction} to increment slot where completions indexes are stored. + */ + protected IncrementQuestAction incrementCompletionsAction(final boolean complete) { + final Pair indexes = questInfo.getCompletionsIndexes(); + return new IncrementQuestAction(getSlotName(), complete ? indexes.second() : indexes.first(), 1); + } + + /** + * Retrieves action to execute when completions count should be incremented. + * + * NOTE: quest completions indexes should be set to use this method + * + * @return + * {@code IncrementQuestAction} to increment slot where completions indexes are stored. + */ + protected IncrementQuestAction incrementCompletionsAction() { + return incrementCompletionsAction(true); + } + + /** + * Swaps completions value from completed index to started index. + * + * @param player + * Player being updated. + */ + protected void swapCompletionsBeforeStart(final Player player) { + final Pair indexes = questInfo.getCompletionsIndexes(); + final String questSlot = getSlotName(); + final int completions = MathHelper.parseIntDefault(player.getQuest(questSlot, indexes.second()), 0); + player.setQuest(questSlot, indexes.first(), String.valueOf(completions)); + player.setQuest(questSlot, indexes.second(), null); + } + + /** + * Creates a chat action to swap completions value from completed index to started index. + * + * @return + * New chat action instance. + */ + protected ChatAction swapCompletionsBeforeStartAction() { + return new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + swapCompletionsBeforeStart(player); + } + }; + } + + /** + * Swaps completions value from started index to completed index. + * + * TODO: maybe increment simultaneously so `incrementCompletions` does not need to be called + * + * @param player + * Player being updated. + */ + protected void swapCompletionsBeforeComplete(final Player player) { + final Pair indexes = questInfo.getCompletionsIndexes(); + final String questSlot = getSlotName(); + final int completions = MathHelper.parseIntDefault(player.getQuest(questSlot, indexes.first()), 0); + player.setQuest(questSlot, indexes.second(), String.valueOf(completions)); + player.setQuest(questSlot, indexes.first(), null); + } + + /** + * Creates a chat action to swap completions value from started index to completed index. + * + * @return + * New chat action instance. + */ + protected ChatAction swapCompletionsBeforeCompleteAction() { + return new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + swapCompletionsBeforeComplete(player); + } + }; + } +} diff --git a/src/games/stendhal/server/maps/quests/DailyItemQuest.java b/src/games/stendhal/server/maps/quests/DailyItemQuest.java index 52cfa843ed3..0f76ff2cf02 100644 --- a/src/games/stendhal/server/maps/quests/DailyItemQuest.java +++ b/src/games/stendhal/server/maps/quests/DailyItemQuest.java @@ -68,7 +68,7 @@ * REPETITIONS: *
  • once a day */ -public class DailyItemQuest extends AbstractQuest { +public class DailyItemQuest extends CompletionsTrackingQuest { private static DailyItemQuest instance; @@ -484,7 +484,7 @@ public List getHistory(final Player player) { + " to help Ados and should deliver " + Grammar.itthem(amount) + " to Mayor Chalmers."); } } - int repetitions = player.getNumberOfRepetitions(getSlotName(), 2); + int repetitions = getCompletions(player); if (repetitions > 0) { res.add("I helped Ados with supplies " + Grammar.quantityplnoun(repetitions, "time") + " so far."); diff --git a/src/games/stendhal/server/maps/quests/DailyMonsterQuest.java b/src/games/stendhal/server/maps/quests/DailyMonsterQuest.java index 5733ff0c165..8b2aae853b2 100644 --- a/src/games/stendhal/server/maps/quests/DailyMonsterQuest.java +++ b/src/games/stendhal/server/maps/quests/DailyMonsterQuest.java @@ -73,7 +73,7 @@ * REPETITIONS: - once a day */ -public class DailyMonsterQuest extends AbstractQuest { +public class DailyMonsterQuest extends CompletionsTrackingQuest { private static DailyMonsterQuest instance; @@ -313,7 +313,7 @@ public List getHistory(final Player player) { } } // add to history how often player helped Semos so far - final int repetitions = player.getNumberOfRepetitions(getSlotName(), 2); + final int repetitions = getCompletions(player); if (repetitions > 0) { res.add("I helped and saved Semos " + Grammar.quantityplnounCreature(repetitions, "time") + " so far."); @@ -506,7 +506,7 @@ public void addToWorld() { fillQuestInfo( "Daily Monster Quest", "Mayor Sakhs needs warriors to keep Semos City safe.", - true); + true, 2); step_1(); step_2(); step_3(); diff --git a/src/games/stendhal/server/maps/quests/ElfPrincess.java b/src/games/stendhal/server/maps/quests/ElfPrincess.java index 2b8d8c7be57..14df012cc0b 100644 --- a/src/games/stendhal/server/maps/quests/ElfPrincess.java +++ b/src/games/stendhal/server/maps/quests/ElfPrincess.java @@ -87,7 +87,7 @@ * for a task again
  • * */ -public class ElfPrincess extends AbstractQuest { +public class ElfPrincess extends CompletionsTrackingQuest { /* delay in minutes */ private static final int DELAY = 5; @@ -314,7 +314,7 @@ public List getHistory(final Player player) { if (isRepeatable(player)) { res.add("I took the flower to the Princess and she gave me gold bars. If I want to make her happy again, I can ask her for another task."); } - final int repetitions = player.getNumberOfRepetitions(getSlotName(), 2); + final int repetitions = getCompletions(player); if (repetitions > 0) { res.add("I've already taken Princess Tywysoga " + Grammar.quantityplnoun(repetitions, "precious flower", "one") + "."); } diff --git a/src/games/stendhal/server/maps/quests/FindRatChildren.java b/src/games/stendhal/server/maps/quests/FindRatChildren.java index eafddd8adda..c17f81defd9 100644 --- a/src/games/stendhal/server/maps/quests/FindRatChildren.java +++ b/src/games/stendhal/server/maps/quests/FindRatChildren.java @@ -16,8 +16,7 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; - -import org.apache.log4j.Logger; +import java.util.Locale; import games.stendhal.common.grammar.Grammar; import games.stendhal.common.parser.Sentence; @@ -26,6 +25,7 @@ import games.stendhal.server.entity.npc.ConversationStates; import games.stendhal.server.entity.npc.EventRaiser; import games.stendhal.server.entity.npc.SpeakerNPC; +import games.stendhal.server.entity.npc.action.MultipleActions; import games.stendhal.server.entity.npc.action.SetQuestAction; import games.stendhal.server.entity.npc.action.SetQuestAndModifyKarmaAction; import games.stendhal.server.entity.npc.condition.AndCondition; @@ -69,9 +69,7 @@ *
  • Once every 24 hours.
  • * */ -public class FindRatChildren extends AbstractQuest { - - private static Logger logger = Logger.getLogger(FindRatChildren.class); +public class FindRatChildren extends CompletionsTrackingQuest { private static final String QUEST_SLOT = "find_rat_kids"; @@ -88,36 +86,172 @@ public String getSlotName() { return QUEST_SLOT; } - private List missingNames(final Player player) { + /** + * Retrieves names of children player still needs to find and report. + * + * @param player + * Player doing quest. + * @return + * Missing children's names. + */ + private static List missingNames(final Player player) { if (!player.hasQuest(QUEST_SLOT)) { return NEEDED_KIDS; } /* * the format of the list quest slot is - * "looking;name;name;...:said;name;name;..." + * "found=name,name,...;said=name,name,..." */ // put the children name to lower case so we can match it, however the player wrote the name - final String npcDoneText = player.getQuest(QUEST_SLOT).toLowerCase(); - final String[] doneAndFound = npcDoneText.split(":"); + final String[] tmp = player.getQuest(QUEST_SLOT, 1).toLowerCase(Locale.ENGLISH).split("="); + if (tmp.length < 2) { + return NEEDED_KIDS; + } + final List said = Arrays.asList(tmp[1].split(",")); final List result = new LinkedList(); - if (doneAndFound.length > 1) { - final String[] done = doneAndFound[1].split(";"); - final List doneList = Arrays.asList(done); - for (final String name : NEEDED_KIDS) { - if (!doneList.contains(name)) { - result.add(name); - } + for (final String name: NEEDED_KIDS) { + if (!said.contains(name)) { + result.add(name); } } return result; } + /** + * Checks if quest state is active. + * + * @param player + * Player doing quest. + * @return + * {@code true} if player has quest and is in active state. + */ + private static boolean lookingForKids(final Player player) { + return new QuestActiveCondition(QUEST_SLOT).fire(player, null, null); + } + + /** + * Adds a name to player's quest state when NPC is found. + * + * @param player + * Player doing quest. + * @param name + * Name of found child. + */ + public static void addFoundName(final Player player, final String name) { + if (!lookingForKids(player)) { + return; + } + String found = player.getQuest(QUEST_SLOT, 0); + if (found.length() > "found=".length()) { + found += ","; + } + found += name; + player.setQuest(QUEST_SLOT, 0, found); + } + + /** + * Adds a name to player's quest state when reported to Agnus. + * + * @param player + * Player doing quest. + * @param name + * Name of found child. + */ + private static void addSaidName(final Player player, final String name) { + if (!lookingForKids(player)) { + return; + } + String said = player.getQuest(QUEST_SLOT, 1); + if (said.length() > "said=".length()) { + said += ","; + } + said += name; + player.setQuest(QUEST_SLOT, 1, said); + } + + /** + * Retrieves names of rat children player has talked to. + * + * @param player + * Player doing quest. + * @return + * List of names of children player has talked to. + */ + public static List getFoundNames(final Player player) { + final List names = new ArrayList<>(); + if (!lookingForKids(player)) { + return names; + } + final String[] tmp = player.getQuest(QUEST_SLOT, 0).toLowerCase(Locale.ENGLISH).split("="); + if (tmp.length < 2) { + return names; + } + names.addAll(Arrays.asList(tmp[1].split(","))); + return names; + } + + /** + * Retrieves names of rat children player has reported to Agnus. + * + * @param player + * Player doing quest. + * @return + * List of names of children player has reported. + */ + public static List getSaidNames(final Player player) { + final List names = new ArrayList<>(); + if (!lookingForKids(player)) { + return names; + } + final String[] tmp = player.getQuest(QUEST_SLOT, 1).toLowerCase(Locale.ENGLISH).split("="); + if (tmp.length < 2) { + return names; + } + names.addAll(Arrays.asList(tmp[1].split(","))); + return names; + } + + /** + * Updates formatting of player's quest state string. + * + * @param player + * Player being updated. + */ + public static void checkPlayerUpdate(final Player player) { + final String state = player.getQuest(QUEST_SLOT); + // 1.48: support completions tracking in Find Rat Children + if (!player.hasQuest(QUEST_SLOT) || !state.startsWith("looking")) { + return; + } + final List found = new ArrayList<>(); + final List said = new ArrayList<>(); + final String[] tmp = state.split(":"); + if (tmp.length > 0) { + for (final String name: tmp[0].split(";")) { + if ("looking".equals(name)) { + continue; + } + found.add(name); + } + } + if (tmp.length > 1) { + for (final String name: tmp[1].split(";")) { + if ("said".equals(name)) { + continue; + } + said.add(name); + } + } + player.setQuest(QUEST_SLOT, "found=" + String.join(",", found) + ";said=" + + String.join(",", said)); + } + private void askingStep() { final SpeakerNPC npc = npcs.get("Agnus"); npc.add(ConversationStates.ATTENDING, ConversationPhrases.QUEST_MESSAGES, - new OrCondition(new QuestNotStartedCondition(QUEST_SLOT), new QuestInStateCondition(QUEST_SLOT, "rejected")), + new OrCondition(new QuestNotStartedCondition(QUEST_SLOT), new QuestInStateCondition(QUEST_SLOT, 0, "rejected")), ConversationStates.QUEST_OFFERED, "I feel so worried. If I only knew my #children were safe I would feel better.", null); @@ -151,7 +285,9 @@ private void askingStep() { null, ConversationStates.ATTENDING, "That's so nice of you. Good luck searching for them.", - new SetQuestAction(QUEST_SLOT, "looking:said")); + new MultipleActions( + new SetQuestAction(QUEST_SLOT, 0, "found="), + new SetQuestAction(QUEST_SLOT, 1, "said="))); npc.add( ConversationStates.QUEST_OFFERED, @@ -159,7 +295,7 @@ private void askingStep() { null, ConversationStates.ATTENDING, "Oh. Never mind. I'm sure someone else would be glad to help me.", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -15.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -15.0)); npc.add( ConversationStates.QUEST_OFFERED, @@ -197,35 +333,17 @@ private void retrievingStep() { new ChatAction() { @Override public void fire(final Player player, final Sentence sentence, final EventRaiser npc) { - final String npcQuestText = player.getQuest(QUEST_SLOT).toLowerCase(); - final String[] npcDoneText = npcQuestText.split(":"); - final String lookingStr; - final String saidStr; - if (npcDoneText.length > 1) { - lookingStr = npcDoneText[0]; - saidStr = npcDoneText[1]; - } else { - // compatibility with broken quests - should never happen - logger.warn("Player " + player.getTitle() + " found with find_rat_kids quest slot in state " + player.getQuest(QUEST_SLOT) + " - now setting this to done."); - player.setQuest(QUEST_SLOT, "done"); - npc.say("Sorry, it looks like you have already found them after all. I got confused."); - player.notifyWorldAboutChanges(); - npc.setCurrentState(ConversationStates.ATTENDING); - return; - } - - final List looking = Arrays.asList(lookingStr.split(";")); - final List said = Arrays.asList(saidStr.split(";")); + final List found = getFoundNames(player); + final List said = getSaidNames(player); String reply = ""; List missing = missingNames(player); final boolean isMissing = missing.contains(name); - if (isMissing && looking.contains(name) && !said.contains(name)) { + if (isMissing && found.contains(name) && !said.contains(name)) { // we haven't said the name yet so we add it to the list - player.setQuest(QUEST_SLOT, lookingStr - + ":" + saidStr + ";" + name); + addSaidName(player, name); reply = "Thank you."; - } else if (!looking.contains(name)) { + } else if (!found.contains(name)) { // we have said it was a valid name but haven't seen them reply = "I don't think you actually checked if they were ok."; } else if (!isMissing && said.contains(name)) { @@ -246,7 +364,9 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser player.addKarma(15); reply += " Now that I know my kids are safe, I can set my mind at rest."; npc.say(reply); - player.setQuest(QUEST_SLOT, "done;" + System.currentTimeMillis()); + player.setQuest(QUEST_SLOT, 0, "done"); + player.setQuest(QUEST_SLOT, 1, String.valueOf(System.currentTimeMillis())); + incrementCompletions(player); player.notifyWorldAboutChanges(); npc.setCurrentState(ConversationStates.ATTENDING); } @@ -287,7 +407,7 @@ public void addToWorld() { fillQuestInfo( "Find Rat Children", "Agnus, who lives in Rat City, asks young heroes to find her children and look after them. They went down into the dark tunnels and haven't returned ...", - true); + true, 2); askingStep(); findingStep(); retrievingStep(); diff --git a/src/games/stendhal/server/maps/quests/FishSoupForHughie.java b/src/games/stendhal/server/maps/quests/FishSoupForHughie.java index d017d49daf0..6bf311fa862 100644 --- a/src/games/stendhal/server/maps/quests/FishSoupForHughie.java +++ b/src/games/stendhal/server/maps/quests/FishSoupForHughie.java @@ -75,7 +75,7 @@ *
  • Unlimited, but 7 days of waiting are required between repetitions
  • * */ -public class FishSoupForHughie extends AbstractQuest { +public class FishSoupForHughie extends CompletionsTrackingQuest { private static final int REQUIRED_MINUTES = 7 * TimeUtil.MINUTES_IN_DAY; @@ -88,12 +88,13 @@ public String getSlotName() { @Override public boolean isCompleted(final Player player) { - return player.hasQuest(QUEST_SLOT) && !"start".equals(player.getQuest(QUEST_SLOT)) && !"rejected".equals(player.getQuest(QUEST_SLOT)); + return player.hasQuest(QUEST_SLOT) && !"start".equals(player.getQuest(QUEST_SLOT, 0)) + && !"rejected".equals(player.getQuest(QUEST_SLOT, 0)); } @Override public boolean isRepeatable(final Player player) { - return new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, "start"), new QuestStartedCondition(QUEST_SLOT), new TimePassedCondition(QUEST_SLOT,REQUIRED_MINUTES)).fire(player, null, null); + return new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new QuestStartedCondition(QUEST_SLOT), new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES)).fire(player, null, null); } @Override @@ -103,7 +104,7 @@ public List getHistory(final Player player) { return res; } res.add("Anastasia asked me to bring fish soup for her boy Hughie."); - final String questState = player.getQuest(QUEST_SLOT); + final String questState = player.getQuest(QUEST_SLOT, 0); if ("rejected".equals(questState)) { res.add("I do not want to help Hughie."); return res; @@ -130,7 +131,7 @@ private void prepareRequestingStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), new PlayerHasItemWithHimCondition("fish soup")), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new PlayerHasItemWithHimCondition("fish soup")), ConversationStates.QUEST_ITEM_BROUGHT, "Hi, you've got fish soup, I see, is that for Hughie?", null); @@ -139,7 +140,7 @@ private void prepareRequestingStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), new NotCondition(new PlayerHasItemWithHimCondition("fish soup"))), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new NotCondition(new PlayerHasItemWithHimCondition("fish soup"))), ConversationStates.ATTENDING, "You're back already? Hughie is getting sicker! Don't forget the fish soup for him, please. I promise to reward you.", null); @@ -156,7 +157,7 @@ private void prepareRequestingStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestNotInStateCondition(QUEST_SLOT, "start"), + new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new QuestStartedCondition(QUEST_SLOT)), ConversationStates.ATTENDING, "Hello again.", @@ -165,7 +166,7 @@ private void prepareRequestingStep() { // if they ask for quest while on it, remind them npc.add(ConversationStates.ATTENDING, ConversationPhrases.QUEST_MESSAGES, - new QuestInStateCondition(QUEST_SLOT, "start"), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), ConversationStates.ATTENDING, "You already promised me to bring me some fish soup for Hughie! Please hurry!", null); @@ -181,7 +182,7 @@ private void prepareRequestingStep() { // player returns - enough time has passed npc.add(ConversationStates.ATTENDING, ConversationPhrases.QUEST_MESSAGES, - new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, "start"), new QuestStartedCondition(QUEST_SLOT), new TimePassedCondition(QUEST_SLOT,REQUIRED_MINUTES)), + new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new QuestStartedCondition(QUEST_SLOT), new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES)), ConversationStates.QUEST_OFFERED, "My Hughie is getting sick again! Please could you bring another bowl of fish soup? It helped last time.", null); @@ -189,7 +190,7 @@ private void prepareRequestingStep() { // player returns - not enough time has passed npc.add(ConversationStates.ATTENDING, ConversationPhrases.QUEST_MESSAGES, - new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, "start"), new QuestStartedCondition(QUEST_SLOT), new NotCondition(new TimePassedCondition(QUEST_SLOT,REQUIRED_MINUTES))), + new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new QuestStartedCondition(QUEST_SLOT), new NotCondition(new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES))), ConversationStates.ATTENDING, "Hughie is sleeping off his fever now and I'm hopeful he recovers. Thank you so much.", null); @@ -200,7 +201,7 @@ private void prepareRequestingStep() { null, ConversationStates.ATTENDING, "Thank you! You can ask Florence Bouillabaisse to make you fish soup. I think she's in Ados market somewhere.", - new SetQuestAction(QUEST_SLOT, "start")); + new SetQuestAction(QUEST_SLOT, 0, "start")); // player is not willing to help npc.add(ConversationStates.QUEST_OFFERED, @@ -208,7 +209,7 @@ private void prepareRequestingStep() { null, ConversationStates.ATTENDING, "Oh no, please, he's so sick.", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -5.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -5.0)); } private void prepareBringingStep() { @@ -218,7 +219,8 @@ private void prepareBringingStep() { final List reward = new LinkedList(); reward.add(new DropItemAction("fish soup")); reward.add(new IncreaseXPAction(200)); - reward.add(new SetQuestToTimeStampAction(QUEST_SLOT)); + reward.add(new SetQuestToTimeStampAction(QUEST_SLOT, 0)); + reward.add(incrementCompletionsAction()); reward.add(new IncreaseKarmaAction(5)); reward.add(new EquipItemAction("potion",10)); reward.add(new ChatAction() { @@ -264,7 +266,7 @@ public void addToWorld() { fillQuestInfo( "Fish Soup for Hughie", "Anastasia's son Hughie is sick and needs something to heal him.", - true); + true, 1); prepareRequestingStep(); prepareBringingStep(); } diff --git a/src/games/stendhal/server/maps/quests/FruitsForCoralia.java b/src/games/stendhal/server/maps/quests/FruitsForCoralia.java index aa3797e68e6..7511ee98cc6 100644 --- a/src/games/stendhal/server/maps/quests/FruitsForCoralia.java +++ b/src/games/stendhal/server/maps/quests/FruitsForCoralia.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -74,7 +74,7 @@ * * @author pinchanzee */ -public class FruitsForCoralia extends AbstractQuest { +public class FruitsForCoralia extends CompletionsTrackingQuest { @@ -101,7 +101,7 @@ public class FruitsForCoralia extends AbstractQuest { public void addToWorld() { fillQuestInfo("Fruits for Coralia", "The Ados Tavern barmaid, Coralia, searches for fresh fruits for her exotic hat.", - true); + true, 0, 2); prepareQuestStep(); prepareBringingStep(); } @@ -140,7 +140,7 @@ public List getHistory(final Player player) { return res; } res.add("Coralia asked me for some fresh fruits for her hat."); - final String questState = player.getQuest(QUEST_SLOT); + final String questState = player.getQuest(QUEST_SLOT, 0); if ("rejected".equals(questState)) { // quest rejected @@ -148,7 +148,7 @@ public List getHistory(final Player player) { } else if (!player.isQuestCompleted(QUEST_SLOT)) { // not yet finished final ItemCollection missingItems = new ItemCollection(); - missingItems.addFromQuestStateString(questState); + missingItems.addFromQuestStateString(questState, 1); res.add("I still need to bring Coralia " + Grammar.enumerateCollection(missingItems.toStringList()) + "."); } else if (isRepeatable(player)) { // may be repeated now @@ -170,7 +170,7 @@ public void prepareQuestStep() { ConversationPhrases.combine(ConversationPhrases.QUEST_MESSAGES, "fruit"), new AndCondition( new QuestNotStartedCondition(QUEST_SLOT), - new QuestNotInStateCondition(QUEST_SLOT, "rejected")), + new QuestNotInStateCondition(QUEST_SLOT, 0, "rejected")), ConversationStates.QUEST_OFFERED, "Would you be kind enough to find me some fresh fruits for my hat? I'd be ever so grateful!", null); @@ -178,7 +178,7 @@ public void prepareQuestStep() { // ask for quest again after rejected npc.add(ConversationStates.ATTENDING, ConversationPhrases.combine(ConversationPhrases.QUEST_MESSAGES, "hat"), - new QuestInStateCondition(QUEST_SLOT, "rejected"), + new QuestInStateCondition(QUEST_SLOT, 0, "rejected"), ConversationStates.QUEST_OFFERED, "Are you willing to find me some fresh fruits for my hat yet?", null); @@ -212,7 +212,7 @@ public void prepareQuestStep() { "hat", new AndCondition( new QuestNotStartedCondition(QUEST_SLOT), - new QuestNotInStateCondition(QUEST_SLOT, "rejected")), + new QuestNotInStateCondition(QUEST_SLOT, 0, "rejected")), ConversationStates.ATTENDING, "It's a shame for you to see it all withered like this, it really needs some fresh #fruits...", null); @@ -224,7 +224,8 @@ public void prepareQuestStep() { ConversationStates.QUESTION_1, null, new MultipleActions( - new SetQuestAction(QUEST_SLOT, NEEDED_ITEMS), + swapCompletionsBeforeStartAction(), + new SetQuestAction(QUEST_SLOT, 1, NEEDED_ITEMS), new SayRequiredItemsFromCollectionAction(QUEST_SLOT, "That's wonderful! I'd like these fresh fruits: [items]."))); // reject quest response @@ -233,7 +234,7 @@ public void prepareQuestStep() { null, ConversationStates.ATTENDING, "These exotic hats don't keep themselves you know...", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -5.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -5.0)); // meet again during quest npc.add(ConversationStates.IDLE, @@ -328,7 +329,9 @@ private void prepareBringingStep() { // set up next step ChatAction completeAction = new MultipleActions( - new SetQuestAction(QUEST_SLOT, "done"), + swapCompletionsBeforeCompleteAction(), + incrementCompletionsAction(), + new SetQuestAction(QUEST_SLOT, 0, "done"), new SayTextAction("My hat has never looked so delightful! Thank you ever so much! Here, take this as a reward."), new IncreaseXPAction(300), new IncreaseKarmaAction(5), @@ -348,6 +351,7 @@ private void prepareBringingStep() { null, new CollectRequestedItemsAction(item.getKey(), QUEST_SLOT, + 1, "Wonderful! Did you bring anything else with you?", "I already have enough of those.", completeAction, ConversationStates.ATTENDING)); diff --git a/src/games/stendhal/server/maps/quests/HelpTomi.java b/src/games/stendhal/server/maps/quests/HelpTomi.java index fbbe108de34..a960f623fe6 100644 --- a/src/games/stendhal/server/maps/quests/HelpTomi.java +++ b/src/games/stendhal/server/maps/quests/HelpTomi.java @@ -62,7 +62,7 @@ *
  • bigger reward each time - with a square law on the XP
  • * */ -public class HelpTomi extends AbstractQuest { +public class HelpTomi extends CompletionsTrackingQuest { /** * Number of repetitions after which the XP growth becomes linear, instead * of quadratic. @@ -78,6 +78,7 @@ public class HelpTomi extends AbstractQuest { public String getSlotName() { return QUEST_SLOT; } + @Override public List getHistory(final Player player) { final List res = new ArrayList(); @@ -88,13 +89,10 @@ public List getHistory(final Player player) { final String questState = player.getQuest(QUEST_SLOT); if (questState.startsWith("done")) { res.add("Tomi asked for \"ice\" and took the ice sword I was carrying!"); - // provided quest isn't in 'old version' we should be able to check how many times it was done - if (!"done".equals(questState)) { - final int repetitions = player.getNumberOfRepetitions(getSlotName(), 1); - if (repetitions>1) { - res.add("I've given " + repetitions + " ice swords to Tomi so far."); - } - } + } + int completions = getCompletions(player); + if (completions > 0) { + res.add("I've given " + completions + " ice swords to Tomi so far."); } return res; } diff --git a/src/games/stendhal/server/maps/quests/IQuest.java b/src/games/stendhal/server/maps/quests/IQuest.java index 21abe224cf8..dccd7f5447e 100644 --- a/src/games/stendhal/server/maps/quests/IQuest.java +++ b/src/games/stendhal/server/maps/quests/IQuest.java @@ -66,6 +66,26 @@ public interface IQuest { */ boolean isCompleted(Player player); + /** + * Has the quest been completed at least one time regardless of current completed state. + * + * @param player + * Player in question. + * @return + * {@code true} if player has completed quest at least once. + */ + boolean hasCompleted(Player player); + + /** + * Retrieves number of times player has completed quest. + * + * @param player + * Player for whom quest is being checked. + * @return + * Number of completions. + */ + int getCompletions(Player player); + /** * Retrieves number of times player has completed quest. * @@ -74,6 +94,7 @@ public interface IQuest { * @return * Number of completions. */ + @Deprecated int getCompletedCount(Player player); /** diff --git a/src/games/stendhal/server/maps/quests/IcecreamForAnnie.java b/src/games/stendhal/server/maps/quests/IcecreamForAnnie.java index 4d670b366a2..4c7ee81f874 100644 --- a/src/games/stendhal/server/maps/quests/IcecreamForAnnie.java +++ b/src/games/stendhal/server/maps/quests/IcecreamForAnnie.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -80,7 +80,7 @@ *
  • Every 60 minutes
  • * */ -public class IcecreamForAnnie extends AbstractQuest { +public class IcecreamForAnnie extends CompletionsTrackingQuest { // constants private static final String QUEST_SLOT = "icecream_for_annie"; @@ -100,7 +100,7 @@ private void icecreamStep() { ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), new QuestNotStartedCondition(QUEST_SLOT), - new QuestNotInStateCondition(QUEST_SLOT, "rejected")), + new QuestNotInStateCondition(QUEST_SLOT, 0, "rejected")), ConversationStates.ATTENDING, "Hello, my name is Annie. I am five years old.", null); @@ -109,7 +109,7 @@ private void icecreamStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new PlayerHasItemWithHimCondition("icecream")), ConversationStates.IDLE, "Mummy says I mustn't talk to you any more. You're a stranger.", @@ -119,7 +119,7 @@ private void icecreamStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new NotCondition(new PlayerHasItemWithHimCondition("icecream"))), ConversationStates.ATTENDING, "Hello. I'm hungry.", @@ -129,7 +129,7 @@ private void icecreamStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "mummy"), + new QuestInStateCondition(QUEST_SLOT, 0, "mummy"), new PlayerHasItemWithHimCondition("icecream")), ConversationStates.QUESTION_1, "Yummy! Is that ice cream for me?", @@ -139,7 +139,7 @@ private void icecreamStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "mummy"), + new QuestInStateCondition(QUEST_SLOT, 0, "mummy"), new NotCondition(new PlayerHasItemWithHimCondition("icecream"))), ConversationStates.ATTENDING, "Hello. I'm hungry.", @@ -150,8 +150,8 @@ private void icecreamStep() { ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), new QuestStartedCondition(QUEST_SLOT), - new QuestNotInStateCondition(QUEST_SLOT, "start"), - new QuestNotInStateCondition(QUEST_SLOT, "mummy")), + new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), + new QuestNotInStateCondition(QUEST_SLOT, 0, "mummy")), ConversationStates.ATTENDING, "Hello.", null); @@ -160,7 +160,7 @@ private void icecreamStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "rejected")), + new QuestInStateCondition(QUEST_SLOT, 0, "rejected")), ConversationStates.ATTENDING, "Hello.", null); @@ -211,7 +211,7 @@ private void icecreamStep() { null, ConversationStates.ATTENDING, "Thank you!", - new SetQuestAction(QUEST_SLOT, "start")); + new SetQuestAction(QUEST_SLOT, 0, "start")); // Player says no, they've lost karma npc.add(ConversationStates.QUEST_OFFERED, @@ -219,15 +219,16 @@ private void icecreamStep() { null, ConversationStates.IDLE, "Ok, I'll ask my mummy instead.", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -5.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -5.0)); // Player has got ice cream and spoken to mummy final List reward = new LinkedList(); reward.add(new DropItemAction("icecream")); reward.add(new EquipItemAction("present")); reward.add(new IncreaseXPAction(500)); - reward.add(new SetQuestAction(QUEST_SLOT, "eating;")); - reward.add(new SetQuestToTimeStampAction(QUEST_SLOT,1)); + reward.add(new SetQuestAction(QUEST_SLOT, 0, "eating")); + reward.add(new SetQuestToTimeStampAction(QUEST_SLOT, 1)); + reward.add(incrementCompletionsAction()); reward.add(new IncreaseKarmaAction(10.0)); reward.add(new InflictStatusOnNPCAction("icecream")); @@ -270,10 +271,10 @@ private void meetMummyStep() { mummyNPC.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(mummyNPC.getName()), - new QuestInStateCondition(QUEST_SLOT, "start")), + new QuestInStateCondition(QUEST_SLOT, 0, "start")), ConversationStates.ATTENDING, "Hello, I see you've met my daughter Annie. I hope she wasn't too demanding. You seem like a nice person.", - new SetQuestAction(QUEST_SLOT, "mummy")); + new SetQuestAction(QUEST_SLOT, 0, "mummy")); // any other state mummyNPC.add(ConversationStates.IDLE, @@ -287,7 +288,7 @@ public void addToWorld() { fillQuestInfo( "Ice Cream for Annie", "The best surprise for a litte girl like Annie is a cool ice cream on summer days while playing on the playground.", - true); + true, 2); icecreamStep(); meetMummyStep(); } @@ -300,14 +301,14 @@ public List getHistory(final Player player) { return res; } res.add("Annie Jones is a sweet little girl playing in Kalavan city gardens."); - final String questState = player.getQuest(QUEST_SLOT); + final String questState = player.getQuest(QUEST_SLOT, 0); if ("rejected".equals(questState)) { res.add("I don't like sweet little girls."); } - if (player.isQuestInState(QUEST_SLOT, "start","mummy") || isCompleted(player)) { + if (player.isQuestInState(QUEST_SLOT, 0, "start","mummy") || isCompleted(player)) { res.add("Little Annie wants an ice cream."); } - if (player.isQuestInState(QUEST_SLOT, "start","mummy") && player.isEquipped("icecream") || isCompleted(player)) { + if (player.isQuestInState(QUEST_SLOT, 0, "start","mummy") && player.isEquipped("icecream") || isCompleted(player)) { res.add("I found a tasty ice cream for Annie."); } if ("mummy".equals(questState) || isCompleted(player)) { diff --git a/src/games/stendhal/server/maps/quests/KillDhohrNuggetcutter.java b/src/games/stendhal/server/maps/quests/KillDhohrNuggetcutter.java index 3b395017f7d..7e3a8bb9aa8 100644 --- a/src/games/stendhal/server/maps/quests/KillDhohrNuggetcutter.java +++ b/src/games/stendhal/server/maps/quests/KillDhohrNuggetcutter.java @@ -66,7 +66,7 @@ * */ -public class KillDhohrNuggetcutter extends AbstractQuest { +public class KillDhohrNuggetcutter extends CompletionsTrackingQuest { private static final String QUEST_SLOT = "kill_dhohr_nuggetcutter"; @@ -86,7 +86,7 @@ private void step_1() { new ChatAction() { @Override public void fire(final Player player, final Sentence sentence, final EventRaiser raiser) { - if (!player.hasQuest(QUEST_SLOT) || player.getQuest(QUEST_SLOT).equals("rejected")) { + if (!player.hasQuest(QUEST_SLOT) || player.getQuest(QUEST_SLOT, 0).equals("rejected")) { raiser.say("We are unable to rid our area of dwarves. Especially one mighty one named Dhohr Nuggetcutter. Would you please kill them?"); } else if (player.getQuest(QUEST_SLOT, 0).equals("start")) { raiser.say("I already asked you to kill Dhohr Nuggetcutter!"); @@ -131,7 +131,7 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser null, ConversationStates.ATTENDING, "Ok, I will await someone having the guts to have the job done.", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -5.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -5.0)); } private void step_2() { @@ -169,7 +169,9 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser player.equipOrPutOnGround(mithrilnug); player.addKarma(25.0); player.addXP(4000); - player.setQuest(QUEST_SLOT, "killed;" + System.currentTimeMillis()); + incrementCompletions(player); + player.setQuest(QUEST_SLOT, 0, "killed"); + player.setQuest(QUEST_SLOT, 1, String.valueOf(System.currentTimeMillis())); } }); } @@ -179,7 +181,7 @@ public void addToWorld() { fillQuestInfo( "Kill Dhohr Nuggetcutter", "Zogfang, the orc who guards the entrance of the Abandoned Keep, isn't feeling safe while some dwarves still remain in the Keep.", - false); + false, 2); step_1(); step_2(); step_3(); diff --git a/src/games/stendhal/server/maps/quests/KillEnemyArmy.java b/src/games/stendhal/server/maps/quests/KillEnemyArmy.java index be6b89400e3..c1a2ff03c8b 100644 --- a/src/games/stendhal/server/maps/quests/KillEnemyArmy.java +++ b/src/games/stendhal/server/maps/quests/KillEnemyArmy.java @@ -76,7 +76,7 @@ * REPETITIONS:
    • once a week.
    */ -public class KillEnemyArmy extends AbstractQuest { +public class KillEnemyArmy extends CompletionsTrackingQuest { private static final String QUEST_NPC = "Despot Halb Errvl"; private static final String QUEST_SLOT = "kill_enemy_army"; @@ -579,7 +579,7 @@ public List getHistory(final Player player) { if (isRepeatable(player)) { history.add("Despot Halb Errvl is getting paranoid again about his safety, I can offer my services now."); } - int repetitions = player.getNumberOfRepetitions(getSlotName(), 3); + int repetitions = getCompletions(player); if (repetitions > 0) { history.add("I've bloodthirstily slain " + Grammar.quantityplnoun(repetitions, "whole army") + " for Despot Halb Errvl."); diff --git a/src/games/stendhal/server/maps/quests/KillMonks.java b/src/games/stendhal/server/maps/quests/KillMonks.java index 27b3b3ea035..e0fb044191e 100644 --- a/src/games/stendhal/server/maps/quests/KillMonks.java +++ b/src/games/stendhal/server/maps/quests/KillMonks.java @@ -74,7 +74,7 @@ * * @author Vanessa Julius, idea by anoyyou */ -public class KillMonks extends AbstractQuest { +public class KillMonks extends CompletionsTrackingQuest { private static final String QUEST_SLOT = "kill_monks"; protected HashMap> creaturestokill = new HashMap>(); @@ -248,7 +248,7 @@ private List getHistory(final Player player, boolean formatted) { res.add("I've killed some monks and Andy finally can sleep a bit better!"); } } - int repetitions = player.getNumberOfRepetitions(getSlotName(), 2); + int repetitions = getCompletions(player); if (repetitions > 0) { res.add("I have taken revenge for Andy " + Grammar.quantityplnoun(repetitions, "time") + " now."); diff --git a/src/games/stendhal/server/maps/quests/KillSpiders.java b/src/games/stendhal/server/maps/quests/KillSpiders.java index 44dc71e82ea..deee5bb40e5 100644 --- a/src/games/stendhal/server/maps/quests/KillSpiders.java +++ b/src/games/stendhal/server/maps/quests/KillSpiders.java @@ -23,8 +23,6 @@ import games.stendhal.server.entity.npc.ConversationStates; import games.stendhal.server.entity.npc.EventRaiser; import games.stendhal.server.entity.npc.SpeakerNPC; -import games.stendhal.server.entity.npc.action.MultipleActions; -import games.stendhal.server.entity.npc.action.SetQuestAction; import games.stendhal.server.entity.npc.action.SetQuestAndModifyKarmaAction; import games.stendhal.server.entity.npc.condition.AndCondition; import games.stendhal.server.entity.npc.condition.GreetingMatchesNameCondition; @@ -34,6 +32,7 @@ import games.stendhal.server.entity.player.Player; import games.stendhal.server.maps.Region; import games.stendhal.server.util.TimeUtil; +import marauroa.common.Pair; /** * QUEST: Kill Spiders @@ -62,7 +61,7 @@ * */ -public class KillSpiders extends AbstractQuest { +public class KillSpiders extends CompletionsTrackingQuest { private static final String QUEST_SLOT = "kill_all_spiders"; @@ -82,12 +81,12 @@ private void step_1() { new ChatAction() { @Override public void fire(final Player player, final Sentence sentence, final EventRaiser raiser) { - if (!player.hasQuest(QUEST_SLOT) || player.getQuest(QUEST_SLOT).equals("rejected")) { + if (!player.hasQuest(QUEST_SLOT) || player.getQuest(QUEST_SLOT, 0).equals("rejected")) { raiser.say("Have you ever been to the basement of the school? The room is full of spiders and some could be dangerous, since the students do experiments! Would you like to help me with this 'little' problem?"); raiser.setCurrentState(ConversationStates.QUEST_OFFERED); } else if (player.getQuest(QUEST_SLOT, 0).equals("started")) { raiser.say("I already asked you to kill all creatures in the basement!"); - } else if (player.getQuest(QUEST_SLOT).startsWith("killed;")) { + } else if (player.getQuest(QUEST_SLOT, 0).equals("killed")) { final String[] tokens = player.getQuest(QUEST_SLOT).split(";"); final long delay = TimeUtil.MILLISECONDS_IN_WEEK; final long timeRemaining = Long.parseLong(tokens[1]) + delay - System.currentTimeMillis(); @@ -103,24 +102,27 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser } }); - final List actions = new LinkedList(); - actions.add(new SetQuestAction(QUEST_SLOT, "started")); - //actions.add(new StartRecordingKillsAction(QUEST_SLOT,1,"spider", "poisonous spider", "giant spider")); - - npc.add(ConversationStates.QUEST_OFFERED, ConversationPhrases.YES_MESSAGES, null, ConversationStates.ATTENDING, "Fine. Go down to the basement and kill all the creatures there!", - new MultipleActions(actions)); + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + final Pair indexes = questInfo.getCompletionsIndexes(); + final String completions = player.getQuest(QUEST_SLOT, indexes.second()); + player.setQuest(QUEST_SLOT, "started"); + player.setQuest(QUEST_SLOT, indexes.first(), completions); + } + }); npc.add(ConversationStates.QUEST_OFFERED, ConversationPhrases.NO_MESSAGES, null, ConversationStates.ATTENDING, "Ok, I have to find someone else to do this 'little' job!", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -5.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -5.0)); } private void step_2() { @@ -132,7 +134,7 @@ private void step_3() { // support for old-style quests npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start")), + new QuestInStateCondition(QUEST_SLOT, 0, "start")), ConversationStates.ATTENDING, null, new ChatAction() { @@ -148,7 +150,9 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser player.equipOrPutOnGround(mythegg); player.addKarma(5.0); player.addXP(5000); - player.setQuest(QUEST_SLOT, "killed;" + System.currentTimeMillis()); + final String completions = player.getQuest(QUEST_SLOT, questInfo.getCompletionsIndexes().first()); + player.setQuest(QUEST_SLOT, "killed;" + System.currentTimeMillis() + ";" + completions); + incrementCompletionsAction().fire(player, null, null); } else { raiser.say("Go down and kill the creatures, no time left."); } @@ -175,7 +179,9 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser player.equipOrPutOnGround(mythegg); player.addKarma(5.0); player.addXP(5000); - player.setQuest(QUEST_SLOT, "killed;" + System.currentTimeMillis()); + final String completions = player.getQuest(QUEST_SLOT, questInfo.getCompletionsIndexes().first()); + player.setQuest(QUEST_SLOT, "killed;" + System.currentTimeMillis() + ";" + completions); + incrementCompletionsAction().fire(player, null, null); } else { raiser.say("Go down and kill the creatures, no time left."); } @@ -188,7 +194,8 @@ public void addToWorld() { fillQuestInfo( "Kill Spiders", "Morgrin, groundskeeper of the magic school, is concerned about spiders in the school basement.", - true); + true, 4, 2); + step_1(); step_2(); step_3(); diff --git a/src/games/stendhal/server/maps/quests/Maze.java b/src/games/stendhal/server/maps/quests/Maze.java index 6053040dcb5..38657881960 100644 --- a/src/games/stendhal/server/maps/quests/Maze.java +++ b/src/games/stendhal/server/maps/quests/Maze.java @@ -38,7 +38,7 @@ import games.stendhal.server.util.ResetSpeakerNPC; import games.stendhal.server.util.TimeUtil; -public class Maze extends AbstractQuest { +public class Maze extends CompletionsTrackingQuest { /** Minimum time between repeats. */ private static final int COOLING_TIME = TimeUtil.MINUTES_IN_HOUR * 24; private MazeSign sign; @@ -81,7 +81,7 @@ public List getHistory(final Player player) { res.add("Haizen won't make me a new maze yet."); } } - final int repetitions = player.getNumberOfRepetitions(getSlotName(), 2); + final int repetitions = getCompletions(player); if (repetitions > 1) { res.add("So far I've solved the maze " + repetitions + " times already!"); } diff --git a/src/games/stendhal/server/maps/quests/Snowballs.java b/src/games/stendhal/server/maps/quests/Snowballs.java index 05fa406d414..d16f32f47b2 100644 --- a/src/games/stendhal/server/maps/quests/Snowballs.java +++ b/src/games/stendhal/server/maps/quests/Snowballs.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -62,7 +62,7 @@ * required between repetitions */ -public class Snowballs extends AbstractQuest { +public class Snowballs extends CompletionsTrackingQuest { private static final int REQUIRED_SNOWBALLS = 25; @@ -78,13 +78,13 @@ public String getSlotName() { @Override public boolean isCompleted(final Player player) { return player.hasQuest(QUEST_SLOT) - && !player.getQuest(QUEST_SLOT).equals("start") - && !player.getQuest(QUEST_SLOT).equals("rejected"); + && !player.getQuest(QUEST_SLOT, 0).equals("start") + && !player.getQuest(QUEST_SLOT, 0).equals("rejected"); } @Override public boolean isRepeatable(final Player player) { - return new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, "start"), new QuestStartedCondition(QUEST_SLOT), new TimePassedCondition(QUEST_SLOT,REQUIRED_MINUTES)).fire(player, null, null); + return new AndCondition(new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new QuestStartedCondition(QUEST_SLOT), new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES)).fire(player, null, null); } @Override @@ -94,7 +94,7 @@ public List getHistory(final Player player) { return res; } res.add("I went down into the icy caves and met Mr. Yeti."); - final String questState = player.getQuest(QUEST_SLOT); + final String questState = player.getQuest(QUEST_SLOT, 0); if (questState.equals("rejected")) { res.add("I didn't want to help Mr. Yeti out this time and he harshly send me away..."); return res; @@ -128,7 +128,7 @@ private void prepareRequestingStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new PlayerHasItemWithHimCondition("snowball", REQUIRED_SNOWBALLS)), ConversationStates.QUEST_ITEM_BROUGHT, "Greetings stranger! I see you have the snow I asked for. Are these snowballs for me?", @@ -138,7 +138,7 @@ private void prepareRequestingStep() { npc.add(ConversationStates.IDLE, ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), - new QuestInStateCondition(QUEST_SLOT, "start"), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), new NotCondition(new PlayerHasItemWithHimCondition("snowball", REQUIRED_SNOWBALLS))), ConversationStates.ATTENDING, "You're back already? Don't forget that you promised to collect a bunch of snowballs for me!", @@ -149,8 +149,8 @@ private void prepareRequestingStep() { ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), new QuestStartedCondition(QUEST_SLOT), - new QuestNotInStateCondition(QUEST_SLOT, "start"), - new TimePassedCondition(QUEST_SLOT, REQUIRED_MINUTES)), + new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), + new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES)), ConversationStates.ATTENDING, "Greetings again! Have you seen my latest snow sculptures? I need a #favor again ...", null); @@ -160,11 +160,11 @@ private void prepareRequestingStep() { ConversationPhrases.GREETING_MESSAGES, new AndCondition(new GreetingMatchesNameCondition(npc.getName()), new QuestStartedCondition(QUEST_SLOT), - new QuestNotInStateCondition(QUEST_SLOT, "start"), - new NotCondition(new TimePassedCondition(QUEST_SLOT, REQUIRED_MINUTES))), + new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), + new NotCondition(new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES))), ConversationStates.ATTENDING, null, - new SayTimeRemainingAction(QUEST_SLOT, REQUIRED_MINUTES, "I have enough snow for my new sculpture. Thank you for helping! " + new SayTimeRemainingAction(QUEST_SLOT, 0, REQUIRED_MINUTES, "I have enough snow for my new sculpture. Thank you for helping! " + "I might start a new one in" )); // asks about quest - has never started it @@ -178,7 +178,7 @@ private void prepareRequestingStep() { // asks about quest but already on it npc.add(ConversationStates.ATTENDING, ConversationPhrases.QUEST_MESSAGES, - new QuestInStateCondition(QUEST_SLOT, "start"), + new QuestInStateCondition(QUEST_SLOT, 0, "start"), ConversationStates.ATTENDING, "You already promised me to bring some snowballs! Twenty five pieces, remember ...", null); @@ -186,7 +186,7 @@ private void prepareRequestingStep() { // asks about quest - has done it but it's repeatable now npc.add(ConversationStates.ATTENDING, ConversationPhrases.QUEST_MESSAGES, - new AndCondition(new QuestStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, "start"), new TimePassedCondition(QUEST_SLOT, REQUIRED_MINUTES)), + new AndCondition(new QuestStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES)), ConversationStates.QUEST_OFFERED, "I like to make snow sculptures, but the snow in this cavern is not good enough. Would you help me and get some snowballs? I need twenty five of them.", null); @@ -194,7 +194,7 @@ private void prepareRequestingStep() { // asks about quest - has done it and it's too soon to do again npc.add(ConversationStates.ATTENDING, ConversationPhrases.QUEST_MESSAGES, - new AndCondition(new QuestStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, "start"), new NotCondition(new TimePassedCondition(QUEST_SLOT, REQUIRED_MINUTES))), + new AndCondition(new QuestStartedCondition(QUEST_SLOT), new QuestNotInStateCondition(QUEST_SLOT, 0, "start"), new NotCondition(new TimePassedCondition(QUEST_SLOT, 0, REQUIRED_MINUTES))), ConversationStates.ATTENDING, "I have enough snow to finish my sculpture, but thanks for asking.", null); @@ -205,7 +205,7 @@ private void prepareRequestingStep() { null, ConversationStates.ATTENDING, "Fine. You can loot the snowballs from the ice golem in this cavern, but be careful there is something huge nearby! Come back when you get twenty five snowballs.", - new SetQuestAction(QUEST_SLOT, "start")); + new SetQuestAction(QUEST_SLOT, 0, "start")); // player is not willing to help npc.add(ConversationStates.QUEST_OFFERED, @@ -213,7 +213,7 @@ private void prepareRequestingStep() { null, ConversationStates.ATTENDING, "So what are you doing here? Go away!", - new SetQuestAndModifyKarmaAction(QUEST_SLOT, "rejected", -5.0)); + new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "rejected", -5.0)); } private void prepareBringingStep() { @@ -223,7 +223,8 @@ private void prepareBringingStep() { final List reward = new LinkedList(); reward.add(new DropItemAction("snowball", REQUIRED_SNOWBALLS)); reward.add(new IncreaseXPAction(50)); - reward.add(new SetQuestToTimeStampAction(QUEST_SLOT)); + reward.add(incrementCompletionsAction()); + reward.add(new SetQuestToTimeStampAction(QUEST_SLOT, 0)); // player gets either cod or perch, which we don't have a standard action for // and the npc says the name of the reward, too reward.add(new ChatAction() { @@ -272,7 +273,7 @@ public void addToWorld() { fillQuestInfo( "Snowballs for Mr. Yeti", "The inhabitant of the icy region in Faiumoni needs your help to collect some snowballs for him.", - false); + false, 1); prepareRequestingStep(); prepareBringingStep(); } diff --git a/src/games/stendhal/server/maps/quests/ThePiedPiper.java b/src/games/stendhal/server/maps/quests/ThePiedPiper.java index e5c92be27c8..93a7f72fb82 100644 --- a/src/games/stendhal/server/maps/quests/ThePiedPiper.java +++ b/src/games/stendhal/server/maps/quests/ThePiedPiper.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -63,7 +63,7 @@ * * REPETITIONS:
    • once between a week and two weeks.
    */ -public class ThePiedPiper extends AbstractQuest implements ITPPQuestConstants { +public class ThePiedPiper extends CompletionsTrackingQuest implements ITPPQuestConstants { protected static final Logger logger = Logger.getLogger(ThePiedPiper.class); @@ -256,7 +256,7 @@ public void addToWorld() { fillQuestInfo( "The Pied Piper", "Ados City has a rat problem from time to time.", - true); + true, 7, 1); startQuest(); } diff --git a/src/games/stendhal/server/maps/quests/WeeklyItemQuest.java b/src/games/stendhal/server/maps/quests/WeeklyItemQuest.java index 2a008b6c949..f26b60b4a52 100644 --- a/src/games/stendhal/server/maps/quests/WeeklyItemQuest.java +++ b/src/games/stendhal/server/maps/quests/WeeklyItemQuest.java @@ -79,7 +79,7 @@ * REPETITIONS: *
    • once a week
    */ -public class WeeklyItemQuest extends AbstractQuest { +public class WeeklyItemQuest extends CompletionsTrackingQuest { /** the logger instance */ private static final Logger logger = Logger.getLogger(WeeklyItemQuest.class); @@ -539,7 +539,7 @@ public List getHistory(final Player player) { res.add("I took the valuable item to Hazel within the last 7 days."); } // add to history how often player helped Hazel so far - final int repetitions = player.getNumberOfRepetitions(getSlotName(), 2); + final int repetitions = getCompletions(player); if (repetitions > 0) { res.add("I've brought exhibits for the museum on " + Grammar.quantityplnoun(repetitions, "occasion") + " so far."); diff --git a/src/games/stendhal/server/maps/quests/ZooFood.java b/src/games/stendhal/server/maps/quests/ZooFood.java index 5d56a937f1d..9b2dc0fc8df 100644 --- a/src/games/stendhal/server/maps/quests/ZooFood.java +++ b/src/games/stendhal/server/maps/quests/ZooFood.java @@ -73,7 +73,7 @@ * * REPETITIONS: - Once per week. */ -public class ZooFood extends AbstractQuest { +public class ZooFood extends CompletionsTrackingQuest { private static final int REQUIRED_HAM = 10; private static final int DELAY = TimeUtil.MINUTES_IN_WEEK; @@ -179,7 +179,7 @@ private void step_1() { npc.add(ConversationStates.QUEST_OFFERED, ConversationPhrases.YES_MESSAGES, null, ConversationStates.ATTENDING, null, - new MultipleActions(new SetQuestAction(QUEST_SLOT, "start;"), + new MultipleActions(new SetQuestAction(QUEST_SLOT, 0, "start"), new StartRecordingRandomItemCollectionAction(QUEST_SLOT, 1, items, "Oh, thank you! Please help us by" + " bringing [item] as soon as you can.")) ); @@ -234,9 +234,10 @@ private void step_3() { new SayRequiredItemAction(QUEST_SLOT, 1, "Welcome back! Have you brought the [item]?")); final List actions = new LinkedList(); - actions.add(new DropRecordedItemAction(QUEST_SLOT,1)); - actions.add(new SetQuestAndModifyKarmaAction(QUEST_SLOT, "done;1", 5.0)); + actions.add(new DropRecordedItemAction(QUEST_SLOT, 1)); + actions.add(new SetQuestAndModifyKarmaAction(QUEST_SLOT, 0, "done", 5.0)); actions.add(new SetQuestToTimeStampAction(QUEST_SLOT, 1)); + actions.add(incrementCompletionsAction()); actions.add(new IncreaseXPAction(200)); npc.add(ConversationStates.QUEST_ITEM_BROUGHT, @@ -284,7 +285,8 @@ public void addToWorld() { fillQuestInfo( "Zoo Food", "The animals at the zoo are hungry and need some food!", - true); + true, 2); + step_1(); step_2(); step_3(); diff --git a/src/games/stendhal/server/maps/quests/piedpiper/InvasionPhase.java b/src/games/stendhal/server/maps/quests/piedpiper/InvasionPhase.java index 26c058f0795..28c998cc80f 100644 --- a/src/games/stendhal/server/maps/quests/piedpiper/InvasionPhase.java +++ b/src/games/stendhal/server/maps/quests/piedpiper/InvasionPhase.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * Copyright (C) 2003-2023 - Arianne * + * Copyright (C) 2003-2024 - Arianne * *************************************************************************** *************************************************************************** * * @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; +import games.stendhal.common.MathHelper; import games.stendhal.common.Rand; import games.stendhal.common.grammar.Grammar; import games.stendhal.common.parser.Sentence; @@ -295,6 +296,22 @@ public void update (Observable obj, Object arg) { } } + /** + * Initializes quest state string after player kills first rat. + * + * @param player + * Player doing quest. + */ + private void initQuestState(final Player player) { + int index = 7; + if ("done".equals(player.getQuest(QUEST_SLOT, 0))) { + index = 1; + } + // preserve completions count + final int completions = MathHelper.parseIntDefault(player.getQuest(QUEST_SLOT, index), 0); + player.setQuest(QUEST_SLOT, "rats;0;0;0;0;0;0;" + completions); + } + /** * method for making records about killing rats * in player's quest slot. @@ -316,17 +333,17 @@ private void killsRecorder(Player player, final RPEntity victim) { } if((player.getQuest(QUEST_SLOT)==null)|| - (player.getQuest(QUEST_SLOT).equals("done")|| + (player.getQuest(QUEST_SLOT, 0).equals("done")|| (player.getQuest(QUEST_SLOT).equals("")))){ // player just killed his first creature. - player.setQuest(QUEST_SLOT, "rats;0;0;0;0;0;0"); + initQuestState(player); } // we using here and after "i+1" because player's quest index 0 // is occupied by quest stage description. if ("".equals(player.getQuest(QUEST_SLOT,i+1))){ // something really wrong, will correct this... - player.setQuest(QUEST_SLOT,"rats;0;0;0;0;0;0"); + initQuestState(player); } int kills; try { diff --git a/src/games/stendhal/server/maps/quests/piedpiper/RewardPlayerAction.java b/src/games/stendhal/server/maps/quests/piedpiper/RewardPlayerAction.java index 05420345673..b1794daf502 100644 --- a/src/games/stendhal/server/maps/quests/piedpiper/RewardPlayerAction.java +++ b/src/games/stendhal/server/maps/quests/piedpiper/RewardPlayerAction.java @@ -1,5 +1,17 @@ +/*************************************************************************** + * Copyright © 2003-2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ package games.stendhal.server.maps.quests.piedpiper; +import games.stendhal.common.MathHelper; import games.stendhal.common.parser.Sentence; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.entity.item.StackableItem; @@ -27,6 +39,18 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser moneys.setQuantity(quantity); player.equipOrPutOnGround(moneys); mayor.say("Please take "+quantity+" money, thank you very much for your help."); - player.setQuest(QUEST_SLOT, "done"); + setQuestDone(player); + } + + /** + * Sets quest state to done and increments number of completions. + * + * @param player + * Player completing quest. + */ + private void setQuestDone(final Player player) { + // preserve completions count + final int completions = MathHelper.parseIntDefault(player.getQuest(QUEST_SLOT, 7), 0); + player.setQuest(QUEST_SLOT, "done;" + (completions + 1)); } } diff --git a/src/games/stendhal/server/maps/quests/piedpiper/TPPQuestHelperFunctions.java b/src/games/stendhal/server/maps/quests/piedpiper/TPPQuestHelperFunctions.java index 3587a614eaa..ca83405c28d 100644 --- a/src/games/stendhal/server/maps/quests/piedpiper/TPPQuestHelperFunctions.java +++ b/src/games/stendhal/server/maps/quests/piedpiper/TPPQuestHelperFunctions.java @@ -1,3 +1,14 @@ +/*************************************************************************** + * Copyright © 2003-2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ package games.stendhal.server.maps.quests.piedpiper; @@ -31,6 +42,11 @@ public static SpeakerNPC getMainNPC() { * gold amount for hunting rats. */ public static int calculateReward(Player player) { + if (player.isQuestInState(QUEST_SLOT, 0, "done")) { + // information for previous raid is no longer available + return 0; + } + int moneys = 0; int kills = 0; for(int i=0; i 0) { + responses = new String[] { + "Hello again.", + "My Hughie is getting sick again! Please could you bring another bowl of fish soup? It helped last time." + }; + } en.step(player, "hi"); - assertEquals("Hi, I really could do with a #favor, please.", getReply(npc)); + assertEquals(responses[0], getReply(npc)); en.step(player, "favor"); - assertEquals("My poor boy is sick and the potions I give him aren't working! Please could you fetch him some fish soup?", getReply(npc)); + assertEquals(responses[1], getReply(npc)); en.step(player, "no"); assertEquals("Oh no, please, he's so sick.", getReply(npc)); - assertEquals(player.getQuest(questSlot), "rejected"); + assertEquals(player.getQuest(questSlot, 0), "rejected"); en.step(player, "task"); + // when rejected she uses response as if quest was never completed + //assertEquals(responses[1], getReply(npc)); assertEquals("My poor boy is sick and the potions I give him aren't working! Please could you fetch him some fish soup?", getReply(npc)); en.step(player, "yes"); assertEquals("Thank you! You can ask Florence Bouillabaisse to make you fish soup. I think she's in Ados market somewhere.", getReply(npc)); en.step(player, "bye"); assertEquals("Goodbye.", getReply(npc)); - assertEquals(player.getQuest(questSlot), "start"); + assertEquals("start", player.getQuest(questSlot, 0)); en.step(player, "hi"); assertEquals("You're back already? Hughie is getting sicker! Don't forget the fish soup for him, please. I promise to reward you.", getReply(npc)); @@ -155,7 +169,7 @@ public void testQuest() { en.step(player, "bye"); assertEquals("Goodbye.", getReply(npc)); - // test reward + // test reward assertEquals(xp + 200, player.getXP()); assertThat(player.getKarma(), greaterThan(karma)); assertTrue(player.isEquipped("potion", 10)); @@ -183,7 +197,7 @@ public void testRepeatingQuest() { npc = SingletonRepository.getNPCList().get("Anastasia"); en = npc.getEngine(); - player.setQuest(questSlot, "-1"); + player.setQuest(questSlot, 0, "-1"); // [17:37] Admin kymara changed your state of the quest 'fishsoup_for_hughie' from '1294594642173' to '-1' // [17:37] Changed the state of quest 'fishsoup_for_hughie' from '1294594642173' to '-1' @@ -206,4 +220,27 @@ public void testRepeatingQuest() { en.step(player, "bye"); assertEquals("Goodbye.", getReply(npc)); } + + @Test + public void testCompletions() { + for (int count = 0; count < 5; count++) { + assertEquals(count, MathHelper.parseIntDefault(player.getQuest(questSlot, 1), 0)); + testQuest(); + // reset so can be repeated + player.setQuest(questSlot, 0, "0"); + } + assertEquals("5", player.getQuest(questSlot, 1)); + + // check that completions count is retained after quest is rejected & started + en.step(player, "hi"); + en.step(player, "task"); + en.step(player, "no"); + assertEquals("rejected", player.getQuest(questSlot, 0)); + assertEquals("5", player.getQuest(questSlot, 1)); + en.step(player, "task"); + en.step(player, "yes"); + assertEquals("start", player.getQuest(questSlot, 0)); + assertEquals("5", player.getQuest(questSlot, 1)); + en.step(player, "bye"); + } } diff --git a/tests/games/stendhal/server/maps/quests/FruitsForCoraliaTest.java b/tests/games/stendhal/server/maps/quests/FruitsForCoraliaTest.java index 57d99464ad3..dc4685fa71a 100644 --- a/tests/games/stendhal/server/maps/quests/FruitsForCoraliaTest.java +++ b/tests/games/stendhal/server/maps/quests/FruitsForCoraliaTest.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2011 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -13,6 +13,7 @@ package games.stendhal.server.maps.quests; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -22,6 +23,7 @@ import org.junit.BeforeClass; import org.junit.Test; +import games.stendhal.common.MathHelper; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.entity.npc.SpeakerNPC; @@ -86,15 +88,17 @@ public void testQuest() { // ----------------------------------------------- - assertEquals("It's a shame for you to see it all withered like this, it really needs some fresh #fruits...", getReply(npc)); + final int completions = MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0); - // ----------------------------------------------- - - en.step(player, "fruit"); - - // ----------------------------------------------- - - assertEquals("Would you be kind enough to find me some fresh fruits for my hat? I'd be ever so grateful!", getReply(npc)); + if (completions == 0) { + assertEquals("It's a shame for you to see it all withered like this, it really needs some fresh #fruits...", getReply(npc)); + en.step(player, "fruit"); + assertEquals("Would you be kind enough to find me some fresh fruits for my hat? I'd be ever so grateful!", getReply(npc)); + } else { + assertEquals( + "I'm sorry to say that the fruits you brought for my hat aren't very fresh anymore." + + " Would you be kind enough to find me some more?", getReply(npc)); + } // ----------------------------------------------- @@ -135,6 +139,9 @@ public void testQuest() { en.step(player, "yes"); + // check that completions count still matches + assertThat(MathHelper.parseIntDefault(player.getQuest(questSlot, 0), 0), is(completions)); + // ----------------------------------------------- assertEquals("That's wonderful! I'd like these fresh fruits: 4 #apples, 5 #bananas, 9 #cherries, 2 #'bunches of grapes', 4 #pears, 2 #pomegranates, and a #watermelon.", getReply(npc)); @@ -316,7 +323,7 @@ public void testQuest() { // ----------------------------------------------- - player.setQuest(questSlot, "done;0"); + //player.setQuest(questSlot, "done;0"); //player.setQuest(questSlot, 1, "0"); This doesn't seem to work either. @@ -332,5 +339,33 @@ public void testQuest() { assertEquals("Bye.", getReply(npc)); */ + + en.step(player, "bye"); + assertThat(player.getQuest(questSlot, 0), is("done")); + + // check that completions updated + assertThat(MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0), is(completions+1)); + } + + @Test + public void testCompletions() { + for (int count = 0; count < 5; count++) { + assertEquals(count, MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0)); + testQuest(); + // reset so can be repeated + player.setQuest(questSlot, 1, "0"); + } + assertEquals("5", player.getQuest(questSlot, 2)); + + // check that completions count is retained after quest is rejected & started + en.step(player, "hi"); + en.step(player, "task"); + en.step(player, "no"); + assertEquals("rejected", player.getQuest(questSlot, 0)); + assertEquals("5", player.getQuest(questSlot, 2)); + en.step(player, "task"); + en.step(player, "yes"); + assertEquals("5", player.getQuest(questSlot, 0)); + en.step(player, "bye"); } } diff --git a/tests/games/stendhal/server/maps/quests/IcecreamForAnnieTest.java b/tests/games/stendhal/server/maps/quests/IcecreamForAnnieTest.java index eb85756e0aa..d0d90877395 100644 --- a/tests/games/stendhal/server/maps/quests/IcecreamForAnnieTest.java +++ b/tests/games/stendhal/server/maps/quests/IcecreamForAnnieTest.java @@ -25,6 +25,7 @@ import org.junit.BeforeClass; import org.junit.Test; +import games.stendhal.common.MathHelper; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.entity.item.Item; @@ -78,8 +79,13 @@ public void testQuest() { npc = SingletonRepository.getNPCList().get("Annie Jones"); en = npc.getEngine(); + final int completions = MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0); en.step(player, "hi"); - assertEquals("Hello, my name is Annie. I am five years old.", getReply(npc)); + if (completions == 0) { + assertEquals("Hello, my name is Annie. I am five years old.", getReply(npc)); + } else { + assertThat(getReply(npc), is("Hello.")); + } en.step(player, "help"); assertEquals("Ask my mummy.", getReply(npc)); en.step(player, "job"); @@ -90,7 +96,7 @@ public void testQuest() { assertEquals("I'm hungry! I'd like an ice cream, please. Vanilla, with a chocolate flake. Will you get me one?", getReply(npc)); en.step(player, "ok"); assertEquals("Thank you!", getReply(npc)); - assertThat(player.getQuest(questSlot), is("start")); + assertThat(player.getQuest(questSlot, 0), is("start")); en.step(player, "bye"); assertEquals("Ta ta.", getReply(npc)); @@ -139,7 +145,7 @@ public void testQuest() { en.step(player, "hi"); assertEquals("Hello, I see you've met my daughter Annie. I hope she wasn't too demanding. You seem like a nice person.", getReply(npc)); - assertThat(player.getQuest(questSlot), is("mummy")); + assertThat(player.getQuest(questSlot, 0), is("mummy")); en.step(player, "task"); assertEquals("Nothing, thank you.", getReply(npc)); en.step(player, "bye"); @@ -158,7 +164,7 @@ public void testQuest() { assertTrue(player.isEquipped("present")); assertThat(player.getXP(), greaterThan(xp)); assertThat(player.getKarma(), greaterThan(karma)); - assertTrue(player.getQuest(questSlot).startsWith("eating")); + assertTrue(player.getQuest(questSlot).startsWith("eating;")); assertEquals("Thank you EVER so much! You are very kind. Here, take this present.", getReply(npc)); en.step(player, "bye"); assertEquals("Ta ta.", getReply(npc)); @@ -176,13 +182,13 @@ public void testQuest() { // ----------------------------------------------- final double newKarma = player.getKarma(); // [15:07] Changed the state of quest 'icecream_for_annie' from 'eating;1219676807283' to 'eating;0' - player.setQuest(questSlot, "eating;0"); + player.setQuest(questSlot, 1, "0"); en.step(player, "hi"); assertEquals("Hello.", getReply(npc)); en.step(player, "task"); assertEquals("I hope another ice cream wouldn't be greedy. Can you get me one?", getReply(npc)); en.step(player, "no"); - assertThat(player.getQuest(questSlot), is("rejected")); + assertThat(player.getQuest(questSlot, 0), is("rejected")); assertThat(player.getKarma(), lessThan(newKarma)); assertEquals("Ok, I'll ask my mummy instead.", getReply(npc)); @@ -193,4 +199,26 @@ public void testQuest() { en.step(player, "bye"); assertEquals("Ta ta.", getReply(npc)); } + + @Test + public void testCompletions() { + for (int count = 0; count < 5; count++) { + assertThat(MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0), is(count)); + testQuest(); + // reset so can be repeated + player.setQuest(questSlot, 1, "0"); + } + assertThat(player.getQuest(questSlot, 2), is("5")); + + // check that completions count is retained after quest is rejected & started + en.step(player, "hi"); + en.step(player, "task"); + en.step(player, "no"); + assertThat(player.getQuest(questSlot), is("rejected;0;5")); + en.step(player, "hi"); + en.step(player, "task"); + en.step(player, "yes"); + assertThat(player.getQuest(questSlot), is("start;0;5")); + en.step(player, "bye"); + } } diff --git a/tests/games/stendhal/server/maps/quests/KillDhohrNuggetcutterTest.java b/tests/games/stendhal/server/maps/quests/KillDhohrNuggetcutterTest.java index 095eaf25dec..d4df52dc757 100644 --- a/tests/games/stendhal/server/maps/quests/KillDhohrNuggetcutterTest.java +++ b/tests/games/stendhal/server/maps/quests/KillDhohrNuggetcutterTest.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2011 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -20,6 +20,7 @@ import org.junit.BeforeClass; import org.junit.Test; +import games.stendhal.common.MathHelper; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.fsm.Engine; @@ -67,13 +68,24 @@ public void testQuest() { npc = SingletonRepository.getNPCList().get("Zogfang"); en = npc.getEngine(); + final int completions = MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0); + String[] responses = new String[] { + "We are unable to rid our area of dwarves. Especially one mighty one named Dhohr" + + " Nuggetcutter. Would you please kill them?" + }; + if (completions > 0) { + responses = new String[] { + "Would you like to help again clearing this Keep of our enemies, those dwarves?" + }; + } + en.step(player, "hi"); assertEquals("Hello my fine fellow. Welcome to Ados Abandoned Keep, our humble dwelling!", getReply(npc)); en.step(player, "task"); - assertEquals("We are unable to rid our area of dwarves. Especially one mighty one named Dhohr Nuggetcutter. Would you please kill them?", getReply(npc)); + assertEquals(responses[0], getReply(npc)); en.step(player, "no"); assertEquals("Ok, I will await someone having the guts to have the job done.", getReply(npc)); - assertEquals("rejected", player.getQuest(questSlot)); + assertEquals("rejected", player.getQuest(questSlot, 0)); en.step(player, "bye"); assertEquals("I wish you well on your journeys.", getReply(npc)); @@ -82,6 +94,8 @@ public void testQuest() { en.step(player, "hi"); assertEquals("Hello my fine fellow. Welcome to Ados Abandoned Keep, our humble dwelling!", getReply(npc)); en.step(player, "task"); + // when rejected he uses response as if quest was never completed + //assertEquals(responses[0], getReply(npc)); assertEquals("We are unable to rid our area of dwarves. Especially one mighty one named Dhohr Nuggetcutter. Would you please kill them?", getReply(npc)); en.step(player, "yes"); assertEquals("Great! Please find all wandering #dwarves somewhere in this level of the keep and make them pay for their tresspassing!", getReply(npc)); @@ -142,9 +156,11 @@ public void testQuest() { player.setSoloKill("mountain dwarf"); player.setSoloKill("mountain dwarf"); + final int xpBefore = player.getXP(); + en.step(player, "hi"); assertEquals("Thank you so much. You are a warrior, indeed! Here, have one of these. We have found them scattered about. We have no idea what they are.", getReply(npc)); - assertEquals(4000, player.getXP()); + assertEquals(xpBefore+4000, player.getXP()); assertEquals("killed", player.getQuest(questSlot, 0)); en.step(player, "task"); @@ -152,4 +168,27 @@ public void testQuest() { en.step(player, "bye"); assertEquals("I wish you well on your journeys.", getReply(npc)); } + + @Test + public void testCompletions() { + for (int count = 0; count < 5; count++) { + assertEquals(count, MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0)); + testQuest(); + // reset so can be repeated + player.setQuest(questSlot, 1, "0"); + } + assertEquals("5", player.getQuest(questSlot, 2)); + + // check that completions count is retained after quest is rejected & started + en.step(player, "hi"); + en.step(player, "task"); + en.step(player, "no"); + assertEquals("rejected", player.getQuest(questSlot, 0)); + assertEquals("5", player.getQuest(questSlot, 2)); + en.step(player, "task"); + en.step(player, "yes"); + assertEquals("start", player.getQuest(questSlot, 0)); + assertEquals("5", player.getQuest(questSlot, 2)); + en.step(player, "bye"); + } } diff --git a/tests/games/stendhal/server/maps/quests/KillSpidersTest.java b/tests/games/stendhal/server/maps/quests/KillSpidersTest.java index b61280b7516..f92f4f52596 100644 --- a/tests/games/stendhal/server/maps/quests/KillSpidersTest.java +++ b/tests/games/stendhal/server/maps/quests/KillSpidersTest.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -186,7 +186,7 @@ public void testQuest() { en.step(player, "no"); assertThat(player.getKarma(), lessThan(newKarma)); assertEquals("Ok, I have to find someone else to do this 'little' job!", getReply(npc)); - assertThat(player.getQuest(questSlot), is("rejected")); + assertThat(player.getQuest(questSlot, 0), is("rejected")); en.step(player, "bye"); assertEquals("Bye.", getReply(npc)); diff --git a/tests/games/stendhal/server/maps/quests/SnowballsTest.java b/tests/games/stendhal/server/maps/quests/SnowballsTest.java index 237e34f7c0a..f5338c4b696 100644 --- a/tests/games/stendhal/server/maps/quests/SnowballsTest.java +++ b/tests/games/stendhal/server/maps/quests/SnowballsTest.java @@ -1,3 +1,14 @@ +/*************************************************************************** + * Copyright © 2010-2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ package games.stendhal.server.maps.quests; import static org.hamcrest.Matchers.greaterThan; @@ -14,6 +25,7 @@ import org.junit.BeforeClass; import org.junit.Test; +import games.stendhal.common.MathHelper; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.entity.npc.SpeakerNPC; @@ -59,8 +71,18 @@ public void testQuest() { // ----------------------------------------------- + final int completions = MathHelper.parseIntDefault(player.getQuest(questSlot, 1), 0); + String[] responses = new String[] { + "Greetings stranger! Have you seen my snow sculptures? I need a #favor from someone friendly like you." + }; + if (completions > 0) { + responses = new String[] { + "Greetings again! Have you seen my latest snow sculptures? I need a #favor again ..." + }; + } + en.step(player, "hi"); - assertEquals("Greetings stranger! Have you seen my snow sculptures? I need a #favor from someone friendly like you.", getReply(npc)); + assertEquals(responses[0], getReply(npc)); en.step(player, "favor"); assertEquals("I like to make snow sculptures, but the snow in this cavern is not good enough. Would you help me and get some snowballs? I need twenty five of them.", getReply(npc)); en.step(player, "no"); @@ -68,7 +90,7 @@ public void testQuest() { en.step(player, "bye"); assertEquals("Bye.", getReply(npc)); - assertEquals(player.getQuest(questSlot), "rejected"); + assertEquals(player.getQuest(questSlot, 0), "rejected"); en.step(player, "hi"); assertEquals("Greetings stranger! Have you seen my snow sculptures? I need a #favor from someone friendly like you.", getReply(npc)); @@ -79,7 +101,7 @@ public void testQuest() { en.step(player, "bye"); assertEquals("Bye.", getReply(npc)); - assertEquals(player.getQuest(questSlot), "start"); + assertEquals(player.getQuest(questSlot, 0), "start"); en.step(player, "hi"); assertEquals("You're back already? Don't forget that you promised to collect a bunch of snowballs for me!", getReply(npc)); @@ -128,7 +150,7 @@ public void testQuest() { assertTrue(player.isEquipped("perch", 20) || player.isEquipped("cod", 20) ); assertNotNull(player.getQuest(questSlot)); - assertFalse(player.getQuest(questSlot).equals("start")); + assertFalse(player.getQuest(questSlot, 0).equals("start")); en.step(player, "hi"); assertEquals("I have enough snow for my new sculpture. Thank you for helping! I might start a new one in 2 hours.", getReply(npc)); @@ -140,7 +162,7 @@ public void testQuest() { // [09:49] Admin kymara changed your state of the quest 'snowballs' from '1288518569387' to '0' // [09:49] Changed the state of quest 'snowballs' from '1288518569387' to '0' - player.setQuest(questSlot, "0"); + player.setQuest(questSlot, 0, "0"); en.step(player, "hi"); assertEquals("Greetings again! Have you seen my latest snow sculptures? I need a #favor again ...", getReply(npc)); @@ -151,4 +173,27 @@ public void testQuest() { en.step(player, "bye"); assertEquals("Bye.", getReply(npc)); } + + @Test + public void testCompletions() { + for (int count = 0; count < 5; count++) { + assertEquals(count, MathHelper.parseIntDefault(player.getQuest(questSlot, 1), 0)); + testQuest(); + // reset so can be repeated + player.setQuest(questSlot, 0, "0"); + } + assertEquals("5", player.getQuest(questSlot, 1)); + + // check that completions count is retained after quest is rejected & started + en.step(player, "hi"); + en.step(player, "task"); + en.step(player, "no"); + assertEquals("rejected", player.getQuest(questSlot, 0)); + assertEquals("5", player.getQuest(questSlot, 1)); + en.step(player, "task"); + en.step(player, "yes"); + assertEquals("start", player.getQuest(questSlot, 0)); + assertEquals("5", player.getQuest(questSlot, 1)); + en.step(player, "bye"); + } } diff --git a/tests/games/stendhal/server/maps/quests/ThePiedPiperTest.java b/tests/games/stendhal/server/maps/quests/ThePiedPiperTest.java index f5b955b18f6..e5edcb3fcb6 100644 --- a/tests/games/stendhal/server/maps/quests/ThePiedPiperTest.java +++ b/tests/games/stendhal/server/maps/quests/ThePiedPiperTest.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -12,13 +12,20 @@ ***************************************************************************/ package games.stendhal.server.maps.quests; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import org.apache.log4j.Logger; import org.junit.BeforeClass; import org.junit.Test; +import games.stendhal.common.MathHelper; +import games.stendhal.server.entity.npc.SpeakerNPC; +import games.stendhal.server.entity.player.Player; import games.stendhal.server.maps.quests.piedpiper.ITPPQuestConstants; +import games.stendhal.server.maps.quests.thepiedpiper.InvasionPhaseTest; import games.stendhal.server.maps.quests.thepiedpiper.TPPTestHelper; public class ThePiedPiperTest implements ITPPQuestConstants { @@ -48,4 +55,28 @@ public void testPhaseChanging() { assertEquals(TPP_Phase.TPP_INACTIVE, ThePiedPiper.getPhase()); } + private void doQuest(final InvasionPhaseTest phase) { + phase.startInvasion(); + phase.killRats(15); + phase.endInvasion(); + phase.collectReward(); + phase.resetReward(); + } + + @Test + public void testCompletions() { + final String questSlot = quest.getSlotName(); + final Player player = TPPTestHelper.getPlayer(); + assertThat(player, notNullValue()); + final SpeakerNPC npc = TPPTestHelper.getNPC(); + assertThat(npc, notNullValue()); + + final InvasionPhaseTest phaseTest = new InvasionPhaseTest(); + for (int count = 0; count < 5; count++) { + assertThat(MathHelper.parseIntDefault(player.getQuest(questSlot, 1), 0), is(count)); + // run quest + doQuest(phaseTest); + } + assertThat(player.getQuest(questSlot), is("done;5")); + } } diff --git a/tests/games/stendhal/server/maps/quests/thepiedpiper/InvasionPhaseTest.java b/tests/games/stendhal/server/maps/quests/thepiedpiper/InvasionPhaseTest.java index 9e27469f016..a4af4dfe5c3 100644 --- a/tests/games/stendhal/server/maps/quests/thepiedpiper/InvasionPhaseTest.java +++ b/tests/games/stendhal/server/maps/quests/thepiedpiper/InvasionPhaseTest.java @@ -1,6 +1,19 @@ +/*************************************************************************** + * Copyright © 2003-2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ package games.stendhal.server.maps.quests.thepiedpiper; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static utilities.SpeakerNPCTestHelper.getReply; @@ -20,11 +33,31 @@ public static void setUpBeforeClass() throws Exception { TPPTestHelper.setUpBeforeClass(); } - @Test - public void testInvasionPhase() { + public void startInvasion() { // [17:50] Mayor Chalmers shouts: Ados city is under rats invasion! Anyone who will help to clean up city, will be rewarded! ThePiedPiper.setPhase(TPP_Phase.TPP_INACTIVE); ThePiedPiper.switchToNextPhase(); + } + + public void endInvasion() { + ThePiedPiper.setPhase(TPP_Phase.TPP_INACTIVE); + } + + public void collectReward() { + en.step(player, "hi"); + en.step(player, "reward"); + assertThat(getReply(npc), is("Please take "+ rewardMoneys +" money, thank you very much for your help.")); + en.step(player, "bye"); + assertThat(getReply(npc), is("Good day to you.")); + } + + public void resetReward() { + rewardMoneys = 0; + } + + @Test + public void testInvasionPhase() { + startInvasion(); //quest.phaseInactiveToInvasion(); en.step(player, "bye"); // in case if previous test was failed en.step(player, "hi"); @@ -53,13 +86,10 @@ public void testInvasionPhase() { "so I will give you "+rewardMoneys+ " money as a #reward for that job.", getReply(npc)); assertEquals(questHistory, quest.getHistory(player)); - en.step(player, "reward"); - assertEquals("Please take "+ rewardMoneys +" money, thank you very much for your help.", getReply(npc)); + collectReward(); questHistory.clear(); questHistory.add("I have killed some rats in Ados city and got a reward from Mayor Chalmers!"); assertEquals(questHistory, quest.getHistory(player)); - en.step(player, "bye"); - assertEquals("Good day to you.", getReply(npc)); } @Test @@ -121,13 +151,10 @@ public void testAccumulatingRewards() { " money as a #reward for that job.", getReply(npc)); assertTrue("", (rewardMoneys > tempReward)); assertEquals(questHistory, quest.getHistory(player)); - en.step(player, "reward"); - assertEquals("Please take "+ rewardMoneys +" money, thank you very much for your help.", getReply(npc)); + collectReward(); questHistory.clear(); questHistory.add("I have killed some rats in Ados city and got a reward from Mayor Chalmers!"); assertEquals(questHistory, quest.getHistory(player)); - en.step(player, "bye"); - assertEquals("Good day to you.", getReply(npc)); } } diff --git a/tests/games/stendhal/server/maps/quests/thepiedpiper/TPPTestHelper.java b/tests/games/stendhal/server/maps/quests/thepiedpiper/TPPTestHelper.java index 0ad81210338..9e24ca38fee 100644 --- a/tests/games/stendhal/server/maps/quests/thepiedpiper/TPPTestHelper.java +++ b/tests/games/stendhal/server/maps/quests/thepiedpiper/TPPTestHelper.java @@ -1,3 +1,14 @@ +/*************************************************************************** + * Copyright © 2003-2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ package games.stendhal.server.maps.quests.thepiedpiper; import org.apache.log4j.Logger; @@ -112,7 +123,7 @@ private void killRat(Creature rat, int count) { * function for killing creatures. * @param numb - number of creatures to kill. */ - protected void killRats(int numb) { + public void killRats(int numb) { int count=0; logger.info("number of rats to kill: "+numb); for (int i=0; i