• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Martomate / Hexacraft / 7351367576

28 Dec 2023 06:50PM UTC coverage: 51.185% (-0.1%) from 51.312%
7351367576

push

github

Martomate
Refactor: Made Chunk not know if it has been saved to file

9 of 12 new or added lines in 2 files covered. (75.0%)

110 existing lines in 32 files now uncovered.

2829 of 5527 relevant lines covered (51.19%)

0.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.03
/game/src/main/scala/hexacraft/world/collision.scala
1
package hexacraft.world
2

3
import hexacraft.math.MathUtils
4
import hexacraft.world.coord.{BlockCoords, BlockRelWorld, CoordUtils, CylCoords, Offset, SkewCylCoords}
5
import org.joml.Vector3d
6

7
class MovingBox(val bounds: HexBox, val pos: CylCoords, val velocity: CylCoords.Offset)
8

9
class CollisionDetector(world: BlocksInWorld)(using cylSize: CylinderSize):
1✔
10
  private val reflectionDirs = Array(
11
    Offset(0, -1, 0),
12
    Offset(0, 1, 0),
13
    Offset(-1, 0, 0),
14
    Offset(1, 0, 0),
15
    Offset(0, 0, -1),
16
    Offset(0, 0, 1),
17
    Offset(1, 0, -1),
18
    Offset(-1, 0, 1)
19
  )
1✔
20
  private val reflDirsCyl = reflectionDirs map { case Offset(x, y, z) =>
1✔
21
    SkewCylCoords.Offset(x, y, z).toCylCoordsOffset
22
  }
23

1✔
24
  private val chunkCache: ChunkCache = new ChunkCache(world)
25

1✔
26
  def collides(
27
      objectBounds: HexBox,
28
      objectCoords: CylCoords,
29
      targetBounds: HexBox,
30
      targetCoords: CylCoords
31
  ): Boolean =
1✔
32
    val box = new MovingBox(objectBounds, objectCoords, CylCoords.Offset(0, 0, 0))
1✔
33
    distanceToCollision(box, targetBounds, targetCoords.toSkewCylCoords)._1 == 0
34

35
  /** pos and velocity should be CylCoords in vector form. Velocity is per tick. */
1✔
36
  def positionAndVelocityAfterCollision(
37
      box: HexBox,
38
      pos: Vector3d,
39
      velocity: Vector3d
40
  ): (Vector3d, Vector3d) =
1✔
41
    chunkCache.clearCache()
42

1✔
43
    val vel = new Vector3d(velocity)
44
    var result = (pos, vel)
1✔
45
    val parts = (velocity.length * 10).toInt + 1
1✔
UNCOV
46
    result._2.div(parts)
×
47
    for (_ <- 1 to parts)
1✔
48
      val currentPos = CylCoords(result._1)
1✔
49
      val currentVelocity = CylCoords.Offset(vel)
1✔
50
      result = _collides(new MovingBox(box, currentPos, currentVelocity), 100)
1✔
51
    result._2.mul(parts)
52
    result
53

1✔
54
  private def _collides(box: MovingBox, ttl: Int): (Vector3d, Vector3d) =
55
    if ttl < 0 // TODO: this is a temporary fix for the StackOverflow problem
×
56
    then (box.pos.toVector3d, new Vector3d)
1✔
57
    else if box.velocity.x == 0 && box.velocity.y == 0 && box.velocity.z == 0 // velocity is 0
1✔
58
    then (box.pos.toVector3d, box.velocity.toVector3d)
59
    else
1✔
60
      val futureCoords = box.pos.offset(box.velocity).toBlockCoords
1✔
61
      val (bc, fc) = CoordUtils.getEnclosingBlock(futureCoords)
1✔
62
      val futurePos = BlockCoords(bc).offset(fc).toCylCoords
1✔
63
      val futureBox = new MovingBox(box.bounds, futurePos, box.velocity)
64

1✔
65
      minDistAndReflectionDir(futureBox, bc) match
