diff --git a/pom.xml b/pom.xml index d914c2d..cfa4e4e 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,11 @@ javafx-fxml 21.0.6 + + org.openjfx + javafx-media + 21.0.6 + org.controlsfx controlsfx diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/FireParticleSystem.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/FireParticleSystem.java new file mode 100644 index 0000000..de21fbc --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/FireParticleSystem.java @@ -0,0 +1,202 @@ +package it.polimi.ingsw.gc14.View.GUI; + +import javafx.animation.AnimationTimer; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import javafx.scene.Scene; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +public class FireParticleSystem { + + private static final int MAX_PARTICLES = 20; + private static final long SPAWN_INTERVAL_NS = 100_000_000L; // ~10 spawns/sec + + private final Canvas canvas; + private final List particles = new ArrayList<>(); + private final Random rnd = new Random(); + private AnimationTimer timer; + private long lastSpawn = 0; + private boolean warmedUp = false; + + public FireParticleSystem(Scene scene) { + canvas = new Canvas(); + canvas.widthProperty().bind(scene.widthProperty()); + canvas.heightProperty().bind(scene.heightProperty()); + canvas.setMouseTransparent(true); + } + + public Canvas getCanvas() { + return canvas; + } + + public void start() { + timer = new AnimationTimer() { + @Override + public void handle(long now) { + tick(now); + } + }; + timer.start(); + } + + public void stop() { + if (timer != null) timer.stop(); + } + + private void tick(long now) { + double w = canvas.getWidth(); + double h = canvas.getHeight(); + if (w == 0 || h == 0) return; + + if (!warmedUp) { + warmedUp = true; + // Pre-populate: spawn a full batch and advance each particle a random + // number of frames so they're spread across the screen from frame 1. + for (int i = 0; i < MAX_PARTICLES; i++) { + Particle p = spawnParticle(w, h); + int advance = rnd.nextInt(800) + 100; // 100-900 frames ahead + for (int f = 0; f < advance; f++) p.update(); + if (!p.isDead(w, h)) particles.add(p); + } + } + + if (now - lastSpawn > SPAWN_INTERVAL_NS && particles.size() < MAX_PARTICLES) { + lastSpawn = now; + int count = rnd.nextInt(2) + 1; // 1-2 per spawn + for (int i = 0; i < count && particles.size() < MAX_PARTICLES; i++) { + particles.add(spawnParticle(w, h)); + } + } + + GraphicsContext gc = canvas.getGraphicsContext2D(); + gc.clearRect(0, 0, w, h); + + Iterator it = particles.iterator(); + while (it.hasNext()) { + Particle p = it.next(); + p.update(); + if (p.isDead(w, h)) { + it.remove(); + } else { + p.draw(gc, w, h); + } + } + } + + private Particle spawnParticle(double w, double h) { + // One of 4 corners, with a small random offset inside the corner area + int corner = rnd.nextInt(4); + double margin = 0.12; + double x, y; + switch (corner) { + case 0 -> { x = rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-left + case 1 -> { x = w - rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-right + case 2 -> { x = rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-left + default -> { x = w - rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-right + } + return new Particle(x, y, w, h, corner, rnd); + } + + private static class Particle { + double x, y; + double vx, vy; + double life; // only sparks use this for burnout + double size; + double wobblePhase; + double wobbleSpeed; + double wobbleAmp; + final int type; + + Particle(double x, double y, double w, double h, int corner, Random rnd) { + this.x = x; + this.y = y; + this.life = 1.0; + this.type = weightedType(rnd); + + // Direction: roughly toward the opposite corner, with angular spread + double targetX = (corner == 0 || corner == 2) ? w * 0.75 : w * 0.25; + double targetY = (corner == 0 || corner == 1) ? h * 0.25 : h * 0.75; + double dx = targetX - x; + double dy = targetY - y; + double len = Math.sqrt(dx * dx + dy * dy); + double baseAngle = Math.atan2(dy, dx); + double spread = Math.PI / 3.5; // ~51 degree spread + double angle = baseAngle + (rnd.nextDouble() - 0.5) * spread; + + double speed; + if (type == 0) { // ember + speed = 0.5 + rnd.nextDouble() * 0.7; + size = rnd.nextDouble() * 3 + 2; + } else if (type == 1) { // spark + speed = 1.5 + rnd.nextDouble() * 2.0; + size = rnd.nextDouble() * 1.5 + 0.5; + } else { // dust + speed = 0.2 + rnd.nextDouble() * 0.35; + size = rnd.nextDouble() * 8 + 5; + } + + vx = Math.cos(angle) * speed; + vy = Math.sin(angle) * speed; + + wobblePhase = rnd.nextDouble() * Math.PI * 2; + wobbleSpeed = 0.025 + rnd.nextDouble() * 0.04; + wobbleAmp = 0.1 + rnd.nextDouble() * 0.4; + } + + private static int weightedType(Random rnd) { + double r = rnd.nextDouble(); + if (r < 0.55) return 0; + if (r < 0.78) return 1; + return 2; + } + + void update() { + wobblePhase += wobbleSpeed; + vx += Math.sin(wobblePhase) * wobbleAmp * 0.05; + vy += Math.cos(wobblePhase) * wobbleAmp * 0.02; + x += vx; + y += vy; + if (type == 1) life -= 0.008 + Math.random() * 0.006; + } + + boolean isDead(double w, double h) { + double pad = size * 4; + boolean offScreen = x < -pad || x > w + pad || y < -pad || y > h + pad; + return offScreen || (type == 1 && life <= 0); + } + + void draw(GraphicsContext gc, double w, double h) { + // Fade only within 5% of any edge + double edge = Math.min(w, h) * 0.05; + double fadeX = Math.min(x / edge, Math.min((w - x) / edge, 1.0)); + double fadeY = Math.min(y / edge, Math.min((h - y) / edge, 1.0)); + double posAlpha = Math.max(0, Math.min(fadeX, fadeY)); + + double lifeAlpha = (type == 1) ? Math.min(life * 2.0, 1.0) : 1.0; + double alpha = posAlpha * lifeAlpha; + if (alpha <= 0.01) return; + + if (type == 0) { // ember: orange, glows + double dist = Math.sqrt((x - w / 2) * (x - w / 2) + (y - h / 2) * (y - h / 2)); + double hotness = Math.max(0, 1.0 - dist / (Math.sqrt(w * w + h * h) * 0.4)); + double g = 0.3 + hotness * 0.5; + gc.setFill(Color.color(1.0, g * 0.35, 0.0, alpha * 0.10)); + gc.fillOval(x - size * 2.2, y - size * 2.2, size * 4.4, size * 4.4); + gc.setFill(Color.color(1.0, g, 0.0, alpha * 0.88)); + gc.fillOval(x - size / 2, y - size / 2, size, size); + } else if (type == 1) { // spark: white-yellow + double brightness = Math.min(life * 2.5, 1.0); + gc.setFill(Color.color(1.0, brightness * 0.85 + 0.15, brightness * 0.15, alpha)); + gc.fillOval(x - size / 2, y - size / 2, size, size); + } else { // dust: gray-brown translucent + gc.setFill(Color.color(0.55, 0.38, 0.22, alpha * 0.20)); + gc.fillOval(x - size, y - size * 0.5, size * 2, size); + } + } + } +} diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java index a6a1d6a..36adaec 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java @@ -4,16 +4,24 @@ import it.polimi.ingsw.gc14.Controller.ClientController; import it.polimi.ingsw.gc14.ErrorType; import it.polimi.ingsw.gc14.Model.MiniModel; import it.polimi.ingsw.gc14.View.IView; +import javafx.animation.FadeTransition; import javafx.application.Application; import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.geometry.Rectangle2D; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyCombination; +import javafx.scene.layout.StackPane; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; import javafx.stage.Screen; import javafx.stage.Stage; +import javafx.util.Duration; import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.ENDED; import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.TOTEM_CHOICE; @@ -46,6 +54,8 @@ public class GUI extends Application implements IView { private ClientController controller; private boolean autoReenterFullscreen = true; + private boolean isFading = false; + private MediaPlayer bgMusic; /** @@ -80,14 +90,23 @@ public class GUI extends Application implements IView { controllerLeaderboard = loaderLeaderboard.getController(); controllerLeaderboard.setController(controller, () -> { controllerLogin.updateLoginButton(true); - primaryStage.setScene(loginScene); controllerLogin.showError(""); + fadeToScene(loginScene); }, loginScene); + wrapScene(loginScene); + wrapScene(totemScene); + wrapScene(mainScene); + wrapScene(leaderboardScene); + + FireParticleSystem fireParticles = new FireParticleSystem(mainScene); + ((StackPane) mainScene.getRoot()).getChildren().add(fireParticles.getCanvas()); + fireParticles.start(); + primaryStage.setScene(loginScene); Rectangle2D tmp = Screen.getPrimary().getVisualBounds(); primaryStage.setWidth(tmp.getWidth()); @@ -123,6 +142,12 @@ public class GUI extends Application implements IView { }); primaryStage.show(); Platform.runLater(() -> primaryStage.setFullScreen(true)); + + String musicUrl = getClass().getResource("/Audio/Music.mp3").toExternalForm(); + bgMusic = new MediaPlayer(new Media(musicUrl)); + bgMusic.setCycleCount(MediaPlayer.INDEFINITE); + bgMusic.setVolume(0.4); + bgMusic.play(); } @@ -154,18 +179,64 @@ public class GUI extends Application implements IView { synchronized (miniModel) { if (controller.miniModel.currentState.getGameStage() == TOTEM_CHOICE) { controllerTotem.render(); - primaryStage.setScene(totemScene); + fadeToScene(totemScene); } else if (controller.miniModel.currentState.getGameStage() == ENDED) { controllerLeaderboard.render(); - primaryStage.setScene(leaderboardScene); + fadeToScene(leaderboardScene); } else { - primaryStage.setScene(mainScene); controllerMain.render(); + fadeToScene(mainScene); } } }); } + private void wrapScene(Scene scene) { + Parent root = scene.getRoot(); + scene.setRoot(new StackPane(root)); + } + + private void fadeToScene(Scene newScene) { + Scene currentScene = primaryStage.getScene(); + if (currentScene == newScene || isFading) return; + isFading = true; + + StackPane currentRoot = (StackPane) currentScene.getRoot(); + Rectangle overlay = new Rectangle(); + overlay.setFill(Color.BLACK); + overlay.setOpacity(0); + overlay.widthProperty().bind(currentRoot.widthProperty()); + overlay.heightProperty().bind(currentRoot.heightProperty()); + currentRoot.getChildren().add(overlay); + + FadeTransition fadeOut = new FadeTransition(Duration.millis(500), overlay); + fadeOut.setFromValue(0); + fadeOut.setToValue(1); + fadeOut.setOnFinished(e -> { + currentRoot.getChildren().remove(overlay); + + StackPane newRoot = (StackPane) newScene.getRoot(); + Rectangle newOverlay = new Rectangle(); + newOverlay.setFill(Color.BLACK); + newOverlay.setOpacity(1); + newOverlay.widthProperty().bind(newRoot.widthProperty()); + newOverlay.heightProperty().bind(newRoot.heightProperty()); + newRoot.getChildren().add(newOverlay); + + primaryStage.setScene(newScene); + + FadeTransition fadeIn = new FadeTransition(Duration.millis(500), newOverlay); + fadeIn.setFromValue(1); + fadeIn.setToValue(0); + fadeIn.setOnFinished(ev -> { + newRoot.getChildren().remove(newOverlay); + isFading = false; + }); + fadeIn.play(); + }); + fadeOut.play(); + } + /** * Displays an error on the JavaFX application thread. * @@ -180,8 +251,8 @@ public class GUI extends Application implements IView { Platform.runLater(() -> { if (error == ErrorType.SERVER_CRASHED) { controllerLogin.updateLoginButton(true); - primaryStage.setScene(loginScene); controllerLogin.showError(error); + fadeToScene(loginScene); } else { synchronized (miniModel) { controllerMain.isError = true; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8bb899e..3592057 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,7 @@ module it.polimi.ingsw.gc14 { requires javafx.controls; requires javafx.fxml; + requires javafx.media; requires org.controlsfx.controls; requires java.rmi; requires java.smartcardio; diff --git a/src/main/resources/Audio/Music.mp3 b/src/main/resources/Audio/Music.mp3 new file mode 100644 index 0000000..667debb Binary files /dev/null and b/src/main/resources/Audio/Music.mp3 differ diff --git a/src/main/resources/GUIImages/Background.png b/src/main/resources/GUIImages/Background.png index f342e39..56ee27e 100644 Binary files a/src/main/resources/GUIImages/Background.png and b/src/main/resources/GUIImages/Background.png differ diff --git a/src/main/resources/GUIScene/login.fxml b/src/main/resources/GUIScene/login.fxml index b350965..8e0d78f 100644 --- a/src/main/resources/GUIScene/login.fxml +++ b/src/main/resources/GUIScene/login.fxml @@ -146,4 +146,4 @@ -fx-padding: 13 48 13 48; -fx-background-radius: 2; -fx-cursor: hand;"/> - \ No newline at end of file +