cubeforge 0.0.2 → 0.0.3

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 React, { type CSSProperties } from 'react';
2
+ import { type Plugin } from '@cubeforge/core';
2
3
  export interface GameControls {
3
4
  pause(): void;
4
5
  resume(): void;
@@ -20,9 +21,11 @@ interface GameProps {
20
21
  scale?: 'none' | 'contain' | 'pixel';
21
22
  /** Called once the engine is ready — receives pause/resume/reset controls */
22
23
  onReady?: (controls: GameControls) => void;
24
+ /** Custom plugins to register after core systems. Each plugin's systems run after Render. */
25
+ plugins?: Plugin[];
23
26
  style?: CSSProperties;
24
27
  className?: string;
25
28
  children?: React.ReactNode;
26
29
  }
27
- export declare function Game({ width, height, gravity, debug, scale, onReady, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
30
+ export declare function Game({ width, height, gravity, debug, scale, onReady, plugins, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
28
31
  export {};
package/dist/index.d.ts CHANGED
@@ -29,7 +29,8 @@ export type { EngineState } from './context';
29
29
  export type { GameControls } from './components/Game';
30
30
  export type { PlatformerControllerOptions } from './hooks/usePlatformerController';
31
31
  export type { TopDownMovementOptions } from './hooks/useTopDownMovement';
32
- export type { EntityId, ECSWorld, ScriptUpdateFn } from '@cubeforge/core';
32
+ export type { EntityId, ECSWorld, ScriptUpdateFn, Plugin } from '@cubeforge/core';
33
+ export { definePlugin } from '@cubeforge/core';
33
34
  export type { InputManager } from '@cubeforge/input';
34
35
  export type { TransformComponent } from '@cubeforge/core';
35
36
  export type { RigidBodyComponent } from '@cubeforge/physics';
package/dist/index.js CHANGED
@@ -7,24 +7,36 @@ class ECSWorld {
7
7
  components = new Map;
8
8
  systems = [];
9
9
  queryCache = new Map;
10
+ dirtyTypes = new Set;
11
+ dirtyAll = false;
10
12
  createEntity() {
11
13
  const id = this.nextId++;
12
14
  this.entities.add(id);
13
15
  this.components.set(id, new Map);
16
+ this.dirtyAll = true;
14
17
  return id;
15
18
  }
16
19
  destroyEntity(id) {
20
+ const comps = this.components.get(id);
21
+ if (comps) {
22
+ for (const type of comps.keys()) {
23
+ this.dirtyTypes.add(type);
24
+ }
25
+ }
17
26
  this.entities.delete(id);
18
27
  this.components.delete(id);
28
+ this.dirtyAll = true;
19
29
  }
20
30
  hasEntity(id) {
21
31
  return this.entities.has(id);
22
32
  }
23
33
  addComponent(id, component) {
24
34
  this.components.get(id)?.set(component.type, component);
35
+ this.dirtyTypes.add(component.type);
25
36
  }
26
37
  removeComponent(id, type) {
27
38
  this.components.get(id)?.delete(type);
39
+ this.dirtyTypes.add(type);
28
40
  }
29
41
  getComponent(id, type) {
30
42
  return this.components.get(id)?.get(type);
@@ -77,7 +89,22 @@ class ECSWorld {
77
89
  this.systems.splice(idx, 1);
78
90
  }
79
91
  update(dt) {
80
- this.queryCache.clear();
92
+ if (this.dirtyAll) {
93
+ this.queryCache.clear();
94
+ } else if (this.dirtyTypes.size > 0) {
95
+ for (const key of this.queryCache.keys()) {
96
+ if (key === "") {
97
+ this.queryCache.delete(key);
98
+ continue;
99
+ }
100
+ const keyTypes = key.split("\x00");
101
+ if (keyTypes.some((t) => this.dirtyTypes.has(t))) {
102
+ this.queryCache.delete(key);
103
+ }
104
+ }
105
+ }
106
+ this.dirtyAll = false;
107
+ this.dirtyTypes.clear();
81
108
  for (const system of this.systems) {
82
109
  system.update(this, dt);
83
110
  }
@@ -85,6 +112,9 @@ class ECSWorld {
85
112
  clear() {
86
113
  this.entities.clear();
87
114
  this.components.clear();
115
+ this.queryCache.clear();
116
+ this.dirtyTypes.clear();
117
+ this.dirtyAll = false;
88
118
  this.nextId = 0;
89
119
  }
90
120
  get entityCount() {
@@ -360,6 +390,10 @@ function tween(from, to, duration, ease = Ease.linear, onUpdate, onComplete) {
360
390
  }
361
391
  };
362
392
  }
393
+ // ../core/src/plugin.ts
394
+ function definePlugin(plugin) {
395
+ return plugin;
396
+ }
363
397
  // ../input/src/keyboard.ts
364
398
  class Keyboard {
365
399
  held = new Set;
@@ -831,6 +865,7 @@ function createRigidBody(opts) {
831
865
  gravityScale: 1,
832
866
  isStatic: false,
833
867
  onGround: false,
868
+ isNearGround: false,
834
869
  bounce: 0,
835
870
  friction: 0.85,
836
871
  ...opts
@@ -846,6 +881,7 @@ function createBoxCollider(width, height, opts) {
846
881
  offsetY: 0,
847
882
  isTrigger: false,
848
883
  layer: "default",
884
+ slope: 0,
849
885
  ...opts
850
886
  };
851
887
  }
@@ -870,6 +906,19 @@ function getOverlap(a, b) {
870
906
  y: dy >= 0 ? oy : -oy
871
907
  };
872
908
  }
909
+ function getSlopeSurfaceY(st, sc, worldX) {
910
+ const hw = sc.width / 2;
911
+ const hh = sc.height / 2;
912
+ const cx = st.x + sc.offsetX;
913
+ const cy = st.y + sc.offsetY;
914
+ const left = cx - hw;
915
+ const right = cx + hw;
916
+ if (worldX < left || worldX > right)
917
+ return null;
918
+ const dx = worldX - left;
919
+ const angleRad = sc.slope * (Math.PI / 180);
920
+ return cy - hh + dx * Math.tan(angleRad);
921
+ }
873
922
 
874
923
  class PhysicsSystem {
875
924
  gravity;
@@ -933,6 +982,7 @@ class PhysicsSystem {
933
982
  for (const id of dynamics) {
934
983
  const rb = world2.getComponent(id, "RigidBody");
935
984
  rb.onGround = false;
985
+ rb.isNearGround = false;
936
986
  rb.vy += this.gravity * rb.gravityScale * dt;
937
987
  }
938
988
  for (const id of dynamics) {
@@ -956,6 +1006,8 @@ class PhysicsSystem {
956
1006
  const sc = world2.getComponent(sid, "BoxCollider");
957
1007
  if (sc.isTrigger)
958
1008
  continue;
1009
+ if (sc.slope !== 0)
1010
+ continue;
959
1011
  const ov = getOverlap(getAABB(transform2, col), getAABB(st, sc));
960
1012
  if (!ov)
961
1013
  continue;
@@ -988,6 +1040,22 @@ class PhysicsSystem {
988
1040
  const sc = world2.getComponent(sid, "BoxCollider");
989
1041
  if (sc.isTrigger)
990
1042
  continue;
1043
+ if (sc.slope !== 0) {
1044
+ const ov2 = getOverlap(getAABB(transform2, col), getAABB(st, sc));
1045
+ if (!ov2)
1046
+ continue;
1047
+ const entityBottom = transform2.y + col.offsetY + col.height / 2;
1048
+ const entityCenterX = transform2.x + col.offsetX;
1049
+ const surfaceY = getSlopeSurfaceY(st, sc, entityCenterX);
1050
+ if (surfaceY !== null && entityBottom > surfaceY) {
1051
+ transform2.y -= entityBottom - surfaceY;
1052
+ rb.onGround = true;
1053
+ if (rb.friction < 1)
1054
+ rb.vx *= rb.friction;
1055
+ rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
1056
+ }
1057
+ continue;
1058
+ }
991
1059
  const ov = getOverlap(getAABB(transform2, col), getAABB(st, sc));
992
1060
  if (!ov)
993
1061
  continue;
@@ -1019,6 +1087,23 @@ class PhysicsSystem {
1019
1087
  this.events?.emit("trigger", { a: ia, b: ib });
1020
1088
  continue;
1021
1089
  }
1090
+ const rba = world2.getComponent(ia, "RigidBody");
1091
+ const rbb = world2.getComponent(ib, "RigidBody");
1092
+ if (Math.abs(ov.y) <= Math.abs(ov.x)) {
1093
+ if (ov.y > 0) {
1094
+ if (rbb.vy > 0) {
1095
+ rba.vy += rbb.vy * 0.3;
1096
+ rbb.vy = 0;
1097
+ }
1098
+ rbb.onGround = true;
1099
+ } else {
1100
+ if (rba.vy > 0) {
1101
+ rbb.vy += rba.vy * 0.3;
1102
+ rba.vy = 0;
1103
+ }
1104
+ rba.onGround = true;
1105
+ }
1106
+ }
1022
1107
  ta.x += ov.x / 2;
1023
1108
  ta.y += ov.y / 2;
1024
1109
  tb.x -= ov.x / 2;
@@ -1026,6 +1111,43 @@ class PhysicsSystem {
1026
1111
  this.events?.emit("collision", { a: ia, b: ib });
1027
1112
  }
1028
1113
  }
1114
+ for (const id of dynamics) {
1115
+ const rb = world2.getComponent(id, "RigidBody");
1116
+ if (rb.onGround) {
1117
+ rb.isNearGround = true;
1118
+ continue;
1119
+ }
1120
+ const transform2 = world2.getComponent(id, "Transform");
1121
+ const col = world2.getComponent(id, "BoxCollider");
1122
+ const probeAABB = {
1123
+ cx: transform2.x + col.offsetX,
1124
+ cy: transform2.y + col.offsetY + 2,
1125
+ hw: col.width / 2,
1126
+ hh: col.height / 2
1127
+ };
1128
+ const candidateCells = this.getCells(probeAABB.cx, probeAABB.cy, probeAABB.hw, probeAABB.hh);
1129
+ const checked = new Set;
1130
+ outer:
1131
+ for (const cell of candidateCells) {
1132
+ const bucket = staticGrid.get(cell);
1133
+ if (!bucket)
1134
+ continue;
1135
+ for (const sid of bucket) {
1136
+ if (checked.has(sid))
1137
+ continue;
1138
+ checked.add(sid);
1139
+ const st = world2.getComponent(sid, "Transform");
1140
+ const sc = world2.getComponent(sid, "BoxCollider");
1141
+ if (sc.isTrigger)
1142
+ continue;
1143
+ const ov = getOverlap(probeAABB, getAABB(st, sc));
1144
+ if (ov && Math.abs(ov.y) <= Math.abs(ov.x) && ov.y < 0) {
1145
+ rb.isNearGround = true;
1146
+ break outer;
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1029
1151
  }
1030
1152
  }
1031
1153
  // src/context.ts
@@ -1140,6 +1262,7 @@ function Game({
1140
1262
  debug = false,
1141
1263
  scale = "none",
1142
1264
  onReady,
1265
+ plugins,
1143
1266
  style,
1144
1267
  className,
1145
1268
  children
@@ -1174,6 +1297,14 @@ function Game({
1174
1297
  });
1175
1298
  const state = { ecs, input, renderer, physics, events, assets, loop, canvas, entityIds };
1176
1299
  setEngine(state);
1300
+ if (plugins) {
1301
+ for (const plugin2 of plugins) {
1302
+ for (const system of plugin2.systems) {
1303
+ ecs.addSystem(system);
1304
+ }
1305
+ plugin2.onInit?.(state);
1306
+ }
1307
+ }
1177
1308
  loop.start();
1178
1309
  onReady?.({
1179
1310
  pause: () => loop.pause(),
@@ -2223,6 +2354,7 @@ export {
2223
2354
  useEvent,
2224
2355
  useEntity,
2225
2356
  tween,
2357
+ definePlugin,
2226
2358
  createAtlas,
2227
2359
  World,
2228
2360
  Transform,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -17,18 +17,20 @@
17
17
  }
18
18
  }
19
19
  },
20
- "files": ["dist"],
20
+ "files": [
21
+ "dist"
22
+ ],
21
23
  "peerDependencies": {
22
24
  "react": "^18.0.0",
23
25
  "react-dom": "^18.0.0"
24
26
  },
25
- "dependencies": {
26
- "@cubeforge/core": "workspace:*",
27
- "@cubeforge/input": "workspace:*",
28
- "@cubeforge/renderer": "workspace:*",
29
- "@cubeforge/physics": "workspace:*"
30
- },
31
- "keywords": ["game-engine", "react", "2d", "browser-game", "cubeforge"],
27
+ "keywords": [
28
+ "game-engine",
29
+ "react",
30
+ "2d",
31
+ "browser-game",
32
+ "cubeforge"
33
+ ],
32
34
  "license": "MIT",
33
35
  "repository": {
34
36
  "type": "git",