Skip to content
Merged
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
7 changes: 3 additions & 4 deletions block_effects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++ {
Expand All @@ -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
}
}
}
Expand Down
21 changes: 16 additions & 5 deletions bubble.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,38 @@ 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 {
return
}
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 {
Expand Down
12 changes: 9 additions & 3 deletions bubble_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
}

Expand Down
4 changes: 3 additions & 1 deletion constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 13 additions & 6 deletions parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -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),
}}}
Expand All @@ -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)
}
}
16 changes: 2 additions & 14 deletions simulation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down