bevy-ts 0.1.0-alpha.1 → 0.1.0-alpha.10

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/bin/bevy-ts.js CHANGED
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // Runs the precompiled engine binary from the platform package npm installed alongside this one
3
3
  // (as esbuild and Biome ship), so `npx bevy-ts dev src/game.ts` needs no Rust toolchain.
4
- // BEVY_TS_BINARY overrides the binary, for engine development.
4
+ // BEVY_TS_BINARY overrides the binary, for engine development. `new` and `examples` run here, in
5
+ // Node, with no engine.
5
6
  import { spawn } from "node:child_process";
7
+ import { create, examples } from "./examples.js";
6
8
  import { binary } from "./resolve.js";
7
9
 
10
+ // exitCode, not exit(): stdout to a pipe is written asynchronously and must drain first.
11
+ const [command, ...rest] = process.argv.slice(2);
12
+ if (command === "new") process.exitCode = create(rest);
13
+ else if (command === "examples") process.exitCode = examples(rest);
14
+ else runEngine();
15
+
8
16
  function binaryOrExit() {
9
17
  try {
10
18
  return binary();
@@ -14,11 +22,13 @@ function binaryOrExit() {
14
22
  }
15
23
  }
16
24
 
17
- // Forward signals so stopping the launcher stops the engine too.
18
- const child = spawn(binaryOrExit(), process.argv.slice(2), { stdio: "inherit" });
19
- for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(sig, () => child.kill(sig));
20
- child.on("error", (e) => {
21
- console.error(`bevy-ts: ${e.message}`);
22
- process.exit(1);
23
- });
24
- child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
25
+ function runEngine() {
26
+ // Forward signals so stopping the launcher stops the engine too.
27
+ const child = spawn(binaryOrExit(), process.argv.slice(2), { stdio: "inherit" });
28
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(sig, () => child.kill(sig));
29
+ child.on("error", (e) => {
30
+ console.error(`bevy-ts: ${e.message}`);
31
+ process.exit(1);
32
+ });
33
+ child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
34
+ }
@@ -0,0 +1,141 @@
1
+ // `bevy-ts examples` and `bevy-ts new`: the launcher's own commands. They read the examples this
2
+ // package ships (copied in at pack time by scripts/sync-examples.mjs) and never start the engine.
3
+ import { spawnSync } from "node:child_process";
4
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
5
+ import { basename, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const root = fileURLToPath(new URL("../examples/", import.meta.url));
9
+ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
10
+ // What `new` leaves out of the copy: the metadata, not the game.
11
+ const META = ["example.json", "thumbnail.png"];
12
+
13
+ /** The shipped examples, in the order to show them: `cube` first, then by id. */
14
+ export function list() {
15
+ if (!existsSync(root)) return [];
16
+ const out = [];
17
+ for (const name of readdirSync(root)) {
18
+ const file = `${root}${name}/example.json`;
19
+ if (!existsSync(file)) continue;
20
+ const { id, title, description, entry } = JSON.parse(readFileSync(file, "utf8"));
21
+ const e = { id, title, description, entry };
22
+ if (existsSync(`${root}${name}/thumbnail.png`)) e.thumbnail = `${root}${name}/thumbnail.png`;
23
+ e.dir = `${root}${name}`;
24
+ out.push(e);
25
+ }
26
+ return out.sort((a, b) => (a.id === "cube" ? -1 : b.id === "cube" ? 1 : a.id < b.id ? -1 : 1));
27
+ }
28
+
29
+ function refuse(msg, code = 1) {
30
+ console.error(`bevy-ts: ${msg}`);
31
+ return code;
32
+ }
33
+
34
+ export function examples(args) {
35
+ const all = list().map(({ dir, ...e }) => e);
36
+ if (args.includes("--json")) {
37
+ process.stdout.write(`${JSON.stringify({ version, examples: all })}\n`);
38
+ } else {
39
+ for (const e of all) console.log(`${e.id.padEnd(10)} ${e.title}: ${e.description}`);
40
+ }
41
+ return 0;
42
+ }
43
+
44
+ const NEW_USAGE = "usage: bevy-ts new <dir> [--example <id>] [--no-install] [--json]";
45
+
46
+ export function create(args) {
47
+ let dir;
48
+ let id = "cube";
49
+ let install = true;
50
+ let json = false;
51
+ for (let i = 0; i < args.length; i++) {
52
+ const a = args[i];
53
+ if (a === "--example") {
54
+ id = args[++i];
55
+ if (!id || id.startsWith("--")) return refuse(`--example takes an id\n${NEW_USAGE}`, 2);
56
+ } else if (a === "--no-install") install = false;
57
+ else if (a === "--json") json = true;
58
+ else if (a.startsWith("--")) return refuse(`unknown option ${a}\n${NEW_USAGE}`, 2);
59
+ else if (dir === undefined) dir = a;
60
+ else return refuse(`unexpected argument ${a}\n${NEW_USAGE}`, 2);
61
+ }
62
+ if (dir === undefined) return refuse(`missing <dir>\n${NEW_USAGE}`, 2);
63
+
64
+ // Everything is checked before the first write, so a refusal writes nothing.
65
+ const all = list();
66
+ const example = all.find((e) => e.id === id);
67
+ if (!example) return refuse(`no example "${id}"; there are: ${all.map((e) => e.id).join(", ")}`);
68
+ const target = resolve(dir);
69
+ if (existsSync(target)) {
70
+ if (!statSync(target).isDirectory()) return refuse(`${target} is a file`);
71
+ if (readdirSync(target).length) return refuse(`${target} is not empty; bevy-ts new makes a project only in an empty or missing directory`);
72
+ }
73
+
74
+ try {
75
+ mkdirSync(target, { recursive: true });
76
+ for (const f of readdirSync(example.dir)) {
77
+ if (!META.includes(f)) cpSync(`${example.dir}/${f}`, `${target}/${f}`, { recursive: true });
78
+ }
79
+ const name = basename(target).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "") || "game";
80
+ const pkg = {
81
+ name,
82
+ private: true,
83
+ type: "module",
84
+ scripts: { dev: `bevy-ts dev ${example.entry}` },
85
+ devDependencies: { "bevy-ts": version },
86
+ };
87
+ // The examples' compiler options, resolving bevy-ts from node_modules instead of the checkout.
88
+ const tsconfig = {
89
+ compilerOptions: {
90
+ target: "es2023",
91
+ module: "esnext",
92
+ moduleResolution: "bundler",
93
+ strict: true,
94
+ noEmit: true,
95
+ verbatimModuleSyntax: true,
96
+ jsx: "react-jsx",
97
+ jsxImportSource: "bevy-ts",
98
+ allowImportingTsExtensions: true,
99
+ },
100
+ include: ["src", ".bevy-ts/types"],
101
+ };
102
+ writeFileSync(`${target}/package.json`, `${JSON.stringify(pkg, null, 2)}\n`);
103
+ writeFileSync(`${target}/tsconfig.json`, `${JSON.stringify(tsconfig, null, 2)}\n`);
104
+ writeFileSync(`${target}/.gitignore`, "node_modules/\n.bevy-ts/\n");
105
+ } catch (e) {
106
+ return refuse(e.message);
107
+ }
108
+
109
+ const pm = packageManager();
110
+ const installed = install ? runInstall(pm, target) : false;
111
+ if (json) {
112
+ process.stdout.write(`${JSON.stringify({ dir: target, entry: example.entry, example: id, installed })}\n`);
113
+ } else {
114
+ console.log(`Made ${target} from ${id}${install && !installed ? "; the install failed" : ""}.`);
115
+ const steps = [`cd ${shellQuote(dir)}`, ...(installed ? [] : [`${pm} install`]), `npx bevy-ts dev ${example.entry}`];
116
+ console.log(` ${steps.join(" && ")}`);
117
+ }
118
+ return install && !installed ? 1 : 0;
119
+ }
120
+
121
+ /** The package manager that ran bevy-ts (npm, pnpm, yarn or bun), npm otherwise. */
122
+ function packageManager() {
123
+ const agent = (process.env.npm_config_user_agent ?? "").split("/")[0];
124
+ return ["npm", "pnpm", "yarn", "bun"].includes(agent) ? agent : "npm";
125
+ }
126
+
127
+ /** The path as it pastes into a POSIX shell: single-quoted when it holds anything but safe characters. */
128
+ function shellQuote(p) {
129
+ return /^[\w@%+=:,./-]+$/.test(p) ? p : `'${p.replaceAll("'", "'\\''")}'`;
130
+ }
131
+
132
+ /** Installs with `pm` in `dir`, its output on stderr. */
133
+ function runInstall(pm, dir) {
134
+ console.error(`bevy-ts: ${pm} install in ${dir}`);
135
+ const r = spawnSync(pm, ["install"], { cwd: dir, stdio: ["ignore", 2, 2], shell: process.platform === "win32" });
136
+ if (r.error || r.status !== 0) {
137
+ console.error(`bevy-ts: ${pm} install failed${r.error ? `: ${r.error.message}` : ""}; the project is in place, run it again there`);
138
+ return false;
139
+ }
140
+ return true;
141
+ }
@@ -0,0 +1,18 @@
1
+ // The arena: where things are. What an enemy is lives in src/enemy.tsx.
2
+
3
+ #EnemyA
4
+ @enemy::Enemy
5
+ Transform { translation: Vec3 { x: -3.0, z: -4.0 } }
6
+ --
7
+ #EnemyB
8
+ @enemy::Enemy { @hp: 250.0 }
9
+ Transform { translation: Vec3 { x: 3.0, z: -4.0 } }
10
+ --
11
+ // Two copies of one room, each with its guard.
12
+ #RoomA
13
+ :"levels/room.bsn"
14
+ Transform { translation: Vec3 { x: -12.0, z: -12.0 } }
15
+ --
16
+ #RoomB
17
+ :"levels/room.bsn"
18
+ Transform { translation: Vec3 { x: 12.0, z: -12.0 } }
@@ -0,0 +1,8 @@
1
+ // A room, placed twice by arena.bsn: its guard is an enemy too.
2
+ #Room
3
+ Transform
4
+ Children [
5
+ #Guard
6
+ @enemy::Enemy { @hp: 50.0 }
7
+ Transform { translation: Vec3 { z: -16.0 } }
8
+ ]
@@ -0,0 +1,103 @@
1
+ {
2
+ "accessors": [
3
+ {
4
+ "bufferView": 0,
5
+ "componentType": 5126,
6
+ "count": 24,
7
+ "max": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "min": [
13
+ -0.5,
14
+ -0.5,
15
+ -0.5
16
+ ],
17
+ "type": "VEC3"
18
+ },
19
+ {
20
+ "bufferView": 1,
21
+ "componentType": 5126,
22
+ "count": 24,
23
+ "type": "VEC3"
24
+ },
25
+ {
26
+ "bufferView": 2,
27
+ "componentType": 5123,
28
+ "count": 36,
29
+ "type": "SCALAR"
30
+ }
31
+ ],
32
+ "asset": {
33
+ "generator": "bevy-ts-viewport tests/assets.rs",
34
+ "version": "2.0"
35
+ },
36
+ "bufferViews": [
37
+ {
38
+ "buffer": 0,
39
+ "byteLength": 288,
40
+ "byteOffset": 0
41
+ },
42
+ {
43
+ "buffer": 0,
44
+ "byteLength": 288,
45
+ "byteOffset": 288
46
+ },
47
+ {
48
+ "buffer": 0,
49
+ "byteLength": 72,
50
+ "byteOffset": 576
51
+ }
52
+ ],
53
+ "buffers": [
54
+ {
55
+ "byteLength": 648,
56
+ "uri": "data:application/octet-stream;base64,AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAPwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA"
57
+ }
58
+ ],
59
+ "materials": [
60
+ {
61
+ "name": "wood",
62
+ "pbrMetallicRoughness": {
63
+ "baseColorFactor": [
64
+ 0.55,
65
+ 0.36,
66
+ 0.18,
67
+ 1.0
68
+ ],
69
+ "metallicFactor": 0.0,
70
+ "roughnessFactor": 0.8
71
+ }
72
+ }
73
+ ],
74
+ "meshes": [
75
+ {
76
+ "name": "crate",
77
+ "primitives": [
78
+ {
79
+ "attributes": {
80
+ "NORMAL": 1,
81
+ "POSITION": 0
82
+ },
83
+ "indices": 2,
84
+ "material": 0
85
+ }
86
+ ]
87
+ }
88
+ ],
89
+ "nodes": [
90
+ {
91
+ "mesh": 0,
92
+ "name": "crate"
93
+ }
94
+ ],
95
+ "scene": 0,
96
+ "scenes": [
97
+ {
98
+ "nodes": [
99
+ 0
100
+ ]
101
+ }
102
+ ]
103
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "arena",
3
+ "title": "Arena",
4
+ "description": "A player and a HUD in TSX, enemies placed by a level file, and systems that bob, hurt and cull them.",
5
+ "entry": "src/game.tsx"
6
+ }
@@ -0,0 +1,28 @@
1
+ // What an enemy is: a scene component. Levels place it (assets/levels/arena.bsn writes
2
+ // `@enemy::Enemy { @hp: 250.0 }`), and TSX can mount it too (`<Enemy at={[0, 0, -6]} />`).
3
+ import { Component, SceneComponent, type f32, type Vec3Like } from "bevy-ts";
4
+ import { Transform } from "bevy-ts/bevy";
5
+
6
+ export class Health extends Component {
7
+ hp: f32 = 100;
8
+ max: f32 = 100;
9
+ }
10
+ /** Marks enemies, for the systems that move and cull them. */
11
+ export class Foe extends Component {}
12
+
13
+ export class Enemy extends SceneComponent {
14
+ static scene({ hp = 100, at = [0, 0, 0] }: { hp?: f32; at?: Vec3Like }) {
15
+ return (
16
+ <entity name="enemy">
17
+ <Transform translation={at} />
18
+ <Health initialHp={hp} max={hp} />
19
+ <Foe />
20
+ <mesh shape="cuboid" size={[1, 1, 1]} color="#cc3333" />
21
+ <entity key="eye">
22
+ <Transform translation={[0, 0.6, 0.5]} />
23
+ <mesh shape="sphere" radius={0.15} color="#ffffff" />
24
+ </entity>
25
+ </entity>
26
+ );
27
+ }
28
+ }
@@ -0,0 +1,57 @@
1
+ // The arena: a player, a HUD and the sun in TSX, the enemies placed by assets/levels/arena.bsn, a camera, and
2
+ // three systems that play. `bevy-ts dev src/game.tsx`.
3
+ import { app, type Commands, type Entity, type Local, type Mut, type Query, ref, type Res, Startup, Time, Transform, type u32, Update, type With, type Without } from "bevy-ts";
4
+ import { DirectionalLight } from "bevy-ts/bevy";
5
+ import { Foe, Health } from "./enemy.tsx";
6
+ import { Hud } from "./hud.tsx";
7
+ import { Player, PlayerUnit } from "./player.tsx";
8
+
9
+ /** The game's camera. It sits where the editor's first view does, so edit mode opens on the same
10
+ * view. */
11
+ export function stage(commands: Commands) {
12
+ commands.spawnNative("camera3d", { position: [-6, 5, 10], lookAt: [0, 0, 0] });
13
+ }
14
+
15
+ /** Every 30 frames, each enemy within 10 units hurts the player by 5. */
16
+ export function damage(
17
+ clock: Local<{ frames: u32 }>,
18
+ enemies: Query<[Transform], With<Foe>>,
19
+ players: Query<[Transform, Mut<Health>], With<Player>>,
20
+ ) {
21
+ clock.frames += 1;
22
+ if (clock.frames % 30 !== 0) return;
23
+ players.each((p, h) => {
24
+ enemies.each((e) => {
25
+ if (e.translation.distance(p.translation) < 10) h.hp = Math.max(0, h.hp - 5);
26
+ });
27
+ });
28
+ }
29
+
30
+ /** Enemies bob up and down. */
31
+ export function bob(time: Res<Time>, enemies: Query<[Mut<Transform>], [With<Foe>, Without<Player>]>) {
32
+ enemies.each((t) => {
33
+ t.translation.y = Math.sin(time.elapsed_secs * 2) * 0.5;
34
+ });
35
+ }
36
+
37
+ /** Enemies at no hp are despawned: a reload does not bring them back. */
38
+ export function cull(commands: Commands, enemies: Query<[Entity, Health], With<Foe>>) {
39
+ enemies.each((e, h) => {
40
+ if (h.hp <= 0) commands.entity(e).despawn();
41
+ });
42
+ }
43
+
44
+ const player = ref();
45
+ app.scene("arena", () => (
46
+ <>
47
+ {/* The sun is scene, not a startup's, so edit mode shows the arena lit. At (3, 6, 2), looking at the origin. */}
48
+ <entity name="sun">
49
+ <DirectionalLight shadow_maps_enabled />
50
+ <Transform translation={[3, 6, 2]} rotation={[-0.4341, 0.4107, 0.2323, 0.7674]} />
51
+ </entity>
52
+ <PlayerUnit handle={player} />
53
+ <Hud player={player} />
54
+ </>
55
+ ));
56
+ app.level("levels/arena.bsn");
57
+ app.addSystems(Startup, stage).addSystems(Update, damage, bob, cull);
@@ -0,0 +1,12 @@
1
+ // The HUD: no hooks. The text binds the player's Health; the button mounts an enemy.
2
+ import { type NodeRef, px } from "bevy-ts";
3
+ import { Enemy, Health } from "./enemy.tsx";
4
+
5
+ export const Hud = ({ player }: { player: NodeRef }) => (
6
+ <node style={{ position: "absolute", top: px(12), left: px(12), padding: px(8), flexDirection: "column", rowGap: px(6) }} background="#00000088">
7
+ <text key="hp" size={20}>{() => `HP ${Math.ceil(player.get(Health).hp)} / ${player.get(Health).max}`}</text>
8
+ <button key="spawn" style={{ padding: px(4) }} background="#3366cc" onPointerClick={({ commands, frame }) => commands.mount(<Enemy key={`e${frame}`} at={[0, 0, -6]} />)}>
9
+ <text>Spawn</text>
10
+ </button>
11
+ </node>
12
+ );
@@ -0,0 +1,14 @@
1
+ import { Component, type NodeRef } from "bevy-ts";
2
+ import { Transform } from "bevy-ts/bevy";
3
+ import { Health } from "./enemy.tsx";
4
+
5
+ export class Player extends Component {}
6
+
7
+ export const PlayerUnit = ({ handle }: { handle: NodeRef }) => (
8
+ <entity name="player" ref={handle}>
9
+ <Transform translation={[0, 0, 0]} />
10
+ <Health initialHp={100} max={100} />
11
+ <Player />
12
+ <mesh shape="sphere" radius={0.5} color="#3366cc" />
13
+ </entity>
14
+ );
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "cube",
3
+ "title": "Cube",
4
+ "description": "One cube bobbing on a sine wave: a scene, a component, a startup system and an update system.",
5
+ "entry": "src/game.ts"
6
+ }
@@ -0,0 +1,19 @@
1
+ // The cube's scene: the cube and the sun, so edit mode shows them before any system runs.
2
+ // game.ts mounts it once `Mover` is defined.
3
+ import { DirectionalLight, Transform } from "bevy-ts/bevy";
4
+ import { Mover } from "./game.ts";
5
+
6
+ export const Cube = () => (
7
+ <>
8
+ {/* At (3, 6, 2), looking at the origin. */}
9
+ <entity name="sun">
10
+ <DirectionalLight shadow_maps_enabled />
11
+ <Transform translation={[3, 6, 2]} rotation={[-0.4341, 0.4107, 0.2323, 0.7674]} />
12
+ </entity>
13
+ <entity name="cube">
14
+ <Transform />
15
+ <mesh shape="cuboid" size={[1, 1, 1]} color="#ff8800" />
16
+ <Mover />
17
+ </entity>
18
+ </>
19
+ );
@@ -0,0 +1,22 @@
1
+ import { app, Component, type f32, Startup, Time, Transform, Update } from "bevy-ts";
2
+ import type { Commands, Mut, Query, Res } from "bevy-ts";
3
+ import { Cube } from "./cube.tsx";
4
+
5
+ export class Mover extends Component {
6
+ speed: f32 = 1;
7
+ amplitude: f32 = 2;
8
+ }
9
+
10
+ /** The game's camera; the cube and the sun are the scene's, in cube.tsx. */
11
+ export function scene(commands: Commands) {
12
+ commands.spawnNative("camera3d", { position: [-3, 3, 6], lookAt: [0, 0, 0] });
13
+ }
14
+
15
+ export function bob(time: Res<Time>, q: Query<[Mover, Mut<Transform>]>) {
16
+ q.each((mover, t) => {
17
+ t.translation.y = Math.sin(time.elapsed_secs * mover.speed) * mover.amplitude;
18
+ });
19
+ }
20
+
21
+ app.scene("cube", Cube);
22
+ app.addSystems(Startup, scene).addSystems(Update, bob);
@@ -0,0 +1,18 @@
1
+ // The cube example with a script material (bevy-ts/material): its params in TS, its shader in
2
+ // glow.wgsl, no Rust. Run this file as the entry: `bevy-ts dev examples/cube/src/glow.tsx`.
3
+ // Save glow.wgsl and the cube redraws with no reload; change the params and it re-encodes.
4
+ import "./game.ts";
5
+ import { app } from "bevy-ts";
6
+ import { Transform } from "bevy-ts/bevy";
7
+ import { color, f32, material } from "bevy-ts/material";
8
+ import glow from "./glow.wgsl";
9
+
10
+ export const Glow = material("game::Glow", { params: { color: color, pulse: f32 }, fragment: glow });
11
+
12
+ app.scene("glow", () => (
13
+ <entity name="glow">
14
+ <Transform translation={[2.5, 0, 0]} />
15
+ <mesh shape="cuboid" size={[1, 1, 1]} />
16
+ <material kind={Glow} color="#ff8800" pulse={2} />
17
+ </entity>
18
+ ));
@@ -0,0 +1,13 @@
1
+ // game::Glow's fragment shader. bevy-ts appends `params` (a GlowParams with color and pulse)
2
+ // from the schema in glow.tsx, so this file does not declare it.
3
+ import bevy_pbr::render::forward_io::VertexOutput;
4
+ import bevy_pbr::render::mesh_view_bindings::globals;
5
+
6
+ const FLOOR: f32 = 0.25;
7
+
8
+ @fragment
9
+ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
10
+ let wave = 0.5 + 0.5 * sin(globals.time * params.pulse);
11
+ let glow = FLOOR + (1.0 - FLOOR) * wave;
12
+ return vec4<f32>(params.color.rgb * glow, params.color.a);
13
+ }
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="./wgsl.d.ts" />
1
2
  // Types for `bevy-ts`: the ruled surface (operator, 2026-10-20; docs/research/dx.md). The runtime
