Sounds für das Kampfsystem hinzugefügt

This commit is contained in:
2026-09-23 19:47:05 +02:00
parent a28197d9d0
commit ed0d622bfd
26 changed files with 161 additions and 9 deletions

View File

@@ -0,0 +1,94 @@
package de.blight.game.audio;
import com.jme3.asset.AssetManager;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode;
import com.jme3.scene.Node;
import de.blight.game.state.AudioSettingsState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
public class WeaponSoundSystem {
private static final Logger log = LoggerFactory.getLogger(WeaponSoundSystem.class);
private static final float BASE_VOLUME = 0.85f;
private static final float PITCH_CENTER = 1.00f;
private static final float PITCH_RANGE = 0.08f;
private final Map<String, List<AudioNode>> pools = new HashMap<>();
private final AudioSettingsState audioSettings;
private final Random random = new Random();
public WeaponSoundSystem(AssetManager am, Node parentNode,
AudioSettingsState audioSettings,
Path assetRoot, String baseDir) {
this.audioSettings = audioSettings;
Path absBase = assetRoot.resolve(baseDir);
if (!Files.isDirectory(absBase)) {
log.warn("[WeaponSound] Basis-Verzeichnis nicht gefunden: {}", absBase);
return;
}
try (var stream = Files.list(absBase)) {
stream.filter(Files::isDirectory).sorted().forEach(dir -> {
String category = dir.getFileName().toString();
List<AudioNode> nodes = scanDir(am, parentNode, assetRoot,
baseDir + category + "/");
if (!nodes.isEmpty()) {
pools.put(category, nodes);
log.info("[WeaponSound] '{}': {} Sound(s) geladen", category, nodes.size());
}
});
} catch (IOException e) {
log.warn("[WeaponSound] Scan-Fehler {}: {}", absBase, e.getMessage());
}
}
public void play(String category) {
List<AudioNode> nodes = pools.get(category);
if (nodes == null || nodes.isEmpty()) return;
float vol = BASE_VOLUME * (audioSettings != null ? audioSettings.effectiveEffects() : 1f);
float pitch = PITCH_CENTER + (random.nextFloat() - 0.5f) * PITCH_RANGE;
AudioNode node = nodes.get(random.nextInt(nodes.size()));
node.setVolume(vol);
node.setPitch(pitch);
node.playInstance();
}
private List<AudioNode> scanDir(AssetManager am, Node parentNode,
Path assetRoot, String relDir) {
Path absDir = assetRoot.resolve(relDir);
if (!Files.isDirectory(absDir)) return Collections.emptyList();
List<Path> files;
try (var stream = Files.list(absDir)) {
files = stream
.filter(p -> Files.isRegularFile(p)
&& p.getFileName().toString().toLowerCase().endsWith(".ogg"))
.sorted()
.collect(Collectors.toList());
} catch (IOException e) {
log.warn("[WeaponSound] Scan-Fehler {}: {}", absDir, e.getMessage());
return Collections.emptyList();
}
List<AudioNode> nodes = new ArrayList<>(files.size());
for (Path f : files) {
String assetPath = relDir + f.getFileName().toString();
try {
AudioNode node = new AudioNode(am, assetPath, AudioData.DataType.Buffer);
node.setPositional(false);
node.setLooping(false);
parentNode.attachChild(node);
nodes.add(node);
} catch (Exception e) {
log.warn("[WeaponSound] Nicht ladbar '{}': {}", assetPath, e.getMessage());
}
}
return nodes;
}
}

View File

