spectra-physics 1.0.0 → 1.2.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,220 @@
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
+ const player = this
38
+ this.collisionContext = {
39
+ get bottom () { return player.pos.y },
40
+ get fallDistance () { return player.fallDistance },
41
+ get walksOnPowderSnow () { return player.V.powderSnow && player.env.bootsItem() === 'leather_boots' },
42
+ get descending () { return player.input.shift }
43
+ }
44
+ this.reset()
45
+ }
46
+
47
+ reset () {
48
+ this.pos = { x: 0, y: 0, z: 0 }
49
+ this.vel = { x: 0, y: 0, z: 0 }
50
+ this.yRot = 0
51
+ this.xRot = 0
52
+ this.pose = 'standing'
53
+ this.onGround = false
54
+ this.horizontalCollision = false
55
+ this.minorHorizontalCollision = false
56
+ this.verticalCollision = false
57
+ this.fallDistance = 0
58
+ this.sprinting = false
59
+ this.swimming = false
60
+ this.riding = false
61
+ this.noJumpDelay = 0
62
+ this.jumpTriggerTime = 0
63
+ this.sprintTriggerTime = 0
64
+ this.legacyFlyingSpeed = F_002
65
+ this.stuck = null
66
+ this.wasTouchingWater = false
67
+ this.fluidHeight = { water: 0, lava: 0 }
68
+ this.eyeInWater = false
69
+ this.wasEyeInWater = false
70
+ this.firstTick = true
71
+ this.supportingBlock = null
72
+ this.onGroundNoBlocks = false
73
+ this.movesThisTick = []
74
+ this.tickStart = { x: 0, y: 0, z: 0 }
75
+ this.abilities = { invulnerable: false, flying: false, mayfly: false, instabuild: false, flyingSpeed: f32(0.05), walkingSpeed: f32(0.1) }
76
+ this.input = { left: 0, forward: 0, forwardKey: 0, jumping: false, shift: false, keys: {} }
77
+ this.xxa = 0
78
+ this.zza = 0
79
+ this.events = { abilitiesChanged: false }
80
+ }
81
+
82
+ height () { return pose.POSES[this.pose].height }
83
+ eyeHeight () { return pose.POSES[this.pose].eye }
84
+ bbAt (x, y, z) { return pose.boxFor(this.pose, x, y, z) }
85
+ bb () { return this.bbAt(this.pos.x, this.pos.y, this.pos.z) }
86
+
87
+ maxUpStep () {
88
+ return this.V.attributes2005 ? f32(this.env.attribute('step_height', 0.6)) : STEP
89
+ }
90
+
91
+ // the server applies the sprint modifier as soon as it sees START_SPRINTING, so the speed follows it in the same tick
92
+ movementSpeed () { return f32(this.env.movementSpeed(this.sprinting)) }
93
+
94
+ isInWater () { return this.wasTouchingWater }
95
+ isInLava () { return !this.firstTick && this.fluidHeight.lava > 0 }
96
+ isUnderWater () { return this.wasEyeInWater && this.isInWater() }
97
+
98
+ hasEnoughImpulseToStartSprint () {
99
+ if (this.isUnderWater()) return this.input.forward > F_1EM5
100
+ return this.input.forwardKey > 0 && (this.V.inputModel === 'modern' || this.input.forward >= 0.8)
101
+ }
102
+
103
+ tick (keys) {
104
+ this.world.beginTick(this.collisionContext)
105
+ try {
106
+ this.tickInner(keys)
107
+ } finally {
108
+ this.world.endTick()
109
+ }
110
+ }
111
+
112
+ tickInner (keys) {
113
+ this.tickStart.x = this.pos.x
114
+ this.tickStart.y = this.pos.y
115
+ this.tickStart.z = this.pos.z
116
+ this.movesThisTick.length = 0
117
+ this.baseTick()
118
+
119
+ const previousJump = this.input.jumping
120
+ const previousShift = this.input.shift
121
+ const previousImpulse = this.hasEnoughImpulseToStartSprint()
122
+ const slow = pose.isMovingSlowly(this, previousShift)
123
+ readKeys(this, keys, slow)
124
+ pose.pushOutOfBlocks(this)
125
+ this.updateSprinting(keys, previousShift, previousImpulse, slow)
126
+ this.updateFlightToggle(previousJump)
127
+ if (this.isInWater() && this.input.shift && !this.abilities.flying) this.vel.y -= F_004
128
+ if (this.abilities.flying) {
129
+ const direction = (this.input.jumping ? 1 : 0) - (this.input.shift ? 1 : 0)
130
+ if (direction !== 0) this.vel.y += f32(f32(direction * this.abilities.flyingSpeed) * 3)
131
+ }
132
+ if (this.jumpTriggerTime > 0) this.jumpTriggerTime--
133
+
134
+ this.livingAiStep()
135
+
136
+ this.legacyFlyingSpeed = this.sprinting ? (this.V.airSprintFloatSum ? AIR_SPRINT : LEGACY_AIR_SPRINT) : F_002
137
+ if (this.onGround && this.abilities.flying && this.env.gameMode() !== 'spectator') {
138
+ this.abilities.flying = false
139
+ this.events.abilitiesChanged = true
140
+ }
141
+ pose.updatePose(this)
142
+ }
143
+
144
+ // Entity#baseTick
145
+ baseTick () {
146
+ updateFluids(this)
147
+ if (this.isInLava()) this.fallDistance = this.V.fallDistanceDouble ? this.fallDistance * 0.5 : f32(this.fallDistance * 0.5)
148
+ this.firstTick = false
149
+ }
150
+
151
+ livingAiStep () {
152
+ if (this.noJumpDelay > 0) this.noJumpDelay--
153
+ this.cutTinyVelocity()
154
+ this.xxa = this.input.left
155
+ this.zza = this.input.forward
156
+ handleJump(this)
157
+ if (this.env.gameMode() === 'spectator') {
158
+ this.pos.x += this.vel.x; this.pos.y += this.vel.y; this.pos.z += this.vel.z
159
+ return
160
+ }
161
+ travel(this)
162
+ applyEffectsAfterTravel(this)
163
+ pushByEntities(this)
164
+ }
165
+
166
+ cutTinyVelocity () {
167
+ const v = this.vel
168
+ if (this.V.horizontalThreshold && !this.riding) {
169
+ if (v.x * v.x + v.z * v.z < 9.0e-6) { v.x = 0; v.z = 0 }
170
+ } else {
171
+ if (Math.abs(v.x) < 0.003) v.x = 0
172
+ if (Math.abs(v.z) < 0.003) v.z = 0
173
+ }
174
+ if (Math.abs(v.y) < 0.003) v.y = 0
175
+ }
176
+
177
+ // sprint rules of LocalPlayer#aiStep
178
+ updateSprinting (keys, previousShift, previousImpulse, slow) {
179
+ const V = this.V
180
+ const food = this.env.food() > 6 || this.abilities.mayfly
181
+ const blocked = this.env.usingItem() || this.env.effect('blindness') != null ||
182
+ (V.playerInputPacket && slow && !this.isUnderWater())
183
+ if (previousShift) this.sprintTriggerTime = 0
184
+ if (this.sprintTriggerTime > 0) this.sprintTriggerTime--
185
+
186
+ const impulse = this.hasEnoughImpulseToStartSprint()
187
+ if (!this.sprinting && (this.onGround || this.isUnderWater()) && !previousShift && !previousImpulse && impulse && food && !blocked) {
188
+ if (this.sprintTriggerTime <= 0 && !keys.sprint) this.sprintTriggerTime = 7
189
+ else this.sprinting = true
190
+ }
191
+ if (!this.sprinting && (!this.isInWater() || this.isUnderWater()) && impulse && food && !blocked && keys.sprint) {
192
+ this.sprinting = true
193
+ }
194
+ if (!this.sprinting) return
195
+
196
+ const noForward = !(this.input.forward > F_1EM5) || !food
197
+ if (this.swimming) {
198
+ if ((!this.onGround && !this.input.shift && noForward) || !this.isInWater()) this.sprinting = false
199
+ return
200
+ }
201
+ const wall = V.minorCollision ? this.horizontalCollision && !this.minorHorizontalCollision : this.horizontalCollision
202
+ const sneaking = V.playerInputPacket && slow && !this.isUnderWater()
203
+ if (noForward || wall || (this.isInWater() && !this.isUnderWater()) || sneaking) this.sprinting = false
204
+ }
205
+
206
+ // double tap jump toggles creative flight
207
+ updateFlightToggle (previousJump) {
208
+ if (!this.abilities.mayfly || this.env.gameMode() === 'spectator') return
209
+ if (previousJump || !this.input.jumping) return
210
+ if (this.jumpTriggerTime === 0) {
211
+ this.jumpTriggerTime = 7
212
+ return
213
+ }
214
+ this.abilities.flying = !this.abilities.flying
215
+ this.events.abilitiesChanged = true
216
+ this.jumpTriggerTime = 0
217
+ }
218
+ }
219
+
220
+ module.exports = { PlayerPhysics }
@@ -0,0 +1,89 @@
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
+ const slowedByItem = (player) => player.env.usingItem() && !player.riding
13
+
14
+ function sneakMultiplier (player) {
15
+ return player.V.efficiencyAttributes ? f32(player.env.attribute('sneaking_speed', 0.3)) : F_03
16
+ }
17
+
18
+ // KeyboardInput + LivingEntity#aiStep scaling, 1.16 - 1.21.4
19
+ function legacyInput (player, left, forward, slow) {
20
+ let x = left
21
+ let z = forward
22
+ if (slow) {
23
+ const k = sneakMultiplier(player)
24
+ x = f32(x * k); z = f32(z * k)
25
+ }
26
+ if (slowedByItem(player)) { x = f32(x * F_02); z = f32(z * F_02) }
27
+ return [f32(x * F_098), f32(z * F_098)]
28
+ }
29
+
30
+ // LocalPlayer#modifyInput, 1.21.5+
31
+ function modernInput (player, left, forward, slow) {
32
+ const length = f32(Math.sqrt(f32(left * left + forward * forward)))
33
+ if (length < F_MIN_LENGTH) return [0, 0]
34
+ let x = f32(f32(left / length) * F_098)
35
+ let z = f32(f32(forward / length) * F_098)
36
+ if (slowedByItem(player)) { x = f32(x * F_02); z = f32(z * F_02) }
37
+ if (slow) {
38
+ const k = sneakMultiplier(player)
39
+ x = f32(x * k); z = f32(z * k)
40
+ }
41
+ const scaled = f32(Math.sqrt(f32(x * x + z * z)))
42
+ if (scaled <= 0) return [x, z]
43
+ const nx = f32(x / scaled)
44
+ const nz = f32(z / scaled)
45
+ const ax = Math.abs(nx)
46
+ const az = Math.abs(nz)
47
+ const ratio = az > ax ? f32(ax / az) : f32(az / ax)
48
+ const toSquare = f32(Math.sqrt(f32(1 + f32(ratio * ratio))))
49
+ const k = Math.min(f32(scaled * toSquare), 1)
50
+ return [f32(nx * k), f32(nz * k)]
51
+ }
52
+
53
+ function readKeys (player, keys, slow) {
54
+ const forward = impulse(keys.forward, keys.back)
55
+ const left = impulse(keys.left, keys.right)
56
+ const [xxa, zza] = player.V.inputModel === 'modern'
57
+ ? modernInput(player, left, forward, slow)
58
+ : legacyInput(player, left, forward, slow)
59
+ if (slowedByItem(player)) player.sprintTriggerTime = 0
60
+ player.input = {
61
+ left: xxa,
62
+ forward: zza,
63
+ forwardKey: forward,
64
+ jumping: !!keys.jump,
65
+ shift: !!keys.sneak,
66
+ keys: { ...keys }
67
+ }
68
+ }
69
+
70
+ // Entity#moveRelative
71
+ function moveRelative (player, speed, ix, iy, iz) {
72
+ const lengthSq = ix * ix + iy * iy + iz * iz
73
+ if (lengthSq < 1.0e-7) return
74
+ let x = ix; let y = iy; let z = iz
75
+ if (lengthSq > 1) {
76
+ const length = player.V.floatSqrt ? f32(Math.sqrt(lengthSq)) : Math.sqrt(lengthSq)
77
+ if (length < player.V.normalizeEpsilon) return
78
+ x /= length; y /= length; z /= length
79
+ }
80
+ x *= speed; y *= speed; z *= speed
81
+ const r = degToRadF(player.yRot)
82
+ const s = sin(r)
83
+ const c = cos(r)
84
+ player.vel.x += x * c - z * s
85
+ player.vel.y += y
86
+ player.vel.z += z * c + x * s
87
+ }
88
+
89
+ module.exports = { readKeys, moveRelative }
@@ -0,0 +1,141 @@
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, updateFluidInteraction } = 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 x = player.vel.x
82
+ if (vx !== r.x) player.vel.x = 0
83
+ if (vz !== r.z) {
84
+ if (V.keepsXOnCornerCollision) player.vel.x = x
85
+ player.vel.z = 0
86
+ }
87
+ }
88
+
89
+ // Entity#move(MoverType.SELF, movement)
90
+ function move (player, mx, my, mz) {
91
+ const V = player.V
92
+ if (player.stuck) {
93
+ mx *= player.stuck.x; my *= player.stuck.y; mz *= player.stuck.z
94
+ player.stuck = null
95
+ player.vel = { x: 0, y: 0, z: 0 }
96
+ }
97
+ const [bx, bz] = backOffFromEdge(player, mx, my, mz)
98
+ mx = bx; mz = bz
99
+
100
+ const bb = player.bb()
101
+ const from = V.blockEffectsPerAxis ? { ...player.pos } : null
102
+ const collide = V.modernStepUp ? collideModern : collideLegacy
103
+ const r = collide(player.world, mx, my, mz, bb, player.onGround, player.maxUpStep())
104
+ const lengthSq = r.x * r.x + r.y * r.y + r.z * r.z
105
+ const wantedSq = mx * mx + my * my + mz * mz
106
+ const applies = V.modernMoveThreshold ? (lengthSq > 1.0e-7 || wantedSq - lengthSq < 1.0e-7) : lengthSq > 1.0e-7
107
+ if (applies) applyPosition(player, bb, r)
108
+
109
+ const xBlocked = V.epsilonCollisionFlags ? !equal(mx, r.x) : mx !== r.x
110
+ const zBlocked = V.epsilonCollisionFlags ? !equal(mz, r.z) : mz !== r.z
111
+ player.horizontalCollision = xBlocked || zBlocked
112
+ player.minorHorizontalCollision = V.minorCollision && player.horizontalCollision && isMinorCollision(player, r.x, r.z)
113
+ player.verticalCollision = my !== r.y
114
+ player.onGround = player.verticalCollision && my < 0
115
+ updateSupportingBlock(player, player.onGround, r.x, r.z)
116
+
117
+ if (V.fluidRecheckInMove && !player.wasTouchingWater && !player.riding) {
118
+ if (V.fluidInteraction) updateFluidInteraction(player)
119
+ else updateWater(player)
120
+ }
121
+ if (player.onGround) {
122
+ player.fallDistance = 0
123
+ } else if (r.y < 0) {
124
+ player.fallDistance = V.fallDistanceDouble ? player.fallDistance - r.y : f32(player.fallDistance - r.y)
125
+ }
126
+
127
+ zeroBlockedAxes(player, mx, mz, r)
128
+ const standingOn = onPos(player)
129
+ const block = player.world.blockAt(standingOn.x, standingOn.y, standingOn.z)
130
+ if (my !== r.y) landOn(player, block)
131
+ if (player.onGround && !player.input.shift) stepOn(player, block)
132
+
133
+ if (V.blockEffectsPerAxis && applies) player.movesThisTick.push({ from, to: { ...player.pos }, input: { x: mx, y: my, z: mz } })
134
+ if (!V.blockEffectsAfterTravel) checkInsideBlocksLegacy(player)
135
+
136
+ const factor = speedFactor(player)
137
+ player.vel.x *= factor
138
+ player.vel.z *= factor
139
+ }
140
+
141
+ 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.intersects(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 }