Refactor game scripts and update vault scene structure. Enhanced organization of game logic by separating data and rendering responsibilities across multiple scripts. Removed unused background assets and updated vault scene to include new game clock and weather system functionalities.

This commit is contained in:
Leonid Pershin
2026-08-11 10:18:30 +03:00
parent 709776116b
commit a81747781d
48 changed files with 979 additions and 4509 deletions
+28 -7
View File
@@ -57,16 +57,37 @@
## Игровой слой ## Игровой слой
Три файла в `scripts/game/`, разделены намеренно: Файлы в `scripts/game/` разделены по принципу «данные и правила отдельно, отрисовка отдельно».
- `vault_grid.gd` — только данные и правила (`RefCounted`, без узлов). Сюда идут изменения Модель и правила — без узлов и без `_draw()`:
логики: типы комнат, размещение, ресурсы.
- `vault_view.gd`только `_draw()`. Сюда идут изменения картинки. Когда появится графика, - `vault_grid.gd`сетка и комнаты. Сюда идут типы комнат, размещение, ресурсы.
переписывается этот файл, остальные не трогаются. Вертикальное смещение бункера под землю — константа `DEPTH`, её учитывают и
- `vault_scene.gd` — связка модели, вида, камеры и HUD плюс контракт сохранения. `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`: кнопка в панели строительства Новый тип комнаты добавляется одной записью в `VaultGrid.KINDS`: кнопка в панели строительства
и подсчёт ресурсов подхватятся сами. и подсчёт ресурсов подхватятся сами. Новый тип погоды — записью в `WeatherSystem.KINDS`
плюс строкой в `TRANSITIONS`.
### Текстуры фона
`background/` — набор Kenney Background Elements. Два стиля, и они не взаимозаменяемы:
`PNG/Flat/*` — бледные, почти белые силуэты, их место на дальних планах, где цвет всё равно
задаётся тонировкой; `PNG/*` — цветные спрайты для ближнего плана. Перепутать легко: деревья
из Flat на переднем плане выглядят выцветшими.
## Автозагрузки ## Автозагрузки
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

