/*
* The MIT License (MIT)
*
* FXGL - JavaFX Game Library
*
* Copyright (c) 2015-2017 AlmasB (almaslvl@gmail.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.almasb.fxglgames.spaceinvaders;
import com.almasb.fxgl.annotation.Handles;
import com.almasb.fxgl.annotation.OnUserAction;
import com.almasb.fxgl.app.ApplicationMode;
import com.almasb.fxgl.app.FXGL;
import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.core.logging.Logger;
import com.almasb.fxgl.ecs.Entity;
import com.almasb.fxgl.entity.GameEntity;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.gameplay.Achievement;
import com.almasb.fxgl.gameplay.AchievementManager;
import com.almasb.fxgl.input.InputMapping;
import com.almasb.fxgl.io.FS;
import com.almasb.fxgl.service.Input;
import com.almasb.fxgl.settings.GameSettings;
import com.almasb.fxgl.ui.UI;
import com.almasb.fxglgames.spaceinvaders.control.PlayerControl;
import com.almasb.fxglgames.spaceinvaders.event.BonusPickupEvent;
import com.almasb.fxglgames.spaceinvaders.event.GameEvent;
import com.almasb.fxglgames.spaceinvaders.tutorial.Tutorial;
import com.almasb.fxglgames.spaceinvaders.tutorial.TutorialStep;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.util.Duration;
import java.util.Map;
import java.util.stream.IntStream;
import static com.almasb.fxglgames.spaceinvaders.Config.*;
/**
* A simple clone of Space Invaders. Demonstrates basic FXGL features.
*
* @author Almas Baimagambetov (AlmasB) (almaslvl@gmail.com)
*/
public class SpaceInvadersApp extends GameApplication {
private Logger log;
@Override
protected void initSettings(GameSettings settings) {
settings.setTitle("Space Invaders");
settings.setVersion("0.8");
settings.setWidth(WIDTH);
settings.setHeight(HEIGHT);
settings.setIntroEnabled(false);
settings.setMenuEnabled(false);
settings.setProfilingEnabled(false);
settings.setApplicationMode(ApplicationMode.DEVELOPER);
}
@Override
protected void initAchievements() {
AchievementManager am = getAchievementManager();
am.registerAchievement(new Achievement("Hitman", "Destroy " + ACHIEVEMENT_ENEMIES_KILLED + " enemies"));
am.registerAchievement(new Achievement("Master Scorer", "Score " + ACHIEVEMENT_MASTER_SCORER + " points"));
}
@Override
protected void initInput() {
Input input = getInput();
input.addInputMapping(new InputMapping("Move Left", KeyCode.A));
input.addInputMapping(new InputMapping("Move Right", KeyCode.D));
input.addInputMapping(new InputMapping("Shoot", MouseButton.PRIMARY));
}
@OnUserAction(name = "Move Left")
public void moveLeft() {
playerControl.left();
}
@OnUserAction(name = "Move Right")
public void moveRight() {
playerControl.right();
}
@OnUserAction(name = "Shoot")
public void shoot() {
playerControl.shoot();
}
private GameEntity player;
private PlayerControl playerControl;
private int highScore;
private String highScoreName;
private GameController uiController;
@Override
protected void preInit() {
getAudioPlayer().setGlobalSoundVolume(0.2);
getAudioPlayer().setGlobalMusicVolume(0.2);
}
private SaveData savedData = null;
@Override
protected void initGameVars(Map<String, Object> vars) {
vars.put("score", 0);
vars.put("level", 0);
vars.put("lives", START_LIVES);
vars.put("enemiesKilled", 0);
}
@Override
protected void initGame() {
log = FXGL.getLogger("SpaceInvaders");
// we have to use file system directly, since we are running without menus
FS.<SaveData>readDataTask(SAVE_DATA_NAME)
.onSuccess(data -> savedData = data)
.onFailure(ignore -> {})
.execute();
initGame(savedData == null
? new SaveData("CPU", ACHIEVEMENT_MASTER_SCORER)
: savedData);
}
private void initGame(SaveData data) {
highScoreName = data.getName();
highScore = data.getHighScore();
getAchievementManager().getAchievementByName("Hitman")
.bind(getGameState().intProperty("enemiesKilled").greaterThanOrEqualTo(ACHIEVEMENT_ENEMIES_KILLED));
getAchievementManager().getAchievementByName("Master Scorer")
.bind(getGameState().intProperty("score").greaterThanOrEqualTo(ACHIEVEMENT_MASTER_SCORER));
spawnBackground();
spawnPlayer();
if (!runningFirstTime)
nextLevel();
}
private void spawnBackground() {
getGameWorld().spawn("Background");
getMasterTimer().runAtInterval(() -> {
getGameWorld().spawn("Meteor");
}, Duration.seconds(3));
}
private void spawnPlayer() {
player = (GameEntity) getGameWorld().spawn("Player", getWidth() / 2 - 20, getHeight() - 40);
playerControl = player.getControlUnsafe(PlayerControl.class);
}
private void spawnWall(double x, double y) {
getGameWorld().spawn("Wall", x, y);
}
private void spawnBonus(double x, double y, BonusType type) {
getGameWorld().spawn("Bonus", new SpawnData(x, y).put("type", type));
}
private void initLevel() {
log.debug("initLevel()");
for (int y = 0; y < ENEMY_ROWS; y++) {
for (int x = 0; x < ENEMIES_PER_ROW; x++) {
getGameWorld().spawn("Enemy", x*60, 150 + 50 * getGameState().getInt("level") + y*60);
}
}
spawnWall(40, getHeight() - 100);
spawnWall(120, getHeight() - 100);
spawnWall(getWidth() - 160, getHeight() - 100);
spawnWall(getWidth() - 80, getHeight() - 100);
getInput().setProcessInput(true);
}
private void cleanupLevel() {
log.debug("cleanupLevel()");
getGameWorld().getEntitiesByType(
SpaceInvadersType.BONUS,
SpaceInvadersType.WALL,
SpaceInvadersType.BULLET)
.forEach(Entity::removeFromWorld);
}
private void nextLevel() {
log.debug("nextLevel()");
getInput().setProcessInput(false);
cleanupLevel();
getGameState().setValue("enemiesKilled", 0);
getGameState().increment("level", +1);
if (getGameState().getInt("level") == 3) {
showGameOver();
return;
}
getGameWorld().spawn("LevelInfo");
getMasterTimer().runOnceAfter(this::initLevel, Duration.seconds(LEVEL_START_DELAY));
getAudioPlayer().playSound(Asset.SOUND_NEW_LEVEL);
}
@Override
protected void initUI() {
uiController = new GameController(getGameScene());
UI ui = getAssetLoader().loadUI(Asset.FXML_MAIN_UI, uiController);
uiController.getLabelScore().textProperty().bind(getGameState().intProperty("score").asString("Score: %d"));
uiController.getLabelHighScore().setText("HiScore: " + highScore + " " + highScoreName + "");
IntStream.range(0, getGameState().getInt("lives"))
.forEach(i -> uiController.addLife());
getGameScene().addUI(ui);
}
private boolean runningFirstTime = true;
@Override
protected void onUpdate(double tpf) {
if (runningFirstTime) {
getDisplay().showConfirmationBox("Play Tutorial?", yes -> {
if (yes)
playTutorial();
else
nextLevel();
});
runningFirstTime = false;
}
}
private void playTutorial() {
getInput().setRegisterInput(false);
// ideally we must obtain dynamic key codes because the keys
// may have been reassigned
TutorialStep step1 = new TutorialStep("Press A to move left", Asset.DIALOG_MOVE_LEFT, () -> {
getInput().mockKeyPress(KeyCode.A);
});
TutorialStep step2 = new TutorialStep("Press D to move right", Asset.DIALOG_MOVE_RIGHT, () -> {
getInput().mockKeyRelease(KeyCode.A);
getInput().mockKeyPress(KeyCode.D);
});
TutorialStep step3 = new TutorialStep("Press F to shoot", Asset.DIALOG_SHOOT, () -> {
getInput().mockKeyRelease(KeyCode.D);
getInput().mockButtonPress(MouseButton.PRIMARY, 0, 0);
getInput().mockButtonRelease(MouseButton.PRIMARY);
});
Text tutorialText = getUIFactory().newText("", Color.AQUA, 24);
tutorialText.textProperty().addListener((o, old, newText) -> {
tutorialText.setTranslateX(getWidth() / 2 - tutorialText.getLayoutBounds().getWidth() / 2);
});
tutorialText.setTranslateY(getHeight() / 2 - 50);
getGameScene().addUINode(tutorialText);
Tutorial tutorial = new Tutorial(tutorialText, () -> {
player.getPositionComponent().setValue(getWidth() / 2 - 20, getHeight() - 40);
getGameScene().removeUINode(tutorialText);
nextLevel();
getInput().setRegisterInput(true);
}, step1, step2, step3);
tutorial.play();
}
@Handles(eventType = "PLAYER_GOT_HIT")
public void onPlayerGotHit(GameEvent event) {
getGameState().increment("lives", -1);
uiController.loseLife();
playerControl.enableInvincibility();
getMasterTimer().runOnceAfter(() -> {
playerControl.disableInvincibility();
}, Duration.seconds(INVINCIBILITY_TIME));
getAudioPlayer().playSound(Asset.SOUND_LOSE_LIFE);
if (getGameState().getInt("lives") == 0)
showGameOver();
}
private int scoreForKill() {
return SCORE_ENEMY_KILL * (getGameState().getGameDifficulty().ordinal() + SCORE_DIFFICULTY_MODIFIER);
}
@Handles(eventType = "ENEMY_KILLED")
public void onEnemyKilled(GameEvent event) {
getGameState().increment("enemiesKilled", +1);
getGameState().increment("score", scoreForKill());
if (getGameState().getInt("enemiesKilled") == ENEMIES_PER_LEVEL)
nextLevel();
if (Math.random() < BONUS_SPAWN_CHANCE) {
int bonusSize = BonusType.values().length;
spawnBonus(Math.random() * (getWidth() - 50), Math.random() * getHeight() / 3,
BonusType.values()[(int)(Math.random()*bonusSize)]);
}
}
@Handles(eventType = "ENEMY_REACHED_END")
public void onEnemyReachedEnd(GameEvent event) {
getGameState().increment("enemiesKilled", +1);
getGameState().increment("lives", -1);
uiController.loseLife();
if (getGameState().getInt("lives") == 0)
showGameOver();
if (getGameState().getInt("enemiesKilled") == ENEMIES_PER_LEVEL)
nextLevel();
}
@Handles(eventType = "ANY")
public void onBonusPickup(BonusPickupEvent event) {
switch (event.getType()) {
case ATTACK_RATE:
playerControl.increaseAttackSpeed(PLAYER_BONUS_ATTACK_SPEED);
break;
case LIFE:
getGameState().increment("lives", +1);
uiController.addLife();
break;
}
}
private void showGameOver() {
getDisplay().showConfirmationBox("Demo Over. Play Again?", yes -> {
if (yes) {
startNewGame();
} else {
int score = getGameState().getInt("score");
if (score > highScore) {
getDisplay().showInputBox("High Score! Enter your name", playerName -> {
// we have to use file system directly, since we are running without menus
FS.writeDataTask(new SaveData(playerName, score), SAVE_DATA_NAME).execute();
exit();
});
} else {
exit();
}
}
});
}
public static void main(String[] args) {
launch(args);
}
}