diff --git a/app/src/main/java/org/vibecoders/moongazer/Game.java b/app/src/main/java/org/vibecoders/moongazer/Game.java index 1ef576c..9c7bc6d 100644 --- a/app/src/main/java/org/vibecoders/moongazer/Game.java +++ b/app/src/main/java/org/vibecoders/moongazer/Game.java @@ -29,6 +29,7 @@ public class Game extends ApplicationAdapter { public Scene mainMenuScene; public Scene settingsScene; public ArrayList gameScenes; + private boolean usingCustomScene = false; @Override public void create() { @@ -60,34 +61,42 @@ public class Game extends ApplicationAdapter { stage.draw(); return; } - switch (this.state) { - case INTRO: - currentScene = introScene; - break; - case MAIN_MENU: - currentScene = mainMenuScene; - break; - case SETTINGS: - currentScene = settingsScene; - break; - case IN_GAME: - // Render in-game scene - break; - default: - log.warn("Unknown state: {}", state); + + // Only use state-based scene switching if not using custom scene + if (!usingCustomScene) { + switch (this.state) { + case INTRO: + currentScene = introScene; + break; + case MAIN_MENU: + currentScene = mainMenuScene; + break; + case SETTINGS: + currentScene = settingsScene; + break; + case IN_GAME: + // Render in-game scene + break; + default: + log.warn("Unknown state: {}", state); + } } for (var scene : gameScenes) { // log.trace("Checking scene visibility: {}", scene.getClass().getSimpleName()); - if (scene != currentScene && scene.root.isVisible()) { + if (scene != currentScene && scene.root != null && scene.root.isVisible()) { log.trace("Hiding scene: {}", scene.getClass().getSimpleName()); scene.root.setVisible(false); } } - batch.begin(); - currentScene.render(batch); - batch.end(); + // Only render if currentScene is not null + if (currentScene != null) { + batch.begin(); + currentScene.render(batch); + batch.end(); + } + // Handle stage drawing for UI elements stage.act(Gdx.graphics.getDeltaTime()); stage.draw(); @@ -108,4 +117,86 @@ public class Game extends ApplicationAdapter { stage.dispose(); log.debug("Resources disposed"); } -} + + /** + * Sets the current scene directly (for VN and other non-state-based scenes). + * + * @param scene the scene to switch to + */ + public void setScene(Scene scene) { + if (currentScene != null && currentScene.root != null) { + currentScene.root.setVisible(false); + } + currentScene = scene; + usingCustomScene = true; + if (scene.root != null) { + scene.root.setVisible(true); + if (!gameScenes.contains(scene)) { + gameScenes.add(scene); + if (scene.root.getStage() == null) { + stage.addActor(scene.root); + } + } + } + log.debug("Scene switched to: {}", scene.getClass().getSimpleName()); + } + + /** + * Sets the game state and returns to state-based scene switching. + * + * @param newState the new game state + */ + public void setState(State newState) { + this.state = newState; + this.usingCustomScene = false; + // Reset input processor to main stage + Gdx.input.setInputProcessor(stage); + log.debug("State changed to: {}", newState); + } + + /** + * Sets the current scene and state (used by transitions). + * + * @param scene the scene to set as current + * @param newState the new state + */ + public void setCurrentSceneAndState(Scene scene, State newState) { + this.currentScene = scene; + this.state = newState; + this.usingCustomScene = false; + if (scene.root != null) { + scene.root.setVisible(true); + } + Gdx.input.setInputProcessor(stage); + log.debug("Current scene set to: {}, state: {}", scene.getClass().getSimpleName(), newState); + } + + /** + * Returns to main menu from a custom scene. + */ + public void returnToMainMenu() { + // Create transition from DialogueScene to MainMenu instead of direct switch + if (currentScene != null && usingCustomScene && currentScene instanceof DialogueScene) { + log.debug("Creating transition from DialogueScene to MainMenu"); + DialogueScene dialogueScene = (DialogueScene) currentScene; + dialogueScene.enterTransition(); // Mark as entering transition to stop rendering + transition = new DialogueToMenuTransition(this, dialogueScene, mainMenuScene, 500); + return; + } + + // Fallback for other custom scenes + if (currentScene != null && usingCustomScene) { + log.debug("Disposing custom scene: {}", currentScene.getClass().getSimpleName()); + currentScene.dispose(); + currentScene = null; + } + + setState(State.MAIN_MENU); + currentScene = mainMenuScene; + if (mainMenuScene.root != null) { + mainMenuScene.root.setVisible(true); + } + Gdx.input.setInputProcessor(stage); + log.debug("Returned to main menu, state={}, usingCustomScene={}", state, usingCustomScene); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/vibecoders/moongazer/managers/Assets.java b/app/src/main/java/org/vibecoders/moongazer/managers/Assets.java index 8e66d90..0de77b0 100644 --- a/app/src/main/java/org/vibecoders/moongazer/managers/Assets.java +++ b/app/src/main/java/org/vibecoders/moongazer/managers/Assets.java @@ -126,6 +126,9 @@ public class Assets { assetManager.load("textures/ui/UI_SliderKnob.png", Texture.class); assetManager.load("textures/ui/UI_SliderBg.png", Texture.class); assetManager.load("textures/ui/UI_SliderBg2.png", Texture.class); + // VN scene textures + assetManager.load("textures/vn_scene/char_base.png", Texture.class); + assetManager.load("textures/vn_scene/separator.png", Texture.class); // "Load" unsupported file types as FileHandle loadingThread = new Thread(() -> { loadAny("videos/main_menu_background.webm"); diff --git a/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueScene.java b/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueScene.java new file mode 100644 index 0000000..c3155a6 --- /dev/null +++ b/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueScene.java @@ -0,0 +1,183 @@ +package org.vibecoders.moongazer.scenes; + +import static org.vibecoders.moongazer.Constants.*; + +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.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.InputListener; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Image; +import com.badlogic.gdx.utils.viewport.ScreenViewport; + +import org.vibecoders.moongazer.Game; +import org.vibecoders.moongazer.managers.Assets; +import org.vibecoders.moongazer.vn.CharacterActor; +import org.vibecoders.moongazer.vn.ChoiceBox; +import org.vibecoders.moongazer.vn.DialogueBoxTransparent; + +public class DialogueScene extends Scene { + private Stage stage; + private final CharacterActor character; + private final DialogueBoxTransparent dialogue; + private ChoiceBox choice; + private int step = 0; + private float alpha = 1f; + private boolean isActive = true; + private boolean inTransition = false; + + public DialogueScene(Game game) { + super(game); + stage = new Stage(new ScreenViewport()); + Gdx.input.setInputProcessor(stage); + + Texture bgTexture = Assets.getAsset("textures/main_menu/background.png", Texture.class); + Image background = new Image(bgTexture); + background.setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + background.setColor(0.5f, 0.5f, 0.5f, 1f); + stage.addActor(background); + + Pixmap overlayPixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); + overlayPixmap.setColor(0, 0, 0, 0.4f); + overlayPixmap.fill(); + Texture overlayTexture = new Texture(overlayPixmap); + overlayPixmap.dispose(); + Image overlay = new Image(overlayTexture); + overlay.setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + stage.addActor(overlay); + + TextureRegion charBase = loadTexture("textures/vn_scene/char_base.png"); + character = new CharacterActor(charBase); + float charX = (WINDOW_WIDTH - character.getWidth()) / 2f; + float charY = (WINDOW_HEIGHT - character.getHeight()) / 2f + 100; + character.setPosition(charX, charY); + stage.addActor(character); + + TextureRegion dialogBg = createDialogBackground(); + TextureRegion separator = loadTexture("textures/vn_scene/separator.png"); + + var font = Assets.getFont("ui", 20); + dialogue = new DialogueBoxTransparent(font, dialogBg, separator, WINDOW_WIDTH - 100); + dialogue.setPosition(50, 20); + stage.addActor(dialogue); + + showStep(0); + + stage.addListener(new InputListener() { + @Override + public boolean keyDown(InputEvent e, int keycode) { + if (choice == null) { + nextOrSkip(); + } + return true; + } + + @Override + public boolean touchDown(InputEvent e, float x, float y, int pointer, int button) { + if (choice == null) { + nextOrSkip(); + return true; + } + return false; + } + }); + } + + private TextureRegion loadTexture(String path) { + try { + Texture tex = Assets.getAsset(path, Texture.class); + return new TextureRegion(tex); + } catch (Exception e) { + log.warn("Failed to load texture: {}, creating placeholder", path); + Pixmap pixmap = new Pixmap(100, 100, Pixmap.Format.RGBA8888); + pixmap.setColor(Color.GRAY); + pixmap.fill(); + Texture tex = new Texture(pixmap); + pixmap.dispose(); + return new TextureRegion(tex); + } + } + + private TextureRegion createDialogBackground() { + Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); + pixmap.setColor(0, 0, 0, 0.7f); + pixmap.fill(); + Texture tex = new Texture(pixmap); + pixmap.dispose(); + return new TextureRegion(tex); + } + + private void nextOrSkip() { + if (!dialogue.isDone()) { + dialogue.skip(); + return; + } + showStep(++step); + } + + private void showStep(int s) { + if (choice != null) { + choice.remove(); + choice = null; + } + + switch (s) { + case 0: + dialogue.setDialogue("Iuno", "Hmph. Apologies, but I need my rest..."); + break; + case 1: + var font = Assets.getFont("ui", 18); + choice = new ChoiceBox(font, new String[]{"New game", "Back to Main Menu"}, idx -> { + if (idx == 0) { + dialogue.setDialogue("Iuno", "Toi yeu tunxd..."); + step = 2; + } else { + game.returnToMainMenu(); + } + }); + choice.setPosition(WINDOW_WIDTH - 260, WINDOW_HEIGHT / 2); + stage.addActor(choice); + break; + case 2: + game.returnToMainMenu(); + break; + } + } + + public void setAlpha(float alpha) { + this.alpha = alpha; + if (stage != null && stage.getRoot() != null) { + stage.getRoot().setColor(1, 1, 1, alpha); + } + } + + public void enterTransition() { + inTransition = true; + } + + @Override + public void render(SpriteBatch batch) { + if (inTransition || !isActive || stage == null) { + return; + } + try { + stage.act(Gdx.graphics.getDeltaTime()); + stage.draw(); + } catch (Exception e) { + isActive = false; + log.warn("Error rendering DialogueScene, marking as inactive", e); + } + } + + @Override + public void dispose() { + if (stage != null) { + stage.dispose(); + stage = null; + } + } +} diff --git a/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueToMenuTransition.java b/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueToMenuTransition.java new file mode 100644 index 0000000..49b4b4d --- /dev/null +++ b/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueToMenuTransition.java @@ -0,0 +1,48 @@ +package org.vibecoders.moongazer.scenes; + +import org.vibecoders.moongazer.Game; +import org.vibecoders.moongazer.State; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; + +public class DialogueToMenuTransition extends Transition { + private final DialogueScene from; + private final Scene to; + private float totalTime = 0f; + private final long duration; + + public DialogueToMenuTransition(Game game, DialogueScene from, Scene to, long duration) { + super(game, from, to, State.MAIN_MENU, duration); + this.from = from; + this.to = to; + this.duration = duration; + } + + @Override + public void render(SpriteBatch batch) { + totalTime += Gdx.graphics.getDeltaTime(); + float progress = totalTime / (((float) duration) / 1000); + + if (progress >= 1.0f) { + game.transition = null; + from.dispose(); + game.setCurrentSceneAndState(to, State.MAIN_MENU); + return; + } + + float fromOpacity = 1 - progress; + from.setAlpha(fromOpacity); + from.render(batch); + + float toOpacity = progress; + if (to.root != null) { + to.root.setVisible(true); + to.root.setColor(1, 1, 1, toOpacity); + } + batch.setColor(1, 1, 1, toOpacity); + to.render(batch); + + batch.setColor(1, 1, 1, 1); + } +} diff --git a/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueTransition.java b/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueTransition.java new file mode 100644 index 0000000..b43d50f --- /dev/null +++ b/app/src/main/java/org/vibecoders/moongazer/scenes/DialogueTransition.java @@ -0,0 +1,47 @@ +package org.vibecoders.moongazer.scenes; + +import org.vibecoders.moongazer.Game; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; + +public class DialogueTransition extends Transition { + private final Scene from; + private final DialogueScene to; + private float totalTime = 0f; + private final long duration; + + public DialogueTransition(Game game, Scene from, DialogueScene to, long duration) { + super(game, from, to, null, duration); + this.from = from; + this.to = to; + this.duration = duration; + } + + @Override + public void render(SpriteBatch batch) { + totalTime += Gdx.graphics.getDeltaTime(); + float progress = totalTime / (((float) duration) / 1000); + + if (progress >= 1.0f) { + game.transition = null; + to.setAlpha(1f); + game.setScene(to); + return; + } + + float fromOpacity = 1 - progress; + if (from.root != null) { + from.root.setVisible(true); + from.root.setColor(1, 1, 1, fromOpacity); + } + batch.setColor(1, 1, 1, fromOpacity); + from.render(batch); + + to.setAlpha(progress); + batch.setColor(1, 1, 1, 1); + to.render(batch); + + batch.setColor(1, 1, 1, 1); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/vibecoders/moongazer/scenes/MainMenu.java b/app/src/main/java/org/vibecoders/moongazer/scenes/MainMenu.java index 6743f9c..e963ce8 100644 --- a/app/src/main/java/org/vibecoders/moongazer/scenes/MainMenu.java +++ b/app/src/main/java/org/vibecoders/moongazer/scenes/MainMenu.java @@ -93,7 +93,14 @@ public class MainMenu extends Scene { exitButton.setPosition(centerX, startY - spacing * 4); // Mouse click handlers - playButton.onClick(() -> log.debug("Play clicked")); + playButton.onClick(() -> { + log.debug("Play clicked"); + // Create transition to DialogueScene + if (game.transition == null) { + DialogueScene dialogueScene = new DialogueScene(game); + game.transition = new DialogueTransition(game, this, dialogueScene, 500); + } + }); loadButton.onClick(() -> log.debug("Load clicked")); leaderboardButton.onClick(() -> log.debug("Leaderboard clicked")); settingsButton.onClick(() -> { diff --git a/app/src/main/java/org/vibecoders/moongazer/vn/CharacterActor.java b/app/src/main/java/org/vibecoders/moongazer/vn/CharacterActor.java new file mode 100644 index 0000000..dba08ed --- /dev/null +++ b/app/src/main/java/org/vibecoders/moongazer/vn/CharacterActor.java @@ -0,0 +1,13 @@ +package org.vibecoders.moongazer.vn; + +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.scenes.scene2d.ui.Image; + +public class CharacterActor extends Image { + public CharacterActor(TextureRegion baseReg) { + super(baseReg); + float ox = getWidth() * 0.5f; + float oy = 0f; + setOrigin(ox, oy); + } +} diff --git a/app/src/main/java/org/vibecoders/moongazer/vn/ChoiceBox.java b/app/src/main/java/org/vibecoders/moongazer/vn/ChoiceBox.java new file mode 100644 index 0000000..b9d2a57 --- /dev/null +++ b/app/src/main/java/org/vibecoders/moongazer/vn/ChoiceBox.java @@ -0,0 +1,36 @@ +package org.vibecoders.moongazer.vn; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.scenes.scene2d.Group; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; + +public class ChoiceBox extends Group { + public interface Listener { + void onChoice(int idx); + } + + public ChoiceBox(BitmapFont font, String[] options, Listener listener) { + TextButton.TextButtonStyle style = new TextButton.TextButtonStyle(); + style.font = font; + + VerticalGroup col = new VerticalGroup().space(8); + + for (int i = 0; i < options.length; i++) { + final int idx = i; + TextButton b = new TextButton(options[i], style); + b.addListener(new ClickListener() { + @Override + public void clicked(InputEvent e, float x, float y) { + listener.onChoice(idx); + } + }); + col.addActor(b); + } + + addActor(col); + col.pack(); + } +} diff --git a/app/src/main/java/org/vibecoders/moongazer/vn/DialogueBoxTransparent.java b/app/src/main/java/org/vibecoders/moongazer/vn/DialogueBoxTransparent.java new file mode 100644 index 0000000..2b64194 --- /dev/null +++ b/app/src/main/java/org/vibecoders/moongazer/vn/DialogueBoxTransparent.java @@ -0,0 +1,104 @@ +package org.vibecoders.moongazer.vn; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.scenes.scene2d.Group; +import com.badlogic.gdx.scenes.scene2d.ui.Image; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.utils.Align; + +/** + * Transparent dialogue box with typing effect. + * Shows speaker name, separator, and dialogue text. + */ +public class DialogueBoxTransparent extends Group { + private final Label nameLabel; + private final Label textLabel; + private CharSequence fullText; + private float shown = 0f; + private boolean done = true; + private final float charPerSec = 45f; + + private static final float BOX_HEIGHT = 200f; + private static final float TEXT_MARGIN = 20f; + + public DialogueBoxTransparent(BitmapFont font, TextureRegion bgRegion, TextureRegion sepRegion, float width) { + Image background = new Image(bgRegion); + background.setSize(width, BOX_HEIGHT); + background.setColor(1f, 1f, 1f, 0.3f); + addActor(background); + + Label.LabelStyle nameStyle = new Label.LabelStyle(); + nameStyle.font = font; + nameStyle.fontColor = Color.GOLD; + nameLabel = new Label("", nameStyle); + nameLabel.setFontScale(1.2f); + nameLabel.setAlignment(Align.center); + nameLabel.setWidth(width); + nameLabel.setPosition(0, BOX_HEIGHT + 40); + addActor(nameLabel); + + Image separator = new Image(sepRegion); + float separatorDisplayWidth = width; + float aspectRatio = (float) sepRegion.getRegionHeight() / (float) sepRegion.getRegionWidth(); + float separatorDisplayHeight = separatorDisplayWidth * aspectRatio; + + if (separatorDisplayHeight > 180f) { + separatorDisplayHeight = 180f; + separatorDisplayWidth = separatorDisplayHeight / aspectRatio; + } + + separator.setSize(separatorDisplayWidth, separatorDisplayHeight); + float sepX = (width - separatorDisplayWidth) / 2f; + float sepY = BOX_HEIGHT - 100; + separator.setPosition(sepX, sepY); + separator.setColor(1f, 1f, 1f, 1f); + addActor(separator); + + Label.LabelStyle textStyle = new Label.LabelStyle(); + textStyle.font = font; + textStyle.fontColor = Color.WHITE; + textLabel = new Label("", textStyle); + textLabel.setWrap(true); + textLabel.setWidth(width - TEXT_MARGIN * 2); + textLabel.setAlignment(Align.center); + textLabel.setPosition(TEXT_MARGIN, (BOX_HEIGHT / 2) - 20); + addActor(textLabel); + + setSize(width, BOX_HEIGHT + 30); + } + + public void setDialogue(String speaker, String text) { + nameLabel.setText(speaker); + fullText = text; + shown = 0f; + done = false; + textLabel.setText(""); + } + + public boolean isDone() { + return done; + } + + public void skip() { + if (fullText != null) { + textLabel.setText(fullText); + done = true; + } + } + + @Override + public void act(float delta) { + super.act(delta); + if (done || fullText == null) return; + + shown += charPerSec * delta; + int n = Math.min(fullText.length(), (int) shown); + textLabel.setText(fullText.subSequence(0, n)); + + if (n >= fullText.length()) { + done = true; + } + } +} \ No newline at end of file diff --git a/app/src/main/resources/textures/vn_scene/char_base.png b/app/src/main/resources/textures/vn_scene/char_base.png new file mode 100644 index 0000000..70c7420 Binary files /dev/null and b/app/src/main/resources/textures/vn_scene/char_base.png differ diff --git a/app/src/main/resources/textures/vn_scene/separator.png b/app/src/main/resources/textures/vn_scene/separator.png new file mode 100644 index 0000000..d5b3a2f Binary files /dev/null and b/app/src/main/resources/textures/vn_scene/separator.png differ