## Summary Phase 3 of the anchored-time work: **every actively-written delta-time value in the engine now stores an anchored timestamp** — absolute on the wire, shifted forward by the downtime at load. Remaining time survives restarts (as delta did), and unlike delta, the bytes do not change on every save, so an idle world serializes identically save after save. The answer to "is it possible everywhere": **yes** — including the one case that looked impossible. ## The GenericPersistence problem, solved `GenericPersistence` bins (`Virtues.bin`, `StealableArtifacts.bin`, …) are raw payloads with no idx header, so they have no anchor of their own — anchored reads there would silently apply zero shift. But the anchor is a property of the **save**, not the file: every file in one save shares one `World.SaveStartTime`, and `Persistence.Load` reads **all** entity indexes (phase 1) before **any** persistence payload (phase 2). So the idx v5 header stamps a save-wide `World.LoadTimeShift`, and generic persistence readers inherit it. No file-format change, no per-bin header, old bins unaffected. ## Converted - **Item v10 → v11**: `LastMoved` — previously whole-minute delta, rewritten every save for every item, the single largest source of idle-save churn — and `DecayResetTime` (retiring the TODO from #2583). **Mobile v37 → v38**: the three stat-gain stamps. **BaseCreature v20 → v21**: `SummonEnd`. - **17 code-generated classes** (`[DeltaDateTime]` → `[AnchoredDateTime]`, version bump + `MigrateFrom` each): the five field spells, TransientItem, VirtueContext (×7 fields), PuzzleChestSolutionAndTime, BaseCamp, BaseBoat, RentedVendor, PlayerVendor, Ethics Player, Sheep, StarRoomGate, ChampionSpawn (×3), Corpse (`TimeOfDeath`, v19). The `MigrateFrom` bodies were generated from each class's current migration schema and are compiler-verified; VirtueContext's save-flagged nullables fall back to the same defaults the old deserialize left in place. Corpse's six migrations moved to a new `Corpse.Migrations.cs`. - **Hand-written sites**: StealableArtifacts (v2), VendorInventory (v1), ML quest objectives (persistence v3) — each gated on its own version. **Not converted, deliberately**: the ~25 read-only `ReadDeltaTime` sites in legacy version fallbacks and migration replays — they decode existing old bytes and must never change. `[DeltaDateTime]`/`WriteDeltaTime` remain available for them. ## Verification - Build 0 errors / 0 warnings; **837 + 708 tests green**. - Schema regeneration produced exactly the 17 expected new `vN.json` files (all `AnchoredTime` rule args), nothing else touched. - **New acceptance tests** pin the point of the whole effort: serializing the same item at two save times **5 hours apart produces byte-identical output**, and `LastMoved`/`DecayResetTime` round-trip **exactly** at sub-minute precision (the old minutes encoding destroyed both properties). ## Notes for review - `LastMoved` grows from a 1–3 byte encoded minutes value to 8-byte ticks per item — the price of byte-stability; it repays itself in incremental-save behavior since unchanged items now produce unchanged bytes. - BaseEscortable-style semantics are unchanged: anchored shift preserves *remaining* time exactly, the same contract delta provided, so no gameplay-visible behavior changes — deadlines simply stop being consumed by downtime that delta already protected against, now with stable bytes. ## Enforcement `WriteDeltaTime` is now `[Obsolete]` (interface + implementation). With the repo's warnings-as-errors, any new delta-time write — hand-written or emitted by a still-unconverted `[DeltaDateTime]` field — fails the build, with the migration instructions in the message. That the full solution still builds with **zero warnings** is itself the proof no active delta writer survived the conversion. `ReadDeltaTime` deliberately stays un-attributed: its remaining callers decode existing old bytes and are correct forever; its XML docs now state the legacy-decode-only contract.
1538 lines
39 KiB
C#
Executable file
1538 lines
39 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.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<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;
|
|
|
|
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>();
|
|
_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);
|
|
|
|
[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();
|
|
}
|
|
}
|
|
|
|
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<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>();
|
|
_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();
|
|
}
|
|
}
|
|
}
|