@aethervtt/shard-physics 0.0.1

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.
@@ -0,0 +1,131 @@
1
+ import { defineSchema, findComponent, t } from "@aethervtt/shard-core";
2
+ import { CharacterState, PhysicsRange } from "./components.js";
3
+ import { createRayHit, Physics } from "./world.js";
4
+ function scenePath(world, entity) {
5
+ const member = findComponent("scene/SceneMember");
6
+ if (!member || entity < 0 || !world.isAlive(entity)) return null;
7
+ const v = world.tryGet(entity, member);
8
+ return v?.path || null;
9
+ }
10
+ function hitJson(world, hit) {
11
+ return {
12
+ entity: hit.entity,
13
+ path: scenePath(world, hit.entity),
14
+ body: hit.body >= 0 ? hit.body : null,
15
+ bodyPath: hit.body >= 0 ? scenePath(world, hit.body) : null,
16
+ distance: hit.distance,
17
+ point: Array.from(hit.point),
18
+ normal: Array.from(hit.normal)
19
+ };
20
+ }
21
+ function ready(world) {
22
+ return world.resource(Physics);
23
+ }
24
+ const filterFields = {
25
+ mask: t.u16({ default: 65535, description: "Only colliders in these layers (bitmask)." }),
26
+ exclude: t.entity({ description: "Skip this entity\u2019s colliders (and its body\u2019s)." }),
27
+ sensors: t.bool({ description: "Include sensors." })
28
+ };
29
+ function options(p, maxDistance) {
30
+ const exclude = p.exclude;
31
+ return {
32
+ maxDistance,
33
+ mask: p.mask,
34
+ exclude: exclude !== null && exclude !== void 0 && exclude >= 0 ? exclude : void 0,
35
+ sensors: p.sensors
36
+ };
37
+ }
38
+ const physicsMethods = [
39
+ {
40
+ name: "physics.raycast",
41
+ description: "Casts a ray against physics colliders: the nearest hit (or every hit with all) with collider entity and scene path, body, point, normal, and distance. Sees the state after the last physics step.",
42
+ params: defineSchema("physics/RaycastParams", {
43
+ origin: t.vec3({ required: true }),
44
+ direction: t.vec3({ required: true }),
45
+ maxDistance: t.f32({ min: 0, description: "Ignore hits past this (0: no limit)." }),
46
+ all: t.bool({ description: "Every collider along the ray, nearest first." }),
47
+ ...filterFields
48
+ }),
49
+ handler: ({ world }, p) => {
50
+ const physics = ready(world);
51
+ const o = options(p, p.maxDistance || void 0);
52
+ if (p.all) {
53
+ const hits = physics.raycastAll(p.origin, p.direction, o);
54
+ return { hits: hits.map((h) => hitJson(world, h)) };
55
+ }
56
+ const hit = createRayHit();
57
+ const found = physics.raycast(p.origin, p.direction, o, hit);
58
+ return { hits: found ? [hitJson(world, hit)] : [] };
59
+ }
60
+ },
61
+ {
62
+ name: "physics.overlap",
63
+ description: "Colliders overlapping a point, or a ball, cuboid, or capsule placed at a position: their entities and scene paths.",
64
+ params: defineSchema("physics/OverlapParams", {
65
+ position: t.vec3({ required: true }),
66
+ shape: t.enum(["point", "ball", "cuboid", "capsule"]),
67
+ radius: t.f32({ default: 0.5, min: 0 }),
68
+ halfExtents: t.vec3({ default: [0.5, 0.5, 0.5], min: 0 }),
69
+ halfHeight: t.f32({ default: 0.5, min: 0 }),
70
+ rotation: t.quat(),
71
+ ...filterFields
72
+ }),
73
+ handler: ({ world }, p) => {
74
+ const physics = ready(world);
75
+ const found = [];
76
+ const visit = (entity) => {
77
+ found.push({ entity, path: scenePath(world, entity) });
78
+ return true;
79
+ };
80
+ const o = options(p);
81
+ if (p.shape === "point") physics.overlapPoint(p.position, o, visit);
82
+ else {
83
+ physics.overlapShape(
84
+ {
85
+ shape: p.shape,
86
+ radius: p.radius,
87
+ halfExtents: p.halfExtents,
88
+ halfHeight: p.halfHeight
89
+ },
90
+ p.position,
91
+ p.rotation,
92
+ o,
93
+ visit
94
+ );
95
+ }
96
+ return { colliders: found };
97
+ }
98
+ },
99
+ {
100
+ name: "physics.describe",
101
+ description: "Physics state: dimension, gravity, bodies by kind and how many sleep, colliders by shape, joints, contact pairs, colliders waiting for a mesh, bodies parked out of range (large worlds) and the range, the last step time, and every character controller with its state (grounded, ground, velocity, up).",
102
+ params: defineSchema("physics/DescribeParams", {}),
103
+ handler: ({ world }) => {
104
+ const physics = ready(world);
105
+ return {
106
+ dimension: physics.dim,
107
+ gravity: physics.config.gravity,
108
+ interpolate: physics.config.interpolate,
109
+ paused: physics.config.paused,
110
+ range: world.tryResource(PhysicsRange)?.radius ?? null,
111
+ ...physics.describe(),
112
+ characters: [...physics.characters.keys()].map((entity) => {
113
+ const s = world.get(entity, CharacterState);
114
+ return {
115
+ entity,
116
+ path: scenePath(world, entity),
117
+ grounded: s.grounded,
118
+ groundEntity: s.groundEntity,
119
+ groundPath: s.groundEntity === null ? null : scenePath(world, s.groundEntity),
120
+ velocity: s.velocity,
121
+ up: s.up,
122
+ airTime: s.airTime
123
+ };
124
+ })
125
+ };
126
+ }
127
+ }
128
+ ];
129
+ export {
130
+ physicsMethods
131
+ };
@@ -0,0 +1,13 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import { type Plugin } from '@aethervtt/shard-runtime';
3
+ import { Physics, PhysicsWorld } from './world.js';
4
+ export { Physics };
5
+ /** Physics sync and step run in this set, in FixedUpdate. Move bodies before it. */
6
+ export declare const PhysicsSystems: import("@aethervtt/shard-core").SystemSetDef;
7
+ /** The physics world, or `physics/not-ready` before the plugin's `ready` step. */
8
+ export declare function physics(world: World): PhysicsWorld;
9
+ /** 3D rigid bodies, colliders, joints, and scene queries (Rapier). */
10
+ export declare const physics3dPlugin: Plugin;
11
+ /** 2D rigid bodies, colliders, joints, and scene queries in the XY plane (Rapier 2D). */
12
+ export declare const physics2dPlugin: Plugin;
13
+ export declare const collidersOverlay: import("@aethervtt/shard-render").OverlayDef;