baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
525
src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs
Normal file
525
src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs
Normal file
@@ -0,0 +1,525 @@
|
||||
// <copyright file="BaseInvasionPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
|
||||
using MUnique.OpenMU.GameLogic.Properties;
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for invasion plugins.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConfiguration">The concrete configuration type.</typeparam>
|
||||
public abstract class BaseInvasionPlugIn<TConfiguration> : PeriodicTaskBasePlugIn<TConfiguration, InvasionGameServerState>, IPeriodicTaskPlugIn, IObjectAddedToMapPlugIn, ISupportCustomConfiguration<TConfiguration>
|
||||
where TConfiguration : PeriodicInvasionConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the map-event type used for UI state broadcasts.
|
||||
/// Override to enable map-event state updates for a specific event type.
|
||||
/// When <c>null</c>, map-event state updates are disabled.
|
||||
/// </summary>
|
||||
protected virtual MapEventType? EventType => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of map IDs from which the event display map is randomly selected.
|
||||
/// When non-empty, <see cref="InvasionGameServerState.MapId"/> is chosen from this
|
||||
/// list and map-event UI is shown only on that single map.
|
||||
/// When <c>null</c>, <see cref="InvasionGameServerState.MapId"/> falls back to the
|
||||
/// minimum of <see cref="InvasionGameServerState.MapIds"/>.
|
||||
/// </summary>
|
||||
protected virtual IReadOnlyList<ushort>? EventDisplayMapIds => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the monster ID of the featured monster whose actual spawn map(s) are named in the
|
||||
/// start/end broadcast (e.g. the Golden Dragon for the Golden Invasion). When set, the
|
||||
/// announcement always points at the map(s) where this monster really spawned this run -
|
||||
/// the single chosen map for <see cref="SpawnMapStrategy.RandomMap"/>, or all its maps for
|
||||
/// <see cref="SpawnMapStrategy.AllMaps"/>. When <c>null</c>, the display map falls back to
|
||||
/// <see cref="EventDisplayMapIds"/>.
|
||||
/// </summary>
|
||||
protected virtual ushort? AnnouncedMonsterId => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async ValueTask ObjectAddedToMapAsync(GameMap map, ILocateable addedObject)
|
||||
{
|
||||
if (this.EventType is not null && addedObject is Player player)
|
||||
{
|
||||
var state = this.GetStateByGameContext(player.GameContext);
|
||||
await this.TrySendMapEventStateUpdateAsync(player, state).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns <paramref name="quantity"/> instances of <paramref name="monsterDefinition"/> on
|
||||
/// <paramref name="gameMap"/>, each placed at a random walkable coordinate (or a fixed
|
||||
/// coordinate when <paramref name="x"/> and <paramref name="y"/> are provided).
|
||||
/// </summary>
|
||||
/// <param name="gameContext">The game context.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="gameMap">The game map.</param>
|
||||
/// <param name="monsterDefinition">The monster definition.</param>
|
||||
/// <param name="quantity">The quantity.</param>
|
||||
/// <param name="announceDeath">If set, a death-broadcast handler is attached to each spawned monster.</param>
|
||||
/// <param name="x">The optional fixed X coordinate.</param>
|
||||
/// <param name="y">The optional fixed Y coordinate.</param>
|
||||
protected async ValueTask CreateMonstersAsync(IGameContext gameContext, ILogger logger, GameMap gameMap, MonsterDefinition monsterDefinition, ushort quantity, bool announceDeath = false, byte? x = null, byte? y = null)
|
||||
{
|
||||
for (var i = 0; i < quantity; i++)
|
||||
{
|
||||
Point? spawnPoint = (x.HasValue && y.HasValue)
|
||||
? new Point(x.Value, y.Value)
|
||||
: gameMap.Terrain.RandomWalkableCoordinate;
|
||||
|
||||
if (spawnPoint is null)
|
||||
{
|
||||
logger.LogDebug("Skipping one {Monster} on {Map}: no walkable cell found.", monsterDefinition.Designation, gameMap.Definition.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
var area = new MonsterSpawnArea
|
||||
{
|
||||
GameMap = gameMap.Definition,
|
||||
MonsterDefinition = monsterDefinition,
|
||||
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
|
||||
Quantity = 1,
|
||||
X1 = spawnPoint.Value.X,
|
||||
X2 = spawnPoint.Value.X,
|
||||
Y1 = spawnPoint.Value.Y,
|
||||
Y2 = spawnPoint.Value.Y,
|
||||
};
|
||||
|
||||
var intelligence = new BasicMonsterIntelligence();
|
||||
var monster = new Monster(
|
||||
area,
|
||||
monsterDefinition,
|
||||
gameMap,
|
||||
gameContext.DropGenerator,
|
||||
intelligence,
|
||||
gameContext.PlugInManager,
|
||||
gameContext.PathFinderPool);
|
||||
|
||||
monster.Initialize();
|
||||
await gameMap.AddAsync(monster).ConfigureAwait(false);
|
||||
monster.OnSpawn();
|
||||
|
||||
var state = this.GetStateByGameContext(gameContext);
|
||||
state.AddMonster(monster);
|
||||
|
||||
if (announceDeath)
|
||||
{
|
||||
this.AttachDeathBroadcast(monster, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns all configured mobs for the given <paramref name="mapId"/>.
|
||||
/// </summary>
|
||||
/// <param name="gameContext">The game context.</param>
|
||||
/// <param name="mapId">The map id.</param>
|
||||
/// <param name="spawns">The spawn configurations.</param>
|
||||
protected async ValueTask SpawnMobsAsync(IGameContext gameContext, ushort mapId, IEnumerable<InvasionSpawnConfiguration> spawns)
|
||||
{
|
||||
var gameMap = await gameContext.GetMapAsync(mapId).ConfigureAwait(false);
|
||||
if (gameMap is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var logger = gameContext.LoggerFactory.CreateLogger(this.GetType());
|
||||
|
||||
foreach (var spawn in spawns)
|
||||
{
|
||||
if (gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.MonsterId) is { } monsterDefinition)
|
||||
{
|
||||
await this.CreateMonstersAsync(gameContext, logger, gameMap, monsterDefinition, spawn.Count, spawn.AnnounceDeath, spawn.X, spawn.Y).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Skipping monster {MobId}: definition not found.", spawn.MonsterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnPrepareEventAsync(InvasionGameServerState state)
|
||||
{
|
||||
var config = this.Configuration;
|
||||
if (config?.Mobs is not { Count: > 0 } mobs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.ForceSingleMap)
|
||||
{
|
||||
this.SelectSingleMap(state, mobs);
|
||||
return;
|
||||
}
|
||||
|
||||
this.SelectSpawnMaps(mobs, state);
|
||||
this.SelectDisplayMap(state);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override InvasionGameServerState CreateState(IGameContext gameContext)
|
||||
=> new(gameContext);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the invasion start message to a single player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="state">The server state.</param>
|
||||
protected async Task TrySendStartMessageAsync(Player player, InvasionGameServerState state)
|
||||
{
|
||||
var configuration = this.Configuration;
|
||||
if (configuration is null || state.MapIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mapName = BuildAnnouncedMapNames(state, player);
|
||||
|
||||
var message = (configuration.StartMessage.GetTranslation(player.Culture)
|
||||
?? PlugInResources.BaseInvasionPlugIn_DefaultStartMessage)
|
||||
.Replace("{mapName}", mapName, StringComparison.InvariantCulture);
|
||||
|
||||
try
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
player.Logger.LogDebug(ex, "Unexpected error sending invasion start message.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the invasion end message to a single player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="state">The server state.</param>
|
||||
protected async Task TrySendEndMessageAsync(Player player, InvasionGameServerState state)
|
||||
{
|
||||
var configuration = this.Configuration;
|
||||
if (configuration is null || state.MapIds.Count == 0 || string.IsNullOrWhiteSpace(configuration.EndMessage))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mapName = BuildAnnouncedMapNames(state, player);
|
||||
|
||||
var message = (configuration.EndMessage.GetTranslation(player.Culture) ?? string.Empty)
|
||||
.Replace("{mapName}", mapName, StringComparison.InvariantCulture);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
player.Logger.LogDebug(ex, "Unexpected error sending invasion end message.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnPreparedAsync(InvasionGameServerState state)
|
||||
{
|
||||
await state.Context.ForEachPlayerAsync(p => this.TrySendStartMessageAsync(p, state)).ConfigureAwait(false);
|
||||
|
||||
if (this.EventType is not null)
|
||||
{
|
||||
await state.Context.ForEachPlayerAsync(p => this.TrySendMapEventStateUpdateAsync(p, state)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnStartedAsync(InvasionGameServerState state)
|
||||
{
|
||||
await this.SpawnMobsOnMapsAsync(state).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnFinishedAsync(InvasionGameServerState state)
|
||||
{
|
||||
await state.Context.ForEachPlayerAsync(p => this.TrySendEndMessageAsync(p, state)).ConfigureAwait(false);
|
||||
|
||||
if (this.EventType is not null)
|
||||
{
|
||||
await state.Context.ForEachPlayerAsync(p => this.TrySendMapEventStateUpdateAsync(p, state)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await state.CleanUpMonstersAsync().ConfigureAwait(false);
|
||||
state.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns mobs on the maps that were selected during <see cref="OnPrepareEventAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
protected virtual async ValueTask SpawnMobsOnMapsAsync(InvasionGameServerState state)
|
||||
{
|
||||
var config = this.Configuration;
|
||||
if (config?.Mobs is not { Count: > 0 } spawns)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gameContext = state.Context;
|
||||
|
||||
foreach (var spawn in spawns)
|
||||
{
|
||||
if (spawn.IsSpawnOnAllMaps)
|
||||
{
|
||||
foreach (var mapId in spawn.MapIds)
|
||||
{
|
||||
await this.SpawnMobsAsync(gameContext, mapId, [spawn]).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (state.SelectedMaps.TryGetValue(spawn.MonsterId, out var selectedMapId))
|
||||
{
|
||||
await this.SpawnMobsAsync(gameContext, selectedMapId, [spawn]).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This indicates an unexpected state or configuration mismatch.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the comma-separated, localized list of map names announced in the broadcast.
|
||||
/// </summary>
|
||||
/// <param name="state">The current invasion state.</param>
|
||||
/// <param name="player">The player whose culture is used for translation.</param>
|
||||
private static string BuildAnnouncedMapNames(InvasionGameServerState state, Player player)
|
||||
{
|
||||
var names = state.AnnouncedMapIds
|
||||
.Select(id => state.Context.Configuration.Maps
|
||||
.FirstOrDefault(m => m.Number == id)
|
||||
?.Name.GetTranslation(player.Culture))
|
||||
.Where(name => !string.IsNullOrEmpty(name));
|
||||
|
||||
return string.Join(", ", names);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a single map from the first mob's configuration and registers all mobs on it.
|
||||
/// Used when <see cref="PeriodicInvasionConfiguration.ForceSingleMap"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="state">The invasion state.</param>
|
||||
/// <param name="mobs">The mob spawn configurations.</param>
|
||||
private void SelectSingleMap(InvasionGameServerState state, IList<InvasionSpawnConfiguration> mobs)
|
||||
{
|
||||
var source = this.AnnouncedMonsterId is { } announcedId
|
||||
? mobs.FirstOrDefault(m => m.MonsterId == announcedId)
|
||||
: null;
|
||||
source ??= mobs[0];
|
||||
|
||||
if (source.MapIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var chosenMap = source.MapIds.Count == 1
|
||||
? source.MapIds[0]
|
||||
: source.MapIds[Rand.NextInt(0, source.MapIds.Count)];
|
||||
|
||||
foreach (var mob in mobs)
|
||||
{
|
||||
state.RegisterMap(chosenMap, mob.MonsterId);
|
||||
}
|
||||
|
||||
state.SetAnnouncedMaps([chosenMap]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the mob configurations and registers the selected spawn maps into the state.
|
||||
/// For mobs configured with <see cref="SpawnMapStrategy.AllMaps"/>, all map IDs are registered.
|
||||
/// For mobs configured with <see cref="SpawnMapStrategy.RandomMap"/>, a single map is picked at random.
|
||||
/// </summary>
|
||||
/// <param name="mobs">The mob spawn configurations.</param>
|
||||
/// <param name="state">The current invasion state.</param>
|
||||
private void SelectSpawnMaps(IList<InvasionSpawnConfiguration> mobs, InvasionGameServerState state)
|
||||
{
|
||||
foreach (var mob in mobs)
|
||||
{
|
||||
if (mob.MapIds.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mob.IsSpawnOnAllMaps)
|
||||
{
|
||||
foreach (var mapId in mob.MapIds)
|
||||
{
|
||||
state.RegisterMap(mapId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var randomMapId = mob.MapIds.Count == 1
|
||||
? mob.MapIds[0]
|
||||
: mob.MapIds[Rand.NextInt(0, mob.MapIds.Count)];
|
||||
|
||||
state.RegisterMap(randomMapId, mob.MonsterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the single map used for UI event display and message broadcast from
|
||||
/// the maps that were actually selected for spawning during <see cref="SelectSpawnMaps"/>.
|
||||
/// When <see cref="EventDisplayMapIds"/> is configured, the display map is restricted
|
||||
/// to the intersection of <see cref="EventDisplayMapIds"/> and the selected spawn maps,
|
||||
/// ensuring the announced map always has active monsters.
|
||||
/// Falls back to the minimum map ID if no intersection exists or no display maps are configured.
|
||||
/// </summary>
|
||||
/// <param name="state">The current invasion state.</param>
|
||||
private void SelectDisplayMap(InvasionGameServerState state)
|
||||
{
|
||||
if (state.MapIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var announcedMaps = this.GetAnnouncedMaps(state);
|
||||
if (announcedMaps.Count > 0)
|
||||
{
|
||||
state.SetAnnouncedMaps(announcedMaps);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.EventDisplayMapIds is { Count: > 0 } displayMaps)
|
||||
{
|
||||
var eligible = state.MapIds
|
||||
.Where(displayMaps.Contains)
|
||||
.ToList();
|
||||
|
||||
var mapId = eligible.Count switch
|
||||
{
|
||||
0 => state.MapIds.Min(),
|
||||
1 => eligible[0],
|
||||
_ => eligible[Rand.NextInt(0, eligible.Count)],
|
||||
};
|
||||
state.SetAnnouncedMaps([mapId]);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.SetAnnouncedMaps([state.MapIds.Min()]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maps where the featured <see cref="AnnouncedMonsterId"/> actually spawns this run,
|
||||
/// so the broadcast names a map that really contains it. Returns an empty list when no announced
|
||||
/// monster is configured or it did not spawn, in which case the caller falls back to
|
||||
/// <see cref="EventDisplayMapIds"/>.
|
||||
/// </summary>
|
||||
/// <param name="state">The current invasion state.</param>
|
||||
private IReadOnlyList<ushort> GetAnnouncedMaps(InvasionGameServerState state)
|
||||
{
|
||||
if (this.AnnouncedMonsterId is not { } monsterId)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// RandomMap: the single chosen map was recorded in SelectedMaps during SelectSpawnMaps.
|
||||
if (state.SelectedMaps.TryGetValue(monsterId, out var selectedMapId))
|
||||
{
|
||||
return [selectedMapId];
|
||||
}
|
||||
|
||||
// AllMaps: the monster spawns on every configured map, so name them all.
|
||||
var mob = this.Configuration?.Mobs.FirstOrDefault(m => m.MonsterId == monsterId);
|
||||
return mob is { IsSpawnOnAllMaps: true, MapIds.Count: > 0 }
|
||||
? mob.MapIds.ToArray()
|
||||
: [];
|
||||
}
|
||||
|
||||
private bool IsPlayerOnRelevantMap(Player player, InvasionGameServerState state)
|
||||
=> state.MapId.HasValue
|
||||
&& player.CurrentMap is { } map
|
||||
&& !player.PlayerState.CurrentState.IsDisconnectedOrFinished()
|
||||
&& map.MapId == state.MapId.Value
|
||||
&& (this.EventDisplayMapIds is null || this.EventDisplayMapIds.Contains(state.MapId.Value));
|
||||
|
||||
private async Task TrySendMapEventStateUpdateAsync(Player player, InvasionGameServerState state)
|
||||
{
|
||||
if (this.EventType is null || !this.IsPlayerOnRelevantMap(player, state))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var enabled = state.State != PeriodicTaskState.NotStarted;
|
||||
await player.InvokeViewPlugInAsync<IMapEventStateUpdatePlugIn>(p => p.UpdateStateAsync(enabled, this.EventType.Value)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
player.Logger.LogDebug(
|
||||
ex,
|
||||
"Unexpected error sending map event state update, event type: {MapEventType}",
|
||||
this.EventType);
|
||||
}
|
||||
}
|
||||
|
||||
private void AttachDeathBroadcast(Monster monster, InvasionGameServerState state)
|
||||
{
|
||||
var context = state.Context;
|
||||
void Handler(object? sender, DeathInformation e)
|
||||
{
|
||||
if (sender is Monster m)
|
||||
{
|
||||
m.Died -= Handler;
|
||||
var mapDefinition = m.CurrentMap?.Definition;
|
||||
_ = Task.Run(() => BroadcastMonsterDeathAsync(m, mapDefinition, e, context));
|
||||
}
|
||||
}
|
||||
|
||||
monster.Died += Handler;
|
||||
}
|
||||
|
||||
private static async Task BroadcastMonsterDeathAsync(Monster monster, GameMapDefinition? mapDefinition, DeathInformation e, IGameContext gameContext)
|
||||
{
|
||||
if (mapDefinition is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mapName = mapDefinition.Name;
|
||||
await gameContext.ForEachPlayerAsync(async player =>
|
||||
{
|
||||
var translatedMapName = mapName.GetTranslation(player.Culture);
|
||||
var monsterName = monster.Definition.Designation.GetTranslation(player.Culture);
|
||||
|
||||
try
|
||||
{
|
||||
await player.ShowLocalizedGoldenMessageAsync(nameof(PlayerMessage.InvasionMonsterDefeated), e.KillerName, translatedMapName, monsterName).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore view invocation errors
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger(typeof(BaseInvasionPlugIn<TConfiguration>)).LogError(ex, "Error during invasion monster death broadcast.");
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs
Normal file
34
src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// <copyright file="GoldenInvasionPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Enables the Golden Invasion feature.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(PlugInResources.GoldenInvasionPlugIn_Name), Description = nameof(PlugInResources.GoldenInvasionPlugIn_Description), ResourceType = typeof(PlugInResources))]
|
||||
[Guid("06D18A9E-2919-4C17-9DBC-6E4F7756495C")]
|
||||
public sealed class GoldenInvasionPlugIn : SimpleInvasionPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GoldenInvasionPlugIn"/> class.
|
||||
/// </summary>
|
||||
public GoldenInvasionPlugIn()
|
||||
: base(() => InvasionConfigurationDefaults.Golden)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override MapEventType? EventType => MapEventType.GoldenDragonInvasion;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ushort? AnnouncedMonsterId => InvasionMonsters.GoldenDragon;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IReadOnlyList<ushort>? EventDisplayMapIds => [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias];
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// <copyright file="InvasionConfigurationDefaults.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Provides ready-to-use default configurations for the built-in invasion types.
|
||||
/// </summary>
|
||||
internal static class InvasionConfigurationDefaults
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default configuration for the Golden Invasion event.
|
||||
/// </summary>
|
||||
public static PeriodicInvasionConfiguration Golden => new()
|
||||
{
|
||||
TaskDuration = TimeSpan.FromMinutes(30),
|
||||
PreStartMessageDelay = TimeSpan.FromSeconds(3),
|
||||
StartMessage = "[{mapName}] Golden invasion!",
|
||||
EndMessage = "[{mapName}] Golden invasion has ended.",
|
||||
Timetable = PeriodicTaskConfiguration.GenerateTimeSequence(TimeSpan.FromHours(4)).ToList(),
|
||||
Mobs =
|
||||
[
|
||||
new(InvasionMonsters.GoldenBudgeDragon, 20, [InvasionMaps.Lorencia], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenGoblin, 20, [InvasionMaps.Noria], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenSoldier, 20, [InvasionMaps.Devias], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenTitan, 10, [InvasionMaps.Devias], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenVepar, 20, [InvasionMaps.Atlans], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenLizardKing, 10, [InvasionMaps.Atlans], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenWheel, 20, [InvasionMaps.Tarkan], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenTantallos, 10, [InvasionMaps.Tarkan], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.GoldenDragon, 10, [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias], SpawnMapStrategy.RandomMap),
|
||||
],
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default configuration for the Red Dragon Invasion event.
|
||||
/// </summary>
|
||||
public static PeriodicInvasionConfiguration RedDragon => new()
|
||||
{
|
||||
TaskDuration = TimeSpan.FromMinutes(30),
|
||||
PreStartMessageDelay = TimeSpan.FromSeconds(3),
|
||||
StartMessage = "[{mapName}] Red Dragon invasion!",
|
||||
EndMessage = "[{mapName}] Red Dragon invasion has ended.",
|
||||
Timetable = PeriodicTaskConfiguration.GenerateTimeSequence(TimeSpan.FromHours(6), new TimeOnly(2, 0)).ToList(),
|
||||
Mobs =
|
||||
[
|
||||
new(InvasionMonsters.RedDragon, 5, [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias], SpawnMapStrategy.RandomMap),
|
||||
],
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default configuration for the White Wizard Invasion event.
|
||||
/// </summary>
|
||||
public static PeriodicInvasionConfiguration WhiteWizard => new()
|
||||
{
|
||||
TaskDuration = TimeSpan.FromMinutes(30),
|
||||
PreStartMessageDelay = TimeSpan.FromSeconds(3),
|
||||
StartMessage = "[{mapName}] White Wizard corps invasion!",
|
||||
EndMessage = "[{mapName}] White Wizard corps invasion has ended.",
|
||||
ForceSingleMap = true,
|
||||
Timetable = PeriodicTaskConfiguration.GenerateTimeSequence(TimeSpan.FromHours(2), new TimeOnly(12, 0), new TimeOnly(23, 0)).ToList(),
|
||||
Mobs =
|
||||
[
|
||||
new(InvasionMonsters.WhiteWizard, 1, [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias], SpawnMapStrategy.RandomMap, announceDeath: true),
|
||||
new(InvasionMonsters.DestructiveOgreSoldier, 15, [], SpawnMapStrategy.RandomMap),
|
||||
new(InvasionMonsters.DestructiveOgreArcher, 10, [], SpawnMapStrategy.RandomMap),
|
||||
],
|
||||
};
|
||||
}
|
||||
142
src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs
Normal file
142
src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
// <copyright file="InvasionGameServerState.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Invasion state that is created for every periodic invasion run.
|
||||
/// </summary>
|
||||
public class InvasionGameServerState : PeriodicTaskGameServerState
|
||||
{
|
||||
private readonly HashSet<ushort> _mapIds = [];
|
||||
private readonly Dictionary<ushort, ushort> _selectedMaps = [];
|
||||
private readonly List<ushort> _announcedMapIds = [];
|
||||
private readonly ConcurrentDictionary<Monster, byte> _monsters = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvasionGameServerState"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The game context.</param>
|
||||
public InvasionGameServerState(IGameContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map identifier used for UI display / map-event state broadcasts.
|
||||
/// <c>null</c> means no event is active or the display map has not been selected yet.
|
||||
/// </summary>
|
||||
public ushort? MapId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of map identifiers on which monsters will spawn this run.
|
||||
/// </summary>
|
||||
public IReadOnlySet<ushort> MapIds => this._mapIds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the read-only mapping of monster ID to selected map identifier.
|
||||
/// Populated for spawns whose <see cref="SpawnMapStrategy"/> is <see cref="SpawnMapStrategy.RandomMap"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<ushort, ushort> SelectedMaps => this._selectedMaps;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of map identifiers that are named in the invasion start/end broadcast.
|
||||
/// These are the maps on which the announced (featured) monster actually spawns this run,
|
||||
/// so the message always points players at a map that really contains it.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ushort> AnnouncedMapIds => this._announcedMapIds;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maps named in the broadcast message and the single <see cref="MapId"/> used
|
||||
/// for the map-event UI state.
|
||||
/// </summary>
|
||||
/// <param name="mapIds">The maps on which the announced monster spawns this run.</param>
|
||||
internal void SetAnnouncedMaps(IReadOnlyCollection<ushort> mapIds)
|
||||
{
|
||||
this._announcedMapIds.Clear();
|
||||
this._announcedMapIds.AddRange(mapIds);
|
||||
this.MapId = this._announcedMapIds.Count > 0 ? this._announcedMapIds[0] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a map as active for this run and optionally records which map was
|
||||
/// randomly chosen for a particular monster type.
|
||||
/// </summary>
|
||||
/// <param name="mapId">The map identifier to register.</param>
|
||||
/// <param name="monsterId">
|
||||
/// When provided, records the <paramref name="mapId"/> as the chosen map for this monster.
|
||||
/// Pass <c>null</c> for "spawn-on-all-maps" entries.
|
||||
/// </param>
|
||||
internal void RegisterMap(ushort mapId, ushort? monsterId = null)
|
||||
{
|
||||
this._mapIds.Add(mapId);
|
||||
if (monsterId.HasValue)
|
||||
{
|
||||
this._selectedMaps[monsterId.Value] = mapId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all state accumulated from a previous run so the object can be reused.
|
||||
/// </summary>
|
||||
internal void Reset()
|
||||
{
|
||||
this.MapId = null;
|
||||
this._mapIds.Clear();
|
||||
this._selectedMaps.Clear();
|
||||
this._announcedMapIds.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks a monster spawned by this invasion and handles its cleanup on death.
|
||||
/// </summary>
|
||||
/// <param name="monster">The monster to track.</param>
|
||||
internal void AddMonster(Monster monster)
|
||||
{
|
||||
if (this._monsters.TryAdd(monster, 0))
|
||||
{
|
||||
monster.Died += this.OnMonsterDied;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Despawns and disposes all active monsters tracked by this invasion state.
|
||||
/// </summary>
|
||||
internal async ValueTask CleanUpMonstersAsync()
|
||||
{
|
||||
if (this._monsters.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var monsters = this._monsters.Keys.ToArray();
|
||||
var tasks = monsters.Select(async monster =>
|
||||
{
|
||||
this._monsters.TryRemove(monster, out _);
|
||||
monster.Died -= this.OnMonsterDied;
|
||||
|
||||
if (!monster.IsDisposed)
|
||||
{
|
||||
await monster.CurrentMap.RemoveAsync(monster).ConfigureAwait(false);
|
||||
monster.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnMonsterDied(object? sender, DeathInformation e)
|
||||
{
|
||||
if (sender is Monster monster)
|
||||
{
|
||||
this._monsters.TryRemove(monster, out _);
|
||||
monster.Died -= this.OnMonsterDied;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/GameLogic/PlugIns/InvasionEvents/InvasionMaps.cs
Normal file
36
src/GameLogic/PlugIns/InvasionEvents/InvasionMaps.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// <copyright file="InvasionMaps.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known map identifiers used across invasion plugins.
|
||||
/// </summary>
|
||||
internal static class InvasionMaps
|
||||
{
|
||||
/// <summary>
|
||||
/// The Lorencia map.
|
||||
/// </summary>
|
||||
public const ushort Lorencia = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The Devias map.
|
||||
/// </summary>
|
||||
public const ushort Devias = 2;
|
||||
|
||||
/// <summary>
|
||||
/// The Noria map.
|
||||
/// </summary>
|
||||
public const ushort Noria = 3;
|
||||
|
||||
/// <summary>
|
||||
/// The Atlans map.
|
||||
/// </summary>
|
||||
public const ushort Atlans = 7;
|
||||
|
||||
/// <summary>
|
||||
/// The Tarkan map.
|
||||
/// </summary>
|
||||
public const ushort Tarkan = 8;
|
||||
}
|
||||
76
src/GameLogic/PlugIns/InvasionEvents/InvasionMonsters.cs
Normal file
76
src/GameLogic/PlugIns/InvasionEvents/InvasionMonsters.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
// <copyright file="InvasionMonsters.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
/// <summary>
|
||||
/// Monster identifiers used across invasion plugins.
|
||||
/// </summary>
|
||||
internal static class InvasionMonsters
|
||||
{
|
||||
/// <summary>
|
||||
/// The Golden Budge Dragon monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenBudgeDragon = 43;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Soldier monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenSoldier = 54;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Titan monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenTitan = 53;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Goblin monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenGoblin = 78;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Dragon monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenDragon = 79;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Lizard King monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenLizardKing = 80;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Vepar monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenVepar = 81;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Tantallos monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenTantallos = 82;
|
||||
|
||||
/// <summary>
|
||||
/// The Golden Wheel monster.
|
||||
/// </summary>
|
||||
public const ushort GoldenWheel = 83;
|
||||
|
||||
/// <summary>
|
||||
/// The Red Dragon monster.
|
||||
/// </summary>
|
||||
public const ushort RedDragon = 44;
|
||||
|
||||
/// <summary>
|
||||
/// The White Wizard monster.
|
||||
/// </summary>
|
||||
public const ushort WhiteWizard = 135;
|
||||
|
||||
/// <summary>
|
||||
/// The Destructive Ogre Soldier monster.
|
||||
/// </summary>
|
||||
public const ushort DestructiveOgreSoldier = 136;
|
||||
|
||||
/// <summary>
|
||||
/// The Destructive Ogre Archer monster.
|
||||
/// </summary>
|
||||
public const ushort DestructiveOgreArcher = 137;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// <copyright file="InvasionSpawnConfiguration.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a spawn configuration for an invasion event monster.
|
||||
/// </summary>
|
||||
public class InvasionSpawnConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvasionSpawnConfiguration"/> class.
|
||||
/// Required for serialization and UI binding.
|
||||
/// </summary>
|
||||
public InvasionSpawnConfiguration()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvasionSpawnConfiguration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="monsterId">The monster ID to spawn.</param>
|
||||
/// <param name="count">The number of monsters to spawn (1-254).</param>
|
||||
/// <param name="mapIds">The list of map IDs where the monster can spawn.</param>
|
||||
/// <param name="mapStrategy">Controls whether to spawn on a random map or all maps.</param>
|
||||
/// <param name="x">The optional fixed X coordinate.</param>
|
||||
/// <param name="y">The optional fixed Y coordinate.</param>
|
||||
/// <param name="announceDeath">Indicates whether the death of this monster type should be announced globally.</param>
|
||||
public InvasionSpawnConfiguration(
|
||||
ushort monsterId,
|
||||
ushort count,
|
||||
IList<ushort> mapIds,
|
||||
SpawnMapStrategy mapStrategy,
|
||||
byte? x = null,
|
||||
byte? y = null,
|
||||
bool announceDeath = false)
|
||||
{
|
||||
this.MonsterId = monsterId;
|
||||
this.Count = count;
|
||||
this.MapIds = mapIds;
|
||||
this.MapStrategy = mapStrategy;
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
this.AnnounceDeath = announceDeath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the monster ID to spawn.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public ushort MonsterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of monsters to spawn (1-254).
|
||||
/// </summary>
|
||||
[Range(1, 254)]
|
||||
public ushort Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of map IDs where the monster can spawn.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
public IList<ushort> MapIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the strategy used to select a map when spawning.
|
||||
/// </summary>
|
||||
public SpawnMapStrategy MapStrategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the death of this monster type
|
||||
/// should be announced with a global broadcast message.
|
||||
/// </summary>
|
||||
public bool AnnounceDeath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the monster spawns on all maps in <see cref="MapIds"/>.
|
||||
/// When set to <c>true</c>, <see cref="MapStrategy"/> is changed to <see cref="SpawnMapStrategy.AllMaps"/>.
|
||||
/// When set to <c>false</c>, <see cref="MapStrategy"/> is changed to <see cref="SpawnMapStrategy.RandomMap"/>.
|
||||
/// This property exists for UI binding and serialization compatibility.
|
||||
/// </summary>
|
||||
public bool IsSpawnOnAllMaps
|
||||
{
|
||||
get => this.MapStrategy == SpawnMapStrategy.AllMaps;
|
||||
set => this.MapStrategy = value ? SpawnMapStrategy.AllMaps : SpawnMapStrategy.RandomMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fixed X coordinate.
|
||||
/// If <c>null</c>, a random walkable coordinate is used.
|
||||
/// </summary>
|
||||
[Range(0, 255)]
|
||||
public byte? X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fixed Y coordinate.
|
||||
/// If <c>null</c>, a random walkable coordinate is used.
|
||||
/// </summary>
|
||||
[Range(0, 255)]
|
||||
public byte? Y { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Equality is based solely on <see cref="MonsterId"/>, as each monster type
|
||||
/// may only have one spawn configuration per invasion event.
|
||||
/// </remarks>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not InvasionSpawnConfiguration other)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.MonsterId == other.MonsterId;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// The hash code is based solely on <see cref="MonsterId"/>, consistent with <see cref="Equals"/>.
|
||||
/// This means the object can be safely mutated (maps, count, strategy, coordinates)
|
||||
/// while held in a hash-based collection without becoming unreachable.
|
||||
/// </remarks>
|
||||
public override int GetHashCode() => this.MonsterId.GetHashCode();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// <copyright file="PeriodicInvasionConfiguration.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration data for a periodic invasion event.
|
||||
/// Responsible only for describing the shape of the configuration (SRP).
|
||||
/// Default values live in <see cref="InvasionConfigurationDefaults"/>.
|
||||
/// </summary>
|
||||
public class PeriodicInvasionConfiguration : PeriodicTaskConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PeriodicInvasionConfiguration"/> class.
|
||||
/// </summary>
|
||||
public PeriodicInvasionConfiguration()
|
||||
{
|
||||
this.StartMessage = "Invasion has started!";
|
||||
this.EndMessage = "Invasion has ended!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether all mobs should spawn on a single map,
|
||||
/// determined from the first mob's <see cref="InvasionSpawnConfiguration.MapIds"/>.
|
||||
/// When <c>true</c>, every mob in the invasion is placed on the same randomly-selected map
|
||||
/// rather than distributing across maps per their individual configurations.
|
||||
/// </summary>
|
||||
[Display(Name = "Force Single Map", Order = 6)]
|
||||
public bool ForceSingleMap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the monster spawns for this invasion.
|
||||
/// </summary>
|
||||
[Display(Name = "Monster Spawns", Order = 7)]
|
||||
public IList<InvasionSpawnConfiguration> Mobs { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// <copyright file="RedDragonInvasionPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Enables the Red Dragon Invasion feature.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(PlugInResources.RedDragonInvasionPlugIn_Name), Description = nameof(PlugInResources.RedDragonInvasionPlugIn_Description), ResourceType = typeof(PlugInResources))]
|
||||
[Guid("548A76CC-242C-441C-BC9D-6C22745A2D72")]
|
||||
public sealed class RedDragonInvasionPlugIn : SimpleInvasionPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RedDragonInvasionPlugIn"/> class.
|
||||
/// </summary>
|
||||
public RedDragonInvasionPlugIn()
|
||||
: base(() => InvasionConfigurationDefaults.RedDragon)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override MapEventType? EventType => MapEventType.RedDragonInvasion;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ushort? AnnouncedMonsterId => InvasionMonsters.RedDragon;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IReadOnlyList<ushort>? EventDisplayMapIds => [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias];
|
||||
}
|
||||
28
src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs
Normal file
28
src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// <copyright file="SimpleInvasionPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience base for invasion plugins that need no logic beyond returning a default configuration.
|
||||
/// </summary>
|
||||
public abstract class SimpleInvasionPlugIn
|
||||
: BaseInvasionPlugIn<PeriodicInvasionConfiguration>, ISupportDefaultCustomConfiguration
|
||||
{
|
||||
private readonly Func<PeriodicInvasionConfiguration> _defaultConfigFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimpleInvasionPlugIn"/> class.
|
||||
/// </summary>
|
||||
/// <param name="defaultConfigFactory">Factory that returns the default configuration.</param>
|
||||
protected SimpleInvasionPlugIn(Func<PeriodicInvasionConfiguration> defaultConfigFactory)
|
||||
{
|
||||
this._defaultConfigFactory = defaultConfigFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object CreateDefaultConfig() => this._defaultConfigFactory();
|
||||
}
|
||||
21
src/GameLogic/PlugIns/InvasionEvents/SpawnMapStrategy.cs
Normal file
21
src/GameLogic/PlugIns/InvasionEvents/SpawnMapStrategy.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="SpawnMapStrategy.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
/// <summary>
|
||||
/// Defines how the spawn map is selected when multiple map IDs are configured.
|
||||
/// </summary>
|
||||
public enum SpawnMapStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// A single map is picked at random from the configured list.
|
||||
/// </summary>
|
||||
RandomMap,
|
||||
|
||||
/// <summary>
|
||||
/// The monster spawns on every map in the configured list.
|
||||
/// </summary>
|
||||
AllMaps,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// <copyright file="WhiteWizardInvasionPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Enables the White Wizard Invasion feature.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = "White Wizard Invasion", Description = "Enables the White Wizard Invasion feature.")]
|
||||
[Guid("4B5D0F55-5B26-4447-B9C0-C272E5D0A141")]
|
||||
public sealed class WhiteWizardInvasionPlugIn : SimpleInvasionPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WhiteWizardInvasionPlugIn"/> class.
|
||||
/// </summary>
|
||||
public WhiteWizardInvasionPlugIn()
|
||||
: base(() => InvasionConfigurationDefaults.WhiteWizard)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ushort? AnnouncedMonsterId => InvasionMonsters.WhiteWizard;
|
||||
}
|
||||
Reference in New Issue
Block a user