@@ -140,6 +140,15 @@ public class PlayerInputControl {
private int currentAttackId = -1;
private boolean hitWindowActive = false;
private float hitWindowCritMult = 1.0f;
// Angriffs-Sound via Waffen-Geschwindigkeit
private static final float ATTACK_SOUND_VELOCITY = 3.5f;
// Attacken mit Backswing → Sound erst nach Mindest-Fortschritt erlauben
private static final float ATTACK2_SOUND_MIN_PROGRESS = 0.35f;
private static final float ATTACK5_SOUND_MIN_PROGRESS = 0.35f;
private boolean attackSoundFired = false;
private int currentComboStep = 0;
private final Vector3f lastGripPos = new Vector3f();
private final Vector3f scratchGripVel = new Vector3f();
// Draw/Sheath Animation System
private boolean drawAnimActive = false;
private float drawAnimTimer = 0f;
@@ -184,6 +193,7 @@ public class PlayerInputControl {
public static final float WATER_WADING_DEPTH = 0.75f;
private de.blight.game.audio.FootstepSystem footstepSystem;
private de.blight.game.audio.WeaponSoundSystem weaponSoundSystem;
private java.util.function.BiFunction<Float, Float, float[]> surfaceQuery;
private java.util.function.BiFunction<Float, Float, Float> waterDepthQuery;
private com.jme3.audio.AudioNode wadingNode;
@@ -866,7 +876,27 @@ public class PlayerInputControl {
comboWindowActive = true;
log.info("[Combat] Kombo-Fenster geöffnet (step={}, remaining={}s)", comboStep, combatAnimRemaining);
}
// Per-Frame Waffen-Treffercheck während Hit-Window
// Per-Frame Waffen-Treffercheck + Geschwindigkeits-Sound
if (isMeleeType(drawnWeaponType) && weaponAttach != null) {
com.jme3.scene.Node hn = weaponAttach.getHandNode();
if (hn != null && !attackSoundFired && weaponSoundSystem != null) {
Vector3f grip = hn.getWorldTranslation();
scratchGripVel.set(grip).subtractLocal(lastGripPos).divideLocal(tpf);
float speed = scratchGripVel.length();
float progress = combatAnimDuration > 0f
? (combatAnimDuration - combatAnimRemaining) / combatAnimDuration : 0f;
float minProg = switch (currentComboStep) {
case 1 -> ATTACK2_SOUND_MIN_PROGRESS;
case 4 -> ATTACK5_SOUND_MIN_PROGRESS;
default -> 0f;
};
if (progress >= minProg && speed >= ATTACK_SOUND_VELOCITY) {
attackSoundFired = true;
weaponSoundSystem.play("attack");
}
lastGripPos.set(grip);
}
}
if (hitWindowActive && isMeleeType(drawnWeaponType) && combatAnimDuration > 0f) {
float t = (combatAnimDuration - combatAnimRemaining) / combatAnimDuration;
if (t >= HIT_WINDOW_START && t <= HIT_WINDOW_END) {
@@ -961,7 +991,10 @@ public class PlayerInputControl {
if (!weaponDrawn) {
boolean hasShield = mainCharacter.getEquippedShield() != null;
startDrawAnimation(hasShield,
() -> { weaponDrawn = true; drawnWeaponType = sub; currentAnim = null; },
() -> {
weaponDrawn = true; drawnWeaponType = sub; currentAnim = null;
if (weaponSoundSystem != null) weaponSoundSystem.play("unsheath");
},
null);
} else if (isMeleeType(drawnWeaponType)) {
sheathCurrentWeapon();
@@ -992,6 +1025,7 @@ public class PlayerInputControl {
attackHoldPending = false;
heavyLungeVel = null;
blockPhase = BlockPhase.NONE;
if (weaponSoundSystem != null) weaponSoundSystem.play("sheath");
boolean hasShield = mainCharacter != null && mainCharacter.getEquippedShield() != null;
startDrawAnimation(hasShield,
() -> { weaponDrawn = false; drawnWeaponType = null; currentAnim = null; },
@@ -1012,8 +1046,12 @@ public class PlayerInputControl {
currentAttackId = ++attackIdCounter;
hitWindowActive = true;
hitWindowCritMult = lastAttackCritical ? 2.0f : 1.0f;
currentComboStep = step;
comboStep = (step + 1) % 5;
comboWindowActive = false;
attackSoundFired = false;
if (weaponAttach != null && weaponAttach.getHandNode() != null)
lastGripPos.set(weaponAttach.getHandNode().getWorldTranslation());
float dur = resolveClipLength(atk, 0.6f);
// Kombo-Folgeschlag: kurzer Blend für sauberen Übergang; erster Schlag: Hard-Cut
nextTransitionLength = isComboFollow ? COMBAT_BLEND : -1.0;
@@ -1388,7 +1426,11 @@ public class PlayerInputControl {
if (!weaponDrawn || drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
return;
}
currentAttackId = ++attackIdCounter;
currentAttackId = ++attackIdCounter;
currentComboStep = -1;
attackSoundFired = false;
if (weaponAttach != null && weaponAttach.getHandNode() != null)
lastGripPos.set(weaponAttach.getHandNode().getWorldTranslation());
float mult = computeHeavyMultiplier();
fireWeaponCallback(mult);
log.info("[Combat] Schwerer Angriff ausgelöst | Haltezeit={}s Multiplikator={}x",
@@ -1437,6 +1479,10 @@ public class PlayerInputControl {
this.surfaceQuery = query;
}
public void setWeaponSoundSystem(de.blight.game.audio.WeaponSoundSystem wss) {
this.weaponSoundSystem = wss;
}
public void setWaterDepthQuery(java.util.function.BiFunction<Float, Float, Float> query) {
this.waterDepthQuery = query;
}

View File

@@ -108,6 +108,7 @@ public class WorldScene extends BaseAppState {
private de.blight.game.state.WaterfallState waterfallState;
private de.blight.game.state.PlacedSoundSystem placedSoundSystem;
private de.blight.game.audio.FootstepSystem footstepSystem;
private de.blight.game.audio.WeaponSoundSystem weaponSoundSystem;
private de.blight.game.state.DrownState drownState;
private WaterInteractionState waterInteractionState;
@@ -319,6 +320,9 @@ public class WorldScene extends BaseAppState {
footstepSystem = new de.blight.game.audio.FootstepSystem(assetManager, rootNode, audioSettings,
AnimationLibrary.findAssetRoot());
playerInput.setFootstepSystem(footstepSystem, this::querySurfaceWeights);
weaponSoundSystem = new de.blight.game.audio.WeaponSoundSystem(assetManager, rootNode,
audioSettings, AnimationLibrary.findAssetRoot(), "audio/weapons/sword/");
playerInput.setWeaponSoundSystem(weaponSoundSystem);
playerInput.setWaterDepthQuery(this::queryWaterDepth);
try {
com.jme3.audio.AudioNode wadingNode = new com.jme3.audio.AudioNode(
@@ -376,8 +380,10 @@ public class WorldScene extends BaseAppState {
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
app.getStateManager().attach(
new WorldInteractableState(keyBindings, physicsChar, playerInput));
app.getStateManager().attach(
new de.blight.game.state.WorldDummyState(physicsChar, playerInput, keyBindings));
de.blight.game.state.WorldDummyState dummyStateRef =
new de.blight.game.state.WorldDummyState(physicsChar, playerInput, keyBindings);
dummyStateRef.setWeaponSoundSystem(weaponSoundSystem);
app.getStateManager().attach(dummyStateRef);
de.blight.common.MonologueRegistry.init(de.blight.common.MonologueIO.load());
de.blight.game.state.DialogHudState dialogHud = new de.blight.game.state.DialogHudState();
dialogHud.setMainCharacter(mc);

View File

@@ -91,6 +91,7 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
private Node guiNode;
private Camera cam;
private BulletAppState bulletAppState;
private de.blight.game.audio.WeaponSoundSystem weaponSoundSystem;
// ── Laufzeit-Zustand ─────────────────────────────────────────────────────
private final List<DummyRuntime> dummies = new ArrayList<>();
@@ -412,17 +413,22 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
float dz = dr.worldZ - gripPos.z;
if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) {
hitThisAttack.add(dr.id);
applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier));
applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier), critMultiplier > 1.0f);
}
}
}
// ── Schaden / Zustand ─────────────────────────────────────────────────────
private void applyDamage(DummyRuntime dr, int damage) {
public void setWeaponSoundSystem(de.blight.game.audio.WeaponSoundSystem wss) {
this.weaponSoundSystem = wss;
}
private void applyDamage(DummyRuntime dr, int damage, boolean isCrit) {
dr.currentHp = Math.max(0, dr.currentHp - damage);
dr.restoreTimer = 0f;
refreshWorldHpBar(dr);
if (weaponSoundSystem != null) weaponSoundSystem.play(isCrit ? "crit" : "impact");
log.info("[Dummy {}] -{} HP → {}/{}{}", dr.id, damage, dr.currentHp, dr.maxHp,
dr.currentHp <= 0 ? " K.O.!" : "");
if (dr.currentHp <= 0) {