mirror of
https://github.com/ProjectSWGCore/Holocore.git
synced 2026-09-11 21:45:26 -04:00
Merge branch 'ProjectSWGCore:master' into master
This commit is contained in:
-507
@@ -1,507 +0,0 @@
|
||||
/***********************************************************************************
|
||||
* Copyright (c) 2023 /// Project SWG /// www.projectswg.com *
|
||||
* *
|
||||
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
|
||||
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
|
||||
* Our goal is to create an emulator which will provide a server for players to *
|
||||
* continue playing a game similar to the one they used to play. We are basing *
|
||||
* it on the final publish of the game prior to end-game events. *
|
||||
* *
|
||||
* This file is part of Holocore. *
|
||||
* *
|
||||
* --------------------------------------------------------------------------------*
|
||||
* *
|
||||
* Holocore 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. *
|
||||
* *
|
||||
* Holocore 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 Affero General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Affero General Public License *
|
||||
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
|
||||
***********************************************************************************/
|
||||
package com.projectswg.holocore.services.gameplay.player.quest;
|
||||
|
||||
import com.projectswg.common.data.CRC;
|
||||
import com.projectswg.common.data.encodables.oob.OutOfBandPackage;
|
||||
import com.projectswg.common.data.encodables.oob.ProsePackage;
|
||||
import com.projectswg.common.data.encodables.oob.StringId;
|
||||
import com.projectswg.common.data.swgfile.ClientFactory;
|
||||
import com.projectswg.common.network.packets.swg.zone.CommPlayerMessage;
|
||||
import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage;
|
||||
import com.projectswg.common.network.packets.swg.zone.chat.ChatSystemMessage;
|
||||
import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestCompletedMessage;
|
||||
import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestTaskCounterMessage;
|
||||
import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent;
|
||||
import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent;
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.AbandonQuestIntent;
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.AdvanceQuestIntent;
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.CompleteQuestIntent;
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.GrantQuestIntent;
|
||||
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent;
|
||||
import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent;
|
||||
import com.projectswg.holocore.resources.support.data.server_info.StandardLog;
|
||||
import com.projectswg.holocore.resources.support.data.server_info.loader.QuestLoader;
|
||||
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData;
|
||||
import com.projectswg.holocore.resources.support.global.player.Player;
|
||||
import com.projectswg.holocore.resources.support.global.zone.sui.SuiButtons;
|
||||
import com.projectswg.holocore.resources.support.global.zone.sui.SuiMessageBox;
|
||||
import com.projectswg.holocore.resources.support.npc.spawn.Spawner;
|
||||
import com.projectswg.holocore.resources.support.objects.ObjectCreator;
|
||||
import com.projectswg.holocore.resources.support.objects.StaticItemCreator;
|
||||
import com.projectswg.holocore.resources.support.objects.swg.SWGObject;
|
||||
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
|
||||
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject;
|
||||
import com.projectswg.holocore.resources.support.objects.swg.player.PlayerObject;
|
||||
import com.projectswg.holocore.resources.support.objects.swg.player.Quest;
|
||||
import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool;
|
||||
import me.joshlarson.jlcommon.control.IntentHandler;
|
||||
import me.joshlarson.jlcommon.control.Service;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class QuestService extends Service {
|
||||
|
||||
private static final StringId PLACED_IN_INVENTORY = new StringId("quest/ground/system_message", "placed_in_inventory");
|
||||
private final ScheduledThreadPool executor;
|
||||
private final QuestLoader questLoader;
|
||||
|
||||
public QuestService() {
|
||||
this.executor = new ScheduledThreadPool(1, "quest-service-%d");
|
||||
this.questLoader = ServerData.INSTANCE.getQuestLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initialize() {
|
||||
executor.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean terminate() {
|
||||
executor.stop();
|
||||
return executor.awaitTermination(1000);
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private void handleGrantQuestIntent(GrantQuestIntent intent) {
|
||||
Player player = intent.getPlayer();
|
||||
String questName = intent.getQuestName();
|
||||
PlayerObject playerObject = player.getPlayerObject();
|
||||
|
||||
QuestLoader.QuestListInfo questListInfo = questLoader.getQuestListInfo(questName);
|
||||
|
||||
boolean repeatable = Boolean.TRUE.equals(questListInfo.isRepeatable());
|
||||
|
||||
if (!repeatable && playerObject.isQuestInJournal(questName)) {
|
||||
StandardLog.onPlayerError(this, player, "already had non-repeatable quest %s", questName);
|
||||
return;
|
||||
}
|
||||
|
||||
playerObject.addQuest(questName);
|
||||
playerObject.addActiveQuestTask(questName, 0);
|
||||
|
||||
StandardLog.onPlayerTrace(this, player, "received quest %s", questName);
|
||||
|
||||
List<QuestLoader.QuestTaskInfo> currentTasks = getActiveTaskInfos(questName, playerObject);
|
||||
|
||||
handleTaskEvents(player, questName, currentTasks);
|
||||
|
||||
ProsePackage prose = new ProsePackage(new StringId("quest/ground/system_message", "quest_received"), "TO", questListInfo.getJournalEntryTitle());
|
||||
SystemMessageIntent.broadcastPersonal(player, prose, ChatSystemMessage.SystemChatType.QUEST);
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private void handleAbandonQuestIntent(AbandonQuestIntent intent) {
|
||||
Player player = intent.getPlayer();
|
||||
String questName = intent.getQuestName();
|
||||
PlayerObject playerObject = player.getPlayerObject();
|
||||
|
||||
if (playerObject.isQuestComplete(questName)) {
|
||||
StandardLog.onPlayerTrace(this, player, "attempted to abandon quest %s which they have completed", questName);
|
||||
return;
|
||||
}
|
||||
|
||||
playerObject.removeQuest(questName);
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private void handleCompleteQuestIntent(CompleteQuestIntent intent) {
|
||||
Player player = intent.getPlayer();
|
||||
PlayerObject playerObject = player.getPlayerObject();
|
||||
String questName = intent.getQuestName();
|
||||
|
||||
if (playerObject.isQuestRewardReceived(questName)) {
|
||||
StandardLog.onPlayerTrace(this, player, "attempted to claim reward for quest %s but they have already received it", questName);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO award XP, load from tier+level matrix: datatables/quest/quest_experience.iff
|
||||
|
||||
playerObject.setQuestRewardReceived(questName, true);
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private void handleAdvanceQuestIntent(AdvanceQuestIntent intent) {
|
||||
Player player = intent.getPlayer();
|
||||
String questName = intent.getQuestName();
|
||||
PlayerObject playerObject = player.getPlayerObject();
|
||||
|
||||
if (!playerObject.isQuestInJournal(questName)) {
|
||||
StandardLog.onPlayerError(this, player, "advanced quest %s that was not in their quest journal", questName);
|
||||
return;
|
||||
}
|
||||
|
||||
List<QuestLoader.QuestTaskInfo> currentTasks = getActiveTaskInfos(questName, playerObject);
|
||||
|
||||
advanceQuest(questName, player, currentTasks);
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private void handleCreatureKilledIntent(CreatureKilledIntent intent) {
|
||||
CreatureObject corpse = intent.getCorpse();
|
||||
|
||||
if (!(corpse instanceof AIObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AIObject npcCorpse = (AIObject) corpse;
|
||||
CreatureObject killer = intent.getKiller();
|
||||
Player owner = killer.getOwner();
|
||||
|
||||
if (owner == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerObject playerObject = killer.getPlayerObject();
|
||||
|
||||
Set<Map.Entry<CRC, Quest>> entries = playerObject.getQuests().entrySet();
|
||||
|
||||
for (Map.Entry<CRC, Quest> entry : entries) {
|
||||
CRC key = entry.getKey();
|
||||
Quest quest = entry.getValue();
|
||||
|
||||
if (quest.isComplete()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String questName = key.getString();
|
||||
List<QuestLoader.QuestTaskInfo> activeTaskListInfos = getActiveTaskInfos(questName, playerObject);
|
||||
|
||||
for (QuestLoader.QuestTaskInfo activeTaskListInfo : activeTaskListInfos) {
|
||||
String type = activeTaskListInfo.getType();
|
||||
|
||||
if ("quest.task.ground.destroy_multi".equals(type)) {
|
||||
if (isKillPartOfTask(activeTaskListInfo, npcCorpse)) {
|
||||
int max = activeTaskListInfo.getCount();
|
||||
int counter = playerObject.incrementQuestCounter(questName);
|
||||
int remaining = max - counter;
|
||||
int task = activeTaskListInfo.getIndex();
|
||||
StandardLog.onPlayerTrace(this, owner, "%d remaining kills required on quest %s", remaining, questName);
|
||||
|
||||
incrementKillCount(questName, task, owner, counter, max);
|
||||
|
||||
if (remaining <= 0) {
|
||||
advanceQuest(questName, owner, activeTaskListInfos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isKillPartOfTask(QuestLoader.QuestTaskInfo swgQuestTask, AIObject npcCorpse) {
|
||||
String targetServerTemplate = swgQuestTask.getTargetServerTemplate();
|
||||
Spawner spawner = npcCorpse.getSpawner();
|
||||
String stfName = spawner.getStfName();
|
||||
String questSocialGroup = swgQuestTask.getSocialGroup();
|
||||
String npcSocialGroup = npcCorpse.getSpawner().getSocialGroup();
|
||||
|
||||
return isMatchingSocialGroup(questSocialGroup, npcSocialGroup) || isMatchingServerTemplate(targetServerTemplate, stfName);
|
||||
}
|
||||
|
||||
private static boolean isMatchingServerTemplate(String targetServerTemplate, String stfName) {
|
||||
return targetServerTemplate != null && Objects.equals(targetServerTemplate, stfName);
|
||||
}
|
||||
|
||||
private static boolean isMatchingSocialGroup(String questSocialGroup, String npcSocialGroup) {
|
||||
return questSocialGroup != null && questSocialGroup.equalsIgnoreCase(npcSocialGroup);
|
||||
}
|
||||
|
||||
private void incrementKillCount(String questName, int task, Player player, int counter, int max) {
|
||||
player.sendPacket(new QuestTaskCounterMessage(player.getCreatureObject().getObjectId(), questName, task, "@quest/groundquests:destroy_counter", counter, max));
|
||||
int remaining = max - counter;
|
||||
ProsePackage prose = new ProsePackage(new StringId("quest/groundquests", "destroy_multiple_success"), "DI", remaining);
|
||||
SystemMessageIntent.broadcastPersonal(player, prose);
|
||||
}
|
||||
|
||||
private void handleTaskEvents(Player player, String questName, Collection<QuestLoader.QuestTaskInfo> currentTasks) {
|
||||
PlayerObject playerObject = player.getPlayerObject();
|
||||
|
||||
for (QuestLoader.QuestTaskInfo currentTask : currentTasks) {
|
||||
String type = currentTask.getType();
|
||||
|
||||
if (type == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentTask.isVisible()) {
|
||||
player.sendPacket(new PlayMusicMessage(0, "sound/ui_journal_updated.snd", 1, false));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "quest.task.ground.comm_player": {
|
||||
handleCommPlayer(player, questName, playerObject, currentTask);
|
||||
break;
|
||||
}
|
||||
case "quest.task.ground.complete_quest": {
|
||||
completeQuest(player, questName);
|
||||
break;
|
||||
}
|
||||
case "quest.task.ground.timer": {
|
||||
handleTimer(player, questName, playerObject, currentTask);
|
||||
break;
|
||||
}
|
||||
case "quest.task.ground.show_message_box": {
|
||||
handleShowMessageBox(player, questName, playerObject, currentTask);
|
||||
break;
|
||||
}
|
||||
case "quest.task.ground.destroy_multi": {
|
||||
handleDestroyMulti(player, questName, currentTask);
|
||||
break;
|
||||
}
|
||||
case "quest.task.ground.reward": {
|
||||
handleReward(player, questName, playerObject, currentTask);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReward(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
|
||||
grantXPReward(player, currentTask);
|
||||
grantFactionPointsReward(playerObject, currentTask);
|
||||
grantCreditsReward(player, currentTask);
|
||||
grantLootRewards(player, currentTask);
|
||||
grantItemRewards(player, currentTask);
|
||||
// weapon stuff: weapon count_weapon speed damage efficiency elemental_value
|
||||
// armor stuff: armor count_armor quality
|
||||
// speed etc. are percentages, the absolute values we must define somewhere else before we can hand out weapons and armor rewards
|
||||
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
|
||||
|
||||
for (Integer taskIndex : currentTask.getNextTasksOnComplete()) {
|
||||
playerObject.addActiveQuestTask(questName, taskIndex);
|
||||
}
|
||||
|
||||
List<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
|
||||
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
|
||||
List<QuestLoader.QuestTaskInfo> nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
|
||||
|
||||
handleTaskEvents(player, questName, nextTasks);
|
||||
}
|
||||
|
||||
private static void grantXPReward(Player player, QuestLoader.QuestTaskInfo currentTask) {
|
||||
String experienceType = currentTask.getExperienceType();
|
||||
if (experienceType != null) {
|
||||
new ExperienceIntent(player.getCreatureObject(), experienceType, currentTask.getExperienceAmount()).broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
private static void grantFactionPointsReward(PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
|
||||
String factionName = currentTask.getFactionName();
|
||||
if (factionName != null) {
|
||||
playerObject.adjustFactionPoints(factionName, currentTask.getFactionAmount());
|
||||
}
|
||||
}
|
||||
|
||||
private static void grantCreditsReward(Player player, QuestLoader.QuestTaskInfo currentTask) {
|
||||
int bankCredits = currentTask.getBankCredits();
|
||||
if (bankCredits != 0) {
|
||||
player.getCreatureObject().addToBank(bankCredits);
|
||||
}
|
||||
}
|
||||
|
||||
private static void grantLootRewards(Player player, QuestLoader.QuestTaskInfo currentTask) {
|
||||
int lootCount = currentTask.getLootCount();
|
||||
if (lootCount > 0) {
|
||||
for (int i = 0; i < lootCount; i++) {
|
||||
String lootName = currentTask.getLootName();
|
||||
if (lootName != null && !lootName.isBlank()) {
|
||||
SWGObject item = StaticItemCreator.INSTANCE.createItem(lootName);
|
||||
if (item != null) {
|
||||
transferItemToInventory(player, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void grantItemRewards(Player player, QuestLoader.QuestTaskInfo currentTask) {
|
||||
int itemCount = currentTask.getItemCount();
|
||||
if (itemCount > 0) {
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
String itemTemplate = currentTask.getItemTemplate();
|
||||
if (itemTemplate != null && !itemTemplate.isBlank()) {
|
||||
SWGObject item = ObjectCreator.createObjectFromTemplate(itemTemplate);
|
||||
transferItemToInventory(player, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void transferItemToInventory(Player player, SWGObject item) {
|
||||
SWGObject inventory = player.getCreatureObject().getInventory();
|
||||
item.moveToContainer(inventory);
|
||||
ObjectCreatedIntent.broadcast(item);
|
||||
SystemMessageIntent.broadcastPersonal(player, new ProsePackage(PLACED_IN_INVENTORY, "TO", item.getStringId()));
|
||||
}
|
||||
|
||||
private static void handleDestroyMulti(Player player, String questName, QuestLoader.QuestTaskInfo currentTask) {
|
||||
int task = currentTask.getIndex();
|
||||
int max = currentTask.getCount();
|
||||
int counter = 0;
|
||||
player.sendPacket(new QuestTaskCounterMessage(player.getCreatureObject().getObjectId(), questName, task, "@quest/groundquests:destroy_counter", counter, max));
|
||||
}
|
||||
|
||||
private void handleShowMessageBox(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
|
||||
String messageBoxTitle = currentTask.getMessageBoxTitle();
|
||||
String messageBoxText = currentTask.getMessageBoxText();
|
||||
SuiMessageBox sui = new SuiMessageBox(SuiButtons.OK, messageBoxTitle, messageBoxText);
|
||||
sui.setSize(384, 256);
|
||||
sui.setLocation(320, 256);
|
||||
sui.display(player);
|
||||
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
|
||||
|
||||
for (Integer taskIndex : currentTask.getNextTasksOnComplete()) {
|
||||
playerObject.addActiveQuestTask(questName, taskIndex);
|
||||
}
|
||||
|
||||
List<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
|
||||
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
|
||||
List<QuestLoader.QuestTaskInfo> nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
|
||||
|
||||
handleTaskEvents(player, questName, nextTasks);
|
||||
}
|
||||
|
||||
private void handleCommPlayer(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
|
||||
String commMessageText = currentTask.getCommMessageText();
|
||||
OutOfBandPackage message = new OutOfBandPackage(new ProsePackage(new StringId(commMessageText)));
|
||||
long objectId = player.getCreatureObject().getObjectId();
|
||||
CRC modelCrc = getModelCrc(currentTask);
|
||||
|
||||
player.sendPacket(new CommPlayerMessage(objectId, message, modelCrc, "", 10));
|
||||
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
|
||||
|
||||
for (Integer taskIndex : currentTask.getNextTasksOnComplete()) {
|
||||
playerObject.addActiveQuestTask(questName, taskIndex);
|
||||
}
|
||||
|
||||
List<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
|
||||
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
|
||||
List<QuestLoader.QuestTaskInfo> nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
|
||||
|
||||
handleTaskEvents(player, questName, nextTasks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static CRC getModelCrc(QuestLoader.QuestTaskInfo currentTask) {
|
||||
String npcAppearanceServerTemplate = currentTask.getNpcAppearanceServerTemplate();
|
||||
if (npcAppearanceServerTemplate != null && !npcAppearanceServerTemplate.isBlank()) {
|
||||
String sharedTemplate = ClientFactory.formatToSharedFile(npcAppearanceServerTemplate);
|
||||
return new CRC(sharedTemplate);
|
||||
}
|
||||
|
||||
return new CRC(0); // Fallback case, as some tasks don't have an appearance set. The player sees their own character in the comm window.
|
||||
}
|
||||
|
||||
private void handleTimer(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
|
||||
int minTime = currentTask.getMinTime();
|
||||
int maxTime = currentTask.getMaxTime();
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
int delay = random.nextInt(minTime, maxTime) * 1000;
|
||||
|
||||
executor.execute(delay, () -> {
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
|
||||
|
||||
List<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
|
||||
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
|
||||
List<QuestLoader.QuestTaskInfo> nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
|
||||
|
||||
handleTaskEvents(player, questName, nextTasks);
|
||||
});
|
||||
}
|
||||
|
||||
private void advanceQuest(String questName, Player player, List<QuestLoader.QuestTaskInfo> currentTasks) {
|
||||
PlayerObject playerObject = player.getPlayerObject();
|
||||
|
||||
for (QuestLoader.QuestTaskInfo currentTask : currentTasks) {
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
|
||||
|
||||
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
|
||||
|
||||
for (Integer nextTaskOnComplete : nextTasksOnComplete) {
|
||||
playerObject.addActiveQuestTask(questName, nextTaskOnComplete);
|
||||
StandardLog.onPlayerTrace(this, player, "advanced quest %s, activated task %d", questName, nextTaskOnComplete);
|
||||
}
|
||||
}
|
||||
|
||||
List<QuestLoader.QuestTaskInfo> nextTasks = getActiveTaskInfos(questName, playerObject);
|
||||
|
||||
if (nextTasks.isEmpty()) {
|
||||
QuestLoader.QuestListInfo questListInfo = questLoader.getQuestListInfo(questName);
|
||||
boolean completeWhenTasksComplete = questListInfo.isCompleteWhenTasksComplete();
|
||||
|
||||
if (completeWhenTasksComplete) {
|
||||
completeQuest(player, questName);
|
||||
|
||||
nextTasks = currentTasks;
|
||||
}
|
||||
}
|
||||
|
||||
handleTaskEvents(player, questName, nextTasks);
|
||||
|
||||
for (QuestLoader.QuestTaskInfo nextTask : nextTasks) {
|
||||
String grantQuestOnComplete = nextTask.getGrantQuestOnComplete();
|
||||
|
||||
if (grantQuestOnComplete != null && !grantQuestOnComplete.isBlank()) {
|
||||
GrantQuestIntent.broadcast(player, grantQuestOnComplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void completeQuest(Player player, String questName) {
|
||||
PlayerObject playerObject = player.getPlayerObject();
|
||||
playerObject.completeQuest(questName);
|
||||
player.sendPacket(new QuestCompletedMessage(player.getCreatureObject().getObjectId(), new CRC(questName)));
|
||||
StandardLog.onPlayerTrace(this, player, "completed quest %s", questName);
|
||||
}
|
||||
|
||||
private List<QuestLoader.QuestTaskInfo> getActiveTaskInfos(String questName, PlayerObject playerObject) {
|
||||
List<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
|
||||
Collection<Integer> questActiveTasks = playerObject.getQuestActiveTasks(questName);
|
||||
|
||||
return mapActiveTasks(questActiveTasks, taskListInfos);
|
||||
}
|
||||
|
||||
private List<QuestLoader.QuestTaskInfo> mapActiveTasks(Collection<Integer> activeTaskIndices, List<QuestLoader.QuestTaskInfo> taskListInfos) {
|
||||
return activeTaskIndices.stream()
|
||||
.map(taskListInfos::get)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/***********************************************************************************
|
||||
* Copyright (c) 2023 /// Project SWG /// www.projectswg.com *
|
||||
* *
|
||||
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
|
||||
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
|
||||
* Our goal is to create an emulator which will provide a server for players to *
|
||||
* continue playing a game similar to the one they used to play. We are basing *
|
||||
* it on the final publish of the game prior to end-game events. *
|
||||
* *
|
||||
* This file is part of Holocore. *
|
||||
* *
|
||||
* --------------------------------------------------------------------------------*
|
||||
* *
|
||||
* Holocore 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. *
|
||||
* *
|
||||
* Holocore 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 Affero General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Affero General Public License *
|
||||
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
|
||||
***********************************************************************************/
|
||||
package com.projectswg.holocore.services.gameplay.player.quest
|
||||
|
||||
import com.projectswg.common.data.CRC
|
||||
import com.projectswg.common.data.encodables.oob.OutOfBandPackage
|
||||
import com.projectswg.common.data.encodables.oob.ProsePackage
|
||||
import com.projectswg.common.data.encodables.oob.StringId
|
||||
import com.projectswg.common.data.swgfile.ClientFactory
|
||||
import com.projectswg.common.network.packets.swg.zone.CommPlayerMessage
|
||||
import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage
|
||||
import com.projectswg.common.network.packets.swg.zone.chat.ChatSystemMessage
|
||||
import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestCompletedMessage
|
||||
import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestTaskCounterMessage
|
||||
import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent
|
||||
import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.AbandonQuestIntent
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.AdvanceQuestIntent
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.CompleteQuestIntent
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.GrantQuestIntent
|
||||
import com.projectswg.holocore.intents.gameplay.player.quest.GrantQuestIntent.Companion.broadcast
|
||||
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
|
||||
import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent
|
||||
import com.projectswg.holocore.resources.support.data.server_info.StandardLog
|
||||
import com.projectswg.holocore.resources.support.data.server_info.loader.QuestLoader.QuestTaskInfo
|
||||
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData
|
||||
import com.projectswg.holocore.resources.support.global.player.Player
|
||||
import com.projectswg.holocore.resources.support.global.zone.sui.SuiButtons
|
||||
import com.projectswg.holocore.resources.support.global.zone.sui.SuiMessageBox
|
||||
import com.projectswg.holocore.resources.support.objects.ObjectCreator
|
||||
import com.projectswg.holocore.resources.support.objects.StaticItemCreator.createItem
|
||||
import com.projectswg.holocore.resources.support.objects.swg.SWGObject
|
||||
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
|
||||
import com.projectswg.holocore.resources.support.objects.swg.player.PlayerObject
|
||||
import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool
|
||||
import me.joshlarson.jlcommon.control.IntentHandler
|
||||
import me.joshlarson.jlcommon.control.Service
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
class QuestService : Service() {
|
||||
private val executor = ScheduledThreadPool(1, "quest-service-%d")
|
||||
private val questLoader = ServerData.questLoader
|
||||
|
||||
override fun initialize(): Boolean {
|
||||
executor.start()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun terminate(): Boolean {
|
||||
executor.stop()
|
||||
return executor.awaitTermination(1000)
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private fun handleGrantQuestIntent(intent: GrantQuestIntent) {
|
||||
val player = intent.player
|
||||
val questName = intent.questName
|
||||
val playerObject = player.getPlayerObject()
|
||||
val questListInfo = questLoader.getQuestListInfo(questName)
|
||||
val repeatable = java.lang.Boolean.TRUE == questListInfo.isRepeatable
|
||||
if (!repeatable && playerObject.isQuestInJournal(questName)) {
|
||||
StandardLog.onPlayerError(this, player, "already had non-repeatable quest %s", questName)
|
||||
return
|
||||
}
|
||||
playerObject.addQuest(questName)
|
||||
playerObject.addActiveQuestTask(questName, 0)
|
||||
StandardLog.onPlayerTrace(this, player, "received quest %s", questName)
|
||||
val currentTasks = getActiveTaskInfos(questName, playerObject)
|
||||
handleTaskEvents(player, questName, currentTasks)
|
||||
val prose = ProsePackage(StringId("quest/ground/system_message", "quest_received"), "TO", questListInfo.journalEntryTitle)
|
||||
SystemMessageIntent.broadcastPersonal(player, prose, ChatSystemMessage.SystemChatType.QUEST)
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private fun handleAbandonQuestIntent(intent: AbandonQuestIntent) {
|
||||
val player = intent.player
|
||||
val questName = intent.questName
|
||||
val playerObject = player.getPlayerObject()
|
||||
if (playerObject.isQuestComplete(questName)) {
|
||||
StandardLog.onPlayerTrace(this, player, "attempted to abandon quest %s which they have completed", questName)
|
||||
return
|
||||
}
|
||||
playerObject.removeQuest(questName)
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private fun handleCompleteQuestIntent(intent: CompleteQuestIntent) {
|
||||
val player = intent.player
|
||||
val playerObject = player.getPlayerObject()
|
||||
val questName = intent.questName
|
||||
if (playerObject.isQuestRewardReceived(questName)) {
|
||||
StandardLog.onPlayerTrace(this, player, "attempted to claim reward for quest %s but they have already received it", questName)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO award XP, load from tier+level matrix: datatables/quest/quest_experience.iff
|
||||
playerObject.setQuestRewardReceived(questName, true)
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private fun handleAdvanceQuestIntent(intent: AdvanceQuestIntent) {
|
||||
val player = intent.player
|
||||
val questName = intent.questName
|
||||
val playerObject = player.getPlayerObject()
|
||||
if (!playerObject.isQuestInJournal(questName)) {
|
||||
StandardLog.onPlayerError(this, player, "advanced quest %s that was not in their quest journal", questName)
|
||||
return
|
||||
}
|
||||
val currentTasks = getActiveTaskInfos(questName, playerObject)
|
||||
advanceQuest(questName, player, currentTasks)
|
||||
}
|
||||
|
||||
@IntentHandler
|
||||
private fun handleCreatureKilledIntent(intent: CreatureKilledIntent) {
|
||||
val corpse = intent.corpse as? AIObject ?: return
|
||||
val killer = intent.killer
|
||||
val owner = killer.owner ?: return
|
||||
val playerObject = killer.playerObject
|
||||
val entries = playerObject.quests.entries
|
||||
for ((key, quest) in entries) {
|
||||
if (quest.isComplete) {
|
||||
continue
|
||||
}
|
||||
val questName = key.string
|
||||
val activeTaskListInfos = getActiveTaskInfos(questName, playerObject)
|
||||
for (activeTaskListInfo in activeTaskListInfos) {
|
||||
val type = activeTaskListInfo.type
|
||||
if ("quest.task.ground.destroy_multi" == type) {
|
||||
if (isKillPartOfTask(activeTaskListInfo, corpse)) {
|
||||
val max = activeTaskListInfo.count
|
||||
val counter = playerObject.incrementQuestCounter(questName)
|
||||
val remaining = max - counter
|
||||
val task = activeTaskListInfo.index
|
||||
StandardLog.onPlayerTrace(this, owner, "%d remaining kills required on quest %s", remaining, questName)
|
||||
incrementKillCount(questName, task, owner, counter, max)
|
||||
if (remaining <= 0) {
|
||||
advanceQuest(questName, owner, activeTaskListInfos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isKillPartOfTask(swgQuestTask: QuestTaskInfo, npcCorpse: AIObject): Boolean {
|
||||
val targetServerTemplate = swgQuestTask.targetServerTemplate
|
||||
val spawner = npcCorpse.spawner
|
||||
val stfName = spawner.stfName
|
||||
val questSocialGroup = swgQuestTask.socialGroup
|
||||
val npcSocialGroup = npcCorpse.spawner.socialGroup
|
||||
return isMatchingSocialGroup(questSocialGroup, npcSocialGroup) || isMatchingServerTemplate(targetServerTemplate, stfName)
|
||||
}
|
||||
|
||||
private fun incrementKillCount(questName: String, task: Int, player: Player, counter: Int, max: Int) {
|
||||
player.sendPacket(
|
||||
QuestTaskCounterMessage(
|
||||
player.creatureObject.objectId,
|
||||
questName,
|
||||
task,
|
||||
"@quest/groundquests:destroy_counter",
|
||||
counter,
|
||||
max
|
||||
)
|
||||
)
|
||||
val remaining = max - counter
|
||||
val prose = ProsePackage(StringId("quest/groundquests", "destroy_multiple_success"), "DI", remaining)
|
||||
SystemMessageIntent.broadcastPersonal(player, prose)
|
||||
}
|
||||
|
||||
private fun handleTaskEvents(player: Player, questName: String, currentTasks: Collection<QuestTaskInfo>) {
|
||||
val playerObject = player.getPlayerObject()
|
||||
for (currentTask in currentTasks) {
|
||||
val type = currentTask.type ?: continue
|
||||
if (currentTask.isVisible) {
|
||||
player.sendPacket(PlayMusicMessage(0, "sound/ui_journal_updated.snd", 1, false))
|
||||
}
|
||||
when (type) {
|
||||
"quest.task.ground.comm_player" -> handleCommPlayer(player, questName, playerObject, currentTask)
|
||||
"quest.task.ground.complete_quest" -> completeQuest(player, questName)
|
||||
"quest.task.ground.timer" -> handleTimer(player, questName, playerObject, currentTask)
|
||||
"quest.task.ground.show_message_box" -> handleShowMessageBox(player, questName, playerObject, currentTask)
|
||||
"quest.task.ground.destroy_multi" -> handleDestroyMulti(player, questName, currentTask)
|
||||
"quest.task.ground.reward" -> handleReward(player, questName, playerObject, currentTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReward(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
|
||||
grantXPReward(player, currentTask)
|
||||
grantFactionPointsReward(playerObject, currentTask)
|
||||
grantCreditsReward(player, currentTask)
|
||||
grantLootRewards(player, currentTask)
|
||||
grantItemRewards(player, currentTask)
|
||||
// weapon stuff: weapon count_weapon speed damage efficiency elemental_value
|
||||
// armor stuff: armor count_armor quality
|
||||
// speed etc. are percentages, the absolute values we must define somewhere else before we can hand out weapons and armor rewards
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.index)
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.index)
|
||||
currentTask.nextTasksOnComplete.forEach { playerObject.addActiveQuestTask(questName, it) }
|
||||
val taskListInfos = questLoader.getTaskListInfos(questName)
|
||||
val nextTasksOnComplete = currentTask.nextTasksOnComplete
|
||||
val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
|
||||
handleTaskEvents(player, questName, nextTasks)
|
||||
}
|
||||
|
||||
private fun handleShowMessageBox(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
|
||||
val messageBoxTitle = currentTask.messageBoxTitle
|
||||
val messageBoxText = currentTask.messageBoxText
|
||||
val sui = SuiMessageBox(SuiButtons.OK, messageBoxTitle, messageBoxText)
|
||||
sui.setSize(384, 256)
|
||||
sui.setLocation(320, 256)
|
||||
sui.display(player)
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.index)
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.index)
|
||||
currentTask.nextTasksOnComplete.forEach { playerObject.addActiveQuestTask(questName, it) }
|
||||
val taskListInfos = questLoader.getTaskListInfos(questName)
|
||||
val nextTasksOnComplete = currentTask.nextTasksOnComplete
|
||||
val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
|
||||
handleTaskEvents(player, questName, nextTasks)
|
||||
}
|
||||
|
||||
private fun handleCommPlayer(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
|
||||
val commMessageText = currentTask.commMessageText
|
||||
val message = OutOfBandPackage(ProsePackage(StringId(commMessageText)))
|
||||
val objectId = player.creatureObject.objectId
|
||||
val modelCrc = getModelCrc(currentTask)
|
||||
player.sendPacket(CommPlayerMessage(objectId, message, modelCrc, "", 10f))
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.index)
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.index)
|
||||
currentTask.nextTasksOnComplete.forEach { playerObject.addActiveQuestTask(questName, it) }
|
||||
val taskListInfos = questLoader.getTaskListInfos(questName)
|
||||
val nextTasksOnComplete = currentTask.nextTasksOnComplete
|
||||
val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
|
||||
handleTaskEvents(player, questName, nextTasks)
|
||||
}
|
||||
|
||||
private fun handleTimer(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
|
||||
val minTime = currentTask.minTime
|
||||
val maxTime = currentTask.maxTime
|
||||
val random = ThreadLocalRandom.current()
|
||||
val delay = random.nextInt(minTime, maxTime) * 1000
|
||||
executor.execute(delay.toLong()) {
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.index)
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.index)
|
||||
val taskListInfos = questLoader.getTaskListInfos(questName)
|
||||
val nextTasksOnComplete = currentTask.nextTasksOnComplete
|
||||
val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
|
||||
handleTaskEvents(player, questName, nextTasks)
|
||||
}
|
||||
}
|
||||
|
||||
private fun advanceQuest(questName: String, player: Player, currentTasks: List<QuestTaskInfo>) {
|
||||
val playerObject = player.getPlayerObject()
|
||||
for (currentTask in currentTasks) {
|
||||
playerObject.removeActiveQuestTask(questName, currentTask.index)
|
||||
playerObject.addCompleteQuestTask(questName, currentTask.index)
|
||||
val nextTasksOnComplete = currentTask.nextTasksOnComplete
|
||||
for (nextTaskOnComplete in nextTasksOnComplete) {
|
||||
playerObject.addActiveQuestTask(questName, nextTaskOnComplete)
|
||||
StandardLog.onPlayerTrace(this, player, "advanced quest %s, activated task %d", questName, nextTaskOnComplete)
|
||||
}
|
||||
}
|
||||
var nextTasks = getActiveTaskInfos(questName, playerObject)
|
||||
if (nextTasks.isEmpty()) {
|
||||
val questListInfo = questLoader.getQuestListInfo(questName)
|
||||
val completeWhenTasksComplete = questListInfo.isCompleteWhenTasksComplete
|
||||
if (completeWhenTasksComplete) {
|
||||
completeQuest(player, questName)
|
||||
nextTasks = currentTasks
|
||||
}
|
||||
}
|
||||
handleTaskEvents(player, questName, nextTasks)
|
||||
for (nextTask in nextTasks) {
|
||||
val grantQuestOnComplete = nextTask.grantQuestOnComplete
|
||||
if (grantQuestOnComplete != null && grantQuestOnComplete.isNotBlank()) {
|
||||
broadcast(player, grantQuestOnComplete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeQuest(player: Player, questName: String) {
|
||||
val playerObject = player.getPlayerObject()
|
||||
playerObject.completeQuest(questName)
|
||||
player.sendPacket(QuestCompletedMessage(player.creatureObject.objectId, CRC(questName)))
|
||||
StandardLog.onPlayerTrace(this, player, "completed quest %s", questName)
|
||||
}
|
||||
|
||||
private fun getActiveTaskInfos(questName: String, playerObject: PlayerObject): List<QuestTaskInfo> {
|
||||
val taskListInfos = questLoader.getTaskListInfos(questName)
|
||||
val questActiveTasks = playerObject.getQuestActiveTasks(questName)
|
||||
return mapActiveTasks(questActiveTasks, taskListInfos)
|
||||
}
|
||||
|
||||
private fun mapActiveTasks(activeTaskIndices: Collection<Int>, taskListInfos: List<QuestTaskInfo>): List<QuestTaskInfo> {
|
||||
return activeTaskIndices.map { taskListInfos[it] }
|
||||
}
|
||||
|
||||
private fun isMatchingServerTemplate(targetServerTemplate: String?, stfName: String): Boolean {
|
||||
return targetServerTemplate != null && targetServerTemplate == stfName
|
||||
}
|
||||
|
||||
private fun isMatchingSocialGroup(questSocialGroup: String?, npcSocialGroup: String?): Boolean {
|
||||
return questSocialGroup != null && questSocialGroup.equals(npcSocialGroup, ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun grantXPReward(player: Player, currentTask: QuestTaskInfo) {
|
||||
val experienceType = currentTask.experienceType
|
||||
if (experienceType != null) {
|
||||
ExperienceIntent(player.creatureObject, experienceType, currentTask.experienceAmount).broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
private fun grantFactionPointsReward(playerObject: PlayerObject, currentTask: QuestTaskInfo) {
|
||||
val factionName = currentTask.factionName
|
||||
if (factionName != null) {
|
||||
playerObject.adjustFactionPoints(factionName, currentTask.factionAmount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun grantCreditsReward(player: Player, currentTask: QuestTaskInfo) {
|
||||
val bankCredits = currentTask.bankCredits
|
||||
if (bankCredits != 0) {
|
||||
player.creatureObject.addToBank(bankCredits.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun grantLootRewards(player: Player, currentTask: QuestTaskInfo) {
|
||||
val lootCount = currentTask.lootCount
|
||||
if (lootCount > 0) {
|
||||
for (i in 0 until lootCount) {
|
||||
val lootName = currentTask.lootName
|
||||
if (lootName != null && lootName.isNotBlank()) {
|
||||
val item = createItem(lootName)
|
||||
if (item != null) {
|
||||
transferItemToInventory(player, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun grantItemRewards(player: Player, currentTask: QuestTaskInfo) {
|
||||
val itemCount = currentTask.itemCount
|
||||
if (itemCount > 0) {
|
||||
for (i in 0 until itemCount) {
|
||||
val itemTemplate = currentTask.itemTemplate
|
||||
if (itemTemplate != null && itemTemplate.isNotBlank()) {
|
||||
val item = ObjectCreator.createObjectFromTemplate(itemTemplate)
|
||||
transferItemToInventory(player, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun transferItemToInventory(player: Player, item: SWGObject) {
|
||||
val inventory = player.creatureObject.getInventory()
|
||||
item.moveToContainer(inventory)
|
||||
ObjectCreatedIntent.broadcast(item)
|
||||
SystemMessageIntent.broadcastPersonal(player,
|
||||
ProsePackage(StringId("quest/ground/system_message", "placed_in_inventory"), "TO", item.stringId))
|
||||
}
|
||||
|
||||
private fun handleDestroyMulti(player: Player, questName: String, currentTask: QuestTaskInfo) {
|
||||
val task = currentTask.index
|
||||
val max = currentTask.count
|
||||
val counter = 0
|
||||
player.sendPacket(
|
||||
QuestTaskCounterMessage(
|
||||
player.creatureObject.objectId,
|
||||
questName,
|
||||
task,
|
||||
"@quest/groundquests:destroy_counter",
|
||||
counter,
|
||||
max
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getModelCrc(currentTask: QuestTaskInfo): CRC {
|
||||
val npcAppearanceServerTemplate = currentTask.npcAppearanceServerTemplate
|
||||
if (npcAppearanceServerTemplate != null && npcAppearanceServerTemplate.isNotBlank()) {
|
||||
val sharedTemplate = ClientFactory.formatToSharedFile(npcAppearanceServerTemplate)
|
||||
return CRC(sharedTemplate)
|
||||
}
|
||||
return CRC(0) // Fallback case, as some tasks don't have an appearance set. The player sees their own character in the comm window.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user