/************************************************************************* * 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 . * *************************************************************************/ using System; using System.Net; using System.Collections.Generic; using System.Runtime.InteropServices; using ModernUO.Serialization; using Server.Engines.Virtues; using Server.Gumps; using Server.Items; using Server.Mobiles; using Server.Regions; using Server.Logging; namespace Server.Engines.CannedEvil; [SerializationGenerator(11, 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 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_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 _creatures; [Tidy] [SerializableField(15)] [SerializedCommandProperty(AccessLevel.GameMaster)] private List _redSkulls; [Tidy] [SerializableField(16)] [SerializedCommandProperty(AccessLevel.GameMaster)] private List _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; 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(); _redSkulls = new List(); _whiteSkulls = new List(); _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(); 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); [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 isVeridia = 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 (!isVeridia) { 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(); 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.Veridia)) { 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.Veridia; double mapChance; if (isFel) { mapChance = 0.001; } else if (Map == Map.Ilshenar || Map == Map.Tokuno) { mapChance = 0.0015; } else // Nothing drops in Underworld, 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(); } 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(); } 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(); } } public virtual void RegisterDamageTo(Mobile m) { if (m == null) { return; } foreach (var de in m.DamageEntries) { if (de.HasExpired) { continue; } 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(); 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(); _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(); var damage = reader.ReadInt(); if (m != null) { _damageEntries.Add(m, damage); } } _confinedRoaming = reader.ReadBool(); _idol = reader.ReadEntity(); _hasBeenAdvanced = reader.ReadBool(); _spawnArea = reader.ReadRect2D(); _randomizeType = reader.ReadBool(); _kills = reader.ReadInt(); _active = reader.ReadBool(); _type = (ChampionSpawnType)reader.ReadInt(); _creatures = reader.ReadEntityList(); _redSkulls = reader.ReadEntityList(); _whiteSkulls = reader.ReadEntityList(); _platform = reader.ReadEntity(); _altar = reader.ReadEntity(); _expireDelay = reader.ReadTimeSpan(); _expireTime = reader.ReadDeltaTime(); _champion = reader.ReadEntity(); _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 _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(); } } }