scanlines, sounds, bumper walls, score keep, control legend, timer
This commit is contained in:
+14
-8
@@ -134,13 +134,14 @@ func _update_tail(delta: float) -> void:
|
||||
|
||||
var flat_speed: float = Vector2(_player.velocity.x, _player.velocity.z).length()
|
||||
var wag_strength: float = lerpf(DP.f("tail_wag_idle"), 0.0, clampf(flat_speed / 3.0, 0.0, 1.0))
|
||||
var wag_freq: float = DP.f("tail_wag_freq")
|
||||
var time: float = Time.get_ticks_msec() / 1000.0
|
||||
var basis_x: Vector3 = _bull.global_transform.basis.x
|
||||
|
||||
for i in range(1, _tail_world.size()):
|
||||
var vel: Vector3 = (_tail_world[i] - _tail_prev[i]) * damping
|
||||
_tail_prev[i] = _tail_world[i]
|
||||
var wag := _bull.global_transform.basis.x \
|
||||
* sin(time * DP.f("tail_wag_freq") + i * 0.5) * wag_strength
|
||||
var wag := basis_x * sin(time * wag_freq + i * 0.5) * wag_strength
|
||||
_tail_world[i] = _tail_world[i] + vel + gravity * (delta * delta) + wag * delta
|
||||
|
||||
for i in range(1, _tail_world.size()):
|
||||
@@ -167,16 +168,21 @@ func _apply_tail() -> void:
|
||||
if _tail_world.is_empty():
|
||||
return
|
||||
|
||||
for i in range(_tail_world.size()):
|
||||
var n := _tail_world.size()
|
||||
var local_pos: Array[Vector3] = []
|
||||
local_pos.resize(n)
|
||||
for i in n:
|
||||
local_pos[i] = _skeleton.to_local(_tail_world[i])
|
||||
|
||||
for i in n:
|
||||
var idx: int = _tail_idx[i]
|
||||
var pos_skel: Vector3 = _skeleton.to_local(_tail_world[i])
|
||||
var pos_skel: Vector3 = local_pos[i]
|
||||
|
||||
var point_dir_skel: Vector3
|
||||
if i < _tail_world.size() - 1:
|
||||
point_dir_skel = (_skeleton.to_local(_tail_world[i + 1]) - pos_skel).normalized()
|
||||
if i < n - 1:
|
||||
point_dir_skel = (local_pos[i + 1] - pos_skel).normalized()
|
||||
else:
|
||||
var prev_skel: Vector3 = _skeleton.to_local(_tail_world[i - 1])
|
||||
point_dir_skel = (pos_skel - prev_skel).normalized()
|
||||
point_dir_skel = (pos_skel - local_pos[i - 1]).normalized()
|
||||
|
||||
var rest_dir: Vector3 = _tail_rest_dir[i]
|
||||
var rest_t: Transform3D = _skeleton.get_bone_global_rest(idx)
|
||||
|
||||
@@ -4,6 +4,7 @@ extends Camera3D
|
||||
@export var horizontal_distance: float = 18.0
|
||||
@export var horizontal_angle_deg: float = 45.0
|
||||
@export var ortho_size: float = 15.0
|
||||
@export var ortho_size_max: float = 40.0
|
||||
|
||||
const _FLASH_DURATION: float = 0.30
|
||||
|
||||
@@ -11,11 +12,13 @@ var _shake_amount: float = 0.0
|
||||
var _shake_decay: float = 0.0
|
||||
var _flash_timer: float = 0.0
|
||||
var _flash_rect: ColorRect = null
|
||||
var _target_size: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_as_top_level(true)
|
||||
projection = PROJECTION_ORTHOGONAL
|
||||
_target_size = ortho_size
|
||||
size = ortho_size
|
||||
_setup_flash()
|
||||
|
||||
@@ -31,7 +34,15 @@ func _setup_flash() -> void:
|
||||
canvas.add_child(_flash_rect)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("zoom_in"):
|
||||
_target_size = clamp(_target_size / 1.2, ortho_size, ortho_size_max)
|
||||
if event.is_action_pressed("zoom_out"):
|
||||
_target_size = clamp(_target_size * 1.2, ortho_size, ortho_size_max)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
size = lerpf(size, _target_size, delta * 10.0)
|
||||
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
||||
var offset := Vector3(
|
||||
sin(angle_rad) * horizontal_distance,
|
||||
|
||||
@@ -24,9 +24,9 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
rotation.x = clamp(rotation.x, min_vert, max_vert)
|
||||
|
||||
if event.is_action_pressed("zoom_in"):
|
||||
_desired_length = clamp(_desired_length - 1.0, min_zoom, max_zoom)
|
||||
_desired_length = clamp(_desired_length / 1.2, min_zoom, max_zoom)
|
||||
if event.is_action_pressed("zoom_out"):
|
||||
_desired_length = clamp(_desired_length + 1.0, min_zoom, max_zoom)
|
||||
_desired_length = clamp(_desired_length * 1.2, min_zoom, max_zoom)
|
||||
|
||||
if event.is_action_pressed("toggle_mouse_capture"):
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
|
||||
+16
-12
@@ -22,12 +22,14 @@ func _register_all() -> void:
|
||||
_reg_f("Movement", "thrust_min_angle", 15.0, 0.0, 90.0, 1.0)
|
||||
_reg_f("Movement", "thrust_max_angle", 90.0, 0.0, 180.0, 1.0)
|
||||
_reg_f("Movement", "thrust_charge_time", 0.5, 0.05, 2.0, 0.05)
|
||||
_reg_f("Movement", "max_speed", 30.0, 2.0, 1000.0)
|
||||
_reg_f("Movement", "max_speed", 50.0, 2.0, 1000.0)
|
||||
_reg_f("Movement", "roll_damping", 0.15, 0.01, 0.99, 0.01)
|
||||
_reg_f("Movement", "jump_velocity", 4.5, 1.0, 200.0)
|
||||
_reg_f("Movement", "visual_turn_speed", 12.0, 1.0, 200.0)
|
||||
_reg_f("Movement", "bounce_restitution", 0.55, 0.0, 2.0, 0.05)
|
||||
_reg_f("Movement", "bounce_min_impact", 1.0, 0.0, 20.0)
|
||||
_reg_f("Movement", "wall_bounce_restitution", 0.6, 0.0, 1.5, 0.05)
|
||||
_reg_f("Movement", "wall_min_bounce_speed", 6.0, 0.0, 30.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", 0.0, 0.0, 15.0)
|
||||
@@ -57,8 +59,9 @@ func _register_all() -> void:
|
||||
_reg_f("Tail", "tail_wag_freq", 4.0, 0.5, 15.0)
|
||||
_reg_f("Tail", "tail_wag_idle", 0.3, 0.0, 2.0, 0.05)
|
||||
# ── Charge (both mouse buttons) ──────────────────────────────────────────
|
||||
_reg_f("Charge", "bull_charge_accel", 20.0, 1.0, 200.0)
|
||||
_reg_f("Charge", "bull_charge_max_speed", 22.0, 5.0, 100.0)
|
||||
_reg_f("Charge", "bull_charge_accel", 80.0, 1.0, 400.0)
|
||||
_reg_f("Charge", "bull_charge_max_speed", 76.0, 5.0, 200.0)
|
||||
_reg_f("Charge", "charge_ramp_time", 0.75, 0.1, 5.0, 0.05)
|
||||
_reg_f("Charge", "charge_head_pitch", 0.3, 0.0, 1.2, 0.01)
|
||||
_reg_f("Charge", "charge_head_speed", 5.0, 0.5, 20.0)
|
||||
# ── Body ──────────────────────────────────────────────────────────────────
|
||||
@@ -83,7 +86,7 @@ func _register_all() -> void:
|
||||
_reg_f("Camera", "cam_lerp_power", 6.0, 0.5, 30.0)
|
||||
_reg_f("Camera", "cam_sensitivity", 0.005, 0.001, 0.05, 0.001)
|
||||
_reg_f("Camera", "cam_min_zoom", 3.5, 0.5, 20.0)
|
||||
_reg_f("Camera", "cam_max_zoom", 12.0, 2.0, 50.0)
|
||||
_reg_f("Camera", "cam_max_zoom", 40.0, 2.0, 80.0)
|
||||
_reg_f("Camera", "cam_spring_length", 7.0, 1.0, 30.0)
|
||||
_reg_f("Camera", "cam_min_vert_angle", -60.0, -90.0, 0.0, 1.0)
|
||||
_reg_f("Camera", "cam_max_vert_angle", 45.0, 0.0, 90.0, 1.0)
|
||||
@@ -94,16 +97,17 @@ 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", 4.0, 0.5, 20.0)
|
||||
_reg_f("Matador", "mat_flee_range", 10.0, 2.0, 40.0)
|
||||
_reg_f("Matador", "mat_flee_speed", 4.0, 0.5, 12.0)
|
||||
_reg_f("Matador", "mat_charge_speed", 6.0, 1.0, 30.0)
|
||||
_reg_f("Matador", "mat_brace_range", 8.0, 2.0, 20.0)
|
||||
_reg_f("Matador", "mat_brace_duration", 0.4, 0.05, 1.5, 0.05)
|
||||
_reg_f("Matador", "mat_ragdoll_impulse", 6.0, 0.5, 20.0)
|
||||
_reg_f("Matador", "mat_flee_range", 3.5, 2.0, 40.0)
|
||||
_reg_f("Matador", "mat_flee_speed", 2.5, 0.5, 12.0)
|
||||
_reg_f("Matador", "mat_charge_speed", 10.0, 1.0, 30.0)
|
||||
_reg_f("Matador", "mat_brace_range", 5.0, 2.0, 20.0)
|
||||
_reg_f("Matador", "mat_brace_duration", 0.7, 0.05, 1.5, 0.05)
|
||||
_reg_f("Matador", "mat_commit_dist", 2.5, 0.5, 6.0, 0.1)
|
||||
_reg_f("Matador", "mat_step_speed", 9.0, 2.0, 20.0)
|
||||
_reg_f("Matador", "mat_step_speed", 3.5, 2.0, 20.0)
|
||||
_reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05)
|
||||
_reg_f("Matador", "mat_dodge_cooldown", 3.0, 0.5, 8.0, 0.25)
|
||||
_reg_f("Matador", "mat_dodge_cooldown", 5.0, 0.5, 12.0, 0.25)
|
||||
_reg_f("Matador", "mat_dodge_chance", 0.25, 0.0, 1.0, 0.05)
|
||||
# ── Debug ─────────────────────────────────────────────────────────────────
|
||||
_reg_b("Debug", "show_collisions", false)
|
||||
|
||||
|
||||
+92
-70
@@ -7,6 +7,8 @@ extends CharacterBody3D
|
||||
# RAGDOLL → hit by bull at speed
|
||||
enum State { WANDER, FLEE, BRACE, SIDESTEP, RAGDOLL }
|
||||
|
||||
signal killed
|
||||
|
||||
var _state: State = State.WANDER
|
||||
var _skeleton: Skeleton3D = null
|
||||
var _sim: PhysicalBoneSimulator3D = null
|
||||
@@ -18,6 +20,10 @@ var _step_dir: Vector3 = Vector3.ZERO
|
||||
var _brace_timer: float = 0.0
|
||||
var _step_timer: float = 0.0
|
||||
var _dodge_cd: float = 0.0
|
||||
var _will_dodge: bool = true
|
||||
var _ole_player: AudioStreamPlayer = null
|
||||
var _death_player: AudioStreamPlayer = null
|
||||
var _blood_burst: CPUParticles3D = null
|
||||
|
||||
@onready var _mesh: Node3D = $matador_v02
|
||||
@onready var _hit_area: Area3D = $HitArea
|
||||
@@ -42,6 +48,17 @@ func _ready() -> void:
|
||||
str(_anim_player.get_animation_list()))
|
||||
_hit_area.body_entered.connect(_on_body_entered)
|
||||
_pick_wander_target()
|
||||
_setup_blood_burst()
|
||||
if ResourceLoader.exists("res://sounds/ole.ogg"):
|
||||
_ole_player = AudioStreamPlayer.new()
|
||||
_ole_player.stream = load("res://sounds/ole.ogg")
|
||||
_ole_player.volume_db = 0.0
|
||||
add_child(_ole_player)
|
||||
if ResourceLoader.exists("res://sounds/matador_death.ogg"):
|
||||
_death_player = AudioStreamPlayer.new()
|
||||
_death_player.stream = load("res://sounds/matador_death.ogg")
|
||||
_death_player.volume_db = 2.0
|
||||
add_child(_death_player)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
@@ -84,7 +101,7 @@ func _update_ai_state() -> void:
|
||||
pass # step timer drives transition
|
||||
|
||||
|
||||
# Bull is moving fast and aimed within ~50° of the matador
|
||||
# Bull is moving fast and aimed within ~30° of the matador
|
||||
func _is_charge_incoming() -> bool:
|
||||
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
if bull_flat.length() < DP.f("mat_charge_speed"):
|
||||
@@ -93,7 +110,7 @@ func _is_charge_incoming() -> bool:
|
||||
to_me.y = 0.0
|
||||
if to_me.length() > DP.f("mat_brace_range"):
|
||||
return false
|
||||
return bull_flat.normalized().dot(to_me.normalized()) > 0.65
|
||||
return bull_flat.normalized().dot(to_me.normalized()) > 0.85
|
||||
|
||||
|
||||
func _tick_wander(delta: float) -> void:
|
||||
@@ -147,7 +164,8 @@ func _start_brace() -> void:
|
||||
charge_dir = (_bull.global_position - global_position)
|
||||
charge_dir.y = 0.0
|
||||
charge_dir = charge_dir.normalized()
|
||||
# Decide which side to step to now, so the choice is fixed before the bull arrives
|
||||
# Decide whether to dodge at all and which side — fixed before the bull arrives
|
||||
_will_dodge = randf() < DP.f("mat_dodge_chance")
|
||||
var side := 1.0 if randf() > 0.5 else -1.0
|
||||
_step_dir = charge_dir.rotated(Vector3.UP, PI * 0.5 * side)
|
||||
_brace_timer = DP.f("mat_brace_duration")
|
||||
@@ -170,8 +188,16 @@ func _tick_brace(delta: float) -> void:
|
||||
var dist := global_position.distance_to(_bull.global_position)
|
||||
# Execute the step when the bull is almost on top or the brace window expires
|
||||
if dist < DP.f("mat_commit_dist") or _brace_timer <= 0.0:
|
||||
if _will_dodge:
|
||||
_state = State.SIDESTEP
|
||||
_step_timer = DP.f("mat_step_duration")
|
||||
if _ole_player:
|
||||
_ole_player.pitch_scale = randf_range(0.88, 1.12)
|
||||
_ole_player.play()
|
||||
else:
|
||||
# Stand and eat it — no juke
|
||||
_state = State.FLEE
|
||||
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||
|
||||
|
||||
# Pase phase: sharp lateral step as the bull commits to its line
|
||||
@@ -196,36 +222,6 @@ func _tick_ragdoll(_delta: float) -> void:
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not (event is InputEventKey):
|
||||
return
|
||||
var key := event as InputEventKey
|
||||
if key.keycode == KEY_R and key.pressed and not key.echo:
|
||||
_reset()
|
||||
|
||||
|
||||
func _reset() -> void:
|
||||
_state = State.WANDER
|
||||
velocity = Vector3.ZERO
|
||||
collision_layer = 1
|
||||
global_position = Vector3(0.0, 1.0, 0.0)
|
||||
|
||||
if _sim:
|
||||
_sim.physical_bones_stop_simulation()
|
||||
_sim.active = false
|
||||
|
||||
if _anim_player and _anim_player.has_animation(_IDLE_ANIM):
|
||||
_anim_player.stop()
|
||||
_anim_player.play(_IDLE_ANIM)
|
||||
_anim_player.seek(0.0, true)
|
||||
|
||||
_dodge_cd = 0.0
|
||||
_brace_timer = 0.0
|
||||
_step_timer = 0.0
|
||||
_idle_timer = 0.0
|
||||
_pick_wander_target()
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if _state == State.RAGDOLL:
|
||||
return
|
||||
@@ -242,12 +238,21 @@ func _on_body_entered(body: Node3D) -> void:
|
||||
|
||||
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
_state = State.RAGDOLL
|
||||
killed.emit()
|
||||
var _t := get_tree().create_timer(4.0)
|
||||
_t.timeout.connect(func() -> void:
|
||||
if is_instance_valid(self):
|
||||
queue_free()
|
||||
)
|
||||
hit_dir.y = 0.0
|
||||
hit_dir = hit_dir.normalized()
|
||||
# Arc upward so the body lifts before falling — feels like a real goring throw
|
||||
var throw_dir := (hit_dir + Vector3(0.0, 0.5, 0.0)).normalized()
|
||||
|
||||
_spawn_blood_burst(hit_dir)
|
||||
if _death_player:
|
||||
_death_player.pitch_scale = randf_range(0.9, 1.1)
|
||||
_death_player.play()
|
||||
|
||||
var cam := _bull.get_node_or_null("Camera3D")
|
||||
if cam:
|
||||
@@ -279,23 +284,40 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
_sim.active = true
|
||||
_sim.physical_bones_start_simulation()
|
||||
var strength := DP.f("mat_ragdoll_impulse") * clampf(bull_speed / 15.0, 0.4, 1.3)
|
||||
var right := throw_dir.cross(Vector3.UP).normalized()
|
||||
for child: Node in _sim.get_children():
|
||||
if child is PhysicalBone3D:
|
||||
var scatter := Vector3(
|
||||
randf_range(-0.12, 0.12),
|
||||
randf_range(-0.05, 0.08),
|
||||
randf_range(-0.12, 0.12))
|
||||
(child as PhysicalBone3D).apply_central_impulse(
|
||||
(throw_dir + scatter).normalized() * strength)
|
||||
if not (child is PhysicalBone3D):
|
||||
continue
|
||||
var pb := child as PhysicalBone3D
|
||||
var impulse := Vector3.ZERO
|
||||
match pb.bone_name:
|
||||
"COG", "matador":
|
||||
# Core of mass takes the full hit
|
||||
impulse = throw_dir * strength
|
||||
"chest":
|
||||
impulse = (throw_dir + Vector3.UP * 0.3).normalized() * strength * 0.9
|
||||
"head":
|
||||
impulse = (throw_dir + Vector3.UP * 0.5).normalized() * strength * 0.7
|
||||
"leg_L":
|
||||
impulse = (throw_dir * 0.4 + right * 0.6 + Vector3.UP * 0.4).normalized() * strength * 0.6
|
||||
"leg_R":
|
||||
impulse = (throw_dir * 0.4 - right * 0.6 + Vector3.UP * 0.4).normalized() * strength * 0.6
|
||||
"arm_L":
|
||||
impulse = (throw_dir * 0.3 + right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
|
||||
"arm_R":
|
||||
impulse = (throw_dir * 0.3 - right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
|
||||
_:
|
||||
# Distal bones (forearms, shins, hands, feet) get no direct impulse;
|
||||
# they trail naturally from the joints above them
|
||||
continue
|
||||
# Small random noise per bone so no two look identical
|
||||
var noise := Vector3(randf_range(-0.15, 0.15), randf_range(-0.08, 0.15), randf_range(-0.15, 0.15))
|
||||
pb.apply_central_impulse(impulse + noise * strength * 0.2)
|
||||
|
||||
|
||||
# ── Blood burst ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _spawn_blood_burst(hit_dir: Vector3) -> void:
|
||||
var p := CPUParticles3D.new()
|
||||
get_tree().current_scene.add_child(p)
|
||||
p.global_position = global_position + Vector3(0.0, 0.9, 0.0)
|
||||
|
||||
func _setup_blood_burst() -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
@@ -318,25 +340,30 @@ func _spawn_blood_burst(hit_dir: Vector3) -> void:
|
||||
scale_curve.add_point(Vector2(0.6, 0.5))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
p.mesh = sphere
|
||||
p.color_ramp = ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.9
|
||||
p.amount = 60
|
||||
p.lifetime = 1.1
|
||||
p.randomness = 0.4
|
||||
p.local_coords = false
|
||||
p.direction = (hit_dir * 0.6 + Vector3.UP * 0.4).normalized()
|
||||
p.spread = 80.0
|
||||
p.gravity = Vector3(0.0, -7.0, 0.0)
|
||||
p.initial_velocity_min = 4.0
|
||||
p.initial_velocity_max = 11.0
|
||||
p.scale_amount_min = 0.5
|
||||
p.scale_amount_max = 1.6
|
||||
p.emitting = true
|
||||
_blood_burst = CPUParticles3D.new()
|
||||
_blood_burst.mesh = sphere
|
||||
_blood_burst.color_ramp = ramp
|
||||
_blood_burst.scale_amount_curve = scale_curve
|
||||
_blood_burst.one_shot = true
|
||||
_blood_burst.explosiveness = 0.9
|
||||
_blood_burst.amount = 25
|
||||
_blood_burst.lifetime = 1.1
|
||||
_blood_burst.randomness = 0.4
|
||||
_blood_burst.local_coords = false
|
||||
_blood_burst.spread = 80.0
|
||||
_blood_burst.gravity = Vector3(0.0, -7.0, 0.0)
|
||||
_blood_burst.initial_velocity_min = 4.0
|
||||
_blood_burst.initial_velocity_max = 11.0
|
||||
_blood_burst.scale_amount_min = 0.5
|
||||
_blood_burst.scale_amount_max = 1.6
|
||||
_blood_burst.emitting = false
|
||||
add_child(_blood_burst)
|
||||
|
||||
get_tree().create_timer(p.lifetime + 0.3).timeout.connect(p.queue_free)
|
||||
|
||||
func _spawn_blood_burst(hit_dir: Vector3) -> void:
|
||||
_blood_burst.global_position = global_position + Vector3(0.0, 0.9, 0.0)
|
||||
_blood_burst.direction = (hit_dir * 0.6 + Vector3.UP * 0.4).normalized()
|
||||
_blood_burst.restart()
|
||||
|
||||
|
||||
# ── Animation helpers ─────────────────────────────────────────────────────────
|
||||
@@ -356,11 +383,6 @@ func _play_anim(anim_name: StringName) -> void:
|
||||
_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
|
||||
@@ -409,8 +431,8 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||
pb.bone_name = bname
|
||||
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
||||
pb.mass = 0.4
|
||||
pb.linear_damp = 0.4
|
||||
pb.angular_damp = 0.8
|
||||
pb.linear_damp = 0.1
|
||||
pb.angular_damp = 0.2
|
||||
pb.collision_layer = 1
|
||||
pb.collision_mask = 1
|
||||
var cs := CollisionShape3D.new()
|
||||
|
||||
@@ -1,15 +1,49 @@
|
||||
extends Node3D
|
||||
|
||||
signal matador_killed
|
||||
|
||||
const MATADOR := preload("res://Matador.tscn")
|
||||
const RESPAWN_COOLDOWN := 8.0
|
||||
|
||||
var _respawn_cooldown: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group(&"matador_spawn")
|
||||
_spawn.call_deferred()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_respawn_cooldown = maxf(0.0, _respawn_cooldown - delta)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
if event.physical_keycode == KEY_R and _respawn_cooldown <= 0.0:
|
||||
_respawn_cooldown = RESPAWN_COOLDOWN
|
||||
reset_spawn()
|
||||
|
||||
|
||||
func _spawn() -> void:
|
||||
var count: int = int(DP.f("mat_spawn_count"))
|
||||
for i: int in count:
|
||||
_spawn_one()
|
||||
|
||||
|
||||
func _spawn_one() -> void:
|
||||
var m: Node3D = MATADOR.instantiate()
|
||||
m.position = Vector3(0.0, 1.0, 0.0)
|
||||
get_parent().add_child(m)
|
||||
m.killed.connect(_on_matador_killed)
|
||||
|
||||
|
||||
func _on_matador_killed() -> void:
|
||||
matador_killed.emit()
|
||||
_spawn_one.call_deferred()
|
||||
|
||||
|
||||
func reset_spawn() -> void:
|
||||
for m: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
m.queue_free()
|
||||
await get_tree().process_frame
|
||||
_spawn()
|
||||
|
||||
@@ -18,6 +18,7 @@ var _kick_timer: float = 0.0
|
||||
var _was_on_floor: bool = false
|
||||
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_right_charge: float = 0.0
|
||||
@@ -26,6 +27,11 @@ var _charge_was_full: bool = false
|
||||
var _charge_overlay: ColorRect = null
|
||||
var _charge_ready_emitter: CPUParticles3D = null
|
||||
|
||||
var _huff_player: AudioStreamPlayer = null
|
||||
var _crowd_player: AudioStreamPlayer = null
|
||||
var _huff_timer: float = 0.0
|
||||
var _roll_damping: float = 0.15
|
||||
|
||||
enum DustState { NONE, WALK, CHARGE }
|
||||
var _dust_state: DustState = DustState.NONE
|
||||
|
||||
@@ -35,7 +41,9 @@ func _ready() -> void:
|
||||
_setup_hoof_dust()
|
||||
_setup_legs()
|
||||
_setup_charge_indicators()
|
||||
_setup_audio()
|
||||
DP.any_changed.connect(_on_dp_changed)
|
||||
_roll_damping = DP.f("roll_damping")
|
||||
|
||||
|
||||
func _setup_legs() -> void:
|
||||
@@ -144,6 +152,23 @@ func _setup_charge_indicators() -> void:
|
||||
cube_guy.add_child(_charge_ready_emitter)
|
||||
|
||||
|
||||
func _setup_audio() -> void:
|
||||
if ResourceLoader.exists("res://sounds/bull_huff.ogg"):
|
||||
_huff_player = AudioStreamPlayer.new()
|
||||
_huff_player.stream = load("res://sounds/bull_huff.ogg")
|
||||
_huff_player.volume_db = 0.0
|
||||
add_child(_huff_player)
|
||||
if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"):
|
||||
_crowd_player = AudioStreamPlayer.new()
|
||||
var s := load("res://sounds/crowd_ambient.ogg") as AudioStreamOggVorbis
|
||||
if s:
|
||||
s.loop = true
|
||||
_crowd_player.stream = s
|
||||
_crowd_player.volume_db = -14.0
|
||||
_crowd_player.autoplay = true
|
||||
add_child(_crowd_player)
|
||||
|
||||
|
||||
func _set_dust_state(new_state: DustState) -> void:
|
||||
if new_state == _dust_state:
|
||||
return
|
||||
@@ -171,6 +196,7 @@ func _set_dust_state(new_state: DustState) -> void:
|
||||
|
||||
|
||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||
_roll_damping = DP.f("roll_damping")
|
||||
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
|
||||
return
|
||||
var prev := _dust_state
|
||||
@@ -194,41 +220,27 @@ func _apply_thrust(dir: Vector3) -> void:
|
||||
velocity.z *= s
|
||||
|
||||
|
||||
func _apply_kick(direction: Vector3, charging: bool) -> void:
|
||||
var base := DP.f("kick_impulse") * (DP.f("charge_kick_mult") if charging else 1.0)
|
||||
|
||||
# Random force variance — each kick punches a different amount
|
||||
var force := base * randf_range(1.0 - DP.f("kick_force_var"), 1.0 + DP.f("kick_force_var"))
|
||||
|
||||
# Random angle spread — slight mis-kick like a real ball
|
||||
func _apply_kick(direction: Vector3) -> void:
|
||||
var force := DP.f("kick_impulse") * randf_range(1.0 - DP.f("kick_force_var"), 1.0 + DP.f("kick_force_var"))
|
||||
var spread := deg_to_rad(randf_range(-DP.f("kick_spread"), DP.f("kick_spread")))
|
||||
var kick_dir := direction.rotated(Vector3.UP, spread)
|
||||
|
||||
velocity.x += kick_dir.x * force
|
||||
velocity.z += kick_dir.z * force
|
||||
|
||||
# Vertical arc — only when grounded so arcs don't chain infinitely in the air
|
||||
if is_on_floor():
|
||||
velocity.y += DP.f("kick_vertical") * randf_range(0.75, 1.25)
|
||||
|
||||
# Cap immediately after kick so charge doesn't overshoot by too much
|
||||
var flat := Vector2(velocity.x, velocity.z)
|
||||
var spd := flat.length()
|
||||
var cap := DP.f("max_speed") * (DP.f("charge_kick_mult") if charging else 1.0)
|
||||
if spd > cap:
|
||||
var s := cap / spd
|
||||
if spd > DP.f("max_speed"):
|
||||
var s := DP.f("max_speed") / spd
|
||||
velocity.x *= s
|
||||
velocity.z *= s
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# ── Gravity & jump ────────────────────────────────────────────────────────
|
||||
# ── Gravity ───────────────────────────────────────────────────────────────
|
||||
if not is_on_floor():
|
||||
velocity += get_gravity() * delta
|
||||
|
||||
if Input.is_action_just_pressed(&"jump") and is_on_floor():
|
||||
velocity.y = DP.f("jump_velocity")
|
||||
|
||||
# ── Camera-relative input direction ───────────────────────────────────────
|
||||
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||
var cam_basis := camera_pivot.global_transform.basis
|
||||
@@ -236,7 +248,6 @@ func _physics_process(delta: float) -> void:
|
||||
var flat_right := Vector3(cam_basis.x.x, 0.0, cam_basis.x.z).normalized()
|
||||
var direction := (flat_forward * -input_dir.y + flat_right * input_dir.x).normalized()
|
||||
|
||||
var charging := Input.is_action_pressed(&"charge")
|
||||
var on_floor := is_on_floor()
|
||||
|
||||
# ── Mouse button actions ──────────────────────────────────────────────────
|
||||
@@ -250,12 +261,24 @@ func _physics_process(delta: float) -> void:
|
||||
if both_held and not _both_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_right_charge = 0.0
|
||||
velocity.x += _charge_dir.x * DP.f("bull_charge_accel") * delta
|
||||
velocity.z += _charge_dir.z * DP.f("bull_charge_accel") * delta
|
||||
_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"):
|
||||
@@ -263,6 +286,7 @@ func _physics_process(delta: float) -> void:
|
||||
velocity.z *= DP.f("bull_charge_max_speed") / spd_c
|
||||
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
|
||||
else:
|
||||
_charge_hold_time = 0.0
|
||||
_legs.set(&"charge_pitch_target", 0.0)
|
||||
|
||||
if left_held and not right_held:
|
||||
@@ -305,11 +329,11 @@ func _physics_process(delta: float) -> void:
|
||||
# ── Periodic kick ─────────────────────────────────────────────────────────
|
||||
_kick_timer = maxf(0.0, _kick_timer - delta)
|
||||
if direction and _kick_timer <= 0.0 and not both_held:
|
||||
_apply_kick(direction, charging)
|
||||
_apply_kick(direction)
|
||||
_kick_timer = DP.f("kick_interval")
|
||||
|
||||
# ── Rolling friction (exponential, frame-rate independent) ────────────────
|
||||
var retain := pow(DP.f("roll_damping"), delta)
|
||||
var retain := pow(_roll_damping, delta)
|
||||
velocity.x *= retain
|
||||
velocity.z *= retain
|
||||
|
||||
@@ -321,8 +345,26 @@ func _physics_process(delta: float) -> void:
|
||||
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
||||
|
||||
_pre_slide_vel_y = velocity.y
|
||||
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||
move_and_slide()
|
||||
|
||||
# ── Wall bounce — no sliding, guaranteed exit ────────────────────────────
|
||||
# pre_wall_vel is captured before move_and_slide strips the into-wall component,
|
||||
# so into_spd correctly reflects how hard the bull hit the wall even when stationary.
|
||||
for i in get_slide_collision_count():
|
||||
var col := get_slide_collision(i)
|
||||
var normal := col.get_normal()
|
||||
if abs(normal.y) < 0.5:
|
||||
var flat_n := Vector3(normal.x, 0.0, normal.z).normalized()
|
||||
var into_spd := -pre_wall_vel.dot(flat_n)
|
||||
var exit_spd := maxf(into_spd * DP.f("wall_bounce_restitution"),
|
||||
DP.f("wall_min_bounce_speed"))
|
||||
velocity.x = flat_n.x * exit_spd
|
||||
velocity.z = flat_n.z * exit_spd
|
||||
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 ─────────────────────────────────────────────────────
|
||||
if is_on_floor() and not _was_on_floor:
|
||||
var impact := -_pre_slide_vel_y
|
||||
@@ -336,3 +378,5 @@ func _physics_process(delta: float) -> void:
|
||||
_set_dust_state(DustState.WALK)
|
||||
else:
|
||||
_set_dust_state(DustState.NONE)
|
||||
|
||||
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -11,10 +11,10 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="Bullosseum"
|
||||
run/main_scene="res://scene.tscn"
|
||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
config/run/main_scene="res://scene.tscn"
|
||||
run/main_scene="res://scene.tscn"
|
||||
|
||||
[autoload]
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_nearest;
|
||||
uniform float pixel_scale : hint_range(1.0, 8.0, 1.0) = 4.0;
|
||||
uniform float color_depth : hint_range(2.0, 8.0, 1.0) = 5.0;
|
||||
uniform float dither_strength : hint_range(0.0, 2.0, 0.05) = 0.8;
|
||||
uniform float scanline_strength : hint_range(0.0, 1.0, 0.05) = 0.2;
|
||||
uniform float roll_speed : hint_range(0.0, 10.0, 0.1) = 3.0;
|
||||
uniform float roll_strength : hint_range(0.0, 1.0, 0.05) = 1.0;
|
||||
|
||||
float bayer4(ivec2 p) {
|
||||
int x = p.x & 3;
|
||||
@@ -28,5 +31,17 @@ void fragment() {
|
||||
float levels = exp2(color_depth) - 1.0;
|
||||
col = clamp(floor(col * levels + t + 0.5) / levels, 0.0, 1.0);
|
||||
|
||||
if (scanline_strength > 0.0) {
|
||||
// Rolling gradient mask scrolling top-to-bottom
|
||||
float screen_y = FRAGCOORD.y * SCREEN_PIXEL_SIZE.y;
|
||||
float roll_mask = sin(screen_y * TAU - TIME * roll_speed) * 0.5 + 0.5;
|
||||
|
||||
// Scanlines modulated by the roll: strong at peak, faint at trough
|
||||
float effective_strength = scanline_strength * mix(1.0 - roll_strength, 1.0, roll_mask);
|
||||
float row_frac = fract(FRAGCOORD.y / pixel_scale);
|
||||
float scan = smoothstep(0.0, 0.3, row_frac) * (1.0 - smoothstep(0.7, 1.0, row_frac));
|
||||
col *= 1.0 - effective_strength * (1.0 - scan);
|
||||
}
|
||||
|
||||
COLOR = vec4(col, 1.0);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[ext_resource type="Script" uid="uid://wj6tgh8nb0ka" path="res://Animations/anim_publik_mees.gd" id="8_73fnb"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bs682ey3p1ivj" path="res://publikum_controller.gd" id="9_pubctrl"]
|
||||
[ext_resource type="Script" path="res://hud.gd" id="10_hud"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ulcgi"]
|
||||
|
||||
@@ -40,6 +41,9 @@ shape = SubResource("BoxShape3D_ulcgi")
|
||||
[node name="MatadorSpawner" type="Node3D" parent="." unique_id=-1293967295]
|
||||
script = ExtResource("3_spawn")
|
||||
|
||||
[node name="HUD" type="CanvasLayer" parent="." unique_id=1100000001]
|
||||
script = ExtResource("10_hud")
|
||||
|
||||
[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=1032421313]
|
||||
layer = 10
|
||||
|
||||
|
||||
Reference in New Issue
Block a user