package ru.dbotthepony.kstarbound.world import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2i import kotlin.collections.ArrayList import kotlin.math.pow import kotlin.math.sqrt const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT data class RayCastResult( val traversedTiles: List, val hitTile: HitCell?, val fraction: Double, val startPos: Vector2d, val hitPos: Vector2d, val direction: Vector2d ) { constructor(startPos: Vector2d, direction: Vector2d) : this(listOf(), null, 0.0, startPos, startPos, direction) data class HitCell(val pos: Vector2i, val normal: Direction, val borderCross: Vector2d, val cell: IChunkCell) } enum class RayFilterResult(val hit: Boolean, val write: Boolean) { // stop tracing, write hit tile into traversed tiles list HIT(true, true), // stop tracing, don't write hit tile into traversed tiles list HIT_SKIP(true, false), // continue tracing, don't write hit tile into traversed tiles list SKIP(false, false), // continue tracing, write hit tile into traversed tiles list CONTINUE(false, true); companion object { fun of(boolean: Boolean): RayFilterResult { return if (boolean) HIT else SKIP } } } fun interface TileRayFilter { /** * [x] and [y] are wrapped around positions */ fun test(cell: IChunkCell, fraction: Double, x: Int, y: Int, normal: Direction, borderX: Double, borderY: Double): RayFilterResult } val NeverFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.CONTINUE } val NonEmptyFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.of(!state.foreground.material.collisionKind.isEmpty) } fun ICellAccess.castRay(startPos: Vector2d, direction: Vector2d, length: Double, filter: TileRayFilter) = castRay(startPos, startPos + direction * length, filter) // https://www.youtube.com/watch?v=NbSee-XM7WA fun ICellAccess.castRay( start: Vector2d, end: Vector2d, filter: TileRayFilter ): RayCastResult { if (start == end) return RayCastResult(start, Vector2d.ZERO) val hitTiles = ArrayList() var cellPosX = roundTowardsNegativeInfinity(start.x) var cellPosY = roundTowardsNegativeInfinity(start.y) var cell = getCell(cellPosX, cellPosY) ?: return RayCastResult(start, Vector2d.ZERO) val direction = (end - start).unitVector var result = filter.test(cell, 0.0, cellPosX, cellPosY, Direction.NONE, start.x, start.y) if (result.write) hitTiles.add(RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), Direction.NONE, start, cell)) if (result.hit) return RayCastResult(hitTiles, RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), Direction.NONE, start, cell), 0.0, start, start, direction) val distance = start.distance(end) var travelled = 0.0 val unitStepSizeX = sqrt(1 + (direction.y / direction.x).pow(2.0)) val unitStepSizeY = sqrt(1 + (direction.x / direction.y).pow(2.0)) val stepX: Int val stepY: Int val xNormal: Direction val yNormal: Direction var rayLengthX: Double var rayLengthY: Double if (direction.x < 0.0) { stepX = -1 rayLengthX = (start.x - cellPosX) * unitStepSizeX xNormal = Direction.RIGHT } else { stepX = 1 rayLengthX = (cellPosX - start.x + 1) * unitStepSizeX xNormal = Direction.LEFT } if (direction.y < 0.0) { stepY = -1 rayLengthY = (start.y - cellPosY) * unitStepSizeY yNormal = Direction.UP } else { stepY = 1 rayLengthY = (cellPosY - start.y + 1) * unitStepSizeY yNormal = Direction.DOWN } while (travelled < distance) { val normal: Direction if (rayLengthX < rayLengthY) { cellPosX += stepX travelled = rayLengthX rayLengthX += unitStepSizeX normal = xNormal } else { cellPosY += stepY travelled = rayLengthY rayLengthY += unitStepSizeY normal = yNormal } cell = getCell(cellPosX, cellPosY) ?: return RayCastResult(hitTiles, null, travelled / distance, start, start + direction * travelled, direction) result = filter.test(cell, 0.0, cellPosX, cellPosY, normal, start.x + direction.x * travelled, start.y + direction.y * travelled) val c = if (result.write || result.hit) { RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), normal, start + direction * travelled, cell) } else { null } if (result.write) hitTiles.add(c!!) if (result.hit) return RayCastResult(hitTiles, c, travelled / distance, start, start + direction * travelled, direction) } return RayCastResult(hitTiles, null, 1.0, start, start + direction * distance, direction) }