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.
@@ -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 }