class_name VaultView extends Node2D ## Отрисовка убежища в разрезе: небо, порода, лифтовая шахта, комнаты ## и призрак строительства. ## ## Ассетов у проекта пока нет, поэтому всё рисуется процедурно в _draw(). ## Когда появится графика, заменять нужно только этот файл — модель ## (VaultGrid) и логика сцены от него не зависят. const DIRT_TOP := Color("#2a231d") const DIRT_BOTTOM := Color("#100d0b") const SURFACE := Color("#3d4f3a") const SHELL := Color("#1b262c") const WALL := Color("#2c3b45") const OUTLINE := Color("#080d11") const SHAFT := Color("#141c21") const RUNG := Color("#c9a227", 0.45) const CABIN := Color("#c9a227", 0.22) const HUT_WALL := Color("#26333b") const HUT_ROOF := Color("#33434d") const HUT_DOOR := Color("#0a1014") const HUT_LAMP := Color("#f2b134") 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 DIRT_DEPTH := 460.0 const PEBBLE_SEED := 20260810 ## Модель, которую рисуем. Ставится сценой сразу после создания. 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] = [] func _ready() -> void: _generate_pebbles() ## Подсветка под курсором: тип строящейся комнаты (пустая строка — ничего), ## ячейка и режим сноса. func set_ghost(kind: String, cell: Vector2i, demolish: bool) -> void: if kind == _ghost_kind and cell == _ghost_cell and demolish == _demolish: return _ghost_kind = kind _ghost_cell = cell _demolish = demolish 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 _draw_dirt() _draw_shell() _draw_empty_cells() _draw_shaft() _draw_entrance() _draw_floor_numbers() for room: Dictionary in grid.rooms: _draw_room(room) _draw_selection() _draw_ghost() # --- Фон -------------------------------------------------------------------- func _draw_dirt() -> void: var bounds := VaultGrid.bounds() var left := -MARGIN var right := bounds.size.x + MARGIN var depth := bounds.end.y + DIRT_DEPTH _fill_gradient(Rect2(Vector2(left, 0.0), Vector2(right - left, depth)), DIRT_TOP, DIRT_BOTTOM) for pebble: Dictionary in _pebbles: draw_circle(pebble["position"], float(pebble["radius"]), pebble["color"]) # Полоса поверхности — граница земли и неба. draw_line(Vector2(left, 0.0), Vector2(right, 0.0), SURFACE, 5.0) ## Заливка прямоугольника вертикальным градиентом: draw_rect умеет только ## сплошной цвет, поэтому идём через полигон с цветами вершин. func _fill_gradient(rect: Rect2, top: Color, bottom: Color) -> void: var points := PackedVector2Array([ rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y), ]) draw_polygon(points, PackedColorArray([top, top, bottom, bottom])) func _generate_pebbles() -> void: var rng := RandomNumberGenerator.new() rng.seed = PEBBLE_SEED var bounds := VaultGrid.bounds() for i in 260: _pebbles.append({ "position": Vector2( rng.randf_range(-MARGIN, bounds.size.x + MARGIN), rng.randf_range(6.0, bounds.end.y + DIRT_DEPTH) ), "radius": rng.randf_range(1.5, 5.0), "color": Color("#000000", rng.randf_range(0.10, 0.28)), }) # --- Убежище ---------------------------------------------------------------- func _draw_shell() -> void: draw_rect(VaultGrid.bounds().grow(10.0), SHELL) func _draw_empty_cells() -> void: for a_floor in VaultGrid.FLOORS: for column in range(VaultGrid.ELEVATOR_COLUMN + 1, VaultGrid.COLUMNS): if grid.room_index_at(a_floor, column) >= 0: continue var rect := VaultGrid.cell_rect(a_floor, column).grow(-3.0) draw_rect(rect, EMPTY_FILL) draw_rect(rect, EMPTY_LINE, false, 1.0) func _draw_shaft() -> void: var bounds := VaultGrid.bounds() var left := VaultGrid.ELEVATOR_COLUMN * VaultGrid.CELL.x # Шахта идёт от самой поверхности до низа: она связывает вход с этажами. var rect := Rect2(Vector2(left, 0.0), Vector2(VaultGrid.CELL.x, bounds.end.y)) # Ствол выше убежища обшиваем — иначе шахта висит в породе без стенок. draw_rect( Rect2(Vector2(left - 9.0, 0.0), Vector2(rect.size.x + 18.0, bounds.position.y)), SHELL ) draw_rect(rect, SHAFT) var y := 16.0 while y < rect.size.y: draw_line(Vector2(rect.position.x + 18.0, y), Vector2(rect.end.x - 18.0, y), RUNG, 2.0) y += 26.0 # Кабина стоит на верхнем этаже убежища, а не на уровне земли. var cabin := Rect2( Vector2(rect.position.x + 22.0, bounds.position.y + 14.0), Vector2(rect.size.x - 44.0, 92.0) ) draw_rect(cabin, CABIN) draw_rect(cabin, RUNG, false, 2.0) draw_rect(rect, OUTLINE, false, 2.0) ## Будка входа на поверхности: то, что видно снаружи над шахтой. func _draw_entrance() -> void: var center_x := VaultGrid.ELEVATOR_COLUMN * VaultGrid.CELL.x + VaultGrid.CELL.x * 0.5 var half := 96.0 var height := 132.0 var body := Rect2(Vector2(center_x - half, -height), Vector2(half * 2.0, height)) draw_rect(body, HUT_WALL) draw_rect(body, OUTLINE, false, 2.0) var roof := Rect2( Vector2(center_x - half - 16.0, -height - 18.0), Vector2((half + 16.0) * 2.0, 18.0) ) draw_rect(roof, HUT_ROOF) draw_rect(roof, OUTLINE, false, 2.0) # Проём над шахтой — вход и есть устье ствола. var door := Rect2(Vector2(center_x - 34.0, -height + 40.0), Vector2(68.0, height - 40.0)) draw_rect(door, HUT_DOOR) draw_rect(door, RUNG, false, 2.0) # Лампа над дверью со скромным ореолом. var lamp := Vector2(center_x, -height + 24.0) draw_circle(lamp, 16.0, Color(HUT_LAMP, 0.10)) draw_circle(lamp, 6.0, HUT_LAMP) # Мачта на крыше — силуэт, который читается издалека. var mast_x := center_x + half - 22.0 draw_line( Vector2(mast_x, -height - 18.0), Vector2(mast_x, -height - 86.0), HUT_ROOF, 4.0 ) draw_line( Vector2(mast_x - 14.0, -height - 62.0), Vector2(mast_x + 14.0, -height - 62.0), HUT_ROOF, 3.0 ) func _draw_floor_numbers() -> void: var font := ThemeDB.fallback_font for a_floor in VaultGrid.FLOORS: var row := VaultGrid.cell_rect(a_floor, 0) var pos := Vector2(-46.0, row.position.y + VaultGrid.CELL.y * 0.5 + 7.0) draw_string(font, pos, str(a_floor + 1), HORIZONTAL_ALIGNMENT_LEFT, -1, 20, Color("#ffffff", 0.28)) func _draw_room(room: Dictionary) -> void: var rect := VaultGrid.room_rect(room) var kind: Dictionary = VaultGrid.KINDS[String(room["kind"])] var accent := Color(String(kind["color"])) draw_rect(rect, WALL) var inner := rect.grow(-7.0) _fill_gradient(inner, accent.lerp(Color("#0b1116"), 0.58), accent.lerp(Color("#060a0d"), 0.86)) # Свет от потолочной лампы. var lamp := Vector2(inner.position.x + inner.size.x * 0.5, inner.position.y + 2.0) draw_polygon( PackedVector2Array([ lamp + Vector2(-16.0, 0.0), lamp + Vector2(16.0, 0.0), Vector2(inner.end.x - 10.0, inner.end.y), Vector2(inner.position.x + 10.0, inner.end.y), ]), PackedColorArray([ Color(accent, 0.13), Color(accent, 0.13), Color(accent, 0.0), Color(accent, 0.0), ]) ) _draw_equipment(inner, accent, VaultGrid.kind_width(String(room["kind"]))) # Пол. draw_rect( Rect2(Vector2(inner.position.x, inner.end.y - 7.0), Vector2(inner.size.x, 7.0)), accent.lerp(Color("#05080a"), 0.55) ) draw_rect(rect, OUTLINE, false, 2.0) draw_string(ThemeDB.fallback_font, rect.position + Vector2(14.0, 30.0), String(kind["label"]), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 20.0, 17, Color("#e6f1f7", 0.92)) ## Силуэты оборудования у пола — чтобы комнаты не выглядели пустыми коробками. func _draw_equipment(inner: Rect2, accent: Color, width: int) -> void: var body := accent.lerp(Color("#04070a"), 0.62) var glow := Color(accent, 0.55) var baseline := inner.end.y - 7.0 var count := 3 if width > 1 else 2 var step := inner.size.x / float(count + 1) for i in range(1, count + 1): var center_x := inner.position.x + step * i var height := 26.0 + float((i * 13) % 22) var machine := Rect2(Vector2(center_x - 17.0, baseline - height), Vector2(34.0, height)) draw_rect(machine, body) 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: return if _demolish: var index := grid.room_index_at(_ghost_cell.y, _ghost_cell.x) if index < 0: return var target := VaultGrid.room_rect(grid.rooms[index]) draw_rect(target, Color(GHOST_BAD, 0.22)) draw_rect(target, GHOST_BAD, false, 3.0) return if _ghost_kind.is_empty(): return var width := VaultGrid.kind_width(_ghost_kind) var rect := VaultGrid.cell_rect(_ghost_cell.y, _ghost_cell.x, width) var allowed := grid.can_place(_ghost_kind, _ghost_cell.y, _ghost_cell.x) var tint := GHOST_OK if allowed else GHOST_BAD draw_rect(rect, Color(tint, 0.16)) draw_rect(rect, tint, false, 3.0)