Phase 2 DoD closeout: curve physics, AI delta, share, setup UX
- Curve bumper: spin-modifier ±35° на отскоке (детерминированный знак на bumper) + визуальный вихрь-индикатор. Теперь curve механически отличается от standard. - Easy/Medium AI delta: распознавательная дистанция (Easy 150px / Medium 280px), reliability (Easy 0.75 / Medium 0.95). difficulty прокидывается из каждой personality в shouldActivateFlipperBase. Hard остаётся placeholder для Phase 3. - Share button на ResultScene: snapshot canvas → PNG blob → platform.shareImage(). Mock логирует, YG использует SDK share. Telemetry: share_clicked / share_failed. - Setup UX переписан: палитра типов сверху (4 типа + trash) + tap-палитра-tap-слот (unified mobile/desktop) + drag-from-palette ghost (desktop). Long-tap слота без выбранного типа удаляет содержимое (быстрый shortcut). Заменил cycle-on-click на normalуй setup, как в DoD. Closes codex finding #2 (Phase 2 DoD gap). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user