Fixes walk-up aggro latency (up to a full 10 s of obliviousness) and hardens the reacquire gate so no state can silence acquisition, while turning `AcquireOnApproach` into the reaction-time knob for future per-creature intelligence tuning. ### Why `AcquireFocusMob` re-armed the 10 s `ReacquireDelay` **before** scanning, success or failure. A creature that scanned an empty room was blind for 10 s to a player walking up — walk-up aggro latency was uniform in 0..10 s. Waking from sector sleep stacked the AI timer's 0–3 s construction stagger on top. And `NextReacquireTime` is not serialized: on hosts whose tick counter starts negative (GCP pass-through), the 0 default blocked **all** acquisition shard-wide after a restart until the counter crossed zero. ### What **Event-driven reaction — `AcquireOnApproachDelay` (the intelligence gradient)** - The paragon `AcquireOnApproach` bool becomes a `TimeSpan` on every creature: an enemy moving inside `AcquireOnApproachRange` (10 for all creatures — on-screen reactive aggro; the periodic scan keeps the wide `RangePerception` sweep) *clamps* the next scan to at most the delay. Repeated steps cannot shorten it further — one scan per delay period, not per step or think. - `Zero` (paragons) also prods the AI timer: the ranked scan engages within a wheel turn — the old snap, minus the special-cased engage path. The target now comes from the normal FightMode ranking instead of whichever mobile happened to move, and the `Combatant == null` guard stops re-engage spam. - The 2 s default reads as "took a beat to notice you"; larger values are dumber; `ReacquireDelay` alone is the oblivious floor. Mover checks are the approach logic's `IsEnemy` + `CanBeHarmful` (so pets count and hidden movers are excluded via `CanSee`), with `IsEnemy` first to cheaply reject same-team wild creatures wandering past. The check rides the `OnMovement` callback every step already pays for — no polling added. **Gate correctness** - Every scan re-arms the full `ReacquireDelay`, success or failure (classic semantics; reaction time is the approach path, not the poll). - Self-healing by construction: a deadline further out than `ReacquireDelay` is an illegal state and reads as open — no wedged or wrapped value can silence acquisition beyond one delay period. - `NextReacquireTime` is seeded from a live tick on deserialize (the GCP negative-tick blackout). **AI timer wake** - Activation (sector wake, spawn, resurrection) starts within a 0–256 ms spread instead of the 0–3 s construction stagger, which read as lag. - The stagger's real job — keeping same-speed cohorts out of lock-step (the RunUO town artifact) — is now a zero-mean ±period/8 jitter on each **idle** think, so phases random-walk apart within seconds and can never re-lock. Instrumentation showed why a one-shot spread can't do this job: the timer wheel fires within ±1 ms, so with 10 creatures on a 500 ms period some pair collides on nearly the same phase ~75% of the time (birthday paradox) and then steps in the same loop iteration *forever*. Jitter is scoped to passive speed: engaged cadence stays exact, since pursuit timing anchors to real step times. **Debug** - The `AcquireFocusMob` scan message no longer re-arms the shared 5 s debug cooldown, which swallowed every AI's "I have detected X" transition line. **API change** for custom scripts: `AcquireOnApproach` (bool) → `AcquireOnApproachDelay` (TimeSpan). Documented in `content-patterns.md` § Target Acquisition, `runuo-migration-docs/09` + `11`, and the migration skill checklist. ### Tests `AcquisitionTests`: both scan outcomes honor `ReacquireDelay`; a 60 s-wedged gate still acquires; enemy movement clamps the deadline (same-team wild movers and out-of-range movers ignored); repeated movement cannot shorten below the delay; `Zero` opens the gate and prods without a direct engage. Full suite: 755 UOContent green.
671 lines
22 KiB
Markdown
671 lines
22 KiB
Markdown
# ModernUO Content Creation Patterns
|
||
|
||
This document covers the patterns and templates for creating game content in ModernUO: items, creatures, spells, skills, loot, context menus, and file organization.
|
||
|
||
## Table of Contents
|
||
1. [New Item](#new-item)
|
||
2. [New Creature](#new-creature)
|
||
3. [New Spell](#new-spell)
|
||
4. [Skill Implementation](#skill-implementation)
|
||
5. [Loot System](#loot-system)
|
||
6. [Context Menus](#context-menus)
|
||
7. [Entity Lifecycle](#entity-lifecycle)
|
||
8. [File Organization](#file-organization)
|
||
|
||
---
|
||
|
||
## New Item
|
||
|
||
### Minimal Item
|
||
```csharp
|
||
using ModernUO.Serialization;
|
||
|
||
namespace Server.Items;
|
||
|
||
[SerializationGenerator(0)]
|
||
public partial class SimpleItem : Item
|
||
{
|
||
[Constructible]
|
||
public SimpleItem() : base(0x1234) // itemID from UO art
|
||
{
|
||
Weight = 1.0;
|
||
}
|
||
|
||
public override string DefaultName => "a simple item";
|
||
// OR: public override int LabelNumber => 1234567; // cliloc number
|
||
}
|
||
```
|
||
|
||
### Item with Properties and Behavior
|
||
```csharp
|
||
using ModernUO.Serialization;
|
||
|
||
namespace Server.Items;
|
||
|
||
[SerializationGenerator(0)]
|
||
public partial class MagicLantern : Item
|
||
{
|
||
[SerializableField(0)]
|
||
[InvalidateProperties]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private int _charges;
|
||
|
||
[SerializableField(1)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private Mobile _owner;
|
||
|
||
private TimerExecutionToken _glowTimer;
|
||
|
||
[Constructible]
|
||
public MagicLantern() : base(0xA25)
|
||
{
|
||
_charges = Utility.RandomMinMax(5, 15);
|
||
Weight = 2.0;
|
||
Light = LightType.Circle300;
|
||
StartGlow();
|
||
}
|
||
|
||
public override string DefaultName => "a magic lantern";
|
||
|
||
private void StartGlow()
|
||
{
|
||
Timer.StartTimer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), Glow, out _glowTimer);
|
||
}
|
||
|
||
[AfterDeserialization]
|
||
private void AfterDeserialization() => StartGlow();
|
||
|
||
public override void OnAfterDelete()
|
||
{
|
||
_glowTimer.Cancel();
|
||
base.OnAfterDelete();
|
||
}
|
||
|
||
private void Glow()
|
||
{
|
||
if (_charges > 0)
|
||
Effects.SendLocationParticles(this, 0x376A, 9, 10, 5042);
|
||
}
|
||
|
||
public override void OnDoubleClick(Mobile from)
|
||
{
|
||
if (!IsChildOf(from.Backpack))
|
||
{
|
||
from.SendLocalizedMessage(1042001); // Must be in your backpack
|
||
return;
|
||
}
|
||
|
||
if (_charges <= 0)
|
||
{
|
||
from.SendMessage("The lantern is depleted.");
|
||
return;
|
||
}
|
||
|
||
Charges--;
|
||
from.SendMessage("The lantern flares brightly!");
|
||
from.FixedParticles(0x376A, 9, 32, 5042, EffectLayer.Waist);
|
||
}
|
||
|
||
public override void GetProperties(IPropertyList list)
|
||
{
|
||
base.GetProperties(list);
|
||
list.Add(1060741, $"{_charges}"); // "charges: ~1_val~"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Common Item Base Classes
|
||
| Base Class | Use For |
|
||
|---|---|
|
||
| `Item` | Generic items |
|
||
| `BaseWeapon` | Melee weapons |
|
||
| `BaseRanged` | Ranged weapons (bows, crossbows) |
|
||
| `BaseArmor` | Armor pieces |
|
||
| `BaseShield` | Shields |
|
||
| `BaseClothing` | Wearable clothing |
|
||
| `BaseJewel` | Rings, bracelets, necklaces |
|
||
| `BaseContainer` | Containers (bags, boxes) |
|
||
| `BasePotion` | Potions |
|
||
| `BaseReagent` | Spell reagents |
|
||
| `Food` | Edible items |
|
||
| `SpellScroll` | Spell scrolls |
|
||
|
||
### Key Item Properties
|
||
```csharp
|
||
Weight = 1.0; // Item weight in stones
|
||
Stackable = true; // Can stack with same type
|
||
Amount = 1; // Stack amount
|
||
Movable = true; // Can be picked up
|
||
Visible = true; // Visible to players
|
||
Hue = 0; // Color hue (0 = default)
|
||
Light = LightType.Circle300; // Light emission
|
||
LootType = LootType.Regular; // Regular, Newbied, Blessed, Cursed
|
||
Layer = Layer.OneHanded; // Equipment layer
|
||
```
|
||
|
||
---
|
||
|
||
## New Creature
|
||
|
||
### Basic Creature
|
||
```csharp
|
||
using ModernUO.Serialization;
|
||
using Server.Items;
|
||
|
||
namespace Server.Mobiles;
|
||
|
||
[SerializationGenerator(0)]
|
||
public partial class ForestWolf : BaseCreature
|
||
{
|
||
[Constructible]
|
||
public ForestWolf() : base(AIType.AI_Melee, FightMode.Closest)
|
||
{
|
||
Body = 225; // Wolf body graphic
|
||
BaseSoundID = 0xE5; // Base sound ID
|
||
|
||
SetStr(80, 120);
|
||
SetDex(90, 110);
|
||
SetInt(20, 40);
|
||
|
||
SetHits(60, 80);
|
||
SetMana(0);
|
||
|
||
SetDamage(8, 14);
|
||
|
||
SetDamageType(ResistanceType.Physical, 100);
|
||
|
||
SetResistance(ResistanceType.Physical, 25, 35);
|
||
SetResistance(ResistanceType.Fire, 5, 10);
|
||
SetResistance(ResistanceType.Cold, 15, 25);
|
||
SetResistance(ResistanceType.Poison, 10, 15);
|
||
SetResistance(ResistanceType.Energy, 5, 10);
|
||
|
||
SetSkill(SkillName.MagicResist, 30.0, 50.0);
|
||
SetSkill(SkillName.Tactics, 50.0, 70.0);
|
||
SetSkill(SkillName.Wrestling, 50.0, 70.0);
|
||
|
||
Fame = 600;
|
||
Karma = 0;
|
||
|
||
VirtualArmor = 28;
|
||
|
||
Tamable = true;
|
||
ControlSlots = 1;
|
||
MinTameSkill = 50.1;
|
||
}
|
||
|
||
public override string CorpseName => "a wolf corpse";
|
||
public override string DefaultName => "a forest wolf";
|
||
public override int Meat => 1;
|
||
public override int Hides => 6;
|
||
public override HideType HideType => HideType.Regular;
|
||
public override FoodType FavoriteFood => FoodType.Meat;
|
||
public override PackInstinct PackInstinct => PackInstinct.Canine;
|
||
|
||
public override void GenerateLoot()
|
||
{
|
||
AddLoot(LootPack.Meager);
|
||
}
|
||
}
|
||
```
|
||
|
||
### AI Types
|
||
| AIType | Use For |
|
||
|---|---|
|
||
| `AI_Melee` | Warriors, melee fighters |
|
||
| `AI_Mage` | Spellcasters |
|
||
| `AI_Archer` | Ranged attackers |
|
||
| `AI_Animal` | Passive animals (flee/fight back) |
|
||
| `AI_Predator` | Hunting animals |
|
||
| `AI_Healer` | Healing NPCs |
|
||
| `AI_Vendor` | Shop NPCs |
|
||
| `AI_Berserk` | Mindless aggressors |
|
||
| `AI_Thief` | Pickpockets |
|
||
|
||
### Fight Modes
|
||
| FightMode | Behavior |
|
||
|---|---|
|
||
| `None` | Never attacks |
|
||
| `Aggressor` | Only retaliates |
|
||
| `Strongest` | Targets highest-stat enemy |
|
||
| `Weakest` | Targets lowest-stat enemy |
|
||
| `Closest` | Targets nearest enemy |
|
||
| `Evil` | Attacks aggressors or evil-karma targets |
|
||
|
||
### Creature Stats Guide
|
||
| Creature Level | Str | Dex | Int | Hits | Damage | Fame |
|
||
|---|---|---|---|---|---|---|
|
||
| Weak | 30-60 | 30-50 | 10-20 | 20-40 | 2-6 | 100-300 |
|
||
| Average | 80-120 | 60-90 | 20-40 | 60-100 | 6-14 | 500-1500 |
|
||
| Strong | 150-250 | 80-120 | 50-100 | 120-200 | 12-22 | 2000-5000 |
|
||
| Elite | 300-500 | 100-150 | 100-200 | 250-500 | 18-30 | 5000-15000 |
|
||
| Boss | 500-1000 | 150-250 | 200-400 | 500-2000 | 25-40 | 15000+ |
|
||
|
||
### Optional Creature Overrides
|
||
```csharp
|
||
public override Poison PoisonImmune => Poison.Regular; // Poison immunity
|
||
public override Poison HitPoison => Poison.Lesser; // Melee poison
|
||
public override double HitPoisonChance => 0.2; // 20% poison chance
|
||
public override bool CanRummageCorpses => true; // Loots corpses
|
||
public override bool BardImmune => true; // Cannot be provoked/peaced
|
||
public override bool Unprovokable => true; // Cannot be provoked
|
||
public override bool CanFly => true; // Can fly
|
||
public override int TreasureMapLevel => 3; // Drops treasure map
|
||
public override double WeaponAbilityChance => 0.4; // Weapon ability chance
|
||
```
|
||
|
||
### Creature Speeds (think vs move clocks)
|
||
|
||
All "speed" values are **delays in seconds** (smaller = faster). A creature runs two clocks:
|
||
|
||
- **Think clock** — `ActiveSpeed`/`PassiveSpeed`/`CurrentSpeed`: seconds per AI decision
|
||
(combat decisions, target acquisition, spell timing).
|
||
- **Move clock** — `ActiveMoveSpeed`/`PassiveMoveSpeed`/`CurrentMoveSpeed`: seconds per
|
||
step. Inherits the matching think value until overridden, so a creature configured with
|
||
only think speeds behaves as one clock. Any value is legal — steps are scheduled
|
||
independently of think ticks, so the two need not divide evenly.
|
||
|
||
Speeds normally come from `Distribution/Data/npc-speeds.json` (via `SpeedClass` or type
|
||
lists); `activeMove`/`passiveMove` are optional per bucket. Prefer data over code:
|
||
|
||
```csharp
|
||
public override SpeedLevel SpeedClass => SpeedLevel.Slow; // bucket in npc-speeds.json
|
||
```
|
||
|
||
Code-level overrides for special cases:
|
||
|
||
```csharp
|
||
SetSpeed(0.5, 2.0); // think clock; ALSO clears move overrides (one-clock legacy semantics)
|
||
SetMoveSpeed(0.45, 0.9); // move clock only — call after SetSpeed if both are wanted
|
||
ClearMoveSpeed(); // back to inheriting the think clock
|
||
```
|
||
|
||
All four are `[props`-tunable per instance (move values: set `0` to re-inherit); per-instance
|
||
move overrides serialize. Being badly hurt slows steps, never decisions (RunUO parity).
|
||
|
||
The client's `Running` bit is derived from the step pace, never passed by callers
|
||
(`BaseAI.ShouldRun`, stamped in `DoMoveImpl`): a step shorter than the client's walk
|
||
interpolation — 400 ms on foot, 200 ms mounted/flying (`Movement.WalkFootDelay` /
|
||
`WalkMountDelay`) — is flagged as a run, or the client falls behind and snaps. An isolated
|
||
step (resuming after at least a walk interval standing) goes out as a walk regardless of
|
||
pace — the client renders each step alone, so a run-flagged single step darts — unless the
|
||
pace beats the run interpolation (a true sprinter), where a walk-rendered first step would
|
||
flood the client's step queue. Movement APIs (`MoveTo`, `WalkMobileRange`,
|
||
`ApproachTarget`, `MoveToPoint`) take no run argument; to make a creature run, make it
|
||
fast. Creatures step at most once per `CurrentMoveSpeed` period, paced from the step just
|
||
taken — a stall never banks catch-up steps, so a resumed chase restarts at full pace.
|
||
|
||
### Target Acquisition: the reaction-time gradient
|
||
|
||
Acquisition is event-driven, not polled. The periodic scan (`AcquireFocusMob`) is gated by
|
||
`ReacquireDelay` (10 s default) and every scan re-arms it in full, success or failure — it
|
||
is target stickiness plus the fallback for what movement cannot signal (reveals, doors,
|
||
summons). Reaction time comes from `BaseCreature.OnMovement`: an enemy moving inside
|
||
`AcquireOnApproachRange` (10 — on-screen; the periodic scan keeps the wider
|
||
`RangePerception`) clamps the next scan to
|
||
at most **`AcquireOnApproachDelay`** — the intelligence gradient. `TimeSpan.Zero`
|
||
(paragons) also prods the AI, so the ranked scan engages within a timer-wheel turn; the
|
||
2 s default reads as "took a beat to notice you"; larger is dumber; a creature that
|
||
overrides the delay above `ReacquireDelay` is effectively oblivious to approach. Repeated
|
||
steps cannot shorten the clamp, so an armed creature scans once per delay period, not once
|
||
per step or think. `ReacquireOnMovement` remains the broader hook (any mover, no enemy
|
||
check, scan next think). The gate self-heals: a deadline further out than `ReacquireDelay`
|
||
is illegal and reads as open, so no wedged or wrapped value can silence acquisition beyond
|
||
one delay period.
|
||
|
||
### OnThink: the excess-call contract
|
||
|
||
`OnThink()` is a scheduler pass, not an action. The AI timer calls it *at least* at the
|
||
think cadence (`CurrentSpeed`), but it can and does fire more often: a player command
|
||
wakes the AI immediately (`AITimer.Prod()`), a speed-up reschedules the pending wake, and
|
||
players run command macros that drive extra thinks deliberately (order spam is spam-safe
|
||
by design — reaction, never action). RunUO had the same property (its timer restarted
|
||
with a random delay on every speed change), so this has never been a fixed-rate callback.
|
||
|
||
**Every `OnThink` override must be excess-call tolerant.** An extra call must never grant
|
||
an extra action:
|
||
|
||
- Gate consequential work on its own deadline field, compared in subtraction form
|
||
(`Core.TickCount - _nextX >= 0` — see `tick-counts.md`), or make it idempotent.
|
||
- Never pace a consequential action with a bare per-call `Utility.RandomDouble()` roll —
|
||
its frequency then scales with think rate, which players can influence. Per-call rolls
|
||
are acceptable only for pure cosmetics (idle animations, flavor sounds).
|
||
- The engine already gates the expensive things: steps (the `NextMove` budget), weapon
|
||
swings, spell casts, detect-hidden, and the base `BaseCreature.OnThink` actions (heal,
|
||
rummage, aura) all carry their own clocks. Follow that pattern.
|
||
|
||
```csharp
|
||
private long _nextSpecial;
|
||
|
||
public override void OnThink()
|
||
{
|
||
base.OnThink();
|
||
|
||
if (Core.TickCount - _nextSpecial >= 0)
|
||
{
|
||
DoSpecial();
|
||
_nextSpecial = Core.TickCount + 5000; // the real rate limit lives here
|
||
}
|
||
}
|
||
```
|
||
|
||
### MonsterAbility: same contract
|
||
|
||
`MonsterAbility.CanTrigger` is sampled once per think for `Think`- and
|
||
`CombatAction`-triggered abilities, so abilities live under the same rule:
|
||
|
||
- **`MinTriggerCooldown`/`MaxTriggerCooldown` is the real rate limit** — the floor holds
|
||
no matter how often thinks fire. Always give a triggered ability a real cooldown.
|
||
- **`ChanceToTrigger` is a per-sample roll**: above the cooldown floor, the expected
|
||
trigger delay shrinks as think rate rises. Treat the chance as flavor jitter, never as
|
||
the rate limiter, and keep cooldowns long relative to the think interval so the jitter
|
||
stays negligible (fire breath — chance 0.5, cooldown 30–45s — varies under 1% between
|
||
natural and spammed think rates).
|
||
- A **zero-cooldown ability records no cooldown at all** and triggers on every sampled
|
||
think that passes its chance — only ever correct for passive alteration hooks, never
|
||
for `Think`/`CombatAction` triggers.
|
||
- An ability that breaks pet orders (fear-style effects) must own its duration explicitly
|
||
(a hold state, or a "refuses orders until" deadline checked in the order handlers) —
|
||
pets react to re-issued commands immediately, so think latency is not a hold.
|
||
|
||
---
|
||
|
||
## New Spell
|
||
|
||
### Targeted Damage Spell (Magery)
|
||
```csharp
|
||
using System;
|
||
using Server.Targeting;
|
||
|
||
namespace Server.Spells.Third;
|
||
|
||
public class FireballSpellCustom : MagerySpell, ITargetingSpell<Mobile>
|
||
{
|
||
private static readonly SpellInfo _info = new(
|
||
"Fireball", // Name
|
||
"Vas Flam", // Mantra
|
||
212, // Cast animation
|
||
9041, // Cast sound
|
||
Reagent.BlackPearl // Reagents (comma-separated)
|
||
);
|
||
|
||
public FireballSpellCustom(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { }
|
||
|
||
public override SpellCircle Circle => SpellCircle.Third;
|
||
public override bool DelayedDamage => true;
|
||
|
||
public void Target(Mobile m)
|
||
{
|
||
if (CheckHSequence(m)) // Harmful spell sequence check
|
||
{
|
||
var source = Caster;
|
||
SpellHelper.Turn(source, m);
|
||
SpellHelper.CheckReflect((int)Circle, ref source, ref m);
|
||
|
||
double damage;
|
||
if (Core.AOS)
|
||
{
|
||
damage = GetNewAosDamage(19, 1, 5, m);
|
||
}
|
||
else
|
||
{
|
||
damage = Utility.Random(10, 7);
|
||
if (CheckResisted(m))
|
||
{
|
||
damage *= 0.75;
|
||
m.SendLocalizedMessage(501783); // You resist
|
||
}
|
||
damage *= GetDamageScalar(m);
|
||
}
|
||
|
||
source.MovingParticles(m, 0x36D4, 7, 0, false, true, 9502, 4019, 0x160);
|
||
source.PlaySound(0x15E);
|
||
|
||
// Damage types must sum to 100
|
||
SpellHelper.Damage(this, m, damage, 0, 100, 0, 0, 0);
|
||
// phys fire cold pois energy
|
||
}
|
||
}
|
||
|
||
public override void OnCast()
|
||
{
|
||
Caster.Target = new SpellTarget<Mobile>(this, TargetFlags.Harmful);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Spell Helper Methods
|
||
```csharp
|
||
SpellHelper.Turn(caster, target); // Face target
|
||
SpellHelper.CheckReflect(circle, ref source, ref target); // Magic reflect
|
||
SpellHelper.Damage(spell, target, damage, phys, fire, cold, poison, energy);
|
||
SpellHelper.AddStatCurse(caster, target, stat);
|
||
SpellHelper.AddStatBonus(caster, target, stat);
|
||
SpellHelper.CanRevealCaster(spell);
|
||
|
||
CheckHSequence(target); // Harmful spell checks (LOS, range, criminal)
|
||
CheckBSequence(target); // Beneficial spell checks
|
||
CheckResisted(target); // Resistance check
|
||
GetNewAosDamage(bonus, dice, sides, target); // AOS damage formula
|
||
GetDamageScalar(target); // Pre-AOS damage multiplier
|
||
```
|
||
|
||
### Spell Circles (Magery)
|
||
| Circle | Mana | Base Delay |
|
||
|---|---|---|
|
||
| First | 4 | 0.25s + circle |
|
||
| Second | 6 | 0.50s + circle |
|
||
| Third | 9 | 0.75s + circle |
|
||
| Fourth | 11 | 1.00s + circle |
|
||
| Fifth | 14 | 1.25s + circle |
|
||
| Sixth | 20 | 1.50s + circle |
|
||
| Seventh | 40 | 1.75s + circle |
|
||
| Eighth | 50 | 2.00s + circle |
|
||
|
||
---
|
||
|
||
## Skill Implementation
|
||
|
||
### Registering a Skill Handler
|
||
```csharp
|
||
namespace Server.SkillHandlers;
|
||
|
||
public static class MySkillHandler
|
||
{
|
||
public static void Initialize()
|
||
{
|
||
SkillInfo.Table[(int)SkillName.Tracking].Callback = OnUse;
|
||
}
|
||
|
||
public static TimeSpan OnUse(Mobile from)
|
||
{
|
||
from.SendMessage("You begin tracking...");
|
||
from.Target = new TrackingTarget();
|
||
return TimeSpan.FromSeconds(10.0); // Cooldown
|
||
}
|
||
}
|
||
```
|
||
|
||
### Skill Check
|
||
```csharp
|
||
// Difficulty-based check (with skill gain chance)
|
||
if (from.CheckSkill(SkillName.Mining, 0.0, 100.0))
|
||
{
|
||
// Success
|
||
}
|
||
|
||
// Direct chance check
|
||
if (from.CheckSkill(SkillName.Hiding, minSkill: 25.0, maxSkill: 75.0))
|
||
{
|
||
// Success
|
||
}
|
||
```
|
||
|
||
### SkillName Enum (58 skills)
|
||
Key skills: `Alchemy`, `Anatomy`, `AnimalLore`, `AnimalTaming`, `Archery`, `ArmsLore`, `Begging`, `Blacksmith`, `Bushido`, `Camping`, `Carpentry`, `Cartography`, `Chivalry`, `Cooking`, `DetectHidden`, `Discordance`, `EvalInt`, `Fencing`, `Fishing`, `Fletching`, `Focus`, `Forensics`, `Healing`, `Herding`, `Hiding`, `Inscribe`, `ItemID`, `Lockpicking`, `Lumberjacking`, `Macing`, `Magery`, `MagicResist`, `Meditation`, `Mining`, `Musicianship`, `Necromancy`, `Ninjitsu`, `Parry`, `Peacemaking`, `Poisoning`, `Provocation`, `RemoveTrap`, `Snooping`, `Spellweaving`, `SpiritSpeak`, `Stealing`, `Stealth`, `Swords`, `Tactics`, `Tailoring`, `TasteID`, `Tinkering`, `Tracking`, `Veterinary`, `Wrestling`
|
||
|
||
---
|
||
|
||
## Loot System
|
||
|
||
### Using Predefined Packs
|
||
```csharp
|
||
public override void GenerateLoot()
|
||
{
|
||
AddLoot(LootPack.Poor); // ~50g equivalent
|
||
AddLoot(LootPack.Meager); // ~100g equivalent
|
||
AddLoot(LootPack.Average); // ~250g equivalent
|
||
AddLoot(LootPack.Rich); // ~500g equivalent
|
||
AddLoot(LootPack.FilthyRich); // ~1000g equivalent
|
||
AddLoot(LootPack.UltraRich); // ~2000g equivalent
|
||
AddLoot(LootPack.SuperBoss); // Boss-level loot
|
||
|
||
// Auxiliary packs
|
||
AddLoot(LootPack.Gems, 2); // 2 random gems
|
||
AddLoot(LootPack.Potions); // Random potion
|
||
AddLoot(LootPack.LowScrolls); // Low circle scroll
|
||
AddLoot(LootPack.MedScrolls); // Med circle scroll
|
||
AddLoot(LootPack.HighScrolls); // High circle scroll
|
||
}
|
||
```
|
||
|
||
Packs auto-select era-appropriate loot (Pre-AOS, AOS, SE variants).
|
||
|
||
### Specific Items
|
||
```csharp
|
||
PackItem(new Arrow(Utility.RandomMinMax(20, 40)));
|
||
PackGold(100, 200);
|
||
PackItem(new Bandage(Utility.RandomMinMax(5, 10)));
|
||
```
|
||
|
||
---
|
||
|
||
## Context Menus
|
||
|
||
```csharp
|
||
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
||
{
|
||
base.GetContextMenuEntries(from, ref list);
|
||
|
||
if (from.Alive && from.InRange(this, 2))
|
||
{
|
||
list.Add(new RepairEntry(this));
|
||
}
|
||
}
|
||
|
||
private class RepairEntry : ContextMenuEntry
|
||
{
|
||
private readonly Item _item;
|
||
|
||
public RepairEntry(Item item) : base(6100) // Cliloc number
|
||
{
|
||
_item = item;
|
||
Enabled = item is { Deleted: false };
|
||
}
|
||
|
||
public override void OnClick(Mobile from, IEntity target)
|
||
{
|
||
if (_item.Deleted || !from.InRange(_item, 2))
|
||
return;
|
||
|
||
from.SendMessage("You repair the item.");
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Entity Lifecycle
|
||
|
||
### Two-Phase Deletion
|
||
```csharp
|
||
// Phase 1: Pre-removal cleanup
|
||
public override void OnDelete()
|
||
{
|
||
_timerToken.Cancel(); // Cancel managed timers
|
||
// Remove from tracking systems
|
||
base.OnDelete();
|
||
}
|
||
|
||
// Phase 2: Post-removal cleanup
|
||
public override void OnAfterDelete()
|
||
{
|
||
_timer?.Stop(); // Stop Timer references
|
||
_timer = null;
|
||
_owner = null; // Null Item/Mobile refs
|
||
base.OnAfterDelete();
|
||
}
|
||
```
|
||
|
||
### OnDoubleClick Validation
|
||
```csharp
|
||
public override void OnDoubleClick(Mobile from)
|
||
{
|
||
if (!IsChildOf(from.Backpack))
|
||
{
|
||
from.SendLocalizedMessage(1042001); // Must be in backpack
|
||
return;
|
||
}
|
||
|
||
if (!from.InRange(GetWorldLocation(), 2))
|
||
{
|
||
from.SendLocalizedMessage(500446); // Too far away
|
||
return;
|
||
}
|
||
|
||
// Item logic here
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## File Organization
|
||
|
||
```
|
||
Projects/UOContent/
|
||
├── Items/
|
||
│ ├── Weapons/Swords/ # Swords
|
||
│ ├── Weapons/Maces/ # Maces
|
||
│ ├── Weapons/Ranged/ # Bows, crossbows
|
||
│ ├── Armor/Plate/ # Plate armor
|
||
│ ├── Armor/Chain/ # Chain armor
|
||
│ ├── Armor/Leather/ # Leather armor
|
||
│ ├── Clothing/ # Wearable clothing
|
||
│ ├── Containers/ # Bags, boxes
|
||
│ ├── Misc/ # General items
|
||
│ ├── Special/ # Unique/quest items
|
||
│ └── Resources/ # Crafting materials
|
||
├── Mobiles/
|
||
│ ├── Animals/Bears/ # Bears (BlackBear, GrizzlyBear)
|
||
│ ├── Animals/Birds/ # Birds
|
||
│ ├── Monsters/AOS/ # AOS-era monsters
|
||
│ ├── Monsters/SE/ # SE-era monsters
|
||
│ ├── Monsters/ML/ # ML-era monsters
|
||
│ ├── Special/ # Champions, bosses
|
||
│ ├── Vendors/ # NPC vendors
|
||
│ └── Townfolk/ # NPCs
|
||
├── Spells/
|
||
│ ├── Base/ # Spell base classes
|
||
│ ├── First/ - Eighth/ # Magery circles
|
||
│ ├── Necromancy/ # Necromancer spells
|
||
│ ├── Chivalry/ # Paladin spells
|
||
│ ├── Bushido/ # Samurai abilities
|
||
│ ├── Ninjitsu/ # Ninja abilities
|
||
│ └── Spellweaving/ # Spellweaving
|
||
├── Skills/ # Skill handlers
|
||
├── Gumps/ # UI dialogs
|
||
│ └── Base/ # Gump base classes
|
||
├── Engines/ # Complex systems
|
||
│ ├── Craft/ # Crafting system
|
||
│ ├── CannedEvil/ # Champion spawns
|
||
│ ├── Factions/ # Faction system
|
||
│ └── Quests/ # Quest system
|
||
└── Misc/ # Miscellaneous
|
||
└── LootPack.cs # Loot tables
|
||
```
|
||
|
||
### Naming Rules
|
||
- File name = class name
|
||
- One primary class per file
|
||
- Group related items in subdirectories
|
||
- Era-specific content goes in era-named subdirectories
|