Implement weather system and developer console features. Added functionality for setting weather conditions, temperature, and wind speed through a new developer console. Enhanced the vault scene to display current weather, temperature, and wind information, and updated game clock to allow time manipulation. Improved cloud rendering and introduced a new climate management system for seasonal temperature adjustments.

This commit is contained in:
Leonid Pershin
2026-08-11 11:37:56 +03:00
parent a81747781d
commit 3b886ab3ab
14 changed files with 928 additions and 63 deletions
+230
View File
@@ -0,0 +1,230 @@
extends CanvasLayer
## Консоль разработчика: ввод команд поверх игры.
##
## Автозагрузка, поэтому доступна из любой сцены. Своих знаний об игре у неё
## нет — команды регистрируют сцены через register(). Так консоль не тянет за
## собой игровую логику, а команды живут рядом с тем, чем управляют.
##
## Открывается тильдой или F1. Слой обрабатывается всегда, поэтому работает
## и на паузе — иначе отлаживать остановленную игру было бы нечем.
const HISTORY_LIMIT := 40
## Файловый мост для внешних инструментов (MCP, скрипты, CI).
##
## Внедрённые события клавиатуры доносят до LineEdit код клавиши, но не символ,
## поэтому набрать команду извне нельзя. Мост обходит клавиатуру: во входной
## файл кладут строки, консоль их выполняет и удаляет файл, а весь вывод
## дублирует в лог. Только в отладочной сборке — в релиз это не попадёт.
const BRIDGE_IN := "user://console_in.txt"
const BRIDGE_LOG := "user://console_log.txt"
const BRIDGE_INTERVAL := 0.25
const COLOR_ECHO := "#5d7288"
const COLOR_INFO := "#dbe7f0"
const COLOR_OK := "#2ee6c5"
const COLOR_ERROR := "#f2704a"
@onready var _root: Control = $Root
@onready var _output: RichTextLabel = $Root/Panel/Body/Output
@onready var _entry: LineEdit = $Root/Panel/Body/InputRow/Input
## Имя команды -> {description, usage, handler}
var _commands := {}
var _history: Array[String] = []
var _history_index := 0
var _log: FileAccess
func _ready() -> void:
_root.visible = false
_entry.text_submitted.connect(_on_submitted)
_register_builtin()
_start_bridge()
print_line("Консоль разработчика. `help` — список команд.", COLOR_INFO)
func _input(event: InputEvent) -> void:
if not (event is InputEventKey and event.is_pressed() and not event.is_echo()):
return
var key := (event as InputEventKey).keycode
if key == KEY_QUOTELEFT or key == KEY_F1:
toggle()
get_viewport().set_input_as_handled()
return
if not _root.visible:
return
# История — стрелками, как в настоящей консоли.
match key:
KEY_UP:
_recall(-1)
get_viewport().set_input_as_handled()
KEY_DOWN:
_recall(1)
get_viewport().set_input_as_handled()
KEY_ESCAPE:
close()
get_viewport().set_input_as_handled()
# --- Открытие / закрытие ----------------------------------------------------
func toggle() -> void:
if _root.visible:
close()
else:
open()
func open() -> void:
_root.visible = true
_entry.clear()
_entry.grab_focus()
func close() -> void:
_root.visible = false
_entry.release_focus()
# --- Реестр команд ----------------------------------------------------------
## Сцены регистрируют свои команды здесь. `handler` принимает Array[String]
## аргументов и возвращает String — что напечатать.
func register(command: String, description: String, handler: Callable, usage := "") -> void:
_commands[command] = {
"description": description,
"usage": usage,
"handler": handler,
}
func unregister(command: String) -> void:
_commands.erase(command)
func print_line(text: String, color := COLOR_INFO) -> void:
_output.append_text("[color=%s]%s[/color]\n" % [color, text])
if _log != null:
# Сбрасываем сразу: внешний инструмент читает файл, пока игра жива.
_log.store_line(text)
_log.flush()
# --- Файловый мост ----------------------------------------------------------
func _start_bridge() -> void:
if not OS.is_debug_build():
return
# Лог перезаписывается на каждый запуск — так в нём только текущая сессия.
_log = FileAccess.open(BRIDGE_LOG, FileAccess.WRITE)
var timer := Timer.new()
timer.wait_time = BRIDGE_INTERVAL
timer.autostart = true
timer.timeout.connect(_poll_bridge)
add_child(timer)
func _poll_bridge() -> void:
if not FileAccess.file_exists(BRIDGE_IN):
return
var file := FileAccess.open(BRIDGE_IN, FileAccess.READ)
if file == null:
return
var payload := file.get_as_text()
file.close()
# Удаляем сразу, иначе команды выполнятся повторно на следующем тике.
DirAccess.remove_absolute(BRIDGE_IN)
for line: String in payload.split("\n", false):
_on_submitted(line)
# --- Выполнение -------------------------------------------------------------
func _on_submitted(line: String) -> void:
_entry.clear()
var trimmed := line.strip_edges()
if trimmed.is_empty():
return
_remember(trimmed)
print_line("> " + trimmed, COLOR_ECHO)
var parts := trimmed.split(" ", false)
var command := String(parts[0]).to_lower()
var arguments: Array[String] = []
for index in range(1, parts.size()):
arguments.append(String(parts[index]))
_execute(command, arguments)
func _execute(command: String, arguments: Array[String]) -> void:
if not _commands.has(command):
print_line("Неизвестная команда: %s. `help` — список." % command, COLOR_ERROR)
return
var entry: Dictionary = _commands[command]
var handler: Callable = entry["handler"]
# Сцена могла смениться — её команды умирают вместе с ней.
if not handler.is_valid():
_commands.erase(command)
print_line("Команда %s больше недоступна в этой сцене." % command, COLOR_ERROR)
return
var result: Variant = handler.call(arguments)
if result is String and not String(result).is_empty():
print_line(String(result), COLOR_OK)
# --- История ----------------------------------------------------------------
func _remember(line: String) -> void:
_history.append(line)
if _history.size() > HISTORY_LIMIT:
_history.remove_at(0)
_history_index = _history.size()
func _recall(step: int) -> void:
if _history.is_empty():
return
_history_index = clampi(_history_index + step, 0, _history.size())
if _history_index == _history.size():
_entry.clear()
return
_entry.text = _history[_history_index]
_entry.caret_column = _entry.text.length()
# --- Встроенные команды -----------------------------------------------------
func _register_builtin() -> void:
register("help", "список команд", _cmd_help)
register("cls", "очистить вывод", _cmd_clear)
func _cmd_help(_arguments: Array[String]) -> String:
var names := _commands.keys()
names.sort()
var lines := PackedStringArray()
for name: String in names:
var entry: Dictionary = _commands[name]
var usage := String(entry["usage"])
var head := name if usage.is_empty() else "%s %s" % [name, usage]
lines.append(" %-24s %s" % [head, String(entry["description"])])
return "\n".join(lines)
func _cmd_clear(_arguments: Array[String]) -> String:
_output.clear()
return ""
+1
View File
@@ -0,0 +1 @@
uid://bp5qxf7p5q3ty
+120
View File
@@ -0,0 +1,120 @@
class_name WeatherIcon
extends Control
## Значок погоды для HUD.
##
## Рисуется процедурно: иконок в наборе фона нет, а тащить шрифт с символами
## ради семи состояний неоправданно. Заодно значок красится темой, а не
## растром, и одинаково чёткий на любом масштабе.
const SUN := Color("#f2b134")
const MOON := Color("#cfd8e6")
const CLOUD := Color("#c6d4e0")
const CLOUD_DARK := Color("#8fa5ba")
const RAIN := Color("#4aa8f2")
const SNOW := Color("#e6f1f7")
const BOLT := Color("#f2b134")
## Тип погоды из WeatherSystem.Kind.
var kind := 0
## Ночью в ясную погоду вместо солнца показываем луну.
var night := false
func _ready() -> void:
custom_minimum_size = Vector2(24.0, 24.0)
func set_state(weather_kind: int, is_night: bool) -> void:
if weather_kind == kind and is_night == night:
return
kind = weather_kind
night = is_night
queue_redraw()
func _draw() -> void:
var box := Rect2(Vector2.ZERO, size)
var center := box.get_center()
match kind:
WeatherSystem.Kind.CLEAR:
_draw_body(center, size.x * 0.30)
WeatherSystem.Kind.CLOUDY:
_draw_body(center - Vector2(size.x * 0.14, size.y * 0.14), size.x * 0.22)
_draw_cloud(center + Vector2(size.x * 0.06, size.y * 0.10), CLOUD)
WeatherSystem.Kind.OVERCAST:
_draw_cloud(center - Vector2(size.x * 0.10, size.y * 0.06), CLOUD_DARK)
_draw_cloud(center + Vector2(size.x * 0.08, size.y * 0.08), CLOUD)
WeatherSystem.Kind.RAIN:
_draw_cloud(center - Vector2(0.0, size.y * 0.14), CLOUD)
_draw_streaks(center, RAIN)
WeatherSystem.Kind.STORM:
_draw_cloud(center - Vector2(0.0, size.y * 0.14), CLOUD_DARK)
_draw_streaks(center, RAIN)
_draw_bolt(center)
WeatherSystem.Kind.FOG:
_draw_fog(box)
WeatherSystem.Kind.SNOW:
_draw_cloud(center - Vector2(0.0, size.y * 0.14), CLOUD)
_draw_flakes(center)
## Солнце или луна — смотря какое время суток.
func _draw_body(center: Vector2, radius: float) -> void:
if night:
# Полумесяц: круг с вырезом, вырез закрашен фоном значка.
draw_circle(center, radius, MOON)
draw_circle(center + Vector2(radius * 0.55, -radius * 0.30), radius * 0.92, Color(0, 0, 0, 0))
draw_arc(center, radius, PI * 0.35, PI * 1.65, 20, MOON, 2.0)
return
draw_circle(center, radius, SUN)
for index in 8:
var angle := TAU * float(index) / 8.0
var direction := Vector2(cos(angle), sin(angle))
draw_line(center + direction * (radius * 1.35), center + direction * (radius * 1.85), SUN, 1.6)
func _draw_cloud(center: Vector2, tint: Color) -> void:
var unit := size.x * 0.12
draw_circle(center + Vector2(-unit * 1.1, 0.0), unit * 1.05, tint)
draw_circle(center + Vector2(unit * 1.1, unit * 0.15), unit * 0.9, tint)
draw_circle(center + Vector2(0.0, -unit * 0.55), unit * 1.25, tint)
draw_rect(Rect2(center + Vector2(-unit * 1.9, -unit * 0.1), Vector2(unit * 3.8, unit * 1.1)), tint)
func _draw_streaks(center: Vector2, tint: Color) -> void:
var unit := size.x * 0.12
for index in 3:
var start := center + Vector2((float(index) - 1.0) * unit * 1.4, unit * 1.3)
draw_line(start, start + Vector2(-unit * 0.35, unit * 1.3), tint, 1.8)
func _draw_flakes(center: Vector2) -> void:
var unit := size.x * 0.12
for index in 3:
draw_circle(center + Vector2((float(index) - 1.0) * unit * 1.4, unit * 1.9), unit * 0.32, SNOW)
func _draw_bolt(center: Vector2) -> void:
var unit := size.x * 0.12
draw_polygon(
PackedVector2Array([
center + Vector2(unit * 0.2, unit * 1.1),
center + Vector2(unit * 1.2, unit * 1.1),
center + Vector2(unit * 0.4, unit * 2.6),
center + Vector2(unit * 0.7, unit * 1.7),
center + Vector2(-unit * 0.2, unit * 1.7),
]),
PackedColorArray([BOLT, BOLT, BOLT, BOLT, BOLT])
)
func _draw_fog(box: Rect2) -> void:
for index in 3:
var y := box.position.y + box.size.y * (0.32 + float(index) * 0.18)
var inset := box.size.x * (0.08 + float(index % 2) * 0.12)
draw_line(
Vector2(box.position.x + inset, y), Vector2(box.end.x - inset, y),
CLOUD_DARK, 2.0
)
+1
View File
@@ -0,0 +1 @@
uid://dpifh4u0gmgxw