diff --git a/src/game/Booster.ts b/src/game/Booster.ts index 87e1c06..0eaa4ad 100644 --- a/src/game/Booster.ts +++ b/src/game/Booster.ts @@ -157,9 +157,12 @@ export class Booster { */ shoot(ctx: BoosterContext): { x: number; y: number } | null { if (this.state !== 'aiming') return null; - const vx = Math.cos(this.aimAngleRad) * this.config.shootSpeed * 0.01; - const vy = Math.sin(this.aimAngleRad) * this.config.shootSpeed * 0.01; - const velocity = { x: vx * 100, y: vy * 100 }; + // Matter velocity = position delta per timestep; конвертируем px/sec → Matter units (/60). + const speedMatter = this.config.shootSpeed / 60; + const velocity = { + x: Math.cos(this.aimAngleRad) * speedMatter, + y: Math.sin(this.aimAngleRad) * speedMatter, + }; this.state = 'cooldown'; this.cradlePos = null; this.cooldownUntil = ctx.now + 200; diff --git a/src/game/Bumper.ts b/src/game/Bumper.ts index cf956c4..6461666 100644 --- a/src/game/Bumper.ts +++ b/src/game/Bumper.ts @@ -135,7 +135,7 @@ export class Bumper { ballPos: { x: ballBody.position.x, y: ballBody.position.y }, ballVel: { x: ballBody.velocity.x, y: ballBody.velocity.y }, bumperPos: { x: this.body.position.x, y: this.body.position.y }, - minSpeed: 2.5, // Matter units (~250 px/sec) + minSpeed: 5, // Matter units (~300 px/sec при 60fps) radialThreshold: 0.5, }); if (outward) { diff --git a/src/game/bumperPhysics.test.ts b/src/game/bumperPhysics.test.ts index e0895ad..a42684f 100644 --- a/src/game/bumperPhysics.test.ts +++ b/src/game/bumperPhysics.test.ts @@ -47,17 +47,30 @@ describe('computeOutwardForce — force apply cases', () => { expect(r.y).toBeCloseTo(0); }); - it('fast + inward (radial<0) → force outward', () => { - // Ball справа, velocity сильная влево (внутрь bumper'а) + it('fast + inward (radial<0) → force outward, PRESERVE SPEED', () => { + // Ball справа, velocity сильная влево (внутрь bumper'а), speed=5 const r = computeOutwardForce( inputs({ ballPos: { x: 150, y: 100 }, ballVel: { x: -5, y: 0 } }), ); expect(r).not.toBeNull(); if (!r) return; - expect(r.x).toBeCloseTo(2.5); // вправо (наружу) + // Direction outward (вправо), magnitude = max(5, minSpeed=2.5) = 5 + expect(r.x).toBeCloseTo(5); // вправо, СОХРАНИЛИ скорость expect(r.y).toBeCloseTo(0); }); + it('REAL fix: very fast inward → preserve high speed, не замедляем', () => { + // Ball справа, velocity сильная внутрь, speed=10 + const r = computeOutwardForce( + inputs({ ballPos: { x: 150, y: 100 }, ballVel: { x: -10, y: 0 } }), + ); + expect(r).not.toBeNull(); + if (!r) return; + const mag = Math.sqrt(r.x * r.x + r.y * r.y); + expect(mag).toBeCloseTo(10); // sped preserved, not clipped to 2.5 + expect(r.x).toBeCloseTo(10); + }); + it('zero velocity → force outward', () => { const r = computeOutwardForce( inputs({ ballPos: { x: 150, y: 100 }, ballVel: { x: 0, y: 0 } }), @@ -68,14 +81,14 @@ describe('computeOutwardForce — force apply cases', () => { expect(r.y).toBeCloseTo(0); }); - it('fast + tangential (radial≈0) → force apply (radial { - // Ball справа, velocity вверх (perpendicular к outward) — radial=0 + it('fast + tangential (radial≈0) → force outward с preserved speed', () => { + // Ball справа, velocity вверх (perpendicular к outward) — speed=5, radial=0 const r = computeOutwardForce( inputs({ ballPos: { x: 150, y: 100 }, ballVel: { x: 0, y: -5 } }), ); expect(r).not.toBeNull(); if (!r) return; - expect(r.x).toBeCloseTo(2.5); + expect(r.x).toBeCloseTo(5); // preserved speed expect(r.y).toBeCloseTo(0); }); }); diff --git a/src/game/bumperPhysics.ts b/src/game/bumperPhysics.ts index 7977e9d..43c907c 100644 --- a/src/game/bumperPhysics.ts +++ b/src/game/bumperPhysics.ts @@ -48,8 +48,11 @@ export function computeOutwardForce( return null; // OK, мяч движется наружу с достаточной скоростью } + // Preserve incoming speed (только направление меняем); minSpeed = floor. + // Это критично: fast inward → outward at SAME speed, не замедляем мяч. + const targetSpeed = Math.max(speed, minSpeed); return { - x: normX * minSpeed, - y: normY * minSpeed, + x: normX * targetSpeed, + y: normY * targetSpeed, }; } diff --git a/src/scenes/MatchScene.ts b/src/scenes/MatchScene.ts index 538becf..c3437dc 100644 --- a/src/scenes/MatchScene.ts +++ b/src/scenes/MatchScene.ts @@ -311,13 +311,16 @@ export class MatchScene extends Phaser.Scene { } private launchBall(): void { - // Random vector ±30° от перпендикуляра к боковой стенке (т.е. влево) + // Random vector ±30° от перпендикуляра к боковой стенке (т.е. влево). + // Matter velocity = position delta per timestep (~16.67ms @ 60fps), + // поэтому конвертируем px/sec → Matter units (/60). + // Старый код имел * 0.01 * 100 = no-op → setVelocity(400) = 24000 px/sec, + // ball tunnel'ил через объекты и watchdog fire'ил постоянно. const angleVariation = (Math.random() * 60 - 30) * (Math.PI / 180); - const baseAngle = Math.PI; // влево + const baseAngle = Math.PI; const angle = baseAngle + angleVariation; - const vx = Math.cos(angle) * BALL_SPEED_START * 0.01; // Matter — мaленькие velocity units - const vy = Math.sin(angle) * BALL_SPEED_START * 0.01; - this.ball.setVelocity(vx * 100, vy * 100); + const speedMatter = BALL_SPEED_START / 60; // ~6.67 Matter units = ~400 px/sec + this.ball.setVelocity(Math.cos(angle) * speedMatter, Math.sin(angle) * speedMatter); } private placeSetupBumpers(setup: SetupConfig, side: 'player' | 'ai'): void {