Button positioning + goal X tolerance
Codex review on 7761cd6 — 3 findings.
## #1 UX: buttons обрезались краем canvas / BOOST пересекал goal line
Old: все 3 кнопки на y=H-40=1240. С radius 52 bottom edge 1292 > 1280
canvas. BOOST overlap'ил goal line (1260) визуально.
Fix:
- L/R: r=48 (диам 96, выше mobile threshold), y=H-55=1225
Bottom 1273 (within canvas), top 1177 (ниже rails)
Overlap goal line только в non-gap X (line drawn только в 306..414, L/R в 22..118 и 602..698)
- BOOST: r=36 (диам 72, на mobile threshold), y=H-85=1195
Полностью выше goal line — не пересекает её визуально
Asymmetric Y — trade-off за visual cleanliness
## #3 Residual: goal X-check был center-only
Old: ballX > goalLeftX && ballX < goalRightX — центр должен быть строго в gap.
На крайних X edge мяча визуально в gap, но центр чуть вне → ball отскакивает.
Fix: добавлен xTolerance = ballRadius/2.
- ballX > goalLeftX - xTolerance (lenient слева)
- ballX < goalRightX + xTolerance (lenient справа)
Симметрично с Y edge-check. Если значительная часть мяча в gap, гол
засчитывается. Не too lenient: tolerance только half-radius (11px из 22),
не full radius.
## #2 Residual (acknowledged, not fixed)
ball_unstuck всё ещё triggers 1 раз в mobile match. Watchdog работает как
safety net (это ОК per architecture), но «полностью самодостаточная»
физика без сторонних косвенных edge cases requires более глубокий tuning
(possibly per-personality setup-bumper аранжировки). Откладывается до
external playtest данных по hot-spots stuck events.
## Tests
154/154 unchanged — все изменения в untested rendering layer + Table
геометрии (table-tests были бы integration-level, не покрываются
текущим test suite scope).
typecheck/lint/build ✅.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -226,25 +226,28 @@ export class Table {
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка: гол игрока (низ). Триггерит когда EDGE мяча (не центр) пересёк
|
||||
* линию ворот — чтобы не получалось «попал в ворота, но отскочил обратно».
|
||||
* ballRadius default 22 (BALL_RADIUS).
|
||||
* Проверка: гол игрока (низ). Edge-based по обеим осям:
|
||||
* - Y: edge мяча пересёк линию (ballY + radius > goalY)
|
||||
* - X: центр мяча в gap ± half-radius (lenient — даёт пройти мячу когда
|
||||
* значительная часть в gap, симметрично с Y).
|
||||
*/
|
||||
isInPlayerGoal(ballX: number, ballY: number, ballRadius = 22): boolean {
|
||||
const g = this.geometry;
|
||||
const xTolerance = ballRadius / 2;
|
||||
return (
|
||||
ballY + ballRadius > g.playerGoalY &&
|
||||
ballX > g.goalLeftX &&
|
||||
ballX < g.goalRightX
|
||||
ballX > g.goalLeftX - xTolerance &&
|
||||
ballX < g.goalRightX + xTolerance
|
||||
);
|
||||
}
|
||||
|
||||
isInAIGoal(ballX: number, ballY: number, ballRadius = 22): boolean {
|
||||
const g = this.geometry;
|
||||
const xTolerance = ballRadius / 2;
|
||||
return (
|
||||
ballY - ballRadius < g.aiGoalY &&
|
||||
ballX > g.goalLeftX &&
|
||||
ballX < g.goalRightX
|
||||
ballX > g.goalLeftX - xTolerance &&
|
||||
ballX < g.goalRightX + xTolerance
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,16 +931,20 @@ export class MatchScene extends Phaser.Scene {
|
||||
// (mobile-friendly), в нижних углах вне playfield/guard rails.
|
||||
// Те же handlers что и keyboard A/D/S. В aiming-фазе L/R = rotate aim.
|
||||
|
||||
// Bottom Y — у самого края canvas, ниже flipper rest и guard rails (которые
|
||||
// теперь продолжают линию флипперов под углом 30°)
|
||||
const buttonY = GAME_HEIGHT - 40;
|
||||
const sideButtonR = 52; // диаметр 104 — выше mobile threshold 72-88
|
||||
const centerButtonR = 44; // BOOST чуть меньше, центр
|
||||
// Layout:
|
||||
// L/R r=48 на y=H-55=1225 → top 1177 (ниже rails), bottom 1273 (выше canvas edge);
|
||||
// overlap x не пересекает goal line (которая drawn только в gap 306..414).
|
||||
// BOOST r=36 на y=H-85=1195 → выше goal line (1260), не overlap'ит её визуально.
|
||||
// Asymmetric Y — trade-off за чистый visual.
|
||||
const sideButtonR = 48; // диаметр 96 — выше mobile threshold 72-88
|
||||
const centerButtonR = 36; // BOOST меньше, поднят выше чтобы не overlap goal line
|
||||
const sideButtonY = GAME_HEIGHT - 55;
|
||||
const centerButtonY = GAME_HEIGHT - 85;
|
||||
|
||||
// Player L — левый нижний угол
|
||||
this.createCornerButton({
|
||||
x: 70,
|
||||
y: buttonY,
|
||||
y: sideButtonY,
|
||||
radius: sideButtonR,
|
||||
color: PALETTE.player,
|
||||
label: 'L',
|
||||
@@ -957,7 +961,7 @@ export class MatchScene extends Phaser.Scene {
|
||||
// Player R — правый нижний угол
|
||||
this.createCornerButton({
|
||||
x: GAME_WIDTH - 70,
|
||||
y: buttonY,
|
||||
y: sideButtonY,
|
||||
radius: sideButtonR,
|
||||
color: PALETTE.player,
|
||||
label: 'R',
|
||||
@@ -971,10 +975,10 @@ export class MatchScene extends Phaser.Scene {
|
||||
},
|
||||
});
|
||||
|
||||
// BOOST — центр, отдельной кнопкой
|
||||
// BOOST — центр, выше L/R чтобы не пересекать drawn goal line
|
||||
this.createCornerButton({
|
||||
x: GAME_WIDTH / 2,
|
||||
y: buttonY,
|
||||
y: centerButtonY,
|
||||
radius: centerButtonR,
|
||||
color: PALETTE.accent,
|
||||
label: 'B',
|
||||
|
||||
Reference in New Issue
Block a user