diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java index e7215002..e915a995 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java @@ -34,6 +34,7 @@ public class GameSound extends AbstractAppState implements GameEventListener { private AudioNode splashSound; private AudioNode shipDestroyedSound; private AudioNode explosionSound; + private AudioNode missileLaunch; /** * Checks if sound is enabled in the preferences. @@ -78,6 +79,7 @@ public void initialize(AppStateManager stateManager, Application app) { shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS + missileLaunch = loadSound(app, "Sound/Effects/missileLaunch.wav"); } /** @@ -93,13 +95,16 @@ private AudioNode loadSound(Application app, String name) { sound.setLooping(false); sound.setPositional(false); return sound; - } - catch (AssetLoadException | AssetNotFoundException ex) { + } catch (AssetLoadException | AssetNotFoundException ex) { LOGGER.log(Level.ERROR, ex.getMessage(), ex); } return null; } + public void missileLaunch() { + missileLaunch.playInstance(); + } + /** * Plays the splash sound effect. */ @@ -126,6 +131,7 @@ public void shipDestroyed() { /** * Checks the according case for the soundeffect + * * @param event the received event */ @Override @@ -134,6 +140,7 @@ public void receivedEvent(SoundEvent event) { case EXPLOSION -> explosion(); case SPLASH -> splash(); case DESTROYED_SHIP -> shipDestroyed(); + case MISSILE_LAUNCH -> missileLaunch(); } } } 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 index 1f7be2f6..37591791 100644 --- 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 @@ -1,6 +1,5 @@ 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; @@ -105,29 +104,16 @@ private ParticleEmitter createParticleEmitter(String name, int count, ColorRGBA private static class EffectCleanupControl extends AbstractControl { private final ParticleEmitter emitter; -// private final Node attachedNode; private float currentTime = 0; private final float duration = 3.5f; - /** - * 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); this.emitter = emitter; } @@ -140,10 +126,8 @@ public EffectCleanupControl(ParticleEmitter emitter) { protected void controlUpdate(float tpf) { currentTime += tpf; if (currentTime <= duration) { - } if (currentTime >= 1f && currentTime <= 1.1) { - //Start emitter.setNumParticles(50); emitter.setParticlesPerSec(50); diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/MapViewSynchronizer.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/MapViewSynchronizer.java index 2e2c840d..7e7f0c48 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/MapViewSynchronizer.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/MapViewSynchronizer.java @@ -12,9 +12,13 @@ import com.jme3.scene.Node; import com.jme3.scene.Spatial; import pp.battleship.model.Battleship; +import pp.battleship.model.IntPoint; import pp.battleship.model.Shell; import pp.battleship.model.Shot; +import pp.util.FloatMath; +import pp.util.FloatPoint; import pp.util.Position; +import java.lang.System.Logger; /** * Synchronizes the visual representation of the ship map with the game model. @@ -28,6 +32,8 @@ class MapViewSynchronizer extends ShipMapSynchronizer { private static final float SHIP_DEPTH = 0f; private static final float INDENT = 4f; + private static final float SHELL_DEPTH = 8f; + // Colors used for different visual elements private static final ColorRGBA HIT_COLOR = ColorRGBA.Red; private static final ColorRGBA MISS_COLOR = ColorRGBA.Blue; @@ -110,9 +116,22 @@ public Spatial visit(Battleship ship) { return shipNode; } + /** + * + * @param shell + * @return + */ @Override public Spatial visit(Shell shell) { - return null; + LOGGER.log(Logger.Level.DEBUG, "Visiting {0}", shell); + final Node shellNode = new Node("shell"); + final Position startPosition = view.modelToView(shell.getCurrentPosition().x,shell.getCurrentPosition().z); + + shellNode.attachChild(view.getApp().getDraw().makeRectangle(startPosition.getX(),startPosition.getY(), SHELL_DEPTH, 30, 30, ColorRGBA.Black)); + + shellNode.addControl(new ShellControl(shell, this.view, shell.getLogic())); + return shellNode; + } /** 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 ea72e7b5..f93d0ea6 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 @@ -109,7 +109,7 @@ private Geometry createCylinder(Shot shot) { geometry.setMaterial(createColoredMaterial(color)); geometry.rotate(HALF_PI, 0f, 0f); - // compute the center of the shot in world coordinates + geometry.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f); return geometry; @@ -140,14 +140,14 @@ public Spatial visit(Shell shell) { final Spatial bombModel = app.getAssetManager().loadModel(BOMB); // Apply transformations to the bomb model (scale, rotate, etc. if needed) - bombModel.scale(0.05f); // Adjust the scale based on your model size - bombModel.rotate(-HALF_PI, 0f, 0f); // Rotate the model if necessary + bombModel.scale(0.05f); + bombModel.rotate(-HALF_PI, 0f, 0f); // Set the position of the bomb at the shell's current position bombModel.setLocalTranslation(shell.getCurrentPosition()); // Add a control to animate the bomb's movement (similar to the previous cylinder animation) - bombModel.addControl(new ShellControl(shell)); + bombModel.addControl(new ShellControl(shell,shell.getLogic())); return bombModel; } diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShellControl.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShellControl.java index df196946..f448f1ad 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShellControl.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShellControl.java @@ -12,8 +12,13 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.control.AbstractControl; +import pp.battleship.game.client.ClientGameLogic; +import pp.battleship.message.server.EffectMessage; import pp.battleship.model.Battleship; import pp.battleship.model.Shell; +import pp.battleship.notification.Sound; +import pp.util.FloatPoint; +import pp.util.Position; import java.lang.System.Logger; import java.lang.System.Logger.Level; @@ -28,21 +33,6 @@ * The ship oscillates to simulate a realistic movement on water, based on its orientation and length. */ class ShellControl extends AbstractControl { - /** - * The axis of rotation for the ship's pitch (tilting forward and backward). - */ - private final Vector3f axis; - - /** - * The duration of one complete oscillation cycle in seconds. - */ - private final float cycle; - - /** - * The amplitude of the pitch oscillation in radians, determining how much the ship tilts. - */ - private final float amplitude; - /** * A quaternion representing the ship's current pitch rotation. */ @@ -54,6 +44,10 @@ class ShellControl extends AbstractControl { private float time; private Shell shell; + private MapView view; + private ClientGameLogic logic; + private EffectMessage msg; + private boolean hasPlayedSound = false; /** * Logger for logging messages related to ShipControl operations. */ @@ -66,14 +60,22 @@ class ShellControl extends AbstractControl { * * @param shell the Battleship object to control */ - public ShellControl(Shell shell) { + public ShellControl(Shell shell, ClientGameLogic clientGameLogic) { this.shell = shell; - // Determine the axis of rotation based on the ship's orientation - axis = Vector3f.UNIT_X; + this.logic = clientGameLogic; + this.msg = shell.getMsg(); + } - // Set the cycle duration and amplitude based on the ship's length - cycle = 1 * 2f; - amplitude = 5f * DEG_TO_RAD / 1; + /** + * @param shell + * @param view + * @param clientGameLogic + */ + public ShellControl(Shell shell, MapView view, ClientGameLogic clientGameLogic) { + this.shell = shell; + this.view = view; + this.logic = clientGameLogic; + this.msg = shell.getMsg(); } /** @@ -85,11 +87,25 @@ public ShellControl(Shell shell) { @Override protected void controlUpdate(float tpf) { // If spatial is null, do nothing - if (spatial == null) return; - - shell.updatePosition(tpf); - spatial.setLocalTranslation(shell.getCurrentPosition()); - + if (spatial == null) + return; + if (shell.isFinished() && !hasPlayedSound) { + if (!msg.getShot().isHit()) + logic.playSound(Sound.SPLASH); + else if (msg.getDestroyedShip() == null) + logic.playSound(Sound.EXPLOSION); + else + logic.playSound(Sound.DESTROYED_SHIP); + hasPlayedSound = true; + } + if (view == null) { + shell.updatePosition(tpf); + spatial.setLocalTranslation(shell.getCurrentPosition()); + } else { + shell.updatePosition(tpf); + Position pos2d = view.modelToView(shell.getCurrentPosition().x - 0.3f, shell.getCurrentPosition().z - 0.3f); + spatial.setLocalTranslation(pos2d.getY(), pos2d.getX(), 0); + } } /** diff --git a/Projekte/battleship/client/src/main/resources/Sound/Effects/missileLaunch.wav b/Projekte/battleship/client/src/main/resources/Sound/Effects/missileLaunch.wav new file mode 100644 index 00000000..092b0bf7 Binary files /dev/null and b/Projekte/battleship/client/src/main/resources/Sound/Effects/missileLaunch.wav differ diff --git a/Projekte/battleship/model/src/main/java/pp/battleship/game/client/BattleState.java b/Projekte/battleship/model/src/main/java/pp/battleship/game/client/BattleState.java index 19915410..675ba3f2 100644 --- a/Projekte/battleship/model/src/main/java/pp/battleship/game/client/BattleState.java +++ b/Projekte/battleship/model/src/main/java/pp/battleship/game/client/BattleState.java @@ -55,11 +55,11 @@ else if (logic.getOpponentMap().isValid(pos)) @Override public void receivedEffect(EffectMessage msg) { ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS - playSound(msg); myTurn = msg.isMyTurn(); + logic.playSound(Sound.MISSILE_LAUNCH); logic.setInfoText(msg.getInfoTextKey()); affectedMap(msg).add(msg.getShot()); - affectedMap(msg).add(new Shell(new Vector3f(0, 10, 0), new Vector3f(msg.getShot().getY() + 0.5f, -0.4f, msg.getShot().getX() + 0.5f), 1f)); + affectedMap(msg).add(new Shell(new Vector3f(0, 50, 0), new Vector3f(msg.getShot().getY() + 0.5f, -0.4f, msg.getShot().getX() + 0.5f), 1f,msg,logic)); if (destroyedOpponentShip(msg)) logic.getOpponentMap().add(msg.getDestroyedShip()); if (msg.isGameOver()) { diff --git a/Projekte/battleship/model/src/main/java/pp/battleship/model/Shell.java b/Projekte/battleship/model/src/main/java/pp/battleship/model/Shell.java index 7ea8e708..48762264 100644 --- a/Projekte/battleship/model/src/main/java/pp/battleship/model/Shell.java +++ b/Projekte/battleship/model/src/main/java/pp/battleship/model/Shell.java @@ -1,6 +1,8 @@ package pp.battleship.model; import com.jme3.math.Vector3f; +import pp.battleship.game.client.ClientGameLogic; +import pp.battleship.message.server.EffectMessage; import pp.util.FloatMath; public class Shell implements Item { @@ -8,26 +10,38 @@ public class Shell implements Item { private Vector3f targetPosition; // Zielposition des Geschosses0 private Vector3f currentPosition; // Aktuelle Position des Geschosses private float speed; // Geschwindigkeit des Geschosses + private EffectMessage msg; + private ClientGameLogic logic; - private float progress; + private float progress; - public Shell(Vector3f startPosition, Vector3f targetPosition, float speed) { + /** + * @param startPosition + * @param targetPosition + * @param speed + * @param msg + * @param logic + */ + public Shell(Vector3f startPosition, Vector3f targetPosition, float speed, EffectMessage msg, ClientGameLogic logic) { this.startPosition = startPosition; this.targetPosition = targetPosition; this.currentPosition = new Vector3f(startPosition); // Initiale Position ist die Startposition this.speed = speed; + this.msg = msg; + this.logic = logic; } // Methode, um die Position des Geschosses basierend auf der Zeit zu aktualisieren public void updatePosition(float deltaTime) { - progress+=deltaTime*speed; + progress += deltaTime * speed; + progress = FloatMath.clamp(progress, 0.0f, 1.0f); + + float t = FloatMath.easeInOutElastic(progress); // Interpoliere die Position zwischen Start- und Zielposition basierend auf dem Fortschritt - currentPosition.x = FloatMath.interpolateLinear(progress,startPosition.x,targetPosition.x); - currentPosition.y = FloatMath.interpolateLinear(progress,startPosition.y,targetPosition.y); - currentPosition.z = FloatMath.interpolateLinear(progress,startPosition.z,targetPosition.z); - - //currentPosition.interpolateLocal(startPosition, targetPosition, progress); + currentPosition.y = FloatMath.extrapolateLinear(t, startPosition.y, targetPosition.y); + currentPosition.x = FloatMath.extrapolateLinear(t, startPosition.x, targetPosition.x); + currentPosition.z = FloatMath.extrapolateLinear(t, startPosition.z, targetPosition.z); } // Aktuelle Position des Geschosses @@ -40,22 +54,44 @@ public boolean isAtTarget() { return currentPosition.distance(targetPosition) < 0.0001f; // Toleranz für die Zielgenauigkeit } - + /** + * @param visitor the visitor performing operations on the item + * @param + * @return + */ @Override public T accept(Visitor visitor) { return visitor.visit(this); } /** - * Accepts a visitor that does not return a value. This method is part of the - * Visitor design pattern. - * * @param visitor the visitor to accept */ @Override public void accept(VoidVisitor visitor) { visitor.visit(this); } + + /** + * @return + */ + public boolean isFinished() { + return progress >= 1; + } + + /** + * @return + */ + public EffectMessage getMsg() { + return msg; + } + + /** + * @return + */ + public ClientGameLogic getLogic() { + return logic; + } } diff --git a/Projekte/battleship/model/src/main/java/pp/battleship/notification/Sound.java b/Projekte/battleship/model/src/main/java/pp/battleship/notification/Sound.java index d82ab698..c963674a 100644 --- a/Projekte/battleship/model/src/main/java/pp/battleship/notification/Sound.java +++ b/Projekte/battleship/model/src/main/java/pp/battleship/notification/Sound.java @@ -22,5 +22,9 @@ public enum Sound { /** * Sound of a ship being destroyed. */ - DESTROYED_SHIP + DESTROYED_SHIP, + /** + * Sound of a fired Missile + */ + MISSILE_LAUNCH } diff --git a/Projekte/common/src/main/java/pp/util/FloatMath.java b/Projekte/common/src/main/java/pp/util/FloatMath.java index 3e00a284..afc32871 100644 --- a/Projekte/common/src/main/java/pp/util/FloatMath.java +++ b/Projekte/common/src/main/java/pp/util/FloatMath.java @@ -96,6 +96,51 @@ public static float extrapolateLinear(float scale, float startValue, float endVa return ((1f - scale) * startValue) + (scale * endValue); } + /** + * Interpolates a value to range [0,1] after given easing function + * https://easings.net/#easeInOutElastic + * @param value the value to interpolate + * @return A new value in range [0,1] + */ + public static float easeInOutElastic(float value) { + var c5 = (2 * Math.PI) / 4.5; + + return value == 0 + ? 0 + : (float) (value == 1 + ? 1 + : value < 0.5 + ? -(Math.pow(2, 20 * value - 10) * Math.sin((20 * value - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * value + 10) * Math.sin((20 * value - 11.125) * c5)) / 2 + 1); + + } + + /** + * Fancy effect for 2D Map animation + * @param x Progress state of animation + * @return new Value for animation + */ + public static float easeOutBounce(float x) { + float n1 = 7.5625f; + float d1 = 2.75f; + + if (x < 1 / d1) { + return n1 * x * x; + } else if (x < 2 / d1) { + return (float) (n1 * (x -= 1.5 / d1) * x + 0.75); + } else if (x < 2.5 / d1) { + return (float) (n1 * (x -= 2.25 / d1) * x + 0.9375); + } else { + return (float) (n1 * (x -= 2.625 / d1) * x + 0.984375); + } + } + + public static float easeInBounce(float x) { + return 1 - easeOutBounce(1 - x); + } + + + /** * Returns the arc cosine of a value.
* Special cases: diff --git a/Projekte/jme-common/src/main/java/pp/view/ModelViewSynchronizer.java b/Projekte/jme-common/src/main/java/pp/view/ModelViewSynchronizer.java index 02538ce8..32e4c938 100644 --- a/Projekte/jme-common/src/main/java/pp/view/ModelViewSynchronizer.java +++ b/Projekte/jme-common/src/main/java/pp/view/ModelViewSynchronizer.java @@ -19,7 +19,7 @@ * Abstract base class for keeping the scene graph (=view) in sync with the model. */ public abstract class ModelViewSynchronizer { - private static final Logger LOGGER = System.getLogger(ModelViewSynchronizer.class.getName()); + protected static final Logger LOGGER = System.getLogger(ModelViewSynchronizer.class.getName()); private final Node itemNode = new Node("items"); //NON-NLS private final Map itemMap = new HashMap<>();