ModernUO/Projects/UOContent/Mobiles/Special/LordOaks.cs
Kamron Batman 69ff5f3940
fix: champion minions belong to their spawn (#2717)
Stacked on #2716. Retarget to `main` after it merges.

## Problem
Mobiles a champion summons mid-fight aren't tracked by anything:

| Summoner | Minions |
|---|---|
| LordOaks | Silvani queen |
| `SummonPixiesCounter` (LordOaks, Silvani, Twaulo) | 3-6 pixies per trigger |
| Barracoon | 3-6 ratmen per trigger |

`ChampionSpawn.DeleteCreatures` (called on expiry and when the spawn is deleted) clears only `Creatures` and the champion. These minions survive every reset and stay in the save. I surveyed every content creature that creates other mobiles; the others already clean up after themselves: Meraktus deletes its followers, Harrower kills its tentacles, PlagueSpawn and the Enraged animals delete themselves when their owner is gone, and BogThing eats its boglings.

## Changes
- **`BaseCreature.SpawnMinion(minion, target, location)`:** sets the team, places the minion and sets its combatant. Ownership: if the summoner is a champion spawn's champion, one of its creatures, or one of its minions (found through the spawn region), the minion joins that spawn. It refuses, and deletes the minion, if the summoner isn't on a real map. `SummonPixiesCounter` read `source.Map` even though `MonsterAbility.Trigger` notes it can fire after the creature has left the world.
- **`ChampionSpawn.Minions`:** a new serialized list (v12, `MigrateFrom(V11Content)`) kept apart from `Creatures`:
  - killing a minion never counts toward a level, so a leftover summoner can't feed a restarted spawn;
  - `DeleteCreatures` deletes minions along with the creatures, which matches when the spawn is deactivated (`ExpireCreatures`, 10 minutes after `Stop`) or deleted.
- **Callers:** LordOaks, `SummonPixiesCounter` and Barracoon spawn through `SpawnMinion`. A summoner that isn't part of a spawn (a GM `[add]`) still makes standalone minions, as today.

## Testing
- `ChampionMinionTests`:
  - the champion's queen is listed in `Minions` and deleted by `DeleteCreatures`;
  - a minion's minion also belongs to the spawn;
  - a Lord Oaks in the region that isn't the spawn's champion keeps a standalone queen.
- UOContent.Tests: 1237 passed, 2 skipped. The Application build is clean.
2026-10-18 17:48:31 -07:00

168 lines
4.6 KiB
C#

using System;
using ModernUO.Serialization;
using Server.Engines.CannedEvil;
using Server.Items;
namespace Server.Mobiles;
[SerializationGenerator(0, false)]
public partial class LordOaks : BaseChampion
{
[SerializableField(0)]
private Silvani _queen;
[SerializableField(1)]
private bool _spawnedQueen;
[Constructible]
public LordOaks() : base(AIType.AI_Mage, FightMode.Evil)
{
Body = 175;
SetStr(403, 850);
SetDex(101, 150);
SetInt(503, 800);
SetHits(3000);
SetStam(202, 400);
SetDamage(21, 33);
SetDamageType(ResistanceType.Physical, 75);
SetDamageType(ResistanceType.Fire, 25);
SetResistance(ResistanceType.Physical, 85, 90);
SetResistance(ResistanceType.Fire, 60, 70);
SetResistance(ResistanceType.Cold, 60, 70);
SetResistance(ResistanceType.Poison, 80, 90);
SetResistance(ResistanceType.Energy, 80, 90);
SetSkill(SkillName.Anatomy, 75.1, 100.0);
SetSkill(SkillName.EvalInt, 120.1, 130.0);
SetSkill(SkillName.Magery, 120.0);
SetSkill(SkillName.Meditation, 120.1, 130.0);
SetSkill(SkillName.MagicResist, 100.5, 150.0);
SetSkill(SkillName.Tactics, 100.0);
SetSkill(SkillName.Wrestling, 100.0);
Fame = 22500;
Karma = 22500;
VirtualArmor = 100;
}
public override ChampionSkullType SkullType => ChampionSkullType.Enlightenment;
public override Type[] UniqueList => new[] { typeof(OrcChieftainHelm) };
public override Type[] SharedList => new[]
{
typeof(RoyalGuardSurvivalKnife),
typeof(DjinnisRing),
typeof(LieutenantOfTheBritannianRoyalGuard),
typeof(SamaritanRobe),
typeof(DetectiveBoots),
typeof(TheMostKnowledgePerson)
};
public override Type[] DecorativeList => new[]
{
typeof(WaterTile),
typeof(WindSpirit),
typeof(Pier),
typeof(DirtPatch)
};
public override MonsterStatuetteType[] StatueTypes => Array.Empty<MonsterStatuetteType>();
public override string DefaultName => "Lord Oaks";
public override bool AutoDispel => true;
public override bool CanFly => true;
public override bool BardImmune => !Core.SE;
public override bool Unprovokable => Core.SE;
public override bool Uncalmable => Core.SE;
public override OppositionGroup OppositionGroup => OppositionGroup.FeyAndUndead;
public override Poison PoisonImmune => Poison.Deadly;
private static MonsterAbility[] _abilities = { MonsterAbilities.SummonPixiesCounter };
public override MonsterAbility[] GetMonsterAbilities() => _abilities;
public override bool CanUseAbility(MonsterAbility ability) => IsQueenAlive;
private bool IsQueenAlive => _queen?.Deleted == false;
public override void GenerateLoot()
{
AddLoot(LootPack.UltraRich, 5);
}
public override int GetAngerSound() => 0x2F8;
public override int GetIdleSound() => 0x2F8;
public override int GetAttackSound() => Utility.Random(0x2F5, 2);
public override int GetHurtSound() => 0x2F9;
public override int GetDeathSound() => 0x2F7;
// Aggression lands before the damage it causes, so the queen is up before the first hit is scaled.
public override void AggressiveAction(Mobile aggressor, bool criminal)
{
base.AggressiveAction(aggressor, criminal);
SpawnQueen();
}
public override void OnCombatantChange()
{
base.OnCombatantChange();
if (Combatant != null)
{
SpawnQueen();
}
}
private void SpawnQueen()
{
if (_spawnedQueen || Deleted || Map == null || Map == Map.Internal)
{
return;
}
Say(1042153); // Come forth my queen!
var queen = new Silvani();
SpawnMinion(queen, location: Location);
Queen = queen;
SpawnedQueen = true;
}
public override void AlterDamageScalarFrom(Mobile caster, ref double scalar)
{
if (IsQueenAlive)
{
scalar *= 0.1;
}
}
public override void OnGaveMeleeAttack(Mobile defender, int damage)
{
base.OnGaveMeleeAttack(defender, damage);
defender.Damage(Utility.Random(20, 10), this);
defender.Stam -= Utility.Random(20, 10);
defender.Mana -= Utility.Random(20, 10);
}
public override void OnGotMeleeAttack(Mobile attacker, int damage)
{
base.OnGotMeleeAttack(attacker, damage);
attacker.Damage(Utility.Random(20, 10), this);
attacker.Stam -= Utility.Random(20, 10);
attacker.Mana -= Utility.Random(20, 10);
}
}