RunUO Community

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

[1.0 & 2] Killable RP Guards

Bujinsho

Page
[1.0 & 2] Killable RP Guards

Every now and then I see requests for Killable Guards to replace the insta-kill teleporting type and there don't seem to be many offerings so I thought its about time to give a little something back to the community.

Most of my stuff contains so much shard specific stuff, its hard to think about releasing them, but these should drop straight in to any shard - fully plug and play ... just drop them in you customs folder and restart your shard.

There are two versions here ... my old RunUO 1.0 Final version, and a slightly improved RC2 version. The improvements are only in neater coding and implementation ... functionality is identical in both versions.

Just spawn these guards with a spawner and they will wander around attacking monsters and criminal players on sight and ... erm ... thats it really. They are Guards aimed at RP shards.

Download Version1 Guards for RUO 1.0 Final

Download KillableGuards for RC2 ... I see no reason they won't work on all RUO 2 Versions, but I only guarantee them for RC2 :)
 

Attachments

  • KillableGuards.rar
    2.8 KB · Views: 523
  • Version1 Guards.rar
    4.9 KB · Views: 145

Greystar

Wanderer
These look okay, I just wanted to let it be known that these ARE NOT a guard replacement. They are a mobile that could function like a guard. There is no code in there for "Calling" them in a guarded region or for them to detect thieves or hidden. I glanced at it and saw there is protection against mobs that are "AGG" if they come in range of the "Guardian."


The more I see posts of people looking for Killable Guards *again*. I might try to re-write mine for at least the SVN version which MAY work with RC2 but I make no promises.


Again these could be usefull to say stick in an outpost somewhere, where a player could run to if they were being overwhelmed and the guards might assist them, but they can't be called to help as they stand now.
 

Bujinsho

Page
Greystar;799993 said:
These look okay, I just wanted to let it be known that these ARE NOT a guard replacement. They are a mobile that could function like a guard. There is no code in there for "Calling" them in a guarded region or for them to detect thieves or hidden. I glanced at it and saw there is protection against mobs that are "AGG" if they come in range of the "Guardian."


The more I see posts of people looking for Killable Guards *again*. I might try to re-write mine for at least the SVN version which MAY work with RC2 but I make no promises.


Again these could be usefull to say stick in an outpost somewhere, where a player could run to if they were being overwhelmed and the guards might assist them, but they can't be called to help as they stand now.

Yep, yep ... dead right ... I should have made that clearer. On my shard I don't have guarded regions, I simply spawn these guards and either have them stand around or follow waypoints so they patrol the town. Also ... as Greystar said ... they live in outposts. The idea is ... You run to them and they will protect you from aggressive mobiles. I've got an AI for them that I'm working on that makes hem react to the keyword Guards! by turning toward the calling mobile and immediately trying to acquire a hostile target ... its a bit of a "beta" atm but if ppl are interested I could post it up here.

I call them RP guards because they are more like RL police than OSI guards ... more realistic ... and allow "criminal" rpers to (try) and avoid them ... good point about detecting hidden criminals tho Greystar ... that I think I'll do.
 

Greystar

Wanderer
Just an FYI you can do that without an AI so to speak. You can make it so that the respond to "voice" look into the

For instance this bit of code I grabbed from the Communications crystals

Code:
		public override bool HandlesOnSpeech
		{
			get{ return Active && Receivers.Count > 0 && ( RootParent == null || RootParent is Mobile ); }
		}

		public override void OnSpeech( SpeechEventArgs e )
		{
			if ( !Active || Receivers.Count == 0 || ( RootParent != null && !(RootParent is Mobile) ) )
				return;

			if ( e.Type == MessageType.Emote )
				return;

			Mobile from = e.Mobile;
			string speech = e.Speech;

			foreach ( ReceiverCrystal receiver in new List<ReceiverCrystal>( Receivers ) )
			{
				if ( receiver.Deleted )
				{
					Receivers.Remove( receiver );
				}
				else if ( Charges > 0 )
				{
					receiver.TransmitMessage( from, speech );
					Charges--;
				}
				else
				{
					this.Active = false;
					break;
				}
			}
		}

I used this stuff in my staff stone
below is what i used in that... there might be something to use as an example in vendors or bankers too..
Code:
		public override bool HandlesOnSpeech{ get{ return true; } }

		public override void OnSpeech( SpeechEventArgs e )
		{
			if ( !e.Handled && Insensitive.Equals( e.Speech, "i claim this staff stone" ) && m_Owner == null )
			{
				m_Owner = e.Mobile;
				if (m_Owner.AccessLevel == AccessLevel.Player)
				{
					if (!HideEffects)
						this.Say( String.Format("You are not authorized to have a Staff Stone!" ) );
					else
						e.Mobile.SendMessage( "You are not authorized to have a Staff Stone!" );
					deleting = true;
					DoDelete(this);
					return;
				}
				else
				{
					if (!HideEffects)
						this.Say( String.Format("How May I serve you master!") );
					else
						e.Mobile.SendMessage( "How May I serve you master!" );
					Name = m_Owner.Name + "'s Staff Stone";
					this.StaffLevel = m_Owner.AccessLevel;
					LootType = LootType.Blessed;
					this.HomeLocation = m_Owner.Location;
					this.HomeMap = m_Owner.Map;
					if (m_Owner.Title != null )
						this.Title = m_Owner.Title;
				}
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "release stone" ) && m_Owner == e.Mobile )
			{
				if (!HideEffects)
					this.Say( String.Format("You released me!") );
				else
					e.Mobile.SendMessage( "You released me!" );
				Movable = true;
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "reveal" ) && m_Owner == e.Mobile )
			{
				if (!HideEffects)
					this.Say( String.Format("You are no longer hidden.") );
				else
					e.Mobile.SendMessage( "You are no longer hidden." );
				e.Mobile.Hidden = false;
				if (!HideEffects)
				{
					Effects.SendLocationEffect( new Point3D( e.Mobile.X + 1, e.Mobile.Y, e.Mobile.Z + 4 ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X + 1, e.Mobile.Y, e.Mobile.Z ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X + 1, e.Mobile.Y, e.Mobile.Z - 4 ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X, e.Mobile.Y + 1, e.Mobile.Z + 4 ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X, e.Mobile.Y + 1, e.Mobile.Z ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X, e.Mobile.Y + 1, e.Mobile.Z - 4 ), e.Mobile.Map, 0x3728, 13 );

					Effects.SendLocationEffect( new Point3D( e.Mobile.X + 1, e.Mobile.Y + 1, e.Mobile.Z + 11 ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X + 1, e.Mobile.Y + 1, e.Mobile.Z + 7 ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X + 1, e.Mobile.Y + 1, e.Mobile.Z + 3 ), e.Mobile.Map, 0x3728, 13 );
					Effects.SendLocationEffect( new Point3D( e.Mobile.X + 1, e.Mobile.Y + 1, e.Mobile.Z - 1 ), e.Mobile.Map, 0x3728, 13 );

					e.Mobile.PlaySound( 0x228 );
				}
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "secure stone" ) && m_Owner == e.Mobile )
			{
				if (!HideEffects)
					this.Say( String.Format("You secured me!") );
				else
					e.Mobile.SendMessage("You secured me!");
				Movable = false;
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "switch player" ) && m_Owner == e.Mobile )
			{
				if (!HideEffects)
					this.Say( String.Format("You change your level to a common Player.") );
				else
					e.Mobile.SendMessage("You change your level to a common Player.");
				m_StaffLevel = e.Mobile.AccessLevel;
				e.Mobile.AccessLevel = AccessLevel.Player;
				if (!HideEffects)
					e.Mobile.BoltEffect( 0 );
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "switch normal" ) && m_Owner == e.Mobile )
			{
				if (!HideEffects)
					this.Say( String.Format("You change your level back.") );
				else
					e.Mobile.SendMessage("You change your level back.");
				e.Mobile.AccessLevel = m_StaffLevel;
				if (!HideEffects)
					e.Mobile.BoltEffect( 0 );
				e.Handled = true;
			}

			// With the autoressurect this may not be needed anymore but I left it in anyway
			if ( !e.Handled && Insensitive.Equals( e.Speech, "resurrect me" ) && m_Owner == e.Mobile )
			{
				e.Mobile.PlaySound( 0x214 );
				e.Mobile.FixedEffect( 0x376A, 10, 16 );

				e.Mobile.Resurrect();
				e.Mobile.Hidden = true;
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "res me" ) && m_Owner == e.Mobile )
			{
				e.Mobile.PlaySound( 0x214 );
				e.Mobile.FixedEffect( 0x376A, 10, 16 );

				e.Mobile.Resurrect();
				e.Mobile.Hidden = true;
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "set title" ) && m_Owner == e.Mobile )
			{
				this.Title = e.Mobile.Title;
				e.Mobile.SendMessage( "Your current title has been saved on the stone." );
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "clear title" ) && m_Owner == e.Mobile )
			{
				this.Title = null;
				e.Mobile.SendMessage( "Your current title has been cleared from the stone." );
				e.Handled = true;
			}

			if ( !e.Handled && Insensitive.Equals( e.Speech, "title" ) && m_Owner == e.Mobile )
			{
				switch ( this.StaffLevel )
				{
					case AccessLevel.Administrator:
					{
						if (e.Mobile.Title != this.Title)
							e.Mobile.Title = this.Title;
						else
							e.Mobile.Title = "[Administrator]";
						break;
					}
					case AccessLevel.Seer:
					{
						if (e.Mobile.Title != this.Title)
							e.Mobile.Title = this.Title;
						else
							e.Mobile.Title = "[Seer]";
						break;
					}
					case AccessLevel.GameMaster:
					{
						if (e.Mobile.Title != this.Title)
							e.Mobile.Title = this.Title;
						else
							e.Mobile.Title = "[GameMaster]";
						break;
					}
					case AccessLevel.Counselor:
					{
						if (e.Mobile.Title != this.Title)
							e.Mobile.Title = this.Title;
						else
							e.Mobile.Title = "[Counselor]";
						break;
					}
					case AccessLevel.Player:
					{
						if (e.Mobile.Title != this.Title)
							e.Mobile.Title = this.Title;
						else
							e.Mobile.Title = null;
						break;
					}
					default:
					{
						if (e.Mobile.Title != this.Title)
							e.Mobile.Title = this.Title;
						else
							e.Mobile.Title = null;
						break;
					}
				}
				e.Mobile.SendMessage( "Your title has been adjusted." );
				if (!HideEffects)
					e.Mobile.BoltEffect( 0 );
				e.Handled = true;
			}

			base.OnSpeech( e );
		}
 

