extends Node2D ## Игровой слой: убежище в разрезе, вид сбоку. ## ## Связывает три части: модель (VaultGrid), отрисовку (VaultView) и HUD. ## Здесь же живёт контракт сохранения — сцена лежит в группе "persistent". ## ## Управление: ЛКМ действует по режиму из нижней панели (по умолчанию — выбор ## комнаты), ПКМ/СКМ — тащить вид, колесо — зум, WASD и стрелки — прокрутка. ## Строки сводки ресурсов: ключ из VaultGrid.totals() → подпись и цвет. const STAT_ROWS := { "power": {"label": "Питание", "color": "#f2b134"}, "water": {"label": "Вода", "color": "#4aa8f2"}, "food": {"label": "Еда", "color": "#f2704a"}, } const HINT := "ЛКМ — выбрать комнату · ПКМ — тащить вид · колесо — зум" ## Насколько мышь может сместиться между нажатием и отпусканием, чтобы это ## всё ещё считалось кликом, а не перетаскиванием вида. const CLICK_SLACK := 6.0 ## Режим курсора. По умолчанию сцена ничего не строит: строительство ## включается только выбором типа комнаты в нижней панели. enum Mode { SELECT, BUILD, DEMOLISH } @onready var _saves: SaveManagerService = get_node("/root/SaveManager") @onready var _vault: VaultView = $View @onready var _camera: VaultCamera = $Camera @onready var _stats: HBoxContainer = $Hud/Root/TopBar/Stats @onready var _tools: HBoxContainer = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Tools @onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status var _grid := VaultGrid.new() var _mode := Mode.SELECT ## Тип комнаты, который построится в режиме Mode.BUILD. var _build_kind := "" ## Якорная ячейка выбранной комнаты; (-1, -1) — ничего не выбрано. var _selected_cell := Vector2i(-1, -1) var _stat_labels := {} var _tool_group := ButtonGroup.new() var _right_pressed := false var _right_press_position := Vector2.ZERO func _ready() -> void: _grid.reset_to_starter() _vault.grid = _grid add_to_group(SaveManagerService.PERSIST_GROUP) _saves.begin_session(scene_file_path) _build_stats() _build_tools() _refresh_stats() _status.text = HINT func _process(_delta: float) -> void: # В режиме выбора призрака нет: пустой тип и (-1, -1) гасят подсветку. if _mode == Mode.SELECT: _vault.set_ghost("", Vector2i(-1, -1), false) return _vault.set_ghost(_build_kind, _cell_under_mouse(), _mode == Mode.DEMOLISH) ## ПКМ обслуживается здесь, а не в _unhandled_input: камера забирает эти ## события себе под перетаскивание вида. Событие не гасим — камера должна ## получить его как обычно. func _input(event: InputEvent) -> void: if not (event is InputEventMouseButton): return var button := event as InputEventMouseButton if button.button_index != MOUSE_BUTTON_RIGHT: return if button.pressed: _right_pressed = true _right_press_position = button.position return if not _right_pressed: return _right_pressed = false # Вид не двигали — значит это клик, а не перетаскивание. if button.position.distance_to(_right_press_position) <= CLICK_SLACK: _reset_to_select() func _unhandled_input(event: InputEvent) -> void: if not (event is InputEventMouseButton): return var button := event as InputEventMouseButton if button.button_index != MOUSE_BUTTON_LEFT or not button.pressed: return # Берём точку из самого события, а не текущее положение курсора: клик # должен срабатывать там, где кликнули. _apply_at(_cell_at_screen(button.position)) get_viewport().set_input_as_handled() # --- Строительство ---------------------------------------------------------- func _cell_under_mouse() -> Vector2i: return VaultGrid.cell_at(_vault.get_local_mouse_position()) ## Ячейка под точкой экрана. Учитывает камеру: переводим из координат ## окна в локальные координаты вида. func _cell_at_screen(screen_position: Vector2) -> Vector2i: var local := _vault.get_global_transform_with_canvas().affine_inverse() * screen_position return VaultGrid.cell_at(local) func _apply_at(cell: Vector2i) -> void: match _mode: Mode.SELECT: _select_at(cell) Mode.DEMOLISH: if _grid.remove_at(cell.y, cell.x): _after_change("Комната снесена.") else: _status.text = "Здесь нечего сносить." Mode.BUILD: if _grid.place(_build_kind, cell.y, cell.x): _after_change("Построено: %s" % VaultGrid.KINDS[_build_kind]["label"]) else: _status.text = "Здесь строить нельзя: занято или вплотную к шахте." func _after_change(message: String) -> void: # Индексы комнат после изменения съезжают — выделение сбрасываем. _set_selection(Vector2i(-1, -1)) _vault.queue_redraw() _refresh_stats() _status.text = message # --- Выбор ------------------------------------------------------------------ func _select_at(cell: Vector2i) -> void: var index := _grid.room_index_at(cell.y, cell.x) if index < 0: _set_selection(Vector2i(-1, -1)) _status.text = HINT return _set_selection(cell) _status.text = _describe(_grid.rooms[index]) func _set_selection(cell: Vector2i) -> void: _selected_cell = cell _vault.set_selection(cell) ## Строка для нижней панели: что за комната, на каком этаже и её баланс. func _describe(room: Dictionary) -> String: var kind: Dictionary = VaultGrid.KINDS[String(room["kind"])] var parts := PackedStringArray() parts.append(String(kind["label"])) parts.append("этаж %d" % (int(room["floor"]) + 1)) for key: String in STAT_ROWS: var amount := int(kind[key]) if amount != 0: parts.append("%s %+d" % [String(STAT_ROWS[key]["label"]).to_lower(), amount]) return " · ".join(parts) # --- HUD -------------------------------------------------------------------- func _build_stats() -> void: for key: String in STAT_ROWS: var row := VBoxContainer.new() row.add_theme_constant_override("separation", 0) var caption := Label.new() caption.text = String(STAT_ROWS[key]["label"]) caption.add_theme_font_size_override("font_size", 14) caption.add_theme_color_override("font_color", Color("#8fa5ba")) row.add_child(caption) var value := Label.new() value.add_theme_font_size_override("font_size", 24) value.add_theme_color_override("font_color", Color(String(STAT_ROWS[key]["color"]))) row.add_child(value) _stat_labels[key] = value _stats.add_child(row) func _refresh_stats() -> void: var totals := _grid.totals() for key: String in _stat_labels: var amount := int(totals[key]) var label: Label = _stat_labels[key] label.text = "%+d" % amount # Дефицит важнее оформления: красим в тревожный цвет, а не в цвет ресурса. if amount < 0: label.add_theme_color_override("font_color", Color("#f2704a")) else: label.add_theme_color_override("font_color", Color(String(STAT_ROWS[key]["color"]))) func _build_tools() -> void: # Отдельной кнопки «Выбор» нет: выбор — это состояние по умолчанию, когда # ни один инструмент не нажат. allow_unpress даёт отжать активный # инструмент повторным кликом и вернуться к выбору. _tool_group.allow_unpress = true for kind: String in VaultGrid.KINDS: var button := Button.new() button.text = String(VaultGrid.KINDS[kind]["label"]) button.toggle_mode = true button.button_group = _tool_group button.add_theme_font_size_override("font_size", 18) button.toggled.connect(_on_kind_toggled.bind(kind)) _tools.add_child(button) var separator := Control.new() separator.custom_minimum_size.x = 20 _tools.add_child(separator) var demolish := Button.new() demolish.text = "Снести" demolish.toggle_mode = true demolish.button_group = _tool_group demolish.add_theme_font_size_override("font_size", 18) demolish.toggled.connect(_on_demolish_toggled) _tools.add_child(demolish) ## Возврат к выбору: ни один инструмент не нажат. func _reset_to_select() -> void: if _mode == Mode.SELECT: return _mode = Mode.SELECT _build_kind = "" var active := _tool_group.get_pressed_button() if active != null: active.set_pressed_no_signal(false) _status.text = HINT func _on_kind_toggled(is_on: bool, kind: String) -> void: if not is_on: # Инструмент отжали — если он и был активным, возвращаемся к выбору. if _mode == Mode.BUILD and _build_kind == kind: _reset_to_select() return _mode = Mode.BUILD _build_kind = kind _set_selection(Vector2i(-1, -1)) _status.text = "Стройка: %s · ЛКМ — поставить · ПКМ — отмена" % VaultGrid.KINDS[kind]["label"] func _on_demolish_toggled(is_on: bool) -> void: if not is_on: if _mode == Mode.DEMOLISH: _reset_to_select() return _mode = Mode.DEMOLISH _build_kind = "" _set_selection(Vector2i(-1, -1)) _status.text = "Снос: кликните по комнате · ПКМ — отмена" # --- Сохранение ------------------------------------------------------------- func save_data() -> Dictionary: return { "rooms": _grid.to_array(), "camera": { "x": _camera.position.x, "y": _camera.position.y, "zoom": _camera.zoom.x, }, } func load_data(data: Dictionary) -> void: _grid.from_array(data.get("rooms", [])) var camera_state: Dictionary = data.get("camera", {}) if not camera_state.is_empty(): _camera.position = Vector2( float(camera_state.get("x", 0.0)), float(camera_state.get("y", 0.0)) ) var level := float(camera_state.get("zoom", 1.0)) _camera.zoom = Vector2(level, level) _set_selection(Vector2i(-1, -1)) _vault.queue_redraw() _refresh_stats() _status.text = "Убежище загружено."