diff --git a/docs/Packets/C1-F7-02-ImperialGuardianEnterResult_by-server.md b/docs/Packets/C1-F7-02-ImperialGuardianEnterResult_by-server.md
new file mode 100644
index 000000000..42c36c687
--- /dev/null
+++ b/docs/Packets/C1-F7-02-ImperialGuardianEnterResult_by-server.md
@@ -0,0 +1,48 @@
+# C1 F7 02 - ImperialGuardianEnterResult (by server)
+
+## Is sent when
+
+The player requested to enter the imperial guardian event, or entered the next zone of it.
+
+## Causes the following actions on the client side
+
+The client shows a message when entering failed. On success, it remembers the day and zone for the timer and the result, and sets the weather of the map.
+
+## Structure
+
+| Index | Length | Data Type | Value | Description |
+|-------|--------|-----------|-------|-------------|
+| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
+| 1 | 1 | Byte | 12 | Packet header - length of the packet |
+| 2 | 1 | Byte | 0xF7 | Packet header - packet type identifier |
+| 3 | 1 | Byte | 0x02 | Packet header - sub packet type identifier |
+| 4 | 1 | EnterResult | | Result; The result of the request. |
+| 5 | 1 | Byte | | Day; The day of the week, from 1 (monday) to 7 (sunday). The client shows it as the round. |
+| 6 | 1 | Byte | | Zone; The zone, starting at 1. |
+| 7 | 1 | WeatherType | | Weather; The weather of the map. |
+| 8 | 4 | IntegerLittleEndian | | RemainingMilliseconds; The remaining time in milliseconds. When the result is NotOpen, the client shows the minutes until the event can be entered. The field is aligned to 4 bytes, because the client structure is not packed. |
+
+### EnterResult Enum
+
+The result of an enter request.
+
+| Value | Name | Description |
+|-------|------|-------------|
+| 0 | Success | The player entered the event. |
+| 1 | NotOpen | The event can not be entered yet. |
+| 2 | MissingTicket | The player has no Gaion's Order or Complete Secromicon. |
+| 3 | Full | The zone is full. |
+| 4 | ZoneTimeRemaining | There is still time remaining in this zone. |
+| 5 | PartyRequired | The player can only enter as a member of a party. |
+| 6 | CharacterLevelTooLow | The character level is too low. The client doesn't show a message. |
+
+### WeatherType Enum
+
+The weather of the map.
+
+| Value | Name | Description |
+|-------|------|-------------|
+| 0 | Sun | The sun shines. |
+| 1 | Rain | It rains. |
+| 2 | Fog | There is fog. |
+| 3 | Storm | There is a storm. |
\ No newline at end of file
diff --git a/docs/Packets/C1-F7-04-ImperialGuardianTimer_by-server.md b/docs/Packets/C1-F7-04-ImperialGuardianTimer_by-server.md
new file mode 100644
index 000000000..1a0476557
--- /dev/null
+++ b/docs/Packets/C1-F7-04-ImperialGuardianTimer_by-server.md
@@ -0,0 +1,31 @@
+# C1 F7 04 - ImperialGuardianTimer (by server)
+
+## Is sent when
+
+Every second during the imperial guardian event.
+
+## Causes the following actions on the client side
+
+The client shows the timer with the remaining time and the number of remaining monsters. The client doesn't count down the time by itself.
+
+## Structure
+
+| Index | Length | Data Type | Value | Description |
+|-------|--------|-----------|-------|-------------|
+| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
+| 1 | 1 | Byte | 16 | Packet header - length of the packet |
+| 2 | 1 | Byte | 0xF7 | Packet header - packet type identifier |
+| 3 | 1 | Byte | 0x04 | Packet header - sub packet type identifier |
+| 4 | 1 | TimerType | | Type; The type of the timer. |
+| 8 | 4 | IntegerLittleEndian | | RemainingMilliseconds; The remaining time in milliseconds. The field is aligned to 4 bytes, because the client structure is not packed. |
+| 12 | 1 | Byte | | MonsterCount; The number of remaining monsters. |
+
+### TimerType Enum
+
+The type of the timer.
+
+| Value | Name | Description |
+|-------|------|-------------|
+| 0 | LootTime | The time to collect the loot. |
+| 1 | Standby | The time until the monsters appear. |
+| 2 | TimeAttack | The time to kill the monsters. |
\ No newline at end of file
diff --git a/docs/Packets/C1-F7-06-ImperialGuardianResult_by-server.md b/docs/Packets/C1-F7-06-ImperialGuardianResult_by-server.md
new file mode 100644
index 000000000..1ea222b22
--- /dev/null
+++ b/docs/Packets/C1-F7-06-ImperialGuardianResult_by-server.md
@@ -0,0 +1,30 @@
+# C1 F7 06 - ImperialGuardianResult (by server)
+
+## Is sent when
+
+A zone of the imperial guardian event has been cleared, or the event ended.
+
+## Causes the following actions on the client side
+
+The client shows the result and hides the timer.
+
+## Structure
+
+| Index | Length | Data Type | Value | Description |
+|-------|--------|-----------|-------|-------------|
+| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
+| 1 | 1 | Byte | 12 | Packet header - length of the packet |
+| 2 | 1 | Byte | 0xF7 | Packet header - packet type identifier |
+| 3 | 1 | Byte | 0x06 | Packet header - sub packet type identifier |
+| 4 | 1 | ResultType | | Result; The result. |
+| 8 | 4 | IntegerLittleEndian | | Experience; The rewarded experience, when the event has been completed. The field is aligned to 4 bytes, because the client structure is not packed. |
+
+### ResultType Enum
+
+The result of the imperial guardian event.
+
+| Value | Name | Description |
+|-------|------|-------------|
+| 0 | Failed | The event failed. |
+| 1 | ZoneCleared | The zone has been cleared. |
+| 2 | Success | The event has been completed. |
\ No newline at end of file
diff --git a/docs/Packets/ServerToClient.md b/docs/Packets/ServerToClient.md
index 433a61409..567d43037 100644
--- a/docs/Packets/ServerToClient.md
+++ b/docs/Packets/ServerToClient.md
@@ -300,4 +300,7 @@
* [C1 F6 1A - QuestStateList (by server)](C1-F6-1A-QuestStateList_by-server.md)
* [C1 F6 1B - QuestState (by server)](C1-F6-1B-QuestState_by-server.md)
* [C2 F6 1B - QuestStateExtended (by server)](C2-F6-1B-QuestStateExtended_by-server.md)
+ * [C1 F7 02 - ImperialGuardianEnterResult (by server)](C1-F7-02-ImperialGuardianEnterResult_by-server.md)
+ * [C1 F7 04 - ImperialGuardianTimer (by server)](C1-F7-04-ImperialGuardianTimer_by-server.md)
+ * [C1 F7 06 - ImperialGuardianResult (by server)](C1-F7-06-ImperialGuardianResult_by-server.md)
* [C3 F9 01 - OpenNpcDialog (by server)](C3-F9-01-OpenNpcDialog_by-server.md)
diff --git a/src/DataModel/Configuration/MiniGameType.cs b/src/DataModel/Configuration/MiniGameType.cs
index c3619b9c5..5e4dfc0cc 100644
--- a/src/DataModel/Configuration/MiniGameType.cs
+++ b/src/DataModel/Configuration/MiniGameType.cs
@@ -43,4 +43,9 @@ public enum MiniGameType
/// The Kanturu Refinery Tower event.
///
Kanturu,
+
+ ///
+ /// The imperial guardian event (fortress of the imperial guardian).
+ ///
+ ImperialGuardian,
}
\ No newline at end of file
diff --git a/src/GameLogic/AttackableExtensions.cs b/src/GameLogic/AttackableExtensions.cs
index 496af71f5..52a46437a 100644
--- a/src/GameLogic/AttackableExtensions.cs
+++ b/src/GameLogic/AttackableExtensions.cs
@@ -636,6 +636,26 @@ public static async ValueTask ApplyMaceMasteryStunEffectAsync(this Player attack
await attackable.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);
}
+ ///
+ /// Applies the stun effect to the player for the specified duration, e.g. by a skill of a monster.
+ ///
+ /// The player.
+ /// The duration of the stun.
+ /// A task representing the asynchronous operation.
+ public static async ValueTask ApplyStunEffectAsync(this Player target, TimeSpan duration)
+ {
+ if (target.Attributes is not { } attributes
+ || target.GameContext.Configuration.MagicEffects.FirstOrDefault(m => m.Number == StunnedMagicEffectNumber) is not { } effectDefinition
+ || effectDefinition.PowerUpDefinitions.FirstOrDefault(pu => pu.TargetAttribute == Stats.IsStunned) is not { } powerUpDefinition)
+ {
+ return;
+ }
+
+ var powerUp = attributes.CreateElement(powerUpDefinition);
+ var magicEffect = new MagicEffect(duration, effectDefinition, [new MagicEffect.ElementWithTarget(powerUp, Stats.IsStunned)]);
+ await target.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);
+ }
+
///
/// Ensures the skill attributes of the specified skill entry are set up.
///
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs b/src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs
new file mode 100644
index 000000000..30fdccad4
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs
@@ -0,0 +1,46 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+using MUnique.OpenMU.GameLogic.Views;
+
+///
+/// View plugin interface for the imperial guardian event.
+///
+public interface IImperialGuardianViewPlugIn : IViewPlugIn
+{
+ ///
+ /// Shows the result of an enter request, or the zone which the player entered.
+ ///
+ /// The result.
+ /// The day of the week (1 = monday, ..., 7 = sunday).
+ /// The zone, starting at 0.
+ /// The weather of the map.
+ /// The remaining time.
+ ValueTask ShowEnterResultAsync(ImperialGuardianEnterResult result, byte day, int zone, ImperialGuardianWeather weather, TimeSpan remainingTime);
+
+ ///
+ /// Shows the timer of the current zone.
+ ///
+ /// The type of the timer.
+ /// The remaining time.
+ /// The number of remaining monsters.
+ ValueTask ShowTimerAsync(ImperialGuardianTimerType type, TimeSpan remainingTime, int monsterCount);
+
+ ///
+ /// Shows the result of a zone or of the event.
+ ///
+ /// The result.
+ /// The rewarded experience.
+ ValueTask ShowResultAsync(ImperialGuardianResult result, int experience);
+
+ ///
+ /// Shows the animation of a skill of a monster.
+ ///
+ /// The monster.
+ /// The target of the skill.
+ /// The number of the skill, which the client uses for the animation.
+ ValueTask ShowMonsterSkillAsync(IAttacker monster, IAttackable target, short skillNumber);
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs
new file mode 100644
index 000000000..b9a7bc9f5
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs
@@ -0,0 +1,499 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+using System.Collections.Concurrent;
+using System.Threading;
+using MUnique.OpenMU.AttributeSystem;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.GameLogic.NPC;
+
+///
+/// The context of an imperial guardian event game.
+///
+///
+/// The event takes place on the map of the current day of the week and consists of several zones.
+/// Each zone starts with a standby time, in which the players gather at its entrance. Then its monsters
+/// appear, which have to be killed within a limited time. The gates of a zone block the way. The gate at
+/// the entrance can be attacked as soon as the monsters appear, the other gates and the statues when all
+/// monsters are killed. The event is completed when the monsters of the last zone are killed, and fails
+/// when the time of a zone is over or when all players left.
+///
+public sealed class ImperialGuardianContext : MiniGameContext
+{
+ ///
+ /// The terrain attribute, with which the gates block the way. The terrain of the game client already
+ /// contains it at the gates, so it has to be removed when a gate is destroyed.
+ ///
+ private const TerrainAttributeType GateTerrainAttribute = TerrainAttributeType.Water;
+
+ ///
+ /// The duration of the countdown between the entering phase and the start of the game.
+ ///
+ private static readonly TimeSpan CountdownDuration = TimeSpan.FromSeconds(30);
+
+ private static readonly TimeSpan TimerInterval = TimeSpan.FromSeconds(1);
+
+ private readonly IGameContext _gameContext;
+ private readonly ImperialGuardianEventDefinition _definition;
+ private readonly ImperialGuardianWeather _weather;
+
+ ///
+ /// The monsters of the current zone, which have to be killed. Gates, statues and traps aren't included.
+ ///
+ private readonly ConcurrentDictionary _zoneMonsters = new();
+
+ private readonly ConcurrentDictionary _gates = new();
+
+ private int _zone;
+ private TimerState _timer;
+ private ImperialGuardianMonsterScaling? _monsterScaling;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The key of this context.
+ /// The definition of the mini game.
+ /// The game context, to which this game belongs.
+ /// The map initializer, which is used when the event starts.
+ public ImperialGuardianContext(
+ MiniGameMapKey key,
+ MiniGameDefinition definition,
+ IGameContext gameContext,
+ IMapInitializer mapInitializer)
+ : base(key, definition, gameContext, mapInitializer)
+ {
+ this._gameContext = gameContext;
+
+ // The definition is resolved once, so that a configuration change doesn't affect a running game.
+ this._definition = ImperialGuardianFeaturePlugIn.GetEventDefinition(gameContext);
+ this.Day = definition.GameLevel;
+ this.ZoneCount = Math.Max(1, this.Map.Definition.MonsterSpawns
+ .Where(spawn => spawn.SpawnTrigger == SpawnTrigger.OnceAtWaveStart && spawn.WaveNumber / 10 == this.Day)
+ .Select(spawn => (spawn.WaveNumber % 10) + 1)
+ .DefaultIfEmpty(0)
+ .Max());
+ this._weather = (ImperialGuardianWeather)Rand.NextInt(0, 4);
+ this._timer = new TimerState(ImperialGuardianTimerType.Standby, this.EnterEndsAtUtc.Add(CountdownDuration));
+
+ _ = Task.Run(() => this.RunTimerLoopAsync(this.GameEndedToken), this.GameEndedToken);
+ }
+
+ ///
+ /// Gets the day of the week (1 = monday, ..., 7 = sunday), for which the game takes place.
+ ///
+ public byte Day { get; }
+
+ ///
+ /// Gets the number of zones of the game.
+ ///
+ public int ZoneCount { get; }
+
+ ///
+ /// Gets the current zone, starting at 0.
+ ///
+ public int Zone => Volatile.Read(ref this._zone);
+
+ ///
+ /// Gets the number of the monsters of the current zone, which are still alive.
+ ///
+ public int RemainingMonsterCount => this._zoneMonsters.Keys.Count(monster => monster.IsAlive);
+
+ ///
+ protected override async ValueTask OnGameStartAsync(ICollection players)
+ {
+ await base.OnGameStartAsync(players).ConfigureAwait(false);
+
+ var playerLevel = players
+ .Select(player => (int)((player.Attributes?[Stats.Level] ?? 0) + (player.Attributes?[Stats.MasterLevel] ?? 0)))
+ .DefaultIfEmpty(0)
+ .Max();
+ this._monsterScaling = this._definition.GetMonsterScaling(playerLevel);
+
+ _ = Task.Run(() => this.RunZonesAsync(this.GameEndedToken), this.GameEndedToken);
+ }
+
+ ///
+ protected override void OnMonsterDied(object? sender, DeathInformation e)
+ {
+ base.OnMonsterDied(sender, e);
+ if (sender is Monster monster && this.Day != ImperialGuardianEventDefinition.Sunday)
+ {
+ _ = this.DropFragmentsAsync(monster, e);
+ }
+ }
+
+ ///
+ protected override async ValueTask OnObjectAddedToMapAsync((GameMap Map, ILocateable Object) args)
+ {
+ await base.OnObjectAddedToMapAsync(args).ConfigureAwait(false);
+ if (args.Object is not Player player)
+ {
+ return;
+ }
+
+ var blockedAreas = this._gates.Keys.Where(gate => gate.IsBlocking && gate.IsAlive).Select(gate => gate.GetBlockedArea()).ToList();
+ if (blockedAreas.Count > 0)
+ {
+ await player.InvokeViewPlugInAsync(p => p.ChangeAttributesAsync(GateTerrainAttribute, true, blockedAreas)).ConfigureAwait(false);
+ }
+
+ await this.ShowZoneAsync(player).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async ValueTask GameEndedAsync(ICollection finishers)
+ {
+ await base.GameEndedAsync(finishers).ConfigureAwait(false);
+ this._gates.Clear();
+ this._zoneMonsters.Clear();
+ }
+
+ private static void Multiply(AttackableNpcBase npc, AttributeDefinition attribute, float multiplier)
+ {
+ if (Math.Abs(multiplier - 1) > 0.001f)
+ {
+ npc.Attributes.AddElement(new SimpleElement(multiplier, AggregateType.Multiplicate), attribute);
+ }
+ }
+
+ private async Task RunTimerLoopAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ await this.SpawnGatesAsync(0).ConfigureAwait(false);
+ using var timer = new PeriodicTimer(TimerInterval);
+ var isCountdownShown = false;
+ while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
+ {
+ if (!isCountdownShown && this.State == MiniGameState.Closed)
+ {
+ // The entering phase is over, e.g. because it was skipped, so the countdown until the start is shown.
+ isCountdownShown = true;
+ this.SetTimer(ImperialGuardianTimerType.Standby, CountdownDuration);
+ }
+
+ var timerState = Volatile.Read(ref this._timer);
+ var remaining = timerState.EndsAtUtc - DateTime.UtcNow;
+ var type = timerState.Type;
+ var monsterCount = this.RemainingMonsterCount;
+ await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p =>
+ p.ShowTimerAsync(type, remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero, monsterCount)).AsTask()).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // The game ended.
+ }
+ catch (Exception ex)
+ {
+ this.Logger.LogError(ex, "{context}: Unexpected error in the timer loop.", this);
+ }
+ }
+
+ private async Task RunZonesAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ for (var zone = 0; zone < this.ZoneCount; zone++)
+ {
+ if (zone > 0)
+ {
+ Volatile.Write(ref this._zone, zone);
+ await this.SpawnGatesAsync(zone).ConfigureAwait(false);
+ this.SetTimer(ImperialGuardianTimerType.Standby, this._definition.StandbyDuration);
+ await this.ForEachPlayerAsync(player => this.ShowZoneAsync(player).AsTask()).ConfigureAwait(false);
+ await this.DelayWithSkipAsync(this._definition.StandbyDuration, cancellationToken).ConfigureAwait(false);
+ }
+
+ this.SetTimer(ImperialGuardianTimerType.TimeAttack, this._definition.ZoneDuration);
+ this.SetGatesAttackable(this._definition.EarlyAttackableGateNumbers);
+ await this.SpawnMonstersAsync(zone).ConfigureAwait(false);
+
+ var isLastZone = zone == this.ZoneCount - 1;
+ if (!await this.WaitUntilZoneIsClearedAsync(isLastZone, cancellationToken).ConfigureAwait(false))
+ {
+ await this.FailAsync().ConfigureAwait(false);
+ return;
+ }
+
+ this.SetGatesAttackable(this._definition.LateAttackableGateNumbers);
+ if (isLastZone)
+ {
+ await this.CompleteAsync().ConfigureAwait(false);
+ return;
+ }
+
+ this.Logger.LogDebug("{context}: Zone {zone} cleared.", this, zone);
+ await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p =>
+ p.ShowResultAsync(ImperialGuardianResult.ZoneCleared, 0)).AsTask()).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // The game ended.
+ }
+ catch (Exception ex)
+ {
+ this.Logger.LogError(ex, "{context}: Unexpected error during the zones.", this);
+ }
+ }
+
+ ///
+ /// Waits until the monsters of the zone are killed.
+ ///
+ /// true, if the monsters were killed in time; otherwise, false.
+ private async ValueTask WaitUntilZoneIsClearedAsync(bool isLastZone, CancellationToken cancellationToken)
+ {
+ using var timer = new PeriodicTimer(TimerInterval);
+ while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
+ {
+ if (this.PlayerCount == 0)
+ {
+ return false;
+ }
+
+ var isCleared = this.RemainingMonsterCount == 0;
+ var isTimeOver = DateTime.UtcNow >= Volatile.Read(ref this._timer).EndsAtUtc;
+ if (isCleared && (isLastZone || this._definition.StartNextZoneWhenCleared || isTimeOver))
+ {
+ return true;
+ }
+
+ if (isTimeOver)
+ {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ private async ValueTask CompleteAsync()
+ {
+ this.Logger.LogInformation("{context}: The event has been completed.", this);
+ this.SetTimer(ImperialGuardianTimerType.LootTime, this.Definition.ExitDuration);
+ await this.ForEachPlayerAsync(async player =>
+ {
+ var playerLevel = (int)((player.Attributes?[Stats.Level] ?? 0) + (player.Attributes?[Stats.MasterLevel] ?? 0));
+ var experience = this._definition.GetExperienceReward(playerLevel, this.Day);
+ if (experience > 0)
+ {
+ await player.AddExperienceAsync(experience, null).ConfigureAwait(false);
+ }
+
+ await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(ImperialGuardianResult.Success, experience)).ConfigureAwait(false);
+ }).ConfigureAwait(false);
+
+ this.FinishEvent();
+ }
+
+ private async ValueTask FailAsync()
+ {
+ this.Logger.LogInformation("{context}: The event failed in zone {zone}.", this, this.Zone);
+ await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p =>
+ p.ShowResultAsync(ImperialGuardianResult.Failed, 0)).AsTask()).ConfigureAwait(false);
+ this.FinishEvent();
+ }
+
+ private void SetTimer(ImperialGuardianTimerType type, TimeSpan duration)
+ {
+ Volatile.Write(ref this._timer, new TimerState(type, DateTime.UtcNow.Add(duration)));
+ }
+
+ private ValueTask ShowZoneAsync(Player player)
+ {
+ var remaining = Volatile.Read(ref this._timer).EndsAtUtc - DateTime.UtcNow;
+ return player.InvokeViewPlugInAsync(p =>
+ p.ShowEnterResultAsync(ImperialGuardianEnterResult.Success, this.Day, this.Zone, this._weather, remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero));
+ }
+
+ private void SetGatesAttackable(ICollection monsterNumbers)
+ {
+ foreach (var gate in this._gates.Keys.Where(gate => monsterNumbers.Contains(gate.Definition.Number)))
+ {
+ gate.IsAttackable = true;
+ }
+ }
+
+ private IEnumerable GetZoneSpawns(int zone)
+ {
+ var waveNumber = ImperialGuardianEventDefinition.GetWaveNumber(this.Day, zone);
+ return this.Map.Definition.MonsterSpawns
+ .Where(spawn => spawn.MonsterDefinition is not null
+ && spawn.SpawnTrigger == SpawnTrigger.OnceAtWaveStart
+ && spawn.WaveNumber == waveNumber);
+ }
+
+ private bool IsGate(MonsterDefinition definition)
+ {
+ var number = definition.Number;
+ return this._definition.BlockingGateNumbers.Contains(number)
+ || this._definition.EarlyAttackableGateNumbers.Contains(number)
+ || this._definition.LateAttackableGateNumbers.Contains(number);
+ }
+
+ private async ValueTask SpawnGatesAsync(int zone)
+ {
+ foreach (var spawnArea in this.GetZoneSpawns(zone).Where(spawn => this.IsGate(spawn.MonsterDefinition!)))
+ {
+ for (var i = 0; i < spawnArea.Quantity; i++)
+ {
+ var isBlocking = this._definition.BlockingGateNumbers.Contains(spawnArea.MonsterDefinition!.Number);
+ var gate = new ImperialGuardianGate(spawnArea, spawnArea.MonsterDefinition, this.Map, this, this.DropGenerator, this._gameContext.PlugInManager, isBlocking);
+ this.ApplyScaling(gate);
+ if (!await this.AddNpcAsync(gate).ConfigureAwait(false))
+ {
+ continue;
+ }
+
+ this._gates.TryAdd(gate, 0);
+ gate.Died += this.OnGateDied;
+ if (isBlocking)
+ {
+ await this.ChangeTerrainAsync(gate.GetBlockedArea(), true).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ private async ValueTask SpawnMonstersAsync(int zone)
+ {
+ foreach (var spawnArea in this.GetZoneSpawns(zone).Where(spawn => !this.IsGate(spawn.MonsterDefinition!)))
+ {
+ var definition = spawnArea.MonsterDefinition!;
+ for (var i = 0; i < spawnArea.Quantity; i++)
+ {
+ if (this._definition.TrapNumbers.Contains(definition.Number))
+ {
+ // The traps are invisible, like in the original game. The game client doesn't know them.
+ var trap = new Trap(spawnArea, definition, this.Map, new RandomAttackInRangeTrapIntelligence(this.Map)) { IsInvisible = true };
+ await this.AddNpcAsync(trap).ConfigureAwait(false);
+ continue;
+ }
+
+ var monster = new Monster(spawnArea, definition, this.Map, this.DropGenerator, new ImperialGuardianMonsterIntelligence(this._definition), this._gameContext.PlugInManager, this._gameContext.PathFinderPool, this);
+ this.ApplyScaling(monster);
+ if (await this.AddNpcAsync(monster).ConfigureAwait(false))
+ {
+ this._zoneMonsters.TryAdd(monster, 0);
+ }
+ }
+ }
+
+ this.Logger.LogDebug("{context}: Zone {zone} started with {count} monsters.", this, zone, this._zoneMonsters.Count);
+ }
+
+ private async ValueTask AddNpcAsync(NonPlayerCharacter npc)
+ {
+ try
+ {
+ npc.Initialize();
+ await this.Map.AddAsync(npc).ConfigureAwait(false);
+ npc.OnSpawn();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this.Logger.LogError(ex, "{context}: Couldn't spawn {npc}.", this, npc.Definition);
+ npc.Dispose();
+ return false;
+ }
+ }
+
+ private void ApplyScaling(AttackableNpcBase npc)
+ {
+ if (this._monsterScaling is not { } scaling)
+ {
+ return;
+ }
+
+ // The multipliers have to be applied before the npc is initialized, which sets its health.
+ Multiply(npc, Stats.Level, scaling.LevelMultiplier);
+ Multiply(npc, Stats.MaximumHealth, scaling.HealthMultiplier);
+ Multiply(npc, Stats.MinimumPhysBaseDmg, scaling.DamageMultiplier);
+ Multiply(npc, Stats.MaximumPhysBaseDmg, scaling.DamageMultiplier);
+ Multiply(npc, Stats.DefenseBase, scaling.DefenseMultiplier);
+ }
+
+ private void OnGateDied(object? sender, DeathInformation e)
+ {
+ if (sender is not ImperialGuardianGate gate)
+ {
+ return;
+ }
+
+ gate.Died -= this.OnGateDied;
+ if (gate.IsBlocking)
+ {
+ _ = this.OpenGateAsync(gate);
+ }
+ }
+
+ private async Task OpenGateAsync(ImperialGuardianGate gate)
+ {
+ try
+ {
+ await this.ChangeTerrainAsync(gate.GetBlockedArea(), false).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ this.Logger.LogError(ex, "{context}: Couldn't open the gate {gate}.", this, gate);
+ }
+ }
+
+ private async ValueTask ChangeTerrainAsync((byte StartX, byte StartY, byte EndX, byte EndY) area, bool setBlocked)
+ {
+ for (var x = area.StartX; x <= area.EndX; x++)
+ {
+ for (var y = area.StartY; y <= area.EndY; y++)
+ {
+ this.Map.Terrain.ApplyTerrainAttribute(x, y, GateTerrainAttribute, setBlocked);
+ }
+ }
+
+ await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p =>
+ p.ChangeAttributesAsync(GateTerrainAttribute, setBlocked, [area])).AsTask()).ConfigureAwait(false);
+ }
+
+ private async Task DropFragmentsAsync(Monster boss, DeathInformation e)
+ {
+ try
+ {
+ await this.DropFragmentItemsAsync(boss, e).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ this.Logger.LogError(ex, "{context}: Couldn't drop the fragments of {boss}.", this, boss);
+ }
+ }
+
+ private async ValueTask DropFragmentItemsAsync(Monster boss, DeathInformation e)
+ {
+ if (this._definition.FragmentDrops.FirstOrDefault(drop => drop.MonsterNumber == boss.Definition.Number) is not { } fragmentDrop
+ || this._gameContext.Configuration.Items.FirstOrDefault(item => item is { Group: 14 } && item.Number == fragmentDrop.ItemNumber) is not { } fragment)
+ {
+ return;
+ }
+
+ var owners = this.Map.GetObject(e.KillerId) is Player killer ? killer.GetAsEnumerable() : [];
+ var count = this._definition.GetFragmentCount(Rand.NextInt(0, 100));
+ for (var i = 0; i < count; i++)
+ {
+ var item = new TemporaryItem { Definition = fragment, Durability = 1 };
+ var position = i == 0 ? boss.Position : this.Map.Terrain.GetRandomCoordinate(boss.Position, 1);
+ await this.Map.AddAsync(new DroppedItem(item, position, this.Map, null, owners)).ConfigureAwait(false);
+ }
+
+ this.Logger.LogDebug("{context}: {boss} dropped {count} {fragment}.", this, boss, count, fragment.Name);
+ }
+
+ ///
+ /// The state of the timer, which is shown to the players. It's replaced as a whole, so that its type and end always match.
+ ///
+ /// The type of the timer.
+ /// The time when the timer ends.
+ private sealed record TimerState(ImperialGuardianTimerType Type, DateTime EndsAtUtc);
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEnterResult.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEnterResult.cs
new file mode 100644
index 000000000..6318e30d0
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEnterResult.cs
@@ -0,0 +1,46 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// The result of a request to enter the imperial guardian event.
+///
+public enum ImperialGuardianEnterResult
+{
+ ///
+ /// The player entered the event.
+ ///
+ Success,
+
+ ///
+ /// The event can not be entered yet.
+ ///
+ NotOpen,
+
+ ///
+ /// The player has no Gaion's Order, or no Complete Secromicon on sunday.
+ ///
+ MissingTicket,
+
+ ///
+ /// The event is full.
+ ///
+ Full,
+
+ ///
+ /// There is still time remaining in the zone.
+ ///
+ ZoneTimeRemaining,
+
+ ///
+ /// The player can only enter as a member of a party.
+ ///
+ PartyRequired,
+
+ ///
+ /// The character level is too low.
+ ///
+ CharacterLevelTooLow,
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs
new file mode 100644
index 000000000..806107187
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs
@@ -0,0 +1,255 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// Describes the run of the imperial guardian event.
+///
+///
+/// It's configured at the , so it can be adapted in the admin panel.
+/// The monsters of a zone are defined by the monster spawns of the event map with the trigger
+/// . Their wave number is the day of the week
+/// (1 = monday, ..., 7 = sunday) multiplied by 10, plus the zone (starting at 0).
+///
+public class ImperialGuardianEventDefinition
+{
+ ///
+ /// The day of the week of sunday, on which the event takes place on its own map.
+ ///
+ public const byte Sunday = 7;
+
+ ///
+ /// Gets or sets the time between the start of a zone and the appearance of its monsters,
+ /// in which the players can gather. The first zone uses the enter duration of the mini game instead.
+ ///
+ public TimeSpan StandbyDuration { get; set; } = TimeSpan.FromSeconds(60);
+
+ ///
+ /// Gets or sets the time in which the monsters of a zone have to be killed.
+ ///
+ public TimeSpan ZoneDuration { get; set; } = TimeSpan.FromMinutes(10);
+
+ ///
+ /// Gets or sets a value indicating whether the next zone starts as soon as the monsters of a zone are killed.
+ /// Otherwise, it starts when the time of the zone is over, like in the original game.
+ ///
+ public bool StartNextZoneWhenCleared { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether the event can only be entered as member of a party.
+ ///
+ public bool IsPartyRequired { get; set; } = true;
+
+ ///
+ /// Gets or sets a fixed day of the week (1 = monday, ..., 7 = sunday) for the event, e.g. for testing.
+ /// When it's 0, the current day of the week of the server is used.
+ ///
+ public byte FixedDay { get; set; }
+
+ ///
+ /// Gets or sets the numbers of the gates, which block the way until they're destroyed.
+ ///
+ public IList BlockingGateNumbers { get; set; } = new List { 524, 525, 527, 528 };
+
+ ///
+ /// Gets or sets the numbers of the gates and statues, which can be attacked as soon as the monsters of a zone appear.
+ ///
+ public IList EarlyAttackableGateNumbers { get; set; } = new List { 525, 528 };
+
+ ///
+ /// Gets or sets the numbers of the gates and statues, which can be attacked when all monsters of a zone are killed.
+ ///
+ public IList LateAttackableGateNumbers { get; set; } = new List { 524, 526, 527 };
+
+ ///
+ /// Gets or sets the numbers of the traps.
+ ///
+ public IList TrapNumbers { get; set; } = new List { 523 };
+
+ ///
+ /// Gets or sets the secromicon fragments, which are dropped by the bosses from monday to saturday.
+ ///
+ public IList FragmentDrops { get; set; } = new List
+ {
+ new() { MonsterNumber = 508, ItemNumber = 103 },
+ new() { MonsterNumber = 509, ItemNumber = 104 },
+ new() { MonsterNumber = 510, ItemNumber = 105 },
+ new() { MonsterNumber = 511, ItemNumber = 106 },
+ new() { MonsterNumber = 507, ItemNumber = 107 },
+ new() { MonsterNumber = 506, ItemNumber = 108 },
+ };
+
+ ///
+ /// Gets or sets the chances in percent to drop one, two, three, ... fragments.
+ ///
+ public IList FragmentCountChances { get; set; } = new List { 50, 29, 21 };
+
+ ///
+ /// Gets or sets the experience which the players get when the event has been completed,
+ /// depending on their level (including the master level).
+ ///
+ public IList ExperienceRewards { get; set; } = new List
+ {
+ new() { MaximumPlayerLevel = int.MaxValue, Experience = 100_000 },
+ };
+
+ ///
+ /// Gets or sets the multiplier of the experience reward on sunday.
+ ///
+ public int SundayExperienceMultiplier { get; set; } = 2;
+
+ ///
+ /// Gets or sets the scaling of the monsters, depending on the highest level (including the master level)
+ /// of the players. When it's empty, the monsters aren't scaled.
+ ///
+ public IList MonsterScalings { get; set; } = new List();
+
+ ///
+ /// Gets or sets the chance in percent, that a monster uses one of its skills instead of a normal attack.
+ ///
+ public int SkillChance { get; set; } = 30;
+
+ ///
+ /// Gets or sets the skills of the monsters. The skill numbers are the ones which the client uses for the animations.
+ ///
+ public IList MonsterSkills { get; set; } = new List
+ {
+ // Gaion
+ ImperialGuardianMonsterSkill.Single(504, 67),
+ ImperialGuardianMonsterSkill.Area(504, 64, 3, true),
+ ImperialGuardianMonsterSkill.Area(504, 65, 5, false),
+ ImperialGuardianMonsterSkill.Area(504, 66, 5, true),
+
+ // Jerint
+ ImperialGuardianMonsterSkill.Single(505, 55, 10, 5),
+ ImperialGuardianMonsterSkill.Area(505, 61, 10, false),
+
+ // Raymond
+ ImperialGuardianMonsterSkill.Area(506, 60, 10, false, 30, 2),
+ ImperialGuardianMonsterSkill.Area(506, 52, 10, false, 30, 2),
+
+ // Ercanne
+ ImperialGuardianMonsterSkill.Area(507, 62, 6, false),
+ ImperialGuardianMonsterSkill.Area(507, 63, 3, false),
+
+ // Daesuler
+ ImperialGuardianMonsterSkill.Area(508, 57, 10, false),
+ ImperialGuardianMonsterSkill.Area(508, 58, 5, true),
+
+ // Vermont
+ ImperialGuardianMonsterSkill.Area(509, 60, 10, false, 30, 2),
+ ImperialGuardianMonsterSkill.Area(509, 61, 10, false),
+
+ // Kato
+ ImperialGuardianMonsterSkill.Area(510, 58, 5, true),
+ ImperialGuardianMonsterSkill.Area(510, 60, 10, false, 30, 2),
+
+ // Gallia
+ ImperialGuardianMonsterSkill.Area(511, 58, 5, true),
+ ImperialGuardianMonsterSkill.Area(511, 60, 10, false, 30, 2),
+
+ // Quarter Master
+ ImperialGuardianMonsterSkill.Area(512, 47, 6, false),
+ ImperialGuardianMonsterSkill.Area(512, 48, 3, false),
+
+ // Combat Instructor
+ ImperialGuardianMonsterSkill.Area(513, 47, 6, false),
+ ImperialGuardianMonsterSkill.Area(513, 49, 10, true),
+
+ // Aticle's Head
+ ImperialGuardianMonsterSkill.Area(514, 47, 6, false),
+ ImperialGuardianMonsterSkill.Single(514, 50, 10, 5),
+
+ // Dark Ghost
+ ImperialGuardianMonsterSkill.Single(515, 51),
+ ImperialGuardianMonsterSkill.Area(515, 52, 10, false, 30, 2),
+
+ // Banshee
+ ImperialGuardianMonsterSkill.Area(516, 47, 6, false),
+ ImperialGuardianMonsterSkill.Area(516, 53, 10, false),
+
+ // Head Mounter
+ ImperialGuardianMonsterSkill.Area(517, 54, 3, false),
+ ImperialGuardianMonsterSkill.Single(517, 55, 10, 5),
+ ImperialGuardianMonsterSkill.Single(517, 56, 30, 2),
+ };
+
+ ///
+ /// Gets or sets the remaining health in percent, at which the bosses get into rage and get stronger.
+ /// When it's 0, they don't get into rage.
+ ///
+ public int RageHealthPercentage { get; set; }
+
+ ///
+ /// Gets or sets the number of the skill, which the client shows when a boss gets into rage.
+ ///
+ public short RageSkillNumber { get; set; } = 59;
+
+ ///
+ /// Gets or sets the numbers of the bosses, which can get into rage.
+ ///
+ public IList RageMonsterNumbers { get; set; } = new List { 504, 505, 506, 507, 508, 509, 510, 511 };
+
+ ///
+ /// Gets or sets the additional damage and defense of a boss in rage.
+ ///
+ public int RageBonus { get; set; } = 150;
+
+ ///
+ /// Gets the wave number of the monster spawns of a zone.
+ ///
+ /// The day of the week (1 = monday, ..., 7 = sunday).
+ /// The zone, starting at 0.
+ /// The wave number.
+ public static byte GetWaveNumber(byte day, int zone) => (byte)((day * 10) + zone);
+
+ ///
+ /// Gets the experience reward for a player.
+ ///
+ /// The level of the player, including the master level.
+ /// The day of the week.
+ /// The experience.
+ public int GetExperienceReward(int playerLevel, byte day)
+ {
+ var experience = this.ExperienceRewards
+ .OrderBy(reward => reward.MaximumPlayerLevel)
+ .FirstOrDefault(reward => playerLevel <= reward.MaximumPlayerLevel)?.Experience ?? 0;
+ return day == Sunday ? experience * this.SundayExperienceMultiplier : experience;
+ }
+
+ ///
+ /// Gets the scaling of the monsters for the highest level of the players.
+ ///
+ /// The highest level of the players, including the master level.
+ /// The scaling, or null if the monsters aren't scaled.
+ public ImperialGuardianMonsterScaling? GetMonsterScaling(int playerLevel)
+ {
+ return this.MonsterScalings
+ .OrderBy(scaling => scaling.MaximumPlayerLevel)
+ .FirstOrDefault(scaling => playerLevel <= scaling.MaximumPlayerLevel)
+ ?? this.MonsterScalings.MaxBy(scaling => scaling.MaximumPlayerLevel);
+ }
+
+ ///
+ /// Gets the number of fragments which a boss drops, based on the .
+ ///
+ /// A random number from 0 to 99.
+ /// The number of fragments.
+ public int GetFragmentCount(int randomPercent)
+ {
+ var count = 0;
+ foreach (var chance in this.FragmentCountChances)
+ {
+ count++;
+ randomPercent -= chance;
+ if (randomPercent < 0)
+ {
+ return count;
+ }
+ }
+
+ return Math.Max(count, 1);
+ }
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianExperienceReward.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianExperienceReward.cs
new file mode 100644
index 000000000..0ed791805
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianExperienceReward.cs
@@ -0,0 +1,21 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// The experience reward of the imperial guardian event for players up to a level.
+///
+public class ImperialGuardianExperienceReward
+{
+ ///
+ /// Gets or sets the maximum level of the player, including the master level.
+ ///
+ public int MaximumPlayerLevel { get; set; }
+
+ ///
+ /// Gets or sets the experience.
+ ///
+ public int Experience { get; set; }
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFeaturePlugIn.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFeaturePlugIn.cs
new file mode 100644
index 000000000..e9e72bcd5
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFeaturePlugIn.cs
@@ -0,0 +1,37 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// The feature plug-in of the imperial guardian event. Its configuration describes the run of the event,
+/// e.g. the times, the experience reward and the scaling of the monsters, so that it can be adapted in the admin panel.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.ImperialGuardianFeaturePlugIn_Name), Description = nameof(PlugInResources.ImperialGuardianFeaturePlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("7E3B9C15-2D84-4F60-A1B7-5C9E0D6F2A38")]
+public class ImperialGuardianFeaturePlugIn : IFeaturePlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration
+{
+ ///
+ public ImperialGuardianEventDefinition? Configuration { get; set; }
+
+ ///
+ /// Gets the definition of the imperial guardian event of the game context.
+ ///
+ /// The game context.
+ /// The definition of the event.
+ public static ImperialGuardianEventDefinition GetEventDefinition(IGameContext gameContext)
+ {
+ return gameContext.FeaturePlugIns.GetPlugIn()?.Configuration ?? new ImperialGuardianEventDefinition();
+ }
+
+ ///
+ public object CreateDefaultConfig()
+ {
+ return new ImperialGuardianEventDefinition();
+ }
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFragmentDrop.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFragmentDrop.cs
new file mode 100644
index 000000000..a418ce57f
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFragmentDrop.cs
@@ -0,0 +1,21 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// A secromicon fragment, which is dropped by a boss of the imperial guardian event.
+///
+public class ImperialGuardianFragmentDrop
+{
+ ///
+ /// Gets or sets the number of the boss.
+ ///
+ public short MonsterNumber { get; set; }
+
+ ///
+ /// Gets or sets the number of the fragment item in the item group 14.
+ ///
+ public short ItemNumber { get; set; }
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs
new file mode 100644
index 000000000..f6d9e1bbc
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs
@@ -0,0 +1,99 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+using System.Threading;
+using MUnique.OpenMU.GameLogic.NPC;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// A gate or statue of the imperial guardian event.
+/// It can only be attacked after the event allowed it, and a gate blocks the way until it's destroyed.
+///
+public sealed class ImperialGuardianGate : AttackableNpcBase
+{
+ private int _isAttackable;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The spawn information.
+ /// The stats.
+ /// The map on which this instance will spawn.
+ /// The event state provider.
+ /// The drop generator.
+ /// The plugin manager.
+ /// If set to true, the gate blocks the way until it's destroyed.
+ public ImperialGuardianGate(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map, IEventStateProvider? eventStateProvider, IDropGenerator dropGenerator, PlugInManager plugInManager, bool isBlocking)
+ : base(spawnInfo, stats, map, eventStateProvider, dropGenerator, plugInManager)
+ {
+ this.IsBlocking = isBlocking;
+ }
+
+ ///
+ /// Gets a value indicating whether the gate blocks the way until it's destroyed.
+ ///
+ public bool IsBlocking { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the gate can be attacked.
+ ///
+ public bool IsAttackable
+ {
+ get => Volatile.Read(ref this._isAttackable) != 0;
+ set => Volatile.Write(ref this._isAttackable, value ? 1 : 0);
+ }
+
+ ///
+ /// Gets the area which the gate blocks, which is in front of it, depending on its direction.
+ ///
+ /// The blocked area.
+ public (byte StartX, byte StartY, byte EndX, byte EndY) GetBlockedArea()
+ {
+ var x = this.Position.X;
+ var y = this.Position.Y;
+
+ // The direction of the game client is the one of OpenMU minus one.
+ return ((int)this.SpawnArea.Direction - 1) switch
+ {
+ 1 => Area(x - 2, y, x + 2, y + 3),
+ 3 => Area(x - 3, y - 2, x, y + 2),
+ 5 => Area(x - 2, y - 3, x + 2, y),
+ 7 => Area(x, y - 2, x + 3, y + 2),
+ _ => Area(x - 1, y - 1, x + 1, y + 1),
+ };
+ }
+
+ ///
+ public override ValueTask ReflectDamageAsync(IAttacker reflector, uint damage)
+ {
+ // A gate doesn't attack, so it doesn't reflect.
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ public override ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage)
+ {
+ // A gate is not an organism which can be poisoned.
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ public override ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage)
+ {
+ // A gate is not an organism which can bleed.
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ protected override bool CanBeAttackedBy(IAttacker attacker) => this.IsAttackable;
+
+ private static (byte StartX, byte StartY, byte EndX, byte EndY) Area(int startX, int startY, int endX, int endY)
+ {
+ return (Clamp(startX), Clamp(startY), Clamp(endX), Clamp(endY));
+
+ static byte Clamp(int value) => (byte)Math.Clamp(value, byte.MinValue, byte.MaxValue);
+ }
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterIntelligence.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterIntelligence.cs
new file mode 100644
index 000000000..cb91d98b2
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterIntelligence.cs
@@ -0,0 +1,115 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+using System.Threading;
+using MUnique.OpenMU.AttributeSystem;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.GameLogic.NPC;
+
+///
+/// The intelligence of the monsters of the imperial guardian event.
+/// Additionally to the basic behavior, the monsters use their skills, and the bosses can get into rage.
+///
+public sealed class ImperialGuardianMonsterIntelligence : BasicMonsterIntelligence
+{
+ private readonly ImperialGuardianEventDefinition _definition;
+ private IList? _skills;
+ private int _isInRage;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The definition of the event.
+ public ImperialGuardianMonsterIntelligence(ImperialGuardianEventDefinition definition)
+ {
+ this._definition = definition;
+ }
+
+ ///
+ /// Gets a value indicating whether the monster is in rage.
+ ///
+ public bool IsInRage => Volatile.Read(ref this._isInRage) != 0;
+
+ ///
+ ///
+ /// When the monster uses one of its skills, it doesn't attack normally in the same tick.
+ ///
+ protected override async ValueTask CanAttackAsync()
+ {
+ var monster = this.Monster;
+ await this.TryGetIntoRageAsync(monster).ConfigureAwait(false);
+
+ this._skills ??= this._definition.MonsterSkills.Where(skill => skill.MonsterNumber == monster.Definition.Number).ToList();
+ if (this._skills.Count == 0
+ || this.CurrentTarget is not { } target
+ || !target.IsActive()
+ || !target.IsInRange(monster.Position, monster.Definition.AttackRange)
+ || Rand.NextInt(0, 100) >= this._definition.SkillChance)
+ {
+ return true;
+ }
+
+ var skill = this._skills[Rand.NextInt(0, this._skills.Count)];
+ await this.UseSkillAsync(monster, target, skill).ConfigureAwait(false);
+ return false;
+ }
+
+ private static ValueTask ShowSkillAsync(Monster monster, IAttackable target, short skillNumber)
+ {
+ return monster.ForEachWorldObserverAsync(p => p.ShowMonsterSkillAsync(monster, target, skillNumber), true);
+ }
+
+ private async ValueTask TryGetIntoRageAsync(Monster monster)
+ {
+ if (this._definition.RageHealthPercentage <= 0
+ || this.IsInRage
+ || !this._definition.RageMonsterNumbers.Contains(monster.Definition.Number))
+ {
+ return;
+ }
+
+ var maximumHealth = monster.Attributes[Stats.MaximumHealth];
+ if (maximumHealth <= 0 || monster.Health * 100.0 / maximumHealth > this._definition.RageHealthPercentage
+ || Interlocked.Exchange(ref this._isInRage, 1) != 0)
+ {
+ return;
+ }
+
+ foreach (var attribute in new[] { Stats.MinimumPhysBaseDmg, Stats.MaximumPhysBaseDmg, Stats.DefenseBase })
+ {
+ monster.Attributes.AddElement(new SimpleElement(this._definition.RageBonus, AggregateType.AddRaw), attribute);
+ }
+
+ // The client shows the rage as the animation of a skill, which targets the monster itself.
+ await ShowSkillAsync(monster, monster, this._definition.RageSkillNumber).ConfigureAwait(false);
+ }
+
+ private async ValueTask UseSkillAsync(Monster monster, IAttackable target, ImperialGuardianMonsterSkill skill)
+ {
+ await ShowSkillAsync(monster, target, skill.SkillNumber).ConfigureAwait(false);
+
+ IEnumerable targets = [target];
+ if (skill.Radius > 0)
+ {
+ var center = skill.IsAroundMonster ? monster.Position : target.Position;
+ targets = monster.CurrentMap.GetAttackablesInRange(center, skill.Radius)
+ .OfType()
+ .Where(player => player.IsActive() && !player.IsAtSafezone())
+ .ToList();
+ }
+
+ foreach (var hitTarget in targets)
+ {
+ await hitTarget.AttackByAsync(monster, null, false).ConfigureAwait(false);
+ if (skill.StunChance > 0
+ && hitTarget is Player { IsAlive: true } player
+ && Rand.NextInt(0, 100) < skill.StunChance)
+ {
+ await player.ApplyStunEffectAsync(skill.StunDuration).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterScaling.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterScaling.cs
new file mode 100644
index 000000000..64df7ca46
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterScaling.cs
@@ -0,0 +1,36 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// The scaling of the monsters of the imperial guardian event for players up to a level.
+///
+public class ImperialGuardianMonsterScaling
+{
+ ///
+ /// Gets or sets the maximum level of the highest player, including the master level.
+ ///
+ public int MaximumPlayerLevel { get; set; }
+
+ ///
+ /// Gets or sets the multiplier of the level of the monsters.
+ ///
+ public float LevelMultiplier { get; set; } = 1;
+
+ ///
+ /// Gets or sets the multiplier of the health of the monsters.
+ ///
+ public float HealthMultiplier { get; set; } = 1;
+
+ ///
+ /// Gets or sets the multiplier of the damage of the monsters.
+ ///
+ public float DamageMultiplier { get; set; } = 1;
+
+ ///
+ /// Gets or sets the multiplier of the defense of the monsters.
+ ///
+ public float DefenseMultiplier { get; set; } = 1;
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterSkill.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterSkill.cs
new file mode 100644
index 000000000..247eaa99c
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterSkill.cs
@@ -0,0 +1,78 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// A skill of a monster of the imperial guardian event.
+///
+public class ImperialGuardianMonsterSkill
+{
+ ///
+ /// Gets or sets the number of the monster.
+ ///
+ public short MonsterNumber { get; set; }
+
+ ///
+ /// Gets or sets the number of the skill, which the client uses to show its animation.
+ ///
+ public short SkillNumber { get; set; }
+
+ ///
+ /// Gets or sets the radius, in which the skill hits the players. When it's 0, it only hits the target.
+ ///
+ public int Radius { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the area of the skill is around the monster.
+ /// Otherwise, it's around the target.
+ ///
+ public bool IsAroundMonster { get; set; }
+
+ ///
+ /// Gets or sets the chance in percent to stun the hit players.
+ ///
+ public int StunChance { get; set; }
+
+ ///
+ /// Gets or sets the duration of the stun.
+ ///
+ public TimeSpan StunDuration { get; set; }
+
+ ///
+ /// Creates a skill, which only hits the target.
+ ///
+ /// The number of the monster.
+ /// The number of the skill.
+ /// The chance in percent to stun the target.
+ /// The duration of the stun in seconds.
+ /// The skill.
+ public static ImperialGuardianMonsterSkill Single(short monsterNumber, short skillNumber, int stunChance = 0, int stunSeconds = 0)
+ {
+ return new() { MonsterNumber = monsterNumber, SkillNumber = skillNumber, StunChance = stunChance, StunDuration = TimeSpan.FromSeconds(stunSeconds) };
+ }
+
+ ///
+ /// Creates a skill, which hits the players in an area.
+ ///
+ /// The number of the monster.
+ /// The number of the skill.
+ /// The radius of the area.
+ /// If set to true, the area is around the monster; otherwise, around the target.
+ /// The chance in percent to stun the hit players.
+ /// The duration of the stun in seconds.
+ /// The skill.
+ public static ImperialGuardianMonsterSkill Area(short monsterNumber, short skillNumber, int radius, bool isAroundMonster, int stunChance = 0, int stunSeconds = 0)
+ {
+ return new()
+ {
+ MonsterNumber = monsterNumber,
+ SkillNumber = skillNumber,
+ Radius = radius,
+ IsAroundMonster = isAroundMonster,
+ StunChance = stunChance,
+ StunDuration = TimeSpan.FromSeconds(stunSeconds),
+ };
+ }
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianResult.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianResult.cs
new file mode 100644
index 000000000..ff62aa297
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianResult.cs
@@ -0,0 +1,26 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// The result of a zone or of the imperial guardian event.
+///
+public enum ImperialGuardianResult
+{
+ ///
+ /// The event failed.
+ ///
+ Failed,
+
+ ///
+ /// The zone has been cleared.
+ ///
+ ZoneCleared,
+
+ ///
+ /// The event has been completed.
+ ///
+ Success,
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianTimerType.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianTimerType.cs
new file mode 100644
index 000000000..987273847
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianTimerType.cs
@@ -0,0 +1,26 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// The type of the timer of the imperial guardian event.
+///
+public enum ImperialGuardianTimerType
+{
+ ///
+ /// The time to collect the loot.
+ ///
+ LootTime,
+
+ ///
+ /// The time until the monsters appear.
+ ///
+ Standby,
+
+ ///
+ /// The time to kill the monsters.
+ ///
+ TimeAttack,
+}
diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianWeather.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianWeather.cs
new file mode 100644
index 000000000..8dd894007
--- /dev/null
+++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianWeather.cs
@@ -0,0 +1,31 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+
+///
+/// The weather of the map of the imperial guardian event, which is chosen randomly for each game.
+///
+public enum ImperialGuardianWeather
+{
+ ///
+ /// The sun shines.
+ ///
+ Sun,
+
+ ///
+ /// It rains.
+ ///
+ Rain,
+
+ ///
+ /// There is fog.
+ ///
+ Fog,
+
+ ///
+ /// There is a storm.
+ ///
+ Storm,
+}
diff --git a/src/GameLogic/MiniGames/MiniGameManager.cs b/src/GameLogic/MiniGames/MiniGameManager.cs
index 911bc8ea9..afc541f39 100644
--- a/src/GameLogic/MiniGames/MiniGameManager.cs
+++ b/src/GameLogic/MiniGames/MiniGameManager.cs
@@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameLogic.MiniGames;
using System.Collections.Concurrent;
using System.Diagnostics.Metrics;
using MUnique.OpenMU.GameLogic.MiniGames.Doppelganger;
+using MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
using MUnique.OpenMU.GameLogic.MiniGames.Kanturu;
using Nito.AsyncEx;
@@ -87,6 +88,9 @@ public async ValueTask GetOrCreateAsync(MiniGameDefinition mini
case MiniGameType.Kanturu:
miniGameContext = new KanturuContext(miniGameKey, miniGameDefinition, this._gameContext, this._mapInitializer);
break;
+ case MiniGameType.ImperialGuardian:
+ miniGameContext = new ImperialGuardianContext(miniGameKey, miniGameDefinition, this._gameContext, this._mapInitializer);
+ break;
default:
miniGameContext = new MiniGameContext(miniGameKey, miniGameDefinition, this._gameContext, this._mapInitializer);
break;
diff --git a/src/GameLogic/NPC/NonPlayerCharacter.cs b/src/GameLogic/NPC/NonPlayerCharacter.cs
index f73177df7..0f3c05f80 100644
--- a/src/GameLogic/NPC/NonPlayerCharacter.cs
+++ b/src/GameLogic/NPC/NonPlayerCharacter.cs
@@ -40,6 +40,12 @@ public NonPlayerCharacter(MonsterSpawnArea spawnInfo, MonsterDefinition stats, G
///
public Direction Rotation { get; set; }
+ ///
+ /// Gets or sets a value indicating whether this instance is invisible, so that it isn't shown to the players,
+ /// e.g. a trap which isn't known by the game client.
+ ///
+ public bool IsInvisible { get; set; }
+
///
public ISet Observers { get; } = new HashSet();
diff --git a/src/GameLogic/ObserverToWorldViewAdapter.cs b/src/GameLogic/ObserverToWorldViewAdapter.cs
index 291bb4d0f..af838fff1 100644
--- a/src/GameLogic/ObserverToWorldViewAdapter.cs
+++ b/src/GameLogic/ObserverToWorldViewAdapter.cs
@@ -60,7 +60,7 @@ public async ValueTask LocateableAddedAsync(ILocateable item)
{
await this._adaptee.InvokeViewPlugInAsync(p => p.NewPlayersInScopeAsync(player.GetAsEnumerable())).ConfigureAwait(false);
}
- else if (item is NonPlayerCharacter npc)
+ else if (item is NonPlayerCharacter { IsInvisible: false } npc)
{
await this._adaptee.InvokeViewPlugInAsync(p => p.NewNpcsInScopeAsync(npc.GetAsEnumerable())).ConfigureAwait(false);
}
@@ -195,7 +195,7 @@ public async ValueTask NewLocateablesInScopeAsync(IEnumerable newOb
await this._adaptee.InvokeViewPlugInAsync(p => p.NewPlayersInScopeAsync(players, false)).ConfigureAwait(false);
}
- var npcs = newItems.OfType().WhereActive();
+ var npcs = newItems.OfType().WhereActive().Where(npc => !npc.IsInvisible);
if (npcs.Any())
{
diff --git a/src/GameLogic/PlayerActions/MiniGames/EnterImperialGuardianAction.cs b/src/GameLogic/PlayerActions/MiniGames/EnterImperialGuardianAction.cs
new file mode 100644
index 000000000..c17b55fca
--- /dev/null
+++ b/src/GameLogic/PlayerActions/MiniGames/EnterImperialGuardianAction.cs
@@ -0,0 +1,125 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlayerActions.MiniGames;
+
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+using MUnique.OpenMU.GameLogic.Views.Inventory;
+
+///
+/// Action to enter the imperial guardian event.
+///
+///
+/// Each day of the week has its own , whose game level is the day
+/// (1 = monday, ..., 7 = sunday). From monday to saturday, a Gaion's Order is required to enter,
+/// on sunday a Complete Secromicon. Each player needs an own ticket, so the definitions don't define
+/// a ticket item. Instead, the ticket is checked and consumed here.
+///
+public class EnterImperialGuardianAction
+{
+ private const byte UndefinedTicketSlot = 0xFF;
+ private const byte TicketGroup = 14;
+ private const short GaionsOrderNumber = 102;
+ private const short CompleteSecromiconNumber = 109;
+
+ private readonly EnterMiniGameAction _enterAction = new();
+
+ ///
+ /// Gets the day of the week of the event (1 = monday, ..., 7 = sunday).
+ ///
+ /// The game context.
+ /// The definition of the event.
+ /// The day of the week.
+ public static byte GetDay(IGameContext gameContext, ImperialGuardianEventDefinition eventDefinition)
+ {
+ if (eventDefinition.FixedDay is >= 1 and <= ImperialGuardianEventDefinition.Sunday)
+ {
+ return eventDefinition.FixedDay;
+ }
+
+ var dayOfWeek = TimeZoneInfo.ConvertTime(DateTime.UtcNow, gameContext.ServerTimeZone).DayOfWeek;
+ return dayOfWeek == DayOfWeek.Sunday ? ImperialGuardianEventDefinition.Sunday : (byte)dayOfWeek;
+ }
+
+ ///
+ /// Tries to enter the imperial guardian event.
+ ///
+ /// The player.
+ public async ValueTask TryEnterAsync(Player player)
+ {
+ var eventDefinition = ImperialGuardianFeaturePlugIn.GetEventDefinition(player.GameContext);
+ var day = GetDay(player.GameContext, eventDefinition);
+ var definition = player.GameContext.Configuration.MiniGameDefinitions
+ .FirstOrDefault(d => d.Type == MiniGameType.ImperialGuardian && d.GameLevel == day);
+ if (definition is null)
+ {
+ await ShowResultAsync(player, ImperialGuardianEnterResult.NotOpen, day).ConfigureAwait(false);
+ return;
+ }
+
+ if (eventDefinition.IsPartyRequired && player.Party is null)
+ {
+ await ShowResultAsync(player, ImperialGuardianEnterResult.PartyRequired, day).ConfigureAwait(false);
+ return;
+ }
+
+ if (FindTicket(player, day) is not { } ticket)
+ {
+ await ShowResultAsync(player, ImperialGuardianEnterResult.MissingTicket, day).ConfigureAwait(false);
+ return;
+ }
+
+ if ((player.Attributes?[Stats.Level] ?? 0) < definition.MinimumCharacterLevel)
+ {
+ // The client doesn't show a message for this result.
+ await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ImperialGuardianLevelTooLow), definition.MinimumCharacterLevel).ConfigureAwait(false);
+ await ShowResultAsync(player, ImperialGuardianEnterResult.CharacterLevelTooLow, day).ConfigureAwait(false);
+ return;
+ }
+
+ await this._enterAction.TryEnterMiniGameAsync(player, MiniGameType.ImperialGuardian, day, UndefinedTicketSlot).ConfigureAwait(false);
+ if (player.CurrentMiniGame is ImperialGuardianContext)
+ {
+ await ConsumeTicketAsync(player, ticket).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Finds the ticket which is required to enter the event on the day.
+ ///
+ /// The player.
+ /// The day of the week.
+ /// The ticket, or null if the player has none.
+ internal static Item? FindTicket(Player player, byte day)
+ {
+ var ticketNumber = day == ImperialGuardianEventDefinition.Sunday ? CompleteSecromiconNumber : GaionsOrderNumber;
+ return player.Inventory?.Items.FirstOrDefault(item =>
+ item.Durability > 0
+ && item.Definition is { Group: TicketGroup } definition
+ && definition.Number == ticketNumber);
+ }
+
+ private static ValueTask ShowResultAsync(Player player, ImperialGuardianEnterResult result, byte day)
+ {
+ return player.InvokeViewPlugInAsync(p => p.ShowEnterResultAsync(result, day, 0, ImperialGuardianWeather.Sun, TimeSpan.Zero));
+ }
+
+ private static async ValueTask ConsumeTicketAsync(Player player, Item ticket)
+ {
+ ticket.Durability -= 1;
+ if (ticket.Durability > 0)
+ {
+ await player.InvokeViewPlugInAsync(p => p.ItemDurabilityChangedAsync(ticket, false)).ConfigureAwait(false);
+ return;
+ }
+
+ if (player.Inventory is { } inventory)
+ {
+ await inventory.RemoveItemAsync(ticket).ConfigureAwait(false);
+ }
+
+ await player.DestroyInventoryItemAsync(ticket).ConfigureAwait(false);
+ }
+}
diff --git a/src/GameLogic/PlugIns/SuspiciousScrapOfPaperStackedPlugIn.cs b/src/GameLogic/PlugIns/SuspiciousScrapOfPaperStackedPlugIn.cs
new file mode 100644
index 000000000..68ce29604
--- /dev/null
+++ b/src/GameLogic/PlugIns/SuspiciousScrapOfPaperStackedPlugIn.cs
@@ -0,0 +1,44 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlugIns;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Transforms a full stack of suspicious scraps of paper into a Gaion's Order,
+/// which is the ticket of the imperial guardian event.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.SuspiciousScrapOfPaperStackedPlugIn_Name), Description = nameof(PlugInResources.SuspiciousScrapOfPaperStackedPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("E1A74C92-8B35-4F0D-9D26-3C5B8E1F7A40")]
+public sealed class SuspiciousScrapOfPaperStackedPlugIn : IItemStackedPlugIn
+{
+ private const byte ItemGroup = 14;
+ private const short SuspiciousScrapOfPaperNumber = 101;
+ private const short GaionsOrderNumber = 102;
+
+ ///
+ public async ValueTask ItemStackedAsync(Player player, Item sourceItem, Item targetItem)
+ {
+ if (targetItem.Definition is not { Group: ItemGroup, Number: SuspiciousScrapOfPaperNumber } scrapOfPaper
+ || targetItem.Durability < scrapOfPaper.Durability)
+ {
+ return;
+ }
+
+ var gaionsOrder = player.GameContext.Configuration.Items.FirstOrDefault(item => item is { Group: ItemGroup, Number: GaionsOrderNumber });
+ if (gaionsOrder is null)
+ {
+ player.Logger.LogWarning("Gaion's Order definition not found.");
+ return;
+ }
+
+ await player.InvokeViewPlugInAsync(p => p.RemoveItemAsync(targetItem.ItemSlot)).ConfigureAwait(false);
+ targetItem.Definition = gaionsOrder;
+ targetItem.Durability = 1;
+ await player.InvokeViewPlugInAsync(p => p.ItemAppearAsync(targetItem)).ConfigureAwait(false);
+ }
+}
diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs
index 0de918dc1..117ab5a6e 100644
--- a/src/GameLogic/Properties/PlayerMessage.Designer.cs
+++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs
@@ -204,6 +204,15 @@ public static string DoppelgangerIceWalkerKilled {
}
}
+ ///
+ /// Looks up a localized string similar to You need at least level {0} to enter the Fortress of Imperial Guardian..
+ ///
+ public static string ImperialGuardianLevelTooLow {
+ get {
+ return ResourceManager.GetString("ImperialGuardianLevelTooLow", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Your {0} has been destroyed! You are falling out of the Kanturu Refinery Tower..
///
diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx
index d2b5ee7bd..fb6c43c28 100644
--- a/src/GameLogic/Properties/PlayerMessage.resx
+++ b/src/GameLogic/Properties/PlayerMessage.resx
@@ -846,4 +846,7 @@
{0} minute(s)
+
+ You need at least level {0} to enter the Fortress of Imperial Guardian.
+
diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs
index e14b119e4..d4841264f 100644
--- a/src/GameLogic/Properties/PlugInResources.Designer.cs
+++ b/src/GameLogic/Properties/PlugInResources.Designer.cs
@@ -1140,6 +1140,24 @@ public static string HigherRefineStoneConsumeHandlerPlugIn_Name {
}
}
+ ///
+ /// Looks up a localized string similar to Configures the run of the imperial guardian event, e.g. its times, the experience reward and the scaling of the monsters..
+ ///
+ public static string ImperialGuardianFeaturePlugIn_Description {
+ get {
+ return ResourceManager.GetString("ImperialGuardianFeaturePlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Imperial Guardian event.
+ ///
+ public static string ImperialGuardianFeaturePlugIn_Name {
+ get {
+ return ResourceManager.GetString("ImperialGuardianFeaturePlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Handles the chat command '/item <group> <number> <lvl?> <exc?> <sk?> <lu?> <opt?> <anc?> <ancBonuslvl?>'. Drops a specific item next to the character..
///
@@ -3149,6 +3167,24 @@ public static string SummonPartySkillPlugin_Name {
}
}
+ ///
+ /// Looks up a localized string similar to Transforms a full stack of suspicious scraps of paper into a Gaion's Order..
+ ///
+ public static string SuspiciousScrapOfPaperStackedPlugIn_Description {
+ get {
+ return ResourceManager.GetString("SuspiciousScrapOfPaperStackedPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Suspicious scrap of paper stack transformation.
+ ///
+ public static string SuspiciousScrapOfPaperStackedPlugIn_Name {
+ get {
+ return ResourceManager.GetString("SuspiciousScrapOfPaperStackedPlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Default (catch-all) handler for targeted skills.
///
diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx
index 0fa9384f7..bd216ba20 100644
--- a/src/GameLogic/Properties/PlugInResources.resx
+++ b/src/GameLogic/Properties/PlugInResources.resx
@@ -1439,4 +1439,16 @@
The event in the hatchery of raklion: when all spider eggs are destroyed, Selupan appears. After the battle, the hatchery is closed for some time.
+
+ Imperial Guardian event
+
+
+ Configures the run of the imperial guardian event, e.g. its times, the experience reward and the scaling of the monsters.
+
+
+ Suspicious scrap of paper stack transformation
+
+
+ Transforms a full stack of suspicious scraps of paper into a Gaion's Order.
+
diff --git a/src/GameServer/MessageHandler/MiniGames/ImperialGuardianEnterRequestHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/ImperialGuardianEnterRequestHandlerPlugIn.cs
new file mode 100644
index 000000000..703255a09
--- /dev/null
+++ b/src/GameServer/MessageHandler/MiniGames/ImperialGuardianEnterRequestHandlerPlugIn.cs
@@ -0,0 +1,53 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.MiniGames;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.PlayerActions.MiniGames;
+using MUnique.OpenMU.GameServer.Properties;
+using MUnique.OpenMU.Network.Packets.ClientToServer;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handler for the request to enter the imperial guardian event (0xF7 0x01), which is sent by the window of Jerint.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.ImperialGuardianEnterRequestHandlerPlugIn_Name), Description = nameof(PlugInResources.ImperialGuardianEnterRequestHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("C83F2A6D-91E4-4B57-A0D6-2E7B5F48C913")]
+[BelongsToGroup(ImperialGuardianGroupHandlerPlugIn.GroupKey)]
+internal class ImperialGuardianEnterRequestHandlerPlugIn : ISubPacketHandlerPlugIn
+{
+ private readonly EnterImperialGuardianAction _enterAction = new();
+
+ ///
+ public bool IsEncryptionExpected => false;
+
+ ///
+ public byte Key => EnterEmpireGuardianEvent.SubCode;
+
+ ///
+ public async ValueTask HandlePacketAsync(Player player, Memory packet)
+ {
+ // The client sends the request again with each click on the button, so it's ignored while the player is in a game.
+ if (packet.Length < EnterEmpireGuardianEvent.Length
+ || player.SelectedCharacter?.CharacterClass is null
+ || player.CurrentMiniGame is not null
+ || player.OpenedNpc?.Definition.NpcWindow != NpcWindow.JerintGaionEvententry)
+ {
+ return;
+ }
+
+ // The item slot of the request is always 1, so the ticket is searched in the inventory.
+ await this._enterAction.TryEnterAsync(player).ConfigureAwait(false);
+
+ if (player.CurrentMiniGame is not null)
+ {
+ // The client closes the window when it changes the map, so the player isn't at Jerint anymore.
+ player.OpenedNpc = null;
+ }
+ }
+}
diff --git a/src/GameServer/MessageHandler/MiniGames/ImperialGuardianGroupHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/ImperialGuardianGroupHandlerPlugIn.cs
new file mode 100644
index 000000000..2fde8db8f
--- /dev/null
+++ b/src/GameServer/MessageHandler/MiniGames/ImperialGuardianGroupHandlerPlugIn.cs
@@ -0,0 +1,42 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.MiniGames;
+
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using MUnique.OpenMU.GameServer.Properties;
+using MUnique.OpenMU.Network.PlugIns;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Packet handler for the packets of the imperial guardian event (0xF7), which are forwarded to the sub handlers.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.ImperialGuardianGroupHandlerPlugIn_Name), Description = nameof(PlugInResources.ImperialGuardianGroupHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("5A91D3E7-4C28-4B6F-8E03-B7D2F19C6A54")]
+internal class ImperialGuardianGroupHandlerPlugIn : GroupPacketHandlerPlugIn
+{
+ ///
+ /// The group key for the packets of the imperial guardian event.
+ ///
+ internal const byte GroupKey = (byte)PacketType.ImperialGuardianGroup;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The client version provider.
+ /// The manager.
+ /// The logger factory.
+ public ImperialGuardianGroupHandlerPlugIn(IClientVersionProvider clientVersionProvider, PlugInManager manager, ILoggerFactory loggerFactory)
+ : base(clientVersionProvider, manager, loggerFactory)
+ {
+ }
+
+ ///
+ public override bool IsEncryptionExpected => false;
+
+ ///
+ public override byte Key => GroupKey;
+}
diff --git a/src/GameServer/PacketType.cs b/src/GameServer/PacketType.cs
index f2549fb32..345bea03f 100644
--- a/src/GameServer/PacketType.cs
+++ b/src/GameServer/PacketType.cs
@@ -526,4 +526,9 @@ public enum PacketType : byte
/// A CashShopGroup packet.
///
CashShopGroup = 0xD2,
+
+ ///
+ /// A group of packets of the imperial guardian event.
+ ///
+ ImperialGuardianGroup = 0xF7,
}
\ No newline at end of file
diff --git a/src/GameServer/Properties/PlugInResources.Designer.cs b/src/GameServer/Properties/PlugInResources.Designer.cs
index d430d6618..03cf20f87 100644
--- a/src/GameServer/Properties/PlugInResources.Designer.cs
+++ b/src/GameServer/Properties/PlugInResources.Designer.cs
@@ -2814,6 +2814,60 @@ public static string HitHandlerPlugIn075_Name {
}
}
+ ///
+ /// Looks up a localized string similar to Handles the request to enter the imperial guardian event, which is sent by the window of Jerint..
+ ///
+ public static string ImperialGuardianEnterRequestHandlerPlugIn_Description {
+ get {
+ return ResourceManager.GetString("ImperialGuardianEnterRequestHandlerPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Imperial Guardian Enter Request Handler.
+ ///
+ public static string ImperialGuardianEnterRequestHandlerPlugIn_Name {
+ get {
+ return ResourceManager.GetString("ImperialGuardianEnterRequestHandlerPlugIn_Name", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Handles the packets of the imperial guardian event (0xF7)..
+ ///
+ public static string ImperialGuardianGroupHandlerPlugIn_Description {
+ get {
+ return ResourceManager.GetString("ImperialGuardianGroupHandlerPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Imperial Guardian Group Handler.
+ ///
+ public static string ImperialGuardianGroupHandlerPlugIn_Name {
+ get {
+ return ResourceManager.GetString("ImperialGuardianGroupHandlerPlugIn_Name", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Sends the enter result, the timer and the result of the imperial guardian event to the client..
+ ///
+ public static string ImperialGuardianViewPlugIn_Description {
+ get {
+ return ResourceManager.GetString("ImperialGuardianViewPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Imperial Guardian View.
+ ///
+ public static string ImperialGuardianViewPlugIn_Name {
+ get {
+ return ResourceManager.GetString("ImperialGuardianViewPlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The default implementation of the IInitializeDuelPlugIn which is forwarding everything to the game client with specific data packets..
///
diff --git a/src/GameServer/Properties/PlugInResources.resx b/src/GameServer/Properties/PlugInResources.resx
index 35db15ac1..9b16e7048 100644
--- a/src/GameServer/Properties/PlugInResources.resx
+++ b/src/GameServer/Properties/PlugInResources.resx
@@ -2501,4 +2501,22 @@
Handles the request of the client for the state of the raklion event.
+
+ Imperial Guardian Group Handler
+
+
+ Handles the packets of the imperial guardian event (0xF7).
+
+
+ Imperial Guardian Enter Request Handler
+
+
+ Handles the request to enter the imperial guardian event, which is sent by the window of Jerint.
+
+
+ Imperial Guardian View
+
+
+ Sends the enter result, the timer and the result of the imperial guardian event to the client.
+
diff --git a/src/GameServer/RemoteView/MiniGames/Extensions.cs b/src/GameServer/RemoteView/MiniGames/Extensions.cs
index e71dc2a3f..4034b28a0 100644
--- a/src/GameServer/RemoteView/MiniGames/Extensions.cs
+++ b/src/GameServer/RemoteView/MiniGames/Extensions.cs
@@ -88,6 +88,26 @@ public static DoppelgangerEnterResult.EnterResult ToDoppelgangerEnterResult(this
};
}
+ ///
+ /// Converts the enter result to the one of the imperial guardian event.
+ ///
+ /// The enter result.
+ /// The enter result of the imperial guardian event.
+ ///
+ /// The client knows no result for a character level which is too high, so this and all other results
+ /// without a counterpart are shown as .
+ ///
+ public static ImperialGuardianEnterResult.EnterResult ToImperialGuardianEnterResult(this EnterResult enterResult)
+ {
+ return enterResult switch
+ {
+ EnterResult.Success => ImperialGuardianEnterResult.EnterResult.Success,
+ EnterResult.Full => ImperialGuardianEnterResult.EnterResult.Full,
+ EnterResult.CharacterLevelTooLow => ImperialGuardianEnterResult.EnterResult.CharacterLevelTooLow,
+ _ => ImperialGuardianEnterResult.EnterResult.NotOpen,
+ };
+ }
+
///
/// Converts the to the corresponding .
///
diff --git a/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs b/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs
new file mode 100644
index 000000000..418d95b45
--- /dev/null
+++ b/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs
@@ -0,0 +1,73 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.RemoteView.MiniGames;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+using MUnique.OpenMU.GameLogic.Views;
+using MUnique.OpenMU.Network.Packets.ServerToClient;
+using MUnique.OpenMU.PlugIns;
+using ImperialGuardianEnterResult = MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian.ImperialGuardianEnterResult;
+using ImperialGuardianResult = MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian.ImperialGuardianResult;
+
+///
+/// The default implementation of the which
+/// sends the imperial guardian event packets (0xF7 0x02, 0x04 and 0x06) to the client.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.ImperialGuardianViewPlugIn_Name), Description = nameof(PlugInResources.ImperialGuardianViewPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("9B47E2C1-6D35-4A80-B9F2-4E1C8D7A3056")]
+public sealed class ImperialGuardianViewPlugIn : IImperialGuardianViewPlugIn
+{
+ private readonly RemotePlayer _player;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player.
+ public ImperialGuardianViewPlugIn(RemotePlayer player) => this._player = player;
+
+ ///
+ public async ValueTask ShowEnterResultAsync(ImperialGuardianEnterResult result, byte day, int zone, ImperialGuardianWeather weather, TimeSpan remainingTime)
+ {
+ // The values of the packet enums are the same as the ones of the game logic. The client shows the zone starting at 1.
+ await this._player.Connection.SendImperialGuardianEnterResultAsync(
+ (Network.Packets.ServerToClient.ImperialGuardianEnterResult.EnterResult)result,
+ day,
+ (byte)(zone + 1),
+ (Network.Packets.ServerToClient.ImperialGuardianEnterResult.WeatherType)weather,
+ ToMilliseconds(remainingTime)).ConfigureAwait(false);
+ }
+
+ ///
+ public async ValueTask ShowTimerAsync(ImperialGuardianTimerType type, TimeSpan remainingTime, int monsterCount)
+ {
+ await this._player.Connection.SendImperialGuardianTimerAsync(
+ (ImperialGuardianTimer.TimerType)type,
+ ToMilliseconds(remainingTime),
+ (byte)Math.Clamp(monsterCount, byte.MinValue, byte.MaxValue)).ConfigureAwait(false);
+ }
+
+ ///
+ public async ValueTask ShowResultAsync(ImperialGuardianResult result, int experience)
+ {
+ await this._player.Connection.SendImperialGuardianResultAsync(
+ (Network.Packets.ServerToClient.ImperialGuardianResult.ResultType)result,
+ (uint)Math.Max(0, experience)).ConfigureAwait(false);
+ }
+
+ ///
+ public async ValueTask ShowMonsterSkillAsync(IAttacker monster, IAttackable target, short skillNumber)
+ {
+ // The client searches the target by the id as it is, so the flag for a successful skill can't be set.
+ await this._player.Connection.SendMonsterSkillAnimationAsync((ushort)skillNumber, monster.GetId(this._player), target.GetId(this._player)).ConfigureAwait(false);
+ }
+
+ private static uint ToMilliseconds(TimeSpan time)
+ {
+ return (uint)Math.Clamp(time.TotalMilliseconds, 0, uint.MaxValue);
+ }
+}
diff --git a/src/GameServer/RemoteView/MiniGames/ShowMiniGameEnterResultViewPlugIn.cs b/src/GameServer/RemoteView/MiniGames/ShowMiniGameEnterResultViewPlugIn.cs
index 27c4fb64b..78b1dc88a 100644
--- a/src/GameServer/RemoteView/MiniGames/ShowMiniGameEnterResultViewPlugIn.cs
+++ b/src/GameServer/RemoteView/MiniGames/ShowMiniGameEnterResultViewPlugIn.cs
@@ -43,6 +43,14 @@ public async ValueTask ShowResultAsync(MiniGameType miniGameType, EnterResult en
break;
case MiniGameType.Doppelganger:
await this._player.Connection.SendDoppelgangerEnterResultAsync(enterResult.ToDoppelgangerEnterResult()).ConfigureAwait(false);
+ break;
+ case MiniGameType.ImperialGuardian:
+ if (enterResult != EnterResult.Success)
+ {
+ // On success, the game shows the zone when the player entered its map.
+ await this._player.Connection.SendImperialGuardianEnterResultAsync(enterResult.ToImperialGuardianEnterResult(), 0, 0, ImperialGuardianEnterResult.WeatherType.Sun, 0).ConfigureAwait(false);
+ }
+
break;
case MiniGameType.Kanturu:
await this._player.Connection.SendKanturuEnterResultAsync(enterResult.ToKanturuEnterResult()).ConfigureAwait(false);
diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
index c75de1161..b00d22ea7 100644
--- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
+++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
@@ -7981,5 +7981,103 @@ int WritePacket()
return packet.Header.Length;
}
+ await connection.SendAsync(WritePacket).ConfigureAwait(false);
+ }
+
+ ///
+ /// Sends a to this connection.
+ ///
+ /// The connection.
+ /// The result of the request.
+ /// The day of the week, from 1 (monday) to 7 (sunday). The client shows it as the round.
+ /// The zone, starting at 1.
+ /// The weather of the map.
+ /// The remaining time in milliseconds. When the result is NotOpen, the client shows the minutes until the event can be entered. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ /// Is sent by the server when: The player requested to enter the imperial guardian event, or entered the next zone of it.
+ /// Causes reaction on client side: The client shows a message when entering failed. On success, it remembers the day and zone for the timer and the result, and sets the weather of the map.
+ ///
+ public static async ValueTask SendImperialGuardianEnterResultAsync(this IConnection? connection, ImperialGuardianEnterResult.EnterResult @result, byte @day, byte @zone, ImperialGuardianEnterResult.WeatherType @weather, uint @remainingMilliseconds)
+ {
+ if (connection is null)
+ {
+ return;
+ }
+
+ int WritePacket()
+ {
+ var length = ImperialGuardianEnterResultRef.Length;
+ var packet = new ImperialGuardianEnterResultRef(connection.Output.GetSpan(length)[..length]);
+ packet.Result = @result;
+ packet.Day = @day;
+ packet.Zone = @zone;
+ packet.Weather = @weather;
+ packet.RemainingMilliseconds = @remainingMilliseconds;
+
+ return packet.Header.Length;
+ }
+
+ await connection.SendAsync(WritePacket).ConfigureAwait(false);
+ }
+
+ ///
+ /// Sends a to this connection.
+ ///
+ /// The connection.
+ /// The type of the timer.
+ /// The remaining time in milliseconds. The field is aligned to 4 bytes, because the client structure is not packed.
+ /// The number of remaining monsters.
+ ///
+ /// Is sent by the server when: Every second during the imperial guardian event.
+ /// Causes reaction on client side: The client shows the timer with the remaining time and the number of remaining monsters. The client doesn't count down the time by itself.
+ ///
+ public static async ValueTask SendImperialGuardianTimerAsync(this IConnection? connection, ImperialGuardianTimer.TimerType @type, uint @remainingMilliseconds, byte @monsterCount)
+ {
+ if (connection is null)
+ {
+ return;
+ }
+
+ int WritePacket()
+ {
+ var length = ImperialGuardianTimerRef.Length;
+ var packet = new ImperialGuardianTimerRef(connection.Output.GetSpan(length)[..length]);
+ packet.Type = @type;
+ packet.RemainingMilliseconds = @remainingMilliseconds;
+ packet.MonsterCount = @monsterCount;
+
+ return packet.Header.Length;
+ }
+
+ await connection.SendAsync(WritePacket).ConfigureAwait(false);
+ }
+
+ ///
+ /// Sends a to this connection.
+ ///
+ /// The connection.
+ /// The result.
+ /// The rewarded experience, when the event has been completed. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ /// Is sent by the server when: A zone of the imperial guardian event has been cleared, or the event ended.
+ /// Causes reaction on client side: The client shows the result and hides the timer.
+ ///
+ public static async ValueTask SendImperialGuardianResultAsync(this IConnection? connection, ImperialGuardianResult.ResultType @result, uint @experience)
+ {
+ if (connection is null)
+ {
+ return;
+ }
+
+ int WritePacket()
+ {
+ var length = ImperialGuardianResultRef.Length;
+ var packet = new ImperialGuardianResultRef(connection.Output.GetSpan(length)[..length]);
+ packet.Result = @result;
+ packet.Experience = @experience;
+
+ return packet.Header.Length;
+ }
+
await connection.SendAsync(WritePacket).ConfigureAwait(false);
}}
\ No newline at end of file
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
index 4eb75f484..072d0b5d3 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
+++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
@@ -37739,6 +37739,436 @@ public ushort TargetId
/// The packet as byte span.
public static implicit operator Memory(MonsterSkillAnimation packet) => packet._data;
}
+
+
+///
+/// Is sent by the server when: The player requested to enter the imperial guardian event, or entered the next zone of it.
+/// Causes reaction on client side: The client shows a message when entering failed. On success, it remembers the day and zone for the timer and the result, and sets the weather of the map.
+///
+public readonly struct ImperialGuardianEnterResult
+{
+ ///
+ /// The result of an enter request.
+ ///
+ public enum EnterResult
+ {
+ ///
+ /// The player entered the event.
+ ///
+ Success = 0,
+
+ ///
+ /// The event can not be entered yet.
+ ///
+ NotOpen = 1,
+
+ ///
+ /// The player has no Gaion's Order or Complete Secromicon.
+ ///
+ MissingTicket = 2,
+
+ ///
+ /// The zone is full.
+ ///
+ Full = 3,
+
+ ///
+ /// There is still time remaining in this zone.
+ ///
+ ZoneTimeRemaining = 4,
+
+ ///
+ /// The player can only enter as a member of a party.
+ ///
+ PartyRequired = 5,
+
+ ///
+ /// The character level is too low. The client doesn't show a message.
+ ///
+ CharacterLevelTooLow = 6,
+ }
+
+ ///
+ /// The weather of the map.
+ ///
+ public enum WeatherType
+ {
+ ///
+ /// The sun shines.
+ ///
+ Sun = 0,
+
+ ///
+ /// It rains.
+ ///
+ Rain = 1,
+
+ ///
+ /// There is fog.
+ ///
+ Fog = 2,
+
+ ///
+ /// There is a storm.
+ ///
+ Storm = 3,
+ }
+
+ private readonly Memory _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public ImperialGuardianEnterResult(Memory data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private ImperialGuardianEnterResult(Memory data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)Math.Min(data.Length, Length);
+ header.SubCode = SubCode;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xF7;
+
+ ///
+ /// Gets the operation sub-code of this data packet.
+ /// The is used as a grouping key.
+ ///
+ public static byte SubCode => 0x02;
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 12;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1HeaderWithSubCode Header => new (this._data);
+
+ ///
+ /// Gets or sets the result of the request.
+ ///
+ public ImperialGuardianEnterResult.EnterResult Result
+ {
+ get => (EnterResult)this._data.Span[4];
+ set => this._data.Span[4] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the day of the week, from 1 (monday) to 7 (sunday). The client shows it as the round.
+ ///
+ public byte Day
+ {
+ get => this._data.Span[5];
+ set => this._data.Span[5] = value;
+ }
+
+ ///
+ /// Gets or sets the zone, starting at 1.
+ ///
+ public byte Zone
+ {
+ get => this._data.Span[6];
+ set => this._data.Span[6] = value;
+ }
+
+ ///
+ /// Gets or sets the weather of the map.
+ ///
+ public ImperialGuardianEnterResult.WeatherType Weather
+ {
+ get => (WeatherType)this._data.Span[7];
+ set => this._data.Span[7] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the remaining time in milliseconds. When the result is NotOpen, the client shows the minutes until the event can be entered. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ public uint RemainingMilliseconds
+ {
+ get => ReadUInt32LittleEndian(this._data.Span[8..]);
+ set => WriteUInt32LittleEndian(this._data.Span[8..], value);
+ }
+
+ ///
+ /// Performs an implicit conversion from a Memory of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator ImperialGuardianEnterResult(Memory packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Memory of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Memory(ImperialGuardianEnterResult packet) => packet._data;
+}
+
+
+///
+/// Is sent by the server when: Every second during the imperial guardian event.
+/// Causes reaction on client side: The client shows the timer with the remaining time and the number of remaining monsters. The client doesn't count down the time by itself.
+///
+public readonly struct ImperialGuardianTimer
+{
+ ///
+ /// The type of the timer.
+ ///
+ public enum TimerType
+ {
+ ///
+ /// The time to collect the loot.
+ ///
+ LootTime = 0,
+
+ ///
+ /// The time until the monsters appear.
+ ///
+ Standby = 1,
+
+ ///
+ /// The time to kill the monsters.
+ ///
+ TimeAttack = 2,
+ }
+
+ private readonly Memory _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public ImperialGuardianTimer(Memory data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private ImperialGuardianTimer(Memory data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)Math.Min(data.Length, Length);
+ header.SubCode = SubCode;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xF7;
+
+ ///
+ /// Gets the operation sub-code of this data packet.
+ /// The is used as a grouping key.
+ ///
+ public static byte SubCode => 0x04;
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 16;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1HeaderWithSubCode Header => new (this._data);
+
+ ///
+ /// Gets or sets the type of the timer.
+ ///
+ public ImperialGuardianTimer.TimerType Type
+ {
+ get => (TimerType)this._data.Span[4];
+ set => this._data.Span[4] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the remaining time in milliseconds. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ public uint RemainingMilliseconds
+ {
+ get => ReadUInt32LittleEndian(this._data.Span[8..]);
+ set => WriteUInt32LittleEndian(this._data.Span[8..], value);
+ }
+
+ ///
+ /// Gets or sets the number of remaining monsters.
+ ///
+ public byte MonsterCount
+ {
+ get => this._data.Span[12];
+ set => this._data.Span[12] = value;
+ }
+
+ ///
+ /// Performs an implicit conversion from a Memory of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator ImperialGuardianTimer(Memory packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Memory of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Memory(ImperialGuardianTimer packet) => packet._data;
+}
+
+
+///
+/// Is sent by the server when: A zone of the imperial guardian event has been cleared, or the event ended.
+/// Causes reaction on client side: The client shows the result and hides the timer.
+///
+public readonly struct ImperialGuardianResult
+{
+ ///
+ /// The result of the imperial guardian event.
+ ///
+ public enum ResultType
+ {
+ ///
+ /// The event failed.
+ ///
+ Failed = 0,
+
+ ///
+ /// The zone has been cleared.
+ ///
+ ZoneCleared = 1,
+
+ ///
+ /// The event has been completed.
+ ///
+ Success = 2,
+ }
+
+ private readonly Memory _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public ImperialGuardianResult(Memory data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private ImperialGuardianResult(Memory data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)Math.Min(data.Length, Length);
+ header.SubCode = SubCode;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xF7;
+
+ ///
+ /// Gets the operation sub-code of this data packet.
+ /// The is used as a grouping key.
+ ///
+ public static byte SubCode => 0x06;
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 12;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1HeaderWithSubCode Header => new (this._data);
+
+ ///
+ /// Gets or sets the result.
+ ///
+ public ImperialGuardianResult.ResultType Result
+ {
+ get => (ResultType)this._data.Span[4];
+ set => this._data.Span[4] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the rewarded experience, when the event has been completed. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ public uint Experience
+ {
+ get => ReadUInt32LittleEndian(this._data.Span[8..]);
+ set => WriteUInt32LittleEndian(this._data.Span[8..], value);
+ }
+
+ ///
+ /// Performs an implicit conversion from a Memory of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator ImperialGuardianResult(Memory packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Memory of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Memory(ImperialGuardianResult packet) => packet._data;
+}
///
/// Defines the role of a guild member.
///
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
index c3361acfe..fe71c8306 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
+++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
@@ -13482,6 +13482,221 @@
+
+ C1HeaderWithSubCode
+ F7
+ 02
+ ImperialGuardianEnterResult
+ 12
+ ServerToClient
+ The player requested to enter the imperial guardian event, or entered the next zone of it.
+ The client shows a message when entering failed. On success, it remembers the day and zone for the timer and the result, and sets the weather of the map.
+
+
+ 4
+ Enum
+ EnterResult
+ Result
+ The result of the request.
+
+
+ 5
+ Byte
+ Day
+ The day of the week, from 1 (monday) to 7 (sunday). The client shows it as the round.
+
+
+ 6
+ Byte
+ Zone
+ The zone, starting at 1.
+
+
+ 7
+ Enum
+ WeatherType
+ Weather
+ The weather of the map.
+
+
+ 8
+ IntegerLittleEndian
+ RemainingMilliseconds
+ The remaining time in milliseconds. When the result is NotOpen, the client shows the minutes until the event can be entered. The field is aligned to 4 bytes, because the client structure is not packed.
+
+
+
+
+ EnterResult
+ The result of an enter request.
+
+
+ Success
+ The player entered the event.
+ 0
+
+
+ NotOpen
+ The event can not be entered yet.
+ 1
+
+
+ MissingTicket
+ The player has no Gaion's Order or Complete Secromicon.
+ 2
+
+
+ Full
+ The zone is full.
+ 3
+
+
+ ZoneTimeRemaining
+ There is still time remaining in this zone.
+ 4
+
+
+ PartyRequired
+ The player can only enter as a member of a party.
+ 5
+
+
+ CharacterLevelTooLow
+ The character level is too low. The client doesn't show a message.
+ 6
+
+
+
+
+ WeatherType
+ The weather of the map.
+
+
+ Sun
+ The sun shines.
+ 0
+
+
+ Rain
+ It rains.
+ 1
+
+
+ Fog
+ There is fog.
+ 2
+
+
+ Storm
+ There is a storm.
+ 3
+
+
+
+
+
+
+ C1HeaderWithSubCode
+ F7
+ 04
+ ImperialGuardianTimer
+ 16
+ ServerToClient
+ Every second during the imperial guardian event.
+ The client shows the timer with the remaining time and the number of remaining monsters. The client doesn't count down the time by itself.
+
+
+ 4
+ Enum
+ TimerType
+ Type
+ The type of the timer.
+
+
+ 8
+ IntegerLittleEndian
+ RemainingMilliseconds
+ The remaining time in milliseconds. The field is aligned to 4 bytes, because the client structure is not packed.
+
+
+ 12
+ Byte
+ MonsterCount
+ The number of remaining monsters.
+
+
+
+
+ TimerType
+ The type of the timer.
+
+
+ LootTime
+ The time to collect the loot.
+ 0
+
+
+ Standby
+ The time until the monsters appear.
+ 1
+
+
+ TimeAttack
+ The time to kill the monsters.
+ 2
+
+
+
+
+
+
+ C1HeaderWithSubCode
+ F7
+ 06
+ ImperialGuardianResult
+ 12
+ ServerToClient
+ A zone of the imperial guardian event has been cleared, or the event ended.
+ The client shows the result and hides the timer.
+
+
+ 4
+ Enum
+ ResultType
+ Result
+ The result.
+
+
+ 8
+ IntegerLittleEndian
+ Experience
+ The rewarded experience, when the event has been completed. The field is aligned to 4 bytes, because the client structure is not packed.
+
+
+
+
+ ResultType
+ The result of the imperial guardian event.
+
+
+ Failed
+ The event failed.
+ 0
+
+
+ ZoneCleared
+ The zone has been cleared.
+ 1
+
+
+ Success
+ The event has been completed.
+ 2
+
+
+
+
+
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
index 7756e4137..c4c44c838 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
+++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
@@ -35654,3 +35654,324 @@ public ushort TargetId
/// The packet as byte span.
public static implicit operator Span(MonsterSkillAnimationRef packet) => packet._data;
}
+
+
+///
+/// Is sent by the server when: The player requested to enter the imperial guardian event, or entered the next zone of it.
+/// Causes reaction on client side: The client shows a message when entering failed. On success, it remembers the day and zone for the timer and the result, and sets the weather of the map.
+///
+public readonly ref struct ImperialGuardianEnterResultRef
+{
+ private readonly Span _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public ImperialGuardianEnterResultRef(Span data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private ImperialGuardianEnterResultRef(Span data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)Math.Min(data.Length, Length);
+ header.SubCode = SubCode;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xF7;
+
+ ///
+ /// Gets the operation sub-code of this data packet.
+ /// The is used as a grouping key.
+ ///
+ public static byte SubCode => 0x02;
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 12;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1HeaderWithSubCodeRef Header => new (this._data);
+
+ ///
+ /// Gets or sets the result of the request.
+ ///
+ public ImperialGuardianEnterResult.EnterResult Result
+ {
+ get => (ImperialGuardianEnterResult.EnterResult)this._data[4];
+ set => this._data[4] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the day of the week, from 1 (monday) to 7 (sunday). The client shows it as the round.
+ ///
+ public byte Day
+ {
+ get => this._data[5];
+ set => this._data[5] = value;
+ }
+
+ ///
+ /// Gets or sets the zone, starting at 1.
+ ///
+ public byte Zone
+ {
+ get => this._data[6];
+ set => this._data[6] = value;
+ }
+
+ ///
+ /// Gets or sets the weather of the map.
+ ///
+ public ImperialGuardianEnterResult.WeatherType Weather
+ {
+ get => (ImperialGuardianEnterResult.WeatherType)this._data[7];
+ set => this._data[7] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the remaining time in milliseconds. When the result is NotOpen, the client shows the minutes until the event can be entered. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ public uint RemainingMilliseconds
+ {
+ get => ReadUInt32LittleEndian(this._data[8..]);
+ set => WriteUInt32LittleEndian(this._data[8..], value);
+ }
+
+ ///
+ /// Performs an implicit conversion from a Span of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator ImperialGuardianEnterResultRef(Span packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Span of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Span(ImperialGuardianEnterResultRef packet) => packet._data;
+}
+
+
+///
+/// Is sent by the server when: Every second during the imperial guardian event.
+/// Causes reaction on client side: The client shows the timer with the remaining time and the number of remaining monsters. The client doesn't count down the time by itself.
+///
+public readonly ref struct ImperialGuardianTimerRef
+{
+ private readonly Span _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public ImperialGuardianTimerRef(Span data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private ImperialGuardianTimerRef(Span data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)Math.Min(data.Length, Length);
+ header.SubCode = SubCode;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xF7;
+
+ ///
+ /// Gets the operation sub-code of this data packet.
+ /// The is used as a grouping key.
+ ///
+ public static byte SubCode => 0x04;
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 16;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1HeaderWithSubCodeRef Header => new (this._data);
+
+ ///
+ /// Gets or sets the type of the timer.
+ ///
+ public ImperialGuardianTimer.TimerType Type
+ {
+ get => (ImperialGuardianTimer.TimerType)this._data[4];
+ set => this._data[4] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the remaining time in milliseconds. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ public uint RemainingMilliseconds
+ {
+ get => ReadUInt32LittleEndian(this._data[8..]);
+ set => WriteUInt32LittleEndian(this._data[8..], value);
+ }
+
+ ///
+ /// Gets or sets the number of remaining monsters.
+ ///
+ public byte MonsterCount
+ {
+ get => this._data[12];
+ set => this._data[12] = value;
+ }
+
+ ///
+ /// Performs an implicit conversion from a Span of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator ImperialGuardianTimerRef(Span packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Span of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Span(ImperialGuardianTimerRef packet) => packet._data;
+}
+
+
+///
+/// Is sent by the server when: A zone of the imperial guardian event has been cleared, or the event ended.
+/// Causes reaction on client side: The client shows the result and hides the timer.
+///
+public readonly ref struct ImperialGuardianResultRef
+{
+ private readonly Span _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public ImperialGuardianResultRef(Span data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private ImperialGuardianResultRef(Span data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)Math.Min(data.Length, Length);
+ header.SubCode = SubCode;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xF7;
+
+ ///
+ /// Gets the operation sub-code of this data packet.
+ /// The is used as a grouping key.
+ ///
+ public static byte SubCode => 0x06;
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 12;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1HeaderWithSubCodeRef Header => new (this._data);
+
+ ///
+ /// Gets or sets the result.
+ ///
+ public ImperialGuardianResult.ResultType Result
+ {
+ get => (ImperialGuardianResult.ResultType)this._data[4];
+ set => this._data[4] = (byte)value;
+ }
+
+ ///
+ /// Gets or sets the rewarded experience, when the event has been completed. The field is aligned to 4 bytes, because the client structure is not packed.
+ ///
+ public uint Experience
+ {
+ get => ReadUInt32LittleEndian(this._data[8..]);
+ set => WriteUInt32LittleEndian(this._data[8..], value);
+ }
+
+ ///
+ /// Performs an implicit conversion from a Span of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator ImperialGuardianResultRef(Span packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Span of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Span(ImperialGuardianResultRef packet) => packet._data;
+}
diff --git a/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs
new file mode 100644
index 000000000..a9e10a7cd
--- /dev/null
+++ b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs
@@ -0,0 +1,96 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Persistence.Initialization.Updates;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Items;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Adds the imperial guardian event configuration to an existing Season 6 database.
+///
+///
+/// It adds the s, lets Jerint open the entrance window, lets the
+/// suspicious scrap of paper drop from monsters, and lets players who die inside the event maps
+/// respawn at Devias. The monster spawns of the event maps are changed, so that they're spawned
+/// by the event, zone by zone, and their directions are corrected, so that the gates are shown properly.
+///
+[PlugIn]
+[Display(Name = PlugInName, Description = PlugInDescription)]
+[Guid("4D8E2A17-B63C-4F95-A0E1-7C2B9F5D3E68")]
+public class AddImperialGuardianDataUpdatePlugIn : UpdatePlugInBase
+{
+ ///
+ /// The plug-in name.
+ ///
+ internal const string PlugInName = "Add Imperial Guardian data";
+
+ ///
+ /// The plug-in description.
+ ///
+ internal const string PlugInDescription = "This update adds the imperial guardian event configuration.";
+
+ private static readonly byte[] MapNumbers =
+ [
+ FortressOfImperialGuardian1.Number,
+ FortressOfImperialGuardian2.Number,
+ FortressOfImperialGuardian3.Number,
+ FortressOfImperialGuardian4.Number,
+ ];
+
+ ///
+ public override string Name => PlugInName;
+
+ ///
+ public override string Description => PlugInDescription;
+
+ ///
+ public override UpdateVersion Version => UpdateVersion.AddImperialGuardianData;
+
+ ///
+ public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id;
+
+ ///
+ public override bool IsMandatory => false;
+
+ ///
+ public override DateTime CreatedAt => new(2026, 09, 28, 0, 0, 0, DateTimeKind.Utc);
+
+ ///
+ protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
+ {
+ if (new EventTicketItems(context, gameConfiguration).CreateSuspiciousScrapOfPaperDropGroup() is { } scrapOfPaperDropGroup)
+ {
+ foreach (var map in gameConfiguration.Maps)
+ {
+ map.DropItemGroups.Add(scrapOfPaperDropGroup);
+ }
+ }
+
+ var devias = gameConfiguration.Maps.FirstOrDefault(map => map.Number == Devias.Number);
+ foreach (var map in gameConfiguration.Maps.Where(map => MapNumbers.Contains((byte)map.Number)))
+ {
+ map.SafezoneMap = devias;
+ foreach (var spawn in ImperialGuardianSpawns.GetSpawns((byte)map.Number))
+ {
+ var id = GuidHelper.CreateGuid(map.Number, spawn.Number);
+ if (map.MonsterSpawns.FirstOrDefault(area => area.GetId() == id) is { } spawnArea)
+ {
+ spawnArea.SpawnTrigger = SpawnTrigger.OnceAtWaveStart;
+ spawnArea.WaveNumber = spawn.WaveNumber;
+ spawnArea.Direction = spawn.Direction;
+ }
+ }
+ }
+
+ var initializer = new ImperialGuardianInitializer(context, gameConfiguration);
+ initializer.CreateMiniGameDefinitions();
+ initializer.ConfigureJerint();
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs
index f9c07a410..838ccb0c2 100644
--- a/src/Persistence/Initialization/Updates/UpdateVersion.cs
+++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs
@@ -609,4 +609,9 @@ public enum UpdateVersion
/// The version of the .
///
AddSelupanFallSkill = 120,
+
+ ///
+ /// The version of the .
+ ///
+ AddImperialGuardianData = 121,
}
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianDay.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianDay.cs
new file mode 100644
index 000000000..e57243343
--- /dev/null
+++ b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianDay.cs
@@ -0,0 +1,46 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
+
+///
+/// The day of the week of the imperial guardian event, as it's used by the event and the game client.
+///
+internal enum ImperialGuardianDay : byte
+{
+ ///
+ /// Monday.
+ ///
+ Monday = 1,
+
+ ///
+ /// Tuesday.
+ ///
+ Tuesday = 2,
+
+ ///
+ /// Wednesday.
+ ///
+ Wednesday = 3,
+
+ ///
+ /// Thursday.
+ ///
+ Thursday = 4,
+
+ ///
+ /// Friday.
+ ///
+ Friday = 5,
+
+ ///
+ /// Saturday.
+ ///
+ Saturday = 6,
+
+ ///
+ /// Sunday.
+ ///
+ Sunday = 7,
+}
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianInitializer.cs
new file mode 100644
index 000000000..5032784fd
--- /dev/null
+++ b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianInitializer.cs
@@ -0,0 +1,103 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
+
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
+
+///
+/// The initializer for the imperial guardian event.
+///
+///
+/// Each day of the week has its own , whose game level is the day
+/// (1 = monday, ..., 7 = sunday). From monday to saturday, the event takes place on one of the first
+/// three maps, on sunday on the fourth one. The event is entered by talking to Jerint in Devias.
+///
+internal class ImperialGuardianInitializer : InitializerBase
+{
+ ///
+ /// The number of the NPC Jerint, at which the event is entered.
+ ///
+ internal const short JerintNumber = 522;
+
+ ///
+ /// The days of the event with their map and the coordinates of the entrance of the first zone.
+ ///
+ private static readonly (ImperialGuardianDay Day, byte MapNumber, byte EntranceX, byte EntranceY)[] Days =
+ [
+ (ImperialGuardianDay.Monday, FortressOfImperialGuardian1.Number, 231, 15),
+ (ImperialGuardianDay.Tuesday, FortressOfImperialGuardian2.Number, 86, 63),
+ (ImperialGuardianDay.Wednesday, FortressOfImperialGuardian3.Number, 154, 187),
+ (ImperialGuardianDay.Thursday, FortressOfImperialGuardian1.Number, 231, 15),
+ (ImperialGuardianDay.Friday, FortressOfImperialGuardian2.Number, 86, 63),
+ (ImperialGuardianDay.Saturday, FortressOfImperialGuardian3.Number, 154, 187),
+ (ImperialGuardianDay.Sunday, FortressOfImperialGuardian4.Number, 93, 66),
+ ];
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The context.
+ /// The game configuration.
+ public ImperialGuardianInitializer(IContext context, GameConfiguration gameConfiguration)
+ : base(context, gameConfiguration)
+ {
+ }
+
+ ///
+ public override void Initialize()
+ {
+ this.CreateMiniGameDefinitions();
+ this.ConfigureJerint();
+ }
+
+ ///
+ /// Creates the mini game definitions of the days, which don't exist yet.
+ ///
+ internal void CreateMiniGameDefinitions()
+ {
+ foreach (var (day, mapNumber, entranceX, entranceY) in Days)
+ {
+ var id = GuidHelper.CreateGuid((short)MiniGameType.ImperialGuardian, (short)day);
+ if (this.GameConfiguration.MiniGameDefinitions.Any(definition => definition.GetId() == id))
+ {
+ continue;
+ }
+
+ var map = this.GameConfiguration.Maps.First(m => m.Number == mapNumber);
+ var definition = this.Context.CreateNew();
+ definition.SetGuid((short)MiniGameType.ImperialGuardian, (short)day);
+ this.GameConfiguration.MiniGameDefinitions.Add(definition);
+ definition.Name = $"Imperial Guardian {day}";
+ definition.Description = $"Event definition for the imperial guardian event on {day}, which takes place on map {mapNumber}.";
+ definition.Type = MiniGameType.ImperialGuardian;
+ definition.GameLevel = (byte)day;
+ definition.EnterDuration = TimeSpan.FromSeconds(60);
+ definition.GameDuration = TimeSpan.FromMinutes(50);
+ definition.ExitDuration = TimeSpan.FromMinutes(1);
+ definition.MaximumPlayerCount = 5;
+ definition.MinimumCharacterLevel = 150;
+ definition.MaximumCharacterLevel = 400;
+ definition.MinimumSpecialCharacterLevel = 150;
+ definition.MaximumSpecialCharacterLevel = 400;
+ definition.Entrance = map.ExitGates.First(gate => gate.X1 == entranceX && gate.Y1 == entranceY);
+ definition.TicketItem = null;
+ definition.MapCreationPolicy = MiniGameMapCreationPolicy.OnePerParty;
+ definition.SaveRankingStatistics = false;
+ definition.AllowParty = true;
+ }
+ }
+
+ ///
+ /// Lets Jerint open the entrance window of the event.
+ ///
+ internal void ConfigureJerint()
+ {
+ if (this.GameConfiguration.Monsters.FirstOrDefault(monster => monster.Number == JerintNumber) is { } jerint)
+ {
+ jerint.NpcWindow = NpcWindow.JerintGaionEvententry;
+ }
+ }
+}
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawn.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawn.cs
new file mode 100644
index 000000000..d190445ed
--- /dev/null
+++ b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawn.cs
@@ -0,0 +1,26 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
+
+using MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+using MUnique.OpenMU.Pathfinding;
+
+///
+/// A monster spawn of the imperial guardian event.
+///
+/// The number of the spawn within its map.
+/// The number of the monster.
+/// The x coordinate.
+/// The y coordinate.
+/// The direction.
+/// The zone, starting at 0.
+/// The day of the week.
+internal readonly record struct ImperialGuardianSpawn(short Number, short MonsterNumber, byte X, byte Y, Direction Direction, byte Zone, ImperialGuardianDay Day)
+{
+ ///
+ /// Gets the wave number of the spawn, which is used by the event to spawn the monsters of a zone.
+ ///
+ public byte WaveNumber => ImperialGuardianEventDefinition.GetWaveNumber((byte)this.Day, this.Zone);
+}
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs
new file mode 100644
index 000000000..129a2b120
--- /dev/null
+++ b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs
@@ -0,0 +1,467 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
+
+using MUnique.OpenMU.Pathfinding;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
+
+///
+/// The monster spawns of the imperial guardian event, for each map, zone and day of the week.
+///
+///
+/// The directions are the ones of the game client plus one, like the of OpenMU.
+/// The spawns of a map are defined twice, once for each of its days, because the boss of the last zone differs.
+///
+internal static class ImperialGuardianSpawns
+{
+ private static readonly ImperialGuardianSpawn[] Map69Spawns =
+ [
+ new(100, 520, 225, 40, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(101, 520, 232, 40, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(102, 520, 239, 40, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(103, 519, 225, 48, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(104, 519, 232, 48, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(105, 519, 239, 48, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(106, 512, 232, 45, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(107, 518, 187, 21, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(108, 518, 185, 26, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(109, 518, 187, 31, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(110, 519, 179, 19, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(111, 519, 177, 26, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(112, 519, 179, 33, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(113, 513, 173, 26, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(114, 521, 174, 89, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(115, 521, 186, 90, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(116, 521, 180, 91, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(117, 508, 180, 95, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(118, 519, 174, 95, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(119, 519, 178, 95, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(120, 519, 186, 95, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(121, 525, 234, 28, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(122, 525, 216, 80, (Direction)4, 0, ImperialGuardianDay.Monday),
+ new(123, 524, 233, 55, (Direction)2, 0, ImperialGuardianDay.Monday),
+ new(124, 526, 217, 72, (Direction)4, 0, ImperialGuardianDay.Monday),
+ new(125, 526, 218, 85, (Direction)4, 0, ImperialGuardianDay.Monday),
+ new(126, 525, 194, 25, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(127, 525, 154, 53, (Direction)2, 1, ImperialGuardianDay.Monday),
+ new(128, 524, 166, 26, (Direction)4, 1, ImperialGuardianDay.Monday),
+ new(129, 526, 152, 30, (Direction)2, 1, ImperialGuardianDay.Monday),
+ new(130, 526, 157, 30, (Direction)2, 1, ImperialGuardianDay.Monday),
+ new(131, 525, 180, 79, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(132, 526, 174, 99, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(133, 526, 186, 100, (Direction)2, 2, ImperialGuardianDay.Monday),
+ new(200, 520, 225, 40, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(201, 520, 232, 40, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(202, 520, 239, 40, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(203, 519, 225, 48, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(204, 519, 232, 48, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(205, 519, 239, 48, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(206, 512, 232, 45, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(207, 518, 187, 21, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(208, 518, 185, 26, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(209, 518, 187, 31, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(210, 519, 179, 19, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(211, 519, 177, 26, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(212, 519, 179, 33, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(213, 513, 173, 26, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(214, 521, 174, 89, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(215, 521, 186, 90, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(216, 521, 180, 91, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(217, 511, 180, 95, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(218, 519, 174, 95, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(219, 519, 178, 95, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(220, 519, 186, 95, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(221, 525, 234, 28, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(222, 525, 216, 80, (Direction)4, 0, ImperialGuardianDay.Thursday),
+ new(223, 524, 233, 55, (Direction)2, 0, ImperialGuardianDay.Thursday),
+ new(224, 526, 217, 72, (Direction)4, 0, ImperialGuardianDay.Thursday),
+ new(225, 526, 218, 85, (Direction)4, 0, ImperialGuardianDay.Thursday),
+ new(226, 525, 194, 25, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(227, 525, 154, 53, (Direction)2, 1, ImperialGuardianDay.Thursday),
+ new(228, 524, 166, 26, (Direction)4, 1, ImperialGuardianDay.Thursday),
+ new(229, 526, 152, 30, (Direction)2, 1, ImperialGuardianDay.Thursday),
+ new(230, 526, 157, 30, (Direction)2, 1, ImperialGuardianDay.Thursday),
+ new(231, 525, 180, 79, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(232, 526, 174, 99, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(233, 526, 186, 100, (Direction)2, 2, ImperialGuardianDay.Thursday),
+ new(300, 523, 154, 48, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(301, 523, 154, 44, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(302, 523, 154, 40, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(303, 523, 154, 36, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(304, 523, 154, 32, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(305, 523, 154, 28, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(306, 523, 154, 24, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(307, 523, 157, 26, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(308, 523, 161, 26, Direction.Undefined, 1, ImperialGuardianDay.Monday),
+ new(309, 523, 233, 60, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(310, 523, 233, 64, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(311, 523, 233, 68, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(312, 523, 230, 66, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(313, 523, 226, 66, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(314, 523, 222, 66, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(315, 523, 218, 66, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(316, 523, 220, 70, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(317, 523, 220, 74, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(318, 523, 220, 78, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(319, 523, 220, 82, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(320, 523, 220, 86, Direction.Undefined, 0, ImperialGuardianDay.Monday),
+ new(321, 523, 154, 48, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(322, 523, 154, 44, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(323, 523, 154, 40, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(324, 523, 154, 36, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(325, 523, 154, 32, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(326, 523, 154, 28, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(327, 523, 154, 24, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(328, 523, 157, 26, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(329, 523, 161, 26, Direction.Undefined, 1, ImperialGuardianDay.Thursday),
+ new(330, 523, 233, 60, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(331, 523, 233, 64, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(332, 523, 233, 68, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(333, 523, 230, 66, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(334, 523, 226, 66, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(335, 523, 222, 66, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(336, 523, 218, 66, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(337, 523, 220, 70, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(338, 523, 220, 74, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(339, 523, 220, 78, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(340, 523, 220, 82, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ new(341, 523, 220, 86, Direction.Undefined, 0, ImperialGuardianDay.Thursday),
+ ];
+
+ private static readonly ImperialGuardianSpawn[] Map70Spawns =
+ [
+ new(100, 520, 66, 59, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(101, 520, 64, 65, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(102, 520, 66, 70, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(103, 519, 56, 58, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(104, 519, 56, 65, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(105, 519, 56, 71, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(106, 514, 59, 65, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(107, 518, 32, 99, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(108, 518, 42, 99, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(109, 518, 38, 101, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(110, 519, 33, 107, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(111, 519, 44, 107, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(112, 519, 39, 110, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(113, 515, 38, 106, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(114, 521, 98, 107, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(115, 521, 96, 112, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(116, 521, 98, 117, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(117, 519, 93, 105, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(118, 519, 92, 112, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(119, 519, 93, 118, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(120, 509, 89, 111, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(121, 525, 75, 67, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(122, 525, 19, 65, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(123, 524, 50, 65, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(124, 526, 24, 67, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(125, 526, 24, 62, (Direction)4, 0, ImperialGuardianDay.Tuesday),
+ new(126, 525, 37, 93, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(127, 525, 55, 154, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(128, 524, 41, 117, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(129, 526, 53, 152, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(130, 526, 56, 152, (Direction)2, 1, ImperialGuardianDay.Tuesday),
+ new(131, 525, 107, 112, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(132, 526, 85, 113, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(133, 526, 85, 107, (Direction)4, 2, ImperialGuardianDay.Tuesday),
+ new(134, 520, 66, 59, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(135, 520, 64, 65, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(136, 520, 66, 70, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(137, 519, 56, 58, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(138, 519, 56, 65, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(139, 519, 56, 71, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(140, 514, 59, 65, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(141, 518, 32, 99, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(142, 518, 42, 99, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(143, 518, 38, 101, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(144, 519, 33, 107, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(145, 519, 44, 107, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(146, 519, 39, 110, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(147, 515, 38, 106, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(148, 521, 98, 107, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(149, 521, 96, 112, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(150, 521, 98, 117, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(151, 519, 93, 105, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(152, 519, 92, 112, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(153, 519, 93, 118, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(154, 507, 89, 111, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(155, 525, 75, 67, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(156, 525, 19, 65, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(157, 524, 50, 65, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(158, 526, 24, 67, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(159, 526, 24, 62, (Direction)4, 0, ImperialGuardianDay.Friday),
+ new(160, 525, 37, 93, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(161, 525, 55, 154, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(162, 524, 41, 117, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(163, 526, 53, 152, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(164, 526, 56, 152, (Direction)2, 1, ImperialGuardianDay.Friday),
+ new(165, 525, 107, 112, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(166, 526, 85, 113, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(167, 526, 85, 107, (Direction)4, 2, ImperialGuardianDay.Friday),
+ new(200, 523, 45, 67, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(201, 523, 42, 62, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(202, 523, 39, 67, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(203, 523, 35, 62, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(204, 523, 32, 67, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(205, 523, 29, 62, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(206, 523, 26, 67, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(207, 523, 24, 62, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(208, 523, 22, 66, Direction.Undefined, 0, ImperialGuardianDay.Tuesday),
+ new(209, 523, 41, 122, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(210, 523, 41, 126, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(211, 523, 41, 130, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(212, 523, 45, 128, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(213, 523, 50, 128, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(214, 523, 55, 129, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(215, 523, 55, 134, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(216, 523, 55, 139, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(217, 523, 55, 144, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(218, 523, 55, 149, Direction.Undefined, 1, ImperialGuardianDay.Tuesday),
+ new(219, 523, 45, 67, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(220, 523, 42, 62, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(221, 523, 39, 67, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(222, 523, 35, 62, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(223, 523, 32, 67, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(224, 523, 29, 62, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(225, 523, 26, 67, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(226, 523, 24, 62, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(227, 523, 22, 66, Direction.Undefined, 0, ImperialGuardianDay.Friday),
+ new(228, 523, 41, 122, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(229, 523, 41, 126, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(230, 523, 41, 130, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(231, 523, 45, 128, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(232, 523, 50, 128, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(233, 523, 55, 129, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(234, 523, 55, 134, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(235, 523, 55, 139, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(236, 523, 55, 144, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ new(237, 523, 55, 149, Direction.Undefined, 1, ImperialGuardianDay.Friday),
+ ];
+
+ private static readonly ImperialGuardianSpawn[] Map71Spawns =
+ [
+ new(100, 520, 132, 185, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(101, 520, 131, 192, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(102, 520, 132, 199, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(103, 519, 126, 185, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(104, 519, 124, 192, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(105, 519, 126, 199, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(106, 516, 127, 192, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(107, 518, 217, 140, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(108, 518, 228, 140, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(109, 518, 222, 142, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(110, 519, 218, 148, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(111, 519, 222, 152, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(112, 519, 227, 148, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(113, 517, 222, 147, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(114, 521, 162, 226, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(115, 521, 166, 228, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(116, 521, 171, 226, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(117, 519, 161, 231, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(118, 519, 164, 233, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(119, 519, 171, 233, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(120, 510, 166, 233, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(121, 525, 146, 191, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(122, 525, 89, 195, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(123, 524, 119, 192, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(124, 526, 111, 197, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(125, 526, 111, 192, (Direction)4, 0, ImperialGuardianDay.Wednesday),
+ new(126, 525, 222, 134, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(127, 525, 223, 193, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(128, 524, 222, 160, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(129, 526, 220, 173, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(130, 526, 224, 173, (Direction)2, 1, ImperialGuardianDay.Wednesday),
+ new(131, 525, 167, 217, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(132, 526, 158, 236, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(133, 526, 175, 238, (Direction)2, 2, ImperialGuardianDay.Wednesday),
+ new(134, 520, 132, 185, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(135, 520, 131, 192, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(136, 520, 132, 199, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(137, 519, 126, 185, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(138, 519, 124, 192, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(139, 519, 126, 199, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(140, 516, 127, 192, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(141, 518, 217, 140, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(142, 518, 228, 140, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(143, 518, 222, 142, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(144, 519, 218, 148, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(145, 519, 222, 152, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(146, 519, 227, 148, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(147, 517, 222, 147, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(148, 521, 162, 226, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(149, 521, 166, 228, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(150, 521, 171, 226, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(151, 519, 161, 231, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(152, 519, 164, 233, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(153, 519, 171, 233, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(154, 506, 166, 233, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(155, 525, 146, 191, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(156, 525, 89, 195, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(157, 524, 119, 192, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(158, 526, 111, 197, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(159, 526, 111, 192, (Direction)4, 0, ImperialGuardianDay.Saturday),
+ new(160, 525, 222, 134, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(161, 525, 223, 193, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(162, 524, 222, 160, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(163, 526, 220, 173, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(164, 526, 224, 173, (Direction)2, 1, ImperialGuardianDay.Saturday),
+ new(165, 525, 167, 217, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(166, 526, 158, 236, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(167, 526, 175, 238, (Direction)2, 2, ImperialGuardianDay.Saturday),
+ new(200, 523, 113, 193, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(201, 523, 112, 198, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(202, 523, 112, 203, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(203, 523, 107, 203, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(204, 523, 102, 203, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(205, 523, 97, 203, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(206, 523, 93, 203, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(207, 523, 93, 198, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(208, 523, 93, 192, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(209, 523, 93, 187, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(210, 523, 97, 186, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(211, 523, 102, 186, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(212, 523, 107, 186, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(213, 523, 112, 187, Direction.Undefined, 0, ImperialGuardianDay.Wednesday),
+ new(214, 523, 222, 165, Direction.Undefined, 1, ImperialGuardianDay.Wednesday),
+ new(215, 523, 222, 170, Direction.Undefined, 1, ImperialGuardianDay.Wednesday),
+ new(216, 523, 222, 175, Direction.Undefined, 1, ImperialGuardianDay.Wednesday),
+ new(217, 523, 222, 180, Direction.Undefined, 1, ImperialGuardianDay.Wednesday),
+ new(218, 523, 222, 185, Direction.Undefined, 1, ImperialGuardianDay.Wednesday),
+ new(219, 523, 222, 190, Direction.Undefined, 1, ImperialGuardianDay.Wednesday),
+ new(220, 523, 113, 193, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(221, 523, 112, 198, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(222, 523, 112, 203, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(223, 523, 107, 203, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(224, 523, 102, 203, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(225, 523, 97, 203, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(226, 523, 93, 203, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(227, 523, 93, 198, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(228, 523, 93, 192, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(229, 523, 93, 187, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(230, 523, 97, 186, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(231, 523, 102, 186, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(232, 523, 107, 186, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(233, 523, 112, 187, Direction.Undefined, 0, ImperialGuardianDay.Saturday),
+ new(234, 523, 222, 165, Direction.Undefined, 1, ImperialGuardianDay.Saturday),
+ new(235, 523, 222, 170, Direction.Undefined, 1, ImperialGuardianDay.Saturday),
+ new(236, 523, 222, 175, Direction.Undefined, 1, ImperialGuardianDay.Saturday),
+ new(237, 523, 222, 180, Direction.Undefined, 1, ImperialGuardianDay.Saturday),
+ new(238, 523, 222, 185, Direction.Undefined, 1, ImperialGuardianDay.Saturday),
+ new(239, 523, 222, 190, Direction.Undefined, 1, ImperialGuardianDay.Saturday),
+ ];
+
+ private static readonly ImperialGuardianSpawn[] Map72Spawns =
+ [
+ new(100, 518, 64, 65, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(101, 518, 64, 69, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(102, 518, 64, 73, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(103, 519, 61, 64, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(104, 519, 61, 69, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(105, 519, 61, 74, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(106, 521, 55, 71, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(107, 521, 55, 66, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(108, 521, 54, 68, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(109, 507, 57, 69, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(110, 518, 29, 186, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(111, 518, 34, 187, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(112, 518, 39, 186, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(113, 519, 34, 194, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(114, 519, 29, 192, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(115, 519, 39, 192, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(116, 521, 26, 191, (Direction)3, 1, ImperialGuardianDay.Sunday),
+ new(117, 521, 32, 195, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(118, 521, 39, 196, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(119, 506, 36, 191, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(120, 518, 173, 129, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(121, 518, 170, 133, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(122, 518, 173, 137, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(123, 519, 181, 129, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(124, 519, 181, 133, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(125, 519, 181, 137, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(126, 521, 184, 129, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(127, 521, 184, 133, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(128, 521, 184, 137, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(129, 505, 177, 133, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(130, 521, 183, 29, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(131, 521, 183, 21, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(132, 521, 187, 32, (Direction)2, 3, ImperialGuardianDay.Sunday),
+ new(133, 521, 187, 20, (Direction)6, 3, ImperialGuardianDay.Sunday),
+ new(134, 518, 192, 31, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(135, 518, 192, 21, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(136, 518, 194, 26, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(137, 519, 189, 21, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(138, 519, 189, 26, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(139, 519, 189, 31, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(140, 504, 183, 24, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(141, 528, 81, 69, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(142, 528, 32, 90, (Direction)2, 0, ImperialGuardianDay.Sunday),
+ new(143, 527, 50, 69, (Direction)4, 0, ImperialGuardianDay.Sunday),
+ new(144, 526, 52, 77, (Direction)3, 0, ImperialGuardianDay.Sunday),
+ new(145, 526, 53, 61, (Direction)5, 0, ImperialGuardianDay.Sunday),
+ new(146, 528, 34, 176, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(147, 528, 69, 166, (Direction)6, 1, ImperialGuardianDay.Sunday),
+ new(148, 527, 52, 191, (Direction)8, 1, ImperialGuardianDay.Sunday),
+ new(149, 526, 21, 198, (Direction)3, 1, ImperialGuardianDay.Sunday),
+ new(150, 526, 47, 199, (Direction)2, 1, ImperialGuardianDay.Sunday),
+ new(151, 528, 156, 132, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(152, 528, 224, 159, (Direction)2, 2, ImperialGuardianDay.Sunday),
+ new(153, 527, 197, 132, (Direction)8, 2, ImperialGuardianDay.Sunday),
+ new(154, 526, 161, 139, (Direction)2, 2, ImperialGuardianDay.Sunday),
+ new(155, 526, 161, 127, (Direction)6, 2, ImperialGuardianDay.Sunday),
+ new(156, 528, 214, 24, (Direction)4, 3, ImperialGuardianDay.Sunday),
+ new(157, 526, 207, 32, (Direction)2, 3, ImperialGuardianDay.Sunday),
+ new(158, 526, 207, 20, (Direction)6, 3, ImperialGuardianDay.Sunday),
+ new(200, 523, 45, 67, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(201, 523, 45, 74, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(202, 523, 41, 74, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(203, 523, 41, 67, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(204, 523, 37, 67, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(205, 523, 37, 74, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(206, 523, 33, 67, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(207, 523, 33, 74, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(208, 523, 29, 67, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(209, 523, 29, 74, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(210, 523, 31, 78, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(211, 523, 31, 83, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(212, 523, 31, 87, Direction.Undefined, 0, ImperialGuardianDay.Sunday),
+ new(213, 523, 57, 191, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(214, 523, 61, 191, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(215, 523, 65, 191, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(216, 523, 69, 191, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(217, 523, 68, 186, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(218, 523, 68, 181, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(219, 523, 68, 176, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(220, 523, 68, 171, Direction.Undefined, 1, ImperialGuardianDay.Sunday),
+ new(221, 523, 202, 133, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(222, 523, 206, 133, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(223, 523, 210, 133, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(224, 523, 214, 133, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(225, 523, 218, 133, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(226, 523, 222, 133, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(227, 523, 226, 133, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(228, 523, 224, 137, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(229, 523, 224, 142, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(230, 523, 224, 147, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(231, 523, 224, 152, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ new(232, 523, 224, 155, Direction.Undefined, 2, ImperialGuardianDay.Sunday),
+ ];
+
+ ///
+ /// Gets the spawns of the map.
+ ///
+ /// The number of the map.
+ /// The spawns of the map.
+ public static IReadOnlyList GetSpawns(byte mapNumber)
+ {
+ return mapNumber switch
+ {
+ FortressOfImperialGuardian1.Number => Map69Spawns,
+ FortressOfImperialGuardian2.Number => Map70Spawns,
+ FortressOfImperialGuardian3.Number => Map71Spawns,
+ FortressOfImperialGuardian4.Number => Map72Spawns,
+ _ => [],
+ };
+ }
+}
diff --git a/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs
index 97a3998fc..a0f4f8179 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs
@@ -92,6 +92,7 @@ public override void Initialize()
new CastleSiegeInitializer(this.Context, this.GameConfiguration).Initialize();
new KanturuInitializer(this.Context, this.GameConfiguration).Initialize();
new DoppelgangerInitializer(this.Context, this.GameConfiguration).Initialize();
+ new ImperialGuardianInitializer(this.Context, this.GameConfiguration).Initialize();
// After all items exist.
new ItemRules(this.Context, this.GameConfiguration).Initialize();
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/EventTicketItems.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/EventTicketItems.cs
index 19e1607e4..a22ab5d1e 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/Items/EventTicketItems.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/Items/EventTicketItems.cs
@@ -61,6 +61,34 @@ public override void Initialize()
this.CreateEventItem(107, 14, 1, 1, "Fifth Secromicon Fragment", false);
this.CreateEventItem(108, 14, 1, 1, "Sixth Secromicon Fragment", false);
this.CreateEventItem(109, 14, 1, 1, "Complete Secromicon", false);
+ if (this.CreateSuspiciousScrapOfPaperDropGroup() is { } scrapOfPaperDropGroup)
+ {
+ BaseMapInitializer.RegisterDefaultDropItemGroup(scrapOfPaperDropGroup);
+ }
+ }
+
+ ///
+ /// Creates the drop item group of the suspicious scrap of paper, of which five transform into a
+ /// Gaion's Order, the ticket of the imperial guardian event.
+ ///
+ /// The drop item group, if it was created.
+ internal DropItemGroup? CreateSuspiciousScrapOfPaperDropGroup()
+ {
+ var scrapOfPaper = this.GameConfiguration.Items.FirstOrDefault(item => item is { Group: 14, Number: 101 });
+ var id = GuidHelper.CreateGuid(14, 101);
+ if (scrapOfPaper is null || this.GameConfiguration.DropItemGroups.Any(group => group.GetId() == id))
+ {
+ return null;
+ }
+
+ var dropItemGroup = this.Context.CreateNew();
+ dropItemGroup.SetGuid(14, 101);
+ dropItemGroup.Chance = 0.001;
+ dropItemGroup.Description = "Suspicious Scrap of Paper";
+ dropItemGroup.MinimumMonsterLevel = 32;
+ dropItemGroup.PossibleItems.Add(scrapOfPaper);
+ this.GameConfiguration.DropItemGroups.Add(dropItemGroup);
+ return dropItemGroup;
}
///
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian1.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian1.cs
index 53d68e242..bad8cffde 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian1.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian1.cs
@@ -9,6 +9,7 @@ namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.Persistence.Initialization.Skills;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
///
/// Map initialization for the Empire fortress 1 event map.
@@ -41,6 +42,9 @@ public FortressOfImperialGuardian1(IContext context, GameConfiguration gameConfi
///
protected override string MapName => Name;
+ ///
+ protected override byte SafezoneMapNumber => Devias.Number;
+
///
protected override void CreateMonsters()
{
@@ -775,124 +779,20 @@ protected override void CreateMonsters()
///
///
- /// The file from which I generated this code, included two additional values at the end of each spawn definition.
- /// I guess this is some kind of event state under which the spawn is valid. Until I find out what they're good for,
- /// I'll keep them as comment at the end of each line.
+ /// The monsters are spawned by the imperial guardian event, zone by zone.
///
protected override IEnumerable CreateMonsterSpawns()
{
- yield return this.CreateMonsterSpawn(100, this.NpcDictionary[520], 225, 40, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(101, this.NpcDictionary[520], 232, 40, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(102, this.NpcDictionary[520], 239, 40, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(103, this.NpcDictionary[519], 225, 48, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(104, this.NpcDictionary[519], 232, 48, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(105, this.NpcDictionary[519], 239, 48, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(106, this.NpcDictionary[512], 232, 45, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(107, this.NpcDictionary[518], 187, 21, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(108, this.NpcDictionary[518], 185, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(109, this.NpcDictionary[518], 187, 31, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(110, this.NpcDictionary[519], 179, 19, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(111, this.NpcDictionary[519], 177, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(112, this.NpcDictionary[519], 179, 33, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(113, this.NpcDictionary[513], 173, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(114, this.NpcDictionary[521], 174, 89, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(115, this.NpcDictionary[521], 186, 90, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(116, this.NpcDictionary[521], 180, 91, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(117, this.NpcDictionary[508], 180, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(118, this.NpcDictionary[519], 174, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(119, this.NpcDictionary[519], 178, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(120, this.NpcDictionary[519], 186, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(121, this.NpcDictionary[525], 234, 28, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(122, this.NpcDictionary[525], 216, 80, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(123, this.NpcDictionary[524], 233, 55, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(124, this.NpcDictionary[526], 217, 72, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(125, this.NpcDictionary[526], 218, 85, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(126, this.NpcDictionary[525], 194, 25, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(127, this.NpcDictionary[525], 154, 53, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(128, this.NpcDictionary[524], 166, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(129, this.NpcDictionary[526], 152, 30, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(130, this.NpcDictionary[526], 157, 30, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(131, this.NpcDictionary[525], 180, 79, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(132, this.NpcDictionary[526], 174, 99, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
- yield return this.CreateMonsterSpawn(133, this.NpcDictionary[526], 186, 100, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 2
-
- yield return this.CreateMonsterSpawn(200, this.NpcDictionary[520], 225, 40, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(201, this.NpcDictionary[520], 232, 40, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(202, this.NpcDictionary[520], 239, 40, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(203, this.NpcDictionary[519], 225, 48, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(204, this.NpcDictionary[519], 232, 48, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(205, this.NpcDictionary[519], 239, 48, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(206, this.NpcDictionary[512], 232, 45, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(207, this.NpcDictionary[518], 187, 21, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(208, this.NpcDictionary[518], 185, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(209, this.NpcDictionary[518], 187, 31, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(210, this.NpcDictionary[519], 179, 19, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(211, this.NpcDictionary[519], 177, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(212, this.NpcDictionary[519], 179, 33, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(213, this.NpcDictionary[513], 173, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(214, this.NpcDictionary[521], 174, 89, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(215, this.NpcDictionary[521], 186, 90, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(216, this.NpcDictionary[521], 180, 91, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(217, this.NpcDictionary[511], 180, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(218, this.NpcDictionary[519], 174, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(219, this.NpcDictionary[519], 178, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(220, this.NpcDictionary[519], 186, 95, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(221, this.NpcDictionary[525], 234, 28, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(222, this.NpcDictionary[525], 216, 80, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(223, this.NpcDictionary[524], 233, 55, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(224, this.NpcDictionary[526], 217, 72, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(225, this.NpcDictionary[526], 218, 85, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(226, this.NpcDictionary[525], 194, 25, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(227, this.NpcDictionary[525], 154, 53, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(228, this.NpcDictionary[524], 166, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(229, this.NpcDictionary[526], 152, 30, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(230, this.NpcDictionary[526], 157, 30, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(231, this.NpcDictionary[525], 180, 79, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(232, this.NpcDictionary[526], 174, 99, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
- yield return this.CreateMonsterSpawn(233, this.NpcDictionary[526], 186, 100, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 5
-
- // Traps:
- yield return this.CreateMonsterSpawn(300, this.NpcDictionary[523], 154, 48, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(301, this.NpcDictionary[523], 154, 44, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(302, this.NpcDictionary[523], 154, 40, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(303, this.NpcDictionary[523], 154, 36, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(304, this.NpcDictionary[523], 154, 32, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(305, this.NpcDictionary[523], 154, 28, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(306, this.NpcDictionary[523], 154, 24, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(307, this.NpcDictionary[523], 157, 26, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(308, this.NpcDictionary[523], 161, 26, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 2
- yield return this.CreateMonsterSpawn(309, this.NpcDictionary[523], 233, 60, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(310, this.NpcDictionary[523], 233, 64, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(311, this.NpcDictionary[523], 233, 68, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(312, this.NpcDictionary[523], 230, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(313, this.NpcDictionary[523], 226, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(314, this.NpcDictionary[523], 222, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(315, this.NpcDictionary[523], 218, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(316, this.NpcDictionary[523], 220, 70, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(317, this.NpcDictionary[523], 220, 74, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(318, this.NpcDictionary[523], 220, 78, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(319, this.NpcDictionary[523], 220, 82, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(320, this.NpcDictionary[523], 220, 86, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 2
- yield return this.CreateMonsterSpawn(321, this.NpcDictionary[523], 154, 48, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(322, this.NpcDictionary[523], 154, 44, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(323, this.NpcDictionary[523], 154, 40, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(324, this.NpcDictionary[523], 154, 36, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(325, this.NpcDictionary[523], 154, 32, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(326, this.NpcDictionary[523], 154, 28, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(327, this.NpcDictionary[523], 154, 24, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(328, this.NpcDictionary[523], 157, 26, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(329, this.NpcDictionary[523], 161, 26, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 5
- yield return this.CreateMonsterSpawn(330, this.NpcDictionary[523], 233, 60, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(331, this.NpcDictionary[523], 233, 64, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(332, this.NpcDictionary[523], 233, 68, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(333, this.NpcDictionary[523], 230, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(334, this.NpcDictionary[523], 226, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(335, this.NpcDictionary[523], 222, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(336, this.NpcDictionary[523], 218, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(337, this.NpcDictionary[523], 220, 70, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(338, this.NpcDictionary[523], 220, 74, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(339, this.NpcDictionary[523], 220, 78, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(340, this.NpcDictionary[523], 220, 82, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
- yield return this.CreateMonsterSpawn(341, this.NpcDictionary[523], 220, 86, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 5
+ return ImperialGuardianSpawns.GetSpawns(Number).Select(spawn => this.CreateMonsterSpawn(
+ spawn.Number,
+ this.NpcDictionary[spawn.MonsterNumber],
+ spawn.X,
+ spawn.X,
+ spawn.Y,
+ spawn.Y,
+ 1,
+ spawn.Direction,
+ SpawnTrigger.OnceAtWaveStart,
+ spawn.WaveNumber));
}
}
\ No newline at end of file
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian2.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian2.cs
index 7f2962d0d..62504ac9a 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian2.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian2.cs
@@ -5,6 +5,7 @@
namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
///
/// Map initialization for the Empire fortress 2 event map.
@@ -37,6 +38,9 @@ public FortressOfImperialGuardian2(IContext context, GameConfiguration gameConfi
///
protected override string MapName => Name;
+ ///
+ protected override byte SafezoneMapNumber => Devias.Number;
+
///
protected override void CreateMonsters()
{
@@ -44,115 +48,21 @@ protected override void CreateMonsters()
}
///
+ ///
+ /// The monsters are spawned by the imperial guardian event, zone by zone.
+ ///
protected override IEnumerable CreateMonsterSpawns()
{
- yield return this.CreateMonsterSpawn(100, this.NpcDictionary[520], 66, 59, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(101, this.NpcDictionary[520], 64, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(102, this.NpcDictionary[520], 66, 70, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(103, this.NpcDictionary[519], 56, 58, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(104, this.NpcDictionary[519], 56, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(105, this.NpcDictionary[519], 56, 71, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(106, this.NpcDictionary[514], 59, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(107, this.NpcDictionary[518], 32, 99, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(108, this.NpcDictionary[518], 42, 99, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(109, this.NpcDictionary[518], 38, 101, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(110, this.NpcDictionary[519], 33, 107, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(111, this.NpcDictionary[519], 44, 107, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(112, this.NpcDictionary[519], 39, 110, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(113, this.NpcDictionary[515], 38, 106, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(114, this.NpcDictionary[521], 98, 107, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(115, this.NpcDictionary[521], 96, 112, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(116, this.NpcDictionary[521], 98, 117, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(117, this.NpcDictionary[519], 93, 105, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(118, this.NpcDictionary[519], 92, 112, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(119, this.NpcDictionary[519], 93, 118, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(120, this.NpcDictionary[509], 89, 111, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(121, this.NpcDictionary[525], 75, 67, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(122, this.NpcDictionary[525], 19, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(123, this.NpcDictionary[524], 50, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(124, this.NpcDictionary[526], 24, 67, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(125, this.NpcDictionary[526], 24, 62, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(126, this.NpcDictionary[525], 37, 93, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(127, this.NpcDictionary[525], 55, 154, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(128, this.NpcDictionary[524], 41, 117, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(129, this.NpcDictionary[526], 53, 152, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(130, this.NpcDictionary[526], 56, 152, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(131, this.NpcDictionary[525], 107, 112, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(132, this.NpcDictionary[526], 85, 113, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(133, this.NpcDictionary[526], 85, 107, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 3
- yield return this.CreateMonsterSpawn(134, this.NpcDictionary[520], 66, 59, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(135, this.NpcDictionary[520], 64, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(136, this.NpcDictionary[520], 66, 70, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(137, this.NpcDictionary[519], 56, 58, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(138, this.NpcDictionary[519], 56, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(139, this.NpcDictionary[519], 56, 71, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(140, this.NpcDictionary[514], 59, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(141, this.NpcDictionary[518], 32, 99, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(142, this.NpcDictionary[518], 42, 99, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(143, this.NpcDictionary[518], 38, 101, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(144, this.NpcDictionary[519], 33, 107, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(145, this.NpcDictionary[519], 44, 107, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(146, this.NpcDictionary[519], 39, 110, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(147, this.NpcDictionary[515], 38, 106, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(148, this.NpcDictionary[521], 98, 107, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(149, this.NpcDictionary[521], 96, 112, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(150, this.NpcDictionary[521], 98, 117, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(151, this.NpcDictionary[519], 93, 105, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(152, this.NpcDictionary[519], 92, 112, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(153, this.NpcDictionary[519], 93, 118, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(154, this.NpcDictionary[507], 89, 111, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(155, this.NpcDictionary[525], 75, 67, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(156, this.NpcDictionary[525], 19, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(157, this.NpcDictionary[524], 50, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(158, this.NpcDictionary[526], 24, 67, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(159, this.NpcDictionary[526], 24, 62, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(160, this.NpcDictionary[525], 37, 93, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(161, this.NpcDictionary[525], 55, 154, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(162, this.NpcDictionary[524], 41, 117, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(163, this.NpcDictionary[526], 53, 152, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(164, this.NpcDictionary[526], 56, 152, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(165, this.NpcDictionary[525], 107, 112, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(166, this.NpcDictionary[526], 85, 113, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
- yield return this.CreateMonsterSpawn(167, this.NpcDictionary[526], 85, 107, (Direction)3, SpawnTrigger.OnceAtEventStart); // 2 6
-
- // Traps:
- yield return this.CreateMonsterSpawn(200, this.NpcDictionary[523], 45, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(201, this.NpcDictionary[523], 42, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(202, this.NpcDictionary[523], 39, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(203, this.NpcDictionary[523], 35, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(204, this.NpcDictionary[523], 32, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(205, this.NpcDictionary[523], 29, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(206, this.NpcDictionary[523], 26, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(207, this.NpcDictionary[523], 24, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(208, this.NpcDictionary[523], 22, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 3
- yield return this.CreateMonsterSpawn(209, this.NpcDictionary[523], 41, 122, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(210, this.NpcDictionary[523], 41, 126, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(211, this.NpcDictionary[523], 41, 130, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(212, this.NpcDictionary[523], 45, 128, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(213, this.NpcDictionary[523], 50, 128, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(214, this.NpcDictionary[523], 55, 129, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(215, this.NpcDictionary[523], 55, 134, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(216, this.NpcDictionary[523], 55, 139, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(217, this.NpcDictionary[523], 55, 144, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(218, this.NpcDictionary[523], 55, 149, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 3
- yield return this.CreateMonsterSpawn(219, this.NpcDictionary[523], 45, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(220, this.NpcDictionary[523], 42, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(221, this.NpcDictionary[523], 39, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(222, this.NpcDictionary[523], 35, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(223, this.NpcDictionary[523], 32, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(224, this.NpcDictionary[523], 29, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(225, this.NpcDictionary[523], 26, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(226, this.NpcDictionary[523], 24, 62, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(227, this.NpcDictionary[523], 22, 66, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 6
- yield return this.CreateMonsterSpawn(228, this.NpcDictionary[523], 41, 122, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(229, this.NpcDictionary[523], 41, 126, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(230, this.NpcDictionary[523], 41, 130, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(231, this.NpcDictionary[523], 45, 128, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(232, this.NpcDictionary[523], 50, 128, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(233, this.NpcDictionary[523], 55, 129, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(234, this.NpcDictionary[523], 55, 134, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(235, this.NpcDictionary[523], 55, 139, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(236, this.NpcDictionary[523], 55, 144, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
- yield return this.CreateMonsterSpawn(237, this.NpcDictionary[523], 55, 149, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 6
+ return ImperialGuardianSpawns.GetSpawns(Number).Select(spawn => this.CreateMonsterSpawn(
+ spawn.Number,
+ this.NpcDictionary[spawn.MonsterNumber],
+ spawn.X,
+ spawn.X,
+ spawn.Y,
+ spawn.Y,
+ 1,
+ spawn.Direction,
+ SpawnTrigger.OnceAtWaveStart,
+ spawn.WaveNumber));
}
}
\ No newline at end of file
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian3.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian3.cs
index 5fb3d9c32..f51eeb059 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian3.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian3.cs
@@ -5,6 +5,7 @@
namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
///
/// Map initialization for the Empire fortress 3 event map.
@@ -37,6 +38,9 @@ public FortressOfImperialGuardian3(IContext context, GameConfiguration gameConfi
///
protected override string MapName => Name;
+ ///
+ protected override byte SafezoneMapNumber => Devias.Number;
+
///
protected override void CreateMonsters()
{
@@ -44,117 +48,21 @@ protected override void CreateMonsters()
}
///
+ ///
+ /// The monsters are spawned by the imperial guardian event, zone by zone.
+ ///
protected override IEnumerable CreateMonsterSpawns()
{
- yield return this.CreateMonsterSpawn(100, this.NpcDictionary[520], 132, 185, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(101, this.NpcDictionary[520], 131, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(102, this.NpcDictionary[520], 132, 199, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(103, this.NpcDictionary[519], 126, 185, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(104, this.NpcDictionary[519], 124, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(105, this.NpcDictionary[519], 126, 199, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(106, this.NpcDictionary[516], 127, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(107, this.NpcDictionary[518], 217, 140, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(108, this.NpcDictionary[518], 228, 140, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(109, this.NpcDictionary[518], 222, 142, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(110, this.NpcDictionary[519], 218, 148, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(111, this.NpcDictionary[519], 222, 152, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(112, this.NpcDictionary[519], 227, 148, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(113, this.NpcDictionary[517], 222, 147, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(114, this.NpcDictionary[521], 162, 226, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(115, this.NpcDictionary[521], 166, 228, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(116, this.NpcDictionary[521], 171, 226, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(117, this.NpcDictionary[519], 161, 231, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(118, this.NpcDictionary[519], 164, 233, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(119, this.NpcDictionary[519], 171, 233, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(120, this.NpcDictionary[510], 166, 233, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(121, this.NpcDictionary[525], 146, 191, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(122, this.NpcDictionary[525], 89, 195, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(123, this.NpcDictionary[524], 119, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(124, this.NpcDictionary[526], 111, 197, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(125, this.NpcDictionary[526], 111, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(126, this.NpcDictionary[525], 222, 134, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(127, this.NpcDictionary[525], 223, 193, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(128, this.NpcDictionary[524], 222, 160, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(129, this.NpcDictionary[526], 220, 173, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(130, this.NpcDictionary[526], 224, 173, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(131, this.NpcDictionary[525], 167, 217, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(132, this.NpcDictionary[526], 158, 236, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(133, this.NpcDictionary[526], 175, 238, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 4
- yield return this.CreateMonsterSpawn(134, this.NpcDictionary[520], 132, 185, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(135, this.NpcDictionary[520], 131, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(136, this.NpcDictionary[520], 132, 199, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(137, this.NpcDictionary[519], 126, 185, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(138, this.NpcDictionary[519], 124, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(139, this.NpcDictionary[519], 126, 199, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(140, this.NpcDictionary[516], 127, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(141, this.NpcDictionary[518], 217, 140, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(142, this.NpcDictionary[518], 228, 140, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(143, this.NpcDictionary[518], 222, 142, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(144, this.NpcDictionary[519], 218, 148, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(145, this.NpcDictionary[519], 222, 152, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(146, this.NpcDictionary[519], 227, 148, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(147, this.NpcDictionary[517], 222, 147, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(148, this.NpcDictionary[521], 162, 226, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(149, this.NpcDictionary[521], 166, 228, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(150, this.NpcDictionary[521], 171, 226, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(151, this.NpcDictionary[519], 161, 231, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(152, this.NpcDictionary[519], 164, 233, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(153, this.NpcDictionary[519], 171, 233, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(154, this.NpcDictionary[506], 166, 233, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(155, this.NpcDictionary[525], 146, 191, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(156, this.NpcDictionary[525], 89, 195, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(157, this.NpcDictionary[524], 119, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(158, this.NpcDictionary[526], 111, 197, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(159, this.NpcDictionary[526], 111, 192, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(160, this.NpcDictionary[525], 222, 134, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(161, this.NpcDictionary[525], 223, 193, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(162, this.NpcDictionary[524], 222, 160, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(163, this.NpcDictionary[526], 220, 173, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(164, this.NpcDictionary[526], 224, 173, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(165, this.NpcDictionary[525], 167, 217, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(166, this.NpcDictionary[526], 158, 236, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
- yield return this.CreateMonsterSpawn(167, this.NpcDictionary[526], 175, 238, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 7
-
- // Traps:
- yield return this.CreateMonsterSpawn(200, this.NpcDictionary[523], 113, 193, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(201, this.NpcDictionary[523], 112, 198, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(202, this.NpcDictionary[523], 112, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(203, this.NpcDictionary[523], 107, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(204, this.NpcDictionary[523], 102, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(205, this.NpcDictionary[523], 97, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(206, this.NpcDictionary[523], 93, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(207, this.NpcDictionary[523], 93, 198, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(208, this.NpcDictionary[523], 93, 192, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(209, this.NpcDictionary[523], 93, 187, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(210, this.NpcDictionary[523], 97, 186, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(211, this.NpcDictionary[523], 102, 186, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(212, this.NpcDictionary[523], 107, 186, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(213, this.NpcDictionary[523], 112, 187, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 4
- yield return this.CreateMonsterSpawn(214, this.NpcDictionary[523], 222, 165, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(215, this.NpcDictionary[523], 222, 170, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(216, this.NpcDictionary[523], 222, 175, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(217, this.NpcDictionary[523], 222, 180, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(218, this.NpcDictionary[523], 222, 185, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(219, this.NpcDictionary[523], 222, 190, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 4
- yield return this.CreateMonsterSpawn(220, this.NpcDictionary[523], 113, 193, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(221, this.NpcDictionary[523], 112, 198, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(222, this.NpcDictionary[523], 112, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(223, this.NpcDictionary[523], 107, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(224, this.NpcDictionary[523], 102, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(225, this.NpcDictionary[523], 97, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(226, this.NpcDictionary[523], 93, 203, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(227, this.NpcDictionary[523], 93, 198, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(228, this.NpcDictionary[523], 93, 192, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(229, this.NpcDictionary[523], 93, 187, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(230, this.NpcDictionary[523], 97, 186, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(231, this.NpcDictionary[523], 102, 186, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(232, this.NpcDictionary[523], 107, 186, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(233, this.NpcDictionary[523], 112, 187, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 7
- yield return this.CreateMonsterSpawn(234, this.NpcDictionary[523], 222, 165, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(235, this.NpcDictionary[523], 222, 170, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(236, this.NpcDictionary[523], 222, 175, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(237, this.NpcDictionary[523], 222, 180, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(238, this.NpcDictionary[523], 222, 185, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 7
- yield return this.CreateMonsterSpawn(239, this.NpcDictionary[523], 222, 190, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 7
+ return ImperialGuardianSpawns.GetSpawns(Number).Select(spawn => this.CreateMonsterSpawn(
+ spawn.Number,
+ this.NpcDictionary[spawn.MonsterNumber],
+ spawn.X,
+ spawn.X,
+ spawn.Y,
+ spawn.Y,
+ 1,
+ spawn.Direction,
+ SpawnTrigger.OnceAtWaveStart,
+ spawn.WaveNumber));
}
}
\ No newline at end of file
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian4.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian4.cs
index f2c28a57e..7f1ba2bb8 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian4.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/FortressOfImperialGuardian4.cs
@@ -5,6 +5,7 @@
namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events;
///
/// Map initialization for the Empire fortress 4 event map.
@@ -37,6 +38,9 @@ public FortressOfImperialGuardian4(IContext context, GameConfiguration gameConfi
///
protected override string MapName => Name;
+ ///
+ protected override byte SafezoneMapNumber => Devias.Number;
+
///
protected override void CreateMonsters()
{
@@ -44,101 +48,21 @@ protected override void CreateMonsters()
}
///
+ ///
+ /// The monsters are spawned by the imperial guardian event, zone by zone.
+ ///
protected override IEnumerable CreateMonsterSpawns()
{
- yield return this.CreateMonsterSpawn(100, this.NpcDictionary[518], 64, 65, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(101, this.NpcDictionary[518], 64, 69, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(102, this.NpcDictionary[518], 64, 73, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(103, this.NpcDictionary[519], 61, 64, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(104, this.NpcDictionary[519], 61, 69, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(105, this.NpcDictionary[519], 61, 74, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(106, this.NpcDictionary[521], 55, 71, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(107, this.NpcDictionary[521], 55, 66, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(108, this.NpcDictionary[521], 54, 68, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(109, this.NpcDictionary[507], 57, 69, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(110, this.NpcDictionary[518], 29, 186, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(111, this.NpcDictionary[518], 34, 187, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(112, this.NpcDictionary[518], 39, 186, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(113, this.NpcDictionary[519], 34, 194, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(114, this.NpcDictionary[519], 29, 192, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(115, this.NpcDictionary[519], 39, 192, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(116, this.NpcDictionary[521], 26, 191, (Direction)2, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(117, this.NpcDictionary[521], 32, 195, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(118, this.NpcDictionary[521], 39, 196, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(119, this.NpcDictionary[506], 36, 191, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(120, this.NpcDictionary[518], 173, 129, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(121, this.NpcDictionary[518], 170, 133, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(122, this.NpcDictionary[518], 173, 137, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(123, this.NpcDictionary[519], 181, 129, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(124, this.NpcDictionary[519], 181, 133, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(125, this.NpcDictionary[519], 181, 137, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(126, this.NpcDictionary[521], 184, 129, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(127, this.NpcDictionary[521], 184, 133, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(128, this.NpcDictionary[521], 184, 137, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(129, this.NpcDictionary[505], 177, 133, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(130, this.NpcDictionary[521], 183, 29, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(131, this.NpcDictionary[521], 183, 21, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(132, this.NpcDictionary[521], 187, 32, (Direction)1, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(133, this.NpcDictionary[521], 187, 20, (Direction)5, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(134, this.NpcDictionary[518], 192, 31, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(135, this.NpcDictionary[518], 192, 21, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(136, this.NpcDictionary[518], 194, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(137, this.NpcDictionary[519], 189, 21, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(138, this.NpcDictionary[519], 189, 26, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(139, this.NpcDictionary[519], 189, 31, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(140, this.NpcDictionary[504], 183, 24, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(141, this.NpcDictionary[528], 81, 69, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(142, this.NpcDictionary[528], 32, 90, (Direction)1, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(143, this.NpcDictionary[527], 50, 69, (Direction)3, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(144, this.NpcDictionary[526], 52, 77, (Direction)2, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(145, this.NpcDictionary[526], 53, 61, (Direction)4, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(146, this.NpcDictionary[528], 34, 176, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(147, this.NpcDictionary[528], 69, 166, (Direction)5, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(148, this.NpcDictionary[527], 52, 191, (Direction)7, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(149, this.NpcDictionary[526], 21, 198, (Direction)2, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(150, this.NpcDictionary[526], 47, 199, (Direction)1, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(151, this.NpcDictionary[528], 156, 132, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(152, this.NpcDictionary[528], 224, 159, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(153, this.NpcDictionary[527], 197, 132, (Direction)7, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(154, this.NpcDictionary[526], 161, 139, (Direction)1, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(155, this.NpcDictionary[526], 161, 127, (Direction)5, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(156, this.NpcDictionary[528], 214, 24, (Direction)3, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(157, this.NpcDictionary[526], 207, 32, (Direction)1, SpawnTrigger.OnceAtEventStart); // 3 1
- yield return this.CreateMonsterSpawn(158, this.NpcDictionary[526], 207, 20, (Direction)5, SpawnTrigger.OnceAtEventStart); // 3 1
-
- // Traps
- yield return this.CreateMonsterSpawn(200, this.NpcDictionary[523], 45, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(201, this.NpcDictionary[523], 45, 74, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(202, this.NpcDictionary[523], 41, 74, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(203, this.NpcDictionary[523], 41, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(204, this.NpcDictionary[523], 37, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(205, this.NpcDictionary[523], 37, 74, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(206, this.NpcDictionary[523], 33, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(207, this.NpcDictionary[523], 33, 74, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(208, this.NpcDictionary[523], 29, 67, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(209, this.NpcDictionary[523], 29, 74, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(210, this.NpcDictionary[523], 31, 78, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(211, this.NpcDictionary[523], 31, 83, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(212, this.NpcDictionary[523], 31, 87, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 0 1
- yield return this.CreateMonsterSpawn(213, this.NpcDictionary[523], 57, 191, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(214, this.NpcDictionary[523], 61, 191, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(215, this.NpcDictionary[523], 65, 191, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(216, this.NpcDictionary[523], 69, 191, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(217, this.NpcDictionary[523], 68, 186, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(218, this.NpcDictionary[523], 68, 181, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(219, this.NpcDictionary[523], 68, 176, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(220, this.NpcDictionary[523], 68, 171, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 1 1
- yield return this.CreateMonsterSpawn(221, this.NpcDictionary[523], 202, 133, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(222, this.NpcDictionary[523], 206, 133, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(223, this.NpcDictionary[523], 210, 133, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(224, this.NpcDictionary[523], 214, 133, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(225, this.NpcDictionary[523], 218, 133, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(226, this.NpcDictionary[523], 222, 133, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(227, this.NpcDictionary[523], 226, 133, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(228, this.NpcDictionary[523], 224, 137, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(229, this.NpcDictionary[523], 224, 142, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(230, this.NpcDictionary[523], 224, 147, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(231, this.NpcDictionary[523], 224, 152, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
- yield return this.CreateMonsterSpawn(232, this.NpcDictionary[523], 224, 155, Direction.Undefined, SpawnTrigger.OnceAtEventStart); // 2 1
+ return ImperialGuardianSpawns.GetSpawns(Number).Select(spawn => this.CreateMonsterSpawn(
+ spawn.Number,
+ this.NpcDictionary[spawn.MonsterNumber],
+ spawn.X,
+ spawn.X,
+ spawn.Y,
+ spawn.Y,
+ 1,
+ spawn.Direction,
+ SpawnTrigger.OnceAtWaveStart,
+ spawn.WaveNumber));
}
}
\ No newline at end of file
diff --git a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs
index 77fbbbc55..333f4a477 100644
--- a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs
+++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs
@@ -7883,4 +7883,86 @@ public void MonsterSkillAnimation_PacketSizeValidation()
Assert.That(8 + 2, Is.LessThanOrEqualTo(expectedLength),
"Field 'TargetId' exceeds packet boundary");
}
+
+ ///
+ /// Tests the packet size calculation for ImperialGuardianEnterResult.
+ ///
+ [Test]
+ public void ImperialGuardianEnterResult_PacketSizeValidation()
+ {
+ // Fixed-length packet validation
+ const int expectedLength = 12;
+ var actualLength = ImperialGuardianEnterResultRef.Length;
+
+ Assert.That(actualLength, Is.EqualTo(expectedLength),
+ "Packet length mismatch: declared length does not match calculated size");
+
+ // Validate field 'Result' boundary
+ Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'Result' exceeds packet boundary");
+
+ // Validate field 'Day' boundary
+ Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'Day' exceeds packet boundary");
+
+ // Validate field 'Zone' boundary
+ Assert.That(6 + 1, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'Zone' exceeds packet boundary");
+
+ // Validate field 'Weather' boundary
+ Assert.That(7 + 1, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'Weather' exceeds packet boundary");
+
+ // Validate field 'RemainingMilliseconds' boundary
+ Assert.That(8 + 4, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'RemainingMilliseconds' exceeds packet boundary");
+ }
+
+ ///
+ /// Tests the packet size calculation for ImperialGuardianTimer.
+ ///
+ [Test]
+ public void ImperialGuardianTimer_PacketSizeValidation()
+ {
+ // Fixed-length packet validation
+ const int expectedLength = 16;
+ var actualLength = ImperialGuardianTimerRef.Length;
+
+ Assert.That(actualLength, Is.EqualTo(expectedLength),
+ "Packet length mismatch: declared length does not match calculated size");
+
+ // Validate field 'Type' boundary
+ Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'Type' exceeds packet boundary");
+
+ // Validate field 'RemainingMilliseconds' boundary
+ Assert.That(8 + 4, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'RemainingMilliseconds' exceeds packet boundary");
+
+ // Validate field 'MonsterCount' boundary
+ Assert.That(12 + 1, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'MonsterCount' exceeds packet boundary");
+ }
+
+ ///
+ /// Tests the packet size calculation for ImperialGuardianResult.
+ ///
+ [Test]
+ public void ImperialGuardianResult_PacketSizeValidation()
+ {
+ // Fixed-length packet validation
+ const int expectedLength = 12;
+ var actualLength = ImperialGuardianResultRef.Length;
+
+ Assert.That(actualLength, Is.EqualTo(expectedLength),
+ "Packet length mismatch: declared length does not match calculated size");
+
+ // Validate field 'Result' boundary
+ Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'Result' exceeds packet boundary");
+
+ // Validate field 'Experience' boundary
+ Assert.That(8 + 4, Is.LessThanOrEqualTo(expectedLength),
+ "Field 'Experience' exceeds packet boundary");
+ }
}
diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ImperialGuardianDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ImperialGuardianDataTest.cs
new file mode 100644
index 000000000..25b77ccbe
--- /dev/null
+++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ImperialGuardianDataTest.cs
@@ -0,0 +1,126 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Persistence.Initialization.Tests;
+
+using Microsoft.Extensions.Logging.Abstractions;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence.Initialization.Updates;
+using MUnique.OpenMU.Persistence.InMemory;
+
+///
+/// Tests the data of the imperial guardian event.
+///
+[TestFixture]
+internal class ImperialGuardianDataTest
+{
+ private const short JerintNumber = 522;
+ private const short DeviasNumber = 2;
+ private static readonly short[] EventMapNumbers = [69, 70, 71, 72];
+
+ ///
+ /// The maps of the days (1 = monday, ..., 7 = sunday) with the number of their zones.
+ ///
+ private static readonly (byte Day, short MapNumber, int ZoneCount)[] Days =
+ [
+ (1, 69, 3), (2, 70, 3), (3, 71, 3), (4, 69, 3), (5, 70, 3), (6, 71, 3), (7, 72, 4),
+ ];
+
+ ///
+ /// Tests that a new season 6 database contains the imperial guardian event.
+ ///
+ [Test]
+ public async Task NewDatabaseContainsEventAsync()
+ {
+ var contextProvider = new InMemoryPersistenceContextProvider();
+ var gameConfiguration = await CreateConfigurationAsync(contextProvider).ConfigureAwait(false);
+
+ AssertEventData(gameConfiguration);
+ }
+
+ ///
+ /// Tests that the update adds the imperial guardian event to a database which was created
+ /// before it existed, and that applying it twice doesn't duplicate anything.
+ ///
+ [Test]
+ public async Task UpdateAddsEventToExistingDatabaseAsync()
+ {
+ var contextProvider = new InMemoryPersistenceContextProvider();
+ var gameConfiguration = await CreateConfigurationAsync(contextProvider).ConfigureAwait(false);
+
+ // Revert the database to the state before the event existed.
+ foreach (var definition in gameConfiguration.MiniGameDefinitions.Where(d => d.Type == MiniGameType.ImperialGuardian).ToList())
+ {
+ gameConfiguration.MiniGameDefinitions.Remove(definition);
+ }
+
+ gameConfiguration.Monsters.Single(monster => monster.Number == JerintNumber).NpcWindow = NpcWindow.Undefined;
+ var scrapDropGroup = gameConfiguration.DropItemGroups.Single(group => group.PossibleItems.Any(item => item is { Group: 14, Number: 101 }));
+ gameConfiguration.DropItemGroups.Remove(scrapDropGroup);
+ foreach (var map in gameConfiguration.Maps)
+ {
+ map.DropItemGroups.Remove(scrapDropGroup);
+ }
+
+ foreach (var map in gameConfiguration.Maps.Where(map => EventMapNumbers.Contains(map.Number)))
+ {
+ map.SafezoneMap = map;
+ foreach (var spawn in map.MonsterSpawns)
+ {
+ spawn.SpawnTrigger = SpawnTrigger.OnceAtEventStart;
+ spawn.WaveNumber = 0;
+ }
+ }
+
+ var update = new AddImperialGuardianDataUpdatePlugIn();
+ await update.ApplyUpdateAsync(contextProvider.CreateNewContext(), gameConfiguration).ConfigureAwait(false);
+ await update.ApplyUpdateAsync(contextProvider.CreateNewContext(), gameConfiguration).ConfigureAwait(false);
+
+ AssertEventData(gameConfiguration);
+ }
+
+ private static async Task CreateConfigurationAsync(InMemoryPersistenceContextProvider contextProvider)
+ {
+ var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
+ await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
+ using var context = contextProvider.CreateNewContext();
+ return (await context.GetAsync().ConfigureAwait(false)).First();
+ }
+
+ private static void AssertEventData(GameConfiguration gameConfiguration)
+ {
+ var definitions = gameConfiguration.MiniGameDefinitions.Where(d => d.Type == MiniGameType.ImperialGuardian).ToList();
+ Assert.That(definitions, Has.Count.EqualTo(Days.Length));
+
+ foreach (var (day, mapNumber, zoneCount) in Days)
+ {
+ var definition = definitions.Single(d => d.GameLevel == day);
+ Assert.That(definition.Entrance?.Map?.Number, Is.EqualTo(mapNumber), $"Map of day {day}");
+ Assert.That(definition.TicketItem, Is.Null, "The ticket is checked by the enter action.");
+ Assert.That(definition.MapCreationPolicy, Is.EqualTo(MiniGameMapCreationPolicy.OnePerParty));
+
+ var map = gameConfiguration.Maps.Single(m => m.Number == mapNumber);
+ var waves = map.MonsterSpawns.Where(spawn => spawn.WaveNumber / 10 == day).Select(spawn => spawn.WaveNumber % 10).Distinct().ToList();
+ Assert.That(waves, Is.EquivalentTo(Enumerable.Range(0, zoneCount)), $"Zones of day {day}");
+ foreach (var zone in waves)
+ {
+ var zoneSpawns = map.MonsterSpawns.Where(spawn => spawn.WaveNumber == (day * 10) + zone).ToList();
+ Assert.That(zoneSpawns.Any(spawn => spawn.MonsterDefinition!.Number is >= 504 and <= 521), Is.True, $"Monsters of day {day}, zone {zone}");
+ Assert.That(zoneSpawns.Any(spawn => spawn.MonsterDefinition!.Number is 525 or 528), Is.True, $"Gate of day {day}, zone {zone}");
+ }
+ }
+
+ foreach (var map in gameConfiguration.Maps.Where(map => EventMapNumbers.Contains(map.Number)))
+ {
+ Assert.That(map.MonsterSpawns.Select(spawn => spawn.SpawnTrigger), Is.All.EqualTo(SpawnTrigger.OnceAtWaveStart), $"Spawns of map {map.Number}");
+ Assert.That(map.SafezoneMap?.Number, Is.EqualTo(DeviasNumber), $"Safezone of map {map.Number}");
+ }
+
+ Assert.That(gameConfiguration.Monsters.Single(monster => monster.Number == JerintNumber).NpcWindow, Is.EqualTo(NpcWindow.JerintGaionEvententry));
+
+ var scrapDropGroups = gameConfiguration.DropItemGroups.Where(group => group.PossibleItems.Any(item => item is { Group: 14, Number: 101 })).ToList();
+ Assert.That(scrapDropGroups, Has.Count.EqualTo(1));
+ Assert.That(gameConfiguration.Maps.Where(map => map.DropItemGroups.Contains(scrapDropGroups[0])), Is.Not.Empty);
+ }
+}
diff --git a/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs b/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs
new file mode 100644
index 000000000..f3b01dad8
--- /dev/null
+++ b/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs
@@ -0,0 +1,113 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Tests;
+
+using Moq;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+using MUnique.OpenMU.GameLogic.PlayerActions.MiniGames;
+
+///
+/// Tests the .
+///
+[TestFixture]
+public class ImperialGuardianEventDefinitionTest
+{
+ ///
+ /// Tests that the wave number contains the day and the zone.
+ ///
+ [Test]
+ public void WaveNumber()
+ {
+ Assert.That(ImperialGuardianEventDefinition.GetWaveNumber(1, 0), Is.EqualTo(10));
+ Assert.That(ImperialGuardianEventDefinition.GetWaveNumber(7, 3), Is.EqualTo(73));
+ }
+
+ ///
+ /// Tests that the experience reward depends on the level of the player and is multiplied on sunday.
+ ///
+ [Test]
+ public void ExperienceReward()
+ {
+ var definition = new ImperialGuardianEventDefinition
+ {
+ ExperienceRewards =
+ [
+ new() { MaximumPlayerLevel = 200, Experience = 1_000 },
+ new() { MaximumPlayerLevel = 400, Experience = 2_000 },
+ ],
+ };
+
+ Assert.That(definition.GetExperienceReward(150, 1), Is.EqualTo(1_000));
+ Assert.That(definition.GetExperienceReward(201, 1), Is.EqualTo(2_000));
+ Assert.That(definition.GetExperienceReward(201, ImperialGuardianEventDefinition.Sunday), Is.EqualTo(4_000));
+ Assert.That(definition.GetExperienceReward(401, 1), Is.Zero, "Players above all levels of the table don't get experience.");
+ }
+
+ ///
+ /// Tests the number of fragments which a boss drops, depending on the chances.
+ ///
+ /// The random number from 0 to 99.
+ /// The expected number of fragments.
+ [TestCase(0, 1)]
+ [TestCase(49, 1)]
+ [TestCase(50, 2)]
+ [TestCase(78, 2)]
+ [TestCase(79, 3)]
+ [TestCase(99, 3)]
+ public void FragmentCount(int randomPercent, int expectedCount)
+ {
+ Assert.That(new ImperialGuardianEventDefinition().GetFragmentCount(randomPercent), Is.EqualTo(expectedCount));
+ }
+
+ ///
+ /// Tests that the monsters aren't scaled by default, and that the scaling depends on the level of the players.
+ ///
+ [Test]
+ public void MonsterScaling()
+ {
+ Assert.That(new ImperialGuardianEventDefinition().GetMonsterScaling(400), Is.Null);
+
+ var low = new ImperialGuardianMonsterScaling { MaximumPlayerLevel = 200, HealthMultiplier = 1 };
+ var high = new ImperialGuardianMonsterScaling { MaximumPlayerLevel = 400, HealthMultiplier = 2 };
+ var definition = new ImperialGuardianEventDefinition { MonsterScalings = [high, low] };
+
+ Assert.That(definition.GetMonsterScaling(150), Is.SameAs(low));
+ Assert.That(definition.GetMonsterScaling(300), Is.SameAs(high));
+ Assert.That(definition.GetMonsterScaling(800), Is.SameAs(high), "Players above all levels of the table use the highest scaling.");
+ }
+
+ ///
+ /// Tests that all bosses have skills by default, whose numbers the client can show,
+ /// and that the rage of the bosses is disabled by default.
+ ///
+ [Test]
+ public void DefaultMonsterSkills()
+ {
+ var definition = new ImperialGuardianEventDefinition();
+
+ Assert.That(Enumerable.Range(504, 8).Select(number => (short)number), Is.SubsetOf(definition.MonsterSkills.Select(skill => skill.MonsterNumber)));
+ Assert.That(definition.MonsterSkills.Select(skill => (int)skill.SkillNumber), Is.All.InRange(44, 77), "The client shows the animations of the skills 1 to 77.");
+ Assert.That(definition.MonsterSkills.Select(skill => skill.StunChance), Is.All.InRange(0, 100));
+ Assert.That(definition.RageHealthPercentage, Is.Zero);
+ }
+
+ ///
+ /// Tests that a fixed day overrides the day of the week of the server.
+ ///
+ [Test]
+ public void FixedDay()
+ {
+ var gameContext = new Mock();
+ gameContext.Setup(c => c.ServerTimeZone).Returns(TimeZoneInfo.Utc);
+ var definition = new ImperialGuardianEventDefinition { FixedDay = ImperialGuardianEventDefinition.Sunday };
+
+ Assert.That(EnterImperialGuardianAction.GetDay(gameContext.Object, definition), Is.EqualTo(ImperialGuardianEventDefinition.Sunday));
+
+ definition.FixedDay = 0;
+ var expectedDay = DateTime.UtcNow.DayOfWeek == DayOfWeek.Sunday ? 7 : (int)DateTime.UtcNow.DayOfWeek;
+ Assert.That(EnterImperialGuardianAction.GetDay(gameContext.Object, definition), Is.EqualTo(expectedDay));
+ }
+}
diff --git a/tests/MUnique.OpenMU.Tests/ImperialGuardianRemoteViewTests.cs b/tests/MUnique.OpenMU.Tests/ImperialGuardianRemoteViewTests.cs
new file mode 100644
index 000000000..ebcdf4500
--- /dev/null
+++ b/tests/MUnique.OpenMU.Tests/ImperialGuardianRemoteViewTests.cs
@@ -0,0 +1,48 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Tests;
+
+using MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian;
+using MUnique.OpenMU.GameServer.RemoteView.MiniGames;
+using ImperialGuardianEnterResult = MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian.ImperialGuardianEnterResult;
+using ImperialGuardianResult = MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian.ImperialGuardianResult;
+
+///
+/// Tests the packets which are sent to the client during the imperial guardian event.
+///
+[TestFixture]
+public class ImperialGuardianRemoteViewTests
+{
+ ///
+ /// Tests the packets of the view, which have to match the (not packed) structures of the client.
+ ///
+ [Test]
+ public async Task PacketsAsync()
+ {
+ var (player, output) = CastleSiegeRemoteViewTestHelper.CreatePlayer();
+ var view = new ImperialGuardianViewPlugIn(player);
+
+ await view.ShowEnterResultAsync(ImperialGuardianEnterResult.Success, 7, 2, ImperialGuardianWeather.Storm, TimeSpan.FromSeconds(60)).ConfigureAwait(false);
+ await view.ShowTimerAsync(ImperialGuardianTimerType.TimeAttack, TimeSpan.FromMilliseconds(599_500), 300).ConfigureAwait(false);
+ await view.ShowResultAsync(ImperialGuardianResult.Success, 123_456).ConfigureAwait(false);
+
+ var data = output.ToArray();
+ Assert.That(data.Length, Is.EqualTo(12 + 16 + 12));
+
+ // The padding bytes of the not packed client structures aren't read by the client, so they're not checked.
+ // F7 02: result, day, zone (starting at 1), weather, remaining milliseconds at offset 8.
+ Assert.That(data[..12], Is.EqualTo(new byte[] { 0xC1, 12, 0xF7, 0x02, 0, 7, 3, 3, 0x60, 0xEA, 0x00, 0x00 }));
+
+ // F7 04: type, remaining milliseconds at offset 8, monster count (at most 255) at offset 12.
+ var timer = data[12..28];
+ Assert.That(timer[..5], Is.EqualTo(new byte[] { 0xC1, 16, 0xF7, 0x04, 2 }));
+ Assert.That(timer[8..13], Is.EqualTo(new byte[] { 0xCC, 0x25, 0x09, 0x00, 255 }));
+
+ // F7 06: result, experience at offset 8.
+ var result = data[28..];
+ Assert.That(result[..5], Is.EqualTo(new byte[] { 0xC1, 12, 0xF7, 0x06, 2 }));
+ Assert.That(result[8..], Is.EqualTo(new byte[] { 0x40, 0xE2, 0x01, 0x00 }));
+ }
+}
diff --git a/tests/MUnique.OpenMU.Tests/SuspiciousScrapOfPaperStackedPlugInTest.cs b/tests/MUnique.OpenMU.Tests/SuspiciousScrapOfPaperStackedPlugInTest.cs
new file mode 100644
index 000000000..a628e0d1e
--- /dev/null
+++ b/tests/MUnique.OpenMU.Tests/SuspiciousScrapOfPaperStackedPlugInTest.cs
@@ -0,0 +1,60 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Tests;
+
+using MUnique.OpenMU.DataModel.Configuration.Items;
+using MUnique.OpenMU.DataModel.Entities;
+using MUnique.OpenMU.GameLogic.PlugIns;
+
+///
+/// Tests the .
+///
+[TestFixture]
+public class SuspiciousScrapOfPaperStackedPlugInTest
+{
+ private readonly SuspiciousScrapOfPaperStackedPlugIn _plugIn = new();
+
+ ///
+ /// Tests that a full stack of five suspicious scraps of paper transforms into a Gaion's Order.
+ ///
+ [Test]
+ public async Task FullStackTransformsIntoGaionsOrderAsync()
+ {
+ var (player, scrap, order) = await CreatePlayerWithScrapAsync(5).ConfigureAwait(false);
+
+ await this._plugIn.ItemStackedAsync(player, new Item(), scrap).ConfigureAwait(false);
+
+ Assert.That(scrap.Definition, Is.SameAs(order));
+ Assert.That(scrap.Durability, Is.EqualTo(1));
+ }
+
+ ///
+ /// Tests that an incomplete stack of suspicious scraps of paper stays as it is.
+ ///
+ [Test]
+ public async Task IncompleteStackStaysAsync()
+ {
+ var (player, scrap, _) = await CreatePlayerWithScrapAsync(4).ConfigureAwait(false);
+ var scrapDefinition = scrap.Definition;
+
+ await this._plugIn.ItemStackedAsync(player, new Item(), scrap).ConfigureAwait(false);
+
+ Assert.That(scrap.Definition, Is.SameAs(scrapDefinition));
+ Assert.That(scrap.Durability, Is.EqualTo(4));
+ }
+
+ private static async Task<(GameLogic.Player Player, Item Scrap, ItemDefinition Order)> CreatePlayerWithScrapAsync(byte stackedScraps)
+ {
+ var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
+ var scrapDefinition = new ItemDefinition { Group = 14, Number = 101, Width = 1, Height = 1, Durability = 5 };
+ var orderDefinition = new ItemDefinition { Group = 14, Number = 102, Width = 1, Height = 1, Durability = 1 };
+ player.GameContext.Configuration.Items.Add(scrapDefinition);
+ player.GameContext.Configuration.Items.Add(orderDefinition);
+
+ var scrap = new Item { Definition = scrapDefinition, Durability = stackedScraps, ItemSlot = 12 };
+ await player.Inventory!.AddItemAsync(12, scrap).ConfigureAwait(false);
+ return (player, scrap, orderDefinition);
+ }
+}