package net.alcuria.umbracraft.engine.windows.message; import net.alcuria.umbracraft.Game; import net.alcuria.umbracraft.editor.Drawables; import net.alcuria.umbracraft.engine.scripts.MessageScriptCommand.MessageEmotion; import net.alcuria.umbracraft.engine.scripts.MessageScriptCommand.MessageStyle; import net.alcuria.umbracraft.engine.windows.WindowLayout; import net.alcuria.umbracraft.engine.windows.WindowTable; import net.alcuria.umbracraft.engine.windows.message.MessageLabel.LabelEffect; import net.alcuria.umbracraft.listeners.Listener; import net.alcuria.umbracraft.util.StringUtils; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.actions.TemporalAction; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Scaling; import com.kotcrab.vis.ui.widget.VisLabel; /** The layout for dialogue messages shown to the player. * @author Andrew Keturi */ public class MessageWindowLayout extends WindowLayout { /** Input states for the message * @author Andrew Keturi */ private static enum MessageState { STEP_1_CREATE, STEP_2_ACCEPT_INPUT, STEP_3_MESSAGE_DISPLAYED } private static final int FACE_WIDTH = 105; private static final int MESSAGE_WIDTH = 340; private static final float SHOW_TRANSITION_TIME = 0.1f; private boolean hasFace = false; private String message; private final Array<MessageLabel> messageLabels = new Array<MessageLabel>(); private MessageStyle messageStyle = MessageStyle.NORMAL; private WindowTable messageWindow; private Label nameLabel; private Table nameTable, messageTable, faceTable; private MessageState state = MessageState.STEP_1_CREATE; private final LabelStyle style = new LabelStyle(Game.assets().get("fonts/message.fnt", BitmapFont.class), Color.WHITE); public MessageWindowLayout(MessageStyle messageStyle) { this.messageStyle = messageStyle; } /** Advances the message state * @return <code>true</code> when we are ready to close the message */ public boolean advance() { switch (state) { case STEP_1_CREATE: return false; case STEP_2_ACCEPT_INPUT: immediatelyShowAllText(); return false; case STEP_3_MESSAGE_DISPLAYED: return true; } return false; } @Override public void hide(final Listener completeListener) { for (MessageLabel messageLabel : messageLabels) { messageLabel.addAction(Actions.sequence(Actions.delay(SHOW_TRANSITION_TIME), Actions.parallel(Actions.moveBy(0, -10, SHOW_TRANSITION_TIME, Interpolation.pow2In), Actions.alpha(0, SHOW_TRANSITION_TIME)))); } nameTable.addAction(Actions.sequence(Actions.parallel(Actions.alpha(0, SHOW_TRANSITION_TIME), Actions.moveBy(-10, 0, SHOW_TRANSITION_TIME, Interpolation.pow2Out)))); faceTable.addAction(Actions.sequence(Actions.parallel(Actions.alpha(0, SHOW_TRANSITION_TIME), Actions.moveBy(-10, 0, SHOW_TRANSITION_TIME, Interpolation.pow2Out)))); messageWindow.addAction(Actions.sequence(Actions.delay(SHOW_TRANSITION_TIME), Actions.parallel(Actions.moveBy(0, -10, SHOW_TRANSITION_TIME, Interpolation.pow2In), Actions.alpha(0, SHOW_TRANSITION_TIME)), Actions.run(new Runnable() { @Override public void run() { completeListener.invoke(); } }))); } private void immediatelyShowAllText() { setMessage(message, true); state = MessageState.STEP_3_MESSAGE_DISPLAYED; } private Table messageTextTable() { return new Table() { { add(messageTable = new Table()).size(MESSAGE_WIDTH + 30, 60); } }; } public void setFace(String id, MessageEmotion emotion) { faceTable.clear(); hasFace = false; if (StringUtils.isNotEmpty(id)) { hasFace = true; final TextureRegion region = Drawables.faces(id, emotion); Image faceImage = new Image(region); faceImage.setScaling(Scaling.none); faceTable.add(faceImage); faceTable.addAction(Actions.sequence(Actions.alpha(0), Actions.delay(SHOW_TRANSITION_TIME), Actions.moveBy(-10, 0), Actions.parallel(Actions.alpha(1, SHOW_TRANSITION_TIME), Actions.moveBy(10, 0, SHOW_TRANSITION_TIME, Interpolation.pow2Out)))); } } /** Sets the message to display on the layout and begins animating it. * @param message the message {@link String} * @param instant if <code>true</code>, show text instantly. Otherwise, it * animates in by letter. */ public void setMessage(String message, boolean instant) { this.message = message; if (messageTable != null) { messageLabels.clear(); messageTable.clear(); // split the message by space characters. for (String word : message.split(" ")) { messageLabels.add(new MessageLabel(MessageLabel.parse(word), style, LabelEffect.from(word))); } // go thru every word int idx = 0; int rows = 0; while (idx < messageLabels.size) { Table row = new Table(); if (hasFace) { row.add().expand().fill().width(FACE_WIDTH / 2); } int rowWidth = hasFace ? FACE_WIDTH / 2 : 0; // add words to a row till we run out or go too wide while (rowWidth < MESSAGE_WIDTH && idx < messageLabels.size) { final float labelWidth = messageLabels.get(idx).getPrefWidth(); row.add(messageLabels.get(idx)).width(labelWidth).padRight(5); rowWidth += (labelWidth + 5); idx++; } row.add().expandX().fillX(); messageTable.add(row).expandX().left().padLeft(4).row(); rows++; } if (rows > 3) { Game.error("Message is cutoff: " + message); } messageTable.add().expand().fill(); // top aligns everything float charPause = instant ? 0f : 0.02f; int currentLetters = 0; for (final MessageLabel word : messageLabels) { final String wordText = word.getText().toString(); final int wordLen = word.getText().length; if (!instant) { word.setText(""); } word.clearActions(); word.addAction(Actions.sequence(Actions.delay(SHOW_TRANSITION_TIME + currentLetters * charPause), new TemporalAction(wordLen * charPause) { float cur = 0; int letters = 0; @Override protected void end() { super.end(); word.setText(wordText); switch (word.getEffect()) { case SQUISHY: word.addAction(Actions.forever(Actions.sequence(Actions.moveBy(0, -1, 0.05f), Actions.moveBy(0, 2, 0.1f), Actions.moveBy(0, -1, 0.05f)))); break; default: break; } } @Override protected void update(float percent) { cur += percent; if (cur > (float) letters / wordLen) { letters++; if (letters > wordLen) { return; } word.setText(wordText.substring(0, letters)); } } })); currentLetters += wordLen - 1; } messageWindow.addAction(Actions.sequence(Actions.delay(SHOW_TRANSITION_TIME + currentLetters * charPause), Actions.run(new Runnable() { @Override public void run() { state = MessageState.STEP_3_MESSAGE_DISPLAYED; } }))); } } public void setName(String name) { if (nameLabel == null) { throw new NullPointerException("nameLabel is null. Call show() first"); } if (StringUtils.isNotEmpty(name)) { nameLabel.setText(name); nameTable.setVisible(true); nameTable.addAction(Actions.sequence(Actions.alpha(0), Actions.delay(SHOW_TRANSITION_TIME), Actions.moveBy(-10, 0), Actions.parallel(Actions.alpha(1, SHOW_TRANSITION_TIME), Actions.moveBy(10, 0, SHOW_TRANSITION_TIME, Interpolation.pow2Out)))); } else { nameTable.setVisible(false); } } @Override public void show() { Table windowFrame = new Table() { { add(nameTable = new Table() { { setBackground(Drawables.ninePatch("ui/namePlate")); add(nameLabel = new VisLabel("Amiru", new LabelStyle(Game.assets().get("fonts/message.fnt", BitmapFont.class), Color.WHITE))).pad(0, 60, 18, 20); } }).height(20).expand().bottom().left().row(); add(messageWindow = new WindowTable(messageStyle) { { getColor().a = 0; put(messageTextTable()); } }).expand().fillX().bottom().padBottom(10); } }; content.add(faceTable = new Table()).bottom().width(0).padRight(-FACE_WIDTH); //= width of face content.add(windowFrame).expand().bottom(); faceTable.toFront(); // do actions messageWindow.addAction(Actions.sequence(Actions.alpha(0), Actions.moveBy(0, -10), Actions.parallel(Actions.moveBy(0, 10, SHOW_TRANSITION_TIME, Interpolation.pow2Out), Actions.alpha(1, SHOW_TRANSITION_TIME)), Actions.run(new Runnable() { @Override public void run() { state = MessageState.STEP_2_ACCEPT_INPUT; } }))); messageTable.addAction(Actions.sequence(Actions.alpha(0), Actions.delay(0.3f), Actions.alpha(1, SHOW_TRANSITION_TIME))); } }