Merge pull request #7 from teppyboy/Iuno_OP_scene

feat(scene): dialogue scene implementation
This commit is contained in:
Nguyễn Thế Hưng
2025-10-04 00:22:31 +07:00
committed by GitHub
11 changed files with 553 additions and 21 deletions

View File

@ -29,6 +29,7 @@ public class Game extends ApplicationAdapter {
public Scene mainMenuScene;
public Scene settingsScene;
public ArrayList<Scene> gameScenes;
private boolean usingCustomScene = false;
@Override
public void create() {
@ -60,6 +61,9 @@ public class Game extends ApplicationAdapter {
stage.draw();
return;
}
// Only use state-based scene switching if not using custom scene
if (!usingCustomScene) {
switch (this.state) {
case INTRO:
currentScene = introScene;
@ -76,18 +80,23 @@ public class Game extends ApplicationAdapter {
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);
}
}
// 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);
}
}

View File

@ -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");

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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(() -> {

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB