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();
+ }
}