bloody-engine 1.0.4 → 1.0.5

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.
@@ -1,4 +1,5 @@
1
1
  import createGL from "gl";
2
+ import sdl from "@kmamal/sdl";
2
3
  import * as fs from "fs/promises";
3
4
  import * as path from "path";
4
5
  class BrowserRenderingContext {
@@ -365,6 +366,99 @@ class GraphicsDevice {
365
366
  );
366
367
  }
367
368
  }
369
+ class SDLWindow {
370
+ constructor(width, height, title = "Bloody Engine") {
371
+ this.closed = false;
372
+ this.width = width;
373
+ this.height = height;
374
+ this.title = title;
375
+ try {
376
+ this.window = sdl.video.createWindow({
377
+ width: this.width,
378
+ height: this.height,
379
+ title: this.title
380
+ });
381
+ if (!this.window) {
382
+ throw new Error("Failed to create SDL window");
383
+ }
384
+ this.window.on("close", () => {
385
+ this.closed = true;
386
+ });
387
+ console.log(`✓ SDL Window created (${width}x${height}): "${title}"`);
388
+ } catch (error) {
389
+ this.cleanup();
390
+ throw new Error(`Window creation failed: ${error}`);
391
+ }
392
+ }
393
+ /**
394
+ * Get window dimensions
395
+ */
396
+ getDimensions() {
397
+ return { width: this.width, height: this.height };
398
+ }
399
+ /**
400
+ * Display pixel data in the window
401
+ * @param pixels Uint8Array of RGBA pixel data
402
+ */
403
+ updatePixels(pixels) {
404
+ if (!this.window || this.closed) {
405
+ return;
406
+ }
407
+ try {
408
+ const buffer2 = Buffer.from(pixels);
409
+ const stride = this.width * 4;
410
+ this.window.render(this.width, this.height, stride, "rgba32", buffer2);
411
+ } catch (error) {
412
+ console.error("Failed to update pixels:", error);
413
+ }
414
+ }
415
+ /**
416
+ * Register an event handler
417
+ */
418
+ on(eventName, handler) {
419
+ if (!this.window || this.closed) {
420
+ return;
421
+ }
422
+ try {
423
+ this.window.on(eventName, (event) => {
424
+ try {
425
+ handler(event);
426
+ } catch (error) {
427
+ console.error(`Error in ${eventName} handler:`, error);
428
+ }
429
+ });
430
+ } catch (error) {
431
+ console.error(`Error registering ${eventName} handler:`, error);
432
+ }
433
+ }
434
+ /**
435
+ * Check if window is still open
436
+ */
437
+ isOpen() {
438
+ return this.window !== null && !this.closed;
439
+ }
440
+ /**
441
+ * Cleanup and close window
442
+ */
443
+ cleanup() {
444
+ if (this.window && !this.closed) {
445
+ try {
446
+ this.window.destroy();
447
+ } catch (error) {
448
+ console.warn("Error destroying window:", error);
449
+ }
450
+ this.window = null;
451
+ this.closed = true;
452
+ }
453
+ console.log("✓ SDL Window cleaned up");
454
+ }
455
+ /**
456
+ * Destroy the window (alias for cleanup)
457
+ */
458
+ destroy() {
459
+ this.cleanup();
460
+ }
461
+ }
368
462
  class Texture {
369
463
  /**
370
464
  * Create a texture from pixel data
@@ -1132,6 +1226,301 @@ class SpriteBatchRenderer {
1132
1226
  }
1133
1227
  }
1134
1228
  }
1229
+ class GPUBasedSpriteBatchRenderer {
1230
+ /**
1231
+ * Create a new GPU-based sprite batch renderer (V3)
1232
+ * @param gl WebGL rendering context
1233
+ * @param shader Shader program to use (should be SHADERS_V3)
1234
+ * @param maxQuads Maximum number of quads to batch (default 1000)
1235
+ * @param tileSize Tile size for isometric projection (default {width: 64, height: 32})
1236
+ * @param zScale Scale factor for Z height (default 1.0)
1237
+ */
1238
+ constructor(gl, shader, maxQuads = 1e3, tileSize = { width: 64, height: 32 }, zScale = 1) {
1239
+ this.vertexBuffer = null;
1240
+ this.quads = [];
1241
+ this.isDirty = false;
1242
+ this.verticesPerQuad = 6;
1243
+ this.floatsPerVertex = 12;
1244
+ this.texture = null;
1245
+ this.depthTestEnabled = true;
1246
+ this.gl = gl;
1247
+ this.shader = shader;
1248
+ this.maxQuads = maxQuads;
1249
+ this.tileSize = tileSize;
1250
+ this.zScale = zScale;
1251
+ this.resolution = { width: gl.canvas.width, height: gl.canvas.height };
1252
+ const totalFloats = maxQuads * this.verticesPerQuad * this.floatsPerVertex;
1253
+ this.vertexData = new Float32Array(totalFloats);
1254
+ const buf = gl.createBuffer();
1255
+ if (!buf) {
1256
+ throw new Error("Failed to create vertex buffer");
1257
+ }
1258
+ this.vertexBuffer = buf;
1259
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
1260
+ gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
1261
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1262
+ }
1263
+ /**
1264
+ * Set the texture for batch rendering
1265
+ * @param texture The texture to use when rendering
1266
+ */
1267
+ setTexture(texture) {
1268
+ this.texture = texture;
1269
+ }
1270
+ /**
1271
+ * Add a sprite quad to the batch
1272
+ * If gridX and gridY are provided, uses GPU transformation.
1273
+ * Otherwise, converts x, y to grid coordinates.
1274
+ * @param quad Sprite quad instance to add
1275
+ */
1276
+ addQuad(quad) {
1277
+ if (this.quads.length >= this.maxQuads) {
1278
+ console.warn(`Sprite batch renderer at max capacity (${this.maxQuads})`);
1279
+ return;
1280
+ }
1281
+ this.quads.push(quad);
1282
+ this.isDirty = true;
1283
+ }
1284
+ /**
1285
+ * Clear all quads from the batch
1286
+ */
1287
+ clear() {
1288
+ this.quads = [];
1289
+ this.isDirty = true;
1290
+ }
1291
+ /**
1292
+ * Get number of quads currently in batch
1293
+ */
1294
+ getQuadCount() {
1295
+ return this.quads.length;
1296
+ }
1297
+ /**
1298
+ * Update the batch - rebuilds vertex buffer if quads changed
1299
+ */
1300
+ update() {
1301
+ if (!this.isDirty || this.quads.length === 0) {
1302
+ return;
1303
+ }
1304
+ let vertexIndex = 0;
1305
+ for (const quad of this.quads) {
1306
+ const {
1307
+ x,
1308
+ y,
1309
+ z = 0,
1310
+ width,
1311
+ height,
1312
+ color = { r: 1, g: 1, b: 1, a: 1 },
1313
+ uvRect = { uMin: 0, vMin: 0, uMax: 1, vMax: 1 },
1314
+ texIndex = 0,
1315
+ gridX,
1316
+ gridY
1317
+ } = quad;
1318
+ let gx, gy;
1319
+ if (gridX !== void 0 && gridY !== void 0) {
1320
+ gx = gridX;
1321
+ gy = gridY;
1322
+ } else {
1323
+ gx = (x / (this.tileSize.width * 0.5) + y / (this.tileSize.height * 0.5)) * 0.5;
1324
+ gy = (y / (this.tileSize.height * 0.5) - x / (this.tileSize.width * 0.5)) * 0.5;
1325
+ }
1326
+ const halfW = width / 2;
1327
+ const halfH = height / 2;
1328
+ const corners = [
1329
+ [-halfW, -halfH],
1330
+ // bottom-left
1331
+ [halfW, -halfH],
1332
+ // bottom-right
1333
+ [halfW, halfH],
1334
+ // top-right
1335
+ [halfW, halfH],
1336
+ // top-right (duplicate)
1337
+ [-halfH, halfH],
1338
+ // top-left
1339
+ [-halfW, -halfH]
1340
+ // bottom-left (duplicate)
1341
+ ];
1342
+ const texCoords = [
1343
+ [uvRect.uMin, uvRect.vMin],
1344
+ [uvRect.uMax, uvRect.vMin],
1345
+ [uvRect.uMax, uvRect.vMax],
1346
+ [uvRect.uMax, uvRect.vMax],
1347
+ [uvRect.uMin, uvRect.vMax],
1348
+ [uvRect.uMin, uvRect.vMin]
1349
+ ];
1350
+ for (let i = 0; i < corners.length; i++) {
1351
+ const [localX, localY] = corners[i];
1352
+ const [u, v] = texCoords[i];
1353
+ this.vertexData[vertexIndex++] = gx;
1354
+ this.vertexData[vertexIndex++] = gy;
1355
+ this.vertexData[vertexIndex++] = z;
1356
+ this.vertexData[vertexIndex++] = localX;
1357
+ this.vertexData[vertexIndex++] = localY;
1358
+ this.vertexData[vertexIndex++] = u;
1359
+ this.vertexData[vertexIndex++] = v;
1360
+ this.vertexData[vertexIndex++] = color.r;
1361
+ this.vertexData[vertexIndex++] = color.g;
1362
+ this.vertexData[vertexIndex++] = color.b;
1363
+ this.vertexData[vertexIndex++] = color.a;
1364
+ this.vertexData[vertexIndex++] = texIndex;
1365
+ }
1366
+ }
1367
+ if (this.vertexBuffer) {
1368
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
1369
+ this.gl.bufferSubData(
1370
+ this.gl.ARRAY_BUFFER,
1371
+ 0,
1372
+ this.vertexData.subarray(0, vertexIndex)
1373
+ );
1374
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
1375
+ }
1376
+ this.isDirty = false;
1377
+ }
1378
+ /**
1379
+ * Set whether depth testing is enabled
1380
+ * @param enabled Whether to enable depth testing (default true)
1381
+ */
1382
+ setDepthTestEnabled(enabled) {
1383
+ this.depthTestEnabled = enabled;
1384
+ }
1385
+ /**
1386
+ * Render the batch with GPU-based transformation
1387
+ * @param camera Camera for view transform
1388
+ */
1389
+ render(camera) {
1390
+ if (this.quads.length === 0) {
1391
+ return;
1392
+ }
1393
+ this.update();
1394
+ this.shader.use();
1395
+ if (this.depthTestEnabled) {
1396
+ this.gl.enable(this.gl.DEPTH_TEST);
1397
+ this.gl.depthFunc(this.gl.LEQUAL);
1398
+ } else {
1399
+ this.gl.disable(this.gl.DEPTH_TEST);
1400
+ }
1401
+ if (this.vertexBuffer) {
1402
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
1403
+ const stride = this.floatsPerVertex * 4;
1404
+ const attrs = {
1405
+ gridPosition: this.shader.getAttributeLocation("aGridPosition"),
1406
+ zPosition: this.shader.getAttributeLocation("aZPosition"),
1407
+ localOffset: this.shader.getAttributeLocation("aLocalOffset"),
1408
+ texCoord: this.shader.getAttributeLocation("aTexCoord"),
1409
+ color: this.shader.getAttributeLocation("aColor"),
1410
+ texIndex: this.shader.getAttributeLocation("aTexIndex")
1411
+ };
1412
+ if (attrs.gridPosition !== -1) {
1413
+ this.gl.enableVertexAttribArray(attrs.gridPosition);
1414
+ this.gl.vertexAttribPointer(
1415
+ attrs.gridPosition,
1416
+ 2,
1417
+ this.gl.FLOAT,
1418
+ false,
1419
+ stride,
1420
+ 0
1421
+ );
1422
+ }
1423
+ if (attrs.zPosition !== -1) {
1424
+ this.gl.enableVertexAttribArray(attrs.zPosition);
1425
+ this.gl.vertexAttribPointer(
1426
+ attrs.zPosition,
1427
+ 1,
1428
+ this.gl.FLOAT,
1429
+ false,
1430
+ stride,
1431
+ 2 * 4
1432
+ );
1433
+ }
1434
+ if (attrs.localOffset !== -1) {
1435
+ this.gl.enableVertexAttribArray(attrs.localOffset);
1436
+ this.gl.vertexAttribPointer(
1437
+ attrs.localOffset,
1438
+ 2,
1439
+ this.gl.FLOAT,
1440
+ false,
1441
+ stride,
1442
+ 3 * 4
1443
+ );
1444
+ }
1445
+ if (attrs.texCoord !== -1) {
1446
+ this.gl.enableVertexAttribArray(attrs.texCoord);
1447
+ this.gl.vertexAttribPointer(
1448
+ attrs.texCoord,
1449
+ 2,
1450
+ this.gl.FLOAT,
1451
+ false,
1452
+ stride,
1453
+ 5 * 4
1454
+ );
1455
+ }
1456
+ if (attrs.color !== -1) {
1457
+ this.gl.enableVertexAttribArray(attrs.color);
1458
+ this.gl.vertexAttribPointer(
1459
+ attrs.color,
1460
+ 4,
1461
+ this.gl.FLOAT,
1462
+ false,
1463
+ stride,
1464
+ 7 * 4
1465
+ );
1466
+ }
1467
+ if (attrs.texIndex !== -1) {
1468
+ this.gl.enableVertexAttribArray(attrs.texIndex);
1469
+ this.gl.vertexAttribPointer(
1470
+ attrs.texIndex,
1471
+ 1,
1472
+ this.gl.FLOAT,
1473
+ false,
1474
+ stride,
1475
+ 11 * 4
1476
+ );
1477
+ }
1478
+ if (this.texture) {
1479
+ this.texture.bind(0);
1480
+ const textureUniform = this.shader.getUniformLocation("uTexture");
1481
+ if (textureUniform !== null) {
1482
+ this.gl.uniform1i(textureUniform, 0);
1483
+ }
1484
+ }
1485
+ const tileSizeUniform = this.shader.getUniformLocation("uTileSize");
1486
+ if (tileSizeUniform !== null) {
1487
+ this.gl.uniform2f(tileSizeUniform, this.tileSize.width, this.tileSize.height);
1488
+ }
1489
+ const cameraUniform = this.shader.getUniformLocation("uCamera");
1490
+ if (cameraUniform !== null) {
1491
+ this.gl.uniform3f(cameraUniform, camera.x, camera.y, camera.zoom);
1492
+ }
1493
+ const zScaleUniform = this.shader.getUniformLocation("uZScale");
1494
+ if (zScaleUniform !== null) {
1495
+ this.gl.uniform1f(zScaleUniform, this.zScale);
1496
+ }
1497
+ const resolutionUniform = this.shader.getUniformLocation("uResolution");
1498
+ if (resolutionUniform !== null) {
1499
+ this.gl.uniform2f(resolutionUniform, this.resolution.width, this.resolution.height);
1500
+ }
1501
+ const rotationUniform = this.shader.getUniformLocation("uRotation");
1502
+ if (rotationUniform !== null) {
1503
+ this.gl.uniform1f(rotationUniform, 0);
1504
+ }
1505
+ const quadSizeUniform = this.shader.getUniformLocation("uQuadSize");
1506
+ if (quadSizeUniform !== null) {
1507
+ this.gl.uniform2f(quadSizeUniform, 1, 1);
1508
+ }
1509
+ const vertexCount = this.quads.length * this.verticesPerQuad;
1510
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, vertexCount);
1511
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
1512
+ }
1513
+ }
1514
+ /**
1515
+ * Clean up GPU resources
1516
+ */
1517
+ dispose() {
1518
+ if (this.vertexBuffer) {
1519
+ this.gl.deleteBuffer(this.vertexBuffer);
1520
+ this.vertexBuffer = null;
1521
+ }
1522
+ }
1523
+ }
1135
1524
  class Matrix4 {
1136
1525
  /**
1137
1526
  * Create an identity matrix
@@ -1399,6 +1788,14 @@ class Camera {
1399
1788
  return { x: screenX, y: screenY };
1400
1789
  }
1401
1790
  }
1791
+ class ProjectionConfig {
1792
+ // Scale factor for height (vertical exaggeration)
1793
+ constructor(tileWidth = 64, tileHeight = 32, zScale = 1) {
1794
+ this.tileWidth = tileWidth;
1795
+ this.tileHeight = tileHeight;
1796
+ this.zScale = zScale;
1797
+ }
1798
+ }
1402
1799
  class BrowserResourceLoader {
1403
1800
  /**
1404
1801
  * Create a new browser resource loader
@@ -2341,14 +2738,17 @@ export {
2341
2738
  BrowserResourceLoader,
2342
2739
  Camera,
2343
2740
  Environment,
2741
+ GPUBasedSpriteBatchRenderer,
2344
2742
  GraphicsDevice,
2345
2743
  IndexBuffer,
2346
2744
  Matrix4,
2347
2745
  NodeRenderingContext,
2348
2746
  NodeResourceLoader,
2747
+ ProjectionConfig,
2349
2748
  RenderingContextFactory,
2350
2749
  ResourceLoaderFactory,
2351
2750
  ResourcePipeline,
2751
+ SDLWindow,
2352
2752
  Shader,
2353
2753
  SpriteBatchRenderer,
2354
2754
  Texture,