ModernUO/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs
Kamron Batman 843a29e7dc
refactor: master views over BaseCreature's one master reference (#2674)
## 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
2026-09-24 22:45:24 -07:00

110 lines
3.7 KiB
C#

using System;
using Server.Mobiles;
using Server.Spells.Ninjitsu;
namespace Server.Items;
/// <summary>
/// Perfect for the foot-soldier, the Dismount special attack can unseat a mounted opponent.
/// The fighter using this ability must be on his own two feet and not in the saddle of a steed
/// (with one exception: players may use a lance to dismount other players while mounted).
/// If it works, the target will be knocked off his own mount and will take some extra damage from the fall!
/// </summary>
public class Dismount : WeaponAbility
{
public static readonly TimeSpan RemountDelay = TimeSpan.FromSeconds(10.0);
public override int BaseMana => 20;
public override void OnHit(Mobile attacker, Mobile defender, int damage, WorldLocation worldLocation)
{
if (!Validate(attacker) || !CheckMana(attacker, true))
{
return;
}
ClearCurrentAbility(attacker);
if (defender is ChaosDragoon or ChaosDragoonElite)
{
return;
}
if (attacker.Mounted || attacker.Flying)
{
if (attacker.Weapon is not Lance || !defender.Mounted && !defender.Flying && defender.Weapon is not Lance)
{
attacker.SendLocalizedMessage(1061283); // You cannot perform that attack while mounted!
return;
}
}
var mount = defender.Mount;
if (mount == null && !AnimalForm.UnderTransformation(defender))
{
attacker.SendLocalizedMessage(1060848); // This attack only works on mounted targets
return;
}
if (Core.ML && attacker is LesserHiryu && Utility.RandomDouble() < 0.8)
{
return; // Lesser Hiryu have an 80% chance of missing this attack
}
DoDismount(attacker, defender, TimeSpan.FromSeconds(10));
if (!attacker.Mounted)
{
AOS.Damage(defender, attacker, Utility.RandomMinMax(15, 25), 100, 0, 0, 0, 0);
}
}
public static void DoDismount(Mobile attacker, Mobile defender, TimeSpan delay, BlockMountType type = BlockMountType.Dazed)
{
attacker.SendLocalizedMessage(1060082); // The force of your attack has dislodged them from their mount!
if (attacker.Mounted)
{
defender.SendLocalizedMessage(1062315); // You fall off your mount!
}
else
{
defender.SendLocalizedMessage(1060083); // You fall off of your mount and take damage!
}
defender.PlaySound(0x140);
defender.FixedParticles(0x3728, 10, 15, 9955, EffectLayer.Waist);
if (defender is PlayerMobile mobile)
{
if (AnimalForm.UnderTransformation(mobile))
{
mobile.SendLocalizedMessage(1114066, attacker.Name); // ~1_NAME~ knocked you out of animal form!
}
else if (defender.Flying)
{
defender.SendLocalizedMessage(1113590, attacker.Name); // You have been grounded by ~1_NAME~!
}
else if (mobile.Mounted)
{
mobile.SendLocalizedMessage(1040023); // You have been knocked off of your mount!
}
mobile.SetMountBlock(type, delay, true);
}
else
{
defender.Mount.Rider = null;
}
if (attacker is PlayerMobile playerMobile)
{
playerMobile.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, true);
}
else if (Core.ML && attacker is BaseCreature { ControlMaster: PlayerMobile pm })
{
pm.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, false);
}
}
}