Coverage Summary for Class: FireParticleSystem (it.polimi.ingsw.gc14.View.GUI)

Class Method, % Branch, % Line, %
FireParticleSystem 0% (0/5) 0% (0/29) 0% (0/49)
FireParticleSystem$1 0% (0/2) 0% (0/2) 0% (0/4)
FireParticleSystem$Particle 0% (0/7) 0% (0/40) 0% (0/69)
Total 0% (0/14) 0% (0/71) 0% (0/122)


 package it.polimi.ingsw.gc14.View.GUI;
 
 import javafx.animation.AnimationTimer;
 import javafx.scene.Scene;
 import javafx.scene.layout.Pane;
 import javafx.scene.paint.Color;
 import javafx.scene.shape.Circle;
 
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Random;
 
 /**
  * Ambient fire-particle effect rendered on a transparent overlay pane.
  *
  * <p>Three particle types float from the screen corners toward the center:
  * <ul>
  *   <li><b>embers</b> (type 0) — slow, glowing orange circles with a soft halo.</li>
  *   <li><b>sparks</b> (type 1) — fast, short-lived bright particles.</li>
  *   <li><b>dust</b>  (type 2) — large, translucent brown drifting circles.</li>
  * </ul>
  *
  * <p>Call {@link #start()} after adding {@link #getPane()} to the scene graph
  */
 public class FireParticleSystem {
 
     /** Maximum number of live particles at any given time. */
     private static final int  MAX_PARTICLES     = 30;
     /** Minimum nanoseconds between particle spawn batches. */
     private static final long SPAWN_INTERVAL_NS = 100_000_000L;
     /** Minimum nanoseconds between rendered frames (30 fps cap). */
     private static final long FRAME_INTERVAL_NS = 1_000_000_000L / 30;
     /** Physics update steps applied per rendered frame (multiplies effective speed). */
     private static final int  STEPS_PER_FRAME   = 3;
 
     /** Mouse-transparent overlay pane that hosts all particle nodes. */
     private final Pane         pane;
     /** The scene whose dimensions are used to position and fade particles. */
     private final Scene        scene;
     /** Currently live particles. */
     private final List<Particle> particles = new ArrayList<>();
     /** Shared random source for particle initialization and physics noise. */
     private final Random       rnd        = new Random();
     /** Timestamp (ns) of the last particle spawn batch. */
     private long               lastSpawn  = 0;
     /** Timestamp (ns) of the last rendered frame. */
     private long               lastFrame  = 0;
     /** {@code true} after the first tick pre-populates the particle pool. */
     private boolean            warmedUp   = false;
 
     /**
      * Creates the particle system bound to the given scene.
      * The internal overlay pane is transparent and mouse-transparent.
      *
      * @param scene the scene whose dimensions are used for particle positioning and fading.
      */
     public FireParticleSystem(Scene scene) {
         this.scene = scene;
         pane = new Pane();
         pane.setMouseTransparent(true);
         pane.setPickOnBounds(false);
     }
 
     /**
      * Returns the transparent overlay pane that holds all particle nodes.
      *
      * @return the mouse-transparent {@link Pane} overlay.
      */
     public Pane getPane() { return pane; }
 
     /** Starts the animation timer. */
     public void start() {
         AnimationTimer timer = new AnimationTimer() {
             @Override
             public void handle(long now) {
                 if (now - lastFrame < FRAME_INTERVAL_NS) return;
                 lastFrame = now;
                 tick(now);
             }
         };
         timer.start();
     }
 
     /**
      * Advances the simulation by one frame: pre-warms on first call, spawns new particles,
      * steps existing ones, removes dead ones, and updates their visual state.
      *
      * @param now the current timestamp in nanoseconds from the animation timer.
      */
     private void tick(long now) {
         double w = scene.getWidth();
         double h = scene.getHeight();
         if (w == 0 || h == 0) return;
 
         if (!warmedUp) {
             warmedUp = true;
             for (int i = 0; i < MAX_PARTICLES; i++) {
                 Particle p = spawnParticle(w, h);
                 int advance = rnd.nextInt(800) + 100;
                 for (int f = 0; f < advance; f++) p.update();
                 if (!p.isDead(w, h)) {
                     p.addTo(pane);
                     p.updateVisual(w, h);
                     particles.add(p);
                 }
             }
         }
 
         if (now - lastSpawn > SPAWN_INTERVAL_NS && particles.size() < MAX_PARTICLES) {
             lastSpawn = now;
             int count = rnd.nextInt(2) + 1;
             for (int i = 0; i < count && particles.size() < MAX_PARTICLES; i++) {
                 Particle p = spawnParticle(w, h);
                 p.addTo(pane);
                 particles.add(p);
             }
         }
 
         Iterator<Particle> it = particles.iterator();
         while (it.hasNext()) {
             Particle p = it.next();
             for (int s = 0; s < STEPS_PER_FRAME; s++) p.update();
             if (p.isDead(w, h)) {
                 p.removeFrom(pane);
                 it.remove();
             } else {
                 p.updateVisual(w, h);
             }
         }
     }
 
     /**
      * Creates a new particle starting near one of the four screen corners.
      *
      * @param w scene width in pixels.
      * @param h scene height in pixels.
      * @return the newly constructed {@link Particle}.
      */
     private Particle spawnParticle(double w, double h) {
         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; }
             case 1 -> { x = w - rnd.nextDouble() * w * margin;  y = h - rnd.nextDouble() * h * margin; }
             case 2 -> { x = rnd.nextDouble() * w * margin;      y = rnd.nextDouble() * h * margin; }
             default -> { x = w - rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; }
         }
         return new Particle(x, y, w, h, corner, rnd);
     }
 
 
     /**
      * A single fire particle with position, velocity, wobble, and JavaFX visual nodes.
      * Particles are typed: 0 = ember, 1 = spark, 2 = dust.
      */
     private static class Particle {
 
         /** Current X position in scene pixels. */
         double x;
         /** Current Y position in scene pixels. */
         double y;
         /** Horizontal velocity component. */
         double vx;
         /** Vertical velocity component. */
         double vy;
         /** Remaining life fraction (1.0 = full, 0.0 = dead); only decrements for sparks. */
         double life;
         /** Visual radius of the particle's core circle. */
         final double size;
         /** Current phase of the sinusoidal wobble. */
         double wobblePhase;
         /** Angular speed of the wobble oscillation. */
         final double wobbleSpeed;
         /** Amplitude factor of the wobble displacement. */
         final double wobbleAmp;
         /** Particle type: 0 = ember, 1 = spark, 2 = dust. */
         final int    type;
         /** Shared random source used during physics updates. */
         final Random rnd;
 
         /** Primary visible circle node. */
         final Circle core;
         /** Soft glow halo circle behind {@link #core}; non-null only for embers (type 0). */
         final Circle glow;
 
         /**
          * Initializes the particle at position ({@code x}, {@code y}) with velocity
          * aimed roughly from the given {@code corner} toward the scene center.
          *
          * @param x      spawn X coordinate.
          * @param y      spawn Y coordinate.
          * @param w      scene width (used to compute target direction).
          * @param h      scene height (used to compute target direction).
          * @param corner spawn corner index: 0 = bottom-left, 1 = bottom-right, 2 = top-left, 3 = top-right.
          * @param rnd    shared random source.
          */
         Particle(double x, double y, double w, double h, int corner, Random rnd) {
             this.x   = x;
             this.y   = y;
             this.life = 1.0;
             this.rnd  = rnd;
             this.type = weightedType(rnd);
 
             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 baseAngle = Math.atan2(targetY - y, targetX - x);
             double angle     = baseAngle + (rnd.nextDouble() - 0.5) * (Math.PI / 3.5);
 
             double speed;
             if (type == 0) {
                 speed = 0.5 + rnd.nextDouble() * 0.7;
                 size  = rnd.nextDouble() * 3 + 2;
                 glow  = new Circle(size * 2.2);
                 core  = new Circle(size / 2);
             } else if (type == 1) {
                 speed = 1.5 + rnd.nextDouble() * 2.0;
                 size  = rnd.nextDouble() * 1.5 + 0.5;
                 glow  = null;
                 core  = new Circle(size / 2);
             } else {
                 speed = 0.2 + rnd.nextDouble() * 0.35;
                 size  = rnd.nextDouble() * 8 + 5;
                 glow  = null;
                 core  = new Circle(size);
                 core.setFill(Color.color(0.55, 0.38, 0.22));
             }
 
             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;
         }
 
         /**
          * Returns a particle type weighted toward embers (55% ember, 23% spark, 22% dust).
          *
          * @param rnd the random source.
          * @return particle type: 0, 1, or 2.
          */
         private static int weightedType(Random rnd) {
             double r = rnd.nextDouble();
             if (r < 0.55) return 0;
             if (r < 0.78) return 1;
             return 2;
         }
 
         /**
          * Adds this particle's visual nodes to {@code pane} (glow first so core renders on top).
          *
          * @param pane the overlay pane to add nodes to.
          */
         void addTo(Pane pane) {
             if (glow != null) pane.getChildren().add(glow);
             pane.getChildren().add(core);
         }
 
         /**
          * Removes this particle's visual nodes from {@code pane}.
          *
          * @param pane the overlay pane to remove nodes from.
          */
         void removeFrom(Pane pane) {
             pane.getChildren().remove(core);
             if (glow != null) pane.getChildren().remove(glow);
         }
 
         /** Advances this particle by one physics step: applies wobble, moves, and decrements spark life. */
         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 + rnd.nextDouble() * 0.006;
         }
 
         /**
          * Returns {@code true} when this particle has left the scene bounds or, for sparks, its life reached zero.
          *
          * @param w scene width.
          * @param h scene height.
          * @return {@code true} if the particle should be removed.
          */
         boolean isDead(double w, double h) {
             double pad = size * 4;
             return x < -pad || x > w + pad || y < -pad || y > h + pad
                     || (type == 1 && life <= 0);
         }
 
         /**
          * Updates color, opacity, and translate position of this particle's visual nodes
          * based on distance from scene edges and remaining life.
          *
          * @param w scene width.
          * @param h scene height.
          */
         void updateVisual(double w, double h) {
             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;
 
             core.setTranslateX(x);
             core.setTranslateY(y);
 
             if (type == 0) {
                 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;
                 core.setFill(Color.color(1.0, g, 0.0));
                 core.setOpacity(alpha * 0.88);
                 glow.setTranslateX(x);
                 glow.setTranslateY(y);
                 glow.setFill(Color.color(1.0, g * 0.35, 0.0));
                 glow.setOpacity(alpha * 0.10);
             } else if (type == 1) {
                 double brightness = Math.min(life * 2.5, 1.0);
                 core.setFill(Color.color(1.0, brightness * 0.85 + 0.15, brightness * 0.15));
                 core.setOpacity(alpha);
             } else {
                 core.setOpacity(alpha * 0.20);
             }
         }
     }
 }