300 lines
10 KiB
GDScript
300 lines
10 KiB
GDScript
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 = "Убежище загружено."
|