diff --git a/assets/imported/animations/Falling To Roll.fbx b/assets/imported/animations/Falling To Roll.fbx new file mode 100644 index 0000000..bd6d467 Binary files /dev/null and b/assets/imported/animations/Falling To Roll.fbx differ diff --git a/assets/imported/animations/Picking Up.fbx b/assets/imported/animations/Picking Up.fbx new file mode 100644 index 0000000..f3a5a86 Binary files /dev/null and b/assets/imported/animations/Picking Up.fbx differ diff --git a/blight-assets/src/main/resources/Characters/clips/Picking Up.j3o b/blight-assets/src/main/resources/Characters/clips/Picking Up.j3o new file mode 100644 index 0000000..820740f Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Picking Up.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Roll.j3o b/blight-assets/src/main/resources/Characters/clips/Roll.j3o new file mode 100644 index 0000000..0e9d4a0 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Roll.j3o differ diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json index b160906..b42ac66 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -24,7 +24,9 @@ "Sword And Shield Block Walk Backwards", "Sword And Shield Strafe Left", "Sword And Shield Strafe Right", - "Sword And Shield Heavy Attack" + "Sword And Shield Heavy Attack", + "Picking Up", + "Roll" ], "actionMap": { "DEFAULT": "Idle", @@ -51,7 +53,9 @@ "SWORD_BLOCK_MOVE_BACKWARD": "Sword And Shield Block Walk Backwards", "SWORD_BLOCK_STRAFE_LEFT": "Sword And Shield Strafe Left", "SWORD_BLOCK_STRAFE_RIGHT": "Sword And Shield Strafe Right", - "SWORD_HEAVY_ATTACK": "Sword And Shield Heavy Attack" + "SWORD_HEAVY_ATTACK": "Sword And Shield Heavy Attack", + "PICK_UP": "Picking Up", + "ROLL": "Roll" }, "previewModelPath": "Characters/Models/hero.j3o", "animOffsets": { diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java index 3f0a6fc..84023ad 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java @@ -14,6 +14,7 @@ public enum AnimationAction { SPRINT, JUMP, RUNNING_JUMP, + ROLL, DUCK, PICK_UP, LIE_DOWN, @@ -78,6 +79,7 @@ public enum AnimationAction { case SPRINT -> TextRegistry.resolve(null, key, "Sprint"); case JUMP -> TextRegistry.resolve(null, key, "Jump"); case RUNNING_JUMP -> TextRegistry.resolve(null, key, "Running Jump"); + case ROLL -> TextRegistry.resolve(null, key, "Rolle"); case DUCK -> TextRegistry.resolve(null, key, "Duck"); case PICK_UP -> TextRegistry.resolve(null, key, "Pick up"); case LIE_DOWN -> TextRegistry.resolve(null, key, "Hinlegen"); diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 6775a16..66663ef 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -64,6 +64,8 @@ public class PlayerInputControl { private static final int STAMINA_ATTACK5_COST = 20; private static final int STAMINA_HEAVY_COST = 30; private static final int STAMINA_BLOCK_HIT_COST = 30; + private static final int STAMINA_ROLL_COST = 20; + private static final float ROLL_DOUBLE_TAP_WINDOW = 0.3f; /** Dauer der Ziehen/Wegstecken-Animation in Sekunden. */ private static final float DRAW_DURATION = 0.85f; // Ziel-Quaternions für die Ziehen-Animation: aus Heavy-Attack-Wind-Up bei t=0.933s gelesen @@ -194,6 +196,8 @@ public class PlayerInputControl { private com.jme3.anim.SkinningControl skinningControl = null; private com.jme3.anim.Armature armature = null; + private com.jme3.anim.Joint hipsJoint = null; + // ── Fußgeräusche ───────────────────────────────────────────────────────── // Bone-Namen nach Phase-0-Bone-Log eintragen: @@ -258,6 +262,17 @@ public class PlayerInputControl { /** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */ private Vector3f autopilotDir = null; + // ── Dodge Roll ──────────────────────────────────────────────────────────── + // Double-tap-Timer pro Richtung: -1 = kein erster Tap ausstehend + private float rollTapFwd = -1f; + private float rollTapBwd = -1f; + private float rollTapLeft = -1f; + private float rollTapRight = -1f; + private int pendingRollDir = 0; // 1=vor, 2=zurück, 3=links, 4=rechts + private boolean rollActive = false; + private float rollRemaining = 0f; + private Vector3f rollDir = null; + // Pre-allokierte Scratch-Objekte — keine Heap-Allokation pro Frame in update() private final Vector3f scratchCamDir = new Vector3f(); private final Vector3f scratchCamLeft = new Vector3f(); @@ -270,10 +285,10 @@ public class PlayerInputControl { private final ActionListener actionListener = (name, isPressed, tpf) -> { if (paused) return; switch (name) { - case "Forward" -> forward = isPressed; - case "Backward" -> backward = isPressed; - case "Left" -> left = isPressed; - case "Right" -> right = isPressed; + case "Forward" -> { if (isPressed) detectRollTap(1); forward = isPressed; } + case "Backward" -> { if (isPressed) detectRollTap(2); backward = isPressed; } + case "Left" -> { if (isPressed) detectRollTap(3); left = isPressed; } + case "Right" -> { if (isPressed) detectRollTap(4); right = isPressed; } case "Sprint" -> sprint = isPressed; case "Walk" -> walk = isPressed; case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } } @@ -283,19 +298,7 @@ public class PlayerInputControl { case ACT_COMBAT_ATK -> { if (isPressed) { if (!weaponDrawn || blockingAnimActive || drawAnimActive || isBlocking()) break; - if (combatAnimActive) { - if (comboWindowActive) { - // Kombo-Follow während zweiter Hälfte: sofort fluid weiter - combatAnimActive = false; - combatAnimRemaining = 0f; - combatAnimSkipComboWindow = false; - heavyLungeVel = null; - heavyLungeStartRemaining = 0f; - heavyLungeEndRemaining = 0f; - handleCombatAttack(); - } - break; - } + // Immer Hold-Timer starten – auch mitten in einer Kombo attackHoldPending = true; attackHoldTimer = 0f; } else { @@ -303,12 +306,20 @@ public class PlayerInputControl { attackHoldPending = false; if (attackHoldTimer > QUICK_TAP_THRESHOLD) { handleHeavyAttack(); - } else { - // Schneller Klick → schwere Anim abbrechen, normale Kombo starten - combatAnimActive = false; - combatAnimRemaining = 0f; + } else if (combatAnimActive && comboWindowActive) { + // Schneller Klick im Kombo-Fenster → Kombo fortsetzen + combatAnimActive = false; + combatAnimRemaining = 0f; + combatAnimSkipComboWindow = false; + heavyLungeVel = null; + heavyLungeStartRemaining = 0f; + heavyLungeEndRemaining = 0f; + handleCombatAttack(); + } else if (!combatAnimActive) { + // Schneller Klick außerhalb Kombo → normaler Angriff handleCombatAttack(); } + // Schneller Klick während Kombo aber außerhalb Fenster → ignorieren } } } @@ -355,6 +366,7 @@ public class PlayerInputControl { if (skinningControl != null) { armature = skinningControl.getArmature(); logJointNames(armature); + hipsJoint = armature.getJoint("mixamorig:Hips"); jointRightArm = armature.getJoint("mixamorig:RightArm"); jointRightForeArm = armature.getJoint("mixamorig:RightForeArm"); jointLeftArm = armature.getJoint("mixamorig:LeftArm"); @@ -685,7 +697,7 @@ public class PlayerInputControl { runningClip = animPlayer.getCurrentClip(); } - if (visual != null && animOffsetElapsed < AnimPlayer.BLEND_DURATION) { + if (visual != null && animOffsetElapsed < AnimPlayer.BLEND_DURATION && !rollActive) { animOffsetElapsed += tpf; float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION); animOffsetCurrent.set(animOffsetFrom); @@ -704,6 +716,29 @@ public class PlayerInputControl { return; } + // Double-tap-Timer ablaufen lassen + if (rollTapFwd > 0f) { rollTapFwd -= tpf; if (rollTapFwd < 0f) rollTapFwd = -1f; } + if (rollTapBwd > 0f) { rollTapBwd -= tpf; if (rollTapBwd < 0f) rollTapBwd = -1f; } + if (rollTapLeft > 0f) { rollTapLeft -= tpf; if (rollTapLeft < 0f) rollTapLeft = -1f; } + if (rollTapRight > 0f) { rollTapRight -= tpf; if (rollTapRight < 0f) rollTapRight = -1f; } + + // Doppeltippen → Rolle einleiten (nicht während Kampf, Block, Pickup usw.) + if (pendingRollDir != 0) { + if (!rollActive && !pickupActive && !blockingAnimActive && !turnActive + && !lockedInPlace && !combatAnimActive && !isBlocking()) { + cam.getDirection(scratchCamDir).setY(0).normalizeLocal(); + cam.getLeft(scratchCamLeft).setY(0).normalizeLocal(); + Vector3f dir = switch (pendingRollDir) { + case 1 -> scratchCamDir.clone(); + case 2 -> scratchCamDir.clone().negateLocal(); + case 3 -> scratchCamLeft.clone(); + default -> scratchCamLeft.clone().negateLocal(); + }; + startRoll(dir); + } + pendingRollDir = 0; + } + // Navigator: hat Vorrang vor allem außer Pause if (navigator != null && navigator.isActive()) { navigator.update(tpf); @@ -722,6 +757,20 @@ public class PlayerInputControl { } } + if (rollActive) { + rollRemaining -= tpf; + if (rollRemaining <= 0f) { + rollActive = false; + rollDir = null; + currentAnim = null; + } else { + if (physicsChar != null) { + physicsChar.setWalkDirection(Vector3f.ZERO); + } + return; + } + } + // Drehung auf der Stelle (kein Vorwärtsbewegen) if (turnActive) { turnRemaining -= tpf; @@ -838,9 +887,20 @@ public class PlayerInputControl { // Hold-Timer muss jede Frame akkumulieren, unabhängig vom Anim-Status if (attackHoldPending) { attackHoldTimer += tpf; - // Erst wenn klar ist dass es ein schwerer Angriff wird → Animation starten - if (attackHoldTimer > QUICK_TAP_THRESHOLD && !combatAnimActive) { - startHeavyCharge(); + if (attackHoldTimer > QUICK_TAP_THRESHOLD) { + if (combatAnimActive && !combatAnimSkipComboWindow) { + // Normale Kombo unterbrechen und schweren Angriff einleiten + combatAnimActive = false; + combatAnimRemaining = 0f; + comboWindowActive = false; + comboStep = 0; + heavyLungeVel = null; + heavyLungeStartRemaining = 0f; + heavyLungeEndRemaining = 0f; + startHeavyCharge(); // setzt combatAnimSkipComboWindow = true → verhindert Loop + } else if (!combatAnimActive) { + startHeavyCharge(); + } } } @@ -1271,6 +1331,41 @@ public class PlayerInputControl { return true; } + private void detectRollTap(int dir) { + float cur = switch (dir) { + case 1 -> rollTapFwd; + case 2 -> rollTapBwd; + case 3 -> rollTapLeft; + default -> rollTapRight; + }; + if (cur > 0f) { + pendingRollDir = dir; + } else { + switch (dir) { + case 1 -> rollTapFwd = ROLL_DOUBLE_TAP_WINDOW; + case 2 -> rollTapBwd = ROLL_DOUBLE_TAP_WINDOW; + case 3 -> rollTapLeft = ROLL_DOUBLE_TAP_WINDOW; + default -> rollTapRight = ROLL_DOUBLE_TAP_WINDOW; + } + } + } + + private void startRoll(Vector3f dir) { + if (!spendStamina(STAMINA_ROLL_COST)) return; + nextTransitionLength = 0.1f; + rollActive = true; + rollRemaining = resolveClipLength(AnimationAction.ROLL, 0.7f); + rollDir = dir; + forward = backward = left = right = false; + if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); + if (visual != null) { + scratchRot.lookAt(dir, Vector3f.UNIT_Y); + visual.setLocalRotation(scratchRot); + } + playAction(AnimationAction.ROLL); + currentAnim = AnimationAction.ROLL; + } + private boolean hasShieldEquipped() { return mainCharacter != null && mainCharacter.getEquippedShield() != null; } diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index df1a904..ecb1ae8 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -409,7 +409,7 @@ public class WorldScene extends BaseAppState { WorldNpcsState npcsState = new WorldNpcsState(keyBindings, physicsChar, playerInput, mc); npcsState.setThirdPersonCamera(thirdPersonCam); app.getStateManager().attach(npcsState); - app.getStateManager().attach(new InteractionHudState()); + app.getStateManager().attach(new InteractionHudState(keyBindings)); inventoryState = new InventoryState(mc, keyBindings); inventoryState.setEnabled(false); app.getStateManager().attach(inventoryState); diff --git a/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java b/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java index c97fb20..29d545e 100644 --- a/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java +++ b/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java @@ -10,6 +10,8 @@ import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.scene.Node; import com.jme3.scene.Spatial; +import de.blight.game.config.KeyBindings; +import de.blight.game.config.KeyNames; import de.blight.lang.TextResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +35,10 @@ public class InteractionHudState extends BaseAppState { private DialogHudState dialogHud; private BitmapText labelText; + private KeyBindings keyBindings; + + public InteractionHudState() {} + public InteractionHudState(KeyBindings keyBindings) { this.keyBindings = keyBindings; } // ── Lifecycle ───────────────────────────────────────────────────────────── @@ -134,6 +140,9 @@ public class InteractionHudState extends BaseAppState { } String text = TextResolver.get().resolveId(labelKey); + if (keyBindings != null) { + text = text.replace("{interact}", KeyNames.of(keyBindings.interact)); + } labelText.setText(text); float textW = labelText.getLineWidth(); labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f); diff --git a/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java b/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java index 1a8dd3d..751e900 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java @@ -8,8 +8,6 @@ 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; import com.jme3.input.MouseInput; import com.jme3.input.controls.ActionListener; @@ -37,6 +35,7 @@ import de.blight.common.PlacedModel; import de.blight.common.PlacedModelIO; import de.blight.common.model.CombatDummy; import de.blight.common.model.CombatDummyIO; +import de.blight.game.animation.AnimationAction; import de.blight.game.combat.DamageCalculator; import de.blight.game.combat.MeleeAttackCallback; import de.blight.game.config.KeyBindings; @@ -62,11 +61,12 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback // ── Spielwelt-Konstanten ────────────────────────────────────────────────── private static final float FOCUS_RANGE = 6f; private static final float RESTORE_DELAY = 60f; - private static final float HP_BAR_SHOW_TIME = 5f; + //private static final float HP_BAR_SHOW_TIME = 5f; private static final float STAND_UP_RANGE = 3f; - private static final int DEFAULT_MAX_HP = 250; - private static final float FALL_DURATION = 0.35f; - private static final float MODEL_HALF_H = 0.75f; + private static final int DEFAULT_MAX_HP = 10; + private static final float MODEL_HALF_H = 0.75f; + private static final float KNOCK_INITIAL_VEL = 0.4f; // rad/s initial push + private static final float KNOCK_GRAVITY = 8f; // angular accel at upright (rad/s²) // ── Welt-HP-Bar-Konstanten ──────────────────────────────────────────────── private static final float WB_WIDTH = 0.8f; @@ -103,8 +103,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback // ── Feind-HUD ───────────────────────────────────────────────────────────── private Node enemyHudNode; private Geometry enemyHpFill; - private BitmapText enemyNameText; - private BitmapText enemyHpText; /** x-Koordinate des linken Randes der Feind-HP-Bar (Screen-Pixel). */ private float ehbX; /** y-Koordinate des unteren Randes der Feind-HP-Bar (Screen-Pixel). */ @@ -314,25 +312,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback enemyHpFill.setLocalTranslation(ehbX + EHB_BORDER, ehbY + EHB_BORDER, 2f); enemyHudNode.attachChild(enemyHpFill); - BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt"); - float renderedSize = font.getCharSet().getRenderedSize(); - - // Gegner-Name (über Bar) - enemyNameText = new BitmapText(font); - enemyNameText.setSize(renderedSize * 2f); - enemyNameText.setLocalScale(0.5f); - enemyNameText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f)); - enemyNameText.setQueueBucket(RenderQueue.Bucket.Gui); - enemyHudNode.attachChild(enemyNameText); - - // HP-Zahlen (in Bar) - enemyHpText = new BitmapText(font); - enemyHpText.setSize(renderedSize * 2f); - enemyHpText.setLocalScale(0.5f); - enemyHpText.setColor(ColorRGBA.White); - enemyHpText.setQueueBucket(RenderQueue.Bucket.Gui); - enemyHudNode.attachChild(enemyHpText); - guiNode.attachChild(enemyHudNode); enemyHudNode.setCullHint(Spatial.CullHint.Always); } @@ -375,23 +354,8 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback } private void updateEnemyHudContent(DummyRuntime dr) { - float sw = cam.getWidth(); float ratio = dr.maxHp > 0 ? (float) dr.currentHp / dr.maxHp : 0f; applyEnemyHpRatio(ratio); - - String name = "Kampfpuppe"; - enemyNameText.setText(name); - float nameW = enemyNameText.getLineWidth() * 0.5f; - enemyNameText.setLocalTranslation( - sw * 0.5f - nameW, - ehbY + EHB_H + EHB_BORDER * 2 + 6f, 3f); - - String hpStr = dr.currentHp + " / " + dr.maxHp; - enemyHpText.setText(hpStr); - float hpW = enemyHpText.getLineWidth() * 0.5f; - enemyHpText.setLocalTranslation( - sw * 0.5f - hpW, - ehbY + EHB_BORDER + 1f, 3f); } // ── MeleeAttackCallback ─────────────────────────────────────────────────── @@ -437,14 +401,19 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback } private void knockout(DummyRuntime dr) { - dr.fallen = true; - dr.fallenLerp = 0f; + dr.fallen = true; + dr.fallenLerp = 0f; + dr.knockAngle = 0f; + dr.knockAngVel = KNOCK_INITIAL_VEL; dr.hpBarNode.setCullHint(Spatial.CullHint.Always); } private void standUp(DummyRuntime dr) { + playerInput.requestAnimation(AnimationAction.PICK_UP, 0f, null); dr.fallen = false; dr.fallenLerp = 0f; + dr.knockAngle = 0f; + dr.knockAngVel = 0f; dr.currentHp = dr.maxHp; dr.restoreTimer = 0f; dr.dummyNode.setLocalRotation(new Quaternion()); @@ -476,13 +445,18 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback for (int i = 0; i < dummies.size(); i++) { DummyRuntime dr = dummies.get(i); - // Umfall-Animation + // Umfall-Animation (physikalisch: Schwerkraft-Drehmoment nimmt mit cos(Winkel) ab) if (dr.fallen && dr.fallenLerp < 1f) { - dr.fallenLerp = Math.min(1f, dr.fallenLerp + tpf / FALL_DURATION); - Quaternion target = new Quaternion().fromAngles(0f, 0f, -FastMath.HALF_PI); - Quaternion cur = new Quaternion(); - cur.slerp(Quaternion.IDENTITY, target, dr.fallenLerp); - dr.dummyNode.setLocalRotation(cur); + float gravAccel = KNOCK_GRAVITY * FastMath.cos(dr.knockAngle); + dr.knockAngVel += gravAccel * tpf; + dr.knockAngle += dr.knockAngVel * tpf; + if (dr.knockAngle >= FastMath.HALF_PI) { + dr.knockAngle = FastMath.HALF_PI; + dr.knockAngVel = 0f; + dr.fallenLerp = 1f; + } + dr.dummyNode.setLocalRotation( + new Quaternion().fromAngles(-dr.knockAngle, 0f, 0f)); } // Welt-HP-Balken-Timer @@ -588,6 +562,8 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback float hpBarTimer = 0f; boolean fallen = false; float fallenLerp = 0f; + float knockAngle = 0f; // aktueller Kippwinkel (0..PI/2) + float knockAngVel = 0f; // Winkelgeschwindigkeit (rad/s) DummyRuntime(String id, float wx, float wy, float wz, Node dummyNode, Node hpBarNode, Geometry hpBarFg, int maxHp, diff --git a/blight-lang/src/main/resources/lang/messages_de.properties b/blight-lang/src/main/resources/lang/messages_de.properties index 20e97fa..d5c54c1 100644 --- a/blight-lang/src/main/resources/lang/messages_de.properties +++ b/blight-lang/src/main/resources/lang/messages_de.properties @@ -7,7 +7,7 @@ dialog.speaker.player=Spieler # ── Interactables ───────────────────────────────────────────────────────────── interactable.bench.name=Bank interactable.bed.name=Bett -dummy.stand_up=Aufstellen [F] +dummy.stand_up=Aufstellen [{interact}] dummy.name=Kampfpuppe # ── Items ──────────────────────────────────────────────────────────────────── diff --git a/blight-lang/src/main/resources/lang/messages_en.properties b/blight-lang/src/main/resources/lang/messages_en.properties index 929734a..742447b 100644 --- a/blight-lang/src/main/resources/lang/messages_en.properties +++ b/blight-lang/src/main/resources/lang/messages_en.properties @@ -7,7 +7,7 @@ dialog.speaker.player=Player # ── Interactables ───────────────────────────────────────────────────────────── interactable.bench.name=Bench interactable.bed.name=Bed -dummy.stand_up=Stand Up [F] +dummy.stand_up=Stand Up [{interact}] dummy.name=Training Dummy # ── Items ────────────────────────────────────────────────────────────────────