toaimin-indie-engine 1.0.1 → 1.0.4
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 +121 -37
- package/cli/bin.cjs +75 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,55 +1,139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# toaimin-indie-engine 🚀
|
|
2
2
|
|
|
3
|
-
A production-grade,
|
|
3
|
+
A production-grade, highly scalable, and developer-friendly **2D Game Engine** built purely in TypeScript. Designed specifically for Indie Developers who want maximum performance with minimum boilerplate.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
|
+

|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- **Fixed-Timestep Simulation**: 60Hz deterministic game loop with render interpolation (`alpha`) and spiral-of-death protection.
|
|
9
|
-
- **Robust Math & Affine Transforms**:
|
|
10
|
-
- `Vector2`: High-performance 2D vector with mutable in-place chaining, immutable helpers, and zero-division guards.
|
|
11
|
-
- `Matrix2D`: 3x3 Canvas-compatible affine transformation matrix with determinant, inversion, point/direction transform.
|
|
12
|
-
- `Transform2D`: Hierarchical scene-graph transform with parent-child propagation and lazy dirty-flag caching.
|
|
13
|
-
- `Rect` & `Circle`: 2D spatial primitives with comprehensive intersection and containment algorithms.
|
|
14
|
-
- `MathUtils`: Frame-rate independent exponential damping (`damp`), angle wrapping, clamp, lerp, and RNG helpers.
|
|
15
|
-
- `Color`: High-precision RGBA color with Hex, RGB, RGBA, HSL parsing and string formatting.
|
|
16
|
-
- **Core Engine Lifecycle**:
|
|
17
|
-
- `GameLoop`: Accumulator-based loop separating `update(dt)`, `fixedUpdate(fixedDt)`, and `render(alpha)`.
|
|
18
|
-
- `Time`: Scaled and unscaled delta time tracking with rolling average FPS counter.
|
|
19
|
-
- `EventEmitter`: Type-safe pub/sub event bus with snapshot iteration and unbind helpers.
|
|
8
|
+
## 🌟 Key Features
|
|
20
9
|
|
|
21
|
-
|
|
10
|
+
* **Fluent API (Builder Pattern):** Create complex game entities without classes. Chain `.withSprite().withPhysics()` seamlessly.
|
|
11
|
+
* **CLI Tool (`indie-engine`):** Generate a complete game project with `init`, run with `dev`, and package with `build` in one click.
|
|
12
|
+
* **Tiled Map Integration (v1.0.1):** Load `.json` tilemaps exported from the popular *Tiled Editor*. Automatically generates static physics colliders for your map layers!
|
|
13
|
+
* **Real-time Multiplayer (v1.0.2):** Built-in Native WebSocket client and `NetworkIdentity` component. Sync player positions globally with zero extra libraries.
|
|
14
|
+
* **Tweening & Easing (v1.0.3):** Butter-smooth animations for UI and sprites. 12+ Easing curves included (Bounce, Elastic, Quad, etc).
|
|
15
|
+
* **Robust Physics 2D:** Rigid bodies, Box/Circle colliders, and contact solvers built-in.
|
|
16
|
+
* **Zero Heavy Dependencies:** Extremely lightweight and fast.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 Quick Start (CLI)
|
|
21
|
+
|
|
22
|
+
No need to configure Webpack or Vite. Just use our global CLI!
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
|
-
#
|
|
25
|
-
|
|
25
|
+
# 1. Create a new game project
|
|
26
|
+
npx toaimin-indie-engine init my-awesome-game
|
|
26
27
|
|
|
27
|
-
#
|
|
28
|
-
|
|
28
|
+
# 2. Go to your folder
|
|
29
|
+
cd my-awesome-game
|
|
29
30
|
|
|
30
|
-
#
|
|
31
|
+
# 3. Start the dev server
|
|
31
32
|
npm run dev
|
|
32
33
|
|
|
33
|
-
# Build
|
|
34
|
-
npm run build:
|
|
34
|
+
# 4. Build for Android/iOS (Auto-installs Capacitor & SDK wrappers)
|
|
35
|
+
npm run build:android
|
|
36
|
+
npm run build:ios
|
|
37
|
+
|
|
38
|
+
# 5. Build for Desktop PC/Mac (Auto-installs Electron)
|
|
39
|
+
npm run build:desktop
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🎮 Writing Code the "Indie" Way
|
|
45
|
+
|
|
46
|
+
Forget messy boilerplate. Here is how you create a moving player:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { createIndieGame, createEntity, Scene } from 'toaimin-indie-engine';
|
|
50
|
+
|
|
51
|
+
class MainScene extends Scene {
|
|
52
|
+
override onStart() {
|
|
53
|
+
// Spawn a player with Physics and Sprite!
|
|
54
|
+
createEntity('Player')
|
|
55
|
+
.withPosition(400, 300)
|
|
56
|
+
.withSprite('assets/ninja.png')
|
|
57
|
+
.withPhysics({ type: 'dynamic', mass: 10 })
|
|
58
|
+
.withBoxCollider(64, 64)
|
|
59
|
+
.onUpdate((dt, entity) => {
|
|
60
|
+
// Your logic here (e.g. Move right)
|
|
61
|
+
entity.transform.position.x += 100 * dt;
|
|
62
|
+
})
|
|
63
|
+
.build()
|
|
64
|
+
.setParent(this.root);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Bootstrap the game!
|
|
69
|
+
createIndieGame()
|
|
70
|
+
.setSize(800, 600)
|
|
71
|
+
.setBackground('#222')
|
|
72
|
+
.addScene('Main', MainScene)
|
|
73
|
+
.start();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🗺️ Tiled Map Support (Auto Collisions)
|
|
79
|
+
|
|
80
|
+
Draw your map in *Tiled*, export as JSON, and load it into your game. If you name your collision layer `"Collisions"`, the engine will automatically generate invisible walls for you!
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { createTilemap } from 'toaimin-indie-engine';
|
|
84
|
+
|
|
85
|
+
const map = createTilemap('assets/level1.json', 'Collisions');
|
|
86
|
+
map.setParent(this.root);
|
|
35
87
|
```
|
|
36
88
|
|
|
37
|
-
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 🌐 Real-time Multiplayer Plugin
|
|
92
|
+
|
|
93
|
+
Make your game online in literally 2 steps:
|
|
38
94
|
|
|
95
|
+
```typescript
|
|
96
|
+
import { MultiplayerPlugin, createEntity } from 'toaimin-indie-engine';
|
|
97
|
+
|
|
98
|
+
// 1. Enable Plugin
|
|
99
|
+
createIndieGame()
|
|
100
|
+
.use(new MultiplayerPlugin({ url: 'wss://your-server.com' }))
|
|
101
|
+
.start();
|
|
102
|
+
|
|
103
|
+
// 2. Add NetworkIdentity to sync position globally
|
|
104
|
+
createEntity('OnlinePlayer')
|
|
105
|
+
.withPosition(100, 100)
|
|
106
|
+
.withNetworkIdentity('player_123', true) // true = Local Player
|
|
107
|
+
.build();
|
|
39
108
|
```
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🎬 Tweening Animations
|
|
113
|
+
|
|
114
|
+
Animate UI buttons, coins, or anything smoothly!
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { tween } from 'toaimin-indie-engine';
|
|
118
|
+
|
|
119
|
+
// Make a coin bounce up
|
|
120
|
+
tween(coin.transform.position)
|
|
121
|
+
.to({ y: coin.transform.position.y - 50 }, 1000)
|
|
122
|
+
.easing('BounceOut')
|
|
123
|
+
.start();
|
|
51
124
|
```
|
|
52
125
|
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🏛️ Architecture
|
|
129
|
+
|
|
130
|
+
If you prefer the hardcore OOP way, the engine fully supports ECS (Entity Component System):
|
|
131
|
+
|
|
132
|
+
* `Game`: Core Orchestrator
|
|
133
|
+
* `Entity`: The Node/GameObject
|
|
134
|
+
* `Component`: Behavior logic (e.g., `ScriptComponent`, `SpriteComponent`)
|
|
135
|
+
* `System`: PhysicsWorld2D, SceneManager, InputManager
|
|
136
|
+
|
|
53
137
|
## License
|
|
54
138
|
|
|
55
|
-
MIT
|
|
139
|
+
MIT © Toaimin
|
package/cli/bin.cjs
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { spawn } = require('child_process');
|
|
4
|
+
const { spawn, execSync } = require('child_process');
|
|
5
5
|
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
7
|
const command = args[0];
|
|
8
8
|
|
|
9
9
|
if (!command) {
|
|
10
10
|
console.log('Usage: indie-engine <command>');
|
|
11
|
-
console.log('Commands: init, dev, build');
|
|
11
|
+
console.log('Commands: init, dev, build, build:android, build:ios, build:desktop');
|
|
12
12
|
process.exit(1);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const runCommand = (cmd, cwd = process.cwd()) => {
|
|
16
|
+
console.log(`> ${cmd}`);
|
|
17
|
+
execSync(cmd, { stdio: 'inherit', cwd });
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
if (command === 'init') {
|
|
16
21
|
const projectName = args[1] || 'my-game';
|
|
17
22
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
@@ -31,19 +36,19 @@ if (command === 'init') {
|
|
|
31
36
|
private: true,
|
|
32
37
|
scripts: {
|
|
33
38
|
dev: "indie-engine dev",
|
|
34
|
-
build: "indie-engine build"
|
|
39
|
+
build: "indie-engine build",
|
|
40
|
+
"build:android": "indie-engine build:android",
|
|
41
|
+
"build:ios": "indie-engine build:ios",
|
|
42
|
+
"build:desktop": "indie-engine build:desktop"
|
|
35
43
|
},
|
|
36
44
|
dependencies: {
|
|
37
|
-
"
|
|
45
|
+
"toaimin-indie-engine": "latest"
|
|
38
46
|
}
|
|
39
47
|
};
|
|
40
48
|
|
|
41
|
-
fs.writeFileSync(
|
|
42
|
-
path.join(targetDir, 'package.json'),
|
|
43
|
-
JSON.stringify(pkgJson, null, 2)
|
|
44
|
-
);
|
|
49
|
+
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
45
50
|
|
|
46
|
-
const mainTs = `import { createIndieGame, createEntity, Scene } from '
|
|
51
|
+
const mainTs = `import { createIndieGame, createEntity, Scene } from 'toaimin-indie-engine';
|
|
47
52
|
|
|
48
53
|
class MainScene extends Scene {
|
|
49
54
|
override onStart() {
|
|
@@ -51,9 +56,6 @@ class MainScene extends Scene {
|
|
|
51
56
|
.withPosition(400, 300)
|
|
52
57
|
.withSprite({ texture: 'assets/player.png', width: 64, height: 64 })
|
|
53
58
|
.withPhysics({ type: 'dynamic', mass: 10 })
|
|
54
|
-
.onUpdate((dt, entity) => {
|
|
55
|
-
// Player logic here
|
|
56
|
-
})
|
|
57
59
|
.build()
|
|
58
60
|
.setParent(this.root);
|
|
59
61
|
}
|
|
@@ -73,10 +75,7 @@ createIndieGame()
|
|
|
73
75
|
<head>
|
|
74
76
|
<meta charset="UTF-8">
|
|
75
77
|
<title>${projectName}</title>
|
|
76
|
-
<style>
|
|
77
|
-
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
|
78
|
-
canvas { display: block; }
|
|
79
|
-
</style>
|
|
78
|
+
<style>body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }</style>
|
|
80
79
|
</head>
|
|
81
80
|
<body>
|
|
82
81
|
<script type="module" src="/src/main.ts"></script>
|
|
@@ -86,13 +85,70 @@ createIndieGame()
|
|
|
86
85
|
fs.writeFileSync(path.join(targetDir, 'index.html'), html);
|
|
87
86
|
|
|
88
87
|
console.log(`Created project ${projectName}`);
|
|
89
|
-
console.log(`cd ${projectName}`);
|
|
90
|
-
console.log(`npm install`);
|
|
91
|
-
console.log(`npm run dev`);
|
|
88
|
+
console.log(`cd ${projectName} && npm install`);
|
|
92
89
|
} else if (command === 'dev' || command === 'build') {
|
|
93
90
|
const viteRunnerPath = path.join(__dirname, 'vite-runner.cjs');
|
|
94
91
|
const child = spawn('node', [viteRunnerPath, command], { stdio: 'inherit' });
|
|
95
92
|
child.on('exit', (code) => process.exit(code));
|
|
93
|
+
} else if (command === 'build:android' || command === 'build:ios') {
|
|
94
|
+
const platform = command.split(':')[1];
|
|
95
|
+
console.log(`🚀 Building for ${platform.toUpperCase()}...`);
|
|
96
|
+
|
|
97
|
+
// 1. Build web first
|
|
98
|
+
const viteRunnerPath = path.join(__dirname, 'vite-runner.cjs');
|
|
99
|
+
runCommand(`node "${viteRunnerPath}" build`);
|
|
100
|
+
|
|
101
|
+
// 2. Init capacitor if missing
|
|
102
|
+
if (!fs.existsSync(path.join(process.cwd(), 'capacitor.config.json'))) {
|
|
103
|
+
console.log('Installing Capacitor CLI...');
|
|
104
|
+
runCommand('npm install @capacitor/cli @capacitor/core');
|
|
105
|
+
runCommand(`npx cap init mygame com.indie.game --web-dir dist`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Add platform if missing
|
|
109
|
+
if (!fs.existsSync(path.join(process.cwd(), platform))) {
|
|
110
|
+
console.log(`Installing Capacitor ${platform}...`);
|
|
111
|
+
runCommand(`npm install @capacitor/${platform}`);
|
|
112
|
+
runCommand(`npx cap add ${platform}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 4. Sync and open
|
|
116
|
+
runCommand(`npx cap sync ${platform}`);
|
|
117
|
+
console.log(`✅ Ready! Run 'npx cap open ${platform}' to build the binary.`);
|
|
118
|
+
} else if (command === 'build:desktop') {
|
|
119
|
+
console.log(`🚀 Building for DESKTOP (Electron)...`);
|
|
120
|
+
|
|
121
|
+
// 1. Build web
|
|
122
|
+
const viteRunnerPath = path.join(__dirname, 'vite-runner.cjs');
|
|
123
|
+
runCommand(`node "${viteRunnerPath}" build`);
|
|
124
|
+
|
|
125
|
+
// 2. Setup Electron
|
|
126
|
+
if (!fs.existsSync(path.join(process.cwd(), 'electron'))) {
|
|
127
|
+
fs.mkdirSync(path.join(process.cwd(), 'electron'));
|
|
128
|
+
const mainJs = `
|
|
129
|
+
const { app, BrowserWindow } = require('electron');
|
|
130
|
+
const path = require('path');
|
|
131
|
+
function createWindow() {
|
|
132
|
+
const win = new BrowserWindow({ width: 800, height: 600 });
|
|
133
|
+
win.loadFile(path.join(__dirname, '../dist/index.html'));
|
|
134
|
+
}
|
|
135
|
+
app.whenReady().then(createWindow);
|
|
136
|
+
`;
|
|
137
|
+
fs.writeFileSync(path.join(process.cwd(), 'electron', 'main.js'), mainJs);
|
|
138
|
+
|
|
139
|
+
// Modify package.json to point to main
|
|
140
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
141
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
142
|
+
pkg.main = "electron/main.js";
|
|
143
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
144
|
+
|
|
145
|
+
console.log('Installing Electron dependencies...');
|
|
146
|
+
runCommand('npm install electron electron-builder --save-dev');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(`✅ Desktop structure ready!`);
|
|
150
|
+
console.log(`Test: npx electron .`);
|
|
151
|
+
console.log(`Build: npx electron-builder --mac --win --linux`);
|
|
96
152
|
} else {
|
|
97
153
|
console.error(`Unknown command: ${command}`);
|
|
98
154
|
process.exit(1);
|