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
+