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.
@@ -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 на переднем плане выглядят выцветшими.
|
||||
|
||||
## Автозагрузки
|
||||
|
||||
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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>
|
||||
|
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
|
||||
|
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
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
@@ -0,0 +1 @@
|
||||
uid://7wstqw8ve2he
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://gdc8itmwcajy
|
||||
@@ -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()),
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
uid://ddaqvur2yl61c
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4c36o4o35gqw
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://by1cwvs3fc3pa
|
||||