feat(CS): authentic Crown-hold throne capture (S6 protocol) replacing Sinior-talk
Some checks failed
.NET Core / build (push) Has been cancelled

Break all gates + hold both switches (defenses down) -> the Crown shield drops
(C1 B2 16=0). The guild master then stands on the Crown (176,212) and holds for
CrownHoldDuration (default 60s, client shows a 60s countdown via C1 B2 15) to
capture; capture broadcasts C1 B2 18 + golden text and sets the occupier. Losing a
switch or leaving the crown resets the hold (contestable until the siege timer ends).
Sinior (223) is now informational guidance. +2 tests (14 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Acentech Dev
2026-07-15 15:04:10 +03:00
parent 4651ef232c
commit 3dd4881406
7 changed files with 322 additions and 8 deletions

View File

@@ -63,6 +63,10 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
(218, 184, 195),
};
// The Crown (NPC 216) position on Valley of Loren — the guild master holds it here to capture the throne.
private static readonly Point CrownPosition = new(176, 212);
private const int CrownHoldRange = 4;
private static readonly ConcurrentDictionary<IGameContext, CastleSiegeContext> Contexts = new();
private string? _cachedFlagOwner;
@@ -350,9 +354,35 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
context.SetSwitchHolder(switchNumber, holder);
}
var now = DateTime.UtcNow;
// Crown-hold capture: when a guild holds both switches with every gate down, the crown shield drops;
// that guild's master then holds the crown for CrownHoldDuration to take the throne (contestable).
var eligible = context.GetShieldEligibleGuild();
var masterOnCrown = false;
if (eligible is not null)
{
foreach (var player in map.GetAttackablesInRange(CrownPosition, CrownHoldRange).OfType<Player>())
{
if (player.GuildStatus?.Position == GuildPosition.GuildMaster
&& await GetGuildNameAsync(player).ConfigureAwait(false) == eligible)
{
masterOnCrown = true;
break;
}
}
}
var holdDuration = context.Configuration.CrownHoldDuration;
var crown = context.TickCrownHold(eligible, masterOnCrown, now, holdDuration);
await BroadcastCrownAsync(gameContext, crown).ConfigureAwait(false);
if (crown.Event == CrownEvent.Captured && crown.Guild is { } capturedGuild)
{
await AnnounceAsync(gameContext, $"Guild '{capturedGuild}' has taken the Crown and now holds the throne!").ConfigureAwait(false);
}
// Keep the client's on-map countdown armed and in sync. Resend every 10s so players who just
// loaded the battle map pick it up, without visibly resetting the second-counter too often.
var now = DateTime.UtcNow;
if ((int)(now - context.PhaseStartedUtc).TotalSeconds % 10 == 0)
{
var remaining = context.GetRemainingSiegeTime(now);
@@ -386,6 +416,36 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
}
});
/// <summary>
/// Sends the crown shield state (each tick) and any crown-hold event (start/reset/capture) to every player
/// currently on the battle map.
/// </summary>
private static ValueTask BroadcastCrownAsync(IGameContext gameContext, CrownTickResult crown)
=> gameContext.ForEachPlayerAsync(async player =>
{
if (player.CurrentMap?.Definition.Number != ValleyOfLorenMapNumber)
{
return;
}
await player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownShieldAsync(crown.ShieldDown)).ConfigureAwait(false);
switch (crown.Event)
{
case CrownEvent.HoldStarted:
await player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(0, 0)).ConfigureAwait(false);
break;
case CrownEvent.HoldReset:
await player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(2, 0)).ConfigureAwait(false);
break;
case CrownEvent.Captured when crown.Guild is { } guild:
await player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.AnnounceSealCapturedAsync(guild)).ConfigureAwait(false);
break;
default:
break;
}
});
private async ValueTask BroadcastCastleFlagAsync(IGameContext gameContext, CastleSiegeContext context)
{
try