tail wag works
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,7 +17,15 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
|
||||
| `player.gd` | Movement, charge, turning logic |
|
||||
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
||||
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
||||
| `matador.gd` | Matador AI (wander / ragdoll state machine) |
|
||||
| `matador_spawn.gd` | Spawns N matadors at random arena positions |
|
||||
| `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) |
|
||||
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
||||
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
||||
| `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
||||
| `Blender/render_anim_preview.py` | Renders animation preview PNGs for visual verification |
|
||||
| `Blender/preview/` | Output directory for animation preview images |
|
||||
| `tests/` | Headless test scripts (screenshot capture, logic validation) |
|
||||
| `addons/rider-plugin/` | JetBrains Rider IDE integration |
|
||||
|
||||
## Tech choices
|
||||
@@ -48,6 +56,57 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
|
||||
- Keep `_physics_process` deterministic and frame-rate independent (always multiply by `delta`)
|
||||
- Prefer signals over direct node references for decoupling
|
||||
|
||||
## Animation & bone conventions
|
||||
|
||||
### Blender rig (matador_v02)
|
||||
- **Armature bones:** `matador` (root) → `COG` → `chest` → `head`, `collarbone_L/R` → `arm_L/R` → `forearm_L/R` → `hand_L/R`, `leg_L/R` → `shin_L/R` → `foot_L/R`
|
||||
- **Rotation mode:** All animated bones use **XYZ Euler** (set in `create_matador_anims.py`)
|
||||
- **Bone-local axes:** Swing (forward/back) is on **local X**. Arm lowering from T-pose is on **local Y** (positive = left arm down, negative = right arm down). Knee bend is **positive X** (shins only bend backward).
|
||||
- **Walk cycle:** 30 frames at 30 fps = 1 second loop. Legs swing ±25° X, knees bend 0–30° X (only when leg is back), arms swing ±15° X opposite phase to legs, arms lowered 55° Y from T-pose, elbows constant 25° X bend.
|
||||
- **Idle pose:** Arms lowered 55° Y, elbows 25° X, everything else at rest.
|
||||
|
||||
### FBX export settings
|
||||
- `primary_bone_axis = 'Y'`, `secondary_bone_axis = 'X'`
|
||||
- `apply_scale_options = 'FBX_SCALE_ALL'`
|
||||
- `add_leaf_bones = False`
|
||||
- `bake_anim_use_all_actions = True`, `bake_anim_use_nla_strips = False`
|
||||
|
||||
### Godot animation names
|
||||
After FBX import, animations appear as `Armature|walk` and `Armature|idle` in AnimationPlayer.
|
||||
|
||||
### Verification workflow
|
||||
1. Edit bones/animations in Blender (`create_matador_anims.py`)
|
||||
2. Run render preview: `blender --background Blender/matador_v02.blend --python Blender/render_anim_preview.py`
|
||||
3. Inspect PNGs in `Blender/preview/` — check bone orientations **before** exporting to Godot
|
||||
4. Export FBX (done automatically by `create_matador_anims.py`)
|
||||
5. Re-import in Godot and test in-game
|
||||
|
||||
## Running tests
|
||||
|
||||
```bash
|
||||
# All tests (lint + logic + gameplay assertions):
|
||||
bash run_tests.sh
|
||||
|
||||
# Individual tests:
|
||||
gdlint *.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
||||
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
||||
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
||||
|
||||
# Gameplay test with real rendering (inspect frame strip visually):
|
||||
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
||||
# Output: tests/output/gameplay/motion_00..04.png, ragdoll_trigger.png, ragdoll_result.png
|
||||
```
|
||||
|
||||
### What the gameplay test catches
|
||||
| Check | What it detects |
|
||||
|---|---|
|
||||
| Tail Verlet chain spread | Tail bunching — consecutive nodes collapsed to same position |
|
||||
| Ragdoll bone positions finite + within 15 m | Matador limbs exploding after ragdoll impulse |
|
||||
| Ragdoll Y position > −10 | Bones falling through the floor |
|
||||
| Frame strip (visual, manual) | Leg IK quality, animation glitches, anything that looks wrong in motion |
|
||||
|
||||
Headless screenshots will be blank (Forward Plus has no display). Run without `--headless` to get real frame strips.
|
||||
|
||||
## Godot 4.x specifics
|
||||
|
||||
- `wrapf` / `wrap` instead of manual modulo for angles
|
||||
|
||||
+35
-11
@@ -108,18 +108,41 @@ func _setup_tail() -> void:
|
||||
push_error("BullLegs: not enough tail bones found")
|
||||
return
|
||||
|
||||
for i in range(_tail_idx.size() - 1):
|
||||
var a: Transform3D = _skeleton.get_bone_global_rest(_tail_idx[i])
|
||||
var b: Transform3D = _skeleton.get_bone_global_rest(_tail_idx[i + 1])
|
||||
_tail_lengths.append((b.origin - a.origin).length())
|
||||
_tail_rest_dir.append((b.origin - a.origin).normalized())
|
||||
_tail_rest_dir.append(_tail_rest_dir[-1])
|
||||
|
||||
# Initialise world positions first
|
||||
for idx: int in _tail_idx:
|
||||
var world: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(idx).origin)
|
||||
_tail_world.append(world)
|
||||
_tail_prev.append(world)
|
||||
|
||||
# Detect degenerate rest pose (FBX bones all at same position)
|
||||
var max_sep: float = 0.0
|
||||
for i in range(_tail_world.size() - 1):
|
||||
max_sep = maxf(max_sep, _tail_world[i].distance_to(_tail_world[i + 1]))
|
||||
if max_sep < 0.01:
|
||||
push_warning("BullLegs: tail bones have zero separation in rest pose, distributing along spine")
|
||||
var root: Vector3 = _tail_world[0]
|
||||
# Backward-downward direction in world space from the skeleton
|
||||
var back: Vector3 = (_skeleton.global_transform.basis.z + Vector3.DOWN * 0.3).normalized()
|
||||
for i in range(_tail_world.size()):
|
||||
_tail_world[i] = root + back * (i * 0.09)
|
||||
_tail_prev[i] = _tail_world[i]
|
||||
|
||||
# Segment lengths in world space — used by the Verlet length constraint
|
||||
const MIN_SEG: float = 0.06
|
||||
for i in range(_tail_world.size() - 1):
|
||||
var seg_len: float = _tail_world[i].distance_to(_tail_world[i + 1])
|
||||
_tail_lengths.append(maxf(seg_len, MIN_SEG))
|
||||
|
||||
# Rest directions in SKELETON-LOCAL space — must match point_dir_skel in _apply_tail
|
||||
# so that Quaternion(rest_dir, point_dir_skel) operates in a consistent frame.
|
||||
# _tail_world holds world positions; to_local converts them to skeleton space.
|
||||
for i in range(_tail_world.size() - 1):
|
||||
var p1_skel: Vector3 = _skeleton.to_local(_tail_world[i])
|
||||
var p2_skel: Vector3 = _skeleton.to_local(_tail_world[i + 1])
|
||||
var seg_skel: Vector3 = p2_skel - p1_skel
|
||||
_tail_rest_dir.append(seg_skel.normalized() if seg_skel.length() > 0.001 else Vector3(0, 0, 1))
|
||||
_tail_rest_dir.append(_tail_rest_dir[-1])
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _skeleton:
|
||||
@@ -271,7 +294,7 @@ func _step_launch(_delta: float) -> void:
|
||||
_foot[id] = _launch_feet[id]
|
||||
|
||||
|
||||
func _step_ragdoll(delta: float) -> void:
|
||||
func _step_ragdoll(_delta: float) -> void:
|
||||
# Procedural floppy dangle — NOT real physics ragdoll
|
||||
var time: float = _state_time
|
||||
var amp: float = DP.f("ragdoll_wobble_amp")
|
||||
@@ -335,10 +358,10 @@ func _get_tucked_foot(id: String) -> Vector3:
|
||||
|
||||
|
||||
func _apply_ik() -> void:
|
||||
var skel_inv: Transform3D = _skeleton.global_transform.affine_inverse()
|
||||
for id: String in _ik:
|
||||
var ik: SkeletonIK3D = _ik[id]
|
||||
var target_local: Vector3 = _skeleton.get_parent().to_local(_foot[id])
|
||||
ik.target = Transform3D(Basis.IDENTITY, target_local)
|
||||
ik.target = Transform3D(Basis.IDENTITY, skel_inv * _foot[id])
|
||||
|
||||
|
||||
# ── Tail ──────────────────────────────────────────────────────────────────────
|
||||
@@ -365,7 +388,8 @@ func _update_tail(delta: float) -> void:
|
||||
var vel: Vector3 = (_tail_world[i] - _tail_prev[i]) * damping
|
||||
_tail_prev[i] = _tail_world[i]
|
||||
# Add wag force (sinusoidal side-to-side)
|
||||
var wag := _bull.global_transform.basis.x * sin(time * DP.f("tail_wag_freq") + i * 0.5) * wag_strength
|
||||
var wag := _bull.global_transform.basis.x \
|
||||
* sin(time * DP.f("tail_wag_freq") + i * 0.5) * wag_strength
|
||||
_tail_world[i] = _tail_world[i] + vel + gravity * (delta * delta) + wag * delta
|
||||
|
||||
# Length constraint
|
||||
|
||||
+9
-4
@@ -27,7 +27,7 @@ func _register_all() -> void:
|
||||
_reg_f("Movement", "bounce_min_impact", 1.0, 0.0, 20.0)
|
||||
_reg_f("Movement", "kick_impulse", 8.0, 1.0, 50.0)
|
||||
_reg_f("Movement", "kick_interval", 0.25, 0.05, 1.0, 0.01)
|
||||
_reg_f("Movement", "kick_vertical", 3.0, 0.0, 15.0)
|
||||
_reg_f("Movement", "kick_vertical", 0.0, 0.0, 15.0)
|
||||
_reg_f("Movement", "kick_force_var", 0.2, 0.0, 1.0, 0.01)
|
||||
_reg_f("Movement", "kick_spread", 10.0, 0.0, 45.0, 1.0)
|
||||
_reg_f("Movement", "charge_kick_mult", 2.0, 1.0, 5.0)
|
||||
@@ -86,7 +86,7 @@ func _register_all() -> void:
|
||||
_reg_f("Matador", "mat_idle_min", 0.5, 0.0, 5.0, 0.1)
|
||||
_reg_f("Matador", "mat_idle_max", 2.5, 0.5, 10.0, 0.1)
|
||||
_reg_f("Matador", "mat_hit_threshold", 4.0, 1.0, 30.0)
|
||||
_reg_f("Matador", "mat_ragdoll_impulse", 8.0, 1.0, 50.0)
|
||||
_reg_f("Matador", "mat_ragdoll_impulse", 3.0, 0.5, 20.0)
|
||||
# ── Debug ─────────────────────────────────────────────────────────────────
|
||||
_reg_b("Debug", "show_collisions", false)
|
||||
|
||||
@@ -149,8 +149,13 @@ func load_saved() -> void:
|
||||
return
|
||||
for section: String in cfg.get_sections():
|
||||
for key: String in cfg.get_section_keys(section):
|
||||
if _params.has(key):
|
||||
_params[key]["value"] = cfg.get_value(section, key)
|
||||
if not _params.has(key):
|
||||
continue
|
||||
var p: Dictionary = _params[key]
|
||||
var val: Variant = cfg.get_value(section, key)
|
||||
if p["type"] == TYPE_FLOAT:
|
||||
val = clampf(val as float, p["min"] as float, p["max"] as float)
|
||||
_params[key]["value"] = val
|
||||
|
||||
|
||||
func reset_all() -> void:
|
||||
|
||||
+86
-23
@@ -5,18 +5,31 @@ enum State { WANDER, RAGDOLL }
|
||||
var _state: State = State.WANDER
|
||||
var _skeleton: Skeleton3D = null
|
||||
var _sim: PhysicalBoneSimulator3D = null
|
||||
var _anim_player: AnimationPlayer = null
|
||||
var _wander_target: Vector3 = Vector3.ZERO
|
||||
var _idle_timer: float = 0.0
|
||||
|
||||
@onready var _mesh: Node3D = $matador_v02
|
||||
@onready var _hit_area: Area3D = $HitArea
|
||||
@onready var _mesh: Node3D = $matador_v02
|
||||
@onready var _hit_area: Area3D = $HitArea
|
||||
@onready var _body_col: CollisionShape3D = $CollisionShape3D
|
||||
|
||||
const _WALK_ANIM: StringName = &"Armature|walk"
|
||||
const _IDLE_ANIM: StringName = &"Armature|idle"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group(&"matador")
|
||||
_skeleton = _find_skeleton(_mesh)
|
||||
_anim_player = _find_anim_player(_mesh)
|
||||
if _skeleton:
|
||||
_sim = _setup_physical_bones()
|
||||
if _anim_player:
|
||||
_ensure_loop(_WALK_ANIM)
|
||||
if _anim_player.has_animation(_IDLE_ANIM):
|
||||
_anim_player.play(_IDLE_ANIM)
|
||||
else:
|
||||
push_warning("Matador: idle animation not found. Available: %s" %
|
||||
str(_anim_player.get_animation_list()))
|
||||
_hit_area.body_entered.connect(_on_body_entered)
|
||||
_pick_wander_target()
|
||||
|
||||
@@ -34,6 +47,7 @@ func _tick_wander(delta: float) -> void:
|
||||
_idle_timer -= delta
|
||||
velocity.x = move_toward(velocity.x, 0.0, 10.0 * delta)
|
||||
velocity.z = move_toward(velocity.z, 0.0, 10.0 * delta)
|
||||
_play_anim(_IDLE_ANIM)
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
@@ -49,12 +63,13 @@ func _tick_wander(delta: float) -> void:
|
||||
velocity.z = dir.z * spd
|
||||
_mesh.rotation.y = lerp_angle(
|
||||
_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
||||
_play_anim(_WALK_ANIM)
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func _tick_ragdoll(delta: float) -> void:
|
||||
velocity.x = move_toward(velocity.x, 0.0, 5.0 * delta)
|
||||
velocity.z = move_toward(velocity.z, 0.0, 5.0 * delta)
|
||||
velocity.x = move_toward(velocity.x, 0.0, 15.0 * delta)
|
||||
velocity.z = move_toward(velocity.z, 0.0, 15.0 * delta)
|
||||
move_and_slide()
|
||||
|
||||
|
||||
@@ -73,8 +88,17 @@ func _on_body_entered(body: Node3D) -> void:
|
||||
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
_state = State.RAGDOLL
|
||||
hit_dir.y = 0.0
|
||||
hit_dir = hit_dir.normalized()
|
||||
velocity = Vector3(hit_dir.x, 0.5, hit_dir.z) * bull_speed * 0.4
|
||||
hit_dir = hit_dir.normalized()
|
||||
|
||||
# Cap lateral slide so matadors don't cross the arena; fixed upward pop
|
||||
var lateral: float = minf(bull_speed * 0.2, 4.0)
|
||||
velocity = Vector3(hit_dir.x * lateral, 2.0, hit_dir.z * lateral)
|
||||
|
||||
# Disable body capsule so a fast bull doesn't bounce off a ragdolling matador
|
||||
_body_col.disabled = true
|
||||
|
||||
if _anim_player:
|
||||
_anim_player.stop()
|
||||
|
||||
if not _sim:
|
||||
return
|
||||
@@ -83,13 +107,35 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
for child: Node in _sim.get_children():
|
||||
if child is PhysicalBone3D:
|
||||
var scatter := Vector3(
|
||||
randf_range(-0.3, 0.3),
|
||||
randf_range(0.2, 0.8),
|
||||
randf_range(-0.3, 0.3))
|
||||
randf_range(-0.4, 0.4),
|
||||
randf_range(0.1, 0.5),
|
||||
randf_range(-0.4, 0.4))
|
||||
(child as PhysicalBone3D).apply_central_impulse(
|
||||
(hit_dir + scatter).normalized() * strength)
|
||||
|
||||
|
||||
# ── Animation helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
func _ensure_loop(anim_name: StringName) -> void:
|
||||
if _anim_player.has_animation(anim_name):
|
||||
var anim := _anim_player.get_animation(anim_name)
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
|
||||
func _play_anim(anim_name: StringName) -> void:
|
||||
if not _anim_player:
|
||||
return
|
||||
if not _anim_player.has_animation(anim_name):
|
||||
return
|
||||
if _anim_player.current_animation != anim_name:
|
||||
_anim_player.play(anim_name)
|
||||
|
||||
|
||||
func _reset_bone_poses() -> void:
|
||||
for i: int in _skeleton.get_bone_count():
|
||||
_skeleton.set_bone_pose_rotation(i, Quaternion.IDENTITY)
|
||||
|
||||
|
||||
func _pick_wander_target() -> void:
|
||||
var radius := DP.f("mat_wander_radius")
|
||||
var angle := randf() * TAU
|
||||
@@ -109,19 +155,25 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||
sim.active = false
|
||||
_skeleton.add_child(sim)
|
||||
|
||||
# [bone_name, sphere_radius] or [bone_name, capsule_radius, capsule_height]
|
||||
const BONES: Array = [
|
||||
["matador", 0.18],
|
||||
["chest", 0.13, 0.30],
|
||||
["head", 0.14],
|
||||
["arm_L", 0.07, 0.22],
|
||||
["arm_R", 0.07, 0.22],
|
||||
["forearm_L", 0.06, 0.22],
|
||||
["forearm_R", 0.06, 0.22],
|
||||
["leg_L", 0.10, 0.32],
|
||||
["leg_R", 0.10, 0.32],
|
||||
["shin_L", 0.08, 0.28],
|
||||
["shin_R", 0.08, 0.28],
|
||||
["matador", 0.18],
|
||||
["COG", 0.12],
|
||||
["chest", 0.13, 0.30],
|
||||
["head", 0.14],
|
||||
["collarbone_L", 0.06, 0.18],
|
||||
["collarbone_R", 0.06, 0.18],
|
||||
["arm_L", 0.07, 0.22],
|
||||
["arm_R", 0.07, 0.22],
|
||||
["forearm_L", 0.06, 0.22],
|
||||
["forearm_R", 0.06, 0.22],
|
||||
["hand_L", 0.05, 0.12],
|
||||
["hand_R", 0.05, 0.12],
|
||||
["leg_L", 0.10, 0.32],
|
||||
["leg_R", 0.10, 0.32],
|
||||
["shin_L", 0.08, 0.28],
|
||||
["shin_R", 0.08, 0.28],
|
||||
["foot_L", 0.07, 0.18],
|
||||
["foot_R", 0.07, 0.18],
|
||||
]
|
||||
|
||||
for entry: Array in BONES:
|
||||
@@ -130,8 +182,9 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||
continue
|
||||
var pb := PhysicalBone3D.new()
|
||||
pb.bone_name = bname
|
||||
pb.linear_damp = 0.5
|
||||
pb.angular_damp = 0.5
|
||||
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
||||
pb.linear_damp = 1.0
|
||||
pb.angular_damp = 1.0
|
||||
var cs := CollisionShape3D.new()
|
||||
var use_capsule: bool = entry.size() >= 3
|
||||
if use_capsule:
|
||||
@@ -159,3 +212,13 @@ func _find_skeleton(node: Node) -> Skeleton3D:
|
||||
if r:
|
||||
return r
|
||||
return null
|
||||
|
||||
|
||||
func _find_anim_player(node: Node) -> AnimationPlayer:
|
||||
if node is AnimationPlayer:
|
||||
return node as AnimationPlayer
|
||||
for child: Node in node.get_children():
|
||||
var r := _find_anim_player(child)
|
||||
if r:
|
||||
return r
|
||||
return null
|
||||
|
||||
@@ -18,8 +18,8 @@ var _kick_timer: float = 0.0
|
||||
var _was_on_floor: bool = false
|
||||
var _pre_slide_vel_y: float = 0.0
|
||||
|
||||
enum _DustState { NONE, WALK, CHARGE }
|
||||
var _dust_state: _DustState = _DustState.NONE
|
||||
enum DustState { NONE, WALK, CHARGE }
|
||||
var _dust_state: DustState = DustState.NONE
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -82,7 +82,7 @@ func _setup_hoof_dust() -> void:
|
||||
_hoof_emitters.append(p)
|
||||
|
||||
|
||||
func _set_dust_state(new_state: _DustState) -> void:
|
||||
func _set_dust_state(new_state: DustState) -> void:
|
||||
if new_state == _dust_state:
|
||||
return
|
||||
_dust_state = new_state
|
||||
@@ -92,15 +92,15 @@ func _set_dust_state(new_state: _DustState) -> void:
|
||||
p.spread = DP.f("dust_spread")
|
||||
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
||||
match new_state:
|
||||
_DustState.NONE:
|
||||
DustState.NONE:
|
||||
p.emitting = false
|
||||
_DustState.WALK:
|
||||
DustState.WALK:
|
||||
p.scale_amount_min = DP.f("walk_scale_min")
|
||||
p.scale_amount_max = DP.f("walk_scale_max")
|
||||
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||
p.emitting = true
|
||||
_DustState.CHARGE:
|
||||
DustState.CHARGE:
|
||||
p.scale_amount_min = DP.f("charge_scale_min")
|
||||
p.scale_amount_max = DP.f("charge_scale_max")
|
||||
p.initial_velocity_min = DP.f("charge_vel_min")
|
||||
@@ -109,10 +109,10 @@ func _set_dust_state(new_state: _DustState) -> void:
|
||||
|
||||
|
||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||
if _hoof_emitters.is_empty() or _dust_state == _DustState.NONE:
|
||||
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
|
||||
return
|
||||
var prev := _dust_state
|
||||
_dust_state = _DustState.NONE
|
||||
_dust_state = DustState.NONE
|
||||
_set_dust_state(prev)
|
||||
|
||||
|
||||
@@ -181,12 +181,14 @@ func _physics_process(delta: float) -> void:
|
||||
# Left click → thrust NE (forward-right diagonal)
|
||||
# Right click → thrust NW (forward-left diagonal)
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
if Input.is_action_just_pressed(&"thrust_left"):
|
||||
var ne := (flat_forward + flat_right).normalized()
|
||||
_apply_thrust(ne if ne.length() > 0.1 else flat_forward)
|
||||
if Input.is_action_just_pressed(&"thrust_right"):
|
||||
var nw := (flat_forward - flat_right).normalized()
|
||||
_apply_thrust(nw if nw.length() > 0.1 else flat_forward)
|
||||
if Input.is_action_just_pressed(&"thrust_left") or Input.is_action_just_pressed(&"thrust_right"):
|
||||
var raw := cube_guy.global_transform.basis.z
|
||||
var bull_fwd := Vector3(raw.x, 0.0, raw.z).normalized()
|
||||
var bull_right := bull_fwd.cross(Vector3.UP)
|
||||
if Input.is_action_just_pressed(&"thrust_left"):
|
||||
_apply_thrust((bull_fwd + bull_right).normalized())
|
||||
else:
|
||||
_apply_thrust((bull_fwd - bull_right).normalized())
|
||||
|
||||
# Reset kick timer on landing so the first kick after a bounce is immediate
|
||||
if on_floor and not _was_on_floor and direction:
|
||||
@@ -222,8 +224,8 @@ func _physics_process(delta: float) -> void:
|
||||
|
||||
# ── Dust ──────────────────────────────────────────────────────────────────
|
||||
if flat_speed > DP.f("dust_charge_spd") and on_floor:
|
||||
_set_dust_state(_DustState.CHARGE)
|
||||
_set_dust_state(DustState.CHARGE)
|
||||
elif flat_speed > DP.f("dust_walk_spd") and on_floor:
|
||||
_set_dust_state(_DustState.WALK)
|
||||
_set_dust_state(DustState.WALK)
|
||||
else:
|
||||
_set_dust_state(_DustState.NONE)
|
||||
_set_dust_state(DustState.NONE)
|
||||
|
||||
Reference in New Issue
Block a user