David

Moderate
Very nice Bujinsho, this is the way I like guards to react. I never really cared for the pop-in pop-out callable guards. And thanks Greystar for your help as well.
 

Greystar

Wanderer
David;800027 said:
Very nice Bujinsho, this is the way I like guards to react. I never really cared for the pop-in pop-out callable guards. And thanks Greystar for your help as well.

Thanks I never really liked them either and it was the entire reason I released my other version of Killable Guards for RUNUO 1.0. Although in my opinion I never finished them, but lost interest in continuing with the development.


I might borrow these and run with em. we'll see what pops up. I'll watch this thread though and offer suggestions or whatnot if the original poster has any.
 

Bujinsho

Page
Another use ...

Code:
If you want or have "blue" vendors who are a little more interactive you can change the base class they derive from like so (RC2 only ... unless you rewrite the old version on similar lines ... it wouldn't be hard but I'm quite lazy :p )

Anyway ... change BaseVendor.cs as follows:

From this ....

Code:
namespace Server.Mobiles
{
	public enum VendorShoeType
	{
		None,
		Shoes,
		Boots,
		Sandals,
		ThighBoots
	}

	public abstract class BaseVendor : BaseCreature, IVendor

to this ....

Code:
namespace Server.Mobiles
{
	public enum VendorShoeType
	{
		None,
		Shoes,
		Boots,
		Sandals,
		ThighBoots
	}

