diff --git a/src/GameLogic/CastleSiege/CastleSiegeContext.cs b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
index 8441b79..2e581ad 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeContext.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
@@ -5,16 +5,23 @@
namespace MUnique.OpenMU.GameLogic.CastleSiege;
///
-/// In-memory Castle Siege phase state machine (P1 skeleton: no battle/persistence).
+/// In-memory Castle Siege phase state machine and battle contention (P3).
/// Time is injected via method parameters so it can be tested deterministically.
+/// Battle rule: attackers must destroy all castle defenses (gates + guardian statues) and then hold BOTH
+/// Crown Switches at the same time — the switches are held by standing on them (evaluated per tick by the
+/// plugin), and once both are held by one guild with the defenses down, that guild captures the throne.
+/// The throne holder when the siege ends becomes the castle owner.
///
public class CastleSiegeContext
{
+ /// The Crown Switch NPC numbers on Valley of Loren; both must be held to take the throne.
+ public static readonly short[] SwitchNumbers = { 217, 218 };
+
private readonly List _registeredGuilds = new();
- private readonly Dictionary _switchHolders = new();
+ private readonly Dictionary _switchHolders = new() { { 217, null }, { 218, null } };
private DateTime _phaseStartedUtc;
private string? _occupier;
- private int _statuesRemaining;
+ private int _defensesRemaining;
/// Initializes a new instance of the class.
/// The cycle timing configuration.
@@ -36,12 +43,15 @@ public class CastleSiegeContext
/// Gets the current owner guild name, or null if unowned.
public string? OwnerGuildName { get; private set; }
- /// Gets the guild names registered for the current cycle.
- public IReadOnlyList RegisteredGuilds => this._registeredGuilds;
-
/// Gets the guild currently holding the throne during the siege (P3), or null.
public string? OccupierGuildName => this._occupier;
+ /// Gets the number of castle defenses (gates + statues) still standing; the throne needs 0.
+ public int DefensesRemaining => this._defensesRemaining;
+
+ /// Gets the guild names registered for the current cycle.
+ public IReadOnlyList RegisteredGuilds => this._registeredGuilds;
+
/// Advances the state machine based on the current time.
/// The current UTC time.
public ValueTask TickAsync(DateTime now)
@@ -77,13 +87,13 @@ public class CastleSiegeContext
break;
case CastleSiegePhase.Settlement:
- // Winner = guild holding the throne at siege end (P3). If none captured, owner unchanged.
+ // Winner = guild holding the throne at siege end. If none captured, owner unchanged.
if (this._occupier is { } occupier)
{
this.SetOwner(occupier);
}
- this._occupier = null;
+ this.ClearBattleState();
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
default:
break;
@@ -97,9 +107,7 @@ public class CastleSiegeContext
public ValueTask ForceStartRegistrationAsync(DateTime now)
{
this._registeredGuilds.Clear();
- this._switchHolders.Clear();
- this._statuesRemaining = 0;
- this._occupier = null;
+ this.ClearBattleState();
return this.TransitionAsync(CastleSiegePhase.Registration, now);
}
@@ -109,14 +117,12 @@ public class CastleSiegeContext
public ValueTask ForcePhaseAsync(CastleSiegePhase phase, DateTime now)
=> this.TransitionAsync(phase, now);
- /// Admin: resets to the ownership (resting) phase and clears registrations.
+ /// Admin: resets to the ownership (resting) phase and clears registrations/battle state.
/// The current UTC time.
public ValueTask ResetAsync(DateTime now)
{
this._registeredGuilds.Clear();
- this._switchHolders.Clear();
- this._statuesRemaining = 0;
- this._occupier = null;
+ this.ClearBattleState();
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
}
@@ -135,69 +141,70 @@ public class CastleSiegeContext
/// The owner guild name, or null to clear.
public void SetOwner(string? guildName) => this.OwnerGuildName = guildName;
- /// Gets the number of guardian statues still standing (must be 0 before the throne can be taken).
- public int StatuesRemaining => this._statuesRemaining;
+ /// Sets how many castle defenses (gates + guardian statues) exist this siege (when spawned).
+ /// The defense count.
+ public void SetDefenseCount(int count) => this._defensesRemaining = count > 0 ? count : 0;
- /// Sets how many guardian statues defend the castle this siege (called when they are spawned).
- /// The statue count.
- public void SetStatueCount(int count) => this._statuesRemaining = count > 0 ? count : 0;
-
- /// Notifies that a guardian statue was destroyed. Lowers the remaining count (min 0).
- public void NotifyStatueDestroyed()
+ /// Notifies that a castle defense (gate/statue) was destroyed. Lowers the remaining count (min 0).
+ public void NotifyDefenseDestroyed()
{
- if (this._statuesRemaining > 0)
+ if (this._defensesRemaining > 0)
{
- this._statuesRemaining--;
+ this._defensesRemaining--;
}
}
///
- /// P3: a registered guild member activates one of the two Crown Switches (217/218) during the siege.
- /// The throne can only be taken by a guild holding BOTH switches. No-op outside the siege phase.
+ /// Sets which guild is currently standing on (holding) a Crown Switch. Called every tick by the plugin
+ /// based on player positions. Pass null when no registered member stands on it. No-op outside the siege.
///
/// The Crown Switch NPC number (217 or 218).
- /// The activating guild's name.
- public void HoldSwitch(short switchNumber, string guildName)
+ /// The holding guild's name, or null.
+ public void SetSwitchHolder(short switchNumber, string? guildName)
{
- if (this.Phase == CastleSiegePhase.Siege)
+ if (this.Phase == CastleSiegePhase.Siege && this._switchHolders.ContainsKey(switchNumber))
{
this._switchHolders[switchNumber] = guildName;
}
}
///
- /// P3: attempts to capture the throne for a guild. Succeeds only during the siege when all guardian
- /// statues are destroyed and the guild holds both Crown Switches.
+ /// 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.
///
- /// The capturing guild's name.
- /// A tuple of success and a human-readable reason/result message.
- public (bool Success, string Reason) TryCaptureThrone(string guildName)
+ /// The guild that just captured the throne (for announcing), or null if nothing changed.
+ public string? EvaluateCapture()
{
- if (this.Phase != CastleSiegePhase.Siege)
+ if (this.Phase != CastleSiegePhase.Siege || this._occupier is not null || this._defensesRemaining > 0)
{
- return (false, "The siege is not running.");
+ return null;
}
- if (this._statuesRemaining > 0)
+ var holder217 = this._switchHolders[217];
+ var holder218 = this._switchHolders[218];
+ if (holder217 is not null && holder217 == holder218)
{
- return (false, $"Destroy the guardian statues first ({this._statuesRemaining} remaining).");
+ this._occupier = holder217;
+ return holder217;
}
- var holdsBoth = this._switchHolders.TryGetValue(217, out var s217) && s217 == guildName
- && this._switchHolders.TryGetValue(218, out var s218) && s218 == guildName;
- if (!holdsBoth)
- {
- return (false, "Your guild must hold both Crown Switches (217 and 218) at once.");
- }
-
- this._occupier = guildName;
- return (true, "throne captured");
+ return null;
}
/// Returns a human-readable status summary for admin display.
public string GetStatusText()
=> $"CS phase={this.Phase}, owner={this.OwnerGuildName ?? "(none)"}, "
- + $"registered={this._registeredGuilds.Count} [{string.Join(", ", this._registeredGuilds)}]";
+ + $"registered={this._registeredGuilds.Count} [{string.Join(", ", this._registeredGuilds)}], "
+ + $"defenses={this._defensesRemaining}, throne={this._occupier ?? "(none)"}, "
+ + $"switch217={this._switchHolders[217] ?? "-"}, switch218={this._switchHolders[218] ?? "-"}";
+
+ private void ClearBattleState()
+ {
+ this._switchHolders[217] = null;
+ this._switchHolders[218] = null;
+ this._defensesRemaining = 0;
+ this._occupier = null;
+ }
private ValueTask TransitionAsync(CastleSiegePhase phase, DateTime now)
{
diff --git a/src/GameLogic/CastleSiege/CastleSiegeCrownTalkPlugIn.cs b/src/GameLogic/CastleSiege/CastleSiegeCrownTalkPlugIn.cs
deleted file mode 100644
index a5f17c2..0000000
--- a/src/GameLogic/CastleSiege/CastleSiegeCrownTalkPlugIn.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-//
-// 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 throne NPCs — Crown (216) and Sinior (223) — on Valley of Loren.
-/// A registered guild takes the throne only when it has destroyed all guardian statues AND holds both
-/// Crown Switches (217/218). The guild on the throne when the siege ends becomes the castle owner.
-///
-[Guid("CA5710A0-7A1B-4C2D-8E3F-000000000216")]
-[PlugIn]
-[Display(Name = "Castle Siege Crown/Throne", Description = "Captures the throne (Crown 216 / Sinior 223) once statues are down and both switches held.")]
-public class CastleSiegeCrownTalkPlugIn : IPlayerTalkToNpcPlugIn
-{
- private static readonly short[] ThroneNpcNumbers = { 216, 223 };
-
- ///
- 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);
- await ShowAsync(
- player,
- success
- ? $"Your guild '{guildName}' has CAPTURED THE THRONE! Hold it until the siege ends to win the castle!"
- : reason).ConfigureAwait(false);
- }
-
- private static ValueTask ShowAsync(Player player, string text)
- => player.InvokeViewPlugInAsync(p => p.ShowMessageAsync(text, MessageType.BlueNormal));
-}
diff --git a/src/GameLogic/CastleSiege/CastleSiegeThroneTalkPlugIn.cs b/src/GameLogic/CastleSiege/CastleSiegeThroneTalkPlugIn.cs
deleted file mode 100644
index 59c361f..0000000
--- a/src/GameLogic/CastleSiege/CastleSiegeThroneTalkPlugIn.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-//
-// 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.HoldSwitch(npc.Definition.Number, guildName);
- await ShowAsync(player, $"Your guild '{guildName}' is holding this Crown Switch. Hold BOTH switches (217 and 218) and destroy the guardian statues, then take the throne!").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 8915b28..e0df12b 100644
--- a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
+++ b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
@@ -11,6 +11,7 @@ using MUnique.OpenMU.GameLogic.CastleSiege;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.Interfaces;
+using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
///
@@ -24,8 +25,25 @@ using MUnique.OpenMU.PlugIns;
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration
{
private const ushort ValleyOfLorenMapNumber = 30;
- private const short GuardianStatueNumber = 132; // reuse the "Statue of Saint" destructible definition
- private static readonly (byte X, byte Y)[] StatuePositions = { (170, 206), (182, 206) };
+ private const short CastleGateNumber = 131; // reuse BloodCastle "Castle Gate" destructible definition
+ private const short GuardianStatueNumber = 132; // reuse BloodCastle "Statue of Saint" destructible definition
+ private const int SwitchHoldRange = 3;
+
+ // Castle defenses spawned at siege start — destructibles the attackers must break: (number, x, y).
+ private static readonly (short Number, byte X, byte Y)[] DefenseSpawns =
+ {
+ (CastleGateNumber, 160, 180),
+ (CastleGateNumber, 190, 180),
+ (GuardianStatueNumber, 170, 206),
+ (GuardianStatueNumber, 182, 206),
+ };
+
+ // Crown Switch positions on Valley of Loren (from the map init) — held by standing on them: (number, x, y).
+ private static readonly (short SwitchNumber, byte X, byte Y)[] SwitchPositions =
+ {
+ (217, 167, 194),
+ (218, 184, 195),
+ };
private static readonly ConcurrentDictionary Contexts = new();
@@ -57,6 +75,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
});
await context.TickAsync(DateTime.UtcNow).ConfigureAwait(false);
+
+ // During the siege, evaluate the Crown Switches (held by standing on them) and the throne capture every tick.
+ if (context.Phase == CastleSiegePhase.Siege)
+ {
+ await ProcessSiegeTickAsync(gameContext, context).ConfigureAwait(false);
+ }
}
///
@@ -79,8 +103,8 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
await AnnounceAsync(gameContext, "Castle Siege registration is now open! Guild masters, register at the Guardsman in the Valley of Loren.").ConfigureAwait(false);
break;
case CastleSiegePhase.Siege:
- await AnnounceAsync(gameContext, "The Castle Siege has begun! Destroy the guardian statues, hold both Crown Switches, then take the throne!").ConfigureAwait(false);
- await SpawnGuardianStatuesAsync(gameContext, context).ConfigureAwait(false);
+ await AnnounceAsync(gameContext, "The Castle Siege has begun! Break the castle gates and guardian statues, then hold BOTH Crown Switches to take the throne!").ConfigureAwait(false);
+ await SpawnCastleDefensesAsync(gameContext, context).ConfigureAwait(false);
await WarpRegisteredMembersToSiegeAsync(gameContext, context).ConfigureAwait(false);
break;
case CastleSiegePhase.Ownership when context.OwnerGuildName is { } owner:
@@ -101,36 +125,41 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
=> gameContext.ForEachPlayerAsync(player =>
player.InvokeViewPlugInAsync(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).AsTask());
- private static async Task SpawnGuardianStatuesAsync(IGameContext gameContext, CastleSiegeContext context)
+ private static async Task SpawnCastleDefensesAsync(IGameContext gameContext, CastleSiegeContext context)
{
try
{
if (gameContext is not GameContext concrete)
{
- context.SetStatueCount(0);
+ context.SetDefenseCount(0);
return;
}
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
- var statueDef = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == GuardianStatueNumber);
- if (map is null || statueDef is null)
+ if (map is null)
{
- context.SetStatueCount(0);
+ context.SetDefenseCount(0);
return;
}
var spawned = 0;
- for (var i = 0; i < StatuePositions.Length; i++)
+ for (var i = 0; i < DefenseSpawns.Length; i++)
{
- var pos = StatuePositions[i];
+ var spawn = DefenseSpawns[i];
+ var definition = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.Number);
+ if (definition is null)
+ {
+ continue;
+ }
+
var spawnArea = new MonsterSpawnArea
{
- MonsterDefinition = statueDef,
+ MonsterDefinition = definition,
Quantity = 1,
- X1 = pos.X,
- X2 = pos.X,
- Y1 = pos.Y,
- Y2 = pos.Y,
+ X1 = spawn.X,
+ X2 = spawn.X,
+ Y1 = spawn.Y,
+ Y2 = spawn.Y,
Direction = Direction.South,
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
};
@@ -138,18 +167,59 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
var npc = await concrete.MapInitializer.InitializeSpawnAsync(1000 + i, map, spawnArea).ConfigureAwait(false);
if (npc is AttackableNpcBase attackable)
{
- attackable.Died += (_, _) => context.NotifyStatueDestroyed();
+ attackable.Died += (_, _) => context.NotifyDefenseDestroyed();
spawned++;
}
}
- context.SetStatueCount(spawned);
+ context.SetDefenseCount(spawned);
}
catch (Exception ex)
{
gameContext.LoggerFactory.CreateLogger()
- .LogError(ex, "Castle Siege: error while spawning guardian statues.");
- context.SetStatueCount(0);
+ .LogError(ex, "Castle Siege: error while spawning castle defenses.");
+ context.SetDefenseCount(0);
+ }
+ }
+
+ private static async Task ProcessSiegeTickAsync(IGameContext gameContext, CastleSiegeContext context)
+ {
+ try
+ {
+ var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
+ if (map is null)
+ {
+ return;
+ }
+
+ // Each Crown Switch is held by whichever registered guild currently has a member standing on it.
+ foreach (var (switchNumber, x, y) in SwitchPositions)
+ {
+ string? holder = null;
+ var nearby = map.GetAttackablesInRange(new Point(x, y), SwitchHoldRange).OfType();
+ foreach (var player in nearby)
+ {
+ var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
+ if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
+ {
+ holder = guildName;
+ break;
+ }
+ }
+
+ 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)
+ {
+ gameContext.LoggerFactory.CreateLogger()
+ .LogError(ex, "Castle Siege: error during siege tick processing.");
}
}
diff --git a/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs b/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs
index 1035379..0fb0179 100644
--- a/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs
+++ b/tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs
@@ -67,25 +67,30 @@ public class CastleSiegeContextTest
Assert.That(ctx.RegisteredGuilds, Does.Contain("Attackers"));
}
- /// Tests the full siege objective chain: destroy statues, hold both switches, then capture the throne.
+ /// Tests the full siege objective chain: destroy defenses, then hold both switches to capture the throne.
[Test]
public async Task FullSiegeObjectiveChainToOwnershipAsync()
{
var ctx = new CastleSiegeContext(Config());
await ctx.ForceStartRegistrationAsync(T0);
await ctx.ForcePhaseAsync(CastleSiegePhase.Siege, T0);
- ctx.SetStatueCount(2);
+ ctx.SetDefenseCount(2);
- // Throne blocked until statues destroyed + both switches held.
- Assert.That(ctx.TryCaptureThrone("Attackers").Success, Is.False);
+ // Both switches held but defenses still up -> no capture.
+ ctx.SetSwitchHolder(217, "Attackers");
+ ctx.SetSwitchHolder(218, "Attackers");
+ Assert.That(ctx.EvaluateCapture(), Is.Null);
- ctx.NotifyStatueDestroyed();
- ctx.NotifyStatueDestroyed();
- ctx.HoldSwitch(217, "Attackers");
- Assert.That(ctx.TryCaptureThrone("Attackers").Success, Is.False); // only one switch held
+ ctx.NotifyDefenseDestroyed();
+ ctx.NotifyDefenseDestroyed();
- ctx.HoldSwitch(218, "Attackers");
- Assert.That(ctx.TryCaptureThrone("Attackers").Success, Is.True); // both switches + statues down
+ // Only one switch held -> still no capture.
+ ctx.SetSwitchHolder(218, null);
+ Assert.That(ctx.EvaluateCapture(), Is.Null);
+
+ // Both switches held by the same guild + defenses down -> capture.
+ ctx.SetSwitchHolder(218, "Attackers");
+ Assert.That(ctx.EvaluateCapture(), Is.EqualTo("Attackers"));
Assert.That(ctx.OccupierGuildName, Is.EqualTo("Attackers"));
await ctx.TickAsync(T0.AddMinutes(20)); // Siege -> Settlement
@@ -93,27 +98,27 @@ public class CastleSiegeContextTest
Assert.That(ctx.OwnerGuildName, Is.EqualTo("Attackers"));
}
- /// Tests that a rival guild holding only one switch cannot steal the throne.
+ /// Tests that two different guilds each holding one switch cannot capture the throne.
[Test]
public async Task ThroneRequiresBothSwitchesBySameGuildAsync()
{
var ctx = new CastleSiegeContext(Config());
await ctx.ForceStartRegistrationAsync(T0);
await ctx.ForcePhaseAsync(CastleSiegePhase.Siege, T0);
- ctx.SetStatueCount(0);
- ctx.HoldSwitch(217, "A");
- ctx.HoldSwitch(218, "B");
- Assert.That(ctx.TryCaptureThrone("A").Success, Is.False);
- Assert.That(ctx.TryCaptureThrone("B").Success, Is.False);
+ ctx.SetDefenseCount(0);
+ ctx.SetSwitchHolder(217, "A");
+ ctx.SetSwitchHolder(218, "B");
+ Assert.That(ctx.EvaluateCapture(), Is.Null);
}
- /// Tests that objective actions outside the siege phase are no-ops.
+ /// Tests that switch holding outside the siege phase is a no-op.
[Test]
- public void ObjectiveActionsOutsideSiegeAreNoOp()
+ public void SwitchHoldOutsideSiegeIsNoOp()
{
var ctx = new CastleSiegeContext(Config());
- ctx.HoldSwitch(217, "A");
- Assert.That(ctx.TryCaptureThrone("A").Success, Is.False);
+ ctx.SetSwitchHolder(217, "A");
+ ctx.SetSwitchHolder(218, "A");
+ Assert.That(ctx.EvaluateCapture(), Is.Null);
Assert.That(ctx.OccupierGuildName, Is.Null);
}