Full Version : Quests
xmlspawner >>Scripting Support >>Quests
<< Prev | Next >>
arul- 01-17-2006
Hello,
I've been planning to implement the new OSI quest engine to the RunUO.
Well, I've written about 1,5k lines of code ( which is not even 10% of the whole system ), then I went to update xmlspawner on my shard to the most recent version and noticed the XmlQuest directory there.
I looked over the XmlQuest system and said to myself "bwahaha, this is amazing stuff" !
So I realized that developing another quest system would be like reinventig the wheel.
Although it's quite easy to write new quests, it's not so easy to edit the whole system to make it support the new features needed for ML quests.
Well, now the question, do you plan to add support for the ML quest features ? (quest chains, some new objectives, gumps and so forth).
I can help out, I have all of the gumps ( it was soooo boring to sniff & extract them from OSI ), important cliloc numbers, some documentation and am able to test some features directly on OSI. Also, of course, I can offer my C# coding hand.
Btw. nice smilies
ArteGordon- 01-17-2006
I have an OSI account but rarely use it so I havent checked out the details of the new quest system, but I would be happy to consider adding support for the new ML features.
Adding new ML gumps and such would be a nice addition and should be fairly easy since the system doesnt really care what the gumps look like and even has hooks for supporting user-defined gumps through the GUMP keyword by giving it the name of your custom gumpconstructor. The constructor just has to support passing the right args.
GUMP,title,number[,gumpconstructor]/text
If you described the changes in more detail we can think about how to get them implemented.
btw, I really hate doing gumpwork so any coding that you would like to contribute would be more than welcome.
ArteGordon- 01-18-2006
If you want to take a shot at adding some new gumps, what you can do is to copy the XmlSimpleGump code in XmlQuestGumps.cs and make it a new class (e.g. call it MLStyleGump).
Then just modify it to give it the new ML look. This is generally how anyone could add their own custom gumps.
Then you can use those gumps by just doing
GUMP,whatever,gumpnumber,MLStyleGump/the text specification for the gump number.
Right now there is just the default XmlSimpleGump style of gumps available, so doing
GUMP,whatever,gumpnumber/the text specification for the gump number.
is exactly the same as doing
GUMP,whatever,gumpnumber,XmlSimpleGump/the text specification for the gump number.
Also, you dont have to support all of the gump numbers (0-5) in your new gumps. Those are just the ones that XmlSimpleGump happens to support. You can even add others. It is flexible.
There is a How-to post that describes the different gump numbers that are supported by default by XmlSimpleGump
arul- 01-18-2006
Well, there are two general version of gumps: "quest offer" and "quest status".
They look more or less the same, but the "offer" version for instance doesn't contain information about slayed monster / obtained items.

