Skip to content

Commit e9b30cf

Browse files
committed
add: await to combo chain handling
1 parent d59007e commit e9b30cf

4 files changed

Lines changed: 97 additions & 34 deletions

File tree

Assets/Code/Services/BallController/AttachingBallChainHandler.cs

Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ public class AttachingBallChainHandler
1919
private readonly ILevelService _levelService;
2020
private readonly ILevelLocalProgressService _levelLocalProgressService;
2121

22+
// Counter instead of bool: handles user-shot combos overlapping with chain reactions
23+
private int _activeComboCount;
24+
public bool IsProcessingCombo => _activeComboCount > 0;
25+
2226
public AttachingBallChainHandler(
2327
PathCreator pathCreator,
2428
BallChainDTO ballChainDto,
@@ -45,7 +49,7 @@ public void TryAttachBall(Ball newBall)
4549
InsertBallToSegment(newBall, segment, index);
4650
}
4751

48-
// Called by BallChainController after two segments merge
52+
// Called by BallChainController.CheckMerges only when no combo chain is active
4953
public void CheckMatchAtJunction(ChainSegment segment, int junctionIdx)
5054
{
5155
if (segment.Count == 0) return;
@@ -55,16 +59,15 @@ public void CheckMatchAtJunction(ChainSegment segment, int junctionIdx)
5559
Ball anchor = balls[junctionIdx];
5660
var matches = FindMatchesAroundBall(anchor, balls);
5761

58-
if (matches.Count >= _ballChainDto.MatchingCount)
59-
{
60-
int score = matches.Count *
61-
_levelService.GetCurrentLevelStaticData().LevelConfig.ScoreConfig.ScorePerItem;
62-
_levelLocalProgressService.AddScore(score);
63-
_widgetBallChainProvider.SetUpWidget(matches, anchor, score);
64-
_winBallChainHandler.TryWin(_segments.Sum(s => s.Count) - matches.Count);
62+
if (matches.Count < _ballChainDto.MatchingCount) return;
6563

66-
DestroyAndSplit(matches, segment).Forget();
67-
}
64+
int score = matches.Count *
65+
_levelService.GetCurrentLevelStaticData().LevelConfig.ScoreConfig.ScorePerItem;
66+
_levelLocalProgressService.AddScore(score);
67+
_widgetBallChainProvider.SetUpWidget(matches, anchor, score);
68+
_winBallChainHandler.TryWin(_segments.Sum(s => s.Count) - matches.Count);
69+
70+
RunComboChain(matches, segment).Forget();
6871
}
6972

7073
private (ChainSegment segment, int index) FindClosestCollision(Ball newBall)
@@ -139,55 +142,113 @@ private async UniTask CheckAndDestroyMatches(Ball pivotBall)
139142
_widgetBallChainProvider.SetUpWidget(matches, pivotBall, score);
140143
_winBallChainHandler.TryWin(_segments.Sum(s => s.Count) - matches.Count);
141144

142-
await DestroyAndSplit(matches, segment);
145+
await RunComboChain(matches, segment);
143146
}
144147
else
145148
{
146149
pivotBall.SetInteractive(false);
147150
}
148151
}
149152

