2d-mini-tennis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bot.js ADDED
@@ -0,0 +1,85 @@
1
+ import { PHYSICS } from "./constants";
2
+ import { calcVelocity, calcArcVz, distance } from "./physics";
3
+ // ── Bot movement AI ──────────────────────────────────────
4
+ export function updateBot(state) {
5
+ const bot = state.bot;
6
+ const ball = state.ball;
7
+ const court = state.court;
8
+ // Default idle position: center of far half
9
+ let target = {
10
+ x: court.centerServiceX,
11
+ y: court.y + court.height * 0.2,
12
+ };
13
+ if (ball.isInPlay) {
14
+ // Chase ball when it's headed toward the bot's side
15
+ if (ball.vel.y < 0) {
16
+ // Predict where ball will be
17
+ const timeToReach = Math.abs(bot.pos.y - ball.pos.y) / Math.abs(ball.vel.y || 1);
18
+ target = {
19
+ x: ball.pos.x + ball.vel.x * timeToReach * 0.6,
20
+ y: ball.pos.y + ball.vel.y * timeToReach * 0.4,
21
+ };
22
+ }
23
+ else {
24
+ // Ball going away — return to ready position
25
+ target = {
26
+ x: court.centerServiceX,
27
+ y: court.y + court.height * 0.22,
28
+ };
29
+ }
30
+ }
31
+ // Clamp target within bot's playable area
32
+ target.x = Math.max(court.singlesLeft, Math.min(court.singlesRight, target.x));
33
+ target.y = Math.max(court.y + 10, Math.min(court.netY - 15, target.y));
34
+ // Move toward target at bot speed
35
+ const dx = target.x - bot.pos.x;
36
+ const dy = target.y - bot.pos.y;
37
+ const dist = Math.sqrt(dx * dx + dy * dy);
38
+ if (dist > 1) {
39
+ const speed = Math.min(PHYSICS.BOT_SPEED, dist);
40
+ bot.pos.x += (dx / dist) * speed;
41
+ bot.pos.y += (dy / dist) * speed;
42
+ }
43
+ }
44
+ // ── Bot hit decision ─────────────────────────────────────
45
+ export function botTryHit(state) {
46
+ const bot = state.bot;
47
+ const ball = state.ball;
48
+ const court = state.court;
49
+ if (!ball.isInPlay)
50
+ return false;
51
+ if (ball.lastHitBy === "bot")
52
+ return false;
53
+ // Ball must be on bot's side and near ground (bounced)
54
+ if (ball.pos.y > court.netY)
55
+ return false;
56
+ if (ball.z > 8)
57
+ return false;
58
+ if (!ball.hasBounced)
59
+ return false;
60
+ const dist = distance(bot.pos, ball.pos);
61
+ if (dist > PHYSICS.HIT_RADIUS)
62
+ return false;
63
+ // Pick a target on the player's side
64
+ const target = botPickRallyTarget(state);
65
+ ball.vel = calcVelocity(ball.pos, target, PHYSICS.BALL_SPEED_RALLY);
66
+ ball.vz = calcArcVz(ball.z, ball.pos, target, PHYSICS.BALL_SPEED_RALLY);
67
+ ball.lastHitBy = "bot";
68
+ ball.hasBounced = false;
69
+ state.bot.swingTimer = PHYSICS.SWING_DURATION;
70
+ return true;
71
+ }
72
+ // ── Bot rally target ─────────────────────────────────────
73
+ function botPickRallyTarget(state) {
74
+ const court = state.court;
75
+ const player = state.player;
76
+ // Aim away from the player with some randomness
77
+ const courtMidX = court.centerServiceX;
78
+ const aimX = player.pos.x > courtMidX
79
+ ? court.singlesLeft + 20 + Math.random() * 60
80
+ : court.singlesRight - 20 - Math.random() * 60;
81
+ const aimY = court.serviceLineNear +
82
+ Math.random() * (court.y + court.height - court.serviceLineNear - 20);
83
+ return { x: aimX, y: aimY };
84
+ }
85
+ //# sourceMappingURL=bot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.js","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ export declare const CANVAS_WIDTH = 480;
2
+ export declare const CANVAS_HEIGHT = 720;
3
+ export declare const COURT: {
4
+ MARGIN_X: number;
5
+ MARGIN_TOP: number;
6
+ MARGIN_BOTTOM: number;
7
+ WIDTH: number;
8
+ HEIGHT: number;
9
+ ALLEY_RATIO: number;
10
+ SERVICE_LINE_RATIO: number;
11
+ SURFACE_COLOR: string;
12
+ LINE_COLOR: string;
13
+ LINE_WIDTH: number;
14
+ NET_COLOR: string;
15
+ NET_WIDTH: number;
16
+ OUT_COLOR: string;
17
+ };
18
+ export declare const BALL: {
19
+ RADIUS: number;
20
+ COLOR: string;
21
+ STROKE_COLOR: string;
22
+ SHADOW_COLOR: string;
23
+ };
24
+ export declare const PLAYER: {
25
+ RADIUS: number;
26
+ COLOR: string;
27
+ BOT_COLOR: string;
28
+ STROKE_COLOR: string;
29
+ };
30
+ export declare const PHYSICS: {
31
+ BALL_SPEED_SERVE: number;
32
+ BALL_SPEED_RALLY: number;
33
+ BALL_GRAVITY: number;
34
+ TOSS_GRAVITY: number;
35
+ TOSS_VELOCITY: number;
36
+ TOSS_PEAK_Z: number;
37
+ BOUNCE_VZ: number;
38
+ BOUNCE_FRICTION: number;
39
+ PLAYER_LERP: number;
40
+ BOT_SPEED: number;
41
+ HIT_RADIUS: number;
42
+ POINT_PAUSE_FRAMES: number;
43
+ BOT_SERVE_DELAY: number;
44
+ FAULT_PAUSE_FRAMES: number;
45
+ SERVE_AIM_SPEED_X: number;
46
+ SERVE_AIM_SPEED_Y: number;
47
+ SWING_DURATION: number;
48
+ };
49
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,MAAM,CAAC;AAChC,eAAO,MAAM,aAAa,MAAM,CAAC;AAIjC,eAAO,MAAM,KAAK;;;;;;;;;;;;;;CAgBjB,CAAC;AAIF,eAAO,MAAM,IAAI;;;;;CAKhB,CAAC;AAIF,eAAO,MAAM,MAAM;;;;;CAKlB,CAAC;AAIF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;CAkBnB,CAAC"}
@@ -0,0 +1,55 @@
1
+ // ── Canvas ──────────────────────────────────────────────
2
+ export const CANVAS_WIDTH = 480;
3
+ export const CANVAS_HEIGHT = 720;
4
+ // ── Court ───────────────────────────────────────────────
5
+ export const COURT = {
6
+ MARGIN_X: 72,
7
+ MARGIN_TOP: 120,
8
+ MARGIN_BOTTOM: 120,
9
+ WIDTH: 336, // 280 * 1.2
10
+ HEIGHT: 480, // 600 * 0.8
11
+ ALLEY_RATIO: 0.125, // doubles alley: 4.5ft / 36ft per side
12
+ SERVICE_LINE_RATIO: 0.54, // service line 21ft from net / 39ft half-court
13
+ // Colors
14
+ SURFACE_COLOR: "#2D6B3F",
15
+ LINE_COLOR: "#FFFFFF",
16
+ LINE_WIDTH: 2,
17
+ NET_COLOR: "#CCCCCC",
18
+ NET_WIDTH: 3,
19
+ OUT_COLOR: "#1A4028",
20
+ };
21
+ // ── Ball ────────────────────────────────────────────────
22
+ export const BALL = {
23
+ RADIUS: 5,
24
+ COLOR: "#CCFF00",
25
+ STROKE_COLOR: "#99AA00",
26
+ SHADOW_COLOR: "rgba(0,0,0,0.25)",
27
+ };
28
+ // ── Players ─────────────────────────────────────────────
29
+ export const PLAYER = {
30
+ RADIUS: 12,
31
+ COLOR: "#4488FF",
32
+ BOT_COLOR: "#FF4444",
33
+ STROKE_COLOR: "rgba(0,0,0,0.3)",
34
+ };
35
+ // ── Physics ─────────────────────────────────────────────
36
+ export const PHYSICS = {
37
+ BALL_SPEED_SERVE: 5,
38
+ BALL_SPEED_RALLY: 4,
39
+ BALL_GRAVITY: 0.05, // gentle arc during flight
40
+ TOSS_GRAVITY: 0.25, // fast toss animation
41
+ TOSS_VELOCITY: 5,
42
+ TOSS_PEAK_Z: 50,
43
+ BOUNCE_VZ: 0.8, // small upward bounce after landing
44
+ BOUNCE_FRICTION: 0.6, // horizontal slowdown on bounce
45
+ PLAYER_LERP: 0.08, // mouse-follow smoothing
46
+ BOT_SPEED: 2.2,
47
+ HIT_RADIUS: 35,
48
+ POINT_PAUSE_FRAMES: 90, // ~1.5s at 60fps
49
+ BOT_SERVE_DELAY: 60, // ~1s before bot auto-serves
50
+ FAULT_PAUSE_FRAMES: 60, // ~1s fault message display
51
+ SERVE_AIM_SPEED_X: 0.022, // horizontal sweep speed (radians/frame)
52
+ SERVE_AIM_SPEED_Y: 0.015, // vertical sweep speed (radians/frame)
53
+ SWING_DURATION: 12, // racket swing animation frames (~200ms at 60fps)
54
+ };
55
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAE3D,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAChC,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AAEjC,2DAA2D;AAE3D,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,GAAG;IACf,aAAa,EAAE,GAAG;IAClB,KAAK,EAAE,GAAG,EAAE,YAAY;IACxB,MAAM,EAAE,GAAG,EAAE,YAAY;IACzB,WAAW,EAAE,KAAK,EAAE,uCAAuC;IAC3D,kBAAkB,EAAE,IAAI,EAAE,+CAA+C;IAEzE,SAAS;IACT,aAAa,EAAE,SAAS;IACxB,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,CAAC;IACb,SAAS,EAAE,SAAS;IACpB,SAAS,EAAE,CAAC;IACZ,SAAS,EAAE,SAAS;CACrB,CAAC;AAEF,2DAA2D;AAE3D,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,MAAM,EAAE,CAAC;IACT,KAAK,EAAE,SAAS;IAChB,YAAY,EAAE,SAAS;IACvB,YAAY,EAAE,kBAAkB;CACjC,CAAC;AAEF,2DAA2D;AAE3D,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,SAAS;IACpB,YAAY,EAAE,iBAAiB;CAChC,CAAC;AAEF,2DAA2D;AAE3D,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,gBAAgB,EAAE,CAAC;IACnB,gBAAgB,EAAE,CAAC;IACnB,YAAY,EAAE,IAAI,EAAE,2BAA2B;IAC/C,YAAY,EAAE,IAAI,EAAE,sBAAsB;IAC1C,aAAa,EAAE,CAAC;IAChB,WAAW,EAAE,EAAE;IACf,SAAS,EAAE,GAAG,EAAE,oCAAoC;IACpD,eAAe,EAAE,GAAG,EAAE,gCAAgC;IACtD,WAAW,EAAE,IAAI,EAAE,yBAAyB;IAC5C,SAAS,EAAE,GAAG;IACd,UAAU,EAAE,EAAE;IACd,kBAAkB,EAAE,EAAE,EAAE,iBAAiB;IACzC,eAAe,EAAE,EAAE,EAAE,6BAA6B;IAClD,kBAAkB,EAAE,EAAE,EAAE,4BAA4B;IACpD,iBAAiB,EAAE,KAAK,EAAE,yCAAyC;IACnE,iBAAiB,EAAE,KAAK,EAAE,uCAAuC;IACjE,cAAc,EAAE,EAAE,EAAE,kDAAkD;CACvE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { CourtDimensions } from "./types";
2
+ export declare function createCourtDimensions(): CourtDimensions;
3
+ export declare function drawCourt(ctx: CanvasRenderingContext2D, court: CourtDimensions): void;
4
+ //# sourceMappingURL=court.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"court.d.ts","sourceRoot":"","sources":["../src/court.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,wBAAgB,qBAAqB,IAAI,eAAe,CAsBvD;AAeD,wBAAgB,SAAS,CACvB,GAAG,EAAE,wBAAwB,EAC7B,KAAK,EAAE,eAAe,QA0CvB"}
package/dist/court.js ADDED
@@ -0,0 +1,63 @@
1
+ import { CANVAS_WIDTH, CANVAS_HEIGHT, COURT } from "./constants";
2
+ export function createCourtDimensions() {
3
+ const x = COURT.MARGIN_X;
4
+ const y = COURT.MARGIN_TOP;
5
+ const width = COURT.WIDTH;
6
+ const height = COURT.HEIGHT;
7
+ const netY = y + height / 2;
8
+ const halfH = height / 2;
9
+ const serviceOffset = halfH * COURT.SERVICE_LINE_RATIO;
10
+ const alleyW = width * COURT.ALLEY_RATIO;
11
+ return {
12
+ x,
13
+ y,
14
+ width,
15
+ height,
16
+ netY,
17
+ serviceLineNear: netY + serviceOffset,
18
+ serviceLineFar: netY - serviceOffset,
19
+ centerServiceX: x + width / 2,
20
+ singlesLeft: x + alleyW,
21
+ singlesRight: x + width - alleyW,
22
+ };
23
+ }
24
+ function line(ctx, x1, y1, x2, y2) {
25
+ ctx.beginPath();
26
+ ctx.moveTo(x1, y1);
27
+ ctx.lineTo(x2, y2);
28
+ ctx.stroke();
29
+ }
30
+ export function drawCourt(ctx, court) {
31
+ // Out-of-bounds background
32
+ ctx.fillStyle = COURT.OUT_COLOR;
33
+ ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
34
+ // Court surface
35
+ ctx.fillStyle = COURT.SURFACE_COLOR;
36
+ ctx.fillRect(court.x, court.y, court.width, court.height);
37
+ // White lines
38
+ ctx.strokeStyle = COURT.LINE_COLOR;
39
+ ctx.lineWidth = COURT.LINE_WIDTH;
40
+ // Outer boundary
41
+ ctx.strokeRect(court.x, court.y, court.width, court.height);
42
+ // Singles sidelines
43
+ line(ctx, court.singlesLeft, court.y, court.singlesLeft, court.y + court.height);
44
+ line(ctx, court.singlesRight, court.y, court.singlesRight, court.y + court.height);
45
+ // Service lines
46
+ line(ctx, court.singlesLeft, court.serviceLineFar, court.singlesRight, court.serviceLineFar);
47
+ line(ctx, court.singlesLeft, court.serviceLineNear, court.singlesRight, court.serviceLineNear);
48
+ // Center service line
49
+ line(ctx, court.centerServiceX, court.serviceLineFar, court.centerServiceX, court.serviceLineNear);
50
+ // Center ticks on baselines
51
+ const tick = 8;
52
+ line(ctx, court.centerServiceX, court.y, court.centerServiceX, court.y + tick);
53
+ line(ctx, court.centerServiceX, court.y + court.height, court.centerServiceX, court.y + court.height - tick);
54
+ // Net
55
+ ctx.strokeStyle = COURT.NET_COLOR;
56
+ ctx.lineWidth = COURT.NET_WIDTH;
57
+ line(ctx, court.x - 10, court.netY, court.x + court.width + 10, court.netY);
58
+ // Net posts
59
+ ctx.fillStyle = COURT.NET_COLOR;
60
+ ctx.fillRect(court.x - 14, court.netY - 3, 6, 6);
61
+ ctx.fillRect(court.x + court.width + 8, court.netY - 3, 6, 6);
62
+ }
63
+ //# sourceMappingURL=court.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"court.js","sourceRoot":"","sources":["../src/court.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { default as TennisGame } from "./TennisGame";
2
+ export type { Vec2, Ball, Player, Score, ServePhase, ServeState, GamePhase, CourtDimensions, GameState, } from "./types";
3
+ export { CANVAS_WIDTH, CANVAS_HEIGHT, COURT, BALL, PLAYER, PHYSICS } from "./constants";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EACV,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,KAAK,EACL,UAAU,EACV,UAAU,EACV,SAAS,EACT,eAAe,EACf,SAAS,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { default as TennisGame } from "./TennisGame";
2
+ export { CANVAS_WIDTH, CANVAS_HEIGHT, COURT, BALL, PLAYER, PHYSICS } from "./constants";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AAYrD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Vec2 } from "./types";
2
+ export interface InputState {
3
+ mousePos: Vec2;
4
+ mouseClicked: boolean;
5
+ mouseDown: boolean;
6
+ }
7
+ export declare function createInputHandlers(canvas: HTMLCanvasElement): {
8
+ state: InputState;
9
+ consumeClick(): void;
10
+ cleanup(): void;
11
+ };
12
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,iBAAiB;;;;EAmF5D"}
package/dist/input.js ADDED
@@ -0,0 +1,79 @@
1
+ export function createInputHandlers(canvas) {
2
+ const state = {
3
+ mousePos: { x: 0, y: 0 },
4
+ mouseClicked: false,
5
+ mouseDown: false,
6
+ };
7
+ function toCanvasMouse(e) {
8
+ const rect = canvas.getBoundingClientRect();
9
+ const scaleX = canvas.width / rect.width;
10
+ const scaleY = canvas.height / rect.height;
11
+ return {
12
+ x: (e.clientX - rect.left) * scaleX,
13
+ y: (e.clientY - rect.top) * scaleY,
14
+ };
15
+ }
16
+ function toCanvasTouch(t) {
17
+ const rect = canvas.getBoundingClientRect();
18
+ const scaleX = canvas.width / rect.width;
19
+ const scaleY = canvas.height / rect.height;
20
+ return {
21
+ x: (t.clientX - rect.left) * scaleX,
22
+ y: (t.clientY - rect.top) * scaleY,
23
+ };
24
+ }
25
+ // Mouse events
26
+ const onMove = (e) => {
27
+ state.mousePos = toCanvasMouse(e);
28
+ };
29
+ const onDown = (e) => {
30
+ state.mousePos = toCanvasMouse(e);
31
+ state.mouseDown = true;
32
+ state.mouseClicked = true;
33
+ };
34
+ const onUp = () => {
35
+ state.mouseDown = false;
36
+ };
37
+ // Touch events
38
+ const onTouchStart = (e) => {
39
+ e.preventDefault();
40
+ const touch = e.touches[0];
41
+ if (touch) {
42
+ state.mousePos = toCanvasTouch(touch);
43
+ state.mouseDown = true;
44
+ state.mouseClicked = true;
45
+ }
46
+ };
47
+ const onTouchMove = (e) => {
48
+ e.preventDefault();
49
+ const touch = e.touches[0];
50
+ if (touch) {
51
+ state.mousePos = toCanvasTouch(touch);
52
+ }
53
+ };
54
+ const onTouchEnd = (e) => {
55
+ e.preventDefault();
56
+ state.mouseDown = false;
57
+ };
58
+ canvas.addEventListener("mousemove", onMove);
59
+ canvas.addEventListener("mousedown", onDown);
60
+ canvas.addEventListener("mouseup", onUp);
61
+ canvas.addEventListener("touchstart", onTouchStart, { passive: false });
62
+ canvas.addEventListener("touchmove", onTouchMove, { passive: false });
63
+ canvas.addEventListener("touchend", onTouchEnd, { passive: false });
64
+ return {
65
+ state,
66
+ consumeClick() {
67
+ state.mouseClicked = false;
68
+ },
69
+ cleanup() {
70
+ canvas.removeEventListener("mousemove", onMove);
71
+ canvas.removeEventListener("mousedown", onDown);
72
+ canvas.removeEventListener("mouseup", onUp);
73
+ canvas.removeEventListener("touchstart", onTouchStart);
74
+ canvas.removeEventListener("touchmove", onTouchMove);
75
+ canvas.removeEventListener("touchend", onTouchEnd);
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Ball, Vec2, CourtDimensions } from "./types";
2
+ export declare function updateBall(ball: Ball): void;
3
+ export declare function calcArcVz(startZ: number, from: Vec2, to: Vec2, speed: number): number;
4
+ export declare function calcVelocity(from: Vec2, to: Vec2, speed: number): Vec2;
5
+ export declare function distance(a: Vec2, b: Vec2): number;
6
+ export declare function magnitude(v: Vec2): number;
7
+ export declare function isInServiceBox(pos: Vec2, court: CourtDimensions, targetSide: "near" | "far", targetBox: "left" | "right"): boolean;
8
+ export declare function isOnSide(pos: Vec2, court: CourtDimensions, side: "near" | "far"): boolean;
9
+ export declare function ballOutOfCourt(ball: Ball, court: CourtDimensions): boolean;
10
+ export declare function isInSingles(pos: Vec2, court: CourtDimensions): boolean;
11
+ //# sourceMappingURL=physics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../src/physics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK3D,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAe3C;AAID,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,EAAE,EAAE,IAAI,EACR,KAAK,EAAE,MAAM,GACZ,MAAM,CAOR;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMtE;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,CAIjD;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAEzC;AAID,wBAAgB,cAAc,CAC5B,GAAG,EAAE,IAAI,EACT,KAAK,EAAE,eAAe,EACtB,UAAU,EAAE,MAAM,GAAG,KAAK,EAC1B,SAAS,EAAE,MAAM,GAAG,OAAO,GAC1B,OAAO,CA0BT;AAED,wBAAgB,QAAQ,CACtB,GAAG,EAAE,IAAI,EACT,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,GAAG,KAAK,GACnB,OAAO,CAKT;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAO1E;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAOtE"}
@@ -0,0 +1,86 @@
1
+ import { PHYSICS } from "./constants";
2
+ // ── Ball movement ───────────────────────────────────────
3
+ export function updateBall(ball) {
4
+ ball.pos.x += ball.vel.x;
5
+ ball.pos.y += ball.vel.y;
6
+ ball.z += ball.vz;
7
+ ball.vz -= PHYSICS.BALL_GRAVITY;
8
+ // For non-in-play balls (toss idle, etc), hard stop at ground
9
+ if (ball.z < 0 && !ball.isInPlay) {
10
+ ball.z = 0;
11
+ ball.vz = 0;
12
+ }
13
+ ball.shadow.x = ball.pos.x;
14
+ ball.shadow.y = ball.pos.y;
15
+ }
16
+ // ── Calculate arc vz so ball lands at target ────────────
17
+ export function calcArcVz(startZ, from, to, speed) {
18
+ const dist = distance(from, to);
19
+ const frames = dist / Math.max(speed, 0.1);
20
+ if (frames <= 1)
21
+ return 0;
22
+ // z(T) = startZ + vz*T - 0.5*g*T² = 0
23
+ // vz = (0.5*g*T² - startZ) / T
24
+ return (0.5 * PHYSICS.BALL_GRAVITY * frames * frames - startZ) / frames;
25
+ }
26
+ // ── Velocity helpers ────────────────────────────────────
27
+ export function calcVelocity(from, to, speed) {
28
+ const dx = to.x - from.x;
29
+ const dy = to.y - from.y;
30
+ const dist = Math.sqrt(dx * dx + dy * dy);
31
+ if (dist === 0)
32
+ return { x: 0, y: -speed };
33
+ return { x: (dx / dist) * speed, y: (dy / dist) * speed };
34
+ }
35
+ export function distance(a, b) {
36
+ const dx = a.x - b.x;
37
+ const dy = a.y - b.y;
38
+ return Math.sqrt(dx * dx + dy * dy);
39
+ }
40
+ export function magnitude(v) {
41
+ return Math.sqrt(v.x * v.x + v.y * v.y);
42
+ }
43
+ // ── Bounds checking ─────────────────────────────────────
44
+ export function isInServiceBox(pos, court, targetSide, targetBox) {
45
+ const mid = court.centerServiceX;
46
+ let boxLeft, boxRight, boxTop, boxBottom;
47
+ if (targetSide === "far") {
48
+ boxTop = court.serviceLineFar;
49
+ boxBottom = court.netY;
50
+ }
51
+ else {
52
+ boxTop = court.netY;
53
+ boxBottom = court.serviceLineNear;
54
+ }
55
+ if (targetBox === "left") {
56
+ boxLeft = court.singlesLeft;
57
+ boxRight = mid;
58
+ }
59
+ else {
60
+ boxLeft = mid;
61
+ boxRight = court.singlesRight;
62
+ }
63
+ return (pos.x >= boxLeft &&
64
+ pos.x <= boxRight &&
65
+ pos.y >= boxTop &&
66
+ pos.y <= boxBottom);
67
+ }
68
+ export function isOnSide(pos, court, side) {
69
+ if (side === "near") {
70
+ return pos.y > court.netY;
71
+ }
72
+ return pos.y < court.netY;
73
+ }
74
+ export function ballOutOfCourt(ball, court) {
75
+ return (ball.pos.y < court.y - 60 ||
76
+ ball.pos.y > court.y + court.height + 60 ||
77
+ ball.pos.x < court.x - 60 ||
78
+ ball.pos.x > court.x + court.width + 60);
79
+ }
80
+ export function isInSingles(pos, court) {
81
+ return (pos.x >= court.singlesLeft &&
82
+ pos.x <= court.singlesRight &&
83
+ pos.y >= court.y &&
84
+ pos.y <= court.y + court.height);
85
+ }
86
+ //# sourceMappingURL=physics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physics.js","sourceRoot":"","sources":["../src/physics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Score } from "./types";
2
+ export declare function createInitialScore(): Score;
3
+ export declare function awardPoint(score: Score, winner: "player" | "bot"): {
4
+ gameOver: boolean;
5
+ setOver: boolean;
6
+ };
7
+ export declare function toggleServiceBox(score: Score): void;
8
+ export declare function formatPointScore(score: Score): string;
9
+ export declare function formatGameScore(score: Score): string;
10
+ //# sourceMappingURL=scoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../src/scoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAIrC,wBAAgB,kBAAkB,IAAI,KAAK,CAY1C;AAID,wBAAgB,UAAU,CACxB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,QAAQ,GAAG,KAAK,GACvB;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAsCzC;AAgCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAEnD;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAMrD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,88 @@
1
+ // ── Create initial score ─────────────────────────────────
2
+ export function createInitialScore() {
3
+ return {
4
+ player: 0,
5
+ bot: 0,
6
+ playerGames: 0,
7
+ botGames: 0,
8
+ isDeuce: false,
9
+ advantage: null,
10
+ servingSide: "player",
11
+ serviceBox: "right",
12
+ serveAttempt: 1,
13
+ };
14
+ }
15
+ // ── Award a point ────────────────────────────────────────
16
+ export function awardPoint(score, winner) {
17
+ const loser = winner === "player" ? "bot" : "player";
18
+ // Handle deuce/advantage
19
+ if (score.isDeuce) {
20
+ if (score.advantage === winner) {
21
+ // Advantage + point = game won
22
+ return finishGame(score, winner);
23
+ }
24
+ else if (score.advantage === loser) {
25
+ // Back to deuce
26
+ score.advantage = null;
27
+ return { gameOver: false, setOver: false };
28
+ }
29
+ else {
30
+ // No advantage — give advantage to winner
31
+ score.advantage = winner;
32
+ return { gameOver: false, setOver: false };
33
+ }
34
+ }
35
+ // Normal scoring: 0 → 15 → 30 → 40
36
+ const points = [0, 15, 30, 40];
37
+ const current = score[winner];
38
+ const idx = points.indexOf(current);
39
+ if (current === 40) {
40
+ if (score[loser] === 40) {
41
+ // Enter deuce
42
+ score.isDeuce = true;
43
+ score.advantage = winner;
44
+ return { gameOver: false, setOver: false };
45
+ }
46
+ // Win the game
47
+ return finishGame(score, winner);
48
+ }
49
+ // Advance score
50
+ score[winner] = points[idx + 1];
51
+ return { gameOver: false, setOver: false };
52
+ }
53
+ // ── Finish a game ────────────────────────────────────────
54
+ function finishGame(score, winner) {
55
+ const gamesKey = winner === "player" ? "playerGames" : "botGames";
56
+ score[gamesKey] += 1;
57
+ // Check for set win (first to 6)
58
+ if (score[gamesKey] >= 6) {
59
+ return { gameOver: true, setOver: true };
60
+ }
61
+ // Reset point score
62
+ score.player = 0;
63
+ score.bot = 0;
64
+ score.isDeuce = false;
65
+ score.advantage = null;
66
+ // Switch server
67
+ score.servingSide = score.servingSide === "player" ? "bot" : "player";
68
+ score.serviceBox = "right"; // always start from right
69
+ score.serveAttempt = 1;
70
+ return { gameOver: false, setOver: false };
71
+ }
72
+ // ── Toggle service box ───────────────────────────────────
73
+ export function toggleServiceBox(score) {
74
+ score.serviceBox = score.serviceBox === "right" ? "left" : "right";
75
+ }
76
+ // ── Format score for display ─────────────────────────────
77
+ export function formatPointScore(score) {
78
+ if (score.isDeuce) {
79
+ if (score.advantage === null)
80
+ return "Deuce";
81
+ return `Adv ${score.advantage === "player" ? "You" : "Bot"}`;
82
+ }
83
+ return `${score.player} - ${score.bot}`;
84
+ }
85
+ export function formatGameScore(score) {
86
+ return `Games: ${score.playerGames} - ${score.botGames}`;
87
+ }
88
+ //# sourceMappingURL=scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoring.js","sourceRoot":"","sources":["../src/scoring.ts"],"names":[],"mappings":"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"}