diff --git a/src/GameLogic/MiniGames/HeykelSavasiContext.cs b/src/GameLogic/MiniGames/HeykelSavasiContext.cs
index 10b222e..5a4b890 100644
--- a/src/GameLogic/MiniGames/HeykelSavasiContext.cs
+++ b/src/GameLogic/MiniGames/HeykelSavasiContext.cs
@@ -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
///
private static readonly (int Dx, int Dy)[] GuardOffsets = { (-2, 0), (2, 0), (0, -2), (0, 2) };
+ ///
+ /// Maps a destroyed statue's index (0..5) to the
+ /// of the class buff granted to the whole attacking team. All of these effects already exist in
+ /// configuration (no custom MagicEffectDefinitions 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 .
+ ///
+ private static readonly short[] StatueBuffEffectNumbers = { 1, 2, 4, 5, 0x52, 129 };
+
+ ///
+ /// The duration of the class buff granted on a statue break, chosen to comfortably outlast a single
+ /// match (matches run longer than 5 minutes).
+ ///
+ private static readonly TimeSpan BuffDuration = TimeSpan.FromMinutes(6);
+
///
/// 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
/// All players assigned to .
public IEnumerable PlayersOf(HeykelSavasiTeam team) => this._teams.Where(kv => kv.Value == team).Select(kv => kv.Key);
+ ///
+ /// Gets , exposed for unit tests only (see
+ /// HeykelSavasiContextTests.StatueBuffMap_HasSixEntriesInExpectedOrder); the array itself stays
+ /// private because it is an implementation detail of .
+ ///
+ internal static short[] StatueBuffEffectNumbersForTest => StatueBuffEffectNumbers;
+
///
/// Gets the number of the given team's own statues which have been broken by the opposing team so far.
///
@@ -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
}
}
+ ///
+ /// Applies the class buff mapped to (see )
+ /// to every player currently on . A no-op for the 7th statue (index 6, out of
+ /// range), which is the win condition rather than a buff trigger.
+ ///
+ /// The attacking team, whose players receive the buff.
+ /// The index (0..6) of the statue that was just destroyed.
+ 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);
+ }
+ }
+
+ ///
+ /// Applies a single, already-existing (looked up by
+ /// ) to , for .
+ /// Mirrors the boost-construction pattern of ApplyMagicEffectConsumeHandlerPlugIn.ConsumeItemAsyncCore:
+ /// each of the definition's PowerUpDefinitions is turned into a boost element bound to the
+ /// player's own system, so the buff scales off that player's stats.
+ ///
+ /// The player to buff.
+ /// The of the effect to apply.
+ 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);
+ }
+
+ ///
+ /// Re-applies every buff 's team has already earned (one per statue broken so
+ /// far, indices [0..progress-1]) to . Intended for use on respawn (Task 5.3),
+ /// since the existing buffs are configured with StopByDeath=true and are cleared on death.
+ ///
+ /// The player to re-buff.
+ 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);
+ }
+ }
+
///
/// Pure, dependency-free tracker for statue-break progress and win detection. Extracted out of
/// so it can be unit-tested directly, without constructing a full
diff --git a/tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs b/tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs
index 28c8f6a..f09a89b 100644
--- a/tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs
+++ b/tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs
@@ -81,4 +81,19 @@ public class HeykelSavasiContextTests
Assert.That(state.GetWinner(), Is.EqualTo(HeykelSavasiTeam.Blue));
Assert.That(state.GetProgress(HeykelSavasiTeam.Red), Is.EqualTo(0));
}
+
+ ///
+ /// Verifies the fixed statue-index -> MagicEffectDefinition.Number mapping used by
+ /// HeykelSavasiContext.ApplyStatueBuffToTeamAsync 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.
+ ///
+ [Test]
+ public void StatueBuffMap_HasSixEntriesInExpectedOrder()
+ {
+ Assert.That(
+ HeykelSavasiContext.StatueBuffEffectNumbersForTest,
+ Is.EqualTo(new short[] { 1, 2, 4, 5, 0x52, 129 }));
+ }
}