Enhance vault scene functionality and user interaction. Updated control instructions for room selection, implemented mode switching for building and demolishing, and refined input handling for mouse events. Added visual selection feedback for rooms and improved ghost placement logic. Updated HUD interactions to reflect new selection mechanics.

This commit is contained in:
Leonid Pershin
2026-08-11 09:19:45 +03:00
parent d9c8a1e0f6
commit ecef4a857c
2 changed files with 187 additions and 31 deletions
+146 -30
View File
@@ -4,8 +4,8 @@ extends Node2D
## Связывает три части: модель (VaultGrid), отрисовку (VaultView) и HUD. ## Связывает три части: модель (VaultGrid), отрисовку (VaultView) и HUD.
## Здесь же живёт контракт сохранения — сцена лежит в группе "persistent". ## Здесь же живёт контракт сохранения — сцена лежит в группе "persistent".
## ##
## Управление: ЛКМ — построить или снести, ПКМ/СКМ — тащить вид, ## Управление: ЛКМ действует по режиму из нижней панели (по умолчанию — выбор
## колесо — зум, WASD и стрелки — прокрутка. ## комнаты), ПКМ/СКМ — тащить вид, колесо — зум, WASD и стрелки — прокрутка.
## Строки сводки ресурсов: ключ из VaultGrid.totals() → подпись и цвет. ## Строки сводки ресурсов: ключ из VaultGrid.totals() → подпись и цвет.
const STAT_ROWS := { const STAT_ROWS := {
@@ -14,7 +14,15 @@ const STAT_ROWS := {
"food": {"label": "Еда", "color": "#f2704a"}, "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 _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _vault: VaultView = $View @onready var _vault: VaultView = $View
@@ -24,9 +32,15 @@ const HINT := "ЛКМ — строить · ПКМ — тащить вид · к
@onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status @onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status
var _grid := VaultGrid.new() var _grid := VaultGrid.new()
var _selected_kind := "living" var _mode := Mode.SELECT
var _demolish := false ## Тип комнаты, который построится в режиме Mode.BUILD.
var _build_kind := ""
## Якорная ячейка выбранной комнаты; (-1, -1) — ничего не выбрано.
var _selected_cell := Vector2i(-1, -1)
var _stat_labels := {} var _stat_labels := {}
var _tool_group := ButtonGroup.new()
var _right_pressed := false
var _right_press_position := Vector2.ZERO
func _ready() -> void: func _ready() -> void:
@@ -43,7 +57,34 @@ func _ready() -> void:
func _process(_delta: float) -> 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: func _unhandled_input(event: InputEvent) -> void:
@@ -52,7 +93,9 @@ func _unhandled_input(event: InputEvent) -> void:
var button := event as InputEventMouseButton var button := event as InputEventMouseButton
if button.button_index != MOUSE_BUTTON_LEFT or not button.pressed: if button.button_index != MOUSE_BUTTON_LEFT or not button.pressed:
return return
_apply_at(_cell_under_mouse()) # Берём точку из самого события, а не текущее положение курсора: клик
# должен срабатывать там, где кликнули.
_apply_at(_cell_at_screen(button.position))
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
@@ -62,26 +105,68 @@ func _cell_under_mouse() -> Vector2i:
return VaultGrid.cell_at(_vault.get_local_mouse_position()) 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): func _cell_at_screen(screen_position: Vector2) -> Vector2i:
_after_change("Комната снесена.") var local := _vault.get_global_transform_with_canvas().affine_inverse() * screen_position
else: return VaultGrid.cell_at(local)
_status.text = "Здесь нечего сносить."
return
if _grid.place(_selected_kind, cell.y, cell.x):
_after_change("Построено: %s" % VaultGrid.KINDS[_selected_kind]["label"]) func _apply_at(cell: Vector2i) -> void:
else: match _mode:
_status.text = "Здесь строить нельзя: занято или вплотную к шахте." 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: func _after_change(message: String) -> void:
# Индексы комнат после изменения съезжают — выделение сбрасываем.
_set_selection(Vector2i(-1, -1))
_vault.queue_redraw() _vault.queue_redraw()
_refresh_stats() _refresh_stats()
_status.text = message _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 -------------------------------------------------------------------- # --- HUD --------------------------------------------------------------------
func _build_stats() -> void: func _build_stats() -> void:
@@ -118,16 +203,18 @@ func _refresh_stats() -> void:
func _build_tools() -> void: func _build_tools() -> void:
var group := ButtonGroup.new() # Отдельной кнопки «Выбор» нет: выбор — это состояние по умолчанию, когда
# ни один инструмент не нажат. allow_unpress даёт отжать активный
# инструмент повторным кликом и вернуться к выбору.
_tool_group.allow_unpress = true
for kind: String in VaultGrid.KINDS: for kind: String in VaultGrid.KINDS:
var button := Button.new() var button := Button.new()
button.text = String(VaultGrid.KINDS[kind]["label"]) button.text = String(VaultGrid.KINDS[kind]["label"])
button.toggle_mode = true button.toggle_mode = true
button.button_group = group button.button_group = _tool_group
button.add_theme_font_size_override("font_size", 18) button.add_theme_font_size_override("font_size", 18)
button.button_pressed = kind == _selected_kind button.toggled.connect(_on_kind_toggled.bind(kind))
button.pressed.connect(_on_kind_selected.bind(kind))
_tools.add_child(button) _tools.add_child(button)
var separator := Control.new() var separator := Control.new()
@@ -137,21 +224,49 @@ func _build_tools() -> void:
var demolish := Button.new() var demolish := Button.new()
demolish.text = "Снести" demolish.text = "Снести"
demolish.toggle_mode = true demolish.toggle_mode = true
demolish.button_group = group demolish.button_group = _tool_group
demolish.add_theme_font_size_override("font_size", 18) 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) _tools.add_child(demolish)
func _on_kind_selected(kind: String) -> void: ## Возврат к выбору: ни один инструмент не нажат.
_selected_kind = kind func _reset_to_select() -> void:
_demolish = false 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 _status.text = HINT
func _on_demolish_selected() -> void: func _on_kind_toggled(is_on: bool, kind: String) -> void:
_demolish = true if not is_on:
_status.text = "Режим сноса: кликните по комнате." # Инструмент отжали — если он и был активным, возвращаемся к выбору.
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)) var level := float(camera_state.get("zoom", 1.0))
_camera.zoom = Vector2(level, level) _camera.zoom = Vector2(level, level)
_set_selection(Vector2i(-1, -1))
_vault.queue_redraw() _vault.queue_redraw()
_refresh_stats() _refresh_stats()
_status.text = "Убежище загружено." _status.text = "Убежище загружено."
+41 -1
View File
@@ -24,6 +24,7 @@ const EMPTY_FILL := Color("#ffffff", 0.02)
const EMPTY_LINE := Color("#ffffff", 0.06) const EMPTY_LINE := Color("#ffffff", 0.06)
const GHOST_OK := Color("#2ee6c5") const GHOST_OK := Color("#2ee6c5")
const GHOST_BAD := Color("#f2704a") const GHOST_BAD := Color("#f2704a")
const SELECTION := Color("#ffffff")
const MARGIN := 1200.0 const MARGIN := 1200.0
const SKY_HEIGHT := 900.0 const SKY_HEIGHT := 900.0
@@ -36,6 +37,8 @@ var grid: VaultGrid
var _ghost_kind := "" var _ghost_kind := ""
var _ghost_cell := Vector2i(-1, -1) var _ghost_cell := Vector2i(-1, -1)
var _demolish := false var _demolish := false
## Якорная ячейка выделенной комнаты; (-1, -1) — ничего не выделено.
var _selected_cell := Vector2i(-1, -1)
var _pebbles: Array[Dictionary] = [] var _pebbles: Array[Dictionary] = []
@@ -54,6 +57,15 @@ func set_ghost(kind: String, cell: Vector2i, demolish: bool) -> void:
queue_redraw() queue_redraw()
## Выделенная комната задаётся ячейкой, а не индексом: индексы съезжают
## при сносе, а ячейка остаётся собой.
func set_selection(cell: Vector2i) -> void:
if cell == _selected_cell:
return
_selected_cell = cell
queue_redraw()
func _draw() -> void: func _draw() -> void:
if grid == null: if grid == null:
return return
@@ -65,6 +77,7 @@ func _draw() -> void:
_draw_floor_numbers() _draw_floor_numbers()
for room: Dictionary in grid.rooms: for room: Dictionary in grid.rooms:
_draw_room(room) _draw_room(room)
_draw_selection()
_draw_ghost() _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) 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: func _draw_ghost() -> void:
if _ghost_cell.x < 0: if _ghost_cell.x < 0: