diff --git a/scripts/game/vault_scene.gd b/scripts/game/vault_scene.gd index 8f13304..1c1df4e 100644 --- a/scripts/game/vault_scene.gd +++ b/scripts/game/vault_scene.gd @@ -4,8 +4,8 @@ extends Node2D ## Связывает три части: модель (VaultGrid), отрисовку (VaultView) и HUD. ## Здесь же живёт контракт сохранения — сцена лежит в группе "persistent". ## -## Управление: ЛКМ — построить или снести, ПКМ/СКМ — тащить вид, -## колесо — зум, WASD и стрелки — прокрутка. +## Управление: ЛКМ действует по режиму из нижней панели (по умолчанию — выбор +## комнаты), ПКМ/СКМ — тащить вид, колесо — зум, WASD и стрелки — прокрутка. ## Строки сводки ресурсов: ключ из VaultGrid.totals() → подпись и цвет. const STAT_ROWS := { @@ -14,7 +14,15 @@ const STAT_ROWS := { "food": {"label": "Еда", "color": "#f2704a"}, } -const HINT := "ЛКМ — строить · ПКМ — тащить вид · колесо — зум" +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 @@ -24,9 +32,15 @@ const HINT := "ЛКМ — строить · ПКМ — тащить вид · к @onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status var _grid := VaultGrid.new() -var _selected_kind := "living" -var _demolish := false +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: @@ -43,7 +57,34 @@ func _ready() -> void: func _process(_delta: float) -> void: - _vault.set_ghost(_selected_kind, _cell_under_mouse(), _demolish) + # В режиме выбора призрака нет: пустой тип и (-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: @@ -52,7 +93,9 @@ func _unhandled_input(event: InputEvent) -> void: var button := event as InputEventMouseButton if button.button_index != MOUSE_BUTTON_LEFT or not button.pressed: return - _apply_at(_cell_under_mouse()) + # Берём точку из самого события, а не текущее положение курсора: клик + # должен срабатывать там, где кликнули. + _apply_at(_cell_at_screen(button.position)) get_viewport().set_input_as_handled() @@ -62,26 +105,68 @@ func _cell_under_mouse() -> Vector2i: return VaultGrid.cell_at(_vault.get_local_mouse_position()) -func _apply_at(cell: Vector2i) -> void: - if _demolish: - if _grid.remove_at(cell.y, cell.x): - _after_change("Комната снесена.") - else: - _status.text = "Здесь нечего сносить." - return +## Ячейка под точкой экрана. Учитывает камеру: переводим из координат +## окна в локальные координаты вида. +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) - if _grid.place(_selected_kind, cell.y, cell.x): - _after_change("Построено: %s" % VaultGrid.KINDS[_selected_kind]["label"]) - else: - _status.text = "Здесь строить нельзя: занято или вплотную к шахте." + +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: @@ -118,16 +203,18 @@ func _refresh_stats() -> void: func _build_tools() -> void: - var group := ButtonGroup.new() + # Отдельной кнопки «Выбор» нет: выбор — это состояние по умолчанию, когда + # ни один инструмент не нажат. 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 = group + button.button_group = _tool_group button.add_theme_font_size_override("font_size", 18) - button.button_pressed = kind == _selected_kind - button.pressed.connect(_on_kind_selected.bind(kind)) + button.toggled.connect(_on_kind_toggled.bind(kind)) _tools.add_child(button) var separator := Control.new() @@ -137,21 +224,49 @@ func _build_tools() -> void: var demolish := Button.new() demolish.text = "Снести" demolish.toggle_mode = true - demolish.button_group = group + demolish.button_group = _tool_group demolish.add_theme_font_size_override("font_size", 18) - demolish.pressed.connect(_on_demolish_selected) + demolish.toggled.connect(_on_demolish_toggled) _tools.add_child(demolish) -func _on_kind_selected(kind: String) -> void: - _selected_kind = kind - _demolish = false +## Возврат к выбору: ни один инструмент не нажат. +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_demolish_selected() -> void: - _demolish = true - _status.text = "Режим сноса: кликните по комнате." +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 = "Снос: кликните по комнате · ПКМ — отмена" # --- Сохранение ------------------------------------------------------------- @@ -178,6 +293,7 @@ func load_data(data: Dictionary) -> void: 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 = "Убежище загружено." diff --git a/scripts/game/vault_view.gd b/scripts/game/vault_view.gd index 7eaa4a8..2e29cd0 100644 --- a/scripts/game/vault_view.gd +++ b/scripts/game/vault_view.gd @@ -24,6 +24,7 @@ const EMPTY_FILL := Color("#ffffff", 0.02) const EMPTY_LINE := Color("#ffffff", 0.06) const GHOST_OK := Color("#2ee6c5") const GHOST_BAD := Color("#f2704a") +const SELECTION := Color("#ffffff") const MARGIN := 1200.0 const SKY_HEIGHT := 900.0 @@ -36,6 +37,8 @@ var grid: VaultGrid var _ghost_kind := "" var _ghost_cell := Vector2i(-1, -1) var _demolish := false +## Якорная ячейка выделенной комнаты; (-1, -1) — ничего не выделено. +var _selected_cell := Vector2i(-1, -1) var _pebbles: Array[Dictionary] = [] @@ -54,6 +57,15 @@ func set_ghost(kind: String, cell: Vector2i, demolish: bool) -> void: queue_redraw() +## Выделенная комната задаётся ячейкой, а не индексом: индексы съезжают +## при сносе, а ячейка остаётся собой. +func set_selection(cell: Vector2i) -> void: + if cell == _selected_cell: + return + _selected_cell = cell + queue_redraw() + + func _draw() -> void: if grid == null: return @@ -65,6 +77,7 @@ func _draw() -> void: _draw_floor_numbers() for room: Dictionary in grid.rooms: _draw_room(room) + _draw_selection() _draw_ghost() @@ -216,7 +229,34 @@ func _draw_equipment(inner: Rect2, accent: Color, width: int) -> void: draw_rect(Rect2(machine.position + Vector2(6.0, 6.0), Vector2(22.0, 4.0)), glow) -# --- Строительство ---------------------------------------------------------- +# --- Выделение и строительство ---------------------------------------------- + +## Рамка с уголками вокруг выбранной комнаты. Уголки нужны, чтобы выделение +## читалось поверх любой заливки — тонкой линии на пёстром фоне мало. +func _draw_selection() -> void: + if _selected_cell.x < 0: + return + var index := grid.room_index_at(_selected_cell.y, _selected_cell.x) + if index < 0: + return + + var rect := VaultGrid.room_rect(grid.rooms[index]).grow(-2.0) + draw_rect(rect, Color(SELECTION, 0.08)) + draw_rect(rect, Color(SELECTION, 0.75), false, 2.0) + + var arm := 20.0 + var corners := [ + [rect.position, Vector2(1.0, 1.0)], + [Vector2(rect.end.x, rect.position.y), Vector2(-1.0, 1.0)], + [rect.end, Vector2(-1.0, -1.0)], + [Vector2(rect.position.x, rect.end.y), Vector2(1.0, -1.0)], + ] + for entry: Array in corners: + var point: Vector2 = entry[0] + var direction: Vector2 = entry[1] + draw_line(point, point + Vector2(arm * direction.x, 0.0), SELECTION, 4.0) + draw_line(point, point + Vector2(0.0, arm * direction.y), SELECTION, 4.0) + func _draw_ghost() -> void: if _ghost_cell.x < 0: