Files
AdamuSw/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs

301 lines
12 KiB
C#

// <copyright file="CastleSiegeEventPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
using System.Collections.Concurrent;
using System.Linq;
using System.Runtime.InteropServices;
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>
/// Drives the Castle Siege phase state machine: ticks it every second and carries its configuration.
/// State is per-<see cref="IGameContext"/> and kept in memory (P1: no persistence).
/// When the siege phase starts (P3), warps registered guild members to the Valley of Loren battle map.
/// </summary>
[PlugIn]
[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (phase state machine, scheduling, siege warp).")]
[Guid("6E2C8B41-9A4D-4C2E-9E7B-1F2A3B4C5D60")]
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<CastleSiegeConfiguration>, ISupportDefaultCustomConfiguration
{
private const ushort ValleyOfLorenMapNumber = 30;
private const short CastleGateNumber = 277; // client MONSTER_CASTLE_GATE1 -> renders a real gate + blocks terrain until broken
private const short GateTemplateNumber = 131; // BloodCastle "Castle Gate" destructible (has HP) — HP template for 277
private const short GuardianStatueNumber = 283; // "Guardian Statue" — inner castle defense
private const short StatueTemplateNumber = 132; // BloodCastle "Statue of Saint" destructible (has HP) — HP template for 283
private const int SwitchHoldRange = 3;
// Castle defenses spawned at siege start — destructibles the attackers must break: (number, x, y).
// Gates are at the 6 REAL castle gate positions (client g_byGateLocation) so the client renders them
// closed and blocks the terrain until broken. Guardian statues stand inside near the throne.
private static readonly (short Number, byte X, byte Y)[] DefenseSpawns =
{
// 6 real castle gates (client g_byGateLocation) — close + block terrain until broken.
(CastleGateNumber, 67, 114),
(CastleGateNumber, 93, 114),
(CastleGateNumber, 119, 114),
(CastleGateNumber, 81, 161),
(CastleGateNumber, 107, 161),
(CastleGateNumber, 93, 204),
// 4 guardian statues inside the castle (real map positions) — must all be destroyed for the throne.
(GuardianStatueNumber, 82, 129),
(GuardianStatueNumber, 107, 129),
(GuardianStatueNumber, 94, 131),
(GuardianStatueNumber, 94, 126),
};
// 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();
/// <inheritdoc />
public CastleSiegeConfiguration? Configuration { get; set; }
/// <summary>
/// Gets the Castle Siege context for a game context, if the periodic tick has initialized it.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <returns>The context, or <c>null</c> if not yet initialized.</returns>
public static CastleSiegeContext? TryGetContext(IGameContext gameContext)
=> Contexts.TryGetValue(gameContext, out var context) ? context : null;
/// <inheritdoc />
public object CreateDefaultConfig() => new CastleSiegeConfiguration();
/// <inheritdoc />
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
{
var context = Contexts.GetOrAdd(gameContext, gc =>
{
var created = new CastleSiegeContext(this.Configuration ?? new CastleSiegeConfiguration());
// Announce phase changes to the whole server and, when the siege begins, warp registered members.
created.PhaseChanged += phase => _ = OnPhaseChangedAsync(gc, created, phase);
return created;
});
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 />
public void ForceStart()
{
// Force-start applies to every active game context's siege.
foreach (var context in Contexts.Values)
{
_ = context.ForceStartRegistrationAsync(DateTime.UtcNow);
}
}
private static async Task OnPhaseChangedAsync(IGameContext gameContext, CastleSiegeContext context, CastleSiegePhase phase)
{
try
{
switch (phase)
{
case CastleSiegePhase.Registration:
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! 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:
await AnnounceAsync(gameContext, $"The Castle Siege has ended. The castle now belongs to the guild '{owner}'!").ConfigureAwait(false);
break;
default:
break;
}
}
catch (Exception ex)
{
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
.LogError(ex, "Castle Siege: error handling phase change to {phase}.", phase);
}
}
private static ValueTask AnnounceAsync(IGameContext gameContext, string message)
=> gameContext.ForEachPlayerAsync(player =>
player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).AsTask());
private static void EnsureDestructible(MonsterDefinition target, MonsterDefinition? template)
{
if (target.ObjectKind == NpcObjectKind.Destructible)
{
return;
}
target.ObjectKind = NpcObjectKind.Destructible;
if (template is not null && target.Attributes.Count == 0)
{
foreach (var attribute in template.Attributes)
{
target.Attributes.Add(attribute);
}
}
}
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;
}
// The stock CS gate (277) / statue (283) definitions have no HP and aren't destructible;
// make them breakable by borrowing the HP/attributes of a working destructible (131/132).
var template = gameContext.Configuration.Monsters.FirstOrDefault(
m => m.Number == (spawn.Number == CastleGateNumber ? GateTemplateNumber : StatueTemplateNumber));
EnsureDestructible(definition, template);
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);
}
}
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)
{
try
{
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
if (map?.SafeZoneSpawnGate is not { } gate)
{
return;
}
await gameContext.ForEachPlayerAsync(async player =>
{
var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
{
await player.WarpToAsync(gate).ConfigureAwait(false);
}
}).ConfigureAwait(false);
}
catch (Exception ex)
{
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
.LogError(ex, "Castle Siege: error while warping registered members to the battle map.");
}
}
private static async ValueTask<string?> GetGuildNameAsync(Player player)
{
if (player.GuildStatus is not { } guildStatus)
{
return null;
}
if (player.GameContext is IGameServerContext serverContext)
{
var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
if (guild?.Name is { Length: > 0 } name)
{
return name;
}
}
return guildStatus.GuildId.ToString();
}
}