Closes #2593. Closes #2595. Two related pet-AI fixes: the post-#2591 pacing/wake regression (#2593), and the guard order silently converting to Attack during combat (#2595). Root-cause analyses are in the issues. ## #2593 — pets follow slowly; stale AITimer wakes **Why pets slowed:** - The per-step budget grew from **half a think interval** (`CurrentSpeed * 500`) to the full RunUO-parity move table (`CurrentMoveSpeed * 1000`). Medium-bucket pets (Horse, Dog, most tamables): passiveMove **1.05s/step**. - Pet order speed depended on stale `Warmode`: `HandleGuardOrder` set it once, but `OnCombatantChange` clears it whenever the combatant drops, so obedience ran active or passive **by combat history** — usually passive. Net: Guard/Come at ~1.05s/step (~2.1x slower than pre-#2591), vs a player running at 0.1–0.2s/step. - The AITimer never rescheduled its pending wheel entry: the wheel reads `Interval` only after the next fire, so a speed-up or a fresh order (`Activate()` no-ops while running) waited out the stale wake — up to a full passive think, stacked on the residual move budget on Guard → Follow. **What changed:** - **Order handlers own obedience speed** (RunUO `OnCurrentOrderChanged`/`DoOrder*` parity, re-derived continuously): issuing a movement order (Come/Follow/Guard/Attack) sets the **active** think clock, resting orders (Stay/None/Transfer) set passive, and the guard/follow peaceful branches write **RunUO's AOS `CurrentSpeed = 0.1` sprint** — RunUO's guard else-branch had the identical write as follow. The bespoke 0.1 fuses to both clocks through #2591's existing classification, so `CurrentMoveSpeed` stays **pure herding + classification** with no obedience special case, and `DoMoveImpl`'s per-step flip skips obeying pets (their handler owns the pace) and loses its old follow-only 0.1 write. Combat still re-derives organically via warmode/combatant. - **`AITimer`**: tracks the pending wake and reschedules (`Stop`, `Delay` = remaining, `Start`) when a speed-up or fresh order moves the earliest deadline up; changes inside a tick still flow through `ScheduleNext`. New `Prod()` wakes the AI immediately on player commands — including from a stopped timer, so stable claims no longer wait out the random construction stagger. Sector/spawn wakes keep the stagger. Spam-safe: a prodded think grants reaction, never action — steps/swings/casts/abilities are gated by their own budgets and timers. The residual move budget is deliberately **not** cleared on order change — that would let order-spam macros grant free steps. Deadline changes reschedule the timer; rate changes take effect at the next deadline computation. ## #2595 — Guard order converts to Attack during combat **Why:** `FindCombatant()` set `ControlOrder = OrderType.Attack` when engaging, so a guarding pet left the Guard order for the whole fight: OPL tags wiped (pet `1080078` + master `501129`), no retargeting (`DoOrderAttack` locks its target), `TeleportPets` left the pet behind on recall/gate, and every engage→kill→resume cycle replayed the guard flourish. **What changed:** - **`FindGuardTarget()`** (was `FindCombatant`): a pure selector — prefers the aggressor **closest to the master** (RunUO guard parity, dynamic retargeting to protect the owner), keeps the current combatant unless a strictly closer one exists, and never mutates order state. `DoOrderGuard` engages through it while **staying in Guard** the whole fight. - **Persistent-order semantics** (the ModernUO improvement over RunUO): an explicit `all attack` completes → `ResumePersistentOrder()` returns to Guard → the guard scan engages remaining threats in-order. The Attack-chaining fallback (`FightMode.Closest/Aggressor`) now applies only to non-guard persistent orders. Resuming Guard no longer replays the sound/"is now guarding you" message. - **Peaceful guard stands down deterministically** (`Warmode`/`Combatant`/`FocusMob` cleared) and returns to the master at the RunUO sprint (see above); at the master's side it stays organically active. - **`WalkMobileRange` honors the caller's run flag** (the internal hardcoded `dist > 5` gate silently overrode it). Run is animation-only server-side; the only callers passing anything but `false` — follow, guard, clone — gate on their own thresholds. ## Resulting behavior (Medium-bucket pet) | Scenario | Broken | This PR | |---|---|---| | Guard trailing master (AOS) | ~1.05s/step, think-grid quantized | 0.1s/step sprint (RunUO parity), smooth move wakes | | Guard during combat | order flips to Attack; tags lost; no retarget; left behind on recall | stays Guard; retargets to master's closest aggressor; teleports with master | | `all attack` while guarding | resume spams guard flourish per kill; chains into Attack | resumes Guard silently; guard scan takes over | | Come / friend-follow | 1.05s/step | activeMove 0.45s/step (≈ pre-#2591 feel) | | Guard → Follow reaction | up to ~1.5s dead time | think within one wheel turn | | Follow master (AOS sprint) | 0.1s/step | 0.1s/step (unchanged) | | Wild creature chase | RunUO-parity move table | unchanged | Also documents two contracts this work leaned on: the `ControlOrder` setter deliberately fires on every assignment (a reissued order is a command — retarget/break-off/re-anchor), and `OnThink`/`MonsterAbility` must be excess-call tolerant (`dev-docs/content-patterns.md` § OnThink: the excess-call contract). ## Testing - Full suite passes (1570: 837 Server + 733 UOContent). - `PetPacingTests`: order-issue think-clock parity, follow-master sprint via Obey, guard organically active at the master's side, combat-chase and herding boundaries, plus two deterministic timer-wheel tests (8ms-lockstep slicing) proving a fresh order and a mid-wait speed-up wake the AI promptly. - `GuardOrderTests`: engage keeps the Guard order; retargets to the aggressor closest to the master; explicit attack resumes Guard without chaining into Attack; peaceful guard stands down. Setup self-validates LOS/terrain. - `GuardFollowTests`: guard-following registers a move intent, steps toward the master, sprints at 0.1 under AOS (per-step flip must not undo it), and runs active pre-AOS. - All behavioral tests were written first and failed for the documented reasons.
641 lines
20 KiB
Markdown
641 lines
20 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).
|
||
|
||
### 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
|