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