CODE
public class QuestOfferGump : Gump { private IQuest m_Quest; public IQuest Quest {
get { return m_Quest; }
set { m_Quest = value; }
}
public QuestOfferGump( IQuest quest )
: base( 75, 25 )
{
#region Page 1 - Quest Description
AddPage(1);
m_Quest = quest;
Closable = false;
AddImageTiled(50, 20, 400, 400, 0x1404);
AddImageTiled(50, 29, 30, 390, 0x28DC);
AddImageTiled(34, 140, 17, 279, 0x242F);
AddImage(48, 135, 0x28AB);
AddImage(-16, 285, 0x28A2);
AddImage(0, 10, 0x28B5);
AddImage(25, 0, 0x28B4);
AddImageTiled(83, 15, 350, 15, 0x280A);
AddImage(34, 419, 0x2842);
AddImage(442, 419, 0x2840);
AddImageTiled(51, 419, 392, 17, 0x2775);
AddImageTiled(415, 29, 44, 390, 0xA2D);
AddImageTiled(415, 29, 30, 390, 0x28DC);
AddLabel(100, 50, 0x481, "");
AddImage(370, 50, 0x589);
AddImage(379, 60, 0x15A9);
AddImage(425, 0, 0x28C9);
AddImage(90, 33, 0x232D);
AddHtmlLocalized(130, 45, 270, 16, 1046026, 0xFFFFFF, false, false); // Quest Log
AddImageTiled(130, 65, 175, 1, 0x238D);
AddButton(95, 395, 0x2EE0, 0x2EE2, 1, GumpButtonType.Reply, 0);
AddButton(313, 395, 0x2EF2, 0x2EF4, 2, GumpButtonType.Reply, 0);
AddHtmlLocalized(160, 108, 250, 16, m_Quest.Title, 0x2710, false, false); // Quest Title
AddHtmlLocalized(98, 140, 312, 16, 1072202, 0x2710, false, false); // Description
AddHtmlLocalized(98, 156, 312, 180, m_Quest.QuestDescription, 0x15F90, false, true); // Quest Description
AddButton(275, 370, 0x2EE9, 0x2EEB, 0, GumpButtonType.Page, 2);
#endregion
#region Page 2 - Quest Objectives
AddPage(2);
Closable = false;
AddImageTiled(50, 20, 400, 400, 0x1404);
AddImageTiled(50, 29, 30, 390, 0x28DC);
AddImageTiled(34, 140, 17, 279, 0x242F);
AddImage(48, 135, 0x28AB);
AddImage(-16, 285, 0x28A2);
AddImage(0, 10, 0x28B5);
AddImage(25, 0, 0x28B4);
AddImageTiled(83, 15, 350, 15, 0x280A);
AddImage(34, 419, 0x2842);
AddImage(442, 419, 0x2840);
AddImageTiled(51, 419, 392, 17, 0x2775);
AddImageTiled(415, 29, 44, 390, 0xA2D);
AddImageTiled(415, 29, 30, 390, 0x28DC);
AddLabel(100, 50, 0x481, "");
AddImage(370, 50, 0x589);
AddImage(379, 60, 0x15A9);
AddImage(425, 0, 0x28C9);
AddImage(90, 33, 0x232D);
AddHtmlLocalized(130, 45, 270, 16, 1046026, 0xFFFFFF, false, false); // Quest Log
AddImageTiled(130, 65, 175, 1, 0x238D);
AddButton(95, 395, 0x2EE0, 0x2EE2, 1, GumpButtonType.Reply, 0);
AddButton(313, 395, 0x2EF2, 0x2EF4, 2, GumpButtonType.Reply, 0);
AddHtmlLocalized(160, 108, 250, 16, m_Quest.QuestTitle, 0x2710, false, false); // Quest Title
AddButton(275, 370, 0x2EE9, 0x2EEB, 0, GumpButtonType.Page, 2);
AddButton(130, 370, 0x2EEF, 0x2EF1, 0, GumpButtonType.Page, 1);
AddHtmlLocalized(98, 140, 312, 16, 1049073, 0x2710, false, false); // Objective:
AddHtmlLocalized(98, 156, 312, 16, 1072208, 0x2710, false, false); // All of the following
for (int i = 0; i < m_Quest.Objective.Length; i++)
{
IObjective objective = m_Quest.Objective[i];
AddHtmlLocalized(98, 172 + (i * 48), 312, 16, (int)objective.Type, 0x15F90, false, false);
if (objective is ISlayObjective)
{
ISlayObjective slay = (ISlayObjective)objective;
AddLabel(133, 172 + (i * 48), 0x481, slay.Amount);
AddLabel(163, 172 + (i * 48), 0x481, Talisman.TypeRegistry[slay.ToSlay]);
if (slay.Area != null)
{
AddHtmlLocalized(103, 188 + (i * 48), 312, 20, 1018327, 0x15F90, false, false); // Location
AddHtml(223, 188 + (i * 50), 312, 20, slay.Area.Name, false, false);
}
}
if (objective is IObtainObjective)
{
IObtainObjective obtain = (IObtainObjective)objective;
Item toObtain = Activator.CreateInstance(obtain.ToObtain) as Item;
if (obtain.Hide)
{
// has been added due to the riddle given by Enigma the Sphinx ( part of teh elven herritage quest )
AddHtmlLocalized(98, 172 + (i * 48), 350, 16, 1074869, 0x15F90, false, false);
}
else
{
AddHtmlLocalized(98, 172 + (i * 48), 350, 16, 1072205, 0x15F90, false, false); // Obtain
AddLabel(143, 172 + (i * 48), 0x481, obtain.Amount.ToString());
AddLabel(158, 172 + (i * 48), 0x481, string.Format("#{0}", toObtain.LabelNumber));
AddItem(350, 172 + (i * 48), toObtain.ItemID);
}
toObtain = null; // clear the resources
}
if (objective is IEscortObjective)
{
IEscortObjective escort = (IEscortObjective)current;
AddHtmlLocalized(173, 172, 312, 20, QuestHelper.GetDestination( escort.Destination.Name ), 0xFFFFFF, false, false); // the city of ~destination~
}
}
AddButton(275, 370, 0x2EE9, 0x2EEB, 0, GumpButtonType.Page, 3);
#endregion
#region Page 3 - Quest Reward
AddPage(3);
Closable = false;
AddImageTiled(50, 20, 400, 400, 0x1404);
AddImageTiled(50, 29, 30, 390, 0x28DC);
AddImageTiled(34, 140, 17, 279, 0x242F);
AddImage(48, 135, 0x28AB);
AddImage(-16, 285, 0x28A2);
AddImage(0, 10, 0x28B5);
AddImage(25, 0, 0x28B4);
AddImageTiled(83, 15, 350, 15, 0x280A);
AddImage(34, 419, 0x2842);
AddImage(442, 419, 0x2840);
AddImageTiled(51, 419, 392, 17, 0x2775);
AddImageTiled(415, 29, 44, 390, 0xA2D);
AddImageTiled(415, 29, 30, 390, 0x28DC);
AddLabel(100, 50, 0x481, "");
AddImage(370, 50, 0x589);
AddImage(379, 60, 0x15A9);
AddImage(425, 0, 0x28C9);
AddImage(90, 33, 0x232D);
AddHtmlLocalized(130, 45, 270, 16, 1046026, 0xFFFFFF, false, false); // Quest Log
AddImageTiled(130, 65, 175, 1, 0x238D);
AddButton(95, 395, 0x2EE0, 0x2EE2, 1, GumpButtonType.Reply, 0);
AddButton(313, 395, 0x2EF2, 0x2EF4, 2, GumpButtonType.Reply, 0);
AddHtmlLocalized(160, 108, 250, 16, m_Quest.QuestTitle, 0x2710, false, false);
AddButton(130, 370, 0x2EEF, 0x2EF1, 0, GumpButtonType.Page, 2);
AddHtmlLocalized(98, 140, 312, 16, 1072201, 0x2710, false, false); // Reward
if (Reward != null)
{
Item it = Activator.CreateInstance((Type)Reward.Type) as Item;
AddImage(105, 163, it.ItemID);
AddLabel(120, 162, 0x481, m_Quest.Reward.ToString());
AddHtmlLocalized(160, 162, 280, 180, it.LabelNumber, 0x15F90, false, false);
it = null;
}
#endregion
}
public override void OnResponse(NetState sender, RelayInfo info)
{
if (m_Quest.OfferGumpHandler != null)
m_Quest.OfferGumpHandler(sender, info);
}
}

