Compare commits
1 Commits
main
...
renovate/o
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b66a66b15 |
Binary file not shown.
|
Before Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 165 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 190 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 177 KiB |
233
core/src/main/java/wtf/beatrice/retrorender/GameScreen.java
Normal file
233
core/src/main/java/wtf/beatrice/retrorender/GameScreen.java
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
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,7 +1,16 @@
|
|||||||
package wtf.beatrice.retrorender;
|
package wtf.beatrice.retrorender;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.ApplicationAdapter;
|
||||||
import com.badlogic.gdx.Game;
|
import com.badlogic.gdx.Game;
|
||||||
import wtf.beatrice.retrorender.engine.render.GameScreen;
|
import com.badlogic.gdx.Gdx;
|
||||||
|
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,25 +1,19 @@
|
|||||||
package wtf.beatrice.retrorender.engine.ui;
|
package wtf.beatrice.retrorender.engine;
|
||||||
|
|
||||||
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 =
|
||||||
@@ -44,13 +38,6 @@ 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
|
||||||
@@ -62,54 +49,24 @@ 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;
|
||||||
|
|
||||||
float panelW = 275f;
|
String posText = String.format("X: %.2f Y: %.2f Z: %.2f", x, y, z);
|
||||||
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: %.2f Pitch: %.2f", yaw, pitch);
|
String angText = String.format("Yaw: %.1f Pitch: %.1f", 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,271 @@
|
|||||||
|
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 final float eyeHeight = 1.8f;
|
||||||
|
private final float jumpSpeed = 12f;
|
||||||
|
private final float gravity = -50f;
|
||||||
|
private float velocityY = 0f;
|
||||||
|
private boolean grounded = false;
|
||||||
|
|
||||||
|
// --- approximation toggles
|
||||||
|
private final boolean retroQuantization = true;
|
||||||
|
|
||||||
|
// 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 = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// snap position to quantization steps
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
public void setMoveSpeed(float moveSpeed) { this.moveSpeed = moveSpeed; }
|
||||||
|
public void setMouseSensitivity(float mouseSensitivity) { this.mouseSensitivity = mouseSensitivity; }
|
||||||
|
}
|
||||||
196
core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java
Normal file
196
core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,387 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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,87 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package wtf.beatrice.retrorender.engine.ui;
|
|
||||||
|
|
||||||
public enum UiMode {
|
|
||||||
GAMEPLAY,
|
|
||||||
PAUSE, // only bottom bar
|
|
||||||
SETTINGS // pause bar + center settings
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(860, 480);
|
configuration.setWindowedMode(640, 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.10.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.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