feat(hs): apply mapped class buffs to team on statue break

This commit is contained in:
Acentech Dev
2026-07-21 00:15:52 +03:00
parent 712773c332
commit ceacc11446
2 changed files with 108 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlayerActions.MiniGames;
using MUnique.OpenMU.Pathfinding;
@@ -74,6 +75,22 @@ public class HeykelSavasiContext : MiniGameContext
/// </summary>
private static readonly (int Dx, int Dy)[] GuardOffsets = { (-2, 0), (2, 0), (0, -2), (0, 2) };
/// <summary>
/// Maps a destroyed statue's index (0..5) to the <see cref="MUnique.OpenMU.DataModel.Configuration.MagicEffectDefinition.Number"/>
/// of the class buff granted to the whole attacking team. All of these effects already exist in
/// configuration (no custom <c>MagicEffectDefinition</c>s were added): 1=GreaterDamage, 2=GreaterDefense,
/// 4=SoulBarrier, 5=CriticalDamageIncrease (DL), 0x52=WizEnhance (SM), 129=IgnoreDefense (RF).
/// Index 6 (the 7th/final statue) intentionally has no entry - breaking it is the win condition, not a
/// buff trigger; see the bounds check in <see cref="ApplyStatueBuffToTeamAsync"/>.
/// </summary>
private static readonly short[] StatueBuffEffectNumbers = { 1, 2, 4, 5, 0x52, 129 };
/// <summary>
/// The duration of the class buff granted on a statue break, chosen to comfortably outlast a single
/// match (matches run longer than 5 minutes).
/// </summary>
private static readonly TimeSpan BuffDuration = TimeSpan.FromMinutes(6);
/// <summary>
/// PLACEHOLDER coordinates: the Heykel Savasi map terrain is not final yet, so these positions are an
/// approximate straight line from mid-map toward the red base (20, 20). Index 0 is the outermost statue
@@ -209,6 +226,13 @@ public class HeykelSavasiContext : MiniGameContext
/// <returns>All players assigned to <paramref name="team"/>.</returns>
public IEnumerable<Player> PlayersOf(HeykelSavasiTeam team) => this._teams.Where(kv => kv.Value == team).Select(kv => kv.Key);
/// <summary>
/// Gets <see cref="StatueBuffEffectNumbers"/>, exposed for unit tests only (see
/// <c>HeykelSavasiContextTests.StatueBuffMap_HasSixEntriesInExpectedOrder</c>); the array itself stays
/// <c>private</c> because it is an implementation detail of <see cref="ApplyStatueBuffToTeamAsync"/>.
/// </summary>
internal static short[] StatueBuffEffectNumbersForTest => StatueBuffEffectNumbers;
/// <summary>
/// Gets the number of the given team's own statues which have been broken by the opposing team so far.
/// </summary>
@@ -432,7 +456,8 @@ public class HeykelSavasiContext : MiniGameContext
{
try
{
// Task 5.1: apply buff here
await this.ApplyStatueBuffToTeamAsync(attacker, index).ConfigureAwait(false);
if (this.GetWinner() == attacker)
{
this.FinishEvent();
@@ -532,6 +557,73 @@ public class HeykelSavasiContext : MiniGameContext
}
}
/// <summary>
/// Applies the class buff mapped to <paramref name="statueIndex"/> (see <see cref="StatueBuffEffectNumbers"/>)
/// to every player currently on <paramref name="team"/>. A no-op for the 7th statue (index 6, out of
/// range), which is the win condition rather than a buff trigger.
/// </summary>
/// <param name="team">The attacking team, whose players receive the buff.</param>
/// <param name="statueIndex">The index (0..6) of the statue that was just destroyed.</param>
private async ValueTask ApplyStatueBuffToTeamAsync(HeykelSavasiTeam team, int statueIndex)
{
if (statueIndex < 0 || statueIndex >= StatueBuffEffectNumbers.Length)
{
return;
}
var effectNumber = StatueBuffEffectNumbers[statueIndex];
foreach (var player in this.PlayersOf(team).ToList())
{
await this.ApplyEffectAsync(player, effectNumber).ConfigureAwait(false);
}
}
/// <summary>
/// Applies a single, already-existing <see cref="MagicEffectDefinition"/> (looked up by
/// <paramref name="effectNumber"/>) to <paramref name="player"/>, for <see cref="BuffDuration"/>.
/// Mirrors the boost-construction pattern of <c>ApplyMagicEffectConsumeHandlerPlugIn.ConsumeItemAsyncCore</c>:
/// each of the definition's <c>PowerUpDefinitions</c> is turned into a boost element bound to the
/// player's own <see cref="Player.Attributes"/> system, so the buff scales off that player's stats.
/// </summary>
/// <param name="player">The player to buff.</param>
/// <param name="effectNumber">The <see cref="MagicEffectDefinition.Number"/> of the effect to apply.</param>
private async ValueTask ApplyEffectAsync(Player player, short effectNumber)
{
var def = player.GameContext.Configuration.MagicEffects.FirstOrDefault(m => m.Number == effectNumber);
if (def is null || player.Attributes is null)
{
return;
}
var boosts = def.PowerUpDefinitions
.Where(d => d.Boost is not null && d.TargetAttribute is not null)
.Select(d => new MagicEffect.ElementWithTarget(player.Attributes.CreateElement(d), d.TargetAttribute!))
.ToArray();
if (boosts.Length == 0)
{
return;
}
var effect = new MagicEffect(BuffDuration, def, boosts!);
await player.MagicEffectList.AddEffectAsync(effect).ConfigureAwait(false);
}
/// <summary>
/// Re-applies every buff <paramref name="player"/>'s team has already earned (one per statue broken so
/// far, indices [0..progress-1]) to <paramref name="player"/>. Intended for use on respawn (Task 5.3),
/// since the existing buffs are configured with <c>StopByDeath=true</c> and are cleared on death.
/// </summary>
/// <param name="player">The player to re-buff.</param>
internal async ValueTask ReapplyBuffsAsync(Player player)
{
var team = this.GetTeam(player);
var earned = this.GetProgress(team);
for (var i = 0; i < earned && i < StatueBuffEffectNumbers.Length; i++)
{
await this.ApplyEffectAsync(player, StatueBuffEffectNumbers[i]).ConfigureAwait(false);
}
}
/// <summary>
/// Pure, dependency-free tracker for statue-break progress and win detection. Extracted out of
/// <see cref="HeykelSavasiContext"/> so it can be unit-tested directly, without constructing a full

View File

@@ -81,4 +81,19 @@ public class HeykelSavasiContextTests
Assert.That(state.GetWinner(), Is.EqualTo(HeykelSavasiTeam.Blue));
Assert.That(state.GetProgress(HeykelSavasiTeam.Red), Is.EqualTo(0));
}
/// <summary>
/// Verifies the fixed statue-index -> <c>MagicEffectDefinition.Number</c> mapping used by
/// <c>HeykelSavasiContext.ApplyStatueBuffToTeamAsync</c> to grant the attacking team a class buff
/// when a statue breaks: GreaterDamage, GreaterDefense, SoulBarrier, CriticalDamageIncrease,
/// WizEnhance (0x52), IgnoreDefense (129). The 7th statue (index 6) intentionally has no entry -
/// breaking it is the win condition, not a buff trigger.
/// </summary>
[Test]
public void StatueBuffMap_HasSixEntriesInExpectedOrder()
{
Assert.That(
HeykelSavasiContext.StatueBuffEffectNumbersForTest,
Is.EqualTo(new short[] { 1, 2, 4, 5, 0x52, 129 }));
}
}