ModernUO/dev-docs/claude-skills/modernuo-content-patterns.md
Kamron Batman c411a035e4
docs: document the OnThink excess-call contract and MonsterAbility pacing
OnThink is a scheduler pass, not an action: the AI timer fires it at least at
the think cadence but also on command prods and reschedules, and players
macro commands deliberately. Overrides must gate consequential work on their
own tick-count deadlines (or be idempotent); bare per-call random rolls are
cosmetics-only. MonsterAbility lives 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.
Order-breaking abilities must own their hold duration — think latency is not
a hold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 09:31:34 -07:00

383 lines
11 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 -- see
`dev-docs/content-patterns.md` § Creature Speeds
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