From ff9f02322b77884d99331641f7f73503d29f98ff Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sun, 8 Feb 2026 02:06:29 +0900 Subject: [PATCH] moving platform wip --- assets/mods/basegame/commands.csv | 1 + .../gameactors/ActorMovingPlatform.kt | 172 +++++++++++++++++- .../terrarum/gameactors/ActorWithBody.kt | 2 +- .../terrarum/modulebasegame/TerrarumIngame.kt | 12 +- .../console/SpawnMovingPlatform.kt | 32 ++++ .../gameactors/ActorTestPlatform.kt | 72 +++++++- 6 files changed, 277 insertions(+), 14 deletions(-) create mode 100644 src/net/torvald/terrarum/modulebasegame/console/SpawnMovingPlatform.kt diff --git a/assets/mods/basegame/commands.csv b/assets/mods/basegame/commands.csv index 310e6a01b..66a729591 100644 --- a/assets/mods/basegame/commands.csv +++ b/assets/mods/basegame/commands.csv @@ -32,6 +32,7 @@ SetSol SetTurb SetTime SetTimeDelta +SpawnMovingPlatform SpawnPhysTestBall Teleport ToggleNoClip diff --git a/src/net/torvald/terrarum/gameactors/ActorMovingPlatform.kt b/src/net/torvald/terrarum/gameactors/ActorMovingPlatform.kt index b72209e23..6f79573ab 100644 --- a/src/net/torvald/terrarum/gameactors/ActorMovingPlatform.kt +++ b/src/net/torvald/terrarum/gameactors/ActorMovingPlatform.kt @@ -1,40 +1,192 @@ package net.torvald.terrarum.gameactors +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.Pixmap +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import com.badlogic.gdx.graphics.g2d.TextureRegion +import net.torvald.terrarum.INGAME import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE -import net.torvald.terrarum.gameactors.ActorID -import net.torvald.terrarum.gameactors.ActorWithBody -import net.torvald.terrarum.gameactors.PhysProperties +import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid +import org.dyn4j.geometry.Vector2 /** + * Base class for autonomously moving platforms that carry other actors standing on them. + * + * Subclasses must set [platformVelocity] before calling `super.updateImpl(delta)`. + * * Created by minjaesong on 2022-02-28. */ open class ActorMovingPlatform() : ActorWithBody() { protected var tilewiseWidth = 3 - @Transient protected val actorsRiding = ArrayList() // saving actorID due to serialisation issues + + constructor(newTilewiseWidth: Int) : this() { + this.tilewiseWidth = newTilewiseWidth + } + + /** Actors currently riding this platform, stored by ActorID for serialisation. */ + @Transient protected val actorsRiding = ArrayList() + + /** Velocity the platform intends to move this tick. Subclasses set this before calling super.updateImpl(). */ + @Transient protected val platformVelocity = Vector2(0.0, 0.0) + + /** Actual displacement applied this tick (after clampHitbox). */ + @Transient private val appliedVelocity = Vector2(0.0, 0.0) + + /** Tolerance in pixels for "feet on top of platform" detection. */ + @Transient private val MOUNT_TOLERANCE_Y = 2.0 + + /** Minimum combined Y velocity to count as "jumping up" (prevents mount while jumping). */ + @Transient private val JUMP_THRESHOLD_Y = -0.5 + + @Transient private var platformTexture: Texture? = null + @Transient private var platformTextureRegion: TextureRegion? = null init { - physProp = PhysProperties.PHYSICS_OBJECT() + physProp = PhysProperties.MOBILE_OBJECT() + collisionType = COLLISION_KINEMATIC setHitboxDimension(TILE_SIZE * tilewiseWidth, TILE_SIZE, 0, 0) } + private fun ensureTexture() { + if (platformTexture == null) { + val w = TILE_SIZE * tilewiseWidth + val h = TILE_SIZE + val pixmap = Pixmap(w, h, Pixmap.Format.RGBA8888) + // grey-blue colour + pixmap.setColor(Color(0.45f, 0.55f, 0.65f, 1f)) + pixmap.fill() + // slightly darker border + pixmap.setColor(Color(0.35f, 0.45f, 0.55f, 1f)) + pixmap.drawRectangle(0, 0, w, h) + platformTexture = Texture(pixmap) + platformTextureRegion = TextureRegion(platformTexture) + pixmap.dispose() + } + } + override fun updateImpl(delta: Float) { - TODO("Not yet implemented") + // Snapshot position before movement + val oldX = hitbox.startX + val oldY = hitbox.startY + + // Set externalV to our platform velocity so super translates the hitbox + externalV.set(platformVelocity.x, platformVelocity.y) + + // super.updateImpl handles: + // - sprite updates + // - hitbox.translate(externalV) (since usePhysics=false -> isNoCollideWorld=true) + // - clampHitbox + // - tilewise hitbox cache updates + // - position vector updates + super.updateImpl(delta) + + // Compute actual displacement (clampHitbox may have wrapped coordinates) + appliedVelocity.set(hitbox.startX - oldX, hitbox.startY - oldY) + + // --- Mount detection and rider management --- + + val ridersToRemove = ArrayList() + + // Build set of actors currently on top of this platform + val newRiders = ArrayList() + + // Check all active actors + actorNowPlaying + val candidates = ArrayList() + INGAME.actorContainerActive.forEach { + if (it is ActorWithBody && it !== this && it !is ActorMovingPlatform) { + candidates.add(it) + } + } + INGAME.actorNowPlaying?.let { candidates.add(it) } + + for (actor in candidates) { + val feetY = actor.hitbox.endY + val platTop = hitbox.startY + + // Check vertical proximity: feet within tolerance of platform top + val verticallyAligned = Math.abs(feetY - platTop) <= MOUNT_TOLERANCE_Y + + // Check horizontal overlap + val horizontalOverlap = actor.hitbox.endX > hitbox.startX && actor.hitbox.startX < hitbox.endX + + // Check not jumping upward + val combinedVelY = actor.externalV.y + (actor.controllerV?.y ?: 0.0) + val notJumping = combinedVelY >= JUMP_THRESHOLD_Y + + if (verticallyAligned && horizontalOverlap && notJumping) { + if (!actorsRiding.contains(actor.referenceID)) { + mount(actor) + } + newRiders.add(actor) + } + } + + // Dismount actors that are no longer on top + val newRiderIds = newRiders.map { it.referenceID }.toSet() + for (riderId in actorsRiding.toList()) { + if (riderId !in newRiderIds) { + val rider = INGAME.getActorByID(riderId) + if (rider is ActorWithBody) { + dismount(rider) + } + else { + ridersToRemove.add(riderId) + } + } + } + ridersToRemove.forEach { actorsRiding.remove(it) } + + // Move riders and suppress their gravity + for (rider in newRiders) { + // Translate rider by platform's actual displacement + rider.hitbox.translate(appliedVelocity) + + // Snap rider's feet to platform top + rider.hitbox.setPositionY(hitbox.startY - rider.hitbox.height) + + // Suppress gravity for this tick + rider.walledBottom = true + } } /** - * Make the actor its externalV controlled by this platform + * Add an actor to the rider list. */ fun mount(actor: ActorWithBody) { - actorsRiding.add(actor.referenceID) + if (!actorsRiding.contains(actor.referenceID)) { + actorsRiding.add(actor.referenceID) + actor.platformsRiding.add(this.referenceID) + } } /** - * Make the actor its externalV no longer controlled by this platform + * Remove an actor from the rider list and apply dismount impulse. */ fun dismount(actor: ActorWithBody) { actorsRiding.remove(actor.referenceID) + actor.platformsRiding.remove(this.referenceID) + + // Conservation of momentum: add platform velocity as impulse + actor.externalV.x += platformVelocity.x + actor.externalV.y += platformVelocity.y } -} \ No newline at end of file + override fun drawBody(frameDelta: Float, batch: SpriteBatch) { + if (isVisible) { + ensureTexture() + platformTextureRegion?.let { + drawTextureInGoodPosition(frameDelta, it, batch) + } + } + } + + override fun dispose() { + platformTexture?.dispose() + platformTexture = null + platformTextureRegion = null + super.dispose() + } +} diff --git a/src/net/torvald/terrarum/gameactors/ActorWithBody.kt b/src/net/torvald/terrarum/gameactors/ActorWithBody.kt index f5a606b28..a434e77c6 100644 --- a/src/net/torvald/terrarum/gameactors/ActorWithBody.kt +++ b/src/net/torvald/terrarum/gameactors/ActorWithBody.kt @@ -331,7 +331,7 @@ open class ActorWithBody : Actor { * * Also see [net.torvald.terrarum.modulebasegame.gameactors.ActorMovingPlatform.actorsRiding] */ - @Transient protected val platformsRiding = ArrayList() + @Transient internal val platformsRiding = ArrayList() /** * Gravitational Constant G. Load from gameworld. diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index 25cff4004..5dc376b51 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -1480,8 +1480,16 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { actorNowPlaying?.update(delta)*/ } else { + // Pass 1: update moving platforms first so riders get displaced before their own update actorContainerActive.forEach { - if (it != actorNowPlaying) { + if (it is ActorMovingPlatform && it != actorNowPlaying) { + it.update(delta) + } + } + + // Pass 2: update all non-platform actors with existing callbacks + actorContainerActive.forEach { + if (it !is ActorMovingPlatform && it != actorNowPlaying) { it.update(delta) if (it is Pocketed) { @@ -1515,6 +1523,8 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { } } } + + // Pass 3: update player actorNowPlaying?.update(delta) //AmmoMeterProxy(player, uiVitalItem.UI as UIVitalMetre) } diff --git a/src/net/torvald/terrarum/modulebasegame/console/SpawnMovingPlatform.kt b/src/net/torvald/terrarum/modulebasegame/console/SpawnMovingPlatform.kt new file mode 100644 index 000000000..db447625f --- /dev/null +++ b/src/net/torvald/terrarum/modulebasegame/console/SpawnMovingPlatform.kt @@ -0,0 +1,32 @@ +package net.torvald.terrarum.modulebasegame.console + +import net.torvald.terrarum.INGAME +import net.torvald.terrarum.Terrarum +import net.torvald.terrarum.console.ConsoleAlias +import net.torvald.terrarum.console.ConsoleCommand +import net.torvald.terrarum.console.Echo +import net.torvald.terrarum.modulebasegame.gameactors.ActorTestPlatform + +/** + * Created by minjaesong on 2026-02-08. + */ +@ConsoleAlias("spawnplatform") +internal object SpawnMovingPlatform : ConsoleCommand { + override fun execute(args: Array) { + val mouseX = Terrarum.mouseX + val mouseY = Terrarum.mouseY + + val platform = ActorTestPlatform() + // setPosition places bottom-centre at the given point; offset Y so the platform is centred at cursor + platform.setPosition(mouseX, mouseY + platform.hitbox.height / 2.0) + + INGAME.queueActorAddition(platform) + + Echo("Spawned ActorTestPlatform at (${"%.1f".format(mouseX)}, ${"%.1f".format(mouseY)})") + } + + override fun printUsage() { + Echo("usage: spawnplatform") + Echo("Spawns a test moving platform centred at the mouse cursor.") + } +} diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/ActorTestPlatform.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/ActorTestPlatform.kt index 276b34098..f3ec09f95 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/ActorTestPlatform.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/ActorTestPlatform.kt @@ -1,9 +1,77 @@ package net.torvald.terrarum.modulebasegame.gameactors import net.torvald.terrarum.gameactors.ActorMovingPlatform +import kotlin.math.cos +import kotlin.math.sin /** + * Test platform that randomly selects a movement pattern on spawn. + * + * Patterns: + * - 0: Horizontal pingpong (sine-eased) + * - 1: Vertical pingpong (sine-eased) + * - 2: Clockwise circular (constant speed) + * - 3: Counter-clockwise circular (constant speed) + * * Created by minjaesong on 2022-03-02. */ -class ActorTestPlatform : ActorMovingPlatform() { -} \ No newline at end of file +class ActorTestPlatform : ActorMovingPlatform(8) { + + /** Movement pattern index (0-3). */ + private val pattern: Int = (0..3).random() + + /** Speed in pixels per tick (2.0 to 4.0). */ + private val speed: Double = 2.0 + Math.random() * 2.0 + + /** Current phase angle in radians. */ + private var phase: Double = 0.0 + + /** + * Phase step per tick. + * + * For pingpong: peak speed = amplitude * phaseStep = speed + * period = 128 ticks (~2s), so phaseStep = 2*PI/128 + * amplitude = speed / phaseStep + * + * For circular: speed = radius * phaseStep + * using same phaseStep, radius = speed / phaseStep + */ + @Transient private val PERIOD_TICKS = 128.0 + @Transient private val phaseStep: Double = 2.0 * Math.PI / PERIOD_TICKS + + /** Amplitude for pingpong patterns, radius for circular patterns. */ + @Transient private val amplitude: Double = speed / phaseStep + + override fun updateImpl(delta: Float) { + val oldPhase = phase + phase += phaseStep + + when (pattern) { + 0 -> { + // Horizontal pingpong: position = A * sin(phase) + // Velocity = finite difference to prevent float drift + val dx = amplitude * (sin(phase) - sin(oldPhase)) + platformVelocity.set(dx, 0.0) + } + 1 -> { + // Vertical pingpong: position = A * sin(phase) + val dy = amplitude * (sin(phase) - sin(oldPhase)) + platformVelocity.set(0.0, dy) + } + 2 -> { + // Clockwise circular: position on circle (cos, sin) + val dx = amplitude * (cos(phase) - cos(oldPhase)) + val dy = amplitude * (sin(phase) - sin(oldPhase)) + platformVelocity.set(dx, dy) + } + 3 -> { + // Counter-clockwise circular: negate Y component + val dx = amplitude * (cos(phase) - cos(oldPhase)) + val dy = -(amplitude * (sin(phase) - sin(oldPhase))) + platformVelocity.set(dx, dy) + } + } + + super.updateImpl(delta) + } +}