abilities first draft

This commit is contained in:
2026-06-10 19:39:35 +03:00
parent dda9862589
commit c1852e8736
5 changed files with 256 additions and 50 deletions
+9
View File
@@ -114,6 +114,15 @@ func _register_all() -> void:
_reg_f("Matador", "mat_attack_min_dist", 2.5, 0.5, 6.0, 0.1) _reg_f("Matador", "mat_attack_min_dist", 2.5, 0.5, 6.0, 0.1)
_reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05) _reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05)
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05) _reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
# ── Abilities ─────────────────────────────────────────────────────────────
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1)
_reg_f("Abilities", "kick_range", 2.5, 0.5, 8.0, 0.1)
_reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0)
_reg_f("Abilities", "slam_cooldown", 4.0, 0.5, 15.0, 0.1)
_reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1)
_reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0)
_reg_f("Abilities", "dash_cooldown", 2.0, 0.5, 10.0, 0.1)
_reg_f("Abilities", "dash_speed", 22.0, 5.0, 60.0)
# ── Debug ───────────────────────────────────────────────────────────────── # ── Debug ─────────────────────────────────────────────────────────────────
_reg_b("Debug", "show_collisions", false) _reg_b("Debug", "show_collisions", false)
+100 -3
View File
@@ -7,19 +7,42 @@ const _BG := Color(0.06, 0.04, 0.02, 0.78)
const _BADGE := Color(0.16, 0.10, 0.03, 0.90) const _BADGE := Color(0.16, 0.10, 0.03, 0.90)
const _BORDER := Color(0.65, 0.48, 0.15, 0.55) const _BORDER := Color(0.65, 0.48, 0.15, 0.55)
const _ABILITY_ICONS: Array[String] = [
"res://HUD/HUD_0000_ability_JALG.png",
"res://HUD/HUD_0001_ability_SLAM.png",
"res://HUD/HUD_0002_ability_DASH.png",
]
const _ABILITY_KEYS: Array[String] = ["F", "G", "H"]
var _controls_visible: bool = true var _controls_visible: bool = true
var _controls_panel: PanelContainer var _controls_panel: PanelContainer
var _toggle_btn: Button var _toggle_btn: Button
var _spawner: Node var _spawner: Node
var _player: Node = null
var _cd_overlays: Array[ColorRect] = []
func _ready() -> void: func _ready() -> void:
layer = 11 layer = 11
process_mode = Node.PROCESS_MODE_ALWAYS process_mode = Node.PROCESS_MODE_ALWAYS
_build_controls_panel() _build_controls_panel()
_build_ability_bar()
_find_spawner.call_deferred() _find_spawner.call_deferred()
func _process(_delta: float) -> void:
if _player == null:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
_player = players[0]
return
for i: int in _cd_overlays.size():
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
var ov := _cd_overlays[i]
ov.anchor_top = 1.0 - fraction
ov.anchor_bottom = 1.0
func _find_spawner() -> void: func _find_spawner() -> void:
var spawners := get_tree().get_nodes_in_group(&"matador_spawn") var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
if not spawners.is_empty(): if not spawners.is_empty():
@@ -28,7 +51,7 @@ func _find_spawner() -> void:
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo: if event is InputEventKey and event.pressed and not event.echo:
if event.physical_keycode == KEY_H: if event.physical_keycode == KEY_TAB:
_toggle_controls() _toggle_controls()
@@ -45,6 +68,78 @@ func _on_reset_pressed() -> void:
_spawner.reset_spawn() _spawner.reset_spawn()
func _build_ability_bar() -> void:
var bar := HBoxContainer.new()
bar.add_theme_constant_override("separation", 10)
bar.anchor_left = 0.5
bar.anchor_right = 0.5
bar.anchor_top = 1.0
bar.anchor_bottom = 1.0
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
bar.offset_left = -120.0
bar.offset_bottom = -20.0
add_child(bar)
for i: int in 3:
bar.add_child(_build_ability_slot(i))
func _build_ability_slot(idx: int) -> Control:
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = _BG
bg_style.corner_radius_top_left = 6
bg_style.corner_radius_top_right = 6
bg_style.corner_radius_bottom_left = 6
bg_style.corner_radius_bottom_right = 6
bg_style.border_width_left = 1
bg_style.border_width_right = 1
bg_style.border_width_top = 1
bg_style.border_width_bottom = 1
bg_style.border_color = _BORDER
bg_style.content_margin_left = 4.0
bg_style.content_margin_right = 4.0
bg_style.content_margin_top = 4.0
bg_style.content_margin_bottom = 4.0
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(72.0, 72.0)
panel.add_theme_stylebox_override("panel", bg_style)
vbox.add_child(panel)
var stack := Control.new()
stack.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_child(stack)
if ResourceLoader.exists(_ABILITY_ICONS[idx]):
var icon := TextureRect.new()
icon.texture = load(_ABILITY_ICONS[idx])
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.set_anchors_preset(Control.PRESET_FULL_RECT)
stack.add_child(icon)
var cd_overlay := ColorRect.new()
cd_overlay.color = Color(0.0, 0.0, 0.0, 0.70)
cd_overlay.anchor_left = 0.0
cd_overlay.anchor_right = 1.0
cd_overlay.anchor_top = 1.0
cd_overlay.anchor_bottom = 1.0
cd_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
stack.add_child(cd_overlay)
_cd_overlays.append(cd_overlay)
var key_lbl := Label.new()
key_lbl.text = _ABILITY_KEYS[idx]
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
key_lbl.add_theme_color_override("font_color", _GOLD)
key_lbl.add_theme_font_size_override("font_size", 12)
vbox.add_child(key_lbl)
return vbox
func _build_controls_panel() -> void: func _build_controls_panel() -> void:
_controls_panel = PanelContainer.new() _controls_panel = PanelContainer.new()
@@ -81,10 +176,12 @@ func _build_controls_panel() -> void:
_row(vbox, "WASD / Arrows", "Move (not intended)", _MUTED) _row(vbox, "WASD / Arrows", "Move (not intended)", _MUTED)
_row(vbox, "LMB (hold)", "Right horn", _CREAM) _row(vbox, "LMB (hold)", "Right horn", _CREAM)
_row(vbox, "RMB (hold)", "Left horn", _CREAM) _row(vbox, "RMB (hold)", "Left horn", _CREAM)
_row(vbox, "LMB + RMB", "Bull charge", _CREAM) _row(vbox, "F", "Kick", _CREAM)
_row(vbox, "G", "Slam", _CREAM)
_row(vbox, "H", "Dash", _CREAM)
_row(vbox, "Scroll", "Zoom", _CREAM) _row(vbox, "Scroll", "Zoom", _CREAM)
_row(vbox, "R", "Respawn matadors (cooldown)", _MUTED) _row(vbox, "R", "Respawn matadors (cooldown)", _MUTED)
_row(vbox, "H", "Toggle controls", _MUTED) _row(vbox, "Tab", "Toggle controls", _MUTED)
var sep := HSeparator.new() var sep := HSeparator.new()
var sep_style := StyleBoxFlat.new() var sep_style := StyleBoxFlat.new()
+10
View File
@@ -322,6 +322,16 @@ func _tick_ragdoll(_delta: float) -> void:
move_and_slide() move_and_slide()
func apply_ability_hit(hit_dir: Vector3, strength: float) -> void:
if _state == State.RAGDOLL or _state == State.ROLL:
return
if _bull == null:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
_bull = players[0] as CharacterBody3D
_enter_ragdoll(hit_dir, strength)
func _on_body_entered(body: Node3D) -> void: func _on_body_entered(body: Node3D) -> void:
if _state == State.RAGDOLL or _state == State.ROLL: if _state == State.RAGDOLL or _state == State.ROLL:
return return
+122 -47
View File
@@ -17,13 +17,18 @@ var _legs: Node
var _kick_timer: float = 0.0 var _kick_timer: float = 0.0
var _was_on_floor: bool = false var _was_on_floor: bool = false
var _pre_slide_vel_y: float = 0.0 var _pre_slide_vel_y: float = 0.0
var _charge_dir: Vector3 = Vector3.ZERO
var _charge_hold_time: float = 0.0
var _both_held: bool = false
var _thrust_left_charge: float = 0.0 var _thrust_left_charge: float = 0.0
var _thrust_right_charge: float = 0.0 var _thrust_right_charge: float = 0.0
var _charge_was_full: bool = false var _charge_was_full: bool = false
# Ability system — kick=0, slam=1, dash=2
var ability_cd: Array[float] = [0.0, 0.0, 0.0]
var _ability_active: bool = false
var _ability_timer: float = 0.0
var _active_ability: int = -1
var _dash_dir: Vector3 = Vector3.ZERO
var _bull_anim: AnimationPlayer = null
var _charge_overlay: ColorRect = null var _charge_overlay: ColorRect = null
var _charge_ready_emitter: CPUParticles3D = null var _charge_ready_emitter: CPUParticles3D = null
@@ -42,6 +47,7 @@ func _ready() -> void:
_setup_legs() _setup_legs()
_setup_charge_indicators() _setup_charge_indicators()
_setup_audio() _setup_audio()
_bull_anim = _find_anim_player(cube_guy)
DP.any_changed.connect(_on_dp_changed) DP.any_changed.connect(_on_dp_changed)
_roll_damping = DP.f("roll_damping") _roll_damping = DP.f("roll_damping")
@@ -117,7 +123,7 @@ func _setup_charge_indicators() -> void:
var ui := CanvasLayer.new() var ui := CanvasLayer.new()
add_child(ui) add_child(ui)
_charge_overlay = ColorRect.new() _charge_overlay = ColorRect.new()
_charge_overlay.color = Color(0.7, 0.05, 0.0, 0.0) _charge_overlay.color = Color(0.9, 0.55, 0.0, 0.0)
_charge_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE _charge_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
_charge_overlay.set_anchors_preset(Control.PRESET_FULL_RECT) _charge_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
ui.add_child(_charge_overlay) ui.add_child(_charge_overlay)
@@ -204,6 +210,98 @@ func _on_dp_changed(_key: String, _val: Variant) -> void:
_set_dust_state(prev) _set_dust_state(prev)
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
func _unhandled_input(event: InputEvent) -> void:
if _ability_active:
return
if event.is_action_pressed(&"ability_kick") and ability_cd[0] <= 0.0:
_activate_kick()
elif event.is_action_pressed(&"ability_slam") and ability_cd[1] <= 0.0:
_activate_slam()
elif event.is_action_pressed(&"ability_dash") and ability_cd[2] <= 0.0:
_activate_dash()
func ability_cooldown_fraction(slot: int) -> float:
var maxcd := 0.0
match slot:
0: maxcd = DP.f("kick_cooldown")
1: maxcd = DP.f("slam_cooldown")
2: maxcd = DP.f("dash_cooldown")
return ability_cd[slot] / maxcd if maxcd > 0.0 else 0.0
func _activate_kick() -> void:
ability_cd[0] = DP.f("kick_cooldown")
_ability_active = true
_active_ability = 0
_ability_timer = 0.6
if _bull_anim and _bull_anim.has_animation(&"Armature|FOOTKICK"):
_bull_anim.play(&"Armature|FOOTKICK")
_ability_timer = _bull_anim.get_animation(&"Armature|FOOTKICK").length
_hit_matadors_cone(DP.f("kick_range"), deg_to_rad(60.0), DP.f("kick_strength"))
func _activate_slam() -> void:
ability_cd[1] = DP.f("slam_cooldown")
_ability_active = true
_active_ability = 1
_ability_timer = 0.9
if _bull_anim and _bull_anim.has_animation(&"Armature|SLAM"):
_bull_anim.play(&"Armature|SLAM")
_ability_timer = _bull_anim.get_animation(&"Armature|SLAM").length
_hit_matadors_radius(DP.f("slam_range"), DP.f("slam_strength"))
func _activate_dash() -> void:
ability_cd[2] = DP.f("dash_cooldown")
_ability_active = true
_active_ability = 2
_ability_timer = 0.45
if _bull_anim and _bull_anim.has_animation(&"Armature|DASH"):
_bull_anim.play(&"Armature|DASH")
_ability_timer = _bull_anim.get_animation(&"Armature|DASH").length
var raw := cube_guy.global_transform.basis.z
_dash_dir = Vector3(raw.x, 0.0, raw.z).normalized()
velocity.x = _dash_dir.x * DP.f("dash_speed")
velocity.z = _dash_dir.z * DP.f("dash_speed")
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
if _huff_player:
_huff_player.pitch_scale = randf_range(0.9, 1.1)
_huff_player.play()
func _hit_matadors_cone(range_m: float, half_angle_rad: float, strength: float) -> void:
var raw := cube_guy.global_transform.basis.z
var forward := Vector3(raw.x, 0.0, raw.z).normalized()
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
to_mat.y = 0.0
if to_mat.length() > range_m:
continue
if to_mat.length() > 0.01 and forward.dot(to_mat.normalized()) < cos(half_angle_rad):
continue
mat.call(&"apply_ability_hit", (forward + Vector3.UP * 0.4).normalized(), strength)
func _hit_matadors_radius(range_m: float, strength: float) -> void:
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
to_mat.y = 0.0
if to_mat.length() > range_m:
continue
var away := to_mat.normalized() if to_mat.length() > 0.01 \
else Vector3(randf() - 0.5, 0.0, randf() - 0.5).normalized()
mat.call(&"apply_ability_hit", (away + Vector3.UP * 0.6).normalized(), strength)
func _apply_thrust(dir: Vector3) -> void: func _apply_thrust(dir: Vector3) -> void:
@@ -250,46 +348,27 @@ func _physics_process(delta: float) -> void:
var on_floor := is_on_floor() var on_floor := is_on_floor()
# ── Mouse button actions ────────────────────────────────────────────────── # ── Ability cooldowns ────────────────────────────────────────────────────────
# Both held → charge: lock direction, ramp speed, lower head for i: int in 3:
# Left only → thrust NE (forward-right diagonal) ability_cd[i] = maxf(0.0, ability_cd[i] - delta)
# Right only → thrust NW (forward-left diagonal) if _ability_active:
_ability_timer -= delta
if _ability_timer <= 0.0:
_ability_active = false
_active_ability = -1
_legs.set(&"charge_pitch_target", 0.0)
# ── Mouse button single-thrust actions ───────────────────────────────────────
# Left only → thrust NE (forward-right diagonal)
# Right only → thrust NW (forward-left diagonal)
var left_held := Input.is_action_pressed(&"thrust_left") var left_held := Input.is_action_pressed(&"thrust_left")
var right_held := Input.is_action_pressed(&"thrust_right") var right_held := Input.is_action_pressed(&"thrust_right")
var both_held := left_held and right_held
if both_held and not _both_held: if left_held and right_held:
var raw := cube_guy.global_transform.basis.z
_charge_dir = Vector3(raw.x, 0.0, raw.z).normalized()
if _huff_player:
_huff_player.pitch_scale = randf_range(0.9, 1.1)
_huff_player.play()
_huff_timer = randf_range(1.4, 2.2)
if both_held:
_thrust_left_charge = 0.0 _thrust_left_charge = 0.0
_thrust_right_charge = 0.0 _thrust_right_charge = 0.0
_charge_hold_time += delta
_huff_timer -= delta
if _huff_timer <= 0.0 and _huff_player and not _huff_player.playing:
_huff_player.pitch_scale = randf_range(0.9, 1.1)
_huff_player.play()
_huff_timer = randf_range(1.4, 2.2)
var ramp := minf(_charge_hold_time / DP.f("charge_ramp_time"), 1.0)
var accel := DP.f("bull_charge_accel") * lerpf(0.2, 1.0, ramp)
velocity.x += _charge_dir.x * accel * delta
velocity.z += _charge_dir.z * accel * delta
var flat_c := Vector2(velocity.x, velocity.z)
var spd_c := flat_c.length()
if spd_c > DP.f("bull_charge_max_speed"):
velocity.x *= DP.f("bull_charge_max_speed") / spd_c
velocity.z *= DP.f("bull_charge_max_speed") / spd_c
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
else: else:
_charge_hold_time = 0.0 if left_held:
_legs.set(&"charge_pitch_target", 0.0)
if left_held and not right_held:
_thrust_left_charge = minf(_thrust_left_charge + delta, DP.f("thrust_charge_time")) _thrust_left_charge = minf(_thrust_left_charge + delta, DP.f("thrust_charge_time"))
elif Input.is_action_just_released(&"thrust_left") and _thrust_left_charge > 0.0: elif Input.is_action_just_released(&"thrust_left") and _thrust_left_charge > 0.0:
var t := _thrust_left_charge / DP.f("thrust_charge_time") var t := _thrust_left_charge / DP.f("thrust_charge_time")
@@ -298,7 +377,7 @@ func _physics_process(delta: float) -> void:
_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, -angle)) _apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, -angle))
_thrust_left_charge = 0.0 _thrust_left_charge = 0.0
if right_held and not left_held: if right_held:
_thrust_right_charge = minf(_thrust_right_charge + delta, DP.f("thrust_charge_time")) _thrust_right_charge = minf(_thrust_right_charge + delta, DP.f("thrust_charge_time"))
elif Input.is_action_just_released(&"thrust_right") and _thrust_right_charge > 0.0: elif Input.is_action_just_released(&"thrust_right") and _thrust_right_charge > 0.0:
var t := _thrust_right_charge / DP.f("thrust_charge_time") var t := _thrust_right_charge / DP.f("thrust_charge_time")
@@ -314,12 +393,10 @@ func _physics_process(delta: float) -> void:
_charge_ready_emitter.restart() _charge_ready_emitter.restart()
_charge_was_full = either_full _charge_was_full = either_full
# Fade red overlay in while charge-ahead, out when released # Briefly flash overlay when dash is active
var target_alpha := 0.30 if both_held else 0.0 var target_alpha := 0.20 if (_active_ability == 2 and _ability_active) else 0.0
var oc := _charge_overlay.color var oc := _charge_overlay.color
_charge_overlay.color = Color(oc.r, oc.g, oc.b, lerpf(oc.a, target_alpha, delta * 10.0)) _charge_overlay.color = Color(oc.r, oc.g, oc.b, lerpf(oc.a, target_alpha, delta * 12.0))
_both_held = both_held
# Reset kick timer on landing so the first kick after a bounce is immediate # Reset kick timer on landing so the first kick after a bounce is immediate
if on_floor and not _was_on_floor and direction: if on_floor and not _was_on_floor and direction:
@@ -328,7 +405,7 @@ func _physics_process(delta: float) -> void:
# ── Periodic kick ───────────────────────────────────────────────────────── # ── Periodic kick ─────────────────────────────────────────────────────────
_kick_timer = maxf(0.0, _kick_timer - delta) _kick_timer = maxf(0.0, _kick_timer - delta)
if direction and _kick_timer <= 0.0 and not both_held: if direction and _kick_timer <= 0.0:
_apply_kick(direction) _apply_kick(direction)
_kick_timer = DP.f("kick_interval") _kick_timer = DP.f("kick_interval")
@@ -362,8 +439,6 @@ func _physics_process(delta: float) -> void:
velocity.x = flat_n.x * exit_spd velocity.x = flat_n.x * exit_spd
velocity.z = flat_n.z * exit_spd velocity.z = flat_n.z * exit_spd
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z) pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
if both_held and _charge_dir.dot(flat_n) < 0.0:
_charge_dir = _charge_dir.reflect(flat_n)
# ── Bounce on landing ───────────────────────────────────────────────────── # ── Bounce on landing ─────────────────────────────────────────────────────
if is_on_floor() and not _was_on_floor: if is_on_floor() and not _was_on_floor:
+15
View File
@@ -98,6 +98,21 @@ thrust_right={
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null)
] ]
} }
ability_kick={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
]
}
ability_slam={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
]
}
ability_dash={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"location":0,"echo":false,"script":null)
]
}
[physics] [physics]