Menu, options with changeable controls, fade transition, placeholder background

This commit is contained in:
2026-07-22 19:45:49 +03:00
parent d379b215e2
commit c283eb0927
13 changed files with 739 additions and 22 deletions
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
uid://csxxlnq74fej4
Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bxv1uuymonrp6"
path="res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/menu_bg.png"
dest_files=["res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+252
View File
@@ -0,0 +1,252 @@
[gd_scene load_steps=5 format=3 uid="uid://bmnmenu0intr1"]
[ext_resource type="Script" path="res://main_menu.gd" id="1_menu"]
[ext_resource type="VideoStream" path="res://Assets/intro_bg.ogv" id="2_video"]
[sub_resource type="StyleBoxFlat" id="sb_normal"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0, 0, 0, 0.4)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="sb_hover"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0, 0, 0, 0.65)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(1, 0.85, 0.2, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="MainMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_menu")
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
stream = ExtResource("2_video")
expand = true
[node name="MainPanel" type="Control" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="PlayButton" type="Button" parent="MainPanel"]
layout_mode = 1
anchor_left = 0.459
anchor_top = 0.449
anchor_right = 0.459
anchor_bottom = 0.449
offset_left = -120.0
offset_top = -55.0
offset_right = 120.0
offset_bottom = 55.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 8
theme_override_font_sizes/font_size = 56
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "PLAY"
[node name="OptionsButton" type="Button" parent="MainPanel"]
layout_mode = 1
anchor_left = 0.11
anchor_top = 0.94
anchor_right = 0.11
anchor_bottom = 0.94
offset_left = -100.0
offset_top = -28.0
offset_right = 100.0
offset_bottom = 28.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "OPTIONS"
[node name="CreditsButton" type="Button" parent="MainPanel"]
layout_mode = 1
anchor_left = 0.489
anchor_top = 0.951
anchor_right = 0.489
anchor_bottom = 0.951
offset_left = -100.0
offset_top = -28.0
offset_right = 100.0
offset_bottom = 28.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "CREDITS"
[node name="ExitButton" type="Button" parent="MainPanel"]
layout_mode = 1
anchor_left = 0.92
anchor_top = 0.951
anchor_right = 0.92
anchor_bottom = 0.951
offset_left = -100.0
offset_top = -28.0
offset_right = 100.0
offset_bottom = 28.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "EXIT"
[node name="OptionsPanel" type="CenterContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Backdrop" type="ColorRect" parent="OptionsPanel"]
layout_mode = 2
color = Color(0, 0, 0, 0.55)
mouse_filter = 2
[node name="VBox" type="VBoxContainer" parent="OptionsPanel"]
custom_minimum_size = Vector2(560, 480)
layout_mode = 2
theme_override_constants/separation = 16
[node name="TitleLabel" type="Label" parent="OptionsPanel/VBox"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 8
theme_override_font_sizes/font_size = 44
text = "Controls"
horizontal_alignment = 1
[node name="Hint" type="Label" parent="OptionsPanel/VBox"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 4
text = "Click a key, then press a new key. Esc cancels."
horizontal_alignment = 1
[node name="Scroll" type="ScrollContainer" parent="OptionsPanel/VBox"]
custom_minimum_size = Vector2(0, 340)
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="RebindList" type="VBoxContainer" parent="OptionsPanel/VBox/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 8
[node name="BackButton" type="Button" parent="OptionsPanel/VBox"]
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Back"
[node name="CreditsPanel" type="CenterContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Backdrop" type="ColorRect" parent="CreditsPanel"]
layout_mode = 2
color = Color(0, 0, 0, 0.55)
mouse_filter = 2
[node name="VBox" type="VBoxContainer" parent="CreditsPanel"]
custom_minimum_size = Vector2(520, 360)
layout_mode = 2
theme_override_constants/separation = 20
[node name="TitleLabel" type="Label" parent="CreditsPanel/VBox"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 8
theme_override_font_sizes/font_size = 44
text = "Credits"
horizontal_alignment = 1
[node name="Names" type="Label" parent="CreditsPanel/VBox"]
layout_mode = 2
size_flags_vertical = 3
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 4
theme_override_font_sizes/font_size = 24
text = "Bullosseum
A game by Siim & Richard"
horizontal_alignment = 1
vertical_alignment = 1
[node name="BackButton" type="Button" parent="CreditsPanel/VBox"]
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Back"
[node name="FadeRect" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0, 0, 0, 0)
+1 -7
View File
@@ -14,7 +14,6 @@ extends Camera3D
var _shake_amount: float = 0.0 var _shake_amount: float = 0.0
var _shake_decay: float = 0.0 var _shake_decay: float = 0.0
var _user_size: float = 0.0 var _user_size: float = 0.0
var _charge_active: bool = false
func _ready() -> void: func _ready() -> void:
@@ -34,8 +33,7 @@ func _unhandled_input(event: InputEvent) -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
var zoom_target := _user_size * DP.f("charge_zoom_factor") if _charge_active else _user_size size = lerpf(size, _user_size, delta * 8.0)
size = lerpf(size, zoom_target, delta * 8.0)
var angle_rad := deg_to_rad(horizontal_angle_deg) var angle_rad := deg_to_rad(horizontal_angle_deg)
var offset := Vector3( var offset := Vector3(
@@ -63,10 +61,6 @@ func trigger_hit(strength: float) -> void:
_shake_decay = _shake_amount / 0.4 _shake_decay = _shake_amount / 0.4
func set_charge_active(active: bool) -> void:
_charge_active = active
func _update_ortho_tilt_shift_dof(target_pos: Vector3) -> void: func _update_ortho_tilt_shift_dof(target_pos: Vector3) -> void:
var cam_attr: CameraAttributesPractical = null var cam_attr: CameraAttributesPractical = null
+93
View File
@@ -0,0 +1,93 @@
extends Node
## Global keybinding manager (autoload `Controls`).
## Loads saved keyboard bindings on startup and applies them to InputMap so
## rebinds made in the intro menu persist into the game scene. Only the
## keyboard event of each action is overridden; joypad/mouse events are kept.
const SAVE_PATH := "user://controls.cfg"
const SECTION := "keybinds"
## Actions the options menu lets the player rebind (keyboard only).
const REBINDABLE_ACTIONS: PackedStringArray = [
&"move_forward",
&"move_back",
&"move_left",
&"move_right",
&"charge",
&"ability_kick",
&"ability_slam",
&"ability_dash",
]
func _ready() -> void:
load_saved()
## Returns the first keyboard event bound to an action, or null.
func get_key_event(action: StringName) -> InputEventKey:
for event: InputEvent in InputMap.action_get_events(action):
if event is InputEventKey:
return event
return null
## Human-readable label for the action's current key (e.g. "W", "Space").
func get_key_label(action: StringName) -> String:
var event := get_key_event(action)
if event == null:
return ""
return event.as_text_physical_keycode()
## Replaces the primary keyboard event of `action` with `event`, keeping
## every other event (e.g. arrow-key alternates, joypad) in its place.
func rebind(action: StringName, event: InputEventKey) -> void:
rebind_no_save(action, event)
save()
func save() -> void:
var cfg := ConfigFile.new()
for action: StringName in REBINDABLE_ACTIONS:
var event := get_key_event(action)
if event != null:
cfg.set_value(SECTION, String(action), event.physical_keycode)
cfg.save(SAVE_PATH)
func load_saved() -> void:
var cfg := ConfigFile.new()
if cfg.load(SAVE_PATH) != OK:
return
for action: StringName in REBINDABLE_ACTIONS:
var key := String(action)
if not cfg.has_section_key(SECTION, key):
continue
var keycode: int = cfg.get_value(SECTION, key)
var event := InputEventKey.new()
event.physical_keycode = keycode as Key
rebind_no_save(action, event)
## Rebuilds the action's event list, swapping the first keyboard event for
## `event` in place so ordering (and thus the "primary" key) is preserved.
func rebind_no_save(action: StringName, event: InputEventKey) -> void:
var events := InputMap.action_get_events(action)
InputMap.action_erase_events(action)
var replaced := false
for existing: InputEvent in events:
if not replaced and existing is InputEventKey:
InputMap.action_add_event(action, event)
replaced = true
else:
InputMap.action_add_event(action, existing)
if not replaced:
InputMap.action_add_event(action, event)
func reset_defaults() -> void:
InputMap.load_from_project_settings()
if FileAccess.file_exists(SAVE_PATH):
DirAccess.remove_absolute(SAVE_PATH)
+1
View File
@@ -0,0 +1 @@
uid://dvxx75l5re2mf
+22 -6
View File
@@ -12,7 +12,7 @@ const _ABILITY_ICONS: Array[String] = [
"res://HUD/HUD_0001_ability_SLAM.png", "res://HUD/HUD_0001_ability_SLAM.png",
"res://HUD/HUD_0002_ability_DASH.png", "res://HUD/HUD_0002_ability_DASH.png",
] ]
const _ABILITY_KEYS: Array[String] = ["F", "G", "H"] const _ABILITY_ACTIONS: Array[StringName] = [&"ability_kick", &"ability_slam", &"ability_dash"]
var _controls_visible: bool = true var _controls_visible: bool = true
var _controls_panel: PanelContainer var _controls_panel: PanelContainer
@@ -68,6 +68,14 @@ 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_TAB: if event.physical_keycode == KEY_TAB:
_toggle_controls() _toggle_controls()
elif event.physical_keycode == KEY_ESCAPE:
_return_to_menu()
func _return_to_menu() -> void:
get_tree().paused = false
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
get_tree().change_scene_to_file("res://MainMenu.tscn")
func _toggle_controls() -> void: func _toggle_controls() -> void:
@@ -208,7 +216,7 @@ func _build_ability_slot(idx: int) -> Control:
_cd_labels.append(cd_lbl) _cd_labels.append(cd_lbl)
var key_lbl := Label.new() var key_lbl := Label.new()
key_lbl.text = _ABILITY_KEYS[idx] key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
key_lbl.add_theme_color_override("font_color", _GOLD) key_lbl.add_theme_color_override("font_color", _GOLD)
key_lbl.add_theme_font_size_override("font_size", 12) key_lbl.add_theme_font_size_override("font_size", 12)
@@ -250,15 +258,23 @@ func _build_controls_panel() -> void:
vbox.add_theme_constant_override("separation", 5) vbox.add_theme_constant_override("separation", 5)
_controls_panel.add_child(vbox) _controls_panel.add_child(vbox)
_row(vbox, "WASD / Arrows", "Move (not intended)", _MUTED) var move_keys := "%s %s %s %s" % [
Controls.get_key_label(&"move_forward"),
Controls.get_key_label(&"move_left"),
Controls.get_key_label(&"move_back"),
Controls.get_key_label(&"move_right"),
]
_row(vbox, move_keys, "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, "F", "Kick", _CREAM) _row(vbox, Controls.get_key_label(&"charge"), "Charge", _CREAM)
_row(vbox, "G", "Slam", _CREAM) _row(vbox, Controls.get_key_label(&"ability_kick"), "Kick", _CREAM)
_row(vbox, "H", "Dash", _CREAM) _row(vbox, Controls.get_key_label(&"ability_slam"), "Slam", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_dash"), "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, "Tab", "Toggle controls", _MUTED) _row(vbox, "Tab", "Toggle controls", _MUTED)
_row(vbox, "Esc", "Back to menu", _MUTED)
var sep := HSeparator.new() var sep := HSeparator.new()
var sep_style := StyleBoxFlat.new() var sep_style := StyleBoxFlat.new()
+147
View File
@@ -0,0 +1,147 @@
extends Control
## Intro menu: Siim's arena mockup as a looping video background with an
## overlaid Play / Options / Credits / Exit UI positioned over his sketch.
## Play fades to black and loads the game; Options rebinds controls.
const GAME_SCENE := "res://scene.tscn"
const FADE_TIME := 0.6
## Friendly labels for the rebindable actions (order defines list order).
const ACTION_LABELS: Dictionary = {
&"move_forward": "Move Forward",
&"move_back": "Move Back",
&"move_left": "Move Left",
&"move_right": "Move Right",
&"charge": "Charge",
&"ability_kick": "Kick",
&"ability_slam": "Slam",
&"ability_dash": "Dash",
}
@onready var main_panel: Control = $MainPanel
@onready var options_panel: Control = $OptionsPanel
@onready var credits_panel: Control = $CreditsPanel
@onready var rebind_list: VBoxContainer = $OptionsPanel/VBox/Scroll/RebindList
@onready var fade_rect: ColorRect = $FadeRect
@onready var play_button: Button = $MainPanel/PlayButton
@onready var options_button: Button = $MainPanel/OptionsButton
@onready var credits_button: Button = $MainPanel/CreditsButton
@onready var exit_button: Button = $MainPanel/ExitButton
@onready var options_back: Button = $OptionsPanel/VBox/BackButton
@onready var credits_back: Button = $CreditsPanel/VBox/BackButton
@onready var video: VideoStreamPlayer = $VideoStreamPlayer
var _listening_action: StringName = &""
var _listening_button: Button = null
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
options_panel.visible = false
credits_panel.visible = false
fade_rect.color = Color(0, 0, 0, 0)
if video.stream != null:
video.play()
video.finished.connect(video.play) # loop the background clip
play_button.pressed.connect(_on_play_pressed)
options_button.pressed.connect(_on_options_pressed)
credits_button.pressed.connect(_on_credits_pressed)
exit_button.pressed.connect(_on_exit_pressed)
options_back.pressed.connect(_close_subpanels)
credits_back.pressed.connect(_close_subpanels)
_build_rebind_rows()
play_button.grab_focus()
func _build_rebind_rows() -> void:
for child in rebind_list.get_children():
child.queue_free()
for action: StringName in ACTION_LABELS:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var name_label := Label.new()
name_label.text = ACTION_LABELS[action]
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
name_label.add_theme_color_override("font_outline_color", Color.BLACK)
name_label.add_theme_constant_override("outline_size", 4)
row.add_child(name_label)
var key_button := Button.new()
key_button.custom_minimum_size = Vector2(160, 0)
key_button.text = Controls.get_key_label(action)
key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button))
row.add_child(key_button)
rebind_list.add_child(row)
func _on_play_pressed() -> void:
_set_buttons_disabled(true)
var tween := create_tween()
tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME)
tween.tween_callback(func() -> void:
get_tree().change_scene_to_file(GAME_SCENE))
func _on_options_pressed() -> void:
main_panel.visible = false
options_panel.visible = true
options_back.grab_focus()
func _on_credits_pressed() -> void:
main_panel.visible = false
credits_panel.visible = true
credits_back.grab_focus()
func _on_exit_pressed() -> void:
get_tree().quit()
func _close_subpanels() -> void:
_cancel_listening()
options_panel.visible = false
credits_panel.visible = false
main_panel.visible = true
play_button.grab_focus()
func _set_buttons_disabled(value: bool) -> void:
play_button.disabled = value
options_button.disabled = value
credits_button.disabled = value
exit_button.disabled = value
func _on_rebind_pressed(action: StringName, button: Button) -> void:
_cancel_listening()
_listening_action = action
_listening_button = button
button.text = "Press a key…"
func _cancel_listening() -> void:
if _listening_button != null:
_listening_button.text = Controls.get_key_label(_listening_action)
_listening_action = &""
_listening_button = null
func _unhandled_input(event: InputEvent) -> void:
if _listening_action == &"":
return
if event is InputEventKey and event.pressed and not event.echo:
if event.physical_keycode == KEY_ESCAPE:
_cancel_listening()
else:
var new_event := InputEventKey.new()
new_event.physical_keycode = event.physical_keycode
Controls.rebind(_listening_action, new_event)
_listening_button.text = Controls.get_key_label(_listening_action)
_listening_action = &""
_listening_button = null
get_viewport().set_input_as_handled()
+1
View File
@@ -0,0 +1 @@
uid://46y10alai56q
+178 -1
View File
@@ -33,6 +33,7 @@ var _bull_anim: AnimationPlayer = null
var _charge_ready_emitter: CPUParticles3D = null var _charge_ready_emitter: CPUParticles3D = null
var _slam_pending: bool = false var _slam_pending: bool = false
var _charge_trail_emitter: CPUParticles3D = null
const MAX_HEALTH: int = 10 const MAX_HEALTH: int = 10
var health: int = MAX_HEALTH var health: int = MAX_HEALTH
@@ -161,6 +162,49 @@ func _setup_charge_indicators() -> void:
_charge_ready_emitter.scale_amount_curve = burst_scale _charge_ready_emitter.scale_amount_curve = burst_scale
cube_guy.add_child(_charge_ready_emitter) cube_guy.add_child(_charge_ready_emitter)
# Blue energy trail — streams backward during charge
var trail_mat := StandardMaterial3D.new()
trail_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
trail_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
trail_mat.vertex_color_use_as_albedo = true
trail_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var trail_sphere := SphereMesh.new()
trail_sphere.radius = 0.065
trail_sphere.height = 0.13
trail_sphere.radial_segments = 4
trail_sphere.rings = 2
trail_sphere.material = trail_mat
var trail_ramp := Gradient.new()
trail_ramp.set_color(0, Color(0.30, 0.65, 1.0, 1.0))
trail_ramp.set_color(1, Color(0.30, 0.65, 1.0, 0.0))
var trail_scale := Curve.new()
trail_scale.add_point(Vector2(0.0, 1.0))
trail_scale.add_point(Vector2(0.6, 0.8))
trail_scale.add_point(Vector2(1.0, 0.0))
_charge_trail_emitter = CPUParticles3D.new()
_charge_trail_emitter.position = Vector3(0.0, 0.25, 0.3)
_charge_trail_emitter.emitting = false
_charge_trail_emitter.one_shot = false
_charge_trail_emitter.amount = 18
_charge_trail_emitter.lifetime = 0.30
_charge_trail_emitter.randomness = 0.4
_charge_trail_emitter.local_coords = false
_charge_trail_emitter.direction = Vector3(0.0, 0.2, 1.0).normalized()
_charge_trail_emitter.spread = 25.0
_charge_trail_emitter.gravity = Vector3(0.0, 2.0, 0.0)
_charge_trail_emitter.initial_velocity_min = 4.0
_charge_trail_emitter.initial_velocity_max = 9.0
_charge_trail_emitter.scale_amount_min = 0.4
_charge_trail_emitter.scale_amount_max = 1.2
_charge_trail_emitter.mesh = trail_sphere
_charge_trail_emitter.color_ramp = trail_ramp
_charge_trail_emitter.scale_amount_curve = trail_scale
cube_guy.add_child(_charge_trail_emitter)
func _setup_audio() -> void: func _setup_audio() -> void:
if ResourceLoader.exists("res://sounds/bull_huff.ogg"): if ResourceLoader.exists("res://sounds/bull_huff.ogg"):
@@ -205,6 +249,8 @@ func _set_dust_state(new_state: DustState) -> void:
p.initial_velocity_min = DP.f("charge_vel_min") p.initial_velocity_min = DP.f("charge_vel_min")
p.initial_velocity_max = DP.f("charge_vel_max") p.initial_velocity_max = DP.f("charge_vel_max")
p.emitting = true p.emitting = true
if _charge_trail_emitter:
_charge_trail_emitter.emitting = _dust_state == DustState.CHARGE
func _on_dp_changed(_key: String, _val: Variant) -> void: func _on_dp_changed(_key: String, _val: Variant) -> void:
@@ -296,6 +342,7 @@ func _activate_dash() -> void:
_dash_dir = Vector3(raw.x, 0.0, raw.z).normalized() _dash_dir = Vector3(raw.x, 0.0, raw.z).normalized()
velocity.x = _dash_dir.x * DP.f("dash_speed") velocity.x = _dash_dir.x * DP.f("dash_speed")
velocity.z = _dash_dir.z * DP.f("dash_speed") velocity.z = _dash_dir.z * DP.f("dash_speed")
_spawn_dash_burst()
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch")) _legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
if _huff_player: if _huff_player:
_huff_player.pitch_scale = randf_range(0.9, 1.1) _huff_player.pitch_scale = randf_range(0.9, 1.1)
@@ -421,6 +468,8 @@ func _spawn_kick_fx(back_dir: Vector3) -> void:
var origin := global_position + back_dir * 0.55 + Vector3(0.0, -0.25, 0.0) var origin := global_position + back_dir * 0.55 + Vector3(0.0, -0.25, 0.0)
_spawn_kick_dust(origin, back_dir) _spawn_kick_dust(origin, back_dir)
_spawn_kick_stones(origin, back_dir) _spawn_kick_stones(origin, back_dir)
_spawn_kick_sparks(origin, back_dir)
_spawn_kick_flash(origin)
camera_pivot.call(&"trigger_hit", 0.35) camera_pivot.call(&"trigger_hit", 0.35)
@@ -520,6 +569,129 @@ func _spawn_kick_stones(origin: Vector3, back_dir: Vector3) -> void:
) )
func _spawn_kick_flash(origin: Vector3) -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.albedo_color = Color(1.0, 0.97, 0.40, 0.0)
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 1.0
sphere.height = 2.0
sphere.radial_segments = 8
sphere.rings = 4
sphere.material = mat
var mi := MeshInstance3D.new()
mi.mesh = sphere
mi.material_override = mat
mi.scale = Vector3(0.05, 0.05, 0.05)
get_parent().add_child(mi)
mi.global_position = origin
var tween := create_tween()
tween.set_parallel(true)
tween.tween_property(mi, "scale", Vector3(0.7, 0.7, 0.7), 0.13)\
.set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
var fade := func(a: float) -> void: mat.albedo_color.a = a
tween.tween_method(fade, 0.92, 0.0, 0.14)\
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
tween.tween_callback(mi.queue_free).set_delay(0.16)
func _spawn_dash_burst() -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var box := BoxMesh.new()
box.size = Vector3(0.045, 0.045, 0.30)
box.material = mat
var ramp := Gradient.new()
ramp.set_color(0, Color(0.50, 0.82, 1.0, 1.0))
ramp.set_color(1, Color(0.50, 0.82, 1.0, 0.0))
var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0.0, 1.0))
scale_curve.add_point(Vector2(1.0, 0.0))
var p := CPUParticles3D.new()
p.one_shot = true
p.explosiveness = 0.92
p.amount = 18
p.lifetime = 0.22
p.randomness = 0.15
p.local_coords = false
p.direction = _dash_dir
p.spread = 15.0
p.gravity = Vector3.ZERO
p.initial_velocity_min = 18.0
p.initial_velocity_max = 35.0
p.scale_amount_min = 0.8
p.scale_amount_max = 2.8
p.mesh = box
p.color_ramp = ramp
p.scale_amount_curve = scale_curve
get_parent().add_child(p)
p.global_position = global_position + Vector3(0.0, 0.2, 0.0)
p.restart()
get_tree().create_timer(0.6).timeout.connect(func() -> void:
if is_instance_valid(p): p.queue_free()
)
func _spawn_kick_sparks(origin: Vector3, back_dir: Vector3) -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.04
sphere.height = 0.08
sphere.radial_segments = 3
sphere.rings = 1
sphere.material = mat
var ramp := Gradient.new()
ramp.set_color(0, Color(1.0, 0.98, 0.60, 1.0))
ramp.set_color(1, Color(1.0, 1.0, 1.0, 0.0))
var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0.0, 1.0))
scale_curve.add_point(Vector2(0.5, 0.6))
scale_curve.add_point(Vector2(1.0, 0.0))
var p := CPUParticles3D.new()
p.one_shot = true
p.explosiveness = 0.95
p.amount = 22
p.lifetime = 0.28
p.randomness = 0.4
p.local_coords = false
p.direction = (back_dir + Vector3(0.0, 0.5, 0.0)).normalized()
p.spread = 30.0
p.gravity = Vector3(0.0, -14.0, 0.0)
p.initial_velocity_min = 8.0
p.initial_velocity_max = 18.0
p.scale_amount_min = 0.6
p.scale_amount_max = 2.0
p.mesh = sphere
p.color_ramp = ramp
p.scale_amount_curve = scale_curve
get_parent().add_child(p)
p.global_position = origin
p.restart()
get_tree().create_timer(1.0).timeout.connect(func() -> void:
if is_instance_valid(p): p.queue_free()
)
func _make_slam_ring_mesh() -> ArrayMesh: func _make_slam_ring_mesh() -> ArrayMesh:
const INNER_R: float = 0.82 const INNER_R: float = 0.82
const OUTER_R: float = 1.00 const OUTER_R: float = 1.00
@@ -616,7 +788,6 @@ func _physics_process(delta: float) -> void:
# Right only → thrust NW (forward-left 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")
camera_pivot.call(&"set_charge_active", left_held and right_held)
if left_held and right_held: if left_held and right_held:
_thrust_left_charge = 0.0 _thrust_left_charge = 0.0
@@ -716,6 +887,12 @@ func _physics_process(delta: float) -> void:
else: else:
_set_dust_state(DustState.NONE) _set_dust_state(DustState.NONE)
# ── Charge trail direction ────────────────────────────────────────────────
if _dust_state == DustState.CHARGE and _charge_trail_emitter:
var vel_flat := Vector3(velocity.x, 0.0, velocity.z)
if vel_flat.length_squared() > 0.1:
_charge_trail_emitter.direction = (-vel_flat.normalized() + Vector3(0.0, 0.25, 0.0)).normalized()
# ── Hoof sounds ─────────────────────────────────────────────────────────── # ── Hoof sounds ───────────────────────────────────────────────────────────
+3 -8
View File
@@ -11,15 +11,16 @@ config_version=5
[application] [application]
config/name="Bullosseum" config/name="Bullosseum"
run/main_scene="res://scene.tscn" run/main_scene="res://MainMenu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus") config/features=PackedStringArray("4.7", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.svg"
config/run/main_scene="res://scene.tscn" config/run/main_scene="res://MainMenu.tscn"
[autoload] [autoload]
DP="*res://debug_params.gd" DP="*res://debug_params.gd"
DebugMenu="*res://debug_menu.gd" DebugMenu="*res://debug_menu.gd"
Controls="*res://controls_manager.gd"
[editor_plugins] [editor_plugins]
@@ -59,12 +60,6 @@ move_right={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
jump={
"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":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
]
}
zoom_in={ zoom_in={
"deadzone": 0.2, "deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(726, 8),"global_position":Vector2(735, 56),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null) "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(726, 8),"global_position":Vector2(735, 56),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)