@nanoforge-dev/schematics 2.3.0 → 3.0.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/CHANGELOG.md +15 -0
- package/dist/collection.json +5 -0
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/libs/component/component.factory.d.ts +1 -4
- package/dist/libs/component/component.factory.d.ts.map +1 -1
- package/dist/libs/component/component.factory.js +2 -4
- package/dist/libs/component/component.factory.js.map +1 -1
- package/dist/libs/plugin/files/README.md +23 -0
- package/dist/libs/plugin/files/__dot__gitignore +2 -0
- package/dist/libs/plugin/files/nanoforge.manifest.json.template +49 -0
- package/dist/libs/plugin/files/package.json.template +21 -0
- package/dist/libs/plugin/files/src/Panel.svelte +35 -0
- package/dist/libs/plugin/files/src/counter.ts +4 -0
- package/dist/libs/plugin/files/src/index.ts +52 -0
- package/dist/libs/plugin/files/svelte.config.js +2 -0
- package/dist/libs/plugin/files/tsconfig.json +14 -0
- package/dist/libs/plugin/files/vite.config.ts +12 -0
- package/dist/libs/plugin/plugin.factory.d.ts +18 -0
- package/dist/libs/plugin/plugin.factory.d.ts.map +1 -0
- package/dist/libs/plugin/plugin.factory.js +111 -0
- package/dist/libs/plugin/plugin.factory.js.map +1 -0
- package/dist/libs/plugin/schema.json +25 -0
- package/dist/libs/plugin/theme-files/README.md +21 -0
- package/dist/libs/plugin/theme-files/__dot__gitignore +2 -0
- package/dist/libs/plugin/theme-files/build.mjs +29 -0
- package/dist/libs/plugin/theme-files/icons/leaf.svg +1 -0
- package/dist/libs/plugin/theme-files/nanoforge.manifest.json.template +30 -0
- package/dist/libs/plugin/theme-files/package.json.template +11 -0
- package/dist/libs/project/files/js/AGENTS.md.template +126 -0
- package/dist/libs/project/files/js/CLAUDE.md.template +1 -0
- package/dist/libs/project/files/js/__dot__gitignore +4 -0
- package/dist/libs/project/files/js/nanoforge.config.js +1 -1
- package/dist/libs/project/files/js/package.json.template +8 -3
- package/dist/libs/project/files/js/src/components/drawable-circle-2d.component.js +1 -1
- package/dist/libs/project/files/js/src/main.js +2 -2
- package/dist/libs/project/files/js/src/systems/draw-2d.system.js +1 -1
- package/dist/libs/project/files/js/src/systems/move-2d.system.js +1 -1
- package/dist/libs/project/files/js/src/systems/position-sync.system.js +1 -1
- package/dist/libs/project/files/ts/AGENTS.md.template +114 -0
- package/dist/libs/project/files/ts/CLAUDE.md.template +1 -0
- package/dist/libs/project/files/ts/__dot__gitignore +4 -0
- package/dist/libs/project/files/ts/nanoforge.config.ts +1 -1
- package/dist/libs/project/files/ts/package.json.template +8 -3
- package/dist/libs/project/files/ts/src/components/drawable-circle-2d.component.ts +1 -1
- package/dist/libs/project/files/ts/src/main.ts +2 -1
- package/dist/libs/project/files/ts/src/systems/draw-2d.system.ts +1 -1
- package/dist/libs/project/files/ts/src/systems/move-2d.system.ts +1 -1
- package/dist/libs/project/files/ts/src/systems/position-sync.system.ts +1 -1
- package/dist/libs/project/project.factory.d.ts +5 -4
- package/dist/libs/project/project.factory.d.ts.map +1 -1
- package/dist/libs/project/project.factory.js +14 -10
- package/dist/libs/project/project.factory.js.map +1 -1
- package/dist/libs/project/schema.json +5 -0
- package/dist/libs/system/system.factory.d.ts +1 -4
- package/dist/libs/system/system.factory.d.ts.map +1 -1
- package/dist/libs/system/system.factory.js +2 -4
- package/dist/libs/system/system.factory.js.map +1 -1
- package/dist/libs/workspace/files/js/AGENTS.md.template +49 -0
- package/dist/libs/workspace/files/js/CLAUDE.md.template +1 -0
- package/dist/libs/workspace/files/js/__dot__gitignore +4 -0
- package/dist/libs/workspace/files/js/nanoforge.config.js +1 -1
- package/dist/libs/workspace/files/js/package.json.template +1 -1
- package/dist/libs/workspace/files/ts/AGENTS.md.template +49 -0
- package/dist/libs/workspace/files/ts/CLAUDE.md.template +1 -0
- package/dist/libs/workspace/files/ts/__dot__gitignore +4 -0
- package/dist/libs/workspace/files/ts/nanoforge.config.ts +1 -1
- package/dist/libs/workspace/files/ts/package.json.template +1 -1
- package/dist/libs/workspace/schema.json +5 -0
- package/dist/libs/workspace/workspace.factory.d.ts +5 -4
- package/dist/libs/workspace/workspace.factory.d.ts.map +1 -1
- package/dist/libs/workspace/workspace.factory.js +7 -8
- package/dist/libs/workspace/workspace.factory.js.map +1 -1
- package/package.json +15 -16
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme has no code: its build copies the manifest and the icons into dist/, the folder the
|
|
3
|
+
* editor loads. With --watch, it copies them again on every change (the editor then reloads it).
|
|
4
|
+
*/
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, rmSync, watch } from "node:fs";
|
|
6
|
+
|
|
7
|
+
const build = () => {
|
|
8
|
+
rmSync("dist", { recursive: true, force: true });
|
|
9
|
+
mkdirSync("dist");
|
|
10
|
+
cpSync("nanoforge.manifest.json", "dist/nanoforge.manifest.json");
|
|
11
|
+
if (existsSync("icons")) cpSync("icons", "dist/icons", { recursive: true });
|
|
12
|
+
console.log("Built dist/");
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// An editor saving mid-rename makes cpSync throw ENOENT: log it and wait for the next change
|
|
16
|
+
// rather than killing the watcher.
|
|
17
|
+
const rebuild = () => {
|
|
18
|
+
try {
|
|
19
|
+
build();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`Build failed: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
build();
|
|
26
|
+
if (process.argv.includes("--watch")) {
|
|
27
|
+
for (const target of ["nanoforge.manifest.json", "icons"])
|
|
28
|
+
if (existsSync(target)) watch(target, { recursive: true }, rebuild);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21c0-9 6-15 16-16-1 10-7 16-16 16Z"/><path d="M5 21 14 12"/></svg>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "plugin",
|
|
3
|
+
"name": "<%= name %>",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"displayName": "<%= title %>",
|
|
6
|
+
"description": "A color theme for the NanoForge editor.",
|
|
7
|
+
"keywords": ["theme"],
|
|
8
|
+
"engines": {
|
|
9
|
+
"editor": ">=0.1.0"
|
|
10
|
+
},
|
|
11
|
+
"contributes": {
|
|
12
|
+
"themes": [
|
|
13
|
+
{
|
|
14
|
+
"id": "<%= short %>",
|
|
15
|
+
"label": "<%= title %>",
|
|
16
|
+
"extends": "nanoforge-dark",
|
|
17
|
+
"colors": {
|
|
18
|
+
"accent": "#2f7d6d",
|
|
19
|
+
"accent-hover": "#286a5d",
|
|
20
|
+
"accent-text": "#7fd6c2"
|
|
21
|
+
},
|
|
22
|
+
"syntax": {
|
|
23
|
+
"keyword": "#7fd6c2",
|
|
24
|
+
"string": "#e6c07b"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"icons": [{ "id": "<%= short %>-leaf", "path": "icons/leaf.svg" }]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; var add = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[packageManager]; var networked = part === "server" || hasServer; %># <%= packageName %>
|
|
2
|
+
|
|
3
|
+
Notes for AI coding agents. This is a NanoForge <%= part %> app, written in JavaScript: ES
|
|
4
|
+
modules, with types in JSDoc comments. NanoForge is a game engine built on an entity component
|
|
5
|
+
system (ECS).
|
|
6
|
+
<% if (part === "client") { %>The game runs in the browser.<% } else { %>This app runs in Bun: it has no graphics, input or DOM.<% } %>
|
|
7
|
+
<% if (workspace) { %>
|
|
8
|
+
This app is part of a workspace: read `../../AGENTS.md` first.
|
|
9
|
+
<% } %>
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
- `nanoforge.config.js`: the app config that `nf` reads (`type: "<%= part %>"`). Keep its
|
|
13
|
+
`entryFile`, `editor.entryFile` and `language: "js"`: the defaults are for TypeScript. Fields
|
|
14
|
+
and defaults: https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
|
|
15
|
+
- `src/main.js`: the entry file. The loader calls its exported `main(options)` from the bundled
|
|
16
|
+
`main.js`, so keep the file name and the export.
|
|
17
|
+
- `src/components/`, `src/systems/`: one component or system per file,
|
|
18
|
+
`<kebab-name>.component.js` and `<kebab-name>.system.js`.
|
|
19
|
+
- `assets/`: copied to the root of `dist/` on each build, so `assets/hero.png` is served as
|
|
20
|
+
`/hero.png`. Read it with `ctx.assets.getAsset("/hero.png")`, an `NfFile` whose `path` is its URL.
|
|
21
|
+
- `dist/` is the build output, emptied on every build. `.nanoforge/` belongs to the NanoForge
|
|
22
|
+
tools: don't edit it.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
`nf` comes from the `@nanoforge-dev/cli` dev dependency: run it as `<%= nf %> <command>`.
|
|
27
|
+
<% if (workspace) { %>
|
|
28
|
+
Run `nf dev` and `nf start` from the workspace root: they run this app with the other one, and
|
|
29
|
+
they read the `.env` of their `-d` folder (the current folder by default). To build this app
|
|
30
|
+
alone, run `<%= nf %> build -d apps/<%= part %>` at the root.
|
|
31
|
+
<% } else { %>
|
|
32
|
+
- `nf dev`: runs `nf build --watch` and `nf start --watch` together.<% if (part === "client") { %> The game is on
|
|
33
|
+
http://localhost:3000 (`port` in the config).<% } %>
|
|
34
|
+
- `nf build`: bundles `src/main.js` with Bun into `dist/`. It does not check the JSDoc types.
|
|
35
|
+
- `nf start`: <% if (part === "client") { %>serves<% } else { %>runs<% } %> the last build.
|
|
36
|
+
<% } %>
|
|
37
|
+
<% if (workspace && packageManager === "npm") { %>Add a package with `npm install <package> -w apps/<%= part %>` at the workspace root.<% } else { %>Add a package with `<%= add %> <package>` in this folder.<% } %>
|
|
38
|
+
|
|
39
|
+
The `@nanoforge-dev/*` packages are on `^2`. The online docs follow the engine's development
|
|
40
|
+
branch and can describe APIs that the installed release lacks: the installed types
|
|
41
|
+
(`@nanoforge-dev/<package>/dist` in `node_modules`) are the reference.
|
|
42
|
+
|
|
43
|
+
## Game code
|
|
44
|
+
|
|
45
|
+
- `main` creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>`, registers each library with
|
|
46
|
+
`app.use(...)`, then calls `await app.init(options)`. `app.use` throws after `init`, and
|
|
47
|
+
`ecs.registry` throws before it. `await app.run()` starts the tick loop and returns.
|
|
48
|
+
- Components are plain classes. The registry keys them by their `name` field: give each class a
|
|
49
|
+
`name` equal to its class name (the generated ones use `this.constructor.name`), and never store
|
|
50
|
+
game data in `name`. Names must be unique in the app, and `entity` and `id` are reserved. An
|
|
51
|
+
entity has one component per class: adding a second one replaces the first.
|
|
52
|
+
- Systems are `(registry, ctx) => void` functions. They run once per tick, in the order of the
|
|
53
|
+
`registry.addSystem` calls. Their result is ignored, so an async system is not awaited.
|
|
54
|
+
- `registry.getZipper([A, B])` returns one object per entity that has both, keyed by component
|
|
55
|
+
name: `({ A, B }) => …`. The values are the live components: change them in place.
|
|
56
|
+
`getIndexedZipper` adds the entity `id`, and `registry.entityFromIndex(id)` turns it back into an
|
|
57
|
+
entity for `killEntity` or `removeComponent`.
|
|
58
|
+
- `ctx.app.delta` is the time since the last tick, in milliseconds. `tickRate` in `main` is the
|
|
59
|
+
number of ticks per second. `ctx.app.requestStop()` stops the game.<% if (part === "client") { %>
|
|
60
|
+
- Graphics use Konva: `@nanoforge-dev/graphics-2d` re-exports its shapes (`Circle`, `Rect`,
|
|
61
|
+
`Layer`…), and `ctx.graphics.baseLayer` is a layer already on the stage. Coordinates are in a
|
|
62
|
+
1920×1080 design space scaled to the window (the `viewport` option of `createClient`).
|
|
63
|
+
- Keys: `ctx.input.isKeyPressed(InputEnum.KeyW)`, with `InputEnum` from `@nanoforge-dev/input`. Its
|
|
64
|
+
values are `KeyboardEvent.code` names.<% } %>
|
|
65
|
+
<% if (networked) { %>
|
|
66
|
+
## Network
|
|
67
|
+
|
|
68
|
+
- The client and the server connect over TCP, a WebSocket, and over UDP, a WebRTC data channel
|
|
69
|
+
set up through a WebSocket. Their address and ports are `NANOFORGE_*` variables, which `nf start`
|
|
70
|
+
reads from the `.env` of <%= workspace ? "the workspace root" : "its `-d` folder" %>. It gives
|
|
71
|
+
`NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the server, as `X` in
|
|
72
|
+
`options.env`. Everything the client gets reaches the browser: never put a secret there.
|
|
73
|
+
https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
|
|
74
|
+
- The generated systems use `ctx.network.tcp` and encode JSON with `TextEncoder`. Check the
|
|
75
|
+
installed `@nanoforge-dev/network` types before using another network API.
|
|
76
|
+
<% } %>
|
|
77
|
+
## Example
|
|
78
|
+
|
|
79
|
+
A component and a system, as the rules above want them:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
// src/components/velocity-2d.component.js
|
|
83
|
+
/** Speed of an entity, in pixels per second. */
|
|
84
|
+
export class Velocity2D {
|
|
85
|
+
name = "Velocity2D";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {number} [x]
|
|
89
|
+
* @param {number} [y]
|
|
90
|
+
*/
|
|
91
|
+
constructor(x = 0, y = 0) {
|
|
92
|
+
this.x = x;
|
|
93
|
+
this.y = y;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/systems/apply-velocity-2d.system.js
|
|
98
|
+
import { Position2D } from "../components/position-2d.component";
|
|
99
|
+
import { Velocity2D } from "../components/velocity-2d.component";
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {import("@nanoforge-dev/common").Context} Context
|
|
103
|
+
* @typedef {import("@nanoforge-dev/ecs").Registry} Registry
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Moves every entity that has a position and a velocity.
|
|
108
|
+
* @param {Registry} registry
|
|
109
|
+
* @param {Context} ctx
|
|
110
|
+
*/
|
|
111
|
+
export const applyVelocity2D = (registry, ctx) => {
|
|
112
|
+
const seconds = ctx.app.delta / 1000;
|
|
113
|
+
|
|
114
|
+
registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => {
|
|
115
|
+
Position2D.x += Velocity2D.x * seconds;
|
|
116
|
+
Position2D.y += Velocity2D.y * seconds;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Register it in `main` with `registry.addSystem(applyVelocity2D)`.
|
|
122
|
+
|
|
123
|
+
## Docs
|
|
124
|
+
|
|
125
|
+
- `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
|
|
126
|
+
- Everything else: https://docs.nanoforge.eu
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -82,6 +82,10 @@ build/Release
|
|
|
82
82
|
|
|
83
83
|
# Dependency directories
|
|
84
84
|
node_modules/
|
|
85
|
+
|
|
86
|
+
# NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
|
|
87
|
+
nf_modules/
|
|
88
|
+
|
|
85
89
|
jspm_packages/
|
|
86
90
|
|
|
87
91
|
# Snowpack dependency directory (https://snowpack.dev/)
|
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
"@nanoforge-dev/network": "<%= networkVersion %>"
|
|
11
11
|
},<% } %>
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@nanoforge-dev/
|
|
13
|
+
"@nanoforge-dev/asset": "<%= engineVersion %>",
|
|
14
|
+
"@nanoforge-dev/cli": "<%= cliVersion %>",
|
|
15
|
+
"@nanoforge-dev/common": "<%= engineVersion %>",
|
|
16
|
+
"@nanoforge-dev/core": "<%= engineVersion %>",
|
|
17
|
+
"@nanoforge-dev/config": "<%= engineVersion %>",
|
|
18
|
+
"@nanoforge-dev/ecs": "<%= ecsVersion %>"<% if (part === "client") { %>,
|
|
19
|
+
"@nanoforge-dev/env": "<%= engineVersion %>",
|
|
14
20
|
"@nanoforge-dev/graphics-2d": "<%= graphics2dVersion %>",
|
|
15
21
|
"@nanoforge-dev/input": "<%= inputVersion %>",
|
|
16
|
-
"@nanoforge-dev/network": "<%= networkVersion %>"
|
|
17
|
-
"nanoforge": "<%= nanoforgeVersion %>"
|
|
22
|
+
"@nanoforge-dev/network": "<%= networkVersion %>"<% } %>
|
|
18
23
|
},<% if (packageManager === "npm" && allowBuilds.length > 0) { %>
|
|
19
24
|
"allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %>
|
|
20
25
|
"<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @typedef {import("@nanoforge-dev/common").NfFile} NfFile
|
|
2
3
|
* @typedef {import("@nanoforge-dev/ecs").EditorComponentManifest} EditorComponentManifest
|
|
3
4
|
* @typedef {import("@nanoforge-dev/graphics-2d").Layer} Layer
|
|
4
|
-
* @typedef {import("nanoforge").NfFile} NfFile
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Circle } from "@nanoforge-dev/graphics-2d";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NanoforgeFactory } from "nanoforge";
|
|
1
|
+
import { NanoforgeFactory } from "@nanoforge-dev/core";
|
|
2
2
|
import { EcsLibrary } from "@nanoforge-dev/ecs/<%= part %>";<% if (part === "client") { %>
|
|
3
3
|
import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d";
|
|
4
4
|
import { InputLibrary } from "@nanoforge-dev/input";<% } %>
|
|
@@ -11,7 +11,7 @@ import { positionSync } from "./systems/position-sync.system";<% } %><% } else {
|
|
|
11
11
|
import { move2D } from "./systems/move-2d.system";<% } %>
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @param {import("nanoforge").<%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %>} options
|
|
14
|
+
* @param {import("@nanoforge-dev/common").<%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %>} options
|
|
15
15
|
* @returns {Promise<void>}
|
|
16
16
|
*/
|
|
17
17
|
export const main = async (options) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @typedef {import("@nanoforge-dev/common").Context} Context
|
|
2
3
|
* @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest
|
|
3
4
|
* @typedef {import("@nanoforge-dev/ecs").Registry} Registry
|
|
4
|
-
* @typedef {import("nanoforge").Context} Context
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { DrawableCircle2D } from "../components/drawable-circle-2d.component";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @typedef {import("@nanoforge-dev/common").Context} Context
|
|
2
3
|
* @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest
|
|
3
4
|
* @typedef {import("@nanoforge-dev/ecs").Registry} Registry
|
|
4
|
-
* @typedef {import("nanoforge").Context} Context
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Position2D } from "../components/position-2d.component";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @typedef {import("@nanoforge-dev/common").Context} Context
|
|
2
3
|
* @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest
|
|
3
4
|
* @typedef {import("@nanoforge-dev/ecs").Registry} Registry
|
|
4
|
-
* @typedef {import("nanoforge").Context} Context
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Position2D } from "../components/position-2d.component";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; var add = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[packageManager]; var networked = part === "server" || hasServer; %># <%= packageName %>
|
|
2
|
+
|
|
3
|
+
Notes for AI coding agents. This is a NanoForge <%= part %> app, written in TypeScript.
|
|
4
|
+
NanoForge is a game engine built on an entity component system (ECS).
|
|
5
|
+
<% if (part === "client") { %>The game runs in the browser.<% } else { %>This app runs in Bun: it has no graphics, input or DOM.<% } %>
|
|
6
|
+
<% if (workspace) { %>
|
|
7
|
+
This app is part of a workspace: read `../../AGENTS.md` first.
|
|
8
|
+
<% } %>
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
- `nanoforge.config.ts`: the app config that `nf` reads (`type: "<%= part %>"`). Fields and defaults:
|
|
12
|
+
https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
|
|
13
|
+
- `src/main.ts`: the entry file. The loader calls its exported `main(options)` from the bundled
|
|
14
|
+
`main.js`, so keep the file name and the export.
|
|
15
|
+
- `src/components/`, `src/systems/`: one component or system per file,
|
|
16
|
+
`<kebab-name>.component.ts` and `<kebab-name>.system.ts`.
|
|
17
|
+
- `assets/`: copied to the root of `dist/` on each build, so `assets/hero.png` is served as
|
|
18
|
+
`/hero.png`. Read it with `ctx.assets.getAsset("/hero.png")`, an `NfFile` whose `path` is its URL.
|
|
19
|
+
- `dist/` is the build output, emptied on every build. `.nanoforge/` belongs to the NanoForge
|
|
20
|
+
tools: don't edit it.
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
`nf` comes from the `@nanoforge-dev/cli` dev dependency: run it as `<%= nf %> <command>`.
|
|
25
|
+
<% if (workspace) { %>
|
|
26
|
+
Run `nf dev` and `nf start` from the workspace root: they run this app with the other one, and
|
|
27
|
+
they read the `.env` of their `-d` folder (the current folder by default). To build this app
|
|
28
|
+
alone, run `<%= nf %> build -d apps/<%= part %>` at the root.
|
|
29
|
+
<% } else { %>
|
|
30
|
+
- `nf dev`: runs `nf build --watch` and `nf start --watch` together.<% if (part === "client") { %> The game is on
|
|
31
|
+
http://localhost:3000 (`port` in the config).<% } %>
|
|
32
|
+
- `nf build`: bundles `src/main.ts` with Bun into `dist/`. It does not type-check.
|
|
33
|
+
- `nf start`: <% if (part === "client") { %>serves<% } else { %>runs<% } %> the last build.
|
|
34
|
+
<% } %>
|
|
35
|
+
<% if (workspace && packageManager === "npm") { %>Add a package with `npm install <package> -w apps/<%= part %>` at the workspace root.<% } else { %>Add a package with `<%= add %> <package>` in this folder.<% } %>
|
|
36
|
+
|
|
37
|
+
The `@nanoforge-dev/*` packages are on `^2`. The online docs follow the engine's development
|
|
38
|
+
branch and can describe APIs that the installed release lacks: the installed types
|
|
39
|
+
(`@nanoforge-dev/<package>/dist` in `node_modules`) are the reference.
|
|
40
|
+
|
|
41
|
+
## Game code
|
|
42
|
+
|
|
43
|
+
- `main` creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>`, registers each library with
|
|
44
|
+
`app.use(...)`, then calls `await app.init(options)`. `app.use` throws after `init`, and
|
|
45
|
+
`ecs.registry` throws before it. `await app.run()` starts the tick loop and returns.
|
|
46
|
+
- Components are plain classes. The registry keys them by their `name` field: give each class a
|
|
47
|
+
`name` equal to its class name (the generated ones use `this.constructor.name`), and never store
|
|
48
|
+
game data in `name`. Names must be unique in the app, and `entity` and `id` are reserved. An
|
|
49
|
+
entity has one component per class: adding a second one replaces the first.
|
|
50
|
+
- Systems are `(registry, ctx) => void` functions. They run once per tick, in the order of the
|
|
51
|
+
`registry.addSystem` calls. Their result is ignored, so an async system is not awaited.
|
|
52
|
+
- `registry.getZipper([A, B])` returns one object per entity that has both, keyed by component
|
|
53
|
+
name: `({ A, B }) => …`. The values are the live components: change them in place.
|
|
54
|
+
`getIndexedZipper` adds the entity `id`, and `registry.entityFromIndex(id)` turns it back into an
|
|
55
|
+
entity for `killEntity` or `removeComponent`.
|
|
56
|
+
- `ctx.app.delta` is the time since the last tick, in milliseconds. `tickRate` in `main` is the
|
|
57
|
+
number of ticks per second. `ctx.app.requestStop()` stops the game.<% if (part === "client") { %>
|
|
58
|
+
- Graphics use Konva: `@nanoforge-dev/graphics-2d` re-exports its shapes (`Circle`, `Rect`,
|
|
59
|
+
`Layer`…), and `ctx.graphics.baseLayer` is a layer already on the stage. Coordinates are in a
|
|
60
|
+
1920×1080 design space scaled to the window (the `viewport` option of `createClient`).
|
|
61
|
+
- Keys: `ctx.input.isKeyPressed(InputEnum.KeyW)`, with `InputEnum` from `@nanoforge-dev/input`. Its
|
|
62
|
+
values are `KeyboardEvent.code` names.<% } %>
|
|
63
|
+
<% if (networked) { %>
|
|
64
|
+
## Network
|
|
65
|
+
|
|
66
|
+
- The client and the server connect over TCP, a WebSocket, and over UDP, a WebRTC data channel
|
|
67
|
+
set up through a WebSocket. Their address and ports are `NANOFORGE_*` variables, which `nf start`
|
|
68
|
+
reads from the `.env` of <%= workspace ? "the workspace root" : "its `-d` folder" %>. It gives
|
|
69
|
+
`NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the server, as `X` in
|
|
70
|
+
`options.env`. Everything the client gets reaches the browser: never put a secret there.
|
|
71
|
+
https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
|
|
72
|
+
- The generated systems use `ctx.network.tcp` and encode JSON with `TextEncoder`. Check the
|
|
73
|
+
installed `@nanoforge-dev/network` types before using another network API.
|
|
74
|
+
<% } %>
|
|
75
|
+
## Example
|
|
76
|
+
|
|
77
|
+
A component and a system, as the rules above want them:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// src/components/velocity-2d.component.ts
|
|
81
|
+
/** Speed of an entity, in pixels per second. */
|
|
82
|
+
export class Velocity2D {
|
|
83
|
+
name = "Velocity2D";
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
public x = 0,
|
|
87
|
+
public y = 0,
|
|
88
|
+
) {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/systems/apply-velocity-2d.system.ts
|
|
92
|
+
import { type Context } from "@nanoforge-dev/common";
|
|
93
|
+
import { type Registry } from "@nanoforge-dev/ecs";
|
|
94
|
+
|
|
95
|
+
import { Position2D } from "../components/position-2d.component";
|
|
96
|
+
import { Velocity2D } from "../components/velocity-2d.component";
|
|
97
|
+
|
|
98
|
+
/** Moves every entity that has a position and a velocity. */
|
|
99
|
+
export const applyVelocity2D = (registry: Registry, ctx: Context): void => {
|
|
100
|
+
const seconds = ctx.app.delta / 1000;
|
|
101
|
+
|
|
102
|
+
registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => {
|
|
103
|
+
Position2D.x += Velocity2D.x * seconds;
|
|
104
|
+
Position2D.y += Velocity2D.y * seconds;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Register it in `main` with `registry.addSystem(applyVelocity2D)`.
|
|
110
|
+
|
|
111
|
+
## Docs
|
|
112
|
+
|
|
113
|
+
- `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
|
|
114
|
+
- Everything else: https://docs.nanoforge.eu
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -82,6 +82,10 @@ build/Release
|
|
|
82
82
|
|
|
83
83
|
# Dependency directories
|
|
84
84
|
node_modules/
|
|
85
|
+
|
|
86
|
+
# NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
|
|
87
|
+
nf_modules/
|
|
88
|
+
|
|
85
89
|
jspm_packages/
|
|
86
90
|
|
|
87
91
|
# Snowpack dependency directory (https://snowpack.dev/)
|
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
"@nanoforge-dev/network": "<%= networkVersion %>"
|
|
11
11
|
},<% } %>
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@nanoforge-dev/
|
|
13
|
+
"@nanoforge-dev/asset": "<%= engineVersion %>",
|
|
14
|
+
"@nanoforge-dev/cli": "<%= cliVersion %>",
|
|
15
|
+
"@nanoforge-dev/common": "<%= engineVersion %>",
|
|
16
|
+
"@nanoforge-dev/core": "<%= engineVersion %>",
|
|
17
|
+
"@nanoforge-dev/config": "<%= engineVersion %>",
|
|
18
|
+
"@nanoforge-dev/ecs": "<%= ecsVersion %>"<% if (part === "client") { %>,
|
|
19
|
+
"@nanoforge-dev/env": "<%= engineVersion %>",
|
|
14
20
|
"@nanoforge-dev/graphics-2d": "<%= graphics2dVersion %>",
|
|
15
21
|
"@nanoforge-dev/input": "<%= inputVersion %>",
|
|
16
|
-
"@nanoforge-dev/network": "<%= networkVersion %>"
|
|
17
|
-
"nanoforge": "<%= nanoforgeVersion %>"
|
|
22
|
+
"@nanoforge-dev/network": "<%= networkVersion %>"<% } %>
|
|
18
23
|
},<% if (packageManager === "npm" && allowBuilds.length > 0) { %>
|
|
19
24
|
"allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %>
|
|
20
25
|
"<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { NfFile } from "@nanoforge-dev/common";
|
|
1
2
|
import type { EditorComponentManifest } from "@nanoforge-dev/ecs";
|
|
2
3
|
import { Circle, type Layer } from "@nanoforge-dev/graphics-2d";
|
|
3
|
-
import type { NfFile } from "nanoforge";
|
|
4
4
|
|
|
5
5
|
export class DrawableCircle2D {
|
|
6
6
|
public readonly name: string = this.constructor.name;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type <%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %> } from "@nanoforge-dev/common";
|
|
2
|
+
import { NanoforgeFactory } from "@nanoforge-dev/core";
|
|
2
3
|
import { EcsLibrary } from "@nanoforge-dev/ecs/<%= part %>";<% if (part === "client") { %>
|
|
3
4
|
import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d";
|
|
4
5
|
import { InputLibrary } from "@nanoforge-dev/input";<% } %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { Context } from "@nanoforge-dev/common";
|
|
1
2
|
import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs";
|
|
2
|
-
import { type Context } from "nanoforge";
|
|
3
3
|
|
|
4
4
|
import { DrawableCircle2D } from "../components/drawable-circle-2d.component";
|
|
5
5
|
import { Position2D } from "../components/position-2d.component";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Rule } from "@angular-devkit/schematics";
|
|
2
|
-
//#region src/libs/project/project.schema.d.ts
|
|
3
2
|
interface ProjectSchema {
|
|
4
3
|
/**
|
|
5
4
|
* The type of project to generate
|
|
@@ -56,9 +55,11 @@ interface ProjectSchema {
|
|
|
56
55
|
* Add editor dependencies
|
|
57
56
|
*/
|
|
58
57
|
editor: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Generate an AGENTS.md file documenting the project for AI coding agents, and a CLAUDE.md that imports it
|
|
60
|
+
*/
|
|
61
|
+
agents: boolean;
|
|
59
62
|
}
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region src/libs/project/project.factory.d.ts
|
|
62
63
|
export declare const main: (schema: ProjectSchema) => Rule;
|
|
63
|
-
|
|
64
|
+
export {};
|
|
64
65
|
//# sourceMappingURL=project.factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.factory.d.ts","names":[],"sources":["../../../src/libs/project/project.schema.d.ts","../../../src/libs/project/project.factory.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"project.factory.d.ts","names":[],"sources":["../../../src/libs/project/project.schema.d.ts","../../../src/libs/project/project.factory.ts"],"mappings":";UAAiB;;;;EAIf;;;;;;EAOA;;;;;;;EAQA;;;;;;EAOA;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAMA;;;;;;;EAQA;;;;EAKA;;;;EAKA;;qBCoEW,OAAQ,QAAQ,kBAAgB"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { join, normalize, strings } from "@angular-devkit/core";
|
|
2
2
|
import { apply, filter, mergeWith, move, renameTemplateFiles, template, url } from "@angular-devkit/schematics";
|
|
3
|
-
|
|
3
|
+
// src/utils/formatting.ts
|
|
4
4
|
const toWords = (str) => {
|
|
5
5
|
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
6
6
|
};
|
|
7
7
|
const toKebabCase = (str) => {
|
|
8
8
|
return toWords(str).map((word) => word.toLowerCase()).join("-");
|
|
9
9
|
};
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */
|
|
11
|
+
const DEFAULT_CLI_DEPENDENCY_VERSION = "latest";
|
|
12
|
+
// src/utils/registry.ts
|
|
12
13
|
const REGISTRY_URL = "https://registry.npmjs.org";
|
|
13
14
|
const DEFAULT_TIMEOUT_MS = 3e3;
|
|
14
15
|
const RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
@@ -46,12 +47,12 @@ const fetchTrustedVersion = async (packageName, fallback, options = {}) => {
|
|
|
46
47
|
clearTimeout(timeout);
|
|
47
48
|
}
|
|
48
49
|
};
|
|
49
|
-
|
|
50
|
-
//#region src/libs/project/project.factory.ts
|
|
50
|
+
// src/libs/project/project.factory.ts
|
|
51
51
|
const resolveDepVersions = async (part) => {
|
|
52
52
|
const isClient = part === "client";
|
|
53
|
-
const [
|
|
54
|
-
fetchTrustedVersion("nanoforge", "^2"),
|
|
53
|
+
const [engine, cli, ecs, network, graphics2d, input] = await Promise.all([
|
|
54
|
+
fetchTrustedVersion("@nanoforge-dev/core", "^2"),
|
|
55
|
+
fetchTrustedVersion("@nanoforge-dev/cli", DEFAULT_CLI_DEPENDENCY_VERSION),
|
|
55
56
|
fetchTrustedVersion("@nanoforge-dev/ecs", "^2"),
|
|
56
57
|
fetchTrustedVersion("@nanoforge-dev/network", "^2"),
|
|
57
58
|
isClient ? fetchTrustedVersion("@nanoforge-dev/graphics-2d", "^2") : "^2",
|
|
@@ -62,7 +63,8 @@ const resolveDepVersions = async (part) => {
|
|
|
62
63
|
graphics2d,
|
|
63
64
|
input,
|
|
64
65
|
network,
|
|
65
|
-
|
|
66
|
+
engine,
|
|
67
|
+
cli
|
|
66
68
|
};
|
|
67
69
|
};
|
|
68
70
|
const transform = async (schema) => {
|
|
@@ -83,10 +85,12 @@ const transform = async (schema) => {
|
|
|
83
85
|
graphics2dVersion: depVersions.graphics2d,
|
|
84
86
|
inputVersion: depVersions.input,
|
|
85
87
|
networkVersion: depVersions.network,
|
|
86
|
-
|
|
88
|
+
engineVersion: depVersions.engine,
|
|
89
|
+
cliVersion: depVersions.cli,
|
|
87
90
|
hasServer: schema.hasServer,
|
|
88
91
|
docker: schema.docker ?? false,
|
|
89
92
|
editor: schema.editor ?? false,
|
|
93
|
+
agents: schema.agents ?? true,
|
|
90
94
|
allowBuilds: workspace ? [] : ["bun"]
|
|
91
95
|
};
|
|
92
96
|
};
|
|
@@ -99,6 +103,7 @@ const generate = (options, path) => {
|
|
|
99
103
|
}),
|
|
100
104
|
filter((path) => options.docker && !options.workspace || !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? "")),
|
|
101
105
|
filter((path) => options.packageManager === "pnpm" && options.allowBuilds.length > 0 || !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml")),
|
|
106
|
+
filter((path) => options.agents || !["AGENTS.md", "CLAUDE.md"].some((name) => (path.split("/").at(-1) ?? "").startsWith(name))),
|
|
102
107
|
filter((path) => {
|
|
103
108
|
const filename = path.split("/").at(-1) ?? "";
|
|
104
109
|
if (filename.startsWith("drawable-circle-2d.component") || filename.startsWith("draw-2d.system")) return options.part === "client";
|
|
@@ -116,7 +121,6 @@ const main = (schema) => {
|
|
|
116
121
|
return mergeWith(generate(options, schema.directory));
|
|
117
122
|
};
|
|
118
123
|
};
|
|
119
|
-
//#endregion
|
|
120
124
|
export { main };
|
|
121
125
|
|
|
122
126
|
//# sourceMappingURL=project.factory.js.map
|