diff --git a/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java b/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java index 37364bf..1fe9f2f 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java +++ b/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java @@ -53,7 +53,7 @@ public class GameScreen implements Screen { private ModelLibrary modelLibrary; - private static final int RETRO_WIDTH = 320; + private static final int RETRO_WIDTH = 430; private static final int RETRO_HEIGHT = 240; public GameScreen(Main game) { @@ -61,7 +61,7 @@ public class GameScreen implements Screen { } private void initCamera() { - camera = new PerspectiveCamera(67f, RETRO_WIDTH, RETRO_HEIGHT); + camera = new PerspectiveCamera(60f, RETRO_WIDTH, RETRO_HEIGHT); settings = new GameSettings(); settings.fov = camera.fieldOfView; cameraController = new FpsCameraController(camera, settings); diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java b/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java index 76a6c84..4bcb559 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java @@ -60,19 +60,37 @@ public class DebugHud { float y = camera.position.y; float z = camera.position.z; - String posText = String.format("X: %.2f Y: %.2f Z: %.2f", x, y, z); + String posText = String.format("X: %.3f Y: %.3f Z: %.3f", x, y, z); font.draw(batch, posText, 5f, height - 15f); - String angText = String.format("Yaw: %.1f Pitch: %.1f", yaw, pitch); + String angText = String.format("Yaw: %.2f Pitch: %.2f", yaw, pitch); font.draw(batch, angText, 5f, height - 25f); - String dayText = String.format("Time of day: %.2f Sun height: %.2f", dayNightCycle.getTimeOfDay(), dayNightCycle.getSunAltitude()); + String dayText = String.format("Time of day: %.3f [%s] Sun height: %.3f", dayNightCycle.getTimeOfDay(), formatTimeOfDay(dayNightCycle.getTimeOfDay()), dayNightCycle.getSunAltitude()); font.draw(batch, dayText, 5f, height - 35f); } batch.end(); } + private String formatTimeOfDay(float timeOfDay) { + // keep it in [0,1) + float t = timeOfDay - (float)Math.floor(timeOfDay); + + float hoursF = t * 24f; + int hours = (int)Math.floor(hoursF); + float minutesF = (hoursF - hours) * 60f; + int minutes = (int)Math.floor(minutesF); + + // clamp just in case of float weirdness + if (hours < 0) hours = 0; + if (hours > 23) hours = 23; + if (minutes < 0) minutes = 0; + if (minutes > 59) minutes = 59; + + return String.format("%02d:%02d", hours, minutes); + } + public void dispose() { batch.dispose(); font.dispose(); diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/FpsCameraController.java b/core/src/main/java/wtf/beatrice/retrorender/engine/FpsCameraController.java index d44b354..665fc9f 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/FpsCameraController.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/FpsCameraController.java @@ -104,20 +104,18 @@ public class FpsCameraController { /** Update each frame with delta time */ public void update(float delta, World3D world) { - // if we don't own the mouse, just don't move camera / player if (!mouseCaptured) { return; } - // skip a few frames after capture to avoid mouse skew - // teleport cursor to center + // warmup: ignore mouse for a few frames after capture if (captureWarmupFrames > 0) { captureWarmupFrames--; Gdx.input.setCursorPosition(centerX, centerY); return; } - // --- mouse look: keep cursor centred + // --- mouse look --- int mouseX = Gdx.input.getX(); int mouseY = Gdx.input.getY(); @@ -130,22 +128,17 @@ public class FpsCameraController { float deltaX = -dx * settings.mouseSensitivity; float deltaY = -dy * settings.mouseSensitivity; - yaw += deltaX; + yaw += deltaX; pitch += deltaY; - // clamp pitch to avoid flipping pitch = Math.max(-89f, Math.min(89f, pitch)); - - // keep yaw in [0, 360) if (yaw >= 360f) yaw -= 360f; - if (yaw < 0f) yaw += 360f; + if (yaw < 0f) yaw += 360f; updateCameraDirection(); - // --- keyboard movement - // forward vector (flattened on XZ) + // --- build movement vectors --- tmpForward.set(camera.direction.x, 0f, camera.direction.z).nor(); - // right vector (perpendicular) tmpRight.set(tmpForward.z, 0f, -tmpForward.x).nor(); camera.fieldOfView = settings.fov; @@ -171,48 +164,143 @@ public class FpsCameraController { moveZ -= tmpRight.z * moveAmount; } - float playerRadius = 0.4f; - // --- try to move in X/Z with collision - float newX = camera.position.x + moveX; - float newZ = camera.position.z + moveZ; - float curY = camera.position.y; - - if (!world.collidesAt(newX, curY, camera.position.z, playerRadius, eyeHeight)) { - camera.position.x = newX; - } - - if (!world.collidesAt(camera.position.x, curY, newZ, playerRadius, eyeHeight)) { - camera.position.z = newZ; - } boolean jumpHeld = Gdx.input.isKeyPressed(Input.Keys.SPACE); - // apply gravity + float playerRadius = 0.4f; + + // ========================================================= + // 1) VERTICAL INTEGRATION + GROUND SNAP (capsule-based) + // ========================================================= + + // Apply gravity velocityY += gravity * delta; - float proposedY = camera.position.y + velocityY * delta; - // feet position if we accept this Y - float feetY = proposedY - eyeHeight; + float oldY = camera.position.y; + float proposedY = oldY + velocityY * delta; - // find ground at current X/Z - float groundY = world.getGroundHeightAt(camera.position.x, camera.position.z, 0f); + // Capsule ground at current XZ + float groundY = world.getCapsuleGroundHeight( + camera.position.x, + camera.position.z, + playerRadius, + 0f + ); - if (feetY <= groundY) { - // ground or other surface below feet - grounded = true; - velocityY = 0f; + float feetNow = oldY - eyeHeight; + float feetNext = proposedY - eyeHeight; + final float GROUND_SNAP_EPS = 0.15f; // max distance to snap down + + if (velocityY <= 0f && feetNext <= groundY + GROUND_SNAP_EPS) { + // Falling or resting, and we reached (or slightly passed) the ground + grounded = true; + velocityY = 0f; camera.position.y = groundY + eyeHeight; } else { - grounded = false; + grounded = false; camera.position.y = proposedY; } - // start jump again if holding down key + // Handle jump AFTER we've updated grounded state if (grounded && jumpHeld) { velocityY = jumpSpeed; - grounded = false; + grounded = false; } - // snap position to quantization steps + // ========================================================= + // 2) HORIZONTAL MOVEMENT WITH COLLISION + OPTIONAL STEP UP + // ========================================================= + + // We re-query ground at the start of the frame for step logic + float currentGroundY = world.getCapsuleGroundHeight( + camera.position.x, + camera.position.z, + playerRadius, + 0f + ); + + float curY = camera.position.y; + + // Max height we auto-step; must be < crate height, < house walls, etc + final float maxStepHeight = 0.6f; // your crate is 1.0 high, so you must jump + final float stepSnapEps = 0.05f; // tolerance when landing on step + + // --- X axis --- + if (moveX != 0f) { + float targetX = camera.position.x + moveX; + float targetZ = camera.position.z; + + boolean collides = world.collidesAt( + targetX, + curY, + targetZ, + playerRadius, + eyeHeight + ); + + if (!collides) { + camera.position.x = targetX; + } else if (grounded && velocityY <= 0.01f) { + // Only try to step up if we are actually on the ground (walking), + // NOT while jumping or falling. + float newGroundY = world.getCapsuleGroundHeight( + targetX, + targetZ, + playerRadius, + 0f + ); + float step = newGroundY - currentGroundY; + + if (step > 0f && step <= maxStepHeight) { + // Step up: teleport onto the higher surface + camera.position.x = targetX; + camera.position.y = newGroundY + eyeHeight + stepSnapEps; + curY = camera.position.y; + currentGroundY = newGroundY; + grounded = true; + velocityY = 0f; + } + // else: it's a real wall or too high → ignore X movement + } + } + + // --- Z axis --- + if (moveZ != 0f) { + float targetX = camera.position.x; + float targetZ = camera.position.z + moveZ; + + boolean collides = world.collidesAt( + targetX, + curY, + targetZ, + playerRadius, + eyeHeight + ); + + if (!collides) { + camera.position.z = targetZ; + } else if (grounded && velocityY <= 0.01f) { + float newGroundY = world.getCapsuleGroundHeight( + targetX, + targetZ, + playerRadius, + 0f + ); + float step = newGroundY - currentGroundY; + + if (step > 0f && step <= maxStepHeight) { + camera.position.z = targetZ; + camera.position.y = newGroundY + eyeHeight + stepSnapEps; + curY = camera.position.y; + currentGroundY = newGroundY; + grounded = true; + velocityY = 0f; + } + } + } + + // ========================================================= + // 3) RETRO QUANTIZATION + // ========================================================= if (retroQuantization) { float q = positionQuantize; camera.position.x = Math.round(camera.position.x * q) / q; @@ -220,7 +308,6 @@ public class FpsCameraController { camera.position.z = Math.round(camera.position.z * q) / q; } } - private void updateCameraDirection() { // build direction vector from yaw/pitch float useYaw = yaw; diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/GameSettings.java b/core/src/main/java/wtf/beatrice/retrorender/engine/GameSettings.java index 7010ef1..003466c 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/GameSettings.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/GameSettings.java @@ -2,7 +2,7 @@ package wtf.beatrice.retrorender.engine; public class GameSettings { - public float fov = 67f; // degrees + public float fov = 60f; // degrees public float mouseSensitivity = 0.15f; public float moveSpeed = 5f; diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java b/core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java index e0579af..44749b6 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java @@ -57,7 +57,7 @@ public class ModelLibrary { // generic 2x2x2 cube unitCubeModel = builder.createBox( - 2f, 2f, 2f, + 1f, 1f, 1f, new Material(ColorAttribute.createDiffuse(1f, 1f, 1f, 1f)), VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal ); diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/SettingsMenu.java b/core/src/main/java/wtf/beatrice/retrorender/engine/SettingsMenu.java index a13c971..ef99330 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/SettingsMenu.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/SettingsMenu.java @@ -62,7 +62,7 @@ public class SettingsMenu { pm.dispose(); // default FOV - fov = 67f; + fov = 60f; } public void setFov(float fov) { diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/TownFactory.java b/core/src/main/java/wtf/beatrice/retrorender/engine/TownFactory.java index 482c090..abbcf16 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/TownFactory.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/TownFactory.java @@ -1,7 +1,6 @@ package wtf.beatrice.retrorender.engine; import com.badlogic.gdx.graphics.g3d.ModelInstance; - public class TownFactory { private final ModelLibrary models; @@ -15,32 +14,39 @@ public class TownFactory { // houseBlockModel is 4x3x4 → center at y=1.5 inst.transform.setToTranslation(x, 1.5f, z); - // collider roughly matches the visual: halfExtents = (2,1.5,2) Collider col = Collider.box(2f, 1.5f, 2f); WorldObject obj = new WorldObject(id, inst, col); - obj.staticObject = true; - obj.castsShadow = true; + obj.staticObject = true; + obj.castsShadow = true; + obj.walkableSurface = false; // you don't walk on roofs + obj.collidable = true; // blocks movement return obj; } public WorldObject createCrate(String id, float x, float z) { ModelInstance inst = new ModelInstance(models.unitCubeModel); - // unit cube is 2x2x2 → center at y=1 - inst.transform.setToTranslation(x, 1f, z); + // unit cube is 1x1x1 → center at y=0.5 + inst.transform.setToTranslation(x, 0.5f, z); + + Collider col = Collider.box(0.5f, 0.5f, 0.5f); - Collider col = Collider.box(1f, 1f, 1f); WorldObject obj = new WorldObject(id, inst, col); - obj.staticObject = true; + obj.staticObject = true; + obj.castsShadow = true; + obj.walkableSurface = true; // you can stand on it + obj.collidable = true; // also blocks sides return obj; } public WorldObject createPathTile(String id, float x, float z) { ModelInstance inst = new ModelInstance(models.pathTileModel); inst.transform.setToTranslation(x, 0f, z); - // purely visual, no collider + WorldObject obj = new WorldObject(id, inst, Collider.none()); - obj.staticObject = true; + obj.staticObject = true; + obj.walkableSurface = false; // ground plane already gives height 0 + obj.collidable = false; return obj; } } diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java b/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java index 7ddec9a..a35abe8 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java @@ -119,35 +119,44 @@ public class World3D { } /** - * Returns the ground height (y) at a given XZ position. - * Starts from baseHeight (e.g. 0) and checks if you're standing on top of any BOX collider. + * Returns the ground height (y) for a capsule with given radius whose center + * is at (x, z) in world space. Starts from baseHeight (e.g. 0) and checks + * walkable BOX colliders using a circle-vs-AABB test in XZ. */ - public float getGroundHeightAt(float x, float z, float baseHeight) { - float maxY = baseHeight; // plane at y = 0 + public float getCapsuleGroundHeight(float x, float z, float radius, float baseHeight) { + float maxY = baseHeight; // plane at y = baseHeight for (WorldObject obj : objects) { + if (!obj.walkableSurface) continue; if (obj.collider.type != Collider.Type.BOX) continue; Collider col = obj.collider; - // Inverse transform: world → object local + // world → local tmpInv.set(obj.instance.transform); tmpInv.inv(); - // Player XZ at ground level in world space + // capsule center projected to XZ in local space tmpWorld.set(x, 0f, z); - tmpWorld.mul(tmpInv); // now in local space + tmpWorld.mul(tmpInv); - float px = tmpWorld.x; - float pz = tmpWorld.z; + float cx = tmpWorld.x; + float cz = tmpWorld.z; - float minX = col.center.x - col.halfExtents.x; - float maxX = col.center.x + col.halfExtents.x; - float minZ = col.center.z - col.halfExtents.z; - float maxZ = col.center.z + col.halfExtents.z; + float boxMinX = col.center.x - col.halfExtents.x; + float boxMaxX = col.center.x + col.halfExtents.x; + float boxMinZ = col.center.z - col.halfExtents.z; + float boxMaxZ = col.center.z + col.halfExtents.z; - // inside horizontal footprint in *local* space - if (px < minX || px > maxX || pz < minZ || pz > maxZ) { + // circle (capsule footprint) vs AABB in local XZ + float closestX = Math.max(boxMinX, Math.min(cx, boxMaxX)); + float closestZ = Math.max(boxMinZ, Math.min(cz, boxMaxZ)); + + float dx = cx - closestX; + float dz = cz - closestZ; + + if (dx * dx + dz * dz > radius * radius) { + // no overlap in XZ with this walkable surface continue; } @@ -166,6 +175,13 @@ public class World3D { return maxY; } + /** + * Backwards-compatible point-sample version (radius = 0). + */ + public float getGroundHeightAt(float x, float z, float baseHeight) { + return getCapsuleGroundHeight(x, z, 0f, baseHeight); + } + /** * Simple horizontal collision check between a player and BOX colliders. * Player is approximated as a capsule: radius in XZ, height = eyeHeight. @@ -174,36 +190,41 @@ public class World3D { float feetY = y - eyeHeight; float headY = y; + final float EPS_TOP = 0.05f; + final float EPS_BOTTOM = 0.01f; + for (WorldObject obj : objects) { + if (!obj.collidable) continue; if (obj.collider.type != Collider.Type.BOX) continue; Collider col = obj.collider; - // world → local tmpInv.set(obj.instance.transform); tmpInv.inv(); tmpFeet.set(x, feetY, z).mul(tmpInv); tmpHead.set(x, headY, z).mul(tmpInv); - // For horizontal test, use the "average" x/z of feet/head - float px = 0.5f * (tmpFeet.x + tmpHead.x); - float pz = 0.5f * (tmpFeet.z + tmpHead.z); - float boxMinY = col.center.y - col.halfExtents.y; float boxMaxY = col.center.y + col.halfExtents.y; - // vertical overlap in local space - if (tmpHead.y <= boxMinY || tmpFeet.y >= boxMaxY) { + if (tmpFeet.y >= boxMaxY - EPS_TOP) { + // on / just above top → let groundHeight handle it continue; } + if (tmpHead.y <= boxMinY + EPS_BOTTOM || tmpFeet.y >= boxMaxY) { + continue; + } + + float px = 0.5f * (tmpFeet.x + tmpHead.x); + float pz = 0.5f * (tmpFeet.z + tmpHead.z); + float minX = col.center.x - col.halfExtents.x; float maxX = col.center.x + col.halfExtents.x; float minZ = col.center.z - col.halfExtents.z; float maxZ = col.center.z + col.halfExtents.z; - // circle (player) vs AABB in local XZ float closestX = Math.max(minX, Math.min(px, maxX)); float closestZ = Math.max(minZ, Math.min(pz, maxZ)); diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java b/core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java index 59bcad1..ef9219b 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java @@ -2,7 +2,6 @@ package wtf.beatrice.retrorender.engine; import com.badlogic.gdx.graphics.g3d.ModelInstance; import com.badlogic.gdx.math.Vector3; - public class WorldObject { public final String id; @@ -11,6 +10,10 @@ public class WorldObject { public boolean staticObject = true; public boolean castsShadow = true; + // NEW: gameplay flags + public boolean walkableSurface = false; // contributes to getGroundHeightAt + public boolean collidable = true; // used by collidesAt + // Cached translation for faster queries private final Vector3 tmpPos = new Vector3(); @@ -22,7 +25,8 @@ public class WorldObject { public void update(float delta) { // default: do nothing - // you can subclass or add strategies later for rotating, animating, AI, etc. + // For rotating decoration, set collidable=false + walkableSurface=false + // and rotate instance.transform here if you want. } public Vector3 getPosition(Vector3 out) { diff --git a/lwjgl3/src/main/java/wtf/beatrice/retrorender/lwjgl3/Lwjgl3Launcher.java b/lwjgl3/src/main/java/wtf/beatrice/retrorender/lwjgl3/Lwjgl3Launcher.java index 2b304dd..201efa3 100644 --- a/lwjgl3/src/main/java/wtf/beatrice/retrorender/lwjgl3/Lwjgl3Launcher.java +++ b/lwjgl3/src/main/java/wtf/beatrice/retrorender/lwjgl3/Lwjgl3Launcher.java @@ -28,7 +28,7 @@ public class Lwjgl3Launcher { //// useful for testing performance, but can also be very stressful to some hardware. //// You may also need to configure GPU drivers to fully disable Vsync; this can cause screen tearing. - configuration.setWindowedMode(640, 480); + configuration.setWindowedMode(860, 480); //// You can change these files; they are in lwjgl3/src/main/resources/ . //// They can also be loaded from the root of assets/ .