Impact System ausgearbeitet

This commit is contained in:
2026-09-22 21:31:30 +02:00
parent 3cef09bb29
commit a28197d9d0
12 changed files with 174 additions and 62 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1,30 @@
#Tue Sep 22 20:54:03 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
attachedSounds.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
modelCategory=DUMMY
name=kampfdummy
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=true
tags=
uniformScale=true

View File

@@ -0,0 +1,30 @@
#Tue Sep 22 20:52:39 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
attachedSounds.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
modelCategory=NONE
name=wooden straw man 3d model
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=true
tags=
uniformScale=true

View File

@@ -3,16 +3,19 @@ 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.
* Wird pro Frame während des Treffer-Fensters einer Nahkampf-Animation aufgerufen.
* Jeder Schwung hat eine eindeutige attackId; hitThisAttack muss pro ID zurückgesetzt werden.
*/
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)
* @param attackId Eindeutige ID pro Schwung (inkrementiert bei jedem Angriff)
* @param gripPos Weltposition des Griffes (Hand-Bone)
* @param bladeDir Normierter Klingen-Richtungsvektor in Weltkoordinaten
* @param bladeLength Länge der Klinge in Metern (Ray-Reichweite)
* @param baseDamage Basisschaden der Waffe (0 = unbewaffnet → Fallback auf 5)
* @param critMultiplier Schadens-Multiplikator (1.0 = normal, 2.0 = Kombo-5, 3.0 = Schwerer Angriff)
*/
void onMeleeAttack(Vector3f charPosition, Vector3f charForward, int baseDamage, float critMultiplier);
void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir,
float bladeLength, int baseDamage, float critMultiplier);
}

View File

