From d5b94a07b537b117b831ee6be63c3a45c7296f53 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:32:30 -0300 Subject: [PATCH 01/11] feat(imperial-guardian): add the packets of the imperial guardian event Co-Authored-By: Claude Opus 5.5 --- ...2-ImperialGuardianEnterResult_by-server.md | 48 ++ ...1-F7-04-ImperialGuardianTimer_by-server.md | 31 ++ ...-F7-06-ImperialGuardianResult_by-server.md | 30 ++ docs/Packets/ServerToClient.md | 3 + .../ServerToClient/ConnectionExtensions.cs | 98 ++++ .../ServerToClient/ServerToClientPackets.cs | 430 ++++++++++++++++++ .../ServerToClient/ServerToClientPackets.xml | 215 +++++++++ .../ServerToClientPacketsRef.cs | 321 +++++++++++++ .../ServerToClientPacketTests.cs | 82 ++++ 9 files changed, 1258 insertions(+) create mode 100644 docs/Packets/C1-F7-02-ImperialGuardianEnterResult_by-server.md create mode 100644 docs/Packets/C1-F7-04-ImperialGuardianTimer_by-server.md create mode 100644 docs/Packets/C1-F7-06-ImperialGuardianResult_by-server.md 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 1a6e9b30c..308bd8781 100644 --- a/docs/Packets/ServerToClient.md +++ b/docs/Packets/ServerToClient.md @@ -299,4 +299,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/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs index 258dd1309..f97cc06c9 100644 --- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs +++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs @@ -7933,5 +7933,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 9e6e6cfb7..e14036687 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs @@ -37568,6 +37568,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 f52c483db..93332fbcd 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml @@ -13408,6 +13408,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 741cf448c..cbcd1c776 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs @@ -35483,3 +35483,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/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs index 13f7f7853..500ccf220 100644 --- a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs +++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs @@ -7867,4 +7867,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"); + } } From 9b5b441195090b641a44e848f0a55f474bd1f70b Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:39:38 -0300 Subject: [PATCH 02/11] feat(imperial-guardian): add the game logic of the imperial guardian event Co-Authored-By: Claude Opus 5.5 --- src/DataModel/Configuration/MiniGameType.cs | 5 + .../IImperialGuardianViewPlugIn.cs | 38 ++ .../ImperialGuardianContext.cs | 477 ++++++++++++++++++ .../ImperialGuardianEnterResult.cs | 46 ++ .../ImperialGuardianEventDefinition.cs | 164 ++++++ .../ImperialGuardianExperienceReward.cs | 21 + .../ImperialGuardianFeaturePlugIn.cs | 37 ++ .../ImperialGuardianFragmentDrop.cs | 21 + .../ImperialGuardian/ImperialGuardianGate.cs | 97 ++++ .../ImperialGuardianMonsterScaling.cs | 36 ++ .../ImperialGuardianResult.cs | 26 + .../ImperialGuardianTimerType.cs | 26 + .../ImperialGuardianWeather.cs | 31 ++ src/GameLogic/MiniGames/MiniGameManager.cs | 6 +- .../MiniGames/EnterImperialGuardianAction.cs | 125 +++++ .../SuspiciousScrapOfPaperStackedPlugIn.cs | 44 ++ .../Properties/PlayerMessage.Designer.cs | 9 + src/GameLogic/Properties/PlayerMessage.resx | 3 + .../Properties/PlugInResources.Designer.cs | 36 ++ src/GameLogic/Properties/PlugInResources.resx | 12 + ...perialGuardianEnterRequestHandlerPlugIn.cs | 53 ++ .../ImperialGuardianGroupHandlerPlugIn.cs | 42 ++ src/GameServer/PacketType.cs | 5 + .../Properties/PlugInResources.Designer.cs | 54 ++ .../Properties/PlugInResources.resx | 18 + .../RemoteView/MiniGames/Extensions.cs | 16 + .../MiniGames/ImperialGuardianViewPlugIn.cs | 64 +++ .../ShowMiniGameEnterResultViewPlugIn.cs | 8 + 28 files changed, 1519 insertions(+), 1 deletion(-) create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEnterResult.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianExperienceReward.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFeaturePlugIn.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianFragmentDrop.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterScaling.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianResult.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianTimerType.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianWeather.cs create mode 100644 src/GameLogic/PlayerActions/MiniGames/EnterImperialGuardianAction.cs create mode 100644 src/GameLogic/PlugIns/SuspiciousScrapOfPaperStackedPlugIn.cs create mode 100644 src/GameServer/MessageHandler/MiniGames/ImperialGuardianEnterRequestHandlerPlugIn.cs create mode 100644 src/GameServer/MessageHandler/MiniGames/ImperialGuardianGroupHandlerPlugIn.cs create mode 100644 src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs 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/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs b/src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs new file mode 100644 index 000000000..53d252942 --- /dev/null +++ b/src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs @@ -0,0 +1,38 @@ +// +// 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); +} diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs new file mode 100644 index 000000000..c1b3da499 --- /dev/null +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs @@ -0,0 +1,477 @@ +// +// 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 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 ImperialGuardianTimerType _timerType = ImperialGuardianTimerType.Standby; + private DateTime _timerEndsAtUtc; + 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._timerEndsAtUtc = 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(TerrainAttributeType.Blocked, 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); + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { + var remaining = this._timerEndsAtUtc - DateTime.UtcNow; + var type = this._timerType; + 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 >= this._timerEndsAtUtc; + 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) + { + this._timerType = type; + this._timerEndsAtUtc = DateTime.UtcNow.Add(duration); + } + + private ValueTask ShowZoneAsync(Player player) + { + var remaining = this._timerEndsAtUtc - 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)) + { + await this.AddNpcAsync(new Trap(spawnArea, definition, this.Map, new RandomAttackInRangeTrapIntelligence(this.Map))).ConfigureAwait(false); + continue; + } + + var monster = new Monster(spawnArea, definition, this.Map, this.DropGenerator, new BasicMonsterIntelligence(), 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, TerrainAttributeType.Blocked, setBlocked); + } + } + + await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p => + p.ChangeAttributesAsync(TerrainAttributeType.Blocked, 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); + } +} 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..2a7b946ab --- /dev/null +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs @@ -0,0 +1,164 @@ +// +// 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; } + + /// + /// 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 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..afb4eac6b --- /dev/null +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs @@ -0,0 +1,97 @@ +// +// 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; + return (int)this.SpawnArea.Direction 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/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/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 75e7a778e..56f98050d 100644 --- a/src/GameLogic/MiniGames/MiniGameManager.cs +++ b/src/GameLogic/MiniGames/MiniGameManager.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -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/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 875ae9c49..d22e81ca3 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 5fa889ecf..d9b33ae07 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.. /// @@ -3113,6 +3131,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 db520a2c1..992c76a27 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1308,4 +1308,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 896133d90..d8f99f047 100644 --- a/src/GameServer/Properties/PlugInResources.Designer.cs +++ b/src/GameServer/Properties/PlugInResources.Designer.cs @@ -2796,6 +2796,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 62f1adf00..a9751adda 100644 --- a/src/GameServer/Properties/PlugInResources.resx +++ b/src/GameServer/Properties/PlugInResources.resx @@ -2493,4 +2493,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 684ff9ae4..983396d73 100644 --- a/src/GameServer/RemoteView/MiniGames/Extensions.cs +++ b/src/GameServer/RemoteView/MiniGames/Extensions.cs @@ -87,4 +87,20 @@ public static DoppelgangerEnterResult.EnterResult ToDoppelgangerEnterResult(this _ => DoppelgangerEnterResult.EnterResult.Failed, }; } + + /// + /// Converts the enter result to the one of the imperial guardian event. + /// + /// The enter result. + /// The enter result of the imperial guardian event. + 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, + }; + } } diff --git a/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs b/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs new file mode 100644 index 000000000..239948b45 --- /dev/null +++ b/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs @@ -0,0 +1,64 @@ +// +// 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.MiniGames.ImperialGuardian; +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); + } + + 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 013f86cec..2a111848d 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.Undefined: throw new ArgumentException("undefined game type", nameof(miniGameType)); From 9640e27b40f6b26605a97572b34ef8542cbbc105 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:44:48 -0300 Subject: [PATCH 03/11] feat(imperial-guardian): add the data of the imperial guardian event The monster spawns of the fortress maps are spawned by the event, zone by zone. Their zone and day, which were only known from comments, are defined in one table, which is used by the map initializers and the data update. Co-Authored-By: Claude Opus 5.5 --- .../AddImperialGuardianDataUpdatePlugIn.cs | 95 ++++ .../Initialization/Updates/UpdateVersion.cs | 5 + .../Events/ImperialGuardianDay.cs | 46 ++ .../Events/ImperialGuardianInitializer.cs | 103 ++++ .../Events/ImperialGuardianSpawn.cs | 26 + .../Events/ImperialGuardianSpawns.cs | 466 ++++++++++++++++++ .../GameConfigurationInitializer.cs | 1 + .../Items/EventTicketItems.cs | 28 ++ .../Maps/FortressOfImperialGuardian1.cs | 132 +---- .../Maps/FortressOfImperialGuardian2.cs | 126 +---- .../Maps/FortressOfImperialGuardian3.cs | 128 +---- .../Maps/FortressOfImperialGuardian4.cs | 112 +---- 12 files changed, 840 insertions(+), 428 deletions(-) create mode 100644 src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianDay.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianInitializer.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawn.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs diff --git a/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs new file mode 100644 index 000000000..9fab15bb5 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs @@ -0,0 +1,95 @@ +// +// 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. +/// +[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, 24, 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; + } + } + } + + 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 3285e94f4..8bd388fef 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -599,4 +599,9 @@ public enum UpdateVersion /// The version of the . /// AddRaklionEvent = 118, + + /// + /// The version of the . + /// + AddImperialGuardianData = 119, } 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..f0ece6842 --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs @@ -0,0 +1,466 @@ +// +// 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 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)1, 0, ImperialGuardianDay.Monday), + new(101, 520, 232, 40, (Direction)1, 0, ImperialGuardianDay.Monday), + new(102, 520, 239, 40, (Direction)1, 0, ImperialGuardianDay.Monday), + new(103, 519, 225, 48, (Direction)1, 0, ImperialGuardianDay.Monday), + new(104, 519, 232, 48, (Direction)1, 0, ImperialGuardianDay.Monday), + new(105, 519, 239, 48, (Direction)1, 0, ImperialGuardianDay.Monday), + new(106, 512, 232, 45, (Direction)1, 0, ImperialGuardianDay.Monday), + new(107, 518, 187, 21, (Direction)3, 1, ImperialGuardianDay.Monday), + new(108, 518, 185, 26, (Direction)3, 1, ImperialGuardianDay.Monday), + new(109, 518, 187, 31, (Direction)3, 1, ImperialGuardianDay.Monday), + new(110, 519, 179, 19, (Direction)3, 1, ImperialGuardianDay.Monday), + new(111, 519, 177, 26, (Direction)3, 1, ImperialGuardianDay.Monday), + new(112, 519, 179, 33, (Direction)3, 1, ImperialGuardianDay.Monday), + new(113, 513, 173, 26, (Direction)3, 1, ImperialGuardianDay.Monday), + new(114, 521, 174, 89, (Direction)1, 2, ImperialGuardianDay.Monday), + new(115, 521, 186, 90, (Direction)1, 2, ImperialGuardianDay.Monday), + new(116, 521, 180, 91, (Direction)1, 2, ImperialGuardianDay.Monday), + new(117, 508, 180, 95, (Direction)1, 2, ImperialGuardianDay.Monday), + new(118, 519, 174, 95, (Direction)1, 2, ImperialGuardianDay.Monday), + new(119, 519, 178, 95, (Direction)1, 2, ImperialGuardianDay.Monday), + new(120, 519, 186, 95, (Direction)1, 2, ImperialGuardianDay.Monday), + new(121, 525, 234, 28, (Direction)1, 0, ImperialGuardianDay.Monday), + new(122, 525, 216, 80, (Direction)3, 0, ImperialGuardianDay.Monday), + new(123, 524, 233, 55, (Direction)1, 0, ImperialGuardianDay.Monday), + new(124, 526, 217, 72, (Direction)3, 0, ImperialGuardianDay.Monday), + new(125, 526, 218, 85, (Direction)3, 0, ImperialGuardianDay.Monday), + new(126, 525, 194, 25, (Direction)3, 1, ImperialGuardianDay.Monday), + new(127, 525, 154, 53, (Direction)1, 1, ImperialGuardianDay.Monday), + new(128, 524, 166, 26, (Direction)3, 1, ImperialGuardianDay.Monday), + new(129, 526, 152, 30, (Direction)1, 1, ImperialGuardianDay.Monday), + new(130, 526, 157, 30, (Direction)1, 1, ImperialGuardianDay.Monday), + new(131, 525, 180, 79, (Direction)1, 2, ImperialGuardianDay.Monday), + new(132, 526, 174, 99, (Direction)1, 2, ImperialGuardianDay.Monday), + new(133, 526, 186, 100, (Direction)1, 2, ImperialGuardianDay.Monday), + new(200, 520, 225, 40, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(201, 520, 232, 40, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(202, 520, 239, 40, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(203, 519, 225, 48, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(204, 519, 232, 48, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(205, 519, 239, 48, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(206, 512, 232, 45, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(207, 518, 187, 21, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(208, 518, 185, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(209, 518, 187, 31, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(210, 519, 179, 19, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(211, 519, 177, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(212, 519, 179, 33, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(213, 513, 173, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(214, 521, 174, 89, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(215, 521, 186, 90, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(216, 521, 180, 91, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(217, 511, 180, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(218, 519, 174, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(219, 519, 178, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(220, 519, 186, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(221, 525, 234, 28, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(222, 525, 216, 80, (Direction)3, 0, ImperialGuardianDay.Thursday), + new(223, 524, 233, 55, (Direction)1, 0, ImperialGuardianDay.Thursday), + new(224, 526, 217, 72, (Direction)3, 0, ImperialGuardianDay.Thursday), + new(225, 526, 218, 85, (Direction)3, 0, ImperialGuardianDay.Thursday), + new(226, 525, 194, 25, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(227, 525, 154, 53, (Direction)1, 1, ImperialGuardianDay.Thursday), + new(228, 524, 166, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), + new(229, 526, 152, 30, (Direction)1, 1, ImperialGuardianDay.Thursday), + new(230, 526, 157, 30, (Direction)1, 1, ImperialGuardianDay.Thursday), + new(231, 525, 180, 79, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(232, 526, 174, 99, (Direction)1, 2, ImperialGuardianDay.Thursday), + new(233, 526, 186, 100, (Direction)1, 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)3, 0, ImperialGuardianDay.Tuesday), + new(101, 520, 64, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(102, 520, 66, 70, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(103, 519, 56, 58, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(104, 519, 56, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(105, 519, 56, 71, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(106, 514, 59, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(107, 518, 32, 99, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(108, 518, 42, 99, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(109, 518, 38, 101, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(110, 519, 33, 107, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(111, 519, 44, 107, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(112, 519, 39, 110, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(113, 515, 38, 106, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(114, 521, 98, 107, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(115, 521, 96, 112, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(116, 521, 98, 117, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(117, 519, 93, 105, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(118, 519, 92, 112, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(119, 519, 93, 118, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(120, 509, 89, 111, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(121, 525, 75, 67, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(122, 525, 19, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(123, 524, 50, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(124, 526, 24, 67, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(125, 526, 24, 62, (Direction)3, 0, ImperialGuardianDay.Tuesday), + new(126, 525, 37, 93, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(127, 525, 55, 154, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(128, 524, 41, 117, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(129, 526, 53, 152, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(130, 526, 56, 152, (Direction)1, 1, ImperialGuardianDay.Tuesday), + new(131, 525, 107, 112, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(132, 526, 85, 113, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(133, 526, 85, 107, (Direction)3, 2, ImperialGuardianDay.Tuesday), + new(134, 520, 66, 59, (Direction)3, 0, ImperialGuardianDay.Friday), + new(135, 520, 64, 65, (Direction)3, 0, ImperialGuardianDay.Friday), + new(136, 520, 66, 70, (Direction)3, 0, ImperialGuardianDay.Friday), + new(137, 519, 56, 58, (Direction)3, 0, ImperialGuardianDay.Friday), + new(138, 519, 56, 65, (Direction)3, 0, ImperialGuardianDay.Friday), + new(139, 519, 56, 71, (Direction)3, 0, ImperialGuardianDay.Friday), + new(140, 514, 59, 65, (Direction)3, 0, ImperialGuardianDay.Friday), + new(141, 518, 32, 99, (Direction)1, 1, ImperialGuardianDay.Friday), + new(142, 518, 42, 99, (Direction)1, 1, ImperialGuardianDay.Friday), + new(143, 518, 38, 101, (Direction)1, 1, ImperialGuardianDay.Friday), + new(144, 519, 33, 107, (Direction)1, 1, ImperialGuardianDay.Friday), + new(145, 519, 44, 107, (Direction)1, 1, ImperialGuardianDay.Friday), + new(146, 519, 39, 110, (Direction)1, 1, ImperialGuardianDay.Friday), + new(147, 515, 38, 106, (Direction)1, 1, ImperialGuardianDay.Friday), + new(148, 521, 98, 107, (Direction)3, 2, ImperialGuardianDay.Friday), + new(149, 521, 96, 112, (Direction)3, 2, ImperialGuardianDay.Friday), + new(150, 521, 98, 117, (Direction)3, 2, ImperialGuardianDay.Friday), + new(151, 519, 93, 105, (Direction)3, 2, ImperialGuardianDay.Friday), + new(152, 519, 92, 112, (Direction)3, 2, ImperialGuardianDay.Friday), + new(153, 519, 93, 118, (Direction)3, 2, ImperialGuardianDay.Friday), + new(154, 507, 89, 111, (Direction)3, 2, ImperialGuardianDay.Friday), + new(155, 525, 75, 67, (Direction)3, 0, ImperialGuardianDay.Friday), + new(156, 525, 19, 65, (Direction)3, 0, ImperialGuardianDay.Friday), + new(157, 524, 50, 65, (Direction)3, 0, ImperialGuardianDay.Friday), + new(158, 526, 24, 67, (Direction)3, 0, ImperialGuardianDay.Friday), + new(159, 526, 24, 62, (Direction)3, 0, ImperialGuardianDay.Friday), + new(160, 525, 37, 93, (Direction)1, 1, ImperialGuardianDay.Friday), + new(161, 525, 55, 154, (Direction)1, 1, ImperialGuardianDay.Friday), + new(162, 524, 41, 117, (Direction)1, 1, ImperialGuardianDay.Friday), + new(163, 526, 53, 152, (Direction)1, 1, ImperialGuardianDay.Friday), + new(164, 526, 56, 152, (Direction)1, 1, ImperialGuardianDay.Friday), + new(165, 525, 107, 112, (Direction)3, 2, ImperialGuardianDay.Friday), + new(166, 526, 85, 113, (Direction)3, 2, ImperialGuardianDay.Friday), + new(167, 526, 85, 107, (Direction)3, 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)3, 0, ImperialGuardianDay.Wednesday), + new(101, 520, 131, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(102, 520, 132, 199, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(103, 519, 126, 185, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(104, 519, 124, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(105, 519, 126, 199, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(106, 516, 127, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(107, 518, 217, 140, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(108, 518, 228, 140, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(109, 518, 222, 142, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(110, 519, 218, 148, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(111, 519, 222, 152, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(112, 519, 227, 148, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(113, 517, 222, 147, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(114, 521, 162, 226, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(115, 521, 166, 228, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(116, 521, 171, 226, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(117, 519, 161, 231, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(118, 519, 164, 233, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(119, 519, 171, 233, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(120, 510, 166, 233, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(121, 525, 146, 191, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(122, 525, 89, 195, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(123, 524, 119, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(124, 526, 111, 197, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(125, 526, 111, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), + new(126, 525, 222, 134, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(127, 525, 223, 193, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(128, 524, 222, 160, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(129, 526, 220, 173, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(130, 526, 224, 173, (Direction)1, 1, ImperialGuardianDay.Wednesday), + new(131, 525, 167, 217, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(132, 526, 158, 236, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(133, 526, 175, 238, (Direction)1, 2, ImperialGuardianDay.Wednesday), + new(134, 520, 132, 185, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(135, 520, 131, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(136, 520, 132, 199, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(137, 519, 126, 185, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(138, 519, 124, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(139, 519, 126, 199, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(140, 516, 127, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(141, 518, 217, 140, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(142, 518, 228, 140, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(143, 518, 222, 142, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(144, 519, 218, 148, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(145, 519, 222, 152, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(146, 519, 227, 148, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(147, 517, 222, 147, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(148, 521, 162, 226, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(149, 521, 166, 228, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(150, 521, 171, 226, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(151, 519, 161, 231, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(152, 519, 164, 233, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(153, 519, 171, 233, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(154, 506, 166, 233, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(155, 525, 146, 191, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(156, 525, 89, 195, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(157, 524, 119, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(158, 526, 111, 197, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(159, 526, 111, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), + new(160, 525, 222, 134, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(161, 525, 223, 193, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(162, 524, 222, 160, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(163, 526, 220, 173, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(164, 526, 224, 173, (Direction)1, 1, ImperialGuardianDay.Saturday), + new(165, 525, 167, 217, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(166, 526, 158, 236, (Direction)1, 2, ImperialGuardianDay.Saturday), + new(167, 526, 175, 238, (Direction)1, 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)3, 0, ImperialGuardianDay.Sunday), + new(101, 518, 64, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(102, 518, 64, 73, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(103, 519, 61, 64, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(104, 519, 61, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(105, 519, 61, 74, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(106, 521, 55, 71, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(107, 521, 55, 66, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(108, 521, 54, 68, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(109, 507, 57, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(110, 518, 29, 186, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(111, 518, 34, 187, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(112, 518, 39, 186, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(113, 519, 34, 194, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(114, 519, 29, 192, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(115, 519, 39, 192, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(116, 521, 26, 191, (Direction)2, 1, ImperialGuardianDay.Sunday), + new(117, 521, 32, 195, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(118, 521, 39, 196, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(119, 506, 36, 191, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(120, 518, 173, 129, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(121, 518, 170, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(122, 518, 173, 137, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(123, 519, 181, 129, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(124, 519, 181, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(125, 519, 181, 137, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(126, 521, 184, 129, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(127, 521, 184, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(128, 521, 184, 137, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(129, 505, 177, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(130, 521, 183, 29, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(131, 521, 183, 21, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(132, 521, 187, 32, (Direction)1, 3, ImperialGuardianDay.Sunday), + new(133, 521, 187, 20, (Direction)5, 3, ImperialGuardianDay.Sunday), + new(134, 518, 192, 31, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(135, 518, 192, 21, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(136, 518, 194, 26, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(137, 519, 189, 21, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(138, 519, 189, 26, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(139, 519, 189, 31, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(140, 504, 183, 24, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(141, 528, 81, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(142, 528, 32, 90, (Direction)1, 0, ImperialGuardianDay.Sunday), + new(143, 527, 50, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), + new(144, 526, 52, 77, (Direction)2, 0, ImperialGuardianDay.Sunday), + new(145, 526, 53, 61, (Direction)4, 0, ImperialGuardianDay.Sunday), + new(146, 528, 34, 176, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(147, 528, 69, 166, (Direction)5, 1, ImperialGuardianDay.Sunday), + new(148, 527, 52, 191, (Direction)7, 1, ImperialGuardianDay.Sunday), + new(149, 526, 21, 198, (Direction)2, 1, ImperialGuardianDay.Sunday), + new(150, 526, 47, 199, (Direction)1, 1, ImperialGuardianDay.Sunday), + new(151, 528, 156, 132, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(152, 528, 224, 159, (Direction)1, 2, ImperialGuardianDay.Sunday), + new(153, 527, 197, 132, (Direction)7, 2, ImperialGuardianDay.Sunday), + new(154, 526, 161, 139, (Direction)1, 2, ImperialGuardianDay.Sunday), + new(155, 526, 161, 127, (Direction)5, 2, ImperialGuardianDay.Sunday), + new(156, 528, 214, 24, (Direction)3, 3, ImperialGuardianDay.Sunday), + new(157, 526, 207, 32, (Direction)1, 3, ImperialGuardianDay.Sunday), + new(158, 526, 207, 20, (Direction)5, 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 From 982366f502e0f9d0fe241cb53d5366f7ac682263 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:50:36 -0300 Subject: [PATCH 04/11] test(imperial-guardian): cover the definition, the packets, the tickets and the data Co-Authored-By: Claude Opus 5.5 --- .../ImperialGuardianDataTest.cs | 126 ++++++++++++++++++ .../ImperialGuardianEventDefinitionTest.cs | 98 ++++++++++++++ .../ImperialGuardianRemoteViewTests.cs | 48 +++++++ ...SuspiciousScrapOfPaperStackedPlugInTest.cs | 60 +++++++++ 4 files changed, 332 insertions(+) create mode 100644 tests/MUnique.OpenMU.Persistence.Initialization.Tests/ImperialGuardianDataTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/ImperialGuardianRemoteViewTests.cs create mode 100644 tests/MUnique.OpenMU.Tests/SuspiciousScrapOfPaperStackedPlugInTest.cs 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..eca2563e9 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs @@ -0,0 +1,98 @@ +// +// 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 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); + } +} From 9fdfb83f69dfad8bf74cfc88e2c7255fcf3d5887 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:01:38 -0300 Subject: [PATCH 05/11] feat(imperial-guardian): let the monsters use their skills and the bosses get into rage The monsters use the skills of the original game with their animations, which hit the target or an area and may stun. The rage of the bosses is configurable and disabled by default. Co-Authored-By: Claude Opus 5.5 --- src/GameLogic/AttackableExtensions.cs | 20 +++ .../IImperialGuardianViewPlugIn.cs | 8 ++ .../ImperialGuardianContext.cs | 2 +- .../ImperialGuardianEventDefinition.cs | 91 ++++++++++++++ .../ImperialGuardianMonsterIntelligence.cs | 115 ++++++++++++++++++ .../ImperialGuardianMonsterSkill.cs | 78 ++++++++++++ .../MiniGames/ImperialGuardianViewPlugIn.cs | 9 ++ .../ImperialGuardianEventDefinitionTest.cs | 15 +++ 8 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterIntelligence.cs create mode 100644 src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianMonsterSkill.cs 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 index 53d252942..30fdccad4 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/IImperialGuardianViewPlugIn.cs @@ -35,4 +35,12 @@ public interface IImperialGuardianViewPlugIn : IViewPlugIn /// 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 index c1b3da499..b4563adfa 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs @@ -359,7 +359,7 @@ private async ValueTask SpawnMonstersAsync(int zone) continue; } - var monster = new Monster(spawnArea, definition, this.Map, this.DropGenerator, new BasicMonsterIntelligence(), this._gameContext.PlugInManager, this._gameContext.PathFinderPool, this); + 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)) { diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs index 2a7b946ab..c9a301d74 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs @@ -106,6 +106,97 @@ public class ImperialGuardianEventDefinition /// 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. /// 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/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/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs b/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs index 239948b45..418d95b45 100644 --- a/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs +++ b/src/GameServer/RemoteView/MiniGames/ImperialGuardianViewPlugIn.cs @@ -5,7 +5,9 @@ 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; @@ -57,6 +59,13 @@ await this._player.Connection.SendImperialGuardianResultAsync( (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/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs b/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs index eca2563e9..f3b01dad8 100644 --- a/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs +++ b/tests/MUnique.OpenMU.Tests/ImperialGuardianEventDefinitionTest.cs @@ -79,6 +79,21 @@ public void MonsterScaling() 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. /// From 4f7be714cd12bf0181139609faafd59fc2fcdd50 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:42:42 -0300 Subject: [PATCH 06/11] fix(imperial-guardian): show the gates properly and open them for the client The directions of the spawns were the ones of the game client, but OpenMU uses them plus one, so the gates were rotated. The terrain of the client already blocks the gates with the water attribute, like the original server does, so this attribute has to be removed to open a gate. Co-Authored-By: Claude Opus 5.5 --- .../ImperialGuardianContext.cs | 12 +- .../ImperialGuardian/ImperialGuardianGate.cs | 4 +- .../AddImperialGuardianDataUpdatePlugIn.cs | 3 +- .../Events/ImperialGuardianSpawns.cs | 527 +++++++++--------- 4 files changed, 278 insertions(+), 268 deletions(-) diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs index b4563adfa..8d9582396 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs @@ -23,6 +23,12 @@ namespace MUnique.OpenMU.GameLogic.MiniGames.ImperialGuardian; /// 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. /// @@ -132,7 +138,7 @@ protected override async ValueTask OnObjectAddedToMapAsync((GameMap Map, ILocate 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(TerrainAttributeType.Blocked, true, blockedAreas)).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ChangeAttributesAsync(GateTerrainAttribute, true, blockedAreas)).ConfigureAwait(false); } await this.ShowZoneAsync(player).ConfigureAwait(false); @@ -435,12 +441,12 @@ private async ValueTask ChangeTerrainAsync((byte StartX, byte StartY, byte EndX, { for (var y = area.StartY; y <= area.EndY; y++) { - this.Map.Terrain.ApplyTerrainAttribute(x, y, TerrainAttributeType.Blocked, setBlocked); + this.Map.Terrain.ApplyTerrainAttribute(x, y, GateTerrainAttribute, setBlocked); } } await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p => - p.ChangeAttributesAsync(TerrainAttributeType.Blocked, setBlocked, [area])).AsTask()).ConfigureAwait(false); + p.ChangeAttributesAsync(GateTerrainAttribute, setBlocked, [area])).AsTask()).ConfigureAwait(false); } private async Task DropFragmentsAsync(Monster boss, DeathInformation e) diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs index afb4eac6b..f6d9e1bbc 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianGate.cs @@ -54,7 +54,9 @@ public bool IsAttackable { var x = this.Position.X; var y = this.Position.Y; - return (int)this.SpawnArea.Direction switch + + // 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), diff --git a/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs index 9fab15bb5..a7d577700 100644 --- a/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs +++ b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs @@ -18,7 +18,7 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; /// 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. +/// by the event, zone by zone, and their directions are corrected, so that the gates are shown properly. /// [PlugIn] [Display(Name = PlugInName, Description = PlugInDescription)] @@ -83,6 +83,7 @@ protected override ValueTask ApplyAsync(IContext context, GameConfiguration game { spawnArea.SpawnTrigger = SpawnTrigger.OnceAtWaveStart; spawnArea.WaveNumber = spawn.WaveNumber; + spawnArea.Direction = spawn.Direction; } } } diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs index f0ece6842..129a2b120 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/ImperialGuardianSpawns.cs @@ -11,80 +11,81 @@ namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events; /// 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)1, 0, ImperialGuardianDay.Monday), - new(101, 520, 232, 40, (Direction)1, 0, ImperialGuardianDay.Monday), - new(102, 520, 239, 40, (Direction)1, 0, ImperialGuardianDay.Monday), - new(103, 519, 225, 48, (Direction)1, 0, ImperialGuardianDay.Monday), - new(104, 519, 232, 48, (Direction)1, 0, ImperialGuardianDay.Monday), - new(105, 519, 239, 48, (Direction)1, 0, ImperialGuardianDay.Monday), - new(106, 512, 232, 45, (Direction)1, 0, ImperialGuardianDay.Monday), - new(107, 518, 187, 21, (Direction)3, 1, ImperialGuardianDay.Monday), - new(108, 518, 185, 26, (Direction)3, 1, ImperialGuardianDay.Monday), - new(109, 518, 187, 31, (Direction)3, 1, ImperialGuardianDay.Monday), - new(110, 519, 179, 19, (Direction)3, 1, ImperialGuardianDay.Monday), - new(111, 519, 177, 26, (Direction)3, 1, ImperialGuardianDay.Monday), - new(112, 519, 179, 33, (Direction)3, 1, ImperialGuardianDay.Monday), - new(113, 513, 173, 26, (Direction)3, 1, ImperialGuardianDay.Monday), - new(114, 521, 174, 89, (Direction)1, 2, ImperialGuardianDay.Monday), - new(115, 521, 186, 90, (Direction)1, 2, ImperialGuardianDay.Monday), - new(116, 521, 180, 91, (Direction)1, 2, ImperialGuardianDay.Monday), - new(117, 508, 180, 95, (Direction)1, 2, ImperialGuardianDay.Monday), - new(118, 519, 174, 95, (Direction)1, 2, ImperialGuardianDay.Monday), - new(119, 519, 178, 95, (Direction)1, 2, ImperialGuardianDay.Monday), - new(120, 519, 186, 95, (Direction)1, 2, ImperialGuardianDay.Monday), - new(121, 525, 234, 28, (Direction)1, 0, ImperialGuardianDay.Monday), - new(122, 525, 216, 80, (Direction)3, 0, ImperialGuardianDay.Monday), - new(123, 524, 233, 55, (Direction)1, 0, ImperialGuardianDay.Monday), - new(124, 526, 217, 72, (Direction)3, 0, ImperialGuardianDay.Monday), - new(125, 526, 218, 85, (Direction)3, 0, ImperialGuardianDay.Monday), - new(126, 525, 194, 25, (Direction)3, 1, ImperialGuardianDay.Monday), - new(127, 525, 154, 53, (Direction)1, 1, ImperialGuardianDay.Monday), - new(128, 524, 166, 26, (Direction)3, 1, ImperialGuardianDay.Monday), - new(129, 526, 152, 30, (Direction)1, 1, ImperialGuardianDay.Monday), - new(130, 526, 157, 30, (Direction)1, 1, ImperialGuardianDay.Monday), - new(131, 525, 180, 79, (Direction)1, 2, ImperialGuardianDay.Monday), - new(132, 526, 174, 99, (Direction)1, 2, ImperialGuardianDay.Monday), - new(133, 526, 186, 100, (Direction)1, 2, ImperialGuardianDay.Monday), - new(200, 520, 225, 40, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(201, 520, 232, 40, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(202, 520, 239, 40, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(203, 519, 225, 48, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(204, 519, 232, 48, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(205, 519, 239, 48, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(206, 512, 232, 45, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(207, 518, 187, 21, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(208, 518, 185, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(209, 518, 187, 31, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(210, 519, 179, 19, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(211, 519, 177, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(212, 519, 179, 33, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(213, 513, 173, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(214, 521, 174, 89, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(215, 521, 186, 90, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(216, 521, 180, 91, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(217, 511, 180, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(218, 519, 174, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(219, 519, 178, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(220, 519, 186, 95, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(221, 525, 234, 28, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(222, 525, 216, 80, (Direction)3, 0, ImperialGuardianDay.Thursday), - new(223, 524, 233, 55, (Direction)1, 0, ImperialGuardianDay.Thursday), - new(224, 526, 217, 72, (Direction)3, 0, ImperialGuardianDay.Thursday), - new(225, 526, 218, 85, (Direction)3, 0, ImperialGuardianDay.Thursday), - new(226, 525, 194, 25, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(227, 525, 154, 53, (Direction)1, 1, ImperialGuardianDay.Thursday), - new(228, 524, 166, 26, (Direction)3, 1, ImperialGuardianDay.Thursday), - new(229, 526, 152, 30, (Direction)1, 1, ImperialGuardianDay.Thursday), - new(230, 526, 157, 30, (Direction)1, 1, ImperialGuardianDay.Thursday), - new(231, 525, 180, 79, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(232, 526, 174, 99, (Direction)1, 2, ImperialGuardianDay.Thursday), - new(233, 526, 186, 100, (Direction)1, 2, ImperialGuardianDay.Thursday), + 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), @@ -131,74 +132,74 @@ internal static class ImperialGuardianSpawns private static readonly ImperialGuardianSpawn[] Map70Spawns = [ - new(100, 520, 66, 59, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(101, 520, 64, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(102, 520, 66, 70, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(103, 519, 56, 58, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(104, 519, 56, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(105, 519, 56, 71, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(106, 514, 59, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(107, 518, 32, 99, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(108, 518, 42, 99, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(109, 518, 38, 101, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(110, 519, 33, 107, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(111, 519, 44, 107, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(112, 519, 39, 110, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(113, 515, 38, 106, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(114, 521, 98, 107, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(115, 521, 96, 112, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(116, 521, 98, 117, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(117, 519, 93, 105, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(118, 519, 92, 112, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(119, 519, 93, 118, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(120, 509, 89, 111, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(121, 525, 75, 67, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(122, 525, 19, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(123, 524, 50, 65, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(124, 526, 24, 67, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(125, 526, 24, 62, (Direction)3, 0, ImperialGuardianDay.Tuesday), - new(126, 525, 37, 93, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(127, 525, 55, 154, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(128, 524, 41, 117, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(129, 526, 53, 152, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(130, 526, 56, 152, (Direction)1, 1, ImperialGuardianDay.Tuesday), - new(131, 525, 107, 112, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(132, 526, 85, 113, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(133, 526, 85, 107, (Direction)3, 2, ImperialGuardianDay.Tuesday), - new(134, 520, 66, 59, (Direction)3, 0, ImperialGuardianDay.Friday), - new(135, 520, 64, 65, (Direction)3, 0, ImperialGuardianDay.Friday), - new(136, 520, 66, 70, (Direction)3, 0, ImperialGuardianDay.Friday), - new(137, 519, 56, 58, (Direction)3, 0, ImperialGuardianDay.Friday), - new(138, 519, 56, 65, (Direction)3, 0, ImperialGuardianDay.Friday), - new(139, 519, 56, 71, (Direction)3, 0, ImperialGuardianDay.Friday), - new(140, 514, 59, 65, (Direction)3, 0, ImperialGuardianDay.Friday), - new(141, 518, 32, 99, (Direction)1, 1, ImperialGuardianDay.Friday), - new(142, 518, 42, 99, (Direction)1, 1, ImperialGuardianDay.Friday), - new(143, 518, 38, 101, (Direction)1, 1, ImperialGuardianDay.Friday), - new(144, 519, 33, 107, (Direction)1, 1, ImperialGuardianDay.Friday), - new(145, 519, 44, 107, (Direction)1, 1, ImperialGuardianDay.Friday), - new(146, 519, 39, 110, (Direction)1, 1, ImperialGuardianDay.Friday), - new(147, 515, 38, 106, (Direction)1, 1, ImperialGuardianDay.Friday), - new(148, 521, 98, 107, (Direction)3, 2, ImperialGuardianDay.Friday), - new(149, 521, 96, 112, (Direction)3, 2, ImperialGuardianDay.Friday), - new(150, 521, 98, 117, (Direction)3, 2, ImperialGuardianDay.Friday), - new(151, 519, 93, 105, (Direction)3, 2, ImperialGuardianDay.Friday), - new(152, 519, 92, 112, (Direction)3, 2, ImperialGuardianDay.Friday), - new(153, 519, 93, 118, (Direction)3, 2, ImperialGuardianDay.Friday), - new(154, 507, 89, 111, (Direction)3, 2, ImperialGuardianDay.Friday), - new(155, 525, 75, 67, (Direction)3, 0, ImperialGuardianDay.Friday), - new(156, 525, 19, 65, (Direction)3, 0, ImperialGuardianDay.Friday), - new(157, 524, 50, 65, (Direction)3, 0, ImperialGuardianDay.Friday), - new(158, 526, 24, 67, (Direction)3, 0, ImperialGuardianDay.Friday), - new(159, 526, 24, 62, (Direction)3, 0, ImperialGuardianDay.Friday), - new(160, 525, 37, 93, (Direction)1, 1, ImperialGuardianDay.Friday), - new(161, 525, 55, 154, (Direction)1, 1, ImperialGuardianDay.Friday), - new(162, 524, 41, 117, (Direction)1, 1, ImperialGuardianDay.Friday), - new(163, 526, 53, 152, (Direction)1, 1, ImperialGuardianDay.Friday), - new(164, 526, 56, 152, (Direction)1, 1, ImperialGuardianDay.Friday), - new(165, 525, 107, 112, (Direction)3, 2, ImperialGuardianDay.Friday), - new(166, 526, 85, 113, (Direction)3, 2, ImperialGuardianDay.Friday), - new(167, 526, 85, 107, (Direction)3, 2, ImperialGuardianDay.Friday), + 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), @@ -241,74 +242,74 @@ internal static class ImperialGuardianSpawns private static readonly ImperialGuardianSpawn[] Map71Spawns = [ - new(100, 520, 132, 185, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(101, 520, 131, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(102, 520, 132, 199, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(103, 519, 126, 185, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(104, 519, 124, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(105, 519, 126, 199, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(106, 516, 127, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(107, 518, 217, 140, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(108, 518, 228, 140, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(109, 518, 222, 142, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(110, 519, 218, 148, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(111, 519, 222, 152, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(112, 519, 227, 148, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(113, 517, 222, 147, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(114, 521, 162, 226, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(115, 521, 166, 228, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(116, 521, 171, 226, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(117, 519, 161, 231, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(118, 519, 164, 233, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(119, 519, 171, 233, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(120, 510, 166, 233, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(121, 525, 146, 191, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(122, 525, 89, 195, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(123, 524, 119, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(124, 526, 111, 197, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(125, 526, 111, 192, (Direction)3, 0, ImperialGuardianDay.Wednesday), - new(126, 525, 222, 134, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(127, 525, 223, 193, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(128, 524, 222, 160, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(129, 526, 220, 173, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(130, 526, 224, 173, (Direction)1, 1, ImperialGuardianDay.Wednesday), - new(131, 525, 167, 217, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(132, 526, 158, 236, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(133, 526, 175, 238, (Direction)1, 2, ImperialGuardianDay.Wednesday), - new(134, 520, 132, 185, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(135, 520, 131, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(136, 520, 132, 199, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(137, 519, 126, 185, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(138, 519, 124, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(139, 519, 126, 199, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(140, 516, 127, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(141, 518, 217, 140, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(142, 518, 228, 140, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(143, 518, 222, 142, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(144, 519, 218, 148, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(145, 519, 222, 152, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(146, 519, 227, 148, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(147, 517, 222, 147, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(148, 521, 162, 226, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(149, 521, 166, 228, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(150, 521, 171, 226, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(151, 519, 161, 231, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(152, 519, 164, 233, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(153, 519, 171, 233, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(154, 506, 166, 233, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(155, 525, 146, 191, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(156, 525, 89, 195, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(157, 524, 119, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(158, 526, 111, 197, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(159, 526, 111, 192, (Direction)3, 0, ImperialGuardianDay.Saturday), - new(160, 525, 222, 134, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(161, 525, 223, 193, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(162, 524, 222, 160, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(163, 526, 220, 173, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(164, 526, 224, 173, (Direction)1, 1, ImperialGuardianDay.Saturday), - new(165, 525, 167, 217, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(166, 526, 158, 236, (Direction)1, 2, ImperialGuardianDay.Saturday), - new(167, 526, 175, 238, (Direction)1, 2, ImperialGuardianDay.Saturday), + 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), @@ -353,65 +354,65 @@ internal static class ImperialGuardianSpawns private static readonly ImperialGuardianSpawn[] Map72Spawns = [ - new(100, 518, 64, 65, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(101, 518, 64, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(102, 518, 64, 73, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(103, 519, 61, 64, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(104, 519, 61, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(105, 519, 61, 74, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(106, 521, 55, 71, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(107, 521, 55, 66, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(108, 521, 54, 68, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(109, 507, 57, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(110, 518, 29, 186, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(111, 518, 34, 187, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(112, 518, 39, 186, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(113, 519, 34, 194, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(114, 519, 29, 192, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(115, 519, 39, 192, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(116, 521, 26, 191, (Direction)2, 1, ImperialGuardianDay.Sunday), - new(117, 521, 32, 195, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(118, 521, 39, 196, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(119, 506, 36, 191, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(120, 518, 173, 129, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(121, 518, 170, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(122, 518, 173, 137, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(123, 519, 181, 129, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(124, 519, 181, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(125, 519, 181, 137, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(126, 521, 184, 129, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(127, 521, 184, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(128, 521, 184, 137, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(129, 505, 177, 133, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(130, 521, 183, 29, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(131, 521, 183, 21, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(132, 521, 187, 32, (Direction)1, 3, ImperialGuardianDay.Sunday), - new(133, 521, 187, 20, (Direction)5, 3, ImperialGuardianDay.Sunday), - new(134, 518, 192, 31, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(135, 518, 192, 21, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(136, 518, 194, 26, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(137, 519, 189, 21, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(138, 519, 189, 26, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(139, 519, 189, 31, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(140, 504, 183, 24, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(141, 528, 81, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(142, 528, 32, 90, (Direction)1, 0, ImperialGuardianDay.Sunday), - new(143, 527, 50, 69, (Direction)3, 0, ImperialGuardianDay.Sunday), - new(144, 526, 52, 77, (Direction)2, 0, ImperialGuardianDay.Sunday), - new(145, 526, 53, 61, (Direction)4, 0, ImperialGuardianDay.Sunday), - new(146, 528, 34, 176, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(147, 528, 69, 166, (Direction)5, 1, ImperialGuardianDay.Sunday), - new(148, 527, 52, 191, (Direction)7, 1, ImperialGuardianDay.Sunday), - new(149, 526, 21, 198, (Direction)2, 1, ImperialGuardianDay.Sunday), - new(150, 526, 47, 199, (Direction)1, 1, ImperialGuardianDay.Sunday), - new(151, 528, 156, 132, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(152, 528, 224, 159, (Direction)1, 2, ImperialGuardianDay.Sunday), - new(153, 527, 197, 132, (Direction)7, 2, ImperialGuardianDay.Sunday), - new(154, 526, 161, 139, (Direction)1, 2, ImperialGuardianDay.Sunday), - new(155, 526, 161, 127, (Direction)5, 2, ImperialGuardianDay.Sunday), - new(156, 528, 214, 24, (Direction)3, 3, ImperialGuardianDay.Sunday), - new(157, 526, 207, 32, (Direction)1, 3, ImperialGuardianDay.Sunday), - new(158, 526, 207, 20, (Direction)5, 3, ImperialGuardianDay.Sunday), + 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), From ee9258f9d0683d147c909d852ee4ed6756b42f50 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:42:49 -0300 Subject: [PATCH 07/11] fix(imperial-guardian): hide the traps and show the countdown after a skipped wait The game client doesn't know the trap monster and shows a default model for it, so the traps are invisible like in the original game. The timer of the client follows the countdown, when the entering phase was skipped. Co-Authored-By: Claude Opus 5.5 --- .../ImperialGuardian/ImperialGuardianContext.cs | 12 +++++++++++- src/GameLogic/NPC/NonPlayerCharacter.cs | 6 ++++++ src/GameLogic/ObserverToWorldViewAdapter.cs | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs index 8d9582396..91dcecca5 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs @@ -166,8 +166,16 @@ private async Task RunTimerLoopAsync(CancellationToken cancellationToken) { 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 remaining = this._timerEndsAtUtc - DateTime.UtcNow; var type = this._timerType; var monsterCount = this.RemainingMonsterCount; @@ -361,7 +369,9 @@ private async ValueTask SpawnMonstersAsync(int zone) { if (this._definition.TrapNumbers.Contains(definition.Number)) { - await this.AddNpcAsync(new Trap(spawnArea, definition, this.Map, new RandomAttackInRangeTrapIntelligence(this.Map))).ConfigureAwait(false); + // 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; } 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()) { From 0103eeed5320d17d1fa9e71f2236765a0ffb715c Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:00:16 -0300 Subject: [PATCH 08/11] fix(imperial-guardian): replace the timer state as a whole The timer is set by the zones and read by the timer loop, which run in different tasks. Its type and end are now replaced together and read with Volatile, like the zone, so they always match. Co-Authored-By: Claude Opus 5.5 --- .../ImperialGuardianContext.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs index 91dcecca5..b9a7bc9f5 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianContext.cs @@ -48,8 +48,7 @@ public sealed class ImperialGuardianContext : MiniGameContext private readonly ConcurrentDictionary _gates = new(); private int _zone; - private ImperialGuardianTimerType _timerType = ImperialGuardianTimerType.Standby; - private DateTime _timerEndsAtUtc; + private TimerState _timer; private ImperialGuardianMonsterScaling? _monsterScaling; /// @@ -77,7 +76,7 @@ public ImperialGuardianContext( .DefaultIfEmpty(0) .Max()); this._weather = (ImperialGuardianWeather)Rand.NextInt(0, 4); - this._timerEndsAtUtc = this.EnterEndsAtUtc.Add(CountdownDuration); + this._timer = new TimerState(ImperialGuardianTimerType.Standby, this.EnterEndsAtUtc.Add(CountdownDuration)); _ = Task.Run(() => this.RunTimerLoopAsync(this.GameEndedToken), this.GameEndedToken); } @@ -176,8 +175,9 @@ private async Task RunTimerLoopAsync(CancellationToken cancellationToken) this.SetTimer(ImperialGuardianTimerType.Standby, CountdownDuration); } - var remaining = this._timerEndsAtUtc - DateTime.UtcNow; - var type = this._timerType; + 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); @@ -256,7 +256,7 @@ private async ValueTask WaitUntilZoneIsClearedAsync(bool isLastZone, Cance } var isCleared = this.RemainingMonsterCount == 0; - var isTimeOver = DateTime.UtcNow >= this._timerEndsAtUtc; + var isTimeOver = DateTime.UtcNow >= Volatile.Read(ref this._timer).EndsAtUtc; if (isCleared && (isLastZone || this._definition.StartNextZoneWhenCleared || isTimeOver)) { return true; @@ -300,13 +300,12 @@ await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowEnterResultAsync(ImperialGuardianEnterResult.Success, this.Day, this.Zone, this._weather, remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero)); } @@ -490,4 +489,11 @@ private async ValueTask DropFragmentItemsAsync(Monster boss, DeathInformation e) 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); } From 5ed5dde79bce009b56a9d48c10373de2c69f9c24 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:01:07 -0300 Subject: [PATCH 09/11] docs(imperial-guardian): describe the enter results which the client doesn't know Co-Authored-By: Claude Opus 5.5 --- src/GameServer/RemoteView/MiniGames/Extensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/GameServer/RemoteView/MiniGames/Extensions.cs b/src/GameServer/RemoteView/MiniGames/Extensions.cs index 983396d73..c5e4ccdb7 100644 --- a/src/GameServer/RemoteView/MiniGames/Extensions.cs +++ b/src/GameServer/RemoteView/MiniGames/Extensions.cs @@ -93,6 +93,10 @@ public static DoppelgangerEnterResult.EnterResult ToDoppelgangerEnterResult(this /// /// 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 From 575400989b6ad932864815e633f37db8f4b6978b Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:08:44 -0300 Subject: [PATCH 10/11] feat(imperial-guardian): require a party by default, like the original game Co-Authored-By: Claude Opus 5.5 --- .../ImperialGuardian/ImperialGuardianEventDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs index c9a301d74..806107187 100644 --- a/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs +++ b/src/GameLogic/MiniGames/ImperialGuardian/ImperialGuardianEventDefinition.cs @@ -40,7 +40,7 @@ public class ImperialGuardianEventDefinition /// /// Gets or sets a value indicating whether the event can only be entered as member of a party. /// - public bool IsPartyRequired { get; set; } + 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. From 3354ea953d4ddeca0def2f6a78e1f96d4d719e7e Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:38:57 -0300 Subject: [PATCH 11/11] Address the review of the imperial guardian event - Sort the update after the ones of master by its creation date. - Don't add a byte order mark to MiniGameManager.cs. Co-Authored-By: Claude Opus 5.5 --- src/GameLogic/MiniGames/MiniGameManager.cs | 2 +- .../Updates/AddImperialGuardianDataUpdatePlugIn.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GameLogic/MiniGames/MiniGameManager.cs b/src/GameLogic/MiniGames/MiniGameManager.cs index 7cc9f3d60..afc541f39 100644 --- a/src/GameLogic/MiniGames/MiniGameManager.cs +++ b/src/GameLogic/MiniGames/MiniGameManager.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // diff --git a/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs index a7d577700..a9e10a7cd 100644 --- a/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs +++ b/src/Persistence/Initialization/Updates/AddImperialGuardianDataUpdatePlugIn.cs @@ -59,7 +59,7 @@ public class AddImperialGuardianDataUpdatePlugIn : UpdatePlugInBase public override bool IsMandatory => false; /// - public override DateTime CreatedAt => new(2026, 09, 24, 0, 0, 0, DateTimeKind.Utc); + public override DateTime CreatedAt => new(2026, 09, 28, 0, 0, 0, DateTimeKind.Utc); /// protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)