Splits creature speed into two clocks so movement pace can be tuned without touching reaction time: - **Think clock** — `ActiveSpeed`/`PassiveSpeed`/`CurrentSpeed`: seconds per AI decision. Unchanged in meaning, storage, and cadence. - **Move clock** — `ActiveMoveSpeed`/`PassiveMoveSpeed` (+ resolved `CurrentMoveSpeed`): seconds per step. `0` = inherit the matching think value. ### How - Move speeds come from optional `activeMove`/`passiveMove` in `npc-speeds.json`, are `[props`-tunable per instance (set `0` to re-inherit), and serialize (BaseCreature v22). - `SetSpeed()` keeps its legacy one-clock semantics — sets the think clock **and clears move overrides** — so existing callers cannot half-configure a creature. `SetMoveSpeed()`/`ClearMoveSpeed()` configure movement explicitly; `ScaleMoveSpeed()` scales overrides for buffs. - `CurrentMoveSpeed` is derived by classifying `CurrentSpeed`: a verbatim active/passive think value maps to the matching move value; a bespoke pace written directly (mount boosts, follow sprint) stays fused to both clocks. External `CurrentSpeed` writers need no changes. - `AITimer` schedules the earlier of the two deadlines. Decisions run at the think cadence exactly as before; while a pursuit/investigation is live, the timer also wakes when the movement budget elapses and advances one step with no decisions. Steps no longer snap to the think grid, so any step delay paces smoothly on the 8ms wheel. A blocked creature schedules no move wakes. - The movement budget is RunUO's `m_NextMove` accumulate-and-clamp at a full step, so long-run pacing averages `CurrentMoveSpeed` exactly. ### Behavior changes - **`npc-speeds.json` buckets get RunUO `TransformMoveDelay`-parity move values**: creatures step at RunUO pace while thinking/reacting at current speed. The situational +0.1/+0.2 offsets are deliberately omitted. - **Existing saves migrate on load**: a pre-v22 creature whose think speeds still match its npc-speeds entry (never hand-tuned) adopts the table's move values — worlds and pets pick up the new pacing without a respawn. Tuned creatures keep movement inheriting their think clock. - **Paragons scale movement by `SpeedBuff` (1.2x)**: RunUO had no deliberate policy here — dividing by 1.2 knocked most speeds off `TransformMoveDelay`'s exact-equality table (raw pass-through, 2x+ faster), while 0.3/0.6 creatures landed back on it for ~1.33x. This applies the uniform 1.2x the buff always claimed. UnConvert snaps speeds back to exact table values within 1e-4 — /1.2 then ×1.2 drifts 0.45 and 0.9 by an ulp, which would read as hand-tuned (and defeat a future skip-table-conformant-values serialization pass); tuned speeds keep. - **Herding paces the movement clock**: the old `CurrentSpeed` getter hack is gone. A herded creature walks at a fixed 0.3s/step — RunUO's forced pace, without its `TransformMoveDelay` inflation to 0.6 — so herding is never penalized by a slow creature. Thinking is untouched, and `CheckHerding` walks through `MoveToPoint`, so herded creatures path around obstacles. - **Badly-hurt slowdown now inflates the step delay only** (RunUO parity), computed from the base each step. Previously it wrote `CurrentSpeed = CurrentSpeed + 0.05..0.15` back on every successful step — compounding unboundedly while hurt and slowing decisions too. - Removes the vestigial `MoveSpeedMod` (never read, written, or serialized). - With no bucket or per-instance move values, both clocks carry identical values and creatures pace as before. ### Testing - Full suite passes (1557, including 12 new `MoveSpeedTests`: resolution classes, `SetSpeed` clearing, `0`-re-inherit, v22 round-trip with exact-consumption check, save migration adopt/skip, buff scale/snap, herding). - In-game verified via local diagnostics build (per-step budget tracing): steady 700ms step cadence on a 0.3s think grid with one-step catch-up after idle, think grid unperturbed by move wakes.
376 lines
11 KiB
Markdown
376 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
|
|
|
|
## 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
|