Animation bezüglich schwert und schild im kampfsystem abgeschlossen
This commit is contained in:
@@ -38,6 +38,7 @@ public enum AnimationAction {
|
||||
SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,
|
||||
SWORD_IDLE_TO_BLOCK, SWORD_COUNTERATTACK, SWORD_BLOCK_IMPACT, SWORD_IDLE_IMPACT,
|
||||
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
|
||||
SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT,
|
||||
|
||||
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
|
||||
@@ -109,6 +110,10 @@ public enum AnimationAction {
|
||||
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
|
||||
case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)");
|
||||
case SWORD_BLOCK_MOVE -> TextRegistry.resolve(null, key, "Block Vorwärts (Schwert)");
|
||||
case SWORD_BLOCK_MOVE_BACKWARD -> TextRegistry.resolve(null, key, "Block Rückwärts (Schwert)");
|
||||
case SWORD_BLOCK_STRAFE_LEFT -> TextRegistry.resolve(null, key, "Block Strafe Links (Schwert)");
|
||||
case SWORD_BLOCK_STRAFE_RIGHT -> TextRegistry.resolve(null, key, "Block Strafe Rechts (Schwert)");
|
||||
|
||||
case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)");
|
||||
case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)");
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.blight.game.combat;
|
||||
|
||||
/**
|
||||
* Berechnet den finalen Schadenswert eines Angriffs.
|
||||
* Erweiterbar für Level-Boni, Fähigkeiten, Rüstungs-Durchdringung etc.
|
||||
*/
|
||||
public final class DamageCalculator {
|
||||
|
||||
private static final int UNARMED_BASE = 5;
|
||||
|
||||
private DamageCalculator() {}
|
||||
|
||||
/**
|
||||
* @param baseDamage Basisschaden der Waffe (≤0 = unbewaffnet)
|
||||
* @param critMultiplier Schadens-Multiplikator (1.0 = normal, 2.0 = Kombo-Krit, 1.5–3.0 = Schwerer Angriff)
|
||||
* @return Schadenswert (min. 1)
|
||||
*/
|
||||
public static int calculate(int baseDamage, float critMultiplier) {
|
||||
int dmg = baseDamage > 0 ? baseDamage : UNARMED_BASE;
|
||||
if (critMultiplier > 1.0f) {
|
||||
dmg = Math.round(dmg * critMultiplier);
|
||||
}
|
||||
return Math.max(1, dmg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.blight.game.combat;
|
||||
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
/**
|
||||
* Wird aufgerufen wenn der Spieler einen Nahkampfangriff auslöst.
|
||||
* Implementierende Klassen können z.B. Treffererkennung auf Zielobjekten durchführen.
|
||||
*/
|
||||
public interface MeleeAttackCallback {
|
||||
|
||||
/**
|
||||
* @param charPosition Weltposition des Charakters beim Angriff
|
||||
* @param charForward Blickrichtung des Charakters (normiert, Y=0)
|
||||
* @param baseDamage Basisschaden der Waffe (0 = unbewaffnet)
|
||||
* @param critMultiplier Schadens-Multiplikator (1.0 = normal, 2.0 = Kombo-5, 1.5–3.0 = Schwerer Angriff)
|
||||
*/
|
||||
void onMeleeAttack(Vector3f charPosition, Vector3f charForward, int baseDamage, float critMultiplier);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package de.blight.game.control;
|
||||
|
||||
import com.jme3.anim.AnimComposer;
|
||||
import com.jme3.anim.ArmatureMask;
|
||||
import com.jme3.anim.tween.Tween;
|
||||
import com.jme3.anim.tween.action.BaseAction;
|
||||
import com.jme3.bullet.control.CharacterControl;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.input.MouseInput;
|
||||
@@ -20,6 +23,7 @@ import de.blight.game.animation.AnimationAction;
|
||||
import de.blight.game.animation.AnimationLibrary;
|
||||
import de.blight.game.animation.RetargetingSystem;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
import de.blight.game.combat.MeleeAttackCallback;
|
||||
import de.blight.game.navigation.CharacterNavigator;
|
||||
import de.blight.game.state.TerrainChunkState;
|
||||
|
||||
@@ -44,6 +48,33 @@ public class PlayerInputControl {
|
||||
private static final float COMBO_WINDOW = 0.6f;
|
||||
/** Maximale Haltezeit für einen Kurz-Klick (Konterangriff); darüber = Block-Eingabe. */
|
||||
private static final float BLOCK_TAP_MAX = 0.20f;
|
||||
/** Maximale Haltezeit, die noch als "schneller Klick" (→ normale Kombo) gilt. */
|
||||
private static final float QUICK_TAP_THRESHOLD = 0.30f;
|
||||
/** Animationsanteil (0–1), ab dem die Vorwärtsbewegung beim schweren Angriff beginnt. */
|
||||
private static final float LUNGE_FRAC_START = 0.15f;
|
||||
/** Animationsanteil (0–1), ab dem die Vorwärtsbewegung endet (Landung). */
|
||||
private static final float LUNGE_FRAC_END = 0.55f;
|
||||
/** AnimComposer-Layer-Name für die prozedurale Ziehen/Wegstecken-Animation (Oberarm-Overlay). */
|
||||
private static final String DRAW_LAYER = "__draw_weapon__";
|
||||
/** Dauer der Ziehen/Wegstecken-Animation in Sekunden. */
|
||||
private static final float DRAW_DURATION = 0.85f;
|
||||
// Ziel-Quaternions für die Ziehen-Animation: aus Heavy-Attack-Wind-Up bei t=0.933s gelesen
|
||||
// (Bone: mixamorig:RightArm_$AssimpFbx$_Rotation und ...RightForeArm...)
|
||||
private static Quaternion quatFromDeg(float x, float y, float z) {
|
||||
return new Quaternion().fromAngles(
|
||||
x * com.jme3.math.FastMath.DEG_TO_RAD,
|
||||
y * com.jme3.math.FastMath.DEG_TO_RAD,
|
||||
z * com.jme3.math.FastMath.DEG_TO_RAD);
|
||||
}
|
||||
private static final Quaternion DRAW_ARM_PEAK_Q = quatFromDeg( 54.1f, 150.0f, -50.0f);
|
||||
private static final Quaternion DRAW_FOREARM_PEAK_Q = quatFromDeg( 23.0f, 30.1f, -35.1f);
|
||||
// Linker Arm: Delta auf Base-Joint (alter Ansatz).
|
||||
// Z positiv = rauf am linken Arm (Spiegel von rechts: dort Z negativ = rauf).
|
||||
// Z negativ = runter/am Körper vorbei. Y = Blickrichtung des Chars.
|
||||
// Startpunkt: Z=-80 (runter), Y=20 (leicht nach vorne). Unterarm: X=-80 (Ellenbogen knickt).
|
||||
// Falls Arm falsche Richtung → Z-Vorzeichen drehen.
|
||||
private static final float[] DRAW_L_ARM_DEG = { 0f, 20f, -80f };
|
||||
private static final float[] DRAW_L_FOREARM_DEG = { -80f, 0f, 0f };
|
||||
// Combo-Overlap-Utility: MELEE_COMBO_ACTIVE + COMBO_ATK2_OFFSET hier eintragen,
|
||||
// sobald die Animationen in Blender angeglichen wurden (passende Übergangsposen).
|
||||
// AnimPlayer.play(clip, transition, startOffset) und nextStartOffset in tryPlay() sind
|
||||
@@ -89,7 +120,40 @@ public class PlayerInputControl {
|
||||
private boolean comboWindowActive = false;
|
||||
private float comboWindowRemaining = 0f;
|
||||
/** Letzter Angriff war ein kritischer Treffer (Kombo-Schritt 3). Für das Schadenssystem. */
|
||||
private boolean lastAttackCritical = false;
|
||||
private boolean lastAttackCritical = false;
|
||||
/** Linke Maustaste wird seit Drücken gehalten (schwerer Angriff in Vorbereitung). */
|
||||
private boolean attackHoldPending = false;
|
||||
private float attackHoldTimer = 0f;
|
||||
private Vector3f heavyLungeVel = null;
|
||||
/** combatAnimRemaining-Wert, ab dem die Lunge beginnt (Sprung-Startframe). */
|
||||
private float heavyLungeStartRemaining = 0f;
|
||||
/** combatAnimRemaining-Wert, ab dem die Lunge endet (Landung). */
|
||||
private float heavyLungeEndRemaining = 0f;
|
||||
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
|
||||
private float combatAnimDuration = 0f;
|
||||
private MeleeAttackCallback meleeAttackCallback = null;
|
||||
// Draw/Sheath Animation System
|
||||
private boolean drawAnimActive = false;
|
||||
private float drawAnimTimer = 0f;
|
||||
private boolean drawMidFired = false;
|
||||
private Runnable drawMidCallback = null;
|
||||
private Runnable drawEndCallback = null;
|
||||
private boolean drawWithShield = false;
|
||||
// Arm-Joints für prozedurale Ziehen/Wegstecken-Animation
|
||||
// Die $AssimpFbx$_Rotation-Nodes enthalten die tatsächlichen Animations-Rotationen
|
||||
private com.jme3.anim.Joint jointRightArm = null;
|
||||
private com.jme3.anim.Joint jointRightForeArm = null;
|
||||
private com.jme3.anim.Joint jointLeftArm = null;
|
||||
private com.jme3.anim.Joint jointLeftForeArm = null;
|
||||
private com.jme3.anim.Joint jointRightArmFbxRot = null;
|
||||
private com.jme3.anim.Joint jointRightForeArmFbxRot = null;
|
||||
private com.jme3.anim.Joint jointLeftArmFbxRot = null;
|
||||
private com.jme3.anim.Joint jointLeftForeArmFbxRot = null;
|
||||
// Start-Pose (beim Ziehen-Start aus der laufenden Animation gesampelt)
|
||||
private Quaternion drawStartArmQ = null;
|
||||
private Quaternion drawStartForeArmQ = null;
|
||||
private Quaternion drawStartLeftArmQ = null;
|
||||
private Quaternion drawStartLeftForeArmQ = null;
|
||||
|
||||
private AnimationLibrary animLib;
|
||||
private String animSetName;
|
||||
@@ -187,8 +251,39 @@ public class PlayerInputControl {
|
||||
case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } }
|
||||
case ACT_DRAW_MELEE -> { if (isPressed) handleDrawMelee(); }
|
||||
case ACT_DRAW_RANGED -> { if (isPressed) handleDrawRanged(); }
|
||||
case ACT_SHEATH -> { if (isPressed && weaponDrawn && !blockingAnimActive) sheathCurrentWeapon(); }
|
||||
case ACT_COMBAT_ATK -> { if (isPressed) handleCombatAttack(); }
|
||||
case ACT_SHEATH -> { if (isPressed && weaponDrawn && !blockingAnimActive && !drawAnimActive) sheathCurrentWeapon(); }
|
||||
case ACT_COMBAT_ATK -> {
|
||||
if (isPressed) {
|
||||
if (!weaponDrawn || blockingAnimActive || drawAnimActive || isBlocking()) break;
|
||||
if (combatAnimActive) {
|
||||
if (comboWindowActive) {
|
||||
// Kombo-Follow während zweiter Hälfte: sofort fluid weiter
|
||||
combatAnimActive = false;
|
||||
combatAnimRemaining = 0f;
|
||||
combatAnimSkipComboWindow = false;
|
||||
heavyLungeVel = null;
|
||||
heavyLungeStartRemaining = 0f;
|
||||
heavyLungeEndRemaining = 0f;
|
||||
handleCombatAttack();
|
||||
}
|
||||
break;
|
||||
}
|
||||
attackHoldPending = true;
|
||||
attackHoldTimer = 0f;
|
||||
} else {
|
||||
if (attackHoldPending) {
|
||||
attackHoldPending = false;
|
||||
if (attackHoldTimer > QUICK_TAP_THRESHOLD) {
|
||||
handleHeavyAttack();
|
||||
} else {
|
||||
// Schneller Klick → schwere Anim abbrechen, normale Kombo starten
|
||||
combatAnimActive = false;
|
||||
combatAnimRemaining = 0f;
|
||||
handleCombatAttack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case ACT_COMBAT_ALT -> handleCombatAlt(isPressed);
|
||||
}
|
||||
};
|
||||
@@ -232,6 +327,17 @@ public class PlayerInputControl {
|
||||
if (skinningControl != null) {
|
||||
armature = skinningControl.getArmature();
|
||||
logJointNames(armature);
|
||||
jointRightArm = armature.getJoint("mixamorig:RightArm");
|
||||
jointRightForeArm = armature.getJoint("mixamorig:RightForeArm");
|
||||
jointLeftArm = armature.getJoint("mixamorig:LeftArm");
|
||||
jointLeftForeArm = armature.getJoint("mixamorig:LeftForeArm");
|
||||
jointRightArmFbxRot = armature.getJoint("mixamorig:RightArm_$AssimpFbx$_Rotation");
|
||||
jointRightForeArmFbxRot = armature.getJoint("mixamorig:RightForeArm_$AssimpFbx$_Rotation");
|
||||
jointLeftArmFbxRot = armature.getJoint("mixamorig:LeftArm_$AssimpFbx$_Rotation");
|
||||
jointLeftForeArmFbxRot = armature.getJoint("mixamorig:LeftForeArm_$AssimpFbx$_Rotation");
|
||||
log.info("[DrawAnim] FbxRot-Joints: R={} RF={} L={} LF={}",
|
||||
jointRightArmFbxRot != null, jointRightForeArmFbxRot != null,
|
||||
jointLeftArmFbxRot != null, jointLeftForeArmFbxRot != null);
|
||||
}
|
||||
animPlayer = (animComposer != null && skinningControl != null)
|
||||
? new AnimPlayer(animComposer, skinningControl)
|
||||
@@ -261,11 +367,12 @@ public class PlayerInputControl {
|
||||
}
|
||||
}
|
||||
|
||||
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
||||
public boolean isWeaponDrawn() { return weaponDrawn; }
|
||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
||||
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
||||
public boolean isWeaponDrawn() { return weaponDrawn; }
|
||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
||||
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||
public void setMeleeAttackCallback(MeleeAttackCallback cb) { this.meleeAttackCallback = cb; }
|
||||
|
||||
public boolean isPaused() { return paused; }
|
||||
|
||||
@@ -664,14 +771,17 @@ public class PlayerInputControl {
|
||||
currentWaterDepth = (waterDepthQuery != null) ? waterDepthQuery.apply(charPos.x, charPos.z) : 0f;
|
||||
boolean inDeepWater = currentWaterDepth > WATER_WADING_DEPTH;
|
||||
|
||||
// Schon ab WAITING Gehgeschwindigkeit erzwingen, damit beim Blockstart kein Sprint-Frame entsteht
|
||||
boolean inBlockStance = isBlocking() || blockPhase == BlockPhase.WAITING;
|
||||
|
||||
if (moving) {
|
||||
scratchMoveDir.normalizeLocal();
|
||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||
: (sprint && !weaponDrawn) ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
float speed = (inDeepWater || walk || inBlockStance) ? MOVE_SPEED * WALK_MULT
|
||||
: (sprint && !weaponDrawn) ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
|
||||
|
||||
if (visual != null) {
|
||||
if (visual != null && !inBlockStance) {
|
||||
scratchRot.lookAt(scratchMoveDir, Vector3f.UNIT_Y);
|
||||
scratchCurRot.set(visual.getLocalRotation());
|
||||
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
|
||||
@@ -681,25 +791,71 @@ public class PlayerInputControl {
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
// Im Block-Modus dreht der Char immer mit der Kamera (Blickrichtung folgt Kamera, nicht WASD)
|
||||
if (visual != null && isBlocking() && scratchCamDir.lengthSquared() > 0.001f) {
|
||||
scratchRot.lookAt(scratchCamDir, Vector3f.UNIT_Y);
|
||||
scratchCurRot.set(visual.getLocalRotation());
|
||||
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
|
||||
visual.setLocalRotation(scratchCurRot);
|
||||
}
|
||||
|
||||
// Hold-Timer muss jede Frame akkumulieren, unabhängig vom Anim-Status
|
||||
if (attackHoldPending) {
|
||||
attackHoldTimer += tpf;
|
||||
// Erst wenn klar ist dass es ein schwerer Angriff wird → Animation starten
|
||||
if (attackHoldTimer > QUICK_TAP_THRESHOLD && !combatAnimActive) {
|
||||
startHeavyCharge();
|
||||
}
|
||||
}
|
||||
|
||||
// Draw/Sheath-Animation Timer (Oberkörper-Overlay, Beine laufen weiter)
|
||||
if (drawAnimActive) {
|
||||
drawAnimTimer += tpf;
|
||||
if (!drawMidFired && drawAnimTimer >= DRAW_DURATION * 0.5f) {
|
||||
drawMidFired = true;
|
||||
if (drawMidCallback != null) drawMidCallback.run();
|
||||
}
|
||||
if (drawAnimTimer >= DRAW_DURATION) {
|
||||
drawAnimActive = false;
|
||||
if (animComposer != null) animComposer.removeLayer(DRAW_LAYER);
|
||||
Runnable cb = drawEndCallback;
|
||||
drawEndCallback = null;
|
||||
if (cb != null) cb.run();
|
||||
}
|
||||
}
|
||||
|
||||
// Kampf-Anim-Timer
|
||||
if (combatAnimActive) {
|
||||
combatAnimRemaining -= tpf;
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
boolean inLungeWindow = heavyLungeVel != null
|
||||
&& combatAnimRemaining <= heavyLungeStartRemaining
|
||||
&& combatAnimRemaining >= heavyLungeEndRemaining;
|
||||
physicsChar.setWalkDirection(inLungeWindow ? heavyLungeVel : Vector3f.ZERO);
|
||||
if (combatAnimRemaining <= 0f) {
|
||||
combatAnimActive = false;
|
||||
combatAnimActive = false;
|
||||
heavyLungeVel = null;
|
||||
heavyLungeStartRemaining = 0f;
|
||||
heavyLungeEndRemaining = 0f;
|
||||
currentAnim = null;
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
Runnable cb = combatAnimCallback;
|
||||
combatAnimCallback = null;
|
||||
boolean skipWindow = combatAnimSkipComboWindow;
|
||||
combatAnimSkipComboWindow = false;
|
||||
if (cb != null) cb.run();
|
||||
if (!skipWindow && !combatAnimActive && isMeleeType(drawnWeaponType)) {
|
||||
comboWindowActive = true;
|
||||
comboWindowRemaining = COMBO_WINDOW;
|
||||
log.info("[Combat] Kombo-Fenster geöffnet (step={} next={})", comboStep, comboStep);
|
||||
if (!combatAnimActive && comboWindowActive) {
|
||||
// Fenster nicht genutzt → Kombo-Reset
|
||||
comboWindowActive = false;
|
||||
comboStep = 0;
|
||||
log.info("[Combat] Kombo-Fenster verpasst → Reset");
|
||||
}
|
||||
} else {
|
||||
// Kombo-Fenster öffnet sich in der zweiten Hälfte der Anim
|
||||
if (!comboWindowActive && !combatAnimSkipComboWindow
|
||||
&& isMeleeType(drawnWeaponType)
|
||||
&& combatAnimRemaining <= combatAnimDuration * 0.5f) {
|
||||
comboWindowActive = true;
|
||||
log.info("[Combat] Kombo-Fenster geöffnet (step={}, remaining={}s)", comboStep, combatAnimRemaining);
|
||||
}
|
||||
pollFootsteps(tpf);
|
||||
return;
|
||||
}
|
||||
@@ -740,6 +896,19 @@ public class PlayerInputControl {
|
||||
playAction(target);
|
||||
currentAnim = target;
|
||||
}
|
||||
} else if (blockPhase == BlockPhase.HOLDING && !combatAnimActive) {
|
||||
// Beim Blocken: Bewegungsanimation nach Taste, Strafe hat Vorrang
|
||||
AnimationAction blockMoveAnim;
|
||||
if (right) blockMoveAnim = AnimationAction.SWORD_BLOCK_STRAFE_RIGHT;
|
||||
else if (left) blockMoveAnim = AnimationAction.SWORD_BLOCK_STRAFE_LEFT;
|
||||
else if (forward) blockMoveAnim = AnimationAction.SWORD_BLOCK_MOVE;
|
||||
else if (backward) blockMoveAnim = AnimationAction.SWORD_BLOCK_MOVE_BACKWARD;
|
||||
else blockMoveAnim = getMeleeBlockAction(drawnWeaponType);
|
||||
|
||||
if (blockMoveAnim != currentAnim) {
|
||||
playAction(blockMoveAnim);
|
||||
currentAnim = blockMoveAnim;
|
||||
}
|
||||
}
|
||||
|
||||
pollFootsteps(tpf);
|
||||
@@ -768,16 +937,13 @@ public class PlayerInputControl {
|
||||
|
||||
private void handleDrawMelee() {
|
||||
if (mainCharacter == null || mainCharacter.getEquippedMeleeWeapon() == null) return;
|
||||
if (blockingAnimActive || combatAnimActive) return;
|
||||
if (blockingAnimActive || combatAnimActive || drawAnimActive) return;
|
||||
ItemSubCategory sub = mainCharacter.getEquippedMeleeWeapon().getSubCategory();
|
||||
if (!weaponDrawn) {
|
||||
AnimationAction drawAnim = getMeleeDrawAction(sub);
|
||||
float dur = resolveClipLength(drawAnim, 0.5f);
|
||||
requestAnimation(drawAnim, dur, () -> {
|
||||
weaponDrawn = true;
|
||||
drawnWeaponType = sub;
|
||||
currentAnim = null;
|
||||
});
|
||||
boolean hasShield = mainCharacter.getEquippedShield() != null;
|
||||
startDrawAnimation(hasShield,
|
||||
() -> { weaponDrawn = true; drawnWeaponType = sub; currentAnim = null; },
|
||||
null);
|
||||
} else if (isMeleeType(drawnWeaponType)) {
|
||||
sheathCurrentWeapon();
|
||||
}
|
||||
@@ -802,16 +968,15 @@ public class PlayerInputControl {
|
||||
|
||||
private void sheathCurrentWeapon() {
|
||||
if (drawnWeaponType == null) { weaponDrawn = false; return; }
|
||||
isAiming = false;
|
||||
rightMouseHeld = false;
|
||||
blockPhase = BlockPhase.NONE;
|
||||
AnimationAction sheathAnim = getSheathAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(sheathAnim, 0.5f);
|
||||
requestAnimation(sheathAnim, dur, () -> {
|
||||
weaponDrawn = false;
|
||||
drawnWeaponType = null;
|
||||
currentAnim = null;
|
||||
});
|
||||
isAiming = false;
|
||||
rightMouseHeld = false;
|
||||
attackHoldPending = false;
|
||||
heavyLungeVel = null;
|
||||
blockPhase = BlockPhase.NONE;
|
||||
boolean hasShield = mainCharacter != null && mainCharacter.getEquippedShield() != null;
|
||||
startDrawAnimation(hasShield,
|
||||
() -> { weaponDrawn = false; drawnWeaponType = null; currentAnim = null; },
|
||||
null);
|
||||
}
|
||||
|
||||
private void handleCombatAttack() {
|
||||
@@ -824,8 +989,9 @@ public class PlayerInputControl {
|
||||
int step = comboStep;
|
||||
boolean isComboFollow = comboWindowActive; // war Kombo-Fenster offen?
|
||||
AnimationAction atk = getMeleeComboAction(drawnWeaponType, step);
|
||||
lastAttackCritical = (step == 3);
|
||||
comboStep = (step + 1) % 4;
|
||||
lastAttackCritical = (step == 4);
|
||||
fireMeleeCallback(lastAttackCritical ? 2.0f : 1.0f);
|
||||
comboStep = (step + 1) % 5;
|
||||
comboWindowActive = false;
|
||||
float dur = resolveClipLength(atk, 0.6f);
|
||||
// Kombo-Folgeschlag: kurzer Blend für sauberen Übergang; erster Schlag: Hard-Cut
|
||||
@@ -834,9 +1000,25 @@ public class PlayerInputControl {
|
||||
step, atk, dur, lastAttackCritical);
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||
combatAnimDuration = dur;
|
||||
combatAnimCallback = null;
|
||||
playAction(atk);
|
||||
currentAnim = atk;
|
||||
// Finisher (step 4): leichte Vorwärtsbewegung
|
||||
if (step == 4) {
|
||||
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.1f);
|
||||
heavyLungeStartRemaining = dur * (1f - LUNGE_FRAC_START);
|
||||
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
||||
} else {
|
||||
heavyLungeVel = null;
|
||||
heavyLungeStartRemaining = 0f;
|
||||
heavyLungeEndRemaining = 0f;
|
||||
}
|
||||
} else if (isRangedType(drawnWeaponType) && isAiming) {
|
||||
isAiming = false;
|
||||
AnimationAction fireAnim = getRangedFireAction(drawnWeaponType);
|
||||
@@ -919,7 +1101,8 @@ public class PlayerInputControl {
|
||||
blockPhase = BlockPhase.ENTERING;
|
||||
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(idleToBlock, 0.5f);
|
||||
blockPhaseTimer = dur;
|
||||
// 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);
|
||||
currentAnim = idleToBlock;
|
||||
@@ -929,7 +1112,7 @@ public class PlayerInputControl {
|
||||
blockPhase = BlockPhase.EXITING;
|
||||
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(idleToBlock, 0.5f);
|
||||
blockPhaseTimer = dur;
|
||||
blockPhaseTimer = Math.max(0.1f, dur - 0.05f);
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
nextPlayReversed = true;
|
||||
playAction(idleToBlock);
|
||||
@@ -1069,11 +1252,12 @@ public class PlayerInputControl {
|
||||
};
|
||||
}
|
||||
|
||||
/** Kombo-Sequenz: 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3 (kritisch). */
|
||||
/** Kombo-Sequenz: 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK2, 4=ATK3 (kritisch 2x). */
|
||||
private static AnimationAction getMeleeComboAction(ItemSubCategory type, int step) {
|
||||
return switch (step) {
|
||||
case 1 -> getMeleeAttack2Action(type);
|
||||
case 3 -> getMeleeAttack3Action(type);
|
||||
case 3 -> getMeleeAttack2Action(type);
|
||||
case 4 -> getMeleeAttack3Action(type);
|
||||
default -> getMeleeAttack1Action(type);
|
||||
};
|
||||
}
|
||||
@@ -1118,6 +1302,78 @@ public class PlayerInputControl {
|
||||
};
|
||||
}
|
||||
|
||||
private void fireMeleeCallback(float critMultiplier) {
|
||||
if (meleeAttackCallback == null || physicsChar == null) {
|
||||
return;
|
||||
}
|
||||
int base = (mainCharacter != null && mainCharacter.getEquippedMeleeWeapon() != null)
|
||||
? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0;
|
||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||
Vector3f fwd = (visual != null)
|
||||
? visual.getLocalRotation().mult(new Vector3f(0f, 0f, -1f))
|
||||
: new Vector3f(0f, 0f, -1f);
|
||||
fwd.y = 0f;
|
||||
if (fwd.lengthSquared() > 0.001f) {
|
||||
fwd.normalizeLocal();
|
||||
}
|
||||
meleeAttackCallback.onMeleeAttack(pos, fwd, base, critMultiplier);
|
||||
}
|
||||
|
||||
private float computeHeavyMultiplier() {
|
||||
return 3.0f;
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeHeavyAttackAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_HEAVY_ATTACK;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_HEAVY_ATTACK;
|
||||
default -> AnimationAction.SWORD_HEAVY_ATTACK;
|
||||
};
|
||||
}
|
||||
|
||||
/** Startet sofort die schwere-Angriff-Animation beim Drücken der Taste. */
|
||||
private void startHeavyCharge() {
|
||||
if (drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
||||
return;
|
||||
}
|
||||
comboStep = 0;
|
||||
comboWindowActive = false;
|
||||
AnimationAction action = getMeleeHeavyAttackAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(action, 0.8f);
|
||||
nextTransitionLength = -1.0;
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||
combatAnimDuration = dur;
|
||||
combatAnimSkipComboWindow = true;
|
||||
combatAnimCallback = null;
|
||||
playAction(action);
|
||||
currentAnim = action;
|
||||
|
||||
// Blickrichtung des Chars in Weltkoordinaten
|
||||
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);
|
||||
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
||||
log.info("[Combat] Schwerer Angriff Windup gestartet.");
|
||||
}
|
||||
|
||||
/** Feuert den Schaden bei Release; Animation läuft bereits seit Press. */
|
||||
private void handleHeavyAttack() {
|
||||
if (!weaponDrawn || drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
||||
return;
|
||||
}
|
||||
float mult = computeHeavyMultiplier();
|
||||
fireMeleeCallback(mult);
|
||||
log.info("[Combat] Schwerer Angriff ausgelöst | Haltezeit={}s Multiplikator={}x",
|
||||
String.format("%.2f", attackHoldTimer), String.format("%.2f", mult));
|
||||
}
|
||||
|
||||
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_DRAW;
|
||||
@@ -1403,4 +1659,104 @@ public class PlayerInputControl {
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ── Prozedurale Ziehen/Wegstecken-Animation ───────────────────────────────
|
||||
|
||||
private void startDrawAnimation(boolean withShield, Runnable onMid, Runnable onEnd) {
|
||||
if (drawAnimActive) return;
|
||||
drawAnimActive = true;
|
||||
drawAnimTimer = 0f;
|
||||
drawMidFired = false;
|
||||
drawMidCallback = onMid;
|
||||
drawEndCallback = onEnd;
|
||||
drawWithShield = withShield;
|
||||
if (animComposer == null || armature == null) {
|
||||
// Kein AnimComposer verfügbar: sofort Callbacks ausführen
|
||||
drawAnimActive = false;
|
||||
if (onMid != null) onMid.run();
|
||||
if (onEnd != null) onEnd.run();
|
||||
return;
|
||||
}
|
||||
// Start-Pose der $AssimpFbx$_Rotation-Nodes für SLERP sampeln
|
||||
drawStartArmQ = jointRightArmFbxRot != null
|
||||
? jointRightArmFbxRot.getLocalRotation().clone() : new Quaternion();
|
||||
drawStartForeArmQ = jointRightForeArmFbxRot != null
|
||||
? jointRightForeArmFbxRot.getLocalRotation().clone() : new Quaternion();
|
||||
drawStartLeftArmQ = jointLeftArmFbxRot != null
|
||||
? jointLeftArmFbxRot.getLocalRotation().clone() : new Quaternion();
|
||||
drawStartLeftForeArmQ = jointLeftForeArmFbxRot != null
|
||||
? jointLeftForeArmFbxRot.getLocalRotation().clone() : new Quaternion();
|
||||
try {
|
||||
ArmatureMask mask = ArmatureMask.createMask(armature, "mixamorig:RightArm");
|
||||
if (withShield && jointLeftArm != null) {
|
||||
mask.addFromJoint(armature, "mixamorig:LeftArm");
|
||||
}
|
||||
DrawArmTween tween = new DrawArmTween(withShield);
|
||||
BaseAction action = new BaseAction(tween);
|
||||
animComposer.addAction(DRAW_LAYER, action);
|
||||
animComposer.makeLayer(DRAW_LAYER, mask);
|
||||
animComposer.setCurrentAction(DRAW_LAYER, DRAW_LAYER);
|
||||
} catch (Exception e) {
|
||||
log.warn("[DrawAnim] Layer-Setup fehlgeschlagen: {}", e.getMessage());
|
||||
drawAnimActive = false;
|
||||
if (onMid != null) onMid.run();
|
||||
if (onEnd != null) onEnd.run();
|
||||
}
|
||||
}
|
||||
|
||||
private final class DrawArmTween implements Tween {
|
||||
private final boolean withShield;
|
||||
|
||||
DrawArmTween(boolean withShield) {
|
||||
this.withShield = withShield;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getLength() { return Double.MAX_VALUE; }
|
||||
|
||||
@Override
|
||||
public boolean interpolate(double t) {
|
||||
if (!drawAnimActive) return true;
|
||||
float progress = drawAnimTimer / DRAW_DURATION;
|
||||
if (progress <= 0f || progress >= 1f) return true;
|
||||
|
||||
// alpha: 0→1 beim Hochheben (0→50%), 1→0 beim Zurückkommen (50→100%)
|
||||
float alpha;
|
||||
if (progress < 0.5f) {
|
||||
float p = progress * 2f;
|
||||
alpha = p * p * (3f - 2f * p);
|
||||
} else {
|
||||
float p = (1f - progress) * 2f;
|
||||
alpha = p * p * (3f - 2f * p);
|
||||
}
|
||||
|
||||
// SLERP von Start-Pose (gesampelt beim Anim-Start) zur Wind-Up-Pose (aus Heavy Attack)
|
||||
if (jointRightArmFbxRot != null && drawStartArmQ != null) {
|
||||
Quaternion q = new Quaternion();
|
||||
q.slerp(drawStartArmQ, DRAW_ARM_PEAK_Q, alpha);
|
||||
jointRightArmFbxRot.setLocalRotation(q);
|
||||
}
|
||||
if (jointRightForeArmFbxRot != null && drawStartForeArmQ != null) {
|
||||
Quaternion q = new Quaternion();
|
||||
q.slerp(drawStartForeArmQ, DRAW_FOREARM_PEAK_Q, alpha);
|
||||
jointRightForeArmFbxRot.setLocalRotation(q);
|
||||
}
|
||||
if (withShield) {
|
||||
applyDelta(jointLeftArm, DRAW_L_ARM_DEG, alpha);
|
||||
applyDelta(jointLeftForeArm, DRAW_L_FOREARM_DEG, alpha);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void applyDelta(com.jme3.anim.Joint joint, float[] deg, float alpha) {
|
||||
if (joint == null) return;
|
||||
Quaternion delta = new Quaternion().fromAngles(
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * deg[0] * alpha,
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * deg[1] * alpha,
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * deg[2] * alpha
|
||||
);
|
||||
joint.setLocalRotation(delta.mult(joint.getLocalRotation()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +374,8 @@ public class WorldScene extends BaseAppState {
|
||||
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
|
||||
app.getStateManager().attach(
|
||||
new WorldInteractableState(keyBindings, physicsChar, playerInput));
|
||||
app.getStateManager().attach(
|
||||
new de.blight.game.state.WorldDummyState(physicsChar, playerInput, keyBindings));
|
||||
de.blight.common.MonologueRegistry.init(de.blight.common.MonologueIO.load());
|
||||
de.blight.game.state.DialogHudState dialogHud = new de.blight.game.state.DialogHudState();
|
||||
dialogHud.setMainCharacter(mc);
|
||||
|
||||
@@ -29,6 +29,7 @@ public class InteractionHudState extends BaseAppState {
|
||||
private WorldItemsState worldItems;
|
||||
private WorldInteractableState worldInteractables;
|
||||
private WorldNpcsState worldNpcs;
|
||||
private WorldDummyState worldDummies;
|
||||
private DialogHudState dialogHud;
|
||||
|
||||
private BitmapText labelText;
|
||||
@@ -55,6 +56,7 @@ public class InteractionHudState extends BaseAppState {
|
||||
worldItems = getStateManager().getState(WorldItemsState.class);
|
||||
worldInteractables = getStateManager().getState(WorldInteractableState.class);
|
||||
worldNpcs = getStateManager().getState(WorldNpcsState.class);
|
||||
worldDummies = getStateManager().getState(WorldDummyState.class);
|
||||
dialogHud = getStateManager().getState(DialogHudState.class);
|
||||
if (worldItems == null)
|
||||
log.warn("[InteractionHud] WorldItemsState nicht gefunden.");
|
||||
@@ -94,6 +96,15 @@ public class InteractionHudState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// Kampfpuppe (Aufstellen) hat Vorrang vor Interactable
|
||||
if (labelKey == null && worldDummies != null) {
|
||||
String key = worldDummies.getHoveredLabelKey();
|
||||
if (key != null && !key.isBlank()) {
|
||||
labelKey = key;
|
||||
worldPos = worldDummies.getHoveredWorldPos();
|
||||
}
|
||||
}
|
||||
|
||||
// Interactable hat Vorrang vor Item
|
||||
if (labelKey == null && worldInteractables != null) {
|
||||
String key = worldInteractables.getHoveredLabelKey();
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bullet.control.CharacterControl;
|
||||
import com.jme3.font.BitmapFont;
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.control.BillboardControl;
|
||||
import com.jme3.scene.shape.Box;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.ModelCategory;
|
||||
import de.blight.common.PlacedModel;
|
||||
import de.blight.common.PlacedModelIO;
|
||||
import de.blight.common.model.CombatDummy;
|
||||
import de.blight.common.model.CombatDummyIO;
|
||||
import de.blight.game.combat.DamageCalculator;
|
||||
import de.blight.game.combat.MeleeAttackCallback;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
import de.blight.game.control.PlayerInputControl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Verwaltet Kampfpuppen auf der Karte: HP-Tracking, Treffer-Erkennung,
|
||||
* Umfallen bei 0 HP, Aufstellen per Interaktion, Feind-HP-Bar oben.
|
||||
*/
|
||||
public class WorldDummyState extends BaseAppState implements MeleeAttackCallback {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WorldDummyState.class);
|
||||
|
||||
// ── Spielwelt-Konstanten ──────────────────────────────────────────────────
|
||||
private static final float ATTACK_RANGE = 2.5f;
|
||||
private static final float ATTACK_CONE_DOT = 0.2f;
|
||||
private static final float FOCUS_RANGE = 6f;
|
||||
private static final float RESTORE_DELAY = 60f;
|
||||
private static final float HP_BAR_SHOW_TIME = 5f;
|
||||
private static final float STAND_UP_RANGE = 3f;
|
||||
private static final int DEFAULT_MAX_HP = 250;
|
||||
private static final float FALL_DURATION = 0.35f;
|
||||
private static final float MODEL_HALF_H = 0.75f;
|
||||
|
||||
// ── Welt-HP-Bar-Konstanten ────────────────────────────────────────────────
|
||||
private static final float WB_WIDTH = 0.8f;
|
||||
private static final float WB_HEIGHT = 0.08f;
|
||||
|
||||
// ── Feind-HUD-Konstanten ──────────────────────────────────────────────────
|
||||
private static final float EHB_W = 200f;
|
||||
private static final float EHB_H = 18f;
|
||||
private static final float EHB_BORDER = 2f;
|
||||
private static final float EHB_Y_FROM_TOP = 55f;
|
||||
|
||||
private static final String INTERACT_ACTION = "InteractDummy";
|
||||
|
||||
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||
private final CharacterControl physicsChar;
|
||||
private final PlayerInputControl playerInput;
|
||||
private final KeyBindings keyBindings;
|
||||
|
||||
private AssetManager assetManager;
|
||||
private InputManager inputManager;
|
||||
private Node rootNode;
|
||||
private Node guiNode;
|
||||
private Camera cam;
|
||||
|
||||
// ── Laufzeit-Zustand ─────────────────────────────────────────────────────
|
||||
private final List<DummyRuntime> dummies = new ArrayList<>();
|
||||
private int hoveredDummyIdx = -1; // gefallene Puppe in Aufstell-Reichweite
|
||||
private int focusedDummyIdx = -1; // nächste Puppe in Fokus-Reichweite (Waffe gezückt)
|
||||
|
||||
// ── Feind-HUD ─────────────────────────────────────────────────────────────
|
||||
private Node enemyHudNode;
|
||||
private Geometry enemyHpFill;
|
||||
private BitmapText enemyNameText;
|
||||
private BitmapText enemyHpText;
|
||||
/** x-Koordinate des linken Randes der Feind-HP-Bar (Screen-Pixel). */
|
||||
private float ehbX;
|
||||
/** y-Koordinate des unteren Randes der Feind-HP-Bar (Screen-Pixel). */
|
||||
private float ehbY;
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public WorldDummyState(CharacterControl physicsChar, PlayerInputControl playerInput, KeyBindings kb) {
|
||||
this.physicsChar = physicsChar;
|
||||
this.playerInput = playerInput;
|
||||
this.keyBindings = kb;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
SimpleApplication sapp = (SimpleApplication) app;
|
||||
this.assetManager = app.getAssetManager();
|
||||
this.inputManager = app.getInputManager();
|
||||
this.rootNode = sapp.getRootNode();
|
||||
this.guiNode = sapp.getGuiNode();
|
||||
this.cam = app.getCamera();
|
||||
|
||||
loadDummies();
|
||||
buildEnemyHud();
|
||||
playerInput.setMeleeAttackCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
inputManager.addMapping(INTERACT_ACTION,
|
||||
new MouseButtonTrigger(MouseInput.BUTTON_LEFT),
|
||||
new KeyTrigger(keyBindings.interact));
|
||||
inputManager.addListener(standUpListener, INTERACT_ACTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
inputManager.removeListener(standUpListener);
|
||||
if (inputManager.hasMapping(INTERACT_ACTION)) {
|
||||
inputManager.deleteMapping(INTERACT_ACTION);
|
||||
}
|
||||
if (enemyHudNode != null) {
|
||||
enemyHudNode.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
for (DummyRuntime dr : dummies) {
|
||||
rootNode.detachChild(dr.dummyNode);
|
||||
}
|
||||
dummies.clear();
|
||||
if (enemyHudNode != null) {
|
||||
guiNode.detachChild(enemyHudNode);
|
||||
enemyHudNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Laden ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadDummies() {
|
||||
List<PlacedModel> models;
|
||||
try {
|
||||
models = PlacedModelIO.load();
|
||||
} catch (IOException e) {
|
||||
log.warn("[WorldDummy] PlacedModels nicht ladbar: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
for (PlacedModel m : models) {
|
||||
if (m.modelCategory() != ModelCategory.DUMMY) {
|
||||
continue;
|
||||
}
|
||||
int maxHp = DEFAULT_MAX_HP;
|
||||
if (m.interactableId() != null && !m.interactableId().isBlank()) {
|
||||
maxHp = CombatDummyIO.load(m.interactableId())
|
||||
.map(CombatDummy::getMaxHp)
|
||||
.orElse(DEFAULT_MAX_HP);
|
||||
}
|
||||
DummyRuntime dr = buildDummy(m, maxHp);
|
||||
if (dr != null) {
|
||||
dummies.add(dr);
|
||||
}
|
||||
}
|
||||
log.info("[WorldDummy] {} Kampfpuppen geladen.", dummies.size());
|
||||
}
|
||||
|
||||
private DummyRuntime buildDummy(PlacedModel m, int maxHp) {
|
||||
Node dummyNode = new Node("dummy_" + m.interactableId());
|
||||
|
||||
Spatial model = loadModel(m);
|
||||
dummyNode.attachChild(model);
|
||||
|
||||
Node hpBarNode = buildWorldHpBar();
|
||||
hpBarNode.setLocalTranslation(0f, MODEL_HALF_H * 2f + 0.2f, 0f);
|
||||
hpBarNode.setCullHint(Spatial.CullHint.Always);
|
||||
dummyNode.attachChild(hpBarNode);
|
||||
|
||||
dummyNode.setLocalTranslation(m.x(), m.y(), m.z());
|
||||
Quaternion rot = new Quaternion().fromAngles(m.rotX(), m.rotY(), m.rotZ());
|
||||
dummyNode.setLocalRotation(rot);
|
||||
dummyNode.setLocalScale(m.scale());
|
||||
rootNode.attachChild(dummyNode);
|
||||
|
||||
Geometry hpFg = (Geometry) ((Node) hpBarNode.getChild(0)).getChild(1);
|
||||
return new DummyRuntime(
|
||||
m.interactableId() != null ? m.interactableId() : "?",
|
||||
m.x(), m.y(), m.z(), dummyNode, hpBarNode, hpFg, maxHp);
|
||||
}
|
||||
|
||||
private Spatial loadModel(PlacedModel m) {
|
||||
String path = m.modelPath();
|
||||
if (path == null || path.isBlank() || "@box".equals(path) || "@sphere".equals(path)) {
|
||||
return buildBoxModel();
|
||||
}
|
||||
try {
|
||||
return assetManager.loadModel(path);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WorldDummy] Modell '{}' nicht ladbar, Fallback auf Box: {}", path, e.getMessage());
|
||||
return buildBoxModel();
|
||||
}
|
||||
}
|
||||
|
||||
private Geometry buildBoxModel() {
|
||||
Box box = new Box(0.15f, MODEL_HALF_H, 0.15f);
|
||||
Geometry geom = new Geometry("dummyBox", box);
|
||||
geom.setLocalTranslation(0f, MODEL_HALF_H, 0f);
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.55f, 0.35f, 0.15f, 1f));
|
||||
geom.setMaterial(mat);
|
||||
geom.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||
return geom;
|
||||
}
|
||||
|
||||
// ── Welt-HP-Bar (Billboard über Puppe) ────────────────────────────────────
|
||||
|
||||
private Node buildWorldHpBar() {
|
||||
Node hpBarNode = new Node("hpBar");
|
||||
Node billChild = new Node("hpBillboard");
|
||||
billChild.addControl(new BillboardControl());
|
||||
|
||||
Geometry bg = new Geometry("hpBg", new Quad(WB_WIDTH, WB_HEIGHT));
|
||||
bg.setLocalTranslation(-WB_WIDTH * 0.5f, -WB_HEIGHT * 0.5f, 0f);
|
||||
Material bgMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
bgMat.setColor("Color", new ColorRGBA(0.15f, 0.15f, 0.15f, 0.85f));
|
||||
bgMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
bg.setMaterial(bgMat);
|
||||
bg.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
billChild.attachChild(bg); // index 0
|
||||
|
||||
Geometry fg = new Geometry("hpFg", new Quad(WB_WIDTH, WB_HEIGHT - 0.01f));
|
||||
fg.setLocalTranslation(-WB_WIDTH * 0.5f, -WB_HEIGHT * 0.5f + 0.005f, 0.001f);
|
||||
Material fgMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
fgMat.setColor("Color", new ColorRGBA(0.2f, 0.8f, 0.2f, 1f));
|
||||
fg.setMaterial(fgMat);
|
||||
fg.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
billChild.attachChild(fg); // index 1
|
||||
|
||||
hpBarNode.attachChild(billChild);
|
||||
return hpBarNode;
|
||||
}
|
||||
|
||||
// ── Feind-HUD (oben, Screen-Space) ────────────────────────────────────────
|
||||
|
||||
private void buildEnemyHud() {
|
||||
float sw = cam.getWidth();
|
||||
float sh = cam.getHeight();
|
||||
ehbX = sw * 0.5f - (EHB_W + EHB_BORDER * 2) * 0.5f;
|
||||
ehbY = sh - EHB_Y_FROM_TOP - (EHB_H + EHB_BORDER * 2);
|
||||
|
||||
enemyHudNode = new Node("enemyHud");
|
||||
|
||||
// Hintergrund
|
||||
Geometry bg = solidRect("ehpBg",
|
||||
EHB_W + EHB_BORDER * 2, EHB_H + EHB_BORDER * 2,
|
||||
new ColorRGBA(0.08f, 0.08f, 0.08f, 0.85f));
|
||||
bg.setLocalTranslation(ehbX, ehbY, 1f);
|
||||
bg.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
enemyHudNode.attachChild(bg);
|
||||
|
||||
// Füllbalken (rot)
|
||||
Mesh fillMesh = new Mesh();
|
||||
buildFillMesh(fillMesh, EHB_W, 1f);
|
||||
Material fillMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
fillMat.setColor("Color", new ColorRGBA(0.85f, 0.10f, 0.10f, 1f));
|
||||
fillMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
enemyHpFill = new Geometry("ehpFill", fillMesh);
|
||||
enemyHpFill.setMaterial(fillMat);
|
||||
enemyHpFill.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
enemyHpFill.setLocalTranslation(ehbX + EHB_BORDER, ehbY + EHB_BORDER, 2f);
|
||||
enemyHudNode.attachChild(enemyHpFill);
|
||||
|
||||
BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt");
|
||||
float renderedSize = font.getCharSet().getRenderedSize();
|
||||
|
||||
// Gegner-Name (über Bar)
|
||||
enemyNameText = new BitmapText(font);
|
||||
enemyNameText.setSize(renderedSize * 2f);
|
||||
enemyNameText.setLocalScale(0.5f);
|
||||
enemyNameText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f));
|
||||
enemyNameText.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
enemyHudNode.attachChild(enemyNameText);
|
||||
|
||||
// HP-Zahlen (in Bar)
|
||||
enemyHpText = new BitmapText(font);
|
||||
enemyHpText.setSize(renderedSize * 2f);
|
||||
enemyHpText.setLocalScale(0.5f);
|
||||
enemyHpText.setColor(ColorRGBA.White);
|
||||
enemyHpText.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
enemyHudNode.attachChild(enemyHpText);
|
||||
|
||||
guiNode.attachChild(enemyHudNode);
|
||||
enemyHudNode.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
private Geometry solidRect(String name, float w, float h, ColorRGBA color) {
|
||||
Geometry g = new Geometry(name, new Quad(w, h));
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
g.setMaterial(mat);
|
||||
return g;
|
||||
}
|
||||
|
||||
/** Analoges Mesh wie in HudState: UV-geclippter Füllbalken. */
|
||||
private static void buildFillMesh(Mesh mesh, float w, float maxU) {
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3,
|
||||
BufferUtils.createFloatBuffer(0, 0, 0, w, 0, 0, w, EHB_H, 0, 0, EHB_H, 0));
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
|
||||
BufferUtils.createFloatBuffer(0f, 0f, maxU, 0f, maxU, 1f, 0f, 1f));
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3,
|
||||
BufferUtils.createShortBuffer((short) 0, (short) 1, (short) 2,
|
||||
(short) 0, (short) 2, (short) 3));
|
||||
mesh.setMode(Mesh.Mode.Triangles);
|
||||
mesh.updateBound();
|
||||
}
|
||||
|
||||
private void applyEnemyHpRatio(float ratio) {
|
||||
ratio = Math.max(0f, Math.min(1f, ratio));
|
||||
float w = EHB_W * ratio;
|
||||
Mesh mesh = enemyHpFill.getMesh();
|
||||
FloatBuffer pos = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.Position).getData();
|
||||
pos.put(3, w);
|
||||
pos.put(6, w);
|
||||
mesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
|
||||
FloatBuffer uv = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
|
||||
uv.put(2, ratio);
|
||||
uv.put(4, ratio);
|
||||
mesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
|
||||
mesh.updateBound();
|
||||
}
|
||||
|
||||
private void updateEnemyHudContent(DummyRuntime dr) {
|
||||
float sw = cam.getWidth();
|
||||
float ratio = dr.maxHp > 0 ? (float) dr.currentHp / dr.maxHp : 0f;
|
||||
applyEnemyHpRatio(ratio);
|
||||
|
||||
String name = "Kampfpuppe";
|
||||
enemyNameText.setText(name);
|
||||
float nameW = enemyNameText.getLineWidth() * 0.5f;
|
||||
enemyNameText.setLocalTranslation(
|
||||
sw * 0.5f - nameW,
|
||||
ehbY + EHB_H + EHB_BORDER * 2 + 6f, 3f);
|
||||
|
||||
String hpStr = dr.currentHp + " / " + dr.maxHp;
|
||||
enemyHpText.setText(hpStr);
|
||||
float hpW = enemyHpText.getLineWidth() * 0.5f;
|
||||
enemyHpText.setLocalTranslation(
|
||||
sw * 0.5f - hpW,
|
||||
ehbY + EHB_BORDER + 1f, 3f);
|
||||
}
|
||||
|
||||
// ── MeleeAttackCallback ───────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void onMeleeAttack(Vector3f charPos, Vector3f charForward, int baseDamage, float critMultiplier) {
|
||||
for (DummyRuntime dr : dummies) {
|
||||
if (dr.fallen) {
|
||||
continue;
|
||||
}
|
||||
float dx = dr.worldX - charPos.x;
|
||||
float dz = dr.worldZ - charPos.z;
|
||||
float distSq = dx * dx + dz * dz;
|
||||
if (distSq > ATTACK_RANGE * ATTACK_RANGE) {
|
||||
continue;
|
||||
}
|
||||
float len = (float) Math.sqrt(distSq);
|
||||
if (len > 0.05f) {
|
||||
float dot = (dx / len) * charForward.x + (dz / len) * charForward.z;
|
||||
if (dot < ATTACK_CONE_DOT) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Schaden / Zustand ─────────────────────────────────────────────────────
|
||||
|
||||
private void applyDamage(DummyRuntime dr, int damage) {
|
||||
dr.currentHp = Math.max(0, dr.currentHp - damage);
|
||||
dr.restoreTimer = 0f;
|
||||
dr.hpBarTimer = HP_BAR_SHOW_TIME;
|
||||
dr.hpBarNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
refreshWorldHpBar(dr);
|
||||
log.info("[Dummy {}] -{} HP → {}/{}{}", dr.id, damage, dr.currentHp, dr.maxHp,
|
||||
dr.currentHp <= 0 ? " K.O.!" : "");
|
||||
if (dr.currentHp <= 0) {
|
||||
knockout(dr);
|
||||
}
|
||||
}
|
||||
|
||||
private void knockout(DummyRuntime dr) {
|
||||
dr.fallen = true;
|
||||
dr.fallenLerp = 0f;
|
||||
dr.hpBarNode.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
private void standUp(DummyRuntime dr) {
|
||||
dr.fallen = false;
|
||||
dr.fallenLerp = 0f;
|
||||
dr.currentHp = dr.maxHp;
|
||||
dr.restoreTimer = 0f;
|
||||
dr.dummyNode.setLocalRotation(new Quaternion());
|
||||
dr.hpBarNode.setCullHint(Spatial.CullHint.Always);
|
||||
refreshWorldHpBar(dr);
|
||||
log.info("[Dummy {}] Aufgestellt.", dr.id);
|
||||
}
|
||||
|
||||
private void refreshWorldHpBar(DummyRuntime dr) {
|
||||
float frac = (float) dr.currentHp / dr.maxHp;
|
||||
ColorRGBA col;
|
||||
if (frac > 0.5f) col = new ColorRGBA(0.2f, 0.8f, 0.2f, 1f);
|
||||
else if (frac > 0.2f) col = new ColorRGBA(0.9f, 0.8f, 0.1f, 1f);
|
||||
else col = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f);
|
||||
dr.hpBarFg.getMaterial().setColor("Color", col);
|
||||
dr.hpBarFg.setLocalScale(Math.max(0.001f, frac), 1f, 1f);
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
Vector3f playerPos = (physicsChar != null) ? physicsChar.getPhysicsLocation() : null;
|
||||
boolean weaponOut = playerInput.isWeaponDrawn();
|
||||
hoveredDummyIdx = -1;
|
||||
focusedDummyIdx = -1;
|
||||
float bestFocusSq = FOCUS_RANGE * FOCUS_RANGE;
|
||||
|
||||
for (int i = 0; i < dummies.size(); i++) {
|
||||
DummyRuntime dr = dummies.get(i);
|
||||
|
||||
// Umfall-Animation
|
||||
if (dr.fallen && dr.fallenLerp < 1f) {
|
||||
dr.fallenLerp = Math.min(1f, dr.fallenLerp + tpf / FALL_DURATION);
|
||||
Quaternion target = new Quaternion().fromAngles(0f, 0f, -FastMath.HALF_PI);
|
||||
Quaternion cur = new Quaternion();
|
||||
cur.slerp(Quaternion.IDENTITY, target, dr.fallenLerp);
|
||||
dr.dummyNode.setLocalRotation(cur);
|
||||
}
|
||||
|
||||
// Welt-HP-Balken-Timer
|
||||
if (!dr.fallen && dr.hpBarTimer > 0f) {
|
||||
dr.hpBarTimer -= tpf;
|
||||
if (dr.hpBarTimer <= 0f) {
|
||||
dr.hpBarTimer = 0f;
|
||||
dr.hpBarNode.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Restore nach 1 Minute ohne Treffer
|
||||
if (!dr.fallen && dr.currentHp < dr.maxHp) {
|
||||
dr.restoreTimer += tpf;
|
||||
if (dr.restoreTimer >= RESTORE_DELAY) {
|
||||
dr.currentHp = dr.maxHp;
|
||||
dr.restoreTimer = 0f;
|
||||
dr.hpBarNode.setCullHint(Spatial.CullHint.Always);
|
||||
refreshWorldHpBar(dr);
|
||||
}
|
||||
}
|
||||
|
||||
if (playerPos == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float dx = dr.worldX - playerPos.x;
|
||||
float dz = dr.worldZ - playerPos.z;
|
||||
float distSq = dx * dx + dz * dz;
|
||||
|
||||
// Fokus (Waffe gezückt): nächste Puppe in Reichweite
|
||||
if (weaponOut && distSq < bestFocusSq) {
|
||||
bestFocusSq = distSq;
|
||||
focusedDummyIdx = i;
|
||||
}
|
||||
|
||||
// Hover (Waffe weggesteckt): gefallene Puppe in Aufstell-Reichweite
|
||||
if (!weaponOut && dr.fallen && dr.fallenLerp >= 1f
|
||||
&& distSq <= STAND_UP_RANGE * STAND_UP_RANGE) {
|
||||
hoveredDummyIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Feind-HP-Bar (oben) aktualisieren
|
||||
if (weaponOut && focusedDummyIdx >= 0 && enemyHudNode != null) {
|
||||
updateEnemyHudContent(dummies.get(focusedDummyIdx));
|
||||
enemyHudNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
} else if (enemyHudNode != null) {
|
||||
enemyHudNode.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Interaktion-HUD-Schnittstelle ─────────────────────────────────────────
|
||||
|
||||
/** Gibt den Label-Schlüssel für die InteractionHudState-Anzeige zurück. */
|
||||
public String getHoveredLabelKey() {
|
||||
if (playerInput.isWeaponDrawn()) {
|
||||
return focusedDummyIdx >= 0 ? "dummy.name" : null;
|
||||
}
|
||||
return hoveredDummyIdx >= 0 ? "dummy.stand_up" : null;
|
||||
}
|
||||
|
||||
/** Gibt die Weltposition zurück, über der das HUD-Label angezeigt wird. */
|
||||
public Vector3f getHoveredWorldPos() {
|
||||
if (playerInput.isWeaponDrawn()) {
|
||||
if (focusedDummyIdx < 0) {
|
||||
return null;
|
||||
}
|
||||
DummyRuntime dr = dummies.get(focusedDummyIdx);
|
||||
return new Vector3f(dr.worldX, dr.worldY + MODEL_HALF_H * 2f + 0.15f, dr.worldZ);
|
||||
}
|
||||
if (hoveredDummyIdx < 0) {
|
||||
return null;
|
||||
}
|
||||
DummyRuntime dr = dummies.get(hoveredDummyIdx);
|
||||
return new Vector3f(dr.worldX, dr.worldY + 0.5f, dr.worldZ);
|
||||
}
|
||||
|
||||
// ── Eingabe ───────────────────────────────────────────────────────────────
|
||||
|
||||
private final ActionListener standUpListener = (name, isPressed, tpf2) -> {
|
||||
if (!isPressed) {
|
||||
return;
|
||||
}
|
||||
// hoveredDummyIdx wird nur gesetzt wenn Waffe weggesteckt + Puppe in Reichweite
|
||||
if (hoveredDummyIdx >= 0) {
|
||||
standUp(dummies.get(hoveredDummyIdx));
|
||||
}
|
||||
};
|
||||
|
||||
// ── Innere Klasse ─────────────────────────────────────────────────────────
|
||||
|
||||
private static class DummyRuntime {
|
||||
final String id;
|
||||
final float worldX, worldY, worldZ;
|
||||
final Node dummyNode;
|
||||
final Node hpBarNode;
|
||||
final Geometry hpBarFg;
|
||||
int currentHp;
|
||||
final int maxHp;
|
||||
float restoreTimer = 0f;
|
||||
float hpBarTimer = 0f;
|
||||
boolean fallen = false;
|
||||
float fallenLerp = 0f;
|
||||
|
||||
DummyRuntime(String id, float wx, float wy, float wz,
|
||||
Node dummyNode, Node hpBarNode, Geometry hpBarFg, int maxHp) {
|
||||
this.id = id;
|
||||
this.worldX = wx;
|
||||
this.worldY = wy;
|
||||
this.worldZ = wz;
|
||||
this.dummyNode = dummyNode;
|
||||
this.hpBarNode = hpBarNode;
|
||||
this.hpBarFg = hpBarFg;
|
||||
this.maxHp = maxHp;
|
||||
this.currentHp = maxHp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ public class WorldInteractableState extends BaseAppState {
|
||||
// ── Hover-Erkennung ───────────────────────────────────────────────────────
|
||||
|
||||
private void updateHover() {
|
||||
if (phase != Phase.IDLE || cam == null) {
|
||||
if (phase != Phase.IDLE || cam == null || playerInput.isWeaponDrawn()) {
|
||||
hoveredIdx = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ public class WorldObjectsState extends BaseAppState {
|
||||
Node root = app.getRootNode();
|
||||
int loaded = 0, failed = 0;
|
||||
for (PlacedModel m : models) {
|
||||
if (m.modelCategory() == de.blight.common.ModelCategory.DUMMY) {
|
||||
continue; // Kampfpuppen werden von WorldDummyState verwaltet
|
||||
}
|
||||
try {
|
||||
Spatial s = buildSpatial(m);
|
||||
s.setLocalTranslation(m.x(), m.y(), m.z());
|
||||
|
||||
Reference in New Issue
Block a user