fix(CS): stop crown re-capture loop + show seal panel only to the master
Some checks failed
.NET Core / build (push) Has been cancelled
Some checks failed
.NET Core / build (push) Has been cancelled
- The occupier can no longer re-register its own throne (canCapture = eligible != occupier), which caused a repeating 60s panel loop; only a DIFFERENT guild can contest. - The 60s registration panel (0xB2/0x15) is now sent ONLY to the master on the crown, not broadcast to switch-holders/other players. Shield(0x16) + capture(0x18) still broadcast. +1 test (15 total).
This commit is contained in:
@@ -231,7 +231,11 @@ public class CastleSiegeContext
|
|||||||
|
|
||||||
var wasHolding = this._crownHoldGuild is not null;
|
var wasHolding = this._crownHoldGuild is not null;
|
||||||
|
|
||||||
if (eligibleGuild is null || !masterHolding)
|
// The guild that already occupies the throne just holds it — no re-registration (avoids a capture loop).
|
||||||
|
// Only a DIFFERENT guild taking both switches can register/capture (contest).
|
||||||
|
var canCapture = eligibleGuild is not null && eligibleGuild != this._occupier;
|
||||||
|
|
||||||
|
if (!canCapture || !masterHolding)
|
||||||
{
|
{
|
||||||
this._crownHoldGuild = null;
|
this._crownHoldGuild = null;
|
||||||
this._crownHoldStartUtc = null;
|
this._crownHoldStartUtc = null;
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
// Crown-hold capture: when a guild holds both switches with every gate down, the crown shield drops;
|
// 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).
|
// that guild's master then holds the crown for CrownHoldDuration to take the throne (contestable).
|
||||||
var eligible = context.GetShieldEligibleGuild();
|
var eligible = context.GetShieldEligibleGuild();
|
||||||
var masterOnCrown = false;
|
Player? masterPlayer = null;
|
||||||
if (eligible is not null)
|
if (eligible is not null)
|
||||||
{
|
{
|
||||||
foreach (var player in map.GetAttackablesInRange(CrownPosition, CrownHoldRange).OfType<Player>())
|
foreach (var player in map.GetAttackablesInRange(CrownPosition, CrownHoldRange).OfType<Player>())
|
||||||
@@ -367,18 +367,35 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
if (player.GuildStatus?.Position == GuildPosition.GuildMaster
|
if (player.GuildStatus?.Position == GuildPosition.GuildMaster
|
||||||
&& await GetGuildNameAsync(player).ConfigureAwait(false) == eligible)
|
&& await GetGuildNameAsync(player).ConfigureAwait(false) == eligible)
|
||||||
{
|
{
|
||||||
masterOnCrown = true;
|
masterPlayer = player;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var holdDuration = context.Configuration.CrownHoldDuration;
|
var crown = context.TickCrownHold(eligible, masterPlayer is not null, now, context.Configuration.CrownHoldDuration);
|
||||||
var crown = context.TickCrownHold(eligible, masterOnCrown, now, holdDuration);
|
|
||||||
await BroadcastCrownAsync(gameContext, crown).ConfigureAwait(false);
|
// Shield drop/raise -> everyone on the battle map, but only when it flips (the packet pops a modal).
|
||||||
if (crown.Event == CrownEvent.Captured && crown.Guild is { } capturedGuild)
|
if (crown.ShieldChanged)
|
||||||
{
|
{
|
||||||
await AnnounceAsync(gameContext, $"Guild '{capturedGuild}' has taken the Crown and now holds the throne!").ConfigureAwait(false);
|
await ForEachOnBattleMapAsync(gameContext, p => p.SetCrownShieldAsync(crown.ShieldDown)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (crown.Event)
|
||||||
|
{
|
||||||
|
case CrownEvent.HoldStarted when masterPlayer is not null:
|
||||||
|
// The 60-second registration panel is shown ONLY to the master taking the crown.
|
||||||
|
await masterPlayer.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(0, 0)).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
case CrownEvent.HoldReset when masterPlayer is not null:
|
||||||
|
await masterPlayer.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(2, 0)).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
case CrownEvent.Captured when crown.Guild is { } captured:
|
||||||
|
await ForEachOnBattleMapAsync(gameContext, p => p.AnnounceSealCapturedAsync(captured)).ConfigureAwait(false);
|
||||||
|
await AnnounceAsync(gameContext, $"Guild '{captured}' has taken the Crown and now holds the throne!").ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the client's on-map countdown armed and in sync. Resend every 10s so players who just
|
// Keep the client's on-map countdown armed and in sync. Resend every 10s so players who just
|
||||||
@@ -416,40 +433,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Invokes the Castle Siege status view for every player currently on the battle map.</summary>
|
||||||
/// Sends the crown shield state (each tick) and any crown-hold event (start/reset/capture) to every player
|
private static ValueTask ForEachOnBattleMapAsync(IGameContext gameContext, Func<ICastleSiegeStatusViewPlugIn, ValueTask> action)
|
||||||
/// currently on the battle map.
|
=> gameContext.ForEachPlayerAsync(player =>
|
||||||
/// </summary>
|
player.CurrentMap?.Definition.Number == ValleyOfLorenMapNumber
|
||||||
private static ValueTask BroadcastCrownAsync(IGameContext gameContext, CrownTickResult crown)
|
? player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(action).AsTask()
|
||||||
=> gameContext.ForEachPlayerAsync(async player =>
|
: Task.CompletedTask);
|
||||||
{
|
|
||||||
if (player.CurrentMap?.Definition.Number != ValleyOfLorenMapNumber)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only tell the client when the shield state actually flips — the 0xB2/0x16 packet pops a modal
|
|
||||||
// message box, so sending it every tick would block player input.
|
|
||||||
if (crown.ShieldChanged)
|
|
||||||
{
|
|
||||||
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)
|
private async ValueTask BroadcastCastleFlagAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -236,6 +236,35 @@ public class CastleSiegeContextTest
|
|||||||
Assert.That(ctx.OccupierGuildName, Is.Null);
|
Assert.That(ctx.OccupierGuildName, Is.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests that the occupier can't re-capture its own throne, but a different guild can contest it.</summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OccupierDoesNotRecaptureButAnotherGuildCanContestAsync()
|
||||||
|
{
|
||||||
|
var ctx = new CastleSiegeContext(Config());
|
||||||
|
await ctx.ForceStartRegistrationAsync(T0);
|
||||||
|
await ctx.ForcePhaseAsync(CastleSiegePhase.Siege, T0);
|
||||||
|
ctx.SetDefenseCount(0);
|
||||||
|
var hold = TimeSpan.FromSeconds(60);
|
||||||
|
|
||||||
|
ctx.SetSwitchHolder(217, "A");
|
||||||
|
ctx.SetSwitchHolder(218, "A");
|
||||||
|
ctx.TickCrownHold("A", true, T0, hold);
|
||||||
|
Assert.That(ctx.TickCrownHold("A", true, T0.AddSeconds(60), hold).Event, Is.EqualTo(CrownEvent.Captured));
|
||||||
|
Assert.That(ctx.OccupierGuildName, Is.EqualTo("A"));
|
||||||
|
|
||||||
|
// A keeps holding — no re-registration loop, but the shield stays down (they hold it).
|
||||||
|
var after = ctx.TickCrownHold("A", true, T0.AddSeconds(61), hold);
|
||||||
|
Assert.That(after.Event, Is.EqualTo(CrownEvent.None));
|
||||||
|
Assert.That(after.ShieldDown, Is.True);
|
||||||
|
|
||||||
|
// B takes both switches and can contest/capture.
|
||||||
|
ctx.SetSwitchHolder(217, "B");
|
||||||
|
ctx.SetSwitchHolder(218, "B");
|
||||||
|
Assert.That(ctx.TickCrownHold("B", true, T0.AddSeconds(62), hold).Event, Is.EqualTo(CrownEvent.HoldStarted));
|
||||||
|
Assert.That(ctx.TickCrownHold("B", true, T0.AddSeconds(122), hold).Event, Is.EqualTo(CrownEvent.Captured));
|
||||||
|
Assert.That(ctx.OccupierGuildName, Is.EqualTo("B"));
|
||||||
|
}
|
||||||
|
|
||||||
private static CastleSiegeConfiguration Config() => new()
|
private static CastleSiegeConfiguration Config() => new()
|
||||||
{
|
{
|
||||||
RegistrationDuration = TimeSpan.FromMinutes(5),
|
RegistrationDuration = TimeSpan.FromMinutes(5),
|
||||||
|
|||||||
Reference in New Issue
Block a user