diff --git a/index.js b/index.js index d95ce7f..9aedce8 100644 --- a/index.js +++ b/index.js @@ -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]) { @@ -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 @@ -581,7 +589,7 @@ 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) @@ -589,18 +597,22 @@ function Physics (mcData, world) { 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) { + 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 } @@ -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 diff --git a/test/flying.test.js b/test/flying.test.js new file mode 100644 index 0000000..1fdd73b --- /dev/null +++ b/test/flying.test.js @@ -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) + }) +})