summaryrefslogtreecommitdiff
path: root/src/game
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2025-06-28 18:41:15 -0700
committerKai Stevenson <kai@kaistevenson.com>2025-06-28 18:41:15 -0700
commitffcdb8b126c5267040ddc8f0304b153fa88755e5 (patch)
tree2b4dc37b743c12b6175022d68472974f2bcd831b /src/game
local play, some physics
Diffstat (limited to 'src/game')
-rw-r--r--src/game/const.ts1
-rw-r--r--src/game/index.ts113
-rw-r--r--src/game/types.ts10
-rw-r--r--src/game/utils.ts24
4 files changed, 148 insertions, 0 deletions
diff --git a/src/game/const.ts b/src/game/const.ts
new file mode 100644
index 0000000..8210768
--- /dev/null
+++ b/src/game/const.ts
@@ -0,0 +1 @@
+export const VELOCITY_SCALING_FACTOR = 1 / 10;
diff --git a/src/game/index.ts b/src/game/index.ts
new file mode 100644
index 0000000..3ab20ca
--- /dev/null
+++ b/src/game/index.ts
@@ -0,0 +1,113 @@
+import { GAME_SIZE, PADDLE_HEIGHT, PADDLE_WIDTH, SessionState } from "../state";
+import { VELOCITY_SCALING_FACTOR } from "./const";
+import { Action, Collider } from "./types";
+import { applyBallBounce, applyBallVelocity } from "./utils";
+
+export const advanceState = async (
+ curState: SessionState,
+ action: Action | undefined
+): Promise<SessionState> => {
+ //simulate network
+ await new Promise((res) => setTimeout(res, 15));
+
+ let candidatePaddle = curState.localPlayerGameState.paddle;
+
+ if (action === Action.MOVE_LEFT) {
+ candidatePaddle.position[0] = Math.max(0, candidatePaddle.position[0] - 1);
+ } else if (action === Action.MOVE_RIGHT) {
+ candidatePaddle.position[0] = Math.min(
+ GAME_SIZE.cols - 1 - PADDLE_WIDTH,
+ candidatePaddle.position[0] + 1
+ );
+ }
+
+ let candidateBricks = curState.localPlayerGameState.bricks;
+
+ const colliders: Collider[] = [];
+
+ //paddle collider
+ colliders.push({
+ normal: [0, -1],
+ boundingBox: [
+ {
+ min: candidatePaddle.position[0],
+ max: candidatePaddle.position[0] + PADDLE_WIDTH,
+ },
+ {
+ min: candidatePaddle.position[1],
+ max: candidatePaddle.position[1] + PADDLE_HEIGHT,
+ },
+ ],
+ });
+
+ //brick colliders
+ candidateBricks.forEach(({ position }, i) => {
+ colliders.push({
+ boundingBox: position.map((pos) => ({
+ min: pos - 0.5,
+ max: pos + 0.5,
+ })) as Collider["boundingBox"],
+ normal: [0, 1],
+ onHit: () => candidateBricks.splice(i, 1),
+ });
+ });
+
+ //wall colliders
+ colliders.push(
+ ...([
+ //left wall
+ {
+ boundingBox: [
+ { min: -1, max: 0 },
+ { min: 0, max: GAME_SIZE.rows },
+ ],
+ normal: [1, 0],
+ },
+ //top wall
+ {
+ boundingBox: [
+ { min: -1, max: GAME_SIZE.cols + 1 },
+ { min: -1, max: 0 },
+ ],
+ normal: [0, 1],
+ },
+ //right wall
+ {
+ boundingBox: [
+ { min: GAME_SIZE.cols, max: GAME_SIZE.cols + 1 },
+ { min: 0, max: GAME_SIZE.rows },
+ ],
+ normal: [-1, 0],
+ },
+ ] satisfies Collider[])
+ );
+
+ const candidateBalls = curState.localPlayerGameState.balls
+ .map((ball) => {
+ let candidateBall = applyBallVelocity(ball);
+
+ const hitCollider = colliders.find(({ boundingBox }) =>
+ candidateBall.position.every(
+ (pos, i) => pos >= boundingBox[i].min && pos <= boundingBox[i].max
+ )
+ );
+
+ if (hitCollider) {
+ hitCollider.onHit && hitCollider.onHit();
+ candidateBall = applyBallBounce(candidateBall, hitCollider.normal);
+ }
+
+ return candidateBall;
+ })
+ .filter((ball) => !!ball);
+
+ return {
+ ...curState,
+ localPlayerGameState: {
+ ...curState.localPlayerGameState,
+ bricks: candidateBricks,
+ paddle: candidatePaddle,
+ balls: candidateBalls,
+ },
+ };
+};
diff --git a/src/game/types.ts b/src/game/types.ts
new file mode 100644
index 0000000..428bb64
--- /dev/null
+++ b/src/game/types.ts
@@ -0,0 +1,10 @@
+export enum Action {
+ MOVE_LEFT = "MOVE_LEFT",
+ MOVE_RIGHT = "MOVE_RIGHT",
+}
+
+export type Collider = {
+ boundingBox: [{ min: number; max: number }, { min: number; max: number }];
+ normal: [number, number];
+ onHit?: () => any;
+};
diff --git a/src/game/utils.ts b/src/game/utils.ts
new file mode 100644
index 0000000..ec81419
--- /dev/null
+++ b/src/game/utils.ts
@@ -0,0 +1,24 @@
+import { Ball } from "../state";
+import { VELOCITY_SCALING_FACTOR } from "./const";
+
+export const applyBallVelocity = (ball: Ball): Ball => {
+ let newPos = ball.position.map(
+ (a, i) => a + ball.velocity[i] * VELOCITY_SCALING_FACTOR
+ ) as [number, number];
+ return { ...ball, position: newPos };
+};
+
+export const applyBallBounce = (ball: Ball, normal: [number, number]): Ball => {
+ //calculate reflection
+ const newVelocity = ball.velocity.map(
+ (v, i) => v - v * Math.abs(normal[i]) * 2
+ ) as [number, number];
+
+ //move the ball out of the collider
+ const newPos = ball.position.map((p, i) => p + normal[i]) as [number, number];
+
+ //this gives a little punch
+ let newBall = { velocity: newVelocity, position: newPos };
+ newBall = applyBallVelocity(newBall);
+ return newBall;
+};