farming-simulator/scripts/prologue/act1.gd
Dmitrii Bykov 315173556e
Some checks failed
Godot CI / Import + smoke test (push) Failing after 16s
fix: реальные расчёты scale через AABB-инспекцию
Написал scripts/util/inspect_models.gd — runtime-инструмент.
Запустил через --headless --script, получил РЕАЛЬНЫЕ размеры:

ХАРАКТЕРЫ (mini-characters):
- Все male: 0.77×0.67-0.79×0.34 м (Ш×В×Г), T-pose шириной 0.77м
- Pivot near center (offset 0.0005м)
- 32 анимации, ВКЛЮЧАЯ «idle», «walk», «sit», «static»

ФУРНИТУРА (выборка):
- bedSingle: 1.62×0.51×1.89м (реалистичный single bed)
- desk: 0.73×0.38×0.56м (мелко, нужно scale ~2.0)
- chairDesk: 0.48×0.42×0.44м (мелко, нужно ~2.0-2.5)
- kitchenFridgeLarge: 0.79×1.32×0.39м (нормально)

РАСЧЁТЫ ОТЦА:
- Цель: рост ~1.7м (взрослый отец)
- Базовая высота 0.722м → scale = 1.7/0.722 = 2.35
- В T-pose ширина 1.8м (огромный) — НО idle-поза узкая, ~0.7м
- Поэтому КРИТИЧНО запустить idle, не T-pose

ФИКСЫ:
1. Father scale 2.35 (рост 1.7м, нормальный взрослый)
2. _start_father_idle через call_deferred — glb children
   могут быть не готовы в _ready, deferred ждёт следующий кадр
3. Animation list = 32 шт. Алгоритм:
   - Ищет первую анимацию содержащую «idle» (нижний регистр)
   - Если нет — играет первую в списке (fallback)
   - Print имя анимации для debug
4. Position x=1.6 (чуть дальше от двери при scale 2.35)

ОСТАЁТСЯ ПРОВЕРИТЬ:
- Реально ли запустилась idle (увидим по позе — руки опущены или
  раскинуты T-pose)
- Возможно scale 2.35 слишком много с idle — иногда мини-чары
  выглядят пухло
- Furniture scales — TODO следующим коммитом если попросишь

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:34:06 +03:00

