diff --git a/src/GameLogic/CastleSiege/CastleSiegeContext.cs b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
index 2e581ad..23e776e 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeContext.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
@@ -169,26 +169,38 @@ public class CastleSiegeContext
}
///
- /// Evaluates the throne capture: if the siege is running, all defenses are destroyed, the throne is not
- /// yet taken, and one guild holds BOTH Crown Switches at once, that guild captures the throne.
+ /// Attempts to capture the throne for a guild (called when a member registers at the Sinior/Crown NPC).
+ /// Succeeds only during the siege when the throne is free, all castle defenses are destroyed, and the
+ /// guild is currently holding BOTH Crown Switches (a member standing on each).
///
- /// The guild that just captured the throne (for announcing), or null if nothing changed.
- public string? EvaluateCapture()
+ /// The capturing guild's name.
+ /// Whether it succeeded and a human-readable reason/result message.
+ public (bool Success, string Reason) TryCaptureThrone(string guildName)
{
- if (this.Phase != CastleSiegePhase.Siege || this._occupier is not null || this._defensesRemaining > 0)
+ if (this.Phase != CastleSiegePhase.Siege)
{
- return null;
+ return (false, "The siege is not running.");
}
- var holder217 = this._switchHolders[217];
- var holder218 = this._switchHolders[218];
- if (holder217 is not null && holder217 == holder218)
+ if (this._occupier is not null)
{
- this._occupier = holder217;
- return holder217;
+ return (false, this._occupier == guildName
+ ? "Your guild already holds the throne."
+ : $"The throne is already held by '{this._occupier}'.");
}
- return null;
+ if (this._defensesRemaining > 0)
+ {
+ return (false, $"Destroy the castle defenses first ({this._defensesRemaining} remaining).");
+ }
+
+ if (this._switchHolders[217] != guildName || this._switchHolders[218] != guildName)
+ {
+ return (false, "Your guild must be holding BOTH Crown Switches at once (stand a member on each).");
+ }
+
+ this._occupier = guildName;
+ return (true, "throne captured");
}
/// Returns a human-readable status summary for admin display.
diff --git a/src/GameLogic/CastleSiege/CastleSiegeThroneCaptureTalkPlugIn.cs b/src/GameLogic/CastleSiege/CastleSiegeThroneCaptureTalkPlugIn.cs
new file mode 100644
index 0000000..2312bac
--- /dev/null
+++ b/src/GameLogic/CastleSiege/CastleSiegeThroneCaptureTalkPlugIn.cs
@@ -0,0 +1,83 @@
+//
+// 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 registering on the throne at the Sinior (223) / Crown (216) NPCs on Valley of Loren.
+/// A registered guild takes the throne only when it has destroyed all castle defenses (gates + statues)
+/// AND is holding both Crown Switches (a member standing on each). The guild on the throne when the siege
+/// ends becomes the castle owner. Capturing broadcasts a server-wide golden message.
+///
+[Guid("CA5710A0-7A1B-4C2D-8E3F-000000000223")]
+[PlugIn]
+[Display(Name = "Castle Siege Throne (Sinior/Crown)", Description = "Registers a guild on the throne (Sinior 223 / Crown 216) once defenses are down and both switches held.")]
+public class CastleSiegeThroneCaptureTalkPlugIn : IPlayerTalkToNpcPlugIn
+{
+ private static readonly short[] ThroneNpcNumbers = { 223, 216 };
+
+ ///
+ public async ValueTask PlayerTalksToNpcAsync(Player player, NonPlayerCharacter npc, NpcTalkEventArgs eventArgs)
+ {
+ if (!ThroneNpcNumbers.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 (player.GuildStatus is not { } guildStatus)
+ {
+ await ShowAsync(player, "Only members of a registered guild can take 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;
+ }
+
+ var (success, reason) = context.TryCaptureThrone(guildName);
+ if (success)
+ {
+ await player.GameContext.ForEachPlayerAsync(p =>
+ p.InvokeViewPlugInAsync(v =>
+ v.ShowMessageAsync($"Guild '{guildName}' has CAPTURED THE THRONE! They will win the castle if they hold it until the siege ends.", MessageType.GoldenCenter)).AsTask()).ConfigureAwait(false);
+ }
+ else
+ {
+ await ShowAsync(player, reason).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 e0df12b..746fc8b 100644
--- a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
+++ b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
@@ -30,12 +30,14 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
private const int SwitchHoldRange = 3;
// Castle defenses spawned at siege start — destructibles the attackers must break: (number, x, y).
+ // Positions are in the throne-room area (near the crown ~176,212 and the switches ~167-184,194-195),
+ // where players can actually reach and hit them (the earlier statue spot 170/182,206 was confirmed reachable).
private static readonly (short Number, byte X, byte Y)[] DefenseSpawns =
{
- (CastleGateNumber, 160, 180),
- (CastleGateNumber, 190, 180),
- (GuardianStatueNumber, 170, 206),
- (GuardianStatueNumber, 182, 206),
+ (CastleGateNumber, 168, 200),
+ (CastleGateNumber, 184, 200),
+ (GuardianStatueNumber, 170, 208),
+ (GuardianStatueNumber, 182, 208),
};
// Crown Switch positions on Valley of Loren (from the map init) — held by standing on them: (number, x, y).
@@ -209,12 +211,6 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
context.SetSwitchHolder(switchNumber, holder);
}
-
- var captured = context.EvaluateCapture();
- if (captured is not null)
- {
- await AnnounceAsync(gameContext, $"Guild '{captured}' has CAPTURED THE THRONE! They win the castle if they hold it until the siege ends.").ConfigureAwait(false);
- }
}
catch (Exception ex)
{
diff --git a/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs b/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs
index 0fb0179..bb3cd06 100644
--- a/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs
+++ b/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs
@@ -79,18 +79,18 @@ public class CastleSiegeContextTest
// Both switches held but defenses still up -> no capture.
ctx.SetSwitchHolder(217, "Attackers");
ctx.SetSwitchHolder(218, "Attackers");
- Assert.That(ctx.EvaluateCapture(), Is.Null);
+ Assert.That(ctx.TryCaptureThrone("Attackers").Success, Is.False);
ctx.NotifyDefenseDestroyed();
ctx.NotifyDefenseDestroyed();
// Only one switch held -> still no capture.
ctx.SetSwitchHolder(218, null);
- Assert.That(ctx.EvaluateCapture(), Is.Null);
+ Assert.That(ctx.TryCaptureThrone("Attackers").Success, Is.False);
- // Both switches held by the same guild + defenses down -> capture.
+ // Both switches held by the same guild + defenses down -> capture at the Sinior/Crown.
ctx.SetSwitchHolder(218, "Attackers");
- Assert.That(ctx.EvaluateCapture(), Is.EqualTo("Attackers"));
+ Assert.That(ctx.TryCaptureThrone("Attackers").Success, Is.True);
Assert.That(ctx.OccupierGuildName, Is.EqualTo("Attackers"));
await ctx.TickAsync(T0.AddMinutes(20)); // Siege -> Settlement
@@ -108,17 +108,18 @@ public class CastleSiegeContextTest
ctx.SetDefenseCount(0);
ctx.SetSwitchHolder(217, "A");
ctx.SetSwitchHolder(218, "B");
- Assert.That(ctx.EvaluateCapture(), Is.Null);
+ Assert.That(ctx.TryCaptureThrone("A").Success, Is.False);
+ Assert.That(ctx.TryCaptureThrone("B").Success, Is.False);
}
- /// Tests that switch holding outside the siege phase is a no-op.
+ /// Tests that capturing the throne outside the siege phase is a no-op.
[Test]
- public void SwitchHoldOutsideSiegeIsNoOp()
+ public void ThroneCaptureOutsideSiegeIsNoOp()
{
var ctx = new CastleSiegeContext(Config());
ctx.SetSwitchHolder(217, "A");
ctx.SetSwitchHolder(218, "A");
- Assert.That(ctx.EvaluateCapture(), Is.Null);
+ Assert.That(ctx.TryCaptureThrone("A").Success, Is.False);
Assert.That(ctx.OccupierGuildName, Is.Null);
}