Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ protected BaseInvasionPlugIn(MapEventType? mapEventType = null)
/// </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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()]);
}
}

/// <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()
: [];
Comment thread
nolt marked this conversation as resolved.
}

/// <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);
}
Comment thread
nolt marked this conversation as resolved.

private bool IsPlayerOnRelevantMap(Player player, InvasionGameServerState state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public sealed class GoldenInvasionPlugIn : SimpleInvasionPlugIn
/// Initializes a new instance of the <see cref="GoldenInvasionPlugIn"/> class.
/// </summary>
public GoldenInvasionPlugIn()
: base(MapEventType.GoldenDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.Golden)
: base(MapEventType.GoldenDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.Golden, InvasionMonsters.GoldenDragon)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
],
};

Expand Down
21 changes: 21 additions & 0 deletions src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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>
Expand Down Expand Up @@ -44,6 +45,25 @@ public InvasionGameServerState(IGameContext context)
/// </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.
Expand All @@ -70,6 +90,7 @@ internal void Reset()
this.MapId = null;
this._mapIds.Clear();
this._selectedMaps.Clear();
this._announcedMapIds.Clear();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public sealed class RedDragonInvasionPlugIn : SimpleInvasionPlugIn
/// Initializes a new instance of the <see cref="RedDragonInvasionPlugIn"/> class.
/// </summary>
public RedDragonInvasionPlugIn()
: base(MapEventType.RedDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.RedDragon)
: base(MapEventType.RedDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.RedDragon, InvasionMonsters.RedDragon)
{
}
}
10 changes: 9 additions & 1 deletion src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,31 @@ public abstract class SimpleInvasionPlugIn
{
private readonly IReadOnlyList<ushort> _displayMaps;
private readonly Func<PeriodicInvasionConfiguration> _defaultConfigFactory;
private readonly ushort? _announcedMonsterId;

/// <summary>
/// Initializes a new instance of the <see cref="SimpleInvasionPlugIn"/> class.
/// </summary>
/// <param name="mapEventType">The map-event type for UI broadcasting.</param>
/// <param name="displayMaps">Maps eligible to be shown in the event UI.</param>
/// <param name="defaultConfigFactory">Factory that returns the default configuration.</param>
protected SimpleInvasionPlugIn(MapEventType mapEventType, IReadOnlyList<ushort> displayMaps, Func<PeriodicInvasionConfiguration> defaultConfigFactory)
/// <param name="announcedMonsterId">
/// The optional featured monster whose actual spawn map(s) are named in the broadcast.
/// </param>
protected SimpleInvasionPlugIn(MapEventType mapEventType, IReadOnlyList<ushort> displayMaps, Func<PeriodicInvasionConfiguration> defaultConfigFactory, ushort? announcedMonsterId = null)
: base(mapEventType)
{
this._displayMaps = displayMaps;
this._defaultConfigFactory = defaultConfigFactory;
this._announcedMonsterId = announcedMonsterId;
}

/// <inheritdoc />
protected override IReadOnlyList<ushort> EventDisplayMapIds => this._displayMaps;

/// <inheritdoc />
protected override ushort? AnnouncedMonsterId => this._announcedMonsterId;

/// <inheritdoc />
public object CreateDefaultConfig() => this._defaultConfigFactory();
}