js-game-zero 0.0.0-stage → 0.4.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/LICENSE +22 -0
- package/README.md +138 -2
- package/dist/active-service.d.ts +21 -0
- package/dist/actor.d.ts +182 -0
- package/dist/animation.d.ts +61 -0
- package/dist/assets.d.ts +148 -0
- package/dist/audio.d.ts +150 -0
- package/dist/browser-asset-backends.d.ts +8 -0
- package/dist/browser.d.ts +10 -0
- package/dist/chunk-q9m20c2h.js +7972 -0
- package/dist/clock.d.ts +29 -0
- package/dist/color.d.ts +15 -0
- package/dist/events.d.ts +145 -0
- package/dist/friendly-names.d.ts +43 -0
- package/dist/geometry.d.ts +125 -0
- package/dist/helpers.d.ts +28 -0
- package/dist/host.d.ts +58 -0
- package/dist/host.js +435 -0
- package/dist/index.d.ts +340 -0
- package/dist/index.js +258 -0
- package/dist/input.d.ts +200 -0
- package/dist/inspection.d.ts +58 -0
- package/dist/jgz.js +8545 -0
- package/dist/node.d.ts +14 -0
- package/dist/node.js +145 -0
- package/dist/pixel-density.d.ts +23 -0
- package/dist/random.d.ts +31 -0
- package/dist/reference/actor.md +197 -0
- package/dist/reference/audio.md +128 -0
- package/dist/reference/clock.md +150 -0
- package/dist/reference/helpers.md +46 -0
- package/dist/reference/hooks.md +379 -0
- package/dist/reference/index.d.ts +2 -0
- package/dist/reference/index.js +39 -0
- package/dist/reference/loaders.md +164 -0
- package/dist/reference/rect.md +129 -0
- package/dist/reference/runtime.md +478 -0
- package/dist/reference/screen.md +392 -0
- package/dist/reference/storage.md +90 -0
- package/dist/rendering.d.ts +216 -0
- package/dist/runtime-contracts.d.ts +117 -0
- package/dist/runtime-diagnostics.d.ts +37 -0
- package/dist/runtime-persistence.d.ts +32 -0
- package/dist/runtime-source.d.ts +82 -0
- package/dist/runtime.d.ts +81 -0
- package/dist/source-scan.d.ts +49 -0
- package/dist/storage-path.d.ts +3 -0
- package/dist/storage.d.ts +88 -0
- package/dist/tone.d.ts +81 -0
- package/dist/touch-controls.d.ts +24 -0
- package/dist/touch-pointer.d.ts +11 -0
- package/dist/util.d.ts +62 -0
- package/package.json +69 -4
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type StorageAdapter } from "./storage.js";
|
|
2
|
+
export type SandboxFileStorageAdapterOptions = {
|
|
3
|
+
readonly projectRoot: string;
|
|
4
|
+
readonly path?: string;
|
|
5
|
+
readonly readText?: (path: string) => Promise<string | null> | string | null;
|
|
6
|
+
readonly writeText?: (path: string, text: string) => Promise<void> | void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Creates a Node.js filesystem-backed adapter for a sandbox project.
|
|
10
|
+
*
|
|
11
|
+
* This lives behind the `js-game-zero/node` entry point so browser
|
|
12
|
+
* consumers never load or bundle Node built-ins.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createSandboxFileStorageAdapter(options: SandboxFileStorageAdapterOptions): StorageAdapter;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* js-game-zero 0.4.0
|
|
3
|
+
* https://github.com/Leaf-Learning/js-game-zero
|
|
4
|
+
*
|
|
5
|
+
* MIT License
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2026 Taha Rafiq
|
|
8
|
+
* Copyright (c) 2001 Robert Penner (easing equations)
|
|
9
|
+
*
|
|
10
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
* in the Software without restriction, including without limitation the rights
|
|
13
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
* furnished to do so, subject to the following conditions:
|
|
16
|
+
*
|
|
17
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
* copies or substantial portions of the Software.
|
|
19
|
+
*
|
|
20
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
* SOFTWARE.
|
|
27
|
+
*/
|
|
28
|
+
import { createRequire } from "node:module";
|
|
29
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
// packages/js-game-zero/src/storage-path.ts
|
|
32
|
+
function normalizeStoragePath(path) {
|
|
33
|
+
if (path.length === 0 || path.includes("\\") || path.startsWith("/")) {
|
|
34
|
+
throw new TypeError("storage path must be a relative forward-slash path.");
|
|
35
|
+
}
|
|
36
|
+
const segments = path.split("/");
|
|
37
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
38
|
+
throw new TypeError("storage path cannot include empty or traversal segments.");
|
|
39
|
+
}
|
|
40
|
+
return segments.join("/");
|
|
41
|
+
}
|
|
42
|
+
function normalizeSandboxStoragePath(path) {
|
|
43
|
+
if (/^(\/|[a-z]:[\\/])/i.test(path)) {
|
|
44
|
+
throw new TypeError("sandbox storage path must be relative.");
|
|
45
|
+
}
|
|
46
|
+
return normalizeStoragePath(path);
|
|
47
|
+
}
|
|
48
|
+
function sandboxStoragePath(projectRoot, safePath) {
|
|
49
|
+
if (!projectRoot || projectRoot.includes("\x00")) {
|
|
50
|
+
throw new TypeError("projectRoot must be a non-empty filesystem path.");
|
|
51
|
+
}
|
|
52
|
+
return `${projectRoot.replace(/[\\/]+$/, "")}/${safePath}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// packages/js-game-zero/src/node.ts
|
|
56
|
+
function createSandboxFileStorageAdapter(options) {
|
|
57
|
+
const safePath = normalizeSandboxStoragePath(options.path ?? "storage.json");
|
|
58
|
+
const path = sandboxStoragePath(options.projectRoot, safePath);
|
|
59
|
+
return {
|
|
60
|
+
path,
|
|
61
|
+
async load() {
|
|
62
|
+
if (options.readText)
|
|
63
|
+
return options.readText(path);
|
|
64
|
+
return readSandboxText(options.projectRoot, safePath);
|
|
65
|
+
},
|
|
66
|
+
async save(text) {
|
|
67
|
+
if (options.writeText) {
|
|
68
|
+
await options.writeText(path, text);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await writeSandboxText(options.projectRoot, safePath, text);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function readSandboxText(projectRoot, safePath) {
|
|
76
|
+
const resolved = await resolveSandboxFilePath(projectRoot, safePath, false);
|
|
77
|
+
if (!resolved)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
return await resolved.fs.readFile(resolved.targetPath, "utf8");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (isMissingFileError(error))
|
|
83
|
+
return null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function writeSandboxText(projectRoot, safePath, text) {
|
|
88
|
+
const resolved = await resolveSandboxFilePath(projectRoot, safePath, true);
|
|
89
|
+
if (!resolved)
|
|
90
|
+
return;
|
|
91
|
+
const tempPath = resolved.path.join(resolved.parentPath, `.jgz-storage-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
|
|
92
|
+
try {
|
|
93
|
+
await resolved.fs.writeFile(tempPath, text, "utf8");
|
|
94
|
+
await resolved.fs.rename(tempPath, resolved.targetPath);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
await resolved.fs.rm(tempPath, { force: true });
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function resolveSandboxFilePath(projectRoot, safePath, createParent) {
|
|
101
|
+
const fs = await import("node:fs/promises");
|
|
102
|
+
const path = await import("node:path");
|
|
103
|
+
const rootPath = path.resolve(projectRoot);
|
|
104
|
+
const realRoot = await fs.realpath(rootPath);
|
|
105
|
+
const lexicalTarget = path.join(realRoot, safePath);
|
|
106
|
+
const lexicalParent = path.dirname(lexicalTarget);
|
|
107
|
+
if (createParent) {
|
|
108
|
+
await fs.mkdir(lexicalParent, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
let parentPath;
|
|
111
|
+
try {
|
|
112
|
+
parentPath = await fs.realpath(lexicalParent);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (!createParent && isMissingFileError(error))
|
|
115
|
+
return null;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
if (!isPathInside(parentPath, realRoot, path)) {
|
|
119
|
+
throw new TypeError("sandbox storage path must stay inside project root.");
|
|
120
|
+
}
|
|
121
|
+
const targetPath = path.join(parentPath, path.basename(lexicalTarget));
|
|
122
|
+
if (!isPathInside(targetPath, realRoot, path)) {
|
|
123
|
+
throw new TypeError("sandbox storage path must stay inside project root.");
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const stat = await fs.lstat(targetPath);
|
|
127
|
+
if (stat.isSymbolicLink()) {
|
|
128
|
+
throw new TypeError("sandbox storage path must not be a symbolic link.");
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!isMissingFileError(error))
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
return { fs, path, parentPath, targetPath };
|
|
135
|
+
}
|
|
136
|
+
function isPathInside(targetPath, rootPath, path) {
|
|
137
|
+
const relative = path.relative(rootPath, targetPath);
|
|
138
|
+
return relative === "" || relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
139
|
+
}
|
|
140
|
+
function isMissingFileError(error) {
|
|
141
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
createSandboxFileStorageAdapter
|
|
145
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RuntimeDiagnostic } from "./runtime-contracts.js";
|
|
2
|
+
/** The highest density the device's pixel ratio picks by itself. */
|
|
3
|
+
export declare const MAX_AUTO_PIXEL_DENSITY = 3;
|
|
4
|
+
/**
|
|
5
|
+
* The most pixels a canvas may hold. iOS Safari leaves a bigger canvas
|
|
6
|
+
* blank, so density comes down until the canvas fits.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_CANVAS_PIXELS: number;
|
|
9
|
+
type DensityCanvas = {
|
|
10
|
+
readonly ownerDocument?: {
|
|
11
|
+
readonly defaultView?: Window | null;
|
|
12
|
+
} | null;
|
|
13
|
+
};
|
|
14
|
+
/** The device's pixel ratio where the canvas is shown, or 1 outside a browser. */
|
|
15
|
+
export declare function devicePixelRatioOf(canvas: DensityCanvas): number;
|
|
16
|
+
/**
|
|
17
|
+
* Canvas pixels per logical pixel. With no `PIXEL_DENSITY`, it's the
|
|
18
|
+
* device's pixel ratio, between 1 and `MAX_AUTO_PIXEL_DENSITY`. Any density
|
|
19
|
+
* comes down so the canvas stays within `MAX_CANVAS_PIXELS`, but never below
|
|
20
|
+
* 1 unless the game asked for less.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolvePixelDensity(value: unknown, width: number, height: number, deviceRatio: number, emit: (diagnostic: RuntimeDiagnostic) => void): number;
|
|
23
|
+
export {};
|
package/dist/random.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeatable randomness for a run that asks for it (`run(source, { seed })`),
|
|
3
|
+
* and the random source the helpers read while a run is active. The CLI's
|
|
4
|
+
* `--seed` passes its seed to the same `run()` option, so one seed gives the
|
|
5
|
+
* same sequence in the CLI and in a host page.
|
|
6
|
+
*/
|
|
7
|
+
export type RandomSource = () => number;
|
|
8
|
+
/** A seed a host can pass: a string, or a number read as its digits. */
|
|
9
|
+
export type RandomSeed = string | number;
|
|
10
|
+
/**
|
|
11
|
+
* The 32-bit state for a seed. Up to nine plain digits are used as the number
|
|
12
|
+
* they spell, so a seed chosen for one harness reproduces the same sequence in
|
|
13
|
+
* another; anything else is hashed (FNV-1a). A number is read as `String(n)`,
|
|
14
|
+
* so `42` and `"42"` are the same seed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hashSeed(seed: RandomSeed): number;
|
|
17
|
+
/**
|
|
18
|
+
* A repeatable stand-in for `Math.random`: numbers from 0 up to 1, the same
|
|
19
|
+
* sequence for the same seed (Mulberry32 over `hashSeed(seed)`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function createSeededRandom(seed: RandomSeed): RandomSource;
|
|
22
|
+
/**
|
|
23
|
+
* A `Math` for one game's scope whose `random` is that game's source. Every
|
|
24
|
+
* other member is the real `Math`'s, so `Math.floor` and `Math.PI` read as
|
|
25
|
+
* usual, and the page's own `Math.random` is untouched.
|
|
26
|
+
*/
|
|
27
|
+
export declare function mathWithRandom(random: RandomSource): Math;
|
|
28
|
+
/** Installs the running game's random source; returns the previous one. */
|
|
29
|
+
export declare function setActiveRandom(random: RandomSource | null): RandomSource | null;
|
|
30
|
+
/** The active run's random source, or `Math.random` outside a run. */
|
|
31
|
+
export declare function random(): number;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Actor
|
|
2
|
+
|
|
3
|
+
`Actor` bundles an image, a position, and a hitbox rectangle into one object
|
|
4
|
+
you can move, rotate, fade, and collide-test. It's the class you'll use for
|
|
5
|
+
players, enemies, bullets — anything that moves around the screen.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
new Actor(image: string, pos?: [number, number] | ActorOptions, options?: ActorOptions): Actor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
let player = new Actor("player.svg", { center: [160, 90] });
|
|
13
|
+
|
|
14
|
+
function draw() {
|
|
15
|
+
screen.fill("#111827");
|
|
16
|
+
player.draw();
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`image` is an asset name resolved under `images/` (see
|
|
21
|
+
[`docs/reference/loaders.md`](loaders.md)). In `jgz`, the sandbox, and a page
|
|
22
|
+
that decodes its images first with
|
|
23
|
+
[`loadImages`](loaders.md#assets-without-a-server), the image is ready
|
|
24
|
+
before your code runs, so `player.width` and an edge position like
|
|
25
|
+
`midBottom` are right from the first line. Anywhere else, loading starts
|
|
26
|
+
when the Actor is made: until it finishes, the Actor has no size and `draw()`
|
|
27
|
+
is a no-op, and the runtime holds the first frame until images touched while
|
|
28
|
+
your script starts up have loaded. `player.ready()` returns a promise for
|
|
29
|
+
the image; the top level of a zero-boilerplate game can't `await`, so use
|
|
30
|
+
`player.ready().then(() => …)` there.
|
|
31
|
+
|
|
32
|
+
## Positioning
|
|
33
|
+
|
|
34
|
+
Give **at most one** position source — either the second constructor
|
|
35
|
+
argument as a `[x, y]` point, or exactly one of these option keys:
|
|
36
|
+
`pos`, `topLeft`, `topRight`, `bottomLeft`, `bottomRight`, `midTop`,
|
|
37
|
+
`midLeft`, `midRight`, `midBottom`, `center`. Passing more than one throws a
|
|
38
|
+
`TypeError`. With no position at all, the actor starts at `topLeft: [0, 0]`.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
new Actor("player.svg", [100, 50]); // shorthand for { pos: [100, 50] }
|
|
42
|
+
new Actor("player.svg", { midBottom: [160, 180] }); // stands on the floor at y=180
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`anchor` controls which point of the image maps to the position — it
|
|
46
|
+
defaults to `["center", "center"]`, so by default `player.x`/`player.y` (and
|
|
47
|
+
`pos`) refer to the image's center, matching pgzero. Each axis accepts
|
|
48
|
+
`"left"`/`"center"`/`"right"` (horizontal) or `"top"`/`"center"`/`"bottom"`
|
|
49
|
+
(vertical), or a literal pixel offset number.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
new Actor("player.svg", { topLeft: [0, 0], anchor: ["left", "top"] });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Properties
|
|
56
|
+
|
|
57
|
+
| Property | Type | Notes |
|
|
58
|
+
| ---------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `image` | `string` | Reassigning starts loading the new image. |
|
|
60
|
+
| `pos`, `x`, `y` | point / number | The anchor point's position. |
|
|
61
|
+
| `angle` | `number` | Degrees, counterclockwise, around the anchor. |
|
|
62
|
+
| `opacity` | `number` | `0`–`1`; out-of-range values are clamped, not rejected. |
|
|
63
|
+
| `anchor` | `[h, v]` | See above. |
|
|
64
|
+
| `scale` | `number` | Drawn size as a multiple of the image's native size; default `1`. |
|
|
65
|
+
| `width`, `height` | `number` | The current (possibly rotated) bounding box size. Setting one resizes the sprite to that size, keeping its proportions. |
|
|
66
|
+
| `topLeft`, `topRight`, `bottomLeft`, `bottomRight`, `midTop`, `midLeft`, `midRight`, `midBottom`, `center` | point | Read or move the actor by any edge/corner of its bounding box. |
|
|
67
|
+
| `left`, `top`, `right`, `bottom` | `number` | Same idea, one axis at a time. |
|
|
68
|
+
|
|
69
|
+
Setting `angle` rotates the sprite around its anchor and changes the
|
|
70
|
+
axis-aligned bounding box returned by `rect()`/`width`/`height` — useful when
|
|
71
|
+
you collide-test a rotated actor, since the hitbox grows to cover the
|
|
72
|
+
rotated silhouette. The edge and corner properties read and move that same
|
|
73
|
+
box, so `ship.left = 0` puts a rotated ship's leftmost point at the left of
|
|
74
|
+
the screen.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
function update(dt) {
|
|
78
|
+
if (keyboard.right) player.x += 120 * dt;
|
|
79
|
+
player.angle += 90 * dt; // spin
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Sizing
|
|
84
|
+
|
|
85
|
+
Sprites draw at their native pixel size by default. To fit a picture to
|
|
86
|
+
your game, give it a `scale`, or a `width` or `height` (at most one of the
|
|
87
|
+
three; the other axis follows so the picture keeps its proportions):
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
let cow = new Actor("cow.png", { midBottom: [240, 620], width: 80 }); // 80px wide
|
|
91
|
+
let car = new Actor("car.png", { scale: 3 }); // three times its native size
|
|
92
|
+
cow.height = 60; // resize later; the midBottom stays on the floor
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The hitbox, `rect()`, and every edge/corner property follow the drawn size,
|
|
96
|
+
and the position you gave (a `midBottom` on the floor, a `center`) stays
|
|
97
|
+
put when the size changes. A size set before the image has loaded is
|
|
98
|
+
applied as soon as it arrives. `inflateIp` still grows only the hitbox,
|
|
99
|
+
never the picture.
|
|
100
|
+
|
|
101
|
+
## Methods
|
|
102
|
+
|
|
103
|
+
- `rect(): Rect` — the actor's current bounding rectangle (see
|
|
104
|
+
[`docs/reference/rect.md`](rect.md)); useful for custom drawing or manual
|
|
105
|
+
collision math.
|
|
106
|
+
- `draw(screen?)` — draws the actor at its current position, angle, and
|
|
107
|
+
opacity. Normally called with no arguments inside `draw()`.
|
|
108
|
+
- `ready(): Promise<SurfaceLike>` — resolves once the current image has
|
|
109
|
+
loaded.
|
|
110
|
+
- `unloadImage()` — releases the cached image (advanced; rarely needed).
|
|
111
|
+
- `angleTo(target)` / `distanceTo(target)` — `target` is another `Actor`
|
|
112
|
+
or a `[x, y]` point. `angleTo` returns degrees, `distanceTo` returns
|
|
113
|
+
pixels.
|
|
114
|
+
- `move(x, y)` / `moveIp(x, y)` — like `Rect.move`/`moveIp`; `move` returns
|
|
115
|
+
a new `Rect`, `moveIp` moves the actor itself.
|
|
116
|
+
- `inflate(x, y)` / `inflateIp(x, y)` — grow/shrink the hitbox around its
|
|
117
|
+
center; the picture keeps its size and place.
|
|
118
|
+
- `touches(target)` — `true` when the actor overlaps `target`, which can be
|
|
119
|
+
another `Actor`, a `Rect`, a `[left, top, width, height]` tuple, or an
|
|
120
|
+
`[x, y]` point such as a click position.
|
|
121
|
+
- `collidePoint(point)`, `collideRect(other)`, `collideList(others)`,
|
|
122
|
+
`collideListAll(others)`, `contains(other)` — Pygame Zero's collision
|
|
123
|
+
tests, delegating to the actor's `rect()`. `other` can be a `Rect`, a
|
|
124
|
+
plain `[left, top, width, height]` tuple, or another `Actor`.
|
|
125
|
+
- `clampIp(area)` — moves the actor (without resizing it) so its bounding
|
|
126
|
+
box fits inside `area`. `player.clampIp(screen.bounds())` keeps the
|
|
127
|
+
player on screen.
|
|
128
|
+
- `clamp(other)`, `clip(other)`, `union(other)`, `unionAll(others)`,
|
|
129
|
+
`fit(other)` — geometry helpers, same semantics as the matching `Rect`
|
|
130
|
+
methods, again accepting `Actor`s directly. They return a new `Rect` and
|
|
131
|
+
leave the actor where it is.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
function update(dt) {
|
|
135
|
+
if (keyboard.right) player.x += 120 * dt;
|
|
136
|
+
player.clampIp(screen.bounds()); // stop at the edges of the screen
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function onMouseDown(pos) {
|
|
140
|
+
if (player.touches(pos)) console.log("clicked the player");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (player.touches(enemy)) loseLife();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Animating an Actor
|
|
147
|
+
|
|
148
|
+
`Actor` is a plain object as far as `animate()` is concerned — any numeric
|
|
149
|
+
property (`x`, `y`, `angle`, `opacity`, ...) can be animated:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
animate(player, { x: 300, duration: 0.6, tween: "bounceEnd" });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
See [`docs/reference/clock.md`](clock.md#animation) for the full tween list.
|
|
156
|
+
|
|
157
|
+
## Flip-book animation: `images` and `fps`
|
|
158
|
+
|
|
159
|
+
Give an actor a list of pictures and it flips through them by itself, like a
|
|
160
|
+
flip book — for walking legs, spinning coins, or explosions:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
let coin = new Actor("coin_0", {
|
|
164
|
+
center: [200, 120],
|
|
165
|
+
images: ["coin_0", "coin_1", "coin_2", "coin_3"],
|
|
166
|
+
fps: 8, // pictures per second
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function draw() {
|
|
170
|
+
coin.draw();
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| Property | Meaning |
|
|
175
|
+
| ---------- | ------------------------------------------------------------------------------------- |
|
|
176
|
+
| `images` | The pictures to show, in order. Setting it starts again from the first; `null` stops. |
|
|
177
|
+
| `fps` | Pictures per second (default `10`). Changing it keeps the current picture. |
|
|
178
|
+
| `loop` | Start over after the last picture (default `true`). `false` stops on the last. |
|
|
179
|
+
| `frame` | Which picture is showing: `0` for the first. |
|
|
180
|
+
| `finished` | `true` once a `loop: false` animation has shown its last picture. |
|
|
181
|
+
|
|
182
|
+
The pictures change on the game's own clock, so they pause when the game
|
|
183
|
+
does and keep the same speed at any frame rate. All of them start loading
|
|
184
|
+
as soon as you set `images`. For a one-shot effect, remove the actor once
|
|
185
|
+
it's `finished`:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
explosions.push(
|
|
189
|
+
new Actor("boom_0", {
|
|
190
|
+
center: pos,
|
|
191
|
+
images: ["boom_0", "boom_1", "boom_2"],
|
|
192
|
+
fps: 16,
|
|
193
|
+
loop: false,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
explosions = explosions.filter((explosion) => !explosion.finished);
|
|
197
|
+
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Sounds, Music, And Tone
|
|
2
|
+
|
|
3
|
+
Browsers block audio from playing until the page has seen a user gesture
|
|
4
|
+
(a click or key press). Every audio API below works fine technically before
|
|
5
|
+
that gesture, but nothing will actually be _audible_ until the player has
|
|
6
|
+
interacted with the page at least once — trigger your first sound from an
|
|
7
|
+
`onMouseDown` or `onKeyDown` handler, not from top-level code or the
|
|
8
|
+
first `draw()` frame.
|
|
9
|
+
|
|
10
|
+
## Sound effects: `sounds`
|
|
11
|
+
|
|
12
|
+
`sounds.<name>` resolves an asset under `sounds/` (see
|
|
13
|
+
[`docs/reference/loaders.md`](loaders.md)) the first time you touch it.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
function onMouseDown() {
|
|
17
|
+
sounds.tap.play();
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### `.play(loops = 0)`
|
|
22
|
+
|
|
23
|
+
Plays the sound. `loops` follows pygame's convention: it counts the
|
|
24
|
+
_extra_ plays after the first. `0` (the default) plays it once, `2` plays it
|
|
25
|
+
three times in total, and `-1` loops forever until `.stop()` is called.
|
|
26
|
+
|
|
27
|
+
Calling `.play()` again while a previous playback is still going **does not
|
|
28
|
+
cut it off** — sounds overlap, just like pygame's channel mixing, so rapid
|
|
29
|
+
repeated triggers (e.g. a machine-gun sound effect) layer instead of
|
|
30
|
+
stuttering.
|
|
31
|
+
|
|
32
|
+
### `.loop()`
|
|
33
|
+
|
|
34
|
+
Plays the sound over and over until `.stop()` — the same as `.play(-1)`.
|
|
35
|
+
Handy for engine hums and background ambience:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
sounds.engine.loop();
|
|
39
|
+
// later
|
|
40
|
+
sounds.engine.stop();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `.stop()`
|
|
44
|
+
|
|
45
|
+
Stops every currently-overlapping playback of this sound.
|
|
46
|
+
|
|
47
|
+
### `.getLength()`
|
|
48
|
+
|
|
49
|
+
Duration in seconds (`0` if not yet known).
|
|
50
|
+
|
|
51
|
+
### `.setVolume(volume)` / `.getVolume()`
|
|
52
|
+
|
|
53
|
+
Per-sound volume, `0`–`1`.
|
|
54
|
+
|
|
55
|
+
## Music: `music`
|
|
56
|
+
|
|
57
|
+
Unlike `sounds`, `music` plays **one track at a time** — starting a new
|
|
58
|
+
track always stops whatever was playing.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
music.play("theme.ogg"); // loops forever
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Method | Behavior |
|
|
65
|
+
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `music.play(name)` | Stops the current track, plays `name` on a loop. |
|
|
67
|
+
| `music.playOnce(name)` | Stops the current track, plays `name` once. |
|
|
68
|
+
| `music.queue(name)` | Appends `name` to the queue; starts immediately (as a one-shot) if nothing is currently playing. Queued tracks play in order, each once. |
|
|
69
|
+
| `music.stop()` | Stops playback and clears the queue. |
|
|
70
|
+
| `music.pause()` / `music.unpause()` | Pause and resume without losing position. |
|
|
71
|
+
| `music.rewind()` | Restarts the current track from the beginning without stopping it. |
|
|
72
|
+
| `music.fadeOut(seconds)` | Fades the volume to zero over `seconds`, then stops. `0` stops immediately. |
|
|
73
|
+
| `music.setVolume(volume)` / `music.getVolume()` | `0`–`1`. |
|
|
74
|
+
| `music.position` | Seconds into the current track (`0` if nothing is loaded). Set it to jump: `music.position = 30`. |
|
|
75
|
+
| `music.getPos()` | **Milliseconds** the current track has been playing, or `-1` if nothing is loaded (Pygame's unit). |
|
|
76
|
+
| `music.setPos(seconds)` | Seeks the current track to a position, in **seconds**. To save and restore a spot, use `music.position` for both. |
|
|
77
|
+
| `music.isPlaying()` | `true` if a track is currently playing and not paused. |
|
|
78
|
+
|
|
79
|
+
`onMusicEnd()` (or a callback given to `music.setOnMusicEnd(callback)`)
|
|
80
|
+
fires once the queue is exhausted and nothing is left to play.
|
|
81
|
+
|
|
82
|
+
## Generated tones: `tone`
|
|
83
|
+
|
|
84
|
+
`tone` synthesizes a short beep with the Web Audio API — no sound asset file
|
|
85
|
+
needed, useful for quick feedback sounds (menu blips, jumps, coin pickups).
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
function onMouseDown() {
|
|
89
|
+
tone.play("A4", 0.2);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `tone.play(noteOrFrequency, duration = 1, options?)`
|
|
94
|
+
|
|
95
|
+
- `noteOrFrequency` — either a note name like `"A4"`, `"C#5"`, or `"Bb3"`
|
|
96
|
+
(letter, optional `#`/`b`, octave number, using standard scientific pitch
|
|
97
|
+
notation with A4 = 440Hz), or a positive number of Hz.
|
|
98
|
+
- `duration` — seconds, **must be greater than `0` and at most `4`**; longer
|
|
99
|
+
values throw a `RangeError`. Generated tones are meant for short blips,
|
|
100
|
+
not background music — use `music` for that.
|
|
101
|
+
- `options.waveform` — `"sine"` (default), `"square"`, `"sawtooth"`,
|
|
102
|
+
`"triangle"`, or the pgzero-style short aliases `"sin"` (→ `"sine"`) and
|
|
103
|
+
`"saw"` (→ `"sawtooth"`).
|
|
104
|
+
- `options.volume` — `0`–`1`, defaults to `0.8` (matching pgzero's `tone`
|
|
105
|
+
module default, which is louder than most other defaults in this API).
|
|
106
|
+
|
|
107
|
+
Returns a `Promise` that resolves once the tone has been scheduled.
|
|
108
|
+
|
|
109
|
+
### `tone.create(noteOrFrequency, duration = 1, options?)`
|
|
110
|
+
|
|
111
|
+
Builds a reusable tone spec without playing it yet — call `.play(options?)`
|
|
112
|
+
on the result whenever you want to trigger it, optionally overriding
|
|
113
|
+
`waveform`/`volume` for that one playback.
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
const jumpTone = tone.create("E5", 0.12, { waveform: "square" });
|
|
117
|
+
|
|
118
|
+
function onKeyDown(key) {
|
|
119
|
+
if (key === keys.SPACE) jumpTone.play();
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `tone.isUnlocked()`
|
|
124
|
+
|
|
125
|
+
Returns whether the browser has granted audio playback yet — it flips to
|
|
126
|
+
`true` the first time a sound or tone actually plays after a user gesture.
|
|
127
|
+
This unlock state is shared across `sounds` and `tone`, so a single tap
|
|
128
|
+
un-gates both.
|