@@ -131,7 +131,15 @@ public class PlayerInputControl {
private float heavyLungeEndRemaining = 0f;
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
private float combatAnimDuration = 0f;
private MeleeAttackCallback meleeAttackCallback = null;
private MeleeAttackCallback meleeAttackCallback = null;
private WeaponAttachmentControl weaponAttach = null;
private static final float HIT_WINDOW_START = 0.20f;
private static final float HIT_WINDOW_END = 0.75f;
private static final float WEAPON_BLADE_LENGTH = 1.0f;
private int attackIdCounter = 0;
private int currentAttackId = -1;
private boolean hitWindowActive = false;
private float hitWindowCritMult = 1.0f;
// Draw/Sheath Animation System
private boolean drawAnimActive = false;
private float drawAnimTimer = 0f;
@@ -372,7 +380,8 @@ public class PlayerInputControl {
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 void setMeleeAttackCallback(MeleeAttackCallback cb) { this.meleeAttackCallback = cb; }
public void setWeaponAttachmentControl(WeaponAttachmentControl w) { this.weaponAttach = w; }
public boolean isPaused() { return paused; }
@@ -833,6 +842,7 @@ public class PlayerInputControl {
physicsChar.setWalkDirection(inLungeWindow ? heavyLungeVel : Vector3f.ZERO);
if (combatAnimRemaining <= 0f) {
combatAnimActive = false;
hitWindowActive = false;
heavyLungeVel = null;
heavyLungeStartRemaining = 0f;
heavyLungeEndRemaining = 0f;
@@ -856,6 +866,15 @@ public class PlayerInputControl {
comboWindowActive = true;
log.info("[Combat] Kombo-Fenster geöffnet (step={}, remaining={}s)", comboStep, combatAnimRemaining);
}
// Per-Frame Waffen-Treffercheck während Hit-Window
if (hitWindowActive && isMeleeType(drawnWeaponType) && combatAnimDuration > 0f) {
float t = (combatAnimDuration - combatAnimRemaining) / combatAnimDuration;
if (t >= HIT_WINDOW_START && t <= HIT_WINDOW_END) {
fireWeaponCallback(hitWindowCritMult);
} else if (t > HIT_WINDOW_END) {
hitWindowActive = false;
}
}
pollFootsteps(tpf);
return;
}
@@ -989,8 +1008,10 @@ public class PlayerInputControl {
int step = comboStep;
boolean isComboFollow = comboWindowActive; // war Kombo-Fenster offen?
AnimationAction atk = getMeleeComboAction(drawnWeaponType, step);
lastAttackCritical = (step == 4);
fireMeleeCallback(lastAttackCritical ? 2.0f : 1.0f);
lastAttackCritical = (step == 4);
currentAttackId = ++attackIdCounter;
hitWindowActive = true;
hitWindowCritMult = lastAttackCritical ? 2.0f : 1.0f;
comboStep = (step + 1) % 5;
comboWindowActive = false;
float dur = resolveClipLength(atk, 0.6f);
@@ -1302,21 +1323,20 @@ public class PlayerInputControl {
};
}
private void fireMeleeCallback(float critMultiplier) {
if (meleeAttackCallback == null || physicsChar == null) {
private void fireWeaponCallback(float critMultiplier) {
if (meleeAttackCallback == null || weaponAttach == null) {
return;
}
com.jme3.scene.Spatial ws = weaponAttach.getWeaponSpatial();
com.jme3.scene.Node hn = weaponAttach.getHandNode();
if (ws == null || hn == 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);
Vector3f grip = hn.getWorldTranslation().clone();
Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal();
meleeAttackCallback.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier);
}
private float computeHeavyMultiplier() {
@@ -1368,8 +1388,9 @@ public class PlayerInputControl {
if (!weaponDrawn || drawnWeaponType == null || !isMeleeType(drawnWeaponType)) {
return;
}
currentAttackId = ++attackIdCounter;
float mult = computeHeavyMultiplier();
fireMeleeCallback(mult);
fireWeaponCallback(mult);
log.info("[Combat] Schwerer Angriff ausgelöst | Haltezeit={}s Multiplikator={}x",
String.format("%.2f", attackHoldTimer), String.format("%.2f", mult));
}

View File

@@ -84,6 +84,9 @@ public class WeaponAttachmentControl extends AbstractControl {
this.mc = mc;
}
public Spatial getWeaponSpatial() { return weaponSpatial; }
public Node getHandNode() { return handNode; }
@Override
protected void controlUpdate(float tpf) {
if (skinCtrl == null) {

View File

@@ -362,7 +362,9 @@ public class WorldScene extends BaseAppState {
MainCharacter mc = findMainCharacter();
if (mc != null) {
playerInput.setMainCharacter(mc);
character.addControl(new de.blight.game.control.WeaponAttachmentControl(assetManager, playerInput, mc));
var wac = new de.blight.game.control.WeaponAttachmentControl(assetManager, playerInput, mc);
character.addControl(wac);
playerInput.setWeaponAttachmentControl(wac);
// SaveGameState mit Charakter und Positions-Lieferant verknüpfen
de.blight.game.state.SaveGameState saveState =
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);

View File

@@ -4,7 +4,10 @@ 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.BulletAppState;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.InputManager;
@@ -44,7 +47,9 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Verwaltet Kampfpuppen auf der Karte: HP-Tracking, Treffer-Erkennung,
@@ -55,8 +60,6 @@ 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;
@@ -82,14 +85,17 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
private final PlayerInputControl playerInput;
private final KeyBindings keyBindings;
private AssetManager assetManager;
private InputManager inputManager;
private Node rootNode;
private Node guiNode;
private Camera cam;
private AssetManager assetManager;
private InputManager inputManager;
private Node rootNode;
private Node guiNode;
private Camera cam;
private BulletAppState bulletAppState;
// ── Laufzeit-Zustand ─────────────────────────────────────────────────────
private final List<DummyRuntime> dummies = new ArrayList<>();
private final List<DummyRuntime> dummies = new ArrayList<>();
private int lastAttackId = -1;
private final Set<String> hitThisAttack = new HashSet<>();
private int hoveredDummyIdx = -1; // gefallene Puppe in Aufstell-Reichweite
private int focusedDummyIdx = -1; // nächste Puppe in Fokus-Reichweite (Waffe gezückt)
@@ -116,11 +122,12 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
@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();
this.assetManager = app.getAssetManager();
this.inputManager = app.getInputManager();
this.rootNode = sapp.getRootNode();
this.guiNode = sapp.getGuiNode();
this.cam = app.getCamera();
this.bulletAppState = app.getStateManager().getState(BulletAppState.class);
loadDummies();
buildEnemyHud();
@@ -149,6 +156,9 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
@Override
protected void cleanup(Application app) {
for (DummyRuntime dr : dummies) {
if (dr.rbc != null && bulletAppState != null) {
bulletAppState.getPhysicsSpace().remove(dr.rbc);
}
rootNode.detachChild(dr.dummyNode);
}
dummies.clear();
@@ -203,10 +213,22 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
dummyNode.setLocalScale(m.scale());
rootNode.attachChild(dummyNode);
RigidBodyControl rbc = null;
if (m.solid() && bulletAppState != null) {
try {
rbc = new RigidBodyControl(CollisionShapeFactory.createMeshShape(model), 0f);
model.addControl(rbc);
bulletAppState.getPhysicsSpace().add(rbc);
} catch (Exception pe) {
log.warn("[WorldDummy] Kollisions-Shape für '{}' fehlgeschlagen: {}", m.modelPath(), pe.getMessage());
rbc = null;
}
}
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);
m.x(), m.y(), m.z(), dummyNode, hpBarNode, hpFg, maxHp, rbc);
}
private Spatial loadModel(PlacedModel m) {
@@ -374,25 +396,24 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
// ── MeleeAttackCallback ───────────────────────────────────────────────────
@Override
public void onMeleeAttack(Vector3f charPos, Vector3f charForward, int baseDamage, float critMultiplier) {
public void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir,
float bladeLength, int baseDamage, float critMultiplier) {
if (attackId != lastAttackId) {
lastAttackId = attackId;
hitThisAttack.clear();
}
for (DummyRuntime dr : dummies) {
if (dr.fallen) {
if (dr.fallen || hitThisAttack.contains(dr.id)) {
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;
// 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) {
hitThisAttack.add(dr.id);
applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier));
}
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));
}
}
@@ -401,8 +422,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
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.!" : "");
@@ -551,20 +570,22 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
// ── Innere Klasse ─────────────────────────────────────────────────────────
private static class DummyRuntime {
final String id;
final float worldX, worldY, worldZ;
final Node dummyNode;
final Node hpBarNode;
final Geometry hpBarFg;
final String id;
final float worldX, worldY, worldZ;
final Node dummyNode;
final Node hpBarNode;
final Geometry hpBarFg;
final RigidBodyControl rbc;
int currentHp;
final int maxHp;
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) {
Node dummyNode, Node hpBarNode, Geometry hpBarFg, int maxHp,
RigidBodyControl rbc) {
this.id = id;
this.worldX = wx;
this.worldY = wy;
@@ -574,6 +595,7 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
this.hpBarFg = hpBarFg;
this.maxHp = maxHp;
this.currentHp = maxHp;
this.rbc = rbc;
}
}
}

View File

@@ -14,3 +14,4 @@ Models/northcoast/alte_fischerhytte.j3o 322.73001 2.00000 -856.09003 -1.83155 1.
Models/imported/feuerstelle1.j3o 1.46137 2.00000 -1.08840 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 NONE
Models/trees/palm/palm_20260914_195434.j3o 226.76038 1.25643 -893.77020 -1.59663 1.00000 -0.00000 0.00000 true true true 50.00000 150.00000 300.00000 PALM
Models/trees/willow/willow_20260914_210538.j3o 256.04678 5.84959 -887.66675 0.00000 1.00000 0.00000 0.00000 true true true 50.00000 150.00000 300.00000 TREE
Models/imported/kampfdummy.j3o -5.95449 2.00000 -11.12527 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 DUMMY