feat(CS-P3): PROPER siege - position-based Crown Switch hold (stand on them) + gates & statues destructibles + auto throne capture; remove non-working talk handlers
Some checks failed
.NET Core / build (push) Has been cancelled
Some checks failed
.NET Core / build (push) Has been cancelled
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -24,8 +25,25 @@ using MUnique.OpenMU.PlugIns;
|
||||
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<CastleSiegeConfiguration>, 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<IGameContext, CastleSiegeContext> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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<IShowMessagePlugIn>(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<CastleSiegeEventPlugIn>()
|
||||
.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<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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user