From d1b3afd0063ebf4381d67a88d9a253de62cc4b4c Mon Sep 17 00:00:00 2001 From: Dmitrii Bykov Date: Sun, 24 May 2026 17:12:04 +0300 Subject: [PATCH] =?UTF-8?q?act1:=20FPV-=D0=BA=D0=BE=D0=BC=D0=BD=D0=B0?= =?UTF-8?q?=D1=82=D0=B0=20=D1=81=20=D0=BF=D0=BE=D0=B4=D0=B0=D1=80=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC,=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=BA=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B8=20=D1=84=D0=BB=D0=B5=D1=88=D0=BA=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3D-сцена 4×4м: стены/пол/потолок, окно (свечение), дверь, стол с пека, кровать с одеялом, подарок-коробка на полу, отец-NPC в дверях. Архитектура: - FPVController (CharacterBody3D + Camera3D + RayCast3D) — mouse look, WASD, raycast-pickup, lock_input для катсцен. - Interactable (StaticBody3D base) — toggle через collision_layer. - DialogManager (autoload CanvasLayer) — субтитры snake-case, fullscreen записка, HUD-подсказки, prompt над крестом, await через signal. - act1.gd — state machine: INTRO → EXPLORE_GIFT → INSIDE_GIFT → NOTE_READ → FATHER_FAREWELL (tween x-2.5) → SEEK_DRIVE → ENDING. Канон-реплики из Пролог_Первый_день.md без правок: - Отец intro + про флешку с Шиндой - Записка «Не сожги. Пароль от Wi-Fi на холодильнике. — Бать» - Мысли героя: «новый пека?», «бать ну ты красавчик», «Андрюхино так Андрюхино» GL Compatibility renderer, unshaded материалы плоскими цветами под pixel-art 640×360. Тестировано на RTX 5070 — стартует без ошибок. main.tscn placeholder удалён, main_scene = act1_room.tscn. Не покрыто в MVP (TBD): - Опц. выход на кухню → холодильник → Wi-Fi пароль - Mastermind hack-mini-game для подбора Wi-Fi - Sfxr звуки (открытие коробки, шаги) - Переход в Акт 2 (пока quit на финале) Co-Authored-By: Claude Opus 4.7 (1M context) --- main.gd | 13 - main.gd.uid | 1 - main.tscn | 35 --- project.godot | 6 +- scenes/components/dialog_ui.tscn | 94 +++++++ scenes/components/player_fpv.tscn | 25 ++ scenes/prologue/act1_room.tscn | 305 ++++++++++++++++++++++ scripts/interactables/interactable.gd | 32 +++ scripts/interactables/interactable.gd.uid | 1 + scripts/player/fpv_controller.gd | 100 +++++++ scripts/player/fpv_controller.gd.uid | 1 + scripts/prologue/act1.gd | 90 +++++++ scripts/prologue/act1.gd.uid | 1 + scripts/ui/dialog_manager.gd | 78 ++++++ scripts/ui/dialog_manager.gd.uid | 1 + 15 files changed, 733 insertions(+), 50 deletions(-) delete mode 100644 main.gd delete mode 100644 main.gd.uid delete mode 100644 main.tscn create mode 100644 scenes/components/dialog_ui.tscn create mode 100644 scenes/components/player_fpv.tscn create mode 100644 scenes/prologue/act1_room.tscn create mode 100644 scripts/interactables/interactable.gd create mode 100644 scripts/interactables/interactable.gd.uid create mode 100644 scripts/player/fpv_controller.gd create mode 100644 scripts/player/fpv_controller.gd.uid create mode 100644 scripts/prologue/act1.gd create mode 100644 scripts/prologue/act1.gd.uid create mode 100644 scripts/ui/dialog_manager.gd create mode 100644 scripts/ui/dialog_manager.gd.uid diff --git a/main.gd b/main.gd deleted file mode 100644 index fe4543e..0000000 --- a/main.gd +++ /dev/null @@ -1,13 +0,0 @@ -extends Node2D - - -func _ready() -> void: - var w: int = ProjectSettings.get_setting("display/window/size/viewport_width") - var h: int = ProjectSettings.get_setting("display/window/size/viewport_height") - print("Farming Simulator pre-alpha. Pixel viewport: %dx%d" % [w, h]) - - -func _input(event: InputEvent) -> void: - if event is InputEventKey and event.pressed: - if event.keycode == KEY_ESCAPE or event.keycode == KEY_Q: - get_tree().quit() diff --git a/main.gd.uid b/main.gd.uid deleted file mode 100644 index 74b377f..0000000 --- a/main.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dog6k44wwqkvh diff --git a/main.tscn b/main.tscn deleted file mode 100644 index 779ad19..0000000 --- a/main.tscn +++ /dev/null @@ -1,35 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://main.gd" id="1_main"] - -[node name="Main" type="Node2D"] -script = ExtResource("1_main") - -[node name="Background" type="ColorRect" parent="."] -offset_right = 640.0 -offset_bottom = 360.0 -color = Color(0.0784314, 0.0784314, 0.117647, 1) - -[node name="TitleLabel" type="Label" parent="."] -offset_left = 0.0 -offset_top = 130.0 -offset_right = 640.0 -offset_bottom = 160.0 -text = "FARMING SIMULATOR" -horizontal_alignment = 1 - -[node name="SubLabel" type="Label" parent="."] -offset_left = 0.0 -offset_top = 180.0 -offset_right = 640.0 -offset_bottom = 210.0 -text = "pre-alpha — pixel viewport 640x360" -horizontal_alignment = 1 - -[node name="HintLabel" type="Label" parent="."] -offset_left = 0.0 -offset_top = 290.0 -offset_right = 640.0 -offset_bottom = 320.0 -text = "ESC или Q — выход" -horizontal_alignment = 1 diff --git a/project.godot b/project.godot index d7dfad7..0dca601 100644 --- a/project.godot +++ b/project.godot @@ -12,9 +12,13 @@ config_version=5 config/name="Farming Simulator" config/description="Подросток-фермер CS/TF2/PUBG-кейсов в стране Р, 2020-2022. Pre-alpha." -run/main_scene="res://main.tscn" +run/main_scene="res://scenes/prologue/act1_room.tscn" config/features=PackedStringArray("4.6", "GL Compatibility") +[autoload] + +DialogManager="*res://scenes/components/dialog_ui.tscn" + [display] window/size/viewport_width=640 diff --git a/scenes/components/dialog_ui.tscn b/scenes/components/dialog_ui.tscn new file mode 100644 index 0000000..bee84e3 --- /dev/null +++ b/scenes/components/dialog_ui.tscn @@ -0,0 +1,94 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/ui/dialog_manager.gd" id="1_dlg"] + +[node name="DialogManager" type="CanvasLayer"] +layer = 10 +script = ExtResource("1_dlg") + +[node name="HudLabel" type="Label" parent="."] +offset_left = 8.0 +offset_top = 4.0 +offset_right = 632.0 +offset_bottom = 22.0 +text = "" +modulate = Color(0.85, 0.85, 0.85, 1) + +[node name="PromptLabel" type="Label" parent="."] +offset_left = 0.0 +offset_top = 200.0 +offset_right = 640.0 +offset_bottom = 220.0 +text = "" +horizontal_alignment = 1 +modulate = Color(1, 0.95, 0.6, 1) + +[node name="SubtitlePanel" type="PanelContainer" parent="."] +offset_left = 20.0 +offset_top = 260.0 +offset_right = 620.0 +offset_bottom = 350.0 + +[node name="V" type="VBoxContainer" parent="SubtitlePanel"] +layout_mode = 2 + +[node name="Who" type="Label" parent="SubtitlePanel/V"] +layout_mode = 2 +text = "[Я]" +modulate = Color(0.9, 0.8, 0.5, 1) + +[node name="Text" type="Label" parent="SubtitlePanel/V"] +layout_mode = 2 +text = "" +autowrap_mode = 3 + +[node name="Hint" type="Label" parent="SubtitlePanel/V"] +layout_mode = 2 +text = "ЛКМ или ПРОБЕЛ — дальше" +horizontal_alignment = 2 +modulate = Color(0.55, 0.55, 0.55, 1) + +[node name="NoteOverlay" type="Control" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = 640.0 +offset_bottom = 360.0 +mouse_filter = 0 + +[node name="DarkBG" type="ColorRect" parent="NoteOverlay"] +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = 640.0 +offset_bottom = 360.0 +color = Color(0, 0, 0, 0.75) + +[node name="Center" type="CenterContainer" parent="NoteOverlay"] +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = 640.0 +offset_bottom = 360.0 + +[node name="P" type="PanelContainer" parent="NoteOverlay/Center"] +custom_minimum_size = Vector2(420, 180) +layout_mode = 2 + +[node name="V2" type="VBoxContainer" parent="NoteOverlay/Center/P"] +layout_mode = 2 + +[node name="Title" type="Label" parent="NoteOverlay/Center/P/V2"] +layout_mode = 2 +text = "Записка" +horizontal_alignment = 1 +modulate = Color(0.9, 0.8, 0.5, 1) + +[node name="Text" type="Label" parent="NoteOverlay/Center/P/V2"] +layout_mode = 2 +text = "" +autowrap_mode = 3 +horizontal_alignment = 1 + +[node name="Hint" type="Label" parent="NoteOverlay/Center/P/V2"] +layout_mode = 2 +text = "ЛКМ или ПРОБЕЛ — закрыть" +horizontal_alignment = 1 +modulate = Color(0.55, 0.55, 0.55, 1) diff --git a/scenes/components/player_fpv.tscn b/scenes/components/player_fpv.tscn new file mode 100644 index 0000000..ba9df0f --- /dev/null +++ b/scenes/components/player_fpv.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/player/fpv_controller.gd" id="1_fpv"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_player"] +height = 1.7 +radius = 0.3 + +[node name="PlayerFPV" type="CharacterBody3D"] +script = ExtResource("1_fpv") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0) +shape = SubResource("CapsuleShape3D_player") + +[node name="Camera3D" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6, 0) +current = true +fov = 70.0 +near = 0.1 +far = 50.0 + +[node name="InteractRay" type="RayCast3D" parent="Camera3D"] +target_position = Vector3(0, 0, -2.5) +collision_mask = 1 diff --git a/scenes/prologue/act1_room.tscn b/scenes/prologue/act1_room.tscn new file mode 100644 index 0000000..0b6858a --- /dev/null +++ b/scenes/prologue/act1_room.tscn @@ -0,0 +1,305 @@ +[gd_scene load_steps=42 format=3] + +[ext_resource type="Script" path="res://scripts/prologue/act1.gd" id="1_act1"] +[ext_resource type="Script" path="res://scripts/interactables/interactable.gd" id="2_int"] +[ext_resource type="PackedScene" path="res://scenes/components/player_fpv.tscn" id="3_player"] + +[sub_resource type="BoxMesh" id="mesh_floor"] +size = Vector3(4, 0.1, 4) + +[sub_resource type="BoxShape3D" id="shape_floor"] +size = Vector3(4, 0.1, 4) + +[sub_resource type="BoxMesh" id="mesh_wall_ns"] +size = Vector3(4, 2.5, 0.1) + +[sub_resource type="BoxShape3D" id="shape_wall_ns"] +size = Vector3(4, 2.5, 0.1) + +[sub_resource type="BoxMesh" id="mesh_wall_ew"] +size = Vector3(0.1, 2.5, 4) + +[sub_resource type="BoxShape3D" id="shape_wall_ew"] +size = Vector3(0.1, 2.5, 4) + +[sub_resource type="BoxMesh" id="mesh_window"] +size = Vector3(1.5, 1.0, 0.06) + +[sub_resource type="BoxMesh" id="mesh_door"] +size = Vector3(0.06, 2.0, 1.0) + +[sub_resource type="BoxMesh" id="mesh_desk"] +size = Vector3(1.5, 0.7, 0.6) + +[sub_resource type="BoxShape3D" id="shape_desk"] +size = Vector3(1.5, 0.7, 0.6) + +[sub_resource type="BoxMesh" id="mesh_old_pc"] +size = Vector3(0.25, 0.4, 0.3) + +[sub_resource type="BoxMesh" id="mesh_bed"] +size = Vector3(2.0, 0.4, 1.0) + +[sub_resource type="BoxShape3D" id="shape_bed"] +size = Vector3(2.0, 0.4, 1.0) + +[sub_resource type="BoxMesh" id="mesh_blanket"] +size = Vector3(1.95, 0.06, 0.95) + +[sub_resource type="BoxMesh" id="mesh_pillow"] +size = Vector3(0.5, 0.1, 0.25) + +[sub_resource type="BoxMesh" id="mesh_gift"] +size = Vector3(0.5, 0.4, 0.5) + +[sub_resource type="BoxShape3D" id="shape_gift"] +size = Vector3(0.5, 0.4, 0.5) + +[sub_resource type="BoxMesh" id="mesh_sys"] +size = Vector3(0.35, 0.5, 0.45) + +[sub_resource type="BoxMesh" id="mesh_note"] +size = Vector3(0.18, 0.012, 0.12) + +[sub_resource type="BoxShape3D" id="shape_note"] +size = Vector3(0.18, 0.012, 0.12) + +[sub_resource type="BoxMesh" id="mesh_flash"] +size = Vector3(0.06, 0.02, 0.025) + +[sub_resource type="BoxShape3D" id="shape_flash"] +size = Vector3(0.06, 0.02, 0.025) + +[sub_resource type="BoxMesh" id="mesh_father"] +size = Vector3(0.4, 1.7, 0.4) + +[sub_resource type="StandardMaterial3D" id="mat_floor"] +shading_mode = 0 +albedo_color = Color(0.55, 0.35, 0.22, 1) + +[sub_resource type="StandardMaterial3D" id="mat_wall"] +shading_mode = 0 +albedo_color = Color(0.79, 0.71, 0.54, 1) + +[sub_resource type="StandardMaterial3D" id="mat_ceiling"] +shading_mode = 0 +albedo_color = Color(0.85, 0.83, 0.78, 1) + +[sub_resource type="StandardMaterial3D" id="mat_window"] +shading_mode = 0 +albedo_color = Color(0.6, 0.85, 1.0, 1) +emission_enabled = true +emission = Color(0.6, 0.85, 1.0, 1) +emission_energy_multiplier = 0.5 + +[sub_resource type="StandardMaterial3D" id="mat_door"] +shading_mode = 0 +albedo_color = Color(0.29, 0.18, 0.09, 1) + +[sub_resource type="StandardMaterial3D" id="mat_desk"] +shading_mode = 0 +albedo_color = Color(0.36, 0.23, 0.11, 1) + +[sub_resource type="StandardMaterial3D" id="mat_old_pc"] +shading_mode = 0 +albedo_color = Color(0.43, 0.43, 0.43, 1) + +[sub_resource type="StandardMaterial3D" id="mat_blanket"] +shading_mode = 0 +albedo_color = Color(0.45, 0.15, 0.15, 1) + +[sub_resource type="StandardMaterial3D" id="mat_pillow"] +shading_mode = 0 +albedo_color = Color(0.92, 0.88, 0.78, 1) + +[sub_resource type="StandardMaterial3D" id="mat_gift"] +shading_mode = 0 +albedo_color = Color(0.1, 0.1, 0.12, 1) + +[sub_resource type="StandardMaterial3D" id="mat_sys"] +shading_mode = 0 +albedo_color = Color(0.3, 0.32, 0.38, 1) +emission_enabled = true +emission = Color(0.2, 0.5, 0.9, 1) +emission_energy_multiplier = 0.6 + +[sub_resource type="StandardMaterial3D" id="mat_note"] +shading_mode = 0 +albedo_color = Color(0.95, 0.95, 0.9, 1) + +[sub_resource type="StandardMaterial3D" id="mat_flash"] +shading_mode = 0 +albedo_color = Color(0.78, 0.18, 0.18, 1) +emission_enabled = true +emission = Color(0.78, 0.18, 0.18, 1) +emission_energy_multiplier = 0.4 + +[sub_resource type="StandardMaterial3D" id="mat_father"] +shading_mode = 0 +albedo_color = Color(0.18, 0.22, 0.4, 1) + +[node name="Act1Room" type="Node3D"] +script = ExtResource("1_act1") + +[node name="Floor" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0) + +[node name="Mesh" type="MeshInstance3D" parent="Floor"] +mesh = SubResource("mesh_floor") +material_override = SubResource("mat_floor") + +[node name="Coll" type="CollisionShape3D" parent="Floor"] +shape = SubResource("shape_floor") + +[node name="Ceiling" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.55, 0) + +[node name="Mesh" type="MeshInstance3D" parent="Ceiling"] +mesh = SubResource("mesh_floor") +material_override = SubResource("mat_ceiling") + +[node name="Coll" type="CollisionShape3D" parent="Ceiling"] +shape = SubResource("shape_floor") + +[node name="WallN" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, -2.05) + +[node name="Mesh" type="MeshInstance3D" parent="WallN"] +mesh = SubResource("mesh_wall_ns") +material_override = SubResource("mat_wall") + +[node name="Coll" type="CollisionShape3D" parent="WallN"] +shape = SubResource("shape_wall_ns") + +[node name="Window" type="MeshInstance3D" parent="WallN"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0.06) +mesh = SubResource("mesh_window") +material_override = SubResource("mat_window") + +[node name="WallS" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 2.05) + +[node name="Mesh" type="MeshInstance3D" parent="WallS"] +mesh = SubResource("mesh_wall_ns") +material_override = SubResource("mat_wall") + +[node name="Coll" type="CollisionShape3D" parent="WallS"] +shape = SubResource("shape_wall_ns") + +[node name="WallE" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.05, 1.25, 0) + +[node name="Mesh" type="MeshInstance3D" parent="WallE"] +mesh = SubResource("mesh_wall_ew") +material_override = SubResource("mat_wall") + +[node name="Coll" type="CollisionShape3D" parent="WallE"] +shape = SubResource("shape_wall_ew") + +[node name="DoorPanel" type="MeshInstance3D" parent="WallE"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.06, -0.25, 0) +mesh = SubResource("mesh_door") +material_override = SubResource("mat_door") + +[node name="WallW" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.05, 1.25, 0) + +[node name="Mesh" type="MeshInstance3D" parent="WallW"] +mesh = SubResource("mesh_wall_ew") +material_override = SubResource("mat_wall") + +[node name="Coll" type="CollisionShape3D" parent="WallW"] +shape = SubResource("shape_wall_ew") + +[node name="Desk" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35, -1.5) + +[node name="Mesh" type="MeshInstance3D" parent="Desk"] +mesh = SubResource("mesh_desk") +material_override = SubResource("mat_desk") + +[node name="Coll" type="CollisionShape3D" parent="Desk"] +shape = SubResource("shape_desk") + +[node name="OldPC" type="MeshInstance3D" parent="Desk"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 0.55, 0) +mesh = SubResource("mesh_old_pc") +material_override = SubResource("mat_old_pc") + +[node name="FlashDrive" type="StaticBody3D" parent="Desk"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.3, 0.36, 0.1) +script = ExtResource("2_int") +prompt = "взять флешку" + +[node name="Mesh" type="MeshInstance3D" parent="Desk/FlashDrive"] +mesh = SubResource("mesh_flash") +material_override = SubResource("mat_flash") + +[node name="Coll" type="CollisionShape3D" parent="Desk/FlashDrive"] +shape = SubResource("shape_flash") + +[node name="Bed" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 0.2, 1.4) + +[node name="Mesh" type="MeshInstance3D" parent="Bed"] +mesh = SubResource("mesh_bed") +material_override = SubResource("mat_desk") + +[node name="Coll" type="CollisionShape3D" parent="Bed"] +shape = SubResource("shape_bed") + +[node name="Blanket" type="MeshInstance3D" parent="Bed"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.23, 0) +mesh = SubResource("mesh_blanket") +material_override = SubResource("mat_blanket") + +[node name="Pillow" type="MeshInstance3D" parent="Bed"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.7, 0.31, 0) +mesh = SubResource("mesh_pillow") +material_override = SubResource("mat_pillow") + +[node name="GiftBox" type="StaticBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.2, -0.3) +script = ExtResource("2_int") +prompt = "открыть коробку" + +[node name="Mesh" type="MeshInstance3D" parent="GiftBox"] +mesh = SubResource("mesh_gift") +material_override = SubResource("mat_gift") + +[node name="Coll" type="CollisionShape3D" parent="GiftBox"] +shape = SubResource("shape_gift") + +[node name="SystemUnit" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, -0.3) +visible = false + +[node name="Mesh" type="MeshInstance3D" parent="SystemUnit"] +mesh = SubResource("mesh_sys") +material_override = SubResource("mat_sys") + +[node name="Note" type="StaticBody3D" parent="SystemUnit"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.26, 0) +script = ExtResource("2_int") +prompt = "прочитать записку" + +[node name="Mesh" type="MeshInstance3D" parent="SystemUnit/Note"] +mesh = SubResource("mesh_note") +material_override = SubResource("mat_note") + +[node name="Coll" type="CollisionShape3D" parent="SystemUnit/Note"] +shape = SubResource("shape_note") + +[node name="FatherNPC" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.7, 0.85, 0) + +[node name="Mesh" type="MeshInstance3D" parent="FatherNPC"] +mesh = SubResource("mesh_father") +material_override = SubResource("mat_father") + +[node name="PlayerFPV" parent="." instance=ExtResource("3_player")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1.2) + +[connection signal="interacted" from="Desk/FlashDrive" to="." method="_on_drive_interacted"] +[connection signal="interacted" from="GiftBox" to="." method="_on_gift_interacted"] +[connection signal="interacted" from="SystemUnit/Note" to="." method="_on_note_interacted"] diff --git a/scripts/interactables/interactable.gd b/scripts/interactables/interactable.gd new file mode 100644 index 0000000..fcfac73 --- /dev/null +++ b/scripts/interactables/interactable.gd @@ -0,0 +1,32 @@ +class_name Interactable extends StaticBody3D + +@export var prompt: String = "Взаимодействовать" +@export var enabled: bool = false + +signal interacted(player: Node) + + +func _ready() -> void: + _apply_collision() + + +func set_interactable(v: bool) -> void: + enabled = v + _apply_collision() + + +func _apply_collision() -> void: + if enabled: + collision_layer = 1 + else: + collision_layer = 0 + + +func get_prompt() -> String: + return prompt + + +func interact(player: Node) -> void: + if not enabled: + return + interacted.emit(player) diff --git a/scripts/interactables/interactable.gd.uid b/scripts/interactables/interactable.gd.uid new file mode 100644 index 0000000..cb79bc8 --- /dev/null +++ b/scripts/interactables/interactable.gd.uid @@ -0,0 +1 @@ +uid://c04k1gckreqao diff --git a/scripts/player/fpv_controller.gd b/scripts/player/fpv_controller.gd new file mode 100644 index 0000000..7416ea8 --- /dev/null +++ b/scripts/player/fpv_controller.gd @@ -0,0 +1,100 @@ +class_name FPVController extends CharacterBody3D + +@export var move_speed: float = 3.0 +@export var mouse_sensitivity: float = 0.0025 +@export var gravity: float = 9.8 + +@onready var camera: Camera3D = $Camera3D +@onready var interact_ray: RayCast3D = $Camera3D/InteractRay + +signal interactable_focused(obj: Node) +signal interactable_unfocused +signal interact_pressed(obj: Node) + +var _captured: bool = false +var _locked: bool = false +var _focused_obj: Node = null + + +func _ready() -> void: + _capture_mouse() + + +func _capture_mouse() -> void: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + _captured = true + + +func _release_mouse() -> void: + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _captured = false + + +func _input(event: InputEvent) -> void: + if event is InputEventMouseMotion and _captured and not _locked: + rotate_y(-event.relative.x * mouse_sensitivity) + camera.rotate_x(-event.relative.y * mouse_sensitivity) + camera.rotation.x = clamp(camera.rotation.x, -PI * 0.45, PI * 0.45) + elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + if not _captured: + _capture_mouse() + return + if _locked: + return + if _focused_obj != null: + interact_pressed.emit(_focused_obj) + if _focused_obj.has_method("interact"): + _focused_obj.interact(self) + elif event is InputEventKey and event.pressed: + if event.keycode == KEY_ESCAPE: + if _captured: + _release_mouse() + else: + _capture_mouse() + elif event.keycode == KEY_Q: + get_tree().quit() + + +func _physics_process(delta: float) -> void: + if _locked: + velocity = Vector3.ZERO + move_and_slide() + _update_focus() + return + var input_dir: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + var direction: Vector3 = (transform.basis * Vector3(input_dir.x, 0.0, input_dir.y)).normalized() + velocity.x = direction.x * move_speed + velocity.z = direction.z * move_speed + if not is_on_floor(): + velocity.y -= gravity * delta + else: + velocity.y = 0.0 + move_and_slide() + _update_focus() + + +func _update_focus() -> void: + interact_ray.force_raycast_update() + var new_focus: Node = null + if interact_ray.is_colliding(): + var col: Object = interact_ray.get_collider() + if col != null and col is Node and col.has_method("get_prompt"): + new_focus = col + if new_focus != _focused_obj: + if new_focus != null: + interactable_focused.emit(new_focus) + else: + interactable_unfocused.emit() + _focused_obj = new_focus + + +func lock_input() -> void: + _locked = true + + +func unlock_input() -> void: + _locked = false + + +func get_focused() -> Node: + return _focused_obj diff --git a/scripts/player/fpv_controller.gd.uid b/scripts/player/fpv_controller.gd.uid new file mode 100644 index 0000000..b8ed97a --- /dev/null +++ b/scripts/player/fpv_controller.gd.uid @@ -0,0 +1 @@ +uid://r5r7c1wxtyot diff --git a/scripts/prologue/act1.gd b/scripts/prologue/act1.gd new file mode 100644 index 0000000..eac8ed4 --- /dev/null +++ b/scripts/prologue/act1.gd @@ -0,0 +1,90 @@ +extends Node3D + +@onready var player: FPVController = $PlayerFPV +@onready var gift_box: Interactable = $GiftBox +@onready var system_unit: Node3D = $SystemUnit +@onready var note: Interactable = $SystemUnit/Note +@onready var flash_drive: Interactable = $Desk/FlashDrive +@onready var father: Node3D = $FatherNPC + +enum State { INTRO, EXPLORE_GIFT, INSIDE_GIFT, NOTE_READ, FATHER_FAREWELL, SEEK_DRIVE, ENDING } +var state: State = State.INTRO + + +func _ready() -> void: + system_unit.visible = false + note.visible = false + gift_box.set_interactable(false) + flash_drive.set_interactable(false) + + player.lock_input() + player.interactable_focused.connect(_on_player_focused) + player.interactable_unfocused.connect(_on_player_unfocused) + + DialogManager.set_hud("Акт 1 — Подарок · ЛКМ/ПРОБЕЛ — дальше · ESC — мышь · Q — выход") + + await get_tree().create_timer(0.8).timeout + await DialogManager.say("ОТЕЦ", "Ну? Чё стоишь. Распаковывай давай. Сам собирал, между прочим — полночи не спал.") + await DialogManager.think("Так… новый пека? Серьёзно? После моего динозавра?") + + state = State.EXPLORE_GIFT + gift_box.set_interactable(true) + player.unlock_input() + + +func _on_player_focused(obj: Node) -> void: + if obj.has_method("get_prompt"): + DialogManager.set_prompt(obj.get_prompt()) + + +func _on_player_unfocused() -> void: + DialogManager.clear_prompt() + + +func _on_gift_interacted(_p: Node) -> void: + if state != State.EXPLORE_GIFT: + return + state = State.INSIDE_GIFT + gift_box.set_interactable(false) + gift_box.visible = false + system_unit.visible = true + note.visible = true + note.set_interactable(true) + DialogManager.clear_prompt() + + +func _on_note_interacted(_p: Node) -> void: + if state != State.INSIDE_GIFT: + return + state = State.NOTE_READ + note.set_interactable(false) + player.lock_input() + DialogManager.clear_prompt() + + await DialogManager.show_note("«Не сожги. Пароль от Wi-Fi на холодильнике. — Бать»") + await DialogManager.think("Бать ну ты красавчик конечно.") + + state = State.FATHER_FAREWELL + await DialogManager.say("ОТЕЦ", "Флешка с Шиндой на столе. Ставь сам, не маленький. Если запорешь — сам и переставляй.") + + var tween: Tween = create_tween() + tween.tween_property(father, "position:x", father.position.x - 2.5, 0.8) + await tween.finished + father.visible = false + + state = State.SEEK_DRIVE + flash_drive.set_interactable(true) + player.unlock_input() + + +func _on_drive_interacted(_p: Node) -> void: + if state != State.SEEK_DRIVE: + return + state = State.ENDING + flash_drive.set_interactable(false) + player.lock_input() + DialogManager.clear_prompt() + + await DialogManager.think("Ну ладно. Андрюхино так Андрюхино. Поехали.") + await DialogManager.say("СИСТЕМА", "Конец Акта 1. Акт 2 — Установка Шиндовс (TBD).") + get_tree().quit() diff --git a/scripts/prologue/act1.gd.uid b/scripts/prologue/act1.gd.uid new file mode 100644 index 0000000..e85e010 --- /dev/null +++ b/scripts/prologue/act1.gd.uid @@ -0,0 +1 @@ +uid://dcn3oei0t5ley diff --git a/scripts/ui/dialog_manager.gd b/scripts/ui/dialog_manager.gd new file mode 100644 index 0000000..c4149d5 --- /dev/null +++ b/scripts/ui/dialog_manager.gd @@ -0,0 +1,78 @@ +extends CanvasLayer + +signal advanced + +@onready var subtitle_panel: PanelContainer = $SubtitlePanel +@onready var who_label: Label = $SubtitlePanel/V/Who +@onready var text_label: Label = $SubtitlePanel/V/Text +@onready var hint_label: Label = $SubtitlePanel/V/Hint + +@onready var note_overlay: Control = $NoteOverlay +@onready var note_text: Label = $NoteOverlay/Center/P/V2/Text + +@onready var prompt_label: Label = $PromptLabel +@onready var hud_label: Label = $HudLabel + +var _waiting: bool = false + + +func _ready() -> void: + subtitle_panel.visible = false + note_overlay.visible = false + prompt_label.visible = false + hud_label.visible = false + + +func say(who: String, line: String) -> Signal: + who_label.text = "[%s]" % who + text_label.text = line + hint_label.text = "ЛКМ или ПРОБЕЛ — дальше" + subtitle_panel.visible = true + note_overlay.visible = false + _waiting = true + return advanced + + +func think(line: String) -> Signal: + return say("Я", line) + + +func show_note(text: String) -> Signal: + note_text.text = text + note_overlay.visible = true + subtitle_panel.visible = false + _waiting = true + return advanced + + +func set_prompt(text: String) -> void: + if text == "": + prompt_label.visible = false + return + prompt_label.text = "[ЛКМ] %s" % text + prompt_label.visible = true + + +func clear_prompt() -> void: + prompt_label.visible = false + + +func set_hud(text: String) -> void: + hud_label.text = text + hud_label.visible = text != "" + + +func _input(event: InputEvent) -> void: + if not _waiting: + return + var advance: bool = false + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + advance = true + elif event is InputEventKey and event.pressed and (event.keycode == KEY_SPACE or event.keycode == KEY_ENTER): + advance = true + if advance: + get_viewport().set_input_as_handled() + subtitle_panel.visible = false + note_overlay.visible = false + _waiting = false + advanced.emit() diff --git a/scripts/ui/dialog_manager.gd.uid b/scripts/ui/dialog_manager.gd.uid new file mode 100644 index 0000000..9133fa0 --- /dev/null +++ b/scripts/ui/dialog_manager.gd.uid @@ -0,0 +1 @@ +uid://eb6vorv2mguj