spectra-physics 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/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # SpectraPhysics
2
+
3
+ [![npm](https://img.shields.io/npm/v/spectra-physics)](https://www.npmjs.com/package/spectra-physics)
4
+ [![Tests](https://github.com/trqxyz/SpectraPhysics/actions/workflows/test.yml/badge.svg)](https://github.com/trqxyz/SpectraPhysics/actions/workflows/test.yml)
5
+
6
+ Improved physics and movement engine for [mineflayer](https://github.com/PrismarineJS/mineflayer).
7
+
8
+ SpectraPhysics replaces mineflayer's built-in `physics` plugin with a port of the vanilla client's movement code
9
+ (`LocalPlayer#aiStep` → `LivingEntity#travel` → `Entity#move`) and of the way the client ticks and reports its
10
+ position to the server. Movement is computed in the same order and with the same float rounding as the game client,
11
+ so positions, velocities and packet timing match a vanilla player tick for tick.
12
+
13
+ Supported versions: **1.16.5 – 26.1** (every version mineflayer supports in that range).
14
+
15
+ ## Why
16
+
17
+ mineflayer's default physics (prismarine-physics) is a close approximation, but it differs from the client in
18
+ several places that are visible to the server:
19
+
20
+ | | mineflayer physics | SpectraPhysics |
21
+ |---|---|---|
22
+ | Collision | legacy 1.8 algorithm | `Shapes.collide` with VoxelShape epsilons, Y → larger horizontal axis order |
23
+ | Step-up | single step | pre-1.21 heuristic and 1.21+ candidate step heights |
24
+ | First tick after a teleport | moves immediately | no movement, gravity applied after `move`, like vanilla |
25
+ | Position packets | time based, last-sent values reset on teleport | `sendPosition` rules: 2·10⁻⁴ threshold, 20-tick reminder, last-sent values kept across teleports |
26
+ | Rotation | continuous interpolation | whole mouse steps of 0.15°, accumulated in float |
27
+ | Sprint | applied the same tick | speed attribute updated at the end of the tick, as in `Player#aiStep` |
28
+ | Tick loop | fixed interval | `Minecraft` timer: catches up at most 10 ticks |
29
+ | 1.21.2+ | no `tick_end` | `tick_end` after every tick, `player_input` on key change |
30
+ | 1.21.4+ | `player_loaded` on first health update | sent once the level around the player is loaded |
31
+ | Teleport answer | one order for all versions | per version (1.21.2–1.21.3 send position before the confirmation) |
32
+ | Vehicles | rotation packets only | boat simulation with paddle / input / vehicle move packets, minecart riding |
33
+ | Brand, 1.20.2+ | sent in play state | sent in the configuration phase, once |
34
+
35
+ It also fixes three mineflayer issues that leave a bot stuck:
36
+
37
+ - an empty full chunk (no sections) sent to a 1.16 client is treated as an unload, so the player never ticks in it;
38
+ - a removed vehicle is never dismounted (`entityGone` is listened for on the wrong emitter);
39
+ - an out-of-range hotbar slot from the server throws inside the packet handler.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install spectra-physics mineflayer
45
+ ```
46
+
47
+ Requires Node.js 22 or newer and mineflayer 4.30 or newer.
48
+
49
+ ## Usage
50
+
51
+ ```js
52
+ const { createBot } = require('spectra-physics')
53
+
54
+ const bot = createBot({
55
+ host: 'localhost',
56
+ port: 25565,
57
+ username: 'Steve',
58
+ version: '1.21.4',
59
+ spectra: { turnSpeed: 30 }
60
+ })
61
+
62
+ bot.once('spawn', async () => {
63
+ await bot.lookAt(bot.entity.position.offset(5, 1.6, 0))
64
+ bot.setControlState('forward', true)
65
+ bot.setControlState('sprint', true)
66
+ await bot.waitForTicks(40)
67
+ bot.clearControlStates()
68
+ })
69
+ ```
70
+
71
+ With an existing `mineflayer.createBot` call, install it under the `physics` key so it replaces the built-in plugin:
72
+
73
+ ```js
74
+ const mineflayer = require('mineflayer')
75
+ const spectraPhysics = require('spectra-physics')
76
+
77
+ const bot = mineflayer.createBot({
78
+ host: 'localhost',
79
+ username: 'Steve',
80
+ plugins: { physics: spectraPhysics() }
81
+ })
82
+ ```
83
+
84
+ More in [`examples/`](examples).
85
+
86
+ ### Options
87
+
88
+ | option | default | |
89
+ |---|---|---|
90
+ | `turnSpeed` | `35` | maximum rotation per tick in degrees |
91
+ | `brand` | `'vanilla'` | client brand sent to the server |
92
+ | `debug` | `false` | `true` logs every outgoing packet, or pass a function `(...args) => {}` |
93
+
94
+ ### Bot API
95
+
96
+ The standard mineflayer movement API keeps working:
97
+ `setControlState`, `getControlState`, `clearControlStates`, `controlState`, `look`, `lookAt`, `waitForTicks`,
98
+ `physicsEnabled`, events `physicsTick`, `move`, `forcedMove`, `mount`, `dismount`.
99
+
100
+ Added:
101
+
102
+ - `bot.spectra.player` – the simulated player (`pos`, `vel`, `yRot`, `xRot`, `onGround`, `horizontalCollision`, `fallDistance`, `sprinting`, `abilities`, …)
103
+ - `bot.spectra.flags` – version switches in effect for the connected protocol
104
+ - `bot.spectra.world` – block and fluid lookups used by the engine
105
+ - event `clientLoaded` – 1.21.4+, emitted when `player_loaded` is sent
106
+
107
+ ### Standalone engine
108
+
109
+ The engine can run without a connection, for prediction or tests:
110
+
111
+ ```js
112
+ const { PlayerPhysics, WorldView, versionFlags } = require('spectra-physics')
113
+
114
+ // `source` needs { registry, world: { getColumn(chunkX, chunkZ) }, game: { minY, height } }
115
+ const player = new PlayerPhysics(new WorldView(source), versionFlags(source.registry), {
116
+ attribute: (name, def) => def,
117
+ movementSpeed: (sprinting) => (sprinting ? 0.13 : 0.1),
118
+ effect: () => null,
119
+ food: () => 20,
120
+ usingItem: () => false,
121
+ gameMode: () => 'survival',
122
+ ultraWarm: () => false
123
+ })
124
+
125
+ player.pos = { x: 0.5, y: 80, z: 0.5 }
126
+ player.tick({ forward: true, back: false, left: false, right: false, jump: false, sprint: false, sneak: false })
127
+ ```
128
+
129
+ See [`test/helpers.js`](test/helpers.js) for a complete in-memory world.
130
+
131
+ ## What is simulated
132
+
133
+ - gravity, air drag and ground friction (slipperiness per block, supporting block on 1.20+)
134
+ - collisions against block shapes, step-up, sneaking edge back-off (both the old and the 1.21.2+ rules)
135
+ - walking, sprinting (double-tap and key), sprint-jumping, crouch pose and its bounding box
136
+ - water and lava with flow pushing, climbing (ladders, vines, scaffolding, trapdoors over ladders)
137
+ - cobweb, sweet berry bush, powder snow, honey (sliding), soul sand, slime and bed bouncing
138
+ - levitation, slow falling, jump boost, speed / slowness via attributes, dolphin's grace
139
+ - creative flight, including toggling with a double jump and landing
140
+ - knockback and explosion velocity, 1.21.2+ relative teleports with velocity
141
+ - riding boats (client-controlled) and minecarts
142
+
143
+ Not simulated yet: elytra flight, swimming pose, bubble columns, entity collisions, world border.
144
+
145
+ ## Tests
146
+
147
+ ```bash
148
+ npm test
149
+ ```
150
+
151
+ Offline tests for 1.16.5, 1.18.2, 1.20.4, 1.21.4 and 26.1 cover free fall, landing on full blocks and slabs,
152
+ walking and sprinting speed, jump height, step-up, wall collision, sneaking at an edge, ice and cobwebs.
153
+
154
+ ## Releasing
155
+
156
+ Releases are published to [npm](https://www.npmjs.com/package/spectra-physics) by GitHub Actions.
157
+
158
+ 1. Bump `version` in `package.json` and push to `main`.
159
+ 2. Create a GitHub release with the tag `v<version>` (for example `v1.0.1`).
160
+
161
+ The *Publish to npm* workflow then runs the tests, checks that the tag matches `package.json`, and runs
162
+ `npm publish --provenance --access public`. Publishing needs the repository secret `NPM_TOKEN`
163
+ (an npm granular access token with read and write access to packages and two-factor bypass enabled;
164
+ set it with `gh secret set NPM_TOKEN`). npm limits the lifetime of such tokens, so rotate it before it expires.
165
+
166
+ ## License
167
+
168
+ [GPL-3.0](LICENSE)
package/index.js ADDED
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ const spectraPhysics = require('./lib/plugin')
4
+ const { PlayerPhysics } = require('./lib/engine/physics')
5
+ const BoatPhysics = require('./lib/engine/boat')
6
+ const WorldView = require('./lib/engine/world')
7
+ const AABB = require('./lib/engine/aabb')
8
+ const { versionFlags } = require('./lib/engine/version')
9
+
10
+ // mineflayer.createBot with SpectraPhysics installed in place of the built-in physics plugin
11
+ function createBot (options = {}) {
12
+ const mineflayer = require('mineflayer')
13
+ const { spectra, ...botOptions } = options
14
+ return mineflayer.createBot({
15
+ ...botOptions,
16
+ plugins: { ...(botOptions.plugins ?? {}), physics: spectraPhysics(spectra) }
17
+ })
18
+ }
19
+
20
+ module.exports = spectraPhysics
21
+ module.exports.spectraPhysics = spectraPhysics
22
+ module.exports.createBot = createBot
23
+ module.exports.PlayerPhysics = PlayerPhysics
24
+ module.exports.BoatPhysics = BoatPhysics
25
+ module.exports.WorldView = WorldView
26
+ module.exports.AABB = AABB
27
+ module.exports.versionFlags = versionFlags
@@ -0,0 +1,46 @@
1
+ 'use strict'
2
+
3
+ // Minimal immutable-style AABB matching net.minecraft.world.phys.AABB semantics.
4
+ class AABB {
5
+ constructor (minX, minY, minZ, maxX, maxY, maxZ) {
6
+ this.minX = Math.min(minX, maxX)
7
+ this.minY = Math.min(minY, maxY)
8
+ this.minZ = Math.min(minZ, maxZ)
9
+ this.maxX = Math.max(minX, maxX)
10
+ this.maxY = Math.max(minY, maxY)
11
+ this.maxZ = Math.max(minZ, maxZ)
12
+ }
13
+
14
+ move (x, y, z) {
15
+ return new AABB(this.minX + x, this.minY + y, this.minZ + z, this.maxX + x, this.maxY + y, this.maxZ + z)
16
+ }
17
+
18
+ expandTowards (x, y, z) {
19
+ let minX = this.minX; let minY = this.minY; let minZ = this.minZ
20
+ let maxX = this.maxX; let maxY = this.maxY; let maxZ = this.maxZ
21
+ if (x < 0) minX += x; else if (x > 0) maxX += x
22
+ if (y < 0) minY += y; else if (y > 0) maxY += y
23
+ if (z < 0) minZ += z; else if (z > 0) maxZ += z
24
+ return new AABB(minX, minY, minZ, maxX, maxY, maxZ)
25
+ }
26
+
27
+ inflate (x, y = x, z = x) {
28
+ return new AABB(this.minX - x, this.minY - y, this.minZ - z, this.maxX + x, this.maxY + y, this.maxZ + z)
29
+ }
30
+
31
+ deflate (v) {
32
+ return this.inflate(-v)
33
+ }
34
+
35
+ // strict positive-volume intersection (Shapes.joinIsNotEmpty(..., AND))
36
+ intersectsBox (b) {
37
+ return b[0] < this.maxX && b[3] > this.minX &&
38
+ b[1] < this.maxY && b[4] > this.minY &&
39
+ b[2] < this.maxZ && b[5] > this.minZ
40
+ }
41
+
42
+ min (axis) { return axis === 0 ? this.minX : (axis === 1 ? this.minY : this.minZ) }
43
+ max (axis) { return axis === 0 ? this.maxX : (axis === 1 ? this.maxY : this.maxZ) }
44
+ }
45
+
46
+ module.exports = AABB
@@ -0,0 +1,137 @@
1
+ 'use strict'
2
+ // Client-controlled boat (Boat#tick when isControlledByLocalInstance()).
3
+
4
+ const AABB = require('./aabb')
5
+ const { f32, sin, cos, degToRadF } = require('./mth')
6
+ const { collideLegacy, collideModern } = require('./collision')
7
+
8
+ const BOAT_W = f32(1.375)
9
+ const BOAT_H = f32(0.5625)
10
+ const G = f32(-0.04) // (double)-0.04F == -0.03999999910593033
11
+
12
+ class BoatPhysics {
13
+ constructor (world, V, entity) {
14
+ this.world = world
15
+ this.V = V
16
+ this.pos = { x: entity.position.x, y: entity.position.y, z: entity.position.z }
17
+ this.vel = { x: 0, y: 0, z: 0 }
18
+ this.yRot = entity.yawDegrees ?? 0
19
+ this.xRot = 0
20
+ this.deltaRotation = 0
21
+ this.status = 'IN_AIR'
22
+ this.oldStatus = 'IN_AIR'
23
+ this.landFriction = 0
24
+ this.waterLevel = 0
25
+ this.onGround = false
26
+ this.paddle = [false, false]
27
+ }
28
+
29
+ bb () {
30
+ const h = f32(BOAT_W / 2)
31
+ return new AABB(this.pos.x - h, this.pos.y, this.pos.z - h, this.pos.x + h, this.pos.y + BOAT_H, this.pos.z + h)
32
+ }
33
+
34
+ groundFriction () {
35
+ const bb = this.bb()
36
+ const probe = new AABB(bb.minX, bb.minY - 0.001, bb.minZ, bb.maxX, bb.minY, bb.maxZ)
37
+ const x0 = Math.floor(probe.minX) - 1; const x1 = Math.ceil(probe.maxX) + 1
38
+ const y0 = Math.floor(probe.minY) - 1; const y1 = Math.ceil(probe.maxY) + 1
39
+ const z0 = Math.floor(probe.minZ) - 1; const z1 = Math.ceil(probe.maxZ) + 1
40
+ let sum = 0; let n = 0
41
+ for (let x = x0; x < x1; x++) {
42
+ for (let z = z0; z < z1; z++) {
43
+ const edges = (x === x0 || x === x1 - 1 ? 1 : 0) + (z === z0 || z === z1 - 1 ? 1 : 0)
44
+ if (edges === 2) continue
45
+ for (let y = y0; y < y1; y++) {
46
+ if (edges > 0 && !(y !== y0 && y !== y1 - 1)) continue
47
+ const b = this.world.blockAt(x, y, z)
48
+ if (b.name.includes('lily_pad')) continue
49
+ let hit = false
50
+ for (const s of b.shapes) {
51
+ if (probe.intersectsBox([s[0] + x, s[1] + y, s[2] + z, s[3] + x, s[4] + y, s[5] + z])) { hit = true; break }
52
+ }
53
+ if (hit) { sum += b.friction; n++ }
54
+ }
55
+ }
56
+ }
57
+ return n > 0 ? f32(sum / n) : 0
58
+ }
59
+
60
+ inWater () {
61
+ const bb = this.bb()
62
+ const x0 = Math.floor(bb.minX); const x1 = Math.ceil(bb.maxX)
63
+ const y0 = Math.floor(bb.minY); const y1 = Math.ceil(bb.minY + 0.001)
64
+ const z0 = Math.floor(bb.minZ); const z1 = Math.ceil(bb.maxZ)
65
+ let found = false
66
+ this.waterLevel = -Infinity
67
+ for (let x = x0; x < x1; x++) {
68
+ for (let y = y0; y < y1; y++) {
69
+ for (let z = z0; z < z1; z++) {
70
+ const fl = this.world.fluidAt(x, y, z)
71
+ if (fl && fl.type === 'water') {
72
+ const top = y + this.world.fluidHeight(x, y, z, fl)
73
+ this.waterLevel = Math.max(top, this.waterLevel)
74
+ found = found || bb.minY < top
75
+ }
76
+ }
77
+ }
78
+ }
79
+ return found
80
+ }
81
+
82
+ getStatus () {
83
+ if (this.inWater()) return 'IN_WATER'
84
+ const f = this.groundFriction()
85
+ if (f > 0) { this.landFriction = f; return 'ON_LAND' }
86
+ return 'IN_AIR'
87
+ }
88
+
89
+ floatBoat () {
90
+ let d2 = 0
91
+ let inv = f32(0.05)
92
+ if (this.oldStatus === 'IN_AIR' && this.status !== 'IN_AIR' && this.status !== 'ON_LAND') {
93
+ this.pos.y = this.waterLevel - BOAT_H + 0.101
94
+ this.vel.y = 0
95
+ this.status = 'IN_WATER'
96
+ return
97
+ }
98
+ if (this.status === 'IN_WATER') { d2 = (this.waterLevel - this.pos.y) / BOAT_H; inv = f32(0.9) } else if (this.status === 'IN_AIR') inv = f32(0.9)
99
+ else if (this.status === 'ON_LAND') { inv = this.landFriction; this.landFriction = f32(this.landFriction / 2) }
100
+ this.vel.x *= inv
101
+ this.vel.y += G
102
+ this.vel.z *= inv
103
+ this.deltaRotation = f32(this.deltaRotation * inv)
104
+ if (d2 > 0) this.vel.y = (this.vel.y + d2 * 0.06153846016296973) * 0.75
105
+ }
106
+
107
+ controlBoat (keys) {
108
+ let f = 0
109
+ if (keys.left) this.deltaRotation = f32(this.deltaRotation - 1)
110
+ if (keys.right) this.deltaRotation = f32(this.deltaRotation + 1)
111
+ if (keys.right !== keys.left && !keys.forward && !keys.back) f = f32(f + f32(0.005))
112
+ this.yRot = f32(this.yRot + this.deltaRotation)
113
+ if (keys.forward) f = f32(f + f32(0.04))
114
+ if (keys.back) f = f32(f - f32(0.005))
115
+ const r = degToRadF(this.yRot)
116
+ this.vel.x += sin(f32(-r)) * f
117
+ this.vel.z += cos(r) * f
118
+ this.paddle = [(keys.right && !keys.left) || keys.forward, (keys.left && !keys.right) || keys.forward]
119
+ }
120
+
121
+ tick (keys) {
122
+ this.oldStatus = this.status
123
+ this.status = this.getStatus()
124
+ this.floatBoat()
125
+ this.controlBoat(keys)
126
+ const collide = this.V.modernStepUp ? collideModern : collideLegacy
127
+ const r = collide(this.world, this.vel.x, this.vel.y, this.vel.z, this.bb(), this.onGround, 0)
128
+ if (r.x * r.x + r.y * r.y + r.z * r.z > 1.0e-7) { this.pos.x += r.x; this.pos.y += r.y; this.pos.z += r.z }
129
+ const vertical = this.vel.y !== r.y
130
+ this.onGround = vertical && this.vel.y < 0
131
+ if (this.vel.x !== r.x) this.vel.x = 0
132
+ if (this.vel.z !== r.z) this.vel.z = 0
133
+ if (vertical) this.vel.y = 0
134
+ }
135
+ }
136
+
137
+ module.exports = BoatPhysics
@@ -0,0 +1,124 @@
1
+ 'use strict'
2
+ // Port of Entity.collide / collideBoundingBox / Shapes.collide (VoxelShape.collide semantics).
3
+
4
+ const { f32 } = require('./mth')
5
+
6
+ const EPS = 1.0e-7
7
+
8
+ // VoxelShape.collide(axis, aabb, d) for a list of boxes. axis: 0=x 1=y 2=z
9
+ function collideAxis (axis, bb, boxes, d) {
10
+ if (Math.abs(d) < EPS) return 0
11
+ const a1 = axis === 0 ? 1 : 0
12
+ const a2 = axis === 2 ? 1 : 2
13
+ const bbMin = bb.min(axis); const bbMax = bb.max(axis)
14
+ const o1Min = bb.min(a1); const o1Max = bb.max(a1)
15
+ const o2Min = bb.min(a2); const o2Max = bb.max(a2)
16
+ for (let i = 0; i < boxes.length; i++) {
17
+ const b = boxes[i]
18
+ // overlap on the two other axes (cell index semantics of VoxelShape.collideX)
19
+ if (!(b[a1 + 3] > o1Min + EPS && b[a1] <= o1Max - EPS)) continue
20
+ if (!(b[a2 + 3] > o2Min + EPS && b[a2] <= o2Max - EPS)) continue
21
+ if (d > 0) {
22
+ if (b[axis] > bbMax - EPS) {
23
+ const g = b[axis] - bbMax
24
+ if (g >= -EPS && g < d) d = g
25
+ }
26
+ } else {
27
+ if (b[axis + 3] <= bbMin + EPS) {
28
+ const g = b[axis + 3] - bbMin
29
+ if (g <= EPS && g > d) d = g
30
+ }
31
+ }
32
+ if (Math.abs(d) < EPS) return 0
33
+ }
34
+ return d
35
+ }
36
+
37
+ // Entity.collideWithShapes / collideBoundingBox axis order: Y, then the larger horizontal axis last
38
+ function collideWithShapes (vx, vy, vz, bb, boxes) {
39
+ if (boxes.length === 0) return { x: vx, y: vy, z: vz }
40
+ let dx = vx; let dy = vy; let dz = vz
41
+ if (dy !== 0) {
42
+ dy = collideAxis(1, bb, boxes, dy)
43
+ if (dy !== 0) bb = bb.move(0, dy, 0)
44
+ }
45
+ const zFirst = Math.abs(dx) < Math.abs(dz)
46
+ if (zFirst && dz !== 0) {
47
+ dz = collideAxis(2, bb, boxes, dz)
48
+ if (dz !== 0) bb = bb.move(0, 0, dz)
49
+ }
50
+ if (dx !== 0) {
51
+ dx = collideAxis(0, bb, boxes, dx)
52
+ if (!zFirst && dx !== 0) bb = bb.move(dx, 0, 0)
53
+ }
54
+ if (!zFirst && dz !== 0) {
55
+ dz = collideAxis(2, bb, boxes, dz)
56
+ }
57
+ return { x: dx, y: dy, z: dz }
58
+ }
59
+
60
+ function collideBoundingBox (world, vx, vy, vz, bb) {
61
+ const boxes = world.collisionBoxes(bb.expandTowards(vx, vy, vz))
62
+ return collideWithShapes(vx, vy, vz, bb, boxes)
63
+ }
64
+
65
+ const hdist = (v) => v.x * v.x + v.z * v.z
66
+
67
+ // pre-1.21 step-up (1.14 - 1.20.6)
68
+ function collideLegacy (world, vx, vy, vz, bb, onGround, maxUpStep) {
69
+ const r = (vx * vx + vy * vy + vz * vz) === 0 ? { x: vx, y: vy, z: vz } : collideBoundingBox(world, vx, vy, vz, bb)
70
+ const fx = vx !== r.x; const fy = vy !== r.y; const fz = vz !== r.z
71
+ const groundish = onGround || (fy && vy < 0)
72
+ if (maxUpStep > 0 && groundish && (fx || fz)) {
73
+ let v1 = collideBoundingBox(world, vx, maxUpStep, vz, bb)
74
+ const v2 = collideBoundingBox(world, 0, maxUpStep, 0, bb.expandTowards(vx, 0, vz))
75
+ if (v2.y < maxUpStep) {
76
+ const v3 = collideBoundingBox(world, vx, 0, vz, bb.move(v2.x, v2.y, v2.z))
77
+ const v3a = { x: v3.x + v2.x, y: v3.y + v2.y, z: v3.z + v2.z }
78
+ if (hdist(v3a) > hdist(v1)) v1 = v3a
79
+ }
80
+ if (hdist(v1) > hdist(r)) {
81
+ const down = collideBoundingBox(world, 0, -v1.y + vy, 0, bb.move(v1.x, v1.y, v1.z))
82
+ return { x: v1.x + down.x, y: v1.y + down.y, z: v1.z + down.z }
83
+ }
84
+ }
85
+ return r
86
+ }
87
+
88
+ // Entity.collectCandidateStepUpHeights
89
+ function candidateStepHeights (bb, boxes, maxStep, skip) {
90
+ const set = new Set()
91
+ for (const b of boxes) {
92
+ for (const yc of [b[1], b[4]]) {
93
+ const f = f32(yc - bb.minY)
94
+ if (f < 0 || f === skip) continue
95
+ if (f > maxStep) break
96
+ set.add(f)
97
+ }
98
+ }
99
+ return [...set].sort((a, b) => a - b)
100
+ }
101
+
102
+ // 1.21+ step-up
103
+ function collideModern (world, vx, vy, vz, bb, onGround, maxUpStep) {
104
+ const r = (vx * vx + vy * vy + vz * vz) === 0 ? { x: vx, y: vy, z: vz } : collideBoundingBox(world, vx, vy, vz, bb)
105
+ const fx = vx !== r.x; const fy = vy !== r.y; const fz = vz !== r.z
106
+ const landed = fy && vy < 0
107
+ if (maxUpStep > 0 && (landed || onGround) && (fx || fz)) {
108
+ const bb1 = landed ? bb.move(0, r.y, 0) : bb
109
+ let bb2 = bb1.expandTowards(vx, maxUpStep, vz)
110
+ if (!landed) bb2 = bb2.expandTowards(0, -f32(1.0e-5), 0)
111
+ const boxes = world.collisionBoxes(bb2)
112
+ const heights = candidateStepHeights(bb1, boxes, maxUpStep, f32(r.y))
113
+ for (const h of heights) {
114
+ const v = collideWithShapes(vx, h, vz, bb1, boxes)
115
+ if (hdist(v) > hdist(r)) {
116
+ const d0 = bb.minY - bb1.minY
117
+ return { x: v.x, y: v.y - d0, z: v.z }
118
+ }
119
+ }
120
+ }
121
+ return r
122
+ }
123
+
124
+ module.exports = { collideAxis, collideWithShapes, collideBoundingBox, collideLegacy, collideModern }
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+ // Port of net.minecraft.util.Mth pieces that affect movement results bit-for-bit.
3
+
4
+ const f32 = Math.fround
5
+
6
+ const SIN = new Float32Array(65536)
7
+ for (let i = 0; i < 65536; i++) SIN[i] = Math.sin(i * Math.PI * 2 / 65536)
8
+
9
+ const F_10430 = f32(10430.378)
10
+ const F_16384 = f32(16384)
11
+ const DEG_TO_RAD_F = f32(Math.PI / 180) // (float)Math.PI / 180F
12
+
13
+ function sin (value) {
14
+ return SIN[Math.trunc(f32(f32(value) * F_10430)) & 65535]
15
+ }
16
+
17
+ function cos (value) {
18
+ return SIN[Math.trunc(f32(f32(f32(value) * F_10430) + F_16384)) & 65535]
19
+ }
20
+
21
+ // yRot (float degrees) -> radians exactly like `yRot * ((float)Math.PI / 180F)`
22
+ function degToRadF (deg) {
23
+ return f32(f32(deg) * DEG_TO_RAD_F)
24
+ }
25
+
26
+ function clamp (v, lo, hi) {
27
+ return v < lo ? lo : (v > hi ? hi : v)
28
+ }
29
+
30
+ // Mth.equal(double, double) uses 1.0E-5F
31
+ const EQ_EPS = f32(1.0e-5)
32
+ function equal (a, b) {
33
+ return Math.abs(b - a) < EQ_EPS
34
+ }
35
+
36
+ function wrapDegrees (deg) {
37
+ let d = deg % 360
38
+ if (d >= 180) d -= 360
39
+ if (d < -180) d += 360
40
+ return d
41
+ }
42
+
43
+ function lengthSqr (x, y, z) {
44
+ return x * x + y * y + z * z
45
+ }
46
+
47
+ module.exports = { f32, sin, cos, degToRadF, clamp, equal, wrapDegrees, lengthSqr }