## Summary After #2670, `BaseCreature` stores one master. Callers still reached it through three differently gated names plus hand-rolled combinations of the `Controlled`/`Summoned` flags. Each gate is now one named view: | Member | Definition | Meaning | |---|---|---| | `Master` | the reference; its setter does the follower bookkeeping; serialized as before | whoever the creature answers to | | `ControlMaster` | `Controlled ? Master : null` | the owner | | `SummonMaster` | `Summoned ? Master : null` | the summoner | | `GetMaster()` | `ControlMaster ?? SummonMaster` | who answers for the creature | `ControlMaster` and `SummonMaster` stay settable so custom code keeps compiling: both setters assign `Master`. There is no guard, so a write through either one can read back as null until the matching flag is set. An enraged creature is the one shape with a master and neither flag. It keeps its meer through `Master`, while `GetMaster()` stays null, so the meer never answers for it (notoriety, kill credit). Readers move onto the view they mean: - "Owner or summoner" pairs and `ControlMaster ?? SummonMaster` collapse into `GetMaster()`: pack instinct, familiars, golem, house access, mounts, pack horse, Solen friendship, spell target and guild checks, and the `[` pet command. - Checks such as `Controlled && ControlMaster == from` drop the flag the view already applies. Checks that don't reduce keep it: `Controlled && ControlMaster != from`, a possibly-null `from`, and `MageAI.CanDispel`'s `Summoned &&`. **No behavior change, save format unchanged** (the schema generator produces no diff). Edge cases for custom code and shard operators: - `SummonMaster = x` on a plain pet now replaces its owner (#2670 made that a no-op), and `ControlMaster = null` on an uncontrolled summon now clears its caster (it did nothing before). - `SpellHelper.GetGuildFor` no longer falls back to an enraged creature's meer. Meer mages have no guild. ## Docs - `dev-docs/content-patterns.md` § Masters: which view answers which question, how to set a master, and which flag checks are redundant. - `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` § Masters: RunUO's two-field reads mapped onto the views, plus the gotcha for creatures that set `SummonMaster` without `Summoned` (the write compiles, the read now returns null; use `Master`). - The `modernuo-content-patterns` and `migrate-items-mobiles` skills point at both. ## Test plan - [x] `MasterViewsTests`: all four views across wild, pet, controlled summon, uncontrolled summon and enraged shapes; reassigning `Master` moves follower slots; `SetControlMaster(null)` keeps an uncontrolled summon's caster; deleting a summon returns the caster's slots - [x] Serialization tests updated to the new views (round-trip, legacy v22, v23 migration) - [x] UOContent.Tests (1178 passed, 2 skipped) and Server.Tests (907) green
393 lines
12 KiB
Markdown
393 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. Herding and pacing to a
|
|
master (`FollowMoveSpeed`) cap the resolved pace without writing either clock. 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
|
|
9. **One master, read through views** -- `ControlMaster` (owner, only while `Controlled`),
|
|
`SummonMaster` (summoner, only while `Summoned`), `GetMaster()` (either: who answers for the
|
|
creature), `Master` (the stored reference, flags ignored; both setters assign it). Tame with
|
|
`SetControlMaster`, summon with `BaseCreature.Summon`. Don't repeat the view's flag:
|
|
`Controlled && ControlMaster == x` -> `ControlMaster == x`, but `!=` does not reduce -- see
|
|
`dev-docs/content-patterns.md` § Masters
|
|
|
|
## 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
|