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

Class Method, % Branch, % Line, %
FireParticleSystem 0% (0/6) 0% (0/31) 0% (0/50)
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/15) 0% (0/73) 0% (0/123)


 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;
 
 //TODO ALL JAVADOC
 public class FireParticleSystem {
 
     private static final int  MAX_PARTICLES     = 30;
     private static final long SPAWN_INTERVAL_NS = 100_000_000L;
     private static final long FRAME_INTERVAL_NS = 1_000_000_000L / 30; // 30 fps cap
     private static final int  STEPS_PER_FRAME   = 3; // physics steps per frame → 3× speed
 
     private final Pane         pane;
     private final Scene        scene;
     private final List<Particle> particles = new ArrayList<>();
     private final Random       rnd        = new Random();
     private AnimationTimer     timer;
     private long               lastSpawn  = 0;
     private long               lastFrame  = 0;
     private boolean            warmedUp   = false;
 
     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. */
     public Pane getPane() { return pane; }
 
     /** Starts the animation timer. */
     public void start() {
         timer = new AnimationTimer() {
             @Override
             public void handle(long now) {
                 if (now - lastFrame < FRAME_INTERVAL_NS) return;
                 lastFrame = now;
                 tick(now);
             }
         };
         timer.start();
     }
 
     /** Stops the animation timer. */
     public void stop() {
         if (timer != null) timer.stop();
     }
 
     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);
             }
         }
     }
 
     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);
     }
 
     // ── Particle ──────────────────────────────────────────────────────────────
 
     private static class Particle {
 
         double x, y, vx, vy;
         double life;
         double size;
         double wobblePhase, wobbleSpeed, wobbleAmp;
         final int    type;
         final Random rnd;
 
         final Circle core;
         final Circle glow; // non-null only for embers
 
         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) {           // ember
                 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) {    // spark
                 speed = 1.5 + rnd.nextDouble() * 2.0;
                 size  = rnd.nextDouble() * 1.5 + 0.5;
                 glow  = null;
                 core  = new Circle(size / 2);
             } else {                   // dust
                 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;
         }
 
         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 addTo(Pane pane) {
             if (glow != null) pane.getChildren().add(glow);
             pane.getChildren().add(core);
         }
 
         void removeFrom(Pane pane) {
             pane.getChildren().remove(core);
             if (glow != null) pane.getChildren().remove(glow);
         }
 
         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;
         }
 
         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);
         }
 
         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);
             }
         }
     }
 }