This is the quest offer gump, it consists of three major pages.
1. Quest Description - contains the let's say NPC speech
2. Quest Objectives - kill / obtain this...
3. Reward
Here is the rest of interfaces / classes.

CODE
public enum ObjectiveType { Slay = 1072204, Obtain, Escort,
Delivery
}
public enum ObjectiveState
{
NotStarted,
InProgress,
Finished,
Failed
}
public interface IObjective
{
bool Completed { get; set; }
ObjectiveType Type { get; }
ObjectiveState State { get; set; }
IQuest Parent { get; set; }
DateTime ObjectiveStarted { get; set; }
TimeSpan LifeTime { get; set; }
void Invalidate ( object o );
void Serialize ( GenericWriter writer );
void Deserialize ( GenericReader reader );
}
public interface ISlayObjective : IObjective
{
Type ToSlay { get; set; }
Region Area { get; set; }
int Amount { get; set; }
int Slayed { get; set; }
}
public interface IObtainObjective : IObjective
{
Type ToObtain { get; set; }
int Amount { get; set; }
int Obtained { get; set; }
bool Hide { get; set; }
int Message { get; set; }
}
public interface IEscortObjective : IObjective
{
Region Destination { get; set; }
}
public interface IQuest
{
int QuestTitle { get; set; }
int QuestDescription { get; set; }
int QuestDecline { get; set; }
IObjective[] Objective { get; set; }
Mobile Quester { get; set; }
Mobile Giver { get; set; }
bool Repeatable { get; set; }
object Reward { get; set; }
IQuest QuestChain { get; set; }
QuestOfferGump OfferGump { get; set; }
GumpHandler OfferGumpHandler { get; set; }
QuestStatusGump StatusGump { get; set; }
GumpHandler StatusGumpHandler { get; set; }
QuestDeclineGump DeclineGump { get; set; }
GumpHandler DeclineGumpHandler { get; set; }
void OnQuestAccept ();
void OnQuestEnd ();
bool GiveReward ();
void Invalidate ();
}
public delegate void GumpHandler ( NetState state, RelayInfo info );
public static class QuestHelper
{
public struct DestinationInfo
{
public string RegionName;
public int Title;
public int CityName;
public int NotYetArrived;
public DestinationInfo(string rn, int t, int cn, int nya)
{
RegionName = rn;
Title = t;
CityName = cn;
NotYetArrived = nya;
}
}
public static DestinationInfo[] Destinations = new DestinationInfo[] {
/* City Name */ /* Title Desc NotArrived */
new DestinationInfo("Britain", 1072286, 1072231, 1072300 ),
new DestinationInfo("Yew", 1072275, 1072227, 1072289 ),
new DestinationInfo("Minoc", 1072282, 1072228, 1072296 ),
new DestinationInfo("Vesper", 1072276, 1072229, 1072290 ),
new DestinationInfo("Cove", 1072285, 1072230, 1072299 ),
new DestinationInfo("Moonglow", 1072281, 1072232, 1072295 ),
new DestinationInfo("Magincia", 1072283, 1072233, 1072297 ),
new DestinationInfo("Occlo", 1072312, 1072234, 1072313 ),
new DestinationInfo("Skara Brae", 1072278, 1072235, 1072292 ),
new DestinationInfo("Trinsic", 1072277, 1072236, 1072291 ),
new DestinationInfo("Nujel'm", 1072280, 1072237, 1072294 ),
new DestinationInfo("Jhelom", 1072284, 1072239, 1072298 )
};
public static int GetDestination(string name)
{
foreach (DestinationInfo di in Destinations)
{
if (di.RegionName == name)
return di.CityName;
}
return -1;
}
}
// Generic slay objective
public class SlayObjective : ISlayObjective
{
private Type m_ToSlay;
private Region m_Area;
private int m_Amount;
private int m_Slayed;
private bool m_Completed;
private ObjectiveType m_Type;
private ObjectiveState m_State;
private IQuest m_Parent;
private DateTime m_ObjectiveStarted;
private TimeSpan m_LifeTime;
public TimeSpan LifeTime
{
get { return m_LifeTime; }
set { m_LifeTime = value; }
}
public DateTime ObjectiveStarted
{
get { return m_ObjectiveStarted; }
set { m_ObjectiveStarted = value; }
}
public IQuest Parent
{
get { return m_Parent; }
set { m_Parent = value; }
}
public ObjectiveState State
{
get { return m_State; }
set { m_State = value; }
}
public ObjectiveType Type
{
get { return m_Type; }
set { m_Type = value; }
}
public bool Completed
{
get { return m_Completed; }
set { m_Completed = value; }
}
public int Slayed
{
get { return m_Slayed; }
set { m_Slayed = value; }
}
public int Amount
{
get { return m_Amount; }
set { m_Amount = value; }
}
public Region Area
{
get { return m_Area; }
set { m_Area = value; }
}
public Type ToSlay
{
get { return m_ToSlay; }
set { m_ToSlay = value; }
}
public SlayObjective(
Type toSlay,
Region area,
int amount,
IQuest parent
)
{
m_ToSlay = toSlay;
m_Area = area;
m_Amount = amount;
m_Parent = parent;
m_ObjectiveStarted = DateTime.Now;
m_State = ObjectiveState.NotStarted;
m_Type = ObjectiveType.Slay;
m_LifeTime = TimeSpan.Zero;
}
public virtual bool Invalidate(object o)
{
if (m_Completed)
{
return false;
}
if (m_LifeTime != TimeSpan.Zero)
{
if (m_ObjectiveStarted + m_LifeTime < DateTime.Now)
{
m_State = ObjectiveState.Failed;
m_Completed = true;
m_Parent.Invalidate();
return false;
}
}
BaseCreature monster = o as BaseCreature;
if (monster != null)
{
if (m_Area == null || m_Area.Contains( monster.Location ))
{
if (m_Slayed++ >= m_Amount)
{
m_Completed = true;
m_State = ObjectiveState.Finished;
m_Parent.Quester.SendLocalizedMessage(1075050); // You have killed all the required quest creatures of this Type.
m_Parent.Invalidate();
}
else
{
m_Parent.Quester.SendLocalizedMessage(1075051, string.Format("{0}", (m_Amount - m_Slayed))); // You have killed a quest creature. ~1_val~ more left.
}
}
}
return true;
}
public void Serialize(GenericWriter writer)
{
// TODO
}
public void Deserialize(GenericReader reader)
{
// TODO
}
}

