From ceacc11446b5cd509c7b77e2f4f3807ce79147fb Mon Sep 17 00:00:00 2001 From: Acentech Dev Date: Tue, 21 Jul 2026 00:15:52 +0300 Subject: [PATCH] feat(hs): apply mapped class buffs to team on statue break --- .../MiniGames/HeykelSavasiContext.cs | 94 ++++++++++++++++++- .../HeykelSavasi/HeykelSavasiContextTests.cs | 15 +++ 2 files changed, 108 insertions(+), 1 deletion(-) 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 })); + } }