Compare commits
3 Commits
b7def4f2e7
...
f88a710b4d
| Author | SHA1 | Date | |
|---|---|---|---|
| f88a710b4d | |||
| 3f5f2b9253 | |||
| 17efa86f34 |
Binary file not shown.
BIN
assets/imported/animations/Crouched Walking.fbx
Normal file
BIN
assets/imported/animations/Crouched Walking.fbx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -59,7 +59,7 @@
|
|||||||
"SWORD_IDLE_TO_BLOCK": "Sword And Shield Block",
|
"SWORD_IDLE_TO_BLOCK": "Sword And Shield Block",
|
||||||
"SWORD_BLOCK_IMPACT": "Sword And Shield Block Impact",
|
"SWORD_BLOCK_IMPACT": "Sword And Shield Block Impact",
|
||||||
"SWORD_IDLE_IMPACT": "Sword And Shield Impact",
|
"SWORD_IDLE_IMPACT": "Sword And Shield Impact",
|
||||||
"SWORD_COUNTERATTACK": "Sword And Shield Attack Counterattack",
|
"SWORD_PARADE": "Sword And Shield Attack Counterattack",
|
||||||
"SWORD_BLOCK_MOVE": "Sword And Shield Block Walk",
|
"SWORD_BLOCK_MOVE": "Sword And Shield Block Walk",
|
||||||
"SWORD_BLOCK_MOVE_BACKWARD": "Sword And Shield Block Walk Backwards",
|
"SWORD_BLOCK_MOVE_BACKWARD": "Sword And Shield Block Walk Backwards",
|
||||||
"SWORD_BLOCK_STRAFE_LEFT": "Sword And Shield Strafe Left",
|
"SWORD_BLOCK_STRAFE_LEFT": "Sword And Shield Strafe Left",
|
||||||
@@ -81,7 +81,8 @@
|
|||||||
"SWORD_DEATH": "Sword And Shield Death",
|
"SWORD_DEATH": "Sword And Shield Death",
|
||||||
"SWORD_IDLE_BORED": "Sword And Shield Idle boring",
|
"SWORD_IDLE_BORED": "Sword And Shield Idle boring",
|
||||||
"SWORD_IDLE_BORED_2": "Sword And Shield Idle boring 2",
|
"SWORD_IDLE_BORED_2": "Sword And Shield Idle boring 2",
|
||||||
"SWORD_IDLE_BORED_3": "Sword And Shield Idle boring 3"
|
"SWORD_IDLE_BORED_3": "Sword And Shield Idle boring 3",
|
||||||
|
"STAND_TO_CROUCH": "Standing To Crouched"
|
||||||
},
|
},
|
||||||
"previewModelPath": "Characters/Models/hero.j3o",
|
"previewModelPath": "Characters/Models/hero.j3o",
|
||||||
"animOffsets": {
|
"animOffsets": {
|
||||||
|
|||||||
@@ -34,16 +34,25 @@ public class SaveGame {
|
|||||||
public static class WorldSave {
|
public static class WorldSave {
|
||||||
/** UUIDs der PlacedItems, die der Spieler aufgesammelt hat. */
|
/** UUIDs der PlacedItems, die der Spieler aufgesammelt hat. */
|
||||||
public Set<String> pickedUpItems = new HashSet<>();
|
public Set<String> pickedUpItems = new HashSet<>();
|
||||||
/** IDs besiegter Gegner (für zukünftige Implementierung). */
|
/** IDs besiegter Gegner. */
|
||||||
public Set<String> defeatedEnemies = new HashSet<>();
|
public Set<String> defeatedEnemies = new HashSet<>();
|
||||||
/** NPC-ID → aktuell verfügbare Dialog-Option-IDs. */
|
/** NPC-ID → aktuell verfügbare Dialog-Option-IDs. */
|
||||||
public Map<String, List<String>> npcDialogState = new HashMap<>();
|
public Map<String, List<String>> npcDialogState = new HashMap<>();
|
||||||
|
/** NPC-ID → Leichen-Zustand (Position, verbleibende Items). */
|
||||||
|
public Map<String, DeadNpcSave> deadNpcs = new LinkedHashMap<>();
|
||||||
/** Bodennässe 0..1 — wird von RainState persistiert. */
|
/** Bodennässe 0..1 — wird von RainState persistiert. */
|
||||||
public float wetness = 0f;
|
public float wetness = 0f;
|
||||||
/** Wetter-Zustand — wird von WeatherState persistiert. */
|
/** Wetter-Zustand — wird von WeatherState persistiert. */
|
||||||
public WeatherSave weather = new WeatherSave();
|
public WeatherSave weather = new WeatherSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class DeadNpcSave {
|
||||||
|
public float x, y, z, rotY;
|
||||||
|
/** Verbleibende Item-IDs; leer = vollständig gelooted. */
|
||||||
|
public List<String> remainingItemIds = new ArrayList<>();
|
||||||
|
public boolean fullyLooted = false;
|
||||||
|
}
|
||||||
|
|
||||||
public static class WeatherSave {
|
public static class WeatherSave {
|
||||||
public String weather = "SUNNY";
|
public String weather = "SUNNY";
|
||||||
public float changeTimer = 180f;
|
public float changeTimer = 180f;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface Lootable {
|
||||||
|
String getLootId();
|
||||||
|
String getLootTitleKey();
|
||||||
|
List<Item> getLootItems();
|
||||||
|
}
|
||||||
@@ -264,12 +264,16 @@ public class GameController {
|
|||||||
}
|
}
|
||||||
String sessionMapPath = sessionDir.resolve("blight_map.blm").toString();
|
String sessionMapPath = sessionDir.resolve("blight_map.blm").toString();
|
||||||
|
|
||||||
|
String gameLogback = ProjectRoot.PATH
|
||||||
|
.resolve("blight-game/src/main/resources/logback.xml")
|
||||||
|
.toString();
|
||||||
java.util.List<String> cmd = new java.util.ArrayList<>(List.of(
|
java.util.List<String> cmd = new java.util.ArrayList<>(List.of(
|
||||||
javaExe,
|
javaExe,
|
||||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||||
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
|
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
|
||||||
"-Xmx2g",
|
"-Xmx2g",
|
||||||
"-Djava.library.path=" + libPath,
|
"-Djava.library.path=" + libPath,
|
||||||
|
"-Dlogback.configurationFile=" + gameLogback,
|
||||||
"-Dblight.project.root=" + projRoot,
|
"-Dblight.project.root=" + projRoot,
|
||||||
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
|
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
|
||||||
"-Dblight.autostart=" + isAutostart));
|
"-Dblight.autostart=" + isAutostart));
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- Kampfsystem: NPC-Aktionen, Schaden, Block/Konterparade-Entscheidungen -->
|
||||||
|
<logger name="de.blight.game.state.WorldNpcsState" level="DEBUG"/>
|
||||||
|
|
||||||
<!-- Karten-Rendering: Modell-Build und PNG-Render auf DEBUG -->
|
<!-- Karten-Rendering: Modell-Build und PNG-Render auf DEBUG -->
|
||||||
<logger name="de.blight.editor.ui.WorldMapView" level="DEBUG"/>
|
<logger name="de.blight.editor.ui.WorldMapView" level="DEBUG"/>
|
||||||
<!-- Weltkarte-Vollbild: Layout-Debugging -->
|
<!-- Weltkarte-Vollbild: Layout-Debugging -->
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ tasks.register('extractNatives', Copy) {
|
|||||||
tasks.register('run', JavaExec) {
|
tasks.register('run', JavaExec) {
|
||||||
group = 'application'
|
group = 'application'
|
||||||
description = 'Startet das Spiel'
|
description = 'Startet das Spiel'
|
||||||
|
dependsOn 'classes' // stellt sicher, dass Java vor dem Start kompiliert wird
|
||||||
mainClass = mainClassName
|
mainClass = mainClassName
|
||||||
classpath = sourceSets.main.runtimeClasspath
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
javaLauncher = javaToolchains.launcherFor {
|
javaLauncher = javaToolchains.launcherFor {
|
||||||
|
|||||||
@@ -709,6 +709,66 @@ public class BlightGame extends SimpleApplication {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.registerCommand("list", args -> {
|
||||||
|
if (args.length < 2)
|
||||||
|
return "Syntax: list item | list npc nearby | list npc dead";
|
||||||
|
return switch (args[1].toLowerCase()) {
|
||||||
|
case "item" -> {
|
||||||
|
de.blight.game.state.WorldNpcsState wns =
|
||||||
|
stateManager.getState(de.blight.game.state.WorldNpcsState.class);
|
||||||
|
if (wns == null) yield "Welt nicht geladen";
|
||||||
|
java.util.List<String> items = wns.listAllItems();
|
||||||
|
yield items.isEmpty() ? "Keine Items gefunden."
|
||||||
|
: items.size() + " Items:\n" + String.join("\n", items);
|
||||||
|
}
|
||||||
|
case "npc" -> {
|
||||||
|
if (args.length < 3)
|
||||||
|
yield "Syntax: list npc nearby | list npc dead";
|
||||||
|
yield switch (args[2].toLowerCase()) {
|
||||||
|
case "nearby" -> {
|
||||||
|
de.blight.game.state.WorldNpcsState wns =
|
||||||
|
stateManager.getState(de.blight.game.state.WorldNpcsState.class);
|
||||||
|
if (wns == null) yield "Welt nicht geladen";
|
||||||
|
java.util.List<String> npcs = wns.listNearbyNpcs();
|
||||||
|
yield npcs.isEmpty() ? "Keine NPCs in der Nähe."
|
||||||
|
: npcs.size() + " NPCs:\n" + String.join("\n", npcs);
|
||||||
|
}
|
||||||
|
case "dead" -> {
|
||||||
|
de.blight.game.state.SaveGameState sgs =
|
||||||
|
stateManager.getState(de.blight.game.state.SaveGameState.class);
|
||||||
|
if (sgs == null) yield "Save-System nicht aktiv";
|
||||||
|
java.util.Map<String, de.blight.common.SaveGame.DeadNpcSave> dead =
|
||||||
|
sgs.getDeadNpcs();
|
||||||
|
if (dead.isEmpty()) yield "Keine toten NPCs gespeichert.";
|
||||||
|
StringBuilder sb = new StringBuilder(dead.size() + " tote NPCs:\n");
|
||||||
|
dead.forEach((id, dns) -> sb
|
||||||
|
.append(String.format("%-30s looted=%b items=%d%n",
|
||||||
|
id, dns.fullyLooted, dns.remainingItemIds.size())));
|
||||||
|
yield sb.toString().trim();
|
||||||
|
}
|
||||||
|
default -> "Unbekannter Subbefehl. Syntax: list npc nearby | list npc dead";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default -> "Unbekannter Typ. Syntax: list item | list npc nearby | list npc dead";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.registerCommand("kill", args -> {
|
||||||
|
if (args.length < 2) return "Syntax: kill <npc_id>";
|
||||||
|
de.blight.game.state.WorldNpcsState wns =
|
||||||
|
stateManager.getState(de.blight.game.state.WorldNpcsState.class);
|
||||||
|
if (wns == null) return "Welt nicht geladen";
|
||||||
|
return wns.killNpcById(args[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.registerCommand("revive", args -> {
|
||||||
|
if (args.length < 2) return "Syntax: revive <npc_id>";
|
||||||
|
de.blight.game.state.WorldNpcsState wns =
|
||||||
|
stateManager.getState(de.blight.game.state.WorldNpcsState.class);
|
||||||
|
if (wns == null) return "Welt nicht geladen";
|
||||||
|
return wns.reviveNpcById(args[1]);
|
||||||
|
});
|
||||||
|
|
||||||
console.registerCommand("set", args -> {
|
console.registerCommand("set", args -> {
|
||||||
if (args.length < 3)
|
if (args.length < 3)
|
||||||
return "Syntax: set <schlüssel> <wert>"
|
return "Syntax: set <schlüssel> <wert>"
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ public class AnimPlayer {
|
|||||||
}
|
}
|
||||||
if (skinning != null && !skinning.isEnabled()) skinning.setEnabled(true);
|
if (skinning != null && !skinning.isEnabled()) skinning.setEnabled(true);
|
||||||
|
|
||||||
if ((currentClip != null || pendingClip != null) && transition > 0f) {
|
if (!pendingReversed && (currentClip != null || pendingClip != null) && transition > 0f) {
|
||||||
com.jme3.anim.Armature arm = skinning != null ? skinning.getArmature() : null;
|
com.jme3.anim.Armature arm = skinning != null ? skinning.getArmature() : null;
|
||||||
AnimClip freeze = buildFreezeClip(arm);
|
AnimClip freeze = buildFreezeClip(arm);
|
||||||
if (freeze != null) {
|
if (freeze != null) {
|
||||||
@@ -182,6 +182,9 @@ public class AnimPlayer {
|
|||||||
currentClip = clipName;
|
currentClip = clipName;
|
||||||
pendingClip = null;
|
pendingClip = null;
|
||||||
blendSpace = null;
|
blendSpace = null;
|
||||||
|
// Speed immer auf 1 zurücksetzen – ClipAction-Instanz wird wiederverwendet
|
||||||
|
// und könnte noch speed=-1 vom letzten reversed-Play haben.
|
||||||
|
if (currentAction != null) { currentAction.setSpeed(1f); }
|
||||||
double len = currentAction != null ? currentAction.getLength() : -1;
|
double len = currentAction != null ? currentAction.getLength() : -1;
|
||||||
if (pendingReversed && currentAction != null && len > 0.01) {
|
if (pendingReversed && currentAction != null && len > 0.01) {
|
||||||
currentAction.setSpeed(-1f);
|
currentAction.setSpeed(-1f);
|
||||||
@@ -206,24 +209,25 @@ public class AnimPlayer {
|
|||||||
|
|
||||||
private void finishBlend() {
|
private void finishBlend() {
|
||||||
String clip = pendingClip;
|
String clip = pendingClip;
|
||||||
float elapsed = blendElapsed;
|
|
||||||
pendingClip = null;
|
pendingClip = null;
|
||||||
blendSpace = null;
|
blendSpace = null;
|
||||||
if (clip == null) return;
|
if (clip == null) return;
|
||||||
try {
|
try {
|
||||||
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
// setCurrentAction vor removeAction: kein 1-Frame-Gap ohne aktive Action
|
||||||
} catch (Exception ignored) {}
|
|
||||||
try {
|
|
||||||
currentAction = composer.setCurrentAction(clip);
|
currentAction = composer.setCurrentAction(clip);
|
||||||
currentClip = clip;
|
currentClip = clip;
|
||||||
|
try {
|
||||||
|
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
||||||
if (pendingReversed && currentAction != null && len > 0.01) {
|
if (pendingReversed && currentAction != null && len > 0.01) {
|
||||||
currentAction.setSpeed(-1f);
|
currentAction.setSpeed(-1f);
|
||||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
|
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
|
||||||
pendingReversed = false;
|
pendingReversed = false;
|
||||||
} else if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
|
} else if (len > 0.01 && (blendDuration > 0 || pendingStartOffset > 0)) {
|
||||||
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
// blendDuration statt blendElapsed: blendElapsed hat bereits das aktuelle tpf
|
||||||
float t = (pendingStartOffset + elapsed) % (float) len;
|
// eingerechnet, die Sub-Action im AnimComposer aber noch nicht → 1-Frame exakt.
|
||||||
|
float t = (pendingStartOffset + blendDuration) % (float) len;
|
||||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
|
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
|
||||||
pendingStartOffset = 0f;
|
pendingStartOffset = 0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public enum AnimationAction {
|
|||||||
JUMP,
|
JUMP,
|
||||||
RUNNING_JUMP,
|
RUNNING_JUMP,
|
||||||
ROLL,
|
ROLL,
|
||||||
DUCK,
|
STAND_TO_CROUCH,
|
||||||
CROUCH_IDLE,
|
CROUCH_IDLE,
|
||||||
CROUCH_TO_STAND,
|
CROUCH_TO_STAND,
|
||||||
SNEAK,
|
SNEAK,
|
||||||
@@ -42,20 +42,20 @@ public enum AnimationAction {
|
|||||||
|
|
||||||
// ── Schwert ───────────────────────────────────────────────────────────────
|
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||||
SWORD_IDLE_BORED, SWORD_IDLE_BORED_2, SWORD_IDLE_BORED_3, SWORD_DEATH, SWORD_DEATH_2,
|
SWORD_IDLE_BORED, SWORD_IDLE_BORED_2, SWORD_IDLE_BORED_3, SWORD_DEATH, SWORD_DEATH_2,
|
||||||
SWORD_ATTACK_1, SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,SWORD_COUNTERATTACK, SWORD_IDLE_IMPACT,
|
SWORD_ATTACK_1, SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK, SWORD_PARADE, SWORD_IDLE_IMPACT,
|
||||||
SWORD_IDLE, SWORD_WALK, SWORD_RUN, SWORD_JUMP, SWORD_RUNNING_JUMP,
|
SWORD_IDLE, SWORD_WALK, SWORD_RUN, SWORD_JUMP, SWORD_RUNNING_JUMP,
|
||||||
SWORD_BLOCK, SWORD_IDLE_TO_BLOCK, SWORD_BLOCK_IMPACT, SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT,
|
SWORD_BLOCK, SWORD_IDLE_TO_BLOCK, SWORD_BLOCK_IMPACT, SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT,
|
||||||
|
|
||||||
// ── Axt ───────────────────────────────────────────────────────────────────
|
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
|
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
|
||||||
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
|
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
|
||||||
AXE_IDLE_TO_BLOCK, AXE_COUNTERATTACK, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT,
|
AXE_IDLE_TO_BLOCK, AXE_PARADE, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT,
|
||||||
AXE_IDLE, AXE_WALK, AXE_RUN,
|
AXE_IDLE, AXE_WALK, AXE_RUN,
|
||||||
|
|
||||||
// ── Zweihänder ────────────────────────────────────────────────────────────
|
// ── Zweihänder ────────────────────────────────────────────────────────────
|
||||||
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK,
|
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK,
|
||||||
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
|
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
|
||||||
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_COUNTERATTACK, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
|
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_PARADE, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
|
||||||
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
||||||
|
|
||||||
// ── Bogen ─────────────────────────────────────────────────────────────────
|
// ── Bogen ─────────────────────────────────────────────────────────────────
|
||||||
@@ -85,7 +85,7 @@ public enum AnimationAction {
|
|||||||
case JUMP -> TextRegistry.resolve(null, key, "Jump");
|
case JUMP -> TextRegistry.resolve(null, key, "Jump");
|
||||||
case RUNNING_JUMP -> TextRegistry.resolve(null, key, "Running Jump");
|
case RUNNING_JUMP -> TextRegistry.resolve(null, key, "Running Jump");
|
||||||
case ROLL -> TextRegistry.resolve(null, key, "Rolle");
|
case ROLL -> TextRegistry.resolve(null, key, "Rolle");
|
||||||
case DUCK -> TextRegistry.resolve(null, key, "Hinducken");
|
case STAND_TO_CROUCH -> TextRegistry.resolve(null, key, "Hinknieen");
|
||||||
case CROUCH_IDLE -> TextRegistry.resolve(null, key, "Ducken (Idle)");
|
case CROUCH_IDLE -> TextRegistry.resolve(null, key, "Ducken (Idle)");
|
||||||
case CROUCH_TO_STAND -> TextRegistry.resolve(null, key, "Aufstehen (Ducken)");
|
case CROUCH_TO_STAND -> TextRegistry.resolve(null, key, "Aufstehen (Ducken)");
|
||||||
case SNEAK -> TextRegistry.resolve(null, key, "Schleichen");
|
case SNEAK -> TextRegistry.resolve(null, key, "Schleichen");
|
||||||
@@ -119,7 +119,7 @@ public enum AnimationAction {
|
|||||||
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
||||||
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
||||||
case SWORD_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Schwert)");
|
case SWORD_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Schwert)");
|
||||||
case SWORD_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Schwert)");
|
case SWORD_PARADE -> TextRegistry.resolve(null, key, "Parade (Schwert)");
|
||||||
case SWORD_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Schwert)");
|
case SWORD_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Schwert)");
|
||||||
case SWORD_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
case SWORD_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
||||||
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||||
@@ -138,7 +138,7 @@ public enum AnimationAction {
|
|||||||
case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
|
case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
|
||||||
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
||||||
case AXE_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Axt)");
|
case AXE_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Axt)");
|
||||||
case AXE_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Axt)");
|
case AXE_PARADE -> TextRegistry.resolve(null, key, "Parade (Axt)");
|
||||||
case AXE_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Axt)");
|
case AXE_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Axt)");
|
||||||
case AXE_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
case AXE_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
||||||
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
||||||
@@ -153,7 +153,7 @@ public enum AnimationAction {
|
|||||||
case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
|
case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
|
||||||
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
||||||
case TWO_HAND_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Zweihänder)");
|
case TWO_HAND_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Zweihänder)");
|
||||||
case TWO_HAND_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Zweihänder)");
|
case TWO_HAND_PARADE -> TextRegistry.resolve(null, key, "Parade (Zweihänder)");
|
||||||
case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)");
|
case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)");
|
||||||
case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
||||||
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package de.blight.game.combat;
|
||||||
|
|
||||||
|
import com.jme3.anim.AnimComposer;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import de.blight.game.animation.AnimationAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstrakte Basisklasse für alle Kampfstile.
|
||||||
|
*
|
||||||
|
* Enthält:
|
||||||
|
* – das {@link AnimReceiver}-Interface zum einheitlichen Empfangen von
|
||||||
|
* Kampf-Animationen (Stagger, Impact) auf Spieler- UND NPC-Seite
|
||||||
|
* – gemeinsame Hilfsmethoden (Clip-Länge lesen, Klingen-Treffertest)
|
||||||
|
* – gemeinsame Konstanten (Treffer-Radius, Impact-Speeds)
|
||||||
|
*
|
||||||
|
* Konkrete Kampfstile (z. B. {@code swordshield.SwordShieldCombat}) erben
|
||||||
|
* von dieser Klasse und ergänzen stil-spezifische Animations-Auflösung.
|
||||||
|
*/
|
||||||
|
public abstract class CombatStyleBase {
|
||||||
|
|
||||||
|
// ── Gemeinsame Konstanten ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Abspielgeschwindigkeit für normalen Treffer-Impact. */
|
||||||
|
public static final float IMPACT_SPEED_NORMAL = 1.0f;
|
||||||
|
/** Abspielgeschwindigkeit nach Konterparade (Slow-Motion-Reaktion). */
|
||||||
|
public static final float IMPACT_SPEED_PARRIED = 0.5f;
|
||||||
|
|
||||||
|
/** Körperradius eines NPCs für die Klingen-Kapsel-Trefferprüfung. */
|
||||||
|
public static final float HIT_RADIUS = 0.5f;
|
||||||
|
|
||||||
|
// ── AnimReceiver-Interface ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einheitliche Schnittstelle für Entitäten, die eine blockierende
|
||||||
|
* Kampfanimation empfangen können – implementiert von
|
||||||
|
* {@code PlayerInputControl} (Held) und NPC-Adaptern in
|
||||||
|
* {@code WorldNpcsState}.
|
||||||
|
*
|
||||||
|
* Eine „blockierende" Animation sperrt die Entität für die Dauer des
|
||||||
|
* Clips von weiteren eigenen Aktionen (Bewegung, Angriff usw.).
|
||||||
|
*
|
||||||
|
* @see CombatStyleBase#readActualClipLength(AnimComposer, float)
|
||||||
|
*/
|
||||||
|
public interface AnimReceiver {
|
||||||
|
/**
|
||||||
|
* Spielt {@code action} ab und sperrt die Entität für die Clip-Dauer.
|
||||||
|
*
|
||||||
|
* @param action die abzuspielende Animation
|
||||||
|
* @param speed Abspielgeschwindigkeit (1.0 = normal, 0.5 = Konterparade)
|
||||||
|
* @param onDone optionaler Callback wenn die Animation endet (darf null sein)
|
||||||
|
*/
|
||||||
|
void receiveBlockingAnim(AnimationAction action, float speed, Runnable onDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die tatsächliche Clip-Länge aus dem AnimComposer des DEFAULT_LAYER.
|
||||||
|
* Muss <em>nach</em> dem Starten der Animation aufgerufen werden, damit
|
||||||
|
* die korrekte Länge im Composer steht.
|
||||||
|
*
|
||||||
|
* @param ac AnimComposer der Entität (darf null sein)
|
||||||
|
* @param fallback Rückgabewert wenn Länge nicht ermittelbar
|
||||||
|
* @return tatsächliche Clip-Länge oder {@code fallback}
|
||||||
|
*/
|
||||||
|
public static float readActualClipLength(AnimComposer ac, float fallback) {
|
||||||
|
if (ac == null) { return fallback; }
|
||||||
|
var layer = ac.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||||
|
if (layer == null) { return fallback; }
|
||||||
|
var a = layer.getCurrentAction();
|
||||||
|
return (a != null && a.getLength() > 0.01) ? (float) a.getLength() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quadratischer Abstand von Punkt {@code p} zum Segment {@code a→b}.
|
||||||
|
* Wird für die Klingen-Kapsel-Trefferprüfung in allen Kampfstilen genutzt.
|
||||||
|
*/
|
||||||
|
public static float segmentDistSq(Vector3f p, Vector3f a, Vector3f b) {
|
||||||
|
float abX = b.x - a.x, abY = b.y - a.y, abZ = b.z - a.z;
|
||||||
|
float lenSq = abX*abX + abY*abY + abZ*abZ;
|
||||||
|
float t = lenSq > 0.0001f
|
||||||
|
? Math.max(0f, Math.min(1f, ((p.x-a.x)*abX + (p.y-a.y)*abY + (p.z-a.z)*abZ) / lenSq))
|
||||||
|
: 0f;
|
||||||
|
float cx = p.x - (a.x + t*abX);
|
||||||
|
float cy = p.y - (a.y + t*abY);
|
||||||
|
float cz = p.z - (a.z + t*abZ);
|
||||||
|
return cx*cx + cy*cy + cz*cz;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,4 +18,14 @@ public interface MeleeAttackCallback {
|
|||||||
*/
|
*/
|
||||||
void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir,
|
void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir,
|
||||||
float bladeLength, int baseDamage, float critMultiplier);
|
float bladeLength, int baseDamage, float critMultiplier);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird beim START eines normalen (nicht-schweren) Angriffs aufgerufen,
|
||||||
|
* damit NPCs die Konterparade-Entscheidung sofort treffen und die
|
||||||
|
* Parade-Animation frühzeitig starten können.
|
||||||
|
*
|
||||||
|
* @param attackId Gleiche ID wie beim nachfolgenden onMeleeAttack
|
||||||
|
* @param isHeavy true = schwerer Angriff (kein Konterparade-Kandidat)
|
||||||
|
*/
|
||||||
|
default void onAttackStart(int attackId, boolean isHeavy) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,11 +94,12 @@ public final class WeaponCombatState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Entscheidet nach dem aktuellen Angriffsschritt ob die Kombo verlängert wird.
|
* Entscheidet nach dem aktuellen Angriffsschritt ob die Kombo verlängert wird.
|
||||||
* Gibt den nächsten Schritt (1 oder 2) zurück, oder -1 wenn keine Verlängerung.
|
* Gleiche 5-stufige Sequenz wie beim Spieler: 0→1→2→3→4 (gerade=ATK1, ungerade=ATK2, 4=Finisher).
|
||||||
|
* Gibt den nächsten Schritt zurück, oder -1 wenn keine Verlängerung.
|
||||||
*/
|
*/
|
||||||
public int nextComboStep(java.util.Random rng) {
|
public int nextComboStep(java.util.Random rng) {
|
||||||
if (phase != Phase.ATTACKING || isHeavy || comboStep >= 2) { return -1; }
|
if (phase != Phase.ATTACKING || isHeavy || comboStep >= 4) { return -1; }
|
||||||
float chance = comboStep == 0 ? COMBO_P1 : COMBO_P2;
|
float chance = (comboStep % 2 == 0) ? COMBO_P1 : COMBO_P2;
|
||||||
return rng.nextFloat() < chance ? comboStep + 1 : -1;
|
return rng.nextFloat() < chance ? comboStep + 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package de.blight.game.combat.swordshield;
|
||||||
|
|
||||||
|
import de.blight.common.model.ItemSubCategory;
|
||||||
|
import de.blight.game.animation.AnimationAction;
|
||||||
|
import de.blight.game.combat.CombatStyleBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemeinsame Kampflogik für den Schwert-und-Schild-Stil.
|
||||||
|
*
|
||||||
|
* Wird von <em>beiden</em> Seiten genutzt:
|
||||||
|
* – {@code PlayerInputControl} (Held)
|
||||||
|
* – {@code WorldNpcsState} (Gegner-KI)
|
||||||
|
*
|
||||||
|
* Alle Animations-Auflösungen (welche Animation für welches Ereignis?) und
|
||||||
|
* alle Reaktions-Anwendungen (Impact, Stagger, Konterparade) sind hier
|
||||||
|
* zentralisiert. Bug-Fixes gelten damit automatisch für Held und NPC.
|
||||||
|
*
|
||||||
|
* Neue Kampfstile erben von {@link CombatStyleBase} und folgen demselben
|
||||||
|
* Muster in einem eigenen Unter-Package.
|
||||||
|
*/
|
||||||
|
public final class SwordShieldCombat extends CombatStyleBase {
|
||||||
|
|
||||||
|
private SwordShieldCombat() {}
|
||||||
|
|
||||||
|
// ── Animations-Auflösung ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Impact-Animation in Idle/Kampf-Haltung (Entität wurde getroffen).
|
||||||
|
* Identisch für Spieler und NPC – weapon type der GETROFFENEN Entität.
|
||||||
|
*/
|
||||||
|
public static AnimationAction resolveIdleImpactAction(ItemSubCategory weaponType) {
|
||||||
|
if (weaponType == null) { return AnimationAction.SWORD_IDLE_IMPACT; }
|
||||||
|
return switch (weaponType) {
|
||||||
|
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
|
||||||
|
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block-Impact-Animation (Entität hat einen Treffer abgeblockt).
|
||||||
|
* Identisch für Spieler und NPC.
|
||||||
|
*/
|
||||||
|
public static AnimationAction resolveBlockImpactAction(ItemSubCategory weaponType) {
|
||||||
|
if (weaponType == null) { return AnimationAction.SWORD_BLOCK_IMPACT; }
|
||||||
|
return switch (weaponType) {
|
||||||
|
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
|
||||||
|
default -> AnimationAction.SWORD_BLOCK_IMPACT;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konterangriff-Animation nach erfolgreicher Parade.
|
||||||
|
* Identisch für Spieler und NPC.
|
||||||
|
*/
|
||||||
|
public static AnimationAction resolveParadeAction(ItemSubCategory weaponType) {
|
||||||
|
if (weaponType == null) { return AnimationAction.SWORD_PARADE; }
|
||||||
|
return switch (weaponType) {
|
||||||
|
case AXE -> AnimationAction.AXE_PARADE;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_PARADE;
|
||||||
|
default -> AnimationAction.SWORD_PARADE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reaktionen anwenden ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaler Treffer-Impact (Entität wurde getroffen, kein Block).
|
||||||
|
* Für Held: {@code PlayerInputControl} implementiert {@link AnimReceiver}.
|
||||||
|
* Für NPC: {@code WorldNpcsState.npcAnimReceiver(s)} liefert den Adapter.
|
||||||
|
*/
|
||||||
|
public static void applyNormalImpact(AnimReceiver receiver, ItemSubCategory weaponType) {
|
||||||
|
receiver.receiveBlockingAnim(resolveIdleImpactAction(weaponType), IMPACT_SPEED_NORMAL, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konterparade-Impact (Angreifer wird durch Parade gestoppt – Slow Motion).
|
||||||
|
* Wird aufgerufen auf dem ANGREIFER, nicht auf dem Parierenden.
|
||||||
|
*/
|
||||||
|
public static void applyParriedImpact(AnimReceiver receiver, ItemSubCategory weaponType) {
|
||||||
|
receiver.receiveBlockingAnim(resolveIdleImpactAction(weaponType), IMPACT_SPEED_PARRIED, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,8 +22,8 @@ public class KeyBindings {
|
|||||||
public int drawMeleeWeapon = KeyInput.KEY_1;
|
public int drawMeleeWeapon = KeyInput.KEY_1;
|
||||||
public int drawRangedWeapon = KeyInput.KEY_2;
|
public int drawRangedWeapon = KeyInput.KEY_2;
|
||||||
public int sheathWeapon = KeyInput.KEY_X;
|
public int sheathWeapon = KeyInput.KEY_X;
|
||||||
public int roll = KeyInput.KEY_LCONTROL;
|
public int roll = KeyInput.KEY_LSHIFT;
|
||||||
public int sneak = KeyInput.KEY_X;
|
public int sneak = KeyInput.KEY_LCONTROL;
|
||||||
|
|
||||||
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
||||||
public static final String[][] ENTRIES = {
|
public static final String[][] ENTRIES = {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class JmeConsole extends BaseAppState {
|
|||||||
private static final Logger log = LoggerFactory.getLogger(JmeConsole.class);
|
private static final Logger log = LoggerFactory.getLogger(JmeConsole.class);
|
||||||
|
|
||||||
// ── Layout ────────────────────────────────────────────────────────────────
|
// ── Layout ────────────────────────────────────────────────────────────────
|
||||||
private static final int HISTORY = 6;
|
private static final int HISTORY = 10;
|
||||||
private static final float LINE_H = 18f;
|
private static final float LINE_H = 18f;
|
||||||
private static final float PAD = 6f;
|
private static final float PAD = 6f;
|
||||||
private static final float BG_H = PAD + LINE_H * (HISTORY + 1) + PAD;
|
private static final float BG_H = PAD + LINE_H * (HISTORY + 1) + PAD;
|
||||||
@@ -146,10 +146,13 @@ public class JmeConsole extends BaseAppState {
|
|||||||
public void feedEnter() { String raw = inputBuf.toString().trim(); inputBuf.setLength(0); execute(raw); refreshInput(); }
|
public void feedEnter() { String raw = inputBuf.toString().trim(); inputBuf.setLength(0); execute(raw); refreshInput(); }
|
||||||
public void feedEscape() { hide(); }
|
public void feedEscape() { hide(); }
|
||||||
|
|
||||||
/** Gibt eine Zeile in der Konsole aus. */
|
/** Gibt eine oder mehrere Zeilen in der Konsole aus. */
|
||||||
public void print(String msg) {
|
public void print(String msg) {
|
||||||
history.addFirst(msg);
|
String[] lines = msg.split("\n", -1);
|
||||||
|
for (String line : lines) {
|
||||||
|
history.addFirst(line);
|
||||||
while (history.size() > HISTORY) history.pollLast();
|
while (history.size() > HISTORY) history.pollLast();
|
||||||
|
}
|
||||||
refreshHistory();
|
refreshHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
|||||||
private Spatial weaponSpatial;
|
private Spatial weaponSpatial;
|
||||||
private Spatial shieldSpatial;
|
private Spatial shieldSpatial;
|
||||||
private boolean drawn = false;
|
private boolean drawn = false;
|
||||||
|
private boolean weaponHidden = false;
|
||||||
|
private boolean shieldHidden = false;
|
||||||
|
|
||||||
public NpcWeaponAttachmentControl(AssetManager assetManager, Item weaponItem, Item shieldItem) {
|
public NpcWeaponAttachmentControl(AssetManager assetManager, Item weaponItem, Item shieldItem) {
|
||||||
this.assetManager = assetManager;
|
this.assetManager = assetManager;
|
||||||
@@ -69,6 +71,16 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
|||||||
this.drawn = drawn;
|
this.drawn = drawn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void hideWeapon() {
|
||||||
|
weaponHidden = true;
|
||||||
|
if (weaponSpatial != null) { weaponSpatial.setCullHint(Spatial.CullHint.Always); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hideShield() {
|
||||||
|
shieldHidden = true;
|
||||||
|
if (shieldSpatial != null) { shieldSpatial.setCullHint(Spatial.CullHint.Always); }
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void controlUpdate(float tpf) {
|
protected void controlUpdate(float tpf) {
|
||||||
if (skinCtrl == null) {
|
if (skinCtrl == null) {
|
||||||
@@ -94,8 +106,14 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
|||||||
weaponItem != null ? weaponItem.getItemId() : "null",
|
weaponItem != null ? weaponItem.getItemId() : "null",
|
||||||
shieldItem != null ? shieldItem.getItemId() : "null");
|
shieldItem != null ? shieldItem.getItemId() : "null");
|
||||||
|
|
||||||
if (weaponItem != null) { weaponSpatial = loadAndAttach(weaponItem); }
|
if (weaponItem != null) {
|
||||||
if (shieldItem != null) { shieldSpatial = loadAndAttach(shieldItem); }
|
weaponSpatial = loadAndAttach(weaponItem);
|
||||||
|
if (weaponHidden && weaponSpatial != null) { weaponSpatial.setCullHint(Spatial.CullHint.Always); }
|
||||||
|
}
|
||||||
|
if (shieldItem != null) {
|
||||||
|
shieldSpatial = loadAndAttach(shieldItem);
|
||||||
|
if (shieldHidden && shieldSpatial != null) { shieldSpatial.setCullHint(Spatial.CullHint.Always); }
|
||||||
|
}
|
||||||
|
|
||||||
log.info("[NpcWeaponAttach] Geladen: weaponSpatial={} shieldSpatial={}",
|
log.info("[NpcWeaponAttach] Geladen: weaponSpatial={} shieldSpatial={}",
|
||||||
weaponSpatial != null, shieldSpatial != null);
|
weaponSpatial != null, shieldSpatial != null);
|
||||||
@@ -113,7 +131,7 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
|||||||
Vector3f charWorldPos = spatial.getWorldTranslation();
|
Vector3f charWorldPos = spatial.getWorldTranslation();
|
||||||
|
|
||||||
// ── Waffe ────────────────────────────────────────────────────────────────
|
// ── Waffe ────────────────────────────────────────────────────────────────
|
||||||
if (weaponSpatial != null) {
|
if (weaponSpatial != null && !weaponHidden) {
|
||||||
Node boneNode = drawn ? handNode : backNode;
|
Node boneNode = drawn ? handNode : backNode;
|
||||||
if (boneNode != null) {
|
if (boneNode != null) {
|
||||||
Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos));
|
Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos));
|
||||||
@@ -132,7 +150,7 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Schild ────────────────────────────────────────────────────────────────
|
// ── Schild ────────────────────────────────────────────────────────────────
|
||||||
if (shieldSpatial != null) {
|
if (shieldSpatial != null && !shieldHidden) {
|
||||||
ItemGrip grip = shieldItem != null ? shieldItem.getGrip() : null;
|
ItemGrip grip = shieldItem != null ? shieldItem.getGrip() : null;
|
||||||
if (drawn && leftForeArmNode != null) {
|
if (drawn && leftForeArmNode != null) {
|
||||||
Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
|
Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ import de.blight.game.animation.AnimationAction;
|
|||||||
import de.blight.game.animation.AnimationLibrary;
|
import de.blight.game.animation.AnimationLibrary;
|
||||||
import de.blight.game.animation.RetargetingSystem;
|
import de.blight.game.animation.RetargetingSystem;
|
||||||
import de.blight.game.config.KeyBindings;
|
import de.blight.game.config.KeyBindings;
|
||||||
|
import de.blight.game.combat.CombatStyleBase;
|
||||||
import de.blight.game.combat.MeleeAttackCallback;
|
import de.blight.game.combat.MeleeAttackCallback;
|
||||||
import de.blight.game.combat.WeaponCombatState;
|
import de.blight.game.combat.WeaponCombatState;
|
||||||
|
import de.blight.game.combat.swordshield.SwordShieldCombat;
|
||||||
import de.blight.game.navigation.CharacterNavigator;
|
import de.blight.game.navigation.CharacterNavigator;
|
||||||
import de.blight.game.state.TerrainChunkState;
|
import de.blight.game.state.TerrainChunkState;
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class PlayerInputControl {
|
public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
||||||
|
|
||||||
@@ -133,8 +135,11 @@ public class PlayerInputControl {
|
|||||||
private ItemSubCategory drawnWeaponType = null;
|
private ItemSubCategory drawnWeaponType = null;
|
||||||
private boolean isAiming = false;
|
private boolean isAiming = false;
|
||||||
private boolean rightMouseHeld = false;
|
private boolean rightMouseHeld = false;
|
||||||
|
private boolean attackBuffered = false; // LMB während Block → nach Block-Exit sofort angreifen
|
||||||
|
private float prevBlockAnimTime = 0f; // Vorzustand für Loop-Erkennung in ENTERING
|
||||||
private BlockPhase blockPhase = BlockPhase.NONE;
|
private BlockPhase blockPhase = BlockPhase.NONE;
|
||||||
private float blockPhaseTimer = 0f;
|
private float blockPhaseTimer = 0f;
|
||||||
|
private float blockTapTimer = 0f;
|
||||||
/** Gemeinsamer Zustandsautomat (Tap/Hold-Erkennung, parryArmed). */
|
/** Gemeinsamer Zustandsautomat (Tap/Hold-Erkennung, parryArmed). */
|
||||||
private final WeaponCombatState weaponCombat = new WeaponCombatState();
|
private final WeaponCombatState weaponCombat = new WeaponCombatState();
|
||||||
private boolean combatAnimActive = false;
|
private boolean combatAnimActive = false;
|
||||||
@@ -156,6 +161,12 @@ public class PlayerInputControl {
|
|||||||
private float heavyLungeStartRemaining = 0f;
|
private float heavyLungeStartRemaining = 0f;
|
||||||
/** combatAnimRemaining-Wert, ab dem die Lunge endet (Landung). */
|
/** combatAnimRemaining-Wert, ab dem die Lunge endet (Landung). */
|
||||||
private float heavyLungeEndRemaining = 0f;
|
private float heavyLungeEndRemaining = 0f;
|
||||||
|
// ── Spieler-Rückstoß (wenn getroffen oder Block absorbiert) ───────────────
|
||||||
|
private float playerKnockbackDirX = 0f;
|
||||||
|
private float playerKnockbackDirZ = 0f;
|
||||||
|
private float playerKnockbackTimer = 0f;
|
||||||
|
private static final float PLAYER_KNOCKBACK_MPS = 1.2f;
|
||||||
|
private static final float PLAYER_KNOCKBACK_TIME = 0.18f;
|
||||||
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
|
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
|
||||||
private float combatAnimDuration = 0f;
|
private float combatAnimDuration = 0f;
|
||||||
private final List<MeleeAttackCallback> meleeCallbacks = new ArrayList<>();
|
private final List<MeleeAttackCallback> meleeCallbacks = new ArrayList<>();
|
||||||
@@ -167,6 +178,8 @@ public class PlayerInputControl {
|
|||||||
private int currentAttackId = -1;
|
private int currentAttackId = -1;
|
||||||
private boolean hitWindowActive = false;
|
private boolean hitWindowActive = false;
|
||||||
private float hitWindowCritMult = 1.0f;
|
private float hitWindowCritMult = 1.0f;
|
||||||
|
private float hitWindowStart = HIT_WINDOW_START;
|
||||||
|
private float hitWindowEnd = HIT_WINDOW_END;
|
||||||
// Angriffs-Sound via Waffen-Geschwindigkeit
|
// Angriffs-Sound via Waffen-Geschwindigkeit
|
||||||
private static final float ATTACK_SOUND_VELOCITY = 3.5f;
|
private static final float ATTACK_SOUND_VELOCITY = 3.5f;
|
||||||
// Attacken mit Backswing → Sound erst nach Mindest-Fortschritt erlauben
|
// Attacken mit Backswing → Sound erst nach Mindest-Fortschritt erlauben
|
||||||
@@ -282,12 +295,25 @@ public class PlayerInputControl {
|
|||||||
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
|
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
|
||||||
private Vector3f autopilotDir = null;
|
private Vector3f autopilotDir = null;
|
||||||
|
|
||||||
|
/** Position des nächsten angreifenden NPCs – für Lungen-Richtung bei schwerem Angriff. */
|
||||||
|
private Vector3f nearestAttackingNpcPos = null;
|
||||||
|
|
||||||
|
public void setNearestAttackingNpcPos(Vector3f pos) {
|
||||||
|
this.nearestAttackingNpcPos = pos != null ? pos.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Schleichen / Ducken ───────────────────────────────────────────────────
|
// ── Schleichen / Ducken ───────────────────────────────────────────────────
|
||||||
private enum SneakPhase { NONE, DOWN, ACTIVE, UP }
|
private enum SneakPhase { NONE, DOWN, ACTIVE, UP }
|
||||||
private SneakPhase sneakPhase = SneakPhase.NONE;
|
private SneakPhase sneakPhase = SneakPhase.NONE;
|
||||||
private float sneakTransitionRemaining = 0f;
|
private float sneakTransitionRemaining = 0f;
|
||||||
private boolean sneakKeyHeld = false;
|
private boolean sneakKeyHeld = false;
|
||||||
|
|
||||||
|
// ── Idle-Override (z.B. CROUCH_IDLE während Looten) ──────────────────────
|
||||||
|
private AnimationAction idleOverride = null;
|
||||||
|
|
||||||
|
public void setIdleOverride(AnimationAction action) { idleOverride = action; currentAnim = null; }
|
||||||
|
public void clearIdleOverride() { idleOverride = null; currentAnim = null; }
|
||||||
|
|
||||||
// ── Dodge Roll ────────────────────────────────────────────────────────────
|
// ── Dodge Roll ────────────────────────────────────────────────────────────
|
||||||
private boolean rollActive = false;
|
private boolean rollActive = false;
|
||||||
private float rollRemaining = 0f;
|
private float rollRemaining = 0f;
|
||||||
@@ -326,7 +352,8 @@ public class PlayerInputControl {
|
|||||||
case ACT_SHEATH -> { if (isPressed && weaponDrawn && !blockingAnimActive && !drawAnimActive) sheathCurrentWeapon(); }
|
case ACT_SHEATH -> { if (isPressed && weaponDrawn && !blockingAnimActive && !drawAnimActive) sheathCurrentWeapon(); }
|
||||||
case ACT_COMBAT_ATK -> {
|
case ACT_COMBAT_ATK -> {
|
||||||
if (isPressed) {
|
if (isPressed) {
|
||||||
if (!weaponDrawn || blockingAnimActive || drawAnimActive || isBlocking()) break;
|
if (!weaponDrawn || blockingAnimActive || drawAnimActive) break;
|
||||||
|
if (isBlocking()) { attackBuffered = true; break; }
|
||||||
// Immer Hold-Timer starten – auch mitten in einer Kombo
|
// Immer Hold-Timer starten – auch mitten in einer Kombo
|
||||||
attackHoldPending = true;
|
attackHoldPending = true;
|
||||||
attackHoldTimer = 0f;
|
attackHoldTimer = 0f;
|
||||||
@@ -471,6 +498,7 @@ public class PlayerInputControl {
|
|||||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
stopWading();
|
stopWading();
|
||||||
rightMouseHeld = false;
|
rightMouseHeld = false;
|
||||||
|
attackBuffered = false;
|
||||||
blockPhase = BlockPhase.NONE;
|
blockPhase = BlockPhase.NONE;
|
||||||
weaponCombat.phase = WeaponCombatState.Phase.IDLE;
|
weaponCombat.phase = WeaponCombatState.Phase.IDLE;
|
||||||
weaponCombat.parryArmed = false;
|
weaponCombat.parryArmed = false;
|
||||||
@@ -519,19 +547,70 @@ public class PlayerInputControl {
|
|||||||
* Sekunden und ruft danach {@code onComplete} auf (im Update-Thread).
|
* Sekunden und ruft danach {@code onComplete} auf (im Update-Thread).
|
||||||
* Bei {@code duration <= 0} wird die echte Clip-Länge aus dem AnimComposer abgefragt.
|
* Bei {@code duration <= 0} wird die echte Clip-Länge aus dem AnimComposer abgefragt.
|
||||||
*/
|
*/
|
||||||
public void requestAnimation(AnimationAction action, float duration, Runnable onComplete) {
|
/** Implementierung von {@link de.blight.game.combat.CombatStyleBase.AnimReceiver}. */
|
||||||
if (duration <= 0f) {
|
@Override
|
||||||
duration = resolveClipLength(action, 1.5f);
|
public void receiveBlockingAnim(AnimationAction action, float speed, Runnable onDone) {
|
||||||
|
requestAnimation(action, 0f, speed, onDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestAnimation(AnimationAction action, float duration, Runnable onComplete) {
|
||||||
|
requestAnimation(action, duration, 1f, onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestAnimation(AnimationAction action, float duration, float speed, Runnable onComplete) {
|
||||||
|
if (playerDead) { return; }
|
||||||
|
// Laufende Kampf-Animation sofort abbrechen – sonst blockiert blockingAnimActive den
|
||||||
|
// combatAnimActive-Timer nicht (blockingAnimActive-Return kommt früher in controlUpdate).
|
||||||
|
if (combatAnimActive) {
|
||||||
|
combatAnimActive = false;
|
||||||
|
hitWindowActive = false;
|
||||||
|
hitWindowStart = HIT_WINDOW_START;
|
||||||
|
hitWindowEnd = HIT_WINDOW_END;
|
||||||
|
heavyLungeVel = null;
|
||||||
|
heavyLungeStartRemaining = 0f;
|
||||||
|
heavyLungeEndRemaining = 0f;
|
||||||
|
combatAnimCallback = null;
|
||||||
|
combatAnimSkipComboWindow = false;
|
||||||
|
comboWindowActive = false;
|
||||||
|
comboStep = 0;
|
||||||
}
|
}
|
||||||
blockingAnimActive = true;
|
|
||||||
blockingAnimRemaining = duration;
|
|
||||||
blockingAnimTotal = duration;
|
|
||||||
blockingAnimCallback = onComplete;
|
|
||||||
autopilotDir = null;
|
autopilotDir = null;
|
||||||
forward = backward = left = right = false;
|
forward = backward = left = right = false;
|
||||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
// Blend-Übergänge für blockierende Animationen (Impact, Stagger) verbieten:
|
||||||
|
// ein noch aktiver nextTransitionLength aus einem Kombo-Schlag würde eine BlendAction
|
||||||
|
// erzeugen deren getLength() 0 zurückgibt → Fallback-Timer zu lang → Animation loopt.
|
||||||
|
nextTransitionLength = -1.0;
|
||||||
playAction(action);
|
playAction(action);
|
||||||
currentAnim = action;
|
currentAnim = action;
|
||||||
|
// Clip-Länge zuverlässig aus dem AnimClip lesen (nicht von der live BlendAction)
|
||||||
|
if (duration <= 0f) {
|
||||||
|
duration = resolveClipLength(action, 1.5f);
|
||||||
|
}
|
||||||
|
if (speed != 1f && animComposer != null) {
|
||||||
|
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||||
|
if (layer != null) {
|
||||||
|
var a = layer.getCurrentAction();
|
||||||
|
if (a != null) { a.setSpeed(speed); }
|
||||||
|
}
|
||||||
|
duration = duration / speed; // Timer an Speed anpassen
|
||||||
|
}
|
||||||
|
// Rückstoß weg vom angreifenden NPC
|
||||||
|
if (nearestAttackingNpcPos != null && physicsChar != null) {
|
||||||
|
Vector3f myPos = physicsChar.getPhysicsLocation();
|
||||||
|
float dx = myPos.x - nearestAttackingNpcPos.x;
|
||||||
|
float dz = myPos.z - nearestAttackingNpcPos.z;
|
||||||
|
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
if (len > 0.001f) {
|
||||||
|
playerKnockbackDirX = dx / len;
|
||||||
|
playerKnockbackDirZ = dz / len;
|
||||||
|
playerKnockbackTimer = PLAYER_KNOCKBACK_TIME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockingAnimActive = true;
|
||||||
|
blockingAnimRemaining = Math.max(0.1f, duration - 0.05f);
|
||||||
|
blockingAnimTotal = blockingAnimRemaining;
|
||||||
|
blockingAnimCallback = onComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Liefert die Länge des Clips für {@code action} in Sekunden, oder {@code fallback} wenn nicht ermittelbar. */
|
/** Liefert die Länge des Clips für {@code action} in Sekunden, oder {@code fallback} wenn nicht ermittelbar. */
|
||||||
@@ -872,13 +951,41 @@ public class PlayerInputControl {
|
|||||||
// Blockierende Einmal-Animation (lie_down, lie_up …)
|
// Blockierende Einmal-Animation (lie_down, lie_up …)
|
||||||
if (blockingAnimActive) {
|
if (blockingAnimActive) {
|
||||||
blockingAnimRemaining -= tpf;
|
blockingAnimRemaining -= tpf;
|
||||||
|
if (playerKnockbackTimer > 0f) {
|
||||||
|
playerKnockbackTimer -= tpf;
|
||||||
|
float ratio = Math.max(0f, playerKnockbackTimer / PLAYER_KNOCKBACK_TIME);
|
||||||
|
float spd = PLAYER_KNOCKBACK_MPS / CharacterNavigator.RUN_SPEED_MPS * MOVE_SPEED;
|
||||||
|
physicsChar.setWalkDirection(scratchWalk.set(
|
||||||
|
playerKnockbackDirX * spd * ratio, 0f, playerKnockbackDirZ * spd * ratio));
|
||||||
|
} else {
|
||||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
if (blockingAnimRemaining <= 0f && !playerDead) {
|
if (blockingAnimRemaining <= 0f && !playerDead) {
|
||||||
blockingAnimActive = false;
|
blockingAnimActive = false;
|
||||||
Runnable cb = blockingAnimCallback;
|
Runnable cb = blockingAnimCallback;
|
||||||
blockingAnimCallback = null;
|
blockingAnimCallback = null;
|
||||||
if (cb != null) cb.run();
|
if (cb != null) cb.run();
|
||||||
|
// Block-Puffer nach Impact/Stagger: RMB wurde während der Anim gehalten
|
||||||
|
if (!combatAnimActive && rightMouseHeld && blockPhase == BlockPhase.NONE) {
|
||||||
|
blockTapTimer = 0f;
|
||||||
|
weaponCombat.parryArmed = false;
|
||||||
|
startBlockEnter();
|
||||||
|
}
|
||||||
|
} else if (playerDead && blockingAnimRemaining <= 0f && blockingAnimRemaining > -500f) {
|
||||||
|
// Todesanim abgelaufen → auf letztem Frame einfrieren
|
||||||
|
if (animComposer != null) {
|
||||||
|
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||||
|
if (layer != null) {
|
||||||
|
com.jme3.anim.tween.action.Action a = layer.getCurrentAction();
|
||||||
|
if (a != null && a.getSpeed() != 0f) {
|
||||||
|
a.setSpeed(0f);
|
||||||
|
layer.setTime(Math.max(0f, (float) a.getLength() - 0.01f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockingAnimRemaining = -1000f;
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -942,8 +1049,7 @@ public class PlayerInputControl {
|
|||||||
currentWaterDepth = (waterDepthQuery != null) ? waterDepthQuery.apply(charPos.x, charPos.z) : 0f;
|
currentWaterDepth = (waterDepthQuery != null) ? waterDepthQuery.apply(charPos.x, charPos.z) : 0f;
|
||||||
boolean inDeepWater = currentWaterDepth > WATER_WADING_DEPTH;
|
boolean inDeepWater = currentWaterDepth > WATER_WADING_DEPTH;
|
||||||
|
|
||||||
// Schon ab WAITING Gehgeschwindigkeit erzwingen, damit beim Blockstart kein Sprint-Frame entsteht
|
boolean inBlockStance = isBlocking();
|
||||||
boolean inBlockStance = isBlocking() || blockPhase == BlockPhase.WAITING;
|
|
||||||
|
|
||||||
boolean sneaking = sneakPhase != SneakPhase.NONE;
|
boolean sneaking = sneakPhase != SneakPhase.NONE;
|
||||||
|
|
||||||
@@ -1029,6 +1135,12 @@ public class PlayerInputControl {
|
|||||||
comboStep = 0;
|
comboStep = 0;
|
||||||
log.info("[Combat] Kombo-Fenster verpasst → Reset");
|
log.info("[Combat] Kombo-Fenster verpasst → Reset");
|
||||||
}
|
}
|
||||||
|
// Gepufferter Block: RMB wurde während des Angriffs gehalten → jetzt Block einleiten
|
||||||
|
if (!combatAnimActive && rightMouseHeld && blockPhase == BlockPhase.NONE) {
|
||||||
|
blockTapTimer = 0f;
|
||||||
|
weaponCombat.parryArmed = false;
|
||||||
|
startBlockEnter();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Kombo-Fenster öffnet sich in der zweiten Hälfte der Anim
|
// Kombo-Fenster öffnet sich in der zweiten Hälfte der Anim
|
||||||
if (!comboWindowActive && !combatAnimSkipComboWindow
|
if (!comboWindowActive && !combatAnimSkipComboWindow
|
||||||
@@ -1060,9 +1172,9 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
if (hitWindowActive && isMeleeType(drawnWeaponType) && combatAnimDuration > 0f) {
|
if (hitWindowActive && isMeleeType(drawnWeaponType) && combatAnimDuration > 0f) {
|
||||||
float t = (combatAnimDuration - combatAnimRemaining) / combatAnimDuration;
|
float t = (combatAnimDuration - combatAnimRemaining) / combatAnimDuration;
|
||||||
if (t >= HIT_WINDOW_START && t <= HIT_WINDOW_END) {
|
if (t >= hitWindowStart && t <= hitWindowEnd) {
|
||||||
fireWeaponCallback(hitWindowCritMult);
|
fireWeaponCallback(hitWindowCritMult);
|
||||||
} else if (t > HIT_WINDOW_END) {
|
} else if (t > hitWindowEnd) {
|
||||||
hitWindowActive = false;
|
hitWindowActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1095,6 +1207,9 @@ public class PlayerInputControl {
|
|||||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||||
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
||||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||||
|
} else if (idleOverride != null && !moving) {
|
||||||
|
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
||||||
|
target = idleOverride;
|
||||||
} else if (sneaking) {
|
} else if (sneaking) {
|
||||||
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
||||||
target = moving ? AnimationAction.SNEAK : AnimationAction.CROUCH_IDLE;
|
target = moving ? AnimationAction.SNEAK : AnimationAction.CROUCH_IDLE;
|
||||||
@@ -1241,6 +1356,10 @@ public class PlayerInputControl {
|
|||||||
currentAttackId = ++attackIdCounter;
|
currentAttackId = ++attackIdCounter;
|
||||||
hitWindowActive = true;
|
hitWindowActive = true;
|
||||||
hitWindowCritMult = lastAttackCritical ? 2.0f : 1.0f;
|
hitWindowCritMult = lastAttackCritical ? 2.0f : 1.0f;
|
||||||
|
// Steps 1+3 = Attack2 (Backswing): Fenster später schließen damit Schwung den NPC erreicht
|
||||||
|
// Rückschwung (steps 1+3): Trefferfenster beginnt später (Schwung muss erst ankommen)
|
||||||
|
hitWindowStart = (step == 1 || step == 3) ? 0.45f : HIT_WINDOW_START;
|
||||||
|
hitWindowEnd = (step == 1 || step == 3) ? 0.92f : HIT_WINDOW_END;
|
||||||
currentComboStep = step;
|
currentComboStep = step;
|
||||||
comboStep = (step + 1) % 5;
|
comboStep = (step + 1) % 5;
|
||||||
comboWindowActive = false;
|
comboWindowActive = false;
|
||||||
@@ -1258,21 +1377,11 @@ public class PlayerInputControl {
|
|||||||
combatAnimCallback = null;
|
combatAnimCallback = null;
|
||||||
playAction(atk);
|
playAction(atk);
|
||||||
currentAnim = atk;
|
currentAnim = atk;
|
||||||
// Finisher (step 4): leichte Vorwärtsbewegung
|
for (MeleeAttackCallback cb : meleeCallbacks) { cb.onAttackStart(currentAttackId, false); }
|
||||||
if (step == 4) {
|
// Lunge: schließt Lücke zum Gegner auf idealen Treffabstand (1.7m)
|
||||||
Vector3f fwd = (visual != null)
|
heavyLungeVel = computeLungeVelocity(dur);
|
||||||
? visual.getWorldRotation().mult(new Vector3f(0f, 0f, 1f))
|
|
||||||
: cam.getDirection().clone();
|
|
||||||
fwd.y = 0f;
|
|
||||||
if (fwd.lengthSquared() > 0.001f) fwd.normalizeLocal();
|
|
||||||
heavyLungeVel = fwd.mult(MOVE_SPEED * 0.1f);
|
|
||||||
heavyLungeStartRemaining = dur * (1f - LUNGE_FRAC_START);
|
heavyLungeStartRemaining = dur * (1f - LUNGE_FRAC_START);
|
||||||
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
||||||
} else {
|
|
||||||
heavyLungeVel = null;
|
|
||||||
heavyLungeStartRemaining = 0f;
|
|
||||||
heavyLungeEndRemaining = 0f;
|
|
||||||
}
|
|
||||||
} else if (isRangedType(drawnWeaponType) && isAiming) {
|
} else if (isRangedType(drawnWeaponType) && isAiming) {
|
||||||
isAiming = false;
|
isAiming = false;
|
||||||
AnimationAction fireAnim = getRangedFireAction(drawnWeaponType);
|
AnimationAction fireAnim = getRangedFireAction(drawnWeaponType);
|
||||||
@@ -1296,24 +1405,32 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleCombatAlt(boolean isPressed) {
|
private void handleCombatAlt(boolean isPressed) {
|
||||||
if (!weaponDrawn || blockingAnimActive) return;
|
if (!weaponDrawn) return;
|
||||||
if (isMeleeType(drawnWeaponType)) {
|
if (isMeleeType(drawnWeaponType)) {
|
||||||
if (isPressed) {
|
if (isPressed) {
|
||||||
if (blockPhase == BlockPhase.NONE) {
|
rightMouseHeld = true;
|
||||||
blockPhase = BlockPhase.WAITING;
|
// Sofort blocken nur wenn keine andere Animation läuft
|
||||||
blockPhaseTimer = 0f;
|
if (blockPhase == BlockPhase.NONE && !combatAnimActive && !blockingAnimActive) {
|
||||||
weaponCombat.onBlockPress();
|
blockTapTimer = 0f;
|
||||||
|
weaponCombat.parryArmed = false;
|
||||||
|
startBlockEnter();
|
||||||
}
|
}
|
||||||
|
// Wenn Angriff oder Impact läuft: Block gepuffert, wird danach ausgelöst
|
||||||
} else {
|
} else {
|
||||||
|
rightMouseHeld = false;
|
||||||
|
if (blockingAnimActive) return; // Loslassen während Impact → ignorieren
|
||||||
switch (blockPhase) {
|
switch (blockPhase) {
|
||||||
case WAITING -> {
|
case ENTERING -> {
|
||||||
// Kurzer Tap: Parade-Animation abspielen (auch wenn NPC nicht im Trefferfenster)
|
if (blockTapTimer <= WeaponCombatState.BLOCK_TAP_MAX) {
|
||||||
if (weaponCombat.onBlockRelease()) {
|
// Kurzer Tap: Block abbrechen, Parade auslösen
|
||||||
playCounterAttack();
|
|
||||||
}
|
|
||||||
blockPhase = BlockPhase.NONE;
|
blockPhase = BlockPhase.NONE;
|
||||||
|
weaponCombat.parryArmed = true;
|
||||||
|
playCounterAttack();
|
||||||
|
} else {
|
||||||
|
startBlockExit();
|
||||||
}
|
}
|
||||||
case ENTERING, HOLDING -> startBlockExit();
|
}
|
||||||
|
case HOLDING -> startBlockExit();
|
||||||
default -> {}
|
default -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1324,25 +1441,33 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
private void updateBlockPhase(float tpf) {
|
private void updateBlockPhase(float tpf) {
|
||||||
switch (blockPhase) {
|
switch (blockPhase) {
|
||||||
case WAITING -> {
|
|
||||||
// Tap-Timer läuft in WeaponCombatState; gibt true zurück wenn Fenster abläuft
|
|
||||||
if (weaponCombat.tickParryWaiting(tpf)) {
|
|
||||||
// Tap-Fenster abgelaufen ohne Loslassen → Haltephase
|
|
||||||
if (hasShieldEquipped()) {
|
|
||||||
startBlockEnter();
|
|
||||||
} else {
|
|
||||||
blockPhase = BlockPhase.NONE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case ENTERING -> {
|
case ENTERING -> {
|
||||||
|
blockTapTimer += tpf;
|
||||||
blockPhaseTimer -= tpf;
|
blockPhaseTimer -= tpf;
|
||||||
if (blockPhaseTimer <= 0f) {
|
// Primär: AnimComposer-Fortschritt (>= 90%) ODER Loop-Erkennung (Zeit fällt zurück)
|
||||||
|
// Sicherung: Timer-Fallback falls AnimComposer-Länge = 0
|
||||||
|
boolean switchToHolding = blockPhaseTimer <= 0f;
|
||||||
|
if (!switchToHolding && animComposer != null) {
|
||||||
|
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||||
|
if (layer != null) {
|
||||||
|
float t = (float) layer.getTime();
|
||||||
|
var a = layer.getCurrentAction();
|
||||||
|
double len = (a != null) ? a.getLength() : 0;
|
||||||
|
if (len > 0.01 && t >= len * 0.90) {
|
||||||
|
switchToHolding = true; // Animation zu >= 90% abgespielt
|
||||||
|
} else if (t < prevBlockAnimTime - 0.05f) {
|
||||||
|
switchToHolding = true; // Loop erkannt: Zeit ist zurückgesprungen
|
||||||
|
}
|
||||||
|
prevBlockAnimTime = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (switchToHolding) {
|
||||||
|
prevBlockAnimTime = 0f;
|
||||||
blockPhase = BlockPhase.HOLDING;
|
blockPhase = BlockPhase.HOLDING;
|
||||||
AnimationAction blockAct = getMeleeBlockAction(drawnWeaponType);
|
AnimationAction holdAnim = getMeleeBlockAction(drawnWeaponType);
|
||||||
nextTransitionLength = COMBAT_BLEND;
|
nextTransitionLength = -1.0;
|
||||||
playAction(blockAct);
|
playAction(holdAnim);
|
||||||
currentAnim = blockAct;
|
currentAnim = holdAnim;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case EXITING -> {
|
case EXITING -> {
|
||||||
@@ -1350,6 +1475,10 @@ public class PlayerInputControl {
|
|||||||
if (blockPhaseTimer <= 0f) {
|
if (blockPhaseTimer <= 0f) {
|
||||||
blockPhase = BlockPhase.NONE;
|
blockPhase = BlockPhase.NONE;
|
||||||
currentAnim = null;
|
currentAnim = null;
|
||||||
|
if (attackBuffered) {
|
||||||
|
attackBuffered = false;
|
||||||
|
handleCombatAttack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default -> {}
|
default -> {}
|
||||||
@@ -1358,13 +1487,24 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
private void startBlockEnter() {
|
private void startBlockEnter() {
|
||||||
blockPhase = BlockPhase.ENTERING;
|
blockPhase = BlockPhase.ENTERING;
|
||||||
|
prevBlockAnimTime = 0f; // Loop-Detection-Vorzustand zurücksetzen
|
||||||
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
|
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
|
||||||
float dur = resolveClipLength(idleToBlock, 0.5f);
|
nextTransitionLength = -1.0;
|
||||||
// Etwas kürzer als Clip-Länge damit die Animation nicht einen Frame lang re-startet bevor HOLDING greift
|
|
||||||
blockPhaseTimer = Math.max(0.1f, dur - 0.05f);
|
|
||||||
nextTransitionLength = COMBAT_BLEND;
|
|
||||||
playAction(idleToBlock);
|
playAction(idleToBlock);
|
||||||
currentAnim = idleToBlock;
|
currentAnim = idleToBlock;
|
||||||
|
// Länge nach playAction() aus dem AnimComposer lesen: Clip ist jetzt aktiv,
|
||||||
|
// daher exakte Länge verfügbar (resolveClipLength kann Fallback liefern → Clip-Loop).
|
||||||
|
float dur = 0.5f;
|
||||||
|
if (animComposer != null) {
|
||||||
|
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||||
|
if (layer != null) {
|
||||||
|
var a = layer.getCurrentAction();
|
||||||
|
if (a != null && a.getLength() > 0.01) {
|
||||||
|
dur = (float) a.getLength();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockPhaseTimer = Math.max(0.1f, dur - 0.05f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startBlockExit() {
|
private void startBlockExit() {
|
||||||
@@ -1380,14 +1520,14 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
private void playCounterAttack() {
|
private void playCounterAttack() {
|
||||||
if (combatAnimActive) return;
|
if (combatAnimActive) return;
|
||||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("counterparade"); }
|
AnimationAction counterAct = getMeleeParadeAction(drawnWeaponType);
|
||||||
AnimationAction counterAct = getMeleeCounterAttackAction(drawnWeaponType);
|
|
||||||
float dur = resolveClipLength(counterAct, 0.7f);
|
float dur = resolveClipLength(counterAct, 0.7f);
|
||||||
nextTransitionLength = COMBAT_BLEND;
|
nextTransitionLength = COMBAT_BLEND;
|
||||||
combatAnimActive = true;
|
combatAnimActive = true;
|
||||||
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||||
combatAnimCallback = null;
|
combatAnimCallback = null;
|
||||||
combatAnimSkipComboWindow = true;
|
combatAnimSkipComboWindow = true;
|
||||||
|
attackSoundFired = true; // kein Swoosh-Sound bei Konterparade
|
||||||
playAction(counterAct);
|
playAction(counterAct);
|
||||||
currentAnim = counterAct;
|
currentAnim = counterAct;
|
||||||
}
|
}
|
||||||
@@ -1399,6 +1539,7 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
/** Spielt die Konter-Animations bei erfolgreicher Parade (nur wenn keine Combat-Animation aktiv). */
|
/** Spielt die Konter-Animations bei erfolgreicher Parade (nur wenn keine Combat-Animation aktiv). */
|
||||||
public void triggerParrySuccess() {
|
public void triggerParrySuccess() {
|
||||||
|
if (weaponSoundSystem != null) { weaponSoundSystem.play("counterparade"); }
|
||||||
playCounterAttack();
|
playCounterAttack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1409,21 +1550,36 @@ public class PlayerInputControl {
|
|||||||
forward = backward = left = right = sprint = walk = false;
|
forward = backward = left = right = sprint = walk = false;
|
||||||
if (physicsChar != null) { physicsChar.setWalkDirection(com.jme3.math.Vector3f.ZERO); }
|
if (physicsChar != null) { physicsChar.setWalkDirection(com.jme3.math.Vector3f.ZERO); }
|
||||||
rightMouseHeld = false;
|
rightMouseHeld = false;
|
||||||
|
attackBuffered = false;
|
||||||
blockPhase = BlockPhase.NONE;
|
blockPhase = BlockPhase.NONE;
|
||||||
combatAnimActive = false;
|
combatAnimActive = false;
|
||||||
comboWindowActive = false;
|
comboWindowActive = false;
|
||||||
weaponCombat.phase = WeaponCombatState.Phase.IDLE;
|
weaponCombat.phase = WeaponCombatState.Phase.IDLE;
|
||||||
weaponCombat.parryArmed = false;
|
weaponCombat.parryArmed = false;
|
||||||
|
if (weaponSoundSystem != null) { weaponSoundSystem.play("argh"); }
|
||||||
AnimationAction deathAct = playerRng.nextBoolean()
|
AnimationAction deathAct = playerRng.nextBoolean()
|
||||||
? AnimationAction.SWORD_DEATH : AnimationAction.SWORD_DEATH_2;
|
? AnimationAction.SWORD_DEATH : AnimationAction.SWORD_DEATH_2;
|
||||||
playAction(deathAct);
|
playAction(deathAct);
|
||||||
currentAnim = deathAct;
|
currentAnim = deathAct;
|
||||||
|
// Länge nach playAction() aus AnimComposer lesen (verhindert Loop durch falschen Fallback-Timer)
|
||||||
|
float deathLen = 2.0f;
|
||||||
|
if (animComposer != null) {
|
||||||
|
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||||
|
if (layer != null) {
|
||||||
|
var a = layer.getCurrentAction();
|
||||||
|
if (a != null && a.getLength() > 0.01) {
|
||||||
|
deathLen = (float) a.getLength();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
blockingAnimActive = true;
|
blockingAnimActive = true;
|
||||||
blockingAnimRemaining = 999f;
|
blockingAnimRemaining = Math.max(0.1f, deathLen - 0.12f); // 120ms vor Clip-Ende: kein Loop
|
||||||
blockingAnimTotal = 999f;
|
blockingAnimTotal = blockingAnimRemaining;
|
||||||
blockingAnimCallback = null;
|
blockingAnimCallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPlayerDead() { return playerDead; }
|
||||||
|
|
||||||
public boolean isBlocking() {
|
public boolean isBlocking() {
|
||||||
return blockPhase == BlockPhase.ENTERING
|
return blockPhase == BlockPhase.ENTERING
|
||||||
|| blockPhase == BlockPhase.HOLDING
|
|| blockPhase == BlockPhase.HOLDING
|
||||||
@@ -1439,7 +1595,7 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
/** Wird aufgerufen wenn ein blockierter Treffer landet. Kostet 30 Ausdauer; unter 0 → Block bricht. */
|
/** Wird aufgerufen wenn ein blockierter Treffer landet. Kostet 30 Ausdauer; unter 0 → Block bricht. */
|
||||||
public void triggerBlockImpact() {
|
public void triggerBlockImpact() {
|
||||||
if (blockPhase != BlockPhase.HOLDING || drawnWeaponType == null) return;
|
if ((blockPhase != BlockPhase.HOLDING && blockPhase != BlockPhase.ENTERING) || drawnWeaponType == null) return;
|
||||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impactblock"); }
|
if (weaponSoundSystem != null) { weaponSoundSystem.play("impactblock"); }
|
||||||
if (mainCharacter != null && mainCharacter.getMaxStamina() > 0) {
|
if (mainCharacter != null && mainCharacter.getMaxStamina() > 0) {
|
||||||
if (mainCharacter.getCurrentStamina() < STAMINA_BLOCK_HIT_COST) {
|
if (mainCharacter.getCurrentStamina() < STAMINA_BLOCK_HIT_COST) {
|
||||||
@@ -1500,10 +1656,10 @@ public class PlayerInputControl {
|
|||||||
&& !turnActive && !lockedInPlace && !combatAnimActive) {
|
&& !turnActive && !lockedInPlace && !combatAnimActive) {
|
||||||
sneakPhase = SneakPhase.DOWN;
|
sneakPhase = SneakPhase.DOWN;
|
||||||
boolean moving = forward || backward || left || right;
|
boolean moving = forward || backward || left || right;
|
||||||
sneakTransitionRemaining = resolveClipLength(AnimationAction.DUCK, 0.5f) * (moving ? 0.5f : 1f);
|
sneakTransitionRemaining = resolveClipLength(AnimationAction.STAND_TO_CROUCH, 0.5f) * (moving ? 0.5f : 1f);
|
||||||
nextTransitionLength = 0.1f;
|
nextTransitionLength = 0.1f;
|
||||||
playAction(AnimationAction.DUCK);
|
playAction(AnimationAction.STAND_TO_CROUCH);
|
||||||
currentAnim = AnimationAction.DUCK;
|
currentAnim = AnimationAction.STAND_TO_CROUCH;
|
||||||
} else if (!pressed && sneakPhase == SneakPhase.ACTIVE) {
|
} else if (!pressed && sneakPhase == SneakPhase.ACTIVE) {
|
||||||
startSneakUp();
|
startSneakUp();
|
||||||
}
|
}
|
||||||
@@ -1519,6 +1675,7 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void triggerRollFromInput() {
|
private void triggerRollFromInput() {
|
||||||
|
if (!weaponDrawn) return;
|
||||||
if (rollActive || pickupActive || blockingAnimActive || turnActive
|
if (rollActive || pickupActive || blockingAnimActive || turnActive
|
||||||
|| lockedInPlace || combatAnimActive || isBlocking()) return;
|
|| lockedInPlace || combatAnimActive || isBlocking()) return;
|
||||||
cam.getDirection(scratchCamDir).setY(0).normalizeLocal();
|
cam.getDirection(scratchCamDir).setY(0).normalizeLocal();
|
||||||
@@ -1695,28 +1852,20 @@ public class PlayerInputControl {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AnimationAction getMeleeCounterAttackAction(ItemSubCategory s) {
|
private static AnimationAction getMeleeParadeAction(ItemSubCategory s) {
|
||||||
return switch (s) {
|
return switch (s) {
|
||||||
case AXE -> AnimationAction.AXE_COUNTERATTACK;
|
case AXE -> AnimationAction.AXE_PARADE;
|
||||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_COUNTERATTACK;
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_PARADE;
|
||||||
default -> AnimationAction.SWORD_COUNTERATTACK;
|
default -> AnimationAction.SWORD_PARADE;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) {
|
public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) {
|
||||||
return switch (s) {
|
return SwordShieldCombat.resolveBlockImpactAction(s);
|
||||||
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
|
|
||||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
|
|
||||||
default -> AnimationAction.SWORD_BLOCK_IMPACT;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) {
|
public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) {
|
||||||
return switch (s) {
|
return SwordShieldCombat.resolveIdleImpactAction(s);
|
||||||
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
|
||||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
|
|
||||||
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fireWeaponCallback(float critMultiplier) {
|
private void fireWeaponCallback(float critMultiplier) {
|
||||||
@@ -1730,8 +1879,23 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
int base = (mainCharacter != null && mainCharacter.getEquippedMeleeWeapon() != null)
|
int base = (mainCharacter != null && mainCharacter.getEquippedMeleeWeapon() != null)
|
||||||
? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0;
|
? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0;
|
||||||
Vector3f grip = hn.getWorldTranslation().clone();
|
// Vorwärtsrichtung des Charakters (Spieler schaut immer Richtung Gegner)
|
||||||
Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal();
|
Vector3f bladeDir = (visual != null)
|
||||||
|
? visual.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Z).normalizeLocal()
|
||||||
|
: cam.getDirection().clone().setY(0).normalizeLocal();
|
||||||
|
// physicsChar.getPhysicsLocation() als XZ-Ursprung: immer aktuell, kein staler Cache.
|
||||||
|
// HandY behalten für korrekte Trefferhöhe; 0.5m forward = gleiche Reichweite wie Attack 1.
|
||||||
|
Vector3f grip;
|
||||||
|
if (physicsChar != null) {
|
||||||
|
com.jme3.math.Vector3f charPos = physicsChar.getPhysicsLocation();
|
||||||
|
grip = new Vector3f(
|
||||||
|
charPos.x + bladeDir.x * 0.5f,
|
||||||
|
hn.getWorldTranslation().y,
|
||||||
|
charPos.z + bladeDir.z * 0.5f
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
grip = hn.getWorldTranslation().clone();
|
||||||
|
}
|
||||||
for (MeleeAttackCallback cb : meleeCallbacks) {
|
for (MeleeAttackCallback cb : meleeCallbacks) {
|
||||||
cb.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier);
|
cb.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier);
|
||||||
}
|
}
|
||||||
@@ -1749,6 +1913,47 @@ public class PlayerInputControl {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Normalisierter Vorwärtsvektor Richtung nächstem Gegner, Fallback: Blickrichtung. */
|
||||||
|
private Vector3f computeAttackDir() {
|
||||||
|
Vector3f fwd = null;
|
||||||
|
if (nearestAttackingNpcPos != null && physicsChar != null) {
|
||||||
|
Vector3f myPos = physicsChar.getPhysicsLocation();
|
||||||
|
fwd = new Vector3f(nearestAttackingNpcPos.x - myPos.x, 0f, nearestAttackingNpcPos.z - myPos.z);
|
||||||
|
if (fwd.lengthSquared() < 0.001f) { fwd = null; }
|
||||||
|
}
|
||||||
|
if (fwd == null) {
|
||||||
|
fwd = (visual != null)
|
||||||
|
? visual.getWorldRotation().mult(new Vector3f(0f, 0f, 1f))
|
||||||
|
: cam.getDirection().clone();
|
||||||
|
fwd.y = 0f;
|
||||||
|
}
|
||||||
|
return fwd.lengthSquared() > 0.001f ? fwd.normalizeLocal() : fwd.set(0f, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Lunge-Geschwindigkeit so, dass der Abstand zum Gegner
|
||||||
|
* während des Lungesfensters auf ~1.7m (sicherer Treffbereich) schließt.
|
||||||
|
* Ohne bekannten Gegner: minimaler Schritt nach vorn.
|
||||||
|
*/
|
||||||
|
private Vector3f computeLungeVelocity(float animDur) {
|
||||||
|
Vector3f fwd = computeAttackDir();
|
||||||
|
float IDEAL_DIST = 1.7f;
|
||||||
|
if (nearestAttackingNpcPos != null && physicsChar != null) {
|
||||||
|
Vector3f myPos2 = physicsChar.getPhysicsLocation();
|
||||||
|
float dx2 = nearestAttackingNpcPos.x - myPos2.x;
|
||||||
|
float dz2 = nearestAttackingNpcPos.z - myPos2.z;
|
||||||
|
float dist = (float) Math.sqrt(dx2 * dx2 + dz2 * dz2);
|
||||||
|
float gap = dist - IDEAL_DIST;
|
||||||
|
if (gap > 0.05f && gap < 2.5f) {
|
||||||
|
float lungeWindowDur = animDur * (LUNGE_FRAC_END - LUNGE_FRAC_START);
|
||||||
|
float neededMps = Math.min(gap / lungeWindowDur, CharacterNavigator.RUN_SPEED_MPS * 0.45f);
|
||||||
|
float speed = neededMps / CharacterNavigator.RUN_SPEED_MPS * MOVE_SPEED;
|
||||||
|
return fwd.multLocal(speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fwd.multLocal(MOVE_SPEED * 0.04f);
|
||||||
|
}
|
||||||
|
|
||||||
/** Startet sofort die schwere-Angriff-Animation beim Drücken der Taste. */
|
/** Startet sofort die schwere-Angriff-Animation beim Drücken der Taste. */
|
||||||
private void startHeavyCharge() {
|
private void startHeavyCharge() {
|
||||||
if (drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
if (drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
||||||
@@ -1768,15 +1973,7 @@ public class PlayerInputControl {
|
|||||||
playAction(action);
|
playAction(action);
|
||||||
currentAnim = action;
|
currentAnim = action;
|
||||||
|
|
||||||
// Blickrichtung des Chars in Weltkoordinaten
|
heavyLungeVel = computeLungeVelocity(dur);
|
||||||
Vector3f fwd = (visual != null)
|
|
||||||
? visual.getWorldRotation().mult(new Vector3f(0f, 0f, 1f))
|
|
||||||
: cam.getDirection().clone();
|
|
||||||
fwd.y = 0f;
|
|
||||||
if (fwd.lengthSquared() > 0.001f) fwd.normalizeLocal();
|
|
||||||
heavyLungeVel = fwd.mult(MOVE_SPEED * 0.25f);
|
|
||||||
|
|
||||||
// Lunge nur während der Sprungphase: combatAnimRemaining-Fenster vorausberechnen
|
|
||||||
heavyLungeStartRemaining = dur * (1f - LUNGE_FRAC_START);
|
heavyLungeStartRemaining = dur * (1f - LUNGE_FRAC_START);
|
||||||
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
||||||
log.info("[Combat] Schwerer Angriff Windup gestartet.");
|
log.info("[Combat] Schwerer Angriff Windup gestartet.");
|
||||||
@@ -1787,15 +1984,22 @@ public class PlayerInputControl {
|
|||||||
if (!weaponDrawn || drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
if (!weaponDrawn || drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Ladung kann fehlen wenn Release während Kombo kam (Race: Release vor controlUpdate-Frame)
|
||||||
|
if (!combatAnimActive) { startHeavyCharge(); }
|
||||||
|
if (!combatAnimActive) { return; } // startHeavyCharge hat versagt (kein Stamina etc.)
|
||||||
currentAttackId = ++attackIdCounter;
|
currentAttackId = ++attackIdCounter;
|
||||||
currentComboStep = -1;
|
currentComboStep = -1;
|
||||||
attackSoundFired = false;
|
attackSoundFired = false;
|
||||||
if (weaponAttach != null && weaponAttach.getHandNode() != null)
|
hitWindowActive = true;
|
||||||
|
hitWindowCritMult = computeHeavyMultiplier();
|
||||||
|
for (MeleeAttackCallback cb : meleeCallbacks) { cb.onAttackStart(currentAttackId, true); }
|
||||||
|
hitWindowStart = HIT_WINDOW_START;
|
||||||
|
hitWindowEnd = 1.0f; // bis Animationsende gültig – Release-Timing unkritisch
|
||||||
|
if (weaponAttach != null && weaponAttach.getHandNode() != null) {
|
||||||
lastGripPos.set(weaponAttach.getHandNode().getWorldTranslation());
|
lastGripPos.set(weaponAttach.getHandNode().getWorldTranslation());
|
||||||
float mult = computeHeavyMultiplier();
|
}
|
||||||
fireWeaponCallback(mult);
|
|
||||||
log.info("[Combat] Schwerer Angriff ausgelöst | Haltezeit={}s Multiplikator={}x",
|
log.info("[Combat] Schwerer Angriff ausgelöst | Haltezeit={}s Multiplikator={}x",
|
||||||
String.format("%.2f", attackHoldTimer), String.format("%.2f", mult));
|
String.format("%.2f", attackHoldTimer), String.format("%.2f", hitWindowCritMult));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
|
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ public class WorldScene extends BaseAppState {
|
|||||||
public void setPaused(boolean paused) {
|
public void setPaused(boolean paused) {
|
||||||
if (playerInput != null) playerInput.setPaused(paused);
|
if (playerInput != null) playerInput.setPaused(paused);
|
||||||
if (thirdPersonCam != null) thirdPersonCam.setPaused(paused);
|
if (thirdPersonCam != null) thirdPersonCam.setPaused(paused);
|
||||||
|
WorldNpcsState npcsState = app != null ? app.getStateManager().getState(WorldNpcsState.class) : null;
|
||||||
|
if (npcsState != null) npcsState.setPaused(paused);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.jme3.scene.Node;
|
|||||||
import com.jme3.scene.Spatial;
|
import com.jme3.scene.Spatial;
|
||||||
import de.blight.game.config.KeyBindings;
|
import de.blight.game.config.KeyBindings;
|
||||||
import de.blight.game.config.KeyNames;
|
import de.blight.game.config.KeyNames;
|
||||||
|
import de.blight.game.config.MenuScreen;
|
||||||
|
import de.blight.game.config.OverlayState;
|
||||||
import de.blight.lang.TextResolver;
|
import de.blight.lang.TextResolver;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -85,8 +87,8 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
// Label ausblenden wenn Dialog aktiv
|
if ((dialogHud != null && dialogHud.isActive())
|
||||||
if (dialogHud != null && dialogHud.isActive()) {
|
|| OverlayState.isAnyOpen() || MenuScreen.isAnyOpen()) {
|
||||||
labelText.setCullHint(Spatial.CullHint.Always);
|
labelText.setCullHint(Spatial.CullHint.Always);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
252
blight-game/src/main/java/de/blight/game/state/LootHudState.java
Normal file
252
blight-game/src/main/java/de/blight/game/state/LootHudState.java
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.font.BitmapText;
|
||||||
|
import com.jme3.input.MouseInput;
|
||||||
|
import com.jme3.input.controls.ActionListener;
|
||||||
|
import com.jme3.input.controls.AnalogListener;
|
||||||
|
import com.jme3.input.controls.MouseAxisTrigger;
|
||||||
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import de.blight.common.model.Item;
|
||||||
|
import de.blight.common.model.Lootable;
|
||||||
|
import de.blight.game.config.OverlayState;
|
||||||
|
import de.blight.game.config.UiTheme;
|
||||||
|
import de.blight.lang.TextResolver;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loot-HUD: öffnet sich wenn der Spieler eine Leiche (NPC oder später Tier) untersucht.
|
||||||
|
* Zeigt die verbleibenden Items als scrollbare Liste; Doppelklick = einzeln nehmen,
|
||||||
|
* "Alles nehmen"-Button = alles auf einmal.
|
||||||
|
*/
|
||||||
|
public class LootHudState extends OverlayState {
|
||||||
|
|
||||||
|
private static final float ROW_H = 34f;
|
||||||
|
private static final float BTN_H = 36f;
|
||||||
|
private static final float BTN_GAP = 10f;
|
||||||
|
private static final long DBLCLICK_MS = 350L;
|
||||||
|
|
||||||
|
private static final String MAP_CLICK = "_LootClick";
|
||||||
|
private static final String MAP_SCROLL_U = "_LootScrollU";
|
||||||
|
private static final String MAP_SCROLL_D = "_LootScrollD";
|
||||||
|
|
||||||
|
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Lootable source;
|
||||||
|
private Consumer<Item> onItemTaken;
|
||||||
|
private Runnable onClosed;
|
||||||
|
|
||||||
|
/** Lokale Kopie der angezeigten Items (wird beim Nehmen verkleinert). */
|
||||||
|
private List<Item> displayItems = new ArrayList<>();
|
||||||
|
|
||||||
|
private Node panel;
|
||||||
|
private Node listNode;
|
||||||
|
private float scrollY = 0f;
|
||||||
|
private float maxScrollY = 0f;
|
||||||
|
private float listAreaH;
|
||||||
|
|
||||||
|
// Double-click-Erkennung
|
||||||
|
private int lastClickIdx = -1;
|
||||||
|
private long lastClickTime = -1L;
|
||||||
|
|
||||||
|
// Button-Bereich [x, y, w, h] in virtuellen Koordinaten
|
||||||
|
private float[] btnBounds;
|
||||||
|
|
||||||
|
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet das Loot-HUD für das angegebene {@link Lootable}.
|
||||||
|
*
|
||||||
|
* @param source Loot-Quelle (NPC-Leiche, Tier, …)
|
||||||
|
* @param onItemTaken Callback: wird für jedes genommene Item aufgerufen
|
||||||
|
*/
|
||||||
|
public void open(Lootable source, Consumer<Item> onItemTaken) {
|
||||||
|
open(source, onItemTaken, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Lootable source, Consumer<Item> onItemTaken, Runnable onClosed) {
|
||||||
|
this.source = source;
|
||||||
|
this.onItemTaken = onItemTaken;
|
||||||
|
this.onClosed = onClosed;
|
||||||
|
this.displayItems = source.getLootItems() != null
|
||||||
|
? new ArrayList<>(source.getLootItems()) : new ArrayList<>();
|
||||||
|
setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── OverlayState-Implementierung ──────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getTitle() {
|
||||||
|
if (source == null) { return "Beute"; }
|
||||||
|
return TextResolver.get().resolveId(source.getLootTitleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initOverlayKeys() {
|
||||||
|
app.getInputManager().addMapping(MAP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||||
|
app.getInputManager().addMapping(MAP_SCROLL_U, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||||
|
app.getInputManager().addMapping(MAP_SCROLL_D, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanupOverlayKeys() {
|
||||||
|
app.getInputManager().deleteMapping(MAP_CLICK);
|
||||||
|
app.getInputManager().deleteMapping(MAP_SCROLL_U);
|
||||||
|
app.getInputManager().deleteMapping(MAP_SCROLL_D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnable() {
|
||||||
|
scrollY = 0f;
|
||||||
|
lastClickIdx = -1;
|
||||||
|
lastClickTime = -1L;
|
||||||
|
super.onEnable();
|
||||||
|
app.getInputManager().addListener(clickListener, MAP_CLICK);
|
||||||
|
app.getInputManager().addListener(scrollListener, MAP_SCROLL_U, MAP_SCROLL_D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDisable() {
|
||||||
|
app.getInputManager().removeListener(clickListener);
|
||||||
|
app.getInputManager().removeListener(scrollListener);
|
||||||
|
panel = null;
|
||||||
|
listNode = null;
|
||||||
|
super.onDisable();
|
||||||
|
if (onClosed != null) { Runnable cb = onClosed; onClosed = null; cb.run(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application app) {
|
||||||
|
super.cleanup(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void buildContent(Node parentPanel, float scale) {
|
||||||
|
this.panel = parentPanel;
|
||||||
|
|
||||||
|
listAreaH = CH - BTN_H - BTN_GAP;
|
||||||
|
|
||||||
|
// ── "Alles nehmen"-Button ─────────────────────────────────────────────
|
||||||
|
float btnW = 180f;
|
||||||
|
float btnX = CX + CW - btnW;
|
||||||
|
float btnY = CY;
|
||||||
|
btnBounds = new float[]{ btnX, btnY, btnW, BTN_H };
|
||||||
|
addQuad(parentPanel, btnX, btnY, btnW, BTN_H, UiTheme.COL_HDR, -18f);
|
||||||
|
|
||||||
|
String takeAllTxt = TextResolver.get().resolveId("ui.loot.take_all");
|
||||||
|
BitmapText btnLbl = crispText(takeAllTxt, UiTheme.FONT_BODY, UiTheme.COL_WHITE);
|
||||||
|
float btnTxtW = btnLbl.getLineWidth() * 0.5f;
|
||||||
|
btnLbl.setLocalTranslation(btnX + (btnW - btnTxtW) * 0.5f, btnY + BTN_H - 8f, -17f);
|
||||||
|
parentPanel.attachChild(btnLbl);
|
||||||
|
|
||||||
|
// ── Item-Liste ────────────────────────────────────────────────────────
|
||||||
|
listNode = new Node("lootList");
|
||||||
|
parentPanel.attachChild(listNode);
|
||||||
|
rebuildList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Item-Liste aufbauen ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void rebuildList() {
|
||||||
|
if (listNode == null) { return; }
|
||||||
|
listNode.detachAllChildren();
|
||||||
|
|
||||||
|
float listTop = CY + BTN_H + BTN_GAP + listAreaH;
|
||||||
|
|
||||||
|
maxScrollY = Math.max(0f, displayItems.size() * ROW_H - listAreaH);
|
||||||
|
scrollY = Math.max(0f, Math.min(maxScrollY, scrollY));
|
||||||
|
|
||||||
|
if (displayItems.isEmpty()) {
|
||||||
|
BitmapText empty = crispText(
|
||||||
|
TextResolver.get().resolveId("ui.loot.empty"),
|
||||||
|
UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||||
|
empty.setLocalTranslation(CX + 8f, CY + BTN_H + BTN_GAP + listAreaH * 0.5f + 8f, -17f);
|
||||||
|
listNode.attachChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < displayItems.size(); i++) {
|
||||||
|
Item item = displayItems.get(i);
|
||||||
|
float rowBottomY = listTop - (i + 1) * ROW_H + scrollY;
|
||||||
|
|
||||||
|
if (rowBottomY + ROW_H < CY + BTN_H + BTN_GAP) { continue; }
|
||||||
|
if (rowBottomY > listTop) { continue; }
|
||||||
|
|
||||||
|
addQuad(listNode, CX, rowBottomY, CW, ROW_H - 2f,
|
||||||
|
i % 2 == 0 ? UiTheme.COL_ITEM_BG : UiTheme.COL_DETAIL_BG, -18f);
|
||||||
|
|
||||||
|
BitmapText lbl = crispText(item.getDisplayText(), UiTheme.FONT_BODY, UiTheme.COL_WHITE);
|
||||||
|
lbl.setLocalTranslation(CX + 8f, rowBottomY + ROW_H - 6f, -17f);
|
||||||
|
listNode.attachChild(lbl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Interaktion ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleClick(float[] v) {
|
||||||
|
// Button: "Alles nehmen"
|
||||||
|
if (btnBounds != null
|
||||||
|
&& v[0] >= btnBounds[0] && v[0] <= btnBounds[0] + btnBounds[2]
|
||||||
|
&& v[1] >= btnBounds[1] && v[1] <= btnBounds[1] + btnBounds[3]) {
|
||||||
|
takeAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item-Zeilen: Doppelklick
|
||||||
|
float listTop = CY + BTN_H + BTN_GAP + listAreaH;
|
||||||
|
for (int i = 0; i < displayItems.size(); i++) {
|
||||||
|
float rowBottomY = listTop - (i + 1) * ROW_H + scrollY;
|
||||||
|
if (v[0] >= CX && v[0] <= CX + CW
|
||||||
|
&& v[1] >= rowBottomY && v[1] <= rowBottomY + ROW_H) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (lastClickIdx == i && (now - lastClickTime) <= DBLCLICK_MS) {
|
||||||
|
takeItem(i);
|
||||||
|
lastClickIdx = -1;
|
||||||
|
lastClickTime = -1L;
|
||||||
|
} else {
|
||||||
|
lastClickIdx = i;
|
||||||
|
lastClickTime = now;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void takeItem(int idx) {
|
||||||
|
if (idx < 0 || idx >= displayItems.size()) { return; }
|
||||||
|
Item item = displayItems.remove(idx);
|
||||||
|
if (source != null) { source.getLootItems().remove(item); }
|
||||||
|
if (onItemTaken != null) { onItemTaken.accept(item); }
|
||||||
|
rebuildList();
|
||||||
|
if (displayItems.isEmpty()) { setEnabled(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void takeAll() {
|
||||||
|
List<Item> taken = new ArrayList<>(displayItems);
|
||||||
|
displayItems.clear();
|
||||||
|
for (Item item : taken) {
|
||||||
|
if (source != null) { source.getLootItems().remove(item); }
|
||||||
|
if (onItemTaken != null) { onItemTaken.accept(item); }
|
||||||
|
}
|
||||||
|
setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Listener ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private final ActionListener clickListener = (name, pressed, tpf) -> {
|
||||||
|
if (!pressed || panel == null) { return; }
|
||||||
|
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||||
|
handleClick(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
private final AnalogListener scrollListener = (name, value, tpf) -> {
|
||||||
|
float step = 50f * value;
|
||||||
|
if (MAP_SCROLL_U.equals(name)) { scrollY = Math.max(0f, scrollY - step); }
|
||||||
|
else { scrollY = Math.min(maxScrollY, scrollY + step); }
|
||||||
|
rebuildList();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,7 +11,10 @@ import de.blight.common.model.MainCharacter;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import de.blight.common.model.Item;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
@@ -84,12 +87,55 @@ public class SaveGameState extends BaseAppState {
|
|||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Meldet einen besiegten Gegner (für zukünftige Implementierung). */
|
/** Meldet einen besiegten Gegner. */
|
||||||
public void reportEnemyDefeated(String enemyId) {
|
public void reportEnemyDefeated(String enemyId) {
|
||||||
save.world.defeatedEnemies.add(enemyId);
|
save.world.defeatedEnemies.add(enemyId);
|
||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Speichert einen getöteten NPC als Leiche mit Position und Loot-Items. */
|
||||||
|
public void reportNpcKilled(String npcId, float x, float y, float z, float rotY, List<Item> items) {
|
||||||
|
SaveGame.DeadNpcSave dns = new SaveGame.DeadNpcSave();
|
||||||
|
dns.x = x; dns.y = y; dns.z = z; dns.rotY = rotY;
|
||||||
|
if (items != null) {
|
||||||
|
for (Item item : items) {
|
||||||
|
if (item.getItemId() != null) { dns.remainingItemIds.add(item.getItemId()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dns.fullyLooted = dns.remainingItemIds.isEmpty();
|
||||||
|
save.world.deadNpcs.put(npcId, dns);
|
||||||
|
save.world.defeatedEnemies.add(npcId);
|
||||||
|
persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entfernt ein Item aus dem Loot-Inventar der Leiche; setzt fullyLooted wenn leer. */
|
||||||
|
public void reportNpcItemTaken(String npcId, String itemId) {
|
||||||
|
SaveGame.DeadNpcSave dns = save.world.deadNpcs.get(npcId);
|
||||||
|
if (dns == null) { return; }
|
||||||
|
dns.remainingItemIds.remove(itemId);
|
||||||
|
if (dns.remainingItemIds.isEmpty()) { dns.fullyLooted = true; }
|
||||||
|
persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Markiert die Leiche als vollständig gelooted (z.B. bei leeren NPCs nach Interact). */
|
||||||
|
public void reportNpcFullyLooted(String npcId) {
|
||||||
|
SaveGame.DeadNpcSave dns = save.world.deadNpcs.get(npcId);
|
||||||
|
if (dns == null) { return; }
|
||||||
|
dns.fullyLooted = true;
|
||||||
|
persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entfernt eine Leiche dauerhaft aus dem Save (nach vollständigem Loot + Entladen). */
|
||||||
|
public void removeDeadNpcRecord(String npcId) {
|
||||||
|
save.world.deadNpcs.remove(npcId);
|
||||||
|
persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle gespeicherten Leichen zurück. */
|
||||||
|
public Map<String, SaveGame.DeadNpcSave> getDeadNpcs() {
|
||||||
|
return save.world.deadNpcs;
|
||||||
|
}
|
||||||
|
|
||||||
/** Verwertet den aktuellen Spielstand und startet ein neues Spiel. */
|
/** Verwertet den aktuellen Spielstand und startet ein neues Spiel. */
|
||||||
public void resetForNewGame() {
|
public void resetForNewGame() {
|
||||||
try { java.nio.file.Files.deleteIfExists(de.blight.common.SaveGameIO.getSavePath()); }
|
try { java.nio.file.Files.deleteIfExists(de.blight.common.SaveGameIO.getSavePath()); }
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import de.blight.common.PlacedModelIO;
|
|||||||
import de.blight.common.model.CombatDummy;
|
import de.blight.common.model.CombatDummy;
|
||||||
import de.blight.common.model.CombatDummyIO;
|
import de.blight.common.model.CombatDummyIO;
|
||||||
import de.blight.game.animation.AnimationAction;
|
import de.blight.game.animation.AnimationAction;
|
||||||
|
import de.blight.game.combat.CombatStyleBase;
|
||||||
import de.blight.game.combat.DamageCalculator;
|
import de.blight.game.combat.DamageCalculator;
|
||||||
import de.blight.game.combat.MeleeAttackCallback;
|
import de.blight.game.combat.MeleeAttackCallback;
|
||||||
import de.blight.game.config.KeyBindings;
|
import de.blight.game.config.KeyBindings;
|
||||||
@@ -367,15 +368,14 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
|
|||||||
lastAttackId = attackId;
|
lastAttackId = attackId;
|
||||||
hitThisAttack.clear();
|
hitThisAttack.clear();
|
||||||
}
|
}
|
||||||
|
float radSq = CombatStyleBase.HIT_RADIUS * CombatStyleBase.HIT_RADIUS;
|
||||||
|
Vector3f bladeTip = gripPos.add(bladeDir.mult(bladeLength));
|
||||||
for (DummyRuntime dr : dummies) {
|
for (DummyRuntime dr : dummies) {
|
||||||
if (dr.fallen || hitThisAttack.contains(dr.id)) {
|
if (dr.fallen || hitThisAttack.contains(dr.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Distanz vom Griff zum Dummy-Zentrum (Torso-Höhe ~0.9m)
|
Vector3f dummyCenter = new Vector3f(dr.worldX, dr.worldY + 0.9f, dr.worldZ);
|
||||||
float dx = dr.worldX - gripPos.x;
|
if (CombatStyleBase.segmentDistSq(dummyCenter, gripPos, bladeTip) <= radSq) {
|
||||||
float dy = (dr.worldY + 0.9f) - gripPos.y;
|
|
||||||
float dz = dr.worldZ - gripPos.z;
|
|
||||||
if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) {
|
|
||||||
hitThisAttack.add(dr.id);
|
hitThisAttack.add(dr.id);
|
||||||
applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier), critMultiplier > 1.0f);
|
applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier), critMultiplier > 1.0f);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,9 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- Kampfsystem: NPC-Aktionen, Schaden, Block/Konterparade-Entscheidungen -->
|
||||||
|
<logger name="de.blight.game.state.WorldNpcsState" level="DEBUG"/>
|
||||||
|
|
||||||
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
||||||
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
||||||
<!-- Minimap: Modell-Build und Render-Zyklen auf DEBUG -->
|
<!-- Minimap: Modell-Build und Render-Zyklen auf DEBUG -->
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_15.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_15.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_16.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_16.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_15.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_15.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_16.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_16.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_0_16.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_0_16.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_16.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_16.blvc
Normal file
Binary file not shown.
Reference in New Issue
Block a user