diff --git a/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs index 741eecd68..b96d06263 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs @@ -41,6 +41,16 @@ protected BaseInvasionPlugIn(MapEventType? mapEventType = null) /// protected virtual IReadOnlyList? EventDisplayMapIds => null; + /// + /// 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 , or all its maps for + /// . When null, the display map falls back to + /// . + /// + protected virtual ushort? AnnouncedMonsterId => null; + /// public virtual async ValueTask ObjectAddedToMapAsync(GameMap map, ILocateable addedObject) { @@ -172,10 +182,7 @@ protected async Task TrySendStartMessageAsync(Player player, InvasionGameServerS return; } - var mapName = state.Context.Configuration.Maps - .FirstOrDefault(m => m.Number == state.MapId) - ?.Name.GetTranslation(player.Culture) - ?? string.Empty; + var mapName = BuildAnnouncedMapNames(state, player); var message = (configuration.StartMessage.GetTranslation(player.Culture) ?? PlugInResources.BaseInvasionPlugIn_DefaultStartMessage) @@ -204,10 +211,7 @@ protected async Task TrySendEndMessageAsync(Player player, InvasionGameServerSta return; } - var mapName = state.Context.Configuration.Maps - .FirstOrDefault(m => m.Number == state.MapId) - ?.Name.GetTranslation(player.Culture) - ?? string.Empty; + var mapName = BuildAnnouncedMapNames(state, player); var message = (configuration.EndMessage.GetTranslation(player.Culture) ?? string.Empty) .Replace("{mapName}", mapName, StringComparison.InvariantCulture); @@ -342,23 +346,74 @@ private void SelectDisplayMap(InvasionGameServerState state) 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(); - state.MapId = eligible.Count switch + var mapId = eligible.Count switch { 0 => state.MapIds.Min(), 1 => eligible[0], _ => eligible[Rand.NextInt(0, eligible.Count)], }; + state.SetAnnouncedMaps([mapId]); } else { - state.MapId = state.MapIds.Min(); + state.SetAnnouncedMaps([state.MapIds.Min()]); + } + } + + /// + /// Gets the maps where the featured 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 + /// . + /// + /// The current invasion state. + private IReadOnlyList 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() + : []; + } + + /// + /// Builds the comma-separated, localized list of map names announced in the broadcast. + /// + /// The current invasion state. + /// The player whose culture is used for translation. + 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); } private bool IsPlayerOnRelevantMap(Player player, InvasionGameServerState state) diff --git a/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs index be99bb858..387a7ad6d 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs @@ -26,7 +26,7 @@ public sealed class GoldenInvasionPlugIn : SimpleInvasionPlugIn /// Initializes a new instance of the class. /// public GoldenInvasionPlugIn() - : base(MapEventType.GoldenDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.Golden) + : base(MapEventType.GoldenDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.Golden, InvasionMonsters.GoldenDragon) { } } \ No newline at end of file diff --git a/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs b/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs index f071126df..97b7346b2 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs @@ -31,7 +31,7 @@ internal static class InvasionConfigurationDefaults 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, InvasionMaps.Atlans, InvasionMaps.Tarkan], SpawnMapStrategy.RandomMap), + new(InvasionMonsters.GoldenDragon, 10, [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias], SpawnMapStrategy.RandomMap), ], }; diff --git a/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs b/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs index e18800366..bbf8b8ea4 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs @@ -16,6 +16,7 @@ public class InvasionGameServerState : PeriodicTaskGameServerState { private readonly HashSet _mapIds = []; private readonly Dictionary _selectedMaps = []; + private readonly List _announcedMapIds = []; private readonly ConcurrentDictionary _monsters = new(); /// @@ -44,6 +45,25 @@ public InvasionGameServerState(IGameContext context) /// public IReadOnlyDictionary SelectedMaps => this._selectedMaps; + /// + /// 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. + /// + public IReadOnlyList AnnouncedMapIds => this._announcedMapIds; + + /// + /// Sets the maps named in the broadcast message and the single used + /// for the map-event UI state. + /// + /// The maps on which the announced monster spawns this run. + internal void SetAnnouncedMaps(IReadOnlyCollection mapIds) + { + this._announcedMapIds.Clear(); + this._announcedMapIds.AddRange(mapIds); + this.MapId = this._announcedMapIds.Count > 0 ? this._announcedMapIds[0] : null; + } + /// /// Registers a map as active for this run and optionally records which map was /// randomly chosen for a particular monster type. @@ -70,6 +90,7 @@ internal void Reset() this.MapId = null; this._mapIds.Clear(); this._selectedMaps.Clear(); + this._announcedMapIds.Clear(); } /// diff --git a/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs index d703a5ec3..141d451a9 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs @@ -26,7 +26,7 @@ public sealed class RedDragonInvasionPlugIn : SimpleInvasionPlugIn /// Initializes a new instance of the class. /// public RedDragonInvasionPlugIn() - : base(MapEventType.RedDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.RedDragon) + : base(MapEventType.RedDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.RedDragon, InvasionMonsters.RedDragon) { } } \ No newline at end of file diff --git a/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs index c0306d1a2..04029a88c 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs @@ -15,6 +15,7 @@ public abstract class SimpleInvasionPlugIn { private readonly IReadOnlyList _displayMaps; private readonly Func _defaultConfigFactory; + private readonly ushort? _announcedMonsterId; /// /// Initializes a new instance of the class. @@ -22,16 +23,23 @@ public abstract class SimpleInvasionPlugIn /// The map-event type for UI broadcasting. /// Maps eligible to be shown in the event UI. /// Factory that returns the default configuration. - protected SimpleInvasionPlugIn(MapEventType mapEventType, IReadOnlyList displayMaps, Func defaultConfigFactory) + /// + /// The optional featured monster whose actual spawn map(s) are named in the broadcast. + /// + protected SimpleInvasionPlugIn(MapEventType mapEventType, IReadOnlyList displayMaps, Func defaultConfigFactory, ushort? announcedMonsterId = null) : base(mapEventType) { this._displayMaps = displayMaps; this._defaultConfigFactory = defaultConfigFactory; + this._announcedMonsterId = announcedMonsterId; } /// protected override IReadOnlyList EventDisplayMapIds => this._displayMaps; + /// + protected override ushort? AnnouncedMonsterId => this._announcedMonsterId; + /// public object CreateDefaultConfig() => this._defaultConfigFactory(); } \ No newline at end of file