Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,9 @@ function Physics (mcData, world) {
let acceleration = 0.0
let inertia = 0.0
const blockUnder = world.getBlock(pos.offset(0, -1, 0))
// Player.travel wraps the move while flying and puts back the vertical velocity the tick
// started with, damped, so it is read before anything below touches it.
const flightEntryVelY = vel.y
if (entity.onGround && blockUnder) {
let playerSpeedAttribute
if (entity.attributes && entity.attributes[physics.movementSpeedAttribute]) {
Expand All @@ -569,6 +572,11 @@ function Physics (mcData, world) {
inertia = (blockSlipperiness[blockUnder.type] || physics.defaultSlipperiness) * 0.91
acceleration = attributeSpeed * (0.1627714 / (inertia * inertia * inertia))
if (acceleration < 0) acceleration = 0 // acceleration should not be negative
} else if (entity.flying) {
// Player.getFlyingSpeed: creative flight accelerates at the abilities' flying speed,
// doubled while sprinting, in place of the 0.02 / 0.026 of a falling player.
acceleration = entity.control.sprint ? entity.flyingSpeed * 2 : entity.flyingSpeed
inertia = physics.airborneInertia
} else {
acceleration = physics.airborneAcceleration
inertia = physics.airborneInertia
Expand All @@ -581,26 +589,30 @@ function Physics (mcData, world) {

applyHeading(entity, strafe, forward, acceleration)

if (isOnLadder(world, pos)) {
if (!entity.flying && isOnLadder(world, pos)) {
vel.x = math.clamp(-physics.ladderMaxSpeed, vel.x, physics.ladderMaxSpeed)
vel.z = math.clamp(-physics.ladderMaxSpeed, vel.z, physics.ladderMaxSpeed)
vel.y = Math.max(vel.y, entity.control.sneak ? 0 : -physics.ladderMaxSpeed)
}

moveEntity(entity, world, vel.x, vel.y, vel.z)

if (isOnLadder(world, pos) && (entity.isCollidedHorizontally ||
if (!entity.flying && isOnLadder(world, pos) && (entity.isCollidedHorizontally ||
(supportFeature('climbUsingJump') && entity.control.jump))) {
vel.y = physics.ladderClimbSpeed // climb ladder
}

// Apply friction and gravity
if (entity.levitation > 0) {
vel.y += (0.05 * entity.levitation - vel.y) * 0.2
if (entity.flying) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Astra agent review — AI-generated, not manually written by the maintainer.

The new flying mode preserves hover but never applies vertical flight input. simulatePlayer only adds upward velocity for water/lava or an on-ground jump, and sneak only scales horizontal input. Using this PR's fakePlayer/world fixture with flying: true and initial vy = 0, holding either jump or sneak for 40 ticks leaves y = 80 and vy = 0 throughout. Once the abilities fields are wired up, the bot can therefore neither ascend nor descend from a hover through its normal controls. Please apply the flying jump/sneak input before movement and damping, using the granted flying speed, and add ascent/descent cases alongside the idle-hover tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Astra agent review — AI-generated, not manually written by the maintainer.

The flight handling here is reached only by the normal-movement branch; a flying player in water or lava still takes the earlier fluid branch, including fluid acceleration and gravity. With this head's PlayerState/Physics, real 1.13.2 and 1.20.4 source-water/source-lava blocks, flying: true, zero initial velocity and no controls, 40 ticks changed Y from 80 to 79.125 in water and 78.48 in lava (air stayed at 80). This is separate from the existing jump/sneak issue: an idle hover already fails. The vanilla 1.20.3-pre1 source's Player.isAffectedByFluids returns false while flying, and LivingEntity uses that gate for both fluid branches. Please route flying players through the appropriate flight movement in fluids too, and add water/lava hover cases.

Skills used: prismarine-behavior-test-review helped verify the environmental branch with real blocks; prismarine-architecture-review helped trace the flight flag across shared movement paths.

vel.y = flightEntryVelY * 0.6
} else {
vel.y -= physics.gravity * gravityMultiplier
if (entity.levitation > 0) {
vel.y += (0.05 * entity.levitation - vel.y) * 0.2
} else {
vel.y -= physics.gravity * gravityMultiplier
}
vel.y *= physics.airdrag
}
vel.y *= physics.airdrag
vel.x *= inertia
vel.z *= inertia
}
Expand Down Expand Up @@ -820,6 +832,9 @@ class PlayerState {
this.fireworkRocketDuration = bot.fireworkRocketDuration

// Input only (not modified)
// The server owns these: it grants flight in the abilities packet and the client obeys.
this.flying = bot.entity.flying ?? false
this.flyingSpeed = bot.entity.flyingSpeed ?? 0.05
this.attributes = bot.entity.attributes
this.yaw = bot.entity.yaw
this.pitch = bot.entity.pitch
Expand Down
90 changes: 90 additions & 0 deletions test/flying.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-env mocha */

const { Physics, PlayerState } = require('prismarine-physics')
const { Vec3 } = require('vec3')
const expect = require('expect')

const version = '1.13.2'
const mcData = require('minecraft-data')(version)
const Block = require('prismarine-block')(version)

const fakeWorld = {
getBlock: (pos) => {
const type = (pos.y < 60) ? mcData.blocksByName.stone.id : mcData.blocksByName.air.id
const b = new Block(type, 0, 0)
b.position = pos
return b
}
}

function fakePlayer (pos, { flying = false, flyingSpeed = 0.05 } = {}) {
return {
entity: {
position: pos,
velocity: new Vec3(0, 0, 0),
onGround: false,
isInWater: false,
isInLava: false,
isInWeb: false,
isCollidedHorizontally: false,
isCollidedVertically: false,
elytraFlying: false,
flying,
flyingSpeed,
yaw: Math.PI * 3 / 2, // east (+x)
pitch: 0,
effects: {}
},
jumpTicks: 0,
jumpQueued: false,
fireworkRocketDuration: 0,
version,
inventory: { slots: [] }
}
}

const idle = () => ({ forward: false, back: false, left: false, right: false, jump: false, sprint: false, sneak: false })

function run (player, controls, ticks) {
const physics = Physics(mcData, fakeWorld)
const state = new PlayerState(player, controls)
for (let i = 0; i < ticks; i++) physics.simulatePlayer(state, fakeWorld).apply(player)
return player.entity
}

describe('creative flight', () => {
it('holds its altitude instead of falling', () => {
const entity = run(fakePlayer(new Vec3(0.5, 80, 0.5), { flying: true }), idle(), 40)
expect(entity.position.y).toEqual(80)
expect(entity.velocity.y).toEqual(0)
})

it('still falls when the server has not granted flight', () => {
const entity = run(fakePlayer(new Vec3(0.5, 80, 0.5), { flying: false }), idle(), 40)
expect(entity.position.y).toBeLessThan(80)
})

it('damps the velocity it entered the tick with rather than adding gravity', () => {
const player = fakePlayer(new Vec3(0.5, 80, 0.5), { flying: true })
player.entity.velocity.y = 1
// Player.travel keeps y * 0.6 per tick, so the climb decays instead of turning into a fall.
const entity = run(player, idle(), 1)
expect(entity.velocity.y).toBeCloseTo(0.6, 10)
expect(entity.position.y).toBeCloseTo(81, 10)
})

it('accelerates at the abilities speed, not the 0.02 of a falling player', () => {
const controls = { ...idle(), forward: true }
const flying = run(fakePlayer(new Vec3(0.5, 80, 0.5), { flying: true }), controls, 1)
const falling = run(fakePlayer(new Vec3(0.5, 80, 0.5), { flying: false }), controls, 1)
expect(flying.velocity.x).toBeGreaterThan(falling.velocity.x)
// 0.05 against the airborne 0.02
expect(flying.velocity.x / falling.velocity.x).toBeCloseTo(2.5, 6)
})

it('doubles that speed while sprinting', () => {
const walk = run(fakePlayer(new Vec3(0.5, 80, 0.5), { flying: true }), { ...idle(), forward: true }, 1)
const sprint = run(fakePlayer(new Vec3(0.5, 80, 0.5), { flying: true }), { ...idle(), forward: true, sprint: true }, 1)
expect(sprint.velocity.x / walk.velocity.x).toBeCloseTo(2, 6)
})
})
Loading