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.
1593 lines
41 KiB
C#
Executable file
1593 lines
41 KiB
C#
Executable file
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: ChampionSpawn.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Net;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using ModernUO.Serialization;
|
|
using Server.Collections;
|
|
using Server.Engines.Virtues;
|
|
using Server.Gumps;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
using Server.Regions;
|
|
using Server.Logging;
|
|
|
|
namespace Server.Engines.CannedEvil;
|
|
|
|
[SerializationGenerator(12, false)]
|
|
public partial class ChampionSpawn : Item
|
|
{
|
|
private void MigrateFrom(V10Content content)
|
|
{
|
|
_level = content.Level;
|
|
_activatedByProximity = content.ActivatedByProximity;
|
|
_nextProximityTime = content.NextProximityTime;
|
|
_maxLevel = content.MaxLevel;
|
|
_activatedByValor = content.ActivatedByValor;
|
|
_damageEntries = content.DamageEntries;
|
|
_confinedRoaming = content.ConfinedRoaming;
|
|
_idol = content.Idol;
|
|
_hasBeenAdvanced = content.HasBeenAdvanced;
|
|
_spawnArea = content.SpawnArea;
|
|
_randomizeType = content.RandomizeType;
|
|
_kills = content.Kills;
|
|
_active = content.Active;
|
|
_type = content.Type;
|
|
_creatures = content.Creatures;
|
|
_redSkulls = content.RedSkulls;
|
|
_whiteSkulls = content.WhiteSkulls;
|
|
_platform = content.Platform;
|
|
_altar = content.Altar;
|
|
_expireDelay = content.ExpireDelay;
|
|
_expireTime = content.ExpireTime;
|
|
_champion = content.Champion;
|
|
_restartDelay = content.RestartDelay;
|
|
_restartTime = content.RestartTime;
|
|
}
|
|
|
|
private void MigrateFrom(V11Content content)
|
|
{
|
|
_level = content.Level;
|
|
_activatedByProximity = content.ActivatedByProximity;
|
|
_nextProximityTime = content.NextProximityTime;
|
|
_maxLevel = content.MaxLevel;
|
|
_activatedByValor = content.ActivatedByValor;
|
|
_damageEntries = content.DamageEntries;
|
|
_confinedRoaming = content.ConfinedRoaming;
|
|
_idol = content.Idol;
|
|
_hasBeenAdvanced = content.HasBeenAdvanced;
|
|
_spawnArea = content.SpawnArea;
|
|
_randomizeType = content.RandomizeType;
|
|
_kills = content.Kills;
|
|
_active = content.Active;
|
|
_type = content.Type;
|
|
_creatures = content.Creatures;
|
|
_redSkulls = content.RedSkulls;
|
|
_whiteSkulls = content.WhiteSkulls;
|
|
_platform = content.Platform;
|
|
_altar = content.Altar;
|
|
_expireDelay = content.ExpireDelay;
|
|
_expireTime = content.ExpireTime;
|
|
_champion = content.Champion;
|
|
_restartDelay = content.RestartDelay;
|
|
_restartTime = content.RestartTime;
|
|
_minions = [];
|
|
}
|
|
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionSpawn));
|
|
|
|
[SerializableField(1)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private bool _activatedByProximity;
|
|
|
|
[AnchoredDateTime]
|
|
[SerializableField(2)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private DateTime _nextProximityTime;
|
|
|
|
[SerializableField(4)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private bool _activatedByValor;
|
|
|
|
[SerializableField(5)]
|
|
private Dictionary<Mobile, int>_damageEntries;
|
|
|
|
[SerializableField(6)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private bool _confinedRoaming;
|
|
|
|
[SerializableField(7)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private IdolOfTheChampion _idol;
|
|
|
|
[SerializableField(8)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private bool _hasBeenAdvanced;
|
|
|
|
[SerializableField(10)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private bool _randomizeType;
|
|
|
|
[InvalidateProperties]
|
|
[SerializableField(13)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private ChampionSpawnType _type;
|
|
|
|
[Tidy]
|
|
[SerializableField(14)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private List<Mobile> _creatures;
|
|
|
|
[Tidy]
|
|
[SerializableField(15)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private List<Item> _redSkulls;
|
|
|
|
[Tidy]
|
|
[SerializableField(16)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private List<Item> _whiteSkulls;
|
|
|
|
[SerializableField(17)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private ChampionPlatform _platform;
|
|
|
|
[SerializableField(18)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private ChampionAltar _altar;
|
|
|
|
[SerializableField(19)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private TimeSpan _expireDelay;
|
|
|
|
[AnchoredDateTime]
|
|
[SerializableField(20)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private DateTime _expireTime;
|
|
|
|
[SerializableField(21)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private Mobile _champion;
|
|
|
|
[SerializableField(22)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private TimeSpan _restartDelay;
|
|
|
|
[AnchoredDateTime]
|
|
[SerializableField(23, setter: "private")]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private DateTime _restartTime;
|
|
|
|
// Mobiles summoned by the champion or the spawn's creatures. Deleted with the creatures, never counted as kills.
|
|
[Tidy]
|
|
[SerializableField(24)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private List<Mobile> _minions;
|
|
|
|
private ChampionSpawnRegion m_Region;
|
|
|
|
//Goes back each level, below level 0 and it goes off!
|
|
|
|
private TimerExecutionToken _timerToken;
|
|
private TimerExecutionToken _restartTimerToken;
|
|
|
|
public virtual string BroadcastMessage => "The Champion has sensed your presence! Beware its wrath!";
|
|
public virtual bool ProximitySpawn => false;
|
|
public virtual bool AlwaysActive => false;
|
|
|
|
public override TimeSpan DecayTime => TimeSpan.FromSeconds(180.0);
|
|
|
|
public virtual bool HasStarRoomGate => true;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Point3D EjectLocation { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Map EjectMap { get; set; }
|
|
|
|
public override int LabelNumber => 1041030; // Evil in a Can: Don't delete me!
|
|
|
|
[Constructible]
|
|
public ChampionSpawn() : base(0xBD2)
|
|
{
|
|
Movable = false;
|
|
Visible = false;
|
|
|
|
_creatures = new List<Mobile>();
|
|
_minions = [];
|
|
_redSkulls = new List<Item>();
|
|
_whiteSkulls = new List<Item>();
|
|
|
|
_platform = new ChampionPlatform(this);
|
|
_altar = new ChampionAltar(this);
|
|
_idol = new IdolOfTheChampion(this);
|
|
|
|
ExpireDelay = TimeSpan.FromMinutes(30.0);
|
|
RestartDelay = TimeSpan.FromMinutes(30.0);
|
|
_damageEntries = new Dictionary<Mobile, int>();
|
|
|
|
Timer.StartTimer(TimeSpan.Zero, SetInitialSpawnArea);
|
|
}
|
|
|
|
public void SetInitialSpawnArea()
|
|
{
|
|
//Previous default used to be 24;
|
|
SpawnArea = new Rectangle2D(new Point2D(X - 24, Y - 24), new Point2D(X + 24, Y + 24));
|
|
}
|
|
|
|
public virtual ChampionSpawnRegion GetRegion() => new(this);
|
|
|
|
public void UpdateRegion()
|
|
{
|
|
m_Region?.Unregister();
|
|
|
|
if (!Deleted && Map != Map.Internal)
|
|
{
|
|
m_Region = GetRegion();
|
|
m_Region.Register();
|
|
}
|
|
}
|
|
|
|
[SerializableProperty(0)]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Level
|
|
{
|
|
get => _level;
|
|
set
|
|
{
|
|
for (var i = _redSkulls.Count - 1; i >= value; --i)
|
|
{
|
|
_redSkulls[i].Delete();
|
|
_redSkulls.RemoveAt(i);
|
|
}
|
|
|
|
for (var i = _redSkulls.Count; i < Math.Min(value, 16); ++i)
|
|
{
|
|
var skull = new Item(0x1854) { Hue = 0x26, Movable = false, Light = LightType.Circle150 };
|
|
skull.MoveToWorld(GetRedSkullLocation(i), Map);
|
|
_redSkulls.Add(skull);
|
|
}
|
|
|
|
_level = value;
|
|
this.MarkDirty();
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
[SerializableField(3, allowFieldChange: nameof(AllowMaxLevelChange))]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
|
|
private int _maxLevel;
|
|
|
|
private bool AllowMaxLevelChange(ref int value)
|
|
{
|
|
value = Math.Clamp(value, 0, 18);
|
|
return true;
|
|
}
|
|
|
|
[SerializableField(9, fieldChanged: nameof(OnSpawnAreaChanged))]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
[InvalidateProperties]
|
|
private Rectangle2D _spawnArea;
|
|
|
|
private void OnSpawnAreaChanged(Rectangle2D oldValue, Rectangle2D newValue)
|
|
{
|
|
UpdateRegion();
|
|
}
|
|
|
|
[SerializableField(11, fieldChanged: nameof(OnKillsChanged))]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
[InvalidateProperties]
|
|
private int _kills;
|
|
|
|
private void OnKillsChanged(int oldValue, int newValue)
|
|
{
|
|
var n = _kills / (double)MaxKills;
|
|
var p = (int)(n * 100);
|
|
if (p < 90)
|
|
{
|
|
SetWhiteSkullCount(p / 20);
|
|
}
|
|
}
|
|
|
|
[SerializableProperty(12)]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Active
|
|
{
|
|
get => _active;
|
|
set
|
|
{
|
|
this.MarkDirty();
|
|
|
|
if (value)
|
|
{
|
|
Start();
|
|
}
|
|
else
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool ReadyToActivate { get; set; }
|
|
|
|
public bool IsChampionSpawn(Mobile m) => _creatures.Contains(m);
|
|
|
|
/// <summary>
|
|
/// The spawn <paramref name="creature" /> fights for: as its champion, one of its creatures, or a minion of either.
|
|
/// </summary>
|
|
public static ChampionSpawn FindSpawnOf(BaseCreature creature)
|
|
{
|
|
var spawn = Region.Find(creature.Location, creature.Map).GetRegion<ChampionSpawnRegion>()?.Spawn;
|
|
|
|
return spawn?.Deleted == false &&
|
|
(spawn.Champion == creature || spawn._minions.Contains(creature) || spawn._creatures.Contains(creature))
|
|
? spawn
|
|
: null;
|
|
}
|
|
|
|
public void AddMinion(Mobile minion) => AddToMinions(minion);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int MaxKills
|
|
{
|
|
get
|
|
{
|
|
return Level switch
|
|
{
|
|
>= 16 => 16,
|
|
>= 12 => 32,
|
|
>= 8 => 64,
|
|
>= 4 => 128,
|
|
_ => 256
|
|
};
|
|
}
|
|
}
|
|
|
|
public void SetWhiteSkullCount(int val)
|
|
{
|
|
for (var i = _whiteSkulls.Count - 1; i >= val; --i)
|
|
{
|
|
_whiteSkulls[i].Delete();
|
|
_whiteSkulls.RemoveAt(i);
|
|
}
|
|
|
|
for (var i = _whiteSkulls.Count; i < val; ++i)
|
|
{
|
|
var skull = new Item(0x1854)
|
|
{
|
|
Movable = false,
|
|
Light = LightType.Circle150
|
|
};
|
|
|
|
skull.MoveToWorld(GetWhiteSkullLocation(i), Map);
|
|
|
|
_whiteSkulls.Add(skull);
|
|
|
|
Effects.PlaySound(skull.Location, skull.Map, 0x29);
|
|
Effects.SendLocationEffect(new Point3D(skull.X + 1, skull.Y + 1, skull.Z), skull.Map, 0x3728, 10);
|
|
}
|
|
|
|
this.MarkDirty();
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (_active || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (RandomizeType)
|
|
{
|
|
Type = Utility.Random(5) switch
|
|
{
|
|
0 => ChampionSpawnType.VerminHorde,
|
|
1 => ChampionSpawnType.UnholyTerror,
|
|
2 => ChampionSpawnType.ColdBlood,
|
|
3 => ChampionSpawnType.Abyss,
|
|
4 => ChampionSpawnType.Arachnid,
|
|
_ => Type
|
|
};
|
|
}
|
|
|
|
_active = true;
|
|
ReadyToActivate = false;
|
|
HasBeenAdvanced = false;
|
|
MaxLevel = 16 + Utility.Random(3);
|
|
|
|
_timerToken.Cancel();
|
|
Timer.StartTimer(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1.0), OnSlice, out _timerToken);
|
|
|
|
_restartTimerToken.Cancel();
|
|
|
|
if (_altar != null)
|
|
{
|
|
_altar.Hue = Champion != null ? 0x26 : 0;
|
|
}
|
|
|
|
if (_platform != null)
|
|
{
|
|
_platform.Hue = 0x452;
|
|
}
|
|
|
|
ExpireTime = Core.Now + ExpireDelay;
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (!_active || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_active = false;
|
|
ActivatedByValor = false;
|
|
HasBeenAdvanced = false;
|
|
MaxLevel = 0;
|
|
|
|
_timerToken.Cancel();
|
|
_restartTimerToken.Cancel();
|
|
|
|
if (_altar != null)
|
|
{
|
|
_altar.Hue = 0;
|
|
}
|
|
|
|
if (_platform != null)
|
|
{
|
|
_platform.Hue = 0x497;
|
|
}
|
|
|
|
if (AlwaysActive)
|
|
{
|
|
BeginRestart(RestartDelay);
|
|
}
|
|
else if (ActivatedByProximity)
|
|
{
|
|
ActivatedByProximity = false;
|
|
NextProximityTime = Core.Now + TimeSpan.FromHours(6.0);
|
|
}
|
|
|
|
Timer.StartTimer(TimeSpan.FromMinutes(10.0), ExpireCreatures);
|
|
}
|
|
|
|
public void BeginRestart(TimeSpan ts)
|
|
{
|
|
RestartTime = Core.Now + ts;
|
|
_restartTimerToken.Cancel();
|
|
Timer.StartTimer(ts, EndRestart, out _restartTimerToken);
|
|
}
|
|
|
|
public void EndRestart()
|
|
{
|
|
HasBeenAdvanced = false;
|
|
ReadyToActivate = true;
|
|
}
|
|
|
|
private static ScrollofTranscendence CreateRandomSoT(bool isFel)
|
|
{
|
|
var min = isFel ? 1 : 6;
|
|
var max = isFel ? 5 : 10;
|
|
return ScrollofTranscendence.CreateRandom(min, max);
|
|
}
|
|
|
|
private static PowerScroll CreateRandomPS() => PowerScroll.CreateRandomNoCraft(5, 5);
|
|
|
|
public static void GiveScrollTo(Mobile killer, SpecialScroll scroll, bool isFelucca = true)
|
|
{
|
|
if (scroll == null || killer == null) // sanity
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (scroll is ScrollofTranscendence)
|
|
{
|
|
killer.SendLocalizedMessage(1094936); // You have received a Scroll of Transcendence!
|
|
}
|
|
else
|
|
{
|
|
killer.SendLocalizedMessage(1049524); // You have received a scroll of power!
|
|
}
|
|
|
|
if (!isFelucca)
|
|
{
|
|
killer.AddToBackpack(scroll);
|
|
return;
|
|
}
|
|
|
|
if (killer.Alive || killer.Corpse is not { Deleted: false })
|
|
{
|
|
killer.AddToBackpack(scroll);
|
|
}
|
|
else
|
|
{
|
|
killer.Corpse.DropItem(scroll);
|
|
}
|
|
|
|
// Justice reward
|
|
var pm = (PlayerMobile)killer;
|
|
var prot = JusticeVirtue.GetProtector(pm);
|
|
if (prot == null || prot.Map != killer.Map || prot.Murderer || prot.Criminal ||
|
|
!JusticeVirtue.CheckMapRegion(killer, prot))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var chance = VirtueSystem.GetLevel(prot, VirtueName.Justice) switch
|
|
{
|
|
VirtueLevel.Seeker => 60,
|
|
VirtueLevel.Follower => 80,
|
|
VirtueLevel.Knight => 100,
|
|
_ => 0
|
|
};
|
|
|
|
if (chance > 0 && chance > Utility.Random(100))
|
|
{
|
|
try
|
|
{
|
|
var scrollDupe = scroll.GetType().CreateEntityInstance<SpecialScroll>();
|
|
|
|
if (scrollDupe != null)
|
|
{
|
|
prot.SendLocalizedMessage(1049368); // You have been rewarded for your dedication to Justice!
|
|
scrollDupe.Skill = scroll.Skill;
|
|
scrollDupe.Value = scroll.Value;
|
|
prot.AddToBackpack(scrollDupe);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnSlice()
|
|
{
|
|
if (!_active || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Champion != null)
|
|
{
|
|
if (Champion.Deleted)
|
|
{
|
|
RegisterDamageTo(Champion);
|
|
|
|
if (Core.ML)
|
|
{
|
|
AwardArtifact((Champion as BaseChampion)?.GetArtifact());
|
|
}
|
|
|
|
ClearDamageEntries();
|
|
|
|
if (_platform != null)
|
|
{
|
|
_platform.Hue = 0x497;
|
|
}
|
|
|
|
if (_altar != null)
|
|
{
|
|
_altar.Hue = 0;
|
|
|
|
if (HasStarRoomGate && (!Core.ML || Map == Map.Felucca))
|
|
{
|
|
new StarRoomGate(_altar.Location, _altar.Map, true);
|
|
}
|
|
}
|
|
|
|
Champion = null;
|
|
Stop();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var kills = _kills;
|
|
|
|
for (var i = 0; i < _creatures.Count; ++i)
|
|
{
|
|
var m = _creatures[i];
|
|
|
|
if (!m.Deleted)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (m.Corpse is { Deleted: false })
|
|
{
|
|
((Corpse)m.Corpse).BeginDecay(TimeSpan.FromMinutes(1));
|
|
}
|
|
|
|
RemoveFromCreaturesAt(i);
|
|
--i;
|
|
++_kills;
|
|
|
|
var killer = m.FindMostRecentDamager(false);
|
|
|
|
RegisterDamageTo(m);
|
|
|
|
if (killer is BaseCreature creature)
|
|
{
|
|
killer = creature.GetMaster();
|
|
}
|
|
|
|
if (killer is not PlayerMobile pm)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var mobSubLevel = GetSubLevelfor(m) + 1;
|
|
|
|
if (mobSubLevel >= 0)
|
|
{
|
|
var gainedPath = false;
|
|
|
|
var pointsToGain = mobSubLevel * 40;
|
|
|
|
if (VirtueSystem.Award(pm, VirtueName.Valor, pointsToGain, ref gainedPath))
|
|
{
|
|
if (gainedPath)
|
|
{
|
|
pm.SendLocalizedMessage(1054032); // You have gained a path in Valor!
|
|
}
|
|
else
|
|
{
|
|
pm.SendLocalizedMessage(1054030); // You have gained in Valor!
|
|
}
|
|
|
|
// No delay on Valor gains
|
|
}
|
|
|
|
pm.ChampionTitles.Award(_type, mobSubLevel);
|
|
}
|
|
|
|
if (!Core.ML)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var isFel = Map == Map.Felucca;
|
|
double mapChance;
|
|
if (isFel)
|
|
{
|
|
mapChance = 0.001;
|
|
}
|
|
else if (Map == Map.Ilshenar || Map == Map.Tokuno)
|
|
{
|
|
mapChance = 0.0015;
|
|
}
|
|
else // Nothing drops in Trammel, Malas or TerMur
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (Utility.RandomDouble() >= mapChance)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!isFel || Utility.RandomBool())
|
|
{
|
|
GiveScrollTo(pm, CreateRandomSoT(isFel), isFel);
|
|
}
|
|
else
|
|
{
|
|
GiveScrollTo(pm, CreateRandomPS());
|
|
}
|
|
}
|
|
|
|
// Only really needed once.
|
|
if (_kills > kills)
|
|
{
|
|
InvalidateProperties();
|
|
}
|
|
|
|
var n = _kills / (double)MaxKills;
|
|
var p = (int)(n * 100);
|
|
|
|
if (p >= 99)
|
|
{
|
|
AdvanceLevel();
|
|
}
|
|
else if (p > 0)
|
|
{
|
|
SetWhiteSkullCount(p / 20);
|
|
}
|
|
|
|
if (Core.Now >= ExpireTime)
|
|
{
|
|
Expire();
|
|
}
|
|
|
|
Respawn();
|
|
}
|
|
|
|
public void AdvanceLevel()
|
|
{
|
|
ExpireTime = Core.Now + ExpireDelay;
|
|
|
|
if (Level < _maxLevel)
|
|
{
|
|
_kills = 0;
|
|
++Level;
|
|
InvalidateProperties();
|
|
SetWhiteSkullCount(0);
|
|
|
|
if (_altar != null)
|
|
{
|
|
Effects.PlaySound(_altar.Location, _altar.Map, 0x29);
|
|
Effects.SendLocationEffect(new Point3D(_altar.X + 1, _altar.Y + 1, _altar.Z), _altar.Map, 0x3728, 10);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SpawnChampion();
|
|
}
|
|
}
|
|
|
|
public void SpawnChampion()
|
|
{
|
|
if (_altar != null)
|
|
{
|
|
_altar.Hue = 0x26;
|
|
}
|
|
|
|
if (_platform != null)
|
|
{
|
|
_platform.Hue = 0x452;
|
|
}
|
|
|
|
_kills = 0;
|
|
Level = 0;
|
|
InvalidateProperties();
|
|
SetWhiteSkullCount(0);
|
|
|
|
try
|
|
{
|
|
Champion = ChampionSpawnInfo.GetInfo(_type).Champion.CreateInstance<Mobile>();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error(
|
|
e,
|
|
"Failed to spawn champion \"{MobileType}\" at {Location} ({Map}).",
|
|
_type,
|
|
Location,
|
|
Map
|
|
);
|
|
}
|
|
|
|
if (Champion != null)
|
|
{
|
|
Champion.MoveToWorld(new Point3D(X, Y, Z - 15), Map);
|
|
|
|
if (Champion is BaseCreature bc)
|
|
{
|
|
if (ConfinedRoaming)
|
|
{
|
|
bc.Home = Location;
|
|
bc.HomeMap = Map;
|
|
bc.RangeHome = Math.Min(_spawnArea.Width / 2, _spawnArea.Height / 2);
|
|
}
|
|
else
|
|
{
|
|
bc.Home = bc.Location;
|
|
bc.HomeMap = bc.Map;
|
|
|
|
var xWall1 = new Point2D(_spawnArea.X, bc.Y);
|
|
var xWall2 = new Point2D(_spawnArea.X + _spawnArea.Width, bc.Y);
|
|
var yWall1 = new Point2D(bc.X, _spawnArea.Y);
|
|
var yWall2 = new Point2D(bc.X, _spawnArea.Y + _spawnArea.Height);
|
|
|
|
var minXDist = Math.Min(bc.GetDistanceToSqrt(xWall1), bc.GetDistanceToSqrt(xWall2));
|
|
var minYDist = Math.Min(bc.GetDistanceToSqrt(yWall1), bc.GetDistanceToSqrt(yWall2));
|
|
|
|
bc.RangeHome = (int)Math.Min(minXDist, minYDist);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Champion Spawn is not inherited from BaseCreature");
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int MaxSpawn => 250 - GetSubLevel() * 40;
|
|
|
|
public void Respawn()
|
|
{
|
|
if (!_active || Deleted || Champion != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
while (_creatures.Count < MaxSpawn)
|
|
{
|
|
var m = Spawn();
|
|
|
|
if (m == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var loc = GetSpawnLocation();
|
|
|
|
// Allow creatures to turn into Paragons at Ilshenar champions.
|
|
m.OnBeforeSpawn(loc, Map);
|
|
|
|
AddToCreatures(m);
|
|
m.MoveToWorld(loc, Map);
|
|
|
|
if (m is BaseCreature bc)
|
|
{
|
|
bc.Tamable = false;
|
|
|
|
if (ConfinedRoaming)
|
|
{
|
|
bc.Home = Location;
|
|
bc.HomeMap = Map;
|
|
bc.RangeHome = Math.Min(_spawnArea.Width / 2, _spawnArea.Height / 2);
|
|
}
|
|
else
|
|
{
|
|
bc.Home = bc.Location;
|
|
bc.HomeMap = bc.Map;
|
|
|
|
var xWall1 = new Point2D(_spawnArea.X, bc.Y);
|
|
var xWall2 = new Point2D(_spawnArea.X + _spawnArea.Width, bc.Y);
|
|
var yWall1 = new Point2D(bc.X, _spawnArea.Y);
|
|
var yWall2 = new Point2D(bc.X, _spawnArea.Y + _spawnArea.Height);
|
|
|
|
var minXDist = Math.Min(bc.GetDistanceToSqrt(xWall1), bc.GetDistanceToSqrt(xWall2));
|
|
var minYDist = Math.Min(bc.GetDistanceToSqrt(yWall1), bc.GetDistanceToSqrt(yWall2));
|
|
|
|
bc.RangeHome = (int)Math.Min(minXDist, minYDist);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public Point3D GetSpawnLocation()
|
|
{
|
|
var map = Map;
|
|
|
|
if (map == null)
|
|
{
|
|
return Location;
|
|
}
|
|
|
|
// Try 20 times to find a spawnable location.
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
var x = Utility.Random(_spawnArea.X, _spawnArea.Width);
|
|
var y = Utility.Random(_spawnArea.Y, _spawnArea.Height);
|
|
|
|
var z = Map.GetAverageZ(x, y);
|
|
|
|
if (Map.CanSpawnMobile(new Point2D(x, y), z))
|
|
{
|
|
return new Point3D(x, y, z);
|
|
}
|
|
}
|
|
|
|
return Location;
|
|
}
|
|
|
|
public int Level1 => 4;
|
|
public int Level2 => 8;
|
|
public int Level3 => 12;
|
|
|
|
public int GetSubLevel()
|
|
{
|
|
var level = Level;
|
|
|
|
return level <= Level1 ? 0 : level <= Level2 ? 1 : level <= Level3 ? 2 : 3;
|
|
}
|
|
|
|
public int GetSubLevelfor (Mobile m)
|
|
{
|
|
var types = ChampionSpawnInfo.GetInfo(_type).SpawnTypes;
|
|
var t = m.GetType();
|
|
|
|
for (var i = 0; i < types.GetLength(0); i++)
|
|
{
|
|
var individualTypes = types[i];
|
|
|
|
for (var j = 0; j < individualTypes.Length; j++)
|
|
{
|
|
if (t == individualTypes[j])
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public Mobile Spawn()
|
|
{
|
|
var types = ChampionSpawnInfo.GetInfo(_type).SpawnTypes;
|
|
|
|
var v = GetSubLevel();
|
|
|
|
if (v >= 0 && v < types.Length)
|
|
{
|
|
return Spawn(types[v]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public Mobile Spawn(params Type[] types)
|
|
{
|
|
var type = types[Utility.Random(types.Length)];
|
|
try
|
|
{
|
|
return type.CreateInstance<Mobile>();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error(
|
|
e,
|
|
"Failed to spawn minion \"{Type}\" for champion {ChampionSpawnType} at {Location} ({Map}).",
|
|
type,
|
|
_type,
|
|
Location,
|
|
Map
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void Expire()
|
|
{
|
|
_kills = 0;
|
|
|
|
if (_whiteSkulls.Count == 0)
|
|
{
|
|
// They didn't even get 20%, go back a level
|
|
|
|
if (Level > 0)
|
|
{
|
|
--Level;
|
|
}
|
|
|
|
if (!AlwaysActive && Level == 0)
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
InvalidateProperties();
|
|
}
|
|
else
|
|
{
|
|
SetWhiteSkullCount(0);
|
|
}
|
|
|
|
ExpireTime = Core.Now + ExpireDelay;
|
|
}
|
|
|
|
public Point3D GetRedSkullLocation(int index)
|
|
{
|
|
int x, y;
|
|
|
|
if (index < 5)
|
|
{
|
|
x = index - 2;
|
|
y = -2;
|
|
}
|
|
else if (index < 9)
|
|
{
|
|
x = 2;
|
|
y = index - 6;
|
|
}
|
|
else if (index < 13)
|
|
{
|
|
x = 10 - index;
|
|
y = 2;
|
|
}
|
|
else
|
|
{
|
|
x = -2;
|
|
y = 14 - index;
|
|
}
|
|
|
|
return new Point3D(X + x, Y + y, Z - 15);
|
|
}
|
|
|
|
public Point3D GetWhiteSkullLocation(int index)
|
|
{
|
|
int x, y;
|
|
|
|
switch(index)
|
|
{
|
|
default: x = -1; y = -1; break;
|
|
case 1: x = 1; y = -1; break;
|
|
case 2: x = 1; y = 1; break;
|
|
case 3: x = -1; y = 1; break;
|
|
}
|
|
|
|
return new Point3D(X + x, Y + y, Z - 15);
|
|
}
|
|
|
|
public override void AddNameProperty(IPropertyList list)
|
|
{
|
|
list.Add("champion spawn");
|
|
}
|
|
|
|
public override void GetProperties(IPropertyList list)
|
|
{
|
|
base.GetProperties(list);
|
|
|
|
if (_active)
|
|
{
|
|
list.Add(1060742); // active
|
|
list.Add(1060658, $"{"Type"}\t{_type}"); // ~1_val~: ~2_val~
|
|
list.Add(1060659, $"{"Level"}\t{Level}"); // ~1_val~: ~2_val~
|
|
var killRatio = 100.0 * ((double)_kills / MaxKills);
|
|
list.Add(1060660, $"{"Kills"}\t{_kills} of {MaxKills} ({killRatio:F1}%)"); // ~1_val~: ~2_val~
|
|
}
|
|
else
|
|
{
|
|
list.Add(1060743); // inactive
|
|
}
|
|
}
|
|
|
|
public override void OnSingleClick(Mobile from)
|
|
{
|
|
if (_active)
|
|
{
|
|
LabelTo(from, $"{_type} (Active; Level: {Level}; Kills: {_kills}/{MaxKills})");
|
|
}
|
|
else
|
|
{
|
|
LabelTo(from, $"{_type} (Inactive)");
|
|
}
|
|
}
|
|
|
|
public override void OnDoubleClick(Mobile from)
|
|
{
|
|
from.SendGump(new PropertiesGump(from, this));
|
|
}
|
|
|
|
public override void OnLocationChange(Point3D oldLoc)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_platform != null)
|
|
{
|
|
_platform.Location = new Point3D(X, Y, Z - 20);
|
|
}
|
|
|
|
if (_altar != null)
|
|
{
|
|
_altar.Location = new Point3D(X, Y, Z - 15);
|
|
}
|
|
|
|
if (_idol != null)
|
|
{
|
|
_idol.Location = new Point3D(X, Y, Z - 15);
|
|
}
|
|
|
|
if (_redSkulls != null)
|
|
{
|
|
for (var i = 0; i < Math.Min(_redSkulls.Count, 16); ++i)
|
|
{
|
|
_redSkulls[i].Location = GetRedSkullLocation(i);
|
|
}
|
|
}
|
|
|
|
if (_whiteSkulls != null)
|
|
{
|
|
for (var i = 0; i < _whiteSkulls.Count; ++i)
|
|
{
|
|
_whiteSkulls[i].Location = GetWhiteSkullLocation(i);
|
|
}
|
|
}
|
|
|
|
_spawnArea.X += Location.X - oldLoc.X;
|
|
_spawnArea.Y += Location.Y - oldLoc.Y;
|
|
|
|
UpdateRegion();
|
|
}
|
|
|
|
public override void OnMapChange()
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_platform != null)
|
|
{
|
|
_platform.Map = Map;
|
|
}
|
|
|
|
if (_altar != null)
|
|
{
|
|
_altar.Map = Map;
|
|
}
|
|
|
|
if (_idol != null)
|
|
{
|
|
_idol.Map = Map;
|
|
}
|
|
|
|
if (_redSkulls != null)
|
|
{
|
|
for (var i = 0; i < _redSkulls.Count; ++i)
|
|
{
|
|
_redSkulls[i].Map = Map;
|
|
}
|
|
}
|
|
|
|
if (_whiteSkulls != null)
|
|
{
|
|
for (var i = 0; i < _whiteSkulls.Count; ++i)
|
|
{
|
|
_whiteSkulls[i].Map = Map;
|
|
}
|
|
}
|
|
|
|
UpdateRegion();
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
base.OnAfterDelete();
|
|
|
|
_platform?.Delete();
|
|
|
|
_altar?.Delete();
|
|
|
|
_idol?.Delete();
|
|
|
|
if (_redSkulls != null)
|
|
{
|
|
for (var i = 0; i < _redSkulls.Count; ++i)
|
|
{
|
|
_redSkulls[i].Delete();
|
|
}
|
|
|
|
ClearRedSkulls();
|
|
}
|
|
|
|
if (_whiteSkulls != null)
|
|
{
|
|
for (var i = 0; i < _whiteSkulls.Count; ++i)
|
|
{
|
|
_whiteSkulls[i].Delete();
|
|
}
|
|
|
|
ClearWhiteSkulls();
|
|
}
|
|
|
|
DeleteCreatures();
|
|
|
|
if (Champion is { Player: false })
|
|
{
|
|
Champion.Delete();
|
|
}
|
|
|
|
Stop();
|
|
|
|
UpdateRegion();
|
|
}
|
|
|
|
public void ExpireCreatures()
|
|
{
|
|
if (!_active && !ReadyToActivate && !AlwaysActive)
|
|
{
|
|
DeleteCreatures();
|
|
}
|
|
}
|
|
|
|
public void DeleteCreatures()
|
|
{
|
|
if (_creatures != null)
|
|
{
|
|
for (var i = 0; i < _creatures.Count; ++i)
|
|
{
|
|
var mob = _creatures[i];
|
|
|
|
if (!mob.Player)
|
|
{
|
|
mob.Delete();
|
|
}
|
|
}
|
|
|
|
ClearCreatures();
|
|
}
|
|
|
|
for (var i = 0; i < _minions.Count; ++i)
|
|
{
|
|
_minions[i].Delete();
|
|
}
|
|
|
|
ClearMinions();
|
|
}
|
|
|
|
public virtual void RegisterDamageTo(Mobile m)
|
|
{
|
|
if (m == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var de in m.DamageEntries)
|
|
{
|
|
var damager = de.Damager;
|
|
var master = damager.GetDamageMaster(m);
|
|
|
|
if (master != null)
|
|
{
|
|
damager = master;
|
|
}
|
|
|
|
RegisterDamage(damager, de.DamageGiven);
|
|
}
|
|
}
|
|
|
|
public void RegisterDamage(Mobile from, int amount)
|
|
{
|
|
if (from?.Player != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(_damageEntries, from, out _);
|
|
value += amount;
|
|
|
|
this.MarkDirty();
|
|
}
|
|
|
|
public virtual void AwardArtifact(Item artifact)
|
|
{
|
|
if (artifact == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (DamageEntries.Count <= 0)
|
|
{
|
|
artifact.Delete();
|
|
return;
|
|
}
|
|
|
|
var totalDamage = 0;
|
|
|
|
var validEntries = new Dictionary<Mobile, int>();
|
|
|
|
foreach (var (key, value) in DamageEntries)
|
|
{
|
|
if (IsEligible(key, artifact))
|
|
{
|
|
validEntries.Add(key, value);
|
|
totalDamage += value;
|
|
}
|
|
}
|
|
|
|
var artifactGiven = false;
|
|
|
|
do
|
|
{
|
|
var randomDamage = Utility.RandomMinMax(1, totalDamage);
|
|
|
|
var checkDamage = 0;
|
|
|
|
foreach (var (key, value) in validEntries)
|
|
{
|
|
checkDamage += value;
|
|
|
|
if (checkDamage > randomDamage)
|
|
{
|
|
if (GiveArtifact(key, artifact))
|
|
{
|
|
artifactGiven = true;
|
|
}
|
|
else
|
|
{
|
|
validEntries.Remove(key);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (validEntries.Count == 0) // EVERYONE has a full backpack?!@
|
|
{
|
|
artifact.Delete();
|
|
break;
|
|
}
|
|
} while (!artifactGiven);
|
|
}
|
|
|
|
public static bool GiveArtifact(Mobile to, Item artifact)
|
|
{
|
|
if (to == null || artifact == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var pack = to.Backpack;
|
|
|
|
if (pack?.TryDropItem(to, artifact, false) != true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
to.SendLocalizedMessage(1062317); // For your valor in combating the fallen beast, a special artifact has been bestowed on you.
|
|
return true;
|
|
}
|
|
|
|
public bool IsEligible(Mobile m, Item Artifact) =>
|
|
m.Player && m.Alive && m.Region != null && m.Region == m_Region &&
|
|
m.Backpack?.CheckHold(m, Artifact, false) == true;
|
|
|
|
private void Deserialize(IGenericReader reader, int version)
|
|
{
|
|
_damageEntries = new Dictionary<Mobile, int>();
|
|
|
|
_level = reader.ReadInt();
|
|
_activatedByProximity = reader.ReadBool();
|
|
_nextProximityTime = reader.ReadDeltaTime();
|
|
_maxLevel = reader.ReadInt();
|
|
_activatedByValor = reader.ReadBool();
|
|
|
|
var entries = reader.ReadInt();
|
|
for (var i = 0; i < entries; ++i)
|
|
{
|
|
var m = reader.ReadEntity<Mobile>();
|
|
var damage = reader.ReadInt();
|
|
if (m != null)
|
|
{
|
|
_damageEntries.Add(m, damage);
|
|
}
|
|
}
|
|
|
|
_confinedRoaming = reader.ReadBool();
|
|
_idol = reader.ReadEntity<IdolOfTheChampion>();
|
|
_hasBeenAdvanced = reader.ReadBool();
|
|
_spawnArea = reader.ReadRect2D();
|
|
_randomizeType = reader.ReadBool();
|
|
_kills = reader.ReadInt();
|
|
|
|
_active = reader.ReadBool();
|
|
_type = (ChampionSpawnType)reader.ReadInt();
|
|
_creatures = reader.ReadEntityList<Mobile>();
|
|
_minions = [];
|
|
_redSkulls = reader.ReadEntityList<Item>();
|
|
_whiteSkulls = reader.ReadEntityList<Item>();
|
|
_platform = reader.ReadEntity<ChampionPlatform>();
|
|
_altar = reader.ReadEntity<ChampionAltar>();
|
|
_expireDelay = reader.ReadTimeSpan();
|
|
_expireTime = reader.ReadDeltaTime();
|
|
_champion = reader.ReadEntity<Mobile>();
|
|
_restartDelay = reader.ReadTimeSpan();
|
|
|
|
if (reader.ReadBool())
|
|
{
|
|
_restartTime = reader.ReadDeltaTime();
|
|
}
|
|
}
|
|
|
|
[AfterDeserialization(false)]
|
|
private void AfterDeserialization()
|
|
{
|
|
if (_restartTime < Core.Now)
|
|
{
|
|
BeginRestart(_restartTime - Core.Now);
|
|
}
|
|
|
|
if (_platform == null || _altar == null || _idol == null)
|
|
{
|
|
Delete();
|
|
}
|
|
else if (_active)
|
|
{
|
|
_active = false; // Set back to false so we can properly start
|
|
Start();
|
|
}
|
|
else if (AlwaysActive)
|
|
{
|
|
ReadyToActivate = true;
|
|
}
|
|
|
|
Timer.StartTimer(TimeSpan.Zero, UpdateRegion);
|
|
}
|
|
}
|
|
|
|
public class ChampionSpawnRegion : BaseRegion
|
|
{
|
|
public ChampionSpawn Spawn { get; }
|
|
|
|
public ChampionSpawnRegion(ChampionSpawn spawn) :
|
|
base(null, spawn.Map, Find(spawn.Location, spawn.Map), spawn.SpawnArea) => Spawn = spawn;
|
|
|
|
public override bool AllowHousing(Mobile from, Point3D p) => false;
|
|
|
|
public bool CanSpawn() => Spawn.EjectLocation != new Point3D(0, 0, 0) && Spawn.EjectMap != null;
|
|
|
|
public override void AlterLightLevel(Mobile m, ref int global, ref int personal)
|
|
{
|
|
base.AlterLightLevel(m, ref global, ref personal);
|
|
global = Math.Max(global, 1 + Spawn.Level); //This is a guesstimate. TODO: Verify & get exact values // OSI testing: at 2 red skulls, light = 0x3 ; 1 red = 0x3.; 3 = 8; 9 = 0xD 8 = 0xD 12 = 0x12 10 = 0xD
|
|
}
|
|
|
|
private static readonly HashSet<IPAddress> _addresses = [];
|
|
|
|
public override void OnEnter(Mobile m)
|
|
{
|
|
if (!m.Player || m.AccessLevel != AccessLevel.Player || Spawn.Active)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Spawn.ReadyToActivate)
|
|
{
|
|
Spawn.Start();
|
|
return;
|
|
}
|
|
|
|
if (!Spawn.ProximitySpawn || Spawn.ActivatedByProximity || Core.Now < Spawn.NextProximityTime)
|
|
{
|
|
return;
|
|
}
|
|
|
|
using var players = (Parent ?? this).GetPlayersPooled();
|
|
|
|
for (var i = 0; i < players.Count; i++)
|
|
{
|
|
var player = players[i];
|
|
if (player.AccessLevel == AccessLevel.Player && player.NetState != null && !((PlayerMobile)player).Young)
|
|
{
|
|
_addresses.Add(player.NetState.Address);
|
|
}
|
|
}
|
|
|
|
if (_addresses.Count >= 15)
|
|
{
|
|
for (var i = 0; i < players.Count; i++)
|
|
{
|
|
players[i].SendMessage(0x20, Spawn.BroadcastMessage);
|
|
}
|
|
|
|
Spawn.ActivatedByProximity = true;
|
|
Spawn.BeginRestart(TimeSpan.FromMinutes(5.0));
|
|
}
|
|
|
|
_addresses.Clear();
|
|
}
|
|
|
|
public override bool OnMoveInto(Mobile m, Direction d, Point3D newLocation, Point3D oldLocation)
|
|
{
|
|
if (!base.OnMoveInto(m, d, newLocation, oldLocation))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!m.Player)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (((PlayerMobile)m).Young)
|
|
{
|
|
m.SendMessage("You decide against going here because of the danger.");
|
|
return false;
|
|
}
|
|
|
|
if (!m.Alive)
|
|
{
|
|
m.SendMessage("A magical force prevents ghosts from entering this region.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public override bool OnBeforeDeath(Mobile m)
|
|
{
|
|
if (Parent?.OnBeforeDeath(m) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (m.Player) //Give them 5 minutes to resurrect, then they are booted.
|
|
{
|
|
m.SendMessage("A magical force encompasses you, attempting to force you out of the area.");
|
|
new EjectTimer(m, this).Start();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private class EjectTimer : Timer
|
|
{
|
|
private readonly Mobile m_From;
|
|
private readonly ChampionSpawnRegion m_Region;
|
|
|
|
public EjectTimer(Mobile from, ChampionSpawnRegion region) : base(TimeSpan.FromMinutes(5.0))
|
|
{
|
|
m_From = from;
|
|
m_Region = region;
|
|
}
|
|
|
|
protected override void OnTick()
|
|
{
|
|
//See if they are dead, or logged out!
|
|
if (m_Region.Spawn == null || !m_Region.CanSpawn() || m_From.Alive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_From.NetState != null)
|
|
{
|
|
if (m_From.Region.IsPartOf(m_Region))
|
|
{
|
|
m_From.MoveToWorld(m_Region.Spawn.EjectLocation, m_Region.Spawn.EjectMap);
|
|
m_From.SendMessage("A magical force forces you out of the area.");
|
|
}
|
|
}
|
|
else if (Find(m_From.LogoutLocation, m_From.LogoutMap).IsPartOf(m_Region))
|
|
{
|
|
m_From.LogoutLocation = m_Region.Spawn.EjectLocation;
|
|
m_From.LogoutMap = m_Region.Spawn.EjectMap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[SerializationGenerator(0, false)]
|
|
public partial class IdolOfTheChampion : Item
|
|
{
|
|
[SerializableField(0)]
|
|
private ChampionSpawn _spawn;
|
|
|
|
public override string DefaultName => "Idol of the Champion";
|
|
|
|
public IdolOfTheChampion(ChampionSpawn spawn): base(0x1F18)
|
|
{
|
|
_spawn = spawn;
|
|
Movable = false;
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
base.OnAfterDelete();
|
|
_spawn?.Delete();
|
|
}
|
|
|
|
[AfterDeserialization(false)]
|
|
private void AfterDeserialization()
|
|
{
|
|
if (_spawn == null)
|
|
{
|
|
Delete();
|
|
}
|
|
}
|
|
}
|