bloody-engine 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bloody Engine Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Bloody Engine
2
+
3
+ A WebGL-based 2.5D graphics engine for isometric rendering, written in TypeScript. Designed for both browser and Node.js environments with full isomorphic support.
4
+
5
+ ## Features
6
+
7
+ - **2.5D Rendering** - Optimized for isometric and dimetric projections
8
+ - **Cross-Platform** - Works in browsers and Node.js (headless rendering)
9
+ - **Batch Rendering** - Efficient sprite batching with GPU-accelerated transformations
10
+ - **Resource Management** - Unified asset loading pipeline for textures and resources
11
+ - **TypeScript** - Fully typed for excellent developer experience
12
+ - **Depth Sorting** - Proper 2.5D occlusion handling
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install bloody-engine
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### Browser
23
+
24
+ ```typescript
25
+ import { BloodyEngine } from 'bloody-engine';
26
+
27
+ // Initialize engine
28
+ const engine = new BloodyEngine({
29
+ width: 800,
30
+ height: 600
31
+ });
32
+
33
+ // Start rendering loop
34
+ engine.start();
35
+ ```
36
+
37
+ ### Node.js
38
+
39
+ ```typescript
40
+ import { BloodyEngine } from 'bloody-engine';
41
+
42
+ // Initialize engine for headless rendering
43
+ const engine = new BloodyEngine({
44
+ width: 800,
45
+ height: 600,
46
+ headless: true
47
+ });
48
+
49
+ // Render and capture output
50
+ engine.renderFrame();
51
+ const pixels = engine.getPixels();
52
+ ```
53
+
54
+ ## Documentation
55
+
56
+ For detailed documentation and architecture, see [docs/README.MD](docs/README.MD).
57
+
58
+ ## Examples
59
+
60
+ ```typescript
61
+ // Create a sprite batch renderer
62
+ import { SpriteBatchRenderer, Texture } from 'bloody-engine';
63
+
64
+ const batchRenderer = new SpriteBatchRenderer(gl, shader);
65
+
66
+ // Add sprites
67
+ batchRenderer.addQuad({
68
+ x: 100,
69
+ y: 100,
70
+ z: 0,
71
+ width: 64,
72
+ height: 64,
73
+ rotation: 0,
74
+ color: { r: 1, g: 1, b: 1, a: 1 },
75
+ uvRect: { uMin: 0, vMin: 0, uMax: 1, vMax: 1 }
76
+ });
77
+
78
+ // Render
79
+ batchRenderer.render(camera);
80
+ ```
81
+
82
+ ## License
83
+
84
+ MIT License - see [LICENSE](LICENSE) for details.
85
+
86
+ ## Repository
87
+
88
+ [https://github.com/BLooDek/bloody-engine](https://github.com/BLooDek/bloody-engine)
89
+
90
+ ## Issues
91
+
92
+ Report bugs and request features at: [https://github.com/BLooDek/bloody-engine/issues](https://github.com/BLooDek/bloody-engine/issues)
@@ -0,0 +1,308 @@
1
+ class SpriteBatchRenderer {
2
+ /**
3
+ * Create a new sprite batch renderer (V2)
4
+ * @param gl WebGL rendering context
5
+ * @param shader Shader program to use (should be SHADERS_V2)
6
+ * @param maxQuads Maximum number of quads to batch (default 1000)
7
+ */
8
+ constructor(gl, shader, maxQuads = 1e3) {
9
+ this.vertexBuffer = null;
10
+ this.quads = [];
11
+ this.isDirty = false;
12
+ this.verticesPerQuad = 6;
13
+ this.floatsPerVertex = 10;
14
+ this.texture = null;
15
+ this.depthTestEnabled = true;
16
+ this.gl = gl;
17
+ this.shader = shader;
18
+ this.maxQuads = maxQuads;
19
+ const totalFloats = maxQuads * this.verticesPerQuad * this.floatsPerVertex;
20
+ this.vertexData = new Float32Array(totalFloats);
21
+ const buf = gl.createBuffer();
22
+ if (!buf) {
23
+ throw new Error("Failed to create vertex buffer");
24
+ }
25
+ this.vertexBuffer = buf;
26
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
27
+ gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
28
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
29
+ }
30
+ /**
31
+ * Set the texture for batch rendering
32
+ * @param texture The texture to use when rendering
33
+ */
34
+ setTexture(texture) {
35
+ this.texture = texture;
36
+ }
37
+ /**
38
+ * Add a sprite quad to the batch
39
+ * @param quad Sprite quad instance to add
40
+ */
41
+ addQuad(quad) {
42
+ if (this.quads.length >= this.maxQuads) {
43
+ console.warn(`Sprite batch renderer at max capacity (${this.maxQuads})`);
44
+ return;
45
+ }
46
+ this.quads.push(quad);
47
+ this.isDirty = true;
48
+ }
49
+ /**
50
+ * Clear all quads from the batch
51
+ */
52
+ clear() {
53
+ this.quads = [];
54
+ this.isDirty = true;
55
+ }
56
+ /**
57
+ * Get number of quads currently in batch
58
+ */
59
+ getQuadCount() {
60
+ return this.quads.length;
61
+ }
62
+ /**
63
+ * Update the batch - rebuilds vertex buffer if quads changed
64
+ */
65
+ update() {
66
+ if (!this.isDirty || this.quads.length === 0) {
67
+ return;
68
+ }
69
+ let vertexIndex = 0;
70
+ for (const quad of this.quads) {
71
+ const {
72
+ x,
73
+ y,
74
+ z = 0,
75
+ width,
76
+ height,
77
+ rotation,
78
+ color = { r: 1, g: 1, b: 1, a: 1 },
79
+ uvRect = { uMin: 0, vMin: 0, uMax: 1, vMax: 1 },
80
+ texIndex = 0
81
+ } = quad;
82
+ const vertices = this.generateQuadVertices({
83
+ x,
84
+ y,
85
+ z,
86
+ width,
87
+ height,
88
+ rotation,
89
+ color,
90
+ uvRect,
91
+ texIndex
92
+ });
93
+ for (const vertex of vertices) {
94
+ this.vertexData[vertexIndex++] = vertex.x;
95
+ this.vertexData[vertexIndex++] = vertex.y;
96
+ this.vertexData[vertexIndex++] = vertex.z;
97
+ this.vertexData[vertexIndex++] = vertex.u;
98
+ this.vertexData[vertexIndex++] = vertex.v;
99
+ this.vertexData[vertexIndex++] = vertex.r;
100
+ this.vertexData[vertexIndex++] = vertex.g;
101
+ this.vertexData[vertexIndex++] = vertex.b;
102
+ this.vertexData[vertexIndex++] = vertex.a;
103
+ this.vertexData[vertexIndex++] = vertex.texIndex;
104
+ }
105
+ }
106
+ if (this.vertexBuffer) {
107
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
108
+ this.gl.bufferSubData(
109
+ this.gl.ARRAY_BUFFER,
110
+ 0,
111
+ this.vertexData.subarray(0, vertexIndex)
112
+ );
113
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
114
+ }
115
+ this.isDirty = false;
116
+ }
117
+ /**
118
+ * Set whether depth testing is enabled
119
+ * When enabled, sprites with lower Z values appear behind sprites with higher Z values
120
+ * @param enabled Whether to enable depth testing (default true)
121
+ */
122
+ setDepthTestEnabled(enabled) {
123
+ this.depthTestEnabled = enabled;
124
+ }
125
+ /**
126
+ * Render the batch
127
+ * @param camera Optional camera for view transform (defaults to identity matrix)
128
+ */
129
+ render(camera) {
130
+ if (this.quads.length === 0) {
131
+ return;
132
+ }
133
+ this.update();
134
+ this.shader.use();
135
+ if (this.depthTestEnabled) {
136
+ this.gl.enable(this.gl.DEPTH_TEST);
137
+ this.gl.depthFunc(this.gl.LEQUAL);
138
+ } else {
139
+ this.gl.disable(this.gl.DEPTH_TEST);
140
+ }
141
+ if (this.vertexBuffer) {
142
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
143
+ const posAttr = this.shader.getAttributeLocation("aPosition");
144
+ const texCoordAttr = this.shader.getAttributeLocation("aTexCoord");
145
+ const colorAttr = this.shader.getAttributeLocation("aColor");
146
+ const texIndexAttr = this.shader.getAttributeLocation("aTexIndex");
147
+ const stride = this.floatsPerVertex * 4;
148
+ if (posAttr !== -1) {
149
+ this.gl.enableVertexAttribArray(posAttr);
150
+ this.gl.vertexAttribPointer(
151
+ posAttr,
152
+ 3,
153
+ // 3 floats (x, y, z)
154
+ this.gl.FLOAT,
155
+ false,
156
+ stride,
157
+ 0
158
+ // offset
159
+ );
160
+ }
161
+ if (texCoordAttr !== -1) {
162
+ this.gl.enableVertexAttribArray(texCoordAttr);
163
+ this.gl.vertexAttribPointer(
164
+ texCoordAttr,
165
+ 2,
166
+ // 2 floats (u, v)
167
+ this.gl.FLOAT,
168
+ false,
169
+ stride,
170
+ 3 * 4
171
+ // offset after position
172
+ );
173
+ }
174
+ if (colorAttr !== -1) {
175
+ this.gl.enableVertexAttribArray(colorAttr);
176
+ this.gl.vertexAttribPointer(
177
+ colorAttr,
178
+ 4,
179
+ // 4 floats (r, g, b, a)
180
+ this.gl.FLOAT,
181
+ false,
182
+ stride,
183
+ 5 * 4
184
+ // offset after texCoord
185
+ );
186
+ }
187
+ if (texIndexAttr !== -1) {
188
+ this.gl.enableVertexAttribArray(texIndexAttr);
189
+ this.gl.vertexAttribPointer(
190
+ texIndexAttr,
191
+ 1,
192
+ // 1 float (texIndex)
193
+ this.gl.FLOAT,
194
+ false,
195
+ stride,
196
+ 9 * 4
197
+ // offset after color
198
+ );
199
+ }
200
+ if (this.texture) {
201
+ this.texture.bind(0);
202
+ const textureUniform = this.shader.getUniformLocation("uTexture");
203
+ if (textureUniform !== null) {
204
+ this.gl.uniform1i(textureUniform, 0);
205
+ }
206
+ }
207
+ const matrixUniform = this.shader.getUniformLocation("uMatrix");
208
+ if (matrixUniform !== null) {
209
+ const matrix = camera ? camera.getViewMatrix() : new Float32Array([
210
+ 1,
211
+ 0,
212
+ 0,
213
+ 0,
214
+ 0,
215
+ 1,
216
+ 0,
217
+ 0,
218
+ 0,
219
+ 0,
220
+ 1,
221
+ 0,
222
+ 0,
223
+ 0,
224
+ 0,
225
+ 1
226
+ ]);
227
+ this.gl.uniformMatrix4fv(matrixUniform, false, matrix);
228
+ }
229
+ const vertexCount = this.quads.length * this.verticesPerQuad;
230
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, vertexCount);
231
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
232
+ }
233
+ }
234
+ /**
235
+ * Generate vertices for a quad with rotation applied
236
+ * Returns 6 vertices (2 triangles)
237
+ * @private
238
+ */
239
+ generateQuadVertices(instance) {
240
+ const { x, y, z, width, height, rotation, color, uvRect, texIndex } = instance;
241
+ const halfW = width / 2;
242
+ const halfH = height / 2;
243
+ const cos = Math.cos(rotation);
244
+ const sin = Math.sin(rotation);
245
+ const rotatePoint = (px, py) => {
246
+ return [px * cos - py * sin, px * sin + py * cos];
247
+ };
248
+ const corners = [
249
+ [-halfW, -halfH],
250
+ // bottom-left
251
+ [halfW, -halfH],
252
+ // bottom-right
253
+ [halfW, halfH],
254
+ // top-right
255
+ [halfW, halfH],
256
+ // top-right (duplicate)
257
+ [-halfW, halfH],
258
+ // top-left
259
+ [-halfW, -halfH]
260
+ // bottom-left (duplicate)
261
+ ];
262
+ const texCoords = [
263
+ [uvRect.uMin, uvRect.vMin],
264
+ // bottom-left
265
+ [uvRect.uMax, uvRect.vMin],
266
+ // bottom-right
267
+ [uvRect.uMax, uvRect.vMax],
268
+ // top-right
269
+ [uvRect.uMax, uvRect.vMax],
270
+ // top-right
271
+ [uvRect.uMin, uvRect.vMax],
272
+ // top-left
273
+ [uvRect.uMin, uvRect.vMin]
274
+ // bottom-left
275
+ ];
276
+ const vertices = [];
277
+ for (let i = 0; i < corners.length; i++) {
278
+ const [localX, localY] = corners[i];
279
+ const [rotX, rotY] = rotatePoint(localX, localY);
280
+ const [u, v] = texCoords[i];
281
+ vertices.push({
282
+ x: x + rotX,
283
+ y: y + rotY,
284
+ z,
285
+ u,
286
+ v,
287
+ r: color.r,
288
+ g: color.g,
289
+ b: color.b,
290
+ a: color.a,
291
+ texIndex
292
+ });
293
+ }
294
+ return vertices;
295
+ }
296
+ /**
297
+ * Dispose resources
298
+ */
299
+ dispose() {
300
+ if (this.vertexBuffer) {
301
+ this.gl.deleteBuffer(this.vertexBuffer);
302
+ this.vertexBuffer = null;
303
+ }
304
+ }
305
+ }
306
+ export {
307
+ SpriteBatchRenderer
308
+ };
@@ -0,0 +1,146 @@
1
+ class BrowserResourceLoader {
2
+ /**
3
+ * Create a new browser resource loader
4
+ * @param baseUrl Optional base URL for resolving relative paths (defaults to current origin)
5
+ * @param timeout Default timeout for requests in milliseconds (default: 10000)
6
+ */
7
+ constructor(baseUrl = "", timeout = 1e4) {
8
+ this.baseUrl = baseUrl || this.getCurrentOrigin();
9
+ this.defaultTimeout = timeout;
10
+ }
11
+ /**
12
+ * Get the current origin (protocol + host + port)
13
+ */
14
+ getCurrentOrigin() {
15
+ return typeof window !== "undefined" ? window.location.origin : "http://localhost";
16
+ }
17
+ /**
18
+ * Resolve a relative path against the base URL
19
+ * @param path Relative or absolute path
20
+ * @returns Resolved absolute URL
21
+ */
22
+ resolvePath(path) {
23
+ try {
24
+ if (path.startsWith("http://") || path.startsWith("https://")) {
25
+ return path;
26
+ }
27
+ if (path.startsWith("//")) {
28
+ return window.location.protocol + path;
29
+ }
30
+ if (path.startsWith("/")) {
31
+ return this.baseUrl + path;
32
+ }
33
+ return `${this.baseUrl}/${path}`;
34
+ } catch {
35
+ return path;
36
+ }
37
+ }
38
+ /**
39
+ * Load a single resource from a URL
40
+ * @param path URL or relative path to the resource
41
+ * @param options Optional loading configuration
42
+ * @returns Promise resolving to the resource content
43
+ */
44
+ async load(path, options) {
45
+ const url = this.resolvePath(path);
46
+ try {
47
+ const fetchOptions = {
48
+ credentials: options?.credentials || "same-origin"
49
+ };
50
+ if (options?.headers) {
51
+ fetchOptions.headers = options.headers;
52
+ }
53
+ const controller = new AbortController();
54
+ const timeoutId = setTimeout(() => controller.abort(), this.defaultTimeout);
55
+ fetchOptions.signal = controller.signal;
56
+ const response = await fetch(url, fetchOptions);
57
+ clearTimeout(timeoutId);
58
+ if (!response.ok) {
59
+ throw new Error(
60
+ `HTTP ${response.status}: ${response.statusText} for URL: ${url}`
61
+ );
62
+ }
63
+ const text = await response.text();
64
+ return text;
65
+ } catch (error) {
66
+ if (error instanceof Error) {
67
+ if (error.name === "AbortError") {
68
+ throw new Error(
69
+ `Request timeout after ${this.defaultTimeout}ms for URL: ${url}`
70
+ );
71
+ }
72
+ throw new Error(`Failed to load resource from ${url}: ${error.message}`);
73
+ }
74
+ throw new Error(`Failed to load resource from ${url}: Unknown error`);
75
+ }
76
+ }
77
+ /**
78
+ * Load multiple resources in parallel
79
+ * @param paths Array of URLs or paths
80
+ * @param options Optional loading configuration
81
+ * @returns Promise resolving to array of load results
82
+ */
83
+ async loadMultiple(paths, options) {
84
+ const promises = paths.map(async (path) => {
85
+ try {
86
+ const data = await this.load(path, options);
87
+ return {
88
+ data,
89
+ path,
90
+ success: true
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ data: "",
95
+ path,
96
+ success: false,
97
+ error: error instanceof Error ? error.message : String(error)
98
+ };
99
+ }
100
+ });
101
+ return Promise.all(promises);
102
+ }
103
+ /**
104
+ * Check if the path is valid for loading in the browser
105
+ * @param path URL or path to check
106
+ * @returns true if the path can be loaded
107
+ */
108
+ canLoad(path) {
109
+ const validPatterns = [
110
+ /^https?:\/\//i,
111
+ // Absolute HTTP(S) URLs
112
+ /^\/\//,
113
+ // Protocol-relative URLs
114
+ /^\//,
115
+ // Absolute paths
116
+ /^\.\.?\//
117
+ // Relative paths starting with ./ or ../
118
+ ];
119
+ const hasFileExtension = /\.[a-z0-9]+$/i.test(path);
120
+ return validPatterns.some((pattern) => pattern.test(path)) || hasFileExtension;
121
+ }
122
+ /**
123
+ * Set a new base URL for resolving relative paths
124
+ * @param baseUrl New base URL
125
+ */
126
+ setBaseUrl(baseUrl) {
127
+ this.baseUrl = baseUrl;
128
+ }
129
+ /**
130
+ * Get the current base URL
131
+ * @returns Current base URL
132
+ */
133
+ getBaseUrl() {
134
+ return this.baseUrl;
135
+ }
136
+ /**
137
+ * Set the default request timeout
138
+ * @param timeout Timeout in milliseconds
139
+ */
140
+ setTimeout(timeout) {
141
+ this.defaultTimeout = timeout;
142
+ }
143
+ }
144
+ export {
145
+ BrowserResourceLoader
146
+ };