Just small addition to this line

CODE
AddLabel(163, 172 + (i * 48), 0x481, Talisman.TypeRegistry[slay.ToSlay]);

, it's a dictionary with pairs "CreatureType" x "CreatureName", I've had to do it because the talismans has the killer a protection properties which uses string as arguments and when I chose for example the ratman it showed up "%Crazy Ratman Name Here% killer: 46%", which looks bad.
So if anyone want I can release the talisman script here, but it's for the c# 2.0 :-/
( I don't want to release it on the RunUO forums now, I don't like the current state of the community it's not what it used to be... )
arul- 01-19-2006
I am wondering about the xmlattachments, do "OnKill(Mobile killed, Mobile killer" method tracks the death of basecreature too ?
I am writing a brand new attachment called, hmm, let's say MLQuestAttachment that will work the similar way that the current quest system does.
ArteGordon- 01-19-2006

QUOTE (arul @ Jan 19 2006, 11:02 AM)
I am wondering about the xmlattachments, do "OnKill(Mobile killed, Mobile killer" method tracks the death of basecreature too ? I am writing a brand new attachment called, hmm, let's say MLQuestAttachment that will work the similar way that the current quest system does.

yes they do. They can track both player and creature kills if both installation steps 2 and 6 have been followed.
There is also the OnKilled method and the OnBeforeKill and OnBeforeKilled methods
from xmlspawner2.txt

QUOTE
- added support for the overridable OnBeforeKill and OnBeforeKilled methods on attachments. public virtual void OnBeforeKill(Mobile killed, Mobile killer ); public virtual void OnBeforeKilled(Mobile killed, Mobile killer ); These are called before the OnKill and OnKilled methods allowing you to check things that might be changed in the OnKill or OnKilled methods.

Galfaroth- 01-26-2006
Here is how new UO's quest system seems like:
http://guide.uo.com/questsystem.html
ArteGordon- 01-26-2006
thanks. Looks similar to the xmlspawner quest system
Galfaroth- 01-27-2006
So will there be an enchancement for XMLSpawner? And where can I find tutorials how to make this kind of quests?
ArteGordon- 01-27-2006
exactly what kind of quest are you interested in making?
Galfaroth- 01-27-2006
Escorts are in standard RUNUO. So maybe: Bring item(from location or monster); kill monster and for those two rewards for completing.
Zyle- 01-27-2006

QUOTE (Galfaroth @ Jan 27 2006, 10:56 AM)
So will there be an enchancement for XMLSpawner? And where can I find tutorials how to make this kind of quests?

This thread will probably be of use to you
http://xmlspawner.15.forumer.com/index.php?showtopic=5
ArteGordon- 01-27-2006
yes, you can make those sorts of quests. You can have escort, collect item, give item, and kill creature sorts of objectives and any combination. The Harmons quest example in the Files section is a quest that combines those types.