diff --git a/src/ZoneServer/Buffs/Handlers/Common/Petrification.cs b/src/ZoneServer/Buffs/Handlers/Common/Petrification.cs new file mode 100644 index 000000000..47b0739ac --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Common/Petrification.cs @@ -0,0 +1,35 @@ +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.World.Actors.Characters; + +namespace Melia.Zone.Buffs.Handlers +{ + /// + /// Handle for the Petrify, Petrified.. + /// + [BuffHandler(BuffId.Petrification)] + public class Petrification : BuffHandler + { + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + var target = buff.Target; + + Send.ZC_SHOW_EMOTICON(target, "I_emo_petrify", buff.Duration); + Send.ZC_PLAY_SOUND(target, "skl_eff_debuff_stone"); + + if (target is Character character) + AddPropertyModifier(buff, character, PropertyName.Jumpable, -1); + } + + public override void OnEnd(Buff buff) + { + var target = buff.Target; + + if (target is Character character) + RemovePropertyModifier(buff, character, PropertyName.Jumpable); + } + } +} diff --git a/src/ZoneServer/Scripting/Dialogues/Dialog.cs b/src/ZoneServer/Scripting/Dialogues/Dialog.cs index 8734902df..8798936fe 100644 --- a/src/ZoneServer/Scripting/Dialogues/Dialog.cs +++ b/src/ZoneServer/Scripting/Dialogues/Dialog.cs @@ -438,6 +438,21 @@ public async Task Select(string text, IEnumerable options) return selectedIndex; } + /// + /// Displays a Yes/No dialog and returns true if the user selects Yes. + /// + /// The question to ask the user + /// True if Yes was selected, false otherwise + public async Task YesNo(string text) + { + // The keys "1" and "2" are what the client sends back for the first and second option respectively. + // Using the integer Select method and checking the result is the most direct way. + var result = await this.Select(text, "Yes", "No"); + + // result will be 1 for "Yes", 2 for "No", and 0 if the dialog was closed/cancelled. + return result == 1; + } + /// /// Sends dialog input message, showing a message and a text field /// for the user to put in a string. diff --git a/src/ZoneServer/Scripting/QuestScript.cs b/src/ZoneServer/Scripting/QuestScript.cs index a15a07bea..d086005c9 100644 --- a/src/ZoneServer/Scripting/QuestScript.cs +++ b/src/ZoneServer/Scripting/QuestScript.cs @@ -3,6 +3,7 @@ using Melia.Zone.Scripting.Hooking; using Melia.Zone.World.Actors.Characters; using Melia.Zone.World.Quests; +using Melia.Zone.World.Quests.Modifiers; using Melia.Zone.World.Quests.Prerequisites; using Yggdrasil.Scripting; @@ -16,6 +17,7 @@ public abstract class QuestScript : IScript, IDisposable private readonly static object ScriptsSyncLock = new(); private readonly static Dictionary Scripts = new(); private readonly static Dictionary Objectives = new(); + private readonly static Dictionary Modifiers = new(); private readonly static List AutoReceiveQuests = new(); /// @@ -60,6 +62,16 @@ public bool Init() } } + foreach (var modifier in this.Data.Modifiers) + { + var type = modifier.GetType(); + if (!Modifiers.ContainsKey(type)) + { + Modifiers[type] = modifier; + modifier.Load(); + } + } + if (this.Data.ReceiveType == QuestReceiveType.Auto) AutoReceiveQuests.Add(this); } @@ -131,13 +143,31 @@ public void Dispose() lock (ScriptsSyncLock) { - if (Objectives.Count == 0) - return; + if (Objectives.Count != 0) + { + foreach (var objective in Objectives.Values) + objective.Unload(); - foreach (var objective in Objectives.Values) - objective.Unload(); + Objectives.Clear(); + } - Objectives.Clear(); + if (Modifiers.Count != 0) + { + foreach (var modifier in Modifiers.Values) + modifier.Unload(); + + Modifiers.Clear(); + } + + if (Scripts.Count != 0) + { + Scripts.Clear(); + } + + if (AutoReceiveQuests.Count != 0) + { + AutoReceiveQuests.Clear(); + } } } @@ -181,6 +211,33 @@ protected void SetName(string name) protected void SetDescription(string description) => this.Data.Description = description; + /// + /// Sets the quest's location (map class name). + /// + /// + protected void SetLocation(string mapClassName) + => this.Data.Location = mapClassName; + + /// + /// Sets the quest's locations (multiple map class names). + /// + /// Map class names separated by commas + protected void SetLocation(params string[] mapClassNames) + => this.Data.Location = string.Join(",", mapClassNames); + + /// + /// Sets the quest giver NPC name and location. + /// + /// The name of the NPC that gives the quest + /// The map class name where the NPC is located + protected void AddQuestGiver(string npcName, string mapClassName) + { + this.Data.StartNpcUniqueName = npcName; + this.Data.QuestGiverLocation = mapClassName; + if (string.IsNullOrEmpty(this.Data.Location)) + this.Data.Location = mapClassName; + } + /// /// Sets the quest's type. /// @@ -272,6 +329,17 @@ protected void AddPrerequisite(QuestPrerequisite prerequisite) this.Data.Prerequisites.Add(prerequisite); } + /// + /// Adds an item drop modifier using item ID and monster IDs directly. + /// + /// The ID of the item to drop + /// Drop probability (0.0 to 1.0, where 0.5 = 50%) + /// Monster IDs that should drop this item + protected void AddDrop(int itemId, float dropChance, params int[] monsterIds) + { + this.Data.Modifiers.Add(new ItemDropModifier(itemId, dropChance, monsterIds)); + } + /// /// Returns an Or prerequisite, which is met if one of the given /// prerequisites is met. diff --git a/src/ZoneServer/World/Actors/Characters/Character.cs b/src/ZoneServer/World/Actors/Characters/Character.cs index 0d2d659c9..d116e8179 100644 --- a/src/ZoneServer/World/Actors/Characters/Character.cs +++ b/src/ZoneServer/World/Actors/Characters/Character.cs @@ -342,6 +342,11 @@ public int JobLevel /// public QuestComponent Quests { get; } + /// + /// Returns the character's time action component. + /// + public TimeActionComponent TimeActions { get; } + /// /// Returns the character's collection manager. /// @@ -405,7 +410,7 @@ public Character() : base() this.Components.Add(new RecoveryComponent(this)); this.Components.Add(new CombatComponent(this)); this.Components.Add(new CooldownComponent(this)); - this.Components.Add(new TimeActionComponent(this)); + this.Components.Add(this.TimeActions = new TimeActionComponent(this)); this.Components.Add(new StateLockComponent(this)); this.Components.Add(this.Quests = new QuestComponent(this)); this.Components.Add(this.Collections = new CollectionComponent(this)); diff --git a/src/ZoneServer/World/Actors/Characters/Components/QuestComponent.cs b/src/ZoneServer/World/Actors/Characters/Components/QuestComponent.cs index ac4ebe9ca..81f76179f 100644 --- a/src/ZoneServer/World/Actors/Characters/Components/QuestComponent.cs +++ b/src/ZoneServer/World/Actors/Characters/Components/QuestComponent.cs @@ -5,6 +5,8 @@ using Melia.Zone.Network; using Melia.Zone.Scripting; using Melia.Zone.World.Quests; +using Melia.Zone.World.Quests.Modifiers; +using Melia.Zone.World.Quests.Objectives; using Yggdrasil.Scheduling; using Yggdrasil.Util; @@ -107,6 +109,22 @@ public bool TryGet(long questObjectId, out Quest quest) } } + /// + /// Gets quest by id and returns it via out, returns false if the + /// quest didn't exist. + /// + /// + /// + /// + public bool TryGetById(QuestId questId, out Quest quest) + { + lock (_syncLock) + { + quest = _quests.FirstOrDefault(a => a.Data.Id == questId); + return quest != null; + } + } + /// /// Returns a list of all active quests. /// @@ -177,6 +195,32 @@ public void UpdateObjectives(QuestObjectivesUpdateFunc u } } + /// + /// Iterates over the quests' modifiers, runs the given function + /// over all modifiers with the given type, and updates the quest + /// if any progresses changed. + /// + /// + /// + public void UpdateModifiers(QuestModifiersUpdateFunc updater) where TModifier : QuestModifier + { + lock (_syncLock) + { + foreach (var quest in _quests) + { + if (quest.Status != QuestStatus.InProgress) + continue; + + quest.UpdateModifiers(updater); + + if (quest.ChangesOnLastUpdate) + { + quest.UpdateUnlock(); + } + } + } + } + /// /// Starts quest for the character, returns false if the quest /// couldn't be started. @@ -393,6 +437,7 @@ public void Complete(QuestId questId, string objectiveIdent) if (!progress.Done) { progress.SetDone(); + quest.UpdateUnlock(); this.UpdateClient_UpdateQuest(quest); continue; } @@ -400,6 +445,14 @@ public void Complete(QuestId questId, string objectiveIdent) } } + /// + /// Completes the objective on all quests with the given id. + /// + /// + /// + public void CompleteObjective(QuestId questId, string objectiveIdent) + => this.Complete(questId, objectiveIdent); + /// /// Completes all quests with the given id and gives the rewards /// to the character. @@ -616,10 +669,40 @@ private LuaTable QuestToTable(Quest quest) var questTable = new LuaTable(); + // Convert map class name(s) to display name(s) + string locationName = null; + if (!string.IsNullOrEmpty(quest.Data.Location)) + { + var mapClassNames = quest.Data.Location.Split(','); + var mapNames = new List(); + + foreach (var mapClassName in mapClassNames) + { + var trimmedClassName = mapClassName.Trim(); + if (ZoneServer.Instance.World.TryGetMap(trimmedClassName, out var map)) + mapNames.Add(map.Data.Name); + else + mapNames.Add(trimmedClassName); + } + + locationName = string.Join(", ", mapNames); + } + + // Convert quest giver map class name to display name + string questGiverLocationName = null; + if (!string.IsNullOrEmpty(quest.Data.QuestGiverLocation)) + { + if (ZoneServer.Instance.World.TryGetMap(quest.Data.QuestGiverLocation, out var map)) + questGiverLocationName = map.Data.Name; + else + questGiverLocationName = quest.Data.QuestGiverLocation; + } + questTable.Insert("ObjectId", "0x" + quest.ObjectId.ToString("X16")); questTable.Insert("ClassId", "0x" + quest.Data.Id.Value.ToString("X16")); questTable.Insert("Name", quest.Data.Name); questTable.Insert("Description", quest.Data.Description); + questTable.Insert("Location", locationName); questTable.Insert("Type", quest.Data.Type.ToString()); questTable.Insert("Level", quest.Data.Level); questTable.Insert("Status", quest.Status.ToString()); @@ -629,6 +712,14 @@ private LuaTable QuestToTable(Quest quest) questTable.Insert("Objectives", objectivesTable); questTable.Insert("Rewards", rewardsTable); + // Add quest giver information if available + if (!string.IsNullOrEmpty(quest.Data.StartNpcUniqueName)) + questTable.Insert("QuestGiver", quest.Data.StartNpcUniqueName); + + // Add quest giver location if available + if (!string.IsNullOrEmpty(questGiverLocationName)) + questTable.Insert("QuestGiverLocation", questGiverLocationName); + return questTable; } @@ -653,6 +744,31 @@ private LuaTable ObjectivesToTable(Quest quest) objectiveTable.Insert("Count", progress.Count); objectiveTable.Insert("TargetCount", objective.TargetCount); + // Add monster names for collection objectives with drop modifiers + if (objective is CollectItemObjective collectObjective) + { + var monsterNames = new List(); + foreach (var modifier in quest.Data.Modifiers) + { + if (modifier is ItemDropModifier dropModifier && dropModifier.ItemId == collectObjective.ItemId) + { + foreach (var monsterId in dropModifier.MonsterIds) + { + if (ZoneServer.Instance.Data.MonsterDb.TryFind(monsterId, out var monsterData)) + monsterNames.Add(monsterData.Name); + } + } + } + + if (monsterNames.Count > 0) + { + var monstersTable = new LuaTable(); + foreach (var monsterName in monsterNames) + monstersTable.Insert(monsterName); + objectiveTable.Insert("Monsters", monstersTable); + } + } + objectivesTable.Insert(objectiveTable); } diff --git a/src/ZoneServer/World/Quests/Modifiers/ItemDropModifier.cs b/src/ZoneServer/World/Quests/Modifiers/ItemDropModifier.cs new file mode 100644 index 000000000..cdc92cb4a --- /dev/null +++ b/src/ZoneServer/World/Quests/Modifiers/ItemDropModifier.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Melia.Shared.Game.Const; +using Melia.Zone.Events.Arguments; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.Monsters; +using Yggdrasil.Util; + +namespace Melia.Zone.World.Quests.Modifiers +{ + /// + /// A modifier that gives a chance to add drops to a monster. + /// + public class ItemDropModifier : QuestModifier + { + /// + /// Returns the item id that a monster drops. + /// + public int ItemId { get; } + + /// + /// Returns the amount of chance the monster has to drop an item. + /// + public float DropChance { get; } + + /// + /// Returns the tags which monsters must match to qualify for this + /// objective. + /// + public HashSet MonsterIds { get; } + + public ItemDropModifier(int itemId, float dropChance, params int[] monsterIds) + { + this.ItemId = itemId; + this.DropChance = dropChance; + this.MonsterIds = new HashSet(monsterIds); + } + + public ItemDropModifier(int itemId, float dropChance, params string[] monsterIds) + { + this.ItemId = itemId; + this.DropChance = dropChance; + this.MonsterIds = new HashSet(monsterIds.Length); + + for (var i = 0; i < monsterIds.Length; i++) + { + var monster = monsterIds[i]; + if (ZoneServer.Instance.Data.MonsterDb.TryFind(monster, out var data)) + this.MonsterIds.Add(data.Id); + } + } + + /// + /// Sets up event subscriptions. + /// + public override void Load() + { + ZoneServer.Instance.ServerEvents.EntityKilled.Subscribe(this.OnEntityKilled); + } + + /// + /// Cleans up event subscriptions. + /// + public override void Unload() + { + ZoneServer.Instance.ServerEvents.EntityKilled.Unsubscribe(this.OnEntityKilled); + } + + /// + /// Called when a character dies. + /// + /// + /// + private void OnEntityKilled(object sender, CombatEventArgs args) + { + if (args.Target is not Mob monster) + return; + + if (args.Attacker is not Character character) + return; + + character.Quests.UpdateModifiers((quest, modifier, progress) => + { + if (modifier.IsTarget(monster)) + { + // Check drop chance + var rnd = RandomProvider.Get(); + if (rnd.NextDouble() < modifier.DropChance) + { + // Add item directly to player's inventory + character.Inventory.Add(modifier.ItemId, 1, InventoryAddType.PickUp); + } + } + }); + } + + /// + /// Returns true if the given monster is a target for this objective. + /// + /// + /// + private bool IsTarget(IMonster monster) + { + return this.MonsterIds.Contains(monster.Id); + } + } +} diff --git a/src/ZoneServer/World/Quests/Quest.cs b/src/ZoneServer/World/Quests/Quest.cs index 595f5da36..79b3a5961 100644 --- a/src/ZoneServer/World/Quests/Quest.cs +++ b/src/ZoneServer/World/Quests/Quest.cs @@ -205,6 +205,42 @@ public void UpdateObjectives(QuestObjectivesUpdateFunc u this.ChangesOnLastUpdate = anythingChanged; } + /// + /// Iterates over the quest's modifiers and runs the given function + /// on all modifiers with the given type. If any progresses changed, + /// the ChangesOnLastUpdate property will be true. + /// + /// + /// + public void UpdateModifiers(QuestModifiersUpdateFunc updater) where TModifier : QuestModifier + { + var quest = this; + var anythingChanged = false; + + foreach (var progress in quest.Progresses) + { + if (!progress.Unlocked) + continue; + + var count = progress.Count; + var done = progress.Done; + var unlocked = progress.Unlocked; + + for (var i = 0; i < quest.Data.Modifiers.Count; i++) + { + var modifier = quest.Data.Modifiers[i]; + if (modifier is not TModifier tModifier) + continue; + updater(this, tModifier, progress); + } + + if (progress.Count != count || progress.Done != done || progress.Unlocked != unlocked) + anythingChanged = true; + } + + this.ChangesOnLastUpdate = anythingChanged; + } + /// /// Marks all of the quest's objectives as done. /// @@ -224,6 +260,15 @@ public void CompleteObjectives() /// public delegate void QuestObjectivesUpdateFunc(Quest quest, TObjective objective, QuestProgress progress) where TObjective : QuestObjective; + /// + /// A function used to update a quest's modifiers. + /// + /// + /// + /// + /// + public delegate void QuestModifiersUpdateFunc(Quest quest, TModifier modifier, QuestProgress progress) where TModifier : QuestModifier; + /// /// Specifies a quest's current status. /// diff --git a/src/ZoneServer/World/Quests/QuestData.cs b/src/ZoneServer/World/Quests/QuestData.cs index 36750878c..6524d5b28 100644 --- a/src/ZoneServer/World/Quests/QuestData.cs +++ b/src/ZoneServer/World/Quests/QuestData.cs @@ -23,6 +23,21 @@ public class QuestData /// public string Description { get; set; } + /// + /// Gets or sets the quest's location (map class name). + /// + public string Location { get; set; } + + /// + /// Gets or sets the map class name where the quest giver is located. + /// + public string QuestGiverLocation { get; set; } + + /// + /// Gets or sets the quest giver NPC unique name. + /// + public string StartNpcUniqueName { get; set; } + /// /// Gets or sets the quest's type. /// @@ -79,6 +94,11 @@ public class QuestData /// met to receive the quest automatically. /// public List Prerequisites { get; } = new List(); + + /// + /// Returns a list of the quest's modifiers. + /// + public List Modifiers { get; } = new List(); } /// diff --git a/src/ZoneServer/World/Quests/QuestModifier.cs b/src/ZoneServer/World/Quests/QuestModifier.cs new file mode 100644 index 000000000..8689af6dc --- /dev/null +++ b/src/ZoneServer/World/Quests/QuestModifier.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Melia.Zone.World.Quests +{ + /// + /// Base class for quest modifiers. + /// + public abstract class QuestModifier + { + /// + /// Called when a quest that uses this modifier is initially loaded. + /// The modifier should set up its progress tracking here, such as + /// subscribing to events. + /// + /// + /// Every quest modifier is loaded only once, to keep track of + /// the progress of all modifiers that use this type. + /// + public virtual void Load() + { + } + + /// + /// Called when the quests that used this modifier were unloaded. + /// The modifier should clean up its progress tracking measures + /// here, such as unsubscribing from events. + /// + public virtual void Unload() + { + } + } +} diff --git a/system/scripts/zone/content/test/quests/c_klaipe_quests.cs b/system/scripts/zone/content/test/quests/c_klaipe_quests.cs new file mode 100644 index 000000000..821eb7dbe --- /dev/null +++ b/system/scripts/zone/content/test/quests/c_klaipe_quests.cs @@ -0,0 +1,540 @@ +//--- Melia Script ---------------------------------------------------------- +// Klaipeda Quest NPCs +//--- Description ----------------------------------------------------------- +// Quest NPCs in Klaipeda for post-demon war storyline. +//--------------------------------------------------------------------------- + +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Scripting; +using Melia.Zone.World.Quests; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.Characters.Components; +using Melia.Zone.World.Quests.Objectives; +using Melia.Zone.World.Quests.Prerequisites; +using Melia.Zone.World.Quests.Rewards; +using static Melia.Zone.Scripting.Shortcuts; + + +public class KlaipeQuestNpcsScript : GeneralScript +{ + protected override void Load() + { + // Withered Branch Collection Helper + //------------------------------------------------------------------------- + void AddWitheredBranchNpc(int sampleNumber, int x, int y, int direction) + { + AddNpc(152080, "Withered Branch", "c_Klaipe", x, y, direction, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("Klaipeda", 1002); + + if (!character.Quests.IsActive(questId)) + { + await dialog.Msg("A dying and burnt branch with blackened, withering leaves. The corruption has taken hold of the vegetation."); + return; + } + + var variableKey = $"Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample{sampleNumber}"; + var collected = character.Variables.Perm.GetBool(variableKey, false); + + if (collected) + { + await dialog.Msg("You've already collected a branch from this location."); + return; + } + + var result = await character.TimeActions.StartAsync("Collecting branch...", "Cancel", "SITGROPE", TimeSpan.FromSeconds(3)); + + if (result == TimeActionResult.Completed) + { + character.Inventory.Add(650427, 1, InventoryAddType.PickUp); + character.Variables.Perm.Set(variableKey, true); + + var currentCount = character.Inventory.CountItem(650427); + character.ServerMessage($"Branches collected: {currentCount}/5"); + + if (currentCount >= 5) + { + character.ServerMessage("All branches collected! Return to Elara."); + } + } + else + { + character.ServerMessage("Collection cancelled."); + } + }); + } + + // Caravan Master Marcus + //------------------------------------------------------------------------- + AddNpc(20165, "[Caravan Master] Marcus", "c_Klaipe", -230, -1025, 90, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("Klaipeda", 1001); + + dialog.SetTitle("Marcus"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("Ah, traveler! Welcome to Klaipeda. We're working hard to rebuild after the war, but it's been... challenging."); + await dialog.Msg("The roads are no longer safe. Monsters roam freely, and our supply caravans can't get through without proper protection."); + + if (await dialog.YesNo("Would you be willing to help escort a supply caravan? The pay is modest, but you'd be helping our city recover.")) + { + character.Quests.Start(questId); + await dialog.Msg("Excellent! Head to the western road and clear out the monsters. We need at least 5 of each of them gone before the caravan can safely pass."); + await dialog.Msg("Be careful out there. These aren't ordinary beasts - some say they're tainted by lingering demon magic from the war."); + } + else + { + await dialog.Msg("I understand. It's dangerous work. If you change your mind, I'll be here organizing our supply efforts."); + } + } + else if (character.Quests.IsActive(questId)) + { + if (!character.Quests.TryGetById(questId, out var quest)) return; + + if (!quest.ObjectivesCompleted) + { + // Get progress for each objective + quest.TryGetProgress("killHanaming", out var progressHanaming); + quest.TryGetProgress("killOnion", out var progressOnion); + quest.TryGetProgress("killLeafDiving", out var progressLeafDiving); + quest.TryGetProgress("killInfrorocktor", out var progressInfrorocktor); + + await dialog.Msg("Keep up the good work! The western road is particularly dangerous near the forest edge. Watch your back out there."); + } + else + { + await dialog.Msg("Fantastic work! The road is clear, and our caravan just reported safe passage!"); + await dialog.Msg("You've earned this reward, and more importantly, you've helped dozens of families who depend on these supplies."); + + character.Quests.Complete(questId); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("Thanks to you, our supply lines are much safer. You're always welcome in Klaipeda, friend."); + await dialog.Msg("If you're looking for more work, you might check with Elara near the river. She's been dealing with some... unusual problems."); + } + }); + + // River Warden Elara + //------------------------------------------------------------------------- + AddNpc(147473, "[River Warden] Elara", "c_Klaipe", -452, 910, 180, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("Klaipeda", 1002); + + dialog.SetTitle("Elara"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("This river is the main water source for the residents of Klaipeda. Without it, the city cannot survive."); + + var response = await dialog.Select("What brings you to the riverside?", + Option("Can I help?", "help"), + Option("Tell me more about the river", "info"), + Option("Just passing through", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("You would help? Thank the goddesses! The corruption is spreading and we need to act quickly."); + + if (await dialog.YesNo("Can you help me by removing corrupted tree branches from the river? They're scattered along the riverbanks and need to be cleared.")) + { + character.Quests.Start(questId); + await dialog.Msg("Thank you so much! Please remove 5 Withered Branches from different locations along the river."); + } + break; + + case "info": + await dialog.Msg("The river used to flow crystal clear, providing fresh water for drinking, cooking, and daily life."); + await dialog.Msg("But corruption from the forest upstream has been falling into the river. Corrupted tree branches and debris are poisoning our water supply."); + break; + + case "leave": + await dialog.Msg("Safe travels. Try not to drink the river water until we've cleaned it..."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var burntBranchCount = character.Inventory.CountItem(650427); + + if (burntBranchCount < 5) + { + await dialog.Msg($"Have you collected the branches? You have {burntBranchCount} out of 5 so far."); + await dialog.Msg("Check the riverbanks for withered bushes and corrupted vegetation. They're the source of the contamination."); + } + else + { + await dialog.Msg("You've collected all the branches! The river is already flowing clearer!"); + await dialog.Msg("{#666666}*She inspects the water, a relieved smile crossing her face*{/}"); + await dialog.Msg("The water quality is improving already. The residents will be able to use the river safely again."); + await dialog.Msg("You've saved countless lives today. Here - take this as thanks for your help."); + + character.Inventory.Remove(650427, 5, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("The river is flowing much clearer now. The residents are grateful to have clean water again."); + await dialog.Msg("Your help was invaluable. Without removing those corrupted branches, the water would have become completely unusable."); + } + }); + + // Withered Branch Collection Points + //------------------------------------------------------------------------- + AddWitheredBranchNpc(1, -542, 987, 0); + AddWitheredBranchNpc(2, -744, 770, 0); + AddWitheredBranchNpc(3, -940, 668, 0); + AddWitheredBranchNpc(4, -1067, 373, 0); + AddWitheredBranchNpc(5, -988, 29, 0); + AddWitheredBranchNpc(6, -259, 960, 0); + + // Reconstruction Coordinator Viktor + //------------------------------------------------------------------------- + AddNpc(20113, "[Reconstruction Coordinator] Viktor", "c_Klaipe", 814, -339, 90, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("Klaipeda", 1003); + + dialog.SetTitle("Viktor"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("Every day we rebuild, and every night I count the buildings still in ruins. This city was once magnificent, you know."); + await dialog.Msg("The demon war left scars on more than just the land. Entire districts were destroyed. But we'll rebuild - we must."); + + var response = await dialog.Select("Would you like to help us rebuilding our homes?", + Option("I'd like to help", "help"), + Option("What do you need most?", "needs"), + Option("Good luck with that", "leave") + ); + + switch (response) + { + + case "help": + await dialog.Msg("A volunteer! We desperately need building materials - wood planks specifically."); + + if (await dialog.YesNo("The forests outside the city have salvageable materials, but they're monster-infested. Can you gather 20 Dry Wood for us?")) + { + character.Quests.Start(questId); + await dialog.Msg("Wonderful! You'll find wood to the west and east of the city. Salvage what you can and bring back to me."); + await dialog.Msg("Watch out for scavenger monsters - they've made homes in the forests and don't take kindly to visitors."); + } + break; + + case "needs": + await dialog.Msg("Wood - the basic material of construction. But they're hard to come by now."); + await dialog.Msg("The old quarries and lumber camps were overrun during the war. We'd need someone to venture out and reclaim them."); + break; + + case "leave": + await dialog.Msg("We rebuild with or without help. It's what we do."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var woodCount = character.Inventory.CountItem(667203); + + if (woodCount >= 20) + { + await dialog.Msg("You've brought everything we need! Excellent work!"); + await dialog.Msg("With these materials, we can repair at least three houses. That's three more families with roofs over their heads."); + await dialog.Msg("You have my gratitude, and the gratitude of everyone in those homes."); + + character.Inventory.Remove(667203, 20, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg($"Thank you for gathering materials! We need 20 wood planks from Dry Woods (you have {woodCount})."); + await dialog.Msg("The forests are dangerous, but the materials there are essential for our recovery."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("The houses you helped rebuild are occupied now. I see children playing in those yards every day."); + await dialog.Msg("That's what reconstruction is really about - giving people their lives back."); + } + }); + + // Cursed Refugee Aldric + //------------------------------------------------------------------------- + AddNpc(147382, "[Cursed Refugee] Aldric", "c_voodoo", -62, -16, 90, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("Klaipeda", 1005); + + dialog.SetTitle("Aldric"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*The man is wrapped in heavy chains, his eyes haunted*{/}"); + await dialog.Msg("Stay back... I'm dangerous. The curse... it makes me lose control when the sun sets."); + + var response = await dialog.Select("Stay back... I'm dangerous. The curse... it makes me lose control when the sun sets.", + Option("How can I help?", "help"), + Option("Tell me about your curse", "curse"), + Option("Leave him be", "leave") + ); + + switch (response) + { + case "curse": + await dialog.Msg("A demon lord marked my village during the war. Those who survived... we carry his curse."); + await dialog.Msg("At night, we become violent, mindless. These chains are the only thing keeping the citizens safe from me."); + break; + + case "help": + await dialog.Msg("You would help someone like me? I... thank you."); + + if (await dialog.YesNo("There might be a way to break the curse. In my village ruins, there's an ancient chest with a broken amulet. If you could retrieve it...")) + { + character.Quests.Start(questId); + await dialog.Msg("The broken amulet absorbs dark energies - it might be able to draw out the curse from my body."); + await dialog.Msg("The cursed villagers still wander the ruins. They were my neighbors once. My friends. Now they're just... shells."); + await dialog.Msg("Head east from the Miner's Village. You'll find the ruins there, and the chest with the amulet. Please... hurry."); + } + break; + + case "leave": + await dialog.Msg("{#666666}*He nods silently, chains rattling*{/}"); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var hasAmulet = character.Inventory.HasItem(668038); + + if (hasAmulet) + { + await dialog.Msg("{#FFFF00}*His eyes widen as he sees the broken amulet*{/}"); + await dialog.Msg("You found it! You actually found it! Please, place it against my chest - carefully, the curse fights back..."); + await dialog.Msg("{#FF0000}*As the amulet touches him, it begins to glow. Dark smoke pours from his body into the amulet. He screams in pain, then... silence*{/}"); + await dialog.Msg("It's... it's gone. The curse is broken! The amulet absorbed all the dark energy!"); + await dialog.Msg("Thank you, stranger. You've given me my life back. Here - this was my father's. He'd want you to have it."); + + character.Inventory.Remove(668038, 1, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg("The broken amulet should be in an ancient chest somewhere in the village ruins. Please, hurry. Every night the curse grows stronger."); + await dialog.Msg("Eastwood Village is east from the Miner's Village. Look for the ruins."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("I'm free of the curse, but my village is still lost. Perhaps one day I'll return and help cleanse it."); + await dialog.Msg("Until then, I owe you a debt I can never repay. You've given me hope when I had none."); + } + }); + } +} + +//----------------------------------------------------------------------------- +// Quests +//----------------------------------------------------------------------------- + +// Quest 1001: Caravan Escort +//----------------------------------------------------------------------------- +public class KlaipeCaravanEscortQuest : QuestScript +{ + protected override void Load() + { + SetId("Klaipeda", 1001); + SetName("Caravan Escort"); + SetDescription("Clear the western road of monsters so supply caravans can safely reach Klaipeda."); + SetLocation("f_siauliai_west"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Caravan Master] Marcus", "c_Klaipe"); + + // Kill 15 monsters on the western road (using common monsters as placeholder) + AddObjective("killHanaming", "Kill Hanamings", new KillObjective(5, new[] { MonsterId.Hanaming })); + AddObjective("killOnion", "Kill Kepas", new KillObjective(5, new[] { MonsterId.Onion })); + AddObjective("killLeafDiving", "Kill Leaf Diving", new KillObjective(5, new[] { MonsterId.Leaf_Diving })); + AddObjective("killInfrorocktor", "Kill Infrorocktor", new KillObjective(5, new[] { MonsterId.InfroRocktor })); + + // Rewards + AddReward(new ExpReward(300, 200)); + AddReward(new SilverReward(3000)); + AddReward(new ItemReward(640002, 15)); // HP potions + AddReward(new ItemReward(640005, 15)); // SP potions + AddReward(new ItemReward(640080, 2)); // Lv1 EXP Cards + } +} + +// Quest 1002: River Corruption Investigation +//----------------------------------------------------------------------------- +public class KlaipeRiverCorruptionQuest : QuestScript +{ + protected override void Load() + { + SetId("Klaipeda", 1002); + SetName("Cleansing the River"); + SetDescription("Help River Warden Elara by removing corrupted tree branches that are polluting Klaipeda's main water source."); + SetLocation("c_Klaipe"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[River Warden] Elara", "c_Klaipe"); + + // Collect 5 burnt branches from withered branches + AddObjective("collectBranches", "Collect Withered Burnt Branches", + new CollectItemObjective(650427, 5)); + + // Rewards + AddReward(new ExpReward(200, 150)); + AddReward(new SilverReward(2500)); + AddReward(new ItemReward(640002, 5)); // HP potions + AddReward(new ItemReward(640005, 5)); // SP potions + AddReward(new ItemReward(640097, 5)); // Stamina Potions + AddReward(new ItemReward(640073, 5)); // Klaipeda Warp Scroll + AddReward(new ItemReward(640080, 2)); // Lv1 EXP Cards + } + + public override void OnComplete(Character character, Quest quest) + { + // Unlock follow-up quest about purification ritual (future content) + character.Variables.Perm.Set("Melia.Quests.Klaipeda.Quest1002.KlaipeRiverQuestComplete", true); + + // Remove quest items + character.Inventory.Remove(650427, character.Inventory.CountItem(650427), + InventoryItemRemoveMsg.Destroyed); + + // Clear all collection flags so they can be reused if quest becomes repeatable + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample1"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample2"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample3"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample4"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample5"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample6"); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(650427, character.Inventory.CountItem(650427), + InventoryItemRemoveMsg.Destroyed); + + // Clear all collection flags so player can re-collect if they re-accept the quest + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample1"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample2"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample3"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample4"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample5"); + character.Variables.Perm.Remove("Melia.Quests.Klaipeda.Quest1002.KlaipePlantSample6"); + } +} + +// Quest 1003: Reconstruction Materials +//----------------------------------------------------------------------------- +public class KlaipeReconstructionQuest : QuestScript +{ + protected override void Load() + { + SetId("Klaipeda", 1003); + SetName("Rebuilding Klaipeda"); + SetDescription("Gather wood planks from the forest monsters to help rebuild Klaipeda."); + SetLocation("f_siauliai_west", "f_siauliai_out"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Reconstruction Coordinator] Viktor", "c_Klaipe"); + + // Add quest item drops from specific monsters when quest is active + // Wood planks drop from wood-type monsters + AddDrop(667203, 0.3f, MonsterId.Hanaming); + AddDrop(667203, 0.3f, MonsterId.Onion); + AddDrop(667203, 0.3f, MonsterId.Leaf_Diving); + AddDrop(667203, 0.3f, MonsterId.Onion_Red); + AddDrop(667203, 0.3f, MonsterId.Jukopus); + AddDrop(667203, 0.3f, MonsterId.Goblin_Spear); + + // Collect materials + AddObjective("collectWood", "Collect Dry Wood", + new CollectItemObjective(667203, 20)); + + // Rewards + AddReward(new ExpReward(400, 300)); + AddReward(new SilverReward(3000)); + AddReward(new ItemReward(640002, 15)); // HP potions + AddReward(new ItemReward(640005, 15)); // SP potions + AddReward(new ItemReward(640080, 3)); // Lv1 EXP Cards + } + + public override void OnComplete(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(667203, character.Inventory.CountItem(667203), + InventoryItemRemoveMsg.Destroyed); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(667203, character.Inventory.CountItem(667203), + InventoryItemRemoveMsg.Destroyed); + } +} + +// Quest 1005: Cursed Refugee +//----------------------------------------------------------------------------- +public class KlaipeCursedRefugeeQuest : QuestScript +{ + protected override void Load() + { + SetId("Klaipeda", 1005); + SetName("Breaking the Curse"); + SetDescription("Retrieve the broken amulet from Eastwood Village at east of Miner's Village to break Aldric's curse. The amulet absorbs dark energies and may be able to draw out the curse."); + SetLocation("c_siauliai_out"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Cursed Refugee] Aldric", "c_voodoo"); + + // Retrieve the broken amulet from the chest + AddObjective("retrieveAmulet", "Retrieve the broken amulet", + new CollectItemObjective(668038, 1)); + + AddReward(new ExpReward(1500, 750)); + AddReward(new SilverReward(7000)); + AddReward(new ItemReward(640002, 10)); // HP potions + AddReward(new ItemReward(640005, 10)); // SP potions + AddReward(new ItemReward(111007, 1)); // Yorgis Knife + AddReward(new ItemReward(640080, 4)); // Lv1 Exp Card + } + + public override void OnComplete(Character character, Quest quest) + { + // Remove quest amulet if player still has it (shouldn't happen but just in case) + character.Inventory.Remove(668038, character.Inventory.CountItem(668038), + InventoryItemRemoveMsg.Destroyed); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest amulet if player has it + character.Inventory.Remove(668038, character.Inventory.CountItem(668038), + InventoryItemRemoveMsg.Destroyed); + } +} diff --git a/system/scripts/zone/content/test/quests/f_siauliai_out_quests.cs b/system/scripts/zone/content/test/quests/f_siauliai_out_quests.cs new file mode 100644 index 000000000..49f151551 --- /dev/null +++ b/system/scripts/zone/content/test/quests/f_siauliai_out_quests.cs @@ -0,0 +1,766 @@ +//--- Melia Script ---------------------------------------------------------- +// Siauliai Out (Miner's Village) Quest NPCs +//--- Description ----------------------------------------------------------- +// Quest NPCs in Miner's Village for post-demon war storyline. +// Also includes Eastwood Village Ruins (Quest: Klaipeda/1005) +//--------------------------------------------------------------------------- + +using System; +using System.Threading; +using Melia.Shared.Game.Const; +using Melia.Shared.World; +using Melia.Zone; +using Melia.Zone.Scripting; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.Characters.Components; +using Melia.Zone.World.Maps; +using Melia.Zone.World.Quests; +using Melia.Zone.World.Quests.Objectives; +using Melia.Zone.World.Quests.Prerequisites; +using Melia.Zone.World.Quests.Rewards; +using Yggdrasil.Logging; +using Yggdrasil.Util; +using static Melia.Zone.Scripting.Shortcuts; + + +public class FSiauliaiOutQuestNpcsScript : GeneralScript +{ + protected override void Load() + { + // ===================================================================== + // MINER'S VILLAGE QUEST NPCs + // ===================================================================== + + // Mine Foreman Karolis + //------------------------------------------------------------------------- + AddNpc(20150, "[Mine Foreman] Karolis", "f_siauliai_out", 163, -986, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_out", 1001); + + dialog.SetTitle("Karolis"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("Welcome to Miner's Village, traveler. I'm the foreman of the Crystal Mine operations."); + await dialog.Msg("{#666666}*He looks exhausted, dark circles under his eyes*{/}"); + + var response = await dialog.Select("But our work has ground to a halt. The goblins from the eastern hills have been raiding our supply routes.", + Option("How can I help?", "help"), + Option("Why not hire guards?", "guards"), + Option("I'm just passing through", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("A willing hand! The goddesses smile on us today!"); + + if (await dialog.YesNo("The Vubbe goblins have been stealing our mining supplies and terrorizing workers. If you could clear them out near the village entrance, we could resume operations.")) + { + character.Quests.Start(questId); + await dialog.Msg("Excellent! Drive back those Vubbes and we'll make it worth your while."); + await dialog.Msg("Be careful - they may look comical, but they're surprisingly organized and dangerous in groups."); + } + break; + + case "guards": + await dialog.Msg("{#666666}*He laughs bitterly*{/} Guards? With what silver? The mine has barely produced since the demon war disrupted our operations."); + await dialog.Msg("Most guards are busy protecting the main cities. Small mining villages like ours are on our own."); + break; + + case "leave": + await dialog.Msg("Safe travels. Watch yourself on the roads - it's not safe out here anymore."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + if (!character.Quests.TryGetById(questId, out var quest)) return; + + if (!quest.ObjectivesCompleted) + { + await dialog.Msg("Keep pushing those Vubbes back! They've been making our lives miserable for weeks now."); + } + else + { + await dialog.Msg("You did it! I can already see the goblins retreating from the area!"); + await dialog.Msg("Our miners can finally work without fear. The village owes you a debt of gratitude."); + await dialog.Msg("Here's your payment, and some supplies from our stores. Thank you, truly."); + + character.Quests.Complete(questId); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("Thanks to you, our mining operations are back on schedule. The village is safer now."); + await dialog.Msg("If you're looking for more work, you should speak with Elara near the old bridge. She mentioned needing help with something."); + } + }); + + // Village Alchemist Elara + //------------------------------------------------------------------------- + AddNpc(147473, "[Village Alchemist] Elara", "f_siauliai_out", -910, -1861, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_out", 1002); + + dialog.SetTitle("Elara"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*A young woman carefully grinds herbs with a mortar and pestle*{/}"); + await dialog.Msg("Oh! A visitor! Welcome to my humble workshop. I'm Elara, the village alchemist."); + + var response = await dialog.Select("I'm working on a remedy for a mysterious illness affecting some of the miners, but I'm missing key ingredients.", + Option("What ingredients do you need?", "help"), + Option("What kind of illness?", "illness"), + Option("Good luck with your research", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("You'd help me? Wonderful! I need samples from the local wildlife."); + + if (await dialog.YesNo("I need Red Kepa Skins and Jukopus Cores. The Red Kepas and Jukopus monsters should have them. Will you gather these for me?")) + { + character.Quests.Start(questId); + await dialog.Msg("Perfect! I need 10 Red Kepa Skins and 8 Jukopus Cores. Both types of monsters can be found in the areas surrounding this village."); + await dialog.Msg("The sooner I have these ingredients, the sooner I can help the afflicted miners!"); + } + break; + + case "illness": + await dialog.Msg("It's strange... miners who work deep in the Crystal Mine have been experiencing weakness and fever."); + await dialog.Msg("I suspect it's related to prolonged exposure to the crystals. The demon war's magical residue may have contaminated them."); + await dialog.Msg("If I can create the right remedy, I might be able to counteract the effects and protect our workers."); + break; + + case "leave": + await dialog.Msg("Thank you. Those miners are counting on me... I can't let them down."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var kepaCount = character.Inventory.CountItem(650463); + var jukopusCount = character.Inventory.CountItem(650464); + + if (kepaCount >= 10 && jukopusCount >= 8) + { + await dialog.Msg("{#666666}*Her eyes light up as she examines the materials*{/}"); + await dialog.Msg("These are perfect specimens! The quality is excellent!"); + await dialog.Msg("{#666666}*She quickly begins mixing the ingredients in a glass vial*{/}"); + await dialog.Msg("Yes... yes! The mixture is turning the right color. This should work!"); + await dialog.Msg("I'll start treating the miners immediately. You've saved lives today. Please, accept this as thanks."); + + character.Inventory.Remove(650463, 10, InventoryItemRemoveMsg.Given); + character.Inventory.Remove(650464, 8, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg($"How's the gathering going? I need 10 Red Kepa Skins and 8 Jukopus Cores."); + await dialog.Msg("The miners are getting worse each day. Please hurry if you can."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("The remedy worked perfectly! The affected miners are already showing improvement."); + await dialog.Msg("Your contribution to this village will not be forgotten."); + } + }); + + // Mysterious Trader Rokas + //------------------------------------------------------------------------- + AddNpc(147426, "[Mysterious Trader] Rokas", "f_siauliai_out", -1341, -1672, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_out", 1003); + + dialog.SetTitle("Rokas"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*A mysterious person examines a strange crystal, muttering to himself*{/}"); + await dialog.Msg("Hmm? A stranger approaches. Tell me, do you believe in fate?"); + + var response = await dialog.Select("I've been searching for three pieces of an ancient broken sword hidden near this village. They were scattered during the chaos of the demon war.", + Option("I'll help you find them", "help"), + Option("What kind of sword?", "relics"), + Option("Sounds suspicious...", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("Excellent! Your assistance is most appreciated."); + + if (await dialog.YesNo("The sword pieces are hidden in three separate locations around the village. Will you retrieve all three broken sword pieces for me?")) + { + character.Quests.Start(questId); + await dialog.Msg("Splendid! Look for the broken sword pieces somewhere in this village. They'll appear as ancient blade fragments."); + await dialog.Msg("Be warned - the areas are infested with monsters. Stay alert."); + } + break; + + case "relics": + await dialog.Msg("Pieces of an ancient legendary sword, shattered during a great battle before the war. Each piece still contains powerful magical energy."); + await dialog.Msg("I tracked them to this region, but the monsters have made searching too dangerous for someone like me."); + break; + + case "leave": + await dialog.Msg("{#666666}*He shrugs*{/} Suspicious? Perhaps. But the sword pieces are real, and they're valuable."); + await dialog.Msg("If you change your mind, I'll be here. These fragments aren't going anywhere without help."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var swordPieceCount = character.Inventory.CountItem(650522); + + if (swordPieceCount >= 3) + { + await dialog.Msg("{#666666}*He eagerly takes the sword pieces and examines them closely*{/}"); + await dialog.Msg("Remarkable! All three pieces, intact after all these years!"); + await dialog.Msg("{#666666}*He carefully fits the pieces together, and they begin to glow with magical energy*{/}"); + await dialog.Msg("The ancient power still flows through them. Collectors in Klaipeda will pay a fortune for these relics."); + await dialog.Msg("You've done well. Here's your payment, plus a bonus for your efficiency."); + + character.Inventory.Remove(650522, 3, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg($"Have you found the sword pieces? You have {swordPieceCount} of 3 so far."); + await dialog.Msg("Search the village carefully. The sword fragments are well hidden."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("Those sword fragments will help us understand the ancient wars better. Such powerful artifacts should be preserved."); + await dialog.Msg("Perhaps we'll work together again someday."); + } + }); + + // Broken Sword Piece Collection Points + //------------------------------------------------------------------------- + void AddSwordPieceNpc(int pieceNumber, int x, int z) + { + AddNpc(47170, "Broken Sword Fragment", "f_siauliai_out", x, z, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_out", 1003); + + if (!character.Quests.IsActive(questId)) + { + await dialog.Msg("{#666666}*A fragment of an ancient broken sword, still faintly glowing with magical energy*{/}"); + return; + } + + var variableKey = $"Melia.Quests.f_siauliai_out.Quest1003.SwordPiece{pieceNumber}"; + var collected = character.Variables.Perm.GetBool(variableKey, false); + + if (collected) + { + await dialog.Msg("{#666666}*You've already collected the sword piece from this location*{/}"); + return; + } + + var result = await character.TimeActions.StartAsync("Extracting broken sword piece...", "Cancel", "SITGROPE", TimeSpan.FromSeconds(4)); + + if (result == TimeActionResult.Completed) + { + character.Inventory.Add(650522, 1, InventoryAddType.PickUp); + character.Variables.Perm.Set(variableKey, true); + + var currentCount = character.Inventory.CountItem(650522); + character.ServerMessage($"Broken sword pieces collected: {currentCount}/3"); + + if (currentCount >= 3) + { + character.ServerMessage("{#FFD700}All sword pieces collected! Return to Rokas.{/}"); + } + } + else + { + character.ServerMessage("Extraction cancelled."); + } + }); + } + + AddSwordPieceNpc(1, -2165, -1546); + AddSwordPieceNpc(2, 1026, -847); + AddSwordPieceNpc(3, 1592, 556); + + // Retired Adventurer Magnus + //------------------------------------------------------------------------- + AddNpc(20156, "[Retired Adventurer] Magnus", "f_siauliai_out", 371, -1108, 0, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_out", 1004); + + dialog.SetTitle("Magnus"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*An elderly man stands with an empty look in his eyes*{/}"); + await dialog.Msg("Heh... another fresh face. You have the look of an adventurer about you."); + + var response = await dialog.Select("In my day, I explored every corner of this region. Now I'm just an old man with stories and regrets.", + Option("Do you need help with something?", "help"), + Option("Tell me a story", "story"), + Option("I should get going", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("Help? {#666666}*He looks surprised*{/} Well... there is something, if you're willing."); + + if (await dialog.YesNo("I dropped my father's pendant somewhere in the village when I was fleeing from monsters. It's all I have left of him. Could you search for it?")) + { + character.Quests.Start(questId); + await dialog.Msg("Thank you, young one. I believe I dropped it somewhere in the central area of the village."); + await dialog.Msg("It's a simple copper pendant, but it means the world to me. Please bring it back if you find it."); + } + else + { + await dialog.Msg("I understand. It's just a sentimental trinket anyway."); + } + break; + + case "story": + await dialog.Msg("Stories? I've got plenty. Did I tell you about the time I fought a Goblin King in the eastern caves?"); + await dialog.Msg("{#666666}*He pauses, looking wistful*{/}"); + await dialog.Msg("But that was before the war. Before everything changed. These days, I don't have the strength for such adventures."); + break; + + case "leave": + await dialog.Msg("Be careful out there. The monsters aren't what they used to be - they're more aggressive now."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + if (character.Inventory.HasItem(664156)) + { + await dialog.Msg("{#666666}*His hands tremble as he takes the pendant*{/}"); + await dialog.Msg("My father's pendant... I thought I'd lost it forever."); + await dialog.Msg("{#666666}*He clutches it to his chest, tears in his eyes*{/}"); + await dialog.Msg("This pendant has been in my family for four generations. It's the only thing I have left from the old days."); + await dialog.Msg("Here, take this. It's my old training blade. May it serve you better than these tired arms can wield it now."); + + character.Inventory.Remove(664156, 1, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg("Have you found my pendant? It should be somewhere in the village central area."); + await dialog.Msg("It's made of copper with my family crest engraved on it."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("{#666666}*He holds the pendant close*{/}"); + await dialog.Msg("Thank you again for returning this to me. You've given an old man his most precious memory back."); + } + }); + + // Magnus's Pendant + //------------------------------------------------------------------------- + AddNpc(155003, "Pile of Dirt", "f_siauliai_out", -105, -1136, 0, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_out", 1004); + var spawnedKey = $"Melia.Quests.f_siauliai_out.Quest1004.Pendant.Spawned"; + + if (!character.Quests.IsActive(questId)) + { + await dialog.Msg("{#666666}*A tarnished copper pendant lies half-buried in the dirt*{/}"); + return; + } + + if (character.Inventory.HasItem(664156)) + { + await dialog.Msg("{#666666}*You've already picked up the pendant*{/}"); + return; + } + + var result = await character.TimeActions.StartAsync("Picking up pendant...", "Cancel", "SITGROPE", TimeSpan.FromSeconds(3)); + + if (result == TimeActionResult.Completed) + { + character.Inventory.Add(664156, 1, InventoryAddType.PickUp); + character.ServerMessage("Found Magnus's pendant!"); + character.ServerMessage("{#FFD700}Return the pendant to Magnus.{/}"); + } + else + { + character.ServerMessage("Action cancelled."); + } + }); + + // ===================================================================== + // EASTWOOD VILLAGE RUINS (Quest: Klaipeda/1005) + // ===================================================================== + // SEVERE CURSE: This area is obviously corrupted (demon lord's direct touch) + // Dark atmosphere, curse marks, cursed villagers + // The broken amulet in the chest absorbs dark energies to break Aldric's curse + // ===================================================================== + + var curseQuestId = new QuestId("Klaipeda", 1005); + + // Village Entrance Sign + //------------------------------------------------------------------------- + AddNpc(40080, "Weathered Sign", "f_siauliai_out", 1893, 13, 0, async dialog => + { + await dialog.Msg("{#666666}*A faded wooden sign reads:*{/}"); + await dialog.Msg("{#FF0000}EASTWOOD VILLAGE - QUARANTINE{/}"); + await dialog.Msg("{#FF0000}Below it, hastily carved: 'DO NOT ENTER. CURSE ACTIVE. SURVIVORS EVACUATED.'{/}"); + + if (dialog.Player.Quests.IsActive(curseQuestId)) + { + await dialog.Msg("{#FFFF00}The broken amulet might be somewhere in the ruins. It's said to absorb dark energies.{/}"); + } + }); + + // Memorial Stone + //------------------------------------------------------------------------- + AddNpc(47212, "Memorial Stone", "f_siauliai_out", 1903, 288, 45, async dialog => + { + await dialog.Msg("{#666666}*A crude memorial stone stands here, covered in names*{/}"); + await dialog.Msg("'In memory of Eastwood Village. 36 souls lost to the demon lord's curse.'"); + await dialog.Msg("'May the goddesses guide you home.'"); + await dialog.Msg("{#666666}*Someone added beneath: 'Aldric survived. Last seen heading to Klaipeda.'{/}"); + }); + + // Chest with Broken Amulet + //------------------------------------------------------------------------- + AddNpc(147341, "Ancient Chest", "f_siauliai_out", 2105, 500, 0, async dialog => + { + var character = dialog.Player; + + if (!character.Quests.IsActive(curseQuestId)) + { + await dialog.Msg("{#666666}*An old chest, covered in dust and curse marks*{/}"); + await dialog.Msg("{#FF0000}*You feel an overwhelming urge not to touch it*{/}"); + return; + } + + var hasAmulet = character.Inventory.HasItem(668038); + + if (hasAmulet) + { + await dialog.Msg("{#666666}*The chest is empty - you've already taken the broken amulet*{/}"); + return; + } + + var result = await character.TimeActions.StartAsync("Opening chest...", "Cancel", "SITGROPE", TimeSpan.FromSeconds(3)); + + if (result == TimeActionResult.Completed) + { + await dialog.Msg("{#666666}*Inside the chest, you find a cracked amulet pulsing with dark energy*{/}"); + await dialog.Msg("{#006600}This must be the broken amulet Aldric mentioned. It seems to absorb the curse's dark energies.{/}"); + + character.Inventory.Add(668038, 1, InventoryAddType.PickUp); + await dialog.Msg("You obtained the Broken Amulet!"); + } + else + { + character.ServerMessage("Opening cancelled."); + } + }); + + // Cursed Villager #1 + //------------------------------------------------------------------------- + AddNpc(47210, "Cursed Villager", "f_siauliai_out", 1819, 295, 90, async dialog => + { + await dialog.Msg("{#666666}*A villager stands frozen in place, their body turned to stone*{/}"); + await dialog.Msg("{#666666}*Dark veins spread across the petrified surface like cracks in marble, pulsing with faint malevolent energy*{/}"); + await dialog.Msg("{#666666}*Their face is locked in an expression of terror - a final moment before the curse petrified them completely*{/}"); + }); + + // Cursed Villager #2 + //------------------------------------------------------------------------- + AddNpc(47210, "Cursed Villager", "f_siauliai_out", 1857, 508, 0, async dialog => + { + await dialog.Msg("{#666666}*Another petrified figure, frozen mid-stride as if trying to flee*{/}"); + await dialog.Msg("{#666666}*Black veins web across their stone skin, thicker and more prominent than the other victim*{/}"); + await dialog.Msg("{#666666}*The curse spread quickly here - you can see the panic frozen in their posture*{/}"); + await dialog.Msg("{#666666}*A faint dark mist seeps from the cracks in the stone, evidence of the lingering curse*{/}"); + }); + } +} + +//----------------------------------------------------------------------------- +// Quests +//----------------------------------------------------------------------------- + +// Quest 1001: Goblin Problem +//----------------------------------------------------------------------------- +public class FSiauliaiOutGoblinProblemQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_out", 1001); + SetName("The Goblin Menace"); + SetDescription("Help Mine Foreman Karolis by clearing out the Goblin Spears that have been raiding the village's supply routes."); + SetLocation("f_siauliai_out"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Mine Foreman] Karolis", "f_siauliai_out"); + + // Kill goblins + AddObjective("killGoblins", "Defeat Goblin Spears", + new KillObjective(18, new[] { MonsterId.Goblin_Spear })); + + // Rewards + AddReward(new ExpReward(700, 500)); + AddReward(new SilverReward(5000)); + AddReward(new ItemReward(640002, 10)); // HP potions + AddReward(new ItemReward(640005, 10)); // SP potions + AddReward(new ItemReward(601115, 1)); // Iron Bangle + AddReward(new ItemReward(640080, 6)); // Lv1 EXP Cards + } +} + +// Quest 1002: Alchemist's Remedy +//----------------------------------------------------------------------------- +public class FSiauliaiOutAlchemistRemedyQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_out", 1002); + SetName("The Cure for Crystal Sickness"); + SetDescription("Gather Red Kepa Skins and Jukopus Cores for Village Alchemist Elara so she can create a remedy for the miners suffering from crystal contamination."); + SetLocation("f_siauliai_out"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Village Alchemist] Elara", "f_siauliai_out"); + + // Add quest item drops + AddDrop(650463, 0.4f, MonsterId.Onion_Red); + AddDrop(650464, 0.3f, MonsterId.Jukopus); + + // Collect ingredients + AddObjective("collectKepa", "Collect Red Kepa Skins from Red Kepas", + new CollectItemObjective(650463, 10)); + AddObjective("collectJukopus", "Collect Jukopus Cores from Jukopus", + new CollectItemObjective(650464, 8)); + + // Rewards + AddReward(new ExpReward(800, 400)); + AddReward(new SilverReward(4000)); + AddReward(new ItemReward(640002, 8)); // HP potions + AddReward(new ItemReward(640005, 8)); // SP potions + AddReward(new ItemReward(926009, 1)); // Recipe - Miner Hammer + AddReward(new ItemReward(640080, 5)); // Lv1 EXP Cards + } + + public override void OnComplete(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(650463, character.Inventory.CountItem(650463), InventoryItemRemoveMsg.Destroyed); + character.Inventory.Remove(650464, character.Inventory.CountItem(650464), InventoryItemRemoveMsg.Destroyed); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(650463, character.Inventory.CountItem(650463), InventoryItemRemoveMsg.Destroyed); + character.Inventory.Remove(650464, character.Inventory.CountItem(650464), InventoryItemRemoveMsg.Destroyed); + } +} + +// Quest 1003: Ancient Sword Pieces +//----------------------------------------------------------------------------- +public class FSiauliaiOutAncientRelicsQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_out", 1003); + SetName("Fragments of the Ancient Blade"); + SetDescription("Help Mysterious Trader Rokas retrieve three pieces of an ancient broken sword hidden around Miner's Village. Search the village to find the legendary sword fragments."); + SetLocation("f_siauliai_out"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Mysterious Trader] Rokas", "f_siauliai_out"); + + // Collect broken sword pieces + AddObjective("collectSwordPieces", "Collect broken sword pieces hidden around the village", + new CollectItemObjective(650522, 3)); + + // Rewards + AddReward(new ExpReward(900, 600)); + AddReward(new SilverReward(12000)); + AddReward(new ItemReward(640100, 10)); // Small Recovery potions + AddReward(new ItemReward(640080, 6)); // Lv1 EXP Cards + } + + public override void OnComplete(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(650522, character.Inventory.CountItem(650522), InventoryItemRemoveMsg.Destroyed); + + // Clear collection flags + character.Variables.Perm.Remove("Melia.Quests.f_siauliai_out.Quest1003.SwordPiece1"); + character.Variables.Perm.Remove("Melia.Quests.f_siauliai_out.Quest1003.SwordPiece2"); + character.Variables.Perm.Remove("Melia.Quests.f_siauliai_out.Quest1003.SwordPiece3"); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(650522, character.Inventory.CountItem(650522), InventoryItemRemoveMsg.Destroyed); + + // Clear collection flags + character.Variables.Perm.Remove("Melia.Quests.f_siauliai_out.Quest1003.SwordPiece1"); + character.Variables.Perm.Remove("Melia.Quests.f_siauliai_out.Quest1003.SwordPiece2"); + character.Variables.Perm.Remove("Melia.Quests.f_siauliai_out.Quest1003.SwordPiece3"); + } +} + +// Quest 1004: Lost Pendant +//----------------------------------------------------------------------------- +public class FSiauliaiOutLostPendantQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_out", 1004); + SetName("A Father's Memory"); + SetDescription("Search Miner's Village for Magnus's lost copper pendant - a precious family heirloom that means everything to the retired adventurer."); + SetLocation("f_siauliai_out"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Retired Adventurer] Magnus", "f_siauliai_out"); + + // Find the pendant + AddObjective("findPendant", "Find Magnus's copper pendant in the village", + new CollectItemObjective(664156, 1)); + + // Rewards + AddReward(new ExpReward(400, 300)); + AddReward(new SilverReward(1500)); + AddReward(new ItemReward(640002, 4)); // HP potions + AddReward(new ItemReward(640005, 4)); // SP potions + AddReward(new ItemReward(640005, 1)); // Leather Sword + AddReward(new ItemReward(640080, 2)); // Lv1 EXP Cards + } + + public override void OnComplete(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(664156, character.Inventory.CountItem(664156), InventoryItemRemoveMsg.Destroyed); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(664156, character.Inventory.CountItem(664156), InventoryItemRemoveMsg.Destroyed); + } +} + +// ===================================================================== +// EASTWOOD VILLAGE RUINS (Quest: Klaipeda/1005) +// ===================================================================== +// SEVERE CURSE: This script petrifies the users in the area every +// few seconds. +// ===================================================================== +public class FSiauliaiOutPetrificationZoneScript : GeneralScript +{ + // Configuration - Adjust these values as needed + //------------------------------------------------------------------------- + + // Center of the petrification zone + private readonly Position ZoneCenter = new Position(1807, 148, 341); + + // Radius of the zone in units + private readonly float ZoneRadius = 400f; + + // How often to check and apply the debuff (in seconds) + private readonly int CheckIntervalSeconds = 15; + + // Duration of the Petrification debuff (in seconds) + private readonly int DebuffDurationSeconds = 3; + + // Buff level (affects strength of the debuff) + private readonly int DebuffLevel = 1; + + //------------------------------------------------------------------------- + + private Timer _petrificationTimer; + private Map _targetMap; + + protected override void Load() + { + // Get the map reference + if (!ZoneServer.Instance.World.TryGetMap("f_siauliai_out", out var map)) + { + Log.Error("Petrification Zone Script: Map 'f_siauliai_out' not found."); + return; + } + + _targetMap = map; + + // Start the periodic check timer + _petrificationTimer = new Timer( + CheckAndApplyPetrification, + null, + TimeSpan.FromSeconds(CheckIntervalSeconds), + TimeSpan.FromSeconds(CheckIntervalSeconds) + ); + } + + private void CheckAndApplyPetrification(object state) + { + if (_targetMap == null) + return; + + try + { + // Get all characters currently on the map + var characters = _targetMap.GetCharacters(); + + foreach (var character in characters) + { + // Skip if character is dead + if (character.IsDead) + continue; + + // Calculate distance from zone center + var distance = character.Position.Get2DDistance(ZoneCenter); + + // If character is within the zone radius, apply Petrification + if (distance <= ZoneRadius) + { + character.StartBuff( + BuffId.Petrification, + DebuffLevel, + 0, + TimeSpan.FromSeconds(DebuffDurationSeconds), + character + ); + } + } + } + catch (Exception ex) + { + Log.Error($"Error in Petrification Zone: {ex.Message}"); + } + } +} diff --git a/system/scripts/zone/content/test/quests/f_siauliai_west_quests.cs b/system/scripts/zone/content/test/quests/f_siauliai_west_quests.cs new file mode 100644 index 000000000..ecdad41fd --- /dev/null +++ b/system/scripts/zone/content/test/quests/f_siauliai_west_quests.cs @@ -0,0 +1,635 @@ +//--- Melia Script ---------------------------------------------------------- +// West Siauliai Woods Quest NPCs +//--- Description ----------------------------------------------------------- +// Quest NPCs in West Siauliai Woods for post-demon war storyline. +//--------------------------------------------------------------------------- + +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Scripting; +using Melia.Zone.World.Quests; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.Characters.Components; +using Melia.Zone.World.Quests.Objectives; +using Melia.Zone.World.Quests.Prerequisites; +using Melia.Zone.World.Quests.Rewards; +using static Melia.Zone.Scripting.Shortcuts; + + +public class FSiauliaiWestQuestNpcsScript : GeneralScript +{ + protected override void Load() + { + // Lost Farmer + //------------------------------------------------------------------------- + AddNpc(20117, "[Lost Farmer] Bronius", "f_siauliai_west", -1895, -1007, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_west", 1001); + + dialog.SetTitle("Bronius"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("I used to tend these farmlands... before the war. Now look at them - overrun by Kepas and those cursed leaf bugs."); + + var response = await dialog.Select("I tried harvesting this morning but those creatures drove me away. My tools are still out there somewhere.", + Option("I'll help you recover your tools", "help"), + Option("Why not just buy new tools?", "buy"), + Option("Good luck with that", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("You would? Thank you! My farming tools are scattered across the farmlands - I dropped them in bags when I was fleeing."); + + if (await dialog.YesNo("I need my Shovel, Rake, and Sickle. They're in three different bags. Will you search for them?")) + { + character.Quests.Start(questId); + await dialog.Msg("The bags should be somewhere in the farmlands near the Kepas and leaf bugs. Please be careful - those creatures are more aggressive than they look."); + } + break; + + case "buy": + await dialog.Msg("Buy new tools? {#666666}*He laughs bitterly*{/} Do I look like I have silver to spare?"); + await dialog.Msg("Those tools belonged to my father, and his father before him. They're family heirlooms, even if they're falling apart."); + break; + + case "leave": + await dialog.Msg("{#666666}*He sighs and returns to staring at his ruined farmland*{/}"); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var hasShovel = character.Inventory.HasItem(662053); + var hasRake = character.Inventory.HasItem(662055); + var hasSickle = character.Inventory.HasItem(662056); + + if (hasShovel && hasRake && hasSickle) + { + await dialog.Msg("{#666666}*His eyes light up when he sees the tools*{/}"); + await dialog.Msg("You found them! All three! I can't believe it - I thought they were lost forever!"); + await dialog.Msg("These tools have been in my family for three generations. My grandfather worked this soil with these very implements."); + await dialog.Msg("Here, take this. It's not much, but it's all I can offer. And... thank you, truly."); + + character.Inventory.Remove(662053, 1, InventoryItemRemoveMsg.Given); + character.Inventory.Remove(662055, 1, InventoryItemRemoveMsg.Given); + character.Inventory.Remove(662056, 1, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + var foundCount = (hasShovel ? 1 : 0) + (hasRake ? 1 : 0) + (hasSickle ? 1 : 0); + await dialog.Msg($"Have you found my tools? You have {foundCount} of 3."); + await dialog.Msg("They should be in bags scattered around the farmlands. The Kepas and leaf bugs probably knocked them around."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("With my tools back, I can start working the fields again. It won't be easy, but it's a beginning."); + await dialog.Msg("If you're heading deeper into the woods, watch yourself. Strange things have been happening near the old broken house."); + } + }); + + // Bag NPCs - Talk to collect tools + //------------------------------------------------------------------------- + void AddBagNpc(int bagNumber, string toolName, int itemId, int x, int z) + { + AddNpc(155005, "Bag", "f_siauliai_west", x, z, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_west", 1001); + + if (!character.Quests.IsActive(questId)) + { + await dialog.Msg("{#666666}*A weathered bag lies on the ground, partially covered in dirt*{/}"); + return; + } + + if (character.Inventory.HasItem(itemId)) + { + await dialog.Msg("{#666666}*You've already searched this bag*{/}"); + return; + } + + var result = await character.TimeActions.StartAsync("Searching bag...", "Cancel", "SITGROPE", TimeSpan.FromSeconds(3)); + + if (result == TimeActionResult.Completed) + { + character.Inventory.Add(itemId, 1, InventoryAddType.PickUp); + character.ServerMessage($"Found: {toolName}"); + + var toolCount = (character.Inventory.HasItem(662053) ? 1 : 0) + + (character.Inventory.HasItem(662055) ? 1 : 0) + + (character.Inventory.HasItem(662056) ? 1 : 0); + + character.ServerMessage($"Tools found: {toolCount}/3"); + + if (toolCount >= 3) + { + character.ServerMessage("{#FFD700}All farming tools found! Return to Bronius.{/}"); + } + } + else + { + character.ServerMessage("Search cancelled."); + } + }); + } + + AddBagNpc(1, "Shovel", 662053, -2044, -1274); + AddBagNpc(2, "Rake", 662055, -2057, -986); + AddBagNpc(3, "Sickle", 662056, -1719, -671); + + // Eccentric Herbalist + //------------------------------------------------------------------------- + AddNpc(147473, "[Herbalist] Vesta", "f_siauliai_west", 730, 381, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_west", 1002); + + dialog.SetTitle("Vesta"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*A woman crouches by the pathway, examining plants with an unusual magnifying crystal*{/}"); + await dialog.Msg("Fascinating! The Chinency monsters have been consuming these herbs, but the plants show signs of magical residue..."); + + var response = await dialog.Select("Oh! I didn't see you there. Are you also studying the local flora?", + Option("I could help with your research", "help"), + Option("What are you researching?", "research"), + Option("Just passing through", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("A willing assistant! Excellent! I need samples from the creatures that feed on these magical herbs."); + + if (await dialog.YesNo("Specifically, I need samples from Chinency monsters. Their digestive systems process the herbs uniquely. Will you collect samples for me?")) + { + character.Quests.Start(questId); + await dialog.Msg("Perfect! I need 8 Chinency Roots. The magical residue in their bodies will tell me so much!"); + await dialog.Msg("They're right over there, shouldn't be too difficult."); + } + break; + + case "research": + await dialog.Msg("I'm studying how magical contamination from the demon war affected plant life in this region."); + await dialog.Msg("The monsters that eat these plants show interesting mutations. If I can understand the process, perhaps we can develop antidotes or even beneficial potions!"); + break; + + case "leave": + await dialog.Msg("Safe travels! Mind the mutated plants - some have developed defensive thorns."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var chinencyCount = character.Inventory.CountItem(ItemId.Misc_Bokchoy2); + + if (chinencyCount >= 8) + { + await dialog.Msg("{#666666}*She eagerly takes the samples*{/}"); + await dialog.Msg("Magnificent specimens! Look at these cellular structures - the magical residue has integrated into their biology!"); + await dialog.Msg("{#666666}*She examines the samples through her magnifying crystal*{/}"); + await dialog.Msg("This confirms my hypothesis! The war's magical fallout has created a new ecological balance. Fascinating and terrifying in equal measure."); + await dialog.Msg("Your contribution to science will not be forgotten! Here, take these - they're experimental potions I've been developing."); + + character.Inventory.Remove(ItemId.Misc_Bokchoy2, 8, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg("You can get some Chinency Roots from these plant-like things over there."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("I'm making excellent progress with my research thanks to your samples!"); + await dialog.Msg("The magical contamination patterns are complex, but I believe I'm close to a breakthrough in understanding post-war ecology."); + } + }); + + // Village Herbalist + //------------------------------------------------------------------------- + AddNpc(20118, "[Village Herbalist] Henrik", "f_siauliai_west", -731, -413, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_west", 1003); + + dialog.SetTitle("Henrik"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*An elderly herbalist stands near his cottage, studying crystalline formations*{/}"); + await dialog.Msg("Ah, a traveler! Welcome. I've been researching the Rootcrystals that grow in these woods."); + + var response = await dialog.Select("These aren't ordinary crystals - they're manifestations of the World Tree's roots, extending throughout the entire continent.", + Option("Tell me more about these Rootcrystals", "more"), + Option("How do they interact with humans?", "interact"), + Option("Just passing through", "leave") + ); + + switch (response) + { + case "more": + await dialog.Msg("The World Tree oversees the entire continent, and its roots run deep beneath the soil. Sometimes these roots crystallize when exposed to magical energy."); + await dialog.Msg("I believe studying how they respond to being destroyed could reveal much about the World Tree's connection to our world."); + + if (await dialog.YesNo("Would you help me with this research? I need you to destroy 2 Rootcrystals so I can observe the World Tree's response.")) + { + character.Quests.Start(questId); + await dialog.Msg("Excellent! When you destroy a Rootcrystal, the World Tree subtly reacts. I'll be monitoring the mystical fluctuations from here."); + await dialog.Msg("You'll find Rootcrystals scattered throughout these woods. Return when you've destroyed 2 of them."); + } + break; + + case "interact": + await dialog.Msg("Fascinating question! The Rootcrystals appear to resonate with human life force. Those who spend time near them report feeling... connected somehow."); + await dialog.Msg("I theorize that the World Tree uses these crystalline formations to monitor and perhaps even protect the life on this continent."); + await dialog.Msg("By studying how the crystals react when destroyed, I hope to understand this relationship better."); + break; + + case "leave": + await dialog.Msg("Safe travels! The Rootcrystals are harmless, but the monsters around them are not."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + if (!character.Quests.TryGetById(questId, out var quest)) return; + if (!quest.TryGetProgress("killRootcrystals", out var monObj)) return; + + var killCount = monObj.Count; + + if (killCount >= 2) + { + await dialog.Msg("{#666666}*His eyes light up with excitement*{/}"); + await dialog.Msg("You've done it! I've been recording the mystical fluctuations this entire time. The data is remarkable!"); + await dialog.Msg("{#666666}*He flips through pages of notes covered in diagrams and measurements*{/}"); + await dialog.Msg("As I suspected, the World Tree responds to each destruction by channeling energy to nearby roots. It's actively maintaining balance across the continent!"); + await dialog.Msg("This proves the World Tree is far more than legend - it's a living entity watching over us all. Your contribution to this research is invaluable!"); + + character.Quests.Complete(questId); + } + else + { + await dialog.Msg("The energy fluctuations are fascinating! Continue destroying them so I can gather more data."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("Thanks to your help, I've made a breakthrough in understanding the World Tree's influence on our world."); + await dialog.Msg("The Rootcrystals are like the World Tree's eyes and hands, spread across the entire continent. Remarkable!"); + } + }); + + // Bridge Guard + //------------------------------------------------------------------------- + AddNpc(150219, "[Bridge Guard] Tomas", "f_siauliai_west", 334, -331, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_west", 1004); + + dialog.SetTitle("Tomas"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*A guard stands watch near the bridge, looking concerned*{/}"); + await dialog.Msg("Greetings, traveler. I'm responsible for maintaining the bridge, but I've run into a problem."); + + var response = await dialog.Select("The bridge supports need reinforcement, but I need special materials.", + Option("I can help gather materials", "help"), + Option("What materials do you need?", "materials"), + Option("Good luck with that", "leave") + ); + + switch (response) + { + case "help": + await dialog.Msg("Thank you! I need Infrorocktor Fragments from the Infrorocktor monsters. They're dense and perfect for reinforcing the foundation."); + + if (await dialog.YesNo("Can you collect 10 Infrorocktor Fragments for me? The bridge safety depends on it.")) + { + character.Quests.Start(questId); + await dialog.Msg("Excellent! Infrorocktor can be found throughout the southern areas of these woods."); + await dialog.Msg("Once you have 10 Infrorocktor Fragments, bring them back and I'll get to work on the repairs."); + } + break; + + case "materials": + await dialog.Msg("The Infrorocktor monsters have these dense cores that are perfect for construction work."); + await dialog.Msg("They're incredibly sturdy - much better than regular stone for bridge reinforcement."); + break; + + case "leave": + await dialog.Msg("{#666666}*He nods and returns to inspecting the bridge*{/}"); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var coreCount = character.Inventory.CountItem(666025); + + if (coreCount >= 10) + { + await dialog.Msg("{#666666}*His face lights up when he sees the Infrorocktor Fragments*{/}"); + await dialog.Msg("Perfect! These are exactly what I need! The density is ideal for the bridge foundation."); + await dialog.Msg("{#666666}*He begins placing the cores strategically around the bridge supports*{/}"); + await dialog.Msg("With these reinforcements, this bridge will be safe for years to come. Thank you for your help!"); + + character.Inventory.Remove(666025, 10, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg($"Have you collected the Infrorocktor Fragments? You have {coreCount}/10 so far."); + await dialog.Msg("Infrorocktor can be found throughout the southern areas of West Siauliai Woods."); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("The bridge is holding up wonderfully thanks to your help!"); + await dialog.Msg("Travelers can cross safely now. I owe you one!"); + } + }); + + // Traveling Merchant + //------------------------------------------------------------------------- + AddNpc(20104, "[Traveling Merchant] Sigrid", "f_siauliai_west", -541, 1334, 45, async dialog => + { + var character = dialog.Player; + var questId = new QuestId("f_siauliai_west", 1005); + + dialog.SetTitle("Sigrid"); + + if (!character.Quests.Has(questId)) + { + await dialog.Msg("{#666666}*A merchant sits surrounded by broken wagon parts, looking distressed*{/}"); + await dialog.Msg("Of all the places for my wagon to break down... right in the middle of monster territory!"); + + var response = await dialog.Select("I'm trying to reach Klaipeda with these goods, but I can't move the wagon like this.", + Option("I can help repair it", "help"), + Option("What happened to your wagon?", "wagon"), + Option("Maybe abandon the wagon?", "abandon") + ); + + switch (response) + { + case "help": + await dialog.Msg("You're a lifesaver! The problem is, I need specific materials that I don't have."); + + if (await dialog.YesNo("I need Leaf Bug Feelers and Hanaming Petals. These materials can be used to fix my wagon! Can you gather these materials for me?")) + { + character.Quests.Start(questId); + await dialog.Msg("Wonderful! Please lookg for these plant-based monsters in the area."); + } + break; + + case "wagon": + await dialog.Msg("Hit a rock hidden in the road. Cracked the axle and broke several support beams."); + await dialog.Msg("Without repairs, this wagon isn't going anywhere. And I can't afford to lose this cargo - it's my entire livelihood!"); + break; + + case "abandon": + await dialog.Msg("{#666666}*She looks horrified*{/} Abandon it? This cargo represents three months of trading!"); + await dialog.Msg("I'd rather take my chances here than lose everything I've worked for."); + break; + } + } + else if (character.Quests.IsActive(questId)) + { + var petalCount = character.Inventory.CountItem(645024); + var feelerCount = character.Inventory.CountItem(645262); + + if (petalCount >= 8 && feelerCount >= 8) + { + await dialog.Msg("{#666666}*She eagerly examines the materials*{/}"); + await dialog.Msg("This is exactly what I need! The petals are strong enough to serve as cover, and this feeler is perfect rope material!"); + await dialog.Msg("{#666666}*She works quickly, replacing broken parts and reinforcing the wagon structure*{/}"); + await dialog.Msg("There! Good as new! Well, good enough to reach Klaipeda at least."); + await dialog.Msg("Here, take these goods from my wagon. It's the least I can do to thank you for saving my business!"); + + // Remove those items here because they're normal materials, + // not actual quest items. + character.Inventory.Remove(645024, 8, InventoryItemRemoveMsg.Given); + character.Inventory.Remove(645262, 8, InventoryItemRemoveMsg.Given); + character.Quests.Complete(questId); + } + else + { + await dialog.Msg($"How's the material gathering going? This wagon won't budge at all!"); + } + } + else if (character.Quests.HasCompleted(questId)) + { + await dialog.Msg("Thanks to you, I can make it safely to Klaipeda!"); + await dialog.Msg("I'm still preparing for the trip, but I'll make it there someday!"); + } + }); + + // Wagon + //------------------------------------------------------------------------- + AddNpc(45316, "Wagon", "f_siauliai_west", -606, 1345, 135); + } +} + +//----------------------------------------------------------------------------- +// Quests +//----------------------------------------------------------------------------- + +// Quest 1001: Lost Farming Tools +//----------------------------------------------------------------------------- +public class FSiauliaiWestFarmingToolsQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_west", 1001); + SetName("The Farmer's Legacy"); + SetDescription("Help Bronius recover his family's heirloom farming tools by searching through bags scattered across the farmlands when monsters drove him away."); + SetLocation("f_siauliai_west"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Lost Farmer] Bronius", "f_siauliai_west"); + + // Search bags to collect the three farming tools + AddObjective("collectShovel", "Collect Shovel from a bag", + new CollectItemObjective(662053, 1)); + AddObjective("collectRake", "Collect Rake from a bag", + new CollectItemObjective(662055, 1)); + AddObjective("collectSickle", "Collect Sickle from a bag", + new CollectItemObjective(662056, 1)); + + // Rewards + AddReward(new ExpReward(350, 230)); + AddReward(new SilverReward(2000)); + AddReward(new ItemReward(640002, 5)); // Small HP potions + AddReward(new ItemReward(640005, 5)); // Small SP potions + AddReward(new ItemReward(221103, 1)); // Wooden Kite Shield + AddReward(new ItemReward(640080, 2)); // Lv1 EXP Cards + } + + public override void OnComplete(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(662053, character.Inventory.CountItem(662053), InventoryItemRemoveMsg.Destroyed); + character.Inventory.Remove(662055, character.Inventory.CountItem(662055), InventoryItemRemoveMsg.Destroyed); + character.Inventory.Remove(662056, character.Inventory.CountItem(662056), InventoryItemRemoveMsg.Destroyed); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(662053, character.Inventory.CountItem(662053), InventoryItemRemoveMsg.Destroyed); + character.Inventory.Remove(662055, character.Inventory.CountItem(662055), InventoryItemRemoveMsg.Destroyed); + character.Inventory.Remove(662056, character.Inventory.CountItem(662056), InventoryItemRemoveMsg.Destroyed); + } +} + +// Quest 1002: Herbalist's Research +//----------------------------------------------------------------------------- +public class FSiauliaiWestHerbalistQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_west", 1002); + SetName("Magical Contamination Study"); + SetDescription("Collect samples from Bokchoy and Chinency monsters to help Vesta's research on post-war magical contamination in the local ecosystem."); + SetLocation("f_siauliai_west"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Herbalist] Vesta", "f_siauliai_west"); + + // Collect samples (items already drop normally) + AddObjective("collectChinency", "Collect Chinency Roots", + new CollectItemObjective(ItemId.Misc_Bokchoy2, 8)); + + // Rewards + AddReward(new ExpReward(420, 280)); + AddReward(new SilverReward(3200)); + AddReward(new ItemReward(640002, 5)); // Small HP potions + AddReward(new ItemReward(640005, 5)); // Small SP potions + AddReward(new ItemReward(640097, 5)); // Stamina potions + AddReward(new ItemReward(531103, 1)); // Leather Armor + AddReward(new ItemReward(640080, 3)); // Lv1 EXP Cards + } +} + +// Quest 1003: World Tree Research +//----------------------------------------------------------------------------- +public class FSiauliaiWestVillageRemedyQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_west", 1003); + SetName("Roots of the World Tree"); + SetDescription("Help Henrik with his research by destroying 2 Rootcrystals."); + SetLocation("f_siauliai_west"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Village Herbalist] Henrik", "f_siauliai_west"); + + // Kill Rootcrystals + AddObjective("killRootcrystals", "Destroy Rootcrystals", + new KillObjective(2, MonsterId.Rootcrystal_01)); + + // Rewards + AddReward(new ExpReward(480, 320)); + AddReward(new SilverReward(3500)); + AddReward(new ItemReward(640002, 12)); // Small HP potions + AddReward(new ItemReward(640005, 12)); // Small SP potions + AddReward(new ItemReward(501103, 1)); // Leather Gloves + AddReward(new ItemReward(640080, 3)); // Lv1 EXP Cards + } +} + +// Quest 1004: Bridge Reinforcement +//----------------------------------------------------------------------------- +public class FSiauliaiWestRunestoneQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_west", 1004); + SetName("Bridge Maintenance"); + SetDescription("Help bridge guard Tomas reinforce the bridge by collecting Infrorocktor Fragments from Infrorocktor monsters."); + SetLocation("f_siauliai_west"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Bridge Guard] Tomas", "f_siauliai_west"); + + // Add quest item drops + AddDrop(666025, 0.35f, MonsterId.InfroRocktor); + + // Collect Infrorocktor Fragments + AddObjective("collectCores", "Collect Infrorocktor Fragments from Infrorocktor", + new CollectItemObjective(666025, 10)); + + // Rewards + AddReward(new ExpReward(400, 300)); + AddReward(new SilverReward(3000)); + AddReward(new ItemReward(640002, 10)); // Small HP potions + AddReward(new ItemReward(640005, 10)); // Small SP potions + AddReward(new ItemReward(511103, 1)); // Leather Boots + AddReward(new ItemReward(640080, 2)); // Lv1 EXP Cards + } + + public override void OnComplete(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(666025, character.Inventory.CountItem(666025), InventoryItemRemoveMsg.Destroyed); + } + + public override void OnCancel(Character character, Quest quest) + { + // Remove quest items + character.Inventory.Remove(666025, character.Inventory.CountItem(666025), InventoryItemRemoveMsg.Destroyed); + } +} + +// Quest 1005: Broken Wagon Repairs +//----------------------------------------------------------------------------- +public class FSiauliaiWestMerchantQuest : QuestScript +{ + protected override void Load() + { + SetId("f_siauliai_west", 1005); + SetName("The Broken Wagon"); + SetDescription("Help traveling merchant Sigrid repair her broken wagon by collecting Hanaming Petals and Leaf Bug Feelers."); + SetLocation("f_siauliai_west"); + SetAutoTracked(true); + + SetReceive(QuestReceiveType.Manual); + SetCancelable(true); + SetUnlock(QuestUnlockType.AllAtOnce); + AddQuestGiver("[Traveling Merchant] Sigrid", "f_siauliai_west"); + + // Collect samples (Items already drop normally) + AddObjective("collectHanaming", "Collect Hanaming Petals", + new CollectItemObjective(645024, 8)); + AddObjective("collectLeafBug", "Collect Leaf Bug Feelers", + new CollectItemObjective(645262, 8)); + + // Rewards + AddReward(new ExpReward(420, 280)); + AddReward(new SilverReward(4500)); + AddReward(new ItemReward(640002, 10)); // Small HP potions + AddReward(new ItemReward(640005, 10)); // Small SP potions + AddReward(new ItemReward(521103, 1)); // Leather Pants + AddReward(new ItemReward(640080, 3)); // Lv1 EXP Cards + } +} diff --git a/system/scripts/zone/core/client/quest_system/001_drawing.lua b/system/scripts/zone/core/client/quest_system/001_drawing.lua index 456ce1294..090fd50f3 100644 --- a/system/scripts/zone/core/client/quest_system/001_drawing.lua +++ b/system/scripts/zone/core/client/quest_system/001_drawing.lua @@ -4,19 +4,9 @@ function M_QUESTS_DRAW_LIST(frame, quests) frame:DeleteAllControl() - local filters = GET_QUEST_MODE_OPTION() - for i = 1, #quests do local quest = quests[i] - - local filtered = filters[quest.Type] == false - if quest.Tracked and filters["Chase"] ~= true then - filtered = true - end - - if not filtered then - y = y + M_QUESTS_DRAW_QUEST(frame, quest, i, x, y) - end + y = y + M_QUESTS_DRAW_QUEST(frame, quest, i, x, y) end frame:Invalidate() diff --git a/system/scripts/zone/core/client/quest_system/002_overrides.lua b/system/scripts/zone/core/client/quest_system/002_overrides.lua index 3ede5d372..9fda94a40 100644 --- a/system/scripts/zone/core/client/quest_system/002_overrides.lua +++ b/system/scripts/zone/core/client/quest_system/002_overrides.lua @@ -3,7 +3,18 @@ Melia.Override("QUEST_UPDATE_ALL", function(original, frame) end) Melia.Override("QUESTINFOSET_2_QUEST_ANGLE", function(original, frame, msg, argStr, argNum) - -- do nothing + -- Block the original function completely - it tries to hide/show the chase window + -- during movement which causes flickering. We manage visibility ourselves. + -- Do nothing here. +end) + +Melia.Override("CHASEINFO_CLOSE_FRAME", function(original) + -- Block the game from closing questinfoset_2 during dash/skills + -- The original function closes chaseinfo, achieveinfoset, and questinfoset_2 + -- We manage questinfoset_2 visibility ourselves, so only close the others + ui.CloseFrame("chaseinfo") + ui.CloseFrame("achieveinfoset") + -- Don't close questinfoset_2 - we manage it ourselves end) Melia.Override("QUEST_TAB_CHANGE", function(original, frame, argStr, argNum) @@ -20,6 +31,8 @@ Melia.Override("QUEST_TAB_CHANGE", function(original, frame, argStr, argNum) end) Melia.Override("QUEST_FILTER_UPDATE", function(original, frame, control, argStr, argNum) + -- TODO: Filter + AUTO_CAST(control) if control:GetName() == "mode_all_check" then @@ -33,7 +46,6 @@ Melia.Override("QUEST_FILTER_UPDATE", function(original, frame, control, argStr, end end end - UPDATE_QUEST_FILTER_ALLCHECK() SET_QUEST_MODE_OPTION(GET_QUEST_FILTER_OPTION_LIST()) diff --git a/system/scripts/zone/core/client/quest_system/003_list.lua b/system/scripts/zone/core/client/quest_system/003_list.lua index 220b6abf3..49e33d9d6 100644 --- a/system/scripts/zone/core/client/quest_system/003_list.lua +++ b/system/scripts/zone/core/client/quest_system/003_list.lua @@ -1,11 +1,3 @@ -local QuestIcons = { - Main = "MAIN", - Sub = "SUB", - Repeat = "REPEAT", - Party = "PARTY", - KeyItem = "except", -} - function M_QUESTS_SET_NAME(questCtrl, quest) local txtName = GET_CHILD(questCtrl, "name", "ui::CRichText") local lvlText = GET_CHILD(questCtrl, "level", "ui::CRichText") @@ -24,11 +16,7 @@ function M_QUESTS_SET_ICON(questCtrl, quest) else questmark:EnableHitTest(1) questmark:SetTextTooltip("{@st59}The quest's objectives have been cleared.{/}") - end - - if QuestIcons[quest.Type] then - local nr = quest.Done and 3 or 1 - iconName = "minimap_" .. nr .. "_" .. QuestIcons[quest.Type] + iconName = "minimap_1_MAIN" end questmark:SetImage(iconName) diff --git a/system/scripts/zone/core/client/quest_system/102_details_summary.lua b/system/scripts/zone/core/client/quest_system/102_details_summary.lua index a0565c454..4a4d93944 100644 --- a/system/scripts/zone/core/client/quest_system/102_details_summary.lua +++ b/system/scripts/zone/core/client/quest_system/102_details_summary.lua @@ -11,20 +11,43 @@ function M_QUESTS_DETAILS_ADD_SUMMARY(frame, x, y, quest) local offsetX = 10 local description = quest.Description and quest.Description or "No description given." - + local contentTitle = frame:CreateOrGetControl("richtext", "QuestSummary", x, y + height, frame:GetWidth() - x - SCROLL_WIDTH, 10) contentTitle:EnableHitTest(0) contentTitle:SetTextFixWidth(1) contentTitle:SetText("{img quest_detail_pic2 24 24}{@st41b}Description{/}") - + height = height + contentTitle:GetHeight() + 10 local content = frame:CreateOrGetControl("richtext", "QuestSummaryDesc", x + offsetX, y + height, frame:GetWidth() - x - SCROLL_WIDTH, 10) content:EnableHitTest(0) content:SetTextFixWidth(1) content:SetText("{@st68}" .. description .. "{/}") - + height = height + content:GetHeight() + local function addSection(title, text) + if text and text ~= "" then + height = height + 15 + local t = frame:CreateOrGetControl("richtext", "Quest"..title, x, y + height, frame:GetWidth() - x - SCROLL_WIDTH, 10) + t:EnableHitTest(0) + t:SetTextFixWidth(1) + t:SetText("{img quest_detail_pic2 24 24}{@st41b}"..title.."{/}") + height = height + t:GetHeight() + 10 + local c = frame:CreateOrGetControl("richtext", "Quest"..title.."Desc", x + offsetX, y + height, frame:GetWidth() - x - SCROLL_WIDTH, 10) + c:EnableHitTest(0) + c:SetTextFixWidth(1) + c:SetText("{@st68}"..text.."{/}") + height = height + c:GetHeight() + end + end + + addSection("Location", quest.Location) + local giverText = quest.QuestGiver + if quest.QuestGiverLocation then + giverText = (giverText or "") .. (giverText and " (" or "(") .. quest.QuestGiverLocation .. ")" + end + addSection("Quest Giver", giverText) + return height end diff --git a/system/scripts/zone/core/client/quest_system/103_details_objectives.lua b/system/scripts/zone/core/client/quest_system/103_details_objectives.lua index 4a2417f5d..b61332541 100644 --- a/system/scripts/zone/core/client/quest_system/103_details_objectives.lua +++ b/system/scripts/zone/core/client/quest_system/103_details_objectives.lua @@ -32,17 +32,16 @@ function M_QUESTS_DETAILS_ADD_OBJECTIVES(frame, x, y, quest) end local lblDesc = frame:CreateOrGetControl("richtext", "QuestObjectiveDesc" .. tostring(i), x + offsetX + chkComplete:GetWidth() + checkSpacing, y + height + 3, width, 10) - --lblDesc:EnableHitTest(0) lblDesc:SetTextFixWidth(1) - if not objective.Unlocked then lblDesc:SetText(string.format("{@st68}?{/}")) lblDesc:SetTextTooltip("{@st59}Continue the quest to reveal this objective.{/}") - elseif objective.TargetCount <= 1 then - lblDesc:SetText(string.format("{@st68}%s{/}", objective.Text)) else - lblDesc:SetText(string.format("{@st68}%s (%d/%d){/}", objective.Text, objective.Count, objective.TargetCount)) - end + local t=objective.Text + if objective.TargetCount>1 then t=t.." ("..objective.Count.."/"..objective.TargetCount..")" end + if objective.Monsters and #objective.Monsters>0 then t=t.."{nl}• "..table.concat(objective.Monsters,"{nl}• ") end + lblDesc:SetText(string.format("{@st68}%s{/}",t)) + end height = height + lblDesc:GetHeight() + 10 end diff --git a/system/scripts/zone/core/client/quest_system/201_chase_general.lua b/system/scripts/zone/core/client/quest_system/201_chase_general.lua index 6b7f8af9b..a575b9a76 100644 --- a/system/scripts/zone/core/client/quest_system/201_chase_general.lua +++ b/system/scripts/zone/core/client/quest_system/201_chase_general.lua @@ -32,14 +32,12 @@ end function M_CHASE_UPDATE_VISIBILITY() local frmQuestInfo = ui.GetFrame("questinfoset_2") + local hasTrackedQuests = Melia.Quests.CountTracked() > 0 - if Melia.Quests.CountTracked() > 0 then - frmQuestInfo:ShowWindow(1) - --CHASEINFO_SHOW_QUEST_TOGGLE(1) - + if hasTrackedQuests then M_CHASE_REDRAW(frmQuestInfo) + frmQuestInfo:ShowWindow(1) else frmQuestInfo:ShowWindow(0) - --CHASEINFO_SHOW_QUEST_TOGGLE(0) end end diff --git a/system/scripts/zone/core/client/quest_system/203_chase_objectives.lua b/system/scripts/zone/core/client/quest_system/203_chase_objectives.lua index e7d3dbefc..c7dc6bc3b 100644 --- a/system/scripts/zone/core/client/quest_system/203_chase_objectives.lua +++ b/system/scripts/zone/core/client/quest_system/203_chase_objectives.lua @@ -8,6 +8,7 @@ function M_CHASE_CREATE_OBJECTIVES(parent, quest, x, y) local checkSize = 10 local checkSpacing = 5 local width = parent:GetWidth() - x - SCROLL_WIDTH + local textWidth = width - offsetX - checkSize - checkSpacing local textStyle = "{@s16}{#ffffff}"; for i = 1, #quest.Objectives do @@ -21,7 +22,7 @@ function M_CHASE_CREATE_OBJECTIVES(parent, quest, x, y) chkComplete:ToggleCheck() end - local lblDesc = parent:CreateOrGetControl("richtext", "QuestObjectiveDesc" .. tostring(i), x + offsetX + chkComplete:GetWidth() + checkSpacing, y + height + 3, width, 10) + local lblDesc = parent:CreateOrGetControl("richtext", "QuestObjectiveDesc" .. tostring(i), x + offsetX + chkComplete:GetWidth() + checkSpacing, y + height + 3, textWidth, 10) --lblDesc:EnableHitTest(0) lblDesc:SetTextFixWidth(1) diff --git a/system/scripts/zone/core/client/quest_system/950_main.lua b/system/scripts/zone/core/client/quest_system/950_main.lua new file mode 100644 index 000000000..d0591380c --- /dev/null +++ b/system/scripts/zone/core/client/quest_system/950_main.lua @@ -0,0 +1,24 @@ +local questFrame = ui.GetFrame("quest") + +local function RemoveObsoleteUiElements() + questFrame:GetChild("bg"):GetChild("tipMessage"):SetVisible(false) + + local questBox = GET_CHILD_RECURSIVELY(questFrame, "questBox", "ui::CTabControl") + if questBox:GetItemCount() > 1 then + questBox:DeleteTab(3) -- Res Sacrae w/e + questBox:DeleteTab(2) -- Complete + questBox:DeleteTab(0) -- Episodes + end + questBox:SelectTab(0) -- Quest List +end + +function M_QUESTS_UPDATE_LIST() + local gb_progressQuest = GET_CHILD_RECURSIVELY(questFrame, "gb_progressQuestItem", "ui::CGroupBox") + local quests = Melia.Quests.GetAll() + + M_QUESTS_DRAW_LIST(gb_progressQuest, quests) + M_CHASE_UPDATE_VISIBILITY() +end + +RemoveObsoleteUiElements() +M_QUESTS_UPDATE_LIST()