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()