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/LICENSE +674 -0
- package/README.md +168 -0
- package/index.js +27 -0
- package/lib/engine/aabb.js +46 -0
- package/lib/engine/boat.js +137 -0
- package/lib/engine/collision.js +124 -0
- package/lib/engine/mth.js +47 -0
- package/lib/engine/physics.js +780 -0
- package/lib/engine/version.js +55 -0
- package/lib/engine/world.js +162 -0
- package/lib/plugin.js +574 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# SpectraPhysics
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/spectra-physics)
|
|
4
|
+
[](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 }
|