diff --git a/src/game/flipperState.ts b/src/game/flipperState.ts index d70feef..6a5ea6a 100644 --- a/src/game/flipperState.ts +++ b/src/game/flipperState.ts @@ -2,16 +2,25 @@ // Extracted из Flipper.ts чтобы isPressed/cooldown логика была unit-тестируемой // без mocking Phaser tweens. // -// State transitions: -// idle (isPressed=false, cooldown expired) -// → press(now) → pressed (isPressed=true) -// → press during cooldown / already pressed → rejected -// pressed -// → release() → releasing (isPressed=false, tween в процессе) -// → press → rejected (already pressed) -// releasing -// → onReleaseComplete(now) → cooldown started (cooldownUntil = now + cooldownMs) -// → press(now) во время releasing → rejected (если в cooldown) +// States and transitions: +// idle (isPressed=false, cooldownUntil expired) +// press(now) → pressed +// press during cooldown → rejected +// +// pressed (isPressed=true) +// release() → releasing +// press → rejected (already pressed) +// +// releasing (isPressed=false, release-tween в процессе, cooldown ЕЩЁ НЕ активен) +// press(now) → pressed (ALLOWED — responsive re-press до завершения tween; +// cooldownUntil ещё старое значение, canPress=true) +// onReleaseComplete(now) → cooldown starts (cooldownUntil = now + cooldownMs) +// +// cooldown (после onReleaseComplete до истечения cooldownMs) +// press(now < cooldownUntil) → rejected +// time passes → idle +// +// Re-press allowed во время releasing — design choice для responsive control. // // Flipper.ts wrapper'ит это с Phaser tweens + Matter body updates. diff --git a/src/scenes/MatchScene.ts b/src/scenes/MatchScene.ts index f8f1c33..1a7d027 100644 --- a/src/scenes/MatchScene.ts +++ b/src/scenes/MatchScene.ts @@ -584,17 +584,37 @@ export class MatchScene extends Phaser.Scene { matchId: this.matchId, attempt: this.stuckWatchdog.getNudgeAttempts(), hadOverlap: overlap !== null, + // Position для hot-spot analysis — где конкретно ball застревает (codex backlog #1) + ballPositionX: Math.round(bp.x), + ballPositionY: Math.round(bp.y), + overlappingBumperType: overlap?.body + ? this.findBumperTypeAtPosition(overlap.body.position) + : null, matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000, }); } + /** Найти bumper type на позиции (для telemetry hot-spot analysis). */ + private findBumperTypeAtPosition(pos: { x: number; y: number }): string | null { + for (const b of [...this.fixedBumpers, ...this.setupBumpers]) { + const body = b.getBody(); + if (Math.abs(body.position.x - pos.x) < 1 && Math.abs(body.position.y - pos.y) < 1) { + return `${b.source}:${b.type}`; + } + } + return null; + } + /** * Hard respawn — мяч действительно не разлипается после 3 nudge. * Уничтожаем тело, спавним из бокового порта в нейтральном цвете. */ private applyStuckHardRespawn(): void { + const bp = this.ball.position; this.trackEvent('ball_hard_respawn', { matchId: this.matchId, + ballPositionX: Math.round(bp.x), + ballPositionY: Math.round(bp.y), matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000, }); this.ball.destroy();