package ru.dbotthepony.kstarbound.world import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.AbstractCell import kotlin.collections.ArrayList import kotlin.math.pow import kotlin.math.sqrt 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: RayDirection, val borderCross: Vector2d, val cell: AbstractCell) } 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 */ BREAK(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: AbstractCell, fraction: Double, x: Int, y: Int, normal: RayDirection, 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.value.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, RayDirection.NONE, start.x, start.y) if (result.write) hitTiles.add(RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.NONE, start, cell)) if (result.hit) return RayCastResult(hitTiles, RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.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: RayDirection val yNormal: RayDirection var rayLengthX: Double var rayLengthY: Double if (direction.x < 0.0) { stepX = -1 rayLengthX = (start.x - cellPosX) * unitStepSizeX xNormal = RayDirection.RIGHT } else { stepX = 1 rayLengthX = (cellPosX - start.x + 1) * unitStepSizeX xNormal = RayDirection.LEFT } if (direction.y < 0.0) { stepY = -1 rayLengthY = (start.y - cellPosY) * unitStepSizeY yNormal = RayDirection.UP } else { stepY = 1 rayLengthY = (cellPosY - start.y + 1) * unitStepSizeY yNormal = RayDirection.DOWN } while (travelled < distance) { val normal: RayDirection if (rayLengthX < rayLengthY) { cellPosX += stepX travelled = rayLengthX rayLengthX += unitStepSizeX normal = xNormal } else { cellPosY += stepY travelled = rayLengthY rayLengthY += unitStepSizeY normal = yNormal } cell = getCell(cellPosX, cellPosY) 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) }