Doc fix + stuck telemetry position для hot-spot analysis
Codex review on c9826ce — 3 findings, 2 actionable, 1 backlog.
## #2 Low: comment в flipperState расходится с реализацией
Старый комментарий писал что press во время releasing rejected, но
тесты и код специально разрешают re-press (responsive design choice).
Fixed: переписан state diagram с explicit «releasing» state как
intermediate (cooldown ещё НЕ активен). Добавлено явное упоминание
что re-press allowed во время releasing — это design choice, не bug.
## #1 High backlog: bonus telemetry для hot-spot analysis
Codex отметил residual ball_unstuck/hard_respawn без конкретных hot-spot
данных. Чтобы убрать blind tuning, добавил позицию + bumper type в
telemetry events:
- ball_unstuck: + ballPositionX, ballPositionY, overlappingBumperType
- ball_hard_respawn: + ballPositionX, ballPositionY
findBumperTypeAtPosition(pos): O(N) lookup среди fixed+setup bumpers
по координатам (с 1px tolerance). Возвращает «<source>:<type>» строку
для аналитики (e.g., «player_setup:curve» — самый частый stuck-spot
будет видно сразу при анализе аналитики).
Этого достаточно для будущего targeted fix без необходимости guess-tuning.
## #3 Known backlog
npm audit Vite/esbuild moderate — отложен до Phase 5 Vite 8 upgrade.
186/186 tests, typecheck/lint/build ✅.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,25 @@
|
|||||||
// Extracted из Flipper.ts чтобы isPressed/cooldown логика была unit-тестируемой
|
// Extracted из Flipper.ts чтобы isPressed/cooldown логика была unit-тестируемой
|
||||||
// без mocking Phaser tweens.
|
// без mocking Phaser tweens.
|
||||||
//
|
//
|
||||||
// State transitions:
|
// States and transitions:
|
||||||
// idle (isPressed=false, cooldown expired)
|
// idle (isPressed=false, cooldownUntil expired)
|
||||||
// → press(now) → pressed (isPressed=true)
|
// press(now) → pressed
|
||||||
// → press during cooldown / already pressed → rejected
|
// press during cooldown → rejected
|
||||||
// pressed
|
//
|
||||||
// → release() → releasing (isPressed=false, tween в процессе)
|
// pressed (isPressed=true)
|
||||||
// → press → rejected (already pressed)
|
// release() → releasing
|
||||||
// releasing
|
// press → rejected (already pressed)
|
||||||
// → onReleaseComplete(now) → cooldown started (cooldownUntil = now + cooldownMs)
|
//
|
||||||
// → press(now) во время releasing → rejected (если в cooldown)
|
// releasing (isPressed=false, release-tween в процессе, cooldown ЕЩЁ НЕ активен)
|
||||||
|
// press(now) → pressed (ALLOWED — responsive re-press до завершения tween;
|
||||||
|
// cooldownUntil ещё старое значение, canPress=true)
|
||||||
|
// onReleaseComplete(now) → cooldown starts (cooldownUntil = now + cooldownMs)
|
||||||
|
//
|
||||||
|
// cooldown (после onReleaseComplete до истечения cooldownMs)
|
||||||
|
// press(now < cooldownUntil) → rejected
|
||||||
|
// time passes → idle
|
||||||
|
//
|
||||||
|
// Re-press allowed во время releasing — design choice для responsive control.
|
||||||
//
|
//
|
||||||
// Flipper.ts wrapper'ит это с Phaser tweens + Matter body updates.
|
// Flipper.ts wrapper'ит это с Phaser tweens + Matter body updates.
|
||||||
|
|
||||||
|
|||||||
@@ -584,17 +584,37 @@ export class MatchScene extends Phaser.Scene {
|
|||||||
matchId: this.matchId,
|
matchId: this.matchId,
|
||||||
attempt: this.stuckWatchdog.getNudgeAttempts(),
|
attempt: this.stuckWatchdog.getNudgeAttempts(),
|
||||||
hadOverlap: overlap !== null,
|
hadOverlap: overlap !== null,
|
||||||
|
// Position для hot-spot analysis — где конкретно ball застревает (codex backlog #1)
|
||||||
|
ballPositionX: Math.round(bp.x),
|
||||||
|
ballPositionY: Math.round(bp.y),
|
||||||
|
overlappingBumperType: overlap?.body
|
||||||
|
? this.findBumperTypeAtPosition(overlap.body.position)
|
||||||
|
: null,
|
||||||
matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000,
|
matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Найти bumper type на позиции (для telemetry hot-spot analysis). */
|
||||||
|
private findBumperTypeAtPosition(pos: { x: number; y: number }): string | null {
|
||||||
|
for (const b of [...this.fixedBumpers, ...this.setupBumpers]) {
|
||||||
|
const body = b.getBody();
|
||||||
|
if (Math.abs(body.position.x - pos.x) < 1 && Math.abs(body.position.y - pos.y) < 1) {
|
||||||
|
return `${b.source}:${b.type}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hard respawn — мяч действительно не разлипается после 3 nudge.
|
* Hard respawn — мяч действительно не разлипается после 3 nudge.
|
||||||
* Уничтожаем тело, спавним из бокового порта в нейтральном цвете.
|
* Уничтожаем тело, спавним из бокового порта в нейтральном цвете.
|
||||||
*/
|
*/
|
||||||
private applyStuckHardRespawn(): void {
|
private applyStuckHardRespawn(): void {
|
||||||
|
const bp = this.ball.position;
|
||||||
this.trackEvent('ball_hard_respawn', {
|
this.trackEvent('ball_hard_respawn', {
|
||||||
matchId: this.matchId,
|
matchId: this.matchId,
|
||||||
|
ballPositionX: Math.round(bp.x),
|
||||||
|
ballPositionY: Math.round(bp.y),
|
||||||
matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000,
|
matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000,
|
||||||
});
|
});
|
||||||
this.ball.destroy();
|
this.ball.destroy();
|
||||||
|
|||||||
Reference in New Issue
Block a user