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.
386 lines
12 KiB
Markdown
386 lines
12 KiB
Markdown
---
|
|
name: modernuo-content-patterns
|
|
description: >
|
|
Trigger when creating new items, mobiles, creatures, spells, skills, loot tables, or any game content under Projects/UOContent/. This is the hub skill that connects to all other ModernUO skills.
|
|
---
|
|
|
|
# ModernUO Content Patterns (Hub Skill)
|
|
|
|
## When This Activates
|
|
- Creating new items, weapons, armor, clothing, containers
|
|
- Creating new creatures, NPCs, vendors
|
|
- Creating new spells
|
|
- Implementing skill handlers
|
|
- Adding loot tables
|
|
- Adding context menus
|
|
- Any new game content under `Projects/UOContent/`
|
|
|
|
## Key Rules
|
|
|
|
1. **Always ask target era** if the user hasn't specified (see `modernuo-era-expansion.md`)
|
|
2. **All serializable classes must be `partial`** with `[SerializationGenerator]`
|
|
3. **All Item/Mobile constructors need `[Constructible]`**
|
|
4. **Clean up timers and references in `OnDelete()`/`OnAfterDelete()`**
|
|
5. **No LINQ** in game logic -- use loops and `PooledRefList<T>`
|
|
6. **File placement** matters -- follow the directory conventions below
|
|
7. **Creature speeds are delays in seconds, on two clocks** -- think
|
|
(`ActiveSpeed`/`PassiveSpeed`, seconds per AI decision) and move
|
|
(`ActiveMoveSpeed`/`PassiveMoveSpeed`, seconds per step; inherits think until
|
|
overridden). Prefer `npc-speeds.json` buckets (`SpeedClass`); `SetSpeed()` sets think
|
|
AND clears move overrides, `SetMoveSpeed()` sets move only. The client `Running` bit is
|
|
derived from the step pace (`BaseAI.ShouldRun`); movement APIs take no run argument --
|
|
see `dev-docs/content-patterns.md` § Creature Speeds. Reaction time to approaching
|
|
enemies is `AcquireOnApproachDelay` (TimeSpan gradient; `Zero` = paragon snap, 2s
|
|
default, `ReacquireDelay`-only = oblivious) -- see § Target Acquisition
|
|
8. **`OnThink` overrides must be excess-call tolerant** -- it fires more often than the
|
|
think cadence (player commands prod it; speed-ups reschedule it). Gate consequential
|
|
work on a tick-count deadline (subtraction form) or make it idempotent; bare per-call
|
|
random rolls are cosmetics-only. `MonsterAbility` is under the same contract: the
|
|
trigger cooldown is the rate limit, `ChanceToTrigger` is per-sample jitter, and a
|
|
zero-cooldown `Think`/`CombatAction` ability triggers every sampled think -- see
|
|
`dev-docs/content-patterns.md` § OnThink: the excess-call contract
|
|
|
|
## New Item Template
|
|
|
|
```csharp
|
|
using ModernUO.Serialization;
|
|
|
|
namespace Server.Items;
|
|
|
|
[SerializationGenerator(0)]
|
|
public partial class MyItem : Item
|
|
{
|
|
[Constructible]
|
|
public MyItem() : base(0x1234) // itemID from art
|
|
{
|
|
Weight = 1.0;
|
|
// Stackable = true; // if stackable
|
|
// Amount = 1; // if stackable
|
|
}
|
|
|
|
public override string DefaultName => "a my item";
|
|
// OR: public override int LabelNumber => 1234567; // cliloc number
|
|
|
|
public override void OnDoubleClick(Mobile from)
|
|
{
|
|
if (!IsChildOf(from.Backpack))
|
|
{
|
|
from.SendLocalizedMessage(1042001); // Must be in backpack
|
|
return;
|
|
}
|
|
|
|
// Item use logic
|
|
}
|
|
|
|
public override void GetProperties(IPropertyList list)
|
|
{
|
|
base.GetProperties(list);
|
|
// list.Add(1060741, $"{_charges}"); // charges: ~1_val~
|
|
}
|
|
}
|
|
```
|
|
|
|
## New Creature Template
|
|
|
|
```csharp
|
|
using ModernUO.Serialization;
|
|
using Server.Items;
|
|
|
|
namespace Server.Mobiles;
|
|
|
|
[SerializationGenerator(0)]
|
|
public partial class MyCreature : BaseCreature
|
|
{
|
|
[Constructible]
|
|
public MyCreature() : base(AIType.AI_Melee, FightMode.Closest)
|
|
{
|
|
Body = 0; // Body graphic ID
|
|
BaseSoundID = 0; // Base sound ID
|
|
|
|
SetStr(100, 150); // Strength min/max
|
|
SetDex(80, 100); // Dexterity min/max
|
|
SetInt(30, 50); // Intelligence min/max
|
|
|
|
SetHits(80, 120);
|
|
SetMana(0);
|
|
|
|
SetDamage(8, 14);
|
|
|
|
SetDamageType(ResistanceType.Physical, 100);
|
|
|
|
SetResistance(ResistanceType.Physical, 30, 40);
|
|
SetResistance(ResistanceType.Fire, 10, 20);
|
|
SetResistance(ResistanceType.Cold, 10, 20);
|
|
SetResistance(ResistanceType.Poison, 15, 25);
|
|
SetResistance(ResistanceType.Energy, 10, 20);
|
|
|
|
SetSkill(SkillName.MagicResist, 30.0, 50.0);
|
|
SetSkill(SkillName.Tactics, 50.0, 70.0);
|
|
SetSkill(SkillName.Wrestling, 50.0, 70.0);
|
|
|
|
Fame = 1000;
|
|
Karma = -1000; // Negative = evil, positive = good, 0 = neutral
|
|
|
|
VirtualArmor = 30;
|
|
}
|
|
|
|
public override string CorpseName => "a creature corpse";
|
|
public override string DefaultName => "a creature";
|
|
|
|
// Optional overrides:
|
|
// public override int Meat => 1;
|
|
// public override int Hides => 8;
|
|
// public override HideType HideType => HideType.Regular;
|
|
// public override FoodType FavoriteFood => FoodType.Meat;
|
|
// public override PackInstinct PackInstinct => PackInstinct.Canine;
|
|
// public override bool CanRummageCorpses => true;
|
|
// public override Poison PoisonImmune => Poison.Lesser;
|
|
// public override Poison HitPoison => Poison.Regular;
|
|
|
|
public override void GenerateLoot()
|
|
{
|
|
AddLoot(LootPack.Average);
|
|
AddLoot(LootPack.Gems, 1);
|
|
// PackItem(new SpecificItem());
|
|
// PackGold(50, 100);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Tameable Creature Additions
|
|
```csharp
|
|
// In constructor:
|
|
Tamable = true;
|
|
ControlSlots = 1; // 1-5, how many pet slots it uses
|
|
MinTameSkill = 35.1; // Required Animal Taming skill
|
|
```
|
|
|
|
### AI Types
|
|
| AIType | Behavior |
|
|
|---|---|
|
|
| `AI_Melee` | Charges into melee combat |
|
|
| `AI_Mage` | Casts spells, keeps distance |
|
|
| `AI_Archer` | Uses ranged attacks |
|
|
| `AI_Animal` | Passive, flees or fights back |
|
|
| `AI_Predator` | Hunts other creatures |
|
|
| `AI_Healer` | Heals allies |
|
|
| `AI_Vendor` | NPC vendor behavior |
|
|
| `AI_Berserk` | Aggressive, attacks everything |
|
|
| `AI_Thief` | Steals from players |
|
|
|
|
### Fight Modes
|
|
| FightMode | Target Selection |
|
|
|---|---|
|
|
| `None` | Never attacks |
|
|
| `Aggressor` | Only attacks those who attack first |
|
|
| `Strongest` | Targets highest stats |
|
|
| `Weakest` | Targets lowest stats |
|
|
| `Closest` | Targets nearest entity |
|
|
| `Evil` | Attacks aggressors or negative-karma entities |
|
|
|
|
## New Spell Template
|
|
|
|
```csharp
|
|
using System;
|
|
using Server.Targeting;
|
|
|
|
namespace Server.Spells.First;
|
|
|
|
public class MySpell : MagerySpell, ITargetingSpell<Mobile>
|
|
{
|
|
private static readonly SpellInfo _info = new(
|
|
"Spell Name", // Display name
|
|
"In Vas Ort", // Power words (mantra)
|
|
212, // Cast animation action
|
|
9041, // Cast sound
|
|
Reagent.Bloodmoss, // Required reagents
|
|
Reagent.MandrakeRoot
|
|
);
|
|
|
|
public MySpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
|
|
{
|
|
}
|
|
|
|
public override SpellCircle Circle => SpellCircle.First;
|
|
|
|
public void Target(Mobile m)
|
|
{
|
|
if (CheckHSequence(m)) // Harmful spell check
|
|
{
|
|
SpellHelper.Turn(Caster, m);
|
|
|
|
double damage = GetNewAosDamage(10, 1, 4, m);
|
|
|
|
SpellHelper.Damage(this, m, damage, 0, 100, 0, 0, 0);
|
|
// Damage types: phys, fire, cold, poison, energy (must sum to 100)
|
|
}
|
|
}
|
|
|
|
public override void OnCast()
|
|
{
|
|
Caster.Target = new SpellTarget<Mobile>(this, TargetFlags.Harmful);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Spell Circles (Magery)
|
|
| Circle | Mana Cost | Min Skill |
|
|
|---|---|---|
|
|
| First | 4 | -50.0 (Pre-ML) / -46.0 (ML+) |
|
|
| Second | 6 | -30.0 / -32.0 |
|
|
| Third | 9 | 0.0 / -18.0 |
|
|
| Fourth | 11 | 10.0 / -4.0 |
|
|
| Fifth | 14 | 20.0 / 10.0 |
|
|
| Sixth | 20 | 30.0 / 24.0 |
|
|
| Seventh | 40 | 40.0 / 38.0 |
|
|
| Eighth | 50 | 50.0 / 52.0 |
|
|
|
|
## Skill Implementation
|
|
|
|
```csharp
|
|
using Server.Targeting;
|
|
|
|
namespace Server.Skills;
|
|
|
|
public static class MySkillHandler
|
|
{
|
|
public static void Initialize()
|
|
{
|
|
// Register handler delegate
|
|
SkillInfo.Table[(int)SkillName.Alchemy].Callback = OnUse;
|
|
}
|
|
|
|
public static TimeSpan OnUse(Mobile from)
|
|
{
|
|
from.SendMessage("You begin working...");
|
|
from.Target = new InternalTarget();
|
|
return TimeSpan.FromSeconds(1.0); // Delay before next use
|
|
}
|
|
|
|
private class InternalTarget : Target
|
|
{
|
|
public InternalTarget() : base(2, false, TargetFlags.None)
|
|
{
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (targeted is Item item)
|
|
{
|
|
// from.CheckSkill(SkillName.Alchemy, minSkill, maxSkill)
|
|
if (from.CheckSkill(SkillName.Alchemy, 0.0, 100.0))
|
|
{
|
|
from.SendMessage("Success!");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("You fail.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Loot Packs
|
|
|
|
Use predefined packs -- they auto-select era-appropriate loot:
|
|
```csharp
|
|
public override void GenerateLoot()
|
|
{
|
|
AddLoot(LootPack.Poor); // ~50 gold equivalent
|
|
AddLoot(LootPack.Meager); // ~100 gold equivalent
|
|
AddLoot(LootPack.Average); // ~250 gold equivalent
|
|
AddLoot(LootPack.Rich); // ~500 gold equivalent
|
|
AddLoot(LootPack.FilthyRich); // ~1000 gold equivalent
|
|
AddLoot(LootPack.UltraRich); // ~2000 gold equivalent
|
|
AddLoot(LootPack.SuperBoss); // Boss-level loot
|
|
|
|
AddLoot(LootPack.Gems, 2); // 2 random gems
|
|
AddLoot(LootPack.Potions); // Random potion
|
|
|
|
// Specific items
|
|
PackItem(new Arrow(Utility.RandomMinMax(20, 40)));
|
|
PackGold(100, 200);
|
|
}
|
|
```
|
|
|
|
## 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 MyContextMenuEntry(this));
|
|
}
|
|
}
|
|
|
|
private class MyContextMenuEntry : ContextMenuEntry
|
|
{
|
|
private readonly Item _item;
|
|
|
|
public MyContextMenuEntry(Item item) : base(6100) // Cliloc number
|
|
{
|
|
_item = item;
|
|
}
|
|
|
|
public override void OnClick(Mobile from, IEntity target)
|
|
{
|
|
// Handle click
|
|
}
|
|
}
|
|
```
|
|
|
|
## Two-Phase Deletion
|
|
|
|
```csharp
|
|
public override void OnDelete()
|
|
{
|
|
_timerToken.Cancel(); // Cancel timers FIRST
|
|
base.OnDelete();
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
_timer?.Stop(); // Stop Timer references
|
|
_timer = null;
|
|
_owner = null; // Clear Mobile/Item references
|
|
base.OnAfterDelete();
|
|
}
|
|
```
|
|
|
|
## File Placement
|
|
|
|
| Content Type | Directory |
|
|
|---|---|
|
|
| Items | `Projects/UOContent/Items/{Category}/` |
|
|
| Weapons | `Projects/UOContent/Items/Weapons/{Type}/` |
|
|
| Armor | `Projects/UOContent/Items/Armor/{Type}/` |
|
|
| Creatures | `Projects/UOContent/Mobiles/{Type}/` |
|
|
| Animals | `Projects/UOContent/Mobiles/Animals/{Species}/` |
|
|
| Monsters | `Projects/UOContent/Mobiles/Monsters/{Era}/` |
|
|
| Spells | `Projects/UOContent/Spells/{School}/` |
|
|
| Skills | `Projects/UOContent/Skills/` |
|
|
| Engines | `Projects/UOContent/Engines/{SystemName}/` |
|
|
| Gumps | `Projects/UOContent/Gumps/` |
|
|
|
|
## Real Examples
|
|
- Simple creature: `Projects/UOContent/Mobiles/Animals/Bears/BlackBear.cs`
|
|
- Boss creature: `Projects/UOContent/Mobiles/Special/Barracoon.cs`
|
|
- SE creature: `Projects/UOContent/Mobiles/Monsters/SE/RaiJu.cs`
|
|
- Spell: `Projects/UOContent/Spells/First/MagicArrow.cs`
|
|
- Skill check: `Projects/UOContent/Skills/SkillCheck.cs`
|
|
- Loot packs: `Projects/UOContent/Misc/LootPack.cs`
|
|
|
|
## See Also
|
|
- `dev-docs/claude-skills/modernuo-serialization.md` - Serialization details
|
|
- `dev-docs/claude-skills/modernuo-era-expansion.md` - Era-conditional code
|
|
- `dev-docs/claude-skills/modernuo-timers.md` - Timer patterns
|
|
- `dev-docs/claude-skills/modernuo-property-lists.md` - Item tooltips
|
|
- `dev-docs/claude-skills/modernuo-gump-system.md` - UI dialogs
|
|
- `dev-docs/claude-skills/modernuo-commands-targeting.md` - Commands and targeting
|
|
- `dev-docs/claude-skills/modernuo-events.md` - Event system
|
|
- `dev-docs/content-patterns.md` - Full content documentation
|