@@ -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
@@ -1,111 +0,0 @@
<TextureAtlas imagePath="sprites.png">
<SubTexture name="castle.png" x="740" y="1416" width="205" height="182"/>
<SubTexture name="castle_beige.png" x="743" y="1174" width="204" height="182"/>
<SubTexture name="castle_grey.png" x="745" y="886" width="204" height="182"/>
<SubTexture name="castle_wall.png" x="0" y="886" width="295" height="138"/>
<SubTexture name="castle_wall.png" x="0" y="886" width="295" height="138"/>
<SubTexture name="cloud1.png" x="947" y="1358" width="190" height="127"/>
<SubTexture name="cloud1.png" x="947" y="1358" width="190" height="127"/>
<SubTexture name="cloud2.png" x="934" y="1810" width="200" height="125"/>
<SubTexture name="cloud2.png" x="934" y="1810" width="200" height="125"/>
<SubTexture name="cloud3.png" x="951" y="886" width="177" height="121"/>
<SubTexture name="cloud3.png" x="951" y="886" width="177" height="121"/>
<SubTexture name="cloud4.png" x="297" y="1006" width="228" height="124"/>
<SubTexture name="cloud4.png" x="297" y="1006" width="228" height="124"/>
<SubTexture name="cloud5.png" x="251" y="1873" width="239" height="134"/>
<SubTexture name="cloud5.png" x="251" y="1873" width="239" height="134"/>
<SubTexture name="cloud6.png" x="0" y="1474" width="266" height="138"/>
<SubTexture name="cloud6.png" x="0" y="1474" width="266" height="138"/>
<SubTexture name="cloud7.png" x="288" y="1303" width="234" height="118"/>
<SubTexture name="cloud7.png" x="288" y="1303" width="234" height="118"/>
<SubTexture name="cloud8.png" x="722" y="1857" width="210" height="119"/>
<SubTexture name="cloud8.png" x="722" y="1857" width="210" height="119"/>
<SubTexture name="cloud9.png" x="528" y="1174" width="213" height="119"/>
<SubTexture name="cloud9.png" x="528" y="1174" width="213" height="119"/>
<SubTexture name="clouds1.png" x="0" y="0" width="1001" height="206"/>
<SubTexture name="clouds2.png" x="0" y="638" width="1000" height="246"/>
<SubTexture name="fence.png" x="745" y="1070" width="128" height="88"/>
<SubTexture name="fence.png" x="745" y="1070" width="128" height="88"/>
<SubTexture name="fence_piece.png" x="740" y="1600" width="110" height="86"/>
<SubTexture name="fence_piece.png" x="740" y="1600" width="110" height="86"/>
<SubTexture name="grass1.png" x="160" y="1998" width="26" height="29"/>
<SubTexture name="grass1.png" x="160" y="1998" width="26" height="29"/>
<SubTexture name="grass2.png" x="120" y="1998" width="38" height="35"/>
<SubTexture name="grass2.png" x="120" y="1998" width="38" height="35"/>
<SubTexture name="grass3.png" x="80" y="1998" width="38" height="35"/>
<SubTexture name="grass4.png" x="0" y="2009" width="38" height="35"/>
<SubTexture name="grass5.png" x="216" y="1998" width="26" height="29"/>
<SubTexture name="grass6.png" x="40" y="1998" width="38" height="35"/>
<SubTexture name="hills1.png" x="0" y="378" width="1001" height="128"/>
<SubTexture name="hills2.png" x="0" y="508" width="1001" height="128"/>
<SubTexture name="house_beige_front.png" x="1281" y="1924" width="102" height="115"/>
<SubTexture name="house_beige_side.png" x="533" y="886" width="189" height="115"/>
<SubTexture name="house_front_short.png" x="1002" y="761" width="102" height="115"/>
<SubTexture name="house_front_tall.png" x="1543" y="464" width="102" height="174"/>
<SubTexture name="house_grey_front.png" x="1549" y="1436" width="102" height="174"/>
<SubTexture name="house_grey_side.png" x="1003" y="0" width="174" height="174"/>
<SubTexture name="house_side_short.png" x="949" y="1070" width="189" height="115"/>
<SubTexture name="house_side_tall.png" x="1003" y="176" width="174" height="174"/>
<SubTexture name="moon_full.png" x="1144" y="1629" width="85" height="85"/>
<SubTexture name="moon_half.png" x="1179" y="254" width="84" height="85"/>
<SubTexture name="mountain1.png" x="0" y="1166" width="286" height="306"/>
<SubTexture name="mountain2.png" x="0" y="1754" width="249" height="242"/>
<SubTexture name="mountain3.png" x="268" y="1474" width="245" height="397"/>
<SubTexture name="piramid.png" x="515" y="1689" width="215" height="166"/>
<SubTexture name="piramid.png" x="515" y="1689" width="215" height="166"/>
<SubTexture name="pointy_mountains.png" x="0" y="208" width="1001" height="168"/>
<SubTexture name="sun.png" x="852" y="1600" width="87" height="86"/>
<SubTexture name="temple.png" x="515" y="1423" width="223" height="131"/>
<SubTexture name="temple.png" x="515" y="1423" width="223" height="131"/>
<SubTexture name="tower.png" x="1648" y="1612" width="66" height="227"/>
<SubTexture name="tower_beige.png" x="1647" y="262" width="66" height="227"/>
<SubTexture name="tower_grey.png" x="1641" y="1164" width="66" height="227"/>
<SubTexture name="tree01.png" x="1281" y="1629" width="136" height="293"/>
<SubTexture name="tree01.png" x="1281" y="1629" width="136" height="293"/>
<SubTexture name="tree02.png" x="1285" y="901" width="136" height="293"/>
<SubTexture name="tree02.png" x="1285" y="901" width="136" height="293"/>
<SubTexture name="tree03.png" x="1287" y="1196" width="136" height="293"/>
<SubTexture name="tree03.png" x="1287" y="1196" width="136" height="293"/>
<SubTexture name="tree04.png" x="1275" y="606" width="136" height="293"/>
<SubTexture name="tree04.png" x="1275" y="606" width="136" height="293"/>
<SubTexture name="tree05.png" x="1322" y="0" width="129" height="230"/>
<SubTexture name="tree05.png" x="1322" y="0" width="129" height="230"/>
<SubTexture name="tree06.png" x="1420" y="232" width="128" height="230"/>
<SubTexture name="tree06.png" x="1420" y="232" width="128" height="230"/>
<SubTexture name="tree07.png" x="1419" y="1723" width="128" height="230"/>
<SubTexture name="tree07.png" x="1419" y="1723" width="128" height="230"/>
<SubTexture name="tree08.png" x="1652" y="491" width="59" height="285"/>
<SubTexture name="tree08.png" x="1652" y="491" width="59" height="285"/>
<SubTexture name="tree09.png" x="1682" y="0" width="59" height="245"/>
<SubTexture name="tree09.png" x="1682" y="0" width="59" height="245"/>
<SubTexture name="tree10.png" x="1425" y="1193" width="106" height="241"/>
<SubTexture name="tree10.png" x="1425" y="1193" width="106" height="241"/>
<SubTexture name="tree11.png" x="1140" y="1068" width="143" height="305"/>
<SubTexture name="tree11.png" x="1140" y="1068" width="143" height="305"/>
<SubTexture name="tree12.png" x="1003" y="352" width="141" height="252"/>
<SubTexture name="tree12.png" x="1003" y="352" width="141" height="252"/>
<SubTexture name="tree13.png" x="1413" y="486" width="128" height="230"/>
<SubTexture name="tree13.png" x="1413" y="486" width="128" height="230"/>
<SubTexture name="tree14.png" x="1059" y="1487" width="48" height="105"/>
<SubTexture name="tree15.png" x="1064" y="1937" width="48" height="105"/>
<SubTexture name="tree16.png" x="1550" y="262" width="70" height="144"/>
<SubTexture name="tree17.png" x="1715" y="247" width="53" height="144"/>
<SubTexture name="tree18.png" x="1180" y="606" width="70" height="144"/>
<SubTexture name="tree19.png" x="1653" y="1393" width="53" height="144"/>
<SubTexture name="tree20.png" x="1553" y="640" width="97" height="260"/>
<SubTexture name="tree21.png" x="1583" y="0" width="97" height="260"/>
<SubTexture name="tree22.png" x="1639" y="902" width="97" height="260"/>
<SubTexture name="tree23.png" x="1713" y="491" width="59" height="285"/>
<SubTexture name="tree24.png" x="1770" y="1039" width="59" height="285"/>
<SubTexture name="tree25.png" x="1743" y="0" width="59" height="245"/>
<SubTexture name="tree26.png" x="1716" y="1686" width="59" height="245"/>
<SubTexture name="tree27.png" x="1716" y="1425" width="59" height="259"/>
<SubTexture name="tree28.png" x="1738" y="778" width="59" height="259"/>
<SubTexture name="tree29.png" x="1136" y="1727" width="143" height="305"/>
<SubTexture name="tree30.png" x="1130" y="761" width="143" height="305"/>
<SubTexture name="tree31.png" x="1146" y="352" width="141" height="252"/>
<SubTexture name="tree32.png" x="1179" y="0" width="141" height="252"/>
<SubTexture name="tree33.png" x="1144" y="1375" width="141" height="252"/>
<SubTexture name="tree34.png" x="1289" y="254" width="129" height="230"/>
<SubTexture name="tree35.png" x="1453" y="0" width="128" height="230"/>
</TextureAtlas>
Binary file not shown.
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 110 KiB

@@ -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
Binary file not shown.
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 60 KiB

@@ -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
+86
View File
@@ -1,10 +1,14 @@
[gd_scene format=3 uid="uid://dprxap0ioe3ps"] [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://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://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="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="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://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"]
content_margin_left = 22.0 content_margin_left = 22.0
@@ -38,9 +42,31 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14 corner_radius_bottom_right = 14
corner_radius_bottom_left = 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] [node name="Vault" type="Node2D" unique_id=1382797618]
script = ExtResource("1_o8yry") 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] [node name="View" type="Node2D" parent="." unique_id=1238774489]
script = ExtResource("2_mkhle") 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 theme_override_font_sizes/font_size = 15
horizontal_alignment = 1 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="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")
+142
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://7wstqw8ve2he
+81
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://gdc8itmwcajy
+115
View File
@@ -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()),
})
+1
View File
@@ -0,0 +1 @@
uid://ddaqvur2yl61c
+255
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://c4c36o4o35gqw
+3 -1
View File
@@ -10,6 +10,8 @@ const MIN_ZOOM := 0.45
const MAX_ZOOM := 2.2 const MAX_ZOOM := 2.2
const KEY_PAN_SPEED := 900.0 const KEY_PAN_SPEED := 900.0
const EDGE_MARGIN := 420.0 const EDGE_MARGIN := 420.0
## Сверху запас больше: там небо со светилами, и на него хочется смотреть.
const SKY_MARGIN := 900.0
var _dragging := false var _dragging := false
@@ -20,7 +22,7 @@ func _ready() -> void:
limit_right = int(bounds.end.x + EDGE_MARGIN) 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) limit_bottom = int(bounds.end.y + EDGE_MARGIN)
# Открываемся на верхних этажах — оттуда видно и вход, и застройку. # Открываемся на верхних этажах — оттуда видно и вход, и застройку.
position = Vector2(bounds.get_center().x, bounds.position.y + VaultGrid.CELL.y) position = Vector2(bounds.get_center().x, bounds.position.y + VaultGrid.CELL.y)
+116
View File
@@ -27,7 +27,15 @@ enum Mode { SELECT, BUILD, DEMOLISH }
@onready var _saves: SaveManagerService = get_node("/root/SaveManager") @onready var _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _vault: VaultView = $View @onready var _vault: VaultView = $View
@onready var _camera: VaultCamera = $Camera @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 _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 _tools: HBoxContainer = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Tools
@onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status @onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status
@@ -41,6 +49,11 @@ var _stat_labels := {}
var _tool_group := ButtonGroup.new() var _tool_group := ButtonGroup.new()
var _right_pressed := false var _right_pressed := false
var _right_press_position := Vector2.ZERO 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: func _ready() -> void:
@@ -52,7 +65,18 @@ func _ready() -> void:
_build_stats() _build_stats()
_build_tools() _build_tools()
_build_flow_controls()
_refresh_stats() _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 _status.text = HINT
@@ -167,6 +191,93 @@ func _describe(room: Dictionary) -> String:
return " · ".join(parts) 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 -------------------------------------------------------------------- # --- HUD --------------------------------------------------------------------
func _build_stats() -> void: func _build_stats() -> void:
@@ -274,6 +385,8 @@ func _on_demolish_toggled(is_on: bool) -> void:
func save_data() -> Dictionary: func save_data() -> Dictionary:
return { return {
"rooms": _grid.to_array(), "rooms": _grid.to_array(),
"clock": _clock.save_data(),
"weather": _weather.save_data(),
"camera": { "camera": {
"x": _camera.position.x, "x": _camera.position.x,
"y": _camera.position.y, "y": _camera.position.y,
@@ -284,6 +397,9 @@ func save_data() -> Dictionary:
func load_data(data: Dictionary) -> void: func load_data(data: Dictionary) -> void:
_grid.from_array(data.get("rooms", [])) _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", {}) var camera_state: Dictionary = data.get("camera", {})
if not camera_state.is_empty(): if not camera_state.is_empty():
-12
View File
@@ -7,8 +7,6 @@ extends Node2D
## Когда появится графика, заменять нужно только этот файл — модель ## Когда появится графика, заменять нужно только этот файл — модель
## (VaultGrid) и логика сцены от него не зависят. ## (VaultGrid) и логика сцены от него не зависят.
const SKY_TOP := Color("#050912")
const SKY_HORIZON := Color("#16333f")
const DIRT_TOP := Color("#2a231d") const DIRT_TOP := Color("#2a231d")
const DIRT_BOTTOM := Color("#100d0b") const DIRT_BOTTOM := Color("#100d0b")
const SURFACE := Color("#3d4f3a") const SURFACE := Color("#3d4f3a")
@@ -32,7 +30,6 @@ const GHOST_BAD := Color("#f2704a")
const SELECTION := Color("#ffffff") const SELECTION := Color("#ffffff")
const MARGIN := 1200.0 const MARGIN := 1200.0
const SKY_HEIGHT := 900.0
const DIRT_DEPTH := 460.0 const DIRT_DEPTH := 460.0
const PEBBLE_SEED := 20260810 const PEBBLE_SEED := 20260810
@@ -74,7 +71,6 @@ func set_selection(cell: Vector2i) -> void:
func _draw() -> void: func _draw() -> void:
if grid == null: if grid == null:
return return
_draw_sky()
_draw_dirt() _draw_dirt()
_draw_shell() _draw_shell()
_draw_empty_cells() _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: func _draw_dirt() -> void:
var bounds := VaultGrid.bounds() var bounds := VaultGrid.bounds()
var left := -MARGIN var left := -MARGIN
+148
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://by1cwvs3fc3pa