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:
@@ -67,7 +67,10 @@
|
|||||||
- `game_clock.gd` — игровые дата и время, скорость, своя пауза. Такт наружу — сигнал
|
- `game_clock.gd` — игровые дата и время, скорость, своя пауза. Такт наружу — сигнал
|
||||||
`minute_passed`; темп задаётся `MINUTES_PER_SECOND`.
|
`minute_passed`; темп задаётся `MINUTES_PER_SECOND`.
|
||||||
- `weather_system.gd` — тип погоды, матрица переходов и плавная смена параметров.
|
- `weather_system.gd` — тип погоды, матрица переходов и плавная смена параметров.
|
||||||
Такт берёт у часов, поэтому стоит вместе с паузой.
|
Такт берёт у часов, поэтому стоит вместе с паузой. Форма осадков решается
|
||||||
|
температурой, а не месяцем: у нуля дождь становится снегом.
|
||||||
|
- `climate.gd` — сезонная норма температуры и ветра (средняя полоса Европы).
|
||||||
|
Перечисление погоды сюда не тянем — вышла бы циклическая ссылка.
|
||||||
- `sky_cycle.gd` — палитра неба по часу суток, положение солнца и луны. Чистые вычисления.
|
- `sky_cycle.gd` — палитра неба по часу суток, положение солнца и луны. Чистые вычисления.
|
||||||
|
|
||||||
Отрисовка — только `_draw()`, читают модель и ничего не решают:
|
Отрисовка — только `_draw()`, читают модель и ничего не решают:
|
||||||
@@ -84,14 +87,58 @@
|
|||||||
|
|
||||||
### Текстуры фона
|
### Текстуры фона
|
||||||
|
|
||||||
|
**Ловушка с плиткой.** `draw_texture_rect(tex, rect, tile = true)` повторяет
|
||||||
|
текстуру в её натуральную величину и **по обеим осям**. Если растянуть
|
||||||
|
прямоугольник ради масштаба, текстура просто уложится в него несколько раз, и
|
||||||
|
на стыке копий пойдёт горизонтальная полоса через весь экран. Плюс при
|
||||||
|
`texture_repeat = ENABLED` фильтрация на краю подмешивает противоположный край
|
||||||
|
текстуры — ещё одна тонкая линия.
|
||||||
|
|
||||||
|
Поэтому полосы пейзажа кладутся плитками вручную. При этом важен ещё один
|
||||||
|
момент: крайние столбцы пикселей у этих текстур полупрозрачные от сглаживания,
|
||||||
|
и встык они складываются дважды, давая **вертикальную** линию. Отсюда
|
||||||
|
`EDGE_INSET` — плитки рисуются через `draw_texture_rect_region()` с отступом
|
||||||
|
в пиксель с каждой стороны, плюс пиксель нахлёста. Повтор на узле выключен.
|
||||||
|
|
||||||
`background/` — набор Kenney Background Elements. Два стиля, и они не взаимозаменяемы:
|
`background/` — набор Kenney Background Elements. Два стиля, и они не взаимозаменяемы:
|
||||||
`PNG/Flat/*` — бледные, почти белые силуэты, их место на дальних планах, где цвет всё равно
|
`PNG/Flat/*` — бледные, почти белые силуэты, их место на дальних планах, где цвет всё равно
|
||||||
задаётся тонировкой; `PNG/*` — цветные спрайты для ближнего плана. Перепутать легко: деревья
|
задаётся тонировкой; `PNG/*` — цветные спрайты для ближнего плана. Перепутать легко: деревья
|
||||||
из Flat на переднем плане выглядят выцветшими.
|
из Flat на переднем плане выглядят выцветшими.
|
||||||
|
|
||||||
|
## Консоль разработчика
|
||||||
|
|
||||||
|
Открывается тильдой или F1 в любой сцене, работает и на паузе. Сама об игре
|
||||||
|
ничего не знает: команды регистрируют сцены через `DevConsole.register(...)`,
|
||||||
|
и они умирают вместе со сценой — консоль выбрасывает команду, когда её
|
||||||
|
`Callable` протухает.
|
||||||
|
|
||||||
|
Команды игровой сцены живут в `vault_scene.gd`, в разделе «Команды консоли»:
|
||||||
|
`stats`, `time`, `date`, `speed`, `weather`, `temp`, `wind`, `build`, `wipe`.
|
||||||
|
У погоды есть латинские ключи (`snow`, `rain`, …) — набирать русские названия
|
||||||
|
с латинской раскладки посреди отладки неудобно.
|
||||||
|
|
||||||
|
### Файловый мост — как выполнять команды из MCP
|
||||||
|
|
||||||
|
Набрать команду через `game_manage(op="input_key")` нельзя: внедрённые события
|
||||||
|
доносят до `LineEdit` код клавиши, но не символ. Enter при этом проходит — то
|
||||||
|
есть дело не в фокусе. Поэтому в консоли есть мост через файлы в `user://`
|
||||||
|
(только `OS.is_debug_build()`, в релиз не попадает):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DIR="$APPDATA/Godot/app_userdata/GWorld"
|
||||||
|
printf 'weather storm\ntemp -3\nstats\n' > "$DIR/console_in.txt"
|
||||||
|
cat "$DIR/console_log.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
Консоль опрашивает `console_in.txt` четыре раза в секунду, выполняет строки и
|
||||||
|
удаляет файл. Весь вывод дублируется в `console_log.txt`, который
|
||||||
|
перезаписывается на каждый запуск игры. Работает и с закрытой консолью, и на
|
||||||
|
паузе.
|
||||||
|
|
||||||
## Автозагрузки
|
## Автозагрузки
|
||||||
|
|
||||||
`GameSettings` (`GameSettingsService`) и `SaveManager` (`SaveManagerService`).
|
`GameSettings` (`GameSettingsService`), `SaveManager` (`SaveManagerService`)
|
||||||
|
и `DevConsole` (сцена `scenes/ui/dev_console.tscn`).
|
||||||
У обеих есть `class_name`: константы, перечисления и типы берём через него, экземпляр —
|
У обеих есть `class_name`: константы, перечисления и типы берём через него, экземпляр —
|
||||||
через `get_node("/root/...")`. Имя автозагрузки редактор не резолвит без перезапуска проекта.
|
через `get_node("/root/...")`. Имя автозагрузки редактор не резолвит без перезапуска проекта.
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ config/icon="res://icon.svg"
|
|||||||
_mcp_game_helper="*res://addons/godot_ai/runtime/game_helper.gd"
|
_mcp_game_helper="*res://addons/godot_ai/runtime/game_helper.gd"
|
||||||
GameSettings="*res://scripts/autoload/game_settings.gd"
|
GameSettings="*res://scripts/autoload/game_settings.gd"
|
||||||
SaveManager="*res://scripts/autoload/save_manager.gd"
|
SaveManager="*res://scripts/autoload/save_manager.gd"
|
||||||
|
DevConsole="*res://scenes/ui/dev_console.tscn"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|||||||
+33
-7
@@ -8,6 +8,7 @@
|
|||||||
[ext_resource type="Theme" uid="uid://c6kkopjj3snag" path="res://themes/main_menu.theme.tres" id="4_sch52"]
|
[ext_resource type="Theme" uid="uid://c6kkopjj3snag" path="res://themes/main_menu.theme.tres" id="4_sch52"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cvx4ot7ugyprk" path="res://scenes/ui/pause_menu.tscn" id="5_lffpp"]
|
[ext_resource type="PackedScene" uid="uid://cvx4ot7ugyprk" path="res://scenes/ui/pause_menu.tscn" id="5_lffpp"]
|
||||||
[ext_resource type="Script" uid="uid://7wstqw8ve2he" path="res://scripts/game/game_clock.gd" id="6_mkhle"]
|
[ext_resource type="Script" uid="uid://7wstqw8ve2he" path="res://scripts/game/game_clock.gd" id="6_mkhle"]
|
||||||
|
[ext_resource type="Script" uid="uid://dpifh4u0gmgxw" path="res://scripts/ui/weather_icon.gd" id="7_lvdr8"]
|
||||||
[ext_resource type="Script" uid="uid://by1cwvs3fc3pa" path="res://scripts/game/weather_system.gd" id="9_m0p8k"]
|
[ext_resource type="Script" uid="uid://by1cwvs3fc3pa" path="res://scripts/game/weather_system.gd" id="9_m0p8k"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lvdr8"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lvdr8"]
|
||||||
@@ -154,18 +155,43 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_mkhle")
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 6
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
|
[node name="Climate" type="HBoxContainer" parent="Hud/Root/TopRight/Row/ClockBar/Body" unique_id=1529257714]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
alignment = 2
|
||||||
|
|
||||||
|
[node name="Icon" type="Control" parent="Hud/Root/TopRight/Row/ClockBar/Body/Climate" unique_id=788552715]
|
||||||
|
custom_minimum_size = Vector2(22, 22)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
script = ExtResource("7_lvdr8")
|
||||||
|
|
||||||
|
[node name="WeatherLabel" type="Label" parent="Hud/Root/TopRight/Row/ClockBar/Body/Climate" unique_id=214206235]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(0.7764706, 0.83137256, 0.8784314, 1)
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "Ясно"
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="TempLabel" type="Label" parent="Hud/Root/TopRight/Row/ClockBar/Body/Climate" unique_id=663157161]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(0.9490196, 0.69411767, 0.20392157, 1)
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "8°C"
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="WindLabel" type="Label" parent="Hud/Root/TopRight/Row/ClockBar/Body/Climate" unique_id=298035379]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(0.48235294, 0.56078434, 0.6392157, 1)
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "2 м/с"
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
[node name="Moment" type="HBoxContainer" parent="Hud/Root/TopRight/Row/ClockBar/Body" unique_id=195744038]
|
[node name="Moment" type="HBoxContainer" parent="Hud/Root/TopRight/Row/ClockBar/Body" unique_id=195744038]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 10
|
theme_override_constants/separation = 10
|
||||||
alignment = 2
|
alignment = 2
|
||||||
|
|
||||||
[node name="WeatherLabel" type="Label" parent="Hud/Root/TopRight/Row/ClockBar/Body/Moment" unique_id=895640468]
|
|
||||||
layout_mode = 2
|
|
||||||
theme_override_colors/font_color = Color(0.48235294, 0.56078434, 0.6392157, 1)
|
|
||||||
theme_override_font_sizes/font_size = 14
|
|
||||||
text = "Ясно"
|
|
||||||
vertical_alignment = 1
|
|
||||||
|
|
||||||
[node name="DateLabel" type="Label" parent="Hud/Root/TopRight/Row/ClockBar/Body/Moment" unique_id=1045252723]
|
[node name="DateLabel" type="Label" parent="Hud/Root/TopRight/Row/ClockBar/Body/Moment" unique_id=1045252723]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.56078434, 0.64705884, 0.7294118, 1)
|
theme_override_colors/font_color = Color(0.56078434, 0.64705884, 0.7294118, 1)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
[gd_scene format=3 uid="uid://wddada4uxq4s"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bp5qxf7p5q3ty" path="res://scripts/ui/dev_console.gd" id="1_0be3j"]
|
||||||
|
[ext_resource type="Theme" uid="uid://c6kkopjj3snag" path="res://themes/main_menu.theme.tres" id="2_4dryq"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fk3y3"]
|
||||||
|
content_margin_left = 20.0
|
||||||
|
content_margin_top = 16.0
|
||||||
|
content_margin_right = 20.0
|
||||||
|
content_margin_bottom = 16.0
|
||||||
|
bg_color = Color(0.019607844, 0.039215688, 0.078431375, 0.9490196)
|
||||||
|
border_width_bottom = 2
|
||||||
|
border_color = Color(0.18039216, 0.9019608, 0.77254903, 0.6)
|
||||||
|
|
||||||
|
[node name="DevConsole" type="CanvasLayer" unique_id=1036744236]
|
||||||
|
process_mode = 3
|
||||||
|
layer = 200
|
||||||
|
script = ExtResource("1_0be3j")
|
||||||
|
|
||||||
|
[node name="Root" type="Control" parent="." unique_id=1883814073]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
mouse_filter = 2
|
||||||
|
theme = ExtResource("2_4dryq")
|
||||||
|
|
||||||
|
[node name="Panel" type="PanelContainer" parent="Root" unique_id=323575355]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.46
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_fk3y3")
|
||||||
|
|
||||||
|
[node name="Body" type="VBoxContainer" parent="Root/Panel" unique_id=1904841198]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Output" type="RichTextLabel" parent="Root/Panel/Body" unique_id=1032431750]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_font_sizes/normal_font_size = 15
|
||||||
|
bbcode_enabled = true
|
||||||
|
scroll_following = true
|
||||||
|
|
||||||
|
[node name="InputRow" type="HBoxContainer" parent="Root/Panel/Body" unique_id=1053997656]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Prompt" type="Label" parent="Root/Panel/Body/InputRow" unique_id=1647652394]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(0.18039216, 0.9019608, 0.77254903, 1)
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
text = ">"
|
||||||
|
|
||||||
|
[node name="Input" type="LineEdit" parent="Root/Panel/Body/InputRow" unique_id=653784574]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
placeholder_text = "команда"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
class_name Climate
|
||||||
|
extends RefCounted
|
||||||
|
## Климатическая норма: какой температуры и ветра ждать в это время года.
|
||||||
|
##
|
||||||
|
## Средняя полоса Европы — ориентир, а не точная модель. Только вычисления:
|
||||||
|
## погода (weather_system.gd) берёт отсюда цель и плавно к ней идёт.
|
||||||
|
|
||||||
|
## Среднемесячная температура, °C, с января по декабрь.
|
||||||
|
const MONTHLY_MEAN: Array[float] = [
|
||||||
|
0.5, 1.5, 5.0, 9.5, 14.0, 17.0, 19.0, 18.5, 14.5, 9.5, 4.5, 1.5,
|
||||||
|
]
|
||||||
|
|
||||||
|
## Суточный размах по месяцам: зимой день и ночь ближе друг к другу, чем летом.
|
||||||
|
const MONTHLY_SWING: Array[float] = [
|
||||||
|
4.0, 5.0, 7.0, 9.0, 10.0, 11.0, 11.0, 10.5, 9.0, 7.0, 4.5, 3.5,
|
||||||
|
]
|
||||||
|
|
||||||
|
## Самый тёплый час суток; самый холодный — через двенадцать часов от него.
|
||||||
|
const WARMEST_HOUR := 15.0
|
||||||
|
|
||||||
|
## Поправка к температуре по типу погоды, °C. Индекс — WeatherSystem.Kind;
|
||||||
|
## перечисление сюда не тянем, иначе получится циклическая ссылка между
|
||||||
|
## классами: погода уже зависит от климата.
|
||||||
|
const WEATHER_OFFSET: Array[float] = [1.5, 0.0, -1.0, -2.0, -3.0, -1.5, -3.0]
|
||||||
|
|
||||||
|
## Характерная скорость ветра по тем же типам, м/с.
|
||||||
|
const WEATHER_WIND: Array[float] = [2.5, 4.0, 5.5, 7.5, 15.0, 1.0, 5.0]
|
||||||
|
|
||||||
|
## Выше этого осадки идут дождём, ниже — снегом.
|
||||||
|
const SNOW_THRESHOLD := 0.5
|
||||||
|
|
||||||
|
## Ветер сильнее этого считаем предельным для визуализации.
|
||||||
|
const WIND_REFERENCE := 18.0
|
||||||
|
|
||||||
|
|
||||||
|
## Норма для месяца (1..12) и часа суток, без учёта погоды.
|
||||||
|
static func base_temperature(month: int, hour: float) -> float:
|
||||||
|
var index := clampi(month - 1, 0, 11)
|
||||||
|
var mean := MONTHLY_MEAN[index]
|
||||||
|
var swing := MONTHLY_SWING[index]
|
||||||
|
# Суточный ход — косинус с максимумом в WARMEST_HOUR.
|
||||||
|
var phase := (hour - WARMEST_HOUR) / 24.0 * TAU
|
||||||
|
return mean + swing * 0.5 * cos(phase)
|
||||||
|
|
||||||
|
|
||||||
|
static func weather_offset(kind: int) -> float:
|
||||||
|
if kind < 0 or kind >= WEATHER_OFFSET.size():
|
||||||
|
return 0.0
|
||||||
|
return WEATHER_OFFSET[kind]
|
||||||
|
|
||||||
|
|
||||||
|
static func base_wind(kind: int) -> float:
|
||||||
|
if kind < 0 or kind >= WEATHER_WIND.size():
|
||||||
|
return 3.0
|
||||||
|
return WEATHER_WIND[kind]
|
||||||
|
|
||||||
|
|
||||||
|
## «12°C» — для HUD.
|
||||||
|
static func temperature_text(celsius: float) -> String:
|
||||||
|
return "%d°C" % roundi(celsius)
|
||||||
|
|
||||||
|
|
||||||
|
## «4 м/с» — для HUD.
|
||||||
|
static func wind_text(speed: float) -> String:
|
||||||
|
return "%d м/с" % roundi(speed)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://lfpudig0j8s7
|
||||||
@@ -102,6 +102,20 @@ func toggle() -> void:
|
|||||||
flow_changed.emit()
|
flow_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Перевести часы на указанный момент (словарь как у Time). Возвращает false,
|
||||||
|
## если момент раньше начала партии — назад отматывать нечего.
|
||||||
|
func set_moment(moment: Dictionary) -> bool:
|
||||||
|
var target := int(Time.get_unix_time_from_datetime_dict(moment))
|
||||||
|
if target < _start_unix:
|
||||||
|
return false
|
||||||
|
|
||||||
|
_elapsed_minutes = float(target - _start_unix) / 60.0
|
||||||
|
_last_whole_minute = int(_elapsed_minutes)
|
||||||
|
_last_day = _day_index()
|
||||||
|
minute_passed.emit(_last_whole_minute)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
# --- Сохранение -------------------------------------------------------------
|
# --- Сохранение -------------------------------------------------------------
|
||||||
|
|
||||||
func save_data() -> Dictionary:
|
func save_data() -> Dictionary:
|
||||||
|
|||||||
@@ -12,8 +12,17 @@ extends Node2D
|
|||||||
const MOUNTAINS := preload("res://background/PNG/Flat/pointy_mountains.png")
|
const MOUNTAINS := preload("res://background/PNG/Flat/pointy_mountains.png")
|
||||||
const HILLS_FAR := preload("res://background/PNG/Flat/hills2.png")
|
const HILLS_FAR := preload("res://background/PNG/Flat/hills2.png")
|
||||||
const HILLS_NEAR := preload("res://background/PNG/Flat/hills1.png")
|
const HILLS_NEAR := preload("res://background/PNG/Flat/hills1.png")
|
||||||
const CLOUDS_HIGH := preload("res://background/PNG/Flat/clouds1.png")
|
## Облака — отдельными клубами, а не сплошной лентой: у ленты прямоугольник
|
||||||
const CLOUDS_LOW := preload("res://background/PNG/Flat/clouds2.png")
|
## обрезает текстуру, и на небе видно горизонтальный шов, который вдобавок
|
||||||
|
## едет вместе с ветром.
|
||||||
|
const CLOUD_PATHS := [
|
||||||
|
"res://background/PNG/Flat/cloud1.png", "res://background/PNG/Flat/cloud2.png",
|
||||||
|
"res://background/PNG/Flat/cloud3.png", "res://background/PNG/Flat/cloud4.png",
|
||||||
|
"res://background/PNG/Flat/cloud5.png", "res://background/PNG/Flat/cloud6.png",
|
||||||
|
"res://background/PNG/Flat/cloud7.png", "res://background/PNG/Flat/cloud8.png",
|
||||||
|
"res://background/PNG/Flat/cloud9.png",
|
||||||
|
]
|
||||||
|
const CLOUD_COUNT := 30
|
||||||
|
|
||||||
## Ближний план берём из основного набора: там деревья и трава уже цветные.
|
## Ближний план берём из основного набора: там деревья и трава уже цветные.
|
||||||
## Набор Flat — бледные силуэты, он идёт на дальние слои, которые всё равно
|
## Набор Flat — бледные силуэты, он идёт на дальние слои, которые всё равно
|
||||||
@@ -36,6 +45,8 @@ const HILLS_NEAR_TONE := Color("#5d8248")
|
|||||||
|
|
||||||
const LEFT := -1600.0
|
const LEFT := -1600.0
|
||||||
const RIGHT := 2900.0
|
const RIGHT := 2900.0
|
||||||
|
## Сколько крайних столбцов текстуры отбрасываем на стыке плиток.
|
||||||
|
const EDGE_INSET := 1.0
|
||||||
const SCENERY_SEED := 20120401
|
const SCENERY_SEED := 20120401
|
||||||
|
|
||||||
## Полоса дёрна на уровне земли.
|
## Полоса дёрна на уровне земли.
|
||||||
@@ -57,15 +68,19 @@ var time_scale := 1.0
|
|||||||
|
|
||||||
var _trees: Array[Dictionary] = []
|
var _trees: Array[Dictionary] = []
|
||||||
var _grass: Array[Dictionary] = []
|
var _grass: Array[Dictionary] = []
|
||||||
|
var _cloud_puffs: Array[Dictionary] = []
|
||||||
var _drops: Array[Vector2] = []
|
var _drops: Array[Vector2] = []
|
||||||
var _drift := 0.0
|
var _drift := 0.0
|
||||||
var _fall := 0.0
|
var _fall := 0.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# Плиточная заливка широких полос требует повтора текстуры на узле.
|
# Повтор выключен намеренно: полосы мы кладём плитками сами, а с включённым
|
||||||
texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
|
# повтором фильтрация на краю кадра подмешивает противоположный край
|
||||||
|
# текстуры — по небу шла тонкая линия во всю ширину.
|
||||||
|
texture_repeat = CanvasItem.TEXTURE_REPEAT_DISABLED
|
||||||
_scatter_scenery()
|
_scatter_scenery()
|
||||||
|
_scatter_clouds()
|
||||||
_scatter_drops()
|
_scatter_drops()
|
||||||
|
|
||||||
|
|
||||||
@@ -116,10 +131,28 @@ func _draw_band(
|
|||||||
texture: Texture2D, tone: Color, scale: float, distance: float, parallax: float,
|
texture: Texture2D, tone: Color, scale: float, distance: float, parallax: float,
|
||||||
light: Color, haze: Color, view: Rect2
|
light: Color, haze: Color, view: Rect2
|
||||||
) -> void:
|
) -> void:
|
||||||
var size := Vector2(texture.get_size()) * scale
|
var tint := (tone * light).lerp(haze, distance)
|
||||||
var offset := view.get_center().x * (1.0 - parallax)
|
var offset := view.get_center().x * (1.0 - parallax)
|
||||||
var rect := Rect2(Vector2(LEFT + offset, -size.y), Vector2(RIGHT - LEFT, size.y))
|
var texture_size := Vector2(texture.get_size())
|
||||||
draw_texture_rect(texture, rect, true, (tone * light).lerp(haze, distance))
|
|
||||||
|
# Плитку кладём вручную: draw_texture_rect(tile = true) повторяет текстуру
|
||||||
|
# в натуральную величину и по обеим осям сразу, из-за чего масштаб
|
||||||
|
# превращался в лишние ряды с полосой на стыке.
|
||||||
|
#
|
||||||
|
# Крайние столбцы пикселей у этих текстур полупрозрачные (сглаживание).
|
||||||
|
# Встык они складываются дважды и дают вертикальную линию, поэтому берём
|
||||||
|
# область с отступом в пиксель с каждой стороны и ещё на пиксель
|
||||||
|
# перекрываем соседние плитки.
|
||||||
|
var source := Rect2(
|
||||||
|
Vector2(EDGE_INSET, 0.0), Vector2(texture_size.x - EDGE_INSET * 2.0, texture_size.y)
|
||||||
|
)
|
||||||
|
var tile := Vector2(source.size.x * scale, texture_size.y * scale)
|
||||||
|
var step := tile.x - 1.0
|
||||||
|
|
||||||
|
var x := LEFT + fposmod(offset, step) - step
|
||||||
|
while x < RIGHT:
|
||||||
|
draw_texture_rect_region(texture, Rect2(Vector2(x, -tile.y), tile), source, tint)
|
||||||
|
x += step
|
||||||
|
|
||||||
|
|
||||||
func _draw_turf(light: Color) -> void:
|
func _draw_turf(light: Color) -> void:
|
||||||
@@ -146,21 +179,28 @@ func _draw_clouds(view: Rect2, light: Color) -> void:
|
|||||||
if clouds <= 0.02:
|
if clouds <= 0.02:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Полосы держим выше линии горизонта: их нижний край не должен наезжать
|
var span := RIGHT - LEFT
|
||||||
# на землю, поэтому высота и масштаб подобраны в паре.
|
var density := clampf(clouds, 0.0, 1.0)
|
||||||
_draw_cloud_band(CLOUDS_HIGH, 1.4, -900.0, 0.35, 0.75, view, light)
|
# Чем плотнее облачность, тем свинцовее облака.
|
||||||
_draw_cloud_band(CLOUDS_LOW, 1.6, -620.0, 0.6, 1.0, view, light)
|
var tint := light.lerp(light * Color("#8ea3b8"), density)
|
||||||
|
|
||||||
|
for cloud: Dictionary in _cloud_puffs:
|
||||||
|
# У каждого облака свой порог: облачность прибавляет их числом, а не
|
||||||
|
# только непрозрачностью, иначе пасмурно выглядит как ясно, но мутнее.
|
||||||
|
var threshold: float = cloud["threshold"]
|
||||||
|
if density <= threshold:
|
||||||
|
continue
|
||||||
|
var alpha := clampf((density - threshold) * 5.0, 0.0, 1.0) * 0.9
|
||||||
|
|
||||||
func _draw_cloud_band(
|
var depth: float = cloud["depth"]
|
||||||
texture: Texture2D, scale: float, height: float, parallax: float,
|
var texture: Texture2D = cloud["texture"]
|
||||||
weight: float, view: Rect2, light: Color
|
var size := Vector2(texture.get_size()) * float(cloud["scale"])
|
||||||
) -> void:
|
var offset := view.get_center().x * (1.0 - depth) - _drift * depth
|
||||||
var size := Vector2(texture.get_size()) * scale
|
var x := LEFT + fposmod(float(cloud["x"]) + offset - LEFT, span)
|
||||||
var offset := view.get_center().x * (1.0 - parallax) - _drift * parallax
|
|
||||||
var rect := Rect2(Vector2(LEFT + offset, height), Vector2(RIGHT - LEFT, size.y))
|
draw_texture_rect(
|
||||||
var alpha := clampf(clouds * weight, 0.0, 1.0)
|
texture, Rect2(Vector2(x, float(cloud["y"])), size), false, Color(tint, alpha)
|
||||||
draw_texture_rect(texture, rect, true, Color(light, alpha))
|
)
|
||||||
|
|
||||||
|
|
||||||
## Дымка у горизонта: чем гуще, тем сильнее пейзаж растворяется в небе.
|
## Дымка у горизонта: чем гуще, тем сильнее пейзаж растворяется в небе.
|
||||||
@@ -241,6 +281,26 @@ func _scatter_scenery() -> void:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func _scatter_clouds() -> void:
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = SCENERY_SEED + 3
|
||||||
|
|
||||||
|
var textures: Array[Texture2D] = []
|
||||||
|
for path: String in CLOUD_PATHS:
|
||||||
|
textures.append(load(path) as Texture2D)
|
||||||
|
|
||||||
|
for index in CLOUD_COUNT:
|
||||||
|
_cloud_puffs.append({
|
||||||
|
"texture": textures[rng.randi_range(0, textures.size() - 1)],
|
||||||
|
"x": rng.randf_range(LEFT, RIGHT),
|
||||||
|
"y": rng.randf_range(-1020.0, -430.0),
|
||||||
|
"scale": rng.randf_range(1.3, 3.4),
|
||||||
|
# Ближние облака едут быстрее дальних.
|
||||||
|
"depth": rng.randf_range(0.25, 0.75),
|
||||||
|
"threshold": rng.randf_range(0.0, 0.9),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
func _scatter_drops() -> void:
|
func _scatter_drops() -> void:
|
||||||
var rng := RandomNumberGenerator.new()
|
var rng := RandomNumberGenerator.new()
|
||||||
rng.seed = SCENERY_SEED + 7
|
rng.seed = SCENERY_SEED + 7
|
||||||
|
|||||||
+173
-1
@@ -31,7 +31,10 @@ enum Mode { SELECT, BUILD, DEMOLISH }
|
|||||||
@onready var _weather: WeatherSystem = $Weather
|
@onready var _weather: WeatherSystem = $Weather
|
||||||
@onready var _sky: SkyView = $Sky
|
@onready var _sky: SkyView = $Sky
|
||||||
@onready var _surface: SurfaceView = $Surface
|
@onready var _surface: SurfaceView = $Surface
|
||||||
@onready var _weather_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Moment/WeatherLabel
|
@onready var _weather_icon: WeatherIcon = $Hud/Root/TopRight/Row/ClockBar/Body/Climate/Icon
|
||||||
|
@onready var _weather_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Climate/WeatherLabel
|
||||||
|
@onready var _temp_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Climate/TempLabel
|
||||||
|
@onready var _wind_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Climate/WindLabel
|
||||||
@onready var _stats: HBoxContainer = $Hud/Root/TopBar/Stats
|
@onready var _stats: HBoxContainer = $Hud/Root/TopBar/Stats
|
||||||
@onready var _date_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Moment/DateLabel
|
@onready var _date_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Moment/DateLabel
|
||||||
@onready var _time_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Moment/TimeLabel
|
@onready var _time_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Moment/TimeLabel
|
||||||
@@ -76,6 +79,7 @@ func _ready() -> void:
|
|||||||
_refresh_moment()
|
_refresh_moment()
|
||||||
_refresh_flow()
|
_refresh_flow()
|
||||||
_on_weather_changed(_weather.kind)
|
_on_weather_changed(_weather.kind)
|
||||||
|
_register_console_commands()
|
||||||
|
|
||||||
_status.text = HINT
|
_status.text = HINT
|
||||||
|
|
||||||
@@ -191,6 +195,163 @@ func _describe(room: Dictionary) -> String:
|
|||||||
return " · ".join(parts)
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Команды консоли --------------------------------------------------------
|
||||||
|
|
||||||
|
## Команды живут здесь, а не в самой консоли: они управляют этой сценой и
|
||||||
|
## умирают вместе с ней — консоль сама выбросит их, когда Callable протухнет.
|
||||||
|
func _register_console_commands() -> void:
|
||||||
|
var console := get_node_or_null("/root/DevConsole")
|
||||||
|
if console == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
console.register("stats", "текущее состояние партии", _cmd_stats)
|
||||||
|
console.register("time", "перевести часы", _cmd_time, "ЧЧ:ММ")
|
||||||
|
console.register("date", "сменить дату", _cmd_date, "ДД.ММ.ГГГГ")
|
||||||
|
console.register("speed", "скорость игры, 0 — пауза", _cmd_speed, "0..4")
|
||||||
|
console.register(
|
||||||
|
"weather", "поставить погоду", _cmd_weather,
|
||||||
|
"clear|cloudy|overcast|rain|storm|fog|snow"
|
||||||
|
)
|
||||||
|
console.register("temp", "задать температуру", _cmd_temp, "°C")
|
||||||
|
console.register("wind", "задать ветер", _cmd_wind, "м/с")
|
||||||
|
console.register("build", "построить комнату", _cmd_build, "тип этаж колонка")
|
||||||
|
console.register("wipe", "снести все комнаты", _cmd_wipe)
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_stats(_arguments: Array[String]) -> String:
|
||||||
|
var totals := _grid.totals()
|
||||||
|
return "\n".join(PackedStringArray([
|
||||||
|
"Время: %s, %s (%s, x%d)" % [
|
||||||
|
_clock.date_text(), _clock.time_text(),
|
||||||
|
"идёт" if _clock.running else "пауза", _clock.speed,
|
||||||
|
],
|
||||||
|
"Погода: %s, %s, ветер %s" % [
|
||||||
|
_weather.label(),
|
||||||
|
Climate.temperature_text(_weather.temperature),
|
||||||
|
Climate.wind_text(_weather.wind_speed),
|
||||||
|
],
|
||||||
|
"Облака: %.2f · осадки %.2f · дымка %.2f" % [
|
||||||
|
_weather.clouds, _weather.precipitation, _weather.fog,
|
||||||
|
],
|
||||||
|
"Комнат: %d" % _grid.rooms.size(),
|
||||||
|
"Ресурсы: питание %+d, вода %+d, еда %+d" % [
|
||||||
|
int(totals["power"]), int(totals["water"]), int(totals["food"]),
|
||||||
|
],
|
||||||
|
]))
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_time(arguments: Array[String]) -> String:
|
||||||
|
if arguments.is_empty():
|
||||||
|
return "Нужен аргумент: time ЧЧ:ММ"
|
||||||
|
var parts := arguments[0].split(":")
|
||||||
|
if parts.size() != 2:
|
||||||
|
return "Не разобрал время: %s" % arguments[0]
|
||||||
|
|
||||||
|
var moment := _clock.now()
|
||||||
|
moment["hour"] = clampi(int(String(parts[0])), 0, 23)
|
||||||
|
moment["minute"] = clampi(int(String(parts[1])), 0, 59)
|
||||||
|
if not _clock.set_moment(moment):
|
||||||
|
return "Этот момент раньше начала партии."
|
||||||
|
|
||||||
|
_refresh_all()
|
||||||
|
return "Часы: %s" % _clock.time_text()
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_date(arguments: Array[String]) -> String:
|
||||||
|
if arguments.is_empty():
|
||||||
|
return "Нужен аргумент: date ДД.ММ.ГГГГ"
|
||||||
|
var parts := arguments[0].split(".")
|
||||||
|
if parts.size() != 3:
|
||||||
|
return "Не разобрал дату: %s" % arguments[0]
|
||||||
|
|
||||||
|
var moment := _clock.now()
|
||||||
|
moment["day"] = clampi(int(String(parts[0])), 1, 31)
|
||||||
|
moment["month"] = clampi(int(String(parts[1])), 1, 12)
|
||||||
|
moment["year"] = int(String(parts[2]))
|
||||||
|
if not _clock.set_moment(moment):
|
||||||
|
return "Этот момент раньше начала партии."
|
||||||
|
|
||||||
|
_refresh_all()
|
||||||
|
return "Дата: %s" % _clock.date_text()
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_speed(arguments: Array[String]) -> String:
|
||||||
|
if arguments.is_empty():
|
||||||
|
return "Нужен аргумент: speed 0..4"
|
||||||
|
var value := int(arguments[0])
|
||||||
|
if value <= 0:
|
||||||
|
_clock.pause()
|
||||||
|
return "Пауза."
|
||||||
|
_clock.set_speed(value)
|
||||||
|
return "Скорость x%d" % _clock.speed
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_weather(arguments: Array[String]) -> String:
|
||||||
|
if arguments.is_empty():
|
||||||
|
return "Нужен аргумент: weather clear|cloudy|overcast|rain|storm|fog|snow"
|
||||||
|
|
||||||
|
# Понимаем и латинский ключ, и русское название — как удобнее.
|
||||||
|
var wanted := arguments[0].to_lower()
|
||||||
|
for kind: int in WeatherSystem.KINDS:
|
||||||
|
var entry: Dictionary = WeatherSystem.KINDS[kind]
|
||||||
|
if String(entry["key"]) != wanted and String(entry["label"]).to_lower() != wanted:
|
||||||
|
continue
|
||||||
|
_weather.force_kind(kind)
|
||||||
|
_refresh_all()
|
||||||
|
return "Погода: %s" % _weather.label()
|
||||||
|
return "Не знаю такой погоды: %s" % arguments[0]
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_temp(arguments: Array[String]) -> String:
|
||||||
|
if arguments.is_empty():
|
||||||
|
return "Нужен аргумент: temp °C"
|
||||||
|
_weather.nudge_temperature(float(arguments[0]))
|
||||||
|
_refresh_all()
|
||||||
|
return "Температура: %s" % Climate.temperature_text(_weather.temperature)
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_wind(arguments: Array[String]) -> String:
|
||||||
|
if arguments.is_empty():
|
||||||
|
return "Нужен аргумент: wind м/с"
|
||||||
|
_weather.set_wind_speed(float(arguments[0]))
|
||||||
|
_refresh_all()
|
||||||
|
return "Ветер: %s" % Climate.wind_text(_weather.wind_speed)
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_build(arguments: Array[String]) -> String:
|
||||||
|
if arguments.size() < 3:
|
||||||
|
return "Нужно: build тип этаж колонка. Типы: %s" % ", ".join(VaultGrid.KINDS.keys())
|
||||||
|
|
||||||
|
var kind := arguments[0]
|
||||||
|
if not VaultGrid.KINDS.has(kind):
|
||||||
|
return "Нет такого типа: %s" % kind
|
||||||
|
|
||||||
|
var a_floor := int(arguments[1]) - 1
|
||||||
|
var column := int(arguments[2])
|
||||||
|
if not _grid.place(kind, a_floor, column):
|
||||||
|
return "Сюда не поставить: занято или вплотную к шахте."
|
||||||
|
|
||||||
|
_after_change("Построено из консоли: %s" % VaultGrid.KINDS[kind]["label"])
|
||||||
|
return "Готово: %s на этаже %d" % [VaultGrid.KINDS[kind]["label"], a_floor + 1]
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_wipe(_arguments: Array[String]) -> String:
|
||||||
|
var removed := _grid.rooms.size()
|
||||||
|
_grid.rooms.clear()
|
||||||
|
_after_change("Убежище очищено.")
|
||||||
|
return "Снесено комнат: %d" % removed
|
||||||
|
|
||||||
|
|
||||||
|
## После вмешательства извне проще обновить всё, чем вспоминать, что зависит
|
||||||
|
## от чего.
|
||||||
|
func _refresh_all() -> void:
|
||||||
|
_refresh_moment()
|
||||||
|
_refresh_environment()
|
||||||
|
_refresh_weather_hud()
|
||||||
|
_refresh_flow()
|
||||||
|
_refresh_stats()
|
||||||
|
|
||||||
|
|
||||||
# --- Ход времени ------------------------------------------------------------
|
# --- Ход времени ------------------------------------------------------------
|
||||||
|
|
||||||
func _build_flow_controls() -> void:
|
func _build_flow_controls() -> void:
|
||||||
@@ -249,10 +410,21 @@ func _flow_style(background: String, border: String) -> StyleBoxFlat:
|
|||||||
func _on_minute_passed(_total_minutes: int) -> void:
|
func _on_minute_passed(_total_minutes: int) -> void:
|
||||||
_refresh_moment()
|
_refresh_moment()
|
||||||
_refresh_environment()
|
_refresh_environment()
|
||||||
|
_refresh_weather_hud()
|
||||||
|
|
||||||
|
|
||||||
func _on_weather_changed(_kind: int) -> void:
|
func _on_weather_changed(_kind: int) -> void:
|
||||||
|
_refresh_weather_hud()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_weather_hud() -> void:
|
||||||
|
var hour := SkyCycle.hour_of(_clock.now())
|
||||||
|
var is_night := SkyCycle.sun_progress(hour) < 0.0
|
||||||
|
|
||||||
|
_weather_icon.set_state(_weather.kind, is_night)
|
||||||
_weather_label.text = _weather.label()
|
_weather_label.text = _weather.label()
|
||||||
|
_temp_label.text = Climate.temperature_text(_weather.temperature)
|
||||||
|
_wind_label.text = Climate.wind_text(_weather.wind_speed)
|
||||||
|
|
||||||
|
|
||||||
func _refresh_moment() -> void:
|
func _refresh_moment() -> void:
|
||||||
|
|||||||
+100
-33
@@ -1,25 +1,50 @@
|
|||||||
class_name WeatherSystem
|
class_name WeatherSystem
|
||||||
extends Node
|
extends Node
|
||||||
## Погода: текущий тип, плавные переходы и смена по игровому времени.
|
## Погода: тип, температура, ветер и плавные переходы между состояниями.
|
||||||
##
|
##
|
||||||
## Данные и правила, без отрисовки. Вид (surface_view.gd, sky_view.gd) читает
|
## Данные и правила, без отрисовки. Вид (surface_view.gd, sky_view.gd) читает
|
||||||
## отсюда `clouds`, `precipitation`, `fog` и `wind` и рисует по ним.
|
## отсюда `clouds`, `precipitation`, `fog` и `wind`, HUD — `temperature`
|
||||||
|
## и `wind_speed`. Норму по сезону даёт climate.gd.
|
||||||
##
|
##
|
||||||
## Такт берётся у GameClock, поэтому погода стоит вместе с паузой и ускоряется
|
## Такт берётся у GameClock, поэтому погода стоит вместе с паузой и ускоряется
|
||||||
## вместе со скоростью игры — своего таймера здесь нет намеренно.
|
## вместе со скоростью игры — своего таймера здесь нет намеренно.
|
||||||
|
|
||||||
enum Kind { CLEAR, CLOUDY, OVERCAST, RAIN, STORM, FOG, SNOW }
|
enum Kind { CLEAR, CLOUDY, OVERCAST, RAIN, STORM, FOG, SNOW }
|
||||||
|
|
||||||
## Целевые параметры каждого типа. `clouds` — плотность облаков, `precipitation`
|
## Целевые параметры картинки для каждого типа. Температура и ветер сюда не
|
||||||
## — сила осадков, `fog` — дымка у горизонта, `wind` — скорость сноса.
|
## входят: они зависят ещё и от сезона, их считает Climate.
|
||||||
|
##
|
||||||
|
## `key` — латинское имя для консоли: набирать русские названия с латинской
|
||||||
|
## раскладки неудобно, а переключаться посреди отладки — тем более.
|
||||||
const KINDS := {
|
const KINDS := {
|
||||||
Kind.CLEAR: {"label": "Ясно", "clouds": 0.12, "precipitation": 0.0, "fog": 0.0, "wind": 0.3},
|
Kind.CLEAR: {
|
||||||
Kind.CLOUDY: {"label": "Облачно", "clouds": 0.45, "precipitation": 0.0, "fog": 0.0, "wind": 0.5},
|
"label": "Ясно", "key": "clear",
|
||||||
Kind.OVERCAST: {"label": "Пасмурно", "clouds": 0.85, "precipitation": 0.0, "fog": 0.06, "wind": 0.4},
|
"clouds": 0.12, "precipitation": 0.0, "fog": 0.0,
|
||||||
Kind.RAIN: {"label": "Дождь", "clouds": 0.95, "precipitation": 0.55, "fog": 0.14, "wind": 0.7},
|
},
|
||||||
Kind.STORM: {"label": "Ливень", "clouds": 1.0, "precipitation": 1.0, "fog": 0.22, "wind": 1.0},
|
Kind.CLOUDY: {
|
||||||
Kind.FOG: {"label": "Туман", "clouds": 0.3, "precipitation": 0.0, "fog": 0.62, "wind": 0.12},
|
"label": "Облачно", "key": "cloudy",
|
||||||
Kind.SNOW: {"label": "Снег", "clouds": 0.9, "precipitation": 0.5, "fog": 0.24, "wind": 0.35},
|
"clouds": 0.45, "precipitation": 0.0, "fog": 0.0,
|
||||||
|
},
|
||||||
|
Kind.OVERCAST: {
|
||||||
|
"label": "Пасмурно", "key": "overcast",
|
||||||
|
"clouds": 0.85, "precipitation": 0.0, "fog": 0.06,
|
||||||
|
},
|
||||||
|
Kind.RAIN: {
|
||||||
|
"label": "Дождь", "key": "rain",
|
||||||
|
"clouds": 0.95, "precipitation": 0.55, "fog": 0.14,
|
||||||
|
},
|
||||||
|
Kind.STORM: {
|
||||||
|
"label": "Ливень", "key": "storm",
|
||||||
|
"clouds": 1.0, "precipitation": 1.0, "fog": 0.22,
|
||||||
|
},
|
||||||
|
Kind.FOG: {
|
||||||
|
"label": "Туман", "key": "fog",
|
||||||
|
"clouds": 0.3, "precipitation": 0.0, "fog": 0.62,
|
||||||
|
},
|
||||||
|
Kind.SNOW: {
|
||||||
|
"label": "Снег", "key": "snow",
|
||||||
|
"clouds": 0.9, "precipitation": 0.5, "fog": 0.24,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
## Куда погода может перейти и с каким относительным весом. Матрица, а не
|
## Куда погода может перейти и с каким относительным весом. Матрица, а не
|
||||||
@@ -27,20 +52,19 @@ const KINDS := {
|
|||||||
const TRANSITIONS := {
|
const TRANSITIONS := {
|
||||||
Kind.CLEAR: {Kind.CLEAR: 4, Kind.CLOUDY: 5, Kind.FOG: 1},
|
Kind.CLEAR: {Kind.CLEAR: 4, Kind.CLOUDY: 5, Kind.FOG: 1},
|
||||||
Kind.CLOUDY: {Kind.CLEAR: 4, Kind.CLOUDY: 2, Kind.OVERCAST: 4, Kind.FOG: 1},
|
Kind.CLOUDY: {Kind.CLEAR: 4, Kind.CLOUDY: 2, Kind.OVERCAST: 4, Kind.FOG: 1},
|
||||||
Kind.OVERCAST: {Kind.CLOUDY: 4, Kind.OVERCAST: 2, Kind.RAIN: 3, Kind.SNOW: 2},
|
Kind.OVERCAST: {Kind.CLOUDY: 4, Kind.OVERCAST: 2, Kind.RAIN: 4, Kind.SNOW: 1},
|
||||||
Kind.RAIN: {Kind.RAIN: 2, Kind.STORM: 2, Kind.OVERCAST: 5},
|
Kind.RAIN: {Kind.RAIN: 2, Kind.STORM: 2, Kind.OVERCAST: 5},
|
||||||
Kind.STORM: {Kind.RAIN: 5, Kind.OVERCAST: 2},
|
Kind.STORM: {Kind.RAIN: 5, Kind.OVERCAST: 2},
|
||||||
Kind.FOG: {Kind.FOG: 2, Kind.CLEAR: 4, Kind.CLOUDY: 3},
|
Kind.FOG: {Kind.FOG: 2, Kind.CLEAR: 4, Kind.CLOUDY: 3},
|
||||||
Kind.SNOW: {Kind.SNOW: 3, Kind.OVERCAST: 4, Kind.CLOUDY: 2},
|
Kind.SNOW: {Kind.SNOW: 3, Kind.OVERCAST: 4, Kind.CLOUDY: 2},
|
||||||
}
|
}
|
||||||
|
|
||||||
## Месяцы, когда осадки выпадают снегом.
|
|
||||||
const WINTER_MONTHS: Array[int] = [11, 12, 1, 2]
|
|
||||||
|
|
||||||
const MIN_DURATION := 120
|
const MIN_DURATION := 120
|
||||||
const MAX_DURATION := 420
|
const MAX_DURATION := 420
|
||||||
## За сколько игровых минут параметры доходят до целевых.
|
## За сколько игровых минут параметры доходят до целевых.
|
||||||
const BLEND_MINUTES := 45.0
|
const BLEND_MINUTES := 45.0
|
||||||
|
## Насколько воздушная масса может отклонить температуру от нормы, °C.
|
||||||
|
const DRIFT_RANGE := 3.0
|
||||||
|
|
||||||
## Сменился тип погоды — для HUD и звука.
|
## Сменился тип погоды — для HUD и звука.
|
||||||
signal changed(kind: int)
|
signal changed(kind: int)
|
||||||
@@ -49,9 +73,14 @@ var kind := Kind.CLEAR
|
|||||||
var clouds := 0.12
|
var clouds := 0.12
|
||||||
var precipitation := 0.0
|
var precipitation := 0.0
|
||||||
var fog := 0.0
|
var fog := 0.0
|
||||||
var wind := 0.3
|
## Ветер 0..1 для картинки; в метрах в секунду — wind_speed.
|
||||||
|
var wind := 0.15
|
||||||
|
var wind_speed := 2.5
|
||||||
|
var temperature := 8.0
|
||||||
|
|
||||||
var _minutes_left := MIN_DURATION
|
var _minutes_left := MIN_DURATION
|
||||||
|
## Отклонение текущей воздушной массы от климатической нормы, °C.
|
||||||
|
var _temperature_drift := 0.0
|
||||||
var _rng := RandomNumberGenerator.new()
|
var _rng := RandomNumberGenerator.new()
|
||||||
var _clock: GameClock
|
var _clock: GameClock
|
||||||
|
|
||||||
@@ -65,6 +94,7 @@ func _ready() -> void:
|
|||||||
func attach_clock(clock: GameClock) -> void:
|
func attach_clock(clock: GameClock) -> void:
|
||||||
_clock = clock
|
_clock = clock
|
||||||
_clock.minute_passed.connect(_on_minute_passed)
|
_clock.minute_passed.connect(_on_minute_passed)
|
||||||
|
_apply_climate(1.0)
|
||||||
|
|
||||||
|
|
||||||
func label() -> String:
|
func label() -> String:
|
||||||
@@ -76,6 +106,29 @@ func is_snowing() -> bool:
|
|||||||
return kind == Kind.SNOW
|
return kind == Kind.SNOW
|
||||||
|
|
||||||
|
|
||||||
|
# --- Ручное управление (консоль разработчика) -------------------------------
|
||||||
|
|
||||||
|
## Поставить конкретную погоду немедленно, минуя матрицу переходов.
|
||||||
|
func force_kind(value: int, hold_minutes := MAX_DURATION) -> void:
|
||||||
|
kind = clampi(value, 0, KINDS.size() - 1)
|
||||||
|
_minutes_left = hold_minutes
|
||||||
|
changed.emit(kind)
|
||||||
|
|
||||||
|
|
||||||
|
## Сдвинуть воздушную массу так, чтобы сейчас было примерно `celsius`.
|
||||||
|
## Значение держится до следующей смены погоды, а не жёстко фиксируется.
|
||||||
|
func nudge_temperature(celsius: float) -> void:
|
||||||
|
_temperature_drift += celsius - temperature
|
||||||
|
temperature = celsius
|
||||||
|
|
||||||
|
|
||||||
|
func set_wind_speed(speed: float) -> void:
|
||||||
|
wind_speed = maxf(speed, 0.0)
|
||||||
|
wind = clampf(wind_speed / Climate.WIND_REFERENCE, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Сохранение -------------------------------------------------------------
|
||||||
|
|
||||||
func save_data() -> Dictionary:
|
func save_data() -> Dictionary:
|
||||||
return {
|
return {
|
||||||
"kind": int(kind),
|
"kind": int(kind),
|
||||||
@@ -83,7 +136,9 @@ func save_data() -> Dictionary:
|
|||||||
"clouds": clouds,
|
"clouds": clouds,
|
||||||
"precipitation": precipitation,
|
"precipitation": precipitation,
|
||||||
"fog": fog,
|
"fog": fog,
|
||||||
"wind": wind,
|
"wind_speed": wind_speed,
|
||||||
|
"temperature": temperature,
|
||||||
|
"drift": _temperature_drift,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -93,14 +148,17 @@ func load_data(data: Dictionary) -> void:
|
|||||||
clouds = float(data.get("clouds", 0.12))
|
clouds = float(data.get("clouds", 0.12))
|
||||||
precipitation = float(data.get("precipitation", 0.0))
|
precipitation = float(data.get("precipitation", 0.0))
|
||||||
fog = float(data.get("fog", 0.0))
|
fog = float(data.get("fog", 0.0))
|
||||||
wind = float(data.get("wind", 0.3))
|
temperature = float(data.get("temperature", 8.0))
|
||||||
|
_temperature_drift = float(data.get("drift", 0.0))
|
||||||
|
set_wind_speed(float(data.get("wind_speed", 2.5)))
|
||||||
changed.emit(kind)
|
changed.emit(kind)
|
||||||
|
|
||||||
|
|
||||||
# --- Внутреннее -------------------------------------------------------------
|
# --- Внутреннее -------------------------------------------------------------
|
||||||
|
|
||||||
func _on_minute_passed(_total_minutes: int) -> void:
|
func _on_minute_passed(_total_minutes: int) -> void:
|
||||||
_blend_toward_target()
|
_apply_target(1.0 / BLEND_MINUTES)
|
||||||
|
_apply_climate(1.0 / BLEND_MINUTES)
|
||||||
|
|
||||||
_minutes_left -= 1
|
_minutes_left -= 1
|
||||||
if _minutes_left > 0:
|
if _minutes_left > 0:
|
||||||
@@ -108,20 +166,29 @@ func _on_minute_passed(_total_minutes: int) -> void:
|
|||||||
|
|
||||||
kind = _roll_next()
|
kind = _roll_next()
|
||||||
_minutes_left = _rng.randi_range(MIN_DURATION, MAX_DURATION)
|
_minutes_left = _rng.randi_range(MIN_DURATION, MAX_DURATION)
|
||||||
|
# Новая погода — новая воздушная масса.
|
||||||
|
_temperature_drift = _rng.randf_range(-DRIFT_RANGE, DRIFT_RANGE)
|
||||||
changed.emit(kind)
|
changed.emit(kind)
|
||||||
|
|
||||||
|
|
||||||
## Параметры не прыгают вместе с типом: они доходят до цели за BLEND_MINUTES.
|
## Параметры картинки не прыгают вместе с типом: они доходят до цели плавно.
|
||||||
func _blend_toward_target() -> void:
|
|
||||||
_apply_target(1.0 / BLEND_MINUTES)
|
|
||||||
|
|
||||||
|
|
||||||
func _apply_target(weight: float) -> void:
|
func _apply_target(weight: float) -> void:
|
||||||
var target: Dictionary = KINDS[kind]
|
var target: Dictionary = KINDS[kind]
|
||||||
clouds = lerpf(clouds, float(target["clouds"]), weight)
|
clouds = lerpf(clouds, float(target["clouds"]), weight)
|
||||||
precipitation = lerpf(precipitation, float(target["precipitation"]), weight)
|
precipitation = lerpf(precipitation, float(target["precipitation"]), weight)
|
||||||
fog = lerpf(fog, float(target["fog"]), weight)
|
fog = lerpf(fog, float(target["fog"]), weight)
|
||||||
wind = lerpf(wind, float(target["wind"]), weight)
|
|
||||||
|
|
||||||
|
## Температура и ветер идут к сезонной норме с поправкой на тип погоды.
|
||||||
|
func _apply_climate(weight: float) -> void:
|
||||||
|
if _clock == null:
|
||||||
|
return
|
||||||
|
var moment := _clock.now()
|
||||||
|
var target := Climate.base_temperature(int(moment["month"]), SkyCycle.hour_of(moment))
|
||||||
|
target += Climate.weather_offset(kind) + _temperature_drift
|
||||||
|
temperature = lerpf(temperature, target, weight)
|
||||||
|
|
||||||
|
set_wind_speed(lerpf(wind_speed, Climate.base_wind(kind), weight))
|
||||||
|
|
||||||
|
|
||||||
func _roll_next() -> int:
|
func _roll_next() -> int:
|
||||||
@@ -134,15 +201,15 @@ func _roll_next() -> int:
|
|||||||
for candidate: int in options:
|
for candidate: int in options:
|
||||||
pick -= int(options[candidate])
|
pick -= int(options[candidate])
|
||||||
if pick <= 0:
|
if pick <= 0:
|
||||||
return _seasonal(candidate)
|
return _by_temperature(candidate)
|
||||||
return kind
|
return kind
|
||||||
|
|
||||||
|
|
||||||
## Снег уместен только зимой; в остальное время он превращается в дождь.
|
## Форму осадков решает градусник, а не календарь: у нуля дождь становится
|
||||||
func _seasonal(candidate: int) -> int:
|
## снегом и наоборот. Так это можно проверить, подкрутив температуру.
|
||||||
if candidate != Kind.SNOW:
|
func _by_temperature(candidate: int) -> int:
|
||||||
return candidate
|
if candidate == Kind.SNOW and temperature > Climate.SNOW_THRESHOLD + 1.5:
|
||||||
if _clock == null:
|
|
||||||
return Kind.RAIN
|
return Kind.RAIN
|
||||||
var month := int(_clock.now()["month"])
|
if candidate == Kind.RAIN and temperature < Climate.SNOW_THRESHOLD:
|
||||||
return Kind.SNOW if WINTER_MONTHS.has(month) else Kind.RAIN
|
return Kind.SNOW
|
||||||
|
return candidate
|
||||||
|
|||||||
@@ -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 ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bp5qxf7p5q3ty
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dpifh4u0gmgxw
|
||||||
Reference in New Issue
Block a user