create-spud 0.1.11 → 0.2.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.
Files changed (41) hide show
  1. package/{example → examples/minimal}/index.html +0 -2
  2. package/{example → examples/minimal}/package.json +4 -4
  3. package/examples/minimal/public/favicon.svg +1 -0
  4. package/examples/minimal/readme.md +43 -0
  5. package/examples/minimal/src/gameplay.ts +16 -0
  6. package/examples/pong/index.html +31 -0
  7. package/examples/pong/package.json +21 -0
  8. package/examples/pong/public/favicon.svg +1 -0
  9. package/{example → examples/pong}/readme.md +7 -4
  10. package/examples/pong/src/assets/audio/goal.wav +0 -0
  11. package/examples/pong/src/assets/audio/paddle-hit.wav +0 -0
  12. package/examples/pong/src/assets/audio/wall-hit.wav +0 -0
  13. package/examples/pong/src/audio.ts +20 -0
  14. package/examples/pong/src/camera.ts +50 -0
  15. package/examples/pong/src/canvas.ts +36 -0
  16. package/examples/pong/src/gameLoop.ts +46 -0
  17. package/examples/pong/src/gameplay.ts +165 -0
  18. package/examples/pong/src/main.ts +23 -0
  19. package/examples/pong/src/state.ts +38 -0
  20. package/examples/pong/tsconfig.json +37 -0
  21. package/examples/pong/vite.config.ts +15 -0
  22. package/index.js +155 -0
  23. package/package.json +13 -12
  24. package/readme.md +19 -18
  25. package/example/bun.lock +0 -212
  26. package/example/public/favicon.svg +0 -1
  27. package/example/public/poster.webp +0 -0
  28. package/example/src/assets/audio/menu-music.mp3 +0 -0
  29. package/example/src/assets/audio/shoot.wav +0 -0
  30. package/example/src/assets/images/sprite.png +0 -0
  31. package/example/src/audio.ts +0 -31
  32. package/example/src/gameplay.ts +0 -53
  33. package/example/src/sprite.ts +0 -53
  34. package/index.ts +0 -199
  35. /package/{example → examples/minimal}/src/canvas.ts +0 -0
  36. /package/{example → examples/minimal}/src/gameLoop.ts +0 -0
  37. /package/{example → examples/minimal}/src/main.ts +0 -0
  38. /package/{example → examples/minimal}/src/state.ts +0 -0
  39. /package/{example → examples/minimal}/tsconfig.json +0 -0
  40. /package/{example → examples/minimal}/vite.config.ts +0 -0
  41. /package/{example → examples/pong}/src/assets/fonts/atari.ttf +0 -0
@@ -6,13 +6,11 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>game_name</title>
8
8
  <style>
9
- /* fonts_face_css */
10
9
  canvas {
11
10
  position: fixed;
12
11
  inset: 0;
13
12
  width: 100%;
14
13
  height: 100%;
15
- /* pixel_art_canvas_css */
16
14
  }
17
15
  </style>
18
16
  </head>
@@ -4,18 +4,18 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "bunx --bun vite",
7
+ "dev": "dev_command",
8
8
  "build": "tsc && vite build",
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@spud.gg/api": "0.0.28"
12
+ "@spud.gg/api": "0.0.30"
13
13
  },
14
14
  "devDependencies": {
15
- "sharp": "0.34.5",
15
+ "sharp": "0.35.2",
16
16
  "svgo": "4.0.1",
17
17
  "typescript": "^5",
18
- "vite": "8.0.13",
18
+ "vite": "8.1.0",
19
19
  "vite-plugin-image-optimizer": "2.0.3"
20
20
  }
