feat(CS-P3): throne registration via Sinior/Crown talk (223/216) after defenses down + both switches held; reachable defense positions
Some checks failed
.NET Core / build (push) Has been cancelled

This commit is contained in:
Acentech Dev
2026-07-15 02:49:27 +03:00
parent db8bdd394f
commit 72ebc96e28
4 changed files with 122 additions and 30 deletions

View File

@@ -169,26 +169,38 @@ public class CastleSiegeContext
}
/// <summary>
/// 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).
/// </summary>
/// <returns>The guild that just captured the throne (for announcing), or <c>null</c> if nothing changed.</returns>
public string? EvaluateCapture()
/// <param name="guildName">The capturing guild's name.</param>
/// <returns>Whether it succeeded and a human-readable reason/result message.</returns>
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");
}
/// <summary>Returns a human-readable status summary for admin display.</summary>

View File

@@ -0,0 +1,83 @@
// <copyright file="CastleSiegeThroneCaptureTalkPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
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;
/// <summary>
/// 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.
/// </summary>
[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 };
/// <inheritdoc />
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<IShowMessagePlugIn>(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<IShowMessagePlugIn>(p => p.ShowMessageAsync(text, MessageType.BlueNormal));
}

View File

@@ -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)
{

View File

@@ -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);
}
/// <summary>Tests that switch holding outside the siege phase is a no-op.</summary>
/// <summary>Tests that capturing the throne outside the siege phase is a no-op.</summary>
[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);
}