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.
- package/dist/components/Game.d.ts +4 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +133 -1
- package/package.json +11 -9
|
@@ -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.
|
|
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.
|
|
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": [
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
21
23
|
"peerDependencies": {
|
|
22
24
|
"react": "^18.0.0",
|
|
23
25
|
"react-dom": "^18.0.0"
|
|
24
26
|
},
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
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",
|