236 lines
9.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 door: Interactable = $WallE/Door
@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:
_hide_noise_items()
_apply_textures()
# Deferred — glb children могут быть не готовы в _ready parent сцены
call_deferred("_start_father_idle")
player.interactable_focused.connect(_on_player_focused)
player.interactable_unfocused.connect(_on_player_unfocused)
DialogManager.set_date_time(GameState.game_date, GameState.game_time)
DialogManager.set_crosshair_visible(true)
DialogManager.set_controls_hint("WASD ходить · мышь смотреть · ЛКМ взаимодействие · ESC меню · Q сохранить и выйти")
func _start_father_idle() -> void:
# Kenney glb имеют 32 анимации, но не играют ничего по умолчанию →
# модель в T-pose (руки в стороны). Запускаем idle если есть.
var anim_player: AnimationPlayer = father.find_child("AnimationPlayer", true, false) as AnimationPlayer
if anim_player == null:
return
var anims: PackedStringArray = anim_player.get_animation_list()
if anims.size() == 0:
return
var idle_name: String = ""
for name in anims:
if "idle" in name.to_lower():
idle_name = name
break
if idle_name == "":
idle_name = anims[0]
anim_player.play(idle_name)
print("[father] playing animation: ", idle_name, " (всего: ", anims.size(), ")")
func _hide_noise_items() -> void:
# Убираем только то что без модели выглядит как мусор.
# Bookcase теперь Kenney модель — Shelf видим.
for path in [
"Sock1",
"Sock2",
"Desk/AdrenalineCan",
"Desk/LaysWrapper",
"WallN/Plant",
"WallN/Cup",
]:
if has_node(path):
get_node(path).visible = false
# Если возвращаемся из кухни — пропускаем интро
if GameState.act1_father_left:
_setup_post_intro_state()
else:
await _play_intro()
func _apply_textures() -> void:
# Текстуры применяем ТОЛЬКО к моим placeholder-мешам (стены, пол,
# потолок, ковёр, постеры, дверь). Мебель теперь Kenney glb с
# собственными материалами — не трогаем.
var floor_mat: BaseMaterial3D = ($Floor/Mesh as MeshInstance3D).material_override
TextureGen.apply_to_material(floor_mat, TextureGen.linoleum(), Vector3(8, 8, 8))
var ceiling_mat: BaseMaterial3D = ($Ceiling/Mesh as MeshInstance3D).material_override
TextureGen.apply_to_material(ceiling_mat, TextureGen.ceiling(), Vector3(6, 6, 6))
for wall_path in ["WallN", "WallS", "WallE", "WallW"]:
var wall_node: Node = get_node(wall_path)
var wall_mat: BaseMaterial3D = (wall_node.get_node("Mesh") as MeshInstance3D).material_override
TextureGen.apply_to_material(wall_mat, TextureGen.wallpaper(), Vector3(8, 5, 8))
var carpet_mat: BaseMaterial3D = ($WallS/WallCarpet as MeshInstance3D).material_override
TextureGen.apply_to_material(carpet_mat, TextureGen.wall_carpet(), Vector3(1, 1, 1))
var csgo_mat: BaseMaterial3D = ($WallN/PosterCSGO as MeshInstance3D).material_override
TextureGen.apply_to_material(csgo_mat, TextureGen.poster_csgo(), Vector3(1, 1, 1))
var morgen_mat: BaseMaterial3D = ($WallN/PosterMorgen as MeshInstance3D).material_override
TextureGen.apply_to_material(morgen_mat, TextureGen.poster_morgen(), Vector3(1, 1, 1))
var door_panel_mat: BaseMaterial3D = ($WallE/DoorPanel as MeshInstance3D).material_override
TextureGen.apply_to_material(door_panel_mat, TextureGen.door(), Vector3(1, 1, 1))
func _play_intro() -> void:
system_unit.visible = false
note.visible = false
gift_box.set_interactable(false)
flash_drive.set_interactable(false)
door.set_interactable(false)
DialogManager.set_objective("Распакуй подарок от бати")
await get_tree().create_timer(0.8).timeout
await DialogManager.say("ОТЕЦ", "Ну? Чё стоишь. Распаковывай давай. Сам собирал, между прочим — полночи не спал.")
await DialogManager.think("Так… новый пека? Серьёзно? После моего динозавра?")
state = State.EXPLORE_GIFT
gift_box.set_interactable(true)
func _setup_post_intro_state() -> void:
# Возвращение из кухни — отец уже ушёл
system_unit.visible = true
note.visible = false
gift_box.visible = false
gift_box.set_interactable(false)
note.set_interactable(false)
father.visible = false
state = State.SEEK_DRIVE
flash_drive.set_interactable(true)
door.set_interactable(true)
DialogManager.set_objective("Возьми флешку с Шиндовс")
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.NOTE_READ
GameState.act1_gift_opened = true
GameState.act1_note_read = true
gift_box.set_interactable(false)
note.set_interactable(false)
DialogManager.clear_prompt()
# Коробка съёживается в 0 (как будто открылась и пропала)
var shrink: Tween = create_tween()
shrink.tween_property(gift_box, "scale", Vector3.ZERO, 0.3)
await shrink.finished
gift_box.visible = false
# Новый системник появляется на столе под монитором
system_unit.visible = true
note.visible = true
DialogManager.set_objective("Прочитай записку")
# Авто-цепочка реплик
await DialogManager.think("Ого. Реально новый системник.")
await DialogManager.think("Поставлю на стол — монитор-то старый остаётся, подключим.")
await DialogManager.think("А это записка на нём…")
await DialogManager.show_note("«Не сожги. Пароль от Wi-Fi на холодильнике. — Бать»")
await DialogManager.think("Бать ну ты красавчик конечно.")
state = State.FATHER_FAREWELL
await DialogManager.say("ОТЕЦ", "Флешка с Шиндой на столе. Ставь сам, не маленький. Если запорешь — сам и переставляй.")
await _walk_father_out()
father.visible = false
GameState.act1_father_left = true
state = State.SEEK_DRIVE
flash_drive.set_interactable(true)
door.set_interactable(true)
DialogManager.set_objective("Возьми флешку с Шиндовс")
func _on_note_interacted(_p: Node) -> void:
# Auto-flow в _on_gift_interacted делает записку автоматически.
# Эта callback оставлена для совместимости signal connections.
pass
func _walk_father_out() -> void:
# Отец — Kenney glb. Поворот на 180° (спиной к игроку, лицом к двери),
# затем шагание с покачиванием тела (legs/arms внутри glb статичны
# пока не подключим AnimationPlayer).
# Поворот корпуса
var turn_tween: Tween = create_tween()
turn_tween.tween_property(father, "rotation:y", father.rotation.y + PI, 0.4)
await turn_tween.finished
# Шагание: короткий путь до двери (0.35м, остаётся в комнате)
var initial_x: float = father.position.x
var walk_duration: float = 1.4
var walk_distance: float = 0.35
var steps: int = 4
var walk_tween: Tween = create_tween()
walk_tween.set_parallel(true)
walk_tween.tween_property(father, "position:x", initial_x + walk_distance, walk_duration)
walk_tween.tween_method(
func(t: float) -> void:
father.position.y = abs(sin(t * PI * float(steps))) * 0.04,
0.0, 1.0, walk_duration
)
await walk_tween.finished
# Попробовать сыграть animation из glb если есть
var anim_player: AnimationPlayer = father.find_child("AnimationPlayer", true, false) as AnimationPlayer
if anim_player != null:
anim_player.stop()
func _on_door_interacted(_p: Node) -> void:
if state != State.SEEK_DRIVE:
return
SceneManager.go_kitchen()
func _on_drive_interacted(_p: Node) -> void:
if state != State.SEEK_DRIVE:
return
state = State.ENDING
GameState.act1_drive_taken = true
flash_drive.set_interactable(false)
door.set_interactable(false)
DialogManager.clear_prompt()
DialogManager.set_objective("")
await DialogManager.think("Ну ладно. Андрюхино так Андрюхино. Поехали.")
await DialogManager.say("СИСТЕМА", "Конец Акта 1. Запускаю Акт 2 — Установка Шиндовс…")
GameState.game_time = "10:42"
SceneManager.go_install()