From b9975657d4fc676270539e31816c112e3f32437a Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Sat, 14 Oct 2023 10:39:20 +0700
Subject: [PATCH] SAT testcode

---
 .../kotlin/ru/dbotthepony/kstarbound/Main.kt  |  72 ++--
 .../kstarbound/client/StarboundClient.kt      |  15 +-
 .../client/gl/vertex/VertexBuilder.kt         |  26 ++
 .../kstarbound/client/render/Box2DRenderer.kt |   4 +-
 .../client/render/entity/ItemRenderer.kt      |   8 +
 .../kstarbound/client/world/ClientChunk.kt    |  20 -
 .../kstarbound/client/world/ClientWorld.kt    |   8 +
 .../ru/dbotthepony/kstarbound/world/Chunk.kt  |   8 +-
 .../kstarbound/world/api/IChunkCell.kt        |  12 +
 .../kstarbound/world/entities/AliveEntity.kt  | 353 ------------------
 .../kstarbound/world/entities/Entity.kt       |  75 ++--
 .../kstarbound/world/entities/ItemEntity.kt   |  44 +--
 .../world/entities/MovementController.kt      | 155 --------
 .../kstarbound/world/entities/PlayerEntity.kt |  74 ----
 .../kstarbound/world/physics/Poly.kt          | 236 ++++++++++++
 15 files changed, 407 insertions(+), 703 deletions(-)
 delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt
 delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt
 delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt

diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
index 71ff8ef0..dfc934d9 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
@@ -1,6 +1,8 @@
 package ru.dbotthepony.kstarbound
 
 
+import com.google.common.collect.ImmutableList
+import com.google.gson.reflect.TypeToken
 import org.apache.logging.log4j.LogManager
 import org.lwjgl.Version
 import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
@@ -13,20 +15,21 @@ import ru.dbotthepony.kstarbound.player.QuestInstance
 import ru.dbotthepony.kstarbound.util.JVMTimeSource
 import ru.dbotthepony.kstarbound.world.api.IChunkCell
 import ru.dbotthepony.kstarbound.world.entities.ItemEntity
-import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
 import ru.dbotthepony.kstarbound.io.json.VersionedJson
 import ru.dbotthepony.kstarbound.io.readVarInt
 import ru.dbotthepony.kstarbound.util.AssetPathStack
-import ru.dbotthepony.kstarbound.world.entities.Move
 import ru.dbotthepony.kstarbound.world.entities.WorldObject
+import ru.dbotthepony.kstarbound.world.physics.Poly
 import ru.dbotthepony.kvector.vector.Vector2d
 import java.io.BufferedInputStream
 import java.io.ByteArrayInputStream
 import java.io.DataInputStream
 import java.io.File
 import java.util.*
+import java.util.concurrent.TimeUnit
 import java.util.zip.Inflater
 import java.util.zip.InflaterInputStream
+import kotlin.collections.ArrayList
 
 private val LOGGER = LogManager.getLogger()
 
@@ -64,8 +67,6 @@ fun main() {
 		Starbound.terminateLoading = true
 	}
 
-	val ent = PlayerEntity(client.world!!)
-
 	Starbound.onInitialize {
 		//for (chunkX in 17 .. 18) {
 		//for (chunkX in 14 .. 24) {
@@ -124,10 +125,10 @@ fun main() {
 		for (i in 0 .. 0) {
 			val item = ItemEntity(client.world!!, item.value)
 
-			item.position = Vector2d(7.0 + i, 685.0)
+			item.position = Vector2d(225.0 + i, 685.0)
 			item.spawn()
+			item.velocity = Vector2d(0.01, -0.08)
 			//item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0))
-			item.movement.applyVelocity(Vector2d(-1.0, 0.0))
 		}
 
 		// println(Starbound.statusEffects["firecharge"])
@@ -154,21 +155,55 @@ fun main() {
 	}
 
 	//ent.position += Vector2d(y = 14.0, x = -10.0)
-	ent.position = Vector2d(238.0, 685.0)
 	client.camera.pos = Vector2d(238.0, 685.0)
 	//client.camera.pos = Vector2f(0f, 0f)
 
 	client.onDrawGUI {
-		client.font.render("${ent.position}", y = 100f, scale = 0.25f)
-		client.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f)
 		client.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f)
 		client.font.render("Cursor: ${client.mouseCoordinates} -> ${client.screenToWorld(client.mouseCoordinates)}", y = 160f, scale = 0.25f)
 		client.font.render("World chunk: ${client.world!!.chunkFromCell(client.camera.pos)}", y = 180f, scale = 0.25f)
 	}
 
