Touch controls v3.12: corner circular buttons L/R/BOOST
Заменяет огромные rectangle touch zones на корнерные circular кнопки. ## Дизайн (per user spec) - Player L — левый нижний угол, диаметр 104 (radius 52, выше mobile threshold 72-88) - Player R — правый нижний угол, симметрично - BOOST — центр-низ, чуть меньше (radius 44, диаметр 88) - Все три в y=GAME_HEIGHT-70 — ниже playfield и guard rails ## Visual style - Двухслойный neon: glow halo (radius+4, alpha 0.12 + outline 0.4) + основной disk (alpha 0.18 + outline 0.85) - Цвет = palette владельца (player магента / BOOST accent yellow) - Label по центру (L/R/B, 32px monospace bold) - Под label — стрелка-хинт ◀/▶ для L/R (14px, alpha 0.6) указывающая направление поворота aim в booster cradle - Press feedback: scale 1.0 → 0.88 yoyo 60ms ## Поведение - Default: L → activate playerFlipperLeft, R → activate playerFlipperRight, B → handleBoosterTap - В booster.aiming: L → rotateAim(-1), R → rotateAim(+1) - Keyboard A/D/S работает параллельно (не тронут) - Mouse click на desktop работает так же как touch ## Refactor Extract'нул создание кнопки в createCornerButton helper. Глобальное DRY: 3 кнопки через единый код. 152/152 tests, typecheck/lint/build ✅. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -925,64 +925,111 @@ export class MatchScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildTouchZones(): void {
|
private buildTouchZones(): void {
|
||||||
// 3 touch zones внизу экрана для mobile + desktop click.
|
// Corner buttons (v3.12) — L/R + BOOST. Disk-форма, ≥100px диаметр
|
||||||
// Visual: полупрозрачные overlay'и; реагируют на pointerdown.
|
// (mobile-friendly), в нижних углах вне playfield/guard rails.
|
||||||
// Booster — placeholder в Phase 2, реальный effect в Phase 3.
|
// Те же handlers что и keyboard A/D/S. В aiming-фазе L/R = rotate aim.
|
||||||
const zoneH = 220;
|
|
||||||
const zoneY = GAME_HEIGHT - zoneH / 2;
|
|
||||||
const boosterW = 140;
|
|
||||||
const sideW = (GAME_WIDTH - boosterW) / 2;
|
|
||||||
|
|
||||||
// Left zone — в aiming-фазе rotate aim вместо flipper (mirror keyboard logic)
|
// Bottom Y — около нижнего края, ниже guard rail и playfield
|
||||||
const leftZone = this.add.rectangle(sideW / 2, zoneY, sideW - 8, zoneH, PALETTE.player, 0.06);
|
const buttonY = GAME_HEIGHT - 70;
|
||||||
leftZone.setStrokeStyle(1, PALETTE.player, 0.25);
|
const sideButtonR = 52; // диаметр 104 — выше mobile threshold 72-88
|
||||||
leftZone.setInteractive();
|
const centerButtonR = 44; // BOOST чуть меньше, центр
|
||||||
leftZone.on('pointerdown', () => {
|
|
||||||
if (this.booster.getState() === 'aiming') {
|
|
||||||
this.booster.rotateAim(-1);
|
|
||||||
} else {
|
|
||||||
this.playerFlipperLeft.activate();
|
|
||||||
}
|
|
||||||
this.tweens.add({ targets: leftZone, fillAlpha: 0.22, duration: 60, yoyo: true });
|
|
||||||
});
|
|
||||||
this.add
|
|
||||||
.text(sideW / 2, zoneY, '◀ L', {
|
|
||||||
fontFamily: 'monospace', fontSize: '20px', color: '#ff006e', fontStyle: 'bold',
|
|
||||||
})
|
|
||||||
.setOrigin(0.5).setAlpha(0.7);
|
|
||||||
|
|
||||||
// Right zone
|
// Player L — левый нижний угол
|
||||||
const rightX = GAME_WIDTH - sideW / 2;
|
this.createCornerButton({
|
||||||
const rightZone = this.add.rectangle(rightX, zoneY, sideW - 8, zoneH, PALETTE.player, 0.06);
|
x: 70,
|
||||||
rightZone.setStrokeStyle(1, PALETTE.player, 0.25);
|
y: buttonY,
|
||||||
rightZone.setInteractive();
|
radius: sideButtonR,
|
||||||
rightZone.on('pointerdown', () => {
|
color: PALETTE.player,
|
||||||
if (this.booster.getState() === 'aiming') {
|
label: 'L',
|
||||||
this.booster.rotateAim(1);
|
arrow: '◀',
|
||||||
} else {
|
onTap: () => {
|
||||||
this.playerFlipperRight.activate();
|
if (this.booster.getState() === 'aiming') {
|
||||||
}
|
this.booster.rotateAim(-1);
|
||||||
this.tweens.add({ targets: rightZone, fillAlpha: 0.22, duration: 60, yoyo: true });
|
} else {
|
||||||
|
this.playerFlipperLeft.activate();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.add
|
|
||||||
.text(rightX, zoneY, 'R ▶', {
|
|
||||||
fontFamily: 'monospace', fontSize: '20px', color: '#ff006e', fontStyle: 'bold',
|
|
||||||
})
|
|
||||||
.setOrigin(0.5).setAlpha(0.7);
|
|
||||||
|
|
||||||
// Booster zone (центр, узкая) — Phase 2 placeholder
|
// Player R — правый нижний угол
|
||||||
const boosterZone = this.add.rectangle(GAME_WIDTH / 2, zoneY, boosterW, zoneH * 0.5, PALETTE.accent, 0.05);
|
this.createCornerButton({
|
||||||
boosterZone.setStrokeStyle(1, PALETTE.accent, 0.3);
|
x: GAME_WIDTH - 70,
|
||||||
boosterZone.setInteractive();
|
y: buttonY,
|
||||||
boosterZone.on('pointerdown', () => {
|
radius: sideButtonR,
|
||||||
this.tweens.add({ targets: boosterZone, fillAlpha: 0.25, duration: 80, yoyo: true });
|
color: PALETTE.player,
|
||||||
this.handleBoosterTap();
|
label: 'R',
|
||||||
|
arrow: '▶',
|
||||||
|
onTap: () => {
|
||||||
|
if (this.booster.getState() === 'aiming') {
|
||||||
|
this.booster.rotateAim(1);
|
||||||
|
} else {
|
||||||
|
this.playerFlipperRight.activate();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// BOOST — центр, отдельной кнопкой
|
||||||
|
this.createCornerButton({
|
||||||
|
x: GAME_WIDTH / 2,
|
||||||
|
y: buttonY,
|
||||||
|
radius: centerButtonR,
|
||||||
|
color: PALETTE.accent,
|
||||||
|
label: 'B',
|
||||||
|
onTap: () => this.handleBoosterTap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCornerButton(args: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
radius: number;
|
||||||
|
color: number;
|
||||||
|
label: string;
|
||||||
|
arrow?: string; // optional rotation hint ◀/▶
|
||||||
|
onTap: () => void;
|
||||||
|
}): void {
|
||||||
|
const { x, y, radius, color, label, arrow, onTap } = args;
|
||||||
|
const colorHex = `#${color.toString(16).padStart(6, '0')}`;
|
||||||
|
|
||||||
|
// Outer glow (двухслойный neon outline)
|
||||||
|
const glow = this.add.circle(x, y, radius + 4, color, 0.12);
|
||||||
|
glow.setStrokeStyle(2, color, 0.4);
|
||||||
|
|
||||||
|
// Основной disk
|
||||||
|
const disk = this.add.circle(x, y, radius, color, 0.18);
|
||||||
|
disk.setStrokeStyle(3, color, 0.85);
|
||||||
|
disk.setInteractive({ useHandCursor: true });
|
||||||
|
disk.on('pointerdown', () => {
|
||||||
|
onTap();
|
||||||
|
this.tweens.add({
|
||||||
|
targets: [glow, disk],
|
||||||
|
scale: { from: 1.0, to: 0.88 },
|
||||||
|
duration: 60,
|
||||||
|
yoyo: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Label (L / R / B)
|
||||||
this.add
|
this.add
|
||||||
.text(GAME_WIDTH / 2, zoneY, 'BOOST', {
|
.text(x, y - (arrow ? 6 : 0), label, {
|
||||||
fontFamily: 'monospace', fontSize: '18px', color: '#ffbe0b', fontStyle: 'bold',
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '32px',
|
||||||
|
color: colorHex,
|
||||||
|
fontStyle: 'bold',
|
||||||
})
|
})
|
||||||
.setOrigin(0.5).setAlpha(0.7);
|
.setOrigin(0.5);
|
||||||
|
|
||||||
|
// Arrow hint под label (только для L/R)
|
||||||
|
if (arrow) {
|
||||||
|
this.add
|
||||||
|
.text(x, y + 18, arrow, {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: colorHex,
|
||||||
|
})
|
||||||
|
.setOrigin(0.5)
|
||||||
|
.setAlpha(0.6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildHUD(): void {
|
private buildHUD(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user