	public abstract class BaseVendor :[COLOR="Red"] BaseGuardian[/COLOR], IVendor

And from this ...

Code:
public BaseVendor( string title ) : base( AIType.AI_Vendor, FightMode.None, 2, 1, 0.5, 2 )
		{
			LoadSBInfo();

			this.Title = title;
			InitBody();
			InitOutfit();

to this ...

Code:
public BaseVendor( [COLOR="red"]AIType ai, FightMode fm, int PR, int FR, double AS, double PS, string title ) : base( ai, fm, PR, FR, AS, PS [/COLOR])

Also make sure that this line is as follows:

Code:
public virtual bool IsInvulnerable{ get{ return [COLOR="red"]false[/COLOR]; } }

Finally ...for each vendor you must give them appropriate combat skills and a fight mode and a different AI

Here is an example ...

Code:
[Constructable]
        public Blacksmith()
            : base([COLOR="red"]AIType.AI_Melee, FightMode.Closest, 3, 1, 0.2, 2,[/COLOR] "the blacksmith")
		{
            SetStr(100, 100);
            SetDex(75, 75);
            SetInt(61, 75);

			SetSkill( SkillName.ArmsLore, 36.0, 68.0 );
			SetSkill( SkillName.Blacksmith, 65.0, 88.0 );
			SetSkill( SkillName.Fencing, 60.0, 83.0 );
			SetSkill( SkillName.Macing, 61.0, 93.0 );
			SetSkill( SkillName.Swords, 60.0, 83.0 );
			SetSkill( SkillName.Tactics, 60.0, 83.0 );
			SetSkill( SkillName.Parry, 61.0, 93.0 );

Quite a bit of work, given the number of vendor files there are, but when you are done you will have vendors that now also act as a low level guard. They need to be a bit quicker (when ActiveSpeed is required) than normal vendors ... but not too much ....and they have a low perception range ... but these vendors will defend their shop or pitch from monsters and criminals ... and will chase any player they catch stealing from them. They are quite easy to escape from ... due to lowish speed and short perception range ... but will add a little more realism to an RP based shard.

BIG DISADVANTAGE:

You will have to delete all your old vendors on server restart ... now I guess this is a Serialisation thing, but sadly I don't understand that subject terribly well and I'm not sure you can have versioning when the base class is different. Maybe somebody else can help ?

Anyway ... not really part of the package but a suggestion of a way this class can be used ... I have many types of NPC derived from the BaseGuardian class, it does not affect any normal functionality but does turn your town folk NPC's (or whatever) into a horde of pitchfork waving peasants on occaision :)
 

Mideon

Page
Just a quick tip if anyone loves these but would like to have the speech functionality. You can always spawn these using an XMLSpawner that only responds to a player's voice.

So if you are in an alley that contains a spawner that responds to "Guards", then when they call...you can make the guards spawn.

Be a neat little thing if you made the AI get angry if there were no targets around haha. Anyways thanks for the release.
 

Bujinsho

Page
Mideon;800060 said:
Just a quick tip if anyone loves these but would like to have the speech functionality. You can always spawn these using an XMLSpawner that only responds to a player's voice.

So if you are in an alley that contains a spawner that responds to "Guards", then when they call...you can make the guards spawn.

Be a neat little thing if you made the AI get angry if there were no targets around haha. Anyways thanks for the release.

Its a good suggestion and I did play with that idea ...

A spawner that responds to the word guards works, except there was no way to stop players running around town shouting "Guards" and spawning loads of guards all over the place :mad:

Also the spawner needs to have a Despawn time ... else you end up with millions of guards :mad:

If you do have a despawn time, guards can suddenly vanish while in Hot Pursuit of a criminal :mad:

I tried a spawner that was triggered by a "Grey Player" passing ... but that was a bit unfair on the Greys *shrugs*

Still ... if you can live with the drawbacks or work your way around them, then this is a fine suggestion :)
 

Thilgon

Sorceror
uhm just thinking...
haven't searched yet, but, how about providing some ways to disable permanentely instakillteleportpopguards that cames with guarded regions?
 

Bujinsho

Page
I guess the easiest way to do that is to go through you Regions.xml file in the Data directory and for each town, add the line as in this example:

Code:
<region type="TownRegion" priority="50" name="Britain">
			<rect x="1416" y="1498" width="324" height="279" zmin="-10" />
			<rect x="1500" y="1408" width="46" height="90" />
			<rect x="1385" y="1538" width="31" height="239" zmin="-10" />
			<rect x="1416" y="1777" width="324" height="60" />
			<rect x="1385" y="1777" width="31" height="130" />
			<rect x="1093" y="1538" width="292" height="369" />
			<go x="1495" y="1629" z="10" />
			<music name="Britain1" />
			[COLOR="Red"]<guards disabled="true" />[/COLOR]
			<zrange min="0" />
			<region>
 

Greystar

Wanderer
Thilgon;800108 said:
uhm just thinking...
haven't searched yet, but, how about providing some ways to disable permanentely instakillteleportpopguards that cames with guarded regions?

this below code is a small excerpt from GuardedRegion.cs
Code:
	public class GuardedRegion : BaseRegion
	{
		private static object[] m_GuardParams = new object[1];
		private Type m_GuardType;
		private bool m_Disabled;

Code:
	public class GuardedRegion : BaseRegion
	{
		private static object[] m_GuardParams = new object[1];
		private Type m_GuardType;
		private bool m_Disabled = true; // <== this change will make it so guarded regions are disabled everywhere

The other way to do it is in the XML file for the regions itself but I haven't gotten into exploring how they work in 2.0 RC2/SVN yet.

EDIT: looks like Bujinsho is more familiar with the XML file :)
 

Ballad

Wanderer
Thanks for this script! This is exactly what I was looking for to put in my shard, with a few minor exceptions. ;)
 
hey love your script help me out??

i tried changing the name and the clothes they wear this is the script i have now converted to

Code:
		Title = "the Battle Mage, Guardian"; 

			AddItem( new PadsOfTheCuSidhe() );
			AddItem( new HatOfTheMagi(1153) );
			AddItem( new Cloak(1153) );
			AddItem( new Robe(1153) );
			AddItem( new RuneBeetleCarapace(1153) );
			AddItem( new InquisitorsResolution(1153) );
			AddItem( new OrnamentOfTheMagician(1153) );
			AddItem(new LeatherLegs());
			

			SetStr( 1000, 1000 );
			SetDex( 250, 250 );
			SetInt( 250, 250 );

			SetSkill( SkillName.MagicResist, 120.0,120.0 );
			SetSkill( SkillName.Poisoning, 100.0,100.0 );
			SetSkill( SkillName.Inscribe, 100.0,100.0 );

that is what i have changed

whenver i try to add it to my server. my server says this

Code:
System.Reflection.TargetInvocationException: Exception has been thrown by the ta
rget of an invocation. ---> System.NullReferenceException: Object reference not
set to an instance of an object.
   at Server.Items.BaseClothing.OnAdded(Object parent)
   at Server.Mobile.AddItem(Item item)
   at Server.Mobiles.MageGuardian..ctor()
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle._InvokeConstructor(Object[] args, SignatureStru
ct& signature, IntPtr declaringType)
   at System.RuntimeMethodHandle.InvokeConstructor(Object[] args, SignatureStruc
t signature, RuntimeTypeHandle declaringType)
   at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, B
inder binder, Object[] parameters, CultureInfo culture)
   at Server.Commands.Add.Build(Mobile from, ConstructorInfo ctor, Object[] valu
es, String[,] props, PropertyInfo[] realProps, Boolean& sendError)
   at Server.Commands.Add.Build(Mobile from, Point3D start, Point3D end, Constru
ctorInfo ctor, Object[] values, String[,] props, PropertyInfo[] realProps, List`
1 packs)


what am i doing wrong
 

Bujinsho

Page
Yeah sure :)

You can't pass colour arguments to the constructors for the artifacts:

Change

Code:
Title = "the Battle Mage, Guardian"; 

			AddItem( new PadsOfTheCuSidhe() );
			AddItem( new HatOfTheMagi(1153) );
			AddItem( new Cloak(1153) );
			AddItem( new Robe(1153) );
			AddItem( new RuneBeetleCarapace(1153) );
			AddItem( new InquisitorsResolution(1153) );
			AddItem( new OrnamentOfTheMagician(1153) );
			AddItem(new LeatherLegs());

to

Code:
Title = "the Battle Mage, Guardian"; 

			AddItem( new PadsOfTheCuSidhe() );
			AddItem( new HatOfTheMagi() );
			AddItem( new Cloak(1153) );
			AddItem( new Robe(1153) );
			AddItem( new RuneBeetleCarapace() );
			AddItem( new InquisitorsResolution() );
			AddItem( new OrnamentOfTheMagician() );
			AddItem(new LeatherLegs());

and it'll be fine.

If you really MUST have different coloured artis then you need to edit the scripts like this:

from this:

Code:
 		[Constructable]
		public HatOfTheMagi()
		{
			Hue = 0x481;

			Attributes.BonusInt = 8;
			Attributes.RegenMana = 4;
			Attributes.SpellDamage = 10;
		}

to this:

Code:
        [Constructable]
        public HatOfTheMagi():this(0x481)
        {
        }

		[Constructable]
		public HatOfTheMagi(int hue)
		{
			Hue = hue;

			Attributes.BonusInt = 8;
			Attributes.RegenMana = 4;
			Attributes.SpellDamage = 10;
		}
 
k one last thing i changed it


now how do i make it so it cant be looted ?


heres the script

using System;
using System.Collections;
using Server.Misc;
using Server.Items;
using Server.Mobiles;

namespace Server.Mobiles
{
public class MageGuardian : BaseGuardian
{

[Constructable]
public MageGuardian() : base( AIType.AI_Mage, FightMode.Weakest, 10, 5, 0.1, 0.2 )
{
Title = "the Battle Mage, Guardian";

AddItem( new PadsOfTheCuSidhe() );
AddItem( new HatOfTheMagi() );
AddItem( new Cloak(1150) );
AddItem( new Robe(1150) );
AddItem( new RuneBeetleCarapace() );
AddItem( new InquisitorsResolution() );
AddItem( new OrnamentOfTheMagician() );
AddItem(new LeatherLegs());

SetStr( 1000, 1000 );
SetDex( 250, 250 );
SetInt( 250, 250 );

SetSkill( SkillName.MagicResist, 120.0,120.0 );
SetSkill( SkillName.Poisoning, 100.0,100.0 );
SetSkill( SkillName.Inscribe, 100.0,100.0 );
SetSkill( SkillName.Magery, 120.0, 120.0 );
SetSkill( SkillName.Tactics, 65.0, 87.5 );
SetSkill( SkillName.Wrestling, 120.0,120.0 );

if ( Female = Utility.RandomBool() )
{
Body = 401;
Name = NameList.RandomName( "female" );

AddItem(new LeatherChest());



}
else
{
Body = 400;
Name = NameList.RandomName( "male" );

AddItem(new LeatherChest());
AddItem(new LeatherArms());



}

Utility.AssignRandomHair( this );
}


public MageGuardian( Serial serial ) : base( serial )
{
}

public override void Serialize( GenericWriter writer )
{
base.Serialize( writer );

writer.Write( (int) 0 ); // version
}

public override void Deserialize( GenericReader reader )
{
base.Deserialize( reader );

int version = reader.ReadInt();
}
}
}
 
Top