Schwert und Schild kampfsystem weiter ausgebaut
This commit is contained in:
@@ -87,7 +87,7 @@ public class AnimPlayer {
|
||||
}
|
||||
if (skinning != null && !skinning.isEnabled()) skinning.setEnabled(true);
|
||||
|
||||
if ((currentClip != null || pendingClip != null) && transition > 0f) {
|
||||
if (!pendingReversed && (currentClip != null || pendingClip != null) && transition > 0f) {
|
||||
com.jme3.anim.Armature arm = skinning != null ? skinning.getArmature() : null;
|
||||
AnimClip freeze = buildFreezeClip(arm);
|
||||
if (freeze != null) {
|
||||
@@ -182,6 +182,9 @@ public class AnimPlayer {
|
||||
currentClip = clipName;
|
||||
pendingClip = null;
|
||||
blendSpace = null;
|
||||
// Speed immer auf 1 zurücksetzen – ClipAction-Instanz wird wiederverwendet
|
||||
// und könnte noch speed=-1 vom letzten reversed-Play haben.
|
||||
if (currentAction != null) { currentAction.setSpeed(1f); }
|
||||
double len = currentAction != null ? currentAction.getLength() : -1;
|
||||
if (pendingReversed && currentAction != null && len > 0.01) {
|
||||
currentAction.setSpeed(-1f);
|
||||
@@ -206,24 +209,25 @@ public class AnimPlayer {
|
||||
|
||||
private void finishBlend() {
|
||||
String clip = pendingClip;
|
||||
float elapsed = blendElapsed;
|
||||
pendingClip = null;
|
||||
blendSpace = null;
|
||||
if (clip == null) return;
|
||||
try {
|
||||
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
// setCurrentAction vor removeAction: kein 1-Frame-Gap ohne aktive Action
|
||||
currentAction = composer.setCurrentAction(clip);
|
||||
currentClip = clip;
|
||||
try {
|
||||
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
||||
} catch (Exception ignored) {}
|
||||
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
||||
if (pendingReversed && currentAction != null && len > 0.01) {
|
||||
currentAction.setSpeed(-1f);
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
|
||||
pendingReversed = false;
|
||||
} else if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
|
||||
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
||||
float t = (pendingStartOffset + elapsed) % (float) len;
|
||||
} else if (len > 0.01 && (blendDuration > 0 || pendingStartOffset > 0)) {
|
||||
// blendDuration statt blendElapsed: blendElapsed hat bereits das aktuelle tpf
|
||||
// eingerechnet, die Sub-Action im AnimComposer aber noch nicht → 1-Frame exakt.
|
||||
float t = (pendingStartOffset + blendDuration) % (float) len;
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
|
||||
pendingStartOffset = 0f;
|
||||
}
|
||||
|
||||
@@ -42,20 +42,20 @@ public enum AnimationAction {
|
||||
|
||||
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||
SWORD_IDLE_BORED, SWORD_IDLE_BORED_2, SWORD_IDLE_BORED_3, SWORD_DEATH, SWORD_DEATH_2,
|
||||
SWORD_ATTACK_1, SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,SWORD_COUNTERATTACK, SWORD_IDLE_IMPACT,
|
||||
SWORD_ATTACK_1, SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK, SWORD_PARADE, SWORD_IDLE_IMPACT,
|
||||
SWORD_IDLE, SWORD_WALK, SWORD_RUN, SWORD_JUMP, SWORD_RUNNING_JUMP,
|
||||
SWORD_BLOCK, SWORD_IDLE_TO_BLOCK, SWORD_BLOCK_IMPACT, SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT,
|
||||
|
||||
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
|
||||
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
|
||||
AXE_IDLE_TO_BLOCK, AXE_COUNTERATTACK, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT,
|
||||
AXE_IDLE_TO_BLOCK, AXE_PARADE, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT,
|
||||
AXE_IDLE, AXE_WALK, AXE_RUN,
|
||||
|
||||
// ── Zweihänder ────────────────────────────────────────────────────────────
|
||||
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK,
|
||||
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
|
||||
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_COUNTERATTACK, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
|
||||
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_PARADE, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
|
||||
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
||||
|
||||
// ── Bogen ─────────────────────────────────────────────────────────────────
|
||||
@@ -119,7 +119,7 @@ public enum AnimationAction {
|
||||
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
||||
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
||||
case SWORD_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Schwert)");
|
||||
case SWORD_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Schwert)");
|
||||
case SWORD_PARADE -> TextRegistry.resolve(null, key, "Parade (Schwert)");
|
||||
case SWORD_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Schwert)");
|
||||
case SWORD_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
||||
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||
@@ -138,7 +138,7 @@ public enum AnimationAction {
|
||||
case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
|
||||
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
||||
case AXE_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Axt)");
|
||||
case AXE_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Axt)");
|
||||
case AXE_PARADE -> TextRegistry.resolve(null, key, "Parade (Axt)");
|
||||
case AXE_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Axt)");
|
||||
case AXE_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
||||
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
||||
@@ -153,7 +153,7 @@ public enum AnimationAction {
|
||||
case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
|
||||
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
||||
case TWO_HAND_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Zweihänder)");
|
||||
case TWO_HAND_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Zweihänder)");
|
||||
case TWO_HAND_PARADE -> TextRegistry.resolve(null, key, "Parade (Zweihänder)");
|
||||
case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)");
|
||||
case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
||||
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package de.blight.game.combat;
|
||||
|
||||
import com.jme3.anim.AnimComposer;
|
||||
import com.jme3.math.Vector3f;
|
||||
import de.blight.game.animation.AnimationAction;
|
||||
|
||||
/**
|
||||
* Abstrakte Basisklasse für alle Kampfstile.
|
||||
*
|
||||
* Enthält:
|
||||
* – das {@link AnimReceiver}-Interface zum einheitlichen Empfangen von
|
||||
* Kampf-Animationen (Stagger, Impact) auf Spieler- UND NPC-Seite
|
||||
* – gemeinsame Hilfsmethoden (Clip-Länge lesen, Klingen-Treffertest)
|
||||
* – gemeinsame Konstanten (Treffer-Radius, Impact-Speeds)
|
||||
*
|
||||
* Konkrete Kampfstile (z. B. {@code swordshield.SwordShieldCombat}) erben
|
||||
* von dieser Klasse und ergänzen stil-spezifische Animations-Auflösung.
|
||||
*/
|
||||
public abstract class CombatStyleBase {
|
||||
|
||||
// ── Gemeinsame Konstanten ─────────────────────────────────────────────────
|
||||
|
||||
/** Abspielgeschwindigkeit für normalen Treffer-Impact. */
|
||||
public static final float IMPACT_SPEED_NORMAL = 1.0f;
|
||||
/** Abspielgeschwindigkeit nach Konterparade (Slow-Motion-Reaktion). */
|
||||
public static final float IMPACT_SPEED_PARRIED = 0.5f;
|
||||
|
||||
/** Körperradius eines NPCs für die Klingen-Kapsel-Trefferprüfung. */
|
||||
public static final float HIT_RADIUS = 0.5f;
|
||||
|
||||
// ── AnimReceiver-Interface ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Einheitliche Schnittstelle für Entitäten, die eine blockierende
|
||||
* Kampfanimation empfangen können – implementiert von
|
||||
* {@code PlayerInputControl} (Held) und NPC-Adaptern in
|
||||
* {@code WorldNpcsState}.
|
||||
*
|
||||
* Eine „blockierende" Animation sperrt die Entität für die Dauer des
|
||||
* Clips von weiteren eigenen Aktionen (Bewegung, Angriff usw.).
|
||||
*
|
||||
* @see CombatStyleBase#readActualClipLength(AnimComposer, float)
|
||||
*/
|
||||
public interface AnimReceiver {
|
||||
/**
|
||||
* Spielt {@code action} ab und sperrt die Entität für die Clip-Dauer.
|
||||
*
|
||||
* @param action die abzuspielende Animation
|
||||
* @param speed Abspielgeschwindigkeit (1.0 = normal, 0.5 = Konterparade)
|
||||
* @param onDone optionaler Callback wenn die Animation endet (darf null sein)
|
||||
*/
|
||||
void receiveBlockingAnim(AnimationAction action, float speed, Runnable onDone);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Liest die tatsächliche Clip-Länge aus dem AnimComposer des DEFAULT_LAYER.
|
||||
* Muss <em>nach</em> dem Starten der Animation aufgerufen werden, damit
|
||||
* die korrekte Länge im Composer steht.
|
||||
*
|
||||
* @param ac AnimComposer der Entität (darf null sein)
|
||||
* @param fallback Rückgabewert wenn Länge nicht ermittelbar
|
||||
* @return tatsächliche Clip-Länge oder {@code fallback}
|
||||
*/
|
||||
public static float readActualClipLength(AnimComposer ac, float fallback) {
|
||||
if (ac == null) { return fallback; }
|
||||
var layer = ac.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||
if (layer == null) { return fallback; }
|
||||
var a = layer.getCurrentAction();
|
||||
return (a != null && a.getLength() > 0.01) ? (float) a.getLength() : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quadratischer Abstand von Punkt {@code p} zum Segment {@code a→b}.
|
||||
* Wird für die Klingen-Kapsel-Trefferprüfung in allen Kampfstilen genutzt.
|
||||
*/
|
||||
public static float segmentDistSq(Vector3f p, Vector3f a, Vector3f b) {
|
||||
float abX = b.x - a.x, abY = b.y - a.y, abZ = b.z - a.z;
|
||||
float lenSq = abX*abX + abY*abY + abZ*abZ;
|
||||
float t = lenSq > 0.0001f
|
||||
? Math.max(0f, Math.min(1f, ((p.x-a.x)*abX + (p.y-a.y)*abY + (p.z-a.z)*abZ) / lenSq))
|
||||
: 0f;
|
||||
float cx = p.x - (a.x + t*abX);
|
||||
float cy = p.y - (a.y + t*abY);
|
||||
float cz = p.z - (a.z + t*abZ);
|
||||
return cx*cx + cy*cy + cz*cz;
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,14 @@ public interface MeleeAttackCallback {
|
||||
*/
|
||||
void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir,
|
||||
float bladeLength, int baseDamage, float critMultiplier);
|
||||
|
||||
/**
|
||||
* Wird beim START eines normalen (nicht-schweren) Angriffs aufgerufen,
|
||||
* damit NPCs die Konterparade-Entscheidung sofort treffen und die
|
||||
* Parade-Animation frühzeitig starten können.
|
||||
*
|
||||
* @param attackId Gleiche ID wie beim nachfolgenden onMeleeAttack
|
||||
* @param isHeavy true = schwerer Angriff (kein Konterparade-Kandidat)
|
||||
*/
|
||||
default void onAttackStart(int attackId, boolean isHeavy) {}
|
||||
}
|
||||
|
||||
@@ -94,11 +94,12 @@ public final class WeaponCombatState {
|
||||
|
||||
/**
|
||||
* Entscheidet nach dem aktuellen Angriffsschritt ob die Kombo verlängert wird.
|
||||
* Gibt den nächsten Schritt (1 oder 2) zurück, oder -1 wenn keine Verlängerung.
|
||||
* Gleiche 5-stufige Sequenz wie beim Spieler: 0→1→2→3→4 (gerade=ATK1, ungerade=ATK2, 4=Finisher).
|
||||
* Gibt den nächsten Schritt zurück, oder -1 wenn keine Verlängerung.
|
||||
*/
|
||||
public int nextComboStep(java.util.Random rng) {
|
||||
if (phase != Phase.ATTACKING || isHeavy || comboStep >= 2) { return -1; }
|
||||
float chance = comboStep == 0 ? COMBO_P1 : COMBO_P2;
|
||||
if (phase != Phase.ATTACKING || isHeavy || comboStep >= 4) { return -1; }
|
||||
float chance = (comboStep % 2 == 0) ? COMBO_P1 : COMBO_P2;
|
||||
return rng.nextFloat() < chance ? comboStep + 1 : -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.blight.game.combat.swordshield;
|
||||
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.game.animation.AnimationAction;
|
||||
import de.blight.game.combat.CombatStyleBase;
|
||||
|
||||
/**
|
||||
* Gemeinsame Kampflogik für den Schwert-und-Schild-Stil.
|
||||
*
|
||||
* Wird von <em>beiden</em> Seiten genutzt:
|
||||
* – {@code PlayerInputControl} (Held)
|
||||
* – {@code WorldNpcsState} (Gegner-KI)
|
||||
*
|
||||
* Alle Animations-Auflösungen (welche Animation für welches Ereignis?) und
|
||||
* alle Reaktions-Anwendungen (Impact, Stagger, Konterparade) sind hier
|
||||
* zentralisiert. Bug-Fixes gelten damit automatisch für Held und NPC.
|
||||
*
|
||||
* Neue Kampfstile erben von {@link CombatStyleBase} und folgen demselben
|
||||
* Muster in einem eigenen Unter-Package.
|
||||
*/
|
||||
public final class SwordShieldCombat extends CombatStyleBase {
|
||||
|
||||
private SwordShieldCombat() {}
|
||||
|
||||
// ── Animations-Auflösung ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Impact-Animation in Idle/Kampf-Haltung (Entität wurde getroffen).
|
||||
* Identisch für Spieler und NPC – weapon type der GETROFFENEN Entität.
|
||||
*/
|
||||
public static AnimationAction resolveIdleImpactAction(ItemSubCategory weaponType) {
|
||||
if (weaponType == null) { return AnimationAction.SWORD_IDLE_IMPACT; }
|
||||
return switch (weaponType) {
|
||||
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
|
||||
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Block-Impact-Animation (Entität hat einen Treffer abgeblockt).
|
||||
* Identisch für Spieler und NPC.
|
||||
*/
|
||||
public static AnimationAction resolveBlockImpactAction(ItemSubCategory weaponType) {
|
||||
if (weaponType == null) { return AnimationAction.SWORD_BLOCK_IMPACT; }
|
||||
return switch (weaponType) {
|
||||
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
|
||||
default -> AnimationAction.SWORD_BLOCK_IMPACT;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Konterangriff-Animation nach erfolgreicher Parade.
|
||||
* Identisch für Spieler und NPC.
|
||||
*/
|
||||
public static AnimationAction resolveParadeAction(ItemSubCategory weaponType) {
|
||||
if (weaponType == null) { return AnimationAction.SWORD_PARADE; }
|
||||
return switch (weaponType) {
|
||||
case AXE -> AnimationAction.AXE_PARADE;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_PARADE;
|
||||
default -> AnimationAction.SWORD_PARADE;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Reaktionen anwenden ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Normaler Treffer-Impact (Entität wurde getroffen, kein Block).
|
||||
* Für Held: {@code PlayerInputControl} implementiert {@link AnimReceiver}.
|
||||
* Für NPC: {@code WorldNpcsState.npcAnimReceiver(s)} liefert den Adapter.
|
||||
*/
|
||||
public static void applyNormalImpact(AnimReceiver receiver, ItemSubCategory weaponType) {
|
||||
receiver.receiveBlockingAnim(resolveIdleImpactAction(weaponType), IMPACT_SPEED_NORMAL, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konterparade-Impact (Angreifer wird durch Parade gestoppt – Slow Motion).
|
||||
* Wird aufgerufen auf dem ANGREIFER, nicht auf dem Parierenden.
|
||||
*/
|
||||
public static void applyParriedImpact(AnimReceiver receiver, ItemSubCategory weaponType) {
|
||||
receiver.receiveBlockingAnim(resolveIdleImpactAction(weaponType), IMPACT_SPEED_PARRIED, null);
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,10 @@ 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.CombatStyleBase;
|
||||
import de.blight.game.combat.MeleeAttackCallback;
|
||||
import de.blight.game.combat.WeaponCombatState;
|
||||
import de.blight.game.combat.swordshield.SwordShieldCombat;
|
||||
import de.blight.game.navigation.CharacterNavigator;
|
||||
import de.blight.game.state.TerrainChunkState;
|
||||
|
||||
@@ -44,7 +46,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PlayerInputControl {
|
||||
public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
||||
|
||||
@@ -133,8 +135,11 @@ public class PlayerInputControl {
|
||||
private ItemSubCategory drawnWeaponType = null;
|
||||
private boolean isAiming = false;
|
||||
private boolean rightMouseHeld = false;
|
||||
private boolean attackBuffered = false; // LMB während Block → nach Block-Exit sofort angreifen
|
||||
private float prevBlockAnimTime = 0f; // Vorzustand für Loop-Erkennung in ENTERING
|
||||
private BlockPhase blockPhase = BlockPhase.NONE;
|
||||
private float blockPhaseTimer = 0f;
|
||||
private float blockTapTimer = 0f;
|
||||
/** Gemeinsamer Zustandsautomat (Tap/Hold-Erkennung, parryArmed). */
|
||||
private final WeaponCombatState weaponCombat = new WeaponCombatState();
|
||||
private boolean combatAnimActive = false;
|
||||
@@ -156,6 +161,12 @@ public class PlayerInputControl {
|
||||
private float heavyLungeStartRemaining = 0f;
|
||||
/** combatAnimRemaining-Wert, ab dem die Lunge endet (Landung). */
|
||||
private float heavyLungeEndRemaining = 0f;
|
||||
// ── Spieler-Rückstoß (wenn getroffen oder Block absorbiert) ───────────────
|
||||
private float playerKnockbackDirX = 0f;
|
||||
private float playerKnockbackDirZ = 0f;
|
||||
private float playerKnockbackTimer = 0f;
|
||||
private static final float PLAYER_KNOCKBACK_MPS = 1.2f;
|
||||
private static final float PLAYER_KNOCKBACK_TIME = 0.18f;
|
||||
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
|
||||
private float combatAnimDuration = 0f;
|
||||
private final List<MeleeAttackCallback> meleeCallbacks = new ArrayList<>();
|
||||
@@ -167,6 +178,8 @@ public class PlayerInputControl {
|
||||
private int currentAttackId = -1;
|
||||
private boolean hitWindowActive = false;
|
||||
private float hitWindowCritMult = 1.0f;
|
||||
private float hitWindowStart = HIT_WINDOW_START;
|
||||
private float hitWindowEnd = HIT_WINDOW_END;
|
||||
// Angriffs-Sound via Waffen-Geschwindigkeit
|
||||
private static final float ATTACK_SOUND_VELOCITY = 3.5f;
|
||||
// Attacken mit Backswing → Sound erst nach Mindest-Fortschritt erlauben
|
||||
@@ -282,6 +295,13 @@ public class PlayerInputControl {
|
||||
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
|
||||
private Vector3f autopilotDir = null;
|
||||
|
||||
/** Position des nächsten angreifenden NPCs – für Lungen-Richtung bei schwerem Angriff. */
|
||||
private Vector3f nearestAttackingNpcPos = null;
|
||||
|
||||
public void setNearestAttackingNpcPos(Vector3f pos) {
|
||||
this.nearestAttackingNpcPos = pos != null ? pos.clone() : null;
|
||||
}
|
||||
|
||||
// ── Schleichen / Ducken ───────────────────────────────────────────────────
|
||||
private enum SneakPhase { NONE, DOWN, ACTIVE, UP }
|
||||
private SneakPhase sneakPhase = SneakPhase.NONE;
|
||||
@@ -326,7 +346,8 @@ public class PlayerInputControl {
|
||||
case ACT_SHEATH -> { if (isPressed && weaponDrawn && !blockingAnimActive && !drawAnimActive) sheathCurrentWeapon(); }
|
||||
case ACT_COMBAT_ATK -> {
|
||||
if (isPressed) {
|
||||
if (!weaponDrawn || blockingAnimActive || drawAnimActive || isBlocking()) break;
|
||||
if (!weaponDrawn || blockingAnimActive || drawAnimActive) break;
|
||||
if (isBlocking()) { attackBuffered = true; break; }
|
||||
// Immer Hold-Timer starten – auch mitten in einer Kombo
|
||||
attackHoldPending = true;
|
||||
attackHoldTimer = 0f;
|
||||
@@ -471,6 +492,7 @@ public class PlayerInputControl {
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
stopWading();
|
||||
rightMouseHeld = false;
|
||||
attackBuffered = false;
|
||||
blockPhase = BlockPhase.NONE;
|
||||
weaponCombat.phase = WeaponCombatState.Phase.IDLE;
|
||||
weaponCombat.parryArmed = false;
|
||||
@@ -519,19 +541,70 @@ public class PlayerInputControl {
|
||||
* Sekunden und ruft danach {@code onComplete} auf (im Update-Thread).
|
||||
* Bei {@code duration <= 0} wird die echte Clip-Länge aus dem AnimComposer abgefragt.
|
||||
*/
|
||||
/** Implementierung von {@link de.blight.game.combat.CombatStyleBase.AnimReceiver}. */
|
||||
@Override
|
||||
public void receiveBlockingAnim(AnimationAction action, float speed, Runnable onDone) {
|
||||
requestAnimation(action, 0f, speed, onDone);
|
||||
}
|
||||
|
||||
public void requestAnimation(AnimationAction action, float duration, Runnable onComplete) {
|
||||
requestAnimation(action, duration, 1f, onComplete);
|
||||
}
|
||||
|
||||
public void requestAnimation(AnimationAction action, float duration, float speed, Runnable onComplete) {
|
||||
if (playerDead) { return; }
|
||||
// Laufende Kampf-Animation sofort abbrechen – sonst blockiert blockingAnimActive den
|
||||
// combatAnimActive-Timer nicht (blockingAnimActive-Return kommt früher in controlUpdate).
|
||||
if (combatAnimActive) {
|
||||
combatAnimActive = false;
|
||||
hitWindowActive = false;
|
||||
hitWindowStart = HIT_WINDOW_START;
|
||||
hitWindowEnd = HIT_WINDOW_END;
|
||||
heavyLungeVel = null;
|
||||
heavyLungeStartRemaining = 0f;
|
||||
heavyLungeEndRemaining = 0f;
|
||||
combatAnimCallback = null;
|
||||
combatAnimSkipComboWindow = false;
|
||||
comboWindowActive = false;
|
||||
comboStep = 0;
|
||||
}
|
||||
autopilotDir = null;
|
||||
forward = backward = left = right = false;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
// Blend-Übergänge für blockierende Animationen (Impact, Stagger) verbieten:
|
||||
// ein noch aktiver nextTransitionLength aus einem Kombo-Schlag würde eine BlendAction
|
||||
// erzeugen deren getLength() 0 zurückgibt → Fallback-Timer zu lang → Animation loopt.
|
||||
nextTransitionLength = -1.0;
|
||||
playAction(action);
|
||||
currentAnim = action;
|
||||
// Clip-Länge zuverlässig aus dem AnimClip lesen (nicht von der live BlendAction)
|
||||
if (duration <= 0f) {
|
||||
duration = resolveClipLength(action, 1.5f);
|
||||
}
|
||||
if (speed != 1f && animComposer != null) {
|
||||
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||
if (layer != null) {
|
||||
var a = layer.getCurrentAction();
|
||||
if (a != null) { a.setSpeed(speed); }
|
||||
}
|
||||
duration = duration / speed; // Timer an Speed anpassen
|
||||
}
|
||||
// Rückstoß weg vom angreifenden NPC
|
||||
if (nearestAttackingNpcPos != null && physicsChar != null) {
|
||||
Vector3f myPos = physicsChar.getPhysicsLocation();
|
||||
float dx = myPos.x - nearestAttackingNpcPos.x;
|
||||
float dz = myPos.z - nearestAttackingNpcPos.z;
|
||||
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0.001f) {
|
||||
playerKnockbackDirX = dx / len;
|
||||
playerKnockbackDirZ = dz / len;
|
||||
playerKnockbackTimer = PLAYER_KNOCKBACK_TIME;
|
||||
}
|
||||
}
|
||||
blockingAnimActive = true;
|
||||
blockingAnimRemaining = duration;
|
||||
blockingAnimTotal = duration;
|
||||
blockingAnimRemaining = Math.max(0.1f, duration - 0.05f);
|
||||
blockingAnimTotal = blockingAnimRemaining;
|
||||
blockingAnimCallback = onComplete;
|
||||
autopilotDir = null;
|
||||
forward = backward = left = right = false;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
playAction(action);
|
||||
currentAnim = action;
|
||||
}
|
||||
|
||||
/** Liefert die Länge des Clips für {@code action} in Sekunden, oder {@code fallback} wenn nicht ermittelbar. */
|
||||
@@ -872,13 +945,41 @@ public class PlayerInputControl {
|
||||
// Blockierende Einmal-Animation (lie_down, lie_up …)
|
||||
if (blockingAnimActive) {
|
||||
blockingAnimRemaining -= tpf;
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
if (playerKnockbackTimer > 0f) {
|
||||
playerKnockbackTimer -= tpf;
|
||||
float ratio = Math.max(0f, playerKnockbackTimer / PLAYER_KNOCKBACK_TIME);
|
||||
float spd = PLAYER_KNOCKBACK_MPS / CharacterNavigator.RUN_SPEED_MPS * MOVE_SPEED;
|
||||
physicsChar.setWalkDirection(scratchWalk.set(
|
||||
playerKnockbackDirX * spd * ratio, 0f, playerKnockbackDirZ * spd * ratio));
|
||||
} else {
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
if (blockingAnimRemaining <= 0f && !playerDead) {
|
||||
blockingAnimActive = false;
|
||||
Runnable cb = blockingAnimCallback;
|
||||
blockingAnimCallback = null;
|
||||
if (cb != null) cb.run();
|
||||
// Block-Puffer nach Impact/Stagger: RMB wurde während der Anim gehalten
|
||||
if (!combatAnimActive && rightMouseHeld && blockPhase == BlockPhase.NONE) {
|
||||
blockTapTimer = 0f;
|
||||
weaponCombat.parryArmed = false;
|
||||
startBlockEnter();
|
||||
}
|
||||
} else if (playerDead && blockingAnimRemaining <= 0f && blockingAnimRemaining > -500f) {
|
||||
// Todesanim abgelaufen → auf letztem Frame einfrieren
|
||||
if (animComposer != null) {
|
||||
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||
if (layer != null) {
|
||||
com.jme3.anim.tween.action.Action a = layer.getCurrentAction();
|
||||
if (a != null && a.getSpeed() != 0f) {
|
||||
a.setSpeed(0f);
|
||||
layer.setTime(Math.max(0f, (float) a.getLength() - 0.01f));
|
||||
}
|
||||
}
|
||||
}
|
||||
blockingAnimRemaining = -1000f;
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -942,8 +1043,7 @@ 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;
|
||||
boolean inBlockStance = isBlocking();
|
||||
|
||||
boolean sneaking = sneakPhase != SneakPhase.NONE;
|
||||
|
||||
@@ -1029,6 +1129,12 @@ public class PlayerInputControl {
|
||||
comboStep = 0;
|
||||
log.info("[Combat] Kombo-Fenster verpasst → Reset");
|
||||
}
|
||||
// Gepufferter Block: RMB wurde während des Angriffs gehalten → jetzt Block einleiten
|
||||
if (!combatAnimActive && rightMouseHeld && blockPhase == BlockPhase.NONE) {
|
||||
blockTapTimer = 0f;
|
||||
weaponCombat.parryArmed = false;
|
||||
startBlockEnter();
|
||||
}
|
||||
} else {
|
||||
// Kombo-Fenster öffnet sich in der zweiten Hälfte der Anim
|
||||
if (!comboWindowActive && !combatAnimSkipComboWindow
|
||||
@@ -1060,9 +1166,9 @@ public class PlayerInputControl {
|
||||
}
|
||||
if (hitWindowActive && isMeleeType(drawnWeaponType) && combatAnimDuration > 0f) {
|
||||
float t = (combatAnimDuration - combatAnimRemaining) / combatAnimDuration;
|
||||
if (t >= HIT_WINDOW_START && t <= HIT_WINDOW_END) {
|
||||
if (t >= hitWindowStart && t <= hitWindowEnd) {
|
||||
fireWeaponCallback(hitWindowCritMult);
|
||||
} else if (t > HIT_WINDOW_END) {
|
||||
} else if (t > hitWindowEnd) {
|
||||
hitWindowActive = false;
|
||||
}
|
||||
}
|
||||
@@ -1241,6 +1347,10 @@ public class PlayerInputControl {
|
||||
currentAttackId = ++attackIdCounter;
|
||||
hitWindowActive = true;
|
||||
hitWindowCritMult = lastAttackCritical ? 2.0f : 1.0f;
|
||||
// Steps 1+3 = Attack2 (Backswing): Fenster später schließen damit Schwung den NPC erreicht
|
||||
// Rückschwung (steps 1+3): Trefferfenster beginnt später (Schwung muss erst ankommen)
|
||||
hitWindowStart = (step == 1 || step == 3) ? 0.45f : HIT_WINDOW_START;
|
||||
hitWindowEnd = (step == 1 || step == 3) ? 0.92f : HIT_WINDOW_END;
|
||||
currentComboStep = step;
|
||||
comboStep = (step + 1) % 5;
|
||||
comboWindowActive = false;
|
||||
@@ -1258,21 +1368,11 @@ public class PlayerInputControl {
|
||||
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;
|
||||
}
|
||||
for (MeleeAttackCallback cb : meleeCallbacks) { cb.onAttackStart(currentAttackId, false); }
|
||||
// Lunge: schließt Lücke zum Gegner auf idealen Treffabstand (1.7m)
|
||||
heavyLungeVel = computeLungeVelocity(dur);
|
||||
heavyLungeStartRemaining = dur * (1f - LUNGE_FRAC_START);
|
||||
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
||||
} else if (isRangedType(drawnWeaponType) && isAiming) {
|
||||
isAiming = false;
|
||||
AnimationAction fireAnim = getRangedFireAction(drawnWeaponType);
|
||||
@@ -1296,24 +1396,32 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private void handleCombatAlt(boolean isPressed) {
|
||||
if (!weaponDrawn || blockingAnimActive) return;
|
||||
if (!weaponDrawn) return;
|
||||
if (isMeleeType(drawnWeaponType)) {
|
||||
if (isPressed) {
|
||||
if (blockPhase == BlockPhase.NONE) {
|
||||
blockPhase = BlockPhase.WAITING;
|
||||
blockPhaseTimer = 0f;
|
||||
weaponCombat.onBlockPress();
|
||||
rightMouseHeld = true;
|
||||
// Sofort blocken nur wenn keine andere Animation läuft
|
||||
if (blockPhase == BlockPhase.NONE && !combatAnimActive && !blockingAnimActive) {
|
||||
blockTapTimer = 0f;
|
||||
weaponCombat.parryArmed = false;
|
||||
startBlockEnter();
|
||||
}
|
||||
// Wenn Angriff oder Impact läuft: Block gepuffert, wird danach ausgelöst
|
||||
} else {
|
||||
rightMouseHeld = false;
|
||||
if (blockingAnimActive) return; // Loslassen während Impact → ignorieren
|
||||
switch (blockPhase) {
|
||||
case WAITING -> {
|
||||
// Kurzer Tap: Parade-Animation abspielen (auch wenn NPC nicht im Trefferfenster)
|
||||
if (weaponCombat.onBlockRelease()) {
|
||||
case ENTERING -> {
|
||||
if (blockTapTimer <= WeaponCombatState.BLOCK_TAP_MAX) {
|
||||
// Kurzer Tap: Block abbrechen, Parade auslösen
|
||||
blockPhase = BlockPhase.NONE;
|
||||
weaponCombat.parryArmed = true;
|
||||
playCounterAttack();
|
||||
} else {
|
||||
startBlockExit();
|
||||
}
|
||||
blockPhase = BlockPhase.NONE;
|
||||
}
|
||||
case ENTERING, HOLDING -> startBlockExit();
|
||||
case HOLDING -> startBlockExit();
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
@@ -1324,25 +1432,33 @@ public class PlayerInputControl {
|
||||
|
||||
private void updateBlockPhase(float tpf) {
|
||||
switch (blockPhase) {
|
||||
case WAITING -> {
|
||||
// Tap-Timer läuft in WeaponCombatState; gibt true zurück wenn Fenster abläuft
|
||||
if (weaponCombat.tickParryWaiting(tpf)) {
|
||||
// Tap-Fenster abgelaufen ohne Loslassen → Haltephase
|
||||
if (hasShieldEquipped()) {
|
||||
startBlockEnter();
|
||||
} else {
|
||||
blockPhase = BlockPhase.NONE;
|
||||
case ENTERING -> {
|
||||
blockTapTimer += tpf;
|
||||
blockPhaseTimer -= tpf;
|
||||
// Primär: AnimComposer-Fortschritt (>= 90%) ODER Loop-Erkennung (Zeit fällt zurück)
|
||||
// Sicherung: Timer-Fallback falls AnimComposer-Länge = 0
|
||||
boolean switchToHolding = blockPhaseTimer <= 0f;
|
||||
if (!switchToHolding && animComposer != null) {
|
||||
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||
if (layer != null) {
|
||||
float t = (float) layer.getTime();
|
||||
var a = layer.getCurrentAction();
|
||||
double len = (a != null) ? a.getLength() : 0;
|
||||
if (len > 0.01 && t >= len * 0.90) {
|
||||
switchToHolding = true; // Animation zu >= 90% abgespielt
|
||||
} else if (t < prevBlockAnimTime - 0.05f) {
|
||||
switchToHolding = true; // Loop erkannt: Zeit ist zurückgesprungen
|
||||
}
|
||||
prevBlockAnimTime = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
case ENTERING -> {
|
||||
blockPhaseTimer -= tpf;
|
||||
if (blockPhaseTimer <= 0f) {
|
||||
if (switchToHolding) {
|
||||
prevBlockAnimTime = 0f;
|
||||
blockPhase = BlockPhase.HOLDING;
|
||||
AnimationAction blockAct = getMeleeBlockAction(drawnWeaponType);
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
playAction(blockAct);
|
||||
currentAnim = blockAct;
|
||||
AnimationAction holdAnim = getMeleeBlockAction(drawnWeaponType);
|
||||
nextTransitionLength = -1.0;
|
||||
playAction(holdAnim);
|
||||
currentAnim = holdAnim;
|
||||
}
|
||||
}
|
||||
case EXITING -> {
|
||||
@@ -1350,6 +1466,10 @@ public class PlayerInputControl {
|
||||
if (blockPhaseTimer <= 0f) {
|
||||
blockPhase = BlockPhase.NONE;
|
||||
currentAnim = null;
|
||||
if (attackBuffered) {
|
||||
attackBuffered = false;
|
||||
handleCombatAttack();
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
@@ -1358,13 +1478,24 @@ public class PlayerInputControl {
|
||||
|
||||
private void startBlockEnter() {
|
||||
blockPhase = BlockPhase.ENTERING;
|
||||
prevBlockAnimTime = 0f; // Loop-Detection-Vorzustand zurücksetzen
|
||||
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(idleToBlock, 0.5f);
|
||||
// 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;
|
||||
nextTransitionLength = -1.0;
|
||||
playAction(idleToBlock);
|
||||
currentAnim = idleToBlock;
|
||||
// Länge nach playAction() aus dem AnimComposer lesen: Clip ist jetzt aktiv,
|
||||
// daher exakte Länge verfügbar (resolveClipLength kann Fallback liefern → Clip-Loop).
|
||||
float dur = 0.5f;
|
||||
if (animComposer != null) {
|
||||
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||
if (layer != null) {
|
||||
var a = layer.getCurrentAction();
|
||||
if (a != null && a.getLength() > 0.01) {
|
||||
dur = (float) a.getLength();
|
||||
}
|
||||
}
|
||||
}
|
||||
blockPhaseTimer = Math.max(0.1f, dur - 0.05f);
|
||||
}
|
||||
|
||||
private void startBlockExit() {
|
||||
@@ -1380,14 +1511,14 @@ public class PlayerInputControl {
|
||||
|
||||
private void playCounterAttack() {
|
||||
if (combatAnimActive) return;
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("counterparade"); }
|
||||
AnimationAction counterAct = getMeleeCounterAttackAction(drawnWeaponType);
|
||||
AnimationAction counterAct = getMeleeParadeAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(counterAct, 0.7f);
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||
combatAnimCallback = null;
|
||||
combatAnimSkipComboWindow = true;
|
||||
attackSoundFired = true; // kein Swoosh-Sound bei Konterparade
|
||||
playAction(counterAct);
|
||||
currentAnim = counterAct;
|
||||
}
|
||||
@@ -1399,6 +1530,7 @@ public class PlayerInputControl {
|
||||
|
||||
/** Spielt die Konter-Animations bei erfolgreicher Parade (nur wenn keine Combat-Animation aktiv). */
|
||||
public void triggerParrySuccess() {
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("counterparade"); }
|
||||
playCounterAttack();
|
||||
}
|
||||
|
||||
@@ -1409,21 +1541,36 @@ public class PlayerInputControl {
|
||||
forward = backward = left = right = sprint = walk = false;
|
||||
if (physicsChar != null) { physicsChar.setWalkDirection(com.jme3.math.Vector3f.ZERO); }
|
||||
rightMouseHeld = false;
|
||||
attackBuffered = false;
|
||||
blockPhase = BlockPhase.NONE;
|
||||
combatAnimActive = false;
|
||||
comboWindowActive = false;
|
||||
weaponCombat.phase = WeaponCombatState.Phase.IDLE;
|
||||
weaponCombat.parryArmed = false;
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("argh"); }
|
||||
AnimationAction deathAct = playerRng.nextBoolean()
|
||||
? AnimationAction.SWORD_DEATH : AnimationAction.SWORD_DEATH_2;
|
||||
playAction(deathAct);
|
||||
currentAnim = deathAct;
|
||||
currentAnim = deathAct;
|
||||
// Länge nach playAction() aus AnimComposer lesen (verhindert Loop durch falschen Fallback-Timer)
|
||||
float deathLen = 2.0f;
|
||||
if (animComposer != null) {
|
||||
var layer = animComposer.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||
if (layer != null) {
|
||||
var a = layer.getCurrentAction();
|
||||
if (a != null && a.getLength() > 0.01) {
|
||||
deathLen = (float) a.getLength();
|
||||
}
|
||||
}
|
||||
}
|
||||
blockingAnimActive = true;
|
||||
blockingAnimRemaining = 999f;
|
||||
blockingAnimTotal = 999f;
|
||||
blockingAnimRemaining = Math.max(0.1f, deathLen - 0.12f); // 120ms vor Clip-Ende: kein Loop
|
||||
blockingAnimTotal = blockingAnimRemaining;
|
||||
blockingAnimCallback = null;
|
||||
}
|
||||
|
||||
public boolean isPlayerDead() { return playerDead; }
|
||||
|
||||
public boolean isBlocking() {
|
||||
return blockPhase == BlockPhase.ENTERING
|
||||
|| blockPhase == BlockPhase.HOLDING
|
||||
@@ -1439,7 +1586,7 @@ public class PlayerInputControl {
|
||||
|
||||
/** Wird aufgerufen wenn ein blockierter Treffer landet. Kostet 30 Ausdauer; unter 0 → Block bricht. */
|
||||
public void triggerBlockImpact() {
|
||||
if (blockPhase != BlockPhase.HOLDING || drawnWeaponType == null) return;
|
||||
if ((blockPhase != BlockPhase.HOLDING && blockPhase != BlockPhase.ENTERING) || drawnWeaponType == null) return;
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impactblock"); }
|
||||
if (mainCharacter != null && mainCharacter.getMaxStamina() > 0) {
|
||||
if (mainCharacter.getCurrentStamina() < STAMINA_BLOCK_HIT_COST) {
|
||||
@@ -1695,28 +1842,20 @@ public class PlayerInputControl {
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeCounterAttackAction(ItemSubCategory s) {
|
||||
private static AnimationAction getMeleeParadeAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_COUNTERATTACK;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_COUNTERATTACK;
|
||||
default -> AnimationAction.SWORD_COUNTERATTACK;
|
||||
case AXE -> AnimationAction.AXE_PARADE;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_PARADE;
|
||||
default -> AnimationAction.SWORD_PARADE;
|
||||
};
|
||||
}
|
||||
|
||||
public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
|
||||
default -> AnimationAction.SWORD_BLOCK_IMPACT;
|
||||
};
|
||||
return SwordShieldCombat.resolveBlockImpactAction(s);
|
||||
}
|
||||
|
||||
public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
|
||||
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
||||
};
|
||||
return SwordShieldCombat.resolveIdleImpactAction(s);
|
||||
}
|
||||
|
||||
private void fireWeaponCallback(float critMultiplier) {
|
||||
@@ -1730,8 +1869,23 @@ public class PlayerInputControl {
|
||||
}
|
||||
int base = (mainCharacter != null && mainCharacter.getEquippedMeleeWeapon() != null)
|
||||
? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0;
|
||||
Vector3f grip = hn.getWorldTranslation().clone();
|
||||
Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal();
|
||||
// Vorwärtsrichtung des Charakters (Spieler schaut immer Richtung Gegner)
|
||||
Vector3f bladeDir = (visual != null)
|
||||
? visual.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Z).normalizeLocal()
|
||||
: cam.getDirection().clone().setY(0).normalizeLocal();
|
||||
// physicsChar.getPhysicsLocation() als XZ-Ursprung: immer aktuell, kein staler Cache.
|
||||
// HandY behalten für korrekte Trefferhöhe; 0.5m forward = gleiche Reichweite wie Attack 1.
|
||||
Vector3f grip;
|
||||
if (physicsChar != null) {
|
||||
com.jme3.math.Vector3f charPos = physicsChar.getPhysicsLocation();
|
||||
grip = new Vector3f(
|
||||
charPos.x + bladeDir.x * 0.5f,
|
||||
hn.getWorldTranslation().y,
|
||||
charPos.z + bladeDir.z * 0.5f
|
||||
);
|
||||
} else {
|
||||
grip = hn.getWorldTranslation().clone();
|
||||
}
|
||||
for (MeleeAttackCallback cb : meleeCallbacks) {
|
||||
cb.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier);
|
||||
}
|
||||
@@ -1749,6 +1903,47 @@ public class PlayerInputControl {
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalisierter Vorwärtsvektor Richtung nächstem Gegner, Fallback: Blickrichtung. */
|
||||
private Vector3f computeAttackDir() {
|
||||
Vector3f fwd = null;
|
||||
if (nearestAttackingNpcPos != null && physicsChar != null) {
|
||||
Vector3f myPos = physicsChar.getPhysicsLocation();
|
||||
fwd = new Vector3f(nearestAttackingNpcPos.x - myPos.x, 0f, nearestAttackingNpcPos.z - myPos.z);
|
||||
if (fwd.lengthSquared() < 0.001f) { fwd = null; }
|
||||
}
|
||||
if (fwd == null) {
|
||||
fwd = (visual != null)
|
||||
? visual.getWorldRotation().mult(new Vector3f(0f, 0f, 1f))
|
||||
: cam.getDirection().clone();
|
||||
fwd.y = 0f;
|
||||
}
|
||||
return fwd.lengthSquared() > 0.001f ? fwd.normalizeLocal() : fwd.set(0f, 0f, 1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Lunge-Geschwindigkeit so, dass der Abstand zum Gegner
|
||||
* während des Lungesfensters auf ~1.7m (sicherer Treffbereich) schließt.
|
||||
* Ohne bekannten Gegner: minimaler Schritt nach vorn.
|
||||
*/
|
||||
private Vector3f computeLungeVelocity(float animDur) {
|
||||
Vector3f fwd = computeAttackDir();
|
||||
float IDEAL_DIST = 1.7f;
|
||||
if (nearestAttackingNpcPos != null && physicsChar != null) {
|
||||
Vector3f myPos2 = physicsChar.getPhysicsLocation();
|
||||
float dx2 = nearestAttackingNpcPos.x - myPos2.x;
|
||||
float dz2 = nearestAttackingNpcPos.z - myPos2.z;
|
||||
float dist = (float) Math.sqrt(dx2 * dx2 + dz2 * dz2);
|
||||
float gap = dist - IDEAL_DIST;
|
||||
if (gap > 0.05f && gap < 2.5f) {
|
||||
float lungeWindowDur = animDur * (LUNGE_FRAC_END - LUNGE_FRAC_START);
|
||||
float neededMps = Math.min(gap / lungeWindowDur, CharacterNavigator.RUN_SPEED_MPS * 0.45f);
|
||||
float speed = neededMps / CharacterNavigator.RUN_SPEED_MPS * MOVE_SPEED;
|
||||
return fwd.multLocal(speed);
|
||||
}
|
||||
}
|
||||
return fwd.multLocal(MOVE_SPEED * 0.04f);
|
||||
}
|
||||
|
||||
/** Startet sofort die schwere-Angriff-Animation beim Drücken der Taste. */
|
||||
private void startHeavyCharge() {
|
||||
if (drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
||||
@@ -1768,15 +1963,7 @@ public class PlayerInputControl {
|
||||
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
|
||||
heavyLungeVel = computeLungeVelocity(dur);
|
||||
heavyLungeStartRemaining = dur * (1f - LUNGE_FRAC_START);
|
||||
heavyLungeEndRemaining = dur * (1f - LUNGE_FRAC_END);
|
||||
log.info("[Combat] Schwerer Angriff Windup gestartet.");
|
||||
@@ -1787,15 +1974,22 @@ public class PlayerInputControl {
|
||||
if (!weaponDrawn || drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
|
||||
return;
|
||||
}
|
||||
currentAttackId = ++attackIdCounter;
|
||||
currentComboStep = -1;
|
||||
attackSoundFired = false;
|
||||
if (weaponAttach != null && weaponAttach.getHandNode() != null)
|
||||
// Ladung kann fehlen wenn Release während Kombo kam (Race: Release vor controlUpdate-Frame)
|
||||
if (!combatAnimActive) { startHeavyCharge(); }
|
||||
if (!combatAnimActive) { return; } // startHeavyCharge hat versagt (kein Stamina etc.)
|
||||
currentAttackId = ++attackIdCounter;
|
||||
currentComboStep = -1;
|
||||
attackSoundFired = false;
|
||||
hitWindowActive = true;
|
||||
hitWindowCritMult = computeHeavyMultiplier();
|
||||
for (MeleeAttackCallback cb : meleeCallbacks) { cb.onAttackStart(currentAttackId, true); }
|
||||
hitWindowStart = HIT_WINDOW_START;
|
||||
hitWindowEnd = 1.0f; // bis Animationsende gültig – Release-Timing unkritisch
|
||||
if (weaponAttach != null && weaponAttach.getHandNode() != null) {
|
||||
lastGripPos.set(weaponAttach.getHandNode().getWorldTranslation());
|
||||
float mult = computeHeavyMultiplier();
|
||||
fireWeaponCallback(mult);
|
||||
}
|
||||
log.info("[Combat] Schwerer Angriff ausgelöst | Haltezeit={}s Multiplikator={}x",
|
||||
String.format("%.2f", attackHoldTimer), String.format("%.2f", mult));
|
||||
String.format("%.2f", attackHoldTimer), String.format("%.2f", hitWindowCritMult));
|
||||
}
|
||||
|
||||
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
|
||||
|
||||
@@ -225,6 +225,8 @@ public class WorldScene extends BaseAppState {
|
||||
public void setPaused(boolean paused) {
|
||||
if (playerInput != null) playerInput.setPaused(paused);
|
||||
if (thirdPersonCam != null) thirdPersonCam.setPaused(paused);
|
||||
WorldNpcsState npcsState = app != null ? app.getStateManager().getState(WorldNpcsState.class) : null;
|
||||
if (npcsState != null) npcsState.setPaused(paused);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -36,6 +36,7 @@ import de.blight.common.PlacedModelIO;
|
||||
import de.blight.common.model.CombatDummy;
|
||||
import de.blight.common.model.CombatDummyIO;
|
||||
import de.blight.game.animation.AnimationAction;
|
||||
import de.blight.game.combat.CombatStyleBase;
|
||||
import de.blight.game.combat.DamageCalculator;
|
||||
import de.blight.game.combat.MeleeAttackCallback;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
@@ -367,15 +368,14 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
|
||||
lastAttackId = attackId;
|
||||
hitThisAttack.clear();
|
||||
}
|
||||
float radSq = CombatStyleBase.HIT_RADIUS * CombatStyleBase.HIT_RADIUS;
|
||||
Vector3f bladeTip = gripPos.add(bladeDir.mult(bladeLength));
|
||||
for (DummyRuntime dr : dummies) {
|
||||
if (dr.fallen || hitThisAttack.contains(dr.id)) {
|
||||
continue;
|
||||
}
|
||||
// Distanz vom Griff zum Dummy-Zentrum (Torso-Höhe ~0.9m)
|
||||
float dx = dr.worldX - gripPos.x;
|
||||
float dy = (dr.worldY + 0.9f) - gripPos.y;
|
||||
float dz = dr.worldZ - gripPos.z;
|
||||
if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) {
|
||||
Vector3f dummyCenter = new Vector3f(dr.worldX, dr.worldY + 0.9f, dr.worldZ);
|
||||
if (CombatStyleBase.segmentDistSq(dummyCenter, gripPos, bladeTip) <= radSq) {
|
||||
hitThisAttack.add(dr.id);
|
||||
applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier), critMultiplier > 1.0f);
|
||||
}
|
||||
|
||||
@@ -47,9 +47,11 @@ import de.blight.common.model.trigger.NpcStatusTrigger;
|
||||
import de.blight.game.animation.AnimationAction;
|
||||
import de.blight.game.animation.AnimationLibrary;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
import de.blight.game.combat.CombatStyleBase;
|
||||
import de.blight.game.combat.DamageCalculator;
|
||||
import de.blight.game.combat.MeleeAttackCallback;
|
||||
import de.blight.game.combat.WeaponCombatState;
|
||||
import de.blight.game.combat.swordshield.SwordShieldCombat;
|
||||
import de.blight.game.npc.AggroEvent;
|
||||
import de.blight.game.npc.NpcAggroBehavior;
|
||||
import de.blight.lang.TextResolver;
|
||||
@@ -161,6 +163,11 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
float knockAngVel = 0f;
|
||||
float dodgeDirX = 0f;
|
||||
float dodgeDirZ = 0f;
|
||||
float knockbackTimer = 0f;
|
||||
boolean paradeFromParry = false; // true = Konterparade (applyParriedImpact wurde gespielt)
|
||||
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
|
||||
float konterParadeDelay = 0f; // Countdown bis die Parade-Anim tatsächlich abgespielt wird
|
||||
float chargeTimer = 0f;
|
||||
float chaseDelayTimer = 0f;
|
||||
de.blight.common.model.ItemSubCategory combatWeaponType = null;
|
||||
@@ -191,8 +198,9 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
// ── Hover + Dialog ────────────────────────────────────────────────────────
|
||||
|
||||
private int hoveredIdx = -1;
|
||||
private boolean dialogActive = false;
|
||||
private SpawnedNpc dialogNpc = null;
|
||||
private boolean dialogActive = false;
|
||||
private boolean playerDeathHandled = false;
|
||||
private SpawnedNpc dialogNpc = null;
|
||||
private ThirdPersonCamera thirdPersonCam;
|
||||
|
||||
// ── NPC Head-Look während Dialog ──────────────────────────────────────────
|
||||
@@ -263,15 +271,15 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
private static final float NPC_CLOSE_IN_SPEED = CharacterNavigator.WALK_SPEED_MPS;
|
||||
|
||||
// ── NPC Kampf-KI ──────────────────────────────────────────────────────────
|
||||
private static final float NPC_THINK_MIN_NORMAL = 0.8f;
|
||||
private static final float NPC_THINK_MAX_NORMAL = 2.0f;
|
||||
private static final float NPC_THINK_MIN_LOW_HP = 0.4f;
|
||||
private static final float NPC_THINK_MAX_LOW_HP = 1.0f;
|
||||
private static final float NPC_THINK_MIN_NORMAL = 0.3f;
|
||||
private static final float NPC_THINK_MAX_NORMAL = 3.5f;
|
||||
private static final float NPC_THINK_MIN_LOW_HP = 0.3f;
|
||||
private static final float NPC_THINK_MAX_LOW_HP = 1.2f;
|
||||
private static final float NPC_LOW_HP_THRESHOLD = 0.5f;
|
||||
private static final float NPC_BLOCK_CHANCE_NORMAL = 0.35f;
|
||||
private static final float NPC_BLOCK_CHANCE_LOW_HP = 0.15f;
|
||||
private static final float NPC_BLOCK_CHANCE_NORMAL = 0.45f;
|
||||
private static final float NPC_BLOCK_CHANCE_LOW_HP = 0.20f;
|
||||
private static final float NPC_DODGE_CHANCE = 0.40f;
|
||||
private static final float NPC_PARRY_CHANCE = 0.25f;
|
||||
private static final float NPC_PARRY_CHANCE = 0.40f;
|
||||
// Kombo/Trefferfenster-Konstanten aus WeaponCombatState (gemeinsam mit Spieler)
|
||||
private static final float NPC_COMBO_EXTEND_P1 = WeaponCombatState.COMBO_P1;
|
||||
private static final float NPC_COMBO_EXTEND_P2 = WeaponCombatState.COMBO_P2;
|
||||
@@ -289,7 +297,8 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
private static final float NPC_CLIP_FALLBACK_ROLL = 0.8f;
|
||||
|
||||
private final java.util.Random npcRng = new java.util.Random();
|
||||
private float playerBlockTimer = 0f;
|
||||
private float playerBlockTimer = 0f;
|
||||
private boolean paused = false;
|
||||
|
||||
// ── NPC HP / Kampf ────────────────────────────────────────────────────────
|
||||
private static final int DEFAULT_NPC_MAX_HP = 100;
|
||||
@@ -335,6 +344,10 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
this.weaponSoundSystem = wss;
|
||||
}
|
||||
|
||||
public void setPaused(boolean paused) {
|
||||
this.paused = paused;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
@@ -418,6 +431,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (paused) return;
|
||||
checkTimer += tpf;
|
||||
|
||||
int currentHour = dayNight != null ? dayNight.getDayTime().getHour() : 12;
|
||||
@@ -451,6 +465,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
// Bestehende NPCs prüfen: Position aktualisieren oder entladen
|
||||
toRemoveBuf.clear();
|
||||
for (SpawnedNpc s : spawned) {
|
||||
if (s.knockedOut) { continue; } // Tote NPCs nicht teleportieren oder überschreiben
|
||||
Vector3f pos = resolveRoutinePosition(s.npc(), currentHour);
|
||||
float dist = pos != null ? dist2d(playerPos, pos) : Float.MAX_VALUE;
|
||||
|
||||
@@ -966,6 +981,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
|
||||
private void playNpcAction(SpawnedNpc s, AnimationAction action) {
|
||||
if (action == s.currentAction || animLib == null) return;
|
||||
log.debug("[Combat] NPC={} Anim: {} → {}", s.npc().getCharacterId(), s.currentAction, action);
|
||||
String setName = s.npc().getAnimSetPath();
|
||||
if (setName == null || setName.isBlank()) return;
|
||||
String clipName = AnimationLibrary.getClipForAction(AnimationLibrary.findAssetRoot(), setName, action);
|
||||
@@ -1151,9 +1167,27 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
|
||||
private void updateAggroBehaviors(float tpf) {
|
||||
if (dialogActive) { return; }
|
||||
if (playerInput != null && playerInput.isPlayerDead()) {
|
||||
if (!playerDeathHandled) {
|
||||
playerDeathHandled = true;
|
||||
for (SpawnedNpc s : spawned) {
|
||||
if (!s.knockedOut && s.aggroBehavior.isAttacking()) {
|
||||
resetNpcCombat(s);
|
||||
s.aggroBehavior.forceCalm();
|
||||
Vector3f returnPos = resolveRoutinePosition(s.npc(), lastHour);
|
||||
if (returnPos != null) {
|
||||
s.isReturning = true;
|
||||
s.returnTarget = returnPos.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||
float soundRadius = playerInput.getSoundRadius();
|
||||
for (SpawnedNpc s : spawned) {
|
||||
if (s.knockedOut) { continue; }
|
||||
Status status = s.npc().getStatus();
|
||||
if (status != Status.ENRAGED && status != Status.ENEMY) { continue; }
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
@@ -1198,6 +1232,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
public void notifyNpcAttackedByPlayer(String npcId) {
|
||||
for (SpawnedNpc s : spawned) {
|
||||
if (!npcId.equals(s.npc().getCharacterId())) { continue; }
|
||||
if (s.knockedOut) { return; }
|
||||
Status status = s.npc().getStatus();
|
||||
if (status != Status.ENRAGED && status != Status.ENEMY) { return; }
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
@@ -1460,12 +1495,31 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
|
||||
private void updateNpcCombat(float tpf) {
|
||||
if (dialogActive) { return; }
|
||||
if (playerInput.isBlocking()) { playerBlockTimer += tpf; }
|
||||
else { playerBlockTimer = 0f; }
|
||||
boolean playerDead = playerInput != null && playerInput.isPlayerDead();
|
||||
if (!playerDead) {
|
||||
if (playerInput.isBlocking()) { playerBlockTimer += tpf; }
|
||||
else { playerBlockTimer = 0f; }
|
||||
}
|
||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||
|
||||
// Nächsten angreifenden NPC für Lungen-Richtung bei schwerem Angriff bestimmen
|
||||
if (playerInput != null && !playerDead) {
|
||||
Vector3f nearestPos = null;
|
||||
float nearestDistSq = Float.MAX_VALUE;
|
||||
for (SpawnedNpc s : spawned) {
|
||||
if (!s.knockedOut && s.aggroBehavior.isAttacking()) {
|
||||
Vector3f p = s.visual().getWorldTranslation();
|
||||
float dx = p.x - playerPos.x, dz = p.z - playerPos.z;
|
||||
float dSq = dx*dx + dz*dz;
|
||||
if (dSq < nearestDistSq) { nearestDistSq = dSq; nearestPos = p; }
|
||||
}
|
||||
}
|
||||
playerInput.setNearestAttackingNpcPos(nearestPos);
|
||||
}
|
||||
|
||||
for (SpawnedNpc s : spawned) {
|
||||
|
||||
// Zieh/Wegsteck-Animation – läuft unabhängig vom Aggro-Zustand (auch nach PURSUIT_BREAK)
|
||||
// Zieh/Wegsteck-Animation – läuft auch nach Spielertod (Waffe wird noch korrekt weggesteckt)
|
||||
if (s.drawAnimActive) {
|
||||
s.drawAnimTimer += tpf;
|
||||
de.blight.game.control.NpcWeaponAttachmentControl wac =
|
||||
@@ -1538,14 +1592,18 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!s.aggroBehavior.isAttacking()) { continue; }
|
||||
// Kampf-Logik nur wenn Spieler noch lebt
|
||||
if (playerDead) { continue; }
|
||||
|
||||
// K.O.: nur Umfall-Physik, keine sonstige Kampflogik
|
||||
// K.O.: Todesanimation einfrieren – vor isAttacking()-Check, da forceCalm()
|
||||
// isAttacking()=false setzt und der knockedOut-Zweig sonst nie erreicht wird.
|
||||
if (s.knockedOut) {
|
||||
updateNpcCombatPhase(s, tpf, playerPos);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!s.aggroBehavior.isAttacking()) { continue; }
|
||||
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
float dist = dist2d(npcPos, playerPos);
|
||||
|
||||
@@ -1567,7 +1625,8 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
// In Kampf-Stopp-Distanz oder Stuck-Timeout → IDLE + Entscheidung
|
||||
if (dist <= NPC_STOP_DIST || s.chargeTimer >= 1.5f) {
|
||||
faceToward(s, playerPos);
|
||||
s.chargeTimer = 0f;
|
||||
s.chargeTimer = 0f;
|
||||
s.chaseDelayTimer = 0f;
|
||||
if (s.combat.phase == WeaponCombatState.Phase.CHARGING) {
|
||||
s.combat.phase = WeaponCombatState.Phase.IDLE;
|
||||
resetNpcThinkTimer(s);
|
||||
@@ -1576,10 +1635,17 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
continue;
|
||||
}
|
||||
|
||||
// Außerhalb Angriffsreichweite → CHARGING
|
||||
// Außerhalb Angriffsreichweite → 0.5s Verzögerung vor Verfolgungsstart
|
||||
if (s.combat.phase == WeaponCombatState.Phase.IDLE) {
|
||||
s.combat.phase = WeaponCombatState.Phase.CHARGING;
|
||||
s.chargeTimer = 0f;
|
||||
s.chaseDelayTimer += tpf;
|
||||
if (s.chaseDelayTimer < 0.5f) {
|
||||
faceToward(s, playerPos);
|
||||
playNpcAction(s, resolveNpcIdleAction(s));
|
||||
continue;
|
||||
}
|
||||
s.chaseDelayTimer = 0f;
|
||||
s.combat.phase = WeaponCombatState.Phase.CHARGING;
|
||||
s.chargeTimer = 0f;
|
||||
}
|
||||
|
||||
// Close-in: Spieler ≤ 3m und flieht nicht → NPC geht langsam ran
|
||||
@@ -1691,6 +1757,31 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
|
||||
// ── MeleeAttackCallback ───────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void onAttackStart(int attackId, boolean isHeavy) {
|
||||
if (isHeavy || physicsChar == null) { return; }
|
||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||
for (SpawnedNpc s : spawned) {
|
||||
if (s.knockedOut) { continue; }
|
||||
if (s.konterParadeAttackId == attackId) { continue; } // bereits entschieden
|
||||
boolean npcIsBlocking = (s.blockGraceTimer > 0f || s.combat.phase == WeaponCombatState.Phase.BLOCKING)
|
||||
&& s.combat.phase != WeaponCombatState.Phase.STAGGERED
|
||||
&& s.combat.phase != WeaponCombatState.Phase.KNOCKED_OUT
|
||||
&& s.combat.phase != WeaponCombatState.Phase.DODGING;
|
||||
if (npcIsBlocking) { continue; }
|
||||
if (s.combat.phase != WeaponCombatState.Phase.IDLE
|
||||
&& s.combat.phase != WeaponCombatState.Phase.CHARGING) { continue; }
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
if (dist2d(npcPos, playerPos) > NPC_ATTACK_RANGE + 1.5f) { continue; }
|
||||
if (npcRng.nextFloat() < NPC_PARRY_CHANCE) {
|
||||
s.konterParadeAttackId = attackId;
|
||||
s.konterParadeDelay = 0.1f + npcRng.nextFloat() * 0.1f; // 100–200 ms Verzug
|
||||
log.debug("[Combat] onAttackStart: NPC={} Parade in {}ms", s.npc().getCharacterId(),
|
||||
String.format("%.0f", s.konterParadeDelay * 1000f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir,
|
||||
float bladeLength, int baseDamage, float critMultiplier) {
|
||||
@@ -1699,13 +1790,12 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
hitThisAttack.clear();
|
||||
}
|
||||
boolean isHeavy = critMultiplier >= 3.0f;
|
||||
float radSq = CombatStyleBase.HIT_RADIUS * CombatStyleBase.HIT_RADIUS;
|
||||
Vector3f bladeTip = gripPos.add(bladeDir.mult(bladeLength));
|
||||
for (SpawnedNpc s : spawned) {
|
||||
if (hitThisAttack.contains(s.npc().getCharacterId())) { continue; }
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
float dx = npcPos.x - gripPos.x;
|
||||
float dy = (npcPos.y + 0.9f) - gripPos.y;
|
||||
float dz = npcPos.z - gripPos.z;
|
||||
if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) {
|
||||
Vector3f npcCenter = s.visual().getWorldTranslation().add(0f, 0.9f, 0f);
|
||||
if (CombatStyleBase.segmentDistSq(npcCenter, gripPos, bladeTip) <= radSq) {
|
||||
hitThisAttack.add(s.npc().getCharacterId());
|
||||
applyNpcDamage(s, DamageCalculator.calculate(baseDamage, critMultiplier),
|
||||
critMultiplier > 1.0f, isHeavy, critMultiplier);
|
||||
@@ -1715,27 +1805,48 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
|
||||
private void applyNpcDamage(SpawnedNpc s, int damage, boolean isCrit, boolean isHeavy, float critMultiplier) {
|
||||
if (s.knockedOut) { return; }
|
||||
log.debug("[Combat] applyNpcDamage NPC={} phase={} grace={} anim={} heavy={}",
|
||||
s.npc().getCharacterId(), s.combat.phase,
|
||||
String.format("%.3f", s.blockGraceTimer), s.currentAction, isHeavy);
|
||||
|
||||
// NPC Konter-Parade: reaktive Parade gegen normalen Angriff (nicht schwer, nicht bereits blockend)
|
||||
// blockGraceTimer > 0: NPC blockt gerade oder kehrt gerade aus dem Block zurück.
|
||||
// Verhindert Konterparade während des gesamten Block-Einstiegs, der Hold-Phase und
|
||||
// eines kurzen Auslauf-Puffers nach Block-Ende (~0.4 s).
|
||||
boolean npcIsBlocking = (s.blockGraceTimer > 0f
|
||||
|| s.combat.phase == WeaponCombatState.Phase.BLOCKING)
|
||||
&& s.combat.phase != WeaponCombatState.Phase.STAGGERED
|
||||
&& s.combat.phase != WeaponCombatState.Phase.KNOCKED_OUT
|
||||
&& s.combat.phase != WeaponCombatState.Phase.DODGING;
|
||||
|
||||
// NPC Konter-Parade: nur wenn in onAttackStart bereits entschieden (earlyAnim=true)
|
||||
boolean konterAlreadyStarted = (s.konterParadeAttackId == lastAttackId);
|
||||
if (!isHeavy
|
||||
&& (s.combat.phase == WeaponCombatState.Phase.IDLE
|
||||
|| s.combat.phase == WeaponCombatState.Phase.CHARGING)
|
||||
&& npcRng.nextFloat() < NPC_PARRY_CHANCE) {
|
||||
AnimationAction blockImpact = resolveNpcBlockImpactAction(s);
|
||||
float dur = resolveNpcClipLength(s, blockImpact, NPC_STAGGER_FALLBACK);
|
||||
s.currentAction = null;
|
||||
playNpcAction(s, blockImpact);
|
||||
setNpcCurrentActionSpeed(s, 0.5f);
|
||||
s.combat.phase = WeaponCombatState.Phase.COUNTER_WINDOW;
|
||||
s.combat.timer = dur * 2f;
|
||||
AnimationAction playerImpact = resolvePlayerImpactAction();
|
||||
if (playerImpact != null) playerInput.requestAnimation(playerImpact, NPC_STAGGER_FALLBACK, null);
|
||||
&& !npcIsBlocking
|
||||
&& konterAlreadyStarted) {
|
||||
log.debug("[Combat] KONTERPARADE NPC={} phase={} grace={} anim={}",
|
||||
s.npc().getCharacterId(), s.combat.phase,
|
||||
String.format("%.3f", s.blockGraceTimer), s.currentAction);
|
||||
AnimationAction parryAnim = resolveNpcParadeAction(s);
|
||||
float dur = resolveNpcClipLength(s, parryAnim, NPC_STAGGER_FALLBACK);
|
||||
s.konterParadeAttackId = -1;
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("counterparade"); }
|
||||
s.combat.phase = WeaponCombatState.Phase.COUNTER_WINDOW;
|
||||
s.combat.timer = dur * 2f;
|
||||
s.paradeFromParry = true;
|
||||
SwordShieldCombat.applyParriedImpact(playerInput, playerInput.getDrawnWeaponType());
|
||||
return;
|
||||
}
|
||||
s.konterParadeAttackId = -1; // Entscheidung war kein Konter → zurücksetzen
|
||||
|
||||
if (s.combat.phase == WeaponCombatState.Phase.BLOCKING) {
|
||||
if (npcIsBlocking) {
|
||||
log.debug("[Combat] BLOCK trifft NPC={} phase={} grace={} heavy={}",
|
||||
s.npc().getCharacterId(), s.combat.phase,
|
||||
String.format("%.3f", s.blockGraceTimer), isHeavy);
|
||||
if (isHeavy) {
|
||||
// Schwerer Angriff: Block bricht, kein Schaden — NPC weicht ggf. aus
|
||||
setNpcKnockbackDir(s);
|
||||
if (npcRng.nextFloat() < NPC_DODGE_CHANCE) {
|
||||
startNpcDodge(s);
|
||||
} else {
|
||||
@@ -1746,29 +1857,37 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
int cost = Math.round(NPC_BLOCK_STAMINA_BASE * critMultiplier);
|
||||
s.currentStamina = Math.max(0, s.currentStamina - cost);
|
||||
if (s.currentStamina <= 0) {
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
|
||||
setNpcKnockbackDir(s);
|
||||
staggerNpc(s);
|
||||
} else {
|
||||
// Block hält: Impact-Animation + Konter
|
||||
// Block hält: kurzer Rückstoß + Impact-Animation + Konter
|
||||
setNpcKnockbackDir(s);
|
||||
AnimationAction impact = resolveNpcBlockImpactAction(s);
|
||||
float dur = resolveNpcClipLength(s, impact, NPC_CLIP_FALLBACK_BLOCK);
|
||||
s.currentAction = null;
|
||||
playNpcAction(s, impact);
|
||||
playerInput.triggerBlockImpact();
|
||||
s.combat.phase = WeaponCombatState.Phase.COUNTER_WINDOW;
|
||||
s.combat.timer = dur;
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impactblock"); }
|
||||
s.combat.phase = WeaponCombatState.Phase.COUNTER_WINDOW;
|
||||
s.combat.timer = dur;
|
||||
s.paradeFromParry = false;
|
||||
}
|
||||
}
|
||||
return; // kein HP-Verlust bei geblockt
|
||||
}
|
||||
|
||||
// Nicht blockend: Schaden anwenden
|
||||
log.debug("[Combat] TREFFER NPC={} -{} HP ({}/{}→{})",
|
||||
s.npc().getCharacterId(), damage, s.currentHp, s.maxHp, Math.max(0, s.currentHp - damage));
|
||||
s.currentHp = Math.max(0, s.currentHp - damage);
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play(isCrit ? "crit" : "impact"); }
|
||||
log.info("[NPC {}] -{} HP → {}/{}{}", s.npc().getCharacterId(), damage,
|
||||
s.currentHp, s.maxHp, s.currentHp <= 0 ? " K.O.!" : "");
|
||||
if (s.currentHp <= 0) {
|
||||
setNpcKnockbackDir(s);
|
||||
knockoutNpc(s);
|
||||
} else {
|
||||
setNpcKnockbackDir(s);
|
||||
staggerNpc(s);
|
||||
}
|
||||
}
|
||||
@@ -1874,14 +1993,46 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
// ── NPC Kampf-KI ─────────────────────────────────────────────────────────
|
||||
|
||||
private void updateNpcCombatPhase(SpawnedNpc s, float tpf, Vector3f playerPos) {
|
||||
if (s.blockGraceTimer > 0f) { s.blockGraceTimer -= tpf; }
|
||||
switch (s.combat.phase) {
|
||||
|
||||
case KNOCKED_OUT -> {
|
||||
// DEATH-Animation läuft einmalig (DontLoop) und friert auf dem letzten Frame ein.
|
||||
applyNpcKnockback(s, tpf);
|
||||
// Todesanimation läuft bis kurz vor dem Ende, dann auf letztem Frame einfrieren
|
||||
if (s.combat.clipDur > 0f) {
|
||||
s.combat.hitTimer += tpf;
|
||||
if (s.combat.hitTimer >= s.combat.clipDur - 0.05f) {
|
||||
AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(s.visual());
|
||||
if (ac != null) {
|
||||
var layer = ac.getLayer(AnimComposer.DEFAULT_LAYER);
|
||||
if (layer != null) {
|
||||
com.jme3.anim.tween.action.Action a = layer.getCurrentAction();
|
||||
if (a != null) {
|
||||
a.setSpeed(0f);
|
||||
layer.setTime(Math.max(0f, s.combat.clipDur - 0.01f));
|
||||
}
|
||||
}
|
||||
}
|
||||
s.combat.clipDur = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case IDLE -> {
|
||||
playNpcAction(s, resolveNpcIdleAction(s));
|
||||
if (s.konterParadeDelay > 0f) {
|
||||
s.konterParadeDelay -= tpf;
|
||||
if (s.konterParadeDelay <= 0f) {
|
||||
s.konterParadeDelay = 0f;
|
||||
AnimationAction parryAnim = resolveNpcParadeAction(s);
|
||||
s.currentAction = null;
|
||||
playNpcAction(s, parryAnim);
|
||||
setNpcCurrentActionSpeed(s, 0.5f);
|
||||
}
|
||||
}
|
||||
// Parade läuft bereits oder Delay läuft noch → kein Idle-Override
|
||||
if (s.konterParadeAttackId == -1) {
|
||||
playNpcAction(s, resolveNpcIdleAction(s));
|
||||
}
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
float dist = dist2d(npcPos, playerPos);
|
||||
if (dist > NPC_STOP_DIST) {
|
||||
@@ -1914,6 +2065,25 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
}
|
||||
|
||||
case ATTACKING, COUNTERING -> {
|
||||
// Auto-Abstand: NPC schließt während Angriffsanfahrt (erste Hälfte) kleine Lücken
|
||||
if (s.combat.phase == WeaponCombatState.Phase.ATTACKING && s.combat.progress() < 0.5f) {
|
||||
Vector3f npcPos2 = s.visual().getWorldTranslation();
|
||||
float distToPlayer = dist2d(npcPos2, playerPos);
|
||||
float idealDist = NPC_ATTACK_RANGE - 0.3f; // 1.7m = sicherer Treffbereich
|
||||
if (distToPlayer > idealDist && distToPlayer < NPC_ATTACK_RANGE + 2.5f) {
|
||||
float gap = distToPlayer - idealDist;
|
||||
float dx2 = playerPos.x - npcPos2.x;
|
||||
float dz2 = playerPos.z - npcPos2.z;
|
||||
float len2 = (float) Math.sqrt(dx2 * dx2 + dz2 * dz2);
|
||||
if (len2 > 0.001f) {
|
||||
float step2 = Math.min(gap, NPC_MOVE_SPEED * 0.35f * tpf);
|
||||
float nx2 = npcPos2.x + dx2 / len2 * step2;
|
||||
float nz2 = npcPos2.z + dz2 / len2 * step2;
|
||||
float ny2 = terrainChunks != null ? terrainChunks.getHeightAt(nx2, nz2) : npcPos2.y;
|
||||
s.visual().setLocalTranslation(nx2, ny2, nz2);
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean done = s.combat.tickAttack(tpf);
|
||||
// Konter-Parade: Spieler tippt Block im Trefferfenster → Stagger (langsam), kein Schaden
|
||||
if (!s.combat.isHeavy && !s.combat.hitFired
|
||||
@@ -1943,25 +2113,34 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
}
|
||||
|
||||
case BLOCKING -> {
|
||||
s.blockGraceTimer = Math.max(s.blockGraceTimer, 0.4f); // erneuern solange aktiv geblockt wird
|
||||
playNpcAction(s, resolveNpcBlockHoldAction(s));
|
||||
s.combat.timer -= tpf;
|
||||
applyNpcKnockback(s, tpf);
|
||||
if (s.combat.timer <= 0f || s.currentStamina <= 0) {
|
||||
s.dodgeDirX = 0f;
|
||||
s.dodgeDirZ = 0f;
|
||||
s.combat.phase = WeaponCombatState.Phase.IDLE;
|
||||
resetNpcThinkTimer(s);
|
||||
}
|
||||
}
|
||||
|
||||
case COUNTER_WINDOW -> {
|
||||
// Immer sofort kontern
|
||||
applyNpcKnockback(s, tpf);
|
||||
s.combat.timer -= tpf;
|
||||
if (s.combat.timer <= 0f) {
|
||||
startNpcCounter(s);
|
||||
s.paradeFromParry = false;
|
||||
s.combat.phase = WeaponCombatState.Phase.IDLE;
|
||||
resetNpcThinkTimer(s);
|
||||
}
|
||||
}
|
||||
|
||||
case STAGGERED -> {
|
||||
s.combat.timer -= tpf;
|
||||
applyNpcKnockback(s, tpf);
|
||||
if (s.combat.timer <= 0f) {
|
||||
s.dodgeDirX = 0f;
|
||||
s.dodgeDirZ = 0f;
|
||||
s.combat.phase = WeaponCombatState.Phase.IDLE;
|
||||
resetNpcThinkTimer(s);
|
||||
}
|
||||
@@ -1989,6 +2168,36 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
}
|
||||
}
|
||||
|
||||
private static final float NPC_KNOCKBACK_SPEED = 1.2f; // m/s
|
||||
private static final float NPC_KNOCKBACK_TIME = 0.20f; // s
|
||||
|
||||
/** Setzt dodgeDir + knockbackTimer für Rückstoß weg vom Spieler. */
|
||||
private void setNpcKnockbackDir(SpawnedNpc s) {
|
||||
if (physicsChar == null) { return; }
|
||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
float dx = npcPos.x - playerPos.x;
|
||||
float dz = npcPos.z - playerPos.z;
|
||||
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0.001f) {
|
||||
s.dodgeDirX = dx / len;
|
||||
s.dodgeDirZ = dz / len;
|
||||
s.knockbackTimer = NPC_KNOCKBACK_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Bewegt NPC für knockbackTimer Sekunden mit linearer Abbremsung rückwärts. */
|
||||
private void applyNpcKnockback(SpawnedNpc s, float tpf) {
|
||||
if (s.knockbackTimer <= 0f) { return; }
|
||||
s.knockbackTimer -= tpf;
|
||||
float ratio = Math.max(0f, s.knockbackTimer / NPC_KNOCKBACK_TIME);
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
float nx = npcPos.x + s.dodgeDirX * NPC_KNOCKBACK_SPEED * ratio * tpf;
|
||||
float nz = npcPos.z + s.dodgeDirZ * NPC_KNOCKBACK_SPEED * ratio * tpf;
|
||||
float ny = terrainChunks != null ? terrainChunks.getHeightAt(nx, nz) : npcPos.y;
|
||||
s.visual().setLocalTranslation(nx, ny, nz);
|
||||
}
|
||||
|
||||
/** Steigt nach 2 s dauerhaftem Block: +10 %/s, max. 60 %. */
|
||||
private float heavyAttackChance() {
|
||||
float t = Math.max(0f, playerBlockTimer - 2f);
|
||||
@@ -2021,13 +2230,17 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
AnimationAction blockEntry = resolveNpcBlockAction(s);
|
||||
float entryDur = resolveNpcClipLength(s, blockEntry, NPC_CLIP_FALLBACK_BLOCK);
|
||||
float holdTime = 0.5f + npcRng.nextFloat() * 1.5f;
|
||||
log.debug("[Combat] startNpcBlock NPC={} anim={} entryDur={} holdTime={}",
|
||||
s.npc().getCharacterId(), blockEntry,
|
||||
String.format("%.2f", entryDur), String.format("%.2f", holdTime));
|
||||
playNpcAction(s, blockEntry);
|
||||
s.combat.phase = WeaponCombatState.Phase.BLOCKING;
|
||||
s.combat.timer = entryDur + holdTime;
|
||||
s.combat.phase = WeaponCombatState.Phase.BLOCKING;
|
||||
s.combat.timer = entryDur + holdTime;
|
||||
s.blockGraceTimer = 0.4f; // Einstieg abdecken; wird in BLOCKING jedes Frame erneuert
|
||||
}
|
||||
|
||||
private void startNpcCounter(SpawnedNpc s) {
|
||||
AnimationAction ctr = resolveNpcCounterAction(s);
|
||||
AnimationAction ctr = resolveNpcParadeAction(s);
|
||||
float dur = resolveNpcClipLength(s, ctr, NPC_CLIP_FALLBACK_CTR);
|
||||
s.currentAction = null;
|
||||
playNpcAction(s, ctr);
|
||||
@@ -2051,21 +2264,28 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
s.combat.timer = dur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen {@link CombatStyleBase.AnimReceiver} für einen NPC.
|
||||
* Die Implementierung spielt die Animation ab, liest die tatsächliche Clip-Länge
|
||||
* NACH dem Start (wie beim Spieler) und versetzt den NPC in STAGGERED.
|
||||
*/
|
||||
private CombatStyleBase.AnimReceiver npcAnimReceiver(SpawnedNpc s) {
|
||||
return (action, speed, onDone) -> {
|
||||
s.currentAction = null;
|
||||
playNpcAction(s, action);
|
||||
AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(s.visual());
|
||||
float dur = CombatStyleBase.readActualClipLength(ac, NPC_STAGGER_FALLBACK);
|
||||
setNpcCurrentActionSpeed(s, speed);
|
||||
s.combat.stagger(dur / speed);
|
||||
};
|
||||
}
|
||||
|
||||
private void staggerNpc(SpawnedNpc s) {
|
||||
AnimationAction stagger = resolveNpcStaggerAction(s);
|
||||
float dur = resolveNpcClipLength(s, stagger, NPC_STAGGER_FALLBACK);
|
||||
s.currentAction = null;
|
||||
playNpcAction(s, stagger);
|
||||
s.combat.stagger(dur);
|
||||
SwordShieldCombat.applyNormalImpact(npcAnimReceiver(s), s.combatWeaponType);
|
||||
}
|
||||
|
||||
private void staggerNpcSlow(SpawnedNpc s) {
|
||||
s.currentAction = null;
|
||||
AnimationAction stagger = resolveNpcStaggerAction(s);
|
||||
float dur = resolveNpcClipLength(s, stagger, NPC_STAGGER_FALLBACK);
|
||||
playNpcAction(s, stagger);
|
||||
setNpcCurrentActionSpeed(s, 0.5f);
|
||||
s.combat.stagger(dur * 2f);
|
||||
SwordShieldCombat.applyParriedImpact(npcAnimReceiver(s), s.combatWeaponType);
|
||||
}
|
||||
|
||||
private void setNpcCurrentActionSpeed(SpawnedNpc s, float speed) {
|
||||
@@ -2079,6 +2299,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
s.knockedOut = true;
|
||||
s.combat.phase = WeaponCombatState.Phase.KNOCKED_OUT;
|
||||
s.aggroBehavior.forceCalm();
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("argh"); }
|
||||
playNpcDeathAnim(s);
|
||||
log.info("[NPC {}] K.O.!", s.npc().getCharacterId());
|
||||
}
|
||||
@@ -2100,33 +2321,38 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(s.visual());
|
||||
if (ac == null) { return; }
|
||||
ac.setCurrentAction(clip, com.jme3.anim.AnimComposer.DEFAULT_LAYER, false);
|
||||
s.currentAction = deathAction;
|
||||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(clip);
|
||||
s.combat.clipDur = (animClip != null && animClip.getLength() > 0.01)
|
||||
? (float) animClip.getLength() : 2.0f;
|
||||
s.combat.hitTimer = 0f;
|
||||
s.currentAction = deathAction;
|
||||
}
|
||||
|
||||
private void fireNpcAttack(SpawnedNpc s, Vector3f playerPos) {
|
||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||
if (dist2d(npcPos, playerPos) > NPC_ATTACK_RANGE + 0.3f) { return; }
|
||||
if (dist2d(npcPos, playerPos) > NPC_ATTACK_RANGE) { return; }
|
||||
Item weapon = findItemById(s.npc().getEquippedWeaponId());
|
||||
int base = (weapon != null && weapon.getDamage() > 0) ? weapon.getDamage() : NPC_UNARMED_DAMAGE;
|
||||
boolean isHeavy = s.combat.isHeavy;
|
||||
log.debug("[Combat] fireNpcAttack NPC={} phase={} heavy={} paradeFromParry={} playerBlocking={}",
|
||||
s.npc().getCharacterId(), s.combat.phase, isHeavy,
|
||||
s.paradeFromParry, playerInput.isBlocking());
|
||||
|
||||
if (isHeavy) {
|
||||
if (playerInput.isBlocking()) {
|
||||
// Schwerer Angriff bricht Block — kein HP-Verlust, aber Stagger
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
|
||||
playerInput.triggerBlockImpact();
|
||||
AnimationAction impact = resolvePlayerImpactAction();
|
||||
if (impact != null) { playerInput.requestAnimation(impact, NPC_STAGGER_FALLBACK, null); }
|
||||
SwordShieldCombat.applyNormalImpact(playerInput, playerInput.getDrawnWeaponType());
|
||||
} else {
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
|
||||
applyPlayerDamage(s, Math.max(1, base * 3));
|
||||
AnimationAction impact = resolvePlayerImpactAction();
|
||||
if (impact != null) { playerInput.requestAnimation(impact, NPC_STAGGER_FALLBACK, null); }
|
||||
SwordShieldCombat.applyNormalImpact(playerInput, playerInput.getDrawnWeaponType());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int damage = Math.max(1, base * (s.combat.comboStep == 2 ? NPC_COMBO3_DAMAGE_MULT : 1));
|
||||
int damage = Math.max(1, base * (s.combat.comboStep == 4 ? NPC_COMBO3_DAMAGE_MULT : 1));
|
||||
if (playerInput.isBlocking()) {
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
|
||||
playerInput.triggerBlockImpact();
|
||||
@@ -2135,15 +2361,21 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
}
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
|
||||
applyPlayerDamage(s, damage);
|
||||
AnimationAction impact = resolvePlayerImpactAction();
|
||||
if (impact != null) { playerInput.requestAnimation(impact, NPC_STAGGER_FALLBACK, null); }
|
||||
// Konterparade: Spieler wurde bereits per applyParriedImpact gestaggert → keine zweite Animation.
|
||||
// Block+Konter: Spieler war NICHT gestaggert → normale Impact-Animation abspielen.
|
||||
if (!s.paradeFromParry) {
|
||||
SwordShieldCombat.applyNormalImpact(playerInput, playerInput.getDrawnWeaponType());
|
||||
}
|
||||
s.paradeFromParry = false;
|
||||
}
|
||||
|
||||
private void applyPlayerDamage(SpawnedNpc killer, int damage) {
|
||||
if (mainCharacter == null) { return; }
|
||||
int hp = Math.max(0, mainCharacter.getCurrentHp() - damage);
|
||||
int before = mainCharacter.getCurrentHp();
|
||||
int hp = Math.max(0, before - damage);
|
||||
mainCharacter.setCurrentHp(hp);
|
||||
log.info("[NPC→Player] -{} HP → {}/{}", damage, hp, mainCharacter.getMaxHp());
|
||||
log.debug("[Combat] HELD -{} HP ({}/{} → {}/{})",
|
||||
damage, before, mainCharacter.getMaxHp(), hp, mainCharacter.getMaxHp());
|
||||
if (hp <= 0 && playerInput != null) {
|
||||
playerInput.playPlayerDeath();
|
||||
if (killer != null) { showNpcSpeech(killer, NpcAggroBehavior.DEATH_TEXTS, "death"); }
|
||||
@@ -2182,19 +2414,19 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
s.combatWeaponType != null ? s.combatWeaponType : de.blight.common.model.ItemSubCategory.SWORD;
|
||||
return switch (wt) {
|
||||
case AXE -> switch (step) {
|
||||
case 1 -> AnimationAction.AXE_ATTACK_2;
|
||||
case 2 -> AnimationAction.AXE_ATTACK_3;
|
||||
default -> AnimationAction.AXE_ATTACK_1;
|
||||
case 1, 3 -> AnimationAction.AXE_ATTACK_2;
|
||||
case 4 -> AnimationAction.AXE_ATTACK_3;
|
||||
default -> AnimationAction.AXE_ATTACK_1;
|
||||
};
|
||||
case TWO_HANDED_SWORD -> switch (step) {
|
||||
case 1 -> AnimationAction.TWO_HAND_ATTACK_2;
|
||||
case 2 -> AnimationAction.TWO_HAND_ATTACK_3;
|
||||
default -> AnimationAction.TWO_HAND_ATTACK_1;
|
||||
case 1, 3 -> AnimationAction.TWO_HAND_ATTACK_2;
|
||||
case 4 -> AnimationAction.TWO_HAND_ATTACK_3;
|
||||
default -> AnimationAction.TWO_HAND_ATTACK_1;
|
||||
};
|
||||
default -> switch (step) {
|
||||
case 1 -> AnimationAction.SWORD_ATTACK_2;
|
||||
case 2 -> AnimationAction.SWORD_ATTACK_3;
|
||||
default -> AnimationAction.SWORD_ATTACK_1;
|
||||
case 1, 3 -> AnimationAction.SWORD_ATTACK_2;
|
||||
case 4 -> AnimationAction.SWORD_ATTACK_3;
|
||||
default -> AnimationAction.SWORD_ATTACK_1;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -2218,40 +2450,15 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
}
|
||||
|
||||
private AnimationAction resolveNpcBlockImpactAction(SpawnedNpc s) {
|
||||
if (s.combatWeaponType == null) { return AnimationAction.SWORD_BLOCK_IMPACT; }
|
||||
return switch (s.combatWeaponType) {
|
||||
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
|
||||
default -> AnimationAction.SWORD_BLOCK_IMPACT;
|
||||
};
|
||||
return SwordShieldCombat.resolveBlockImpactAction(s.combatWeaponType);
|
||||
}
|
||||
|
||||
private AnimationAction resolveNpcCounterAction(SpawnedNpc s) {
|
||||
if (s.combatWeaponType == null) { return AnimationAction.SWORD_COUNTERATTACK; }
|
||||
return switch (s.combatWeaponType) {
|
||||
case AXE -> AnimationAction.AXE_COUNTERATTACK;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_COUNTERATTACK;
|
||||
default -> AnimationAction.SWORD_COUNTERATTACK;
|
||||
};
|
||||
private AnimationAction resolveNpcParadeAction(SpawnedNpc s) {
|
||||
return SwordShieldCombat.resolveParadeAction(s.combatWeaponType);
|
||||
}
|
||||
|
||||
private AnimationAction resolveNpcStaggerAction(SpawnedNpc s) {
|
||||
if (s.combatWeaponType == null) { return AnimationAction.SWORD_IDLE_IMPACT; }
|
||||
return switch (s.combatWeaponType) {
|
||||
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
|
||||
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
||||
};
|
||||
}
|
||||
|
||||
private AnimationAction resolvePlayerImpactAction() {
|
||||
de.blight.common.model.ItemSubCategory wt = playerInput.getDrawnWeaponType();
|
||||
if (wt == null) { return AnimationAction.SWORD_IDLE_IMPACT; }
|
||||
return switch (wt) {
|
||||
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
|
||||
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
||||
};
|
||||
return SwordShieldCombat.resolveIdleImpactAction(s.combatWeaponType);
|
||||
}
|
||||
|
||||
private float resolveNpcClipLength(SpawnedNpc s, AnimationAction action, float fallback) {
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- Kampfsystem: NPC-Aktionen, Schaden, Block/Konterparade-Entscheidungen -->
|
||||
<logger name="de.blight.game.state.WorldNpcsState" level="DEBUG"/>
|
||||
|
||||
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
||||
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
||||
<!-- Minimap: Modell-Build und Render-Zyklen auf DEBUG -->
|
||||
|
||||
Reference in New Issue
Block a user