diff --git a/block_effects.go b/block_effects.go index daae343..51d0e7a 100644 --- a/block_effects.go +++ b/block_effects.go @@ -91,10 +91,10 @@ func (s *Simulator) applyInsideBlockEffects(state *MovementState) { s.applyHoneyWallSlide(state) } +// applyHoneyWallSlide slows the entity once per overlapped honey block. Contact +// with the block volume is enough; a horizontal collision is not required, and +// overlapping two blocks compounds the horizontal factor. func (s *Simulator) applyHoneyWallSlide(state *MovementState) { - if !state.CollideX && !state.CollideZ { - return - } bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl32.Vec3{1e-3, 0, 1e-3}) min, maxPoint := bb.Min(), bb.Max() for x := int(math32.Floor(min.X())); x < int(math32.Ceil(maxPoint.X())); x++ { @@ -110,7 +110,6 @@ func (s *Simulator) applyHoneyWallSlide(state *MovementState) { velocity[1] = max(-0.12, velocity[1]) velocity[2] *= 0.4 state.SetVel(velocity) - return } } } diff --git a/bubble.go b/bubble.go index 53286f7..2e5af00 100644 --- a/bubble.go +++ b/bubble.go @@ -40,6 +40,10 @@ func applyBubbleColumn(state *MovementState, direction BubbleColumnDirection, su state.SetVel(velocity) } +// applyBubbleColumns applies at most one column impulse per tick. An entity +// carries a single resolved column contact regardless of how many column cells +// its hitbox overlaps, and the topmost overlapped cell decides the contact: +// only that cell can have the open air above it that selects the surface form. func (s *Simulator) applyBubbleColumns(state *MovementState) { provider, ok := s.World.(BubbleColumnProvider) if !ok { @@ -47,20 +51,27 @@ func (s *Simulator) applyBubbleColumns(state *MovementState) { } bb := state.BoundingBox(s.Options.UseSlideOffset) min, max := bb.Min(), bb.Max() + contact, found := cube.Pos{}, false + var direction BubbleColumnDirection for x := int(math32.Floor(min.X())); x < int(math32.Ceil(max.X())); x++ { for y := int(math32.Floor(min.Y())); y < int(math32.Ceil(max.Y())); y++ { for z := int(math32.Floor(min.Z())); z < int(math32.Ceil(max.Z())); z++ { pos := cube.Pos{x, y, z} - direction, found := provider.BubbleColumn(pos) - if !found { + cellDirection, ok := provider.BubbleColumn(pos) + if !ok || (found && pos.Y() <= contact.Y()) { continue } - above := pos.Side(cube.FaceUp) - _, liquidAbove := s.liquidAt(above) - applyBubbleColumn(state, direction, !liquidAbove && s.blockAir(s.blockAtPos(above))) + contact, direction, found = pos, cellDirection, true } } } + if !found { + return + } + above := contact.Side(cube.FaceUp) + _, liquidAbove := s.liquidAt(above) + applyBubbleColumn(state, direction, !liquidAbove && s.blockAir(s.blockAtPos(above))) + state.FallDistance = 0 } func (s *Simulator) attemptRiptide(state *MovementState, touchingWater bool) bool { diff --git a/bubble_test.go b/bubble_test.go index a2b72b1..05cad52 100644 --- a/bubble_test.go +++ b/bubble_test.go @@ -55,7 +55,7 @@ func TestBubbleColumnSurfaceAcceptsRegistryBackedAir(t *testing.T) { } } -func TestBubbleColumnAppliesForEachOccupiedCell(t *testing.T) { +func TestBubbleColumnAppliesOnceForOverlappedCells(t *testing.T) { w := environmentWorld{ bubbles: map[cube.Pos]BubbleColumnDirection{ {0, 0, 0}: BubbleColumnUp, @@ -68,11 +68,17 @@ func TestBubbleColumnAppliesForEachOccupiedCell(t *testing.T) { } state := newBaseState() state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.FallDistance = 4 (&Simulator{World: w}).applyBubbleColumns(state) - if want := float32(0.16); math32.Abs(state.Vel.Y()-want) > 1e-6 { - t.Fatalf("bubble-column velocity = %v, want per-cell impulses totaling %v", state.Vel.Y(), want) + // The topmost overlapped cell has open air above it, so this resolves to the + // surface form and applies once: 0.1, not 0.06+0.1 for the two cells. + if want := float32(0.1); math32.Abs(state.Vel.Y()-want) > 1e-6 { + t.Fatalf("bubble-column velocity = %v, want a single impulse of %v", state.Vel.Y(), want) + } + if state.FallDistance != 0 { + t.Fatalf("bubble-column contact left fall distance = %v", state.FallDistance) } } diff --git a/constants.go b/constants.go index 2c4f698..231fdbd 100644 --- a/constants.go +++ b/constants.go @@ -11,7 +11,9 @@ const ( StepHeight = float32(0.5625) SlideOffsetMultiplier = float32(0.4) SlimeBounceMultiplier = float32(-1) - BedBounceMultiplier = float32(-0.75) + BedBounceMultiplier = float32(-0.66) + // BedBounceCap bounds the upward bounce velocity. + BedBounceCap = float32(1) // This can be validated in Mob::ascendLadder(). ClimbSpeed = float32(0.2) MaxConsumingImpulse = float32(0.1225) diff --git a/parity_test.go b/parity_test.go index 31e263e..fdbf3ea 100644 --- a/parity_test.go +++ b/parity_test.go @@ -61,15 +61,22 @@ func TestBedrockStepHeight(t *testing.T) { } } -func TestBedBounceUsesBedrockRestitutionAndCap(t *testing.T) { +func TestBedBounceUsesCorroboratedRestitutionAndCap(t *testing.T) { sim := &Simulator{BlockSemantics: overrideBlockSemantics{semantics: movementblock.MovementSemantics{Bounce: movementblock.BounceBed}}} state := newBaseState() state.Vel = mgl32.Vec3{0, -2} sim.landOnBlock(state, state.Vel, block.Air{}) - if want := float32(0.75); state.Vel.Y() != want { - t.Fatalf("expected bed bounce %v, got %v", want, state.Vel.Y()) + // -0.66 * -2 = 1.32, above the cap. + if want := BedBounceCap; math32.Abs(state.Vel.Y()-want) > 1e-6 { + t.Fatalf("expected capped bed bounce %v, got %v", want, state.Vel.Y()) + } + + state.Vel = mgl32.Vec3{0, -1} + sim.landOnBlock(state, state.Vel, block.Air{}) + if want := -BedBounceMultiplier; math32.Abs(state.Vel.Y()-want) > 1e-6 { + t.Fatalf("expected uncapped bed bounce %v, got %v", want, state.Vel.Y()) } } @@ -101,7 +108,7 @@ func TestSlowFallingChangesGlideGravity(t *testing.T) { } } -func TestSneakEdgeProtectionWhileSlightlyAboveGround(t *testing.T) { +func TestSneakEdgeProtectionRequiresGround(t *testing.T) { sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ cube.Box32(-1, -1, -1, 0, 0, 1), }}} @@ -113,7 +120,7 @@ func TestSneakEdgeProtectionWhileSlightlyAboveGround(t *testing.T) { sim.avoidEdge(state) - if state.Vel.X() >= 0.5 { - t.Fatalf("expected edge protection above nearby ground, got velocity %v", state.Vel) + if state.Vel.X() != 0.5 { + t.Fatalf("edge protection ran while airborne: %v", state.Vel) } } diff --git a/simulation.go b/simulation.go index 8ad3341..4220e6d 100644 --- a/simulation.go +++ b/simulation.go @@ -725,7 +725,7 @@ func (s *Simulator) landOnBlock(state *MovementState, old mgl32.Vec3, blockUnder newVel[1] = 0.0 } case movementblock.BounceBed: - newVel[1] = math32.Min(0.75, BedBounceMultiplier*old.Y()) + newVel[1] = math32.Min(BedBounceCap, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 } @@ -1051,7 +1051,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { if w == nil { return } - if !state.Sneaking || !s.isAboveGround(state) || state.Vel.Y() > 0 { + if !state.Sneaking || !state.OnGround || state.Vel.Y() > 0 { s.debugf( "avoidEdge: conditions not met (sneaking=%v onGround=%v yVel=%v)", state.Sneaking, @@ -1128,18 +1128,6 @@ func (s *Simulator) avoidEdge(state *MovementState) { s.debugf("(avoidEdge): oldVel=%v newVel=%v", oldVel, newVel) } -func (s *Simulator) isAboveGround(state *MovementState) bool { - if state.OnGround { - return true - } - if state.FallDistance >= 0.6 || s.World == nil { - return false - } - distance := 0.6 - state.FallDistance - bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl32.Vec3{-0.025, 0, -0.025}) - return s.hasNearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -distance})) -} - func (s *Simulator) isInsideCobweb(state *MovementState) bool { if s.World == nil { return false