1✔
66
        case Some((minDist, reflectionDir)) =>
1✔
67
          resultAfterCollision(box, minDist, reflectionDir, ttl)
1✔
68
        case None =>
1✔
69
          (box.pos.offset(box.velocity).toVector3d, box.velocity.toVector3d) // no collision found
70

71
  /** This will check all blocks that could intersect the bounds of the object. If any block
72
    * intersects the bounds of the object it will return (dist: 0, side: -1) If no blocks are in the
73
    * way it will return None Otherwise it will return (dist: 'distance to the closest block', side:
74
    * 'side of collision for that block')
75
    */
1✔
76
  private def minDistAndReflectionDir(box: MovingBox, bc: BlockRelWorld): Option[(Double, Int)] =
1✔
77
    val yLo = math.floor((box.pos.y + box.bounds.bottom) * 2).toInt
1✔
78
    val yHi = math.floor((box.pos.y + box.bounds.top) * 2).toInt
79

80
    val candidates =
1✔
81
      for
×
82
        y <- yLo to yHi
×
UNCOV
83
        dx <- -1 to 1
×
84
        dz <- -1 to 1
85
        if dx * dz != 1 // remove corners
1✔
86
      yield distanceToBlock(box, BlockRelWorld(bc.x + dx, y, bc.z + dz))
87

1✔
88
    candidates.flatten // remove blocks that are not in the way
1✔
89
      .minByOption(_._1)
90

91
  /** Returns the distance to the target block along `vec` and the side of the collision.
92
    *
93
    * If the chunk of the target block is not loaded it will return (dist: 0, side: -1), which is
94
    * the same as if the object was intersecting the block.
95
    *
96
    * If the target block is air it will return None
97
    */
1✔
98
  private def distanceToBlock(box: MovingBox, targetBlock: BlockRelWorld): Option[(Double, Int)] =
1✔
99
    val chunk = chunkCache.getChunk(targetBlock.getChunkRelWorld)
100
    if chunk == null // Chunk isn't loaded, you're stuck (so that you don't fall into the void or something)
1✔
101
    then Some((0, -1))
102
    else
1✔
103
      val blockState = chunk.getBlock(targetBlock.getBlockRelChunk)
104

1✔
105
      if !blockState.blockType.isSolid
1✔
106
      then None
107
      else
1✔
108
        val targetBounds = blockState.blockType.bounds(blockState.metadata)
1✔
109
        val targetCoords = BlockCoords(targetBlock).toSkewCylCoords
110

1✔
111
        Some(distanceToCollision(box, targetBounds, targetCoords))
112

1✔
113
  private def resultAfterCollision(
114
      box: MovingBox,
115
      minDist: Double,
116
      reflectionDir: Int,
117
      ttl: Int
118
  ): (Vector3d, Vector3d) =
119
    if minDist >= 1d // no collision found
1✔
120
    then (box.pos.offset(box.velocity).toVector3d, box.velocity.toVector3d)
1✔
121
    else if reflectionDir == -1 // inside a block
1✔
122
    then (box.pos.toVector3d, new Vector3d)
1✔
123
    else resultAfterCollisionImpl(box, minDist, reflectionDir, ttl)
124

1✔
125
  private def resultAfterCollisionImpl(
126
      box: MovingBox,
127
      minDist: Double,
128
      reflectionDir: Int,
129
      ttl: Int
130
  ): (Vector3d, Vector3d) =
1✔
131
    val normal = reflDirsCyl(reflectionDir).toVector3d.normalize()
132
    val newPos =
1✔
133
      box.pos.offset(box.velocity.x * minDist, box.velocity.y * minDist, box.velocity.z * minDist)
1✔
134
    val vel = box.velocity.toVector3d.mul(1 - minDist)
1✔
135
    val dot = vel.dot(normal)
1✔
136
    vel.sub(normal.mul(dot))
1✔
137
    val result = _collides(new MovingBox(box.bounds, newPos, CylCoords.Offset(vel)), ttl - 1)
