diff --git a/Projekte/battleship/client/build.gradle b/Projekte/battleship/client/build.gradle index 03c3222..64ab82a 100644 --- a/Projekte/battleship/client/build.gradle +++ b/Projekte/battleship/client/build.gradle @@ -7,6 +7,10 @@ description = 'Battleship Client' dependencies { implementation project(":jme-common") implementation project(":battleship:model") + implementation 'org.jmonkeyengine:jme3-core:3.6.0-stable' + implementation 'org.jmonkeyengine:jme3-effects:3.6.0-stable' + + implementation libs.jme3.desktop implementation project(path: ':battleship:server') 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 2d663b7..1f31648 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 @@ -10,6 +10,9 @@ package pp.battleship.client.gui; import static java.util.Objects.requireNonNull; +import com.jme3.effect.ParticleEmitter; +import com.jme3.effect.ParticleMesh.Type; +import com.jme3.effect.shapes.EmitterSphereShape; import com.jme3.material.Material; import com.jme3.material.RenderState.BlendMode; import com.jme3.math.ColorRGBA; @@ -54,6 +57,199 @@ class SeaSynchronizer extends ShipMapSynchronizer { private final ShipMap map; private final BattleshipApp app; + private ParticleEmitter flame, flash, spark, roundspark, smoketrail, debris, + shockwave; + + + private static final int COUNT_FACTOR = 1; + private static final float COUNT_FACTOR_F = 1f; + + private static final boolean POINT_SPRITE = true; + private static final Type EMITTER_TYPE = POINT_SPRITE ? Type.Point : Type.Triangle; + /** + * Creates a Flame texture with indefininite Lifetime + * @return flame texture + */ + + private ParticleEmitter createFlame() { + flame = new ParticleEmitter("Flame", EMITTER_TYPE, 32 * COUNT_FACTOR); + flame.setSelectRandomImage(true); + flame.setStartColor(new ColorRGBA(1f, 0.4f, 0.05f, (1f / COUNT_FACTOR_F))); + flame.setEndColor(new ColorRGBA(.4f, .22f, .12f, 0f)); + flame.setStartSize(0.1f); + flame.setEndSize(0.5f); + flame.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f)); + flame.setParticlesPerSec(0); + flame.setGravity(0, -5, 0); + flame.setLowLife(.4f); + flame.setHighLife(.5f); + flame.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 7, 0)); + flame.getParticleInfluencer().setVelocityVariation(1f); + flame.setImagesX(2); + flame.setImagesY(2); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/flame.png")); + mat.setBoolean("PointSprite", POINT_SPRITE); + flame.setMaterial(mat); + + // Setze die Höhe auf 2f + flame.setLocalTranslation(0, 2f, 0); + + return flame; + } + + private ParticleEmitter createSpark() { + spark = new ParticleEmitter("Spark", Type.Triangle, 30 * COUNT_FACTOR); + spark.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1.0f / COUNT_FACTOR_F)); + spark.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f)); + spark.setStartSize(.5f); + spark.setEndSize(.5f); + spark.setFacingVelocity(true); + spark.setParticlesPerSec(0); + spark.setGravity(0, 5, 0); + spark.setLowLife(1.1f); + spark.setHighLife(1.5f); + spark.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 20, 0)); + spark.getParticleInfluencer().setVelocityVariation(1); + spark.setImagesX(1); + spark.setImagesY(1); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/spark.png")); + spark.setMaterial(mat); + + // Setze die Höhe auf 2f + spark.setLocalTranslation(0, 2f, 0); + + return spark; + } + + private ParticleEmitter createRoundSpark() { + roundspark = new ParticleEmitter("RoundSpark", EMITTER_TYPE, 20 * COUNT_FACTOR); + roundspark.setStartColor(new ColorRGBA(1f, 0.29f, 0.34f, (float) (1.0 / COUNT_FACTOR_F))); + roundspark.setEndColor(new ColorRGBA(0, 0, 0, 0.5f / COUNT_FACTOR_F)); + roundspark.setStartSize(0.2f); + roundspark.setEndSize(0.8f); + roundspark.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f)); + roundspark.setParticlesPerSec(0); + roundspark.setGravity(0, -.5f, 0); + roundspark.setLowLife(1.8f); + roundspark.setHighLife(2f); + roundspark.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 3, 0)); + roundspark.getParticleInfluencer().setVelocityVariation(.5f); + roundspark.setImagesX(1); + roundspark.setImagesY(1); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/roundspark.png")); + mat.setBoolean("PointSprite", POINT_SPRITE); + roundspark.setMaterial(mat); + + // Setze die Höhe auf 2f + roundspark.setLocalTranslation(0, 2f, 0); + + return roundspark; + } + + + + private ParticleEmitter createSmokeTrail() { + smoketrail = new ParticleEmitter("SmokeTrail", Type.Triangle, 22 * COUNT_FACTOR); + smoketrail.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1.0f / COUNT_FACTOR_F)); + smoketrail.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f)); + smoketrail.setStartSize(.2f); + smoketrail.setEndSize(1f); + smoketrail.setFacingVelocity(true); + smoketrail.setParticlesPerSec(0); + smoketrail.setGravity(0, 1, 0); + smoketrail.setLowLife(.4f); + smoketrail.setHighLife(.5f); + smoketrail.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 12, 0)); + smoketrail.getParticleInfluencer().setVelocityVariation(1); + smoketrail.setImagesX(1); + smoketrail.setImagesY(3); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/smoketrail.png")); + smoketrail.setMaterial(mat); + + // Setze die Höhe auf 2f + smoketrail.setLocalTranslation(0, 2f, 0); + + return smoketrail; + } + + + private ParticleEmitter createDebris() { + debris = new ParticleEmitter("Debris", Type.Triangle, 15 * COUNT_FACTOR); + debris.setSelectRandomImage(true); + debris.setRandomAngle(true); + debris.setRotateSpeed(FastMath.TWO_PI * 4); + debris.setStartColor(new ColorRGBA(1f, 0.59f, 0.28f, 1.0f / COUNT_FACTOR_F)); + debris.setEndColor(new ColorRGBA(.5f, 0.5f, 0.5f, 0f)); + debris.setStartSize(.10f); + debris.setEndSize(.15f); + debris.setParticlesPerSec(0); + debris.setGravity(0, 12f, 0); + debris.setLowLife(1.4f); + debris.setHighLife(1.5f); + debris.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 15, 0)); + debris.getParticleInfluencer().setVelocityVariation(.60f); + debris.setImagesX(3); + debris.setImagesY(3); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/Debris.png")); + debris.setMaterial(mat); + + // Setze die Höhe auf 2f + debris.setLocalTranslation(0, 2f, 0); + + return debris; + } + + + private ParticleEmitter createShockwave(){ + shockwave = new ParticleEmitter("Shockwave", Type.Triangle, 1 * COUNT_FACTOR); +// shockwave.setRandomAngle(true); + shockwave.setFaceNormal(Vector3f.UNIT_Y); + shockwave.setStartColor(new ColorRGBA(.48f, 0.17f, 0.01f, .8f / COUNT_FACTOR_F)); + shockwave.setEndColor(new ColorRGBA(.48f, 0.17f, 0.01f, 0f)); + + shockwave.setStartSize(0f); + shockwave.setEndSize(3f); + + shockwave.setParticlesPerSec(0); + shockwave.setGravity(0, 0, 0); + shockwave.setLowLife(0.5f); + shockwave.setHighLife(0.5f); + shockwave.getParticleInfluencer() + .setInitialVelocity(new Vector3f(0, 0, 0)); + shockwave.getParticleInfluencer().setVelocityVariation(0f); + shockwave.setImagesX(1); + shockwave.setImagesY(1); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/shockwave.png")); + shockwave.setMaterial(mat); + + return shockwave; + } + + private ParticleEmitter createMovingSmokeEmitter() { + ParticleEmitter smokeEmitter = new ParticleEmitter("SmokeEmitter", Type.Triangle, 300); + smokeEmitter.setGravity(0, 0, 0); + smokeEmitter.getParticleInfluencer().setVelocityVariation(1); + smokeEmitter.setLowLife(1); + smokeEmitter.setHighLife(1); + smokeEmitter.getParticleInfluencer().setInitialVelocity(new Vector3f(0, .5f, 0)); + smokeEmitter.setImagesX(15); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Smoke/Smoke.png")); + smokeEmitter.setMaterial(mat); + + // Setze die Höhe auf 2f + smokeEmitter.setLocalTranslation(0, 2f, 0); + + return smokeEmitter; + } + + /** * Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map. * @@ -93,11 +289,52 @@ class SeaSynchronizer extends ShipMapSynchronizer { 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); + //final Geometry representation = createCylinder(shot); + //representation.getLocalTranslation().subtractLocal(shipNode.getLocalTranslation()); + //shipNode.attachChild(representation); + // Create a new node specifically for the hit effects + Node hitEffectNode = new Node("HitEffectNode"); - return null; + // Create particle effects + ParticleEmitter flame = createFlame(); + // ParticleEmitter flash = createFlash(); + ParticleEmitter spark = createSpark(); + ParticleEmitter roundSpark = createRoundSpark(); + ParticleEmitter smokeTrail = createSmokeTrail(); + ParticleEmitter debris = createDebris(); + ParticleEmitter shockwave = createShockwave(); + ParticleEmitter movingSmoke = createMovingSmokeEmitter(); // New moving smoke emitter + + // Attach all effects to the hitEffectNode + hitEffectNode.attachChild(flame); + hitEffectNode.attachChild(flash); + hitEffectNode.attachChild(spark); + hitEffectNode.attachChild(roundSpark); + hitEffectNode.attachChild(smokeTrail); + hitEffectNode.attachChild(debris); + hitEffectNode.attachChild(shockwave); + hitEffectNode.attachChild(movingSmoke); + + // Set the local translation for the hit effect to the point of impact + hitEffectNode.setLocalTranslation(shot.getY() + 0.5f - shipNode.getLocalTranslation().x, + 0.5f, // Adjust as needed for height above the ship + shot.getX() + 0.5f - shipNode.getLocalTranslation().z); + + // Attach the hitEffectNode to the shipNode so it moves with the ship + shipNode.attachChild(hitEffectNode); + + // Emit particles when the hit happens + flash.emitAllParticles(); + spark.emitAllParticles(); + smokeTrail.emitAllParticles(); + debris.emitAllParticles(); + shockwave.emitAllParticles(); + flame.emitAllParticles(); + roundSpark.emitAllParticles(); + + + + return null; } /** diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SinkingShip.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SinkingShip.java new file mode 100644 index 0000000..58a5c8e --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SinkingShip.java @@ -0,0 +1,116 @@ +package pp.battleship.client.gui; + +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.control.AbstractControl; + +/** + * Controls the burning, tilting, and sinking behavior of a battleship. + * The ship will burn and tilt for a specified duration, then sink below the water surface. + */ +public class SinkingShip extends AbstractControl { + + private float elapsedTime = 0f; + private final float burnTiltDuration; + private final float sinkDuration; + private final float sinkDepth; + private final float tiltAngle; + private final Vector3f initialPosition; + private boolean sinkingStarted = false; + + /** + * Constructs a new ShipSinkingControl instance. + * + * @param burnTiltDuration Time in seconds for the ship to burn and tilt on the surface + * @param sinkDuration Time in seconds for the ship to fully sink + * @param sinkDepth Depth below the water to sink the ship + * @param tiltAngle Final tilt angle in degrees + */ + public SinkingShip(float burnTiltDuration, float sinkDuration, float sinkDepth, float tiltAngle) { + this.burnTiltDuration = burnTiltDuration; + this.sinkDuration = sinkDuration; + this.sinkDepth = sinkDepth; + this.tiltAngle = tiltAngle; + this.initialPosition = new Vector3f(); // Placeholder; will be set in controlUpdate + } + + /** + * Overrides controlUpdate in AbstractControl + * regulates the burn and tilt timeframe + * @param tpf time per frame (in seconds) + */ + + @Override + protected void controlUpdate(float tpf) { + if (spatial == null) return; + + elapsedTime += tpf; + + if (elapsedTime < burnTiltDuration) { + float progress = elapsedTime / burnTiltDuration; + + float angleInRadians = FastMath.DEG_TO_RAD * FastMath.interpolateLinear(progress, 0f, -tiltAngle); + Quaternion tiltRotation = new Quaternion().fromAngles(angleInRadians, 0, 0); + spatial.setLocalRotation(tiltRotation); + return; + } + + // Start sinking if it hasn't started yet + if (!sinkingStarted) { + sinkingStarted = true; + // Save the initial position when sinking starts + initialPosition.set(spatial.getLocalTranslation()); + + // Remove the hitEffectNode + Node parentNode = (Node) spatial; + Spatial hitEffects = parentNode.getChild("HitEffectNode"); + if (hitEffects != null) { + parentNode.detachChild(hitEffects); + } + } + + // Calculate the progress of the sinking (0 to 1) + float progress = Math.min((elapsedTime - burnTiltDuration) / sinkDuration, 1f); + + // Apply the tilt angle (remains constant during sinking) + Quaternion tiltRotation = new Quaternion().fromAngles(-FastMath.DEG_TO_RAD * tiltAngle, 0, 0); + spatial.setLocalRotation(tiltRotation); + + // Sink the ship by interpolating the Y position + float currentY = FastMath.interpolateLinear(progress, initialPosition.y, sinkDepth); + spatial.setLocalTranslation(initialPosition.x, currentY, initialPosition.z); + + if (currentY <= sinkDepth) { + Node parentNode = (Node) spatial.getParent(); + if (parentNode != null) { + parentNode.detachChild(spatial); + } + spatial.removeControl(this); + } + } + + /** + * This method is called during the rendering phase, but it does not perform any + * operations in this implementation as the control only influences the spatial's + * transformation, not its rendering process. + * + * @param rm the RenderManager rendering the controlled Spatial (not null) + * @param vp the ViewPort being rendered (not null) + */ + @Override + protected void controlRender(RenderManager rm, ViewPort vp) { + // No rendering logic is needed for this control + } + + /** + * Starts the sinking process for the ship. + */ + public void startSinking() { + // Nothing to do here as the control update handles the timing and sequence + } +} diff --git a/Projekte/battleship/client/src/main/resources/Effects/Explosion/Debris.png b/Projekte/battleship/client/src/main/resources/Effects/Explosion/Debris.png new file mode 100644 index 0000000..c6eab7b Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Explosion/Debris.png differ diff --git a/Projekte/battleship/client/src/main/resources/Effects/Explosion/flame.png b/Projekte/battleship/client/src/main/resources/Effects/Explosion/flame.png new file mode 100644 index 0000000..d3a31f7 Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Explosion/flame.png differ diff --git a/Projekte/battleship/client/src/main/resources/Effects/Explosion/flash.png b/Projekte/battleship/client/src/main/resources/Effects/Explosion/flash.png new file mode 100644 index 0000000..d240917 Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Explosion/flash.png differ diff --git a/Projekte/battleship/client/src/main/resources/Effects/Explosion/roundspark.png b/Projekte/battleship/client/src/main/resources/Effects/Explosion/roundspark.png new file mode 100644 index 0000000..bb2b5d8 Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Explosion/roundspark.png differ diff --git a/Projekte/battleship/client/src/main/resources/Effects/Explosion/shockwave.png b/Projekte/battleship/client/src/main/resources/Effects/Explosion/shockwave.png new file mode 100644 index 0000000..62856bd Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Explosion/shockwave.png differ diff --git a/Projekte/battleship/client/src/main/resources/Effects/Explosion/smoketrail.png b/Projekte/battleship/client/src/main/resources/Effects/Explosion/smoketrail.png new file mode 100644 index 0000000..5b96518 Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Explosion/smoketrail.png differ diff --git a/Projekte/battleship/client/src/main/resources/Effects/Explosion/spark.png b/Projekte/battleship/client/src/main/resources/Effects/Explosion/spark.png new file mode 100644 index 0000000..fa2d25a Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Explosion/spark.png differ diff --git a/Projekte/battleship/client/src/main/resources/Effects/Smoke/Smoke.png b/Projekte/battleship/client/src/main/resources/Effects/Smoke/Smoke.png new file mode 100644 index 0000000..ec46564 Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Effects/Smoke/Smoke.png differ