Compare commits
22 Commits
e47768863f
...
renovate/g
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bf2d69284 | |||
| 7e01fc8bd4 | |||
| 9e86abe60b | |||
| dbbcd7f926 | |||
| a951b3bcb2 | |||
| eb1542e161 | |||
| 94fefa6d2c | |||
| 0c726ed416 | |||
| 7a599ab1e5 | |||
| 398f1fa26f | |||
| db8d78043f | |||
| edc983091b | |||
| 6868f53e38 | |||
| caa59cd72f | |||
| a31c018b92 | |||
| 008bbff99c | |||
| ccb7cd90d0 | |||
| 2d68cabedf | |||
| 6f28e01e5c | |||
| c7313a0487 | |||
| 15b3a17f62 | |||
| 95ef82d757 |
BIN
assets/textures/dirt.png
Normal file
BIN
assets/textures/dirt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
BIN
assets/textures/grass.png
Normal file
BIN
assets/textures/grass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
BIN
assets/textures/moss.png
Normal file
BIN
assets/textures/moss.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
BIN
assets/textures/paving_2.png
Normal file
BIN
assets/textures/paving_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
BIN
assets/textures/rocky_grass.png
Normal file
BIN
assets/textures/rocky_grass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
BIN
assets/textures/rocky_grass_2.png
Normal file
BIN
assets/textures/rocky_grass_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
@@ -1,233 +0,0 @@
|
|||||||
package wtf.beatrice.retrorender;
|
|
||||||
|
|
||||||
import com.badlogic.gdx.Gdx;
|
|
||||||
import com.badlogic.gdx.Input;
|
|
||||||
import com.badlogic.gdx.Screen;
|
|
||||||
import com.badlogic.gdx.graphics.GL20;
|
|
||||||
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
|
||||||
import com.badlogic.gdx.graphics.Texture;
|
|
||||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
|
||||||
import com.badlogic.gdx.graphics.g2d.TextureRegion;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.Environment;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.ModelBatch;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider;
|
|
||||||
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
|
|
||||||
import wtf.beatrice.retrorender.engine.DebugHud;
|
|
||||||
import wtf.beatrice.retrorender.engine.FpsCameraController;
|
|
||||||
import wtf.beatrice.retrorender.engine.World3D;
|
|
||||||
|
|
||||||
public class GameScreen implements Screen {
|
|
||||||
|
|
||||||
private final Main game;
|
|
||||||
|
|
||||||
private PerspectiveCamera camera;
|
|
||||||
private ModelBatch modelBatch;
|
|
||||||
private Environment environment;
|
|
||||||
|
|
||||||
private FpsCameraController cameraController;
|
|
||||||
private World3D world;
|
|
||||||
private DebugHud hud;
|
|
||||||
|
|
||||||
// Shadow Mapping
|
|
||||||
private ModelBatch shadowBatch;
|
|
||||||
private DirectionalShadowLight shadowLight;
|
|
||||||
|
|
||||||
// Retro rendering
|
|
||||||
private FrameBuffer frameBuffer;
|
|
||||||
private TextureRegion frameRegion;
|
|
||||||
private SpriteBatch screenBatch;
|
|
||||||
|
|
||||||
private static final int RETRO_WIDTH = 320;
|
|
||||||
private static final int RETRO_HEIGHT = 240;
|
|
||||||
|
|
||||||
private boolean showHud = false;
|
|
||||||
|
|
||||||
public GameScreen(Main game) {
|
|
||||||
this.game = game;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initCamera() {
|
|
||||||
camera = new PerspectiveCamera(
|
|
||||||
67f,
|
|
||||||
RETRO_WIDTH,
|
|
||||||
RETRO_HEIGHT
|
|
||||||
);
|
|
||||||
|
|
||||||
// near/far + initial direction handled in controller constructor
|
|
||||||
cameraController = new FpsCameraController(camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initEnvironment() {
|
|
||||||
environment = new Environment();
|
|
||||||
|
|
||||||
// ambient color
|
|
||||||
environment.set(
|
|
||||||
new ColorAttribute(ColorAttribute.AmbientLight,
|
|
||||||
0.4f, 0.4f, 0.5f, 1f)
|
|
||||||
);
|
|
||||||
|
|
||||||
// shadow-casting directional light
|
|
||||||
shadowLight = new DirectionalShadowLight(
|
|
||||||
1024, 1024, // shadow map resolution
|
|
||||||
60f, 60f, // viewport size
|
|
||||||
1f, 50f // near/far for the light camera
|
|
||||||
);
|
|
||||||
shadowLight.set(
|
|
||||||
1.0f, 0.85f, 0.9f, // casting light color
|
|
||||||
-0.7f, -1.0f, -0.3f // direction
|
|
||||||
);
|
|
||||||
|
|
||||||
environment.add(shadowLight);
|
|
||||||
environment.shadowMap = shadowLight;
|
|
||||||
|
|
||||||
shadowBatch = new ModelBatch(new DepthShaderProvider());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initWorld() {
|
|
||||||
world = new World3D();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initHud() {
|
|
||||||
hud = new DebugHud();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- screen methods
|
|
||||||
private void initRetroBuffer() {
|
|
||||||
// RGB + depth
|
|
||||||
frameBuffer = new FrameBuffer(
|
|
||||||
com.badlogic.gdx.graphics.Pixmap.Format.RGBA8888,
|
|
||||||
RETRO_WIDTH,
|
|
||||||
RETRO_HEIGHT,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
Texture fbTex = frameBuffer.getColorBufferTexture();
|
|
||||||
frameRegion = new TextureRegion(fbTex);
|
|
||||||
fbTex.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
|
||||||
// FBO textures are Y-flipped in libGDX, so flip once here
|
|
||||||
frameRegion.flip(false, true);
|
|
||||||
|
|
||||||
screenBatch = new com.badlogic.gdx.graphics.g2d.SpriteBatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void show() {
|
|
||||||
modelBatch = new ModelBatch();
|
|
||||||
initCamera();
|
|
||||||
initEnvironment();
|
|
||||||
initWorld();
|
|
||||||
initHud();
|
|
||||||
initRetroBuffer();
|
|
||||||
|
|
||||||
cameraController.onShow();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void render(float delta) {
|
|
||||||
|
|
||||||
cameraController.update(delta, world);
|
|
||||||
world.update(delta);
|
|
||||||
|
|
||||||
if (Gdx.input.isKeyJustPressed(Input.Keys.TAB)) {
|
|
||||||
showHud = !showHud;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- shadow pass: render depth from light's point of view
|
|
||||||
// point to center the shadow camera on;
|
|
||||||
// can be adjusted this later (e.g. follow player).
|
|
||||||
shadowLight.begin(new com.badlogic.gdx.math.Vector3(0f, 0f, 0f), camera.direction);
|
|
||||||
|
|
||||||
shadowBatch.begin(shadowLight.getCamera());
|
|
||||||
world.render(shadowBatch, environment); // depth-only pass
|
|
||||||
shadowBatch.end();
|
|
||||||
|
|
||||||
shadowLight.end();
|
|
||||||
|
|
||||||
// --- render scene into low res framebuffer
|
|
||||||
frameBuffer.begin();
|
|
||||||
Gdx.gl.glViewport(0, 0, RETRO_WIDTH, RETRO_HEIGHT);
|
|
||||||
Gdx.gl.glClearColor(0.5f, 0.6f, 1.0f, 1f);
|
|
||||||
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
|
|
||||||
|
|
||||||
camera.update();
|
|
||||||
modelBatch.begin(camera);
|
|
||||||
world.render(modelBatch, environment); // DefaultShader uses shadowLight + env.shadowMap
|
|
||||||
modelBatch.end();
|
|
||||||
|
|
||||||
// --- HUD
|
|
||||||
if (showHud)
|
|
||||||
hud.render(
|
|
||||||
RETRO_WIDTH,
|
|
||||||
RETRO_HEIGHT,
|
|
||||||
camera,
|
|
||||||
cameraController.getYaw(),
|
|
||||||
cameraController.getPitch());
|
|
||||||
|
|
||||||
frameBuffer.end();
|
|
||||||
|
|
||||||
// -- scale framebuffer to screen, preserve aspect ratio --
|
|
||||||
int windowW = Gdx.graphics.getWidth(); // logical size
|
|
||||||
int windowH = Gdx.graphics.getHeight();
|
|
||||||
|
|
||||||
int backW = Gdx.graphics.getBackBufferWidth(); // actual GL framebuffer
|
|
||||||
int backH = Gdx.graphics.getBackBufferHeight();
|
|
||||||
|
|
||||||
// compute how much we can scale the FBO into the window
|
|
||||||
float scale = Math.min(
|
|
||||||
windowW / (float) RETRO_WIDTH,
|
|
||||||
windowH / (float) RETRO_HEIGHT
|
|
||||||
);
|
|
||||||
|
|
||||||
// strict integer scaling
|
|
||||||
// scale = (float)Math.max(1, Math.floor(scale));
|
|
||||||
|
|
||||||
int drawW = Math.round(RETRO_WIDTH * scale);
|
|
||||||
int drawH = Math.round(RETRO_HEIGHT * scale);
|
|
||||||
|
|
||||||
// center the image
|
|
||||||
int offsetX = (windowW - drawW) / 2;
|
|
||||||
int offsetY = (windowH - drawH) / 2;
|
|
||||||
|
|
||||||
// viewport in backbuffer coordinates
|
|
||||||
Gdx.gl.glViewport(0, 0, backW, backH);
|
|
||||||
Gdx.gl.glClearColor(0f, 0f, 0f, 1f);
|
|
||||||
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
// projection in window (logical) coordinates
|
|
||||||
screenBatch.getProjectionMatrix().setToOrtho2D(0, 0, windowW, windowH);
|
|
||||||
|
|
||||||
screenBatch.begin();
|
|
||||||
screenBatch.draw(frameRegion, offsetX, offsetY, drawW, drawH);
|
|
||||||
screenBatch.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void resize(int width, int height) {
|
|
||||||
camera.viewportWidth = RETRO_WIDTH;
|
|
||||||
camera.viewportHeight = RETRO_HEIGHT;
|
|
||||||
camera.update();
|
|
||||||
|
|
||||||
cameraController.resize(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void pause() { }
|
|
||||||
@Override public void resume() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void hide() {
|
|
||||||
cameraController.onHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
cameraController.dispose();
|
|
||||||
world.dispose();
|
|
||||||
modelBatch.dispose();
|
|
||||||
hud.dispose();
|
|
||||||
|
|
||||||
shadowBatch.dispose();
|
|
||||||
shadowLight.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,7 @@
|
|||||||
package wtf.beatrice.retrorender;
|
package wtf.beatrice.retrorender;
|
||||||
|
|
||||||
import com.badlogic.gdx.ApplicationAdapter;
|
|
||||||
import com.badlogic.gdx.Game;
|
import com.badlogic.gdx.Game;
|
||||||
import com.badlogic.gdx.Gdx;
|
import wtf.beatrice.retrorender.engine.render.GameScreen;
|
||||||
import com.badlogic.gdx.graphics.*;
|
|
||||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.*;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
|
|
||||||
import com.badlogic.gdx.math.Vector3;
|
|
||||||
import com.badlogic.gdx.utils.ScreenUtils;
|
|
||||||
|
|
||||||
/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */
|
/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */
|
||||||
public class Main extends Game
|
public class Main extends Game
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
package wtf.beatrice.retrorender.engine;
|
|
||||||
|
|
||||||
import com.badlogic.gdx.Gdx;
|
|
||||||
import com.badlogic.gdx.Input;
|
|
||||||
import com.badlogic.gdx.graphics.Cursor;
|
|
||||||
import com.badlogic.gdx.graphics.Pixmap;
|
|
||||||
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
|
||||||
import com.badlogic.gdx.math.Vector3;
|
|
||||||
|
|
||||||
public class FpsCameraController {
|
|
||||||
|
|
||||||
private final PerspectiveCamera camera;
|
|
||||||
|
|
||||||
// movement + look
|
|
||||||
private float moveSpeed = 5f; // units / second
|
|
||||||
private float mouseSensitivity = 0.15f;
|
|
||||||
private float yaw = 0f;
|
|
||||||
private float pitch = 0f;
|
|
||||||
|
|
||||||
private float eyeHeight = 1.8f;
|
|
||||||
private float velocityY = 0f;
|
|
||||||
private float gravity = -50f;
|
|
||||||
private float jumpSpeed = 12f;
|
|
||||||
private boolean grounded = false;
|
|
||||||
|
|
||||||
private final Vector3 tmpForward = new Vector3();
|
|
||||||
private final Vector3 tmpRight = new Vector3();
|
|
||||||
|
|
||||||
// mouse capture / centering
|
|
||||||
private boolean mouseCaptured = true;
|
|
||||||
private int centerX;
|
|
||||||
private int centerY;
|
|
||||||
private int captureWarmupFrames = 0;
|
|
||||||
private static final int WARMUP_FRAMES = 6; // frames to ignore after capture
|
|
||||||
|
|
||||||
private Cursor invisibleCursor;
|
|
||||||
|
|
||||||
public FpsCameraController(PerspectiveCamera camera) {
|
|
||||||
this.camera = camera;
|
|
||||||
|
|
||||||
// default camera setup
|
|
||||||
camera.position.set(0f, eyeHeight, 6f);
|
|
||||||
camera.near = 0.1f;
|
|
||||||
camera.far = 100f;
|
|
||||||
|
|
||||||
yaw = 180f;
|
|
||||||
pitch = -15f;
|
|
||||||
updateCameraDirection();
|
|
||||||
|
|
||||||
centerX = Gdx.graphics.getWidth() / 2;
|
|
||||||
centerY = Gdx.graphics.getHeight() / 2;
|
|
||||||
|
|
||||||
grounded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Call from Screen.show() */
|
|
||||||
public void onShow() {
|
|
||||||
mouseCaptured = true;
|
|
||||||
centerX = Gdx.graphics.getWidth() / 2;
|
|
||||||
centerY = Gdx.graphics.getHeight() / 2;
|
|
||||||
Gdx.input.setCursorPosition(centerX, centerY);
|
|
||||||
|
|
||||||
ensureInvisibleCursor();
|
|
||||||
Gdx.graphics.setCursor(invisibleCursor);
|
|
||||||
|
|
||||||
captureWarmupFrames = WARMUP_FRAMES;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Call from Screen.hide() */
|
|
||||||
public void onHide() {
|
|
||||||
mouseCaptured = false;
|
|
||||||
Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Call from Screen.resize() */
|
|
||||||
public void resize(int width, int height) {
|
|
||||||
centerX = width / 2;
|
|
||||||
centerY = height / 2;
|
|
||||||
if (mouseCaptured) {
|
|
||||||
Gdx.input.setCursorPosition(centerX, centerY);
|
|
||||||
}
|
|
||||||
// after resize, skip frames to avoid mouse skew
|
|
||||||
captureWarmupFrames = WARMUP_FRAMES;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Update each frame with delta time */
|
|
||||||
public void update(float delta, World3D world) {
|
|
||||||
// --- ESC: release mouse, stop movement / camera
|
|
||||||
if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
|
|
||||||
mouseCaptured = false;
|
|
||||||
Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
|
|
||||||
}
|
|
||||||
|
|
||||||
// only capture inputs if focused/captured
|
|
||||||
if (!mouseCaptured) {
|
|
||||||
if (Gdx.input.justTouched()) { // if clicked
|
|
||||||
mouseCaptured = true;
|
|
||||||
centerX = Gdx.graphics.getWidth() / 2;
|
|
||||||
centerY = Gdx.graphics.getHeight() / 2;
|
|
||||||
Gdx.input.setCursorPosition(centerX, centerY);
|
|
||||||
ensureInvisibleCursor();
|
|
||||||
Gdx.graphics.setCursor(invisibleCursor);
|
|
||||||
captureWarmupFrames = WARMUP_FRAMES;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip a few frames after capture to avoid mouse skew
|
|
||||||
// teleport cursor to center
|
|
||||||
if (captureWarmupFrames > 0) {
|
|
||||||
captureWarmupFrames--;
|
|
||||||
Gdx.input.setCursorPosition(centerX, centerY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- mouse look: keep cursor centred
|
|
||||||
int mouseX = Gdx.input.getX();
|
|
||||||
int mouseY = Gdx.input.getY();
|
|
||||||
|
|
||||||
int dx = mouseX - centerX;
|
|
||||||
int dy = mouseY - centerY;
|
|
||||||
|
|
||||||
// recenter for next frame
|
|
||||||
Gdx.input.setCursorPosition(centerX, centerY);
|
|
||||||
|
|
||||||
float deltaX = -dx * mouseSensitivity;
|
|
||||||
float deltaY = -dy * mouseSensitivity;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
updateCameraDirection();
|
|
||||||
|
|
||||||
// --- keyboard movement
|
|
||||||
// forward vector (flattened on XZ)
|
|
||||||
tmpForward.set(camera.direction.x, 0f, camera.direction.z).nor();
|
|
||||||
// right vector (perpendicular)
|
|
||||||
tmpRight.set(tmpForward.z, 0f, -tmpForward.x).nor();
|
|
||||||
|
|
||||||
float moveAmount = moveSpeed * delta;
|
|
||||||
float moveX = 0f;
|
|
||||||
float moveZ = 0f;
|
|
||||||
|
|
||||||
if (Gdx.input.isKeyPressed(Input.Keys.W)) {
|
|
||||||
moveX += tmpForward.x * moveAmount;
|
|
||||||
moveZ += tmpForward.z * moveAmount;
|
|
||||||
}
|
|
||||||
if (Gdx.input.isKeyPressed(Input.Keys.S)) {
|
|
||||||
moveX -= tmpForward.x * moveAmount;
|
|
||||||
moveZ -= tmpForward.z * moveAmount;
|
|
||||||
}
|
|
||||||
if (Gdx.input.isKeyPressed(Input.Keys.A)) {
|
|
||||||
moveX += tmpRight.x * moveAmount;
|
|
||||||
moveZ += tmpRight.z * moveAmount;
|
|
||||||
}
|
|
||||||
if (Gdx.input.isKeyPressed(Input.Keys.D)) {
|
|
||||||
moveX -= tmpRight.x * moveAmount;
|
|
||||||
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
|
|
||||||
velocityY += gravity * delta;
|
|
||||||
float proposedY = camera.position.y + velocityY * delta;
|
|
||||||
|
|
||||||
// feet position if we accept this Y
|
|
||||||
float feetY = proposedY - eyeHeight;
|
|
||||||
|
|
||||||
// find ground at current X/Z
|
|
||||||
float groundY = world.getGroundHeightAt(camera.position.x, camera.position.z, 0f);
|
|
||||||
|
|
||||||
if (feetY <= groundY) {
|
|
||||||
// ground or other surface below feet
|
|
||||||
grounded = true;
|
|
||||||
velocityY = 0f;
|
|
||||||
camera.position.y = groundY + eyeHeight;
|
|
||||||
} else {
|
|
||||||
grounded = false;
|
|
||||||
camera.position.y = proposedY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start jump again if holding down key
|
|
||||||
if (grounded && jumpHeld) {
|
|
||||||
velocityY = jumpSpeed;
|
|
||||||
grounded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCameraDirection() {
|
|
||||||
// build direction vector from yaw/pitch
|
|
||||||
float yawRad = (float) Math.toRadians(yaw);
|
|
||||||
float pitchRad = (float) Math.toRadians(pitch);
|
|
||||||
|
|
||||||
float x = (float) (Math.cos(pitchRad) * Math.sin(yawRad));
|
|
||||||
float y = (float) Math.sin(pitchRad);
|
|
||||||
float z = (float) (Math.cos(pitchRad) * Math.cos(yawRad));
|
|
||||||
|
|
||||||
camera.direction.set(x, y, z).nor();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureInvisibleCursor() {
|
|
||||||
if (invisibleCursor == null) {
|
|
||||||
Pixmap pm = new Pixmap(16, 16, Pixmap.Format.RGBA8888);
|
|
||||||
pm.setColor(0, 0, 0, 0);
|
|
||||||
pm.fill();
|
|
||||||
invisibleCursor = Gdx.graphics.newCursor(pm, 0, 0);
|
|
||||||
pm.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose() {
|
|
||||||
if (invisibleCursor != null) {
|
|
||||||
invisibleCursor.dispose();
|
|
||||||
invisibleCursor = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getYaw() { return yaw; }
|
|
||||||
public float getPitch() { return pitch; }
|
|
||||||
|
|
||||||
public void setMoveSpeed(float moveSpeed) { this.moveSpeed = moveSpeed; }
|
|
||||||
public void setMouseSensitivity(float mouseSensitivity) { this.mouseSensitivity = mouseSensitivity; }
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
package wtf.beatrice.retrorender.engine;
|
|
||||||
|
|
||||||
import com.badlogic.gdx.Gdx;
|
|
||||||
import com.badlogic.gdx.graphics.GL20;
|
|
||||||
import com.badlogic.gdx.graphics.Texture;
|
|
||||||
import com.badlogic.gdx.graphics.VertexAttributes;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.*;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
|
|
||||||
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
|
|
||||||
import com.badlogic.gdx.math.Vector3;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class World3D {
|
|
||||||
|
|
||||||
// textures
|
|
||||||
private final Texture groundTexture;
|
|
||||||
|
|
||||||
// models (base mesh)
|
|
||||||
private final Model groundModel;
|
|
||||||
private final Model cubeModel;
|
|
||||||
|
|
||||||
// instances
|
|
||||||
private final ModelInstance groundInstance;
|
|
||||||
private final List<ModelInstance> cubeInstances = new ArrayList<>();
|
|
||||||
|
|
||||||
public World3D() {
|
|
||||||
ModelBuilder modelBuilder = new ModelBuilder();
|
|
||||||
|
|
||||||
// --- load floor texture
|
|
||||||
groundTexture = new Texture(Gdx.files.internal("textures/paving.png"));
|
|
||||||
groundTexture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
|
||||||
groundTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
|
|
||||||
|
|
||||||
// --- create ground material
|
|
||||||
Material groundMat = new Material(
|
|
||||||
ColorAttribute.createDiffuse(1f, 1f, 1f, 1f),
|
|
||||||
TextureAttribute.createDiffuse(groundTexture)
|
|
||||||
);
|
|
||||||
|
|
||||||
// plane size (halfSize in X/Z)
|
|
||||||
float halfSize = 32f;
|
|
||||||
|
|
||||||
// times the texture repeats across the whole plane
|
|
||||||
float tileScale = 16f;
|
|
||||||
|
|
||||||
modelBuilder.begin();
|
|
||||||
|
|
||||||
MeshPartBuilder mpb = modelBuilder.part(
|
|
||||||
"ground",
|
|
||||||
GL20.GL_TRIANGLES,
|
|
||||||
VertexAttributes.Usage.Position
|
|
||||||
| VertexAttributes.Usage.Normal
|
|
||||||
| VertexAttributes.Usage.TextureCoordinates, // 👈 important
|
|
||||||
groundMat
|
|
||||||
);
|
|
||||||
|
|
||||||
// set UV range so texture repeats tileScale times
|
|
||||||
mpb.setUVRange(0f, 0f, tileScale, tileScale);
|
|
||||||
|
|
||||||
// build the quad
|
|
||||||
mpb.rect(
|
|
||||||
-halfSize, 0f, halfSize, // top-left
|
|
||||||
halfSize, 0f, halfSize, // top-right
|
|
||||||
halfSize, 0f, -halfSize, // bottom-right
|
|
||||||
-halfSize, 0f, -halfSize, // bottom-left
|
|
||||||
0f, 1f, 0f // normal up
|
|
||||||
);
|
|
||||||
|
|
||||||
groundModel = modelBuilder.end();
|
|
||||||
groundInstance = new ModelInstance(groundModel);
|
|
||||||
|
|
||||||
// --- create reusable cube model
|
|
||||||
cubeModel = modelBuilder.createBox(
|
|
||||||
2f, 2f, 2f,
|
|
||||||
new Material(ColorAttribute.createDiffuse(1f, 1f, 1f, 1f)),
|
|
||||||
VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- place random cubes in the world
|
|
||||||
// center
|
|
||||||
cubeInstances.add(new ModelInstance(cubeModel)); // at origin
|
|
||||||
|
|
||||||
// four around
|
|
||||||
cubeInstances.add(new ModelInstance(cubeModel));
|
|
||||||
cubeInstances.get(1).transform.setToTranslation(8f, 1f, 8f);
|
|
||||||
|
|
||||||
cubeInstances.add(new ModelInstance(cubeModel));
|
|
||||||
cubeInstances.get(2).transform.setToTranslation(-8f, 1f, 8f);
|
|
||||||
|
|
||||||
cubeInstances.add(new ModelInstance(cubeModel));
|
|
||||||
cubeInstances.get(3).transform.setToTranslation(8f, 1f, -8f);
|
|
||||||
|
|
||||||
cubeInstances.add(new ModelInstance(cubeModel));
|
|
||||||
cubeInstances.get(4).transform.setToTranslation(-8f, 1f, -8f);
|
|
||||||
|
|
||||||
// one tall pillar in front of you
|
|
||||||
ModelInstance pillar = new ModelInstance(cubeModel);
|
|
||||||
pillar.transform
|
|
||||||
.setToScaling(1f, 3f, 1f)
|
|
||||||
.translate(0f, 3f, -10f);
|
|
||||||
cubeInstances.add(pillar);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(float delta) {
|
|
||||||
// rotate cubes around y axis
|
|
||||||
for (ModelInstance instance : cubeInstances) {
|
|
||||||
instance.transform.rotate(Vector3.Y, 20f * delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void render(ModelBatch batch, Environment environment) {
|
|
||||||
// draw ground
|
|
||||||
batch.render(groundInstance, environment);
|
|
||||||
// draw cubes
|
|
||||||
for (ModelInstance instance : cubeInstances) {
|
|
||||||
batch.render(instance, environment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 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 cube.
|
|
||||||
*/
|
|
||||||
public float getGroundHeightAt(float x, float z, float baseHeight) {
|
|
||||||
float maxY = baseHeight; // ground plane at y = 0
|
|
||||||
|
|
||||||
// a cube is 2 units tall, from y = 0 to y = 2
|
|
||||||
float cubeHalfSize = 1f;
|
|
||||||
float cubeTopY = 2f;
|
|
||||||
|
|
||||||
Vector3 tmp = new Vector3();
|
|
||||||
for (ModelInstance cube : cubeInstances) {
|
|
||||||
cube.transform.getTranslation(tmp);
|
|
||||||
|
|
||||||
float minX = tmp.x - cubeHalfSize;
|
|
||||||
float maxX = tmp.x + cubeHalfSize;
|
|
||||||
float minZ = tmp.z - cubeHalfSize;
|
|
||||||
float maxZ = tmp.z + cubeHalfSize;
|
|
||||||
|
|
||||||
if (x >= minX && x <= maxX && z >= minZ && z <= maxZ) {
|
|
||||||
if (cubeTopY > maxY) {
|
|
||||||
maxY = cubeTopY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple horizontal collision check between a player and cube sides.
|
|
||||||
* Player is approximated as a capsule: radius in XZ, height = eyeHeight.
|
|
||||||
*/
|
|
||||||
public boolean collidesAt(float x, float y, float z, float radius, float eyeHeight) {
|
|
||||||
float feetY = y - eyeHeight;
|
|
||||||
float headY = y;
|
|
||||||
|
|
||||||
float cubeHalfSize = 1f;
|
|
||||||
float cubeBottomY = 0f;
|
|
||||||
float cubeTopY = 2f;
|
|
||||||
|
|
||||||
Vector3 tmp = new Vector3();
|
|
||||||
for (ModelInstance cube : cubeInstances) {
|
|
||||||
cube.transform.getTranslation(tmp);
|
|
||||||
|
|
||||||
float minX = tmp.x - cubeHalfSize;
|
|
||||||
float maxX = tmp.x + cubeHalfSize;
|
|
||||||
float minZ = tmp.z - cubeHalfSize;
|
|
||||||
float maxZ = tmp.z + cubeHalfSize;
|
|
||||||
|
|
||||||
// no vertical overlap -> ignore
|
|
||||||
if (headY <= cubeBottomY || feetY >= cubeTopY) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// circle (player) vs AABB (cube) in XZ
|
|
||||||
boolean overlapX = (x + radius > minX) && (x - radius < maxX);
|
|
||||||
boolean overlapZ = (z + radius > minZ) && (z - radius < maxZ);
|
|
||||||
|
|
||||||
if (overlapX && overlapZ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose() {
|
|
||||||
groundModel.dispose();
|
|
||||||
cubeModel.dispose();
|
|
||||||
groundTexture.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.config;
|
||||||
|
|
||||||
|
public class GameSettings {
|
||||||
|
|
||||||
|
public float fov = 60f; // degrees
|
||||||
|
public float mouseSensitivity = 0.15f;
|
||||||
|
public float moveSpeed = 5f;
|
||||||
|
|
||||||
|
public final float minFov = 40f;
|
||||||
|
public final float maxFov = 100f;
|
||||||
|
public final float fovStep = 2f;
|
||||||
|
|
||||||
|
public boolean bobbingEnabled = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.render;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight;
|
||||||
|
import com.badlogic.gdx.math.MathUtils;
|
||||||
|
import com.badlogic.gdx.math.Vector3;
|
||||||
|
|
||||||
|
public class DayNightCycle {
|
||||||
|
|
||||||
|
private final DirectionalShadowLight sun;
|
||||||
|
private final ColorAttribute ambientLight;
|
||||||
|
|
||||||
|
// base colors
|
||||||
|
private final Color dayAmbient = new Color(0.6f, 0.7f, 0.7f, 1f);
|
||||||
|
private final Color nightAmbient = new Color(0.2f, 0.15f, 0.2f, 1f);
|
||||||
|
|
||||||
|
private final Color daySunColor = new Color(1.0f, 0.9f, 1f, 1f);
|
||||||
|
private final Color nightSunColor = new Color(0.4f, 0.4f, 0.6f, 1f);
|
||||||
|
|
||||||
|
private final Color daySkyColor = new Color(0.8f, 0.9f, 1.0f, 1f);
|
||||||
|
private final Color nightSkyColor = new Color(0.1f, 0.01f, 0.2f, 1f);
|
||||||
|
|
||||||
|
private final Color currentSkyColor = new Color();
|
||||||
|
private final Color tmpSunColor = new Color();
|
||||||
|
private final Vector3 tmpDir = new Vector3();
|
||||||
|
private final Vector3 sunDirection = new Vector3();
|
||||||
|
|
||||||
|
// Rotate around Z so the sun path is tilted relative to world up.
|
||||||
|
private final Vector3 tiltAxis = new Vector3(0f, 0f, 1f).nor();
|
||||||
|
private float axialTilt = 30f; // in degrees
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If false (default), simple equator-style model:
|
||||||
|
* timeOfDay=0 -> midnight
|
||||||
|
* timeOfDay=0.25-> sunrise
|
||||||
|
* timeOfDay=0.5 -> noon
|
||||||
|
* timeOfDay=0.75-> sunset
|
||||||
|
*
|
||||||
|
* Brightness is driven purely by this ideal curve; Z-tilt only affects
|
||||||
|
* visual direction (shadow slant), not timing of day/night.
|
||||||
|
*
|
||||||
|
* If true, brightness and day length depend on the final tilted direction,
|
||||||
|
* so axialTiltDeg affects how long days are and how high the sun gets.
|
||||||
|
*/
|
||||||
|
private boolean seasonal = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0–1: 0 = midnight, 0.25 = sunrise, 0.5 = noon, 0.75 = sunset.
|
||||||
|
*/
|
||||||
|
private float timeOfDay = 0.25f;
|
||||||
|
|
||||||
|
/** Seconds for one full 24h cycle. */
|
||||||
|
private float dayLengthSeconds = 60 * 5f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daylight / altitude factor in [0, 1].
|
||||||
|
* 0 = "sun below horizon / no direct light"
|
||||||
|
* 1 = "sun as high/bright as it gets for current configuration".
|
||||||
|
*/
|
||||||
|
private float sunAltitude = 0f;
|
||||||
|
|
||||||
|
public DayNightCycle(DirectionalShadowLight sun, ColorAttribute ambientLight) {
|
||||||
|
this.sun = sun;
|
||||||
|
this.ambientLight = ambientLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float delta) {
|
||||||
|
if (dayLengthSeconds <= 0f) return;
|
||||||
|
|
||||||
|
// Advance time of day
|
||||||
|
timeOfDay = (timeOfDay + delta / dayLengthSeconds) % 1f;
|
||||||
|
|
||||||
|
// 0..2π over a day
|
||||||
|
float angle = timeOfDay * MathUtils.PI2;
|
||||||
|
|
||||||
|
// Ideal equator curve:
|
||||||
|
// t=0 -> midnight (height=-1)
|
||||||
|
// t=0.25-> sunrise (height=0)
|
||||||
|
// t=0.5 -> noon (height=1)
|
||||||
|
// t=0.75-> sunset (height=0)
|
||||||
|
float orbitAngle = angle - MathUtils.PI / 2f;
|
||||||
|
float height = MathUtils.sin(orbitAngle); // -1..1
|
||||||
|
float horizontal = MathUtils.cos(orbitAngle); // -1..1
|
||||||
|
|
||||||
|
// Base direction before Z-tilt, as if on equator.
|
||||||
|
float baseX = -horizontal * 0.7f;
|
||||||
|
float baseY = -height; // negative when sun is above
|
||||||
|
float baseZ = -horizontal * 0.3f;
|
||||||
|
|
||||||
|
if (!seasonal) {
|
||||||
|
// === Simple "equator-style" mode ===
|
||||||
|
|
||||||
|
float sunFactor = MathUtils.clamp(height, 0f, 1f); // 0..1
|
||||||
|
sunAltitude = sunFactor; // 👈 always 0..1
|
||||||
|
|
||||||
|
float ambientFactor = 0.25f + 0.75f * sunFactor;
|
||||||
|
|
||||||
|
// Visual direction with axial tilt
|
||||||
|
tmpDir.set(baseX, baseY, baseZ).nor();
|
||||||
|
tmpDir.rotate(tiltAxis, axialTilt);
|
||||||
|
sunDirection.set(tmpDir);
|
||||||
|
|
||||||
|
// Sun color / intensity
|
||||||
|
tmpSunColor.set(nightSunColor).lerp(daySunColor, sunFactor);
|
||||||
|
tmpSunColor.mul(sunFactor);
|
||||||
|
|
||||||
|
sun.set(tmpSunColor.r, tmpSunColor.g, tmpSunColor.b,
|
||||||
|
tmpDir.x, tmpDir.y, tmpDir.z);
|
||||||
|
|
||||||
|
// Ambient + sky
|
||||||
|
ambientLight.color.set(nightAmbient).lerp(dayAmbient, ambientFactor);
|
||||||
|
currentSkyColor.set(nightSkyColor).lerp(daySkyColor, ambientFactor);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// === Seasonal / physical mode ===
|
||||||
|
|
||||||
|
tmpDir.set(baseX, baseY, baseZ).nor();
|
||||||
|
tmpDir.rotate(tiltAxis, axialTilt);
|
||||||
|
sunDirection.set(tmpDir);
|
||||||
|
|
||||||
|
// True altitude from final direction: 0..1
|
||||||
|
float altitude = Math.max(-tmpDir.y, 0f);
|
||||||
|
sunAltitude = altitude; // 👈 also 0..1
|
||||||
|
|
||||||
|
float sunFactor = altitude;
|
||||||
|
float ambientFactor = 0.25f + 0.75f * altitude;
|
||||||
|
|
||||||
|
tmpSunColor.set(nightSunColor).lerp(daySunColor, sunFactor);
|
||||||
|
tmpSunColor.mul(sunFactor);
|
||||||
|
|
||||||
|
sun.set(tmpSunColor.r, tmpSunColor.g, tmpSunColor.b,
|
||||||
|
tmpDir.x, tmpDir.y, tmpDir.z);
|
||||||
|
|
||||||
|
ambientLight.color.set(nightAmbient).lerp(dayAmbient, ambientFactor);
|
||||||
|
currentSkyColor.set(nightSkyColor).lerp(daySkyColor, ambientFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color getSkyColor() {
|
||||||
|
return currentSkyColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getTimeOfDay() {
|
||||||
|
return timeOfDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daylight / altitude factor in [0, 1].
|
||||||
|
* 0 = no direct sunlight, 1 = max noon brightness.
|
||||||
|
*/
|
||||||
|
public float getSunAltitude() {
|
||||||
|
return sunAltitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeOfDay(float t) {
|
||||||
|
timeOfDay = MathUtils.clamp(t, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** World-space direction the sun is shining *from*. */
|
||||||
|
public Vector3 getSunDirection() {
|
||||||
|
return sunDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if we consider the sun above the horizon (i.e. there should be direct light / shadows). */
|
||||||
|
public boolean isSunAboveHorizon() {
|
||||||
|
return sunAltitude > 0f; // strictly > 0; you can use >= 0.01f if you want a tiny cutoff
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getAxialTilt()
|
||||||
|
{
|
||||||
|
return axialTilt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAxialTilt(float degrees)
|
||||||
|
{
|
||||||
|
axialTilt = degrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getDayLength()
|
||||||
|
{
|
||||||
|
return dayLengthSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDayLength(float seconds)
|
||||||
|
{
|
||||||
|
dayLengthSeconds = seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.render;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
import com.badlogic.gdx.Input;
|
||||||
|
import com.badlogic.gdx.graphics.Cursor;
|
||||||
|
import com.badlogic.gdx.graphics.Pixmap;
|
||||||
|
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
||||||
|
import com.badlogic.gdx.math.Vector3;
|
||||||
|
import wtf.beatrice.retrorender.engine.config.GameSettings;
|
||||||
|
import wtf.beatrice.retrorender.engine.world.World3D;
|
||||||
|
|
||||||
|
public class FpsCameraController {
|
||||||
|
|
||||||
|
private final PerspectiveCamera camera;
|
||||||
|
private final GameSettings settings;
|
||||||
|
|
||||||
|
// movement + look
|
||||||
|
private float yaw = 0f;
|
||||||
|
private float pitch = 0f;
|
||||||
|
|
||||||
|
private final float eyeHeight = 1.8f;
|
||||||
|
private final float jumpSpeed = 12f;
|
||||||
|
private final float gravity = -50f;
|
||||||
|
private float velocityY = 0f;
|
||||||
|
private boolean grounded = false;
|
||||||
|
|
||||||
|
// view bobbing
|
||||||
|
private float bobTime = 0f;
|
||||||
|
private float bobOffsetY = 0f;
|
||||||
|
private final float bobAmount = 0.04f; // amplitude in world units
|
||||||
|
private final float bobSpeed = 16f;
|
||||||
|
|
||||||
|
// --- approximation toggles
|
||||||
|
private final boolean retroQuantization = false;
|
||||||
|
|
||||||
|
// how coarse the rotation is (0.25f = round to 0.25°)
|
||||||
|
private final float yawStepDegrees = 0.2f;
|
||||||
|
private final float pitchStepDegrees = 0.2f;
|
||||||
|
|
||||||
|
// how coarse the position is (16f = steps of 1/16 units)
|
||||||
|
private final float positionQuantize = 64f;
|
||||||
|
|
||||||
|
private final Vector3 tmpForward = new Vector3();
|
||||||
|
private final Vector3 tmpRight = new Vector3();
|
||||||
|
|
||||||
|
// mouse capture / centering
|
||||||
|
private boolean mouseCaptured = true;
|
||||||
|
private int centerX;
|
||||||
|
private int centerY;
|
||||||
|
private int captureWarmupFrames = 0;
|
||||||
|
private static final int WARMUP_FRAMES = 10; // frames to ignore after capture
|
||||||
|
|
||||||
|
private Cursor invisibleCursor;
|
||||||
|
|
||||||
|
public FpsCameraController(PerspectiveCamera camera, GameSettings settings) {
|
||||||
|
this.camera = camera;
|
||||||
|
this.settings = settings;
|
||||||
|
|
||||||
|
camera.position.set(0f, eyeHeight, 6f);
|
||||||
|
camera.near = 0.1f;
|
||||||
|
camera.far = 100f;
|
||||||
|
|
||||||
|
yaw = 180f;
|
||||||
|
pitch = -15f;
|
||||||
|
updateCameraDirection();
|
||||||
|
|
||||||
|
centerX = Gdx.graphics.getWidth() / 2;
|
||||||
|
centerY = Gdx.graphics.getHeight() / 2;
|
||||||
|
|
||||||
|
grounded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call from Screen.show() */
|
||||||
|
public void onShow() {
|
||||||
|
captureMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call from Screen.hide() */
|
||||||
|
public void onHide() {
|
||||||
|
releaseMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Explicitly capture mouse (used when resuming from pause). */
|
||||||
|
public void captureMouse() {
|
||||||
|
mouseCaptured = true;
|
||||||
|
centerX = Gdx.graphics.getWidth() / 2;
|
||||||
|
centerY = Gdx.graphics.getHeight() / 2;
|
||||||
|
Gdx.input.setCursorPosition(centerX, centerY);
|
||||||
|
|
||||||
|
ensureInvisibleCursor();
|
||||||
|
Gdx.graphics.setCursor(invisibleCursor);
|
||||||
|
|
||||||
|
captureWarmupFrames = WARMUP_FRAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Explicitly release mouse (used when pausing). */
|
||||||
|
public void releaseMouse() {
|
||||||
|
mouseCaptured = false;
|
||||||
|
Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call from Screen.resize() */
|
||||||
|
public void resize(int width, int height) {
|
||||||
|
centerX = width / 2;
|
||||||
|
centerY = height / 2;
|
||||||
|
if (mouseCaptured) {
|
||||||
|
Gdx.input.setCursorPosition(centerX, centerY);
|
||||||
|
}
|
||||||
|
// after resize, skip frames to avoid mouse skew
|
||||||
|
captureWarmupFrames = WARMUP_FRAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update each frame with delta time */
|
||||||
|
public void update(float delta, World3D world) {
|
||||||
|
if (settings.bobbingEnabled) {
|
||||||
|
camera.position.y -= bobOffsetY;
|
||||||
|
bobOffsetY = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mouseCaptured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// warmup: ignore mouse for a few frames after capture
|
||||||
|
if (captureWarmupFrames > 0) {
|
||||||
|
captureWarmupFrames--;
|
||||||
|
Gdx.input.setCursorPosition(centerX, centerY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- mouse look ---
|
||||||
|
int mouseX = Gdx.input.getX();
|
||||||
|
int mouseY = Gdx.input.getY();
|
||||||
|
|
||||||
|
int dx = mouseX - centerX;
|
||||||
|
int dy = mouseY - centerY;
|
||||||
|
|
||||||
|
// recenter for next frame
|
||||||
|
Gdx.input.setCursorPosition(centerX, centerY);
|
||||||
|
|
||||||
|
float deltaX = -dx * settings.mouseSensitivity;
|
||||||
|
float deltaY = -dy * settings.mouseSensitivity;
|
||||||
|
|
||||||
|
yaw += deltaX;
|
||||||
|
pitch += deltaY;
|
||||||
|
|
||||||
|
pitch = Math.max(-89f, Math.min(89f, pitch));
|
||||||
|
if (yaw >= 360f) yaw -= 360f;
|
||||||
|
if (yaw < 0f) yaw += 360f;
|
||||||
|
|
||||||
|
updateCameraDirection();
|
||||||
|
|
||||||
|
// --- build movement vectors ---
|
||||||
|
tmpForward.set(camera.direction.x, 0f, camera.direction.z).nor();
|
||||||
|
tmpRight.set(tmpForward.z, 0f, -tmpForward.x).nor();
|
||||||
|
|
||||||
|
camera.fieldOfView = settings.fov;
|
||||||
|
|
||||||
|
float moveAmount = settings.moveSpeed * delta;
|
||||||
|
float moveX = 0f;
|
||||||
|
float moveZ = 0f;
|
||||||
|
|
||||||
|
if (Gdx.input.isKeyPressed(Input.Keys.W)) {
|
||||||
|
moveX += tmpForward.x * moveAmount;
|
||||||
|
moveZ += tmpForward.z * moveAmount;
|
||||||
|
}
|
||||||
|
if (Gdx.input.isKeyPressed(Input.Keys.S)) {
|
||||||
|
moveX -= tmpForward.x * moveAmount;
|
||||||
|
moveZ -= tmpForward.z * moveAmount;
|
||||||
|
}
|
||||||
|
if (Gdx.input.isKeyPressed(Input.Keys.A)) {
|
||||||
|
moveX += tmpRight.x * moveAmount;
|
||||||
|
moveZ += tmpRight.z * moveAmount;
|
||||||
|
}
|
||||||
|
if (Gdx.input.isKeyPressed(Input.Keys.D)) {
|
||||||
|
moveX -= tmpRight.x * moveAmount;
|
||||||
|
moveZ -= tmpRight.z * moveAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean jumpHeld = Gdx.input.isKeyPressed(Input.Keys.SPACE);
|
||||||
|
|
||||||
|
float playerRadius = 0.4f;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 1) VERTICAL INTEGRATION + GROUND SNAP (capsule-based)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
// Apply gravity
|
||||||
|
velocityY += gravity * delta;
|
||||||
|
|
||||||
|
float oldY = camera.position.y;
|
||||||
|
float proposedY = oldY + velocityY * delta;
|
||||||
|
|
||||||
|
// Capsule ground at current XZ
|
||||||
|
float groundY = world.getCapsuleGroundHeight(
|
||||||
|
camera.position.x,
|
||||||
|
camera.position.z,
|
||||||
|
playerRadius,
|
||||||
|
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;
|
||||||
|
camera.position.y = proposedY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle jump AFTER we've updated grounded state
|
||||||
|
if (grounded && jumpHeld) {
|
||||||
|
velocityY = jumpSpeed;
|
||||||
|
grounded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 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;
|
||||||
|
camera.position.y = Math.round(camera.position.y * q) / q;
|
||||||
|
camera.position.z = Math.round(camera.position.z * q) / q;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 4) VIEW BOBBING (purely visual)
|
||||||
|
// =========================================================
|
||||||
|
// horizontal movement magnitude (world units per frame)
|
||||||
|
if (settings.bobbingEnabled) {
|
||||||
|
float horizLen = (float)Math.sqrt(moveX * moveX + moveZ * moveZ);
|
||||||
|
|
||||||
|
if (grounded && horizLen > 0.0001f) {
|
||||||
|
// scale bob speed by how fast you're moving
|
||||||
|
float speedFactor = horizLen / (settings.moveSpeed * delta + 1e-6f);
|
||||||
|
bobTime += delta * bobSpeed * speedFactor;
|
||||||
|
|
||||||
|
bobOffsetY = (float)Math.sin(bobTime) * bobAmount;
|
||||||
|
} else {
|
||||||
|
// not moving or in the air → relax bobbing back to zero
|
||||||
|
bobTime = 0f;
|
||||||
|
// optional: smooth return to 0 instead of snapping
|
||||||
|
bobOffsetY += (0f - bobOffsetY) * 10f * delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.position.y += bobOffsetY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void updateCameraDirection() {
|
||||||
|
// build direction vector from yaw/pitch
|
||||||
|
float useYaw = yaw;
|
||||||
|
float usePitch = pitch;
|
||||||
|
|
||||||
|
if (retroQuantization) {
|
||||||
|
// snap yaw/pitch to fixed angular steps
|
||||||
|
useYaw = Math.round(useYaw / yawStepDegrees) * yawStepDegrees;
|
||||||
|
usePitch = Math.round(usePitch / pitchStepDegrees) * pitchStepDegrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
float yawRad = (float) Math.toRadians(useYaw);
|
||||||
|
float pitchRad = (float) Math.toRadians(usePitch);
|
||||||
|
|
||||||
|
float x = (float) (Math.cos(pitchRad) * Math.sin(yawRad));
|
||||||
|
float y = (float) Math.sin(pitchRad);
|
||||||
|
float z = (float) (Math.cos(pitchRad) * Math.cos(yawRad));
|
||||||
|
|
||||||
|
camera.direction.set(x, y, z).nor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureInvisibleCursor() {
|
||||||
|
if (invisibleCursor == null) {
|
||||||
|
Pixmap pm = new Pixmap(16, 16, Pixmap.Format.RGBA8888);
|
||||||
|
pm.setColor(0, 0, 0, 0);
|
||||||
|
pm.fill();
|
||||||
|
invisibleCursor = Gdx.graphics.newCursor(pm, 0, 0);
|
||||||
|
pm.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
if (invisibleCursor != null) {
|
||||||
|
invisibleCursor.dispose();
|
||||||
|
invisibleCursor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getYaw() { return yaw; }
|
||||||
|
public float getPitch() { return pitch; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.render;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
import com.badlogic.gdx.Input;
|
||||||
|
import com.badlogic.gdx.Screen;
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.graphics.GL20;
|
||||||
|
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
||||||
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.TextureRegion;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.Environment;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.ModelBatch;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider;
|
||||||
|
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
|
||||||
|
import com.badlogic.gdx.math.Vector3;
|
||||||
|
import wtf.beatrice.retrorender.Main;
|
||||||
|
import wtf.beatrice.retrorender.engine.config.GameSettings;
|
||||||
|
import wtf.beatrice.retrorender.engine.world.ModelLibrary;
|
||||||
|
import wtf.beatrice.retrorender.engine.ui.DebugHud;
|
||||||
|
import wtf.beatrice.retrorender.engine.ui.GameUi;
|
||||||
|
import wtf.beatrice.retrorender.engine.ui.UiMode;
|
||||||
|
import wtf.beatrice.retrorender.engine.world.World3D;
|
||||||
|
|
||||||
|
public class GameScreen implements Screen {
|
||||||
|
|
||||||
|
private final Main game;
|
||||||
|
|
||||||
|
private PerspectiveCamera camera;
|
||||||
|
private ModelBatch modelBatch;
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
private ColorAttribute ambientLight;
|
||||||
|
private DayNightCycle dayNightCycle;
|
||||||
|
|
||||||
|
private FpsCameraController cameraController;
|
||||||
|
private World3D world;
|
||||||
|
private DebugHud hud;
|
||||||
|
|
||||||
|
private boolean showHud = false;
|
||||||
|
|
||||||
|
// menus
|
||||||
|
private GameSettings settings;
|
||||||
|
private GameUi gameUi;
|
||||||
|
private UiMode uiMode = UiMode.GAMEPLAY;
|
||||||
|
|
||||||
|
// Shadow Mapping
|
||||||
|
private ModelBatch shadowBatch;
|
||||||
|
private DirectionalShadowLight shadowLight;
|
||||||
|
|
||||||
|
// Retro rendering
|
||||||
|
private FrameBuffer frameBuffer;
|
||||||
|
private TextureRegion frameRegion;
|
||||||
|
private SpriteBatch screenBatch;
|
||||||
|
|
||||||
|
// world
|
||||||
|
private ModelLibrary modelLibrary;
|
||||||
|
|
||||||
|
|
||||||
|
private static final int RETRO_WIDTH = 430;
|
||||||
|
private static final int RETRO_HEIGHT = 240;
|
||||||
|
|
||||||
|
public GameScreen(Main game) {
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCamera() {
|
||||||
|
settings = new GameSettings();
|
||||||
|
camera = new PerspectiveCamera(settings.fov, RETRO_WIDTH, RETRO_HEIGHT);
|
||||||
|
cameraController = new FpsCameraController(camera, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initEnvironment() {
|
||||||
|
environment = new Environment();
|
||||||
|
|
||||||
|
// keep a handle to ambient so we can animate it
|
||||||
|
ambientLight = new ColorAttribute(
|
||||||
|
ColorAttribute.AmbientLight,
|
||||||
|
0.4f, 0.4f, 0.5f, 1f
|
||||||
|
);
|
||||||
|
environment.set(ambientLight);
|
||||||
|
|
||||||
|
// shadow-casting directional light (our sun)
|
||||||
|
shadowLight = new DirectionalShadowLight(
|
||||||
|
1024, 1024, // shadow map resolution
|
||||||
|
60f, 60f, // viewport size
|
||||||
|
1f, 100f // near/far for the light camera
|
||||||
|
);
|
||||||
|
|
||||||
|
environment.add(shadowLight);
|
||||||
|
environment.shadowMap = shadowLight;
|
||||||
|
|
||||||
|
shadowBatch = new ModelBatch(new DepthShaderProvider());
|
||||||
|
|
||||||
|
// create the cycle controller
|
||||||
|
dayNightCycle = new DayNightCycle(shadowLight, ambientLight);
|
||||||
|
// optional: start at morning/noon/etc
|
||||||
|
dayNightCycle.setTimeOfDay(0.2f); // good sun
|
||||||
|
dayNightCycle.setAxialTilt(30f);
|
||||||
|
dayNightCycle.setDayLength(10f); // 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initWorld() {
|
||||||
|
modelLibrary = new ModelLibrary();
|
||||||
|
world = new World3D(modelLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initMenus() {
|
||||||
|
hud = new DebugHud(dayNightCycle);
|
||||||
|
gameUi = new GameUi(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initRetroBuffer() {
|
||||||
|
frameBuffer = new FrameBuffer(
|
||||||
|
com.badlogic.gdx.graphics.Pixmap.Format.RGBA8888,
|
||||||
|
RETRO_WIDTH,
|
||||||
|
RETRO_HEIGHT,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
Texture fbTex = frameBuffer.getColorBufferTexture();
|
||||||
|
frameRegion = new TextureRegion(fbTex);
|
||||||
|
fbTex.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
||||||
|
frameRegion.flip(false, true);
|
||||||
|
|
||||||
|
screenBatch = new SpriteBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show() {
|
||||||
|
modelBatch = new ModelBatch();
|
||||||
|
initCamera();
|
||||||
|
initEnvironment();
|
||||||
|
initWorld();
|
||||||
|
initMenus();
|
||||||
|
initRetroBuffer();
|
||||||
|
|
||||||
|
cameraController.onShow(); // captures mouse with warmup
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(float delta) {
|
||||||
|
|
||||||
|
// TAB: toggle hud
|
||||||
|
if (Gdx.input.isKeyJustPressed(Input.Keys.TAB)) {
|
||||||
|
showHud = !showHud;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC: toggle pause and mouse capture
|
||||||
|
if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
|
||||||
|
UiMode newMode = gameUi.onEsc(uiMode);
|
||||||
|
boolean goingToGameplay = (newMode == UiMode.GAMEPLAY);
|
||||||
|
uiMode = newMode;
|
||||||
|
|
||||||
|
if (uiMode == UiMode.GAMEPLAY) {
|
||||||
|
cameraController.captureMouse();
|
||||||
|
} else {
|
||||||
|
cameraController.releaseMouse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean gameplay = (uiMode == UiMode.GAMEPLAY);
|
||||||
|
|
||||||
|
if (gameplay) {
|
||||||
|
cameraController.update(delta, world);
|
||||||
|
world.update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update day/night (you can choose to run even when paused if you prefer)
|
||||||
|
if (dayNightCycle != null && gameplay) {
|
||||||
|
dayNightCycle.update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- shadow pass ---
|
||||||
|
boolean doShadows = (dayNightCycle == null) || dayNightCycle.isSunAboveHorizon();
|
||||||
|
|
||||||
|
if (doShadows) {
|
||||||
|
if (dayNightCycle != null) {
|
||||||
|
Vector3 center = new Vector3(camera.position.x, 0f, camera.position.z);
|
||||||
|
Vector3 lightDir = dayNightCycle.getSunDirection();
|
||||||
|
|
||||||
|
shadowLight.begin(center, lightDir);
|
||||||
|
} else {
|
||||||
|
shadowLight.begin(new Vector3(0f, 0f, 0f), new Vector3(-0.7f, -1f, -0.3f));
|
||||||
|
}
|
||||||
|
|
||||||
|
shadowBatch.begin(shadowLight.getCamera());
|
||||||
|
world.render(shadowBatch, environment); // depth-only pass
|
||||||
|
shadowBatch.end();
|
||||||
|
shadowLight.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- render scene into low res framebuffer ---
|
||||||
|
frameBuffer.begin();
|
||||||
|
Gdx.gl.glViewport(0, 0, RETRO_WIDTH, RETRO_HEIGHT);
|
||||||
|
if (dayNightCycle != null) {
|
||||||
|
Color sky = dayNightCycle.getSkyColor();
|
||||||
|
Gdx.gl.glClearColor(sky.r, sky.g, sky.b, 1f);
|
||||||
|
} else {
|
||||||
|
Gdx.gl.glClearColor(0.5f, 0.6f, 1.0f, 1f);
|
||||||
|
}
|
||||||
|
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
camera.update();
|
||||||
|
modelBatch.begin(camera);
|
||||||
|
world.render(modelBatch, environment);
|
||||||
|
modelBatch.end();
|
||||||
|
|
||||||
|
// HUD
|
||||||
|
if (showHud) {
|
||||||
|
hud.render(
|
||||||
|
RETRO_WIDTH,
|
||||||
|
RETRO_HEIGHT,
|
||||||
|
camera,
|
||||||
|
cameraController.getYaw(),
|
||||||
|
cameraController.getPitch()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pause UI
|
||||||
|
if (uiMode != UiMode.GAMEPLAY) {
|
||||||
|
gameUi.render(RETRO_WIDTH, RETRO_HEIGHT, uiMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
frameBuffer.end();
|
||||||
|
|
||||||
|
// -- scale framebuffer to screen, preserve aspect ratio --
|
||||||
|
int windowW = Gdx.graphics.getWidth();
|
||||||
|
int windowH = Gdx.graphics.getHeight();
|
||||||
|
int backW = Gdx.graphics.getBackBufferWidth();
|
||||||
|
int backH = Gdx.graphics.getBackBufferHeight();
|
||||||
|
|
||||||
|
RetroViewportHelper.RetroViewport vp =
|
||||||
|
RetroViewportHelper.computeViewport(windowW, windowH, RETRO_WIDTH, RETRO_HEIGHT);
|
||||||
|
|
||||||
|
// click → UI
|
||||||
|
if (uiMode != UiMode.GAMEPLAY && Gdx.input.justTouched()) {
|
||||||
|
int mouseX = Gdx.input.getX();
|
||||||
|
int mouseY = Gdx.input.getY();
|
||||||
|
|
||||||
|
RetroViewportHelper.RetroClick rc =
|
||||||
|
RetroViewportHelper.toRetroCoords(
|
||||||
|
vp,
|
||||||
|
RETRO_WIDTH, RETRO_HEIGHT,
|
||||||
|
mouseX, mouseY,
|
||||||
|
windowH
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rc.inside) {
|
||||||
|
GameUi.UiResult result = gameUi.handleClick(rc.x, rc.y, uiMode);
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case RESUME:
|
||||||
|
uiMode = UiMode.GAMEPLAY;
|
||||||
|
cameraController.captureMouse();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPEN_SETTINGS:
|
||||||
|
uiMode = UiMode.SETTINGS;
|
||||||
|
// mouse already released when we entered PAUSE, so nothing to do
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLOSE_SETTINGS:
|
||||||
|
uiMode = UiMode.PAUSE;
|
||||||
|
// stay in pause, mouse remains free
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QUIT:
|
||||||
|
Gdx.app.exit();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NONE:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewport in backbuffer coordinates
|
||||||
|
// final draw using vp.offsetX / vp.offsetY / vp.drawW / vp.drawH
|
||||||
|
Gdx.gl.glViewport(0, 0, backW, backH);
|
||||||
|
screenBatch.getProjectionMatrix().setToOrtho2D(0, 0, windowW, windowH);
|
||||||
|
|
||||||
|
screenBatch.begin();
|
||||||
|
screenBatch.draw(frameRegion, vp.offsetX, vp.offsetY, vp.drawW, vp.drawH);
|
||||||
|
screenBatch.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resize(int width, int height) {
|
||||||
|
camera.viewportWidth = RETRO_WIDTH;
|
||||||
|
camera.viewportHeight = RETRO_HEIGHT;
|
||||||
|
camera.update();
|
||||||
|
|
||||||
|
cameraController.resize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void pause() { }
|
||||||
|
@Override public void resume() { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hide() {
|
||||||
|
cameraController.onHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
cameraController.dispose();
|
||||||
|
world.dispose();
|
||||||
|
modelBatch.dispose();
|
||||||
|
hud.dispose();
|
||||||
|
shadowBatch.dispose();
|
||||||
|
shadowLight.dispose();
|
||||||
|
if (gameUi != null) gameUi.dispose();
|
||||||
|
if (frameBuffer != null) frameBuffer.dispose();
|
||||||
|
if (screenBatch != null) screenBatch.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.render;
|
||||||
|
|
||||||
|
public class RetroViewportHelper {
|
||||||
|
|
||||||
|
public static class RetroViewport {
|
||||||
|
public int offsetX, offsetY;
|
||||||
|
public int drawW, drawH;
|
||||||
|
public float scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RetroClick {
|
||||||
|
public boolean inside;
|
||||||
|
public float x, y; // retro coords
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RetroViewport computeViewport(
|
||||||
|
int windowW, int windowH,
|
||||||
|
int retroW, int retroH) {
|
||||||
|
|
||||||
|
RetroViewport vp = new RetroViewport();
|
||||||
|
|
||||||
|
float scale = Math.min(
|
||||||
|
windowW / (float) retroW,
|
||||||
|
windowH / (float) retroH
|
||||||
|
);
|
||||||
|
|
||||||
|
vp.scale = scale;
|
||||||
|
vp.drawW = Math.round(retroW * scale);
|
||||||
|
vp.drawH = Math.round(retroH * scale);
|
||||||
|
vp.offsetX = (windowW - vp.drawW) / 2;
|
||||||
|
vp.offsetY = (windowH - vp.drawH) / 2;
|
||||||
|
|
||||||
|
return vp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RetroClick toRetroCoords(
|
||||||
|
RetroViewport vp,
|
||||||
|
int retroW, int retroH,
|
||||||
|
int mouseX, int mouseYWindow,
|
||||||
|
int windowHeight) {
|
||||||
|
|
||||||
|
RetroClick rc = new RetroClick();
|
||||||
|
// libGDX gives mouse from top-left; you already invert Y like this:
|
||||||
|
int yBottomOrigin = windowHeight - mouseYWindow;
|
||||||
|
|
||||||
|
if (mouseX < vp.offsetX || mouseX > vp.offsetX + vp.drawW ||
|
||||||
|
yBottomOrigin < vp.offsetY || yBottomOrigin > vp.offsetY + vp.drawH) {
|
||||||
|
rc.inside = false;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
float relX = (mouseX - vp.offsetX) / (float) vp.drawW;
|
||||||
|
float relY = (yBottomOrigin - vp.offsetY) / (float) vp.drawH;
|
||||||
|
|
||||||
|
rc.inside = true;
|
||||||
|
rc.x = relX * retroW;
|
||||||
|
rc.y = relY * retroH;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
package wtf.beatrice.retrorender.engine;
|
package wtf.beatrice.retrorender.engine.ui;
|
||||||
|
|
||||||
import com.badlogic.gdx.graphics.Color;
|
import com.badlogic.gdx.graphics.Color;
|
||||||
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
||||||
|
import com.badlogic.gdx.graphics.Pixmap;
|
||||||
import com.badlogic.gdx.graphics.Texture;
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
||||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||||
import com.badlogic.gdx.Gdx;
|
import com.badlogic.gdx.Gdx;
|
||||||
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
|
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
|
||||||
|
import wtf.beatrice.retrorender.engine.render.DayNightCycle;
|
||||||
|
|
||||||
public class DebugHud {
|
public class DebugHud {
|
||||||
|
|
||||||
private final SpriteBatch batch;
|
private final SpriteBatch batch;
|
||||||
private final BitmapFont font;
|
private final BitmapFont font;
|
||||||
|
private DayNightCycle dayNightCycle;
|
||||||
|
private final Texture pixel;
|
||||||
|
|
||||||
|
public DebugHud(DayNightCycle dayNightCycle) {
|
||||||
|
this.dayNightCycle = dayNightCycle;
|
||||||
|
|
||||||
public DebugHud() {
|
|
||||||
batch = new SpriteBatch();
|
batch = new SpriteBatch();
|
||||||
|
|
||||||
FreeTypeFontGenerator generator =
|
FreeTypeFontGenerator generator =
|
||||||
@@ -38,6 +44,13 @@ public class DebugHud {
|
|||||||
|
|
||||||
font = generator.generateFont(param);
|
font = generator.generateFont(param);
|
||||||
generator.dispose();
|
generator.dispose();
|
||||||
|
|
||||||
|
// 1x1 pixel
|
||||||
|
Pixmap pm = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
|
||||||
|
pm.setColor(1f, 1f, 1f, 1f);
|
||||||
|
pm.fill();
|
||||||
|
pixel = new Texture(pm);
|
||||||
|
pm.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// render HUD in virtual resolution
|
// render HUD in virtual resolution
|
||||||
@@ -49,24 +62,54 @@ public class DebugHud {
|
|||||||
|
|
||||||
batch.begin();
|
batch.begin();
|
||||||
|
|
||||||
String fpsText = Gdx.graphics.getFramesPerSecond() + " FPS";
|
|
||||||
font.draw(batch, fpsText, 5f, height - 5f);
|
|
||||||
|
|
||||||
if (camera != null) {
|
if (camera != null) {
|
||||||
float x = camera.position.x;
|
float x = camera.position.x;
|
||||||
float y = camera.position.y;
|
float y = camera.position.y;
|
||||||
float z = camera.position.z;
|
float z = camera.position.z;
|
||||||
|
|
||||||
String posText = String.format("X: %.2f Y: %.2f Z: %.2f", x, y, z);
|
float panelW = 275f;
|
||||||
|
float panelH = 45f;
|
||||||
|
float panelX = 2f;
|
||||||
|
float panelY = height - panelH - 2f;
|
||||||
|
|
||||||
|
// background
|
||||||
|
batch.setColor(0f, 0f, 0f, 0.10f);
|
||||||
|
batch.draw(pixel, panelX, panelY, panelW, panelH);
|
||||||
|
|
||||||
|
String fpsText = Gdx.graphics.getFramesPerSecond() + " FPS";
|
||||||
|
font.draw(batch, fpsText, 5f, height - 5f);
|
||||||
|
|
||||||
|
String posText = String.format("X: %.3f Y: %.3f Z: %.3f", x, y, z);
|
||||||
font.draw(batch, posText, 5f, height - 15f);
|
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);
|
font.draw(batch, angText, 5f, height - 25f);
|
||||||
|
|
||||||
|
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();
|
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() {
|
public void dispose() {
|
||||||
batch.dispose();
|
batch.dispose();
|
||||||
font.dispose();
|
font.dispose();
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.ui;
|
||||||
|
|
||||||
|
import wtf.beatrice.retrorender.engine.config.GameSettings;
|
||||||
|
|
||||||
|
public class GameUi {
|
||||||
|
|
||||||
|
public enum UiResult {
|
||||||
|
NONE,
|
||||||
|
RESUME,
|
||||||
|
OPEN_SETTINGS,
|
||||||
|
CLOSE_SETTINGS,
|
||||||
|
QUIT
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PauseMenu pauseMenu;
|
||||||
|
private final SettingsMenu settingsMenu;
|
||||||
|
private final GameSettings settings;
|
||||||
|
|
||||||
|
public GameUi(GameSettings settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
this.pauseMenu = new PauseMenu();
|
||||||
|
this.settingsMenu = new SettingsMenu(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UiMode onEsc(UiMode current) {
|
||||||
|
switch (current) {
|
||||||
|
case GAMEPLAY:
|
||||||
|
return UiMode.PAUSE;
|
||||||
|
case PAUSE:
|
||||||
|
case SETTINGS:
|
||||||
|
return UiMode.GAMEPLAY;
|
||||||
|
default:
|
||||||
|
return UiMode.GAMEPLAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(int retroW, int retroH, UiMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case PAUSE:
|
||||||
|
pauseMenu.render(retroW, retroH);
|
||||||
|
break;
|
||||||
|
case SETTINGS:
|
||||||
|
settingsMenu.render(retroW, retroH);
|
||||||
|
break;
|
||||||
|
case GAMEPLAY:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UiResult handleClick(float retroX, float retroY, UiMode mode) {
|
||||||
|
if (mode == UiMode.SETTINGS) {
|
||||||
|
boolean close = settingsMenu.handleClick(retroX, retroY);
|
||||||
|
// always sync FOV to settings
|
||||||
|
settings.fov = settingsMenu.getFov();
|
||||||
|
settings.bobbingEnabled = settingsMenu.isBobbingEnabled();
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
// Settings “Close” button clicked -> go back to PAUSE
|
||||||
|
return UiResult.CLOSE_SETTINGS;
|
||||||
|
}
|
||||||
|
return UiResult.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == UiMode.PAUSE) {
|
||||||
|
PauseMenu.MenuAction action = pauseMenu.getActionAt(retroX, retroY);
|
||||||
|
return switch (action)
|
||||||
|
{
|
||||||
|
case RESUME -> UiResult.RESUME;
|
||||||
|
case QUIT -> UiResult.QUIT;
|
||||||
|
case SETTINGS -> UiResult.OPEN_SETTINGS;
|
||||||
|
default -> UiResult.NONE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return UiResult.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
pauseMenu.dispose();
|
||||||
|
settingsMenu.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SettingsMenu getSettingsMenu() {
|
||||||
|
return settingsMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.ui;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.graphics.Pixmap;
|
||||||
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PauseMenu {
|
||||||
|
|
||||||
|
private final SpriteBatch batch;
|
||||||
|
private final BitmapFont font;
|
||||||
|
private final Texture pixel;
|
||||||
|
|
||||||
|
// simple action enum for the menu
|
||||||
|
public enum MenuAction {
|
||||||
|
NONE,
|
||||||
|
RESUME,
|
||||||
|
SETTINGS,
|
||||||
|
QUIT
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple button model
|
||||||
|
private static class Button {
|
||||||
|
String label;
|
||||||
|
MenuAction action;
|
||||||
|
float x, y, w, h;
|
||||||
|
|
||||||
|
Button(String label, MenuAction action) {
|
||||||
|
this.label = label;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<Button> buttons = new ArrayList<>();
|
||||||
|
|
||||||
|
public PauseMenu() {
|
||||||
|
batch = new SpriteBatch();
|
||||||
|
|
||||||
|
// Same style as DebugHud
|
||||||
|
FreeTypeFontGenerator generator =
|
||||||
|
new FreeTypeFontGenerator(Gdx.files.internal("fonts/red-hat-mono.ttf"));
|
||||||
|
FreeTypeFontGenerator.FreeTypeFontParameter param =
|
||||||
|
new FreeTypeFontGenerator.FreeTypeFontParameter();
|
||||||
|
|
||||||
|
param.size = 12;
|
||||||
|
param.color = Color.WHITE;
|
||||||
|
param.mono = true;
|
||||||
|
param.hinting = FreeTypeFontGenerator.Hinting.None;
|
||||||
|
param.borderWidth = 0f;
|
||||||
|
param.borderColor = Color.WHITE;
|
||||||
|
param.shadowOffsetX = 0;
|
||||||
|
param.shadowOffsetY = 0;
|
||||||
|
param.minFilter = Texture.TextureFilter.Nearest;
|
||||||
|
param.magFilter = Texture.TextureFilter.Nearest;
|
||||||
|
|
||||||
|
font = generator.generateFont(param);
|
||||||
|
generator.dispose();
|
||||||
|
|
||||||
|
// create buttons (ordered left → right)
|
||||||
|
buttons.add(new Button(" Resume ", MenuAction.RESUME));
|
||||||
|
buttons.add(new Button(" Settings ", MenuAction.SETTINGS));
|
||||||
|
buttons.add(new Button(" Quit ", MenuAction.QUIT));
|
||||||
|
|
||||||
|
// 1x1 pixel texture (white, we tint it)
|
||||||
|
Pixmap pm = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
|
||||||
|
pm.setColor(1f, 1f, 1f, 1f);
|
||||||
|
pm.fill();
|
||||||
|
pixel = new Texture(pm);
|
||||||
|
pm.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(int width, int height) {
|
||||||
|
batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
|
||||||
|
batch.begin();
|
||||||
|
|
||||||
|
// --- black bar at bottom ---
|
||||||
|
float barH = 80f; // in retro pixels
|
||||||
|
float barY = 0f;
|
||||||
|
|
||||||
|
batch.setColor(0f, 0f, 0f, 0.9f);
|
||||||
|
batch.draw(pixel, 0f, barY, width, barH);
|
||||||
|
|
||||||
|
// --- buttons ---
|
||||||
|
float spacing = 10f; // horizontal space between buttons
|
||||||
|
|
||||||
|
// measure each button label
|
||||||
|
GlyphLayout layout = new GlyphLayout();
|
||||||
|
float totalWidth = 0f;
|
||||||
|
float maxButtonH = 0f;
|
||||||
|
|
||||||
|
for (Button b : buttons) {
|
||||||
|
layout.setText(font, b.label);
|
||||||
|
float bw = layout.width + 16f; // padding
|
||||||
|
float bh = layout.height + 12f;
|
||||||
|
b.w = bw;
|
||||||
|
b.h = bh;
|
||||||
|
totalWidth += bw;
|
||||||
|
if (bh > maxButtonH) maxButtonH = bh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add spacing between buttons
|
||||||
|
if (!buttons.isEmpty()) {
|
||||||
|
totalWidth += spacing * (buttons.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
float startX = (width - totalWidth) / 2f;
|
||||||
|
float centerY = barY + (barH - maxButtonH) / 2f;
|
||||||
|
|
||||||
|
// draw each button
|
||||||
|
float currentX = startX;
|
||||||
|
for (Button b : buttons) {
|
||||||
|
b.x = currentX;
|
||||||
|
b.y = centerY;
|
||||||
|
|
||||||
|
// button background
|
||||||
|
batch.setColor(0.1f, 0.1f, 0.1f, 1f);
|
||||||
|
batch.draw(pixel, b.x, b.y, b.w, b.h);
|
||||||
|
|
||||||
|
// simple 1px white border
|
||||||
|
batch.setColor(1f, 1f, 1f, 1f);
|
||||||
|
float border = 1f;
|
||||||
|
// top
|
||||||
|
batch.draw(pixel, b.x, b.y + b.h - border, b.w, border);
|
||||||
|
// bottom
|
||||||
|
batch.draw(pixel, b.x, b.y, b.w, border);
|
||||||
|
// left
|
||||||
|
batch.draw(pixel, b.x, b.y, border, b.h);
|
||||||
|
// right
|
||||||
|
batch.draw(pixel, b.x + b.w - border, b.y, border, b.h);
|
||||||
|
|
||||||
|
// text
|
||||||
|
layout.setText(font, b.label);
|
||||||
|
float textX = b.x + (b.w - layout.width) / 2f;
|
||||||
|
float textY = b.y + b.h - (b.h - layout.height) / 2f - 2f;
|
||||||
|
font.draw(batch, layout, textX, textY);
|
||||||
|
|
||||||
|
currentX += b.w + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MenuAction getActionAt(float x, float y) {
|
||||||
|
for (Button b : buttons) {
|
||||||
|
if (x >= b.x && x <= b.x + b.w &&
|
||||||
|
y >= b.y && y <= b.y + b.h) {
|
||||||
|
return b.action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MenuAction.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
batch.dispose();
|
||||||
|
font.dispose();
|
||||||
|
pixel.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.ui;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.graphics.Pixmap;
|
||||||
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
|
||||||
|
import com.badlogic.gdx.math.MathUtils;
|
||||||
|
import wtf.beatrice.retrorender.engine.config.GameSettings;
|
||||||
|
|
||||||
|
public class SettingsMenu {
|
||||||
|
|
||||||
|
private final SpriteBatch batch;
|
||||||
|
private final BitmapFont font;
|
||||||
|
private final BitmapFont boldFont;
|
||||||
|
private final Texture pixel;
|
||||||
|
|
||||||
|
private final GameSettings settings;
|
||||||
|
|
||||||
|
private boolean bobbingEnabled = true;
|
||||||
|
|
||||||
|
// button / hit areas
|
||||||
|
private float fovDecX, fovDecY, fovDecW, fovDecH;
|
||||||
|
private float fovIncX, fovIncY, fovIncW, fovIncH;
|
||||||
|
private float closeX, closeY, closeW, closeH;
|
||||||
|
|
||||||
|
public SettingsMenu(GameSettings settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
|
||||||
|
batch = new SpriteBatch();
|
||||||
|
|
||||||
|
FreeTypeFontGenerator generator =
|
||||||
|
new FreeTypeFontGenerator(Gdx.files.internal("fonts/red-hat-mono.ttf"));
|
||||||
|
FreeTypeFontGenerator.FreeTypeFontParameter param =
|
||||||
|
new FreeTypeFontGenerator.FreeTypeFontParameter();
|
||||||
|
|
||||||
|
param.size = 12;
|
||||||
|
param.color = Color.WHITE;
|
||||||
|
param.mono = true;
|
||||||
|
param.hinting = FreeTypeFontGenerator.Hinting.None;
|
||||||
|
param.borderWidth = 0f;
|
||||||
|
param.borderColor = Color.WHITE;
|
||||||
|
param.shadowOffsetX = 0;
|
||||||
|
param.shadowOffsetY = 0;
|
||||||
|
param.minFilter = Texture.TextureFilter.Nearest;
|
||||||
|
param.magFilter = Texture.TextureFilter.Nearest;
|
||||||
|
|
||||||
|
font = generator.generateFont(param);
|
||||||
|
|
||||||
|
param.borderWidth = 0.2f;
|
||||||
|
boldFont = generator.generateFont(param);
|
||||||
|
|
||||||
|
generator.dispose();
|
||||||
|
|
||||||
|
// 1x1 pixel
|
||||||
|
Pixmap pm = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
|
||||||
|
pm.setColor(1f, 1f, 1f, 1f);
|
||||||
|
pm.fill();
|
||||||
|
pixel = new Texture(pm);
|
||||||
|
pm.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFov(float fov) {
|
||||||
|
settings.fov = MathUtils.clamp(fov, settings.minFov, settings.maxFov);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getFov() {
|
||||||
|
return settings.fov;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBobbingEnabled()
|
||||||
|
{
|
||||||
|
return bobbingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBobbingEnabled(boolean enabled){
|
||||||
|
bobbingEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param width RETRO_WIDTH
|
||||||
|
* @param height RETRO_HEIGHT
|
||||||
|
*/
|
||||||
|
public void render(int width, int height) {
|
||||||
|
batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
|
||||||
|
batch.begin();
|
||||||
|
|
||||||
|
GlyphLayout layout = new GlyphLayout();
|
||||||
|
|
||||||
|
// --- central panel ---
|
||||||
|
float panelW = 300f;
|
||||||
|
float panelH = 220f;
|
||||||
|
float panelX = (width - panelW) / 2f;
|
||||||
|
float panelY = (height - panelH) / 2f;
|
||||||
|
|
||||||
|
// background
|
||||||
|
batch.setColor(0f, 0f, 0f, 0.9f);
|
||||||
|
batch.draw(pixel, panelX, panelY, panelW, panelH);
|
||||||
|
|
||||||
|
// border
|
||||||
|
batch.setColor(1f, 1f, 1f, 1f);
|
||||||
|
float border = 1f;
|
||||||
|
|
||||||
|
// --- title ---
|
||||||
|
String title = "Settings";
|
||||||
|
layout.setText(boldFont, title);
|
||||||
|
float titleX = panelX + (panelW - layout.width) / 2f;
|
||||||
|
float titleY = panelY + panelH - 10f;
|
||||||
|
boldFont.draw(batch, layout, titleX, titleY);
|
||||||
|
|
||||||
|
// small separator line under title
|
||||||
|
batch.draw(pixel, panelX + 8f, titleY - 14f, panelW - 16f, 1f);
|
||||||
|
|
||||||
|
// --- FOV row ---
|
||||||
|
float rowY = panelY + panelH - 50f;
|
||||||
|
float rowH = 18f;
|
||||||
|
|
||||||
|
// label
|
||||||
|
String fovLabel = "Field of View";
|
||||||
|
layout.setText(font, fovLabel);
|
||||||
|
float labelX = panelX + 12f;
|
||||||
|
float labelY = rowY + rowH - 4f;
|
||||||
|
font.draw(batch, layout, labelX, labelY);
|
||||||
|
|
||||||
|
// buttons + value area
|
||||||
|
fovDecW = 20f;
|
||||||
|
fovDecH = rowH;
|
||||||
|
fovDecX = panelX + 180f;
|
||||||
|
fovDecY = rowY;
|
||||||
|
|
||||||
|
fovIncW = 20f;
|
||||||
|
fovIncH = rowH;
|
||||||
|
fovIncX = panelX + panelW - 32f;
|
||||||
|
fovIncY = rowY;
|
||||||
|
|
||||||
|
// draw dec button "<"
|
||||||
|
drawButtonWithLabel("<", fovDecX, fovDecY, fovDecW, fovDecH);
|
||||||
|
|
||||||
|
// draw inc button ">"
|
||||||
|
drawButtonWithLabel(">", fovIncX, fovIncY, fovIncW, fovIncH);
|
||||||
|
|
||||||
|
// numeric value centered between dec/inc
|
||||||
|
String fovText = String.format("%3.0f°", settings.fov);
|
||||||
|
layout.setText(font, fovText);
|
||||||
|
|
||||||
|
float valueCenter = (fovDecX + fovDecW + fovIncX) / 2f;
|
||||||
|
float valueX = valueCenter - layout.width / 2f;
|
||||||
|
float valueY = rowY + rowH - 4f;
|
||||||
|
font.draw(batch, layout, valueX, valueY);
|
||||||
|
|
||||||
|
// --- Close button at bottom ---
|
||||||
|
String closeLabel = " Close ";
|
||||||
|
layout.setText(font, closeLabel);
|
||||||
|
closeW = layout.width + 64f;
|
||||||
|
closeH = layout.height + 12f;
|
||||||
|
closeX = panelX + (panelW - closeW) / 2f;
|
||||||
|
closeY = panelY + 16f;
|
||||||
|
|
||||||
|
// button bg + border
|
||||||
|
batch.setColor(0.1f, 0.1f, 0.1f, 1f);
|
||||||
|
batch.draw(pixel, closeX, closeY, closeW, closeH);
|
||||||
|
batch.setColor(1f, 1f, 1f, 1f);
|
||||||
|
// border
|
||||||
|
batch.draw(pixel, closeX, closeY + closeH - border, closeW, border);
|
||||||
|
batch.draw(pixel, closeX, closeY, closeW, border);
|
||||||
|
batch.draw(pixel, closeX, closeY, border, closeH);
|
||||||
|
batch.draw(pixel, closeX + closeW - border, closeY, border, closeH);
|
||||||
|
|
||||||
|
float closeTextX = closeX + (closeW - layout.width) / 2f;
|
||||||
|
float closeTextY = closeY + closeH - (closeH - layout.height) / 2f;
|
||||||
|
font.draw(batch, closeLabel, closeTextX, closeTextY);
|
||||||
|
|
||||||
|
batch.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawButtonWithLabel(String text, float x, float y, float w, float h) {
|
||||||
|
// background
|
||||||
|
batch.setColor(0.1f, 0.1f, 0.1f, 1f);
|
||||||
|
batch.draw(pixel, x, y, w, h);
|
||||||
|
// border
|
||||||
|
batch.setColor(1f, 1f, 1f, 1f);
|
||||||
|
float border = 1f;
|
||||||
|
batch.draw(pixel, x, y + h - border, w, border);
|
||||||
|
batch.draw(pixel, x, y, w, border);
|
||||||
|
batch.draw(pixel, x, y, border, h);
|
||||||
|
batch.draw(pixel, x + w - border, y, border, h);
|
||||||
|
|
||||||
|
GlyphLayout layout = new GlyphLayout(font, text);
|
||||||
|
float tx = x + (w - layout.width) / 2f;
|
||||||
|
float ty = y + h - (h - layout.height) / 2f;
|
||||||
|
font.draw(batch, layout, tx, ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a click in RETRO coords.
|
||||||
|
*
|
||||||
|
* @return true if the menu wants to close (Close button clicked)
|
||||||
|
*/
|
||||||
|
public boolean handleClick(float x, float y) {
|
||||||
|
// FOV -
|
||||||
|
if (x >= fovDecX && x <= fovDecX + fovDecW &&
|
||||||
|
y >= fovDecY && y <= fovDecY + fovDecH) {
|
||||||
|
settings.fov = MathUtils.clamp(settings.fov - settings.fovStep, settings.minFov, settings.maxFov);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FOV +
|
||||||
|
if (x >= fovIncX && x <= fovIncX + fovIncW &&
|
||||||
|
y >= fovIncY && y <= fovIncY + fovIncH) {
|
||||||
|
settings.fov = MathUtils.clamp(settings.fov + settings.fovStep, settings.minFov, settings.maxFov);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close
|
||||||
|
if (x >= closeX && x <= closeX + closeW &&
|
||||||
|
y >= closeY && y <= closeY + closeH) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
batch.dispose();
|
||||||
|
font.dispose();
|
||||||
|
pixel.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.ui;
|
||||||
|
|
||||||
|
public enum UiMode {
|
||||||
|
GAMEPLAY,
|
||||||
|
PAUSE, // only bottom bar
|
||||||
|
SETTINGS // pause bar + center settings
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.world;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Vector3;
|
||||||
|
|
||||||
|
public class Collider {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
NONE,
|
||||||
|
BOX // axis-aligned box
|
||||||
|
// later: SPHERE, CAPSULE, MESH, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type type = Type.NONE;
|
||||||
|
|
||||||
|
// Local-space center + extents relative to ModelInstance transform
|
||||||
|
public final Vector3 center = new Vector3(0f, 0f, 0f);
|
||||||
|
public final Vector3 halfExtents = new Vector3(1f, 1f, 1f);
|
||||||
|
|
||||||
|
public static Collider none() {
|
||||||
|
return new Collider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Collider box(float hx, float hy, float hz) {
|
||||||
|
Collider c = new Collider();
|
||||||
|
c.type = Type.BOX;
|
||||||
|
c.halfExtents.set(hx, hy, hz);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.world;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
import com.badlogic.gdx.graphics.GL20;
|
||||||
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
|
import com.badlogic.gdx.graphics.VertexAttributes;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.*;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
|
||||||
|
|
||||||
|
public class ModelLibrary {
|
||||||
|
|
||||||
|
private final ModelBuilder builder = new ModelBuilder();
|
||||||
|
|
||||||
|
public final Texture groundTexture;
|
||||||
|
public final Texture pathTexture;
|
||||||
|
public final Texture houseTexture; // <-- new
|
||||||
|
|
||||||
|
public final Model groundModel;
|
||||||
|
public final Model unitCubeModel;
|
||||||
|
|
||||||
|
// town-related primitives
|
||||||
|
public final Model houseBlockModel;
|
||||||
|
public final Model pathTileModel;
|
||||||
|
|
||||||
|
public ModelLibrary() {
|
||||||
|
// --- ground texture + model ---
|
||||||
|
groundTexture = new Texture(Gdx.files.internal("textures/rocky_grass_2.png"));
|
||||||
|
groundTexture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
||||||
|
groundTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
|
||||||
|
|
||||||
|
float halfSize = 32f;
|
||||||
|
float tileScale = 16f;
|
||||||
|
|
||||||
|
builder.begin();
|
||||||
|
Material groundMat = new Material(
|
||||||
|
ColorAttribute.createDiffuse(1f, 1f, 1f, 1f),
|
||||||
|
TextureAttribute.createDiffuse(groundTexture)
|
||||||
|
);
|
||||||
|
|
||||||
|
MeshPartBuilder mpb = builder.part(
|
||||||
|
"ground",
|
||||||
|
GL20.GL_TRIANGLES,
|
||||||
|
VertexAttributes.Usage.Position
|
||||||
|
| VertexAttributes.Usage.Normal
|
||||||
|
| VertexAttributes.Usage.TextureCoordinates,
|
||||||
|
groundMat
|
||||||
|
);
|
||||||
|
mpb.setUVRange(0f, 0f, tileScale, tileScale);
|
||||||
|
mpb.rect(
|
||||||
|
-halfSize, 0f, halfSize,
|
||||||
|
halfSize, 0f, halfSize,
|
||||||
|
halfSize, 0f, -halfSize,
|
||||||
|
-halfSize, 0f, -halfSize,
|
||||||
|
0f, 1f, 0f
|
||||||
|
);
|
||||||
|
groundModel = builder.end();
|
||||||
|
|
||||||
|
// generic cube (still untextured)
|
||||||
|
unitCubeModel = builder.createBox(
|
||||||
|
1f, 1f, 1f,
|
||||||
|
new Material(ColorAttribute.createDiffuse(1f, 1f, 1f, 1f)),
|
||||||
|
VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- house texture + model ---
|
||||||
|
// assumes textures/texture_09.png exists
|
||||||
|
houseTexture = new Texture(Gdx.files.internal("textures/texture_03.png"));
|
||||||
|
houseTexture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
||||||
|
houseTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
|
||||||
|
|
||||||
|
houseBlockModel = builder.createBox(
|
||||||
|
4f, 3f, 4f,
|
||||||
|
new Material(
|
||||||
|
TextureAttribute.createDiffuse(houseTexture)
|
||||||
|
// optionally tint:
|
||||||
|
// ColorAttribute.createDiffuse(0.9f, 0.8f, 0.7f, 1f)
|
||||||
|
),
|
||||||
|
VertexAttributes.Usage.Position
|
||||||
|
| VertexAttributes.Usage.Normal
|
||||||
|
| VertexAttributes.Usage.TextureCoordinates // <-- important
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- path tile texture + model ---
|
||||||
|
pathTexture = new Texture(Gdx.files.internal("textures/paving_2.png"));
|
||||||
|
pathTexture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
||||||
|
pathTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
|
||||||
|
|
||||||
|
builder.begin();
|
||||||
|
Material pathMat = new Material(
|
||||||
|
TextureAttribute.createDiffuse(pathTexture)
|
||||||
|
);
|
||||||
|
|
||||||
|
MeshPartBuilder pathMpb = builder.part(
|
||||||
|
"pathTile",
|
||||||
|
GL20.GL_TRIANGLES,
|
||||||
|
VertexAttributes.Usage.Position
|
||||||
|
| VertexAttributes.Usage.Normal
|
||||||
|
| VertexAttributes.Usage.TextureCoordinates,
|
||||||
|
pathMat
|
||||||
|
);
|
||||||
|
|
||||||
|
float tHalf = 1f; // 2x2 tile
|
||||||
|
float y = 0.01f;
|
||||||
|
|
||||||
|
pathMpb.setUVRange(0f, 0f, 0.5f, 0.5f);
|
||||||
|
pathMpb.rect(
|
||||||
|
-tHalf, y, tHalf,
|
||||||
|
tHalf, y, tHalf,
|
||||||
|
tHalf, y, -tHalf,
|
||||||
|
-tHalf, y, -tHalf,
|
||||||
|
0f, 1f, 0f
|
||||||
|
);
|
||||||
|
pathTileModel = builder.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
groundModel.dispose();
|
||||||
|
unitCubeModel.dispose();
|
||||||
|
houseBlockModel.dispose();
|
||||||
|
pathTileModel.dispose();
|
||||||
|
|
||||||
|
groundTexture.dispose();
|
||||||
|
pathTexture.dispose();
|
||||||
|
houseTexture.dispose(); // <-- new
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.world;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.g3d.ModelInstance;
|
||||||
|
|
||||||
|
public class TownFactory {
|
||||||
|
|
||||||
|
private final ModelLibrary models;
|
||||||
|
|
||||||
|
public TownFactory(ModelLibrary models) {
|
||||||
|
this.models = models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldObject createHouse(String id, float x, float z) {
|
||||||
|
ModelInstance inst = new ModelInstance(models.houseBlockModel);
|
||||||
|
// houseBlockModel is 4x3x4 → center at y=1.5
|
||||||
|
inst.transform.setToTranslation(x, 1.5f, z);
|
||||||
|
|
||||||
|
Collider col = Collider.box(2f, 1.5f, 2f);
|
||||||
|
|
||||||
|
WorldObject obj = new WorldObject(id, inst, col);
|
||||||
|
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 1x1x1 → center at y=0.5
|
||||||
|
inst.transform.setToTranslation(x, 0.5f, z);
|
||||||
|
|
||||||
|
Collider col = Collider.box(0.5f, 0.5f, 0.5f);
|
||||||
|
|
||||||
|
WorldObject obj = new WorldObject(id, inst, col);
|
||||||
|
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);
|
||||||
|
|
||||||
|
WorldObject obj = new WorldObject(id, inst, Collider.none());
|
||||||
|
obj.staticObject = true;
|
||||||
|
obj.walkableSurface = false; // ground plane already gives height 0
|
||||||
|
obj.collidable = false;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.world;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.g3d.Environment;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.ModelBatch;
|
||||||
|
import com.badlogic.gdx.graphics.g3d.ModelInstance;
|
||||||
|
import com.badlogic.gdx.math.Matrix4;
|
||||||
|
import com.badlogic.gdx.math.Vector3;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class World3D {
|
||||||
|
|
||||||
|
private final ModelLibrary models;
|
||||||
|
|
||||||
|
private final WorldObject ground;
|
||||||
|
private final List<WorldObject> objects = new ArrayList<>();
|
||||||
|
|
||||||
|
private final TownFactory townFactory;
|
||||||
|
|
||||||
|
private final Vector3 tmpWorld = new Vector3();
|
||||||
|
private final Vector3 tmpFeet = new Vector3();
|
||||||
|
private final Vector3 tmpHead = new Vector3();
|
||||||
|
private final Vector3 tmpLocalFeet = new Vector3();
|
||||||
|
private final Vector3 tmpLocalHead = new Vector3();
|
||||||
|
|
||||||
|
private final Matrix4 tmpMat = new Matrix4();
|
||||||
|
private final Matrix4 tmpInv = new Matrix4();
|
||||||
|
|
||||||
|
private final Vector3 tmp = new Vector3();
|
||||||
|
|
||||||
|
public World3D(ModelLibrary models) {
|
||||||
|
this.models = models;
|
||||||
|
this.townFactory = new TownFactory(models);
|
||||||
|
|
||||||
|
// --- ground ---
|
||||||
|
ground = new WorldObject(
|
||||||
|
"ground",
|
||||||
|
new ModelInstance(models.groundModel),
|
||||||
|
Collider.none()
|
||||||
|
);
|
||||||
|
ground.staticObject = true;
|
||||||
|
|
||||||
|
// --- build a tiny town layout instead of random cubes ---
|
||||||
|
buildTestTown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addObject(WorldObject obj) {
|
||||||
|
objects.add(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildTestTown() {
|
||||||
|
float tileSize = 2f; // world units per “map tile”
|
||||||
|
|
||||||
|
// Simple 7x7 layout:
|
||||||
|
// H = house, P = path, . = empty
|
||||||
|
String[] layout = {
|
||||||
|
".............",
|
||||||
|
"..H.C.....H..",
|
||||||
|
"..P.......P..",
|
||||||
|
"..P.......P..",
|
||||||
|
"..P.......P..",
|
||||||
|
"..P.......P..",
|
||||||
|
"..P.......P..",
|
||||||
|
"..P.......P..",
|
||||||
|
"..H.......H..",
|
||||||
|
"............."
|
||||||
|
};
|
||||||
|
|
||||||
|
int rows = layout.length;
|
||||||
|
int cols = layout[0].length();
|
||||||
|
|
||||||
|
float offsetX = -(cols - 1) * tileSize * 0.5f;
|
||||||
|
float offsetZ = -(rows - 1) * tileSize * 0.5f;
|
||||||
|
|
||||||
|
for (int row = 0; row < rows; row++) {
|
||||||
|
String line = layout[row];
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
char c = line.charAt(col);
|
||||||
|
|
||||||
|
float x = offsetX + col * tileSize;
|
||||||
|
float z = offsetZ + row * tileSize;
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
case 'H':
|
||||||
|
addObject(townFactory.createHouse("house_" + row + "_" + col, x, z));
|
||||||
|
break;
|
||||||
|
case 'P':
|
||||||
|
addObject(townFactory.createPathTile("path_" + row + "_" + col, x, z));
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
addObject(townFactory.createCrate("crate_" + row + "_" + col, x, z));
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe put a crate in the middle
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void update(float delta) {
|
||||||
|
// here you can update objects that animate / move
|
||||||
|
for (WorldObject obj : objects) {
|
||||||
|
obj.update(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(ModelBatch batch, Environment environment) {
|
||||||
|
// draw ground
|
||||||
|
batch.render(ground.instance, environment);
|
||||||
|
|
||||||
|
// draw objects
|
||||||
|
for (WorldObject obj : objects) {
|
||||||
|
batch.render(obj.instance, environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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;
|
||||||
|
|
||||||
|
// world → local
|
||||||
|
tmpInv.set(obj.instance.transform);
|
||||||
|
tmpInv.inv();
|
||||||
|
|
||||||
|
// capsule center projected to XZ in local space
|
||||||
|
tmpWorld.set(x, 0f, z);
|
||||||
|
tmpWorld.mul(tmpInv);
|
||||||
|
|
||||||
|
float cx = tmpWorld.x;
|
||||||
|
float cz = tmpWorld.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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local top Y of the box
|
||||||
|
float localTopY = col.center.y + col.halfExtents.y;
|
||||||
|
|
||||||
|
// Convert that top point back to world space
|
||||||
|
tmpWorld.set(col.center.x, localTopY, col.center.z);
|
||||||
|
tmpWorld.mul(obj.instance.transform);
|
||||||
|
|
||||||
|
if (tmpWorld.y > maxY) {
|
||||||
|
maxY = tmpWorld.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public boolean collidesAt(float x, float y, float z, float radius, float eyeHeight) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
tmpInv.set(obj.instance.transform);
|
||||||
|
tmpInv.inv();
|
||||||
|
|
||||||
|
tmpFeet.set(x, feetY, z).mul(tmpInv);
|
||||||
|
tmpHead.set(x, headY, z).mul(tmpInv);
|
||||||
|
|
||||||
|
float boxMinY = col.center.y - col.halfExtents.y;
|
||||||
|
float boxMaxY = col.center.y + col.halfExtents.y;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
float closestX = Math.max(minX, Math.min(px, maxX));
|
||||||
|
float closestZ = Math.max(minZ, Math.min(pz, maxZ));
|
||||||
|
|
||||||
|
float dx = px - closestX;
|
||||||
|
float dz = pz - closestZ;
|
||||||
|
|
||||||
|
if (dx * dx + dz * dz <= radius * radius) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
models.dispose(); // or let GameScreen own/dispose the library
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package wtf.beatrice.retrorender.engine.world;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.g3d.ModelInstance;
|
||||||
|
import com.badlogic.gdx.math.Vector3;
|
||||||
|
public class WorldObject {
|
||||||
|
|
||||||
|
public final String id;
|
||||||
|
public final ModelInstance instance;
|
||||||
|
public final Collider collider;
|
||||||
|
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();
|
||||||
|
|
||||||
|
public WorldObject(String id, ModelInstance instance, Collider collider) {
|
||||||
|
this.id = id;
|
||||||
|
this.instance = instance;
|
||||||
|
this.collider = collider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float delta) {
|
||||||
|
// default: do nothing
|
||||||
|
// For rotating decoration, set collidable=false + walkableSurface=false
|
||||||
|
// and rotate instance.transform here if you want.
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 getPosition(Vector3 out) {
|
||||||
|
instance.transform.getTranslation(out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 getPosition() {
|
||||||
|
return getPosition(tmpPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ org.gradle.configureondemand=false
|
|||||||
# the link to the scan won't get shown at all.
|
# the link to the scan won't get shown at all.
|
||||||
# Documented at: https://docs.gradle.org/current/userguide/command_line_interface.html#sec:command_line_logging
|
# Documented at: https://docs.gradle.org/current/userguide/command_line_interface.html#sec:command_line_logging
|
||||||
org.gradle.logging.level=quiet
|
org.gradle.logging.level=quiet
|
||||||
gdxControllersVersion=2.2.3
|
gdxControllersVersion=2.2.4
|
||||||
enableGraalNative=false
|
enableGraalNative=false
|
||||||
graalHelperVersion=2.0.1
|
graalHelperVersion=2.0.1
|
||||||
gdxVersion=1.14.0
|
gdxVersion=1.14.0
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ buildscript {
|
|||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "io.github.fourlastor:construo:2.0.2"
|
classpath "io.github.fourlastor:construo:2.1.0"
|
||||||
if(enableGraalNative == 'true') {
|
if(enableGraalNative == 'true') {
|
||||||
classpath "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.28"
|
classpath "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.11.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class Lwjgl3Launcher {
|
|||||||
//// useful for testing performance, but can also be very stressful to some hardware.
|
//// 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.
|
//// 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/ .
|
//// You can change these files; they are in lwjgl3/src/main/resources/ .
|
||||||
//// They can also be loaded from the root of assets/ .
|
//// They can also be loaded from the root of assets/ .
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
// Applies the foojay-resolver plugin to allow automatic download of JDKs.
|
// Applies the foojay-resolver plugin to allow automatic download of JDKs.
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
||||||
}
|
}
|
||||||
// A list of which subprojects to load as part of the same larger project.
|
// A list of which subprojects to load as part of the same larger project.
|
||||||
// You can remove Strings from the list and reload the Gradle project
|
// You can remove Strings from the list and reload the Gradle project
|
||||||
|
|||||||
Reference in New Issue
Block a user