act1: FPV-комната с подарком, запиской и флешкой
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) <noreply@anthropic.com>
2026-05-24 17:12:04 +03:00
|
|
|
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
|
2026-05-24 17:17:43 +03:00
|
|
|
var input_dir: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
act1: FPV-комната с подарком, запиской и флешкой
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) <noreply@anthropic.com>
2026-05-24 17:12:04 +03:00
|
|
|
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
|