create-spud 0.1.3 → 0.1.5
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/example/_gitignore +24 -0
- package/example/index.html +0 -2
- package/example/src/demo.ts +23 -12
- package/example/src/gameplay.ts +17 -12
- package/example/src/helpers.ts +13 -3
- package/example/src/index.ts +35 -16
- package/example/vite.config.ts +1 -0
- package/index.ts +16 -12
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.suo
|
|
21
|
+
*.ntvs*
|
|
22
|
+
*.njsproj
|
|
23
|
+
*.sln
|
|
24
|
+
*.sw?
|
package/example/index.html
CHANGED
package/example/src/demo.ts
CHANGED
|
@@ -5,35 +5,46 @@
|
|
|
5
5
|
sounds, use local storage, or handle inputs.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { render, update } from "./gameplay";
|
|
9
9
|
import { state } from "./state";
|
|
10
|
-
import {
|
|
10
|
+
import { handleCanvasDpi } from "./helpers";
|
|
11
11
|
|
|
12
12
|
const canvas = document.createElement("canvas");
|
|
13
13
|
document.body.appendChild(canvas);
|
|
14
14
|
|
|
15
15
|
const ctx = canvas.getContext("2d", { alpha: false })!;
|
|
16
16
|
|
|
17
|
-
let lastFrameTime =
|
|
18
|
-
let
|
|
17
|
+
let lastFrameTime: number | null = null;
|
|
18
|
+
let physicsAccumulatorMs = 0;
|
|
19
19
|
|
|
20
20
|
function gameLoop(now: number) {
|
|
21
|
-
|
|
21
|
+
// initialize lastFrameTime on the first tick
|
|
22
|
+
if (lastFrameTime === null) {
|
|
23
|
+
lastFrameTime = now;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// cap extreme frame deltas so one frame doesn't do a huge catch-up after tab restore
|
|
27
|
+
const deltaTime = Math.min(now - lastFrameTime, 100);
|
|
22
28
|
lastFrameTime = now;
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
const rect = handleCanvasDpi(ctx);
|
|
25
31
|
|
|
26
32
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
// process physics updates at a fixed 120hz time step
|
|
34
|
+
physicsAccumulatorMs += deltaTime;
|
|
35
|
+
const physicsTicksPerSecond = 120;
|
|
36
|
+
const fixedDeltaMs = 1000 / physicsTicksPerSecond;
|
|
37
|
+
while (physicsAccumulatorMs >= fixedDeltaMs) {
|
|
38
|
+
physicsAccumulatorMs -= fixedDeltaMs;
|
|
39
|
+
update(state, fixedDeltaMs);
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
render({ state, ctx, rect });
|
|
36
44
|
requestAnimationFrame(gameLoop); // queue up the next tick
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
// render immediately, then start the game loop
|
|
48
|
+
const rect = handleCanvasDpi(ctx);
|
|
49
|
+
render({ state, ctx, rect });
|
|
39
50
|
requestAnimationFrame(gameLoop);
|
package/example/src/gameplay.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { Button, gamepads } from "@spud.gg/api";
|
|
2
2
|
import { State } from "./state";
|
|
3
|
+
import { CanvasFrameRect } from "./helpers";
|
|
3
4
|
/* moon_sprite_import */
|
|
4
5
|
/* audio_import */
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
the update loop runs in a fixed time-step and should be used for any
|
|
8
|
-
state updates. `
|
|
9
|
+
state updates. `deltaTime` is the time since the last update call.
|
|
9
10
|
*/
|
|
10
|
-
export function update(state: State,
|
|
11
|
-
state.elapsedSeconds +=
|
|
11
|
+
export function update(state: State, deltaTime: number) {
|
|
12
|
+
state.elapsedSeconds += deltaTime / 1000;
|
|
12
13
|
if (gamepads.anyPlayer.buttonJustPressed(Button.RightTrigger)) {
|
|
13
14
|
/* audio_shoot_sfx */
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
export function render({
|
|
19
|
+
state,
|
|
20
|
+
ctx,
|
|
21
|
+
rect,
|
|
22
|
+
}: {
|
|
23
|
+
state: State;
|
|
24
|
+
ctx: CanvasRenderingContext2D;
|
|
25
|
+
rect: CanvasFrameRect;
|
|
26
|
+
}) {
|
|
27
|
+
const { width, height, centerX, centerY } = rect;
|
|
23
28
|
|
|
24
29
|
/* pixel_art_image_smoothing */
|
|
25
30
|
|
|
@@ -32,10 +37,10 @@ export function draw(state: State, ctx: CanvasRenderingContext2D) {
|
|
|
32
37
|
radius: 30,
|
|
33
38
|
orbitRadius: 100,
|
|
34
39
|
get x() {
|
|
35
|
-
return
|
|
40
|
+
return centerX + Math.cos(state.elapsedSeconds) * moon.orbitRadius;
|
|
36
41
|
},
|
|
37
42
|
get y() {
|
|
38
|
-
return
|
|
43
|
+
return centerY + Math.sin(state.elapsedSeconds) * moon.orbitRadius;
|
|
39
44
|
},
|
|
40
45
|
};
|
|
41
46
|
/* moon_sprite_draw */
|
|
@@ -45,5 +50,5 @@ export function draw(state: State, ctx: CanvasRenderingContext2D) {
|
|
|
45
50
|
ctx.textAlign = "center";
|
|
46
51
|
ctx.textBaseline = "middle";
|
|
47
52
|
/* fonts_ctx_font */
|
|
48
|
-
ctx.fillText("hello, gamer",
|
|
53
|
+
ctx.fillText("hello, gamer", centerX, centerY);
|
|
49
54
|
}
|
package/example/src/helpers.ts
CHANGED
|
@@ -6,9 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
see also: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
9
|
+
export function handleCanvasDpi(ctx: CanvasRenderingContext2D) {
|
|
10
10
|
const { width, height } = ctx.canvas.getBoundingClientRect();
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const targetWidth = width * window.devicePixelRatio;
|
|
12
|
+
const targetHeight = height * window.devicePixelRatio;
|
|
13
|
+
const rect = { width, height, centerX: width / 2, centerY: height / 2 };
|
|
14
|
+
|
|
15
|
+
// optimization: since setting canvas.width and .height reset the canvas state, skip if the size didn't change
|
|
16
|
+
if (ctx.canvas.width === targetWidth && ctx.canvas.height === targetHeight) return rect;
|
|
17
|
+
|
|
18
|
+
ctx.canvas.width = targetWidth;
|
|
19
|
+
ctx.canvas.height = targetHeight;
|
|
13
20
|
ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
|
|
21
|
+
return rect;
|
|
14
22
|
}
|
|
23
|
+
|
|
24
|
+
export type CanvasFrameRect = ReturnType<typeof handleCanvasDpi>;
|
package/example/src/index.ts
CHANGED
|
@@ -4,42 +4,61 @@
|
|
|
4
4
|
step physics/update loop, and a variable framerate draw cycle.
|
|
5
5
|
see also:
|
|
6
6
|
- https://gafferongames.com/post/fix_your_timestep/
|
|
7
|
+
- https://www.youtube.com/watch?v=yGhfUcPjXuE
|
|
7
8
|
- https://gist.github.com/HipHopHuman/3e9b4a94b30ac9387d9a99ef2d29eb1a
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { spud, gamepads } from "@spud.gg/api";
|
|
11
|
-
import {
|
|
12
|
+
import { render, update } from "./gameplay";
|
|
12
13
|
import { state } from "./state";
|
|
13
|
-
import {
|
|
14
|
+
import { handleCanvasDpi } from "./helpers";
|
|
14
15
|
|
|
15
16
|
const canvas = document.createElement("canvas");
|
|
16
17
|
document.body.appendChild(canvas);
|
|
17
18
|
|
|
18
19
|
const ctx = canvas.getContext("2d", { alpha: false })!;
|
|
19
20
|
|
|
20
|
-
let lastFrameTime =
|
|
21
|
-
let
|
|
21
|
+
let lastFrameTime: number | null = null;
|
|
22
|
+
let physicsAccumulatorMs = 0;
|
|
23
|
+
let animationFrame = -1;
|
|
22
24
|
|
|
23
25
|
function gameLoop(now: number) {
|
|
24
|
-
|
|
26
|
+
// initialize lastFrameTime on the first tick
|
|
27
|
+
if (lastFrameTime === null) {
|
|
28
|
+
lastFrameTime = now;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// cap extreme frame deltas after tab restore so one frame doesn't do a huge catch-up
|
|
32
|
+
const deltaTime = Math.min(now - lastFrameTime, 100);
|
|
25
33
|
lastFrameTime = now;
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
const rect = handleCanvasDpi(ctx);
|
|
28
36
|
|
|
29
37
|
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
// process physics updates at a fixed 120hz time step
|
|
39
|
+
physicsAccumulatorMs = spud.isPaused ? 0 : physicsAccumulatorMs + deltaTime;
|
|
40
|
+
const physicsTicksPerSecond = 120;
|
|
41
|
+
const fixedDeltaMs = 1000 / physicsTicksPerSecond;
|
|
42
|
+
while (physicsAccumulatorMs >= fixedDeltaMs) {
|
|
43
|
+
physicsAccumulatorMs -= fixedDeltaMs;
|
|
44
|
+
update(state, fixedDeltaMs);
|
|
37
45
|
gamepads.clearInputs();
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
requestAnimationFrame(gameLoop); // queue up the next tick
|
|
49
|
+
render({ state, ctx, rect });
|
|
50
|
+
animationFrame = requestAnimationFrame(gameLoop); // queue up the next tick
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
// render immediately, then start the game loop
|
|
54
|
+
const rect = handleCanvasDpi(ctx);
|
|
55
|
+
render({ state, ctx, rect });
|
|
56
|
+
animationFrame = requestAnimationFrame(gameLoop);
|
|
57
|
+
|
|
58
|
+
// enable hot reloading in dev mode, but ensure the canvas is properly cleaned up
|
|
59
|
+
if (import.meta.hot) {
|
|
60
|
+
import.meta.hot.accept(() => {
|
|
61
|
+
cancelAnimationFrame(animationFrame);
|
|
62
|
+
canvas.remove();
|
|
63
|
+
});
|
|
64
|
+
}
|
package/example/vite.config.ts
CHANGED
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdir, readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { intro,
|
|
5
|
+
import { intro, note, cancel, text, confirm, tasks, group, multiselect } from "@clack/prompts";
|
|
6
6
|
import kebabcase from "lodash.kebabcase";
|
|
7
7
|
|
|
8
8
|
intro("It's spud time.");
|
|
@@ -20,14 +20,16 @@ const { rawGameName, features, shouldContinue } = await group(
|
|
|
20
20
|
rawGameName: () =>
|
|
21
21
|
text({
|
|
22
22
|
message: "Game name",
|
|
23
|
-
placeholder: "
|
|
23
|
+
placeholder: "e.g. Space Invaders",
|
|
24
24
|
validate(value) {
|
|
25
|
-
|
|
25
|
+
const trimmed = value?.trim();
|
|
26
|
+
if (!trimmed) return "Game name is required. Esc to cancel.";
|
|
27
|
+
if (!kebabcase(trimmed)) return "Game name must include letters or numbers.";
|
|
26
28
|
},
|
|
27
29
|
}),
|
|
28
30
|
features: () =>
|
|
29
31
|
multiselect({
|
|
30
|
-
message: "
|
|
32
|
+
message: "Select features",
|
|
31
33
|
initialValues: [],
|
|
32
34
|
required: false,
|
|
33
35
|
options: [
|
|
@@ -38,7 +40,7 @@ const { rawGameName, features, shouldContinue } = await group(
|
|
|
38
40
|
}),
|
|
39
41
|
shouldContinue: ({ results }) =>
|
|
40
42
|
confirm({
|
|
41
|
-
message: `
|
|
43
|
+
message: `Create a folder for "${kebabcase((results.rawGameName ?? "").trim())}" in the current directory?`,
|
|
42
44
|
}),
|
|
43
45
|
},
|
|
44
46
|
{
|
|
@@ -50,7 +52,7 @@ const { rawGameName, features, shouldContinue } = await group(
|
|
|
50
52
|
);
|
|
51
53
|
|
|
52
54
|
if (!shouldContinue) {
|
|
53
|
-
cancel("
|
|
55
|
+
cancel("Cancelled.");
|
|
54
56
|
process.exit(0);
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -71,16 +73,17 @@ await tasks([
|
|
|
71
73
|
{
|
|
72
74
|
title: "Installing dependencies with Bun",
|
|
73
75
|
async task() {
|
|
74
|
-
const proc = Bun.spawn(["bun", "install"], {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const proc = Bun.spawn(["bun", "install"], { cwd: targetDir });
|
|
77
|
+
const exitCode = await proc.exited;
|
|
78
|
+
if (exitCode !== 0) {
|
|
79
|
+
throw new Error(`bun install failed with exit code ${exitCode}`);
|
|
80
|
+
}
|
|
78
81
|
return "Installed via bun";
|
|
79
82
|
},
|
|
80
83
|
},
|
|
81
84
|
]);
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
note(`cd ${slug}\nbun dev`, "Next steps");
|
|
84
87
|
|
|
85
88
|
async function copyDir(
|
|
86
89
|
sourceDir: string,
|
|
@@ -92,7 +95,8 @@ async function copyDir(
|
|
|
92
95
|
|
|
93
96
|
for (const entry of entries) {
|
|
94
97
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
95
|
-
const
|
|
98
|
+
const outputName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
99
|
+
const destPath = path.join(destDir, outputName);
|
|
96
100
|
|
|
97
101
|
if (entry.isDirectory()) {
|
|
98
102
|
if (entry.name === "node_modules" || entry.name === "dist") continue;
|