feat: real per-weapon reload/recoil choreography + knife slash, fully matte hair
Weapons no longer reload by spinning the gun 360 (that was the upside-down
flip; nothing detached). New ViewmodelAnim module drives per-style manual-of-
arms sequences on the existing model_root + named arm pivots, with a temporary
magazine/shell/rocket/cell prop that visibly leaves and returns to the gun:
- mag (M4/AK/MP7/DMR): roll gun to present the well, hand rips the mag
down and away, fresh mag up, seat, charge. Gun stays UPRIGHT.
- boltmag (AWP): mag swap + a distinct bolt-cycle rock.
- break (double barrel): hinge open, flick both shells out, drop fresh
shells in, snap shut.
- tube (rocket launcher): tip the tube in, shove a rocket home, shoulder.
- cell (plasma/nail gun): glowing energy cell swapped on the side.
Per-shot viewmodel recoil kick (ViewmodelAnim.apply_kick) scaled by each
weapon's recoil_amplitude, so an AWP slams and an MP7 chatters — replaces the
old flat model behaviour. Weapons expose reload_style; base classes call
play_reload / stop (on unequip) / apply_kick.
Knife: the old barely-visible flick is now a real diagonal slash that
alternates backhand/forehand, whips the blade through screen centre, and
fires the third-person melee arm-swing action (synced to other players).
Hair/materials: specular fully to 0 (even a 2% stepped glint swept as shine
across Taila's hair when the camera moved); rim trimmed to a whisper. Fully
matte cloth/hair now.
Verify: debug/fp_weapon_capture.gd screenshots each weapon's reload phases,
the knife slash, and the fire kick.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d46530bd8c
commit
df0bf18e0f
@@ -0,0 +1,156 @@
|
||||
extends SceneTree
|
||||
|
||||
## Dev tool: screenshot first-person weapon choreography — reload phases for
|
||||
## representative weapons (mag / boltmag / tube / break), the knife slash,
|
||||
## and the fire kick. Run:
|
||||
## godot --path . --windowed --resolution 1280x720 -s res://debug/fp_weapon_capture.gd -- <out_dir>
|
||||
|
||||
var _frames := 0
|
||||
var _out_dir := "."
|
||||
var _player: Node = null
|
||||
var _wman: Node = null
|
||||
var _mode := "boot"
|
||||
var _phase_frame := 0
|
||||
var _step := 0
|
||||
|
||||
# [tag, slot, weapon_id, screenshot fractions of reload_time]
|
||||
# Slots 11+ so the loadout's own weapons get properly hidden by _equip_slot
|
||||
# (re-spawning INTO an occupied slot leaves the old node visible).
|
||||
var _weapons := [
|
||||
["m4", 11, "m4", [0.12, 0.25, 0.38, 0.55, 0.72, 0.9]],
|
||||
["awp", 12, "awp", [0.3, 0.6, 0.85]],
|
||||
["rocket", 13, "rocket_launcher", [0.25, 0.55, 0.8]],
|
||||
["shotgun", 14, "double_barrel_shotgun", [0.25, 0.6, 0.9]],
|
||||
]
|
||||
var _wi := 0
|
||||
var _shots: Array = []
|
||||
var _reload_T := 0.0
|
||||
var _reload_start_ms := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.size() > 0:
|
||||
_out_dir = args[0]
|
||||
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
_frames += 1
|
||||
if _mode == "boot":
|
||||
if _frames == 40:
|
||||
var nm = root.get_node_or_null("NetworkManager")
|
||||
if nm and nm.has_method("start_singleplayer_match"):
|
||||
nm.start_singleplayer_match("Deathmatch")
|
||||
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
|
||||
elif _frames >= 160:
|
||||
for p in root.find_children("*", "CharacterBody3D", true, false):
|
||||
if p.has_method("get_visual_model") and p.is_multiplayer_authority():
|
||||
_player = p
|
||||
break
|
||||
if not _player:
|
||||
printerr("fp_capture: no player")
|
||||
return true
|
||||
_wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if not _wman:
|
||||
for c in _player.find_children("*", "Node", true, false):
|
||||
if c.name == "WeaponManager":
|
||||
_wman = c
|
||||
break
|
||||
if not _wman:
|
||||
printerr("fp_capture: no WeaponManager")
|
||||
return true
|
||||
_player.set_physics_process(false)
|
||||
_player.global_position = Vector3(0, 1.2, 14)
|
||||
_mode = "spawn"
|
||||
return false
|
||||
|
||||
if _mode == "spawn":
|
||||
# Force-spawn the test weapons into known slots.
|
||||
for w in _weapons:
|
||||
_wman._spawn_weapon(w[1], w[2])
|
||||
_mode = "next_weapon"
|
||||
return false
|
||||
|
||||
if _mode == "next_weapon":
|
||||
if _wi >= _weapons.size():
|
||||
_mode = "knife_equip"
|
||||
_phase_frame = 0
|
||||
return false
|
||||
var entry: Array = _weapons[_wi]
|
||||
_wman._equip_slot(entry[1])
|
||||
_phase_frame = 0
|
||||
_mode = "reload_start"
|
||||
return false
|
||||
|
||||
if _mode == "reload_start":
|
||||
_phase_frame += 1
|
||||
if _phase_frame < 15:
|
||||
return false
|
||||
var entry: Array = _weapons[_wi]
|
||||
var w = _wman.weapons.get(entry[1])
|
||||
if w == null:
|
||||
_wi += 1
|
||||
_mode = "next_weapon"
|
||||
return false
|
||||
# Drain ammo state and kick off the reload.
|
||||
w._start_reload()
|
||||
_reload_T = w.reload_time
|
||||
_shots = entry[3].duplicate()
|
||||
_reload_start_ms = Time.get_ticks_msec()
|
||||
_mode = "reload_watch"
|
||||
return false
|
||||
|
||||
if _mode == "reload_watch":
|
||||
var entry: Array = _weapons[_wi]
|
||||
var elapsed := (Time.get_ticks_msec() - _reload_start_ms) / 1000.0
|
||||
if _shots.size() > 0:
|
||||
if elapsed >= float(_shots[0]) * _reload_T:
|
||||
_snap("fp_%s_reload_%d" % [entry[0], entry[3].size() - _shots.size() + 1])
|
||||
_shots.pop_front()
|
||||
elif elapsed >= _reload_T + 0.3:
|
||||
_wi += 1
|
||||
_mode = "next_weapon"
|
||||
return false
|
||||
|
||||
if _mode == "knife_equip":
|
||||
_wman._equip_slot(4)
|
||||
_phase_frame = 0
|
||||
_mode = "knife_swing"
|
||||
return false
|
||||
|
||||
if _mode == "knife_swing":
|
||||
_phase_frame += 1
|
||||
if _phase_frame == 20:
|
||||
var knife = _wman.weapons.get(4)
|
||||
if knife and knife.has_method("_swing"):
|
||||
knife._swing()
|
||||
elif _phase_frame == 26:
|
||||
_snap("fp_knife_windup")
|
||||
elif _phase_frame == 32:
|
||||
_snap("fp_knife_slash")
|
||||
elif _phase_frame >= 60:
|
||||
_mode = "kick"
|
||||
_phase_frame = 0
|
||||
return false
|
||||
|
||||
if _mode == "kick":
|
||||
_phase_frame += 1
|
||||
if _phase_frame == 5:
|
||||
_wman._equip_slot(11)
|
||||
elif _phase_frame == 30:
|
||||
var w = _wman.weapons.get(11)
|
||||
if w:
|
||||
w._fire()
|
||||
elif _phase_frame == 33:
|
||||
_snap("fp_m4_kick")
|
||||
return true
|
||||
return false
|
||||
return false
|
||||
|
||||
|
||||
func _snap(tag: String) -> void:
|
||||
var img := root.get_viewport().get_texture().get_image()
|
||||
var path := _out_dir + "/" + tag + ".png"
|
||||
img.save_png(path)
|
||||
print("fp_capture: saved ", path)
|
||||
Reference in New Issue
Block a user