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,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 }
|