Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/ZoneServer/Buffs/Handlers/Common/Petrification.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Handle for the Petrify, Petrified..
/// </summary>
[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);
}
}
}
15 changes: 15 additions & 0 deletions src/ZoneServer/Scripting/Dialogues/Dialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,21 @@ public async Task<int> Select(string text, IEnumerable<string> options)
return selectedIndex;
}

/// <summary>
/// Displays a Yes/No dialog and returns true if the user selects Yes.
/// </summary>
/// <param name="text">The question to ask the user</param>
/// <returns>True if Yes was selected, false otherwise</returns>
public async Task<bool> 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;
}

/// <summary>
/// Sends dialog input message, showing a message and a text field
/// for the user to put in a string.
Expand Down
78 changes: 73 additions & 5 deletions src/ZoneServer/Scripting/QuestScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -16,6 +17,7 @@ public abstract class QuestScript : IScript, IDisposable
private readonly static object ScriptsSyncLock = new();
private readonly static Dictionary<QuestId, QuestScript> Scripts = new();
private readonly static Dictionary<Type, QuestObjective> Objectives = new();
private readonly static Dictionary<Type, QuestModifier> Modifiers = new();
private readonly static List<QuestScript> AutoReceiveQuests = new();

/// <summary>
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
}
}
}

Expand Down Expand Up @@ -181,6 +211,33 @@ protected void SetName(string name)
protected void SetDescription(string description)
=> this.Data.Description = description;

/// <summary>
/// Sets the quest's location (map class name).
/// </summary>
/// <param name="mapClassName"></param>
protected void SetLocation(string mapClassName)
=> this.Data.Location = mapClassName;

/// <summary>
/// Sets the quest's locations (multiple map class names).
/// </summary>
/// <param name="mapClassNames">Map class names separated by commas</param>
protected void SetLocation(params string[] mapClassNames)
=> this.Data.Location = string.Join(",", mapClassNames);

/// <summary>
/// Sets the quest giver NPC name and location.
/// </summary>
/// <param name="npcName">The name of the NPC that gives the quest</param>
/// <param name="mapClassName">The map class name where the NPC is located</param>
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;
}

/// <summary>
/// Sets the quest's type.
/// </summary>
Expand Down Expand Up @@ -272,6 +329,17 @@ protected void AddPrerequisite(QuestPrerequisite prerequisite)
this.Data.Prerequisites.Add(prerequisite);
}

/// <summary>
/// Adds an item drop modifier using item ID and monster IDs directly.
/// </summary>
/// <param name="itemId">The ID of the item to drop</param>
/// <param name="dropChance">Drop probability (0.0 to 1.0, where 0.5 = 50%)</param>
/// <param name="monsterIds">Monster IDs that should drop this item</param>
protected void AddDrop(int itemId, float dropChance, params int[] monsterIds)
{
this.Data.Modifiers.Add(new ItemDropModifier(itemId, dropChance, monsterIds));
}

/// <summary>
/// Returns an Or prerequisite, which is met if one of the given
/// prerequisites is met.
Expand Down
7 changes: 6 additions & 1 deletion src/ZoneServer/World/Actors/Characters/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ public int JobLevel
/// </summary>
public QuestComponent Quests { get; }

/// <summary>
/// Returns the character's time action component.
/// </summary>
public TimeActionComponent TimeActions { get; }

/// <summary>
/// Returns the character's collection manager.
/// </summary>
Expand Down Expand Up @@ -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));
Expand Down
116 changes: 116 additions & 0 deletions src/ZoneServer/World/Actors/Characters/Components/QuestComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -107,6 +109,22 @@ public bool TryGet(long questObjectId, out Quest quest)
}
}

/// <summary>
/// Gets quest by id and returns it via out, returns false if the
/// quest didn't exist.
/// </summary>
/// <param name="questId"></param>
/// <param name="quest"></param>
/// <returns></returns>
public bool TryGetById(QuestId questId, out Quest quest)
{
lock (_syncLock)
{
quest = _quests.FirstOrDefault(a => a.Data.Id == questId);
return quest != null;
}
}

/// <summary>
/// Returns a list of all active quests.
/// </summary>
Expand Down Expand Up @@ -177,6 +195,32 @@ public void UpdateObjectives<TObjective>(QuestObjectivesUpdateFunc<TObjective> u
}
}

/// <summary>
/// Iterates over the quests' modifiers, runs the given function
/// over all modifiers with the given type, and updates the quest
/// if any progresses changed.
/// </summary>
/// <typeparam name="TModifier"></typeparam>
/// <param name="updater"></param>
public void UpdateModifiers<TModifier>(QuestModifiersUpdateFunc<TModifier> 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();
}
}
}
}

/// <summary>
/// Starts quest for the character, returns false if the quest
/// couldn't be started.
Expand Down Expand Up @@ -393,13 +437,22 @@ public void Complete(QuestId questId, string objectiveIdent)
if (!progress.Done)
{
progress.SetDone();
quest.UpdateUnlock();
this.UpdateClient_UpdateQuest(quest);
continue;
}
}
}
}

/// <summary>
/// Completes the objective on all quests with the given id.
/// </summary>
/// <param name="questId"></param>
/// <param name="objectiveIdent"></param>
public void CompleteObjective(QuestId questId, string objectiveIdent)
=> this.Complete(questId, objectiveIdent);

/// <summary>
/// Completes all quests with the given id and gives the rewards
/// to the character.
Expand Down Expand Up @@ -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<string>();

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());
Expand All @@ -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;
}

Expand All @@ -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<string>();
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);
}

Expand Down
Loading