From c867c6594449e6fae6c37f438099690c1c70232d Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Fri, 26 Apr 2024 20:41:06 -0700 Subject: [PATCH 01/17] Deprecate IQuest.getCompletedCount for IQuest.getCompletions --- .../server/maps/quests/AbstractQuest.java | 25 ++++++++++++++++++- .../stendhal/server/maps/quests/IQuest.java | 11 ++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/games/stendhal/server/maps/quests/AbstractQuest.java b/src/games/stendhal/server/maps/quests/AbstractQuest.java index e60dd0f2b73..c45a78651f5 100644 --- a/src/games/stendhal/server/maps/quests/AbstractQuest.java +++ b/src/games/stendhal/server/maps/quests/AbstractQuest.java @@ -146,8 +146,16 @@ public boolean isCompleted(final Player player) { && player.isQuestCompleted(getSlotName()); } + /** + * Retrieves number of times player has completed quest. + * + * @param player + * Player for whom quest is being checked. + * @return + * Number of completions. + */ @Override - public int getCompletedCount(final Player player) { + public int getCompletions(final Player player) { final String questSlot = getSlotName(); final boolean completed = isCompleted(player); if (player.hasQuest(questSlot)) { @@ -167,6 +175,21 @@ public int getCompletedCount(final Player player) { return completed ? 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) { + return getCompletions(player); + } + @Override public boolean isRepeatable(final Player player) { // TODO implement diff --git a/src/games/stendhal/server/maps/quests/IQuest.java b/src/games/stendhal/server/maps/quests/IQuest.java index 21abe224cf8..9bfefaec8d8 100644 --- a/src/games/stendhal/server/maps/quests/IQuest.java +++ b/src/games/stendhal/server/maps/quests/IQuest.java @@ -74,6 +74,17 @@ public interface IQuest { * @return * Number of completions. */ + int getCompletions(Player player); + + /** + * Retrieves number of times player has completed quest. + * + * @param player + * Player for whom quest is being checked. + * @return + * Number of completions. + */ + @Deprecated int getCompletedCount(Player player); /** From 22e35c2973586cedead8a9353f6cfb5c4afa113c Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Tue, 28 May 2024 21:51:37 -0700 Subject: [PATCH 02/17] Method to check if a quest has been completed at least once... ...regardless of current completed state https://github.com/arianne/stendhal/issues/230 --- .../stendhal/server/maps/quests/AbstractQuest.java | 5 +++++ src/games/stendhal/server/maps/quests/IQuest.java | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/games/stendhal/server/maps/quests/AbstractQuest.java b/src/games/stendhal/server/maps/quests/AbstractQuest.java index c45a78651f5..f0b416c5e15 100644 --- a/src/games/stendhal/server/maps/quests/AbstractQuest.java +++ b/src/games/stendhal/server/maps/quests/AbstractQuest.java @@ -146,6 +146,11 @@ 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. * diff --git a/src/games/stendhal/server/maps/quests/IQuest.java b/src/games/stendhal/server/maps/quests/IQuest.java index 9bfefaec8d8..dccd7f5447e 100644 --- a/src/games/stendhal/server/maps/quests/IQuest.java +++ b/src/games/stendhal/server/maps/quests/IQuest.java @@ -66,6 +66,16 @@ 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. * From 291aaeed38f03cb853fab35637123a09ec168763 Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Thu, 16 May 2024 14:40:27 -0700 Subject: [PATCH 03/17] Add temporary list of quests without completions --- quests_without_completions.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 quests_without_completions.txt 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) From 53109c85f541d9bc79d751a1c244e52905ddd223 Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Fri, 26 Apr 2024 01:13:51 -0700 Subject: [PATCH 04/17] Quest super class for tracking completions --- .../server/maps/quests/AbstractQuest.java | 19 +- .../maps/quests/CompletionsTrackingQuest.java | 165 ++++++++++++++++++ 2 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java diff --git a/src/games/stendhal/server/maps/quests/AbstractQuest.java b/src/games/stendhal/server/maps/quests/AbstractQuest.java index f0b416c5e15..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; @@ -161,23 +160,7 @@ public boolean hasCompleted(final Player player) { */ @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; + return isCompleted(player) ? 1 : 0; } /** 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..4c70f5a9031 --- /dev/null +++ b/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java @@ -0,0 +1,165 @@ +/*************************************************************************** + * 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. + * + * @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); + } + }; + } +} From f3a9fdacc3ca1da528e505d02e35a5ef0681cf9d Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Mon, 15 Apr 2024 23:35:40 -0700 Subject: [PATCH 05/17] Track completions of Chocolate for Elisabeth & Ice Cream for Annie quests --- .../server/entity/player/UpdateConverter.java | 10 +++ .../maps/quests/ChocolateForElisabeth.java | 39 ++++----- .../server/maps/quests/IcecreamForAnnie.java | 41 +++++----- .../quests/ChocolateForElisabethTest.java | 80 ++++++++++++++----- .../maps/quests/IcecreamForAnnieTest.java | 40 ++++++++-- 5 files changed, 144 insertions(+), 66 deletions(-) diff --git a/src/games/stendhal/server/entity/player/UpdateConverter.java b/src/games/stendhal/server/entity/player/UpdateConverter.java index e6b7c706711..3a3e15d735d 100644 --- a/src/games/stendhal/server/entity/player/UpdateConverter.java +++ b/src/games/stendhal/server/entity/player/UpdateConverter.java @@ -673,6 +673,16 @@ 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.47: support completions tracking + 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"); + } + } } 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/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/tests/games/stendhal/server/maps/quests/ChocolateForElisabethTest.java b/tests/games/stendhal/server/maps/quests/ChocolateForElisabethTest.java index aa07f259d2a..7b7dd6f9d48 100644 --- a/tests/games/stendhal/server/maps/quests/ChocolateForElisabethTest.java +++ b/tests/games/stendhal/server/maps/quests/ChocolateForElisabethTest.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2016 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -11,9 +11,12 @@ ***************************************************************************/ package games.stendhal.server.maps.quests; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static utilities.SpeakerNPCTestHelper.getReply; @@ -23,6 +26,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; @@ -106,7 +110,7 @@ public void testRefuseQuest() { en.step(player, "no"); assertEquals(LIZ_TALK_QUEST_REJECT, getReply(npc)); - assertEquals("rejected", player.getQuest(questSlot)); + assertEquals("rejected", player.getQuest(questSlot, 0)); assertFalse(npc.isTalking()); assertLoseKarma(5); assertHistory(HISTORY_DEFAULT, HISTORY_REJECTED); @@ -123,13 +127,13 @@ public void testAcceptQuest() { en.step(player, "yes"); assertEquals(LIZ_TALK_QUEST_ACCEPT, getReply(npc)); - assertEquals("start", player.getQuest(questSlot)); + assertEquals("start", player.getQuest(questSlot, 0)); assertHistory(HISTORY_DEFAULT, HISTORY_START); } @Test public void testAskForQuestAlreadyAccepted() { - player.setQuest(questSlot, "start"); + player.setQuest(questSlot, 0, "start"); String responseToGreeting = startTalkingToNpc(ELISABETH); assertEquals(LIZ_TALK_GREETING_WITHOUT_CHOCOLATE, responseToGreeting); @@ -137,23 +141,23 @@ public void testAskForQuestAlreadyAccepted() { en.step(player, "quest"); assertEquals(LIZ_TALK_QUEST_ALREADY_OFFERED, getReply(npc)); - assertEquals("start", player.getQuest(questSlot)); + assertEquals("start", player.getQuest(questSlot, 0)); assertHistory(HISTORY_DEFAULT, HISTORY_START); } @Test public void testFoundChocolate() { - player.setQuest(questSlot, "start"); + player.setQuest(questSlot, 0, "start"); equipWithItem(player, CHOCOLATE); - assertEquals("start", player.getQuest(questSlot)); + assertEquals("start", player.getQuest(questSlot, 0)); assertHistory(HISTORY_DEFAULT, HISTORY_START, HISTORY_GOT_CHOCOLATE); } @Test public void testBringChocolateBeforeTalkingToMum() { - player.setQuest(questSlot, "start"); + player.setQuest(questSlot, 0, "start"); equipWithItem(player, CHOCOLATE); @@ -161,18 +165,18 @@ public void testBringChocolateBeforeTalkingToMum() { assertEquals(LIZ_TALK_GREETING_WITH_CHOCOLATE_NOT_ALLOWED, responseToGreeting); assertTrue(player.isEquipped(CHOCOLATE)); - assertEquals("start", player.getQuest(questSlot)); + assertEquals("start", player.getQuest(questSlot, 0)); assertHistory(HISTORY_DEFAULT, HISTORY_START, HISTORY_GOT_CHOCOLATE); } @Test public void testTalkToMumAfterQuestStart() { - player.setQuest(questSlot, "start"); + player.setQuest(questSlot, 0, "start"); String responseToGreeting = startTalkingToNpc(CAREY); assertEquals(MUM_TALK_GREET_AND_APPROVE, responseToGreeting); - assertEquals("mummy", player.getQuest(questSlot)); + assertEquals("mummy", player.getQuest(questSlot, 0)); assertHistory(HISTORY_DEFAULT, HISTORY_START, HISTORY_MUM_APPROVES); } @@ -189,7 +193,8 @@ public void testTalkToMumBeforeQuestStart() { @Test public void testBringChocolateAfterTalkingToMum() { - player.setQuest(questSlot, "mummy"); + player.setQuest(questSlot, 0, "mummy"); + final double startKarma = player.getKarma(); equipWithItem(player, CHOCOLATE); @@ -201,14 +206,14 @@ public void testBringChocolateAfterTalkingToMum() { assertFalse(player.isEquipped(CHOCOLATE)); assertTrue(isEquippedWithFlower()); - assertGainKarma(10); + assertThat(player.getKarma(), is(closeTo(startKarma + 10, 0.01))); assertTrue(player.getQuest(questSlot).startsWith("eating;")); assertHistory(HISTORY_DEFAULT, HISTORY_START, HISTORY_GOT_CHOCOLATE, HISTORY_MUM_APPROVES, HISTORY_DONE); } @Test public void testRefuseToGiveChocolate() { - player.setQuest(questSlot, "mummy"); + player.setQuest(questSlot, 0, "mummy"); equipWithItem(player, CHOCOLATE); @@ -221,13 +226,13 @@ public void testRefuseToGiveChocolate() { assertTrue(player.isEquipped(CHOCOLATE)); assertFalse(isEquippedWithFlower()); assertLoseKarma(5); - assertEquals("mummy", player.getQuest(questSlot)); + assertEquals("mummy", player.getQuest(questSlot, 0)); assertHistory(HISTORY_DEFAULT, HISTORY_START, HISTORY_GOT_CHOCOLATE, HISTORY_MUM_APPROVES); } @Test public void testAskQuestAgain() { - player.setQuest(questSlot, "eating"); + player.setQuest(questSlot, 0, "eating"); PlayerTestHelper.setPastTime(player, questSlot, 1, TimeUnit.HOURS.toSeconds(1)); String responseToGreeting = startTalkingToNpc(ELISABETH); @@ -236,13 +241,13 @@ public void testAskQuestAgain() { en.step(player, "quest"); assertEquals(LIZ_TALK_QUEST_OFFER_AGAIN, getReply(npc)); - assertTrue(player.getQuest(questSlot).startsWith("eating")); + assertTrue(player.getQuest(questSlot).startsWith("eating;")); assertHistory(HISTORY_DEFAULT, HISTORY_START, HISTORY_GOT_CHOCOLATE, HISTORY_MUM_APPROVES, HISTORY_REPEATABLE); } @Test public void testAskQuestAgaintTooSoon() { - player.setQuest(questSlot, "eating"); + player.setQuest(questSlot, 0, "eating"); PlayerTestHelper.setPastTime(player, questSlot, 1, TimeUnit.MINUTES.toSeconds(30)); String responseToGreeting = startTalkingToNpc(ELISABETH); @@ -251,13 +256,13 @@ public void testAskQuestAgaintTooSoon() { en.step(player, "quest"); assertEquals(LIZ_TALK_QUEST_NOT_NOW, getReply(npc)); - assertTrue(player.getQuest(questSlot).startsWith("eating")); + assertTrue(player.getQuest(questSlot).startsWith("eating;")); assertHistory(HISTORY_DEFAULT, HISTORY_START, HISTORY_GOT_CHOCOLATE, HISTORY_MUM_APPROVES, HISTORY_DONE); } @Test public void testAskQuestAgainAndAccept() { - player.setQuest(questSlot, "eating"); + player.setQuest(questSlot, 0, "eating"); PlayerTestHelper.setPastTime(player, questSlot, 1, TimeUnit.HOURS.toSeconds(1)); String responseToGreeting = startTalkingToNpc(ELISABETH); @@ -268,7 +273,7 @@ public void testAskQuestAgainAndAccept() { en.step(player, "yes"); - assertEquals("start", player.getQuest(questSlot)); + assertEquals("start", player.getQuest(questSlot, 0)); assertHistory(HISTORY_DEFAULT, HISTORY_START); } @@ -288,4 +293,37 @@ private boolean isEquippedWithFlower() { } return false; } + + private void doQuest(final int completions) { + if (completions == 0) { + testAcceptQuest(); + } else { + testAskQuestAgainAndAccept(); + } + en.step(player, "bye"); + testBringChocolateAfterTalkingToMum(); + en.step(player, "bye"); + } + + @Test + public void testCompletions() { + for (int count = 0; count < 5; count++) { + assertThat(MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0), is(count)); + doQuest(count); + // 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/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"); + } } From 90b6e3804e70b45c01a5171c0d28ca42bdf540a8 Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Fri, 26 Apr 2024 01:14:48 -0700 Subject: [PATCH 06/17] Track completions of Zoo Food quest --- src/games/stendhal/server/maps/quests/ZooFood.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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(); From eec5458557cdf9a3b4d5eb28d0ef29a256d6babb Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Fri, 26 Apr 2024 02:00:55 -0700 Subject: [PATCH 07/17] Track completions of Kill Spiders quest --- .../server/maps/quests/KillSpiders.java | 39 +++++++++++-------- .../server/maps/quests/KillSpidersTest.java | 4 +- 2 files changed, 25 insertions(+), 18 deletions(-) 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/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)); From 8b538f4095e42b0f350df45ccc2b263f9b5514ba Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Tue, 28 May 2024 23:18:37 -0700 Subject: [PATCH 08/17] Track completions of Fish Soup for Hughie --- .../server/maps/quests/FishSoupForHughie.java | 30 ++++++----- .../maps/quests/FishSoupForHughieTest.java | 51 ++++++++++++++++--- 2 files changed, 60 insertions(+), 21 deletions(-) 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/tests/games/stendhal/server/maps/quests/FishSoupForHughieTest.java b/tests/games/stendhal/server/maps/quests/FishSoupForHughieTest.java index 96cd5efe27c..e569fb1dc88 100644 --- a/tests/games/stendhal/server/maps/quests/FishSoupForHughieTest.java +++ b/tests/games/stendhal/server/maps/quests/FishSoupForHughieTest.java @@ -24,6 +24,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; @@ -64,6 +65,7 @@ public void setUp() { questSlot = quest.getSlotName(); player = PlayerTestHelper.createPlayer("bob"); + assertNull(player.getQuest(questSlot)); } @After @@ -107,25 +109,37 @@ public void testQuest() { npc = SingletonRepository.getNPCList().get("Anastasia"); en = npc.getEngine(); - assertNull(player.getQuest(questSlot)); + final int completions = MathHelper.parseIntDefault(player.getQuest(questSlot, 1), 0); + String[] responses = new String[] { + "Hi, I really could do with a #favor, please.", + "My poor boy is sick and the potions I give him aren't working! Please could you fetch him some fish soup?" + }; + if (completions > 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"); + } } From 5e02faa2bd105efde03a64419a6b37ec9e0b7c4d Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Wed, 29 May 2024 00:40:59 -0700 Subject: [PATCH 09/17] Track completions of Fruits for Coralia --- .../server/maps/quests/FruitsForCoralia.java | 26 +++++---- .../maps/quests/FruitsForCoraliaTest.java | 55 +++++++++++++++---- 2 files changed, 60 insertions(+), 21 deletions(-) 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/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"); } } From 7608eacd76a7d0c3c02dc9a1d0d093c68e1edd9c Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Wed, 29 May 2024 02:15:33 -0700 Subject: [PATCH 10/17] Track completions of Kill Dhohr Nuggetcutter --- .../maps/quests/KillDhohrNuggetcutter.java | 12 +++-- .../quests/KillDhohrNuggetcutterTest.java | 47 +++++++++++++++++-- 2 files changed, 50 insertions(+), 9 deletions(-) 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/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"); + } } From 5639d739d59d5e57b48bde717ca7d73d83620fbb Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Wed, 29 May 2024 18:45:36 -0700 Subject: [PATCH 11/17] Track completions of Snowballs for Mr. Yeti --- .../server/maps/quests/Snowballs.java | 41 +++++++------- .../server/maps/quests/SnowballsTest.java | 55 +++++++++++++++++-- 2 files changed, 71 insertions(+), 25 deletions(-) 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/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"); + } } From a0ec062ad91cd7be7dc85b7a540d4c69d62511b0 Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Thu, 6 Jun 2024 12:53:39 -0700 Subject: [PATCH 12/17] Change Find Rat Children quest state string to format more... ...suited for completions tracking --- .../server/entity/npc/RatKidsNPCBase.java | 39 ++-- .../server/entity/player/UpdateConverter.java | 6 +- .../server/maps/quests/FindRatChildren.java | 203 ++++++++++++++---- .../maps/quests/FindRatChildrenTest.java | 53 ++++- 4 files changed, 224 insertions(+), 77 deletions(-) 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/UpdateConverter.java b/src/games/stendhal/server/entity/player/UpdateConverter.java index 3a3e15d735d..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; @@ -674,7 +675,7 @@ public static void updateQuests(final Player player) { - (TimeUtil.MINUTES_IN_HALF_YEAR / 2 * TimeUtil.MILLISECONDS_IN_MINUTE))); } - // 1.47: support completions tracking + // 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; @@ -683,6 +684,9 @@ public static void updateQuests(final Player player) { player.setQuest(slot, 2, "1"); } } + + // update Find Rat Children + FindRatChildren.checkPlayerUpdate(player); } diff --git a/src/games/stendhal/server/maps/quests/FindRatChildren.java b/src/games/stendhal/server/maps/quests/FindRatChildren.java index eafddd8adda..da80e0084e6 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; @@ -71,8 +71,6 @@ */ public class FindRatChildren extends AbstractQuest { - private static Logger logger = Logger.getLogger(FindRatChildren.class); - private static final String QUEST_SLOT = "find_rat_kids"; // twenty four hours @@ -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,8 @@ 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())); player.notifyWorldAboutChanges(); npc.setCurrentState(ConversationStates.ATTENDING); } diff --git a/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java b/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java index 0cc555c3f40..d26dcba19f6 100644 --- a/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java +++ b/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - 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.assertNull; import static org.junit.Assert.assertThat; @@ -129,7 +130,7 @@ public void testStartQuest() { assertEquals("Bye", getReply(npc)); // check quest slot - assertEquals(player.getQuest(QUEST_SLOT),"looking:said"); + assertThat(player.getQuest(QUEST_SLOT), is("found=;said=")); } @Test @@ -165,6 +166,8 @@ public void testMeetingCodyAfterQuestStarted() { // the state wasn't remembered across the new test method so we need to set it to what it was when we ended the last player.setQuest(QUEST_SLOT, "looking:said"); + FindRatChildren.checkPlayerUpdate(player); + assertThat(player.getQuest(QUEST_SLOT), is("found=;said=")); // remember the xp and karma, did it go up? final int xp = player.getXP(); @@ -173,16 +176,16 @@ public void testMeetingCodyAfterQuestStarted() { assertEquals("Hello my name is Cody. Please tell mother that I am ok.", getReply(npc)); // [11:49] kymara earns 500 experience points. - assertThat(player.getXP(), greaterThan(xp)); // check quest slot - assertEquals(player.getQuest(QUEST_SLOT),"looking;cody:said"); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody;said=")); + assertThat(player.getXP(), greaterThan(xp)); // return after having met in this quest run en.step(player, "hi"); assertEquals("Oh hello again.", getReply(npc)); // check quest slot - assertEquals(player.getQuest(QUEST_SLOT),"looking;cody:said"); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody;said=")); } @Test @@ -193,6 +196,8 @@ public void testNamingKidsMet() { // the state wasn't remembered across the new test method so we need to set it to what it was when we ended the last player.setQuest(QUEST_SLOT, "looking;cody:said"); + FindRatChildren.checkPlayerUpdate(player); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody;said=")); en.step(player, "hi"); assertEquals("If you found any of my #children, please tell me their name.", getReply(npc)); @@ -206,7 +211,7 @@ public void testNamingKidsMet() { assertEquals("No problem, come back later.", getReply(npc)); // check quest slot - assertEquals(player.getQuest(QUEST_SLOT),"looking;cody:said;cody"); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody;said=cody")); } @Test @@ -216,6 +221,8 @@ public void testMeetingRemainingKids() { // the state wasn't remembered across the new test method so we need to set it to what it was when we ended the last player.setQuest(QUEST_SLOT, "looking;cody:said;cody"); + FindRatChildren.checkPlayerUpdate(player); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody;said=cody")); en.step(player, "hi"); assertEquals("Hello my name is Mariel. Please tell mother that I am ok.", getReply(npc)); @@ -239,17 +246,19 @@ public void testMeetingRemainingKids() { // [11:50] kymara earns 500 experience points. // check quest slot - assertEquals(player.getQuest(QUEST_SLOT),"looking;cody;mariel;opal;avalon:said;cody"); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody,mariel,opal,avalon;said=cody")); } + @Test public void testNamingRemainingKids() { - npc = SingletonRepository.getNPCList().get("Agnus"); en = npc.getEngine(); // the state wasn't remembered across the new test method so we need to set it to what it was when we ended the last player.setQuest(QUEST_SLOT, "looking;cody;mariel;opal;avalon:said;cody"); + FindRatChildren.checkPlayerUpdate(player); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody,mariel,opal,avalon;said=cody")); en.step(player, "hi"); assertEquals("If you found any of my #children, please tell me their name.", getReply(npc)); @@ -259,7 +268,7 @@ public void testNamingRemainingKids() { assertEquals("No problem, come back later.", getReply(npc)); // check quest slot - assertEquals(player.getQuest(QUEST_SLOT),"looking;cody;mariel;opal;avalon:said;cody;mariel"); + assertThat(player.getQuest(QUEST_SLOT), is("found=cody,mariel,opal,avalon;said=cody,mariel")); // remember the xp and karma, did it go up? final int xp = player.getXP(); @@ -303,7 +312,7 @@ public void testReturningBeforeTimePassed() { assertEquals("Bye", getReply(npc)); } - @Test + @Test public void testReturningAfterTimePassed() { // [11:51] Admin kymara changed your state of the quest 'find_rat_kids' from 'done;1270205441630' to 'done;1' @@ -332,4 +341,28 @@ public void testReturningAfterTimePassed() { // [11:51] kymara earns 500 experience points. en.step(player, "bye"); } + + @Test + public void testUpdates() { + // 1.47-1.48 + String found = "looking;"; + String said = "said;"; + String foundResult = "found="; + String saidResult = "said="; + for (final String name: new String[] {"avalon", "cody", "mariel", "opal"}) { + if (!"avalon".equals(name)) { + found += ";"; + said += ";"; + foundResult += ","; + saidResult += ","; + } + found += name; + said += name; + foundResult += name; + saidResult += name; + player.setQuest(QUEST_SLOT, found + ":" + said); + FindRatChildren.checkPlayerUpdate(player); + assertThat(player.getQuest(QUEST_SLOT), is(foundResult + ";" + saidResult)); + } + } } From 15f0d09bb635eac6b7196a0f845e7ea5f900122b Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Thu, 6 Jun 2024 14:36:17 -0700 Subject: [PATCH 13/17] Track completions of Find Rat Children --- .../server/maps/quests/FindRatChildren.java | 5 +- .../maps/quests/FindRatChildrenTest.java | 83 +++++++++++++++---- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/games/stendhal/server/maps/quests/FindRatChildren.java b/src/games/stendhal/server/maps/quests/FindRatChildren.java index da80e0084e6..c17f81defd9 100644 --- a/src/games/stendhal/server/maps/quests/FindRatChildren.java +++ b/src/games/stendhal/server/maps/quests/FindRatChildren.java @@ -69,7 +69,7 @@ *
  • Once every 24 hours.
  • * */ -public class FindRatChildren extends AbstractQuest { +public class FindRatChildren extends CompletionsTrackingQuest { private static final String QUEST_SLOT = "find_rat_kids"; @@ -366,6 +366,7 @@ public void fire(final Player player, final Sentence sentence, final EventRaiser npc.say(reply); player.setQuest(QUEST_SLOT, 0, "done"); player.setQuest(QUEST_SLOT, 1, String.valueOf(System.currentTimeMillis())); + incrementCompletions(player); player.notifyWorldAboutChanges(); npc.setCurrentState(ConversationStates.ATTENDING); } @@ -406,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/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java b/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java index d26dcba19f6..fe8c70edb97 100644 --- a/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java +++ b/tests/games/stendhal/server/maps/quests/FindRatChildrenTest.java @@ -14,6 +14,8 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; @@ -25,8 +27,10 @@ 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.NPCList; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.fsm.Engine; import games.stendhal.server.entity.player.Player; @@ -98,23 +102,31 @@ public void testStartQuest() { npc = SingletonRepository.getNPCList().get("Agnus"); en = npc.getEngine(); + final int completions = MathHelper.parseIntDefault(player.getQuest(QUEST_SLOT, 2), 0); en.step(player, "hi"); - assertEquals("Hello there.", getReply(npc)); - en.step(player, "help"); - assertEquals("I have no help to offer you.", getReply(npc)); - en.step(player, "job"); - assertEquals("Leave it to my children to not check in once in a while.", getReply(npc)); - en.step(player, "task"); - assertEquals("I feel so worried. If I only knew my #children were safe I would feel better.", getReply(npc)); - en.step(player, "children"); - assertEquals("My children have gone to play in the sewers. They have been gone for a long time. Will you find them and see if they are ok?", getReply(npc)); - en.step(player, "no"); - assertEquals("Oh. Never mind. I'm sure someone else would be glad to help me.", getReply(npc)); - en.step(player, "bye"); + if (completions == 0) { + assertEquals("Hello there.", getReply(npc)); + en.step(player, "help"); + assertEquals("I have no help to offer you.", getReply(npc)); + en.step(player, "job"); + assertEquals("Leave it to my children to not check in once in a while.", getReply(npc)); + en.step(player, "task"); + assertEquals("I feel so worried. If I only knew my #children were safe I would feel better.", getReply(npc)); + en.step(player, "children"); + assertEquals("My children have gone to play in the sewers. They have been gone for a long time. Will you find them and see if they are ok?", getReply(npc)); + en.step(player, "no"); + assertEquals("Oh. Never mind. I'm sure someone else would be glad to help me.", getReply(npc)); + en.step(player, "bye"); + } else { + assertThat(getReply(npc), is("Do you think you could find my children again?")); + en.step(player, "no"); + assertThat(getReply(npc), is("Oh. Never mind. I'm sure someone else would be glad to help me.")); + en.step(player, "bye"); + } assertEquals("Bye", getReply(npc)); // check quest slot - assertEquals(player.getQuest(QUEST_SLOT),"rejected"); + assertThat(player.getQuest(QUEST_SLOT, 0), is("rejected")); en.step(player, "hi"); assertEquals("Hello there.", getReply(npc)); @@ -130,7 +142,7 @@ public void testStartQuest() { assertEquals("Bye", getReply(npc)); // check quest slot - assertThat(player.getQuest(QUEST_SLOT), is("found=;said=")); + assertThat(player.getQuest(QUEST_SLOT), startsWith("found=;said=")); } @Test @@ -365,4 +377,47 @@ public void testUpdates() { assertThat(player.getQuest(QUEST_SLOT), is(foundResult + ";" + saidResult)); } } + + private void finishQuestAfterStart() { + final NPCList npcs = NPCList.get(); + SpeakerNPC npc; + Engine en; + final String[] names = new String[] {"Avalon", "Cody", "Mariel", "Opal"}; + for (final String name: names) { + npc = npcs.get(name); + assertThat(npc, notNullValue()); + en = npc.getEngine(); + en.step(player, "hi"); + } + npc = npcs.get("Agnus"); + assertThat(npc, notNullValue()); + en = npc.getEngine(); + en.step(player, "hi"); + for (final String name: names) { + en.step(player, name); + } + en.step(player, "bye"); + assertThat(player.getQuest(QUEST_SLOT, 0), is("done")); + } + + @Test + public void testCompletions() { + for (int count = 0; count < 5; count++) { + assertThat(MathHelper.parseIntDefault(player.getQuest(QUEST_SLOT, 2), 0), is(count)); + testStartQuest(); + finishQuestAfterStart(); + // reset so can be repeated + player.setQuest(QUEST_SLOT, 1, "0"); + } + assertEquals("5", player.getQuest(QUEST_SLOT, 2)); + + // check that completions count is retained after quest is rejected & started + en.step(player, "hi"); + en.step(player, "no"); + assertThat(player.getQuest(QUEST_SLOT), is("rejected;0;5")); + en.step(player, "task"); + en.step(player, "yes"); + assertThat(player.getQuest(QUEST_SLOT), is("found=;said=;5")); + en.step(player, "bye"); + } } From 2803c82faafcafc3b666d08d2ab5bc24dd44e7f3 Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Thu, 6 Jun 2024 22:04:31 -0700 Subject: [PATCH 14/17] Track completions of The Pied Piper --- .../server/maps/quests/ThePiedPiper.java | 6 +-- .../maps/quests/piedpiper/InvasionPhase.java | 25 ++++++++-- .../quests/piedpiper/RewardPlayerAction.java | 26 +++++++++- .../piedpiper/TPPQuestHelperFunctions.java | 16 +++++++ .../server/maps/quests/ThePiedPiperTest.java | 33 ++++++++++++- .../thepiedpiper/InvasionPhaseTest.java | 47 +++++++++++++++---- .../quests/thepiedpiper/TPPTestHelper.java | 20 +++++++- 7 files changed, 153 insertions(+), 20 deletions(-) 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/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 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 Date: Thu, 25 Jul 2024 03:14:56 -0700 Subject: [PATCH 15/17] Add FIXME note --- .../stendhal/server/maps/quests/CompletionsTrackingQuest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java b/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java index 4c70f5a9031..e4190093e5e 100644 --- a/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java +++ b/src/games/stendhal/server/maps/quests/CompletionsTrackingQuest.java @@ -28,6 +28,8 @@ 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 From e209b2886f74dbb5366afeef518d639247df176f Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Thu, 25 Jul 2024 03:17:30 -0700 Subject: [PATCH 16/17] Convert quests to CompletionsTrackingQuest --- .../server/maps/quests/DailyItemQuest.java | 4 ++-- .../server/maps/quests/DailyMonsterQuest.java | 6 +++--- .../stendhal/server/maps/quests/ElfPrincess.java | 4 ++-- .../stendhal/server/maps/quests/HelpTomi.java | 14 ++++++-------- .../stendhal/server/maps/quests/KillEnemyArmy.java | 4 ++-- .../stendhal/server/maps/quests/KillMonks.java | 4 ++-- src/games/stendhal/server/maps/quests/Maze.java | 4 ++-- .../server/maps/quests/WeeklyItemQuest.java | 4 ++-- 8 files changed, 21 insertions(+), 23 deletions(-) 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/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/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/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/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."); From dc140c21e380e18a80fde43e0d89dcdabbbfa6fd Mon Sep 17 00:00:00 2001 From: Jordan Irwin Date: Thu, 25 Jul 2024 03:18:36 -0700 Subject: [PATCH 17/17] Delete unused getNumberOfRepetitions methods --- .../stendhal/server/entity/player/Player.java | 14 ------------- .../server/entity/player/PlayerQuests.java | 21 +------------------ 2 files changed, 1 insertion(+), 34 deletions(-) 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); - } - }