1✔
138
    result._2.mul(1 / (1 - minDist))
139
    result
140

141
  /** Returns the distance to the other object along the vector `vec`. Also returns the side of the
142
    * other object that will be collided with.
143
    *
144
    * If the objects are already intersecting, it will return (dist: 1, side: -1) The maximum
145
    * distance returned is 1 (meaning the full length of `vec`). If no collision is found within
146
    * that distance it will return (dist: 1, side: -1) Otherwise it will return (dist: 'distance in
147
    * units of `vec.length`, side: 'side of collision')
148
    */
1✔
149
  private def distanceToCollision(
150
      box1: MovingBox,
151
      box2: HexBox,
152
      _pos2: SkewCylCoords
153
  ): (Double, Int) =
1✔
154
    val pos1 = box1.pos.toSkewCylCoords
1✔
155
    val vel1 = box1.velocity.toSkewCylCoordsOffset
156
    // The following line ensures that the code works when z is close to 0
157
    val pos2 = SkewCylCoords.Offset(
158
      _pos2.x,
159
      _pos2.y,
1✔
160
      MathUtils.absmin(_pos2.z - pos1.z, cylSize.circumference) + pos1.z
161
    )
162

163
    val x1 = pos1.x + 0.5 * pos1.z
164
    val y1 = pos1.y
165
    val z1 = pos1.z + 0.5 * pos1.x
166
    val x2 = pos2.x + 0.5 * pos2.z
167
    val y2 = pos2.y
168
    val z2 = pos2.z + 0.5 * pos2.x
169

170
    val r1 = box1.bounds.smallRadius
171
    val r2 = box2.smallRadius
172
    val b1 = box1.bounds.bottom
173
    val b2 = box2.bottom
174
    val t1 = box1.bounds.top
175
    val t2 = box2.top
176

177
    val dx = x2 - x1
178
    val dy = y2 - y1
179
    val dz = z2 - z1
180
    val d = r2 + r1
181

182
    val vx = vel1.x + 0.5 * vel1.z
183
    val vy = vel1.y
184
    val vz = vel1.z + 0.5 * vel1.x
185

186
    // index corresponds to `reflectionDirs`
1✔
187
    val distances = Array(
188
      t2 - b1 + dy, // (  y2    + t2) - (  y1    + b1),
189
      t1 - b2 - dy, // (  y1    + t1) - (  y2    + b2),
190
      d + dx, //       (     x2 + r2) - (     x1 - r1),
191
      d - dx, //       (     x1 + r1) - (     x2 - r2),
192
      d + dz, //       (z2      + r2) - (z1      - r1),
193
      d - dz, //       (z1      + r1) - (z2      - r2),
194
      d + dz - dx, //  (z2 - x2 + r2) - (z1 - x1 - r1),
195
      d - dz + dx //   (z1 - x1 + r1) - (z2 - x2 - r2)
196
    )
197

1✔
198
    if !distances.forall(_ >= 0)
1✔
199
    then (1, -1)
UNCOV
200
    else // no intersection before moving
×
201
      val velDists = reflectionDirs.map(t => t.dx * vx + t.dy * vy + t.dz * vz) // the length of v along the normals
202
      // TODO: possible bug: the dot product above uses normals that are not normalized. Could this lead to incorrect velDists?
1✔
203
      if !distances.indices.exists(i => distances(i) <= velDists(i))
1✔
204
      then (0, -1)
205
      else // intersection after moving
×
206
        val zipped = for (i <- distances.indices) yield {
1✔
207
          val velDist = velDists(i)
1✔
208
          val distBefore = ((distances(i) - velDist) * 1e9).toLong / 1e9
1✔
209
          val a = if (velDist <= 0 || distBefore > 0) 1 else -distBefore / velDist
210
          (a, i)
211
        }
1✔
212
        val minInZip = zipped.minBy(_._1)
1✔
213
        (math.min(minInZip._1, 1), minInZip._2)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc