diff --git a/project.godot b/project.godot index e90eecb..77777f5 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,6 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="KickSurvivors" -run/main_scene="res://scenes/Main.tscn" +run/main_scene="res://scenes/MainMenu.tscn" config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" diff --git a/scenes/MainMenu.tscn b/scenes/MainMenu.tscn new file mode 100644 index 0000000..7deca2e --- /dev/null +++ b/scenes/MainMenu.tscn @@ -0,0 +1,8 @@ +[gd_scene format=3 uid="uid://mainmenu2024"] + +[ext_resource type="Script" path="res://scripts/MainMenu.gd" id="1_menu"] + +[node name="MainMenu" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1_menu") diff --git a/scripts/IronShield.gd b/scripts/IronShield.gd index e5a2f4e..17749d6 100644 --- a/scripts/IronShield.gd +++ b/scripts/IronShield.gd @@ -21,3 +21,6 @@ func _process(delta: float) -> void: func interact(player: Node) -> void: if player.call("apply_iron_shield"): queue_free() + var mains := get_tree().get_nodes_in_group("main") + if not mains.is_empty(): + mains[0].call("show_tutorial", "Tutorial_Shield") diff --git a/scripts/LeatherArmor.gd b/scripts/LeatherArmor.gd index c793968..8303a2a 100644 --- a/scripts/LeatherArmor.gd +++ b/scripts/LeatherArmor.gd @@ -21,3 +21,6 @@ func _process(delta: float) -> void: func interact(player: Node) -> void: if player.call("apply_leather_armor"): queue_free() + var mains := get_tree().get_nodes_in_group("main") + if not mains.is_empty(): + mains[0].call("show_tutorial", "Tutorial_LeatherArmor") diff --git a/scripts/LeatherBoots.gd b/scripts/LeatherBoots.gd index a1dcf32..fac088f 100644 --- a/scripts/LeatherBoots.gd +++ b/scripts/LeatherBoots.gd @@ -23,3 +23,6 @@ func _process(delta: float) -> void: func interact(player: Node) -> void: player.call("apply_upgrade_boots", 2.0, tier) queue_free() + var mains := get_tree().get_nodes_in_group("main") + if not mains.is_empty(): + mains[0].call("show_tutorial", "Tutorial_LeatherBoots") diff --git a/scripts/Main.gd b/scripts/Main.gd index f5297d8..724ad01 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -27,6 +27,16 @@ var kills_for_next: int = 10 var game_active: bool = false var upgrading: bool = false +# Tutorial +var tutorial_canvas: CanvasLayer +var tutorial_image: TextureRect +var tutorial_hint: Label +var tutorial_active: bool = false +var tutorial_hint_ready: bool = false +var tutorial_timer: float = 0.0 +var tutorial_on_dismiss: Callable = Callable() +var shown_tutorials: Dictionary = {} + # UI nodes var canvas: CanvasLayer var score_label: Label @@ -40,17 +50,25 @@ var upgrade_panel: Panel var gameover_panel: Panel func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS _spawn_level() _create_camera() _create_ui() + _create_tutorial_overlay() _spawn_player() _spawn_rocks() _spawn_sticks() - _start_game() add_to_group("main") Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + show_tutorial("Tutorial_StartGame", _start_game) func _input(event: InputEvent) -> void: + if tutorial_active and tutorial_hint_ready: + var mb := event as InputEventMouseButton + if mb != null and mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: + _dismiss_tutorial() + return + var motion := event as InputEventMouseMotion if motion != null and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: cam_yaw -= motion.relative.x * MOUSE_SENS @@ -80,6 +98,12 @@ func _create_camera() -> void: add_child(camera) func _process(delta: float) -> void: + if tutorial_active: + tutorial_timer -= delta + if tutorial_timer <= 0.0 and not tutorial_hint_ready: + tutorial_hint_ready = true + tutorial_hint.visible = true + return if is_instance_valid(player): var yaw_r: float = deg_to_rad(cam_yaw) var pitch_r: float = deg_to_rad(cam_pitch) @@ -335,6 +359,67 @@ func _restart() -> void: get_tree().paused = false get_tree().reload_current_scene() +# ─── Tutorial ───────────────────────────────────────────────────────────────── + +func _create_tutorial_overlay() -> void: + tutorial_canvas = CanvasLayer.new() + tutorial_canvas.process_mode = Node.PROCESS_MODE_ALWAYS + tutorial_canvas.visible = false + add_child(tutorial_canvas) + + var bg := ColorRect.new() + bg.color = Color(0.0, 0.0, 0.0, 0.78) + bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + tutorial_canvas.add_child(bg) + + tutorial_image = TextureRect.new() + tutorial_image.anchor_left = 0.1 + tutorial_image.anchor_right = 0.9 + tutorial_image.anchor_top = 0.07 + tutorial_image.anchor_bottom = 0.84 + tutorial_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tutorial_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL + tutorial_canvas.add_child(tutorial_image) + + tutorial_hint = Label.new() + tutorial_hint.anchor_left = 0.5 + tutorial_hint.anchor_right = 0.5 + tutorial_hint.anchor_top = 0.88 + tutorial_hint.offset_left = -280 + tutorial_hint.offset_right = 280 + tutorial_hint.text = "Нажмите ЛКМ чтобы продолжить" + tutorial_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + tutorial_hint.add_theme_font_size_override("font_size", 22) + tutorial_hint.add_theme_color_override("font_color", Color.WHITE) + tutorial_hint.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.9)) + tutorial_hint.add_theme_constant_override("shadow_offset_x", 2) + tutorial_hint.add_theme_constant_override("shadow_offset_y", 2) + tutorial_hint.visible = false + tutorial_canvas.add_child(tutorial_hint) + +func show_tutorial(key: String, on_dismiss: Callable = Callable()) -> void: + if shown_tutorials.get(key, false): + if on_dismiss.is_valid(): + on_dismiss.call() + return + shown_tutorials[key] = true + var path := "res://assets/%s.png" % key + tutorial_image.texture = load(path) if ResourceLoader.exists(path) else null + tutorial_on_dismiss = on_dismiss + tutorial_active = true + tutorial_hint_ready = false + tutorial_timer = 3.0 + tutorial_hint.visible = false + tutorial_canvas.visible = true + get_tree().paused = true + +func _dismiss_tutorial() -> void: + tutorial_active = false + tutorial_canvas.visible = false + get_tree().paused = false + if tutorial_on_dismiss.is_valid(): + tutorial_on_dismiss.call() + # ─── UI ─────────────────────────────────────────────────────────────────────── func _create_ui() -> void: diff --git a/scripts/MainMenu.gd b/scripts/MainMenu.gd new file mode 100644 index 0000000..1c80668 --- /dev/null +++ b/scripts/MainMenu.gd @@ -0,0 +1,115 @@ +extends Control + +static var volume: float = 100.0 + +var settings_panel: Panel + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _build_ui() + _apply_volume(volume) + +func _build_ui() -> void: + var bg := ColorRect.new() + bg.color = Color(0.06, 0.04, 0.10) + bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(bg) + + var title := Label.new() + title.text = "KickSurvivors" + title.add_theme_font_size_override("font_size", 52) + title.add_theme_color_override("font_color", Color.WHITE) + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title.anchor_left = 0.5 + title.anchor_right = 0.5 + title.anchor_top = 0.0 + title.offset_left = -200 + title.offset_right = 200 + title.offset_top = 110 + add_child(title) + + var vbox := VBoxContainer.new() + vbox.add_theme_constant_override("separation", 14) + vbox.anchor_left = 0.5 + vbox.anchor_right = 0.5 + vbox.anchor_top = 0.5 + vbox.anchor_bottom = 0.5 + vbox.offset_left = -110 + vbox.offset_right = 110 + vbox.offset_top = -90 + vbox.offset_bottom = 90 + add_child(vbox) + + vbox.add_child(_btn("Играть", _on_play)) + vbox.add_child(_btn("Настройки", _on_settings)) + vbox.add_child(_btn("Выход", _on_exit)) + + _build_settings_panel() + +func _btn(text: String, cb: Callable) -> Button: + var b := Button.new() + b.text = text + b.custom_minimum_size = Vector2(220, 54) + b.add_theme_font_size_override("font_size", 20) + b.connect("pressed", cb) + return b + +func _build_settings_panel() -> void: + settings_panel = Panel.new() + settings_panel.visible = false + settings_panel.anchor_left = 0.5 + settings_panel.anchor_right = 0.5 + settings_panel.anchor_top = 0.5 + settings_panel.anchor_bottom = 0.5 + settings_panel.offset_left = -200 + settings_panel.offset_right = 200 + settings_panel.offset_top = -130 + settings_panel.offset_bottom = 130 + add_child(settings_panel) + + var vbox := VBoxContainer.new() + vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + vbox.alignment = BoxContainer.ALIGNMENT_CENTER + vbox.add_theme_constant_override("separation", 18) + settings_panel.add_child(vbox) + + var lbl := Label.new() + lbl.text = "Настройки" + lbl.add_theme_font_size_override("font_size", 26) + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + vbox.add_child(lbl) + + var vol_lbl := Label.new() + vol_lbl.name = "VolumeLabel" + vol_lbl.text = "Громкость: %d" % int(volume) + vol_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + vbox.add_child(vol_lbl) + + var slider := HSlider.new() + slider.min_value = 0 + slider.max_value = 100 + slider.step = 1 + slider.value = volume + slider.custom_minimum_size = Vector2(320, 32) + slider.connect("value_changed", func(v: float) -> void: + volume = v + vol_lbl.text = "Громкость: %d" % int(v) + _apply_volume(v) + ) + vbox.add_child(slider) + + vbox.add_child(_btn("Назад", func(): settings_panel.visible = false)) + +func _apply_volume(v: float) -> void: + var db := linear_to_db(v / 100.0) if v > 0.0 else -80.0 + AudioServer.set_bus_volume_db(0, db) + +func _on_play() -> void: + get_tree().change_scene_to_file("res://scenes/Main.tscn") + +func _on_settings() -> void: + settings_panel.visible = true + +func _on_exit() -> void: + get_tree().quit() diff --git a/scripts/MainMenu.gd.uid b/scripts/MainMenu.gd.uid new file mode 100644 index 0000000..0341919 --- /dev/null +++ b/scripts/MainMenu.gd.uid @@ -0,0 +1 @@ +uid://on1o20vpycgm diff --git a/scripts/WoodenShield.gd b/scripts/WoodenShield.gd index 3b76edc..e12560e 100644 --- a/scripts/WoodenShield.gd +++ b/scripts/WoodenShield.gd @@ -21,3 +21,6 @@ func _process(delta: float) -> void: func interact(player: Node) -> void: if player.call("apply_wooden_shield"): queue_free() + var mains := get_tree().get_nodes_in_group("main") + if not mains.is_empty(): + mains[0].call("show_tutorial", "Tutorial_Shield")