diff --git a/src/ai/AIPlayer.ts b/src/ai/AIPlayer.ts index 9a439a6..1cef9c7 100644 --- a/src/ai/AIPlayer.ts +++ b/src/ai/AIPlayer.ts @@ -38,29 +38,32 @@ export function shouldActivateFlipperBase( side: 'left' | 'right', params: AIDifficultyParams, lastReactionTime: number, + difficulty: AIDifficulty = 'medium', ): boolean { const ballPos = ctx.ball.position; const ballVel = ctx.ball.velocity; // AI находится сверху → реагирует на мяч, летящий вверх (vy < 0) - // У AI ворота сверху → нужно отбить мяч когда он близко к AI-флипперам. - // Player flippers внизу, AI flippers вверху. - // Для AI «приближение к воротам» = мяч с маленьким Y И направлением вверх. + const aiFlippersY = 50; - const aiFlippersY = 50; // примерное location (Y ворот AI) - const ballNearGoal = ballPos.y < aiFlippersY + 200; + // Distance threshold: Easy реагирует поздно, Medium — заранее (predictive). + const triggerDist = difficulty === 'easy' ? 150 : 280; + const ballNearGoal = ballPos.y < aiFlippersY + triggerDist; const ballHeadingToAIGoal = ballVel.y < -50; if (!ballNearGoal || !ballHeadingToAIGoal) return false; - // Выбор стороны: если мяч слева от центра → left flipper, справа → right - // (упрощённо; для Hard сделаем точнее в Phase 3) + // Выбор стороны const isBallOnLeft = ballPos.x < 360; if (side === 'left' && !isBallOnLeft) return false; if (side === 'right' && isBallOnLeft) return false; - // Reaction delay + jitter (см. bot-ai-design.md#базовая-реакция) + // Reaction delay + jitter const now = performance.now(); const sinceLastReaction = now - lastReactionTime; const reactionDelay = params.reactionMs + (Math.random() * 2 - 1) * params.jitterMs; - return sinceLastReaction > reactionDelay; + if (sinceLastReaction <= reactionDelay) return false; + + // Reliability: Easy иногда «промахивается» (75%), Medium — 95%, Hard — Phase 3 + const reliability = difficulty === 'easy' ? 0.75 : 0.95; + return Math.random() < reliability; } diff --git a/src/ai/personalities/AggressiveAI.ts b/src/ai/personalities/AggressiveAI.ts index d759e65..e40d74a 100644 --- a/src/ai/personalities/AggressiveAI.ts +++ b/src/ai/personalities/AggressiveAI.ts @@ -33,6 +33,7 @@ export class AggressiveAI implements AIPlayer { 'left', this.params, this.lastReactionTimeLeft, + this.difficulty, ); if (shouldLeft && ctx.leftFlipper.activate()) { this.lastReactionTimeLeft = performance.now(); @@ -42,6 +43,7 @@ export class AggressiveAI implements AIPlayer { 'right', this.params, this.lastReactionTimeRight, + this.difficulty, ); if (shouldRight && ctx.rightFlipper.activate()) { this.lastReactionTimeRight = performance.now(); diff --git a/src/ai/personalities/DefensiveAI.ts b/src/ai/personalities/DefensiveAI.ts index 0f392ae..be57e6b 100644 --- a/src/ai/personalities/DefensiveAI.ts +++ b/src/ai/personalities/DefensiveAI.ts @@ -35,6 +35,7 @@ export class DefensiveAI implements AIPlayer { 'left', this.params, this.lastReactionTimeLeft, + this.difficulty, ); if (shouldLeft) { if (ctx.leftFlipper.activate()) { @@ -46,6 +47,7 @@ export class DefensiveAI implements AIPlayer { 'right', this.params, this.lastReactionTimeRight, + this.difficulty, ); if (shouldRight) { if (ctx.rightFlipper.activate()) { diff --git a/src/ai/personalities/GhostAI.ts b/src/ai/personalities/GhostAI.ts index a2a257d..34867d5 100644 --- a/src/ai/personalities/GhostAI.ts +++ b/src/ai/personalities/GhostAI.ts @@ -41,6 +41,7 @@ export class GhostAI implements AIPlayer { 'left', this.params, this.lastReactionTimeLeft, + this.difficulty, ); if (shouldLeft && ctx.leftFlipper.activate()) { this.lastReactionTimeLeft = performance.now(); @@ -50,6 +51,7 @@ export class GhostAI implements AIPlayer { 'right', this.params, this.lastReactionTimeRight, + this.difficulty, ); if (shouldRight && ctx.rightFlipper.activate()) { this.lastReactionTimeRight = performance.now(); diff --git a/src/ai/personalities/TricksterAI.ts b/src/ai/personalities/TricksterAI.ts index 53ef45d..5431da7 100644 --- a/src/ai/personalities/TricksterAI.ts +++ b/src/ai/personalities/TricksterAI.ts @@ -38,6 +38,7 @@ export class TricksterAI implements AIPlayer { 'left', modifiedParams, this.lastReactionTimeLeft, + this.difficulty, ); if (shouldLeft && ctx.leftFlipper.activate()) { this.lastReactionTimeLeft = performance.now(); @@ -47,6 +48,7 @@ export class TricksterAI implements AIPlayer { 'right', modifiedParams, this.lastReactionTimeRight, + this.difficulty, ); if (shouldRight && ctx.rightFlipper.activate()) { this.lastReactionTimeRight = performance.now(); diff --git a/src/game/Bumper.ts b/src/game/Bumper.ts index ba3d5ee..1e2a20e 100644 --- a/src/game/Bumper.ts +++ b/src/game/Bumper.ts @@ -19,6 +19,8 @@ export class Bumper { private events: BumperEvents; private lastHitTime = 0; private cooldownMs = 0; // turbo bumper имеет cooldown 1 сек + private curveDirection: 1 | -1 = 1; // curve bumper — направление закрутки (фиксируется при создании) + private spinSprite?: Phaser.GameObjects.Graphics; // curve — вращающийся индикатор constructor( scene: Phaser.Scene, @@ -48,6 +50,21 @@ export class Bumper { // Visual — концентрические окружности (Tier-S effect) this.sprite = scene.add.circle(x, y, radius, this.computeFillColor()); this.sprite.setStrokeStyle(2, PALETTE.white, 0.7); + + // Curve — визуальный spin-индикатор + детерминированное направление по позиции + if (type === 'curve') { + this.curveDirection = ((Math.round(x + y) % 2) === 0) ? 1 : -1; + this.spinSprite = scene.add.graphics(); + this.spinSprite.lineStyle(2, PALETTE.accent, 0.9); + // Две дуги — закрученный «вихрь» + this.spinSprite.beginPath(); + this.spinSprite.arc(0, 0, radius + 4, 0, Math.PI * 0.6, false); + this.spinSprite.strokePath(); + this.spinSprite.beginPath(); + this.spinSprite.arc(0, 0, radius + 4, Math.PI, Math.PI * 1.6, false); + this.spinSprite.strokePath(); + this.spinSprite.setPosition(x, y); + } } private computeRadius(): number { @@ -94,6 +111,19 @@ export class Bumper { }); } + // Curve — поворачивает вектор скорости на ±35° (детерминированный знак на bumper) + // → траектория «закручивается», отличая от standard. Tier-S: visual spin. + if (this.type === 'curve') { + const v = ballBody.velocity; + const speed = Math.sqrt(v.x * v.x + v.y * v.y); + const inAngle = Math.atan2(v.y, v.x); + const outAngle = inAngle + this.curveDirection * (35 * Math.PI / 180); + this.scene.matter.body.setVelocity(ballBody, { + x: Math.cos(outAngle) * speed, + y: Math.sin(outAngle) * speed, + }); + } + // Bounce animation this.scene.tweens.add({ targets: this.sprite, @@ -103,6 +133,16 @@ export class Bumper { ease: 'Sine.easeOut', }); + // Curve — постоянный spin визуал-индикатор + if (this.type === 'curve' && this.spinSprite) { + this.scene.tweens.add({ + targets: this.spinSprite, + angle: this.spinSprite.angle + this.curveDirection * 180, + duration: 400, + ease: 'Cubic.easeOut', + }); + } + this.events.onHit?.(this, this.points); return this.points; } @@ -114,5 +154,6 @@ export class Bumper { destroy(): void { this.scene.matter.world.remove(this.body); this.sprite.destroy(); + this.spinSprite?.destroy(); } } diff --git a/src/scenes/ResultScene.ts b/src/scenes/ResultScene.ts index 33b7398..f604ca4 100644 --- a/src/scenes/ResultScene.ts +++ b/src/scenes/ResultScene.ts @@ -74,9 +74,49 @@ export class ResultScene extends Phaser.Scene { .setOrigin(0.5, 0); // === Buttons === - this.makeButton(cx, GAME_HEIGHT - 280, 'РЕВАНШ', () => this.onRematch()); - this.makeButton(cx, GAME_HEIGHT - 190, 'НОВАЯ ПАРТИЯ', () => this.onNewMatch()); - this.makeButton(cx, GAME_HEIGHT - 100, 'В МЕНЮ', () => this.onMenu()); + this.makeButton(cx, GAME_HEIGHT - 360, 'ПОДЕЛИТЬСЯ', () => void this.onShare()); + this.makeButton(cx, GAME_HEIGHT - 270, 'РЕВАНШ', () => this.onRematch()); + this.makeButton(cx, GAME_HEIGHT - 180, 'НОВАЯ ПАРТИЯ', () => this.onNewMatch()); + this.makeButton(cx, GAME_HEIGHT - 90, 'В МЕНЮ', () => this.onMenu()); + } + + private async onShare(): Promise { + const platform = this.registry.get('platform'); + if (!platform || typeof platform.shareImage !== 'function') return; + + const { matchResult } = this.sceneData; + const verdict = matchResult.victory ? 'Победа' : 'Поражение'; + const message = + `Пинбол-Дуэль: ${verdict} ${matchResult.goalsScored}:${matchResult.goalsAgainst} ` + + `vs ${matchResult.opponentPersonality}/${matchResult.opponentDifficulty}. ` + + `Очков: ${matchResult.seasonalPointsBase}.`; + + // Snapshot текущего фрейма camera → Blob → platform.shareImage + const blob = await new Promise((resolve) => { + this.game.renderer.snapshot((image) => { + if (image instanceof HTMLImageElement) { + const canvas = document.createElement('canvas'); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + const ctx = canvas.getContext('2d'); + if (!ctx) return resolve(null); + ctx.drawImage(image, 0, 0); + canvas.toBlob((b) => resolve(b), 'image/png'); + } else { + resolve(null); + } + }); + }); + + if (!blob) { + void platform.trackEvent?.('share_failed', { reason: 'snapshot_failed' }); + return; + } + void platform.trackEvent?.('share_clicked', { + matchId: matchResult.matchId, + victory: matchResult.victory, + }); + await platform.shareImage(blob, message); } private makeButton(x: number, y: number, label: string, onClick: () => void): void { diff --git a/src/scenes/SetupScene.ts b/src/scenes/SetupScene.ts index d3b7ea7..77788bc 100644 --- a/src/scenes/SetupScene.ts +++ b/src/scenes/SetupScene.ts @@ -5,6 +5,7 @@ import { PALETTE, ACTIVE_SETUP_SLOTS, SETUP_PHASE_DURATION_SEC, + BUMPER_POINTS, } from '../config/defaults'; import type { BumperType, @@ -15,34 +16,52 @@ import type { } from '../types'; // SetupScene — pre-match setup-фаза. -// Игрок выбирает bumper-тип для каждого из 5 активных слотов. +// Phase 2 DoD: палитра типов сверху + tap-палитра→tap-слот (unified mobile/desktop) + +// drag-from-palette (desktop). Trash-чип убирает bumper из слота. // см. concept-design.md#setup-фаза -// -// Phase 2: упрощённый UI — клик по слоту → циклически меняет тип (null → standard → slingshot → curve → turbo → null). -// Phase 3-4 будет полноценный drag-and-drop с панелью bumpers + AI-setup preview. -const BUMPER_CYCLE: ReadonlyArray = [ - null, - 'standard', - 'slingshot', - 'curve', - 'turbo', -]; +const BUMPER_TYPES: ReadonlyArray = ['standard', 'slingshot', 'curve', 'turbo']; +const SLOT_RADIUS = 36; +const PALETTE_CHIP_R = 32; interface SlotUI { slotId: SetupSlotId; position: { x: number; y: number }; current: BumperType | null; - visual: Phaser.GameObjects.Container; + hitArea: Phaser.GameObjects.Arc; + visualLayer: Phaser.GameObjects.Container; } +interface PaletteChip { + type: BumperType | 'trash'; + hitArea: Phaser.GameObjects.Arc; + highlight: Phaser.GameObjects.Arc; +} + +const TYPE_COLORS: Record = { + standard: PALETTE.white, + slingshot: PALETTE.ai, + curve: PALETTE.player, + turbo: PALETTE.accent, +}; +const TYPE_LABELS: Record = { + standard: 'STD', + slingshot: 'SLG', + curve: 'CUR', + turbo: 'TUR', +}; + export class SetupScene extends Phaser.Scene { private sceneData!: SetupSceneData; private slots: SlotUI[] = []; + private palette: PaletteChip[] = []; private timerText!: Phaser.GameObjects.Text; private timeRemaining = SETUP_PHASE_DURATION_SEC; private startTime = 0; + private selectedType: BumperType | 'trash' | null = null; + private dragGhost: Phaser.GameObjects.Container | null = null; + constructor() { super({ key: 'SetupScene' }); } @@ -55,9 +74,8 @@ export class SetupScene extends Phaser.Scene { this.startTime = this.time.now; this.timeRemaining = SETUP_PHASE_DURATION_SEC; - // === Title === this.add - .text(GAME_WIDTH / 2, 80, 'SETUP-ФАЗА', { + .text(GAME_WIDTH / 2, 60, 'SETUP-ФАЗА', { fontFamily: 'monospace', fontSize: '36px', color: '#ff006e', @@ -65,66 +83,65 @@ export class SetupScene extends Phaser.Scene { }) .setOrigin(0.5); - // === AI opponent info === const personalityRu: Record = { - defensive: 'Бэтти-Защитница', - aggressive: 'Турбо-Тор', + defensive: 'Бэтти', + aggressive: 'Турбо', trickster: 'Хитрый Хо', ghost: 'Эхо', }; - const difficultyRu: Record = { - easy: 'Easy', - medium: 'Medium', - hard: 'Hard', - }; this.add .text( GAME_WIDTH / 2, - 130, - `Противник: ${personalityRu[this.sceneData.opponentPersonality]} (${difficultyRu[this.sceneData.opponentDifficulty]})`, - { - fontFamily: 'monospace', - fontSize: '18px', - color: '#00f0ff', - }, + 105, + `Противник: ${personalityRu[this.sceneData.opponentPersonality]} (${this.sceneData.opponentDifficulty})`, + { fontFamily: 'monospace', fontSize: '16px', color: '#00f0ff' }, ) .setOrigin(0.5); - // === Timer === this.timerText = this.add - .text(GAME_WIDTH / 2, 170, `${this.timeRemaining}s`, { + .text(GAME_WIDTH / 2, 135, `${this.timeRemaining}s`, { fontFamily: 'monospace', - fontSize: '24px', + fontSize: '22px', color: '#ffbe0b', }) .setOrigin(0.5); - // === Instructions === + this.buildPalette(); + this.add .text( GAME_WIDTH / 2, - 220, - 'Кликни по слоту, чтобы выбрать тип bumper.\nКлик повторно — следующий тип.', + 260, + 'Выбери тип сверху → ткни в слот.\nИли перетащи (desktop). Корзина — убрать.', { fontFamily: 'monospace', - fontSize: '14px', - color: '#cccccc', + fontSize: '13px', + color: '#aaaaaa', align: 'center', }, ) .setOrigin(0.5); - // === Build slot grid === this.buildSlots(); - - // === Ready button === this.buildReadyButton(); - // === Telemetry: setup_phase_start === + // Drag-ghost follow pointer (для desktop drag UX) + this.input.on('pointermove', (p: Phaser.Input.Pointer) => { + if (this.dragGhost) { + this.dragGhost.setPosition(p.worldX, p.worldY); + } + }); + this.input.on('pointerup', () => { + if (this.dragGhost) { + this.dragGhost.destroy(); + this.dragGhost = null; + } + }); + this.trackEvent('setup_phase_start', { matchId: 'pending', mode: this.sceneData.mode, - aiSetupVisible: false, // в Phase 2-3 будем показывать AI setup + aiSetupVisible: false, }); } @@ -134,139 +151,177 @@ export class SetupScene extends Phaser.Scene { if (remaining !== this.timeRemaining) { this.timeRemaining = remaining; this.timerText.setText(`${remaining}s`); - if (remaining === 0) { - this.onReady(); - } + if (remaining === 0) this.onReady(); } } + private buildPalette(): void { + const items: Array = [...BUMPER_TYPES, 'trash']; + const totalWidth = items.length * (PALETTE_CHIP_R * 2 + 22); + const startX = (GAME_WIDTH - totalWidth) / 2 + PALETTE_CHIP_R + 11; + const y = 195; + + items.forEach((type, idx) => { + const x = startX + idx * (PALETTE_CHIP_R * 2 + 22); + + // Highlight ring (отображается когда выбран) + const highlight = this.add.circle(x, y, PALETTE_CHIP_R + 6, PALETTE.accent, 0.25); + highlight.setStrokeStyle(3, PALETTE.accent); + highlight.setVisible(false); + + // Chip + const fillColor = type === 'trash' ? PALETTE.bg : TYPE_COLORS[type]; + const chip = this.add.circle(x, y, PALETTE_CHIP_R, fillColor, 0.9); + chip.setStrokeStyle(2, PALETTE.white, 0.7); + chip.setInteractive({ useHandCursor: true }); + + const label = + type === 'trash' ? '✕' : `${TYPE_LABELS[type]}\n+${BUMPER_POINTS[type]}`; + this.add + .text(x, y, label, { + fontFamily: 'monospace', + fontSize: type === 'trash' ? '28px' : '12px', + color: type === 'trash' ? '#ff006e' : '#000000', + fontStyle: 'bold', + align: 'center', + }) + .setOrigin(0.5); + + chip.on('pointerdown', (p: Phaser.Input.Pointer) => { + this.selectType(type); + // Сразу создаём drag-ghost — если игрок начнёт двигать pointer, ghost полетит за ним; + // если просто отпустит над слотом — обработает pointerup на слоте (place). + this.startDragGhost(type, p.worldX, p.worldY); + }); + + this.palette.push({ type, hitArea: chip, highlight }); + }); + } + + private selectType(type: BumperType | 'trash'): void { + this.selectedType = type; + for (const chip of this.palette) { + chip.highlight.setVisible(chip.type === type); + } + } + + private startDragGhost(type: BumperType | 'trash', x: number, y: number): void { + if (this.dragGhost) this.dragGhost.destroy(); + const container = this.add.container(x, y); + const color = type === 'trash' ? PALETTE.bg : TYPE_COLORS[type]; + const circle = this.add.circle(0, 0, PALETTE_CHIP_R - 4, color, 0.7); + circle.setStrokeStyle(2, PALETTE.accent); + const text = this.add + .text(0, 0, type === 'trash' ? '✕' : TYPE_LABELS[type], { + fontFamily: 'monospace', + fontSize: '14px', + color: type === 'trash' ? '#ff006e' : '#000000', + fontStyle: 'bold', + }) + .setOrigin(0.5); + container.add([circle, text]); + container.setAlpha(0.85); + container.setDepth(1000); + this.dragGhost = container; + } + private buildSlots(): void { - // 5 активных slot'ов располагаются в игровой зоне игрока (нижняя половина) const slotPositions: Record = { goal_corner_left: { x: GAME_WIDTH * 0.2, y: GAME_HEIGHT * 0.85 }, goal_corner_right: { x: GAME_WIDTH * 0.8, y: GAME_HEIGHT * 0.85 }, near_goal_center: { x: GAME_WIDTH * 0.5, y: GAME_HEIGHT * 0.78 }, mid_left: { x: GAME_WIDTH * 0.3, y: GAME_HEIGHT * 0.65 }, mid_right: { x: GAME_WIDTH * 0.7, y: GAME_HEIGHT * 0.65 }, - // reserved post-MVP: far_corner: { x: 0, y: 0 }, opposite_center: { x: 0, y: 0 }, }; for (const slotId of ACTIVE_SETUP_SLOTS) { const pos = slotPositions[slotId]; - const container = this.add.container(pos.x, pos.y); + const hit = this.add.circle(pos.x, pos.y, SLOT_RADIUS, PALETTE.bg, 0.001); + hit.setInteractive({ useHandCursor: true }); - // Empty slot indicator — кольцо с пунктиром - const ring = this.add.circle(0, 0, 32, PALETTE.bg, 0.5); - ring.setStrokeStyle(2, PALETTE.white, 0.3); - container.add(ring); + const visualLayer = this.add.container(pos.x, pos.y); - // Label (тип bumper'а) - const label = this.add - .text(0, 0, '–', { - fontFamily: 'monospace', - fontSize: '20px', - color: '#888888', - }) - .setOrigin(0.5); - container.add(label); - - ring.setInteractive( - new Phaser.Geom.Circle(0, 0, 32), - Phaser.Geom.Circle.Contains, - ); - ring.on('pointerdown', () => this.cycleSlot(slotId)); - - this.slots.push({ + const slot: SlotUI = { slotId, position: pos, current: null, - visual: container, + hitArea: hit, + visualLayer, + }; + this.refreshSlotVisual(slot); + + hit.on('pointerup', () => this.applyToSlot(slot)); + hit.on('pointerdown', () => { + // Если в слоте есть bumper — клик по слоту удаляет его (быстрый shortcut). + // Если выбран type — applyToSlot ниже (через pointerup) поставит. + // Долгий тап на слоте удалит, чтобы не нужна была trash чтобы убрать. + // Реализация: pointerdown на слот = удалить ТОЛЬКО если selectedType === null и slot.current !== null + if (this.selectedType === null && slot.current !== null) { + slot.current = null; + this.refreshSlotVisual(slot); + } }); + + this.slots.push(slot); } } - private cycleSlot(slotId: SetupSlotId): void { - const slot = this.slots.find((s) => s.slotId === slotId); - if (!slot) return; - const idx = BUMPER_CYCLE.indexOf(slot.current); - const nextIdx = (idx + 1) % BUMPER_CYCLE.length; - slot.current = BUMPER_CYCLE[nextIdx]; + private applyToSlot(slot: SlotUI): void { + if (this.selectedType === null) return; + const newType: BumperType | null = + this.selectedType === 'trash' ? null : this.selectedType; + slot.current = newType; this.refreshSlotVisual(slot); - // Bounce animation this.tweens.add({ - targets: slot.visual, - scale: { from: 1.0, to: 0.8 }, - duration: 80, - yoyo: true, + targets: slot.visualLayer, + scale: { from: 1.2, to: 1.0 }, + duration: 120, + ease: 'Back.easeOut', }); } private refreshSlotVisual(slot: SlotUI): void { - const container = slot.visual; - // Очистить и пересоздать - container.removeAll(true); + slot.visualLayer.removeAll(true); if (slot.current === null) { - const ring = this.add.circle(0, 0, 32, PALETTE.bg, 0.5); - ring.setStrokeStyle(2, PALETTE.white, 0.3); - ring.setInteractive( - new Phaser.Geom.Circle(0, 0, 32), - Phaser.Geom.Circle.Contains, - ); - ring.on('pointerdown', () => this.cycleSlot(slot.slotId)); - const label = this.add - .text(0, 0, '–', { + const ring = this.add.circle(0, 0, SLOT_RADIUS, PALETTE.bg, 0.4); + ring.setStrokeStyle(2, PALETTE.white, 0.25); + const dash = this.add + .text(0, 0, '+', { fontFamily: 'monospace', - fontSize: '20px', - color: '#888888', + fontSize: '24px', + color: '#666666', }) .setOrigin(0.5); - container.add([ring, label]); + slot.visualLayer.add([ring, dash]); } else { - const colorMap: Record = { - standard: PALETTE.white, - slingshot: PALETTE.ai, - curve: PALETTE.player, - turbo: PALETTE.accent, - }; - const labelMap: Record = { - standard: 'STD', - slingshot: 'SLG', - curve: 'CUR', - turbo: 'TUR', - }; - const circle = this.add.circle(0, 0, 32, colorMap[slot.current], 0.85); + const circle = this.add.circle(0, 0, SLOT_RADIUS, TYPE_COLORS[slot.current], 0.85); circle.setStrokeStyle(2, PALETTE.white, 0.9); - circle.setInteractive( - new Phaser.Geom.Circle(0, 0, 32), - Phaser.Geom.Circle.Contains, - ); - circle.on('pointerdown', () => this.cycleSlot(slot.slotId)); const label = this.add - .text(0, 0, labelMap[slot.current], { + .text(0, 0, TYPE_LABELS[slot.current], { fontFamily: 'monospace', fontSize: '14px', color: '#000000', fontStyle: 'bold', }) .setOrigin(0.5); - container.add([circle, label]); + slot.visualLayer.add([circle, label]); } } private buildReadyButton(): void { const x = GAME_WIDTH / 2; - const y = GAME_HEIGHT * 0.92; - const bg = this.add.rectangle(x, y, 280, 60, PALETTE.bg, 0.9); + const y = GAME_HEIGHT * 0.95; + const bg = this.add.rectangle(x, y, 280, 55, PALETTE.bg, 0.9); bg.setStrokeStyle(3, PALETTE.player); bg.setInteractive({ useHandCursor: true }); const text = this.add .text(x, y, 'ГОТОВ', { fontFamily: 'monospace', - fontSize: '28px', + fontSize: '24px', color: '#ffffff', fontStyle: 'bold', }) @@ -275,25 +330,16 @@ export class SetupScene extends Phaser.Scene { bg.on('pointerover', () => bg.setStrokeStyle(3, PALETTE.accent)); bg.on('pointerout', () => bg.setStrokeStyle(3, PALETTE.player)); bg.on('pointerdown', () => { - this.tweens.add({ - targets: [bg, text], - scale: 0.95, - duration: 80, - yoyo: true, - }); + this.tweens.add({ targets: [bg, text], scale: 0.95, duration: 80, yoyo: true }); this.time.delayedCall(100, () => this.onReady()); }); } private onReady(): void { const playerSetup: SetupConfig = { - slots: this.slots.map((s) => ({ - slotId: s.slotId, - bumperType: s.current, - })), + slots: this.slots.map((s) => ({ slotId: s.slotId, bumperType: s.current })), }; - // Telemetry: setup_phase_complete this.trackEvent('setup_phase_complete', { matchId: 'pending', durationSeconds: (this.time.now - this.startTime) / 1000, @@ -317,10 +363,7 @@ export class SetupScene extends Phaser.Scene { private trackEvent(event: string, properties: Record): void { const platform = this.registry.get('platform'); if (platform && typeof platform.trackEvent === 'function') { - void platform.trackEvent(event, { - ...properties, - timestamp: Date.now(), - }); + void platform.trackEvent(event, { ...properties, timestamp: Date.now() }); } } }