diff --git a/Assets/intro_bg.ogv b/Assets/intro_bg.ogv new file mode 100644 index 0000000..e6928ac Binary files /dev/null and b/Assets/intro_bg.ogv differ diff --git a/Assets/intro_bg.ogv.uid b/Assets/intro_bg.ogv.uid new file mode 100644 index 0000000..8005473 --- /dev/null +++ b/Assets/intro_bg.ogv.uid @@ -0,0 +1 @@ +uid://csxxlnq74fej4 diff --git a/Assets/menu_bg.png b/Assets/menu_bg.png new file mode 100644 index 0000000..41539b1 Binary files /dev/null and b/Assets/menu_bg.png differ diff --git a/Assets/menu_bg.png.import b/Assets/menu_bg.png.import new file mode 100644 index 0000000..8f04bf9 --- /dev/null +++ b/Assets/menu_bg.png.import @@ -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 diff --git a/MainMenu.tscn b/MainMenu.tscn new file mode 100644 index 0000000..c58d04d --- /dev/null +++ b/MainMenu.tscn @@ -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) diff --git a/camera_iso.gd b/camera_iso.gd index c711361..944c1f7 100644 --- a/camera_iso.gd +++ b/camera_iso.gd @@ -14,7 +14,6 @@ extends Camera3D var _shake_amount: float = 0.0 var _shake_decay: float = 0.0 var _user_size: float = 0.0 -var _charge_active: bool = false func _ready() -> void: @@ -34,8 +33,7 @@ func _unhandled_input(event: InputEvent) -> 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, zoom_target, delta * 8.0) + size = lerpf(size, _user_size, delta * 8.0) var angle_rad := deg_to_rad(horizontal_angle_deg) var offset := Vector3( @@ -63,10 +61,6 @@ func trigger_hit(strength: float) -> void: _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: var cam_attr: CameraAttributesPractical = null diff --git a/controls_manager.gd b/controls_manager.gd new file mode 100644 index 0000000..bba50b0 --- /dev/null +++ b/controls_manager.gd @@ -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) diff --git a/controls_manager.gd.uid b/controls_manager.gd.uid new file mode 100644 index 0000000..da0dc64 --- /dev/null +++ b/controls_manager.gd.uid @@ -0,0 +1 @@ +uid://dvxx75l5re2mf diff --git a/hud.gd b/hud.gd index fa6be2b..1c0a371 100644 --- a/hud.gd +++ b/hud.gd @@ -12,7 +12,7 @@ const _ABILITY_ICONS: Array[String] = [ "res://HUD/HUD_0001_ability_SLAM.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_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.physical_keycode == KEY_TAB: _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: @@ -208,7 +216,7 @@ func _build_ability_slot(idx: int) -> Control: _cd_labels.append(cd_lbl) 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.add_theme_color_override("font_color", _GOLD) 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) _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, "RMB (hold)", "Left horn", _CREAM) - _row(vbox, "F", "Kick", _CREAM) - _row(vbox, "G", "Slam", _CREAM) - _row(vbox, "H", "Dash", _CREAM) + _row(vbox, Controls.get_key_label(&"charge"), "Charge", _CREAM) + _row(vbox, Controls.get_key_label(&"ability_kick"), "Kick", _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, "R", "Respawn matadors (cooldown)", _MUTED) _row(vbox, "Tab", "Toggle controls", _MUTED) + _row(vbox, "Esc", "Back to menu", _MUTED) var sep := HSeparator.new() var sep_style := StyleBoxFlat.new() diff --git a/main_menu.gd b/main_menu.gd new file mode 100644 index 0000000..50a456a --- /dev/null +++ b/main_menu.gd @@ -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() diff --git a/main_menu.gd.uid b/main_menu.gd.uid new file mode 100644 index 0000000..fc52009 --- /dev/null +++ b/main_menu.gd.uid @@ -0,0 +1 @@ +uid://46y10alai56q diff --git a/player.gd b/player.gd index 83208df..ff72d6e 100644 --- a/player.gd +++ b/player.gd @@ -33,6 +33,7 @@ var _bull_anim: AnimationPlayer = null var _charge_ready_emitter: CPUParticles3D = null var _slam_pending: bool = false +var _charge_trail_emitter: CPUParticles3D = null const MAX_HEALTH: int = 10 var health: int = MAX_HEALTH @@ -161,6 +162,49 @@ func _setup_charge_indicators() -> void: _charge_ready_emitter.scale_amount_curve = burst_scale 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: 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_max = DP.f("charge_vel_max") p.emitting = true + if _charge_trail_emitter: + _charge_trail_emitter.emitting = _dust_state == DustState.CHARGE 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() velocity.x = _dash_dir.x * 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")) if _huff_player: _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) _spawn_kick_dust(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) @@ -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: const INNER_R: float = 0.82 const OUTER_R: float = 1.00 @@ -616,7 +788,6 @@ func _physics_process(delta: float) -> void: # Right only → thrust NW (forward-left diagonal) var left_held := Input.is_action_pressed(&"thrust_left") 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: _thrust_left_charge = 0.0 @@ -716,6 +887,12 @@ func _physics_process(delta: float) -> void: else: _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 ─────────────────────────────────────────────────────────── diff --git a/project.godot b/project.godot index d845017..fec5ea9 100644 --- a/project.godot +++ b/project.godot @@ -11,15 +11,16 @@ config_version=5 [application] config/name="Bullosseum" -run/main_scene="res://scene.tscn" +run/main_scene="res://MainMenu.tscn" config/features=PackedStringArray("4.7", "Forward Plus") config/icon="res://icon.svg" -config/run/main_scene="res://scene.tscn" +config/run/main_scene="res://MainMenu.tscn" [autoload] DP="*res://debug_params.gd" DebugMenu="*res://debug_menu.gd" +Controls="*res://controls_manager.gd" [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) ] } -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={ "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)