diff --git a/doc/CHANGES.txt b/doc/CHANGES.txt index bac0f249a45..4b3f9ff3264 100644 --- a/doc/CHANGES.txt +++ b/doc/CHANGES.txt @@ -8,6 +8,7 @@ Changelog - renamed chef Patrick to Preston - new achievements: - 30 Minutes or Less: Deliver 25 hot pizzas +- Balduin allows requesting different item for Ultimate Collector quest *web client* - fixed text extending past edge of notification bubbles diff --git a/src/games/stendhal/server/entity/player/UpdateConverter.java b/src/games/stendhal/server/entity/player/UpdateConverter.java index bc704299597..e6b7c706711 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.util.TimeUtil; import marauroa.common.Pair; import marauroa.common.game.RPObject; import marauroa.common.game.RPSlot; @@ -662,6 +663,16 @@ public static void updateQuests(final Player player) { if (player.hasQuest(questSlot) && !slotState.startsWith(";")) { player.setQuest(questSlot, ";" + slotState); } + + // 1.47: Ultimate Collector supports aborting & requesting another item after 6 months + questSlot = "ultimate_collector"; + if (player.hasQuest(questSlot) && !"done".equals(player.getQuest(questSlot, 0)) + && "".equals(player.getQuest(questSlot, 1))) { + // since we don't know when quest was started assume that at least half the required time (3 + // months) have passed + player.setQuest(questSlot, 1, Long.toString(System.currentTimeMillis() + - (TimeUtil.MINUTES_IN_HALF_YEAR / 2 * TimeUtil.MILLISECONDS_IN_MINUTE))); + } } diff --git a/src/games/stendhal/server/maps/quests/UltimateCollector.java b/src/games/stendhal/server/maps/quests/UltimateCollector.java index f4b1dbaa0cc..63124a7d7fb 100644 --- a/src/games/stendhal/server/maps/quests/UltimateCollector.java +++ b/src/games/stendhal/server/maps/quests/UltimateCollector.java @@ -36,6 +36,7 @@ import games.stendhal.server.entity.npc.action.MultipleActions; import games.stendhal.server.entity.npc.action.SayRequiredItemAction; import games.stendhal.server.entity.npc.action.SetQuestAction; +import games.stendhal.server.entity.npc.action.SetQuestToTimeStampAction; import games.stendhal.server.entity.npc.action.StartRecordingRandomItemCollectionAction; import games.stendhal.server.entity.npc.condition.AndCondition; import games.stendhal.server.entity.npc.condition.GreetingMatchesNameCondition; @@ -47,9 +48,11 @@ import games.stendhal.server.entity.npc.condition.QuestCompletedCondition; import games.stendhal.server.entity.npc.condition.QuestNotCompletedCondition; import games.stendhal.server.entity.npc.condition.QuestNotStartedCondition; +import games.stendhal.server.entity.npc.condition.TimePassedCondition; import games.stendhal.server.entity.player.Player; import games.stendhal.server.events.SoundEvent; import games.stendhal.server.maps.Region; +import games.stendhal.server.util.TimeUtil; /** @@ -214,11 +217,16 @@ private void checkCollectingQuests() { } - private void requestItem() { - - final SpeakerNPC npc = npcs.get("Balduin"); - final Map items = new HashMap(); - + /** + * Determines items to select from. + * + * @param exclude + * An item name to exclude from returned value or {@code null} to include all. Used to prevent + * re-requesting same item when player asks for "another". + * @return + * Items that may be requested from player. + */ + private Map getItems(final String exclude) { /* Updated 2022-05-22 * * Rarity calculations (lower means more rare): @@ -231,20 +239,28 @@ private void requestItem() { * Items given as rewards from quests or otherwise acquirable via * methods other than creature drops should not be included. */ + final Map items = new HashMap<>(); + items.put("nihonto", 1); // 1.39 + items.put("magic twoside axe", 1); // 1.72 + items.put("imperator sword", 1); // 0.33 + items.put("durin axe", 1); // 0.39 + items.put("vulcano hammer", 1); // 0.18 + items.put("xeno sword", 1); // 0.67 + items.put("black scythe", 1); // 0.09 + items.put("chaos dagger", 1); // 2.0 + items.put("black sword", 1); // 0.15 + items.put("golden orc sword", 1); // 0.09 + items.put("ice war hammer", 1); // 0.15 + items.put("orcish sword", 1); // 0.86 + items.put("black halberd", 1); // 0.12 + if (exclude != null) { + items.remove(exclude); + } + return items; + } - items.put("nihonto",1); // 1.39 - items.put("magic twoside axe",1); // 1.72 - items.put("imperator sword",1); // 0.33 - items.put("durin axe",1); // 0.39 - items.put("vulcano hammer",1); // 0.18 - items.put("xeno sword",1); // 0.67 - items.put("black scythe",1); // 0.09 - items.put("chaos dagger",1); // 2.0 - items.put("black sword",1); // 0.15 - items.put("golden orc sword",1); // 0.09 - items.put("ice war hammer",1); // 0.15 - items.put("orcish sword",1); // 0.86 - items.put("black halberd",1); // 0.12 + private void requestItem() { + final SpeakerNPC npc = npcs.get("Balduin"); // If all quests are completed, ask for an item npc.add(ConversationStates.ATTENDING, @@ -264,8 +280,11 @@ private void requestItem() { new QuestCompletedCondition(IMMORTAL_SWORD_QUEST_SLOT)), ConversationStates.ATTENDING, null, - new StartRecordingRandomItemCollectionAction(QUEST_SLOT, items, "Well, you've certainly proved to the residents of Faiumoni " + - "that you could be the ultimate collector, but I have one more task for you. Please bring me [item].")); + new MultipleActions( + new StartRecordingRandomItemCollectionAction(QUEST_SLOT, 0, getItems(null), + "Well, you've certainly proved to the residents of Faiumoni that you could be the" + + " ultimate collector, but I have one more task for you. Please bring me [item]."), + new SetQuestToTimeStampAction(QUEST_SLOT, 1))); } private void collectItem() { @@ -329,6 +348,58 @@ private void offerSteps() { "I'll buy black items from you when you have completed each #challenge I set you.", null); } + private void abortQuest() { + final SpeakerNPC npc = npcs.get("Balduin"); + // approximately 6 months + final int expireTime = TimeUtil.MINUTES_IN_HALF_YEAR; + + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.ABORT_MESSAGES, + new AndCondition( + new QuestActiveCondition(QUEST_SLOT), + new NotCondition(new TimePassedCondition(QUEST_SLOT, 1, expireTime)) + ), + ConversationStates.ATTENDING, + null, + new SayRequiredItemAction(QUEST_SLOT, 0, "You are on a quest to find [item]. You cannot" + + " request a new item yet.")); + + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.ABORT_MESSAGES, + new AndCondition( + new QuestActiveCondition(QUEST_SLOT), + new TimePassedCondition(QUEST_SLOT, 1, expireTime) + ), + ConversationStates.QUEST_OFFERED, + null, + new SayRequiredItemAction(QUEST_SLOT, 0, "You are on a quest to find [item]. Would you like" + + " to look for a different item?")); + + npc.add( + ConversationStates.QUEST_OFFERED, + ConversationPhrases.YES_MESSAGES, + new AndCondition( + new QuestActiveCondition(QUEST_SLOT), + new TimePassedCondition(QUEST_SLOT, 1, expireTime) + ), + ConversationStates.ATTENDING, + null, + new RequestAnotherAction()); + + npc.add( + ConversationStates.QUEST_OFFERED, + ConversationPhrases.NO_MESSAGES, + new AndCondition( + new QuestActiveCondition(QUEST_SLOT), + new TimePassedCondition(QUEST_SLOT, 1, expireTime) + ), + ConversationStates.ATTENDING, + null, + new SayRequiredItemAction(QUEST_SLOT, 0, "Then bring me [item].")); + } + private void replaceLRSwords() { final SpeakerNPC npc = npcs.get("Balduin"); final Map prices = SingletonRepository.getShopsList().get("twohandswords"); @@ -468,6 +539,7 @@ public void addToWorld() { requestItem(); collectItem(); offerSteps(); + abortQuest(); replaceLRSwords(); } @@ -491,4 +563,16 @@ public String getNPCName() { public String getRegion() { return Region.ADOS_SURROUNDS; } + + + private class RequestAnotherAction implements ChatAction { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + final String previousItem = player.getQuest(QUEST_SLOT, 0).split("=")[0]; + new StartRecordingRandomItemCollectionAction(QUEST_SLOT, 0, getItems(previousItem), + "Perhaps finding " + Grammar.a_noun(previousItem) + " proved to be too difficult for you." + + " This time, I want you to find [item].").fire(player, sentence, npc); + new SetQuestToTimeStampAction(QUEST_SLOT, 1).fire(player, sentence, npc); + } + } } diff --git a/tests/games/stendhal/server/maps/quests/UltimateCollectorTest.java b/tests/games/stendhal/server/maps/quests/UltimateCollectorTest.java index d60c0d0dc96..b07e77effaa 100644 --- a/tests/games/stendhal/server/maps/quests/UltimateCollectorTest.java +++ b/tests/games/stendhal/server/maps/quests/UltimateCollectorTest.java @@ -14,7 +14,9 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.oneOf; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -22,11 +24,13 @@ import static org.junit.Assert.assertTrue; import static utilities.SpeakerNPCTestHelper.getReply; +import org.apache.log4j.Logger; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import games.stendhal.common.grammar.Grammar; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.entity.item.Item; @@ -35,11 +39,14 @@ import games.stendhal.server.entity.npc.fsm.Engine; import games.stendhal.server.entity.player.Player; import games.stendhal.server.maps.ados.rock.WeaponsCollectorNPC; +import games.stendhal.server.util.TimeUtil; import utilities.PlayerTestHelper; import utilities.QuestHelper; public class UltimateCollectorTest { + private static final Logger logger = Logger.getLogger(UltimateCollectorTest.class); + private Player player = null; private SpeakerNPC npc = null; private Engine en = null; @@ -66,25 +73,36 @@ public void tearDown() throws Exception { SingletonRepository.getNPCList().remove("Balduin"); } - @Test - public void testQuest() { - - - - npc = SingletonRepository.getNPCList().get("Balduin"); - en = npc.getEngine(); - // ----------------------------------------------- - + private void initPrerequisites(boolean all) { // [22:23] Admin kymara changed your state of the quest 'weapons_collector' from '' to 'done' // [22:23] Changed the state of quest 'weapons_collector' from '' to 'done' - player.setQuest("weapons_collector", "done"); - // [22:23] Admin kymara changed your state of the quest 'weapons_collector2' from 'null' to 'done' // [22:23] Changed the state of quest 'weapons_collector2' from 'null' to 'done' - player.setQuest("weapons_collector2", "done"); + if (all) { + player.setQuest("mithril_cloak", "done"); + player.setQuest("mithrilshield_quest", "done"); + player.setQuest("immortalsword_quest", "done"); + player.setQuest("club_thorns", "done"); + player.setQuest("soldier_henry", "done"); + player.setQuest("cloaks_collector_2", "done"); + player.setQuest("cloaks_for_bario", "done"); + player.setQuest("elvish_armor", "done"); + player.setQuest("obsidian_knife", "done"); + player.setQuest("vs_quest", "done"); + } + } + + @Test + public void testQuest() { + npc = SingletonRepository.getNPCList().get("Balduin"); + en = npc.getEngine(); + // ----------------------------------------------- + + initPrerequisites(false); + en.step(player, "hi"); assertEquals("Greetings old friend. I have another collecting #challenge for you.", getReply(npc)); en.step(player, "challenge"); @@ -363,4 +381,80 @@ private void testReplaceSwords() { npc.getName(), false)); } + + @Test + public void testAbort() { + npc = SingletonRepository.getNPCList().get("Balduin"); + en = npc.getEngine(); + final String questSlot = "ultimate_collector"; + + initPrerequisites(true); + + assertThat(player.hasQuest(questSlot), is(false)); + + assertThat(en.step(player, "hi"), is(true)); + assertThat(getReply(npc), + is("Greetings old friend. I have another collecting #challenge for you.")); + assertThat(en.step(player, "challenge"), is(true)); + assertThat(getReply(npc), + startsWith("Well, you've certainly proved to the residents of Faiumoni that you could be" + + " the ultimate collector, but I have one more task for you. Please bring me ")); + + assertThat(player.hasQuest(questSlot), is(true)); + final String initialItem = player.getQuest(questSlot, 0).split("=")[0]; + logger.info("initial item: " + initialItem); + + final long startTime = Long.parseLong(player.getQuest(questSlot, 1)); + final long msInHalfYear = TimeUtil.MINUTES_IN_HALF_YEAR * TimeUtil.MILLISECONDS_IN_MINUTE; + + // no time has passed + assertThat(en.step(player, "another"), is(true)); + assertThat(getReply(npc), is("You are on a quest to find " + Grammar.a_noun(initialItem) + + ". You cannot request a new item yet.")); + + // approx. 3 months have passed (2,190 hours) + player.setQuest(questSlot, 1, String.valueOf(startTime - (msInHalfYear / 2))); + assertThat(en.step(player, "another"), is(true)); + assertThat(getReply(npc), is("You are on a quest to find " + Grammar.a_noun(initialItem) + + ". You cannot request a new item yet.")); + + // 26 weeks have passed (4,368 hours, approx half year) + player.setQuest(questSlot, 1, String.valueOf(startTime - (TimeUtil.MILLISECONDS_IN_WEEK * 26))); + assertThat(en.step(player, "another"), is(true)); + assertThat(getReply(npc), is("You are on a quest to find " + Grammar.a_noun(initialItem) + + ". You cannot request a new item yet.")); + + // half year has passed (4,380 hours) + player.setQuest(questSlot, 1, String.valueOf(startTime - msInHalfYear)); + assertThat(en.step(player, "another"), is(true)); + assertThat(getReply(npc), is("You are on a quest to find " + Grammar.a_noun(initialItem) + + ". Would you like to look for a different item?")); + // doesn't want a new item + assertThat(en.step(player, "no"), is(true)); + assertThat(getReply(npc), is("Then bring me " + Grammar.a_noun(initialItem) + ".")); + + // requested item should not have changed + assertThat(player.getQuest(questSlot, 0).split("=")[0], is(initialItem)); + + // ask again + assertThat(en.step(player, "another"), is(true)); + assertThat(getReply(npc), is("You are on a quest to find " + Grammar.a_noun(initialItem) + + ". Would you like to look for a different item?")); + // does want a new item + assertThat(en.step(player, "yes"), is(true)); + + final String newItem = player.getQuest(questSlot, 0).split("=")[0]; + + assertThat(getReply(npc), is("Perhaps finding " + Grammar.a_noun(initialItem) + + " proved to be too difficult for you. This time, I want you to find " + + Grammar.a_noun(newItem) + ".")); + + logger.info("new item: " + newItem + " != " + initialItem); + assertThat(newItem, not(initialItem)); + + // ask for another immediately after receiving new quest + assertThat(en.step(player, "another"), is(true)); + assertThat(getReply(npc), is("You are on a quest to find " + Grammar.a_noun(newItem) + + ". You cannot request a new item yet.")); + } }