ModernUO/dev-docs/content-patterns.md
Kamron Batman 1e891094fe
fix(ai): FamiliarAI owns familiar movement and combat; herding reaches its tile; ForcedAI read once (#2644)
## Problem

`BaseFamiliar.OnThink` drove its own movement (`WalkMobileRange` toward the master) while the familiar was also a controlled pet running `Obey()`. `Summon → SetControlMaster` issues `Come`; `DoOrderCome` converts it to `Stay` within two tiles and anchors `Home`; from then on `OnThink` walked toward the caster while `DoOrderStay` greedy-stepped back toward the stale post — the backtracking. Combat never approached anything: main only copied `Combatant` while already adjacent to the caster. `CurrentSpeed = 0.01` was a 10 ms think / 50 ms step sprint hack, and `RangeCheck` teleported the familiar to a spot eight tiles *from* the caster.

## Change

A dedicated `FamiliarAI : BaseAI` (registered through `ForcedAI`, like `CloneAI`) owns every familiar decision, for both the controlled (`Obey`) and uncontrolled (`Think`) dispatch:

1. **Lifecycle** — caster gone → drop pack, delete. Caster on another map → stand down and wait for `TeleportPets`.
2. **Herding** (Dark Tides) — stands down, then `CheckHerding()`. Outranks combat.
3. **Assist** — combat-capable familiars (dark wolf, vampire bat, horde minion) engage the caster's target; otherwise anything in a fight with the caster's side — attacked the caster or the familiar, or attacked by the caster (a pet's attack is credited to the caster) — that is still fighting the caster, the familiar, or one of the caster's pets. Leashed to `RangePerception` of the caster; dropped when the caster hides. Shadow wisp and death adder never fight (`AssistsMaster => false`, enforced at the `Combatant` setter so no path can hand them a target).
4. **Follow** — `MoveTo(master, 1)` through the centralized `ApproachTarget` (greedy step / persistent `PathFollower` / stall detection).
5. **Keep-up** — snap to a validated tile beside the caster (on the caster's floor) when outpaced on open ground beyond 10 tiles, or when `ApproachTarget` gave up; never while a detour is working.

**Command immunity** is expressed inside the order machinery rather than around it: `FamiliarAI.IssueOrder` does nothing and rests the order on `Come`, so `TeleportPets` keeps working and no system-issued `Attack` (retaliation on ML's stand-down rule) can strand the familiar. `StandsDownOnCommand => false` so the ML rule never mutes it.

**Visibility** mirrors the caster from the familiar's own state (a step reveals a hidden NPC in `Mobile.OnMove`; the old cache compared the caster's previous state), `RevealingAction` is suppressed while the caster is hidden, and becoming hidden drops Warmode so no swing gives the caster away.

**Speed** is a flat 0.1 (`ReduceSpeedWithDamage => false`).

### Engine-side (all `Projects/UOContent`)

- `ApproachTarget` records which exit it took in `BaseAI.LastApproach` (`ApproachOutcome`: Arrived / Waiting / DirectProgress / Routing / Blocked / GaveUp / InvalidGoal). Callers' booleans are unchanged; keep-up reads this instead of running a second scheduler. `MoveTo`'s arrival return now also clears the move intent, as `ApproachTarget`'s own arrival does.
- `MoveToPoint(goal, range = 1)`; `CheckHerding` passes 0. **Fixes a main regression from #2591:** herding stopped one tile short, never cleared `TargetLocation`, and left the creature pinned to the herding pace — affects the shepherd's crook and the Dark Tides scroll fetch for every herded creature, not just familiars.
- `ChangeAIType` reads `ForcedAI` once. It read it twice, and each `BaseAI` ctor activates its timer for a non-sector-gated creature, so a `ForcedAI` creature with `PlayerRangeSensitive => false` got an orphan AI ticking it.

## Tests

`FamiliarAITests` are timer-wheel driven (the real `AITimer` thinks and moves; `PetPacingTests` style) against live Trammel statics, gated on client map data: follow without backtracking, the five-way assist theory, leash, retaliation, aggressor fallback (caster's own `Combatant` expired; caster's pet in the fight), target dropped when it stops fighting, keep-up on open ground / not while routing / after give-up, hidden mirror across steps, herding priority with a visible fighting caster, stand-down when left behind, no stale move intent. `ApproachOutcomeTests`, `HerdingTests` (fails on main), `ForcedAITests` (fails on main) cover the engine-side pieces.

Against `origin/main` with the familiar tests dropped in: 16/16 fail, including the reported backtracking. On this branch: `UOContent.Tests` 1082 passed / 2 skipped, `Server.Tests` 891/891, solution builds with 0 warnings.
2026-09-14 23:14:30 -07:00

693 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |
**Bespoke policy: `ForcedAI`.** A creature whose behavior is not one of the stock AIs
overrides `protected override BaseAI ForcedAI => new MyAI(this);` and `ChangeAIType` uses that
instance regardless of `AIType` (`CloneAI` for mirror images, `FamiliarAI` for necromancy
familiars, `FactionGuardAI`, `SpellbinderAI`). The property is read exactly once per
`ChangeAIType`; do not put side effects in it. Movement or order decisions belong in the AI
class — `OnThink` is for content extras (see the excess-call contract below). A controlled
creature is dispatched through `Obey()`, an uncontrolled one through `Think()`; an AI that owns
both routes them into one decision (`FamiliarAI.Act`). Overriding `IssueOrder` to return a
fixed order is how "command immunity" is expressed without bypassing the order machinery.
### 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. The properties read the raw override (`0` =
inheriting); `CurrentMoveSpeed` is the resolved pace. 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).
Two conditions cap the resolved step pace without touching either clock, so nothing is
stored and nothing needs undoing when the condition ends:
- **Herding** — a creature with a `TargetLocation` is driven at a fixed `HerdingMoveSpeed`.
- **Pacing to the master** — a pet following its master, or guarding from outside guard
range, is capped at `FollowMoveSpeed` (AOS 0.1, earlier eras 0; RunUO's pet sprint). It is
a cap, not an override: a creature configured faster keeps its own pace, and its
`ActiveMoveSpeed`/`PassiveMoveSpeed` are left untouched. Override the virtual to change
the pace or to enable it in an era that has it off. Decisions are unaffected — a following
pet thinks on its active clock.
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 3045s — 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