/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.client.gui.controller.game; import de.lessvoid.nifty.Nifty; import de.lessvoid.nifty.NiftyEventSubscriber; import de.lessvoid.nifty.builder.ElementBuilder.Align; import de.lessvoid.nifty.controls.*; import de.lessvoid.nifty.controls.label.builder.LabelBuilder; import de.lessvoid.nifty.effects.EffectEventId; import de.lessvoid.nifty.elements.Element; import de.lessvoid.nifty.screen.Screen; import de.lessvoid.nifty.screen.ScreenController; import de.lessvoid.nifty.tools.SizeValue; import illarion.client.IllaClient; import illarion.client.graphics.FontLoader; import illarion.client.gui.QuestGui; import illarion.client.world.World; import illarion.common.types.ServerCoordinate; import org.intellij.lang.annotations.Flow; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * This class takes care for managing the quest log. * * @author Martin Karing <nitram@illarion.org> */ public final class QuestHandler implements QuestGui, ScreenController { /** * This is a single quest that is shown in the quest log. */ private static final class QuestEntry implements Comparable<QuestEntry> { /** * The ID of the quest. */ private final int questId; /** * The displayed name of the quest. */ @Nonnull private final String name; /** * The description of the current quest state. */ @Nonnull private final String description; /** * The flag if this quest is now finished or not. */ @Nonnull private final boolean finished; /** * The list of valid target locations for this quest. */ @Nonnull private final List<ServerCoordinate> targetLocations; /** * The constructor of the quest. * * @param questId the ID of the quest * @param name the name of the quest * @param description the description of the quest state * @param finished {@code true} in case the quest is finished * @param locations the valid target locations */ QuestEntry(int questId, @Nonnull String name, @Nonnull String description, boolean finished, @Nonnull @Flow(targetIsContainer = true, sourceIsContainer = true) List<ServerCoordinate> locations) { this.questId = questId; this.name = name; this.description = description; this.finished = finished; //noinspection AssignmentToCollectionOrArrayFieldFromParameter targetLocations = locations; } @Override public int compareTo(@Nonnull QuestEntry o) { if (o.finished && !finished) { return -1; } if (!o.finished && finished) { return 1; } return name.compareTo(o.name); } @Override @Contract(value = "null -> false", pure = true) public boolean equals(Object obj) { return super.equals(obj) || ((obj instanceof QuestEntry) && (questId == ((QuestEntry) obj).questId)); } @Override @Contract(pure = true) public int hashCode() { return questId; } @Nonnull @Override @Contract(pure = true) public String toString() { return name; } /** * Get the description of the quest. * * @return the quest description */ @Nonnull @Contract(pure = true) public String getDescription() { return description; } /** * Get the name of the quest. * * @return the name of the quest */ @Nonnull @Contract(pure = true) public String getName() { return name; } /** * Get the ID of the quest. * * @return the ID of the quest */ @Contract(pure = true) public int getQuestId() { return questId; } /** * Check if the quest is finished. * * @return {@code true} in case the quest is finished */ @Contract(pure = true) public boolean isFinished() { return finished; } /** * Get the amount of target locations assigned to this quest entry. * * @return the target locations */ @Contract(pure = true) public int getTargetLocationCount() { return targetLocations.size(); } /** * Get the target location that is assigned to the index. * * @param index the index of the target location * @return the assigned target location * @throws ArrayIndexOutOfBoundsException in case index is {@code < 0} or greater then or equal to the count */ @Nonnull @Contract(pure = true) public ServerCoordinate getTargetLocation(int index) { return targetLocations.get(index); } } /** * The logging instance of this class. */ @Nonnull private static final Logger LOGGER = LoggerFactory.getLogger(QuestHandler.class); /** * The window that shows the quest log. */ @Nullable private Window questWindow; /** * This is the list of quests that are currently not shown in the GUI. */ @Nonnull private final List<QuestEntry> hiddenList; /** * In case this is {@code true} even the finished quests are shown. */ private boolean showFinishedQuests; /** * The reference to the Nifty instance this handler is bound to. */ @Nullable private Nifty nifty; /** * The reference to the screen this handler is bound to. */ @Nullable private Screen screen; /** * This value is turned true once the login sequence is done. */ private boolean loginDone; /** * Default constructor. */ public QuestHandler() { hiddenList = new ArrayList<>(); } @Nullable private Window getQuestWindow() { if (screen == null) { LOGGER.warn("Can't fetch the quest window as long as the quest handler is not bound to a screen."); return null; } if (questWindow == null) { questWindow = screen.findNiftyControl("questLog", Window.class); } if (questWindow == null) { LOGGER.error("Fetching the quest window failed. Seems its not yet created."); } return questWindow; } @Nullable private Element getQuestWindowElement() { Window questWindow = getQuestWindow(); if (questWindow == null) { return null; } return questWindow.getElement(); } @Override public boolean isQuestLogVisible() { Element questWindow = getQuestWindowElement(); return (questWindow != null) && questWindow.isVisible(); } @Override public void hideQuestLog() { Window questWindow = getQuestWindow(); if (questWindow != null) { questWindow.closeWindow(); } } @Override public void showQuestLog() { Element questWindowElement = getQuestWindowElement(); if (questWindowElement == null) { LOGGER.error("Showing the quest log failed. The required GUI element can't be located."); } else { questWindowElement.show(() -> { Window questWindow1 = getQuestWindow(); if (questWindow1 != null) { questWindow1.moveToFront(); } }); } } /** * The event subscriber for click events on the quest button. * * @param topic the event topic * @param data the event data */ @NiftyEventSubscriber(id = "openQuestBtn") public void onQuestLogButtonClicked(String topic, ButtonClickedEvent data) { if (isQuestLogVisible()) { hideQuestLog(); } else { showQuestLog(); } } /** * Event Handler for change events of the selection int he quest list box. * * @param topic the event topic * @param event the event data */ @NiftyEventSubscriber(id = "questLog#questList") public void onSelectedQuestChanged( @Nonnull String topic, @Nonnull ListBoxSelectionChangedEvent<QuestEntry> event) { Element descriptionArea = getDescriptionArea(); if (descriptionArea != null) { descriptionArea.hide(this::updateDisplayedQuest); } } @Override public void updateAllQuests() { World.getUpdateTaskManager().addTask((container, delta) -> updateAllQuestsInternal()); } private void updateAllQuestsInternal() { ListBox<QuestEntry> questList = getQuestList(); if (questList == null) { return; } List<QuestEntry> selectedEntries = questList.getItems(); selectedEntries.forEach(QuestHandler::updateQuest); } private static void updateQuest(@Nonnull QuestEntry quest) { Collection<ServerCoordinate> locationList = new ArrayList<>(quest.getTargetLocationCount()); for (int i = 0; i < quest.getTargetLocationCount(); i++) { ServerCoordinate target = quest.getTargetLocation(i); locationList.add(target); } World.getMap().removeQuestMarkers(locationList); World.getMap().applyQuestTargetLocations(locationList); } /** * Update the quest that is currently displayed in the dialog. */ private void updateDisplayedQuest() { if ((nifty == null) || (screen == null)) { LOGGER.error("Can't update the quest display as long as the handler is not bound."); return; } Element descriptionArea = getDescriptionArea(); if (descriptionArea == null) { LOGGER.error("Can't update displayed quest. Description area not found."); return; } descriptionArea.getChildren().forEach(Element::markForRemoval); QuestEntry selectedEntry = getSelectedQuest(); if (selectedEntry == null) { return; } LabelBuilder titleLabel = new LabelBuilder(); titleLabel.label(selectedEntry.getName()); titleLabel.font(FontLoader.MENU_FONT); titleLabel.marginLeft("5px"); titleLabel.marginRight("5px"); titleLabel.marginBottom("10px"); titleLabel.width((descriptionArea.getWidth() - 10) + "px"); titleLabel.wrap(true); titleLabel.build(nifty, screen, descriptionArea); if (!selectedEntry.getDescription().isEmpty()) { LabelBuilder descriptionLabel = new LabelBuilder(); descriptionLabel.label(selectedEntry.getDescription()); descriptionLabel.font(FontLoader.TEXT_FONT); descriptionLabel.marginLeft("5px"); descriptionLabel.marginRight("5px"); descriptionLabel.width((descriptionArea.getWidth() - 10) + "px"); descriptionLabel.textHAlign(Align.Left); descriptionLabel.wrap(true); descriptionLabel.build(nifty, screen, descriptionArea); } if (selectedEntry.isFinished()) { LabelBuilder finishedLabel = new LabelBuilder(); finishedLabel.label("${gamescreen-bundle.questFinished}"); finishedLabel.font(FontLoader.TEXT_FONT); finishedLabel.marginLeft("5px"); finishedLabel.marginRight("5px"); finishedLabel.marginTop("15px"); finishedLabel.width((descriptionArea.getWidth() - 10) + "px"); finishedLabel.textHAlign(Align.Center); finishedLabel.wrap(true); finishedLabel.build(nifty, screen, descriptionArea); } updateQuest(selectedEntry); descriptionArea.show(); } /** * Get the GUI area that contains the description. * * @return the element of the description area */ @Nullable private Element getDescriptionArea() { Element questWindow = getQuestWindowElement(); if (questWindow == null) { return null; } return questWindow.findElementById("#questDescription"); } /** * Fetch the quest that is currently selected. * * @return the currently selected quest or {@code null} in case no quest is selected */ @Nullable private QuestEntry getSelectedQuest() { ListBox<QuestEntry> questList = getQuestList(); if (questList == null) { return null; } List<QuestEntry> selectedEntries = questList.getSelection(); if (selectedEntries.isEmpty()) { return null; } return selectedEntries.get(0); } /** * Event Handler for change events of the "Show finished quests" checkbox. * * @param topic the event topic * @param event the event data */ @NiftyEventSubscriber(id = "questLog#showFinishedCheckbox") public void onShowFinishedChange(@Nonnull String topic, @Nonnull CheckBoxStateChangedEvent event) { IllaClient.getCfg().set("questShowFinished", event.isChecked()); showFinishedQuests = event.isChecked(); if (showFinishedQuests) { hiddenList.forEach(this::insertToGuiList); hiddenList.clear(); } else { ListBox<QuestEntry> questList = getQuestList(); if (questList != null) { questList.getItems().stream().filter(visibleEntry -> visibleEntry.isFinished() && !hiddenList.contains(visibleEntry)).forEach(hiddenList::add); } getQuestList().removeAllItems(hiddenList); } } /** * This function is used to insert a quest into the GUI list. This takes are to apply the required order to the * quest. * * @param entry the entry to add */ private void insertToGuiList(@Nonnull QuestEntry entry) { ListBox<QuestEntry> guiList = getQuestList(); if (guiList == null) { LOGGER.error("Updating the GUI list failed. GUI element not located."); return; } List<QuestEntry> questEntries = guiList.getItems(); int currentStart = 0; int currentEnd = questEntries.size() - 1; while (currentStart <= currentEnd) { int middle = currentStart + ((currentEnd - currentStart) >> 1); QuestEntry foundItem = questEntries.get(middle); int compareResult = foundItem.compareTo(entry); if (compareResult < 0) { currentStart = middle + 1; } else if (compareResult > 0) { currentEnd = middle - 1; } else { guiList.insertItem(entry, middle); return; } } guiList.insertItem(entry, currentStart); updateQuest(entry); } /** * Fetch the reference to the quest list. * * @return the quest list */ @Nullable private ListBox<QuestEntry> getQuestList() { Element questWindow = getQuestWindowElement(); if (questWindow == null) { return null; } //noinspection unchecked return (ListBox<QuestEntry>) questWindow.findNiftyControl("#questList", ListBox.class); } @Override public void bind(@Nonnull Nifty nifty, @Nonnull Screen screen) { this.nifty = nifty; this.screen = screen; } @Override public void onEndScreen() { if (nifty != null) { nifty.unsubscribeAnnotations(this); } Element questWindow = getQuestWindowElement(); if (questWindow != null) { IllaClient.getCfg().set("questWindowPosX", questWindow.getX() + "px"); IllaClient.getCfg().set("questWindowPosY", questWindow.getY() + "px"); } hideQuestLog(); ListBox<QuestEntry> questList = getQuestList(); if (questList != null) { questList.clear(); } } @Override public void onStartScreen() { if ((nifty == null) || (screen == null)) { LOGGER.error("Quest handler is not bound. Can't properly launch."); } nifty.subscribeAnnotations(this); Element questWindowElement = getQuestWindowElement(); if (questWindowElement != null) { questWindowElement.setConstraintX(new SizeValue(IllaClient.getCfg().getString("questWindowPosX"))); questWindowElement.setConstraintY(new SizeValue(IllaClient.getCfg().getString("questWindowPosY"))); CheckBox showFinished = questWindowElement.findNiftyControl("#showFinishedCheckbox", CheckBox.class); if (showFinished != null) { showFinished.setChecked(IllaClient.getCfg().getBoolean("questShowFinished")); showFinishedQuests = showFinished.isChecked(); } } } @Override public void removeQuest(int questId) { World.getUpdateTaskManager().addTask((container, delta) -> removeQuestInternal(questId)); } /** * Remove a quest from the quest list. * * @param questId the ID of the quest */ private void removeQuestInternal(int questId) { for (QuestEntry entry : hiddenList) { if (entry.getQuestId() == questId) { hiddenList.remove(entry); return; } } ListBox<QuestEntry> questList = getQuestList(); if (questList != null) { for (QuestEntry entry : questList.getItems()) { if (entry.getQuestId() == questId) { questList.removeItem(entry); return; } } } } @Override public void setDisplayedQuest(int questId) { ListBox<QuestEntry> guiList = getQuestList(); if (guiList != null) { guiList.getItems().stream().filter(guiListEntry -> guiListEntry.getQuestId() == questId).forEach(guiList::selectItem); } } @Override public void setQuest( int questId, @Nonnull String name, @Nonnull String description, boolean finished, @Nonnull List<ServerCoordinate> locations) { World.getUpdateTaskManager().addTask((container, delta) -> setQuestInternal(questId, name, description, finished, locations)); } @Override public void toggleQuestLog() { if (isQuestLogVisible()) { hideQuestLog(); } else { showQuestLog(); } } /** * The internal method to set the quest. This needs to be called during the update call before Nifty itself is * updated. * * @param questId the ID of the quest * @param name the name of the quest * @param description the current description of the quest * @param finished {@code true} if the quest is finished * @param locations the valid target locations */ private void setQuestInternal( int questId, @Nonnull String name, @Nonnull String description, boolean finished, @Nonnull List<ServerCoordinate> locations) { QuestEntry oldEntry = findQuest(questId); if (finished && (oldEntry != null)) { Collection<ServerCoordinate> locationList = new ArrayList<>(oldEntry.getTargetLocationCount()); for (int i = 0; i < oldEntry.getTargetLocationCount(); i++) { ServerCoordinate target = oldEntry.getTargetLocation(i); locationList.add(target); } World.getMap().removeQuestMarkers(locationList); } if (oldEntry != null) { ListBox<QuestEntry> questList = getQuestList(); if (questList != null) { questList.removeItem(oldEntry); } } QuestEntry newEntry = new QuestEntry(questId, name, description, finished, locations); if (!finished || showFinishedQuests) { insertToGuiList(newEntry); pulseQuestButton(); } else { hiddenList.add(newEntry); } QuestEntry selectedEntry = getSelectedQuest(); if (selectedEntry == null) { return; } if (selectedEntry.getQuestId() == questId) { updateDisplayedQuest(); } } /** * Find a existing instance of the quest object with the ID supplied. This function searches both the GUI and the * hidden list. * * @param questId the ID of the quest to search * @return the quest instance or {@code null} in case its not found */ @Nullable private QuestEntry findQuest(int questId) { for (QuestEntry hiddenListEntry : hiddenList) { if (hiddenListEntry.getQuestId() == questId) { return hiddenListEntry; } } ListBox<QuestEntry> questList = getQuestList(); if (questList != null) { for (QuestEntry guiListEntry : questList.getItems()) { if (guiListEntry.getQuestId() == questId) { return guiListEntry; } } } return null; } /** * Show the pulsing animation of the quest button. */ private void pulseQuestButton() { if (loginDone && (screen != null)) { @Nullable Element questBtn = screen.findElementById("openQuestBtn"); if (questBtn != null) { questBtn.startEffect(EffectEventId.onCustom, null, "pulse"); } } } }