-	client.onPreDrawWorld {
+	var p = Poly(Starbound.gson.fromJson<List<Vector2d>>("""[ [1.75, 2.55], [2.25, 2.05],  [2.75, -3.55], [2.25, -3.95],  [-2.25, -3.95], [-2.75, -3.55],  [-2.25, 2.05], [-1.75, 2.55] ]""", TypeToken.getParameterized(ImmutableList::class.java, Vector2d::class.java).type))
+	val polies = ArrayList<Poly>()
+
+	val box = Poly(listOf(Vector2d(-0.5, -0.5), Vector2d(-0.5, 0.5), Vector2d(0.5, 0.5), Vector2d(0.5, -0.5)))
+	p = box
+	val triangle = Poly(listOf(Vector2d(-0.5, -0.5), Vector2d(0.5, 0.5), Vector2d(0.5, -0.5)))
+
+	//polies.add(triangle + Vector2d(0.0, -1.5))
+	polies.add(box)
+	polies.add(box + Vector2d(1.0))
+	//polies.add(box + Vector2d(1.0, -1.0))
+	//polies.add(box + Vector2d(1.0, -2.0))
+	//polies.add(box + Vector2d(1.0, -3.0))
+
+	//client.foregroundExecutor.scheduleAtFixedRate({ p.intersect(p2)?.let { p += it; println(it) } }, 1, 1, TimeUnit.SECONDS)
+
+	p += client.camera.pos
+
+	for (i in polies.indices)
+		polies[i] = polies[i] + client.camera.pos
+
+	client.onPostDrawWorld {
 		//client.camera.pos.x = ent.pos.x.toFloat()
 		//client.camera.pos.y = ent.pos.y.toFloat()
+
+		p += client.screenToWorld(client.mouseCoordinates) - p.aabb.centre
+
+		for (i in 0 until 10) {
+			val intersects = ArrayList<Poly.Penetration>()
+
+			polies.forEach { p.intersect(it)?.let { intersects.add(it) } }
+
+			if (intersects.isEmpty())
+				break
+			else
+				p += intersects.max()
+		}
+
+		p.render()
+		polies.forEach { it.render() }
 	}
 
 	client.box2dRenderer.drawShapes = false
@@ -200,23 +235,6 @@ fun main() {
 
 		//println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1)
 
-		//if (ent.onGround)
-			//ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1
-
-		if (client.input.KEY_LEFT_DOWN) {
-			ent.movement.moveDirection = Move.MOVE_LEFT
-		} else if (client.input.KEY_RIGHT_DOWN) {
-			ent.movement.moveDirection = Move.MOVE_RIGHT
-		} else {
-			ent.movement.moveDirection = Move.STAND_STILL
-		}
-
-		if (client.input.KEY_SPACE_PRESSED && ent.movement.onGround) {
-			ent.movement.requestJump()
-		} else if (client.input.KEY_SPACE_RELEASED) {
-			ent.movement.recallJump()
-		}
-
 		if (client.input.KEY_ESCAPE_PRESSED) {
 			glfwSetWindowShouldClose(client.window, true)
 		}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
index 4c1f6b4d..eb06a766 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
@@ -74,7 +74,6 @@ import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import java.time.Duration
 import java.util.*
-import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ForkJoinPool
 import java.util.concurrent.ForkJoinWorkerThread
 import java.util.concurrent.atomic.AtomicInteger
@@ -326,23 +325,23 @@ class StarboundClient : Closeable {
 	private fun executeQueuedTasks() {
 		foregroundExecutor.executeQueuedTasks()
 
-		var next3 = openglCleanQueue.poll() as CleanRef?
+		var next = openglCleanQueue.poll() as CleanRef?
 
-		while (next3 != null) {
+		while (next != null) {
 			openglObjectsCleaned++
-			next3.fn.accept(next3.value)
+			next.fn.accept(next.value)
 			checkForGLError("Removing unreachable OpenGL object")
 
 			val head = openglCleanQueueHead
 
-			if (next3 === head) {
+			if (next === head) {
 				openglCleanQueueHead = head.next
 			} else {
-				next3.prev?.next = next3.next
-				next3.next?.prev = next3.prev
+				next.prev?.next = next.next
+				next.next?.prev = next.prev
 			}
 
-			next3 = openglCleanQueue.poll() as CleanRef?
+			next = openglCleanQueue.poll() as CleanRef?
 		}
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt
index c0de4a3f..ce1ba841 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt
@@ -5,6 +5,7 @@ import org.lwjgl.opengl.GL45.GL_UNSIGNED_INT
 import org.lwjgl.opengl.GL45.GL_UNSIGNED_SHORT
 import org.lwjgl.opengl.GL45.GL_UNSIGNED_BYTE
 import ru.dbotthepony.kstarbound.client.gl.BufferObject
+import ru.dbotthepony.kvector.api.IStruct2d
 import ru.dbotthepony.kvector.api.IStruct2f
 import ru.dbotthepony.kvector.api.IStruct4f
 import ru.dbotthepony.kvector.arrays.Matrix3f
@@ -279,12 +280,36 @@ class VertexBuilder(
 		return this
 	}
 
+	fun vertex(value: IStruct2f): VertexBuilder {
+		vertex()
+		position(value.component1(), value.component2())
+		return this
+	}
+
+	fun vertex(value: IStruct2d): VertexBuilder {
+		vertex()
+		position(value.component1().toFloat(), value.component2().toFloat())
+		return this
+	}
+
 	fun vertex(transform: Matrix3f, x: Float, y: Float): VertexBuilder {
 		vertex()
 		position(transform, x, y)
 		return this
 	}
 
+	fun vertex(transform: Matrix3f, value: IStruct2f): VertexBuilder {
+		vertex()
+		position(transform, value)
+		return this
+	}
+
+	fun vertex(transform: Matrix3f, value: IStruct2d): VertexBuilder {
+		vertex()
+		position(transform, value)
+		return this
+	}
+
 	private fun pushFloat(attr: VertexAttributes.Attribute, x: Float): VertexBuilder {
 		vertexMemory.position(vertexCount * attributes.vertexStride + attr.relativeOffset)
 		vertexMemory.putFloat(x)
@@ -352,6 +377,7 @@ class VertexBuilder(
 	}
 
 	fun position(transform: Matrix3f, value: IStruct2f) = position(transform, value.component1(), value.component2())
+	fun position(transform: Matrix3f, value: IStruct2d) = position(transform, value.component1().toFloat(), value.component2().toFloat())
 
 	fun uv(x: Float, y: Float): VertexBuilder {
 		return pushFloat2(requireNotNull(uv) { "Vertex format does not have texture UV attribute" }, x, y)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt
index cc6519ae..76ce24cc 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt
@@ -56,8 +56,8 @@ class Box2DRenderer : IDebugDraw {
 		for (i in vertices.indices) {
 			val current = vertices[i]
 			val next = vertices[(i + 1) % vertices.size]
-			lines.builder.vertex(state.stack.last(), current.x.toFloat(), current.y.toFloat()).color(color)
-			lines.builder.vertex(state.stack.last(), next.x.toFloat(), next.y.toFloat()).color(color)
+			lines.builder.vertex(state.stack.last(), current).color(color)
+			lines.builder.vertex(state.stack.last(), next).color(color)
 		}
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt
index dbc3e84c..ab025bfb 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt
@@ -7,8 +7,16 @@ import ru.dbotthepony.kvector.arrays.Matrix4fStack
 
 class ItemRenderer(client: StarboundClient, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(client, entity, chunk) {
 	private val def = entity.def
+	private val textures = def.inventoryIcon?.stream()?.map { it.image }?.toList() ?: listOf()
 
 	override fun render(stack: Matrix4fStack) {
+		//client.programs.positionTexture.use()
+		//client.programs.positionTexture.texture0 = 0
 
+		//for (texture in textures) {
+		//	client.textures2D[0] = texture.image?.texture ?: continue
+		//}
+
+		entity.hitboxes.forEach { it.render(client) }
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt
index e2cfa54b..055b6e1a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt
@@ -29,24 +29,4 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
 			it.liquidIsDirty = true
 		}
 	}
-
-	private val entityRenderers = HashMap<Entity, EntityRenderer>()
-
-	override fun onEntityAdded(entity: Entity) {
-		entityRenderers[entity] = EntityRenderer.getRender(world.client, entity, this)
-	}
-
-	override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {
-		val renderer = otherChunk.entityRenderers[entity] ?: throw IllegalStateException("$otherChunk has no renderer for $entity!")
-		entityRenderers[entity] = renderer
-		renderer.chunk = this
-	}
-
-	override fun onEntityTransferedFromThis(entity: Entity, otherChunk: ClientChunk) {
-		entityRenderers.remove(entity)
-	}
-
-	override fun onEntityRemoved(entity: Entity) {
-		entityRenderers.remove(entity)
-	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
index f6a512ac..0e35ee90 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
@@ -311,6 +311,14 @@ class ClientWorld(
 				obj.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY)
 			}
 		}
+
+		for (ent in entities) {
+			if (ent.position.x.toInt() in client.viewportCellX .. client.viewportCellX + client.viewportCellWidth && ent.position.y.toInt() in client.viewportCellY .. client.viewportCellY + client.viewportCellHeight) {
+				layers.add(RenderLayer.Overlay.point()) {
+					ent.render(client)
+				}
+			}
+		}
 	}
 
 	override fun thinkInner() {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
index 3df15be3..b95e8dd2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
@@ -324,10 +324,10 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
 
 	protected val entities = ReferenceOpenHashSet<Entity>()
 
-	protected abstract fun onEntityAdded(entity: Entity)
-	protected abstract fun onEntityTransferedToThis(entity: Entity, otherChunk: This)
-	protected abstract fun onEntityTransferedFromThis(entity: Entity, otherChunk: This)
-	protected abstract fun onEntityRemoved(entity: Entity)
+	protected open fun onEntityAdded(entity: Entity) { }
+	protected open fun onEntityTransferedToThis(entity: Entity, otherChunk: This) { }
+	protected open fun onEntityTransferedFromThis(entity: Entity, otherChunk: This) { }
+	protected open fun onEntityRemoved(entity: Entity) { }
 
 	fun addEntity(entity: Entity) {
 		if (!entities.add(entity)) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt
index b7ca691f..71d7a6af 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt
@@ -4,9 +4,14 @@ import ru.dbotthepony.kstarbound.RegistryObject
 import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
 import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
 import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
+import ru.dbotthepony.kstarbound.world.physics.Poly
 import ru.dbotthepony.kvector.api.IStruct2i
+import ru.dbotthepony.kvector.util2d.AABB
+import ru.dbotthepony.kvector.vector.Vector2d
 import java.io.DataInputStream
 
+private val rect = Poly(listOf(Vector2d.ZERO, Vector2d(0.0, 1.0), Vector2d(1.0, 1.0), Vector2d(1.0, 0.0)))
+
 interface IChunkCell : IStruct2i {
 	/**
 	 * absolute (in world)
@@ -26,6 +31,13 @@ interface IChunkCell : IStruct2i {
 		return y
 	}
 
+	val polies: Collection<Poly> get() {
+		if (foreground.material.isMeta)
+			return emptyList()
+
+		return listOf(rect + Vector2d(this.x.toDouble(), this.y.toDouble()))
+	}
+
 	val foreground: ITileState
 	val background: ITileState
 	val liquid: ILiquidState
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt
deleted file mode 100644
index 864f6baa..00000000
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt
+++ /dev/null
@@ -1,353 +0,0 @@
-package ru.dbotthepony.kstarbound.world.entities
-
-import ru.dbotthepony.kbox2d.api.ContactEdge
-import ru.dbotthepony.kbox2d.api.FixtureDef
-import ru.dbotthepony.kbox2d.api.b2_polygonRadius
-import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
-import ru.dbotthepony.kbox2d.dynamics.B2Fixture
-import ru.dbotthepony.kstarbound.Starbound
-import ru.dbotthepony.kstarbound.world.World
-import ru.dbotthepony.kvector.util2d.AABB
-import ru.dbotthepony.kvector.vector.Vector2d
-import ru.dbotthepony.kvector.vector.times
-import kotlin.math.absoluteValue
-
-enum class Move {
-	STAND_STILL,
-	MOVE_LEFT,
-	MOVE_RIGHT
-}
-
-/**
- * Базовый абстрактный класс, реализующий сущность, которая ходит по земле
- */
-abstract class WalkableMovementController<T : AliveWalkingEntity>(entity: T) : MovementController<T>(entity) {
-	init {
-		body.isFixedRotation = true
-	}
-
-	protected abstract val moveDirection: Move
-
-	protected var sensorA: B2Fixture? = null
-	protected var sensorB: B2Fixture? = null
-	protected var bodyFixture: B2Fixture? = null
-
-	protected abstract fun recreateBodyFixture()
-
-	protected open fun recreateSensors() {
-		sensorA?.destroy()
-		sensorB?.destroy()
-
-		val bodyFixture = bodyFixture ?: return
-		val aabb = bodyFixture.shape.computeAABB(0)
-
-		val sensorheight = (aabb.height - stepSize) / 2.0
-
-		sensorA = body.createFixture(FixtureDef(
-			shape = PolygonShape().also { it.setAsBox(0.2, sensorheight, Vector2d(-aabb.width / 2.0 - 0.2, aabb.height / 2.0 - sensorheight), 0.0) },
-			isSensor = true,
-		))
-
-		sensorB = body.createFixture(FixtureDef(
-			shape = PolygonShape().also { it.setAsBox(0.2, sensorheight, Vector2d(aabb.width / 2.0 + 0.2, aabb.height / 2.0 - sensorheight), 0.0) },
-			isSensor = true,
-		))
-	}
-
-	var wantsToDuck = false
-	open var isDucked = false
-		protected set
-
-	override fun think() {
-		super.think()
-		thinkMovement()
-	}
-
-	/**
-	 * See [IWalkableEntity.topSpeed]
-	 */
-	open val topSpeed by entity::topSpeed
-
-	/**
-	 * See [IWalkableEntity.moveSpeed]
-	 */
-	open val moveSpeed by entity::moveSpeed
-
-	/**
-	 * See [IWalkableEntity.freeFallMoveSpeed]
-	 */
-	open val freeFallMoveSpeed by entity::freeFallMoveSpeed
-
-	/**
-	 * See [IWalkableEntity.brakeForce]
-	 */
-	open val brakeForce by entity::brakeForce
-
-	/**
-	 * See [IWalkableEntity.stepSize]
-	 */
-	open val stepSize by entity::stepSize
-
-	/**
-	 * See [IWalkableEntity.jumpForce]
-	 */
-	open val jumpForce by entity::jumpForce
-
-	protected var jumpRequested = false
-	protected var nextJump = 0
-
-	open fun requestJump() {
-		if (jumpRequested || nextJump > world.ticks)
-			return
-
-		jumpRequested = true
-		nextJump = world.ticks + 15
-	}
-
-	open fun recallJump() {
-		if (jumpRequested) {
-			jumpRequested = false
-			nextJump = 0
-		}
-	}
-
-	protected abstract fun canUnDuck(): Boolean
-
-	protected var previousVelocity = Vector2d.ZERO
-
-	protected open fun thinkMovement() {
-		if (onGround && !isDucked) {
-			when (moveDirection) {
-				Move.STAND_STILL -> {
-					body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce)
-				}
-
-				Move.MOVE_LEFT -> {
-					if (body.linearVelocity.x > 0.0) {
-						body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce)
-					}
-
-					if (body.linearVelocity.x > -topSpeed) {
-						body.linearVelocity += Vector2d(x = -moveSpeed * Starbound.TICK_TIME_ADVANCE)
-
-						// Ghost collision prevention
-						if (body.linearVelocity.x.absoluteValue < 1) {
-							body.linearVelocity += -Starbound.TICK_TIME_ADVANCE * world.gravity * 2.0
-						}
-					}
-
-					var wantToStepUp = false
-					var foundContact: ContactEdge? = null
-
-					for (contact in body.contactEdgeIterator) {
-						if (contact.contact.manifold.localNormal.dot(Vector2d.NEGATIVE_X) >= 0.95) {
-							// we hit something to our left
-							wantToStepUp = true
-							foundContact = contact
-							break
-						}
-					}
-
-					if (wantToStepUp) {
-						// make sure something we hit is actually a staircase of sort, and not geometry edges
-						val aabbFound: AABB
-
-						if (foundContact!!.contact.fixtureB == sensorA) {
-							aabbFound = foundContact.contact.fixtureA.getAABB(foundContact.contact.childIndexA)
-						} else {
-							aabbFound = foundContact.contact.fixtureB.getAABB(foundContact.contact.childIndexB)
-						}
-
-						if (aabbFound.maxs.y - b2_polygonRadius * 2.0 <= body.worldSpaceAABB.mins.y) {
-							// just a crack on sidewalk
-							wantToStepUp = false
-						}
-					}
-
-					if (wantToStepUp) {
-						var stepHeightClear = true
-
-						for (contact in body.contactEdgeIterator) {
-							if (contact.contact.fixtureA == sensorA || contact.contact.fixtureB == sensorA) {
-								if (contact.contact.isTouching) {
-									stepHeightClear = false
-									break
-								}
-							}
-						}
-
-						if (stepHeightClear) {
-							val velocity = if (previousVelocity.length > body.linearVelocity.length) previousVelocity else body.linearVelocity
-							body.setTransform(body.position + Vector2d(x = -0.05, y = stepSize), body.angle)
-							body.linearVelocity = velocity
-							//body.linearVelocity += Vector2d(y = -Starbound.SECONDS_IN_FRAME * 18.0 * stepSize * world.gravity.y)
-						}
-					}
-				}
-
-				Move.MOVE_RIGHT -> {
-					if (body.linearVelocity.x < 0.0) {
-						body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce)
-					}
-
-					if (body.linearVelocity.x < topSpeed) {
-						body.linearVelocity += Vector2d(x = moveSpeed * Starbound.TICK_TIME_ADVANCE)
-
-						// Ghost collision prevention
-						if (body.linearVelocity.x.absoluteValue < 1) {
-							body.linearVelocity += -Starbound.TICK_TIME_ADVANCE * world.gravity * 2.0
-						}
-					}
-
-					var wantToStepUp = false
-					var foundContact: ContactEdge? = null
-
-					for (contact in body.contactEdgeIterator) {
-						if (contact.contact.manifold.localNormal.dot(Vector2d.POSITIVE_X) >= 0.95) {
-							// we hit something to our right
-							wantToStepUp = true
-							foundContact = contact
-							break
-						}
-					}
-
-					if (wantToStepUp) {
-						// make sure something we hit is actually a staircase of sort, and not geometry edges
-						val aabbFound: AABB
-
-						if (foundContact!!.contact.fixtureB == sensorB) {
-							aabbFound = foundContact.contact.fixtureA.getAABB(foundContact.contact.childIndexA)
-						} else {
-							aabbFound = foundContact.contact.fixtureB.getAABB(foundContact.contact.childIndexB)
-						}
-
-						if (aabbFound.maxs.y - b2_polygonRadius * 2.0 <= body.worldSpaceAABB.mins.y) {
-							// just a crack on sidewalk
-							wantToStepUp = false
-						}
-					}
-
-					if (wantToStepUp) {
-						var stepHeightClear = true
-
-						for (contact in body.contactEdgeIterator) {
-							if (contact.contact.fixtureA == sensorB || contact.contact.fixtureB == sensorB) {
-								if (contact.contact.isTouching) {
-									stepHeightClear = false
-									break
-								}
-							}
-						}
-
-						if (stepHeightClear) {
-							val velocity = if (previousVelocity.length > body.linearVelocity.length) previousVelocity else body.linearVelocity
-							body.setTransform(body.position + Vector2d(x = 0.05, y = stepSize), body.angle)
-							body.linearVelocity = velocity
-							//body.linearVelocity += Vector2d(y = -Starbound.SECONDS_IN_FRAME * 18.0 * stepSize * world.gravity.y)
-						}
-					}
-				}
-			}
-
-			previousVelocity = body.linearVelocity
-
-			if (jumpRequested) {
-				jumpRequested = false
-				nextJump = world.ticks + 15
-
-				body.linearVelocity += Vector2d(y = jumpForce)
-			}
-		} else if (!onGround && !isDucked && freeFallMoveSpeed != 0.0) {
-			when (moveDirection) {
-				Move.STAND_STILL -> {
-					// do nothing
-				}
-
-				Move.MOVE_LEFT -> {
-					body.linearVelocity += Vector2d(x = -freeFallMoveSpeed * Starbound.TICK_TIME_ADVANCE)
-				}
-
-				Move.MOVE_RIGHT -> {
-					body.linearVelocity += Vector2d(x = freeFallMoveSpeed * Starbound.TICK_TIME_ADVANCE)
-				}
-			}
-		} else if (onGround && isDucked) {
-			body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce)
-		}
-
-		if (wantsToDuck && onGround) {
-			isDucked = true
-			recreateBodyFixture()
-			recreateSensors()
-			body.isAwake = true
-		} else if (isDucked) {
-			if (canUnDuck()) {
-				isDucked = false
-				recreateBodyFixture()
-				recreateSensors()
-				body.isAwake = true
-			}
-		}
-	}
-}
-
-abstract class AliveEntity(world: World<*, *>) : Entity(world) {
-	open var maxHealth = 10.0
-	open var health = 10.0
-}
-
-abstract class AliveWalkingEntity(world: World<*, *>) : AliveEntity(world) {
-	abstract override val movement: WalkableMovementController<*>
-
-	/**
-	 * Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда
-	 *
-	 * Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе.
-	 *
-	 * Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения,
-	 * то сущность быстро тормозит (учитывая силу трения)
-	 */
-	open val topSpeed: Double = 20.0
-
-	/**
-	 * Скорость ускорения сущности в Starbound Units/секунда^2
-	 *
-	 * Если сущность хочет двигаться вправо или влево,
-	 * то она разгоняется с данной скоростью.
-	 */
-	open val moveSpeed: Double = 64.0
-
-	/**
-	 * То, как сущность может влиять на свою скорость в Starbound Units/секунда^2
-	 * когда находится в свободном падении или в состоянии невесомости.
-	 *
-	 * Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake.
-	 */
-	open val freeFallMoveSpeed: Double = 8.0
-
-	/**
-	 * "Сила", с которой сущность останавливается, если не хочет двигаться.
-	 *
-	 * Зависит от текущего трения, так как технически является множителем трения поверхности,
-	 * на которой стоит сущность.
-	 */
-	open val brakeForce: Double = 16.0
-
-	/**
-	 * Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков
-	 * сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть.
-	 */
-	open val jumpForce: Double = 20.0
-
-	/**
-	 * Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой
-	 * полезной нагрузки.
-	 */
-	open val stepSize: Double = 1.1
-
-	open var wantsToDuck
-		get() = movement.wantsToDuck
-		set(value) { movement.wantsToDuck = value }
-
-	open val isDucked get() = movement.isDucked
-}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt
index c657a947..85ee373e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt
@@ -1,7 +1,9 @@
 package ru.dbotthepony.kstarbound.world.entities
 
+import ru.dbotthepony.kstarbound.client.StarboundClient
 import ru.dbotthepony.kstarbound.world.Chunk
 import ru.dbotthepony.kstarbound.world.World
+import ru.dbotthepony.kstarbound.world.physics.Poly
 import ru.dbotthepony.kvector.vector.Vector2d
 
 abstract class Entity(val world: World<*, *>) {
@@ -56,8 +58,6 @@ abstract class Entity(val world: World<*, *>) {
 			val old = field
 			field = Vector2d(world.x.cell(value.x), world.y.cell(value.y))
 
-			movement.notifyPositionChanged()
-
 			if (isSpawned && !isRemoved) {
 				val oldChunkPos = world.chunkFromCell(old)
 				val newChunkPos = world.chunkFromCell(field)
@@ -85,9 +85,10 @@ abstract class Entity(val world: World<*, *>) {
 				return
 
 			field = value
-			movement.notifyPositionChanged()
 		}
 
+	var velocity = Vector2d.ZERO
+	val hitboxes = ArrayList<Poly>()
 
 	/**
 	 * Whenever is this entity spawned in world ([spawn] called).
@@ -113,8 +114,6 @@ abstract class Entity(val world: World<*, *>) {
 		if (chunk == null) {
 			world.orphanedEntities.add(this)
 		}
-
-		movement.onSpawnedInWorld()
 	}
 
 	open fun remove() {
@@ -127,24 +126,8 @@ abstract class Entity(val world: World<*, *>) {
 			world.entities.remove(this)
 			chunk?.removeEntity(this)
 		}
-
-		movement.destroy()
 	}
 
-	/**
-	 * This entity's movement controller. Even logical entities have one, but they have
-	 * dummy movement controller, which does nothing.
-	 *
-	 * If entity is physical, this controller handle interaction with Box2D world, update angles and
-	 * position and other stuff.
-	 */
-	abstract val movement: MovementController<*>
-
-	/**
-	 * Внутренний блок "раздумья" сущности, вызывается на каждом тике мира
-	 */
-	protected abstract fun thinkAI()
-
 	/**
 	 * Заставляет сущность "думать".
 	 */
@@ -153,11 +136,57 @@ abstract class Entity(val world: World<*, *>) {
 			throw IllegalStateException("Tried to think before spawning in world")
 		}
 
-		movement.think()
-		thinkAI()
+		move()
+		thinkInner()
+	}
+
+	protected abstract fun thinkInner()
+
+	protected open fun move() {
+		position += velocity
+
+		val polies = ArrayList<Poly>()
+
+		for (x in -1 .. 1) {
+			for (y in -1 .. 1) {
+				world.chunkMap.getCell(position.x.toInt() + x, position.y.toInt() + y)?.let {
+					polies.addAll(it.polies)
+				}
+			}
+		}
+
+		for (i in 0 until 10) {
+			val intersects = ArrayList<Poly.Penetration>()
+
+			this.hitboxes.forEach { hitbox0 ->
+				val hitbox = hitbox0 + position
+				polies.forEach { poly -> hitbox.intersect(poly)?.let { intersects.add(it) } }
+			}
+
+			if (intersects.isEmpty())
+				break
+			else {
+				val max = intersects.max()
+				// resolve collision
+				position += max.vector
+				// collision response
+				velocity -= max.axis * velocity.dot(max.axis * 1.1)
+			}
+		}
 	}
 
 	open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) {
 
 	}
+
+	open fun render(client: StarboundClient = StarboundClient.current()) {
+		hitboxes.forEach { (it + position).render(client) }
+	}
+
+	open var maxHealth = 0.0
+	open var health = 0.0
+
+	open fun hurt(amount: Double): Boolean {
+		return false
+	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt
index 0e6e3b68..3c0a7b39 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt
@@ -5,49 +5,19 @@ import ru.dbotthepony.kbox2d.api.FixtureDef
 import ru.dbotthepony.kbox2d.api.Manifold
 import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
 import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
+import ru.dbotthepony.kstarbound.client.StarboundClient
 import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
 import ru.dbotthepony.kstarbound.world.World
+import ru.dbotthepony.kstarbound.world.physics.Poly
+import ru.dbotthepony.kvector.util2d.AABB
+import ru.dbotthepony.kvector.vector.Vector2d
 
 class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) {
-	override val movement = object : MovementController<ItemEntity>(this) {
-		override fun beginContact(contact: AbstractContact) {
-			// тут надо код подбора предмета игроком, если мы начинаем коллизию с окружностью подбора
-		}
-
-		override fun endContact(contact: AbstractContact) {
-
-		}
-
-		override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) {
-
-		}
-
-		override fun preSolve(contact: AbstractContact, oldManifold: Manifold) {
-			if (contact.fixtureA.userData is ItemEntity && contact.fixtureB.userData is ItemEntity)
-				contact.isEnabled = false
-		}
-
-		override fun onSpawnedInWorld() {
-			super.onSpawnedInWorld()
-
-			// все предметы в мире являются коробками
-			val fixture = FixtureDef(
-				shape = PolygonShape().also { it.setAsBox(0.75, 0.75) },
-				restitution = 0.0,
-				friction = 1.0,
-				density = 0.3,
-			)
-
-			fixture.userData = this@ItemEntity
-
-			body.createFixture(fixture)
-
-			// предметы не могут поворачиваться в мире, всегда падают плашмя
-			body.isFixedRotation = true
-		}
+	init {
+		hitboxes.add(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75)))
 	}
 
-	override fun thinkAI() {
+	override fun thinkInner() {
 		// TODO: деспавнинг?
 		// просто, как бы, предметы не должны уж так сильно нагружать процессор
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt
deleted file mode 100644
index 2c27d78f..00000000
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-package ru.dbotthepony.kstarbound.world.entities
-
-import ru.dbotthepony.kbox2d.api.*
-import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
-import ru.dbotthepony.kvector.util2d.AABB
-import ru.dbotthepony.kvector.vector.Vector2d
-
-enum class CollisionResolution {
-	STOP,
-	BOUNCE,
-	PUSH,
-	SLIDE,
-}
-
-abstract class MovementController<T : Entity>(val entity: T) : IContactListener {
-	val world = entity.world
-	open var position by entity::position
-	open var angle by entity::angle
-
-	/**
-	 * Уничтожает данный movement controller.
-	 *
-	 * Вызывается изнутри [Entity.remove]
-	 */
-	open fun destroy() {
-		body.world.destroyBody(body)
-	}
-
-	private val bodyInit = lazy {
-		world.physics.createBody(BodyDef(
-			position = position,
-			angle = angle,
-			type = BodyType.DYNAMIC,
-			userData = this
-		))
-	}
-
-	/**
-	 * Было ли создано физическое тело
-	 */
-	val bodyInitialized: Boolean
-		get() = bodyInit.isInitialized()
-
-	/**
-	 * Физическое тело данного контроллера перемещения
-	 *
-	 * Не создаётся в мире пока к нему не обратятся
-	 */
-	protected val body by bodyInit
-
-	/**
-	 * Вызывается изнутри [Entity], когда оно спавнится в самом мире.
-	 *
-	 * Так как никто не запрещает нам создавать физические тела в физическом мире
-	 * до того, как мы появимся в самом мире, это негативно сказывается на производительности И не является корректным
-	 * поведением.
-	 *
-	 * Поэтому, прикреплять фигуры к физическому телу лучше всего из данной функции.
-	 */
-	open fun onSpawnedInWorld() {
-
-	}
-
-	open val velocity get() = body.linearVelocity
-
-	open fun applyVelocity(velocity: Vector2d) {
-		if (velocity != Vector2d.ZERO) {
-			body.applyLinearImpulseToCenter(velocity)
-		}
-	}
-
-	/**
-	 * Проверяет, находится ли что-либо под нами
-	 */
-	open val onGround: Boolean get() {
-		for (contact in body.contactEdgeIterator) {
-			if (contact.contact.manifold.localNormal.dot(world.gravity.unitVector) >= 0.97) {
-				return true
-			}
-		}
-
-		return false
-	}
-
-	/**
-	 * World space AABB, by default returning combined AABB of physical body.
-	 */
-	open val worldAABB: AABB get() {
-		return body.worldSpaceAABB
-	}
-
-	/**
-	 * This is called on each world step to update variables and account of changes of
-	 * physics world and this physics body.
-	 */
-	open fun think() {
-		mutePositionChanged = true
-		position = body.position
-		angle = body.angle
-		mutePositionChanged = false
-	}
-
-	protected open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) {
-		entity.onTouchSurface(velocity, normal)
-	}
-
-	protected var mutePositionChanged = false
-
-	open fun notifyPositionChanged() {
-		if (mutePositionChanged) {
-			return
-		}
-
-		body.setTransform(entity.position, entity.angle)
-	}
-}
-
-/**
- * Movement controller for logical entities, which does nothing.
- */
-class LogicalMovementController(entity: Entity) : MovementController<Entity>(entity) {
-	override val worldAABB: AABB get() = AABB(position, position)
-
-	// Dummies never touch anything, since they don't have Box2D body
-	override val onGround: Boolean = false
-	override val velocity: Vector2d = Vector2d.ZERO
-
-	override fun think() {
-		// no-op
-	}
-
-	override fun notifyPositionChanged() {
-		// no-op
-	}
-
-	override fun beginContact(contact: AbstractContact) {
-		// no-op
-	}
-
-	override fun endContact(contact: AbstractContact) {
-		// no-op
-	}
-
-	override fun preSolve(contact: AbstractContact, oldManifold: Manifold) {
-		// no-op
-	}
-
-	override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) {
-		// no-op
-	}
-
-	companion object {
-		private val DUMMY_AABB = AABB.rectangle(Vector2d.ZERO, 0.1, 0.1)
-	}
-}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt
deleted file mode 100644
index b8843707..00000000
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package ru.dbotthepony.kstarbound.world.entities
-
-import ru.dbotthepony.kbox2d.api.ContactImpulse
-import ru.dbotthepony.kbox2d.api.FixtureDef
-import ru.dbotthepony.kbox2d.api.Manifold
-import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
-import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
-import ru.dbotthepony.kstarbound.world.World
-import ru.dbotthepony.kvector.vector.Vector2d
-
-class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController<PlayerEntity>(entity) {
-	public override var moveDirection = Move.STAND_STILL
-
-	override fun recreateBodyFixture() {
-		bodyFixture?.destroy()
-
-		if (isDucked) {
-			bodyFixture = body.createFixture(FixtureDef(
-				shape = DUCKING,
-				friction = 0.4,
-				density = 1.9,
-			))
-		} else {
-			bodyFixture = body.createFixture(FixtureDef(
-				shape = STANDING,
-				friction = 0.4,
-				density = 1.9,
-			))
-		}
-	}
-
-	init {
-		recreateBodyFixture()
-		recreateSensors()
-	}
-
-	override fun beginContact(contact: AbstractContact) {
-
-	}
-
-	override fun endContact(contact: AbstractContact) {
-
-	}
-
-	override fun preSolve(contact: AbstractContact, oldManifold: Manifold) {
-
-	}
-
-	override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) {
-
-	}
-
-	override fun canUnDuck(): Boolean {
-		return !world.testSpace(STANDING_AABB + position)
-	}
-
-	companion object {
-		private val STANDING = PolygonShape().also { it.setAsBox(0.9, 1.8) }
-		private val STANDING_AABB = STANDING.computeAABB(0)
-		private val DUCKING = PolygonShape().also { it.setAsBox(0.9, 0.9, Vector2d(y = -0.9), 0.0) }
-		private val DUCKING_AABB = DUCKING.computeAABB(0)
-	}
-}
-
-/**
- * Физический аватар игрока в мире
- */
-open class PlayerEntity(world: World<*, *>) : AliveWalkingEntity(world) {
-	override val movement = PlayerMovementController(this)
-
-	override fun thinkAI() {
-
-	}
-}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt
new file mode 100644
index 00000000..2cd821c2
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt
@@ -0,0 +1,236 @@
+package ru.dbotthepony.kstarbound.world.physics
+
+import com.google.common.collect.ImmutableList
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
+import org.lwjgl.opengl.GL11.GL_LINES
+import ru.dbotthepony.kstarbound.client.StarboundClient
+import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
+import ru.dbotthepony.kvector.api.IStruct2d
+import ru.dbotthepony.kvector.arrays.Matrix3f
+import ru.dbotthepony.kvector.util2d.AABB
+import ru.dbotthepony.kvector.util2d.intersectSegments
+import ru.dbotthepony.kvector.vector.RGBAColor
+import ru.dbotthepony.kvector.vector.Vector2d
+import kotlin.math.absoluteValue
+
+private fun calculateEdges(points: List<Vector2d>): ImmutableList<Poly.Edge> {
+	require(points.size >= 3) { "Provided poly is invalid (only ${points.size} points are defined)" }
+
+	val edges = ImmutableList.Builder<Poly.Edge>()
+
+	for (i in points.indices) {
+		val p0 = points[i]
+		val p1 = points[(i + 1) % points.size]
+
+		val diff = (p1 - p0).unitVector
+		val normal = Vector2d(-diff.y, diff.x)
+
+		edges.add(Poly.Edge(p0, p1, normal))
+	}
+
+	return edges.build()
+}
+
+/**
+ * edges are built in clockwise winding
+ *
+ * If poly shape is not convex behavior of SAT is undefined
+ */
+class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: ImmutableList<Vector2d>) {
+	constructor(points: List<Vector2d>) : this(calculateEdges(points), ImmutableList.copyOf(points))
+	constructor(aabb: AABB) : this(listOf(aabb.bottomLeft, aabb.topLeft, aabb.topRight, aabb.bottomRight))
+
+	val aabb = AABB(
+		Vector2d(vertices.minOf { it.x }, vertices.minOf { it.y }),
+		Vector2d(vertices.maxOf { it.x }, vertices.maxOf { it.y }),
+	)
+
+	data class Edge(val p0: Vector2d, val p1: Vector2d, val normal: Vector2d) {
+		operator fun plus(other: IStruct2d): Edge {
+			return Edge(p0 + other, p1 + other, normal)
+		}
+
+		operator fun minus(other: IStruct2d): Edge {
+			return Edge(p0 - other, p1 - other, normal)
+		}
+
+		operator fun times(other: IStruct2d): Edge {
+			return Edge(p0 * other, p1 * other, (normal * other).unitVector)
+		}
+
+		operator fun times(other: Double): Edge {
+			return Edge(p0 * other, p1 * other, (normal * other).unitVector)
+		}
+	}
+
+	data class Penetration(val axis: Vector2d, val penetration: Double) : Comparable<Penetration> {
+		val vector = axis * penetration
+
+		override fun compareTo(other: Penetration): Int {
+			return penetration.absoluteValue.compareTo(other.penetration.absoluteValue)
+		}
+	}
+
+	operator fun plus(value: Penetration): Poly {
+		val vertices = ImmutableList.Builder<Vector2d>()
+		val edges = ImmutableList.Builder<Edge>()
+
+		for (v in this.vertices) vertices.add(v + value.vector)
+		for (v in this.edges) edges.add(v + value.vector)
+
+		return Poly(edges.build(), vertices.build())
+	}
+
+	operator fun plus(value: IStruct2d): Poly {
+		val vertices = ImmutableList.Builder<Vector2d>()
+		val edges = ImmutableList.Builder<Edge>()
+
+		for (v in this.vertices) vertices.add(v + value)
+		for (v in this.edges) edges.add(v + value)
+
+		return Poly(edges.build(), vertices.build())
+	}
+
+	operator fun minus(value: IStruct2d): Poly {
+		val vertices = ImmutableList.Builder<Vector2d>()
+		val edges = ImmutableList.Builder<Edge>()
+
+		for (v in this.vertices) vertices.add(v - value)
+		for (v in this.edges) edges.add(v - value)
+
+		return Poly(edges.build(), vertices.build())
+	}
+
+	operator fun times(value: IStruct2d): Poly {
+		val vertices = ImmutableList.Builder<Vector2d>()
+		val edges = ImmutableList.Builder<Edge>()
+
+		for (v in this.vertices) vertices.add(v * value)
+		for (v in this.edges) edges.add(v * value)
+
+		return Poly(edges.build(), vertices.build())
+	}
+
+	operator fun times(value: Double): Poly {
+		val vertices = ImmutableList.Builder<Vector2d>()
+		val edges = ImmutableList.Builder<Edge>()
+
+		for (v in this.vertices) vertices.add(v * value)
+		for (v in this.edges) edges.add(v * value)
+
+		return Poly(edges.build(), vertices.build())
+	}
+
+	// min / max
+	fun project(normal: Vector2d): IStruct2d {
+		var min = vertices.first().dot(normal)
+		var max = min
+
+		for (vertex in vertices) {
+			val dist = vertex.dot(normal)
+
+			min = min.coerceAtMost(dist)
+			max = max.coerceAtLeast(dist)
+		}
+
+		return Vector2d(min, max)
+	}
+
+	fun intersect(other: Poly): Penetration? {
+		if (!aabb.intersectWeak(other.aabb))
+			return null
+
+		val normals = ObjectOpenHashSet<Vector2d>()
+		edges.forEach { normals.add(it.normal) }
+		other.edges.forEach { normals.add(it.normal) }
+
+		val intersections = ArrayList<Penetration>()
+
+		for (normal in normals) {
+			val projectThis = project(normal)
+			val projectOther = other.project(normal)
+
+			if (!intersectSegments(projectThis.component1(), projectThis.component2(), projectOther.component1(), projectOther.component2())) {
+				return null
+			} else {
+				val width = projectThis.component2() - projectThis.component1()
+
+				if (
+					projectOther.component1() in projectThis.component1() .. projectThis.component2() &&
+					projectOther.component2() in projectThis.component1() .. projectThis.component2()
+				) {
+					// other inside this
+					val minMin = projectThis.component1() - projectOther.component1()
+					val maxMax = projectThis.component2() - projectOther.component2()
+
+					if (minMin <= maxMax) {
+						// push to left
+						intersections.add(Penetration(normal, -minMin - width))
+					} else {
+						// push to right
+						intersections.add(Penetration(normal, maxMax + width))
+					}
+				} else if (
+					projectThis.component1() in projectOther.component1() .. projectOther.component2() &&
+					projectThis.component2() in projectOther.component1() .. projectOther.component2()
+				) {
+					// this inside other
+					val minMin = projectThis.component1() - projectOther.component1()
+					val maxMax = projectOther.component2() - projectThis.component2()
+
+					if (minMin <= maxMax) {
+						// push to left
+						intersections.add(Penetration(normal, -minMin - width))
+					} else {
+						// push to right
+						intersections.add(Penetration(normal, maxMax + width))
+					}
+				} else if (projectOther.component1() in projectThis.component1() .. projectThis.component2()) {
+					// other's min point is within this
+					intersections.add(Penetration(normal, projectOther.component1() - projectThis.component2()))
+				} else {
+					// other's max point in within this
+					intersections.add(Penetration(normal, projectOther.component2() - projectThis.component1()))
+				}
+
+				if (intersections.last().penetration == 0.0) {
+					return null
+				}
+			}
+		}
+
+		if (intersections.isEmpty())
+			return null
+
+		return intersections.min()
+	}
+
+	fun render(client: StarboundClient = StarboundClient.current(), color: RGBAColor = RGBAColor.LIGHT_GREEN) {
+		val program = client.programs.position
+		val lines = program.builder.builder
+		program.use()
+		lines.begin(GeometryType.LINES)
+
+		for (edge in edges) {
+			val current = edge.p0
+			val next = edge.p1
+			lines.vertex(client.stack.last(), current)
+			lines.vertex(client.stack.last(), next)
+			lines.vertex(client.stack.last(), (next + current) / 2.0)
+			lines.vertex(client.stack.last(), (next + current) / 2.0 + edge.normal * 3.0)
+		}
+
+		program.modelMatrix = identity
+		program.colorMultiplier = color
+		program.builder.upload()
+		program.builder.draw(GL_LINES)
+	}
+
+	override fun toString(): String {
+		return "Poly[edges = $edges]"
+	}
+
+	companion object {
+		private val identity = Matrix3f.identity()
+	}
+}