Compare commits

...

14 Commits

Author SHA1 Message Date
65880853ac feat(scene/main_menu): handle key down holding 2025-10-01 11:07:02 +07:00
b1cbef4dd8 chore(constants): remove unused imports 2025-10-01 10:34:52 +07:00
affa81a3e8 feat(ui/button): implement more events & simulators
This commit implements events onHoverEnter/onHoverExit.

Simulators have .click() to simulate click, .hoverEnter()/.hoverExit() and so on.
2025-10-01 10:17:13 +07:00
63c81f98c2 feat(game): keyboard input handling 2025-10-01 10:15:07 +07:00
381ff306cb chore(scene): set game in super 2025-10-01 08:53:39 +07:00
a4bece69f9 chore(scene): cleanup main menu 2025-10-01 02:45:51 +07:00
967926c4d2 feat(assets): support FileHandle 2025-10-01 02:45:25 +07:00
f4d5a351fb Merge pull request #5 from teppyboy/gdx-video-background
feat(scene/main_menu): add background video
2025-10-01 02:19:07 +07:00
e7c614c6cd Merge branch 'main' into gdx-video-background 2025-10-01 02:18:55 +07:00
b79865d59c add background video 2025-10-01 02:12:04 +07:00
0ad13ad368 feat(scene): implement settings scene
Also move testing bits to settings so yeah :P
2025-10-01 00:36:22 +07:00
67100eba23 Merge pull request #4 from teppyboy/button
feat(button): add UI image button
2025-10-01 00:12:17 +07:00
94b618f66c Merge branch 'main' into button 2025-10-01 00:11:59 +07:00
ac37bb43eb chore: add UI image button 2025-09-30 23:51:29 +07:00
17 changed files with 357 additions and 44 deletions

View File

