diff --git a/src/GameLogic/CastleSiege/CastleSiegeThroneTalkPlugIn.cs b/src/GameLogic/CastleSiege/CastleSiegeThroneTalkPlugIn.cs new file mode 100644 index 0000000..ac61655 --- /dev/null +++ b/src/GameLogic/CastleSiege/CastleSiegeThroneTalkPlugIn.cs @@ -0,0 +1,79 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.CastleSiege; + +using System.Linq; +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.GameLogic.PlugIns; +using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; +using MUnique.OpenMU.GameLogic.Views; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.PlugIns; + +/// +/// Handles talking to the Castle Siege 'Crown Switch' NPCs (217, 218) on Valley of Loren: while the +/// siege phase is running, a registered guild member using a switch captures the throne for their guild. +/// The guild holding the throne when the siege ends becomes the castle owner (see ). +/// +[Guid("CA5710A0-7A1B-4C2D-8E3F-000000000217")] +[PlugIn] +[Display(Name = "Castle Siege Crown Switch", Description = "Captures the throne for a registered guild during the siege (Crown Switch NPCs 217/218).")] +public class CastleSiegeThroneTalkPlugIn : IPlayerTalkToNpcPlugIn +{ + private static readonly short[] CrownSwitchNumbers = { 217, 218 }; + + /// + public async ValueTask PlayerTalksToNpcAsync(Player player, NonPlayerCharacter npc, NpcTalkEventArgs eventArgs) + { + if (!CrownSwitchNumbers.Contains(npc.Definition.Number)) + { + return; + } + + eventArgs.HasBeenHandled = true; + + var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext); + if (context is null) + { + await ShowAsync(player, "Castle Siege is not active on this server.").ConfigureAwait(false); + return; + } + + if (context.Phase != CastleSiegePhase.Siege) + { + await ShowAsync(player, "The siege is not running right now.").ConfigureAwait(false); + return; + } + + if (player.GuildStatus is not { } guildStatus) + { + await ShowAsync(player, "Only members of a registered guild can capture the throne.").ConfigureAwait(false); + return; + } + + var guildName = guildStatus.GuildId.ToString(); + if (player.GameContext is IGameServerContext serverContext) + { + var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false); + if (guild?.Name is { Length: > 0 } name) + { + guildName = name; + } + } + + if (!context.RegisteredGuilds.Contains(guildName)) + { + await ShowAsync(player, "Your guild is not registered for this Castle Siege.").ConfigureAwait(false); + return; + } + + context.CaptureThrone(guildName); + await ShowAsync(player, $"Your guild '{guildName}' has captured the throne! Hold it until the siege ends to win the castle.").ConfigureAwait(false); + } + + private static ValueTask ShowAsync(Player player, string text) + => player.InvokeViewPlugInAsync(p => p.ShowMessageAsync(text, MessageType.BlueNormal)); +} diff --git a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs index d0304f5..1976d6d 100644 --- a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs +++ b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs @@ -5,19 +5,24 @@ namespace MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using System.Collections.Concurrent; +using System.Linq; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic.CastleSiege; +using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.PlugIns; /// /// Drives the Castle Siege phase state machine: ticks it every second and carries its configuration. /// State is per- and kept in memory (P1: no persistence). +/// When the siege phase starts (P3), warps registered guild members to the Valley of Loren battle map. /// [PlugIn] -[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (P1 skeleton: phase state machine + scheduling).")] +[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (phase state machine, scheduling, siege warp).")] [Guid("6E2C8B41-9A4D-4C2E-9E7B-1F2A3B4C5D60")] public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration { + private const ushort ValleyOfLorenMapNumber = 30; + private static readonly ConcurrentDictionary Contexts = new(); /// @@ -28,9 +33,6 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom /// /// The game context. /// The context, or null if not yet initialized. - /// Static so GM chat commands can reach the state machine without a plugin instance - /// (GetKnownPlugInsOf returns Types, not instances). The tick runs every second, so the - /// context exists within ~1s of server start. public static CastleSiegeContext? TryGetContext(IGameContext gameContext) => Contexts.TryGetValue(gameContext, out var context) ? context : null; @@ -40,7 +42,22 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom /// public async ValueTask ExecuteTaskAsync(GameContext gameContext) { - var context = Contexts.GetOrAdd(gameContext, _ => new CastleSiegeContext(this.Configuration ?? new CastleSiegeConfiguration())); + var context = Contexts.GetOrAdd(gameContext, gc => + { + var created = new CastleSiegeContext(this.Configuration ?? new CastleSiegeConfiguration()); + + // When the siege phase begins, pull all registered guild members onto the battle map. + created.PhaseChanged += phase => + { + if (phase == CastleSiegePhase.Siege) + { + _ = WarpRegisteredMembersToSiegeAsync(gc, created); + } + }; + + return created; + }); + await context.TickAsync(DateTime.UtcNow).ConfigureAwait(false); } @@ -53,4 +70,49 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom _ = context.ForceStartRegistrationAsync(DateTime.UtcNow); } } + + private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context) + { + try + { + var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false); + if (map?.SafeZoneSpawnGate is not { } gate) + { + return; + } + + await gameContext.ForEachPlayerAsync(async player => + { + var guildName = await GetGuildNameAsync(player).ConfigureAwait(false); + if (guildName is not null && context.RegisteredGuilds.Contains(guildName)) + { + await player.WarpToAsync(gate).ConfigureAwait(false); + } + }).ConfigureAwait(false); + } + catch (Exception ex) + { + gameContext.LoggerFactory.CreateLogger() + .LogError(ex, "Castle Siege: error while warping registered members to the battle map."); + } + } + + private static async ValueTask GetGuildNameAsync(Player player) + { + if (player.GuildStatus is not { } guildStatus) + { + return null; + } + + if (player.GameContext is IGameServerContext serverContext) + { + var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false); + if (guild?.Name is { Length: > 0 } name) + { + return name; + } + } + + return guildStatus.GuildId.ToString(); + } }