diff --git a/src/game/sideSlingshot.ts b/src/game/sideSlingshot.ts index 272d9f7..dcaa07a 100644 --- a/src/game/sideSlingshot.ts +++ b/src/game/sideSlingshot.ts @@ -21,7 +21,7 @@ export interface SlingshotConfig { export const DEFAULT_SLINGSHOT_CONFIG: SlingshotConfig = { minSpeedPxSec: 400, maxSpeedPxSec: 600, - cooldownMs: 200, + cooldownMs: 250, // 200 → 250: extra safety после physical separation fix }; /** diff --git a/src/scenes/MatchScene.ts b/src/scenes/MatchScene.ts index 6aa795d..ed499d6 100644 --- a/src/scenes/MatchScene.ts +++ b/src/scenes/MatchScene.ts @@ -22,6 +22,7 @@ import { } from '../game/BallStuckWatchdog'; import { BallEnergyController } from '../game/BallEnergyController'; import { + computeRailNormal, computeSlingshotImpulse, RailCooldownTracker, matterVelocityToPxSec, @@ -741,7 +742,10 @@ export class MatchScene extends Phaser.Scene { /** * Side slingshot — активный rebound от боковой rail. * НЕ начисляет очки, НЕ меняет owner, НЕ влияет на leaderboard. - * Только impulse + cooldown + visual feedback + telemetry. + * + * Important: physical separation после impulse — раньше просто + * setVelocity оставлял ball в contact manifold rail body, что вызывало + * regression stuck/повторный fire после cooldown. */ private handleSideSlingshot( side: GuardWallSpec['side'], @@ -755,11 +759,26 @@ export class MatchScene extends Phaser.Scene { y: ballBody.velocity.y, }); const impulse = computeSlingshotImpulse(side, speedBeforePxSec); - this.matter.body.setVelocity(ballBody, impulse); + + // PHYSICAL SEPARATION: сдвигаем ball в направлении inward normal так, + // чтобы он вышел из contact manifold rail body. Иначе следующий frame + // Matter сразу re-triggers collision → stuck loop. + // Distance = ballRadius (22) + railThickness/2 (6) + margin (8) = 36 + const normal = computeRailNormal(side); + const safeDistance = 36; + const newX = ballBody.position.x + normal.x * safeDistance; + const newY = ballBody.position.y + normal.y * safeDistance; + // Clamp к границам стола чтобы не телепортировать в стенку + const clampedX = Math.max(30, Math.min(GAME_WIDTH - 30, newX)); + const clampedY = Math.max(30, Math.min(GAME_HEIGHT - 30, newY)); + this.ball.setPosition(clampedX, clampedY); + + // Impulse + this.ball.setVelocity(impulse.x, impulse.y); // Visual: flash rail + spark particles this.table.flashRail(side); - this.spawnSlingshotParticles(side, ballBody.position); + this.spawnSlingshotParticles(side, { x: newX, y: newY }); // Telemetry this.trackEvent('side_slingshot_hit', {