@ -6,6 +6,7 @@
*/
val libgdxVersion = "1.13.5"
val gdxVideoVersion = "1.3.3"
plugins {
// Apply the application plugin to add support for building a CLI application in Java.
@ -46,6 +47,10 @@ dependencies {
// Logging
implementation("org.slf4j:slf4j-api:2.1.0-alpha1")
implementation("ch.qos.logback:logback-classic:1.5.18")
// gdx-video
implementation("com.badlogicgames.gdx-video:gdx-video:${gdxVideoVersion}")
implementation("com.badlogicgames.gdx-video:gdx-video-lwjgl3:${gdxVideoVersion}")
}
// Apply a specific Java toolchain to ease working on different environments.

View File

@ -1,9 +1,5 @@
package org.vibecoders.moongazer;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
/**
* Client configuration constants and default values
* used throughout the Moongazer client application.

View File

@ -26,6 +26,7 @@ public class Game extends ApplicationAdapter {
Scene currentScene;
Scene introScene;
public Scene mainMenuScene;
public Scene settingsScene;
public ArrayList<Scene> gameScenes;
@Override
@ -44,7 +45,7 @@ public class Game extends ApplicationAdapter {
gameScenes = new ArrayList<>();
currentScene = introScene = new Intro(this);
gameScenes.add(introScene);
// By the end of the intro, the main menu scene will be created and assigned to Game.mainMenuScene
// By the end of the intro, other secenes will be created and assigned to Game.mainMenuScene
}
@Override
@ -65,6 +66,9 @@ public class Game extends ApplicationAdapter {
case MAIN_MENU:
currentScene = mainMenuScene;
break;
case SETTINGS:
currentScene = settingsScene;
break;
case IN_GAME:
// Render in-game scene
break;
@ -80,11 +84,6 @@ public class Game extends ApplicationAdapter {
}
}
if (!currentScene.root.isVisible()) {
log.trace("Showing current scene: {}", currentScene.getClass().getSimpleName());
currentScene.root.setVisible(true);
}
batch.begin();
currentScene.render(batch);
batch.end();

View File

@ -3,5 +3,6 @@ package org.vibecoders.moongazer;
public enum State {
INTRO,
MAIN_MENU,
SETTINGS,
IN_GAME
}

View File

@ -2,9 +2,11 @@ package org.vibecoders.moongazer.managers;
import org.slf4j.Logger;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
@ -13,18 +15,26 @@ import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGeneratorLoader;
import com.badlogic.gdx.graphics.g2d.freetype.FreetypeFontLoader;
import java.util.ArrayList;
import java.util.HashMap;
public class Assets {
private static final AssetManager assetManager = new AssetManager();
private static final FileHandleResolver resolver = new InternalFileHandleResolver();
private static final Logger log = org.slf4j.LoggerFactory.getLogger(Assets.class);
private static final ArrayList<String> loadedFonts = new ArrayList<>();
private static final HashMap<String, FileHandle> loadedFiles = new HashMap<>();
private static boolean startLoadAll = false;
private static boolean loadedAll = false;
private static Texture textureWhite;
private static Texture textureBlack;
public static <T> T getAsset(String fileName, Class<T> type) {
if (type == FileHandle.class) {
if (!loadedFiles.containsKey(fileName)) {
loadAny(fileName);
}
return type.cast(loadedFiles.get(fileName));
}
try {
if (!assetManager.isLoaded(fileName, type)) {
log.warn("Asset not loaded: {}", fileName);
@ -41,10 +51,11 @@ public class Assets {
/**
* Loads and returns a BitmapFont of the specified size from the given TTF file.
* <p>
* Special file name "ui" is mapped to "fonts/H7GBKHeavy.ttf" (Wuthering Waves UI font).
* Special file name "ui" is mapped to "fonts/H7GBKHeavy.ttf" (Wuthering Waves
* UI font).
*
* @param fileName the font name
* @param size the font size
* @param size the font size
* @return the loaded BitmapFont
*/
public static BitmapFont getFont(String fileName, int size) {
@ -74,6 +85,19 @@ public class Assets {
waitUntilLoaded();
}
public static void loadAny(String fileName) {
FileHandle fh = Gdx.files.internal(fileName);
if (!fh.exists()) {
log.error("File does not exist: {}", fileName);
return;
}
if (loadedFiles.containsKey(fileName)) {
return;
}
loadedFiles.put(fileName, fh);
}
public static void loadAll() {
if (startLoadAll) {
log.warn("loadAll() called multiple times!");
@ -89,6 +113,12 @@ public class Assets {
assetManager.load("textures/main_menu/background.png", Texture.class);
assetManager.load("textures/main_menu/title.png", Texture.class);
assetManager.load("textures/ui/text_button.png", Texture.class);
assetManager.load("textures/ui/IconExitGame.png", Texture.class);
assetManager.load("textures/ui/UI_Icon_Setting.png", Texture.class);
assetManager.load("textures/ui/ImgReShaSoundOn.png", Texture.class);
assetManager.load("textures/ui/UI_Gcg_Icon_Close.png", Texture.class);
// "Load" unsupported file types as FileHandle
loadAny("videos/main_menu_background.webm");
}
public static boolean isLoadedAll() {
@ -101,7 +131,7 @@ public class Assets {
public static void waitUntilLoaded() {
assetManager.finishLoading();
if (startLoadAll) {;
if (startLoadAll) {
loadedAll = true;
}
}
@ -116,7 +146,7 @@ public class Assets {
pixmap.setColor(Color.WHITE);
pixmap.fill();
textureWhite = new Texture(pixmap);
pixmap.dispose(); // Important: dispose pixmap after creating texture
pixmap.dispose();
}
return textureWhite;
}
@ -133,6 +163,13 @@ public class Assets {
}
public static void dispose() {
for (var fontKey : loadedFonts) {
if (assetManager.isLoaded(fontKey, BitmapFont.class)) {
assetManager.unload(fontKey);
}
}
loadedFonts.clear();
loadedFiles.clear();
assetManager.dispose();
if (textureWhite != null) {
textureWhite.dispose();

View File

@ -29,8 +29,11 @@ public class Intro extends Scene {
startTime = System.currentTimeMillis() + 500;
log.info("Starting to load all remaining assets...");
Assets.loadAll();
// Create scenes
game.mainMenuScene = new MainMenu(game);
game.settingsScene = new Settings(game);
game.gameScenes.add(game.mainMenuScene);
game.gameScenes.add(game.settingsScene);
}
/**
@ -57,8 +60,12 @@ public class Intro extends Scene {
}
currentOpacity = 1 - ((float) (System.currentTimeMillis() - endTime) / 1000);
}
batch.setColor(1, 1, 1, currentOpacity);
// Multiply with any externally applied alpha (e.g., Transition)
float externalAlpha = batch.getColor().a;
float finalAlpha = currentOpacity * externalAlpha;
batch.setColor(1, 1, 1, finalAlpha);
batch.draw(logo, WINDOW_WIDTH / 2 - logo.getWidth() / 4, WINDOW_HEIGHT / 2 - logo.getHeight() / 4,
logo.getWidth() / 2, logo.getHeight() / 2);
batch.setColor(1, 1, 1, externalAlpha);
}
}

View File

@ -2,32 +2,58 @@ package org.vibecoders.moongazer.scenes;
import static org.vibecoders.moongazer.Constants.*;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;
import org.vibecoders.moongazer.Game;
import org.vibecoders.moongazer.State;
import org.vibecoders.moongazer.managers.Assets;
import org.vibecoders.moongazer.ui.UITextButton;
import org.vibecoders.moongazer.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.video.VideoPlayer;
import com.badlogic.gdx.video.VideoPlayerCreator;
public class MainMenu extends Scene {
private Texture backgroundTexture;
private VideoPlayer videoPlayer;
private FileHandle videoFileHandle;
private Texture titleTexture;
private float titleY;
private float titleX;
private float titleWidth;
private float titleHeight;
private UITextButton[] buttons;
private HashMap<Integer, Long> currentKeyDown = new HashMap<>();
private float titleY, titleX, titleWidth, titleHeight;
private boolean videoPrepared = false;
private int currentChoice = -1;
public MainMenu(Game game) {
super(game);
backgroundTexture = Assets.getAsset("textures/main_menu/background.png", Texture.class);
initVideo();
initUI();
}
private void initVideo() {
videoPlayer = VideoPlayerCreator.createVideoPlayer();
videoFileHandle = Assets.getAsset("videos/main_menu_background.webm", FileHandle.class);
try {
videoPlayer.load(videoFileHandle);
} catch (FileNotFoundException e) {
log.error("Failed to load video", e);
}
}
private void initUI() {
// Title
titleTexture = Assets.getAsset("textures/main_menu/title.png", Texture.class);
// Scale and position title
float targetTitleWidth = 500f;
float originalWidth = titleTexture.getWidth();
float originalHeight = titleTexture.getHeight();
float scale = targetTitleWidth / originalWidth;
titleWidth = originalWidth * scale;
titleHeight = originalHeight * scale;
float scale = targetTitleWidth / titleTexture.getWidth();
titleWidth = titleTexture.getWidth() * scale;
titleHeight = titleTexture.getHeight() * scale;
titleX = (WINDOW_WIDTH - titleWidth) / 2f;
titleY = WINDOW_HEIGHT / 2f - titleHeight / 8f;
@ -37,9 +63,11 @@ public class MainMenu extends Scene {
UITextButton loadButton = new UITextButton("Load", font);
UITextButton settingsButton = new UITextButton("Settings", font);
UITextButton exitButton = new UITextButton("Exit", font);
buttons = new UITextButton[] { playButton, loadButton, settingsButton, exitButton };
int buttonWidth = 300;
int buttonHeight = 80;
playButton.setSize(buttonWidth, buttonHeight);
loadButton.setSize(buttonWidth, buttonHeight);
settingsButton.setSize(buttonWidth, buttonHeight);
@ -47,28 +75,147 @@ public class MainMenu extends Scene {
int centerX = WINDOW_WIDTH / 2 - buttonWidth / 2;
int startY = WINDOW_HEIGHT / 2 - buttonHeight / 2;
int buttonSpacing = 65;
int spacing = 65;
playButton.setSize(buttonWidth, buttonHeight);
playButton.setPosition(centerX, startY);
loadButton.setPosition(centerX, startY - buttonSpacing);
settingsButton.setPosition(centerX, startY - buttonSpacing * 2);
exitButton.setPosition(centerX, startY - buttonSpacing * 3);
loadButton.setSize(buttonWidth, buttonHeight);
loadButton.setPosition(centerX, startY - spacing);
settingsButton.setSize(buttonWidth, buttonHeight);
settingsButton.setPosition(centerX, startY - spacing * 2);
exitButton.setSize(buttonWidth, buttonHeight);
exitButton.setPosition(centerX, startY - spacing * 3);
// Mouse click handlers
playButton.onClick(() -> log.debug("Play clicked"));
loadButton.onClick(() -> log.debug("Load clicked"));
settingsButton.onClick(() -> log.debug("Settings clicked"));
exitButton.onClick(() -> log.debug("Exit clicked"));
settingsButton.onClick(() -> {
log.debug("Settings clicked");
if (game.transition == null) {
game.transition = new Transition(game, this, game.settingsScene, State.SETTINGS, 350);
}
});
exitButton.onClick(() -> {
log.debug("Exit clicked");
Gdx.app.exit();
});
root.addActor(playButton.getActor());
root.addActor(loadButton.getActor());
root.addActor(settingsButton.getActor());
root.addActor(exitButton.getActor());
// Keyboard navigation handling
initKeyboardHandling();
game.stage.addActor(root);
}
private void initKeyboardHandling() {
for (int i = 0; i < buttons.length; i++) {
final int index = i;
buttons[i].onHoverEnter(() -> {
log.trace("Button hover enter: {}", index);
if (currentChoice != index) {
if (currentChoice != -1) {
buttons[currentChoice].hoverExit();
}
currentChoice = index;
}
});
buttons[i].onHoverExit(() -> {
if (currentChoice == index) {
currentChoice = -1;
}
});
}
root.addListener(new InputListener() {
@Override
public boolean keyDown(InputEvent event, int keycode) {
sKeyDown(event, keycode);
currentKeyDown.put(keycode, System.currentTimeMillis());
return true;
}
});
root.addListener(new InputListener() {
@Override
public boolean keyUp(InputEvent event, int keycode) {
currentKeyDown.remove(keycode);
return true;
}
});
}
private void startVideoOnce() {
if (videoPlayer == null || videoPrepared)
return;
if (videoFileHandle == null || !videoFileHandle.exists())
return;
if (game.transition == null && game.state != State.MAIN_MENU)
return;
videoPlayer.setLooping(true);
videoPlayer.play();
videoPrepared = true;
}
/**
* The actual key down handler. ((s)cene key down)
*
* @param event not used for now, can be null
* @param keycode the keycode of the pressed key
*/
public void sKeyDown(InputEvent event, int keycode) {
log.trace("Key pressed: {}", keycode);
switch (keycode) {
case Input.Keys.UP:
currentChoice = (currentChoice - 1 + 4) % 4;
break;
case Input.Keys.DOWN:
currentChoice = (currentChoice + 1) % 4;
break;
case Input.Keys.RIGHT:
case Input.Keys.ENTER:
if (currentChoice != -1) {
buttons[currentChoice].click();
}
break;
default:
break;
}
if (currentChoice != -1) {
log.trace("Current choice: {}", currentChoice);
for (int i = 0; i < buttons.length; i++) {
if (i == currentChoice) {
buttons[i].hoverEnter();
} else {
buttons[i].hoverExit();
}
}
}
}
@Override
public void render(SpriteBatch batch) {
batch.draw(backgroundTexture, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
// SDL way of handling key input XD
for (Map.Entry<Integer, Long> entry : currentKeyDown.entrySet()) {
Integer keyCode = entry.getKey();
Long timeStamp = entry.getValue();
if (System.currentTimeMillis() - timeStamp > 100) {
sKeyDown(null, keyCode);
currentKeyDown.put(keyCode, System.currentTimeMillis());
}
}
startVideoOnce();
videoPlayer.update();
Texture videoTexture = videoPlayer.getTexture();
batch.draw(videoTexture, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
batch.draw(titleTexture, titleX, titleY, titleWidth, titleHeight);
}
@Override
public void dispose() {
super.dispose();
videoPlayer.dispose();
}
}

View File

@ -10,8 +10,10 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table;
public abstract class Scene {
protected final Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
public Table root;
public Game game;
public Scene(Game game) {
this();
this.game = game;
}
public Scene() {
root = new Table();

View File

@ -0,0 +1,50 @@
package org.vibecoders.moongazer.scenes;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import static org.vibecoders.moongazer.Constants.*;
import org.vibecoders.moongazer.Game;
import org.vibecoders.moongazer.State;
import org.vibecoders.moongazer.managers.Assets;
import org.vibecoders.moongazer.ui.UIImageButton;
public class Settings extends Scene {
public Settings(Game game) {
super(game);
// WIP
UIImageButton settingButton = new UIImageButton("textures/ui/UI_Icon_Setting.png");
UIImageButton exitImgButton = new UIImageButton("textures/ui/IconExitGame.png");
UIImageButton soundButton = new UIImageButton("textures/ui/ImgReShaSoundOn.png");
UIImageButton closeButton = new UIImageButton("textures/ui/UI_Gcg_Icon_Close.png");
settingButton.setSize(50, 50);
exitImgButton.setSize(50, 50);
soundButton.setSize(50, 50);
closeButton.setSize(50, 50);
settingButton.setPosition(20, WINDOW_HEIGHT - 70);
exitImgButton.setPosition(WINDOW_WIDTH - 70, WINDOW_HEIGHT - 70);
soundButton.setPosition(20, 20);
closeButton.setPosition(WINDOW_WIDTH - 70, 20);
root.addActor(settingButton.getActor());
root.addActor(exitImgButton.getActor());
root.addActor(soundButton.getActor());
root.addActor(closeButton.getActor());
settingButton.onClick(() -> log.debug("Settings clicked"));
exitImgButton.onClick(() -> {
log.debug("Exit clicked");
if (game.transition == null) {
game.transition = new Transition(game, this, game.mainMenuScene, State.MAIN_MENU, 350);
}
});
soundButton.onClick(() -> log.debug("Sound clicked"));
closeButton.onClick(() -> log.debug("Close clicked"));
game.stage.addActor(root);
}
@Override
public void render(SpriteBatch batch) {
batch.draw(Assets.getWhiteTexture(), 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
}
}

View File

@ -9,7 +9,6 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch;
* Handles transitions between scenes with a linear transition effect.
*/
public class Transition extends Scene {
private Game game;
private Scene from;
private Scene to;
private State targetState;
@ -25,8 +24,8 @@ public class Transition extends Scene {
*/
public Transition(Game game, Scene from, Scene to, State targetState, long duration) {
// Transition does not need to render UI elements
super(game);
this.root = null;
this.game = game;
this.from = from;
this.to = to;
this.targetState = targetState;
@ -45,6 +44,10 @@ public class Transition extends Scene {
log.trace("Transition complete to state: {}", targetState);
game.state = targetState;
game.transition = null;
// Set keyboard focus to the new scene's root
from.root.setVisible(false);
to.root.setVisible(true);
game.stage.setKeyboardFocus(to.root);
return;
}
var fromOpacity = 1 - toOpacity;

View File

@ -1,12 +1,15 @@
package org.vibecoders.moongazer.ui;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.EventListener;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
public abstract class UIButton {
protected Actor actor;
protected Button button;
public Actor actor;
public Button button;
public Actor getActor() {
return actor;
@ -24,12 +27,65 @@ public abstract class UIButton {
actor.addListener(eventListener);
}
public void click() {
// Thx ChatGPT
InputEvent down = new InputEvent();
down.setType(InputEvent.Type.touchDown);
down.setButton(Input.Buttons.LEFT);
down.setStageX(button.getX());
down.setStageY(button.getY());
button.fire(down);
InputEvent up = new InputEvent();
up.setType(InputEvent.Type.touchUp);
up.setButton(Input.Buttons.LEFT);
up.setStageX(button.getX());
up.setStageY(button.getY());
button.fire(up);
}
public void hoverEnter() {
InputEvent e = new InputEvent();
e.setType(InputEvent.Type.enter);
e.setPointer(-1);
button.fire(e);
}
public void hoverExit() {
InputEvent e = new InputEvent();
e.setType(InputEvent.Type.exit);
e.setPointer(-1);
button.fire(e);
}
public void onClick(Runnable action) {
button.addListener(new com.badlogic.gdx.scenes.scene2d.utils.ClickListener() {
button.addListener(new ClickListener() {
@Override
public void clicked(com.badlogic.gdx.scenes.scene2d.InputEvent event, float x, float y) {
public void clicked(InputEvent event, float x, float y) {
action.run();
}
});
}
public void onHoverEnter(Runnable action) {
button.addListener(new ClickListener() {
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
if (pointer == -1) {
action.run();
}
}
});
}
public void onHoverExit(Runnable action) {
button.addListener(new ClickListener() {
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
if (pointer == -1) {
action.run();
}
}
});
}
}

View File

@ -1,10 +1,20 @@
package org.vibecoders.moongazer.ui;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
import org.vibecoders.moongazer.managers.Assets;
public class UIImageButton extends UIButton {
public UIImageButton() {
this.button = new ImageButton(new ImageButton.ImageButtonStyle());
public UIImageButton(String texturePath) {
Texture texture = Assets.getAsset(texturePath, Texture.class);
TextureRegionDrawable drawable = new TextureRegionDrawable(new TextureRegion(texture));
ImageButton.ImageButtonStyle style = new ImageButton.ImageButtonStyle();
style.imageUp = drawable;
style.imageDown = drawable;
style.imageOver = drawable;
this.button = new ImageButton(style);
this.actor = button;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB