diff --git a/CLAUDE.md b/CLAUDE.md index ab3d850..c36ec78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,16 +57,37 @@ ## Игровой слой -Три файла в `scripts/game/`, разделены намеренно: +Файлы в `scripts/game/` разделены по принципу «данные и правила отдельно, отрисовка отдельно». -- `vault_grid.gd` — только данные и правила (`RefCounted`, без узлов). Сюда идут изменения - логики: типы комнат, размещение, ресурсы. -- `vault_view.gd` — только `_draw()`. Сюда идут изменения картинки. Когда появится графика, - переписывается этот файл, остальные не трогаются. -- `vault_scene.gd` — связка модели, вида, камеры и HUD плюс контракт сохранения. +Модель и правила — без узлов и без `_draw()`: + +- `vault_grid.gd` — сетка и комнаты. Сюда идут типы комнат, размещение, ресурсы. + Вертикальное смещение бункера под землю — константа `DEPTH`, её учитывают и + `cell_rect()`, и `cell_at()`, поэтому картинка и попадание мышью не разъезжаются. +- `game_clock.gd` — игровые дата и время, скорость, своя пауза. Такт наружу — сигнал + `minute_passed`; темп задаётся `MINUTES_PER_SECOND`. +- `weather_system.gd` — тип погоды, матрица переходов и плавная смена параметров. + Такт берёт у часов, поэтому стоит вместе с паузой. +- `sky_cycle.gd` — палитра неба по часу суток, положение солнца и луны. Чистые вычисления. + +Отрисовка — только `_draw()`, читают модель и ничего не решают: + +- `sky_view.gd` — градиент неба, звёзды, светила. +- `surface_view.gd` — поверхность: горы, холмы, деревья, облака, осадки. +- `vault_view.gd` — разрез: порода, корпус, шахта, комнаты, будка входа. + +`vault_scene.gd` связывает всё это с камерой и HUD и держит контракт сохранения. Новый тип комнаты добавляется одной записью в `VaultGrid.KINDS`: кнопка в панели строительства -и подсчёт ресурсов подхватятся сами. +и подсчёт ресурсов подхватятся сами. Новый тип погоды — записью в `WeatherSystem.KINDS` +плюс строкой в `TRANSITIONS`. + +### Текстуры фона + +`background/` — набор Kenney Background Elements. Два стиля, и они не взаимозаменяемы: +`PNG/Flat/*` — бледные, почти белые силуэты, их место на дальних планах, где цвет всё равно +задаётся тонировкой; `PNG/*` — цветные спрайты для ближнего плана. Перепутать легко: деревья +из Flat на переднем плане выглядят выцветшими. ## Автозагрузки diff --git a/background/Samples/colored_castle.png b/background/Samples/colored_castle.png deleted file mode 100644 index 107d276..0000000 Binary files a/background/Samples/colored_castle.png and /dev/null differ diff --git a/background/Samples/colored_castle.png.import b/background/Samples/colored_castle.png.import deleted file mode 100644 index 194e50e..0000000 --- a/background/Samples/colored_castle.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://caxx1gqjw4p3f" -path="res://.godot/imported/colored_castle.png-41cd94defcf5b6bd80a752d0fb461a53.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/colored_castle.png" -dest_files=["res://.godot/imported/colored_castle.png-41cd94defcf5b6bd80a752d0fb461a53.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/colored_desert.png b/background/Samples/colored_desert.png deleted file mode 100644 index 5ce4ad6..0000000 Binary files a/background/Samples/colored_desert.png and /dev/null differ diff --git a/background/Samples/colored_desert.png.import b/background/Samples/colored_desert.png.import deleted file mode 100644 index 20e3730..0000000 --- a/background/Samples/colored_desert.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://crsg8yt6dsw4x" -path="res://.godot/imported/colored_desert.png-dc25543c1ff9794be0c25da84f41ae84.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/colored_desert.png" -dest_files=["res://.godot/imported/colored_desert.png-dc25543c1ff9794be0c25da84f41ae84.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/colored_forest.png b/background/Samples/colored_forest.png deleted file mode 100644 index 68cc7c5..0000000 Binary files a/background/Samples/colored_forest.png and /dev/null differ diff --git a/background/Samples/colored_forest.png.import b/background/Samples/colored_forest.png.import deleted file mode 100644 index 1009346..0000000 --- a/background/Samples/colored_forest.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://dqgm3ros2l86y" -path="res://.godot/imported/colored_forest.png-b603082a8e002be5f2044e5aeed8e028.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/colored_forest.png" -dest_files=["res://.godot/imported/colored_forest.png-b603082a8e002be5f2044e5aeed8e028.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/colored_talltrees.png b/background/Samples/colored_talltrees.png deleted file mode 100644 index f335acf..0000000 Binary files a/background/Samples/colored_talltrees.png and /dev/null differ diff --git a/background/Samples/colored_talltrees.png.import b/background/Samples/colored_talltrees.png.import deleted file mode 100644 index 117e852..0000000 --- a/background/Samples/colored_talltrees.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://clkg3a7j25vcy" -path="res://.godot/imported/colored_talltrees.png-c8c61e11c017dc56870d082ab76b2bd0.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/colored_talltrees.png" -dest_files=["res://.godot/imported/colored_talltrees.png-c8c61e11c017dc56870d082ab76b2bd0.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_castle.png b/background/Samples/uncolored_castle.png deleted file mode 100644 index 9ddc542..0000000 Binary files a/background/Samples/uncolored_castle.png and /dev/null differ diff --git a/background/Samples/uncolored_castle.png.import b/background/Samples/uncolored_castle.png.import deleted file mode 100644 index 0a251a0..0000000 --- a/background/Samples/uncolored_castle.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://btmicau3ia0gf" -path="res://.godot/imported/uncolored_castle.png-9dc9ddef5f268efb7b94ac168d74c32a.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_castle.png" -dest_files=["res://.godot/imported/uncolored_castle.png-9dc9ddef5f268efb7b94ac168d74c32a.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_desert.png b/background/Samples/uncolored_desert.png deleted file mode 100644 index 781110b..0000000 Binary files a/background/Samples/uncolored_desert.png and /dev/null differ diff --git a/background/Samples/uncolored_desert.png.import b/background/Samples/uncolored_desert.png.import deleted file mode 100644 index 23b3505..0000000 --- a/background/Samples/uncolored_desert.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://lssvxv0eijl4" -path="res://.godot/imported/uncolored_desert.png-78b9bf49dd60ef634b76a8b132ec0dd5.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_desert.png" -dest_files=["res://.godot/imported/uncolored_desert.png-78b9bf49dd60ef634b76a8b132ec0dd5.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_forest.png b/background/Samples/uncolored_forest.png deleted file mode 100644 index 47bc261..0000000 Binary files a/background/Samples/uncolored_forest.png and /dev/null differ diff --git a/background/Samples/uncolored_forest.png.import b/background/Samples/uncolored_forest.png.import deleted file mode 100644 index c3be63b..0000000 --- a/background/Samples/uncolored_forest.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://c5338moupk3w8" -path="res://.godot/imported/uncolored_forest.png-18a539af26d75f1bbaff45fcd1a43d00.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_forest.png" -dest_files=["res://.godot/imported/uncolored_forest.png-18a539af26d75f1bbaff45fcd1a43d00.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_hills.png b/background/Samples/uncolored_hills.png deleted file mode 100644 index 7bc084d..0000000 Binary files a/background/Samples/uncolored_hills.png and /dev/null differ diff --git a/background/Samples/uncolored_hills.png.import b/background/Samples/uncolored_hills.png.import deleted file mode 100644 index 5dc13dc..0000000 --- a/background/Samples/uncolored_hills.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://3hpiabdluup4" -path="res://.godot/imported/uncolored_hills.png-5dcac44bd26d4ba7c0ba6cc14c962e90.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_hills.png" -dest_files=["res://.godot/imported/uncolored_hills.png-5dcac44bd26d4ba7c0ba6cc14c962e90.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_peaks.png b/background/Samples/uncolored_peaks.png deleted file mode 100644 index 98c8f14..0000000 Binary files a/background/Samples/uncolored_peaks.png and /dev/null differ diff --git a/background/Samples/uncolored_peaks.png.import b/background/Samples/uncolored_peaks.png.import deleted file mode 100644 index 9494161..0000000 --- a/background/Samples/uncolored_peaks.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://bq8myo1ptombq" -path="res://.godot/imported/uncolored_peaks.png-5d7a74c628db7a38aee0bffd844eaeea.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_peaks.png" -dest_files=["res://.godot/imported/uncolored_peaks.png-5d7a74c628db7a38aee0bffd844eaeea.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_piramids.png b/background/Samples/uncolored_piramids.png deleted file mode 100644 index 867961a..0000000 Binary files a/background/Samples/uncolored_piramids.png and /dev/null differ diff --git a/background/Samples/uncolored_piramids.png.import b/background/Samples/uncolored_piramids.png.import deleted file mode 100644 index aaab802..0000000 --- a/background/Samples/uncolored_piramids.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://dowtuhvq6hxiu" -path="res://.godot/imported/uncolored_piramids.png-a11e1df6b36de6418f938866ad2259d0.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_piramids.png" -dest_files=["res://.godot/imported/uncolored_piramids.png-a11e1df6b36de6418f938866ad2259d0.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_plain.png b/background/Samples/uncolored_plain.png deleted file mode 100644 index f096bce..0000000 Binary files a/background/Samples/uncolored_plain.png and /dev/null differ diff --git a/background/Samples/uncolored_plain.png.import b/background/Samples/uncolored_plain.png.import deleted file mode 100644 index f4be3c8..0000000 --- a/background/Samples/uncolored_plain.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://bjx5sgy01v5bh" -path="res://.godot/imported/uncolored_plain.png-81be7210a0ef6c6bb12da9811f304d6a.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_plain.png" -dest_files=["res://.godot/imported/uncolored_plain.png-81be7210a0ef6c6bb12da9811f304d6a.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Samples/uncolored_talltrees.png b/background/Samples/uncolored_talltrees.png deleted file mode 100644 index 3f82b5f..0000000 Binary files a/background/Samples/uncolored_talltrees.png and /dev/null differ diff --git a/background/Samples/uncolored_talltrees.png.import b/background/Samples/uncolored_talltrees.png.import deleted file mode 100644 index 3cd2fd7..0000000 --- a/background/Samples/uncolored_talltrees.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://dveara1yud63p" -path="res://.godot/imported/uncolored_talltrees.png-6782893694c5a062ddb71ccc51cbd94b.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Samples/uncolored_talltrees.png" -dest_files=["res://.godot/imported/uncolored_talltrees.png-6782893694c5a062ddb71ccc51cbd94b.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Spritesheet/bgElements_spritesheet.png b/background/Spritesheet/bgElements_spritesheet.png deleted file mode 100644 index d6b5830..0000000 Binary files a/background/Spritesheet/bgElements_spritesheet.png and /dev/null differ diff --git a/background/Spritesheet/bgElements_spritesheet.png.import b/background/Spritesheet/bgElements_spritesheet.png.import deleted file mode 100644 index 1089a17..0000000 --- a/background/Spritesheet/bgElements_spritesheet.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://rgpbket2k73e" -path="res://.godot/imported/bgElements_spritesheet.png-aa30f72db7f148897f99e11cf82e2edc.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Spritesheet/bgElements_spritesheet.png" -dest_files=["res://.godot/imported/bgElements_spritesheet.png-aa30f72db7f148897f99e11cf82e2edc.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/background/Spritesheet/bgElements_spritesheet.xml b/background/Spritesheet/bgElements_spritesheet.xml deleted file mode 100644 index abf5afc..0000000 --- a/background/Spritesheet/bgElements_spritesheet.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/background/Vector/bgElements_elements.ai b/background/Vector/bgElements_elements.ai deleted file mode 100644 index f755b72..0000000 Binary files a/background/Vector/bgElements_elements.ai and /dev/null differ diff --git a/background/Vector/bgElements_elements.svg b/background/Vector/bgElements_elements.svg deleted file mode 100644 index 60e048f..0000000 --- a/background/Vector/bgElements_elements.svg +++ /dev/null @@ -1,2596 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/background/Vector/bgElements_elements.svg.import b/background/Vector/bgElements_elements.svg.import deleted file mode 100644 index 27763c7..0000000 --- a/background/Vector/bgElements_elements.svg.import +++ /dev/null @@ -1,43 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://cm7l4bo0ikkdq" -path="res://.godot/imported/bgElements_elements.svg-5571e3fb12427e8e66317a647726aa5c.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Vector/bgElements_elements.svg" -dest_files=["res://.godot/imported/bgElements_elements.svg-5571e3fb12427e8e66317a647726aa5c.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 -svg/scale=1.0 -editor/scale_with_editor_scale=false -editor/convert_colors_with_editor_theme=false diff --git a/background/Vector/bgElements_samples.ai b/background/Vector/bgElements_samples.ai deleted file mode 100644 index 4fc77e3..0000000 Binary files a/background/Vector/bgElements_samples.ai and /dev/null differ diff --git a/background/Vector/bgElements_samples.svg b/background/Vector/bgElements_samples.svg deleted file mode 100644 index 2d862ec..0000000 --- a/background/Vector/bgElements_samples.svg +++ /dev/null @@ -1,1176 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/background/Vector/bgElements_samples.svg.import b/background/Vector/bgElements_samples.svg.import deleted file mode 100644 index 6859a6f..0000000 --- a/background/Vector/bgElements_samples.svg.import +++ /dev/null @@ -1,43 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://daju353jwtama" -path="res://.godot/imported/bgElements_samples.svg-7ee724785130274b980b75c813b26fa2.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://background/Vector/bgElements_samples.svg" -dest_files=["res://.godot/imported/bgElements_samples.svg-7ee724785130274b980b75c813b26fa2.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 -svg/scale=1.0 -editor/scale_with_editor_scale=false -editor/convert_colors_with_editor_theme=false diff --git a/scenes/game/vault.tscn b/scenes/game/vault.tscn index 4a12d05..955e1ec 100644 --- a/scenes/game/vault.tscn +++ b/scenes/game/vault.tscn @@ -1,10 +1,14 @@ [gd_scene format=3 uid="uid://dprxap0ioe3ps"] [ext_resource type="Script" uid="uid://lsclvc1dgviq" path="res://scripts/game/vault_scene.gd" id="1_o8yry"] +[ext_resource type="Script" uid="uid://ddaqvur2yl61c" path="res://scripts/game/sky_view.gd" id="2_lffpp"] [ext_resource type="Script" uid="uid://bvimo3kgmefgf" path="res://scripts/game/vault_view.gd" id="2_mkhle"] +[ext_resource type="Script" uid="uid://c4c36o4o35gqw" path="res://scripts/game/surface_view.gd" id="3_lvdr8"] [ext_resource type="Script" uid="uid://dg751mcd0v178" path="res://scripts/game/vault_camera.gd" id="3_x7j0k"] [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="Script" uid="uid://7wstqw8ve2he" path="res://scripts/game/game_clock.gd" id="6_mkhle"] +[ext_resource type="Script" uid="uid://by1cwvs3fc3pa" path="res://scripts/game/weather_system.gd" id="9_m0p8k"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lvdr8"] content_margin_left = 22.0 @@ -38,9 +42,31 @@ corner_radius_top_right = 14 corner_radius_bottom_right = 14 corner_radius_bottom_left = 14 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mkhle"] +content_margin_left = 14.0 +content_margin_top = 8.0 +content_margin_right = 14.0 +content_margin_bottom = 8.0 +bg_color = Color(0.043137256, 0.07450981, 0.13333334, 0.8784314) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.18039216, 0.9019608, 0.77254903, 0.2) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + [node name="Vault" type="Node2D" unique_id=1382797618] script = ExtResource("1_o8yry") +[node name="Sky" type="Node2D" parent="." unique_id=1428657021] +script = ExtResource("2_lffpp") + +[node name="Surface" type="Node2D" parent="." unique_id=320825958] +script = ExtResource("3_lvdr8") + [node name="View" type="Node2D" parent="." unique_id=1238774489] script = ExtResource("2_mkhle") @@ -104,4 +130,64 @@ theme_override_colors/font_color = Color(0.3647059, 0.44705883, 0.53333336, 1) theme_override_font_sizes/font_size = 15 horizontal_alignment = 1 +[node name="TopRight" type="MarginContainer" parent="Hud/Root" unique_id=417759891] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +theme_override_constants/margin_left = 36 +theme_override_constants/margin_top = 36 +theme_override_constants/margin_right = 36 +theme_override_constants/margin_bottom = 36 + +[node name="Row" type="HBoxContainer" parent="Hud/Root/TopRight" unique_id=705018436] +layout_mode = 2 +size_flags_vertical = 0 +mouse_filter = 2 +alignment = 2 + +[node name="ClockBar" type="PanelContainer" parent="Hud/Root/TopRight/Row" unique_id=2028708536] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_mkhle") + +[node name="Body" type="VBoxContainer" parent="Hud/Root/TopRight/Row/ClockBar" unique_id=220234733] +layout_mode = 2 +theme_override_constants/separation = 6 + +[node name="Moment" type="HBoxContainer" parent="Hud/Root/TopRight/Row/ClockBar/Body" unique_id=195744038] +layout_mode = 2 +theme_override_constants/separation = 10 +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] +layout_mode = 2 +theme_override_colors/font_color = Color(0.56078434, 0.64705884, 0.7294118, 1) +theme_override_font_sizes/font_size = 14 +text = "1 апреля 2012" +vertical_alignment = 1 + +[node name="TimeLabel" type="Label" parent="Hud/Root/TopRight/Row/ClockBar/Body/Moment" unique_id=686404941] +layout_mode = 2 +theme_override_colors/font_color = Color(0.18039216, 0.9019608, 0.77254903, 1) +theme_override_font_sizes/font_size = 20 +text = "06:00" + +[node name="Controls" type="HBoxContainer" parent="Hud/Root/TopRight/Row/ClockBar/Body" unique_id=278368540] +layout_mode = 2 +theme_override_constants/separation = 4 +alignment = 2 + [node name="PauseMenu" parent="." unique_id=1649378803 instance=ExtResource("5_lffpp")] + +[node name="Clock" type="Node" parent="." unique_id=1353564659] +script = ExtResource("6_mkhle") + +[node name="Weather" type="Node" parent="." unique_id=1790958949] +script = ExtResource("9_m0p8k") diff --git a/scripts/game/game_clock.gd b/scripts/game/game_clock.gd new file mode 100644 index 0000000..4b0e306 --- /dev/null +++ b/scripts/game/game_clock.gd @@ -0,0 +1,142 @@ +class_name GameClock +extends Node +## Игровые дата и время. +## +## Отдельная сущность, а не поле сцены: ход времени понадобится производству, +## жителям и событиям, и сохранение обязано его восстанавливать. +## +## Пауза здесь своя, не движковая. `get_tree().paused` останавливает вообще всё +## и занята меню паузы; здесь же нужно останавливать только ход партии, оставляя +## интерфейс живым. При этом узел обычный, PAUSABLE, поэтому меню паузы +## замораживает время само собой. + +## Старт партии: 1 апреля 2012 года, шесть утра. +const START := {"year": 2012, "month": 4, "day": 1, "hour": 6, "minute": 0, "second": 0} + +## Сколько игровых минут проходит за секунду реального времени на скорости x1. +## Сутки при этом занимают 8 минут реального времени. Множители x2…x4 +## умножают именно это значение, так что менять темп достаточно здесь. +const MINUTES_PER_SECOND := 3.0 + +## Доступные множители скорости. +const SPEEDS: Array[int] = [1, 2, 3, 4] + +const MINUTES_PER_DAY := 24 * 60 + +const MONTHS: Array[String] = [ + "января", "февраля", "марта", "апреля", "мая", "июня", + "июля", "августа", "сентября", "октября", "ноября", "декабря", +] + +## Прошла игровая минута — сюда подключаются HUD, производство, события. +signal minute_passed(total_minutes: int) +## Сменились сутки. +signal day_passed(day_index: int) +## Изменились скорость или состояние паузы. +signal flow_changed + +var running := true +var speed := 1 + +var _start_unix := int(Time.get_unix_time_from_datetime_dict(START)) +var _elapsed_minutes := 0.0 +var _last_whole_minute := 0 +var _last_day := 0 + + +func _process(delta: float) -> void: + if not running: + return + _advance(delta * MINUTES_PER_SECOND * float(speed)) + + +# --- Текущий момент --------------------------------------------------------- + +## Момент как словарь Time: year, month, day, hour, minute, second. +func now() -> Dictionary: + return Time.get_datetime_dict_from_unix_time(unix_time()) + + +func unix_time() -> int: + return _start_unix + int(_elapsed_minutes) * 60 + + +## «1 апреля 2012» +func date_text() -> String: + var moment := now() + return "%d %s %d" % [ + int(moment["day"]), MONTHS[int(moment["month"]) - 1], int(moment["year"]), + ] + + +## «06:00» +func time_text() -> String: + var moment := now() + return "%02d:%02d" % [int(moment["hour"]), int(moment["minute"])] + + +# --- Управление ходом ------------------------------------------------------- + +func set_speed(value: int) -> void: + speed = clampi(value, SPEEDS[0], SPEEDS[SPEEDS.size() - 1]) + running = true + flow_changed.emit() + + +func play() -> void: + if running: + return + running = true + flow_changed.emit() + + +func pause() -> void: + if not running: + return + running = false + flow_changed.emit() + + +func toggle() -> void: + running = not running + flow_changed.emit() + + +# --- Сохранение ------------------------------------------------------------- + +func save_data() -> Dictionary: + return {"minutes": _elapsed_minutes, "speed": speed, "running": running} + + +func load_data(data: Dictionary) -> void: + _elapsed_minutes = maxf(float(data.get("minutes", 0.0)), 0.0) + speed = clampi(int(data.get("speed", 1)), SPEEDS[0], SPEEDS[SPEEDS.size() - 1]) + running = bool(data.get("running", true)) + _last_whole_minute = int(_elapsed_minutes) + _last_day = _day_index() + flow_changed.emit() + minute_passed.emit(_last_whole_minute) + + +# --- Внутреннее ------------------------------------------------------------- + +func _advance(minutes: float) -> void: + _elapsed_minutes += minutes + + # Сигналим по целым минутам, а не каждый кадр: подписчикам нужен такт, + # а не дробное значение. + var whole := int(_elapsed_minutes) + if whole == _last_whole_minute: + return + _last_whole_minute = whole + minute_passed.emit(whole) + + var day := _day_index() + if day == _last_day: + return + _last_day = day + day_passed.emit(day) + + +func _day_index() -> int: + return floori(_elapsed_minutes / float(MINUTES_PER_DAY)) diff --git a/scripts/game/game_clock.gd.uid b/scripts/game/game_clock.gd.uid new file mode 100644 index 0000000..d677756 --- /dev/null +++ b/scripts/game/game_clock.gd.uid @@ -0,0 +1 @@ +uid://7wstqw8ve2he diff --git a/scripts/game/sky_cycle.gd b/scripts/game/sky_cycle.gd new file mode 100644 index 0000000..f0016e2 --- /dev/null +++ b/scripts/game/sky_cycle.gd @@ -0,0 +1,81 @@ +class_name SkyCycle +extends RefCounted +## Палитра неба по времени суток. +## +## Только вычисления, без узлов: по часу суток отдаёт цвета неба, цвет света +## для сцены, яркость звёзд и положение светил. Рисование — в sky_view.gd, +## пейзаж — в surface_view.gd. + +## Ключевые моменты суток; между ними всё интерполируется линейно. +## `light` — общий тон, которым красится пейзаж: он делает утро розоватым, +## полдень белым, а ночь синей. +const KEYFRAMES := [ + {"hour": 0.0, "top": "#040711", "bottom": "#0b1626", "light": "#4a5876", "stars": 1.0}, + {"hour": 4.5, "top": "#070d1e", "bottom": "#1b2340", "light": "#55637f", "stars": 1.0}, + {"hour": 6.0, "top": "#16294a", "bottom": "#6b4a68", "light": "#8a7c92", "stars": 0.35}, + {"hour": 7.0, "top": "#2c5183", "bottom": "#e0895a", "light": "#d0a184", "stars": 0.0}, + {"hour": 9.0, "top": "#3d7cba", "bottom": "#a9d6ec", "light": "#f2f7fa", "stars": 0.0}, + {"hour": 13.0, "top": "#4a90d9", "bottom": "#c3e6f5", "light": "#ffffff", "stars": 0.0}, + {"hour": 17.0, "top": "#4585c8", "bottom": "#d2e2ec", "light": "#f7efe2", "stars": 0.0}, + {"hour": 19.5, "top": "#27456f", "bottom": "#e0734a", "light": "#c78a6e", "stars": 0.05}, + {"hour": 21.0, "top": "#0d1a30", "bottom": "#2b2b4c", "light": "#666b8a", "stars": 0.6}, + {"hour": 24.0, "top": "#040711", "bottom": "#0b1626", "light": "#4a5876", "stars": 1.0}, +] + +const SUNRISE := 6.0 +const SUNSET := 20.0 + + +## Состояние неба на указанный час суток: top, bottom, light, stars. +static func sample(hour: float) -> Dictionary: + var moment := fposmod(hour, 24.0) + + for index in range(KEYFRAMES.size() - 1): + var from: Dictionary = KEYFRAMES[index] + var to: Dictionary = KEYFRAMES[index + 1] + var start := float(from["hour"]) + var end := float(to["hour"]) + if moment < start or moment > end: + continue + + var span := end - start + var weight := 0.0 if is_zero_approx(span) else (moment - start) / span + return { + "top": Color(String(from["top"])).lerp(Color(String(to["top"])), weight), + "bottom": Color(String(from["bottom"])).lerp(Color(String(to["bottom"])), weight), + "light": Color(String(from["light"])).lerp(Color(String(to["light"])), weight), + "stars": lerpf(float(from["stars"]), float(to["stars"]), weight), + } + + var last: Dictionary = KEYFRAMES[KEYFRAMES.size() - 1] + return { + "top": Color(String(last["top"])), + "bottom": Color(String(last["bottom"])), + "light": Color(String(last["light"])), + "stars": float(last["stars"]), + } + + +## Доля пути солнца по небу: 0 — восход слева, 1 — закат справа. +## -1, если солнце под горизонтом. +static func sun_progress(hour: float) -> float: + var moment := fposmod(hour, 24.0) + if moment < SUNRISE or moment > SUNSET: + return -1.0 + return (moment - SUNRISE) / (SUNSET - SUNRISE) + + +## То же для луны — она идёт по небу оставшуюся часть суток. +static func moon_progress(hour: float) -> float: + var moment := fposmod(hour, 24.0) + if moment > SUNRISE and moment < SUNSET: + return -1.0 + + var night_length := 24.0 - (SUNSET - SUNRISE) + var elapsed := moment - SUNSET if moment >= SUNSET else moment + (24.0 - SUNSET) + return elapsed / night_length + + +## Час суток дробным числом из словаря Time. +static func hour_of(moment: Dictionary) -> float: + return float(int(moment["hour"])) + float(int(moment["minute"])) / 60.0 diff --git a/scripts/game/sky_cycle.gd.uid b/scripts/game/sky_cycle.gd.uid new file mode 100644 index 0000000..e3d35d9 --- /dev/null +++ b/scripts/game/sky_cycle.gd.uid @@ -0,0 +1 @@ +uid://gdc8itmwcajy diff --git a/scripts/game/sky_view.gd b/scripts/game/sky_view.gd new file mode 100644 index 0000000..29e7829 --- /dev/null +++ b/scripts/game/sky_view.gd @@ -0,0 +1,115 @@ +class_name SkyView +extends Node2D +## Небо над убежищем: градиент по времени суток, звёзды, солнце и луна. +## +## Рисуется первым, за пейзажем и породой. Что рисовать — решает SkyCycle, +## насколько это видно сквозь облака — WeatherSystem. + +const SUN_TEXTURE := preload("res://background/PNG/sun.png") +const MOON_TEXTURE := preload("res://background/PNG/moon_full.png") + +## Небо начинается здесь и идёт до уровня земли (y = 0). +const TOP := -1700.0 +const LEFT := -1600.0 +const RIGHT := 2900.0 + +## Дуга, по которой идут светила. +const ARC_RADIUS := Vector2(900.0, 620.0) +const ARC_BASE := -40.0 + +const STAR_COUNT := 240 +const STAR_SEED := 20120401 + +## Час суток; сцена обновляет его вслед за часами. +var hour := 6.0 +## Плотность облаков 0..1 — сквозь них хуже видно звёзды и светила. +var clouds := 0.0 + +var _stars: Array[Dictionary] = [] + + +func _ready() -> void: + _generate_stars() + + +## Одним вызовом — чтобы не гонять перерисовку дважды. +func set_conditions(hour_of_day: float, cloudiness: float) -> void: + hour = hour_of_day + clouds = cloudiness + queue_redraw() + + +func _draw() -> void: + var sky := SkyCycle.sample(hour) + _draw_gradient(sky) + _draw_stars(float(sky["stars"])) + _draw_bodies() + + +func _draw_gradient(sky: Dictionary) -> void: + var points := PackedVector2Array([ + Vector2(LEFT, TOP), Vector2(RIGHT, TOP), Vector2(RIGHT, 0.0), Vector2(LEFT, 0.0), + ]) + var top: Color = sky["top"] + var bottom: Color = sky["bottom"] + draw_polygon(points, PackedColorArray([top, top, bottom, bottom])) + + +func _draw_stars(brightness: float) -> void: + # Плотная облачность гасит звёзды почти полностью. + var visibility := brightness * (1.0 - clampf(clouds, 0.0, 1.0) * 0.9) + if visibility <= 0.01: + return + + for star: Dictionary in _stars: + var alpha: float = float(star["alpha"]) * visibility + draw_circle(star["position"], float(star["radius"]), Color(star["tint"], alpha)) + + +func _draw_bodies() -> void: + # Сквозь облака светило просвечивает, но тускло. + var through := 1.0 - clampf(clouds, 0.0, 1.0) * 0.75 + + var sun := SkyCycle.sun_progress(hour) + if sun >= 0.0: + var position := _arc_position(sun) + draw_circle(position, 150.0, Color("#ffd27a", 0.10 * through)) + draw_circle(position, 92.0, Color("#ffdc93", 0.14 * through)) + _draw_body(SUN_TEXTURE, position, 1.7, Color(1.0, 1.0, 1.0, through)) + return + + var moon := SkyCycle.moon_progress(hour) + if moon >= 0.0: + var position := _arc_position(moon) + draw_circle(position, 110.0, Color("#cfe0ff", 0.07 * through)) + _draw_body(MOON_TEXTURE, position, 1.35, Color(1.0, 1.0, 1.0, through)) + + +func _draw_body(texture: Texture2D, position: Vector2, scale: float, tint: Color) -> void: + var size := Vector2(texture.get_size()) * scale + draw_texture_rect(texture, Rect2(position - size * 0.5, size), false, tint) + + +## Точка на дуге: 0 — у левого горизонта, 1 — у правого. +func _arc_position(progress: float) -> Vector2: + var angle := PI * (1.0 - progress) + var center_x := VaultGrid.bounds().get_center().x + return Vector2( + center_x + cos(angle) * ARC_RADIUS.x, + ARC_BASE - sin(angle) * ARC_RADIUS.y + ) + + +func _generate_stars() -> void: + var rng := RandomNumberGenerator.new() + rng.seed = STAR_SEED + + for index in STAR_COUNT: + _stars.append({ + "position": Vector2( + rng.randf_range(LEFT, RIGHT), rng.randf_range(TOP, -140.0) + ), + "radius": rng.randf_range(1.4, 3.6), + "alpha": rng.randf_range(0.35, 1.0), + "tint": Color("#bcd2ff").lerp(Color("#fff2d8"), rng.randf()), + }) diff --git a/scripts/game/sky_view.gd.uid b/scripts/game/sky_view.gd.uid new file mode 100644 index 0000000..f7aceab --- /dev/null +++ b/scripts/game/sky_view.gd.uid @@ -0,0 +1 @@ +uid://ddaqvur2yl61c diff --git a/scripts/game/surface_view.gd b/scripts/game/surface_view.gd new file mode 100644 index 0000000..2e414d5 --- /dev/null +++ b/scripts/game/surface_view.gd @@ -0,0 +1,255 @@ +class_name SurfaceView +extends Node2D +## Поверхность над убежищем: горы, холмы, деревья, облака и осадки. +## +## Текстуры — набор Kenney из `background/PNG/Flat`. Слои рисуются с разной +## скоростью относительно камеры (параллакс) и подмешивают цвет неба тем +## сильнее, чем дальше стоят, — это даёт воздушную перспективу. +## +## Как и остальной вид, всё живёт в _draw(): узлов-спрайтов нет, поэтому +## перекрасить пейзаж под время суток — это один параметр, а не обход дерева. + +const MOUNTAINS := preload("res://background/PNG/Flat/pointy_mountains.png") +const HILLS_FAR := preload("res://background/PNG/Flat/hills2.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") + +## Ближний план берём из основного набора: там деревья и трава уже цветные. +## Набор Flat — бледные силуэты, он идёт на дальние слои, которые всё равно +## перекрашиваются под воздушную перспективу. +const TREE_PATHS := [ + "res://background/PNG/tree01.png", "res://background/PNG/tree05.png", + "res://background/PNG/tree09.png", "res://background/PNG/tree12.png", + "res://background/PNG/tree18.png", "res://background/PNG/tree24.png", + "res://background/PNG/tree30.png", +] +const GRASS_PATHS := [ + "res://background/PNG/grass1.png", "res://background/PNG/grass2.png", + "res://background/PNG/grass3.png", "res://background/PNG/grass4.png", +] + +## Собственный цвет дальних слоёв: сами текстуры почти белые, тон задаём тут. +const MOUNTAIN_TONE := Color("#7e93ad") +const HILLS_FAR_TONE := Color("#6b8f5e") +const HILLS_NEAR_TONE := Color("#5d8248") + +const LEFT := -1600.0 +const RIGHT := 2900.0 +const SCENERY_SEED := 20120401 + +## Полоса дёрна на уровне земли. +const TURF := Color("#4a6b3a") +const TURF_HEIGHT := 16.0 + +## Не ставим деревья вплотную к будке входа, чтобы её не загораживали. +const ENTRANCE_CLEAR := Vector2(-140.0, 300.0) + +## Час суток и погода — сцена обновляет их вслед за часами. +var hour := 6.0 +var clouds := 0.0 +var precipitation := 0.0 +var fog := 0.0 +var wind := 0.3 +var snowing := false +## Множитель хода: 0 на паузе, иначе скорость игры. +var time_scale := 1.0 + +var _trees: Array[Dictionary] = [] +var _grass: Array[Dictionary] = [] +var _drops: Array[Vector2] = [] +var _drift := 0.0 +var _fall := 0.0 + + +func _ready() -> void: + # Плиточная заливка широких полос требует повтора текстуры на узле. + texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED + _scatter_scenery() + _scatter_drops() + + +func _process(delta: float) -> void: + if is_zero_approx(time_scale): + return + var step := delta * time_scale + _drift += step * (18.0 + wind * 120.0) + _fall += step * (900.0 + precipitation * 900.0) + queue_redraw() + + +## Одним вызовом — чтобы не гонять перерисовку на каждый параметр. +func set_conditions( + hour_of_day: float, weather: WeatherSystem +) -> void: + hour = hour_of_day + clouds = weather.clouds + precipitation = weather.precipitation + fog = weather.fog + wind = weather.wind + snowing = weather.is_snowing() + queue_redraw() + + +func _draw() -> void: + var sky := SkyCycle.sample(hour) + var light: Color = sky["light"] + var haze: Color = sky["bottom"] + var view := _view_rect() + + _draw_band(MOUNTAINS, MOUNTAIN_TONE, 1.7, 0.45, 0.25, light, haze, view) + _draw_band(HILLS_FAR, HILLS_FAR_TONE, 1.5, 0.24, 0.5, light, haze, view) + _draw_clouds(view, light) + _draw_band(HILLS_NEAR, HILLS_NEAR_TONE, 1.15, 0.08, 0.75, light, haze, view) + _draw_turf(light) + _draw_scenery(light) + _draw_fog(haze) + _draw_precipitation(view, light) + + +# --- Слои пейзажа ----------------------------------------------------------- + +## Широкая полоса, замощённая по горизонтали и стоящая на уровне земли. +## `distance` — насколько слой выцветает в цвет неба, `parallax` — насколько +## он привязан к миру (1.0 — намертво, меньше — отстаёт от камеры). +func _draw_band( + texture: Texture2D, tone: Color, scale: float, distance: float, parallax: float, + light: Color, haze: Color, view: Rect2 +) -> void: + var size := Vector2(texture.get_size()) * scale + var offset := view.get_center().x * (1.0 - parallax) + var rect := Rect2(Vector2(LEFT + offset, -size.y), Vector2(RIGHT - LEFT, size.y)) + draw_texture_rect(texture, rect, true, (tone * light).lerp(haze, distance)) + + +func _draw_turf(light: Color) -> void: + draw_rect(Rect2(Vector2(LEFT, -TURF_HEIGHT), Vector2(RIGHT - LEFT, TURF_HEIGHT)), TURF * light) + + +func _draw_scenery(light: Color) -> void: + for tuft: Dictionary in _grass: + _draw_item(tuft, light) + for tree: Dictionary in _trees: + _draw_item(tree, light) + + +func _draw_item(item: Dictionary, light: Color) -> void: + var texture: Texture2D = item["texture"] + var size := Vector2(texture.get_size()) * float(item["scale"]) + var origin := Vector2(float(item["x"]) - size.x * 0.5, -size.y + TURF_HEIGHT * 0.4) + draw_texture_rect(texture, Rect2(origin, size), false, light * Color(item["tint"])) + + +# --- Погода ----------------------------------------------------------------- + +func _draw_clouds(view: Rect2, light: Color) -> void: + if clouds <= 0.02: + return + + # Полосы держим выше линии горизонта: их нижний край не должен наезжать + # на землю, поэтому высота и масштаб подобраны в паре. + _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) + + +func _draw_cloud_band( + texture: Texture2D, scale: float, height: float, parallax: float, + weight: float, view: Rect2, light: Color +) -> void: + var size := Vector2(texture.get_size()) * scale + var offset := view.get_center().x * (1.0 - parallax) - _drift * parallax + var rect := Rect2(Vector2(LEFT + offset, height), Vector2(RIGHT - LEFT, size.y)) + var alpha := clampf(clouds * weight, 0.0, 1.0) + draw_texture_rect(texture, rect, true, Color(light, alpha)) + + +## Дымка у горизонта: чем гуще, тем сильнее пейзаж растворяется в небе. +func _draw_fog(haze: Color) -> void: + if fog <= 0.01: + return + var top := -520.0 + var points := PackedVector2Array([ + Vector2(LEFT, top), Vector2(RIGHT, top), Vector2(RIGHT, 0.0), Vector2(LEFT, 0.0), + ]) + var clear := Color(haze, 0.0) + var dense := Color(haze, clampf(fog, 0.0, 1.0) * 0.85) + # Ниже нуля тумана нет — там уже разрез породы. + draw_polygon(points, PackedColorArray([clear, clear, dense, dense])) + + +func _draw_precipitation(view: Rect2, light: Color) -> void: + if precipitation <= 0.02: + return + + # Рисуем только то, что попадает в кадр и находится над землёй. + var band := view.intersection(Rect2(LEFT, -1700.0, RIGHT - LEFT, 1700.0)) + if band.size.x <= 0.0 or band.size.y <= 0.0: + return + + var count := int(float(_drops.size()) * clampf(precipitation, 0.0, 1.0)) + var slant := wind * 0.45 + + for index in count: + var drop := _drops[index] + var x := band.position.x + drop.x * band.size.x + var y := band.position.y + fposmod(drop.y * band.size.y + _fall, band.size.y) + + if snowing: + draw_circle(Vector2(x, y), 3.0, Color(light, 0.75)) + continue + var length := 16.0 + precipitation * 12.0 + draw_line( + Vector2(x, y), Vector2(x - slant * length, y + length), + Color(light.lerp(Color("#9fc4e8"), 0.5), 0.5), 1.5 + ) + + +# --- Расстановка ------------------------------------------------------------ + +func _scatter_scenery() -> void: + var rng := RandomNumberGenerator.new() + rng.seed = SCENERY_SEED + + var trees: Array[Texture2D] = [] + for path: String in TREE_PATHS: + trees.append(load(path) as Texture2D) + + var grass: Array[Texture2D] = [] + for path: String in GRASS_PATHS: + grass.append(load(path) as Texture2D) + + var x := LEFT + while x < RIGHT: + x += rng.randf_range(90.0, 260.0) + if x > ENTRANCE_CLEAR.x and x < ENTRANCE_CLEAR.y: + continue + _trees.append({ + "texture": trees[rng.randi_range(0, trees.size() - 1)], + "x": x, + "scale": rng.randf_range(0.55, 1.0), + "tint": Color("#ffffff").lerp(Color("#9db98a"), rng.randf() * 0.45), + }) + + var tuft_x := LEFT + while tuft_x < RIGHT: + tuft_x += rng.randf_range(34.0, 130.0) + _grass.append({ + "texture": grass[rng.randi_range(0, grass.size() - 1)], + "x": tuft_x, + "scale": rng.randf_range(0.9, 1.7), + "tint": Color("#ffffff"), + }) + + +func _scatter_drops() -> void: + var rng := RandomNumberGenerator.new() + rng.seed = SCENERY_SEED + 7 + for index in 520: + _drops.append(Vector2(rng.randf(), rng.randf())) + + +## Прямоугольник, который сейчас видно камерой, в координатах мира. +func _view_rect() -> Rect2: + var canvas := get_canvas_transform() + var zoom := canvas.get_scale() + return Rect2(-canvas.origin / zoom, get_viewport_rect().size / zoom) diff --git a/scripts/game/surface_view.gd.uid b/scripts/game/surface_view.gd.uid new file mode 100644 index 0000000..b893f7d --- /dev/null +++ b/scripts/game/surface_view.gd.uid @@ -0,0 +1 @@ +uid://c4c36o4o35gqw diff --git a/scripts/game/vault_camera.gd b/scripts/game/vault_camera.gd index 7be2f5f..36faf32 100644 --- a/scripts/game/vault_camera.gd +++ b/scripts/game/vault_camera.gd @@ -10,6 +10,8 @@ const MIN_ZOOM := 0.45 const MAX_ZOOM := 2.2 const KEY_PAN_SPEED := 900.0 const EDGE_MARGIN := 420.0 +## Сверху запас больше: там небо со светилами, и на него хочется смотреть. +const SKY_MARGIN := 900.0 var _dragging := false @@ -20,7 +22,7 @@ func _ready() -> void: limit_right = int(bounds.end.x + EDGE_MARGIN) # Сверху отмеряем от поверхности, а не от бункера: над землёй стоит будка # входа, и до неё нужно доезжать. - limit_top = int(-EDGE_MARGIN) + limit_top = int(-SKY_MARGIN) limit_bottom = int(bounds.end.y + EDGE_MARGIN) # Открываемся на верхних этажах — оттуда видно и вход, и застройку. position = Vector2(bounds.get_center().x, bounds.position.y + VaultGrid.CELL.y) diff --git a/scripts/game/vault_scene.gd b/scripts/game/vault_scene.gd index 1c1df4e..f65c727 100644 --- a/scripts/game/vault_scene.gd +++ b/scripts/game/vault_scene.gd @@ -27,7 +27,15 @@ 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 _clock: GameClock = $Clock +@onready var _weather: WeatherSystem = $Weather +@onready var _sky: SkyView = $Sky +@onready var _surface: SurfaceView = $Surface +@onready var _weather_label: Label = $Hud/Root/TopRight/Row/ClockBar/Body/Moment/WeatherLabel @onready var _stats: HBoxContainer = $Hud/Root/TopBar/Stats +@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 _flow_controls: HBoxContainer = $Hud/Root/TopRight/Row/ClockBar/Body/Controls @onready var _tools: HBoxContainer = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Tools @onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status @@ -41,6 +49,11 @@ var _stat_labels := {} var _tool_group := ButtonGroup.new() var _right_pressed := false var _right_press_position := Vector2.ZERO +var _flow_group := ButtonGroup.new() +var _speed_group := ButtonGroup.new() +var _play_button: Button +var _pause_button: Button +var _speed_buttons := {} func _ready() -> void: @@ -52,7 +65,18 @@ func _ready() -> void: _build_stats() _build_tools() + _build_flow_controls() _refresh_stats() + + _weather.attach_clock(_clock) + _weather.changed.connect(_on_weather_changed) + + _clock.minute_passed.connect(_on_minute_passed) + _clock.flow_changed.connect(_refresh_flow) + _refresh_moment() + _refresh_flow() + _on_weather_changed(_weather.kind) + _status.text = HINT @@ -167,6 +191,93 @@ func _describe(room: Dictionary) -> String: return " · ".join(parts) +# --- Ход времени ------------------------------------------------------------ + +func _build_flow_controls() -> void: + # Тема рассчитана на крупные кнопки меню; для панели времени нужен свой, + # компактный набор стилей. Ресурсы общие на все кнопки — их всего четыре. + var styles := { + "normal": _flow_style("#ffffff0d", "#ffffff1f"), + "hover": _flow_style("#2ee6c51f", "#2ee6c566"), + "pressed": _flow_style("#2ee6c52e", "#2ee6c5cc"), + "focus": _flow_style("#2ee6c51a", "#2ee6c599"), + } + + _play_button = _add_flow_button("▶", _flow_group, styles) + _play_button.pressed.connect(_clock.play) + + _pause_button = _add_flow_button("❚❚", _flow_group, styles) + _pause_button.pressed.connect(_clock.pause) + + var gap := Control.new() + gap.custom_minimum_size.x = 8 + _flow_controls.add_child(gap) + + for multiplier: int in GameClock.SPEEDS: + # Выбор скорости заодно снимает паузу — отдельно жать «play» не нужно. + var button := _add_flow_button("x%d" % multiplier, _speed_group, styles) + button.pressed.connect(_clock.set_speed.bind(multiplier)) + _speed_buttons[multiplier] = button + + +func _add_flow_button(label: String, group: ButtonGroup, styles: Dictionary) -> Button: + var button := Button.new() + button.text = label + button.toggle_mode = true + button.button_group = group + button.custom_minimum_size.x = 34 + button.add_theme_font_size_override("font_size", 13) + for state: String in styles: + button.add_theme_stylebox_override(state, styles[state]) + _flow_controls.add_child(button) + return button + + +func _flow_style(background: String, border: String) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = Color(background) + style.border_color = Color(border) + style.set_border_width_all(1) + style.set_corner_radius_all(5) + style.content_margin_left = 9.0 + style.content_margin_right = 9.0 + style.content_margin_top = 4.0 + style.content_margin_bottom = 4.0 + return style + + +func _on_minute_passed(_total_minutes: int) -> void: + _refresh_moment() + _refresh_environment() + + +func _on_weather_changed(_kind: int) -> void: + _weather_label.text = _weather.label() + + +func _refresh_moment() -> void: + _date_label.text = _clock.date_text() + _time_label.text = _clock.time_text() + + +## Небо и поверхность зависят от одних и тех же двух вещей — часа и погоды. +func _refresh_environment() -> void: + var hour := SkyCycle.hour_of(_clock.now()) + _sky.set_conditions(hour, _weather.clouds) + _surface.set_conditions(hour, _weather) + + +## Кнопки только отражают состояние часов — источник правды там. +func _refresh_flow() -> void: + # Облака и осадки идут в такт игре: на паузе они замирают. + _surface.time_scale = float(_clock.speed) if _clock.running else 0.0 + _play_button.set_pressed_no_signal(_clock.running) + _pause_button.set_pressed_no_signal(not _clock.running) + for multiplier: int in _speed_buttons: + var button: Button = _speed_buttons[multiplier] + button.set_pressed_no_signal(multiplier == _clock.speed) + + # --- HUD -------------------------------------------------------------------- func _build_stats() -> void: @@ -274,6 +385,8 @@ func _on_demolish_toggled(is_on: bool) -> void: func save_data() -> Dictionary: return { "rooms": _grid.to_array(), + "clock": _clock.save_data(), + "weather": _weather.save_data(), "camera": { "x": _camera.position.x, "y": _camera.position.y, @@ -284,6 +397,9 @@ func save_data() -> Dictionary: func load_data(data: Dictionary) -> void: _grid.from_array(data.get("rooms", [])) + _clock.load_data(data.get("clock", {})) + _weather.load_data(data.get("weather", {})) + _refresh_environment() var camera_state: Dictionary = data.get("camera", {}) if not camera_state.is_empty(): diff --git a/scripts/game/vault_view.gd b/scripts/game/vault_view.gd index a492347..5890368 100644 --- a/scripts/game/vault_view.gd +++ b/scripts/game/vault_view.gd @@ -7,8 +7,6 @@ extends Node2D ## Когда появится графика, заменять нужно только этот файл — модель ## (VaultGrid) и логика сцены от него не зависят. -const SKY_TOP := Color("#050912") -const SKY_HORIZON := Color("#16333f") const DIRT_TOP := Color("#2a231d") const DIRT_BOTTOM := Color("#100d0b") const SURFACE := Color("#3d4f3a") @@ -32,7 +30,6 @@ const GHOST_BAD := Color("#f2704a") const SELECTION := Color("#ffffff") const MARGIN := 1200.0 -const SKY_HEIGHT := 900.0 const DIRT_DEPTH := 460.0 const PEBBLE_SEED := 20260810 @@ -74,7 +71,6 @@ func set_selection(cell: Vector2i) -> void: func _draw() -> void: if grid == null: return - _draw_sky() _draw_dirt() _draw_shell() _draw_empty_cells() @@ -89,14 +85,6 @@ func _draw() -> void: # --- Фон -------------------------------------------------------------------- -func _draw_sky() -> void: - var bounds := VaultGrid.bounds() - var left := -MARGIN - var right := bounds.size.x + MARGIN - _fill_gradient(Rect2(Vector2(left, -SKY_HEIGHT), Vector2(right - left, SKY_HEIGHT)), - SKY_TOP, SKY_HORIZON) - - func _draw_dirt() -> void: var bounds := VaultGrid.bounds() var left := -MARGIN diff --git a/scripts/game/weather_system.gd b/scripts/game/weather_system.gd new file mode 100644 index 0000000..5eb28f4 --- /dev/null +++ b/scripts/game/weather_system.gd @@ -0,0 +1,148 @@ +class_name WeatherSystem +extends Node +## Погода: текущий тип, плавные переходы и смена по игровому времени. +## +## Данные и правила, без отрисовки. Вид (surface_view.gd, sky_view.gd) читает +## отсюда `clouds`, `precipitation`, `fog` и `wind` и рисует по ним. +## +## Такт берётся у GameClock, поэтому погода стоит вместе с паузой и ускоряется +## вместе со скоростью игры — своего таймера здесь нет намеренно. + +enum Kind { CLEAR, CLOUDY, OVERCAST, RAIN, STORM, FOG, SNOW } + +## Целевые параметры каждого типа. `clouds` — плотность облаков, `precipitation` +## — сила осадков, `fog` — дымка у горизонта, `wind` — скорость сноса. +const KINDS := { + Kind.CLEAR: {"label": "Ясно", "clouds": 0.12, "precipitation": 0.0, "fog": 0.0, "wind": 0.3}, + Kind.CLOUDY: {"label": "Облачно", "clouds": 0.45, "precipitation": 0.0, "fog": 0.0, "wind": 0.5}, + Kind.OVERCAST: {"label": "Пасмурно", "clouds": 0.85, "precipitation": 0.0, "fog": 0.06, "wind": 0.4}, + 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.FOG: {"label": "Туман", "clouds": 0.3, "precipitation": 0.0, "fog": 0.62, "wind": 0.12}, + Kind.SNOW: {"label": "Снег", "clouds": 0.9, "precipitation": 0.5, "fog": 0.24, "wind": 0.35}, +} + +## Куда погода может перейти и с каким относительным весом. Матрица, а не +## общий мешок: так дождь приходит через пасмурно, а не из ясного неба. +const TRANSITIONS := { + 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.OVERCAST: {Kind.CLOUDY: 4, Kind.OVERCAST: 2, Kind.RAIN: 3, Kind.SNOW: 2}, + Kind.RAIN: {Kind.RAIN: 2, Kind.STORM: 2, Kind.OVERCAST: 5}, + Kind.STORM: {Kind.RAIN: 5, Kind.OVERCAST: 2}, + Kind.FOG: {Kind.FOG: 2, Kind.CLEAR: 4, Kind.CLOUDY: 3}, + 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 MAX_DURATION := 420 +## За сколько игровых минут параметры доходят до целевых. +const BLEND_MINUTES := 45.0 + +## Сменился тип погоды — для HUD и звука. +signal changed(kind: int) + +var kind := Kind.CLEAR +var clouds := 0.12 +var precipitation := 0.0 +var fog := 0.0 +var wind := 0.3 + +var _minutes_left := MIN_DURATION +var _rng := RandomNumberGenerator.new() +var _clock: GameClock + + +func _ready() -> void: + _rng.randomize() + _apply_target(1.0) + + +## Сцена связывает погоду с часами: без этого она стоит на месте. +func attach_clock(clock: GameClock) -> void: + _clock = clock + _clock.minute_passed.connect(_on_minute_passed) + + +func label() -> String: + return String(KINDS[kind]["label"]) + + +## Идёт ли снег, а не дождь — виду нужно рисовать разное. +func is_snowing() -> bool: + return kind == Kind.SNOW + + +func save_data() -> Dictionary: + return { + "kind": int(kind), + "minutes_left": _minutes_left, + "clouds": clouds, + "precipitation": precipitation, + "fog": fog, + "wind": wind, + } + + +func load_data(data: Dictionary) -> void: + kind = clampi(int(data.get("kind", Kind.CLEAR)), 0, KINDS.size() - 1) + _minutes_left = int(data.get("minutes_left", MIN_DURATION)) + clouds = float(data.get("clouds", 0.12)) + precipitation = float(data.get("precipitation", 0.0)) + fog = float(data.get("fog", 0.0)) + wind = float(data.get("wind", 0.3)) + changed.emit(kind) + + +# --- Внутреннее ------------------------------------------------------------- + +func _on_minute_passed(_total_minutes: int) -> void: + _blend_toward_target() + + _minutes_left -= 1 + if _minutes_left > 0: + return + + kind = _roll_next() + _minutes_left = _rng.randi_range(MIN_DURATION, MAX_DURATION) + changed.emit(kind) + + +## Параметры не прыгают вместе с типом: они доходят до цели за BLEND_MINUTES. +func _blend_toward_target() -> void: + _apply_target(1.0 / BLEND_MINUTES) + + +func _apply_target(weight: float) -> void: + var target: Dictionary = KINDS[kind] + clouds = lerpf(clouds, float(target["clouds"]), weight) + precipitation = lerpf(precipitation, float(target["precipitation"]), weight) + fog = lerpf(fog, float(target["fog"]), weight) + wind = lerpf(wind, float(target["wind"]), weight) + + +func _roll_next() -> int: + var options: Dictionary = TRANSITIONS[kind] + var total := 0 + for weight: int in options.values(): + total += weight + + var pick := _rng.randi_range(1, total) + for candidate: int in options: + pick -= int(options[candidate]) + if pick <= 0: + return _seasonal(candidate) + return kind + + +## Снег уместен только зимой; в остальное время он превращается в дождь. +func _seasonal(candidate: int) -> int: + if candidate != Kind.SNOW: + return candidate + if _clock == null: + return Kind.RAIN + var month := int(_clock.now()["month"]) + return Kind.SNOW if WINTER_MONTHS.has(month) else Kind.RAIN diff --git a/scripts/game/weather_system.gd.uid b/scripts/game/weather_system.gd.uid new file mode 100644 index 0000000..8ef3ac0 --- /dev/null +++ b/scripts/game/weather_system.gd.uid @@ -0,0 +1 @@ +uid://by1cwvs3fc3pa