spectra-physics 1.0.0 → 1.1.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 +31 -22
- package/index.js +2 -2
- package/lib/client/connection.js +83 -0
- package/lib/client/controls.js +78 -0
- package/lib/client/environment.js +125 -0
- package/lib/client/index.js +72 -0
- package/lib/client/reporter.js +91 -0
- package/lib/client/riding.js +65 -0
- package/lib/client/server.js +90 -0
- package/lib/client/teleport.js +108 -0
- package/lib/client/ticker.js +144 -0
- package/lib/engine/player/blocks.js +134 -0
- package/lib/engine/player/effects.js +152 -0
- package/lib/engine/player/fluids.js +141 -0
- package/lib/engine/player/index.js +202 -0
- package/lib/engine/player/input.js +86 -0
- package/lib/engine/player/move.js +135 -0
- package/lib/engine/player/pose.js +95 -0
- package/lib/engine/player/push.js +27 -0
- package/lib/engine/player/travel.js +203 -0
- package/lib/engine/version.js +49 -26
- package/lib/engine/world.js +3 -0
- package/package.json +1 -1
- package/lib/engine/physics.js +0 -780
- package/lib/plugin.js +0 -574
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const WorldView = require('../world')
|
|
4
|
+
const { f32 } = require('../mth')
|
|
5
|
+
|
|
6
|
+
const EYE_OFFSET = f32(0.11111111)
|
|
7
|
+
const WATER_PUSH = 0.014
|
|
8
|
+
const MIN_PUSH = 0.0045000000000000005
|
|
9
|
+
const SIDES = [[0, -1], [0, 1], [-1, 0], [1, 0]]
|
|
10
|
+
|
|
11
|
+
// FlowingFluid#getFlow
|
|
12
|
+
function flowAt (world, x, y, z, fluid) {
|
|
13
|
+
const own = WorldView.ownHeight(fluid)
|
|
14
|
+
let dx = 0
|
|
15
|
+
let dz = 0
|
|
16
|
+
for (const [sx, sz] of SIDES) {
|
|
17
|
+
const nx = x + sx
|
|
18
|
+
const nz = z + sz
|
|
19
|
+
const other = world.fluidAt(nx, y, nz)
|
|
20
|
+
if (other && other.type !== fluid.type) continue
|
|
21
|
+
let height = other ? WorldView.ownHeight(other) : 0
|
|
22
|
+
let diff = 0
|
|
23
|
+
if (height === 0) {
|
|
24
|
+
const block = world.blockAt(nx, y, nz)
|
|
25
|
+
if (block.shapes.length === 0 || block.fluid) {
|
|
26
|
+
const below = world.fluidAt(nx, y - 1, nz)
|
|
27
|
+
if (!below || below.type === fluid.type) {
|
|
28
|
+
height = below ? WorldView.ownHeight(below) : 0
|
|
29
|
+
if (height > 0) diff = f32(own - f32(height - f32(0.8888889)))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
diff = f32(own - height)
|
|
34
|
+
}
|
|
35
|
+
dx += sx * diff
|
|
36
|
+
dz += sz * diff
|
|
37
|
+
}
|
|
38
|
+
let vx = dx; let vy = 0; let vz = dz
|
|
39
|
+
if (fluid.falling) {
|
|
40
|
+
for (const [sx, sz] of SIDES) {
|
|
41
|
+
const side = world.blockAt(x + sx, y, z + sz)
|
|
42
|
+
const sideAbove = world.blockAt(x + sx, y + 1, z + sz)
|
|
43
|
+
if ((side.fullCube && !side.fluid) || (sideAbove.fullCube && !sideAbove.fluid)) {
|
|
44
|
+
const l = Math.sqrt(vx * vx + vz * vz)
|
|
45
|
+
if (l < 1.0e-4) { vx = 0; vz = 0 } else { vx /= l; vz /= l }
|
|
46
|
+
vy = -6
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const length = Math.sqrt(vx * vx + vy * vy + vz * vz)
|
|
52
|
+
return length < 1.0e-4 ? { x: 0, y: 0, z: 0 } : { x: vx / length, y: vy / length, z: vz / length }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Entity#updateFluidHeightAndDoFluidPushing
|
|
56
|
+
function touchFluid (player, type, scale) {
|
|
57
|
+
const bb = player.bb().deflate(0.001)
|
|
58
|
+
const x0 = Math.floor(bb.minX); const x1 = Math.ceil(bb.maxX)
|
|
59
|
+
const y0 = Math.floor(bb.minY); const y1 = Math.ceil(bb.maxY)
|
|
60
|
+
const z0 = Math.floor(bb.minZ); const z1 = Math.ceil(bb.maxZ)
|
|
61
|
+
const pushed = !player.abilities.flying
|
|
62
|
+
let height = 0
|
|
63
|
+
let touching = false
|
|
64
|
+
let px = 0; let py = 0; let pz = 0; let count = 0
|
|
65
|
+
for (let x = x0; x < x1; x++) {
|
|
66
|
+
for (let y = y0; y < y1; y++) {
|
|
67
|
+
for (let z = z0; z < z1; z++) {
|
|
68
|
+
const fluid = player.world.fluidAt(x, y, z)
|
|
69
|
+
if (!fluid || fluid.type !== type) continue
|
|
70
|
+
const top = y + player.world.fluidHeight(x, y, z, fluid)
|
|
71
|
+
if (top < bb.minY) continue
|
|
72
|
+
touching = true
|
|
73
|
+
height = Math.max(top - bb.minY, height)
|
|
74
|
+
if (!pushed) continue
|
|
75
|
+
const flow = flowAt(player.world, x, y, z, fluid)
|
|
76
|
+
const k = height < 0.4 ? height : 1
|
|
77
|
+
px += flow.x * k; py += flow.y * k; pz += flow.z * k
|
|
78
|
+
count++
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (px * px + py * py + pz * pz > 0) {
|
|
83
|
+
px /= count; py /= count; pz /= count
|
|
84
|
+
px *= scale; py *= scale; pz *= scale
|
|
85
|
+
const length = Math.sqrt(px * px + py * py + pz * pz)
|
|
86
|
+
if (Math.abs(player.vel.x) < 0.003 && Math.abs(player.vel.z) < 0.003 && length < MIN_PUSH) {
|
|
87
|
+
px = px / length * MIN_PUSH; py = py / length * MIN_PUSH; pz = pz / length * MIN_PUSH
|
|
88
|
+
}
|
|
89
|
+
player.vel.x += px; player.vel.y += py; player.vel.z += pz
|
|
90
|
+
}
|
|
91
|
+
player.fluidHeight[type] = height
|
|
92
|
+
return touching
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Entity#updateInWaterStateAndDoWaterCurrentPushing
|
|
96
|
+
function updateWater (player) {
|
|
97
|
+
player.wasTouchingWater = touchFluid(player, 'water', WATER_PUSH)
|
|
98
|
+
if (player.wasTouchingWater) player.fallDistance = 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function updateEyes (player) {
|
|
102
|
+
player.wasEyeInWater = player.eyeInWater
|
|
103
|
+
const eyeY = player.pos.y + player.eyeHeight() - EYE_OFFSET
|
|
104
|
+
const bx = Math.floor(player.pos.x)
|
|
105
|
+
const by = Math.floor(eyeY)
|
|
106
|
+
const bz = Math.floor(player.pos.z)
|
|
107
|
+
const fluid = player.world.fluidAt(bx, by, bz)
|
|
108
|
+
player.eyeInWater = !!(fluid && fluid.type === 'water' && by + player.world.fluidHeight(bx, by, bz, fluid) > eyeY)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Player#updateSwimming
|
|
112
|
+
function updateSwimming (player) {
|
|
113
|
+
if (player.abilities.flying || player.riding) {
|
|
114
|
+
player.swimming = false
|
|
115
|
+
} else if (player.swimming) {
|
|
116
|
+
player.swimming = player.sprinting && player.isInWater()
|
|
117
|
+
} else {
|
|
118
|
+
const fluid = player.world.fluidAt(Math.floor(player.pos.x), Math.floor(player.pos.y), Math.floor(player.pos.z))
|
|
119
|
+
const feet = !player.V.swimmingNeedsFeetInWater || (fluid && fluid.type === 'water')
|
|
120
|
+
player.swimming = player.sprinting && player.isUnderWater() && !!feet
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function updateFluids (player) {
|
|
125
|
+
player.fluidHeight.water = 0
|
|
126
|
+
player.fluidHeight.lava = 0
|
|
127
|
+
updateWater(player)
|
|
128
|
+
touchFluid(player, 'lava', player.env.ultraWarm() ? 0.007 : 0.0023333333333333335)
|
|
129
|
+
updateEyes(player)
|
|
130
|
+
updateSwimming(player)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// LivingEntity#getFluidFallingAdjustedMovement
|
|
134
|
+
function fluidFallingAdjusted (player, gravity, falling) {
|
|
135
|
+
if (player.sprinting) return
|
|
136
|
+
const vy = player.vel.y
|
|
137
|
+
if (falling && Math.abs(vy - 0.005) >= 0.003 && Math.abs(vy - gravity / 16) < 0.003) player.vel.y = -0.003
|
|
138
|
+
else player.vel.y = vy - gravity / 16
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { updateFluids, updateWater, fluidFallingAdjusted }
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// One client tick of the local player: LocalPlayer#aiStep -> Player#aiStep -> LivingEntity#aiStep -> travel.
|
|
3
|
+
|
|
4
|
+
const { f32 } = require('../mth')
|
|
5
|
+
const pose = require('./pose')
|
|
6
|
+
const { readKeys } = require('./input')
|
|
7
|
+
const { updateFluids } = require('./fluids')
|
|
8
|
+
const { travel, handleJump } = require('./travel')
|
|
9
|
+
const { applyEffectsAfterTravel } = require('./effects')
|
|
10
|
+
const { pushByEntities } = require('./push')
|
|
11
|
+
|
|
12
|
+
const F_002 = f32(0.02)
|
|
13
|
+
const F_004 = f32(0.04)
|
|
14
|
+
const F_1EM5 = f32(1.0e-5)
|
|
15
|
+
const LEGACY_AIR_SPRINT = f32(F_002 + 0.005999999865889549)
|
|
16
|
+
const AIR_SPRINT = f32(F_002 + f32(0.006))
|
|
17
|
+
const STEP = f32(0.6)
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ENV = {
|
|
20
|
+
attribute: (name, fallback) => fallback,
|
|
21
|
+
movementSpeed: (sprinting) => sprinting ? 0.13 : 0.1,
|
|
22
|
+
effect: () => null,
|
|
23
|
+
bootsEnchant: () => 0,
|
|
24
|
+
bootsItem: () => null,
|
|
25
|
+
food: () => 20,
|
|
26
|
+
usingItem: () => false,
|
|
27
|
+
gameMode: () => 'survival',
|
|
28
|
+
ultraWarm: () => false,
|
|
29
|
+
nearbyEntities: () => []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class PlayerPhysics {
|
|
33
|
+
constructor (world, V, env = {}) {
|
|
34
|
+
this.world = world
|
|
35
|
+
this.V = V
|
|
36
|
+
this.env = { ...DEFAULT_ENV, ...env }
|
|
37
|
+
this.reset()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
reset () {
|
|
41
|
+
this.pos = { x: 0, y: 0, z: 0 }
|
|
42
|
+
this.vel = { x: 0, y: 0, z: 0 }
|
|
43
|
+
this.yRot = 0
|
|
44
|
+
this.xRot = 0
|
|
45
|
+
this.pose = 'standing'
|
|
46
|
+
this.onGround = false
|
|
47
|
+
this.horizontalCollision = false
|
|
48
|
+
this.minorHorizontalCollision = false
|
|
49
|
+
this.verticalCollision = false
|
|
50
|
+
this.fallDistance = 0
|
|
51
|
+
this.sprinting = false
|
|
52
|
+
this.swimming = false
|
|
53
|
+
this.riding = false
|
|
54
|
+
this.noJumpDelay = 0
|
|
55
|
+
this.jumpTriggerTime = 0
|
|
56
|
+
this.sprintTriggerTime = 0
|
|
57
|
+
this.legacyFlyingSpeed = F_002
|
|
58
|
+
this.stuck = null
|
|
59
|
+
this.wasTouchingWater = false
|
|
60
|
+
this.fluidHeight = { water: 0, lava: 0 }
|
|
61
|
+
this.eyeInWater = false
|
|
62
|
+
this.wasEyeInWater = false
|
|
63
|
+
this.firstTick = true
|
|
64
|
+
this.supportingBlock = null
|
|
65
|
+
this.onGroundNoBlocks = false
|
|
66
|
+
this.movesThisTick = []
|
|
67
|
+
this.tickStart = { x: 0, y: 0, z: 0 }
|
|
68
|
+
this.abilities = { invulnerable: false, flying: false, mayfly: false, instabuild: false, flyingSpeed: f32(0.05), walkingSpeed: f32(0.1) }
|
|
69
|
+
this.input = { left: 0, forward: 0, forwardKey: 0, jumping: false, shift: false, keys: {} }
|
|
70
|
+
this.xxa = 0
|
|
71
|
+
this.zza = 0
|
|
72
|
+
this.events = { abilitiesChanged: false }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
height () { return pose.POSES[this.pose].height }
|
|
76
|
+
eyeHeight () { return pose.POSES[this.pose].eye }
|
|
77
|
+
bbAt (x, y, z) { return pose.boxFor(this.pose, x, y, z) }
|
|
78
|
+
bb () { return this.bbAt(this.pos.x, this.pos.y, this.pos.z) }
|
|
79
|
+
|
|
80
|
+
maxUpStep () {
|
|
81
|
+
return this.V.attributes2005 ? f32(this.env.attribute('step_height', 0.6)) : STEP
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// the server applies the sprint modifier as soon as it sees START_SPRINTING, so the speed follows it in the same tick
|
|
85
|
+
movementSpeed () { return f32(this.env.movementSpeed(this.sprinting)) }
|
|
86
|
+
|
|
87
|
+
isInWater () { return this.wasTouchingWater }
|
|
88
|
+
isInLava () { return !this.firstTick && this.fluidHeight.lava > 0 }
|
|
89
|
+
isUnderWater () { return this.wasEyeInWater && this.isInWater() }
|
|
90
|
+
|
|
91
|
+
hasEnoughImpulseToStartSprint () {
|
|
92
|
+
if (this.isUnderWater()) return this.input.forward > F_1EM5
|
|
93
|
+
return this.input.forwardKey > 0 && (this.V.inputModel === 'modern' || this.input.forward >= 0.8)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
tick (keys) {
|
|
97
|
+
this.tickStart = { ...this.pos }
|
|
98
|
+
this.movesThisTick.length = 0
|
|
99
|
+
this.baseTick()
|
|
100
|
+
|
|
101
|
+
const previousJump = this.input.jumping
|
|
102
|
+
const previousShift = this.input.shift
|
|
103
|
+
const previousImpulse = this.hasEnoughImpulseToStartSprint()
|
|
104
|
+
const slow = pose.isMovingSlowly(this, previousShift)
|
|
105
|
+
readKeys(this, keys, slow)
|
|
106
|
+
pose.pushOutOfBlocks(this)
|
|
107
|
+
this.updateSprinting(keys, previousShift, previousImpulse, slow)
|
|
108
|
+
this.updateFlightToggle(previousJump)
|
|
109
|
+
if (this.isInWater() && this.input.shift && !this.abilities.flying) this.vel.y -= F_004
|
|
110
|
+
if (this.abilities.flying) {
|
|
111
|
+
const direction = (this.input.jumping ? 1 : 0) - (this.input.shift ? 1 : 0)
|
|
112
|
+
if (direction !== 0) this.vel.y += f32(f32(direction * this.abilities.flyingSpeed) * 3)
|
|
113
|
+
}
|
|
114
|
+
if (this.jumpTriggerTime > 0) this.jumpTriggerTime--
|
|
115
|
+
|
|
116
|
+
this.livingAiStep()
|
|
117
|
+
|
|
118
|
+
this.legacyFlyingSpeed = this.sprinting ? (this.V.airSprintFloatSum ? AIR_SPRINT : LEGACY_AIR_SPRINT) : F_002
|
|
119
|
+
if (this.onGround && this.abilities.flying && this.env.gameMode() !== 'spectator') {
|
|
120
|
+
this.abilities.flying = false
|
|
121
|
+
this.events.abilitiesChanged = true
|
|
122
|
+
}
|
|
123
|
+
pose.updatePose(this)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Entity#baseTick
|
|
127
|
+
baseTick () {
|
|
128
|
+
updateFluids(this)
|
|
129
|
+
if (this.isInLava()) this.fallDistance = this.V.fallDistanceDouble ? this.fallDistance * 0.5 : f32(this.fallDistance * 0.5)
|
|
130
|
+
this.firstTick = false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
livingAiStep () {
|
|
134
|
+
if (this.noJumpDelay > 0) this.noJumpDelay--
|
|
135
|
+
this.cutTinyVelocity()
|
|
136
|
+
this.xxa = this.input.left
|
|
137
|
+
this.zza = this.input.forward
|
|
138
|
+
handleJump(this)
|
|
139
|
+
if (this.env.gameMode() === 'spectator') {
|
|
140
|
+
this.pos.x += this.vel.x; this.pos.y += this.vel.y; this.pos.z += this.vel.z
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
travel(this)
|
|
144
|
+
applyEffectsAfterTravel(this)
|
|
145
|
+
pushByEntities(this)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
cutTinyVelocity () {
|
|
149
|
+
const v = this.vel
|
|
150
|
+
if (this.V.horizontalThreshold && !this.riding) {
|
|
151
|
+
if (v.x * v.x + v.z * v.z < 9.0e-6) { v.x = 0; v.z = 0 }
|
|
152
|
+
} else {
|
|
153
|
+
if (Math.abs(v.x) < 0.003) v.x = 0
|
|
154
|
+
if (Math.abs(v.z) < 0.003) v.z = 0
|
|
155
|
+
}
|
|
156
|
+
if (Math.abs(v.y) < 0.003) v.y = 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// sprint rules of LocalPlayer#aiStep
|
|
160
|
+
updateSprinting (keys, previousShift, previousImpulse, slow) {
|
|
161
|
+
const V = this.V
|
|
162
|
+
const food = this.env.food() > 6 || this.abilities.mayfly
|
|
163
|
+
const blocked = this.env.usingItem() || this.env.effect('blindness') != null ||
|
|
164
|
+
(V.playerInputPacket && slow && !this.isUnderWater())
|
|
165
|
+
if (previousShift) this.sprintTriggerTime = 0
|
|
166
|
+
if (this.sprintTriggerTime > 0) this.sprintTriggerTime--
|
|
167
|
+
|
|
168
|
+
const impulse = this.hasEnoughImpulseToStartSprint()
|
|
169
|
+
if (!this.sprinting && (this.onGround || this.isUnderWater()) && !previousShift && !previousImpulse && impulse && food && !blocked) {
|
|
170
|
+
if (this.sprintTriggerTime <= 0 && !keys.sprint) this.sprintTriggerTime = 7
|
|
171
|
+
else this.sprinting = true
|
|
172
|
+
}
|
|
173
|
+
if (!this.sprinting && (!this.isInWater() || this.isUnderWater()) && impulse && food && !blocked && keys.sprint) {
|
|
174
|
+
this.sprinting = true
|
|
175
|
+
}
|
|
176
|
+
if (!this.sprinting) return
|
|
177
|
+
|
|
178
|
+
const noForward = !(this.input.forward > F_1EM5) || !food
|
|
179
|
+
if (this.swimming) {
|
|
180
|
+
if ((!this.onGround && !this.input.shift && noForward) || !this.isInWater()) this.sprinting = false
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
const wall = V.minorCollision ? this.horizontalCollision && !this.minorHorizontalCollision : this.horizontalCollision
|
|
184
|
+
const sneaking = V.playerInputPacket && slow && !this.isUnderWater()
|
|
185
|
+
if (noForward || wall || (this.isInWater() && !this.isUnderWater()) || sneaking) this.sprinting = false
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// double tap jump toggles creative flight
|
|
189
|
+
updateFlightToggle (previousJump) {
|
|
190
|
+
if (!this.abilities.mayfly || this.env.gameMode() === 'spectator') return
|
|
191
|
+
if (previousJump || !this.input.jumping) return
|
|
192
|
+
if (this.jumpTriggerTime === 0) {
|
|
193
|
+
this.jumpTriggerTime = 7
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
this.abilities.flying = !this.abilities.flying
|
|
197
|
+
this.events.abilitiesChanged = true
|
|
198
|
+
this.jumpTriggerTime = 0
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { PlayerPhysics }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { f32, sin, cos, degToRadF } = require('../mth')
|
|
4
|
+
|
|
5
|
+
const F_098 = f32(0.98)
|
|
6
|
+
const F_02 = f32(0.2)
|
|
7
|
+
const F_03 = f32(0.3)
|
|
8
|
+
const F_MIN_LENGTH = f32(1.0e-4)
|
|
9
|
+
|
|
10
|
+
const impulse = (positive, negative) => positive === negative ? 0 : (positive ? 1 : -1)
|
|
11
|
+
|
|
12
|
+
function sneakMultiplier (player) {
|
|
13
|
+
return player.V.efficiencyAttributes ? f32(player.env.attribute('sneaking_speed', 0.3)) : F_03
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// KeyboardInput + LivingEntity#aiStep scaling, 1.16 - 1.21.4
|
|
17
|
+
function legacyInput (player, left, forward, slow) {
|
|
18
|
+
let x = left
|
|
19
|
+
let z = forward
|
|
20
|
+
if (slow) {
|
|
21
|
+
const k = sneakMultiplier(player)
|
|
22
|
+
x = f32(x * k); z = f32(z * k)
|
|
23
|
+
}
|
|
24
|
+
if (player.env.usingItem()) { x = f32(x * F_02); z = f32(z * F_02) }
|
|
25
|
+
return [f32(x * F_098), f32(z * F_098)]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// LocalPlayer#modifyInput, 1.21.5+
|
|
29
|
+
function modernInput (player, left, forward, slow) {
|
|
30
|
+
const length = f32(Math.sqrt(f32(left * left + forward * forward)))
|
|
31
|
+
if (length < F_MIN_LENGTH) return [0, 0]
|
|
32
|
+
let x = f32(f32(left / length) * F_098)
|
|
33
|
+
let z = f32(f32(forward / length) * F_098)
|
|
34
|
+
if (player.env.usingItem()) { x = f32(x * F_02); z = f32(z * F_02) }
|
|
35
|
+
if (slow) {
|
|
36
|
+
const k = sneakMultiplier(player)
|
|
37
|
+
x = f32(x * k); z = f32(z * k)
|
|
38
|
+
}
|
|
39
|
+
const scaled = f32(Math.sqrt(f32(x * x + z * z)))
|
|
40
|
+
if (scaled <= 0) return [x, z]
|
|
41
|
+
const nx = f32(x / scaled)
|
|
42
|
+
const nz = f32(z / scaled)
|
|
43
|
+
const ax = Math.abs(nx)
|
|
44
|
+
const az = Math.abs(nz)
|
|
45
|
+
const ratio = az > ax ? f32(ax / az) : f32(az / ax)
|
|
46
|
+
const toSquare = f32(Math.sqrt(f32(1 + f32(ratio * ratio))))
|
|
47
|
+
const k = Math.min(f32(scaled * toSquare), 1)
|
|
48
|
+
return [f32(nx * k), f32(nz * k)]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readKeys (player, keys, slow) {
|
|
52
|
+
const forward = impulse(keys.forward, keys.back)
|
|
53
|
+
const left = impulse(keys.left, keys.right)
|
|
54
|
+
const [xxa, zza] = player.V.inputModel === 'modern'
|
|
55
|
+
? modernInput(player, left, forward, slow)
|
|
56
|
+
: legacyInput(player, left, forward, slow)
|
|
57
|
+
player.input = {
|
|
58
|
+
left: xxa,
|
|
59
|
+
forward: zza,
|
|
60
|
+
forwardKey: forward,
|
|
61
|
+
jumping: !!keys.jump,
|
|
62
|
+
shift: !!keys.sneak,
|
|
63
|
+
keys: { ...keys }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Entity#moveRelative
|
|
68
|
+
function moveRelative (player, speed, ix, iy, iz) {
|
|
69
|
+
const lengthSq = ix * ix + iy * iy + iz * iz
|
|
70
|
+
if (lengthSq < 1.0e-7) return
|
|
71
|
+
let x = ix; let y = iy; let z = iz
|
|
72
|
+
if (lengthSq > 1) {
|
|
73
|
+
const length = player.V.floatSqrt ? f32(Math.sqrt(lengthSq)) : Math.sqrt(lengthSq)
|
|
74
|
+
if (length < player.V.normalizeEpsilon) return
|
|
75
|
+
x /= length; y /= length; z /= length
|
|
76
|
+
}
|
|
77
|
+
x *= speed; y *= speed; z *= speed
|
|
78
|
+
const r = degToRadF(player.yRot)
|
|
79
|
+
const s = sin(r)
|
|
80
|
+
const c = cos(r)
|
|
81
|
+
player.vel.x += x * c - z * s
|
|
82
|
+
player.vel.y += y
|
|
83
|
+
player.vel.z += z * c + x * s
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { readKeys, moveRelative }
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { f32, sin, cos, degToRadF, equal } = require('../mth')
|
|
4
|
+
const { collideLegacy, collideModern } = require('../collision')
|
|
5
|
+
const { onPos, speedFactor, updateSupportingBlock } = require('./blocks')
|
|
6
|
+
const { checkInsideBlocksLegacy } = require('./effects')
|
|
7
|
+
const { updateWater } = require('./fluids')
|
|
8
|
+
|
|
9
|
+
const F_066 = f32(0.66)
|
|
10
|
+
const F_1EM5 = f32(1.0e-5)
|
|
11
|
+
const MINOR_ANGLE = f32(0.13962634)
|
|
12
|
+
|
|
13
|
+
// Entity#maybeBackOffFromEdge (1.11+ form, which is also what server-side prediction uses)
|
|
14
|
+
function backOffFromEdge (player, vx, vy, vz) {
|
|
15
|
+
if (player.abilities.flying || !player.input.shift || vy > 0) return [vx, vz]
|
|
16
|
+
const step = player.maxUpStep()
|
|
17
|
+
const bb = player.bb()
|
|
18
|
+
const aboveGround = player.onGround ||
|
|
19
|
+
(player.fallDistance < step && !player.world.noCollision(bb.move(0, player.fallDistance - step, 0)))
|
|
20
|
+
if (!aboveGround) return [vx, vz]
|
|
21
|
+
const free = (x, z) => player.world.noCollision(bb.move(x, -step, z))
|
|
22
|
+
const shrink = (d) => (d < 0.05 && d >= -0.05) ? 0 : (d > 0 ? d - 0.05 : d + 0.05)
|
|
23
|
+
let x = vx
|
|
24
|
+
let z = vz
|
|
25
|
+
while (x !== 0 && free(x, 0)) x = shrink(x)
|
|
26
|
+
while (z !== 0 && free(0, z)) z = shrink(z)
|
|
27
|
+
while (x !== 0 && z !== 0 && free(x, z)) { x = shrink(x); z = shrink(z) }
|
|
28
|
+
return [x, z]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// LocalPlayer#isHorizontalCollisionMinor
|
|
32
|
+
function isMinorCollision (player, mx, mz) {
|
|
33
|
+
const r = degToRadF(player.yRot)
|
|
34
|
+
const s = sin(r)
|
|
35
|
+
const c = cos(r)
|
|
36
|
+
const ix = player.xxa * c - player.zza * s
|
|
37
|
+
const iz = player.zza * c + player.xxa * s
|
|
38
|
+
const inputSq = ix * ix + iz * iz
|
|
39
|
+
const moveSq = mx * mx + mz * mz
|
|
40
|
+
if (inputSq < F_1EM5 || moveSq < F_1EM5) return false
|
|
41
|
+
return Math.acos((ix * mx + iz * mz) / Math.sqrt(inputSq * moveSq)) < MINOR_ANGLE
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function landOn (player, block) {
|
|
45
|
+
const bounce = !player.input.shift && (block.isSlime ? 1 : (block.isBed ? F_066 : 0))
|
|
46
|
+
if (bounce && player.vel.y < 0) player.vel.y = -player.vel.y * bounce
|
|
47
|
+
else if (!bounce) player.vel.y = 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stepOn (player, block) {
|
|
51
|
+
if (!block.isSlime) return
|
|
52
|
+
const vy = Math.abs(player.vel.y)
|
|
53
|
+
if (vy < 0.1) {
|
|
54
|
+
const k = 0.4 + vy * 0.2
|
|
55
|
+
player.vel.x *= k
|
|
56
|
+
player.vel.z *= k
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function applyPosition (player, bb, r) {
|
|
61
|
+
if (player.V.posFromBoundingBox) {
|
|
62
|
+
const moved = bb.move(r.x, r.y, r.z)
|
|
63
|
+
player.pos.x = (moved.minX + moved.maxX) / 2
|
|
64
|
+
player.pos.y = moved.minY
|
|
65
|
+
player.pos.z = (moved.minZ + moved.maxZ) / 2
|
|
66
|
+
} else {
|
|
67
|
+
player.pos.x += r.x
|
|
68
|
+
player.pos.y += r.y
|
|
69
|
+
player.pos.z += r.z
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function zeroBlockedAxes (player, vx, vz, r) {
|
|
74
|
+
const V = player.V
|
|
75
|
+
if (V.epsilonCollisionFlags) {
|
|
76
|
+
if (!equal(vx, r.x)) player.vel.x = 0
|
|
77
|
+
if (!equal(vz, r.z)) player.vel.z = 0
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
// 1.14 - 1.18.1: the second write restores X when both axes are blocked
|
|
81
|
+
const before = { ...player.vel }
|
|
82
|
+
if (vx !== r.x) player.vel = { x: 0, y: before.y, z: before.z }
|
|
83
|
+
if (vz !== r.z) player.vel = { x: V.keepsXOnCornerCollision ? before.x : player.vel.x, y: before.y, z: 0 }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Entity#move(MoverType.SELF, movement)
|
|
87
|
+
function move (player, mx, my, mz) {
|
|
88
|
+
const V = player.V
|
|
89
|
+
if (player.stuck) {
|
|
90
|
+
mx *= player.stuck.x; my *= player.stuck.y; mz *= player.stuck.z
|
|
91
|
+
player.stuck = null
|
|
92
|
+
player.vel = { x: 0, y: 0, z: 0 }
|
|
93
|
+
}
|
|
94
|
+
const [bx, bz] = backOffFromEdge(player, mx, my, mz)
|
|
95
|
+
mx = bx; mz = bz
|
|
96
|
+
|
|
97
|
+
const bb = player.bb()
|
|
98
|
+
const from = { ...player.pos }
|
|
99
|
+
const collide = V.modernStepUp ? collideModern : collideLegacy
|
|
100
|
+
const r = collide(player.world, mx, my, mz, bb, player.onGround, player.maxUpStep())
|
|
101
|
+
const lengthSq = r.x * r.x + r.y * r.y + r.z * r.z
|
|
102
|
+
const wantedSq = mx * mx + my * my + mz * mz
|
|
103
|
+
const applies = V.modernMoveThreshold ? (lengthSq > 1.0e-7 || wantedSq - lengthSq < 1.0e-7) : lengthSq > 1.0e-7
|
|
104
|
+
if (applies) applyPosition(player, bb, r)
|
|
105
|
+
|
|
106
|
+
const xBlocked = V.epsilonCollisionFlags ? !equal(mx, r.x) : mx !== r.x
|
|
107
|
+
const zBlocked = V.epsilonCollisionFlags ? !equal(mz, r.z) : mz !== r.z
|
|
108
|
+
player.horizontalCollision = xBlocked || zBlocked
|
|
109
|
+
player.minorHorizontalCollision = V.minorCollision && player.horizontalCollision && isMinorCollision(player, r.x, r.z)
|
|
110
|
+
player.verticalCollision = my !== r.y
|
|
111
|
+
player.onGround = player.verticalCollision && my < 0
|
|
112
|
+
updateSupportingBlock(player, player.onGround, r.x, r.z)
|
|
113
|
+
|
|
114
|
+
if (V.fluidRecheckInMove && !player.wasTouchingWater && !player.riding) updateWater(player)
|
|
115
|
+
if (player.onGround) {
|
|
116
|
+
player.fallDistance = 0
|
|
117
|
+
} else if (r.y < 0) {
|
|
118
|
+
player.fallDistance = V.fallDistanceDouble ? player.fallDistance - r.y : f32(player.fallDistance - r.y)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
zeroBlockedAxes(player, mx, mz, r)
|
|
122
|
+
const standingOn = onPos(player)
|
|
123
|
+
const block = player.world.blockAt(standingOn.x, standingOn.y, standingOn.z)
|
|
124
|
+
if (my !== r.y) landOn(player, block)
|
|
125
|
+
if (player.onGround && !player.input.shift) stepOn(player, block)
|
|
126
|
+
|
|
127
|
+
if (V.blockEffectsPerAxis && applies) player.movesThisTick.push({ from, to: { ...player.pos } })
|
|
128
|
+
if (!V.blockEffectsAfterTravel) checkInsideBlocksLegacy(player)
|
|
129
|
+
|
|
130
|
+
const factor = speedFactor(player)
|
|
131
|
+
player.vel.x *= factor
|
|
132
|
+
player.vel.z *= factor
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { move }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const AABB = require('../aabb')
|
|
4
|
+
const { f32 } = require('../mth')
|
|
5
|
+
|
|
6
|
+
const POSES = {
|
|
7
|
+
standing: { width: f32(0.6), height: f32(1.8), eye: f32(1.62) },
|
|
8
|
+
crouching: { width: f32(0.6), height: f32(1.5), eye: f32(1.27) },
|
|
9
|
+
swimming: { width: f32(0.6), height: f32(0.6), eye: f32(0.4) }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function boxFor (pose, x, y, z) {
|
|
13
|
+
const d = POSES[pose]
|
|
14
|
+
const r = f32(d.width / 2)
|
|
15
|
+
return new AABB(x - r, y, z - r, x + r, y + d.height, z + r)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function canEnterPose (player, pose, x = player.pos.x, y = player.pos.y, z = player.pos.z) {
|
|
19
|
+
return player.world.noCollision(boxFor(pose, x, y, z).deflate(1.0e-7))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Player#updatePlayerPose, end of the tick
|
|
23
|
+
function updatePose (player) {
|
|
24
|
+
if (!canEnterPose(player, 'swimming')) return
|
|
25
|
+
let pose = 'standing'
|
|
26
|
+
if (player.swimming) pose = 'swimming'
|
|
27
|
+
else if (player.input.shift && !player.abilities.flying) pose = 'crouching'
|
|
28
|
+
if (!canEnterPose(player, pose)) pose = canEnterPose(player, 'crouching') ? 'crouching' : 'swimming'
|
|
29
|
+
player.pose = pose
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// LocalPlayer#isMovingSlowly, decided from the previous tick's keys and pose
|
|
33
|
+
function isMovingSlowly (player, previousShift) {
|
|
34
|
+
const crouching = !player.abilities.flying && !player.swimming && canEnterPose(player, 'crouching') &&
|
|
35
|
+
(previousShift || !canEnterPose(player, 'standing'))
|
|
36
|
+
const crawling = player.pose === 'swimming' && !player.isInWater()
|
|
37
|
+
return crouching || crawling
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function suffocatesAt (player, bx, bz) {
|
|
41
|
+
const bb = player.bb()
|
|
42
|
+
const box = new AABB(bx, bb.minY, bz, bx + 1, bb.maxY, bz + 1).deflate(1.0e-7)
|
|
43
|
+
const x0 = Math.floor(box.minX); const x1 = Math.floor(box.maxX)
|
|
44
|
+
const y0 = Math.floor(box.minY); const y1 = Math.floor(box.maxY)
|
|
45
|
+
const z0 = Math.floor(box.minZ); const z1 = Math.floor(box.maxZ)
|
|
46
|
+
for (let x = x0; x <= x1; x++) {
|
|
47
|
+
for (let y = y0; y <= y1; y++) {
|
|
48
|
+
for (let z = z0; z <= z1; z++) {
|
|
49
|
+
const block = player.world.blockAt(x, y, z)
|
|
50
|
+
if (!block.suffocating) continue
|
|
51
|
+
for (const s of block.shapes) {
|
|
52
|
+
if (box.intersectsBox([s[0] + x, s[1] + y, s[2] + z, s[3] + x, s[4] + y, s[5] + z])) return true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SIDES = [[-1, 0], [1, 0], [0, -1], [0, 1]] // west east north south
|
|
61
|
+
|
|
62
|
+
function moveTowardsClosestSpace (player, x, z) {
|
|
63
|
+
const bx = Math.floor(x)
|
|
64
|
+
const bz = Math.floor(z)
|
|
65
|
+
if (!suffocatesAt(player, bx, bz)) return
|
|
66
|
+
const rx = x - bx
|
|
67
|
+
const rz = z - bz
|
|
68
|
+
let best = null
|
|
69
|
+
let bestDistance = Infinity
|
|
70
|
+
for (const [sx, sz] of SIDES) {
|
|
71
|
+
const along = sx !== 0 ? rx : rz
|
|
72
|
+
const distance = (sx > 0 || sz > 0) ? 1 - along : along
|
|
73
|
+
if (distance < bestDistance && !suffocatesAt(player, bx + sx, bz + sz)) {
|
|
74
|
+
bestDistance = distance
|
|
75
|
+
best = [sx, sz]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!best) return
|
|
79
|
+
if (best[0] !== 0) player.vel.x = 0.1 * best[0]
|
|
80
|
+
else player.vel.z = 0.1 * best[1]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// LocalPlayer pushes itself out of blocks it is stuck in, probing the four corners of its box
|
|
84
|
+
function pushOutOfBlocks (player) {
|
|
85
|
+
const bb = player.bb()
|
|
86
|
+
const w = (bb.maxX - bb.minX) * 0.35
|
|
87
|
+
const d = (bb.maxZ - bb.minZ) * 0.35
|
|
88
|
+
const { x, z } = player.pos
|
|
89
|
+
moveTowardsClosestSpace(player, x - w, z + d)
|
|
90
|
+
moveTowardsClosestSpace(player, x - w, z - d)
|
|
91
|
+
moveTowardsClosestSpace(player, x + w, z - d)
|
|
92
|
+
moveTowardsClosestSpace(player, x + w, z + d)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { POSES, boxFor, canEnterPose, updatePose, isMovingSlowly, pushOutOfBlocks }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const AABB = require('../aabb')
|
|
4
|
+
const { onClimbable } = require('./blocks')
|
|
5
|
+
|
|
6
|
+
// Entity#push(Entity): overlapping pushable entities nudge the player away
|
|
7
|
+
function pushByEntities (player) {
|
|
8
|
+
if (player.riding || player.env.gameMode() === 'spectator' || onClimbable(player)) return
|
|
9
|
+
const bb = player.bb()
|
|
10
|
+
for (const e of player.env.nearbyEntities()) {
|
|
11
|
+
const half = e.width / 2
|
|
12
|
+
const box = new AABB(e.x - half, e.y, e.z - half, e.x + half, e.y + e.height, e.z + half)
|
|
13
|
+
if (!(box.minX < bb.maxX && box.maxX > bb.minX && box.minY < bb.maxY && box.maxY > bb.minY && box.minZ < bb.maxZ && box.maxZ > bb.minZ)) continue
|
|
14
|
+
let dx = player.pos.x - e.x
|
|
15
|
+
let dz = player.pos.z - e.z
|
|
16
|
+
let distance = Math.max(Math.abs(dx), Math.abs(dz))
|
|
17
|
+
if (distance < 0.01) continue
|
|
18
|
+
distance = Math.sqrt(distance)
|
|
19
|
+
dx /= distance
|
|
20
|
+
dz /= distance
|
|
21
|
+
const falloff = Math.min(1, 1 / distance)
|
|
22
|
+
player.vel.x += dx * falloff * 0.05
|
|
23
|
+
player.vel.z += dz * falloff * 0.05
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { pushByEntities }
|