create-gamenative-app 0.1.1
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/README.md +116 -0
- package/bin/create.js +255 -0
- package/create-gamenative-app/README.md +33 -0
- package/create-gamenative-app/index.js +107 -0
- package/create-gamenative-app/package.json +33 -0
- package/create-gamenative-app/sync-template.js +177 -0
- package/create-gamenative-app/template/FRAMEWORK.md +14 -0
- package/create-gamenative-app/template/README.md +10 -0
- package/create-gamenative-app/template/assets/icon.png +0 -0
- package/create-gamenative-app/template/game.config.json +9 -0
- package/create-gamenative-app/template/package.json +39 -0
- package/create-gamenative-app/template/scripts/copy-config.js +8 -0
- package/create-gamenative-app/template/src/Game.ts +97 -0
- package/create-gamenative-app/template/src/Graphics.ts +93 -0
- package/create-gamenative-app/template/src/Input.ts +83 -0
- package/create-gamenative-app/template/src/config.ts +25 -0
- package/create-gamenative-app/template/src/games/Game.ts +32 -0
- package/create-gamenative-app/template/src/icon.ts +19 -0
- package/create-gamenative-app/template/src/index.ts +6 -0
- package/create-gamenative-app/template/src/main.ts +39 -0
- package/create-gamenative-app/template/src/pngjs.d.ts +5 -0
- package/create-gamenative-app/template/src/types.ts +35 -0
- package/create-gamenative-app/template/tsconfig.json +23 -0
- package/example.ts +45 -0
- package/game.config.json +9 -0
- package/package.json +47 -0
- package/scripts/copy-config.js +8 -0
- package/src/Game.ts +97 -0
- package/src/Graphics.ts +93 -0
- package/src/Input.ts +83 -0
- package/src/assets/icon.png +0 -0
- package/src/config.ts +25 -0
- package/src/games/ExampleGame.ts +49 -0
- package/src/icon.ts +19 -0
- package/src/index.ts +6 -0
- package/src/main.ts +39 -0
- package/src/pngjs.d.ts +5 -0
- package/src/types.ts +35 -0
- package/tsconfig.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# GameNative
|
|
2
|
+
|
|
3
|
+
Native TypeScript framework for desktop games. No game engine — just a thin layer over **SDL2** (window, input) and **WebGL** (via [@kmamal/gl](https://github.com/kmamal/headless-gl)) so you write games in TypeScript and run them with Node.
|
|
4
|
+
|
|
5
|
+
- **Window**: SDL window with OpenGL, resize, vsync, optional icon.
|
|
6
|
+
- **Input**: Keyboard (scancode-based) and mouse (position + buttons).
|
|
7
|
+
- **Rendering**: Raw WebGL context; optional 2D helper (`createDraw2D`) for clear + filled rects.
|
|
8
|
+
- **Loop**: Fixed lifecycle: `init(ctx)` once, then `update(dt)` and `draw()` every frame.
|
|
9
|
+
|
|
10
|
+
Works on **Windows**, **macOS**, and **Linux** (x64; arm64 on Mac).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Create a new game project (CLI)
|
|
19
|
+
|
|
20
|
+
**Without cloning the repo** (recommended):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx create-gamenative-app@latest
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**From the GameNative repo** (local CLI):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run create
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A TUI will prompt for **project name** and **directory**. It then:
|
|
33
|
+
|
|
34
|
+
- Creates a new folder with the framework wired up
|
|
35
|
+
- Sets `game.config.json` (window title, size, default icon)
|
|
36
|
+
- Adds `assets/` and copies the default `icon.png`
|
|
37
|
+
- Adds a starter game at `src/games/Game.ts`
|
|
38
|
+
- Runs `npm install` in the new project
|
|
39
|
+
|
|
40
|
+
Then `cd <directory>/<project-name>` and `npm run dev` to start. Edit `src/games/Game.ts` and `game.config.json` to build your game. The new project includes **FRAMEWORK.md** (2D, 3D, lighting, assets, sound, UI, camera).
|
|
41
|
+
|
|
42
|
+
## Run (dev)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Runs the app from source with the game and window settings from `game.config.json`. Edit code and restart to see changes.
|
|
49
|
+
|
|
50
|
+
## Config: `game.config.json`
|
|
51
|
+
|
|
52
|
+
At the project root. Controls which game runs and the window.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"window": {
|
|
57
|
+
"title": "My Game",
|
|
58
|
+
"width": 800,
|
|
59
|
+
"height": 600,
|
|
60
|
+
"icon": "assets/icon.png"
|
|
61
|
+
},
|
|
62
|
+
"game": "ExampleGame"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **window.title** — Window title.
|
|
67
|
+
- **window.width / height** — Window size.
|
|
68
|
+
- **window.icon** — Optional path to a PNG (relative to project root) for window/taskbar icon.
|
|
69
|
+
- **game** — Name of the game to run (no extension). Must match a file under `src/games/<name>.ts`.
|
|
70
|
+
|
|
71
|
+
## Adding a game
|
|
72
|
+
|
|
73
|
+
1. Add `src/games/MyGame.ts`.
|
|
74
|
+
2. Export a default object that implements `IGame` (`init?`, `update(dt)`, `draw()`, `dispose?`).
|
|
75
|
+
3. Set `"game": "MyGame"` in `game.config.json`.
|
|
76
|
+
|
|
77
|
+
Example game: `src/games/ExampleGame.ts`. Escape to exit.
|
|
78
|
+
|
|
79
|
+
## Build and run
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run build
|
|
83
|
+
npm start
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Runs the compiled app from `dist/` using `game.config.json` in the current directory.
|
|
87
|
+
|
|
88
|
+
## Build to .exe (Windows)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm run build:exe
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Produces `release/GameNative.exe` and copies `game.config.json` into `release/`. Run the exe from the `release/` folder (or put the exe and `game.config.json` in the same folder). You can change window title, icon path, and game name in that config; the exe includes the default game(s) from `dist/games/`.
|
|
95
|
+
|
|
96
|
+
Requires [pkg](https://github.com/vercel/pkg); target is `node18-win-x64`. For other platforms, adjust the `pkg.targets` in `package.json`.
|
|
97
|
+
|
|
98
|
+
## Scripts
|
|
99
|
+
|
|
100
|
+
| Script | Description |
|
|
101
|
+
|---------------|--------------------------------------------------|
|
|
102
|
+
| `npm run dev` | Run from source (tsx) using `game.config.json` |
|
|
103
|
+
| `npm run build` | Compile TypeScript to `dist/` |
|
|
104
|
+
| `npm start` | Run `dist/main.js` (needs `game.config.json`) |
|
|
105
|
+
| `npm run build:exe` | Build then package to `release/GameNative.exe` |
|
|
106
|
+
|
|
107
|
+
## API overview (for game code)
|
|
108
|
+
|
|
109
|
+
- **`run(game, config?)`** — Framework entry; config: `title`, `width`, `height`, `vsync`, `resizable`, `icon`.
|
|
110
|
+
- **`IGame`**: optional `init(ctx, config)`, required `update(dt)`, `draw()`, optional `dispose()`.
|
|
111
|
+
- **`GameContext`**: `gl`, `input`, `width`, `height`.
|
|
112
|
+
- **Input**: `ctx.input.mouseX`, `mouseY`, `mouseLeft`, `mouseRight`, `mouseMiddle`, `isKeyDown(scancode)`.
|
|
113
|
+
- **`SCANCODE`**: e.g. `SCANCODE.W`, `SCANCODE.Escape`.
|
|
114
|
+
- **`createDraw2D(gl)`**: `clear(r,g,b,a?)`, `fillRect(x,y,w,h,r,g,b,a?)`, `dispose()`.
|
|
115
|
+
|
|
116
|
+
For custom rendering, use `ctx.gl` (WebGL 1) directly.
|
package/bin/create.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import inquirer from 'inquirer'
|
|
3
|
+
import { mkdir, writeFile, copyFile, readFile } from 'fs/promises'
|
|
4
|
+
import { join, dirname } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { spawn } from 'child_process'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const FRAMEWORK_ROOT = join(__dirname, '..')
|
|
10
|
+
|
|
11
|
+
function kebab(s) {
|
|
12
|
+
return s
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/\s+/g, '-')
|
|
15
|
+
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
16
|
+
.toLowerCase() || 'my-game'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function copyFrameworkFile(relPath) {
|
|
20
|
+
return readFile(join(FRAMEWORK_ROOT, relPath), 'utf-8')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function scaffold(projectName, parentDir) {
|
|
24
|
+
const name = kebab(projectName)
|
|
25
|
+
const projectPath = join(parentDir, name)
|
|
26
|
+
const src = join(projectPath, 'src')
|
|
27
|
+
const games = join(projectPath, 'src', 'games')
|
|
28
|
+
const assets = join(projectPath, 'assets')
|
|
29
|
+
const scripts = join(projectPath, 'scripts')
|
|
30
|
+
|
|
31
|
+
await mkdir(projectPath, { recursive: true })
|
|
32
|
+
await mkdir(games, { recursive: true })
|
|
33
|
+
await mkdir(assets, { recursive: true })
|
|
34
|
+
await mkdir(scripts, { recursive: true })
|
|
35
|
+
|
|
36
|
+
const [
|
|
37
|
+
mainTs,
|
|
38
|
+
configTs,
|
|
39
|
+
gameTs,
|
|
40
|
+
inputTs,
|
|
41
|
+
graphicsTs,
|
|
42
|
+
typesTs,
|
|
43
|
+
indexTs,
|
|
44
|
+
iconTs,
|
|
45
|
+
pngjsDts,
|
|
46
|
+
copyConfigJs,
|
|
47
|
+
] = await Promise.all([
|
|
48
|
+
copyFrameworkFile('src/main.ts'),
|
|
49
|
+
copyFrameworkFile('src/config.ts'),
|
|
50
|
+
copyFrameworkFile('src/Game.ts'),
|
|
51
|
+
copyFrameworkFile('src/Input.ts'),
|
|
52
|
+
copyFrameworkFile('src/Graphics.ts'),
|
|
53
|
+
copyFrameworkFile('src/types.ts'),
|
|
54
|
+
copyFrameworkFile('src/index.ts'),
|
|
55
|
+
copyFrameworkFile('src/icon.ts'),
|
|
56
|
+
copyFrameworkFile('src/pngjs.d.ts'),
|
|
57
|
+
readFile(join(FRAMEWORK_ROOT, 'scripts/copy-config.js'), 'utf-8'),
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
const gameConfig = {
|
|
61
|
+
window: {
|
|
62
|
+
title: projectName.trim() || name,
|
|
63
|
+
width: 800,
|
|
64
|
+
height: 600,
|
|
65
|
+
icon: 'assets/icon.png',
|
|
66
|
+
},
|
|
67
|
+
game: 'Game',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const packageJson = {
|
|
71
|
+
name,
|
|
72
|
+
version: '0.1.0',
|
|
73
|
+
description: `Game built with GameNative`,
|
|
74
|
+
type: 'module',
|
|
75
|
+
main: 'dist/main.js',
|
|
76
|
+
scripts: {
|
|
77
|
+
dev: 'cross-env NODE_ENV=development tsx src/main.ts',
|
|
78
|
+
build: 'tsc',
|
|
79
|
+
start: 'node dist/main.js',
|
|
80
|
+
'build:exe': 'npm run build && node scripts/copy-config.js && pkg . --output release/' + name + '.exe',
|
|
81
|
+
},
|
|
82
|
+
dependencies: {
|
|
83
|
+
'@kmamal/gl': '^9.1.0',
|
|
84
|
+
'@kmamal/sdl': '^0.11.13',
|
|
85
|
+
pngjs: '^7.0.0',
|
|
86
|
+
},
|
|
87
|
+
devDependencies: {
|
|
88
|
+
'@types/node': '^22.10.0',
|
|
89
|
+
cross-env: '^7.0.3',
|
|
90
|
+
pkg: '^5.8.1',
|
|
91
|
+
tsx: '^4.19.2',
|
|
92
|
+
typescript: '^5.7.0',
|
|
93
|
+
},
|
|
94
|
+
engines: { node: '>=18' },
|
|
95
|
+
pkg: {
|
|
96
|
+
scripts: 'dist/main.js',
|
|
97
|
+
assets: ['node_modules/@kmamal/sdl/**', 'node_modules/@kmamal/gl/**', 'dist/games/**'],
|
|
98
|
+
targets: ['node18-win-x64'],
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const tsconfig = {
|
|
103
|
+
compilerOptions: {
|
|
104
|
+
target: 'ES2022',
|
|
105
|
+
module: 'NodeNext',
|
|
106
|
+
moduleResolution: 'NodeNext',
|
|
107
|
+
outDir: 'dist',
|
|
108
|
+
rootDir: '.',
|
|
109
|
+
strict: true,
|
|
110
|
+
declaration: true,
|
|
111
|
+
declarationMap: true,
|
|
112
|
+
sourceMap: true,
|
|
113
|
+
skipLibCheck: true,
|
|
114
|
+
esModuleInterop: true,
|
|
115
|
+
forceConsistentCasingInFileNames: true,
|
|
116
|
+
},
|
|
117
|
+
include: ['src/**/*.ts'],
|
|
118
|
+
exclude: ['node_modules', 'dist'],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const gameTemplate = `import { createDraw2D, SCANCODE } from '../index.js'
|
|
122
|
+
import type { GameContext, IGame } from '../types.js'
|
|
123
|
+
|
|
124
|
+
interface Game extends IGame {
|
|
125
|
+
ctx: GameContext | null
|
|
126
|
+
draw2d: ReturnType<typeof createDraw2D> | null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const game: Game = {
|
|
130
|
+
ctx: null,
|
|
131
|
+
draw2d: null,
|
|
132
|
+
|
|
133
|
+
async init(ctx: GameContext) {
|
|
134
|
+
this.ctx = ctx
|
|
135
|
+
this.draw2d = createDraw2D(ctx.gl)
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
update(dt: number) {
|
|
139
|
+
if (this.ctx?.input.isKeyDown(SCANCODE.Escape)) process.exit(0)
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
draw() {
|
|
143
|
+
this.draw2d?.clear(0.08, 0.08, 0.12, 1)
|
|
144
|
+
this.draw2d?.fillRect(100, 100, 120, 80, 0.2, 0.5, 0.9, 1)
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
dispose() {
|
|
148
|
+
this.draw2d?.dispose()
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default game
|
|
153
|
+
`
|
|
154
|
+
|
|
155
|
+
await Promise.all([
|
|
156
|
+
writeFile(join(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2)),
|
|
157
|
+
writeFile(join(projectPath, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2)),
|
|
158
|
+
writeFile(join(projectPath, 'game.config.json'), JSON.stringify(gameConfig, null, 2)),
|
|
159
|
+
writeFile(join(src, 'main.ts'), mainTs),
|
|
160
|
+
writeFile(join(src, 'config.ts'), configTs),
|
|
161
|
+
writeFile(join(src, 'Game.ts'), gameTs),
|
|
162
|
+
writeFile(join(src, 'Input.ts'), inputTs),
|
|
163
|
+
writeFile(join(src, 'Graphics.ts'), graphicsTs),
|
|
164
|
+
writeFile(join(src, 'types.ts'), typesTs),
|
|
165
|
+
writeFile(join(src, 'index.ts'), indexTs),
|
|
166
|
+
writeFile(join(src, 'icon.ts'), iconTs),
|
|
167
|
+
writeFile(join(src, 'pngjs.d.ts'), pngjsDts),
|
|
168
|
+
writeFile(join(games, 'Game.ts'), gameTemplate),
|
|
169
|
+
writeFile(join(scripts, 'copy-config.js'), copyConfigJs),
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
const iconSrc = join(FRAMEWORK_ROOT, 'src', 'assets', 'icon.png')
|
|
173
|
+
try {
|
|
174
|
+
await copyFile(iconSrc, join(assets, 'icon.png'))
|
|
175
|
+
} catch {
|
|
176
|
+
// no icon if missing
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const readme = `# ${projectName.trim() || name}
|
|
180
|
+
|
|
181
|
+
Game built with [GameNative](https://github.com/your-org/GameNative).
|
|
182
|
+
|
|
183
|
+
- \`npm run dev\` — run from source
|
|
184
|
+
- \`npm run build\` then \`npm start\` — run built app
|
|
185
|
+
- Edit \`game.config.json\` for window title, size, icon
|
|
186
|
+
- Edit \`src/games/Game.ts\` to build your game
|
|
187
|
+
|
|
188
|
+
See FRAMEWORK.md for 2D, 3D, lighting, sound, UI, and camera.
|
|
189
|
+
`
|
|
190
|
+
const frameworkMd = `# GameNative capabilities
|
|
191
|
+
|
|
192
|
+
Your game has access to:
|
|
193
|
+
|
|
194
|
+
- **2D**: \`createDraw2D(ctx.gl)\` — \`clear()\`, \`fillRect()\`. Use \`ctx.gl\` (WebGL 1) for textures, sprites, batching.
|
|
195
|
+
- **3D**: Use \`ctx.gl\` directly: buffers, shaders, matrices. Implement camera (view/projection), meshes, and lighting in shaders.
|
|
196
|
+
- **Lighting**: In 3D shaders use uniforms for light position/color; in 2D use tint or custom fragment shaders.
|
|
197
|
+
- **Assets**: Load images (decode PNG with pngjs or similar), upload to \`ctx.gl\` textures; load audio (see sound).
|
|
198
|
+
- **Sound**: \`ctx.sdl.audio\` (SDL audio) — open device, enqueue buffers. Or add a small wrapper in your game.
|
|
199
|
+
- **Visuals**: Full WebGL — post-process by rendering to framebuffer, then to screen; particles, blur, etc. in shaders.
|
|
200
|
+
- **UI**: Draw quads with \`fillRect\` or textured quads; track \`ctx.input.mouseX/Y\` and \`mouseLeft\` for clicks; implement panels/buttons in \`update\`/\`draw\`.
|
|
201
|
+
- **Camera**: 2D: store \`offsetX, offsetY, scale\` and pass to your draw calls or a uniform. 3D: \`view\` and \`projection\` matrices from position/target/up and perspective.
|
|
202
|
+
|
|
203
|
+
All of this is done in your \`Game.ts\` using \`ctx.gl\`, \`ctx.input\`, and optional helpers you add.
|
|
204
|
+
`
|
|
205
|
+
await writeFile(join(projectPath, 'README.md'), readme)
|
|
206
|
+
await writeFile(join(projectPath, 'FRAMEWORK.md'), frameworkMd)
|
|
207
|
+
|
|
208
|
+
return { projectPath: projectPath, name }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function npmInstall(cwd) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const isWin = process.platform === 'win32'
|
|
214
|
+
const child = spawn(isWin ? 'npm.cmd' : 'npm', ['install'], {
|
|
215
|
+
cwd,
|
|
216
|
+
stdio: 'inherit',
|
|
217
|
+
shell: isWin,
|
|
218
|
+
})
|
|
219
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error('npm install failed'))))
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function main() {
|
|
224
|
+
console.log('\n GameNative — create a new game project\n')
|
|
225
|
+
const answers = await inquirer.prompt([
|
|
226
|
+
{
|
|
227
|
+
type: 'input',
|
|
228
|
+
name: 'projectName',
|
|
229
|
+
message: 'Project name:',
|
|
230
|
+
default: 'my-game',
|
|
231
|
+
validate: (v) => (v?.trim() ? true : 'Enter a name'),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: 'input',
|
|
235
|
+
name: 'directory',
|
|
236
|
+
message: 'Create project in directory:',
|
|
237
|
+
default: '.',
|
|
238
|
+
},
|
|
239
|
+
])
|
|
240
|
+
const dir = answers.directory?.trim() || '.'
|
|
241
|
+
const resolvedDir = join(process.cwd(), dir)
|
|
242
|
+
console.log('\n Creating project...')
|
|
243
|
+
const { projectPath, name } = await scaffold(answers.projectName, resolvedDir)
|
|
244
|
+
console.log(' Installing dependencies...')
|
|
245
|
+
await npmInstall(projectPath)
|
|
246
|
+
console.log('\n Done!\n')
|
|
247
|
+
console.log(' Next:\n')
|
|
248
|
+
console.log(` cd ${dir}/${name}`)
|
|
249
|
+
console.log(' npm run dev\n')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main().catch((err) => {
|
|
253
|
+
console.error(err)
|
|
254
|
+
process.exit(1)
|
|
255
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# create-gamenative-app
|
|
2
|
+
|
|
3
|
+
Scaffold a new [GameNative](https://github.com/your-org/GameNative) desktop game project.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-gamenative-app@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You’ll be prompted for **project name** and **directory**. The CLI creates the project, installs dependencies, and copies the default window icon into `assets/`.
|
|
12
|
+
|
|
13
|
+
Then:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd <directory>/<project-name>
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Publish (maintainers)
|
|
21
|
+
|
|
22
|
+
From the GameNative repo:
|
|
23
|
+
|
|
24
|
+
1. Update framework code in `src/`, `scripts/`, etc. as needed.
|
|
25
|
+
2. From `create-gamenative-app/`: run `node sync-template.js` to refresh `template/` from the parent repo (or rely on `prepublishOnly`).
|
|
26
|
+
3. Bump version in `create-gamenative-app/package.json`, then:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd create-gamenative-app
|
|
30
|
+
npm publish
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Users can then run `npx create-gamenative-app@latest` to get the new template.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import inquirer from 'inquirer'
|
|
3
|
+
import { mkdir, readFile, writeFile, copyFile } from 'fs/promises'
|
|
4
|
+
import { join, dirname } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { spawn } from 'child_process'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const TEMPLATE_DIR = join(__dirname, 'template')
|
|
10
|
+
|
|
11
|
+
function kebab(s) {
|
|
12
|
+
return s
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/\s+/g, '-')
|
|
15
|
+
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
16
|
+
.toLowerCase() || 'my-game'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TEMPLATE_FILES = [
|
|
20
|
+
'package.json',
|
|
21
|
+
'tsconfig.json',
|
|
22
|
+
'game.config.json',
|
|
23
|
+
'README.md',
|
|
24
|
+
'FRAMEWORK.md',
|
|
25
|
+
'src/main.ts',
|
|
26
|
+
'src/config.ts',
|
|
27
|
+
'src/Game.ts',
|
|
28
|
+
'src/Input.ts',
|
|
29
|
+
'src/Graphics.ts',
|
|
30
|
+
'src/types.ts',
|
|
31
|
+
'src/index.ts',
|
|
32
|
+
'src/icon.ts',
|
|
33
|
+
'src/pngjs.d.ts',
|
|
34
|
+
'src/games/Game.ts',
|
|
35
|
+
'scripts/copy-config.js',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
async function scaffold(projectName, parentDir) {
|
|
39
|
+
const name = kebab(projectName)
|
|
40
|
+
const title = projectName.trim() || name
|
|
41
|
+
const projectPath = join(parentDir, name)
|
|
42
|
+
|
|
43
|
+
await mkdir(projectPath, { recursive: true })
|
|
44
|
+
await mkdir(join(projectPath, 'src', 'games'), { recursive: true })
|
|
45
|
+
await mkdir(join(projectPath, 'scripts'), { recursive: true })
|
|
46
|
+
await mkdir(join(projectPath, 'assets'), { recursive: true })
|
|
47
|
+
|
|
48
|
+
for (const rel of TEMPLATE_FILES) {
|
|
49
|
+
const src = join(TEMPLATE_DIR, rel)
|
|
50
|
+
const dest = join(projectPath, rel)
|
|
51
|
+
const content = await readFile(src, 'utf-8')
|
|
52
|
+
const out = content.replace(/\{\{NAME\}\}/g, name).replace(/\{\{TITLE\}\}/g, title)
|
|
53
|
+
await writeFile(dest, out)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await copyFile(join(TEMPLATE_DIR, 'assets', 'icon.png'), join(projectPath, 'assets', 'icon.png'))
|
|
58
|
+
} catch {}
|
|
59
|
+
|
|
60
|
+
return { projectPath, name }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function npmInstall(cwd) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const isWin = process.platform === 'win32'
|
|
66
|
+
const child = spawn(isWin ? 'npm.cmd' : 'npm', ['install'], {
|
|
67
|
+
cwd,
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
shell: isWin,
|
|
70
|
+
})
|
|
71
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error('npm install failed'))))
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
console.log('\n GameNative — create a new game project\n')
|
|
77
|
+
const answers = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: 'input',
|
|
80
|
+
name: 'projectName',
|
|
81
|
+
message: 'Project name:',
|
|
82
|
+
default: 'my-game',
|
|
83
|
+
validate: (v) => (v?.trim() ? true : 'Enter a name'),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'input',
|
|
87
|
+
name: 'directory',
|
|
88
|
+
message: 'Create project in directory:',
|
|
89
|
+
default: '.',
|
|
90
|
+
},
|
|
91
|
+
])
|
|
92
|
+
const dir = answers.directory?.trim() || '.'
|
|
93
|
+
const resolvedDir = join(process.cwd(), dir)
|
|
94
|
+
console.log('\n Creating project...')
|
|
95
|
+
const { projectPath, name } = await scaffold(answers.projectName, resolvedDir)
|
|
96
|
+
console.log(' Installing dependencies...')
|
|
97
|
+
await npmInstall(projectPath)
|
|
98
|
+
console.log('\n Done!\n')
|
|
99
|
+
console.log(' Next:\n')
|
|
100
|
+
console.log(` cd ${dir}/${name}`)
|
|
101
|
+
console.log(' npm run dev\n')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
main().catch((err) => {
|
|
105
|
+
console.error(err)
|
|
106
|
+
process.exit(1)
|
|
107
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-gamenative-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new GameNative desktop game project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-gamenative-app": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"template"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"prepublishOnly": "node sync-template.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"inquirer": "^9.2.23"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"gamenative",
|
|
25
|
+
"game",
|
|
26
|
+
"sdl",
|
|
27
|
+
"webgl",
|
|
28
|
+
"typescript",
|
|
29
|
+
"scaffold",
|
|
30
|
+
"create-app"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|