mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
1058 lines
36 KiB
Kotlin
1058 lines
36 KiB
Kotlin
package net.torvald.terrarum
|
|
|
|
import com.badlogic.gdx.Gdx
|
|
import com.badlogic.gdx.Screen
|
|
import com.badlogic.gdx.graphics.Color
|
|
import com.badlogic.gdx.graphics.GL20
|
|
import com.badlogic.gdx.graphics.OrthographicCamera
|
|
import com.badlogic.gdx.graphics.Texture
|
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
|
import com.badlogic.gdx.graphics.glutils.FrameBuffer
|
|
import com.badlogic.gdx.graphics.glutils.ShapeRenderer
|
|
import com.badlogic.gdx.utils.Disposable
|
|
import com.jme3.math.FastMath
|
|
import net.torvald.gdx.graphics.Cvec
|
|
import net.torvald.random.HQRNG
|
|
import net.torvald.terrarum.App.*
|
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
|
|
import net.torvald.terrarum.audio.AudioCodex
|
|
import net.torvald.terrarum.audio.AudioMixer
|
|
import net.torvald.terrarum.blockproperties.BlockCodex
|
|
import net.torvald.terrarum.blockproperties.FluidCodex
|
|
import net.torvald.terrarum.blockproperties.OreCodex
|
|
import net.torvald.terrarum.blockproperties.WireCodex
|
|
import net.torvald.terrarum.gameactors.Actor
|
|
import net.torvald.terrarum.gameactors.ActorID
|
|
import net.torvald.terrarum.gameactors.ActorWithBody
|
|
import net.torvald.terrarum.gameactors.ActorWithBody.Companion.METER
|
|
import net.torvald.terrarum.gameactors.faction.FactionCodex
|
|
import net.torvald.terrarum.gameworld.fmod
|
|
import net.torvald.terrarum.itemproperties.CraftingCodex
|
|
import net.torvald.terrarum.itemproperties.ItemCodex
|
|
import net.torvald.terrarum.itemproperties.MaterialCodex
|
|
import net.torvald.terrarum.savegame.DiskSkimmer
|
|
import net.torvald.terrarum.serialise.Common
|
|
import net.torvald.terrarum.ui.UICanvas
|
|
import net.torvald.terrarum.ui.UINotControllable
|
|
import net.torvald.terrarum.weather.WeatherCodex
|
|
import net.torvald.terrarum.worlddrawer.WorldCamera
|
|
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap
|
|
import net.torvald.unsafe.UnsafeHelper
|
|
import net.torvald.util.CircularArray
|
|
import org.dyn4j.geometry.Vector2
|
|
import java.io.File
|
|
import java.io.PrintStream
|
|
import kotlin.math.*
|
|
import kotlin.reflect.full.findAnnotation
|
|
|
|
|
|
typealias RGBA8888 = Int
|
|
|
|
|
|
/**
|
|
* Slick2d Version Created by minjaesong on 2015-12-30.
|
|
*
|
|
* LibGDX Version Created by minjaesong on 2017-06-15.
|
|
*
|
|
* Note: Blocks are NOT automatically added; they must be registered manually on the module's EntryPoint
|
|
*/
|
|
object Terrarum : Disposable {
|
|
|
|
init {
|
|
}
|
|
|
|
/**
|
|
* All singleplayer "Player" must have this exact reference ID.
|
|
*/
|
|
const val PLAYER_REF_ID: Int = 0x91A7E2
|
|
|
|
/*inline fun inShapeRenderer(shapeRendererType: ShapeRenderer.ShapeType = ShapeRenderer.ShapeType.Filled, action: (ShapeRenderer) -> Unit) {
|
|
shapeRender.begin(shapeRendererType)
|
|
action(shapeRender)
|
|
shapeRender.end()
|
|
}*/
|
|
|
|
var blockCodex = BlockCodex(); internal set
|
|
/** The actual contents of the ItemCodex is sum of Player's Codex and the World's Codex */
|
|
var itemCodex = ItemCodex(); internal set
|
|
var wireCodex = WireCodex(); internal set
|
|
var materialCodex = MaterialCodex(); internal set
|
|
var factionCodex = FactionCodex(); internal set
|
|
var craftingCodex = CraftingCodex(); internal set
|
|
var apocryphas = HashMap<String, Any>(); internal set
|
|
var fluidCodex = FluidCodex(); internal set
|
|
var oreCodex = OreCodex(); internal set
|
|
var audioCodex = AudioCodex(); internal set
|
|
var weatherCodex = WeatherCodex(); internal set
|
|
|
|
|
|
//////////////////////////////
|
|
// GLOBAL IMMUTABLE CONFIGS //
|
|
//////////////////////////////
|
|
|
|
/**
|
|
* To be used with physics simulator. This is a magic number.
|
|
*/
|
|
const val PHYS_TIME_FRAME: Double = METER
|
|
// 26.0 + (2.0 / 3.0) // lower value == faster gravity response (IT WON'T HOTSWAP!!)
|
|
// protip: using METER, game unit and SI unit will have same number
|
|
|
|
|
|
|
|
var previousScreen: Screen? = null // to be used with temporary states like StateMonitorCheck
|
|
|
|
|
|
/** Current ingame instance the game is holding.
|
|
*
|
|
* The ingame instance this variable is subject to change.
|
|
*
|
|
* Don't do:
|
|
* ```
|
|
* private val ingame = Terrarum.ingame
|
|
* ```
|
|
*
|
|
* Do instead:
|
|
* ```
|
|
* private val ingame: IngameInstance
|
|
* get() = Terrarum.ingame
|
|
* ```
|
|
*/
|
|
var ingame: IngameInstance? = null
|
|
private set
|
|
|
|
private val javaHeapCircularArray = CircularArray<Int>(64, true)
|
|
private val nativeHeapCircularArray = CircularArray<Int>(64, true)
|
|
private val updateRateCircularArray = CircularArray<Double>(64, true)
|
|
|
|
val memJavaHeap: Int
|
|
get() {
|
|
javaHeapCircularArray.appendHead((Gdx.app.javaHeap shr 20).toInt())
|
|
return javaHeapCircularArray.maxOrNull() ?: 0
|
|
}
|
|
/*val memNativeHeap: Int // as long as you're sticking to the LWJGL, nativeHeap just returns javaHeap
|
|
get() {
|
|
nativeHeapCircularArray.appendHead((Gdx.app.nativeHeap shr 20).toInt())
|
|
return nativeHeapCircularArray.maxOrNull() ?: 0
|
|
}*/
|
|
val memUnsafe: Int
|
|
get() = (UnsafeHelper.unsafeAllocatedSize shr 20).toInt()
|
|
val memXmx: Int
|
|
get() = (Runtime.getRuntime().maxMemory() shr 20).toInt()
|
|
val updateRateStr: String
|
|
get() {
|
|
updateRateCircularArray.appendHead(updateRate)
|
|
return String.format("%.2f", updateRateCircularArray.average())
|
|
}
|
|
|
|
lateinit var testTexture: Texture
|
|
|
|
init {
|
|
println("[Terrarum] init called by:")
|
|
printStackTrace(this)
|
|
|
|
println("[Terrarum] ${App.GAME_NAME} version ${App.getVERSION_STRING()}")
|
|
println("[Terrarum] LibGDX version ${com.badlogic.gdx.Version.VERSION}")
|
|
|
|
|
|
println("[Terrarum] os.arch = $systemArch") // debug info
|
|
|
|
if (is32BitJVM) {
|
|
printdbgerr(this, "32 Bit JVM detected")
|
|
}
|
|
|
|
|
|
println("[Terrarum] processor = $processor")
|
|
println("[Terrarum] vendor = $processorVendor")
|
|
|
|
|
|
App.disposables.add(this)
|
|
|
|
|
|
|
|
println("[Terrarum] init complete")
|
|
}
|
|
|
|
|
|
@JvmStatic fun initialise() {
|
|
// dummy init method
|
|
}
|
|
|
|
|
|
val RENDER_FPS = getConfigInt("displayfps")
|
|
val USE_VSYNC = getConfigBoolean("usevsync")
|
|
val VSYNC_TRIGGER_THRESHOLD = 56
|
|
val GL_VERSION: Int
|
|
get() = Gdx.graphics.glVersion.majorVersion * 100 +
|
|
Gdx.graphics.glVersion.minorVersion * 10 +
|
|
Gdx.graphics.glVersion.releaseVersion
|
|
/*val GL_MAX_TEXTURE_SIZE: Int
|
|
get() {
|
|
val intBuffer = BufferUtils.createIntBuffer(16) // size must be at least 16, or else LWJGL complains
|
|
Gdx.gl.glGetIntegerv(GL20.GL_MAX_TEXTURE_SIZE, intBuffer)
|
|
|
|
intBuffer.rewind()
|
|
|
|
return intBuffer.get()
|
|
}
|
|
val MINIMAL_GL_MAX_TEXTURE_SIZE = 4096*/
|
|
|
|
fun setCurrentIngameInstance(ingame: IngameInstance) {
|
|
this.ingame = ingame
|
|
|
|
printdbg("ListSavegames", "Accepting new ingame instance '${ingame.javaClass.canonicalName}', called by:")
|
|
printStackTrace(this)
|
|
}
|
|
|
|
/** Don't call this! Call AppLoader.dispose() */
|
|
override fun dispose() {
|
|
//dispose any other resources used in this level
|
|
ingame?.dispose()
|
|
}
|
|
|
|
|
|
/** For the actual resize, call AppLoader.resize() */
|
|
/*fun resize(width: Int, height: Int) {
|
|
ingame?.resize(width, height)
|
|
|
|
printdbg("ListSavegames", "newsize: ${Gdx.graphics.width}x${Gdx.graphics.height} | internal: ${width}x$height")
|
|
}*/
|
|
|
|
|
|
|
|
val currentSaveDir: File
|
|
get() {
|
|
val file = File(saveDir + "/test")
|
|
|
|
// failsafe?
|
|
if (!file.exists()) file.mkdir()
|
|
|
|
return file // TODO TEST CODE
|
|
}
|
|
|
|
/** Position of the cursor in the world, rounded */
|
|
val mouseX: Double
|
|
get() = (WorldCamera.zoomedX + Gdx.input.x / (ingame?.screenZoom ?: 1f).times(scr.magn.toDouble())).fmod(WorldCamera.worldWidth.toDouble())
|
|
/** Position of the cursor in the world */
|
|
val mouseY: Double
|
|
get() = (WorldCamera.zoomedY + Gdx.input.y / (ingame?.screenZoom ?: 1f).times(scr.magn.toDouble()))
|
|
/** Position of the cursor in the world, rounded */
|
|
val oldMouseX: Double
|
|
get() = (WorldCamera.zoomedX + (Gdx.input.x - Gdx.input.deltaX) / (ingame?.screenZoom ?: 1f).times(scr.magn.toDouble())).fmod(WorldCamera.worldWidth.toDouble())
|
|
/** Position of the cursor in the world */
|
|
val oldMouseY: Double
|
|
get() = WorldCamera.zoomedY + (Gdx.input.y - Gdx.input.deltaY) / (ingame?.screenZoom ?: 1f).times(scr.magn.toDouble())
|
|
/** Position of the cursor in the world, rounded */
|
|
@JvmStatic val mouseTileX: Int
|
|
get() = (mouseX / TILE_SIZE).floorToInt()
|
|
/** Position of the cursor in the world */
|
|
@JvmStatic val mouseTileY: Int
|
|
get() = (mouseY / TILE_SIZE).floorToInt()
|
|
/** Position of the cursor in the world, rounded */
|
|
@JvmStatic val oldMouseTileX: Int
|
|
get() = (oldMouseX / TILE_SIZE).floorToInt()
|
|
/** Position of the cursor in the world */
|
|
@JvmStatic val oldMouseTileY: Int
|
|
get() = (oldMouseY / TILE_SIZE).floorToInt()
|
|
inline val mouseScreenX: Int
|
|
get() = Gdx.input.x.div(scr.magn).roundToInt()
|
|
inline val mouseScreenY: Int
|
|
get() = Gdx.input.y.div(scr.magn).roundToInt()
|
|
inline val mouseDeltaX: Int
|
|
get() = Gdx.input.deltaX.div(scr.magn).roundToInt()
|
|
inline val mouseDeltaY: Int
|
|
get() = Gdx.input.deltaY.div(scr.magn).roundToInt()
|
|
/** Delta converted as it it was a FPS */
|
|
inline val updateRate: Double
|
|
get() = 1.0 / Gdx.graphics.deltaTime
|
|
val mouseDown: Boolean
|
|
get() = Gdx.input.isButtonPressed(App.getConfigInt("config_mouseprimary"))
|
|
val mouseJustDown: Boolean
|
|
get() = Gdx.input.isButtonJustPressed(App.getConfigInt("config_mouseprimary"))
|
|
|
|
|
|
/**
|
|
* Subtile and Vector indices:
|
|
* ```
|
|
* 4px 8px
|
|
* +-+-----+-+
|
|
* | | 4 | |
|
|
* +-+-----+-+
|
|
* |3| 0 |1|
|
|
* +-+-----+-+
|
|
* | | 2 | |
|
|
* +-+-----+-+
|
|
* ```
|
|
*/
|
|
|
|
enum class SubtileVector {
|
|
INVALID, CENTRE, LEFT, BOTTOM, RIGHT, TOP
|
|
}
|
|
|
|
fun SubtileVector.toInt() = when (this) {
|
|
SubtileVector.INVALID -> throw IllegalArgumentException()
|
|
SubtileVector.RIGHT -> 1
|
|
SubtileVector.BOTTOM -> 2
|
|
SubtileVector.LEFT -> 4
|
|
SubtileVector.TOP -> 8
|
|
SubtileVector.CENTRE -> 0
|
|
}
|
|
|
|
data class MouseSubtile4(val x: Int, val y: Int, val vector: SubtileVector) {
|
|
|
|
val nx = when (vector) {
|
|
SubtileVector.CENTRE -> x
|
|
SubtileVector.RIGHT -> x + 1
|
|
SubtileVector.BOTTOM -> x
|
|
SubtileVector.LEFT -> x - 1
|
|
SubtileVector.TOP -> x
|
|
else -> x
|
|
}
|
|
|
|
val ny = when (vector) {
|
|
SubtileVector.CENTRE -> y
|
|
SubtileVector.RIGHT -> y
|
|
SubtileVector.BOTTOM -> y + 1
|
|
SubtileVector.LEFT -> y
|
|
SubtileVector.TOP -> y - 1
|
|
else -> y
|
|
}
|
|
|
|
val currentTileCoord = x to y
|
|
val nextTileCoord = nx to ny
|
|
}
|
|
|
|
fun getMouseSubtile4(): MouseSubtile4 {
|
|
val SMALLGAP = 4.0
|
|
val LARGEGAP = 8.0 // 2*SMALLGAP + LARGEGAP must be equal to TILE_SIZE
|
|
assert(2 * SMALLGAP + LARGEGAP == TILE_SIZED)
|
|
|
|
val mx = mouseX
|
|
val my = mouseY
|
|
val mtx = (mouseX / TILE_SIZE).floorToInt()
|
|
val mty = (mouseY / TILE_SIZE).floorToInt()
|
|
val msx = mx fmod TILE_SIZED
|
|
val msy = my fmod TILE_SIZED
|
|
val vector = if (msx < SMALLGAP) { // X to the left
|
|
if (msy < SMALLGAP) SubtileVector.INVALID // TOP LEFT
|
|
else if (msy < SMALLGAP + LARGEGAP) SubtileVector.LEFT // LEFT
|
|
else SubtileVector.INVALID // BOTTOM LEFT
|
|
}
|
|
else if (msx < SMALLGAP + LARGEGAP) { // X to the centre
|
|
if (msy < SMALLGAP) SubtileVector.TOP // TOP
|
|
else if (msy < SMALLGAP + LARGEGAP) SubtileVector.CENTRE // CENTRE
|
|
else SubtileVector.BOTTOM // BOTTOM
|
|
}
|
|
else { // X to the right
|
|
if (msy < SMALLGAP) SubtileVector.INVALID // TOP RIGHT
|
|
else if (msy < SMALLGAP + LARGEGAP) SubtileVector.RIGHT // RIGHT
|
|
else SubtileVector.INVALID // BOTTOM RIGHT
|
|
}
|
|
|
|
return MouseSubtile4(mtx, mty, vector)
|
|
}
|
|
|
|
/**
|
|
* Usage:
|
|
*
|
|
* override var referenceID: Int = generateUniqueReferenceID()
|
|
*/
|
|
fun generateUniqueReferenceID(): ActorID {
|
|
// render orders can be changed arbitrarily so the whole "renderorder to actor id" is only there for an initial sorting
|
|
fun hasCollision(value: ActorID) =
|
|
try {
|
|
Terrarum.ingame?.theGameHasActor(value) == true
|
|
}
|
|
catch (gameNotInitialisedException: KotlinNullPointerException) {
|
|
false
|
|
}
|
|
|
|
var ret: Int
|
|
do {
|
|
val range = ReferencingRanges.ACTORS
|
|
val size = range.last - range.first + 1
|
|
ret = (HQRNG().nextInt().rem(size) + range.first) and 0x7FFF_FF00 // make room for sub-actors
|
|
} while (hasCollision(ret)) // check for collision
|
|
return ret
|
|
}
|
|
|
|
|
|
|
|
fun getWorldSaveFiledesc(filename: String) = File(App.worldsDir, filename)
|
|
fun getPlayerSaveFiledesc(filename: String) = File(App.playersDir, filename)
|
|
fun getSharedSaveFiledesc(filename: String) = File(App.saveSharedDir, filename)
|
|
|
|
}
|
|
|
|
inline fun SpriteBatch.inUse(action: (SpriteBatch) -> Unit) {
|
|
this.begin()
|
|
action(this)
|
|
this.end()
|
|
}
|
|
|
|
inline fun ShapeRenderer.inUse(shapeRendererType: ShapeRenderer.ShapeType = ShapeRenderer.ShapeType.Filled, action: (ShapeRenderer) -> Unit) {
|
|
this.begin(shapeRendererType)
|
|
action(this)
|
|
this.end()
|
|
}
|
|
|
|
/** Use Batch inside of it! */
|
|
inline fun FrameBuffer.inAction(camera: OrthographicCamera?, batch: SpriteBatch?, action: (FrameBuffer) -> Unit) {
|
|
//this.begin()
|
|
FrameBufferManager.begin(this)
|
|
|
|
val oldCamPos = camera?.position?.cpy()
|
|
|
|
camera?.setToOrtho(true, this.width.toFloat(), this.height.toFloat())
|
|
camera?.position?.set((this.width / 2f).roundToFloat(), (this.height / 2f).roundToFloat(), 0f) // TODO floor? ceil? round?
|
|
camera?.update()
|
|
batch?.projectionMatrix = camera?.combined
|
|
|
|
action(this)
|
|
|
|
//this.end()
|
|
FrameBufferManager.end()
|
|
|
|
camera?.setToOrtho(true, App.scr.wf, App.scr.hf)
|
|
camera?.position?.set(oldCamPos)
|
|
camera?.update()
|
|
batch?.projectionMatrix = camera?.combined
|
|
}
|
|
|
|
/**
|
|
* Vertically flipped version of [FrameBuffer.inAction]
|
|
*/
|
|
inline fun FrameBuffer.inActionF(camera: OrthographicCamera?, batch: SpriteBatch?, action: (FrameBuffer) -> Unit) {
|
|
//this.begin()
|
|
FrameBufferManager.begin(this)
|
|
|
|
val oldCamPos = camera?.position?.cpy()
|
|
|
|
camera?.setToOrtho(false, this.width.toFloat(), this.height.toFloat())
|
|
camera?.position?.set((this.width / 2f).roundToFloat(), (this.height / 2f).roundToFloat(), 0f) // TODO floor? ceil? round?
|
|
camera?.update()
|
|
batch?.projectionMatrix = camera?.combined
|
|
|
|
action(this)
|
|
|
|
//this.end()
|
|
FrameBufferManager.end()
|
|
|
|
camera?.setToOrtho(true, App.scr.wf, App.scr.hf)
|
|
camera?.position?.set(oldCamPos)
|
|
camera?.update()
|
|
batch?.projectionMatrix = camera?.combined
|
|
}
|
|
|
|
|
|
private val rgbMultLUT = Array(256) { y -> IntArray(256) { x ->
|
|
val i = x / 255f
|
|
val j = y / 255f
|
|
(i * j).times(255f).roundToInt()
|
|
} }
|
|
|
|
infix fun Int.rgbamul(other: Int): Int {
|
|
val r = rgbMultLUT[this.ushr(24).and(255)][other.ushr(24).and(255)]
|
|
val g = rgbMultLUT[this.ushr(16).and(255)][other.ushr(16).and(255)]
|
|
val b = rgbMultLUT[this.ushr(8).and(255)][other.ushr(8).and(255)]
|
|
val a = rgbMultLUT[this.ushr(0).and(255)][other.ushr(0).and(255)]
|
|
return r.shl(24) or g.shl(16) or b.shl(8) or a
|
|
}
|
|
|
|
infix fun Color.mul(other: Color): Color = this.cpy().mul(other)
|
|
infix fun Color.mulAndAssign(other: Color): Color {
|
|
this.r *= other.r
|
|
this.g *= other.g
|
|
this.b *= other.b
|
|
this.a *= other.a
|
|
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Use demultiplier shader on GL Source (foreground) if source has semitransparency
|
|
*/
|
|
fun blendMul(batch: SpriteBatch) {
|
|
batch.enableBlending()
|
|
batch.setBlendFunction(GL20.GL_DST_COLOR, GL20.GL_ONE_MINUS_SRC_ALPHA)
|
|
}
|
|
|
|
fun blendAlphaMask(batch: SpriteBatch) {
|
|
batch.enableBlending()
|
|
// batch.setBlendFunction(GL20.GL_ZERO, GL20.GL_SRC_ALPHA)
|
|
batch.setBlendFunction(GL20.GL_ZERO, GL20.GL_SRC_COLOR)
|
|
}
|
|
|
|
/**
|
|
* Use demultiplier shader on GL Source (foreground) if source has semitransparency
|
|
*/
|
|
fun blendScreen(batch: SpriteBatch) {
|
|
// will break if the colour image contains semitransparency
|
|
batch.enableBlending()
|
|
batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_COLOR)
|
|
}
|
|
|
|
fun blendDisable(batch: SpriteBatch) {
|
|
batch.disableBlending()
|
|
}
|
|
|
|
fun blendNormalStraightAlpha(batch: SpriteBatch) {
|
|
batch.enableBlending()
|
|
batch.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA)
|
|
// helpful links:
|
|
// - https://stackoverflow.com/questions/19674740/opengl-es2-premultiplied-vs-straight-alpha-blending#37869033
|
|
// - https://gamedev.stackexchange.com/questions/82741/normal-blend-mode-with-opengl-trouble
|
|
// - https://www.andersriggelsen.dk/glblendfunc.php
|
|
// - https://stackoverflow.com/questions/45781683/how-to-get-correct-sourceover-alpha-compositing-in-sdl-with-opengl
|
|
}
|
|
fun blendNormalPremultAlpha(batch: SpriteBatch) {
|
|
batch.enableBlending()
|
|
batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA)
|
|
// helpful links:
|
|
// - https://stackoverflow.com/questions/19674740/opengl-es2-premultiplied-vs-straight-alpha-blending#37869033
|
|
// - https://gamedev.stackexchange.com/questions/82741/normal-blend-mode-with-opengl-trouble
|
|
// - https://www.andersriggelsen.dk/glblendfunc.php
|
|
// - https://stackoverflow.com/questions/45781683/how-to-get-correct-sourceover-alpha-compositing-in-sdl-with-opengl
|
|
}
|
|
|
|
fun gdxClearAndEnableBlend(color: Color) {
|
|
gdxClearAndEnableBlend(color.r, color.g, color.b, color.a)
|
|
}
|
|
|
|
fun gdxClearAndEnableBlend(r: Float, g: Float, b: Float, a: Float) {
|
|
Gdx.gl.glClearColor(r,g,b,a)
|
|
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
|
|
gdxEnableBlend()
|
|
}
|
|
|
|
fun gdxEnableBlend() {
|
|
Gdx.gl.glEnable(GL20.GL_TEXTURE_2D)
|
|
Gdx.gl.glEnable(GL20.GL_BLEND)
|
|
}
|
|
|
|
fun gdxBlendNormalStraightAlpha() {
|
|
gdxEnableBlend()
|
|
Gdx.gl.glBlendFuncSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA)
|
|
// helpful links:
|
|
// - https://stackoverflow.com/questions/19674740/opengl-es2-premultiplied-vs-straight-alpha-blending#37869033
|
|
// - https://gamedev.stackexchange.com/questions/82741/normal-blend-mode-with-opengl-trouble
|
|
// - https://www.andersriggelsen.dk/glblendfunc.php
|
|
// - https://stackoverflow.com/questions/45781683/how-to-get-correct-sourceover-alpha-compositing-in-sdl-with-opengl
|
|
}
|
|
|
|
fun gdxBlendNormalPremultAlpha() {
|
|
gdxEnableBlend()
|
|
Gdx.gl.glBlendFunc(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA)
|
|
// helpful links:
|
|
// - https://stackoverflow.com/questions/19674740/opengl-es2-premultiplied-vs-straight-alpha-blending#37869033
|
|
// - https://gamedev.stackexchange.com/questions/82741/normal-blend-mode-with-opengl-trouble
|
|
// - https://www.andersriggelsen.dk/glblendfunc.php
|
|
// - https://stackoverflow.com/questions/45781683/how-to-get-correct-sourceover-alpha-compositing-in-sdl-with-opengl
|
|
}
|
|
|
|
fun gdxBlendMul() {
|
|
gdxEnableBlend()
|
|
Gdx.gl.glBlendFunc(GL20.GL_DST_COLOR, GL20.GL_ONE_MINUS_SRC_ALPHA)
|
|
}
|
|
|
|
object BlendMode {
|
|
const val SCREEN = "screen"
|
|
const val MULTIPLY = "multiply"
|
|
const val NORMAL = "normal"
|
|
//const val MAX = "GL_MAX" // not supported by GLES -- use shader
|
|
|
|
fun resolve(mode: String, batch: SpriteBatch) {
|
|
when (mode) {
|
|
SCREEN -> blendScreen(batch)
|
|
MULTIPLY -> blendMul(batch)
|
|
NORMAL -> blendNormalStraightAlpha(batch)
|
|
//MAX -> blendLightenOnly() // not supported by GLES -- use shader
|
|
else -> throw Error("Unknown blend mode: $mode")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum class RunningEnvironment {
|
|
PC, CONSOLE//, MOBILE
|
|
}
|
|
|
|
infix fun Color.screen(other: Color) = Color(
|
|
1f - (1f - this.r) * (1f - other.r),
|
|
1f - (1f - this.g) * (1f - other.g),
|
|
1f - (1f - this.b) * (1f - other.b),
|
|
1f - (1f - this.a) * (1f - other.a)
|
|
)
|
|
|
|
infix fun Color.minus(other: Color) = Color( // don't turn into an operator!
|
|
this.r - other.r,
|
|
this.g - other.g,
|
|
this.b - other.b,
|
|
this.a - other.a
|
|
)
|
|
|
|
fun Int.toHex() = this.toLong().and(0xFFFFFFFF).toString(16).padStart(8, '0').toUpperCase()
|
|
|
|
fun MutableList<Any>.shuffle() {
|
|
for (i in this.size - 1 downTo 1) {
|
|
val rndIndex = (Math.random() * (i + 1)).toInt()
|
|
|
|
val t = this[rndIndex]
|
|
this[rndIndex] = this[i]
|
|
this[i] = t
|
|
}
|
|
}
|
|
|
|
|
|
val ccW = TerrarumSansBitmap.toColorCode(0xFFFF)
|
|
val ccY = TerrarumSansBitmap.toColorCode(0xFFE8)
|
|
val ccO = TerrarumSansBitmap.toColorCode(0xFFB2)
|
|
val ccR = TerrarumSansBitmap.toColorCode(0xFF88)
|
|
val ccF = TerrarumSansBitmap.toColorCode(0xFFAE)
|
|
val ccM = TerrarumSansBitmap.toColorCode(0xFEAF)
|
|
val ccB = TerrarumSansBitmap.toColorCode(0xF6CF)
|
|
val ccC = TerrarumSansBitmap.toColorCode(0xF8FF)
|
|
val ccG = TerrarumSansBitmap.toColorCode(0xF8F8)
|
|
val ccV = TerrarumSansBitmap.toColorCode(0xF080)
|
|
val ccX = TerrarumSansBitmap.toColorCode(0xF853)
|
|
val ccK = TerrarumSansBitmap.toColorCode(0xF888)
|
|
val ccE = TerrarumSansBitmap.toColorCode(0xFBBB)
|
|
|
|
// Zelda-esque text colour emphasis
|
|
val emphRed = TerrarumSansBitmap.toColorCode(0xFF88)
|
|
val emphObj = TerrarumSansBitmap.toColorCode(0xF0FF)
|
|
val emphVerb = TerrarumSansBitmap.toColorCode(0xFFF6)
|
|
|
|
|
|
typealias Second = Float
|
|
|
|
inline fun Double.floorToInt() = floor(this).toInt()
|
|
inline fun Float.floorToInt() = FastMath.floor(this)
|
|
inline fun Double.ceilToInt() = Math.ceil(this).toInt()
|
|
inline fun Float.ceilToFloat(): Float = FastMath.ceil(this).toFloat()
|
|
inline fun Float.ceilToInt() = FastMath.ceil(this)
|
|
inline fun Float.floorToFloat() = FastMath.floor(this).toFloat()
|
|
inline fun Float.roundToFloat(): Float = round(this)
|
|
//inline fun Double.round() = Math.round(this).toDouble()
|
|
inline fun Double.floorToDouble() = floor(this)
|
|
inline fun Double.ceilToDouble() = ceil(this)
|
|
inline fun Int.sqr(): Int = this * this
|
|
inline fun Double.sqr() = this * this
|
|
inline fun Float.sqr() = this * this
|
|
inline fun Double.sqrt() = Math.sqrt(this)
|
|
inline fun Float.sqrt() = FastMath.sqrt(this)
|
|
inline fun Int.abs() = this.absoluteValue
|
|
inline fun Double.abs() = this.absoluteValue
|
|
inline fun Double.bipolarClamp(limit: Double) = this.coerceIn(-limit, limit)
|
|
inline fun Boolean.toInt(shift: Int = 0) = if (this) 1.shl(shift) else 0
|
|
inline fun Boolean.toLong(shift: Int = 0) = if (this) 1L.shl(shift) else 0L
|
|
inline fun Int.bitCount() = java.lang.Integer.bitCount(this)
|
|
inline fun Long.bitCount() = java.lang.Long.bitCount(this)
|
|
inline fun Double.signedSqrt() = this.abs().sqrt() * this.sign
|
|
|
|
|
|
fun absMax(left: Double, right: Double): Double {
|
|
if (left > 0 && right > 0)
|
|
if (left > right) return left
|
|
else return right
|
|
else if (left < 0 && right < 0)
|
|
if (left < right) return left
|
|
else return right
|
|
else {
|
|
val absL = left.abs()
|
|
val absR = right.abs()
|
|
if (absL > absR) return left
|
|
else return right
|
|
}
|
|
}
|
|
|
|
fun Double.magnSqr() = if (this >= 0.0) this.sqr() else -this.sqr()
|
|
fun interpolateLinear(scale: Double, startValue: Double, endValue: Double): Double {
|
|
if (startValue == endValue) {
|
|
return startValue
|
|
}
|
|
if (scale <= 0.0) {
|
|
return startValue
|
|
}
|
|
if (scale >= 1.0) {
|
|
return endValue
|
|
}
|
|
return (1.0 - scale) * startValue + scale * endValue
|
|
}
|
|
|
|
fun <T> List<T>.linearSearch(selector: (T) -> Boolean): Int? {
|
|
this.forEachIndexed { index, it ->
|
|
if (selector.invoke(it)) return index
|
|
}
|
|
|
|
return null
|
|
}
|
|
fun <T> List<T>.linearSearchBy(selector: (T) -> Boolean): T? {
|
|
this.forEach {
|
|
if (selector.invoke(it)) return it
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
inline fun printStackTrace(obj: Any) = printStackTrace(obj, System.out) // because of Java
|
|
|
|
fun printStackTrace(obj: Any, out: PrintStream = System.out) {
|
|
if (App.IS_DEVELOPMENT_BUILD) {
|
|
val timeNow = System.currentTimeMillis()
|
|
val ss = timeNow / 1000 % 60
|
|
val mm = timeNow / 60000 % 60
|
|
val hh = timeNow / 3600000 % 24
|
|
val ms = timeNow % 1000
|
|
val objName = if (obj is String) obj else obj.javaClass.simpleName
|
|
val hash = objName.hashCode().and(0x7FFFFFFF) % csis.size
|
|
val prompt = csis[hash] + String.format("%02d:%02d:%02d.%03d [%s]%s ", hh, mm, ss, ms, objName, csi0)
|
|
val indentation = " ".repeat(objName.length + 16)
|
|
Thread.currentThread().stackTrace.forEachIndexed { index, it ->
|
|
if (index == 1)
|
|
out.println("$prompt$it")
|
|
else if (index > 1)
|
|
out.println("$indentation$it")
|
|
}
|
|
}
|
|
}
|
|
|
|
class UIContainer {
|
|
internal val data = ArrayList<Any>()
|
|
fun add(vararg things: Any) {
|
|
things.forEach {
|
|
if (it is UICanvas || it is Id_UICanvasNullable)
|
|
if (!data.contains(it)) data.add(it)
|
|
else throw IllegalArgumentException(it.javaClass.name)
|
|
}
|
|
}
|
|
|
|
fun iterator() = object : Iterator<UICanvas?> {
|
|
private var cursor = 0
|
|
|
|
override fun hasNext() = cursor < data.size
|
|
|
|
override fun next(): UICanvas? {
|
|
val it = data[cursor]
|
|
// whatever the fucking reason when() does not work
|
|
if (it is UICanvas) {
|
|
cursor += 1
|
|
return it
|
|
}
|
|
else if (it is Id_UICanvasNullable) {
|
|
cursor += 1
|
|
return it.get()
|
|
}
|
|
else throw IllegalArgumentException("Unacceptable type ${it.javaClass.name}, instance of ${it.javaClass.superclass.name}")
|
|
}
|
|
}
|
|
fun iterator2() = object : Iterator<Pair<Int, UICanvas?>> {
|
|
private var cursor = 0
|
|
|
|
override fun hasNext() = cursor < data.size
|
|
|
|
override fun next(): Pair<Int, UICanvas?> {
|
|
val it = data[cursor]
|
|
// whatever the fucking reason when() does not work
|
|
if (it is UICanvas) {
|
|
cursor += 1
|
|
return (cursor - 1) to it
|
|
}
|
|
else if (it is Id_UICanvasNullable) {
|
|
cursor += 1
|
|
return (cursor - 1) to it.get()
|
|
}
|
|
else throw IllegalArgumentException("Unacceptable type ${it.javaClass.name}, instance of ${it.javaClass.superclass.name}")
|
|
}
|
|
}
|
|
|
|
fun forEach(operation: (UICanvas?) -> Unit) = iterator().forEach(operation)
|
|
fun forEachIndexed(operation: (Pair<Int, UICanvas?>) -> Unit) = iterator2().forEach(operation)
|
|
fun countVisible(): Int {
|
|
var c = 0
|
|
forEach { if (it?.isVisible == true) c += 1 }
|
|
return c
|
|
}
|
|
|
|
fun contains(element: Any) = data.contains(element)
|
|
|
|
fun <T> map(transformation: (UICanvas?) -> T) = iterator().asSequence().map(transformation)
|
|
|
|
fun filter(predicate: (Any) -> Boolean) = data.filter(predicate)
|
|
|
|
fun filterNotNull(): List<UICanvas> = data.filter {
|
|
if (it is UICanvas) true
|
|
else if (it is Id_UICanvasNullable) it.get() != null
|
|
else false
|
|
}.map {
|
|
if (it is UICanvas) it
|
|
else if (it is Id_UICanvasNullable) it.get()!!
|
|
else throw InternalError()
|
|
}
|
|
|
|
val UIsUnderMouse: List<UICanvas>
|
|
get() = filterNotNull().filter { it.isVisible && it.mouseUp && it::class.findAnnotation<UINotControllable>() == null }
|
|
|
|
val hasNoUIsUnderMouse: Boolean
|
|
get() = UIsUnderMouse.isEmpty()
|
|
|
|
val hasUIunderMouse: Boolean
|
|
get() = UIsUnderMouse.isNotEmpty()
|
|
|
|
}
|
|
|
|
interface Id_UICanvasNullable {
|
|
fun get(): UICanvas?
|
|
}
|
|
|
|
// haskell-inspired array selectors
|
|
// head and last use first() and last()
|
|
fun <T> Array<T>.tail() = this.sliceArray(1 until this.size)
|
|
fun <T> Array<T>.init() = this.sliceArray(0 until this.lastIndex)
|
|
fun <T> List<T>.tail() = this.subList(1, this.size)
|
|
fun <T> List<T>.init() = this.subList(0, this.lastIndex)
|
|
|
|
fun <T> Collection<T>.notEmptyOrNull() = this.ifEmpty { null }
|
|
|
|
val BlockCodex: BlockCodex
|
|
get() = Terrarum.blockCodex
|
|
val ItemCodex: ItemCodex
|
|
get() = Terrarum.itemCodex
|
|
val WireCodex: WireCodex
|
|
get() = Terrarum.wireCodex
|
|
val MaterialCodex: MaterialCodex
|
|
get() = Terrarum.materialCodex
|
|
val FactionCodex: FactionCodex
|
|
get() = Terrarum.factionCodex
|
|
val CraftingRecipeCodex: CraftingCodex
|
|
get() = Terrarum.craftingCodex
|
|
val Apocryphas: HashMap<String, Any>
|
|
get() = Terrarum.apocryphas
|
|
val FluidCodex: FluidCodex
|
|
get() = Terrarum.fluidCodex
|
|
val OreCodex: OreCodex
|
|
get() = Terrarum.oreCodex
|
|
val WeatherCodex: WeatherCodex
|
|
get() = Terrarum.weatherCodex
|
|
|
|
class Codex : KVHashMap() {
|
|
|
|
fun getAsCvec(key: String): Cvec? {
|
|
val value = get(key)
|
|
|
|
if (value == null) return null
|
|
|
|
return value as Cvec
|
|
}
|
|
|
|
}
|
|
|
|
fun AppUpdateListOfSavegames() {
|
|
App.sortedSavegameWorlds.clear()
|
|
App.savegameWorlds.clear()
|
|
App.savegameWorldsName.clear()
|
|
App.sortedPlayers.clear()
|
|
App.savegamePlayers.clear()
|
|
App.savegamePlayersName.clear()
|
|
|
|
|
|
// create list of worlds
|
|
// printdbg("ListSavegames", "Listing saved worlds...")
|
|
val worldsDirLs = File(worldsDir).listFiles().filter { !it.isDirectory && !it.name.contains('.') }.mapNotNull { file ->
|
|
try {
|
|
DiskSkimmer(file, true)
|
|
}
|
|
catch (e: Throwable) {
|
|
System.err.println("Unable to load a world file ${file.absolutePath}")
|
|
e.printStackTrace()
|
|
null
|
|
}
|
|
}.sortedByDescending { it.getLastModifiedTime() }
|
|
val filteringResults = arrayListOf<List<DiskSkimmer>>() // first element of the list is always file with no suffix
|
|
worldsDirLs.forEach {
|
|
val li = arrayListOf(it)
|
|
listOf(".1",".2",".3",".a",".b",".c").forEach { suffix ->
|
|
val file = File(it.diskFile.absolutePath + suffix)
|
|
try {
|
|
val d = DiskSkimmer(file, true)
|
|
li.add(d)
|
|
}
|
|
catch (e: Throwable) {}
|
|
}
|
|
filteringResults.add(li)
|
|
}
|
|
filteringResults.forEachIndexed { index, list ->
|
|
val it = list.first()
|
|
// printdbg("ListSavegames", " ${index+1}.\t${it.diskFile.absolutePath}")
|
|
|
|
// printdbg("ListSavegames", " collecting...")
|
|
val collection = SavegameCollection.collectFromBaseFilename(list, it.diskFile.name)
|
|
// printdbg("ListSavegames", " disk rebuilding...")
|
|
collection.rebuildLoadable()
|
|
// printdbg("ListSavegames", " get UUID...")
|
|
val worldUUID = collection.getUUID()
|
|
|
|
// printdbg("ListSavegames", " registration...")
|
|
// if multiple valid savegames with same UUID exist, only the most recent one is retained
|
|
if (!App.savegameWorlds.contains(worldUUID)) {
|
|
App.savegameWorlds[worldUUID] = collection
|
|
App.sortedSavegameWorlds.add(worldUUID)
|
|
App.savegameWorldsName[worldUUID] = it.getDiskName(Common.CHARSET)
|
|
}
|
|
}
|
|
|
|
|
|
// create list of players
|
|
// printdbg("ListSavegames", "Listing saved players...")
|
|
val playersDirLs = File(playersDir).listFiles().filter { !it.isDirectory && !it.name.contains('.') }.mapNotNull { file ->
|
|
try {
|
|
DiskSkimmer(file, true)
|
|
}
|
|
catch (e: Throwable) {
|
|
System.err.println("Unable to load a world file ${file.absolutePath}")
|
|
e.printStackTrace()
|
|
null
|
|
}
|
|
}.sortedByDescending { it.getLastModifiedTime() }
|
|
val filteringResults2 = arrayListOf<List<DiskSkimmer>>()
|
|
playersDirLs.forEach {
|
|
val li = arrayListOf(it)
|
|
listOf(".1",".2",".3",".a",".b",".c").forEach { suffix ->
|
|
val file = File(it.diskFile.absolutePath + suffix)
|
|
try {
|
|
val d = DiskSkimmer(file, true)
|
|
li.add(d)
|
|
}
|
|
catch (e: Throwable) {}
|
|
}
|
|
filteringResults2.add(li)
|
|
}
|
|
filteringResults2.forEachIndexed { index, list ->
|
|
val it = list.first()
|
|
// printdbg("ListSavegames", " ${index+1}.\t${it.diskFile.absolutePath}")
|
|
|
|
// printdbg("ListSavegames", " collecting...")
|
|
val collection = SavegameCollection.collectFromBaseFilename(list, it.diskFile.name)
|
|
// printdbg("ListSavegames", " disk rebuilding...")
|
|
collection.rebuildLoadable()
|
|
// printdbg("ListSavegames", " get UUID...")
|
|
val playerUUID = collection.getUUID()
|
|
|
|
// printdbg("ListSavegames", " registration...")
|
|
// if multiple valid savegames with same UUID exist, only the most recent one is retained
|
|
if (!App.savegamePlayers.contains(playerUUID)) {
|
|
App.savegamePlayers[playerUUID] = collection
|
|
App.sortedPlayers.add(playerUUID)
|
|
App.savegamePlayersName[playerUUID] = it.getDiskName(Common.CHARSET)
|
|
}
|
|
}
|
|
|
|
/*println("SortedPlayers...")
|
|
App.sortedPlayers.forEach {
|
|
println(it)
|
|
}*/
|
|
|
|
}
|
|
|
|
/**
|
|
* @param skimmer loaded with the savefile, rebuilt/updated beforehand
|
|
*/
|
|
fun checkForSavegameDamage(skimmer: DiskSkimmer): Boolean {
|
|
try {
|
|
// # check for meta
|
|
/*val metaFile = skimmer.requestFile(-1) ?: return true
|
|
// # check if The Player is there
|
|
val player = skimmer.requestFile(PLAYER_REF_ID.toLong().and(0xFFFFFFFFL))?.contents ?: return true
|
|
// # check if:
|
|
// the world The Player is at actually exists
|
|
// all the actors for the world actually exists
|
|
// TODO SAX parser for JSON -- use JsonReader().parse(<String>) for now...
|
|
val currentWorld = (player as EntryFile).bytes.let {
|
|
(ReadActor.readActorBare(ByteArray64Reader(it, Common.CHARSET)) as? IngamePlayer
|
|
?: return true).worldCurrentlyPlaying
|
|
}
|
|
if (currentWorld == 0) return true
|
|
val worldData = (skimmer.requestFile(currentWorld.toLong())?.contents as? EntryFile)?.bytes ?: return true
|
|
val world = Common.jsoner.fromJson(GameWorld::class.java, ByteArray64Reader(worldData, Common.CHARSET))
|
|
|
|
var hasMissingActor = false
|
|
world.actors.forEach {
|
|
if (!skimmer.hasEntry(it.toLong().and(0xFFFFFFFFL))) {
|
|
System.err.println("Nonexisting actor $it for savegame ${skimmer.diskFile.absolutePath}")
|
|
hasMissingActor = true
|
|
}
|
|
}; if (hasMissingActor) return true*/
|
|
|
|
|
|
return false
|
|
}
|
|
catch (e: Throwable) {
|
|
e.printStackTrace()
|
|
return true
|
|
}
|
|
}
|
|
|
|
/**
|
|
* No lateinit!
|
|
*/
|
|
inline fun Disposable.tryDispose() {
|
|
try { this.dispose() }
|
|
catch (_: Throwable) {}
|
|
}
|
|
|
|
fun distBetweenActors(a: ActorWithBody, b: ActorWithBody): Double {
|
|
return distBetweenPoints(a.centrePosVector, b.centrePosVector)
|
|
}
|
|
|
|
fun distBetweenPoints(a: Vector2, b: Vector2): Double {
|
|
val ww = INGAME.world.width * TILE_SIZED
|
|
val apos1 = a
|
|
val apos2 = Vector2(apos1.x + ww, apos1.y)
|
|
val apos3 = Vector2(apos1.x - ww, apos1.y)
|
|
val bpos = b
|
|
val dist = min(min(bpos.distanceSquared(apos1), bpos.distanceSquared(apos2)), bpos.distanceSquared(apos3))
|
|
return dist.sqrt()
|
|
}
|
|
|
|
/**
|
|
* @return positive if the otherActor is on the right side of the player, negative if on the left
|
|
*/
|
|
fun relativeXposition(thisActor: ActorWithBody, otherActor: ActorWithBody): Double {
|
|
return relativeXposition(thisActor, otherActor.centrePosVector)
|
|
}
|
|
/**
|
|
* @return positive if the otherActor is on the right side of the player, negative if on the left
|
|
*/
|
|
fun relativeXposition(thisActor: ActorWithBody, otherActor: Vector2): Double {
|
|
val ww = INGAME.world.width * TILE_SIZED
|
|
val thisPos = thisActor.centrePosVector
|
|
val pos1 = otherActor
|
|
val pos2 = Vector2(pos1.x + ww, pos1.y)
|
|
val pos3 = Vector2(pos1.x - ww, pos1.y)
|
|
val posToUse = listOf(
|
|
pos1 to thisPos.distanceSquared(pos1),
|
|
pos2 to thisPos.distanceSquared(pos2),
|
|
pos3 to thisPos.distanceSquared(pos3),
|
|
).sortedBy { it.second }.first().first
|
|
return posToUse.x - thisPos.x
|
|
}
|
|
|
|
fun distBetween(a: ActorWithBody, bpos: Vector2): Double {
|
|
val ww = INGAME.world.width * TILE_SIZED
|
|
val apos1 = a.centrePosVector
|
|
val apos2 = Vector2(apos1.x + ww, apos1.y)
|
|
val apos3 = Vector2(apos1.x - ww, apos1.y)
|
|
val dist = min(min(bpos.distanceSquared(apos1), bpos.distanceSquared(apos2)), bpos.distanceSquared(apos3))
|
|
return dist.sqrt()
|
|
}
|
|
const val hashStrMap = "YBNDRFG8EJKMCPQXOTLVWIS2A345H769"
|
|
fun getHashStr(length: Int = 5) = (0 until length).map { hashStrMap[Math.random().times(32).toInt()] }.joinToString("")
|
|
|
|
fun <S, T> List<S>.cartesianProduct(other: List<T>) = this.flatMap { thisIt ->
|
|
other.map { otherIt ->
|
|
thisIt to otherIt
|
|
}
|
|
}
|
|
|
|
fun Float.ditherToInt() = (this + Math.random() - 0.5).roundToInt()
|
|
fun Double.ditherToInt() = (this + Math.random() - 0.5).roundToInt()
|