Compare commits
2 Commits
5549c2bec7
...
db8bdd394f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db8bdd394f | ||
|
|
c50af5e5c2 |
@@ -167,6 +167,7 @@ docker push <hub>/adamu-openmu:<tag>
|
|||||||
| `GameLogic/GameMap.cs` | 0b | remote-NPC: `GetNpcByNumber` helper (additive metod) | ✅ Uygulandı, `// ADAMU-CUSTOM` işaretli; test: `GameMapTest.GetNpcByNumberFindsSpawnedNpcAsync` |
|
| `GameLogic/GameMap.cs` | 0b | remote-NPC: `GetNpcByNumber` helper (additive metod) | ✅ Uygulandı, `// ADAMU-CUSTOM` işaretli; test: `GameMapTest.GetNpcByNumberFindsSpawnedNpcAsync` |
|
||||||
| `GameServer/MessageHandler/TalkNpcHandlerPlugInBase.cs` | 0b | remote-NPC: `0x8000` marker dalı (tek gerçek core-logic dokunuşu) | ✅ Uygulandı, `// ADAMU-CUSTOM` işaretli |
|
| `GameServer/MessageHandler/TalkNpcHandlerPlugInBase.cs` | 0b | remote-NPC: `0x8000` marker dalı (tek gerçek core-logic dokunuşu) | ✅ Uygulandı, `// ADAMU-CUSTOM` işaretli |
|
||||||
| `GameLogic/Player.cs` | P3 | CS PvP: Siege fazında Valley of Loren'de (map 30) kill izni (`IsCastleSiegeBattleActive`) | ✅ Uygulandı, `// ADAMU-CUSTOM` |
|
| `GameLogic/Player.cs` | P3 | CS PvP: Siege fazında Valley of Loren'de (map 30) kill izni (`IsCastleSiegeBattleActive`) | ✅ Uygulandı, `// ADAMU-CUSTOM` |
|
||||||
|
| `GameLogic/GameContext.cs` | P3 | `MapInitializer` property expose (guardian heykel Destructible runtime spawn) | ✅ Uygulandı, `// ADAMU-CUSTOM` |
|
||||||
| DataModel + Initialization (CS entity'leri) | P4 | CS persistence (sahiplik/vergi) | ⏳ Planlanacak (EF migration gerekir) |
|
| DataModel + Initialization (CS entity'leri) | P4 | CS persistence (sahiplik/vergi) | ⏳ Planlanacak (EF migration gerekir) |
|
||||||
| *(fazlar ilerledikçe eklenecek)* | | | |
|
| *(fazlar ilerledikçe eklenecek)* | | | |
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,23 @@
|
|||||||
namespace MUnique.OpenMU.GameLogic.CastleSiege;
|
namespace MUnique.OpenMU.GameLogic.CastleSiege;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CastleSiegeContext
|
public class CastleSiegeContext
|
||||||
{
|
{
|
||||||
|
/// <summary>The Crown Switch NPC numbers on Valley of Loren; both must be held to take the throne.</summary>
|
||||||
|
public static readonly short[] SwitchNumbers = { 217, 218 };
|
||||||
|
|
||||||
private readonly List<string> _registeredGuilds = new();
|
private readonly List<string> _registeredGuilds = new();
|
||||||
|
private readonly Dictionary<short, string?> _switchHolders = new() { { 217, null }, { 218, null } };
|
||||||
private DateTime _phaseStartedUtc;
|
private DateTime _phaseStartedUtc;
|
||||||
private string? _occupier;
|
private string? _occupier;
|
||||||
|
private int _defensesRemaining;
|
||||||
|
|
||||||
/// <summary>Initializes a new instance of the <see cref="CastleSiegeContext"/> class.</summary>
|
/// <summary>Initializes a new instance of the <see cref="CastleSiegeContext"/> class.</summary>
|
||||||
/// <param name="configuration">The cycle timing configuration.</param>
|
/// <param name="configuration">The cycle timing configuration.</param>
|
||||||
@@ -34,12 +43,15 @@ public class CastleSiegeContext
|
|||||||
/// <summary>Gets the current owner guild name, or null if unowned.</summary>
|
/// <summary>Gets the current owner guild name, or null if unowned.</summary>
|
||||||
public string? OwnerGuildName { get; private set; }
|
public string? OwnerGuildName { get; private set; }
|
||||||
|
|
||||||
/// <summary>Gets the guild names registered for the current cycle.</summary>
|
|
||||||
public IReadOnlyList<string> RegisteredGuilds => this._registeredGuilds;
|
|
||||||
|
|
||||||
/// <summary>Gets the guild currently holding the throne during the siege (P3), or null.</summary>
|
/// <summary>Gets the guild currently holding the throne during the siege (P3), or null.</summary>
|
||||||
public string? OccupierGuildName => this._occupier;
|
public string? OccupierGuildName => this._occupier;
|
||||||
|
|
||||||
|
/// <summary>Gets the number of castle defenses (gates + statues) still standing; the throne needs 0.</summary>
|
||||||
|
public int DefensesRemaining => this._defensesRemaining;
|
||||||
|
|
||||||
|
/// <summary>Gets the guild names registered for the current cycle.</summary>
|
||||||
|
public IReadOnlyList<string> RegisteredGuilds => this._registeredGuilds;
|
||||||
|
|
||||||
/// <summary>Advances the state machine based on the current time.</summary>
|
/// <summary>Advances the state machine based on the current time.</summary>
|
||||||
/// <param name="now">The current UTC time.</param>
|
/// <param name="now">The current UTC time.</param>
|
||||||
public ValueTask TickAsync(DateTime now)
|
public ValueTask TickAsync(DateTime now)
|
||||||
@@ -75,13 +87,13 @@ public class CastleSiegeContext
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case CastleSiegePhase.Settlement:
|
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)
|
if (this._occupier is { } occupier)
|
||||||
{
|
{
|
||||||
this.SetOwner(occupier);
|
this.SetOwner(occupier);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._occupier = null;
|
this.ClearBattleState();
|
||||||
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
|
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -95,7 +107,7 @@ public class CastleSiegeContext
|
|||||||
public ValueTask ForceStartRegistrationAsync(DateTime now)
|
public ValueTask ForceStartRegistrationAsync(DateTime now)
|
||||||
{
|
{
|
||||||
this._registeredGuilds.Clear();
|
this._registeredGuilds.Clear();
|
||||||
this._occupier = null;
|
this.ClearBattleState();
|
||||||
return this.TransitionAsync(CastleSiegePhase.Registration, now);
|
return this.TransitionAsync(CastleSiegePhase.Registration, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,12 +117,12 @@ public class CastleSiegeContext
|
|||||||
public ValueTask ForcePhaseAsync(CastleSiegePhase phase, DateTime now)
|
public ValueTask ForcePhaseAsync(CastleSiegePhase phase, DateTime now)
|
||||||
=> this.TransitionAsync(phase, now);
|
=> this.TransitionAsync(phase, now);
|
||||||
|
|
||||||
/// <summary>Admin: resets to the ownership (resting) phase and clears registrations.</summary>
|
/// <summary>Admin: resets to the ownership (resting) phase and clears registrations/battle state.</summary>
|
||||||
/// <param name="now">The current UTC time.</param>
|
/// <param name="now">The current UTC time.</param>
|
||||||
public ValueTask ResetAsync(DateTime now)
|
public ValueTask ResetAsync(DateTime now)
|
||||||
{
|
{
|
||||||
this._registeredGuilds.Clear();
|
this._registeredGuilds.Clear();
|
||||||
this._occupier = null;
|
this.ClearBattleState();
|
||||||
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
|
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,20 +141,70 @@ public class CastleSiegeContext
|
|||||||
/// <param name="guildName">The owner guild name, or null to clear.</param>
|
/// <param name="guildName">The owner guild name, or null to clear.</param>
|
||||||
public void SetOwner(string? guildName) => this.OwnerGuildName = guildName;
|
public void SetOwner(string? guildName) => this.OwnerGuildName = guildName;
|
||||||
|
|
||||||
/// <summary>P3: a registered guild captures the throne during the siege. No-op outside the siege phase.</summary>
|
/// <summary>Sets how many castle defenses (gates + guardian statues) exist this siege (when spawned).</summary>
|
||||||
/// <param name="guildName">The capturing guild's name.</param>
|
/// <param name="count">The defense count.</param>
|
||||||
public void CaptureThrone(string guildName)
|
public void SetDefenseCount(int count) => this._defensesRemaining = count > 0 ? count : 0;
|
||||||
|
|
||||||
|
/// <summary>Notifies that a castle defense (gate/statue) was destroyed. Lowers the remaining count (min 0).</summary>
|
||||||
|
public void NotifyDefenseDestroyed()
|
||||||
{
|
{
|
||||||
if (this.Phase == CastleSiegePhase.Siege)
|
if (this._defensesRemaining > 0)
|
||||||
{
|
{
|
||||||
this._occupier = guildName;
|
this._defensesRemaining--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets which guild is currently standing on (holding) a Crown Switch. Called every tick by the plugin
|
||||||
|
/// based on player positions. Pass <c>null</c> when no registered member stands on it. No-op outside the siege.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="switchNumber">The Crown Switch NPC number (217 or 218).</param>
|
||||||
|
/// <param name="guildName">The holding guild's name, or null.</param>
|
||||||
|
public void SetSwitchHolder(short switchNumber, string? guildName)
|
||||||
|
{
|
||||||
|
if (this.Phase == CastleSiegePhase.Siege && this._switchHolders.ContainsKey(switchNumber))
|
||||||
|
{
|
||||||
|
this._switchHolders[switchNumber] = guildName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The guild that just captured the throne (for announcing), or <c>null</c> if nothing changed.</returns>
|
||||||
|
public string? EvaluateCapture()
|
||||||
|
{
|
||||||
|
if (this.Phase != CastleSiegePhase.Siege || this._occupier is not null || this._defensesRemaining > 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var holder217 = this._switchHolders[217];
|
||||||
|
var holder218 = this._switchHolders[218];
|
||||||
|
if (holder217 is not null && holder217 == holder218)
|
||||||
|
{
|
||||||
|
this._occupier = holder217;
|
||||||
|
return holder217;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Returns a human-readable status summary for admin display.</summary>
|
/// <summary>Returns a human-readable status summary for admin display.</summary>
|
||||||
public string GetStatusText()
|
public string GetStatusText()
|
||||||
=> $"CS phase={this.Phase}, owner={this.OwnerGuildName ?? "(none)"}, "
|
=> $"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)
|
private ValueTask TransitionAsync(CastleSiegePhase phase, DateTime now)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
// <copyright file="CastleSiegeThroneTalkPlugIn.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 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 <see cref="CastleSiegeContext"/>).
|
|
||||||
/// </summary>
|
|
||||||
[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 };
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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.CaptureThrone(guildName);
|
|
||||||
await ShowAsync(player, $"Your guild '{guildName}' has captured the throne! Hold it until the siege ends to win the castle.").ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ValueTask ShowAsync(Player player, string text)
|
|
||||||
=> player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(text, MessageType.BlueNormal));
|
|
||||||
}
|
|
||||||
@@ -116,6 +116,10 @@ public class GameContext : AsyncDisposable, IGameContext
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual float MasterExperienceRate => this.Configuration.MasterExperienceRate;
|
public virtual float MasterExperienceRate => this.Configuration.MasterExperienceRate;
|
||||||
|
|
||||||
|
// ADAMU-CUSTOM: expose the map initializer so the Castle Siege can spawn guardian statues (Destructibles) at runtime.
|
||||||
|
/// <summary>Gets the map initializer (used to spawn NPCs/destructibles at runtime, e.g. Castle Siege statues).</summary>
|
||||||
|
public IMapInitializer MapInitializer => this._mapInitializer;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool PvpEnabled { get; }
|
public virtual bool PvpEnabled { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ using System.Collections.Concurrent;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using MUnique.OpenMU.GameLogic.CastleSiege;
|
using MUnique.OpenMU.GameLogic.CastleSiege;
|
||||||
|
using MUnique.OpenMU.GameLogic.NPC;
|
||||||
using MUnique.OpenMU.GameLogic.Views;
|
using MUnique.OpenMU.GameLogic.Views;
|
||||||
using MUnique.OpenMU.Interfaces;
|
using MUnique.OpenMU.Interfaces;
|
||||||
|
using MUnique.OpenMU.Pathfinding;
|
||||||
using MUnique.OpenMU.PlugIns;
|
using MUnique.OpenMU.PlugIns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -23,6 +25,25 @@ using MUnique.OpenMU.PlugIns;
|
|||||||
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<CastleSiegeConfiguration>, ISupportDefaultCustomConfiguration
|
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<CastleSiegeConfiguration>, ISupportDefaultCustomConfiguration
|
||||||
{
|
{
|
||||||
private const ushort ValleyOfLorenMapNumber = 30;
|
private const ushort ValleyOfLorenMapNumber = 30;
|
||||||
|
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<IGameContext, CastleSiegeContext> Contexts = new();
|
private static readonly ConcurrentDictionary<IGameContext, CastleSiegeContext> Contexts = new();
|
||||||
|
|
||||||
@@ -54,6 +75,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
});
|
});
|
||||||
|
|
||||||
await context.TickAsync(DateTime.UtcNow).ConfigureAwait(false);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -76,7 +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);
|
await AnnounceAsync(gameContext, "Castle Siege registration is now open! Guild masters, register at the Guardsman in the Valley of Loren.").ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
case CastleSiegePhase.Siege:
|
case CastleSiegePhase.Siege:
|
||||||
await AnnounceAsync(gameContext, "The Castle Siege has begun! Fight your way to the throne and capture it to win the castle!").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);
|
await WarpRegisteredMembersToSiegeAsync(gameContext, context).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
case CastleSiegePhase.Ownership when context.OwnerGuildName is { } owner:
|
case CastleSiegePhase.Ownership when context.OwnerGuildName is { } owner:
|
||||||
@@ -97,6 +125,104 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
=> gameContext.ForEachPlayerAsync(player =>
|
=> gameContext.ForEachPlayerAsync(player =>
|
||||||
player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).AsTask());
|
player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).AsTask());
|
||||||
|
|
||||||
|
private static async Task SpawnCastleDefensesAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (gameContext is not GameContext concrete)
|
||||||
|
{
|
||||||
|
context.SetDefenseCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
||||||
|
if (map is null)
|
||||||
|
{
|
||||||
|
context.SetDefenseCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var spawned = 0;
|
||||||
|
for (var i = 0; i < DefenseSpawns.Length; 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 = definition,
|
||||||
|
Quantity = 1,
|
||||||
|
X1 = spawn.X,
|
||||||
|
X2 = spawn.X,
|
||||||
|
Y1 = spawn.Y,
|
||||||
|
Y2 = spawn.Y,
|
||||||
|
Direction = Direction.South,
|
||||||
|
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
|
||||||
|
};
|
||||||
|
|
||||||
|
var npc = await concrete.MapInitializer.InitializeSpawnAsync(1000 + i, map, spawnArea).ConfigureAwait(false);
|
||||||
|
if (npc is AttackableNpcBase attackable)
|
||||||
|
{
|
||||||
|
attackable.Died += (_, _) => context.NotifyDefenseDestroyed();
|
||||||
|
spawned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.SetDefenseCount(spawned);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||||
|
.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<Player>();
|
||||||
|
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<CastleSiegeEventPlugIn>()
|
||||||
|
.LogError(ex, "Castle Siege: error during siege tick processing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
|
private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -67,27 +67,58 @@ public class CastleSiegeContextTest
|
|||||||
Assert.That(ctx.RegisteredGuilds, Does.Contain("Attackers"));
|
Assert.That(ctx.RegisteredGuilds, Does.Contain("Attackers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Tests that the throne-holding guild at siege end becomes the owner on settlement (P3).</summary>
|
/// <summary>Tests the full siege objective chain: destroy defenses, then hold both switches to capture the throne.</summary>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task ThroneCaptureDuringSiegeBecomesOwnerOnSettlementAsync()
|
public async Task FullSiegeObjectiveChainToOwnershipAsync()
|
||||||
{
|
{
|
||||||
var ctx = new CastleSiegeContext(Config());
|
var ctx = new CastleSiegeContext(Config());
|
||||||
await ctx.ForceStartRegistrationAsync(T0);
|
await ctx.ForceStartRegistrationAsync(T0);
|
||||||
await ctx.ForcePhaseAsync(CastleSiegePhase.Siege, T0);
|
await ctx.ForcePhaseAsync(CastleSiegePhase.Siege, T0);
|
||||||
ctx.CaptureThrone("Attackers");
|
ctx.SetDefenseCount(2);
|
||||||
|
|
||||||
|
// Both switches held but defenses still up -> no capture.
|
||||||
|
ctx.SetSwitchHolder(217, "Attackers");
|
||||||
|
ctx.SetSwitchHolder(218, "Attackers");
|
||||||
|
Assert.That(ctx.EvaluateCapture(), Is.Null);
|
||||||
|
|
||||||
|
ctx.NotifyDefenseDestroyed();
|
||||||
|
ctx.NotifyDefenseDestroyed();
|
||||||
|
|
||||||
|
// 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"));
|
Assert.That(ctx.OccupierGuildName, Is.EqualTo("Attackers"));
|
||||||
|
|
||||||
await ctx.TickAsync(T0.AddMinutes(20)); // Siege -> Settlement
|
await ctx.TickAsync(T0.AddMinutes(20)); // Siege -> Settlement
|
||||||
await ctx.TickAsync(T0.AddMinutes(20)); // Settlement -> Ownership (owner assigned)
|
await ctx.TickAsync(T0.AddMinutes(20)); // Settlement -> Ownership
|
||||||
Assert.That(ctx.Phase, Is.EqualTo(CastleSiegePhase.Ownership));
|
|
||||||
Assert.That(ctx.OwnerGuildName, Is.EqualTo("Attackers"));
|
Assert.That(ctx.OwnerGuildName, Is.EqualTo("Attackers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Tests that capturing the throne outside the siege phase is a no-op.</summary>
|
/// <summary>Tests that two different guilds each holding one switch cannot capture the throne.</summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void CaptureThroneOutsideSiegeIsNoOp()
|
public async Task ThroneRequiresBothSwitchesBySameGuildAsync()
|
||||||
{
|
{
|
||||||
var ctx = new CastleSiegeContext(Config());
|
var ctx = new CastleSiegeContext(Config());
|
||||||
ctx.CaptureThrone("Attackers");
|
await ctx.ForceStartRegistrationAsync(T0);
|
||||||
|
await ctx.ForcePhaseAsync(CastleSiegePhase.Siege, T0);
|
||||||
|
ctx.SetDefenseCount(0);
|
||||||
|
ctx.SetSwitchHolder(217, "A");
|
||||||
|
ctx.SetSwitchHolder(218, "B");
|
||||||
|
Assert.That(ctx.EvaluateCapture(), Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests that switch holding outside the siege phase is a no-op.</summary>
|
||||||
|
[Test]
|
||||||
|
public void SwitchHoldOutsideSiegeIsNoOp()
|
||||||
|
{
|
||||||
|
var ctx = new CastleSiegeContext(Config());
|
||||||
|
ctx.SetSwitchHolder(217, "A");
|
||||||
|
ctx.SetSwitchHolder(218, "A");
|
||||||
|
Assert.That(ctx.EvaluateCapture(), Is.Null);
|
||||||
Assert.That(ctx.OccupierGuildName, Is.Null);
|
Assert.That(ctx.OccupierGuildName, Is.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user