Mehrere Probleme beim Thema Kämpfen, Schleichen und Looten behoben
This commit is contained in:
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.
@@ -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();
|
||||||
|
}
|
||||||
@@ -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>"
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
while (history.size() > HISTORY) history.pollLast();
|
for (String line : lines) {
|
||||||
|
history.addFirst(line);
|
||||||
|
while (history.size() > HISTORY) history.pollLast();
|
||||||
|
}
|
||||||
refreshHistory();
|
refreshHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ 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));
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
|||||||
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;
|
||||||
@@ -1201,6 +1207,9 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
|||||||
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;
|
||||||
@@ -1647,10 +1656,10 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
|||||||
&& !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();
|
||||||
}
|
}
|
||||||
@@ -1666,6 +1675,7 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
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()); }
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import de.blight.game.config.MenuCanvas;
|
|||||||
import de.blight.game.config.NinePatch;
|
import de.blight.game.config.NinePatch;
|
||||||
import de.blight.common.PlacedModel;
|
import de.blight.common.PlacedModel;
|
||||||
import de.blight.common.PlacedModelIO;
|
import de.blight.common.PlacedModelIO;
|
||||||
|
import de.blight.common.SaveGame;
|
||||||
import de.blight.common.SaveGameIO;
|
import de.blight.common.SaveGameIO;
|
||||||
import de.blight.common.model.*;
|
import de.blight.common.model.*;
|
||||||
import de.blight.common.model.trigger.ChangeRoutineTrigger;
|
import de.blight.common.model.trigger.ChangeRoutineTrigger;
|
||||||
@@ -154,6 +155,9 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
Node hpBarNode = null;
|
Node hpBarNode = null;
|
||||||
Geometry hpBarFg = null;
|
Geometry hpBarFg = null;
|
||||||
|
|
||||||
|
// ── Hüft-Knochen Attachment (lazy) ───────────────────────────────────
|
||||||
|
com.jme3.scene.Node hipsNode = null;
|
||||||
|
|
||||||
// ── Physik-Kapsel (Kollision) ─────────────────────────────────────────
|
// ── Physik-Kapsel (Kollision) ─────────────────────────────────────────
|
||||||
RigidBodyControl rbc = null;
|
RigidBodyControl rbc = null;
|
||||||
|
|
||||||
@@ -166,6 +170,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
float dodgeDirX = 0f;
|
float dodgeDirX = 0f;
|
||||||
float dodgeDirZ = 0f;
|
float dodgeDirZ = 0f;
|
||||||
float knockbackTimer = 0f;
|
float knockbackTimer = 0f;
|
||||||
|
List<de.blight.common.model.Item> lootItems = null; // null = lebend; leer = vollst. gelooted
|
||||||
boolean paradeFromParry = false; // true = Konterparade (applyParriedImpact wurde gespielt)
|
boolean paradeFromParry = false; // true = Konterparade (applyParriedImpact wurde gespielt)
|
||||||
float blockGraceTimer = 0f; // > 0 solange NPC blockt oder gerade aus Block zurückkehrt
|
float blockGraceTimer = 0f; // > 0 solange NPC blockt oder gerade aus Block zurückkehrt
|
||||||
int konterParadeAttackId = -1; // attackId für die der NPC bereits die Parade gestartet hat
|
int konterParadeAttackId = -1; // attackId für die der NPC bereits die Parade gestartet hat
|
||||||
@@ -197,6 +202,16 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
private float checkTimer = CHECK_INTERVAL; // sofortiger erster Check
|
private float checkTimer = CHECK_INTERVAL; // sofortiger erster Check
|
||||||
private int lastHour = -1;
|
private int lastHour = -1;
|
||||||
|
|
||||||
|
// ── Loot-Annäherung ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private enum LootApproach { WALKING, CROUCHING, STANDING_UP }
|
||||||
|
private LootApproach lootApproachPhase = null;
|
||||||
|
private SpawnedNpc pendingLootNpc = null;
|
||||||
|
private Vector3f lootApproachTarget = null;
|
||||||
|
|
||||||
|
private static final float LOOT_APPROACH_DIST = 1.0f; // m vom NPC-Zentrum
|
||||||
|
private static final float LOOT_ARRIVE_THRESH = 0.25f; // innerhalb = angekommen
|
||||||
|
|
||||||
// ── Hover + Dialog ────────────────────────────────────────────────────────
|
// ── Hover + Dialog ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private int hoveredIdx = -1;
|
private int hoveredIdx = -1;
|
||||||
@@ -322,6 +337,9 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
private int lastAttackId = -1;
|
private int lastAttackId = -1;
|
||||||
private final Set<String> hitThisAttack = new HashSet<>();
|
private final Set<String> hitThisAttack = new HashSet<>();
|
||||||
|
|
||||||
|
// ── Loot ──────────────────────────────────────────────────────────────────
|
||||||
|
private LootHudState lootHud;
|
||||||
|
|
||||||
// ── Feind-HUD ─────────────────────────────────────────────────────────────
|
// ── Feind-HUD ─────────────────────────────────────────────────────────────
|
||||||
private Node npcEnemyHudNode;
|
private Node npcEnemyHudNode;
|
||||||
private Geometry npcEnemyHpFill;
|
private Geometry npcEnemyHpFill;
|
||||||
@@ -389,6 +407,10 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
bulletAppState = app.getStateManager().getState(BulletAppState.class);
|
bulletAppState = app.getStateManager().getState(BulletAppState.class);
|
||||||
playerInput.addMeleeAttackCallback(this);
|
playerInput.addMeleeAttackCallback(this);
|
||||||
buildNpcEnemyHud();
|
buildNpcEnemyHud();
|
||||||
|
|
||||||
|
lootHud = new LootHudState();
|
||||||
|
lootHud.setEnabled(false);
|
||||||
|
getStateManager().attach(lootHud);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -452,6 +474,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
updateNpcRevert(tpf);
|
updateNpcRevert(tpf);
|
||||||
updateAggroBehaviors(tpf);
|
updateAggroBehaviors(tpf);
|
||||||
updateNpcCombat(tpf);
|
updateNpcCombat(tpf);
|
||||||
|
updateLootApproach(tpf);
|
||||||
updateNpcEnemyHud();
|
updateNpcEnemyHud();
|
||||||
updateSpeechBubble(tpf);
|
updateSpeechBubble(tpf);
|
||||||
if (dialogActive && dialogHeadTween != null) {
|
if (dialogActive && dialogHeadTween != null) {
|
||||||
@@ -467,7 +490,20 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
// Bestehende NPCs prüfen: Position aktualisieren oder entladen
|
// Bestehende NPCs prüfen: Position aktualisieren oder entladen
|
||||||
toRemoveBuf.clear();
|
toRemoveBuf.clear();
|
||||||
for (SpawnedNpc s : spawned) {
|
for (SpawnedNpc s : spawned) {
|
||||||
if (s.knockedOut) { continue; } // Tote NPCs nicht teleportieren oder überschreiben
|
if (s.knockedOut) {
|
||||||
|
// Leichen: nur entladen wenn außer Reichweite
|
||||||
|
float dist = dist2d(playerPos, s.visual().getWorldTranslation());
|
||||||
|
if (dist > UNLOAD_RADIUS) {
|
||||||
|
if (s.lootItems != null && s.lootItems.isEmpty()) {
|
||||||
|
// Vollständig gelooted → dauerhaft aus Save entfernen
|
||||||
|
SaveGameState saveState = getStateManager().getState(SaveGameState.class);
|
||||||
|
if (saveState != null) { saveState.removeDeadNpcRecord(s.npc().getCharacterId()); }
|
||||||
|
}
|
||||||
|
unloadNpc(s);
|
||||||
|
toRemoveBuf.add(s);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Vector3f pos = resolveRoutinePosition(s.npc(), currentHour);
|
Vector3f pos = resolveRoutinePosition(s.npc(), currentHour);
|
||||||
float dist = pos != null ? dist2d(playerPos, pos) : Float.MAX_VALUE;
|
float dist = pos != null ? dist2d(playerPos, pos) : Float.MAX_VALUE;
|
||||||
|
|
||||||
@@ -507,6 +543,71 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
spawnedIdsBuf.add(s.npc().getCharacterId());
|
spawnedIdsBuf.add(s.npc().getCharacterId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gespeicherte Leichen laden
|
||||||
|
SaveGameState saveStateForCorpses = getStateManager().getState(SaveGameState.class);
|
||||||
|
if (saveStateForCorpses != null) {
|
||||||
|
for (java.util.Map.Entry<String, SaveGame.DeadNpcSave> entry
|
||||||
|
: saveStateForCorpses.getDeadNpcs().entrySet()) {
|
||||||
|
String npcId = entry.getKey();
|
||||||
|
SaveGame.DeadNpcSave dns = entry.getValue();
|
||||||
|
if (dns.fullyLooted) { continue; }
|
||||||
|
if (spawnedIdsBuf.contains(npcId)) { continue; }
|
||||||
|
Vector3f corpsePos = new Vector3f(dns.x, dns.y, dns.z);
|
||||||
|
if (dist2d(playerPos, corpsePos) > LOAD_RADIUS) { continue; }
|
||||||
|
NPC npc = allNpcs.stream()
|
||||||
|
.filter(n -> npcId.equals(n.getCharacterId())).findFirst().orElse(null);
|
||||||
|
if (npc == null) { continue; }
|
||||||
|
|
||||||
|
Spatial vis = buildVisual(npc);
|
||||||
|
vis.setLocalTranslation(corpsePos);
|
||||||
|
vis.setLocalRotation(new com.jme3.math.Quaternion().fromAngles(0f, dns.rotY, 0f));
|
||||||
|
npcsRoot.attachChild(vis);
|
||||||
|
SpawnedNpc s = new SpawnedNpc(npc, vis, dns.x, dns.z);
|
||||||
|
s.knockedOut = true;
|
||||||
|
s.combat.phase = WeaponCombatState.Phase.KNOCKED_OUT;
|
||||||
|
s.aggroBehavior.forceCalm();
|
||||||
|
attachNpcCapsule(s);
|
||||||
|
|
||||||
|
java.util.List<String> remaining = dns.remainingItemIds;
|
||||||
|
s.lootItems = npc.getItems() != null
|
||||||
|
? npc.getItems().stream()
|
||||||
|
.filter(it -> it.getItemId() != null && remaining.contains(it.getItemId()))
|
||||||
|
.collect(java.util.stream.Collectors.toList())
|
||||||
|
: new ArrayList<>();
|
||||||
|
// Ausgerüstete Items: in lootItems wenn noch vorhanden, sonst visuell ausblenden
|
||||||
|
NpcWeaponAttachmentControl wac = vis.getControl(NpcWeaponAttachmentControl.class);
|
||||||
|
Item equippedWeapon = findItemById(npc.getEquippedWeaponId());
|
||||||
|
Item equippedShield = findItemById(npc.getEquippedShieldId());
|
||||||
|
if (equippedWeapon != null) {
|
||||||
|
if (remaining.contains(equippedWeapon.getItemId())) {
|
||||||
|
s.lootItems.add(0, equippedWeapon);
|
||||||
|
} else if (wac != null) {
|
||||||
|
wac.hideWeapon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (equippedShield != null) {
|
||||||
|
if (remaining.contains(equippedShield.getItemId())) {
|
||||||
|
s.lootItems.add(equippedWeapon != null
|
||||||
|
&& remaining.contains(equippedWeapon.getItemId()) ? 1 : 0, equippedShield);
|
||||||
|
} else if (wac != null) {
|
||||||
|
wac.hideShield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
de.blight.game.scene.WorldScene.collectPbrMaterials(vis, s.pbrMaterials);
|
||||||
|
if (dayNight != null && !s.pbrMaterials.isEmpty()) {
|
||||||
|
com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient();
|
||||||
|
scratchEmissive.set(ac.r, ac.g, ac.b, 1f);
|
||||||
|
for (Material mat : s.pbrMaterials) { mat.setColor("Emissive", scratchEmissive); }
|
||||||
|
}
|
||||||
|
playNpcDeathAnim(s);
|
||||||
|
s.combat.hitTimer = s.combat.clipDur; // sofort auf letztem Frame einfrieren
|
||||||
|
spawned.add(s);
|
||||||
|
spawnedIdsBuf.add(npcId);
|
||||||
|
log.debug("[WorldNpcs] Leiche '{}' geladen bei ({}, {})", npcId, dns.x, dns.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Neue NPCs einladen wenn in Reichweite
|
// Neue NPCs einladen wenn in Reichweite
|
||||||
for (NPC npc : allNpcs) {
|
for (NPC npc : allNpcs) {
|
||||||
if (spawnedIdsBuf.contains(npc.getCharacterId())) continue;
|
if (spawnedIdsBuf.contains(npc.getCharacterId())) continue;
|
||||||
@@ -602,7 +703,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
if (d > INTERACT_RANGE) continue;
|
if (d > INTERACT_RANGE) continue;
|
||||||
|
|
||||||
Vector3f headPos = pos.add(0f, 1.5f, 0f);
|
Vector3f headPos = getNpcBodyCenter(s);
|
||||||
Vector3f camToHead = headPos.subtract(cam.getLocation());
|
Vector3f camToHead = headPos.subtract(cam.getLocation());
|
||||||
if (camToHead.dot(cam.getDirection()) <= 0f) continue;
|
if (camToHead.dot(cam.getDirection()) <= 0f) continue;
|
||||||
|
|
||||||
@@ -618,12 +719,111 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
|
|
||||||
// ── Listener ─────────────────────────────────────────────────────────────
|
// ── Listener ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private boolean isWeaponDrawn() { return playerInput != null && playerInput.isWeaponDrawn(); }
|
||||||
|
|
||||||
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
||||||
if (!isPressed || dialogActive || hoveredIdx < 0) return;
|
if (!isPressed || dialogActive || hoveredIdx < 0) { return; }
|
||||||
if (spawned.get(hoveredIdx).aggroBehavior.isAttacking()) return;
|
if (isWeaponDrawn()) { return; }
|
||||||
|
SpawnedNpc s = spawned.get(hoveredIdx);
|
||||||
|
if (s.knockedOut) {
|
||||||
|
if (s.lootItems != null && s.lootItems.isEmpty()) {
|
||||||
|
SaveGameState saveState = getStateManager().getState(SaveGameState.class);
|
||||||
|
if (saveState != null) { saveState.reportNpcFullyLooted(s.npc().getCharacterId()); }
|
||||||
|
} else if (s.lootItems != null && lootHud != null) {
|
||||||
|
startLootApproach(s);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s.aggroBehavior.isAttacking()) { return; }
|
||||||
startDialog(hoveredIdx);
|
startDialog(hoveredIdx);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private void startLootApproach(SpawnedNpc s) {
|
||||||
|
pendingLootNpc = s;
|
||||||
|
Vector3f npcCenter = getNpcBodyCenter(s);
|
||||||
|
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||||
|
Vector3f toPlayer = playerPos.subtract(npcCenter);
|
||||||
|
toPlayer.y = 0f;
|
||||||
|
float dist = toPlayer.length();
|
||||||
|
if (dist < 0.001f) { toPlayer.set(1f, 0f, 0f); dist = 1f; }
|
||||||
|
toPlayer.divideLocal(dist);
|
||||||
|
lootApproachTarget = npcCenter.add(toPlayer.mult(LOOT_APPROACH_DIST));
|
||||||
|
lootApproachTarget.y = playerPos.y;
|
||||||
|
|
||||||
|
float dx = lootApproachTarget.x - playerPos.x;
|
||||||
|
float dz = lootApproachTarget.z - playerPos.z;
|
||||||
|
if ((float) Math.sqrt(dx * dx + dz * dz) < LOOT_ARRIVE_THRESH) {
|
||||||
|
beginLootCrouch();
|
||||||
|
} else {
|
||||||
|
lootApproachPhase = LootApproach.WALKING;
|
||||||
|
Vector3f walkDir = new Vector3f(dx, 0f, dz).normalizeLocal();
|
||||||
|
playerInput.setAutopilotDirection(walkDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLootApproach(float tpf) {
|
||||||
|
if (lootApproachPhase != LootApproach.WALKING || pendingLootNpc == null) { return; }
|
||||||
|
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||||
|
float dx = lootApproachTarget.x - playerPos.x;
|
||||||
|
float dz = lootApproachTarget.z - playerPos.z;
|
||||||
|
float distLeft = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
if (distLeft < LOOT_ARRIVE_THRESH) {
|
||||||
|
playerInput.setAutopilotDirection(null);
|
||||||
|
beginLootCrouch();
|
||||||
|
} else {
|
||||||
|
// Richtung jedes Frame neu berechnen (Physik kann abrutschen)
|
||||||
|
playerInput.setAutopilotDirection(new Vector3f(dx, 0f, dz).normalizeLocal());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginLootCrouch() {
|
||||||
|
lootApproachPhase = LootApproach.CROUCHING;
|
||||||
|
Vector3f npcPos = pendingLootNpc.visual().getWorldTranslation();
|
||||||
|
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||||
|
Vector3f dir = npcPos.subtract(playerPos);
|
||||||
|
dir.y = 0f;
|
||||||
|
if (dir.lengthSquared() > 0.001f) { playerInput.requestTurn(dir.normalizeLocal(), 0.15f, null); }
|
||||||
|
SpawnedNpc target = pendingLootNpc;
|
||||||
|
playerInput.requestAnimation(AnimationAction.STAND_TO_CROUCH, 0f, () -> {
|
||||||
|
// CROUCH_IDLE mit sehr langer Dauer fixieren – läuft auch wenn Welt pausiert (OverlayState)
|
||||||
|
playerInput.requestAnimation(AnimationAction.CROUCH_IDLE, Float.MAX_VALUE, null);
|
||||||
|
openLootHud(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startStandUp() {
|
||||||
|
lootApproachPhase = LootApproach.STANDING_UP;
|
||||||
|
pendingLootNpc = null;
|
||||||
|
lootApproachTarget = null;
|
||||||
|
playerInput.requestAnimation(AnimationAction.CROUCH_TO_STAND, 0f, () -> lootApproachPhase = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openLootHud(SpawnedNpc s) {
|
||||||
|
lootHud.open(new SpawnedNpcLootable(s), item -> {
|
||||||
|
mainCharacter.getInventar().collect(item);
|
||||||
|
if (item.getItemId() != null) {
|
||||||
|
NpcWeaponAttachmentControl wac =
|
||||||
|
s.visual().getControl(NpcWeaponAttachmentControl.class);
|
||||||
|
if (wac != null) {
|
||||||
|
if (item.getItemId().equals(s.npc().getEquippedWeaponId())) { wac.hideWeapon(); }
|
||||||
|
if (item.getItemId().equals(s.npc().getEquippedShieldId())) { wac.hideShield(); }
|
||||||
|
}
|
||||||
|
SaveGameState saveState = getStateManager().getState(SaveGameState.class);
|
||||||
|
if (saveState != null) {
|
||||||
|
saveState.reportNpcItemTaken(s.npc().getCharacterId(), item.getItemId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, () -> startStandUp());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SpawnedNpcLootable implements de.blight.common.model.Lootable {
|
||||||
|
private final SpawnedNpc s;
|
||||||
|
SpawnedNpcLootable(SpawnedNpc s) { this.s = s; }
|
||||||
|
@Override public String getLootId() { return s.npc().getCharacterId(); }
|
||||||
|
@Override public String getLootTitleKey() { return s.npc().getLabelKey(); }
|
||||||
|
@Override public java.util.List<de.blight.common.model.Item> getLootItems() { return s.lootItems; }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spielt einen Dialog-Animations-Clip einmalig auf dem aktuellen Dialog-NPC.
|
* Spielt einen Dialog-Animations-Clip einmalig auf dem aktuellen Dialog-NPC.
|
||||||
* Falls der NPC sitzt, wird stattdessen "talk_sitting" gespielt.
|
* Falls der NPC sitzt, wird stattdessen "talk_sitting" gespielt.
|
||||||
@@ -906,8 +1106,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
|
|
||||||
public Vector3f getHoveredWorldPos() {
|
public Vector3f getHoveredWorldPos() {
|
||||||
if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;
|
if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;
|
||||||
Vector3f pos = spawned.get(hoveredIdx).visual().getLocalTranslation();
|
return getNpcBodyCenter(spawned.get(hoveredIdx));
|
||||||
return pos.add(0f, 1.5f, 0f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||||
@@ -2320,6 +2519,22 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
s.knockedOut = true;
|
s.knockedOut = true;
|
||||||
s.combat.phase = WeaponCombatState.Phase.KNOCKED_OUT;
|
s.combat.phase = WeaponCombatState.Phase.KNOCKED_OUT;
|
||||||
s.aggroBehavior.forceCalm();
|
s.aggroBehavior.forceCalm();
|
||||||
|
|
||||||
|
s.lootItems = s.npc().getItems() != null
|
||||||
|
? new ArrayList<>(s.npc().getItems()) : new ArrayList<>();
|
||||||
|
Item equippedWeapon = findItemById(s.npc().getEquippedWeaponId());
|
||||||
|
Item equippedShield = findItemById(s.npc().getEquippedShieldId());
|
||||||
|
if (equippedWeapon != null) { s.lootItems.add(0, equippedWeapon); }
|
||||||
|
if (equippedShield != null) { s.lootItems.add(equippedWeapon != null ? 1 : 0, equippedShield); }
|
||||||
|
|
||||||
|
SaveGameState saveState = getStateManager().getState(SaveGameState.class);
|
||||||
|
if (saveState != null) {
|
||||||
|
Vector3f pos = s.visual().getWorldTranslation();
|
||||||
|
float[] angles = s.visual().getWorldRotation().toAngles(null);
|
||||||
|
saveState.reportNpcKilled(s.npc().getCharacterId(),
|
||||||
|
pos.x, pos.y, pos.z, angles[1], s.lootItems);
|
||||||
|
}
|
||||||
|
|
||||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("argh"); }
|
if (weaponSoundSystem != null) { weaponSoundSystem.play("argh"); }
|
||||||
playNpcDeathAnim(s);
|
playNpcDeathAnim(s);
|
||||||
log.info("[NPC {}] K.O.!", s.npc().getCharacterId());
|
log.info("[NPC {}] K.O.!", s.npc().getCharacterId());
|
||||||
@@ -2482,6 +2697,98 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
return SwordShieldCombat.resolveIdleImpactAction(s.combatWeaponType);
|
return SwordShieldCombat.resolveIdleImpactAction(s.combatWeaponType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die Weltposition der Hüfte zurück (mixamorig:Hips).
|
||||||
|
* Bewegt sich mit der Death-Animation mit, sodass das Label auch bei liegenden NPCs korrekt sitzt.
|
||||||
|
* Fallback: visual.getWorldTranslation() + 1.0m Y.
|
||||||
|
*/
|
||||||
|
private Vector3f getNpcBodyCenter(SpawnedNpc s) {
|
||||||
|
if (s.hipsNode == null) {
|
||||||
|
com.jme3.anim.SkinningControl sc =
|
||||||
|
de.blight.game.animation.RetargetingSystem.findSkinningControl(s.visual());
|
||||||
|
if (sc != null) {
|
||||||
|
try { s.hipsNode = sc.getAttachmentsNode("mixamorig:Hips"); }
|
||||||
|
catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s.hipsNode != null) {
|
||||||
|
return s.hipsNode.getWorldTranslation().add(0f, 0.5f, 0f);
|
||||||
|
}
|
||||||
|
return s.visual().getWorldTranslation().add(0f, 1.5f, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Konsolen-API ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Gibt alle gespawnten (lebenden) NPCs als "id (HP cur/max)" zurück. */
|
||||||
|
public List<String> listNearbyNpcs() {
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
for (SpawnedNpc s : spawned) {
|
||||||
|
if (!s.knockedOut) {
|
||||||
|
out.add(String.format("%-30s HP %d/%d", s.npc().getCharacterId(),
|
||||||
|
s.currentHp, s.maxHp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tötet einen NPC anhand seiner ID sofort (wie 0 HP). */
|
||||||
|
public String killNpcById(String npcId) {
|
||||||
|
for (SpawnedNpc s : spawned) {
|
||||||
|
if (npcId.equals(s.npc().getCharacterId())) {
|
||||||
|
if (s.knockedOut) { return npcId + " ist bereits tot."; }
|
||||||
|
s.currentHp = 0;
|
||||||
|
knockoutNpc(s);
|
||||||
|
return npcId + " getötet.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "NPC '" + npcId + "' nicht in der Nähe.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Versetzt einen toten NPC zurück in den Lebendig-Zustand. */
|
||||||
|
public String reviveNpcById(String npcId) {
|
||||||
|
// Aus dem Save entfernen (auch defeatedEnemies)
|
||||||
|
SaveGameState saveState = getStateManager().getState(SaveGameState.class);
|
||||||
|
boolean inSave = saveState != null && saveState.getDeadNpcs().containsKey(npcId);
|
||||||
|
if (saveState != null) {
|
||||||
|
saveState.removeDeadNpcRecord(npcId);
|
||||||
|
saveState.getSave().world.defeatedEnemies.remove(npcId);
|
||||||
|
saveState.persist();
|
||||||
|
}
|
||||||
|
// Falls noch als Leiche gespawnt: entladen → respawnt automatisch
|
||||||
|
for (SpawnedNpc s : spawned) {
|
||||||
|
if (npcId.equals(s.npc().getCharacterId()) && s.knockedOut) {
|
||||||
|
unloadNpc(s);
|
||||||
|
spawned.remove(s);
|
||||||
|
return npcId + " wiederbelebt (neu gespawnt).";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inSave ? npcId + " wiederbelebt." : "NPC '" + npcId + "' nicht tot oder nicht gefunden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Listet alle Items im items-Verzeichnis auf. */
|
||||||
|
public List<String> listAllItems() {
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
java.nio.file.Path itemsDir = AnimationLibrary.findAssetRoot().resolve("items");
|
||||||
|
try (java.util.stream.Stream<java.nio.file.Path> walk = java.nio.file.Files.walk(itemsDir)) {
|
||||||
|
walk.filter(p -> p.getFileName().toString().endsWith(".item"))
|
||||||
|
.sorted()
|
||||||
|
.forEach(p -> {
|
||||||
|
String id = p.getFileName().toString().replace(".item", "");
|
||||||
|
try {
|
||||||
|
de.blight.common.model.Item item = de.blight.common.model.ItemIO.load(p);
|
||||||
|
out.add(String.format("%-30s %s", id, item.getDisplayText()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
out.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
out.add("Fehler beim Laden der Items: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
private float resolveNpcClipLength(SpawnedNpc s, AnimationAction action, float fallback) {
|
private float resolveNpcClipLength(SpawnedNpc s, AnimationAction action, float fallback) {
|
||||||
if (animLib == null) { return fallback; }
|
if (animLib == null) { return fallback; }
|
||||||
String setName = s.npc().getAnimSetPath();
|
String setName = s.npc().getAnimSetPath();
|
||||||
|
|||||||
Reference in New Issue
Block a user