150-
private async UniTask DestroyAndSplit(List<Ball> matchBalls, ChainSegment segment)
153+
// Entry point for all combo chains — tracks nesting so IsProcessingCombo stays true
154+
// for the full duration of a chain reaction, even across multiple sequential steps
155+
private async UniTask RunComboChain(List<Ball> matchBalls, ChainSegment segment)
151156
{
157+
_activeComboCount++;
158+
try
159+
{
160+
await ComboStep(matchBalls, segment);
161+
}
162+
finally
163+
{
164+
_activeComboCount--;
165+
}
166+
}
167+
168+
// One step of a combo: destroy → split → wait for physical merge → check next match
169+
private async UniTask ComboStep(List<Ball> matchBalls, ChainSegment segment)
170+
{
171+
// 1. Wait for all destroy animations to finish
152172
await UniTask.WhenAll(matchBalls.Select(WaitForDestroyAnimation));
153173

154174
int lowestIndex = matchBalls.Min(b => b.Index);
155175

156-
// Remove from high to low to preserve indices during removal
176+
// 2. Remove matched balls from high to low to keep indices valid
157177
foreach (var ball in matchBalls.OrderByDescending(b => b.Index))
158178
{
159179
segment.RemoveBallAt(ball.Index);
160180
ball.Deactivate();
161181
}
162182

163-
// Split remaining balls after match into back segment
164-
if (lowestIndex < segment.Count)
183+
if (lowestIndex >= segment.Count)
165184
{
166-
ChainSegment back = segment.SplitAt(lowestIndex);
185+
// Nothing behind the gap — just reindex and clean up
167186
segment.ReIndexBalls();
168-
back.ReIndexBalls();
187+
TryCleanupEmptySegment(segment);
188+
return;
189+
}
169190

170-
int segIdx = _segments.IndexOf(segment);
191+
// 3. Split: balls [lowestIndex..] become the new back segment
192+
ChainSegment back = segment.SplitAt(lowestIndex);
193+
segment.ReIndexBalls();
194+
back.ReIndexBalls();
171195

172-
if (segment.Count > 0)
173-
{
174-
// Front has balls — back catches up
175-
back.IsCatchingUp = true;
176-
back.CurrentSpeed = back.BaseSpeed * _ballChainDto.CatchupSpeedMultiplier;
177-
_segments.Insert(segIdx + 1, back);
178-
}
179-
else
180-
{
181-
// Front is empty — back becomes the lead, no one to catch
182-
_segments[segIdx] = back;
183-
}
196+
int segIdx = _segments.IndexOf(segment);
197+
198+
if (segment.Count == 0)
199+
{
200+
// Front wiped — back takes over, no gap to close
201+
_segments[segIdx] = back;
202+
TryCleanupEmptySegment(segment);
203+
return;
184204
}
185-
else
205+
206+
// Index of front's last ball — this becomes the junction after merge
207+
int junctionIdx = segment.Count - 1;
208+
209+
// 4. Launch back segment catch-up; CheckMerges will do the physical merge
210+
back.IsCatchingUp = true;
211+
back.CurrentSpeed = back.BaseSpeed * _ballChainDto.CatchupSpeedMultiplier;
212+
_segments.Insert(segIdx + 1, back);
213+
214+
// 5. Wait until CheckMerges has logically merged back into front
215+
await UniTask.WaitUntil(() => !_segments.Contains(back));
216+
217+
// 6. Wait for the visual gap at the junction to actually close.
218+
// Logical merge fires early (distance math), but SmoothDamp needs more time.
219+
// junctionIdx = last ball of original front
220+
// junctionIdx + 1 = first ball of original back (now part of segment after merge)
221+
int nextIdx = junctionIdx + 1;
222+
if (nextIdx < segment.Count)
186223
{
187-
segment.ReIndexBalls();
224+
await UniTask.WaitUntil(() =>
225+
nextIdx < segment.Count &&
226+
segment.GetVisualDistance(junctionIdx) - segment.GetVisualDistance(nextIdx)
227+
<= _ballChainDto.SpacingBalls * 1.5f
228+
);
188229
}
189230

190-
// Clean up empty front segment
231+
// 7. Chain is visually closed — check for a new match at the junction
232+
if (junctionIdx < 0 || junctionIdx >= segment.Count) return;
233+
234+
var mergedBalls = segment.Balls.ToList();
235+
Ball anchor = mergedBalls[junctionIdx];
236+
var nextMatches = FindMatchesAroundBall(anchor, mergedBalls);
237+
238+
if (nextMatches.Count < _ballChainDto.MatchingCount) return;
239+
240+
int nextScore = nextMatches.Count *
241+
_levelService.GetCurrentLevelStaticData().LevelConfig.ScoreConfig.ScorePerItem;
242+
_levelLocalProgressService.AddScore(nextScore);
243+
_widgetBallChainProvider.SetUpWidget(nextMatches, anchor, nextScore);
244+
_winBallChainHandler.TryWin(_segments.Sum(s => s.Count) - nextMatches.Count);
245+
246+
// 8. Continue combo chain sequentially — same _activeComboCount scope
247+
await ComboStep(nextMatches, segment);
248+
}
249+
250+
private void TryCleanupEmptySegment(ChainSegment segment)
251+
{
191252
if (segment.Count == 0 && _segments.Contains(segment))
192253
_segments.Remove(segment);
193254
}

Assets/Code/Services/BallController/BallChainController.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ private void CheckMerges()
159159
_segments.RemoveAt(i + 1);
160160
front.ReIndexBalls();
161161

162-
_attachingBallChainHandler.CheckMatchAtJunction(front, junctionIdx);
162+
if (!_attachingBallChainHandler.IsProcessingCombo)
163+
_attachingBallChainHandler.CheckMatchAtJunction(front, junctionIdx);
163164
i--;
164165
}
165166
}

Assets/Resources/StaticData/BallChainConfig.asset

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ MonoBehaviour:
1818
DurationMovingOffset: 0.2
1919
ChainSpringStrength: 25
2020
ChainGapSpringStrength: 4
21+
CatchupSpeedMultiplier: 2.5
2122
CollisionThreshold: 1
2223
MatchingCount: 3
2324
InitialSpeedMultiplier: 2.5

Recordings/Movie_001.mp4

-8.4 MB
Binary file not shown.

0 commit comments

Comments
 (0)