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,90 @@
1
+ 'use strict'
2
+ // Clientbound packets that change the local player, answered the way the game client does.
3
+
4
+ const { f32 } = require('../engine/mth')
5
+
6
+ function installServerHandlers (ctx) {
7
+ const { bot, client, V, player, state, registry } = ctx
8
+
9
+ client.on('abilities', (packet) => {
10
+ const flags = packet.flags
11
+ player.abilities.invulnerable = !!(flags & 1)
12
+ player.abilities.flying = !!(flags & 2)
13
+ player.abilities.mayfly = !!(flags & 4)
14
+ player.abilities.instabuild = !!(flags & 8)
15
+ player.abilities.flyingSpeed = f32(packet.flyingSpeed)
16
+ player.abilities.walkingSpeed = f32(packet.walkingSpeed)
17
+ })
18
+
19
+ client.on('entity_velocity', (packet) => {
20
+ if (!bot.entity || packet.entityId !== bot.entity.id) return
21
+ const scale = V.lpVelocity ? 1 : 1 / 8000
22
+ player.vel = { x: packet.velocity.x * scale, y: packet.velocity.y * scale, z: packet.velocity.z * scale }
23
+ })
24
+
25
+ client.on('explosion', (packet) => {
26
+ const push = packet.playerKnockback ??
27
+ (('playerMotionX' in packet) ? { x: packet.playerMotionX, y: packet.playerMotionY, z: packet.playerMotionZ } : null)
28
+ if (!push) return
29
+ player.vel.x += push.x
30
+ player.vel.y += push.y
31
+ player.vel.z += push.z
32
+ })
33
+
34
+ // an invalid slot is ignored, a valid one is confirmed on the next tick by MultiPlayerGameMode#tick
35
+ client.removeAllListeners('held_item_slot')
36
+ const select = (slot) => {
37
+ state.selectedSlot = slot
38
+ bot.quickBarSlot = slot
39
+ try { bot.updateHeldItem?.() } catch (e) {}
40
+ }
41
+ client.on('held_item_slot', (packet) => {
42
+ const slot = packet.slot ?? packet.slotId
43
+ if (slot >= 0 && slot <= 8) select(slot)
44
+ })
45
+ bot.setQuickBarSlot = (slot) => {
46
+ if (!(slot >= 0 && slot < 9)) throw new Error('invalid hotbar slot ' + slot)
47
+ select(slot)
48
+ }
49
+
50
+ // LocalPlayer#swing is triggered by the animate packet for our own entity and sends a swing back
51
+ client.on('animation', (packet) => {
52
+ if (!bot.entity || packet.entityId !== bot.entity.id) return
53
+ if (packet.animation === 0) ctx.send('arm_animation', { hand: 0 })
54
+ else if (packet.animation === 3) ctx.send('arm_animation', { hand: 1 })
55
+ })
56
+
57
+ client.on('vehicle_move', (packet) => {
58
+ const boat = state.boat
59
+ if (!boat) return
60
+ boat.pos = { x: packet.x, y: packet.y, z: packet.z }
61
+ boat.yRot = f32(packet.yaw)
62
+ boat.xRot = f32(packet.pitch)
63
+ ctx.sendVehicleMove()
64
+ })
65
+
66
+ // mineflayer treats a full chunk without sections as an unload before 1.17; the game client keeps it loaded
67
+ const Chunk = require('prismarine-chunk')(registry)
68
+ client.on('map_chunk', (packet) => {
69
+ const empty = packet.bitMap === 0 ||
70
+ (Array.isArray(packet.bitMap) && packet.bitMap.every(n => !n || (Array.isArray(n) && n.every(m => !m))))
71
+ if (!empty || !packet.groundUp || bot.world.getColumn(packet.x, packet.z)) return
72
+ const column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
73
+ try {
74
+ if (packet.biomes !== undefined) column.loadBiomes(packet.biomes)
75
+ } catch (e) {}
76
+ bot.world.setColumn(packet.x, packet.z, column)
77
+ })
78
+
79
+ // mineflayer listens for 'entityGone' on the client instead of the bot, so a removed vehicle is never left
80
+ client.on('entity_destroy', (packet) => {
81
+ const vehicle = bot.vehicle
82
+ if (!vehicle || !packet.entityIds.includes(vehicle.id)) return
83
+ bot.vehicle = null
84
+ if (bot.entity) bot.entity.vehicle = null
85
+ state.boat = null
86
+ bot.emit('dismount', vehicle)
87
+ })
88
+ }
89
+
90
+ module.exports = { installServerHandlers }
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+ // ClientboundPlayerPositionPacket handling and the answer each client version sends back.
3
+
4
+ const conv = require('mineflayer/lib/conversions')
5
+ const { f32, clamp } = require('../engine/mth')
6
+
7
+ function relativeFlags (flags) {
8
+ if (flags && typeof flags === 'object') {
9
+ return {
10
+ x: !!flags.x, y: !!flags.y, z: !!flags.z, yaw: !!flags.yaw, pitch: !!flags.pitch,
11
+ dx: !!flags.dx, dy: !!flags.dy, dz: !!flags.dz, rotateDelta: !!flags.yawDelta
12
+ }
13
+ }
14
+ const n = flags | 0
15
+ return { x: !!(n & 1), y: !!(n & 2), z: !!(n & 4), yaw: !!(n & 8), pitch: !!(n & 16) }
16
+ }
17
+
18
+ // 1.16 - 1.21.1: relative axes keep their velocity, absolute ones reset it
19
+ function legacyTarget (player, packet, r) {
20
+ const v = { ...player.vel }
21
+ const pos = {
22
+ x: r.x ? player.pos.x + packet.x : packet.x,
23
+ y: r.y ? player.pos.y + packet.y : packet.y,
24
+ z: r.z ? player.pos.z + packet.z : packet.z
25
+ }
26
+ if (!r.x) v.x = 0
27
+ if (!r.y) v.y = 0
28
+ if (!r.z) v.z = 0
29
+ let yaw = f32(packet.yaw)
30
+ let pitch = f32(packet.pitch)
31
+ if (r.pitch) pitch = f32(pitch + player.xRot)
32
+ if (r.yaw) yaw = f32(yaw + player.yRot)
33
+ return { pos, vel: v, yaw: f32(yaw % 360), pitch: f32(clamp(pitch, -90, 90) % 360) }
34
+ }
35
+
36
+ // 1.21.2+: PositionMoveRotation#calculateAbsolute
37
+ function modernTarget (player, packet, r) {
38
+ const pos = {
39
+ x: (r.x ? player.pos.x : 0) + packet.x,
40
+ y: (r.y ? player.pos.y : 0) + packet.y,
41
+ z: (r.z ? player.pos.z : 0) + packet.z
42
+ }
43
+ const yaw = f32((r.yaw ? player.yRot : 0) + packet.yaw)
44
+ const pitch = f32(clamp(f32((r.pitch ? player.xRot : 0) + packet.pitch), -90, 90))
45
+ let v = { ...player.vel }
46
+ if (r.rotateDelta) {
47
+ const ax = (player.xRot - pitch) * Math.PI / 180
48
+ const ay = (player.yRot - yaw) * Math.PI / 180
49
+ const y1 = v.y * Math.cos(ax) + v.z * Math.sin(ax)
50
+ const z1 = v.z * Math.cos(ax) - v.y * Math.sin(ax)
51
+ v = { x: v.x * Math.cos(ay) + z1 * Math.sin(ay), y: y1, z: z1 * Math.cos(ay) - v.x * Math.sin(ay) }
52
+ }
53
+ const delta = (current, change, relative) => relative ? current + (change ?? 0) : (change ?? 0)
54
+ return {
55
+ pos,
56
+ vel: { x: delta(v.x, packet.dx, r.dx), y: delta(v.y, packet.dy, r.dy), z: delta(v.z, packet.dz, r.dz) },
57
+ yaw,
58
+ pitch
59
+ }
60
+ }
61
+
62
+ function installTeleport (ctx) {
63
+ const { bot, client, V, player, state } = ctx
64
+
65
+ const answerPosition = () => ctx.send('position_look', {
66
+ x: player.pos.x,
67
+ y: player.pos.y,
68
+ z: player.pos.z,
69
+ yaw: player.yRot,
70
+ pitch: player.xRot,
71
+ onGround: false,
72
+ flags: { onGround: false, hasHorizontalCollision: false }
73
+ })
74
+
75
+ client.on('position', (packet) => {
76
+ const r = relativeFlags(packet.flags)
77
+ const target = V.relativeTeleportVelocity ? modernTarget(player, packet, r) : legacyTarget(player, packet, r)
78
+ if (!bot.vehicle) {
79
+ player.pos = target.pos
80
+ player.vel = target.vel
81
+ player.onGround = false
82
+ }
83
+ player.yRot = target.yaw
84
+ player.xRot = target.pitch
85
+ bot.entity.yaw = conv.fromNotchianYaw(target.yaw)
86
+ bot.entity.pitch = conv.fromNotchianPitch(target.pitch)
87
+ ctx.syncEntity()
88
+
89
+ if (V.posRotBeforeConfirm) {
90
+ answerPosition()
91
+ ctx.send('teleport_confirm', { teleportId: packet.teleportId })
92
+ } else {
93
+ if (bot.supportFeature('teleportUsesOwnPacket')) ctx.send('teleport_confirm', { teleportId: packet.teleportId })
94
+ answerPosition()
95
+ }
96
+ state.positionReceived = true
97
+ bot.emit('forcedMove')
98
+ })
99
+
100
+ client.on('player_rotation', (packet) => {
101
+ player.yRot = f32(packet.yaw)
102
+ player.xRot = f32(clamp(packet.pitch, -90, 90))
103
+ bot.entity.yaw = conv.fromNotchianYaw(player.yRot)
104
+ bot.entity.pitch = conv.fromNotchianPitch(player.xRot)
105
+ })
106
+ }
107
+
108
+ module.exports = { installTeleport }
@@ -0,0 +1,144 @@
1
+ 'use strict'
2
+ // Minecraft#tick for the local player plus the timer that drives it.
3
+
4
+ const { performance } = require('perf_hooks')
5
+
6
+ const TICK_MS = 50
7
+ const MAX_CATCH_UP = 10
8
+ const LOAD_TIMEOUT_TICKS = 60
9
+ const MAX_TURN_DELAY = 40
10
+
11
+ const released = () => ({ forward: false, backward: false, left: false, right: false, jump: false, shift: false, sprint: false })
12
+
13
+ function installTicker (ctx) {
14
+ const { bot, client, V, player, state, world } = ctx
15
+
16
+ ctx.syncEntity = () => {
17
+ const e = bot.entity
18
+ if (!e) return
19
+ e.position.set(player.pos.x, player.pos.y, player.pos.z)
20
+ e.velocity.set(player.vel.x, player.vel.y, player.vel.z)
21
+ e.onGround = player.onGround
22
+ e.isInWater = player.isInWater()
23
+ e.isInLava = player.isInLava()
24
+ e.height = player.height()
25
+ e.eyeHeight = player.eyeHeight()
26
+ e.isCollidedHorizontally = player.horizontalCollision
27
+ e.isCollidedVertically = player.verticalCollision
28
+ }
29
+
30
+ function resetPlayer () {
31
+ const abilities = player.abilities
32
+ player.reset()
33
+ player.abilities = abilities
34
+ Object.assign(state, {
35
+ positionReceived: false,
36
+ clientLoaded: false,
37
+ loadedReadyTicks: 0,
38
+ loadTimeout: LOAD_TIMEOUT_TICKS,
39
+ last: { x: 0, y: 0, z: 0, yRot: 0, xRot: 0, onGround: false, horizontalCollision: false },
40
+ positionReminder: 0,
41
+ creeping: false,
42
+ turnDelay: 0,
43
+ wasSprinting: false,
44
+ wasShift: false,
45
+ lastInput: released(),
46
+ boat: null
47
+ })
48
+ }
49
+
50
+ // 1.21.4+: the player only ticks once the level screen closes, which is when player_loaded is sent
51
+ function waitForLevel () {
52
+ const outside = player.pos.y < world.minY || player.pos.y >= world.maxY
53
+ state.loadedReadyTicks = (outside || world.hasChunk(player.pos.x, player.pos.z)) ? state.loadedReadyTicks + 1 : 0
54
+ if (state.loadedReadyTicks < 2 && --state.loadTimeout > 0) return false
55
+ state.clientLoaded = true
56
+ ctx.send('player_loaded', {})
57
+ bot.emit('clientLoaded')
58
+ return true
59
+ }
60
+
61
+ function playerTick () {
62
+ if (!state.positionReceived) return
63
+ if (!world.hasChunk(player.pos.x, player.pos.z)) return
64
+ if (V.clientLoaded && !state.clientLoaded) {
65
+ waitForLevel()
66
+ return
67
+ }
68
+ if (bot.isAlive === false) return
69
+ player.riding = false
70
+ // 1.17 - 1.18.1: a turn while creeping through the air can't be reported cleanly, the mouse waits a moment
71
+ if (state.creeping && state.turnDelay < MAX_TURN_DELAY) state.turnDelay++
72
+ else {
73
+ ctx.turn(state.turnDelay >= MAX_TURN_DELAY ? Infinity : undefined)
74
+ state.turnDelay = 0
75
+ }
76
+ if (bot.physicsEnabled) {
77
+ player.tick(ctx.keys())
78
+ if (player.events.abilitiesChanged) {
79
+ player.events.abilitiesChanged = false
80
+ ctx.send('abilities', { flags: player.abilities.flying ? 2 : 0 })
81
+ }
82
+ }
83
+ ctx.syncEntity()
84
+ if (V.playerInputPacket) {
85
+ ctx.sendShift()
86
+ ctx.sendInput()
87
+ }
88
+ ctx.sendPosition()
89
+ }
90
+
91
+ function clientTick () {
92
+ if (client.state !== 'play' || !state.inLevel || !bot.entity) return
93
+ if (state.selectedSlot !== state.carriedSlot) {
94
+ state.carriedSlot = state.selectedSlot
95
+ ctx.send('held_item_slot', { slotId: state.selectedSlot })
96
+ }
97
+ ctx.flushHeldBrand()
98
+ const vehicle = bot.vehicle
99
+ if (vehicle && vehicle !== bot.entity && state.positionReceived) {
100
+ ctx.ridingTick(vehicle)
101
+ } else {
102
+ state.boat = null
103
+ playerTick()
104
+ }
105
+ if (V.tickEnd) ctx.send('tick_end', {})
106
+ bot.emit('physicsTick')
107
+ }
108
+
109
+ let timer = null
110
+ let nextAt = 0
111
+ function start () {
112
+ if (timer) return
113
+ nextAt = performance.now() + TICK_MS
114
+ const loop = () => {
115
+ const now = performance.now()
116
+ for (let n = 0; now >= nextAt && n < MAX_CATCH_UP; n++) {
117
+ try { clientTick() } catch (err) { bot.emit('error', err) }
118
+ nextAt += TICK_MS
119
+ }
120
+ if (now - nextAt > 1000) nextAt = now + TICK_MS
121
+ timer = setTimeout(loop, Math.max(0, nextAt - performance.now()))
122
+ }
123
+ timer = setTimeout(loop, TICK_MS)
124
+ }
125
+
126
+ client.on('login', () => {
127
+ state.inLevel = true
128
+ state.playSettingsSent = false
129
+ state.selectedSlot = 0
130
+ state.carriedSlot = 0
131
+ resetPlayer()
132
+ start()
133
+ })
134
+ client.on('respawn', resetPlayer)
135
+ client.on('start_configuration', () => { state.inLevel = false })
136
+ bot.on('end', () => {
137
+ clearTimeout(timer)
138
+ timer = null
139
+ })
140
+
141
+ resetPlayer()
142
+ }
143
+
144
+ module.exports = { installTicker }
@@ -0,0 +1,134 @@
1
+ 'use strict'
2
+
3
+ const AABB = require('../aabb')
4
+ const { f32 } = require('../mth')
5
+
6
+ const ON_POS = f32(0.2)
7
+ const BELOW_MODERN = f32(0.500001)
8
+ const BELOW_LEGACY = 0.5000001
9
+
10
+ // Entity#getOnPos(yOffset)
11
+ function onPos (player, yOffset = ON_POS) {
12
+ const { pos, world } = player
13
+ const support = player.supportingBlock
14
+ if (player.V.supportingBlock && support) {
15
+ if (!(yOffset > 1.0e-5)) return support
16
+ const block = world.blockAt(support.x, support.y, support.z)
17
+ if (!(yOffset <= 0.5) || !block.isFenceLike) return { x: support.x, y: Math.floor(pos.y - yOffset), z: support.z }
18
+ return support
19
+ }
20
+ const p = { x: Math.floor(pos.x), y: Math.floor(pos.y - yOffset), z: Math.floor(pos.z) }
21
+ if (!player.V.supportingBlock && yOffset === ON_POS && world.blockAt(p.x, p.y, p.z).isAir) {
22
+ if (world.blockAt(p.x, p.y - 1, p.z).isFenceLike) return { x: p.x, y: p.y - 1, z: p.z }
23
+ }
24
+ return p
25
+ }
26
+
27
+ function belowPos (player) {
28
+ if (player.V.supportingBlock) return onPos(player, BELOW_MODERN)
29
+ return { x: Math.floor(player.pos.x), y: Math.floor(player.pos.y - BELOW_LEGACY), z: Math.floor(player.pos.z) }
30
+ }
31
+
32
+ function feetBlock (player) {
33
+ return player.world.blockAt(Math.floor(player.pos.x), Math.floor(player.pos.y), Math.floor(player.pos.z))
34
+ }
35
+
36
+ function friction (player) {
37
+ const p = belowPos(player)
38
+ return player.world.blockAt(p.x, p.y, p.z).friction
39
+ }
40
+
41
+ function soulSpeed (player, block) {
42
+ return block.name === 'soul_sand' && player.V.soulSpeedBoots && player.env.bootsEnchant('soul_speed') > 0
43
+ }
44
+
45
+ function blockSpeed (player, block) {
46
+ return soulSpeed(player, block) ? 1 : block.speedFactor
47
+ }
48
+
49
+ // Entity#getBlockSpeedFactor
50
+ function speedFactor (player) {
51
+ if (player.abilities.flying) return 1
52
+ const inside = feetBlock(player)
53
+ let factor = blockSpeed(player, inside)
54
+ const keepsOwn = inside.name === 'water' || inside.name === 'bubble_column' ||
55
+ (!player.V.supportingBlock && inside.name === 'soul_sand')
56
+ if (factor === 1 && !keepsOwn) {
57
+ const p = belowPos(player)
58
+ factor = blockSpeed(player, player.world.blockAt(p.x, p.y, p.z))
59
+ }
60
+ if (player.V.efficiencyAttributes) {
61
+ const efficiency = f32(player.env.attribute('movement_efficiency', 0))
62
+ factor = f32(factor + efficiency * (1 - factor))
63
+ }
64
+ return factor
65
+ }
66
+
67
+ function jumpFactor (player) {
68
+ const inside = feetBlock(player).jumpFactor
69
+ if (inside !== 1) return inside
70
+ const p = belowPos(player)
71
+ return player.world.blockAt(p.x, p.y, p.z).jumpFactor
72
+ }
73
+
74
+ function onClimbable (player) {
75
+ const { world } = player
76
+ const bx = Math.floor(player.pos.x)
77
+ const by = Math.floor(player.pos.y)
78
+ const bz = Math.floor(player.pos.z)
79
+ const block = world.blockAt(bx, by, bz)
80
+ if (block.climbable) return true
81
+ if (block.isTrapdoor && (block.props.open === true || block.props.open === 'true')) {
82
+ const below = world.blockAt(bx, by - 1, bz)
83
+ return below.isLadder && below.props.facing === block.props.facing
84
+ }
85
+ return false
86
+ }
87
+
88
+ function onPowderSnowWithBoots (player) {
89
+ return player.V.powderSnow && feetBlock(player).isPowderSnow && player.env.bootsItem() === 'leather_boots'
90
+ }
91
+
92
+ function findSupport (player, box) {
93
+ let best = null
94
+ let bestDistance = Infinity
95
+ const x0 = Math.floor(box.minX) - 1; const x1 = Math.floor(box.maxX) + 1
96
+ const y0 = Math.floor(box.minY) - 1; const y1 = Math.floor(box.maxY) + 1
97
+ const z0 = Math.floor(box.minZ) - 1; const z1 = Math.floor(box.maxZ) + 1
98
+ for (let x = x0; x <= x1; x++) {
99
+ for (let y = y0; y <= y1; y++) {
100
+ for (let z = z0; z <= z1; z++) {
101
+ const shapes = player.world.blockAt(x, y, z).shapes
102
+ if (!shapes.some(s => box.intersectsBox([s[0] + x, s[1] + y, s[2] + z, s[3] + x, s[4] + y, s[5] + z]))) continue
103
+ const cx = x + 0.5 - player.pos.x
104
+ const cy = y + 0.5 - player.pos.y
105
+ const cz = z + 0.5 - player.pos.z
106
+ const distance = cx * cx + cy * cy + cz * cz
107
+ const greater = best && (y !== best.y ? y > best.y : (z !== best.z ? z > best.z : x > best.x))
108
+ if (distance < bestDistance || (distance === bestDistance && (!best || greater))) {
109
+ bestDistance = distance
110
+ best = { x, y, z }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return best
116
+ }
117
+
118
+ // Entity#checkSupportingBlock, 1.20+
119
+ function updateSupportingBlock (player, onGround, mx, mz) {
120
+ if (!player.V.supportingBlock) return
121
+ if (!onGround) {
122
+ player.supportingBlock = null
123
+ player.onGroundNoBlocks = false
124
+ return
125
+ }
126
+ const bb = player.bb()
127
+ const feet = new AABB(bb.minX, bb.minY - 1.0e-6, bb.minZ, bb.maxX, bb.minY, bb.maxZ)
128
+ let found = findSupport(player, feet)
129
+ if (!found && !player.onGroundNoBlocks) found = findSupport(player, feet.move(-mx, 0, -mz))
130
+ player.supportingBlock = found
131
+ player.onGroundNoBlocks = !found
132
+ }
133
+
134
+ module.exports = { onPos, belowPos, feetBlock, friction, speedFactor, jumpFactor, onClimbable, onPowderSnowWithBoots, updateSupportingBlock }
@@ -0,0 +1,152 @@
1
+ 'use strict'
2
+
3
+ const AABB = require('../aabb')
4
+ const { f32 } = require('../mth')
5
+
6
+ const F_098 = f32(0.98)
7
+ const F_08 = f32(0.8)
8
+ const F_09 = f32(0.9)
9
+ const F_005 = f32(0.05)
10
+ const F_1EM5 = f32(1.0e-5)
11
+ const LONG_MOVE = f32(0.99999) * f32(0.99999)
12
+ const HALF_WIDTH = f32(0.3)
13
+
14
+ const STUCK = {
15
+ cobweb: { x: 0.25, y: F_005, z: 0.25 },
16
+ cobwebWeaving: { x: 0.5, y: 0.25, z: 0.5 },
17
+ berryBush: { x: F_08, y: 0.75, z: F_08 },
18
+ powderSnow: { x: F_09, y: 1.5, z: F_09 }
19
+ }
20
+
21
+ function makeStuck (player, multiplier) {
22
+ player.fallDistance = 0
23
+ player.stuck = multiplier
24
+ }
25
+
26
+ // 1.21.2 moved block effects after gravity and drag; honey keeps its old numbers by converting back and forth
27
+ const oldDeltaY = (player, vy) => player.V.blockEffectsAfterTravel ? vy / F_098 + 0.08 : vy
28
+ const newDeltaY = (player, vy) => player.V.blockEffectsAfterTravel ? (vy - 0.08) * F_098 : vy
29
+
30
+ function honeySlide (player, bx, by, bz) {
31
+ if (player.onGround) return
32
+ if (player.pos.y > by + 0.9375 - 1.0e-7) return
33
+ const vy = oldDeltaY(player, player.vel.y)
34
+ if (vy >= -0.08) return
35
+ const dx = Math.abs(bx + 0.5 - player.pos.x)
36
+ const dz = Math.abs(bz + 0.5 - player.pos.z)
37
+ const edge = 0.4375 + HALF_WIDTH
38
+ if (!(dx + 1.0e-7 > edge || dz + 1.0e-7 > edge)) return
39
+ if (vy < -0.13) {
40
+ const k = -0.05 / vy
41
+ player.vel.x *= k
42
+ player.vel.z *= k
43
+ }
44
+ player.vel.y = newDeltaY(player, -0.05)
45
+ player.fallDistance = 0
46
+ }
47
+
48
+ function bubbleColumn (player, block, bx, by, bz) {
49
+ const surface = player.world.blockAt(bx, by + 1, bz).isAir
50
+ const vy = player.vel.y
51
+ if (block.bubbleDrag) player.vel.y = surface ? Math.max(-0.9, vy - 0.03) : Math.max(-0.3, vy - 0.03)
52
+ else player.vel.y = surface ? Math.min(1.8, vy + 0.1) : Math.min(0.7, vy + 0.06)
53
+ player.fallDistance = 0
54
+ }
55
+
56
+ function onInsideBlock (player, block, bx, by, bz, withVelocityEffects) {
57
+ if (block.isCobweb) {
58
+ makeStuck(player, player.env.effect('weaving') != null ? STUCK.cobwebWeaving : STUCK.cobweb)
59
+ } else if (block.isBerryBush) {
60
+ makeStuck(player, STUCK.berryBush)
61
+ } else if (block.isPowderSnow) {
62
+ if (bx === Math.floor(player.pos.x) && by === Math.floor(player.pos.y) && bz === Math.floor(player.pos.z)) {
63
+ makeStuck(player, STUCK.powderSnow)
64
+ }
65
+ } else if (block.isBubbleColumn) {
66
+ if (withVelocityEffects) bubbleColumn(player, block, bx, by, bz)
67
+ } else if (block.isHoney) {
68
+ honeySlide(player, bx, by, bz)
69
+ }
70
+ }
71
+
72
+ function forEachBlockIn (box, visit) {
73
+ const x0 = Math.floor(box.minX); const x1 = Math.floor(box.maxX)
74
+ const y0 = Math.floor(box.minY); const y1 = Math.floor(box.maxY)
75
+ const z0 = Math.floor(box.minZ); const z1 = Math.floor(box.maxZ)
76
+ for (let x = x0; x <= x1; x++) {
77
+ for (let y = y0; y <= y1; y++) {
78
+ for (let z = z0; z <= z1; z++) visit(x, y, z)
79
+ }
80
+ }
81
+ }
82
+
83
+ function settleStuck (player) {
84
+ if (player.abilities.flying) player.stuck = null
85
+ }
86
+
87
+ // Entity#checkInsideBlocks, before 1.21.2: runs inside Entity#move against the new box
88
+ function checkInsideBlocksLegacy (player) {
89
+ const box = player.bb().deflate(player.V.insideBlockDeflate)
90
+ forEachBlockIn(box, (x, y, z) => {
91
+ const block = player.world.blockAt(x, y, z)
92
+ if (!block.isAir) onInsideBlock(player, block, x, y, z, true)
93
+ })
94
+ settleStuck(player)
95
+ }
96
+
97
+ // voxel walk of the box corner between two points, used for moves longer than a block
98
+ function blocksAlongTravel (from, to, box, out) {
99
+ const dx = to.x - from.x; const dy = to.y - from.y; const dz = to.z - from.z
100
+ const start = { x: box.minX - dx, y: box.minY - dy, z: box.minZ - dz }
101
+ let cx = Math.floor(start.x); let cy = Math.floor(start.y); let cz = Math.floor(start.z)
102
+ const sx = Math.sign(dx); const sy = Math.sign(dy); const sz = Math.sign(dz)
103
+ const tx = sx === 0 ? Infinity : sx / dx
104
+ const ty = sy === 0 ? Infinity : sy / dy
105
+ const tz = sz === 0 ? Infinity : sz / dz
106
+ const frac = (v) => v - Math.floor(v)
107
+ let nx = tx * (sx > 0 ? 1 - frac(start.x) : frac(start.x))
108
+ let ny = ty * (sy > 0 ? 1 - frac(start.y) : frac(start.y))
109
+ let nz = tz * (sz > 0 ? 1 - frac(start.z) : frac(start.z))
110
+ const sizeX = box.maxX - box.minX; const sizeY = box.maxY - box.minY; const sizeZ = box.maxZ - box.minZ
111
+ for (let steps = 0; (nx <= 1 || ny <= 1 || nz <= 1) && steps <= 16; steps++) {
112
+ if (nx < ny) {
113
+ if (nx < nz) { cx += sx; nx += tx } else { cz += sz; nz += tz }
114
+ } else if (ny < nz) { cy += sy; ny += ty } else { cz += sz; nz += tz }
115
+ forEachBlockIn(new AABB(cx, cy, cz, cx + sizeX, cy + sizeY, cz + sizeZ), (x, y, z) => out.add(`${x},${y},${z}`))
116
+ }
117
+ }
118
+
119
+ // Entity#applyEffectsFromBlocks, 1.21.2+: after travel, along every movement made this tick
120
+ function applyEffectsAfterTravel (player) {
121
+ if (!player.V.blockEffectsAfterTravel) return
122
+ player.stuck = null
123
+ const end = { ...player.pos }
124
+ const moves = player.V.blockEffectsPerAxis && player.movesThisTick.length ? [...player.movesThisTick] : [{ from: player.tickStart, to: end }]
125
+ const last = moves[moves.length - 1].to
126
+ if ((last.x - end.x) ** 2 + (last.y - end.y) ** 2 + (last.z - end.z) ** 2 > 9.9999994e-11) moves.push({ from: last, to: end })
127
+ const visited = new Set()
128
+ for (const { from, to } of moves) {
129
+ const endBox = player.bbAt(to.x, to.y, to.z).deflate(F_1EM5)
130
+ const keys = new Set()
131
+ forEachBlockIn(endBox, (x, y, z) => keys.add(`${x},${y},${z}`))
132
+ const lengthSq = (to.x - from.x) ** 2 + (to.y - from.y) ** 2 + (to.z - from.z) ** 2
133
+ const far = lengthSq >= LONG_MOVE
134
+ if (player.V.blockEffectsPerAxis && lengthSq > 0) {
135
+ forEachBlockIn(player.bbAt(from.x, from.y, from.z).deflate(F_1EM5), (x, y, z) => keys.add(`${x},${y},${z}`))
136
+ }
137
+ if (far) blocksAlongTravel(from, to, endBox, keys)
138
+ for (const key of keys) {
139
+ if (visited.has(key)) continue
140
+ visited.add(key)
141
+ const [x, y, z] = key.split(',').map(Number)
142
+ const block = player.world.blockAt(x, y, z)
143
+ if (block.isAir) continue
144
+ const inEndBox = endBox.intersectsBox([x, y, z, x + 1, y + 1, z + 1])
145
+ onInsideBlock(player, block, x, y, z, far || inEndBox)
146
+ }
147
+ }
148
+ player.movesThisTick.length = 0
149
+ settleStuck(player)
150
+ }
151
+
152
+ module.exports = { checkInsideBlocksLegacy, applyEffectsAfterTravel }