#W# Merge: Merged with upstream. Conflicts resolved.

This commit is contained in:
WarrentyExpired 2026-10-27 19:56:03 -04:00
commit f6f50470a9
276 changed files with 6742 additions and 8845 deletions

View file

@ -1,9 +1,10 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using ModernUO.Serialization;
using Server.Collections;
using Server.ContextMenus;
using Server.Engines.CannedEvil;
using Server.Engines.ConPVP;
using Server.Engines.MLQuests;
using Server.Engines.Quests.Doom;
@ -1315,6 +1316,8 @@ namespace Server.Mobiles
public virtual int TreasureMapLevel => -1;
public virtual double TreasureMapChance => TreasureMap.LootChance;
public virtual bool IgnoreYoungProtection => false;
public bool NoKillAwards { get; set; }
@ -1503,10 +1506,47 @@ namespace Server.Mobiles
public List<MLQuest> MLQuests =>
(m_MLQuests ??= StaticMLQuester ? MLQuestSystem.FindQuestList(GetType()) : ConstructQuestList()) ?? MLQuestSystem.EmptyList;
/// <summary>
/// The abilities this creature has. Must be a pure, stable query: it is called from lookups, damage
/// calculations and cleanup, so it must not spawn, mutate state, or change with the creature's state.
/// Gate abilities that should only work some of the time with <see cref="CanUseAbility" />.
/// </summary>
public virtual MonsterAbility[] GetMonsterAbilities() => null;
/// <summary>
/// Whether <paramref name="ability" /> may trigger or be looked up right now. Must be a pure read.
/// </summary>
public virtual bool CanUseAbility(MonsterAbility ability) => true;
private MonsterAbilityTrigger _activeTriggers;
// Deadlines in Core.TickCount, keyed by ability. Lives on the creature so it is collected with it.
private Dictionary<MonsterAbility, long> _abilityCooldowns;
public bool IsAbilityOnCooldown(MonsterAbility ability)
{
if (_abilityCooldowns?.TryGetValue(ability, out var nextTrigger) != true)
{
return false;
}
if (nextTrigger - Core.TickCount > 0)
{
return true;
}
_abilityCooldowns.Remove(ability);
return false;
}
public void SetAbilityCooldown(MonsterAbility ability, TimeSpan cooldown)
{
_abilityCooldowns ??= new Dictionary<MonsterAbility, long>();
_abilityCooldowns[ability] = Core.TickCount + (long)cooldown.TotalMilliseconds;
}
public void ClearAbilityCooldowns() => _abilityCooldowns = null;
public virtual MonsterAbility GetAbility(MonsterAbilityType type)
{
var abilities = GetMonsterAbilities();
@ -1519,6 +1559,11 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (!CanUseAbility(ability))
{
continue;
}
if (ability is MonsterAbilityGroup group)
{
ability = group.GetAbilityWithType(type);
@ -1582,7 +1627,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.CanTrigger(this, trigger))
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
{
ability.Trigger(trigger, this, defender);
triggered = true;
@ -1609,7 +1654,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.CanTrigger(this, trigger))
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
{
ability.Move(this, d);
}
@ -1628,7 +1673,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.CanTrigger(this, trigger))
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
{
if ((trigger & MonsterAbilityTrigger.GiveMeleeDamage) != 0)
{
@ -1669,7 +1714,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.CanTrigger(this, trigger))
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
{
if ((trigger & MonsterAbilityTrigger.GiveSpellDamage) != 0)
{
@ -3242,7 +3287,7 @@ namespace Server.Mobiles
{
PackItem(new ParagonChest(Name, treasureLevel));
}
else if ((Map == Map.Felucca || Map == Map.Trammel) && Utility.RandomDouble() < TreasureMap.LootChance)
else if ((Map == Map.Felucca || Map == Map.Trammel) && Utility.RandomDouble() < TreasureMapChance)
{
PackItem(new TreasureMap(treasureLevel, Map));
}
@ -5834,6 +5879,47 @@ namespace Server.Mobiles
}
}
/// <summary>
/// Equips <paramref name="item" /> in place of whatever an inherited outfit put on its layer.
/// AddItem would stack both on one layer, and EquipItem would refuse and orphan the item.
/// </summary>
public void ReplaceEquipment(Item item)
{
FindItemOnLayer(item.Layer)?.Delete();
if (!EquipItem(item))
{
item.Delete();
}
}
/// <summary>
/// Brings <paramref name="minion" /> into the world on this creature's side. A minion of a champion spawn's
/// champion or creatures belongs to that spawn, which deletes it when it clears its creatures.
/// </summary>
/// <returns>False, and the minion is deleted, if this creature is not in the world.</returns>
public bool SpawnMinion(BaseCreature minion, Mobile target = null, Point3D? location = null)
{
var map = Map;
if (Deleted || map == null || map == Map.Internal)
{
minion.Delete();
return false;
}
minion.Team = Team;
ChampionSpawn.FindSpawnOf(this)?.AddMinion(minion);
minion.MoveToWorld(location ?? map.GetRandomNearbyLocation(Location), map);
if (target != null)
{
minion.Combatant = target;
}
return true;
}
public virtual void HealStart(Mobile patient)
{
StopHeal();