diff --git a/src/net/torvald/terrarum/IngameInstance.kt b/src/net/torvald/terrarum/IngameInstance.kt index a6c06afa6..d1e3dded5 100644 --- a/src/net/torvald/terrarum/IngameInstance.kt +++ b/src/net/torvald/terrarum/IngameInstance.kt @@ -135,6 +135,9 @@ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boo open var gameFullyLoaded = false internal set + var worldGenVer: Long? = null + protected set + val ACTORCONTAINER_INITIAL_SIZE = 64 val actorContainerActive = SortedArrayList(ACTORCONTAINER_INITIAL_SIZE) val actorContainerInactive = SortedArrayList(ACTORCONTAINER_INITIAL_SIZE) diff --git a/src/net/torvald/terrarum/modulebasegame/IngameRenderer.kt b/src/net/torvald/terrarum/modulebasegame/IngameRenderer.kt index e8cc9a630..3d74bc85e 100644 --- a/src/net/torvald/terrarum/modulebasegame/IngameRenderer.kt +++ b/src/net/torvald/terrarum/modulebasegame/IngameRenderer.kt @@ -20,7 +20,9 @@ import net.torvald.terrarum.blockproperties.Block import net.torvald.terrarum.blockproperties.FluidCodex import net.torvald.terrarum.gameactors.ActorWithBody import net.torvald.terrarum.gameactors.ActorWithBody.Companion.METER +import net.torvald.terrarum.gameactors.ActorWithBody.Companion.PHYS_EPSILON_DIST import net.torvald.terrarum.gameactors.ActorWithBody.Companion.SI_TO_GAME_ACC +import net.torvald.terrarum.gameactors.Hitbox import net.torvald.terrarum.gamecontroller.KeyToggler import net.torvald.terrarum.gameitems.GameItem import net.torvald.terrarum.gameitems.mouseInInteractableRange @@ -38,6 +40,7 @@ import net.torvald.terrarum.worlddrawer.LightmapRenderer import net.torvald.terrarum.worlddrawer.WorldCamera import net.torvald.util.CircularArray import org.dyn4j.geometry.Vector2 +import kotlin.math.min import kotlin.system.exitProcess @@ -786,9 +789,80 @@ object IngameRenderer : Disposable { } } private val cubeSize = 7.0 + private val hcubeSize = cubeSize / 2 private val externalV = Vector2() private val maxStep = 56 private val trajectoryFlow = 30 + + private fun getSubmergedHeight(gravitation: Vector2, hitbox: Hitbox): Double { + val straightGravity = (gravitation.y > 0) + // TODO reverse gravity + if (!straightGravity) TODO() + + val itsY = (hitbox.startY / TILE_SIZED).toInt() + val iteY = (hitbox.endY / TILE_SIZED).toInt() + val txL = (hitbox.startX / TILE_SIZED).floorToInt() + val txR = (hitbox.endX / TILE_SIZED).floorToInt() + + var hL = 0.0 + var hR = 0.0 + + val rec = java.util.ArrayList() + + for (ty in itsY..iteY) { + val fL = world.getFluid(txL, ty).amount.coerceAtMost(1f) * TILE_SIZED // 0-16 + val fR = world.getFluid(txR, ty).amount.coerceAtMost(1f) * TILE_SIZED // 0-16 + + // if head + if (ty == itsY) { + val actorHs = hitbox.startY % TILE_SIZED // 0-16 + val yp = TILE_SIZED - actorHs // 0-16 + + hL += min(yp, fL) + hR += min(yp, fR) + + rec.add(min(yp, fL)) + } + // if tail + else if (ty == iteY) { + val actorHe = hitbox.endY % TILE_SIZED // 0-16 + + hL += (actorHe - TILE_SIZED + fL).coerceAtLeast(0.0) + hR += (actorHe - TILE_SIZED + fR).coerceAtLeast(0.0) + + rec.add((actorHe - TILE_SIZED + fL).coerceAtLeast(0.0)) + } + else { + hL += fL + hR += fR + + rec.add(fL) + } + } + + // returns average of two sides + return (hL + hR) / 2.0 + } + + + private fun forEachOccupyingFluid(hitbox: Hitbox, consumer: (GameWorld.FluidInfo?) -> Unit) { + val hIntTilewiseHitbox = Hitbox(0.0, 0.0, 1.0, 1.0).setFromTwoPoints( + hitbox.startX.plus(PHYS_EPSILON_DIST).div(TILE_SIZE).floorToDouble() + 0.5, + hitbox.startY.plus(PHYS_EPSILON_DIST).div(TILE_SIZE).floorToDouble() + 0.5, + hitbox.endX.plus(PHYS_EPSILON_DIST).div(TILE_SIZE).floorToDouble() + 0.5, + hitbox.endY.plus(PHYS_EPSILON_DIST).div(TILE_SIZE).floorToDouble() + 0.5 + ) + val tileProps = java.util.ArrayList() + for (y in hIntTilewiseHitbox.startY.toInt()..hIntTilewiseHitbox.endY.toInt()) { + for (x in hIntTilewiseHitbox.startX.toInt()..hIntTilewiseHitbox.endX.toInt()) { + tileProps.add(world.getFluid(x, y)) + } + } + + return tileProps.forEach(consumer) + } + + private fun drawTrajectoryForThrowable(frameBuffer: FrameBuffer, batch: SpriteBatch, frameDelta: Float, player: ActorWithBody, world: GameWorld, item: ItemThrowable) { val ww = world.width * TILE_SIZEF @@ -809,6 +883,17 @@ object IngameRenderer : Disposable { // simulate physics applyGravitation(grav, cubeSize) // TODO use actual value instead of `cubeSize` + val hb = Hitbox(throwPos.x - hcubeSize, throwPos.y - hcubeSize, cubeSize, cubeSize) + var tileDensityFluid = 0 + forEachOccupyingFluid(hb) { + // get max density for each tile + if (it?.isFluid() == true && it.getProp().density > tileDensityFluid) { + tileDensityFluid = it.getProp().density + } + } + val submergedHeight = getSubmergedHeight(grav, hb) + val submergedRatio = submergedHeight / cubeSize + applyBuoyancy(grav, item.mass, item.material.density.toDouble(), tileDensityFluid, submergedRatio) // move the point throwPos += externalV // more physics @@ -818,10 +903,10 @@ object IngameRenderer : Disposable { // break if colliding with a tile val hitSolid = listOf( - throwPos + Vector2(-cubeSize/2, -cubeSize/2), - throwPos + Vector2(-cubeSize/2, +cubeSize/2), - throwPos + Vector2(+cubeSize/2, +cubeSize/2), - throwPos + Vector2(+cubeSize/2, -cubeSize/2), + throwPos + Vector2(-hcubeSize, -hcubeSize), + throwPos + Vector2(-hcubeSize, +hcubeSize), + throwPos + Vector2(+hcubeSize, +hcubeSize), + throwPos + Vector2(+hcubeSize, -hcubeSize), ).any { val wx = (it.x / TILE_SIZED).toInt() val wy = (it.y / TILE_SIZED).toInt() @@ -872,6 +957,29 @@ object IngameRenderer : Disposable { applyForce(getDrag(externalV, gravitation, hitboxWidth)) } + private fun applyBuoyancy(grav: Vector2, mass: Double, density: Double, tileDensityFluid: Int, submergedRatio: Double) { + + + val rho = tileDensityFluid // kg / m^3 + val V_full = mass / density * 2.0 // density = mass / volume, simply rearrange this. Multiplier of 2.0 is a hack! + val V = V_full * submergedRatio + val F_k = grav * mass // F = ma where a is g; TODO add jump-accel into 'a' to allow better jumping under water + val F_bo = grav * (rho * V) // Newtons + + // mh'' = mg - rho*gv + // h'' = (mg - rho*gv) / m + + // if tileDensity = actorDensity, F_k = F_bo (this will be the case if there was no hack) +// printdbg(this, "F_k=$F_k [N] \t F_bo=${F_bo} [N] \t density=$density") + + val F = F_k - F_bo + + val acc = F / mass // (kg * m / s^2) / kg = m / s^2 + val acc_game = acc.let { Vector2(it.x, it.y.coerceAtMost(0.0)) } * SI_TO_GAME_ACC + + applyForce(acc_game) + } + private fun Int.frictionToMult(): Double = this / 16.0 private fun Int.viscosityToMult(): Double = 16.0 / (16.0 + this) diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index fb61b5d2c..18d3e5a51 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -387,6 +387,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { } loadCallback = codices.callbackAfterLoad + worldGenVer = codices.worldGenver } /** Load rest of the game with GL context */ @@ -560,6 +561,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { KeyToggler.forceSet(Input.Keys.Q, false) loadCallback = newGameParams.callbackAfterLoad + worldGenVer = null } val ingameController = IngameController(this) diff --git a/src/net/torvald/terrarum/modulebasegame/serialise/QuickSaveThread.kt b/src/net/torvald/terrarum/modulebasegame/serialise/QuickSaveThread.kt index 623eecba2..6abebca3d 100644 --- a/src/net/torvald/terrarum/modulebasegame/serialise/QuickSaveThread.kt +++ b/src/net/torvald/terrarum/modulebasegame/serialise/QuickSaveThread.kt @@ -98,7 +98,7 @@ class QuickSingleplayerWorldSavingThread( playersList.forEach { ingame.world.playersLastStatus[it.uuid] = PlayerLastStatus(it, ingame.isMultiplayer) } - val worldMeta = EntryFile(WriteWorld.encodeToByteArray64(ingame, time_t, actorsList, playersList)) + val worldMeta = EntryFile(WriteWorld.encodeToByteArray64(ingame.worldGenVer, ingame, time_t, actorsList, playersList)) val world = DiskEntry(SAVEGAMEINFO, ROOT, creation_t, time_t, worldMeta) addFile(disk, world); skimmer.appendEntry(world) diff --git a/src/net/torvald/terrarum/modulebasegame/serialise/WorldSavingThread.kt b/src/net/torvald/terrarum/modulebasegame/serialise/WorldSavingThread.kt index 456451b42..2bbbadc69 100644 --- a/src/net/torvald/terrarum/modulebasegame/serialise/WorldSavingThread.kt +++ b/src/net/torvald/terrarum/modulebasegame/serialise/WorldSavingThread.kt @@ -121,7 +121,7 @@ class WorldSavingThread( // Write World // - val worldMeta = EntryFile(WriteWorld.encodeToByteArray64(ingame, time_t, actorsList, playersList)) + val worldMeta = EntryFile(WriteWorld.encodeToByteArray64(ingame.worldGenVer, ingame, time_t, actorsList, playersList)) val world = DiskEntry(VDFileID.SAVEGAMEINFO, VDFileID.ROOT, creation_t, time_t, worldMeta) addFile(disk, world) diff --git a/src/net/torvald/terrarum/modulebasegame/serialise/WriteWorld.kt b/src/net/torvald/terrarum/modulebasegame/serialise/WriteWorld.kt index 61039b979..8fd6809f7 100644 --- a/src/net/torvald/terrarum/modulebasegame/serialise/WriteWorld.kt +++ b/src/net/torvald/terrarum/modulebasegame/serialise/WriteWorld.kt @@ -51,15 +51,15 @@ object WriteWorld { } // genver must be found on fixed location of the JSON string - operator fun invoke(ingame: TerrarumIngame, time_t: Long, actorsList: List, playersList: List): String { + operator fun invoke(oldGenVer: Long?, ingame: TerrarumIngame, time_t: Long, actorsList: List, playersList: List): String { val s = Common.jsoner.toJson(preWrite(ingame, time_t, actorsList, playersList)) - return """{"genver":${Common.GENVER},${s.substring(1)}""" + return """{"genver":${oldGenVer ?: Common.GENVER},${s.substring(1)}""" } - fun encodeToByteArray64(ingame: TerrarumIngame, time_t: Long, actorsList: List, playersList: List): ByteArray64 { + fun encodeToByteArray64(oldGenVer: Long?, ingame: TerrarumIngame, time_t: Long, actorsList: List, playersList: List): ByteArray64 { val baw = ByteArray64Writer(Common.CHARSET) - val header = """{"genver":${Common.GENVER}""" + val header = """{"genver":${oldGenVer ?: Common.GENVER}""" baw.write(header) Common.jsoner.toJson(preWrite(ingame, time_t, actorsList, playersList), baw) baw.flush(); baw.close() diff --git a/work_files/DataFormats/SAVE_FORMAT.md b/work_files/DataFormats/SAVE_FORMAT.md index de5b0fadc..581fb0f2e 100644 --- a/work_files/DataFormats/SAVE_FORMAT.md +++ b/work_files/DataFormats/SAVE_FORMAT.md @@ -62,8 +62,10 @@ Worlds must overwrite new Actor's position to make them spawn in right place. ### Remarks -Making `inventory` transient is impossible as it would render Storage Chests unusable. - +* Making `inventory` transient is impossible as it would render Storage Chests unusable. +* `genver` used in World and Player have different meaning + * in World, the value is the version of the game the world is generated first, so that the future version of the game with different worldgen algorithm would still use old algorithm for the savegames made from the old version + * in Player, the value is the version the game has been saved, should be equal to or larger than the World genver ## Prerequisites 1. Player ID must not be strictly 9545698 (0x91A7E2)