2
3
  // is built into the engine binary.
3
4
  //
@@ -427,6 +428,8 @@ export interface App {
427
428
 
428
429
  /** Writes a manifest section for a Rust plugin. */
429
430
  export declare function section(name: string, value: Uint8Array | string | object): void;
431
+ /** The loading generation's `{ items }` list for a JSON section, for a built-in module's per-generation declarations. */
432
+ export declare function sectionItems(name: string): unknown[];
430
433
  /** Decodes UTF-8 bytes. */
431
434
  export declare function text(bytes: Uint8Array): string;
432
435
  /** A plugin: an exported function taking the app, as Bevy's `fn build(&self, app: &mut App)`. */
package/jsx-runtime.d.ts CHANGED
@@ -132,6 +132,17 @@ export interface MaterialProps {
132
132
  roughness?: number;
133
133
  emissive?: string;
134
134
  unlit?: boolean;
135
+ kind?: never;
136
+ }
137
+
138
+ /**
139
+ * `<material kind={Glow} color="#f80" pulse={2} />`: a script material from `material()` in
140
+ * `bevy-ts/material`, with its params by name and its textures as asset paths. Checked against
141
+ * the kind's schema when the generation loads.
142
+ */
143
+ export interface ScriptMaterialProps {
144
+ kind: { readonly name: string };
145
+ [param: string]: unknown;
135
146
  }
136
147
 
137
148
  type DescElement = Element;
@@ -161,7 +172,7 @@ export declare namespace JSX {
161
172
  interface IntrinsicElements {
162
173
  entity: EntityProps & KeyAttribute;
163
174
  mesh: MeshProps;
164
- material: MaterialProps;
175
+ material: MaterialProps | ScriptMaterialProps;
165
176
  node: NodeProps & KeyAttribute;
166
177
  button: NodeProps & KeyAttribute;
167
178
  text: TextProps & KeyAttribute;
package/material.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ // @generated from crates/bevy-ts-material/ts/material.d.ts by scripts/sync-runtime.mjs. Do not edit.
2
+ /** The value a `.wgsl` import gives: the file, by path. Its WGSL stays in the engine. */
3
+ export interface ShaderFile {
4
+ readonly kind: "shaderFile";
5
+ readonly path: string;
6
+ }
7
+
8
+ export type Base = "f32" | "i32" | "u32" | "vec2f" | "vec3f" | "vec4f" | "mat4x4f" | "color";
9
+ export type ParamType = Base | { readonly array: Base; readonly len: number };
10
+
11
+ export declare const f32: "f32";
12
+ export declare const i32: "i32";
13
+ export declare const u32: "u32";
14
+ export declare const vec2f: "vec2f";
15
+ export declare const vec3f: "vec3f";
16
+ export declare const vec4f: "vec4f";
17
+ export declare const mat4x4f: "mat4x4f";
18
+ /** A `vec4<f32>` in linear RGBA, written as `"#f80"` or sRGB `[r, g, b, a?]`. */
19
+ export declare const color: "color";
20
+ /** A 2D texture with a filtering sampler: `name` and `name_sampler` in WGSL. */
21
+ export declare const texture2d: "texture2d";
22
+
23
+ /** A fixed array, strided to 16 bytes; scalar and `vec2f` elements read as `.x`/`.xy` of a `vec4`. */
24
+ export declare function array<B extends Base, N extends number>(of: B, len: N): { readonly array: B; readonly len: N };
25
+
26
+ export type AlphaMode = "opaque" | "blend" | "premultiplied" | "add" | "multiply" | "alphaToCoverage" | { mask: number };
27
+
28
+ /** A param's value in TS: numbers, number tuples, or a color. */
29
+ export type ParamValue<T extends ParamType> = T extends "f32" | "i32" | "u32"
30
+ ? number
31
+ : T extends "vec2f"
32
+ ? [number, number]
33
+ : T extends "vec3f"
34
+ ? [number, number, number]
35
+ : T extends "vec4f"
36
+ ? [number, number, number, number]
37
+ : T extends "mat4x4f"
38
+ ? number[]
39
+ : T extends "color"
40
+ ? string | [number, number, number] | [number, number, number, number]
41
+ : T extends { array: infer E extends Base }
42
+ ? ParamValue<E>[]
43
+ : never;
44
+
45
+ /** A material kind, what `<material kind={...} />` names. */
46
+ export interface MaterialKind<P extends Record<string, ParamType> = Record<string, ParamType>, T extends Record<string, "texture2d"> = {}> {
47
+ readonly name: string;
48
+ /** Types only: the params and textures `<material>` takes for this kind. */
49
+ readonly __props?: { [K in keyof P]?: ParamValue<P[K]> } & { [K in keyof T]?: string };
50
+ }
51
+
52
+ /**
53
+ * Declares a script material: its params, laid out by WGSL's uniform rules in Rust; its textures;
54
+ * and its shaders, `.wgsl` files imported like modules (`import glow from "./glow.wgsl"`). The WGSL
55
+ * reads the params as `params.<name>` and each texture as `<name>` with `<name>_sampler`; bevy-ts
56
+ * appends those declarations, so the file does not declare them.
57
+ */
58
+ export declare function material<P extends Record<string, ParamType>, T extends Record<string, "texture2d"> = {}>(
59
+ name: string,
60
+ decl: { params: P; textures?: T; fragment: ShaderFile; vertex?: ShaderFile; alphaMode?: AlphaMode },
61
+ ): MaterialKind<P, T>;