diff --git a/Projekte/battleship/client/build.gradle b/Projekte/battleship/client/build.gradle index a8aeff3c..248d1e83 100644 --- a/Projekte/battleship/client/build.gradle +++ b/Projekte/battleship/client/build.gradle @@ -9,6 +9,7 @@ implementation project(":jme-common") implementation project(":battleship:model") implementation libs.jme3.desktop + implementation libs.jme3.effects runtimeOnly libs.jme3.awt.dialogs runtimeOnly libs.jme3.plugins diff --git a/Projekte/battleship/client/maps/map4.json b/Projekte/battleship/client/maps/map4.json new file mode 100644 index 00000000..3c823c2a --- /dev/null +++ b/Projekte/battleship/client/maps/map4.json @@ -0,0 +1,66 @@ +{ + "width": 10, + "height": 10, + "ships": [ + { + "length": 1, + "x": 9, + "y": 0, + "rot": "RIGHT" + }, + { + "length": 1, + "x": 9, + "y": 1, + "rot": "RIGHT" + }, + { + "length": 1, + "x": 8, + "y": 0, + "rot": "RIGHT" + }, + { + "length": 1, + "x": 8, + "y": 1, + "rot": "RIGHT" + }, + { + "length": 2, + "x": 8, + "y": 2, + "rot": "RIGHT" + }, + { + "length": 2, + "x": 6, + "y": 0, + "rot": "RIGHT" + }, + { + "length": 2, + "x": 6, + "y": 1, + "rot": "RIGHT" + }, + { + "length": 3, + "x": 5, + "y": 2, + "rot": "RIGHT" + }, + { + "length": 3, + "x": 7, + "y": 3, + "rot": "RIGHT" + }, + { + "length": 4, + "x": 6, + "y": 4, + "rot": "RIGHT" + } + ] +} \ No newline at end of file diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ImpactEffectManager.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ImpactEffectManager.java new file mode 100644 index 00000000..76e37da3 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ImpactEffectManager.java @@ -0,0 +1,150 @@ +package pp.battleship.client.gui; + +import com.jme3.app.Application; +import com.jme3.asset.AssetManager; +import com.jme3.effect.ParticleEmitter; +import com.jme3.effect.ParticleMesh.Type; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import com.jme3.scene.control.AbstractControl; +import pp.battleship.model.Shot; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; + +/** + * Manages the visual effects for impact events such as hits and misses. + */ +public class ImpactEffectManager { + + private final AssetManager assetManager; + private static final Logger LOGGER = System.getLogger(ImpactEffectManager.class.getName()); + + private Material particleMaterial; + + /** + * Constructor to initialize the asset manager via the main application. + * + * @param app The main application instance. + */ + public ImpactEffectManager(Application app) { + this.assetManager = app.getAssetManager(); + this.particleMaterial = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"); + } + + /** + * Generates a visual effect for a hit at the specified position. + * + * @param battleshipNode The node where the effect should be attached. + * @param shot The details of the shot where the hit occurred. + */ + public void triggerHitEffect(Node battleshipNode, Shot shot) { + ParticleEmitter hitEffect = createParticleEmitter("HitEffect", 30, ColorRGBA.Orange, ColorRGBA.Red, 0.45f, 0.1f, 1f, 2f); + hitEffect.setLocalTranslation(shot.getY() + 0.5f, 0, shot.getX() + 0.5f); + + LOGGER.log(Level.DEBUG, "Hit effect created at position: {0}", hitEffect.getLocalTranslation().toString()); + + hitEffect.emitAllParticles(); + battleshipNode.attachChild(hitEffect); + hitEffect.addControl(new EffectCleanupControl(hitEffect, battleshipNode)); + } + + /** + * Creates a visual effect for a missed shot at the specified location. + * + * @param shot The details of the missed shot. + * @return The particle emitter representing the miss effect. + */ + public ParticleEmitter triggerMissEffect(Shot shot) { + ParticleEmitter missEffect = createParticleEmitter("MissEffect", 15, ColorRGBA.Blue, ColorRGBA.Cyan, 0.3f, 0.05f, 0.5f, 1.5f); + missEffect.setLocalTranslation(shot.getY() + 0.5f, 0, shot.getX() + 0.5f); + missEffect.emitAllParticles(); + missEffect.addControl(new EffectCleanupControl(missEffect)); + return missEffect; + } + + /** + * Helper method to create a particle emitter with predefined parameters. + * + * @param name The name of the particle emitter. + * @param count The number of particles to emit. + * @param startColor The initial color of the particles. + * @param endColor The final color of the particles. + * @param startSize The initial size of the particles. + * @param endSize The final size of the particles. + * @param lowLife The minimum lifetime of the particles. + * @param highLife The maximum lifetime of the particles. + * @return The configured ParticleEmitter instance. + */ + private ParticleEmitter createParticleEmitter(String name, int count, ColorRGBA startColor, ColorRGBA endColor, float startSize, float endSize, float lowLife, float highLife) { + ParticleEmitter emitter = new ParticleEmitter(name, Type.Triangle, count); + emitter.setMaterial(particleMaterial); + emitter.setImagesX(2); + emitter.setImagesY(2); + emitter.setStartColor(startColor); + emitter.setEndColor(endColor); + emitter.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1, 0)); + emitter.setStartSize(startSize); + emitter.setEndSize(endSize); + emitter.setGravity(0, -0.5f, 0); + emitter.setLowLife(lowLife); + emitter.setHighLife(highLife); + emitter.setParticlesPerSec(0); + return emitter; + } + + /** + * Custom control class to handle the cleanup of particle effects once they are no longer visible. + */ + private static class EffectCleanupControl extends AbstractControl { + + private final ParticleEmitter emitter; + private final Node attachedNode; + + /** + * Constructor for managing cleanup when a particle emitter is attached to a specific node. + * + * @param emitter The particle emitter to manage. + * @param attachedNode The node to which the emitter is attached. + */ + public EffectCleanupControl(ParticleEmitter emitter, Node attachedNode) { + this.emitter = emitter; + this.attachedNode = attachedNode; + } + + /** + * Constructor for managing cleanup of a standalone particle emitter. + * + * @param emitter The particle emitter to manage. + */ + public EffectCleanupControl(ParticleEmitter emitter) { + this(emitter, null); + } + + /** + * Removes the emitter when all particles are no longer visible. + * + * @param tpf Time per frame. + */ + @Override + protected void controlUpdate(float tpf) { + if (emitter.getParticlesPerSec() == 0 && emitter.getNumVisibleParticles() == 0) { + if (attachedNode != null) { + attachedNode.detachChild(emitter); + } + } + } + + /** + * No custom rendering needed; particle behavior is handled in controlUpdate. + * @param rm handles rendering, and ViewPort + * @param vp defines the view where the scene is displayed. + */ + @Override + protected void controlRender(com.jme3.renderer.RenderManager rm, com.jme3.renderer.ViewPort vp) { + // No rendering-specific behavior needed for this control. + } + } +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java index e9842b15..18167c89 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java @@ -47,6 +47,7 @@ class SeaSynchronizer extends ShipMapSynchronizer { private final ShipMap map; private final BattleshipApp app; + private ImpactEffectManager effectHandler; /** * Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map. @@ -59,6 +60,7 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) { super(app.getGameLogic().getOwnMap(), root); this.app = app; this.map = map; + effectHandler = new ImpactEffectManager(app); addExisting(); } @@ -72,7 +74,7 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) { */ @Override public Spatial visit(Shot shot) { - return shot.isHit() ? handleHit(shot) : createCylinder(shot); + return shot.isHit() ? handleHit(shot) : effectHandler.triggerMissEffect(shot); } /** @@ -87,9 +89,7 @@ private Spatial handleHit(Shot shot) { final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship"); final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node"); - final Geometry representation = createCylinder(shot); - representation.getLocalTranslation().subtractLocal(shipNode.getLocalTranslation()); - shipNode.attachChild(representation); + effectHandler.triggerHitEffect(shipNode, shot); return null; } diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java index 81acfbb1..2c0214c4 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java @@ -14,6 +14,10 @@ import com.jme3.scene.control.AbstractControl; import pp.battleship.model.Battleship; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; + + import static pp.util.FloatMath.DEG_TO_RAD; import static pp.util.FloatMath.TWO_PI; import static pp.util.FloatMath.sin; @@ -47,6 +51,14 @@ class ShipControl extends AbstractControl { * The current time within the oscillation cycle, used to calculate the ship's pitch angle. */ private float time; + /** + * The current Ship + */ + private final Battleship battleship; + /** + * + */ + static final Logger LOGGER = System.getLogger(ShipControl.class.getName()); /** * Constructs a new ShipControl instance for the specified Battleship. @@ -56,6 +68,7 @@ class ShipControl extends AbstractControl { * @param ship the Battleship object to control */ public ShipControl(Battleship ship) { + battleship = ship; // Determine the axis of rotation based on the ship's orientation axis = switch (ship.getRot()) { case LEFT, RIGHT -> Vector3f.UNIT_X; @@ -63,7 +76,7 @@ public ShipControl(Battleship ship) { }; // Set the cycle duration and amplitude based on the ship's length - cycle = ship.getLength() * 2f; + cycle = battleship.getLength() * 2f; amplitude = 5f * DEG_TO_RAD / ship.getLength(); } @@ -80,15 +93,21 @@ protected void controlUpdate(float tpf) { // Update the time within the oscillation cycle time = (time + tpf) % cycle; + if (battleship.isDestroyed() && spatial.getLocalTranslation().getY() <= -0.6f) { + LOGGER.log(Level.INFO, "Ship removed {0}", spatial.getName()); + spatial.getParent().detachChild(spatial); + } else if (battleship.isDestroyed()) { + spatial.move(0, -0.05f * tpf, 0); + } else { + // Update the time within the oscillation cycle + time = (time + tpf) % cycle; - // Calculate the current angle of the oscillation - final float angle = amplitude * sin(time * TWO_PI / cycle); + // Apply the pitch rotation to the spatial + spatial.setLocalRotation(pitch); + } - // Update the pitch Quaternion with the new angle - pitch.fromAngleAxis(angle, axis); - - // Apply the pitch rotation to the spatial spatial.setLocalRotation(pitch); + } /** diff --git a/Projekte/settings.gradle b/Projekte/settings.gradle index 7440607c..d9526adc 100644 --- a/Projekte/settings.gradle +++ b/Projekte/settings.gradle @@ -29,6 +29,8 @@ library('mockito-core', 'org.mockito:mockito-core:3.+') library('groovy-jsr223', 'org.apache.groovy:groovy-jsr223:4.0.22') library('slf4j-nop', 'org.slf4j:slf4j-nop:2.0.13') + + library('jme3-effects', 'org.jmonkeyengine', 'jme3-effects').versionRef('jme') } } }