21
21
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><style>.cls-1{fill:#2f0007;}.cls-2{fill:#fff;}</style></defs><rect class="cls-1" y="7.8" width="32" height="16.36" rx="8.2"/><rect class="cls-2" x="1.7" y="9.5" width="28.6" height="13.03" rx="6.5"/><path class="cls-1" d="M7.4,11.6h0A1.4,1.4,0,0,1,8.8,13v5.7a1.4,1.4,0,0,1-1.4,1.4h0a1.3,1.3,0,0,1-1.3-1.4V13A1.3,1.3,0,0,1,7.4,11.6Z"/><path class="cls-1" d="M11.8,15.8h0a1.4,1.4,0,0,1-1.4,1.4H4.7a1.4,1.4,0,0,1-1.4-1.4h0a1.3,1.3,0,0,1,1.4-1.3h5.7A1.3,1.3,0,0,1,11.8,15.8Z"/><circle class="cls-1" cx="20.7" cy="16" r="1.7"/><circle class="cls-1" cx="23.8" cy="12.8" r="1.7"/><circle class="cls-1" cx="23.8" cy="19.2" r="1.7"/><circle class="cls-1" cx="27.1" cy="16" r="1.7"/><ellipse class="cls-1" cx="14" cy="17.9" rx="1" ry="0.9"/><ellipse class="cls-1" cx="17" cy="17.9" rx="1" ry="0.9"/></svg>
@@ -0,0 +1,43 @@
1
+ # game_name
2
+
3
+ ## Getting started
4
+
5
+ Useful scripts:
6
+
7
+ ```bash
8
+ # (re)install dependencies
9
+ install_command
10
+ ```
11
+
12
+ ```bash
13
+ # start the local development server
14
+ dev_command_readme
15
+ ```
16
+
17
+ ```bash
18
+ # prepare a production build
19
+ build_command
20
+ ```
21
+
22
+ ## Resources:
23
+
24
+ - **Game assets**
25
+ - [itch.io game assets](https://itch.io/game-assets)
26
+
27
+ - **Animation**
28
+ - [easings.net](https://easings.net/)
29
+
30
+ - **Sprites & pixel art**
31
+ - [Aseprite](https://www.aseprite.org/)
32
+ - [Piskel](https://www.piskelapp.com/)
33
+ - [Pixel planet generator (itch.io)](https://deep-fold.itch.io/pixel-planet-generator)
34
+ - [Paint of Persia](https://dunin.itch.io/ptop)
35
+ - [Tiled](https://www.mapeditor.org/)
36
+
37
+ - **Documentation for dev tools & APIs used in this project**
38
+ - [Handmade Web Games](https://handmade.spud.gg/docs/guides/handmade-web-games)
39
+ - [Vite docs](https://vite.dev/)
40
+ bun_docs_resource
41
+ - [TypeScript handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
42
+ - [MDN Canvas API tutorial](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial)
43
+ - [@spud.gg/api (npm)](https://www.npmjs.com/package/@spud.gg/api)
@@ -0,0 +1,16 @@
1
+ import type { State } from "./state";
2
+
3
+ export function fixedUpdate(state: State, dt: number) {
4
+ // your gameplay logic here
5
+ }
6
+
7
+ export function update(state: State, dt: number) {
8
+ // your gameplay logic here
9
+ }
10
+
11
+ export function render(state: State, ctx: CanvasRenderingContext2D) {
12
+ const { width, height } = state.bounds;
13
+ ctx.clearRect(0, 0, width, height);
14
+
15
+ // your draw code here
16
+ }
@@ -0,0 +1,31 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>game_name</title>
8
+ <style>
9
+ @font-face {
10
+ font-family: "Atari";
11
+ font-display: block;
12
+ src: url("/src/assets/fonts/atari.ttf") format("truetype");
13
+ }
14
+
15
+ body {
16
+ margin: 0;
17
+ background: black;
18
+ }
19
+
20
+ canvas {
21
+ position: fixed;
22
+ inset: 0;
23
+ width: 100%;
24
+ height: 100%;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <script type="module" src="/src/main.ts"></script>
30
+ </body>
31
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "game_slug",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "dev_command",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@spud.gg/api": "0.0.30"
13
+ },
14
+ "devDependencies": {
15
+ "sharp": "0.35.2",
16
+ "svgo": "4.0.1",
17
+ "typescript": "^5",
18
+ "vite": "8.1.0",
19
+ "vite-plugin-image-optimizer": "2.0.3"
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><style>.cls-1{fill:#2f0007;}.cls-2{fill:#fff;}</style></defs><rect class="cls-1" y="7.8" width="32" height="16.36" rx="8.2"/><rect class="cls-2" x="1.7" y="9.5" width="28.6" height="13.03" rx="6.5"/><path class="cls-1" d="M7.4,11.6h0A1.4,1.4,0,0,1,8.8,13v5.7a1.4,1.4,0,0,1-1.4,1.4h0a1.3,1.3,0,0,1-1.3-1.4V13A1.3,1.3,0,0,1,7.4,11.6Z"/><path class="cls-1" d="M11.8,15.8h0a1.4,1.4,0,0,1-1.4,1.4H4.7a1.4,1.4,0,0,1-1.4-1.4h0a1.3,1.3,0,0,1,1.4-1.3h5.7A1.3,1.3,0,0,1,11.8,15.8Z"/><circle class="cls-1" cx="20.7" cy="16" r="1.7"/><circle class="cls-1" cx="23.8" cy="12.8" r="1.7"/><circle class="cls-1" cx="23.8" cy="19.2" r="1.7"/><circle class="cls-1" cx="27.1" cy="16" r="1.7"/><ellipse class="cls-1" cx="14" cy="17.9" rx="1" ry="0.9"/><ellipse class="cls-1" cx="17" cy="17.9" rx="1" ry="0.9"/></svg>
@@ -6,17 +6,17 @@ Useful scripts:
6
6
 
7
7
  ```bash
8
8
  # (re)install dependencies
9
- bun i
9
+ install_command
10
10
  ```
11
11
 
12
12
  ```bash
13
13
  # start the local development server
14
- bun dev
14
+ dev_command_readme
15
15
  ```
16
16
 
17
17
  ```bash
18
18
  # prepare a production build
19
- bun run build
19
+ build_command
20
20
  ```
21
21
 
22
22
  ## Resources:
@@ -34,8 +34,10 @@ bun run build
34
34
 
35
35
  - **Sprites & pixel art**
36
36
  - [Aseprite](https://www.aseprite.org/)
37
+ - [Piskel](https://www.piskelapp.com/)
37
38
  - [Pixel planet generator (itch.io)](https://deep-fold.itch.io/pixel-planet-generator)
38
39
  - [Paint of Persia](https://dunin.itch.io/ptop)
40
+ - [Tiled](https://www.mapeditor.org/)
39
41
 
40
42
  - **Fonts**
41
43
  - [Google Fonts](https://fonts.google.com/)
@@ -43,8 +45,9 @@ bun run build
43
45
  - [Font Library](https://fontlibrary.org/)
44
46
 
45
47
  - **Documentation for dev tools & APIs used in this project**
48
+ - [Handmade Web Games](https://handmade.spud.gg/docs/guides/handmade-web-games)
46
49
  - [Vite docs](https://vite.dev/)
47
- - [Bun docs](https://bun.com/docs)
50
+ bun_docs_resource
48
51
  - [TypeScript handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
49
52
  - [MDN Canvas API tutorial](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial)
50
53
  - [@spud.gg/api (npm)](https://www.npmjs.com/package/@spud.gg/api)
@@ -0,0 +1,20 @@
1
+ import { audio } from "@spud.gg/api";
2
+
3
+ /*
4
+ when you import binary assets using vite, you'll get a url string.
5
+ read more at https://vite.dev/guide/assets#importing-asset-as-url
6
+ */
7
+ import goalUrl from "./assets/audio/goal.wav";
8
+ import paddleHitUrl from "./assets/audio/paddle-hit.wav";
9
+ import wallHitUrl from "./assets/audio/wall-hit.wav";
10
+
11
+ /*
12
+ use createSounds for short, snappy SFX that you want pre-buffered
13
+ for low-latency playback. we recommend creating your own effects
14
+ using a tool like https://pro.sfxr.me/
15
+ */
16
+ export const sounds = audio.createSounds({
17
+ goal: { url: goalUrl },
18
+ paddleHit: { url: paddleHitUrl },
19
+ wallHit: { url: wallHitUrl },
20
+ });
@@ -0,0 +1,50 @@
1
+ import { game, State } from "./state";
2
+
3
+ export function shakeCamera(state: State, offset: { x: number; y: number }) {
4
+ state.camera.offset.x += offset.x;
5
+ state.camera.offset.y += offset.y;
6
+ }
7
+
8
+ export function easeCameraOffsetToZero(state: State, dt: number) {
9
+ const ease = 1 - Math.exp(-14 * dt);
10
+ state.camera.offset.x += (0 - state.camera.offset.x) * ease;
11
+ state.camera.offset.y += (0 - state.camera.offset.y) * ease;
12
+ }
13
+
14
+ function gameArea(state: State) {
15
+ const margin = 25;
16
+ const width = Math.max(0, state.bounds.width - margin * 2);
17
+ const height = Math.max(0, state.bounds.height - margin * 2);
18
+ const ratio = width / height;
19
+ const targetRatio = game.width / game.height;
20
+ const drawWidth = ratio > targetRatio ? height * targetRatio : width;
21
+ const drawHeight = ratio > targetRatio ? height : width / targetRatio;
22
+
23
+ return {
24
+ x: state.bounds.centerX - drawWidth / 2,
25
+ y: state.bounds.centerY - drawHeight / 2,
26
+ width: drawWidth,
27
+ height: drawHeight,
28
+ };
29
+ }
30
+
31
+ export function drawInGameArea(
32
+ state: State,
33
+ ctx: CanvasRenderingContext2D,
34
+ draw: (withScreenShake: (drawWorld: () => void) => void) => void,
35
+ ) {
36
+ const area = gameArea(state);
37
+ const scale = Math.min(area.width / game.width, area.height / game.height) * state.camera.zoom;
38
+
39
+ ctx.save();
40
+ ctx.translate(area.x + area.width / 2, area.y + area.height / 2);
41
+ ctx.scale(scale, scale);
42
+ ctx.translate(-state.camera.x, -state.camera.y);
43
+ draw((drawWorld) => {
44
+ ctx.save();
45
+ ctx.translate(state.camera.offset.x, state.camera.offset.y);
46
+ drawWorld();
47
+ ctx.restore();
48
+ });
49
+ ctx.restore();
50
+ }
@@ -0,0 +1,36 @@
1
+ export function createBounds() {
2
+ return { x: 0, y: 0, width: 0, height: 0, centerX: 0, centerY: 0 };
3
+ }
4
+
5
+ type Bounds = ReturnType<typeof createBounds>;
6
+
7
+ /*
8
+ by default, canvas rendering is blurry on high-dpi screens.
9
+ this fixes it by scaling the pixel buffer by the devicePixelRatio
10
+ and then setting a matching transform so that the drawing coords
11
+ can stay in CSS pixels.
12
+
13
+ see also: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
14
+ */
15
+ export function scaleAndObserveCanvasSize(ctx: CanvasRenderingContext2D, bounds: Bounds) {
16
+ const { canvas } = ctx;
17
+
18
+ function updateCanvasSize() {
19
+ const { x, y, width, height } = canvas.getBoundingClientRect();
20
+ const dpr = window.devicePixelRatio;
21
+
22
+ canvas.width = width * dpr;
23
+ canvas.height = height * dpr;
24
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25
+
26
+ bounds.x = x;
27
+ bounds.y = y;
28
+ bounds.width = width;
29
+ bounds.height = height;
30
+ bounds.centerX = width / 2;
31
+ bounds.centerY = height / 2;
32
+ }
33
+
34
+ updateCanvasSize();
35
+ new ResizeObserver(updateCanvasSize).observe(canvas);
36
+ }
@@ -0,0 +1,46 @@
1
+ type GameLoopConfig<State> = {
2
+ ctx: CanvasRenderingContext2D;
3
+ state: State;
4
+ update?: (state: State, dt: number) => void;
5
+ fixedUpdate?: (state: State, dt: number) => void;
6
+ render: (state: State, ctx: CanvasRenderingContext2D) => void;
7
+ fixedDeltaTime?: number;
8
+ };
9
+
10
+ export function gameLoop<State>({
11
+ ctx,
12
+ state,
13
+ update,
14
+ fixedUpdate,
15
+ render,
16
+ fixedDeltaTime = 8 / 1000,
17
+ }: GameLoopConfig<State>) {
18
+ let lastFrameTime: number | null = null;
19
+ let accumulator = 0;
20
+
21
+ function tick(now: number) {
22
+ if (lastFrameTime === null) {
23
+ lastFrameTime = now;
24
+ }
25
+
26
+ const elapsed = now - lastFrameTime;
27
+ const dt = Math.min(elapsed / 1000, 0.1);
28
+ lastFrameTime = now;
29
+
30
+ if (fixedUpdate) {
31
+ accumulator += dt;
32
+ while (accumulator >= fixedDeltaTime) {
33
+ fixedUpdate(state, fixedDeltaTime);
34
+ accumulator -= fixedDeltaTime;
35
+ }
36
+ }
37
+
38
+ update?.(state, dt);
39
+
40
+ render(state, ctx);
41
+ requestAnimationFrame(tick);
42
+ }
43
+
44
+ render(state, ctx);
45
+ requestAnimationFrame(tick);
46
+ }
@@ -0,0 +1,165 @@
1
+ import { Button, clamp, gamepads, HapticIntensity, keyboard, Player } from "@spud.gg/api";
2
+ import { sounds } from "./audio";
3
+ import { drawInGameArea, easeCameraOffsetToZero, shakeCamera } from "./camera";
4
+ import {
5
+ ballSize,
6
+ game,
7
+ paddleHeight,
8
+ paddleWidth,
9
+ paddleX,
10
+ State,
11
+ wallWidth,
12
+ } from "./state";
13
+
14
+ const ballHalf = ballSize / 2;
15
+ const wallX = game.width - wallWidth;
16
+ const paddleSpeed = 260;
17
+ const startSpeed = 240;
18
+ const maxSpeed = 620;
19
+ const speedIncreaseFactor = 0.035;
20
+
21
+ function serve(state: State, direction = -1) {
22
+ state.ball.x = game.width / 2;
23
+ state.ball.y = game.height / 2;
24
+ state.ball.dx = direction * Math.cos(Math.PI / 4);
25
+ state.ball.dy = (Math.random() < 0.5 ? -1 : 1) * Math.sin(Math.PI / 4);
26
+ state.ball.speed = startSpeed;
27
+ }
28
+
29
+ function movePaddle(state: State, dt: number) {
30
+ const p1 = gamepads.players.list[Player.P1];
31
+ const gamepadDirection =
32
+ p1.leftStick.y || (p1.isDown(Button.DpadDown) ? 1 : p1.isDown(Button.DpadUp) ? -1 : 0);
33
+ const keyboardDirection =
34
+ (keyboard.keysDown.has("ArrowDown") ? 1 : 0) + (keyboard.keysDown.has("ArrowUp") ? -1 : 0);
35
+ const direction = gamepadDirection || keyboardDirection;
36
+
37
+ state.paddle.y += direction * paddleSpeed * dt;
38
+ state.paddle.y = clamp(state.paddle.y, 0, game.height - paddleHeight);
39
+ }
40
+
41
+ function increaseBallSpeed(state: State) {
42
+ state.ball.speed += (maxSpeed - state.ball.speed) * speedIncreaseFactor;
43
+ }
44
+
45
+ function hitPaddle(state: State) {
46
+ const centerDist = state.ball.y - (state.paddle.y + paddleHeight / 2);
47
+ const normalized = clamp(centerDist / (paddleHeight / 2), -1, 1);
48
+ const angle = (Math.PI / 4) * normalized;
49
+
50
+ state.ball.dx = Math.cos(angle);
51
+ state.ball.dy = Math.sin(angle);
52
+ state.ball.x = paddleX + paddleWidth + ballHalf;
53
+ state.hits++;
54
+
55
+ increaseBallSpeed(state);
56
+ shakeCamera(state, { x: -4 - state.ball.speed / 90, y: 0 });
57
+ sounds("paddleHit").play();
58
+ gamepads.players.list[Player.P1].rumble(35, HapticIntensity.Balanced);
59
+ }
60
+
61
+ function updateBall(state: State, dt: number) {
62
+ state.ball.x += state.ball.dx * state.ball.speed * dt;
63
+ state.ball.y += state.ball.dy * state.ball.speed * dt;
64
+
65
+ if (state.ball.y - ballHalf <= 0) {
66
+ state.ball.y = ballHalf;
67
+ state.ball.dy = Math.abs(state.ball.dy);
68
+ shakeCamera(state, { x: 0, y: -2 - state.ball.speed / 120 });
69
+ sounds("wallHit").play();
70
+ }
71
+
72
+ if (state.ball.y + ballHalf >= game.height) {
73
+ state.ball.y = game.height - ballHalf;
74
+ state.ball.dy = -Math.abs(state.ball.dy);
75
+ shakeCamera(state, { x: 0, y: 2 + state.ball.speed / 120 });
76
+ sounds("wallHit").play();
77
+ }
78
+
79
+ if (state.ball.x + ballHalf >= wallX) {
80
+ state.ball.x = wallX - ballHalf;
81
+ state.ball.dx = -Math.abs(state.ball.dx);
82
+ state.hits++;
83
+ increaseBallSpeed(state);
84
+ shakeCamera(state, { x: 3 + state.ball.speed / 100, y: 0 });
85
+ sounds("wallHit").play();
86
+ }
87
+
88
+ const ball = {
89
+ left: state.ball.x - ballHalf,
90
+ right: state.ball.x + ballHalf,
91
+ top: state.ball.y - ballHalf,
92
+ bottom: state.ball.y + ballHalf,
93
+ };
94
+
95
+ const paddle = {
96
+ left: paddleX,
97
+ right: paddleX + paddleWidth,
98
+ top: state.paddle.y - ballSize,
99
+ bottom: state.paddle.y + paddleHeight + ballSize,
100
+ };
101
+
102
+ if (
103
+ state.ball.dx < 0 &&
104
+ ball.left <= paddle.right &&
105
+ ball.right >= paddle.left &&
106
+ ball.top <= paddle.bottom &&
107
+ ball.bottom >= paddle.top
108
+ ) {
109
+ hitPaddle(state);
110
+ }
111
+
112
+ if (state.ball.x + ballHalf < 0) {
113
+ state.hits = 0;
114
+ shakeCamera(state, { x: -8, y: 0 });
115
+ sounds("goal").play();
116
+ serve(state, -1);
117
+ }
118
+ }
119
+
120
+ export function fixedUpdate(state: State, dt: number) {
121
+ movePaddle(state, dt);
122
+ updateBall(state, dt);
123
+ easeCameraOffsetToZero(state, dt);
124
+ gamepads.clearInputs();
125
+ keyboard.clearInputs();
126
+ }
127
+
128
+ function drawGrid(ctx: CanvasRenderingContext2D) {
129
+ ctx.fillStyle = "rgba(255, 255, 255, 0.08)";
130
+ for (let y = 7; y < game.height; y += 14) {
131
+ for (let x = 7; x < game.width; x += 14) {
132
+ ctx.beginPath();
133
+ ctx.arc(x, y, 1, 0, Math.PI * 2);
134
+ ctx.fill();
135
+ }
136
+ }
137
+ }
138
+
139
+ function drawScore(state: State, ctx: CanvasRenderingContext2D) {
140
+ ctx.fillStyle = "white";
141
+ ctx.font = "34px Atari";
142
+ ctx.textAlign = "center";
143
+ ctx.textBaseline = "middle";
144
+ ctx.fillText(`${state.hits}`, game.width / 2, 40);
145
+ }
146
+
147
+ export function render(state: State, ctx: CanvasRenderingContext2D) {
148
+ ctx.fillStyle = "black";
149
+ ctx.fillRect(0, 0, state.bounds.width, state.bounds.height);
150
+
151
+ drawInGameArea(state, ctx, (withScreenShake) => {
152
+ withScreenShake(() => {
153
+ ctx.fillStyle = "#151515";
154
+ ctx.fillRect(0, 0, game.width, game.height);
155
+ drawGrid(ctx);
156
+
157
+ ctx.fillStyle = "white";
158
+ ctx.fillRect(paddleX, state.paddle.y, paddleWidth, paddleHeight);
159
+ ctx.fillRect(wallX, 0, wallWidth, game.height);
160
+ ctx.fillRect(state.ball.x - ballHalf, state.ball.y - ballHalf, ballSize, ballSize);
161
+ });
162
+
163
+ drawScore(state, ctx);
164
+ });
165
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ main.ts is the main entry point for your game.
3
+ */
4
+
5
+ import { keyboard, spud } from "@spud.gg/api";
6
+ import { scaleAndObserveCanvasSize } from "./canvas";
7
+ import { gameLoop } from "./gameLoop";
8
+ import { render, fixedUpdate } from "./gameplay";
9
+ import { state } from "./state";
10
+
11
+ if (!spud.isListening) {
12
+ spud.listen();
13
+ }
14
+
15
+ keyboard.listen();
16
+
17
+ const canvas = document.createElement("canvas");
18
+ document.body.appendChild(canvas);
19
+
20
+ const ctx = canvas.getContext("2d", { alpha: false })!;
21
+ scaleAndObserveCanvasSize(ctx, state.bounds);
22
+
23
+ gameLoop({ ctx, state, fixedUpdate, render, fixedDeltaTime: 1 / 500 });
@@ -0,0 +1,38 @@
1
+ import { createBounds } from "./canvas";
2
+
3
+ export const game = {
4
+ width: 640,
5
+ height: 360,
6
+ };
7
+
8
+ export const ballSize = 6;
9
+ export const paddleWidth = 6;
10
+ export const paddleHeight = 54;
11
+ export const paddleX = 14;
12
+ export const wallWidth = 6;
13
+
14
+ export const state = {
15
+ bounds: createBounds(),
16
+ hits: 0,
17
+ paddle: {
18
+ y: game.height / 2 - paddleHeight / 2,
19
+ },
20
+ ball: {
21
+ x: game.width / 2,
22
+ y: game.height / 2,
23
+ dx: -Math.cos(Math.PI / 4),
24
+ dy: Math.sin(Math.PI / 4),
25
+ speed: 240,
26
+ },
27
+ camera: {
28
+ x: game.width / 2,
29
+ y: game.height / 2,
30
+ zoom: 1,
31
+ offset: {
32
+ x: 0,
33
+ y: 0,
34
+ },
35
+ },
36
+ };
37
+
38
+ export type State = typeof state;
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "ESNext",
5
+ "lib": [
6
+ "ESNext",
7
+ "DOM",
8
+ "DOM.Iterable"
9
+ ],
10
+ "types": [
11
+ "vite/client"
12
+ ],
13
+ "skipLibCheck": true,
14
+ /* Bundler mode */
15
+ "moduleResolution": "bundler",
16
+ "allowImportingTsExtensions": true,
17
+ "moduleDetection": "force",
18
+ "noEmit": true,
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noPropertyAccessFromIndexSignature": true,
23
+ "noImplicitOverride": true,
24
+ "erasableSyntaxOnly": true,
25
+ "noFallthroughCasesInSwitch": true,
26
+ "noUncheckedSideEffectImports": true,
27
+ /* Aliases */
28
+ "paths": {
29
+ "~/*": [
30
+ "./src/*"
31
+ ]
32
+ }
33
+ },
34
+ "include": [
35
+ "src"
36
+ ]
37
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "vite";
2
+ import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ sourcemap: true,
7
+ modulePreload: {
8
+ polyfill: false,
9
+ },
10
+ },
11
+ plugins: [ViteImageOptimizer()],
12
+ resolve: {
13
+ tsconfigPaths: true,
14
+ },
15
+ });