Files
g-world/scripts/game/vault_scene.gd
T

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