Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc94fa8aa |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "airdef:block/barge"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "airdef:block/launch_ramp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "airdef:block/launcher_truck"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,9 @@
|
||||
"block.airdef.radar_truck": "Шасси РЛС",
|
||||
"block.airdef.depot": "Склад боеприпасов",
|
||||
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
|
||||
"block.airdef.barge": "Баржа",
|
||||
"block.airdef.launch_ramp": "Рампа запуска дронов",
|
||||
"block.airdef.launcher_truck": "Пусковая машина ракет",
|
||||
"block.airdef.ew_truck": "Станция РЭБ",
|
||||
"block.airdef.searchlight": "Прожектор",
|
||||
"block.airdef.searchlight_head": "Голова прожектора",
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"block.airdef.radar_truck": "Шасси РЛС",
|
||||
"block.airdef.depot": "Склад боеприпасов",
|
||||
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
|
||||
"block.airdef.barge": "Баржа",
|
||||
"block.airdef.launch_ramp": "Рампа запуска дронов",
|
||||
"block.airdef.launcher_truck": "Пусковая машина ракет",
|
||||
"block.airdef.ew_truck": "Станция РЭБ",
|
||||
"block.airdef.searchlight": "Прожектор",
|
||||
"block.airdef.searchlight_head": "Голова прожектора",
|
||||
|
||||
@@ -0,0 +1,896 @@
|
||||
{
|
||||
"ambientocclusion": false,
|
||||
"textures": {
|
||||
"dark_metal": "airdef:block/dark_metal",
|
||||
"metal": "airdef:block/metal",
|
||||
"ru_khaki_s31": "airdef:block/ru_khaki_s31",
|
||||
"ru_khaki_s33": "airdef:block/ru_khaki_s33",
|
||||
"particle": "airdef:block/dark_metal"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [
|
||||
-16.0,
|
||||
0.0,
|
||||
-16.0
|
||||
],
|
||||
"to": [
|
||||
32.0,
|
||||
3.0,
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-16.0,
|
||||
3.0,
|
||||
-16.0
|
||||
],
|
||||
"to": [
|
||||
32.0,
|
||||
4.0,
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-15.0,
|
||||
3.0,
|
||||
-15.0
|
||||
],
|
||||
"to": [
|
||||
31.0,
|
||||
4.4,
|
||||
31.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#ru_khaki_s31"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#ru_khaki_s31"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#ru_khaki_s31"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#ru_khaki_s31"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"texture": "#ru_khaki_s33"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"texture": "#ru_khaki_s33"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-16.0,
|
||||
4.0,
|
||||
-16.0
|
||||
],
|
||||
"to": [
|
||||
-14.0,
|
||||
6.0,
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
30.0,
|
||||
4.0,
|
||||
-16.0
|
||||
],
|
||||
"to": [
|
||||
32.0,
|
||||
6.0,
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-16.0,
|
||||
4.0,
|
||||
30.0
|
||||
],
|
||||
"to": [
|
||||
32.0,
|
||||
6.0,
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-16.0,
|
||||
4.0,
|
||||
-16.0
|
||||
],
|
||||
"to": [
|
||||
32.0,
|
||||
6.0,
|
||||
-14.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-12.0,
|
||||
4.4,
|
||||
-10.0
|
||||
],
|
||||
"to": [
|
||||
-9.0,
|
||||
6.5,
|
||||
-7.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
25.0,
|
||||
4.4,
|
||||
-10.0
|
||||
],
|
||||
"to": [
|
||||
28.0,
|
||||
6.5,
|
||||
-7.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-12.0,
|
||||
4.4,
|
||||
8.0
|
||||
],
|
||||
"to": [
|
||||
-9.0,
|
||||
6.5,
|
||||
11.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
25.0,
|
||||
4.4,
|
||||
8.0
|
||||
],
|
||||
"to": [
|
||||
28.0,
|
||||
6.5,
|
||||
11.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
-12.0,
|
||||
4.4,
|
||||
26.0
|
||||
],
|
||||
"to": [
|
||||
-9.0,
|
||||
6.5,
|
||||
29.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
25.0,
|
||||
4.4,
|
||||
26.0
|
||||
],
|
||||
"to": [
|
||||
28.0,
|
||||
6.5,
|
||||
29.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
2.0999999999999996
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
{
|
||||
"ambientocclusion": false,
|
||||
"textures": {
|
||||
"dark_metal": "airdef:block/dark_metal",
|
||||
"metal": "airdef:block/metal",
|
||||
"ru_khaki": "airdef:block/ru_khaki",
|
||||
"particle": "airdef:block/dark_metal"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [
|
||||
1.0,
|
||||
0.0,
|
||||
-6.0
|
||||
],
|
||||
"to": [
|
||||
15.0,
|
||||
2.0,
|
||||
30.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
16
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
6.0,
|
||||
2.0,
|
||||
0.0
|
||||
],
|
||||
"to": [
|
||||
10.0,
|
||||
4.0,
|
||||
26.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.0,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.0,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
3.0,
|
||||
9.0,
|
||||
-14.0
|
||||
],
|
||||
"to": [
|
||||
5.0,
|
||||
11.0,
|
||||
30.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
},
|
||||
"rotation": {
|
||||
"angle": 22.5,
|
||||
"axis": "x",
|
||||
"origin": [
|
||||
4.0,
|
||||
10.0,
|
||||
8.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
11.0,
|
||||
9.0,
|
||||
-14.0
|
||||
],
|
||||
"to": [
|
||||
13.0,
|
||||
11.0,
|
||||
30.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
16
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
},
|
||||
"rotation": {
|
||||
"angle": 22.5,
|
||||
"axis": "x",
|
||||
"origin": [
|
||||
12.0,
|
||||
10.0,
|
||||
8.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
3.0,
|
||||
0.0,
|
||||
-1.0
|
||||
],
|
||||
"to": [
|
||||
5.0,
|
||||
12.0,
|
||||
1.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
11.0,
|
||||
0.0,
|
||||
-1.0
|
||||
],
|
||||
"to": [
|
||||
13.0,
|
||||
12.0,
|
||||
1.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
12.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
3.0,
|
||||
0.0,
|
||||
17.0
|
||||
],
|
||||
"to": [
|
||||
5.0,
|
||||
6.0,
|
||||
19.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
11.0,
|
||||
0.0,
|
||||
17.0
|
||||
],
|
||||
"to": [
|
||||
13.0,
|
||||
6.0,
|
||||
19.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
4.0,
|
||||
10.0,
|
||||
24.0
|
||||
],
|
||||
"to": [
|
||||
12.0,
|
||||
14.0,
|
||||
30.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
1.0,
|
||||
2.0,
|
||||
28.0
|
||||
],
|
||||
"to": [
|
||||
15.0,
|
||||
6.0,
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#ru_khaki"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#ru_khaki"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#ru_khaki"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#ru_khaki"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#ru_khaki"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
14.0,
|
||||
4.0
|
||||
],
|
||||
"texture": "#ru_khaki"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
@@ -12,6 +12,7 @@ use std::collections::HashMap;
|
||||
mod damage;
|
||||
pub mod fires;
|
||||
mod raid;
|
||||
pub mod sites;
|
||||
pub mod record;
|
||||
mod sight;
|
||||
mod terrain;
|
||||
@@ -1424,6 +1425,8 @@ pub struct Sim {
|
||||
later: Vec<(u64, DVec3, u32, bool)>,
|
||||
/// A planned raid's drones waiting for their launch tick.
|
||||
pub queue: Vec<(u64, Drone)>,
|
||||
/// The raid's launch sites and their jobs (#100).
|
||||
pub sites: sites::Sites,
|
||||
/// A raid being planned.
|
||||
plan: Option<raid::Plan>,
|
||||
/// Each marked object's health, 0 to 100 (`targets`' order).
|
||||
@@ -1492,6 +1495,7 @@ impl Sim {
|
||||
horizons: HashMap::new(),
|
||||
later: Vec::new(),
|
||||
queue: Vec::new(),
|
||||
sites: sites::Sites::default(),
|
||||
plan: None,
|
||||
target_health: Vec::new(),
|
||||
wave: None,
|
||||
@@ -1597,6 +1601,7 @@ impl Sim {
|
||||
self.fx.clear();
|
||||
self.blasts.clear();
|
||||
self.queue.clear();
|
||||
self.sites.clear();
|
||||
self.plan = None;
|
||||
self.wave = None;
|
||||
// Ammunition still to cook off, flares in the sky.
|
||||
@@ -1611,7 +1616,7 @@ impl Sim {
|
||||
self.tick += 1;
|
||||
self.plan_step(Some(&mut *ground), out);
|
||||
self.adapt();
|
||||
self.release_queue();
|
||||
self.release_queue(out);
|
||||
self.step_drones(ground, player, out);
|
||||
self.step_later(ground, player, out);
|
||||
self.step_moves(ground, out);
|
||||
@@ -1623,6 +1628,7 @@ impl Sim {
|
||||
self.step_missiles(ground, out);
|
||||
self.step_fx(ground, out);
|
||||
self.draw_targets(out);
|
||||
self.draw_sites(out);
|
||||
self.step_wrecks(out);
|
||||
self.step_fires(ground, out);
|
||||
self.report_wave(out);
|
||||
|
||||
+200
-14
@@ -69,6 +69,9 @@ struct Item {
|
||||
key: Option<(DroneKind, i32, i32, i32)>,
|
||||
early: f64,
|
||||
gap: f64,
|
||||
/// The site it is a job of: the want (see `sites`) and its pad there.
|
||||
want: usize,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
/// A raid being planned: its drones, the prices and routes so far, and the planned ones.
|
||||
@@ -78,8 +81,12 @@ pub(super) struct Plan {
|
||||
routes: HashMap<(DroneKind, i32, i32, i32), Vec<DVec3>>,
|
||||
items: Vec<Item>,
|
||||
next: usize,
|
||||
/// (drone, flight time, how early it arrives, its wave's delay)
|
||||
done: Vec<(Drone, f64, f64, f64)>,
|
||||
/// (drone, flight time, how early it arrives, its wave's delay, its site)
|
||||
done: Vec<(Drone, f64, f64, f64, u32)>,
|
||||
/// The launch sites wanted, and whether they are settled; the ticks spent waiting for the game's answers.
|
||||
wants: Vec<sites::Want>,
|
||||
sited: bool,
|
||||
site_wait: u32,
|
||||
smart: bool,
|
||||
approach: Approach,
|
||||
total: u32,
|
||||
@@ -242,7 +249,9 @@ impl Sim {
|
||||
let smart = order.aim == Aim::Smart;
|
||||
// A smart enemy comes from where its raids got through best.
|
||||
let bearing = if smart { self.best_bearing() } else { self.rng.f() * TAU };
|
||||
let second = bearing + std::f64::consts::PI * (0.6 + self.rng.f() * 0.6);
|
||||
// The raid's sides (1, 2, or 4-5): its launch sites go there (#100).
|
||||
let sides = sites::pick_sides(&mut self.rng, order.approach, bearing);
|
||||
let mut wants: Vec<sites::Want> = Vec::new();
|
||||
// One grid over the whole raid; each kind's prices are worked out once, routes are shared
|
||||
// by the drones of a kind going from the same bearing to the same target.
|
||||
let span = order.dist + 450.0;
|
||||
@@ -252,25 +261,31 @@ impl Sim {
|
||||
for (group, &(kind, count)) in order.kinds.iter().enumerate() {
|
||||
let spec = kind.spec();
|
||||
for _ in 0..count {
|
||||
let a = match order.approach {
|
||||
Approach::Sector | Approach::Waves => bearing + self.rng.s() * 0.35,
|
||||
Approach::TwoSectors => (if self.rng.chance(0.5) { bearing } else { second }) + self.rng.s() * 0.3,
|
||||
Approach::AllSides => self.rng.f() * TAU,
|
||||
let side = match order.approach {
|
||||
Approach::Sector | Approach::Waves => 0,
|
||||
Approach::TwoSectors => usize::from(self.rng.chance(0.5)),
|
||||
Approach::AllSides => (self.rng.f() * sides.len() as f64) as usize % sides.len(),
|
||||
};
|
||||
let a = sides[side];
|
||||
let object = objects[(self.rng.f() * objects.len() as f64) as usize % objects.len()];
|
||||
let (target, at_sam) = self.choose_target(kind, object, order.aim);
|
||||
let aim = target + DVec3::new(self.rng.s() * 4.0, 0.0, self.rng.s() * 4.0);
|
||||
let dist = spec.ballistic.map_or(order.dist, |far| far.max(order.dist)) + self.rng.f() * 50.0;
|
||||
let height = if spec.ballistic.is_some() { 10.0 } else if spec.cruise > 0.0 { spec.cruise } else { 25.0 };
|
||||
// Where it starts is provisional: the site's pad, once the site is placed.
|
||||
let from = centre + DVec3::new(a.sin() * dist, height + self.rng.s() * 6.0, a.cos() * dist);
|
||||
let mut d = self.make_drone(kind, from, horizontal(aim - from), aim);
|
||||
let range = spec.ballistic.map_or(order.dist, |far| far.max(order.dist));
|
||||
let want = sites::want_index(&mut wants, centre, side, a, sites::SiteClass::of(kind), range, order.dist.max(120.0));
|
||||
let pad = wants[want].jobs;
|
||||
wants[want].jobs += 1;
|
||||
d.sector = ew::sector(a) as u8;
|
||||
let routed = smart && spec.ballistic.is_none() && !spec.decoy && !at_sam.is_some_and(|_| kind == DroneKind::Lancet);
|
||||
let key = routed.then(|| (kind, (a.rem_euclid(TAU) / 0.35) as i32, (target.x / 24.0) as i32, (target.z / 24.0) as i32));
|
||||
// Decoys come 15 s ahead, the rest together; "waves" one kind after another.
|
||||
let early = if spec.decoy { 300.0 } else { 0.0 };
|
||||
let gap = if order.approach == Approach::Waves { group as f64 * 300.0 } else { 0.0 };
|
||||
items.push(Item { d, from, aim, key, early, gap });
|
||||
items.push(Item { d, from, aim, key, early, gap, want, pad });
|
||||
}
|
||||
}
|
||||
self.plan = Some(Plan {
|
||||
@@ -280,6 +295,9 @@ impl Sim {
|
||||
items,
|
||||
next: 0,
|
||||
done: Vec::new(),
|
||||
wants,
|
||||
sited: false,
|
||||
site_wait: 0,
|
||||
smart,
|
||||
approach: order.approach,
|
||||
total,
|
||||
@@ -297,6 +315,20 @@ impl Sim {
|
||||
/// Works on the raid's plan a while (a tick's share), and launches it when it is ready.
|
||||
pub(super) fn plan_step(&mut self, ground: Option<&mut Ground>, out: &mut Out) {
|
||||
let Some(mut plan) = self.plan.take() else { return };
|
||||
let mut ground = ground;
|
||||
// First the launch sites: suitable ground the game has told of, or no raid (#100).
|
||||
if !plan.sited {
|
||||
let Some(g) = ground.as_deref_mut() else {
|
||||
self.plan = Some(plan);
|
||||
return;
|
||||
};
|
||||
if !self.settle_sites(&mut plan, g, out) {
|
||||
if !plan.items.is_empty() {
|
||||
self.plan = Some(plan);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// A smart raid first learns the land under its grid: asked for in bulk, a thousand columns a tick, for
|
||||
// up to two seconds (what the game has no land for stays unknown and costs nothing).
|
||||
if !plan.mapped {
|
||||
@@ -342,7 +374,7 @@ impl Sim {
|
||||
let mut d = plan.items[i].d.clone();
|
||||
// It starts over the land, not in it: a hill under the starting point lifts it.
|
||||
let mut from = from;
|
||||
if !plan.heights.is_empty() {
|
||||
if !plan.heights.is_empty() && plan.wants[plan.items[i].want].found.is_none() {
|
||||
let (gi, gj) = plan.grid.index(from);
|
||||
let g = plan.heights[(gi * plan.grid.size + gj) as usize];
|
||||
let floor = f64::from(g) + if spec.ballistic.is_some() { 10.0 } else { spec.clearance + 10.0 };
|
||||
@@ -388,7 +420,7 @@ impl Sim {
|
||||
Flight::Ballistic => (horizontal(aim - from).length() / spec.speed).max(30.0),
|
||||
_ => path / spec.speed,
|
||||
};
|
||||
plan.done.push((d, time, plan.items[i].early, plan.items[i].gap));
|
||||
plan.done.push((d, time, plan.items[i].early, plan.items[i].gap, plan.wants[plan.items[i].want].site));
|
||||
plan.next += 1;
|
||||
work += 1;
|
||||
}
|
||||
@@ -398,10 +430,13 @@ impl Sim {
|
||||
}
|
||||
// Timing: smart raids arrive at once; others leave at once (a moment apart).
|
||||
let now = self.tick;
|
||||
let arrive = plan.done.iter().map(|(_, t, e, g)| t + e - g).fold(0.0, f64::max);
|
||||
for (mut d, time, early, gap) in plan.done {
|
||||
let arrive = plan.done.iter().map(|(_, t, e, g, _)| t + e - g).fold(0.0, f64::max);
|
||||
for (mut d, time, early, gap, site) in plan.done {
|
||||
d.smart = plan.smart;
|
||||
let wait = if plan.smart { arrive - (early - gap) - time } else { gap + self.rng.f() * 40.0 };
|
||||
if site != 0 {
|
||||
self.sites.job(d.id, site);
|
||||
}
|
||||
self.queue.push((now + wait.clamp(0.0, 2400.0) as u64, d));
|
||||
}
|
||||
self.wave = Some(WaveStats {
|
||||
@@ -575,17 +610,156 @@ impl Sim {
|
||||
}
|
||||
|
||||
/// A planned raid's drones take off when their time comes.
|
||||
pub(super) fn release_queue(&mut self) {
|
||||
pub(super) fn release_queue(&mut self, out: &mut Out) {
|
||||
let now = self.tick;
|
||||
if self.queue.iter().all(|q| q.0 > now) {
|
||||
return;
|
||||
}
|
||||
let (go, wait): (Vec<_>, Vec<_>) = std::mem::take(&mut self.queue).into_iter().partition(|q| q.0 <= now);
|
||||
self.queue = wait;
|
||||
for (_, d) in go {
|
||||
let mut cancelled = 0;
|
||||
for (t, d) in go {
|
||||
// A site's job leaves its site one at a time, from its stock; a destroyed or empty site cancels it.
|
||||
match self.site_slot(d.id) {
|
||||
sites::Slot::Free => {}
|
||||
sites::Slot::Busy => {
|
||||
self.queue.push((t, d));
|
||||
continue;
|
||||
}
|
||||
sites::Slot::Gone => {
|
||||
cancelled += 1;
|
||||
continue;
|
||||
}
|
||||
sites::Slot::Go => {
|
||||
let dir = d.vel.normalize_or_zero();
|
||||
out.sound(d.kind.launch_sound(), d.pos, 1.0, 1.0);
|
||||
out.particles("minecraft:flame", d.pos - dir * 1.5, 0.15, -dir * 0.1, 0.02, 4);
|
||||
out.particles("minecraft:large_smoke", d.pos - dir * 2.5, 0.5, DVec3::new(0.0, 0.05, 0.0), 0.02, 4);
|
||||
}
|
||||
}
|
||||
self.drones.push(d);
|
||||
self.launched += 1;
|
||||
}
|
||||
if cancelled > 0 {
|
||||
out.messages.push(format!("Пусковая площадка не запустила {cancelled}: нет запаса или уничтожена"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks for the raid's launch sites. Returns whether they are settled (placed, or given up: drones whose
|
||||
/// side has no suitable ground go to another side's site, or are cancelled with a message; a raid left with
|
||||
/// nothing is cancelled whole).
|
||||
fn settle_sites(&mut self, plan: &mut Plan, ground: &mut Ground, out: &mut Out) -> bool {
|
||||
plan.site_wait += 1;
|
||||
let mut keep: Vec<DVec3> = self.sams.iter().filter(|s| s.alive()).map(|s| s.pos).collect();
|
||||
keep.extend(self.targets.iter().copied());
|
||||
keep.push(plan.centre);
|
||||
let last = plan.site_wait >= sites::SEARCH_TICKS;
|
||||
if !sites::search(&mut plan.wants, ground, plan.centre, &keep, self.tick, last) {
|
||||
return false;
|
||||
}
|
||||
use sites::Search;
|
||||
// A side that found nothing hands its drones to a site of the same class and range on another side.
|
||||
let mut moved = 0;
|
||||
let mut dropped = 0;
|
||||
let found: Vec<usize> = (0..plan.wants.len()).filter(|&i| plan.wants[i].state == Search::Found).collect();
|
||||
let mut keep_items = Vec::new();
|
||||
for mut it in std::mem::take(&mut plan.items) {
|
||||
let w = &plan.wants[it.want];
|
||||
if w.state != Search::Found {
|
||||
let (class, range) = (w.class, w.ordered);
|
||||
let near = |x: &sites::Want| x.class == class && (x.ordered - range).abs() < range * 0.35 + 30.0;
|
||||
match found.iter().copied().find(|&i| near(&plan.wants[i])) {
|
||||
Some(i) => {
|
||||
it.want = i;
|
||||
moved += 1;
|
||||
}
|
||||
None => {
|
||||
dropped += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
keep_items.push(it);
|
||||
}
|
||||
plan.items = keep_items;
|
||||
if moved + dropped > 0 {
|
||||
out.messages.push(format!(
|
||||
"Нет пригодной земли для части площадок: {moved} целей ушли с другой стороны, {dropped} отменено"
|
||||
));
|
||||
}
|
||||
plan.total -= dropped;
|
||||
if plan.items.is_empty() {
|
||||
out.messages.push("Налёт отменён: нет пригодной известной земли для пусковых площадок".into());
|
||||
return false;
|
||||
}
|
||||
// The pads, and the sites themselves; the old raid's sites go unless they still have jobs to fly.
|
||||
let queued: Vec<u32> = self.queue.iter().map(|q| q.1.id).collect();
|
||||
self.sites.retire(&queued);
|
||||
for w in plan.wants.iter_mut() {
|
||||
w.jobs = 0;
|
||||
}
|
||||
for it in plan.items.iter_mut() {
|
||||
it.pad = plan.wants[it.want].jobs;
|
||||
plan.wants[it.want].jobs += 1;
|
||||
}
|
||||
let mut shortened = false;
|
||||
for w in plan.wants.iter_mut().filter(|w| w.state == Search::Found && w.jobs > 0) {
|
||||
let mut pos = w.found.unwrap_or_default();
|
||||
if w.water {
|
||||
pos.y += sites::DECK;
|
||||
}
|
||||
shortened |= w.range < w.ordered - if w.class == sites::SiteClass::Battery { 1.0 } else { 75.0 };
|
||||
w.site = self.sites.add(sites::LaunchSite {
|
||||
id: 0,
|
||||
side: w.side as u8,
|
||||
sector: ew::sector(w.bearing) as u8,
|
||||
bearing: w.bearing,
|
||||
class: w.class,
|
||||
pos,
|
||||
range: w.range,
|
||||
water: w.water,
|
||||
stock: w.jobs,
|
||||
planned: w.jobs,
|
||||
alive: true,
|
||||
next_free: 0,
|
||||
});
|
||||
}
|
||||
if shortened {
|
||||
out.messages.push("Пуск ближе обычного: дальше нет известной земли или воды".into());
|
||||
}
|
||||
// Each drone onto its pad, climbing off the ramp or leaving the tube (ballistic ones aim as always).
|
||||
for it in plan.items.iter_mut() {
|
||||
let w = &plan.wants[it.want];
|
||||
let Some(site) = self.sites.get(w.site) else { continue };
|
||||
let from = site.muzzle(it.pad);
|
||||
let spec = it.d.kind.spec();
|
||||
it.from = from;
|
||||
it.d.pos = from;
|
||||
if spec.ballistic.is_some() {
|
||||
let flat = horizontal(it.d.target - from);
|
||||
let time = (flat.length() / spec.speed).max(30.0);
|
||||
it.d.vel = flat / time + DVec3::Y * ((it.d.target.y - from.y + 0.5 * GRAVITY * time * time) / time);
|
||||
} else {
|
||||
let pitch = site.class.pitch();
|
||||
let dir = horizontal(it.d.target - from).normalize_or(site.inward());
|
||||
it.d.vel = (dir * pitch.cos() + DVec3::Y * pitch.sin()) * spec.speed;
|
||||
}
|
||||
it.d.sector = site.sector;
|
||||
}
|
||||
let n = self.sites.list.iter().filter(|s| plan.wants.iter().any(|w| w.site == s.id)).count();
|
||||
let barges = self.sites.list.iter().filter(|s| s.water && plan.wants.iter().any(|w| w.site == s.id)).count();
|
||||
out.messages.push(format!("Пусковые площадки противника: {n}{}", if barges > 0 { format!(" (на баржах: {barges})") } else { String::new() }));
|
||||
// Where they are is the enemy's secret, unless the sandbox's true picture is on.
|
||||
if self.truth {
|
||||
for site in self.sites.list.iter().filter(|s| plan.wants.iter().any(|w| w.site == s.id)) {
|
||||
out.messages.push(format!(
|
||||
"Площадка #{} ({:?}, сторона {}): {:.0} {:.0} {:.0}, запас {}",
|
||||
site.id, site.class, site.side, site.pos.x, site.pos.y, site.pos.z, site.stock
|
||||
));
|
||||
}
|
||||
}
|
||||
plan.sited = true;
|
||||
true
|
||||
}
|
||||
|
||||
/// When the raid is over: what it cost both sides.
|
||||
@@ -632,6 +806,7 @@ fn route_length(from: DVec3, route: &[DVec3], to: DVec3) -> f64 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::sites::testland::{Land, serve};
|
||||
|
||||
#[test]
|
||||
fn a_smart_raid_plans_and_flies_in_time() {
|
||||
@@ -641,8 +816,10 @@ mod tests {
|
||||
sim.sams.push(Sam::new(id, kind, DVec3::new(-30.0 + 15.0 * i as f64, 70.0, 0.0), 0.0));
|
||||
}
|
||||
let mut ground = Ground::default();
|
||||
let land = Land(|_, _| 64);
|
||||
let mut out = Out::default();
|
||||
for _ in 0..30 {
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
@@ -655,6 +832,7 @@ mod tests {
|
||||
sim.order_raid(&order, &[DVec3::new(0.0, 70.0, -40.0)], &mut out);
|
||||
let mut ticks = 0;
|
||||
while sim.planning() {
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
ticks += 1;
|
||||
}
|
||||
@@ -663,6 +841,7 @@ mod tests {
|
||||
for k in 0..4000 {
|
||||
let s = std::time::Instant::now();
|
||||
out = Out::default();
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
assert!(s.elapsed().as_millis() < 200, "tick {k} took {:?}", s.elapsed());
|
||||
}
|
||||
@@ -679,8 +858,10 @@ mod tests {
|
||||
sim.sams.push(Sam::new(id, SamKind::Tor, DVec3::new(2.0 * i as f64, 70.0, 0.0), 0.0));
|
||||
}
|
||||
let mut ground = Ground::default();
|
||||
let land = Land(|_, _| 64);
|
||||
let mut out = Out::default();
|
||||
for _ in 0..30 {
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
}
|
||||
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, -400.0), DVec3::Z, DVec3::new(0.0, 70.0, 0.0));
|
||||
@@ -697,6 +878,7 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod big {
|
||||
use super::*;
|
||||
use super::sites::testland::{Land, serve};
|
||||
|
||||
#[test]
|
||||
fn sixty_one_systems_and_a_big_smart_raid() {
|
||||
@@ -710,8 +892,10 @@ mod big {
|
||||
sim.targets.push(DVec3::new(0.0, 70.0, 0.0));
|
||||
sim.target_health.push(100.0);
|
||||
let mut ground = Ground::default();
|
||||
let land = Land(|_, _| 64);
|
||||
let mut out = Out::default();
|
||||
for _ in 0..200 {
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
}
|
||||
let mut w = crate::wave::WaveConfig::default();
|
||||
@@ -725,6 +909,7 @@ mod big {
|
||||
let mut slowest = std::time::Duration::ZERO;
|
||||
while sim.planning() {
|
||||
let s = std::time::Instant::now();
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
slowest = slowest.max(s.elapsed());
|
||||
ticks += 1;
|
||||
@@ -734,6 +919,7 @@ mod big {
|
||||
for _ in 0..3000 {
|
||||
let s = std::time::Instant::now();
|
||||
out = Out::default();
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
worst = worst.max(s.elapsed());
|
||||
}
|
||||
|
||||
@@ -1348,11 +1348,13 @@ fn raids_have_the_same_strike_accuracy_as_manual_launches() {
|
||||
sim.intel.revealed.push((id, DVec3::new(0.0, 70.0, 900.0), 0));
|
||||
let order = RaidOrder { kinds: vec![(DroneKind::Zircon, 20)], approach: Approach::Sector, dist: 400.0, aim: Aim::Objects };
|
||||
sim.order_raid(&order, &[DVec3::new(0.0, 70.0, 0.0)], &mut Out::default());
|
||||
// (The raid's launch sites need land the game has told of: a flat one is served.)
|
||||
let (mut ground, land) = (Ground::default(), sites::testland::Land(|_, _| 64));
|
||||
for _ in 0..40 {
|
||||
if !sim.planning() {
|
||||
break;
|
||||
}
|
||||
sim.step(&mut Ground::default(), None, &mut Out::default());
|
||||
sites::testland::step(&mut sim, &mut ground, &land, &mut Out::default());
|
||||
}
|
||||
assert!(!sim.queue.is_empty() && sim.queue.iter().all(|(_, d)| d.err != DVec3::ZERO), "raid drones without a navigation error");
|
||||
let mut d = sim.make_drone(DroneKind::Zircon, DVec3::new(0.0, 100.0, -500.0), DVec3::Z, DVec3::new(0.0, 70.0, 0.0));
|
||||
|
||||
@@ -0,0 +1,832 @@
|
||||
//! Launch sites (#100): where a raid really starts from. A raid picks its sides (one, two, or four to five),
|
||||
//! looks for suitable ground on each side (only land the game has told of: flat, open to the sky, clear in
|
||||
//! front, away from the defence), and sets up a site there: a ramp for the drones, a launcher vehicle for the
|
||||
//! cruise missiles, a battery for the ballistic ones (at their own range). The raid's drones are jobs of the
|
||||
//! sites: each one leaves its site, one at a time, from the stock the site holds. Ground nobody has told of is
|
||||
//! never suitable, and nothing is force-loaded: a side without suitable ground waits for the game's answers
|
||||
//! for a few seconds, and is then given up with a message.
|
||||
use super::*;
|
||||
|
||||
/// The most candidate places tried for one site, and how long (ticks) a raid waits for the game's answers.
|
||||
pub const MAX_TRIES: usize = 90;
|
||||
pub const SEARCH_TICKS: u32 = 300;
|
||||
/// Candidates asked about ahead of the one being waited for.
|
||||
const PREFETCH: usize = 3;
|
||||
/// The footprint's samples (offsets in blocks), the most the ground may differ over it, and how far from
|
||||
/// the defence and the objects a site must be.
|
||||
const FOOTPRINT: [(f64, f64); 9] = [(0.0, 0.0), (8.0, 0.0), (-8.0, 0.0), (0.0, 8.0), (0.0, -8.0), (6.0, 6.0), (6.0, -6.0), (-6.0, 6.0), (-6.0, -6.0)];
|
||||
const FLAT: i32 = 2;
|
||||
const KEEP_OUT: f64 = 45.0;
|
||||
|
||||
/// What a site is for.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum SiteClass {
|
||||
/// Drones: a catapult ramp.
|
||||
Ramp,
|
||||
/// Cruise and supersonic missiles: a launcher vehicle.
|
||||
Launcher,
|
||||
/// Ballistic missiles: a heavier launcher, far out.
|
||||
Battery,
|
||||
}
|
||||
|
||||
impl SiteClass {
|
||||
pub fn of(kind: DroneKind) -> SiteClass {
|
||||
let spec = kind.spec();
|
||||
if spec.ballistic.is_some() {
|
||||
SiteClass::Battery
|
||||
} else if matches!(spec.class, Class::Drone | Class::Jet) {
|
||||
SiteClass::Ramp
|
||||
} else {
|
||||
SiteClass::Launcher
|
||||
}
|
||||
}
|
||||
/// Whether a site of this class can launch this kind.
|
||||
pub fn supports(self, kind: DroneKind) -> bool {
|
||||
SiteClass::of(kind) == self
|
||||
}
|
||||
/// Ticks between two launches from one site.
|
||||
pub fn cadence(self) -> u64 {
|
||||
match self {
|
||||
SiteClass::Ramp => 3,
|
||||
SiteClass::Launcher => 8,
|
||||
SiteClass::Battery => 20,
|
||||
}
|
||||
}
|
||||
fn slots(self) -> u32 {
|
||||
match self {
|
||||
SiteClass::Ramp => 4,
|
||||
SiteClass::Launcher => 3,
|
||||
SiteClass::Battery => 2,
|
||||
}
|
||||
}
|
||||
fn spacing(self) -> f64 {
|
||||
match self {
|
||||
SiteClass::Ramp => 4.0,
|
||||
SiteClass::Launcher => 9.0,
|
||||
SiteClass::Battery => 12.0,
|
||||
}
|
||||
}
|
||||
fn model(self) -> &'static str {
|
||||
match self {
|
||||
SiteClass::Ramp => "airdef:launch_ramp",
|
||||
SiteClass::Launcher | SiteClass::Battery => "airdef:launcher_truck",
|
||||
}
|
||||
}
|
||||
fn scale(self) -> f64 {
|
||||
match self {
|
||||
SiteClass::Ramp => 1.4,
|
||||
SiteClass::Launcher => 1.5,
|
||||
SiteClass::Battery => 1.9,
|
||||
}
|
||||
}
|
||||
/// The barge's scale: its deck is 3 blocks a side at 1.
|
||||
fn barge(self) -> f64 {
|
||||
match self {
|
||||
SiteClass::Ramp => 4.5,
|
||||
_ => 7.0,
|
||||
}
|
||||
}
|
||||
/// Where a drone leaves the kit: blocks ahead of its centre, and up from the ground.
|
||||
fn muzzle(self) -> (f64, f64) {
|
||||
match self {
|
||||
SiteClass::Ramp => (1.8, 1.7),
|
||||
SiteClass::Launcher => (2.6, 3.4),
|
||||
SiteClass::Battery => (3.2, 4.2),
|
||||
}
|
||||
}
|
||||
/// How steeply a drone leaves it (radians).
|
||||
pub fn pitch(self) -> f64 {
|
||||
match self {
|
||||
SiteClass::Ramp => 0.35,
|
||||
SiteClass::Launcher => 0.6,
|
||||
SiteClass::Battery => 0.9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One launch site of a raid: stable id, side, where, what it can launch, what is left, and when it may launch next.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LaunchSite {
|
||||
pub id: u32,
|
||||
/// Which side of the raid it is on (0-4) and the enemy's sector of it.
|
||||
pub side: u8,
|
||||
pub sector: u8,
|
||||
pub bearing: f64,
|
||||
pub class: SiteClass,
|
||||
/// The ground at its centre (the top of the surface), and how far from the raid's centre it is.
|
||||
pub pos: DVec3,
|
||||
pub range: f64,
|
||||
/// On a barge over open water (its `pos` is the water's surface plus the deck).
|
||||
pub water: bool,
|
||||
/// Rounds left, and the jobs given to it (also how many pads are drawn).
|
||||
pub stock: u32,
|
||||
pub planned: u32,
|
||||
pub alive: bool,
|
||||
pub next_free: u64,
|
||||
}
|
||||
|
||||
impl LaunchSite {
|
||||
/// The direction a launch goes: from the site in, towards the raid's centre.
|
||||
pub fn inward(&self) -> DVec3 {
|
||||
DVec3::new(-self.bearing.sin(), 0.0, -self.bearing.cos())
|
||||
}
|
||||
fn lateral(&self) -> DVec3 {
|
||||
DVec3::new(self.bearing.cos(), 0.0, -self.bearing.sin())
|
||||
}
|
||||
/// The base of pad `i` on the ground.
|
||||
pub fn pad(&self, i: u32) -> DVec3 {
|
||||
let n = self.class.slots();
|
||||
self.pos + self.lateral() * ((f64::from(i % n) - f64::from(n - 1) / 2.0) * self.class.spacing())
|
||||
}
|
||||
/// Where a drone of pad `i` leaves the kit.
|
||||
pub fn muzzle(&self, i: u32) -> DVec3 {
|
||||
let (ahead, up) = self.class.muzzle();
|
||||
self.pad(i) + self.inward() * ahead + DVec3::Y * up
|
||||
}
|
||||
pub fn supports(&self, kind: DroneKind) -> bool {
|
||||
self.class.supports(kind)
|
||||
}
|
||||
}
|
||||
|
||||
/// The sites in play and the launch jobs given to them: (drone id, site id) for each drone still in the queue.
|
||||
#[derive(Default)]
|
||||
pub struct Sites {
|
||||
pub list: Vec<LaunchSite>,
|
||||
next: u32,
|
||||
jobs: Vec<(u32, u32)>,
|
||||
}
|
||||
|
||||
/// What a site says about a drone due to leave.
|
||||
pub(super) enum Slot {
|
||||
/// Not a site's job (a manual launch, an old save): off it goes.
|
||||
Free,
|
||||
Go,
|
||||
/// The site launched a moment ago.
|
||||
Busy,
|
||||
/// The site is gone or empty: the job is cancelled.
|
||||
Gone,
|
||||
}
|
||||
|
||||
impl Sites {
|
||||
pub fn get(&self, id: u32) -> Option<&LaunchSite> {
|
||||
self.list.iter().find(|s| s.id == id)
|
||||
}
|
||||
/// Jobs waiting for a site.
|
||||
pub fn pending(&self, id: u32) -> usize {
|
||||
self.jobs.iter().filter(|j| j.1 == id).count()
|
||||
}
|
||||
/// The site a queued drone is a job of.
|
||||
pub fn site_of(&self, drone: u32) -> Option<u32> {
|
||||
self.jobs.iter().find(|j| j.0 == drone).map(|j| j.1)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.list.clear();
|
||||
self.jobs.clear();
|
||||
}
|
||||
pub(super) fn add(&mut self, mut site: LaunchSite) -> u32 {
|
||||
self.next += 1;
|
||||
site.id = self.next;
|
||||
let id = site.id;
|
||||
self.list.push(site);
|
||||
id
|
||||
}
|
||||
pub(super) fn job(&mut self, drone: u32, site: u32) {
|
||||
self.jobs.push((drone, site));
|
||||
}
|
||||
/// The old raid's sites go, but not those with jobs still to fly.
|
||||
pub(super) fn retire(&mut self, queued: &[u32]) {
|
||||
self.jobs.retain(|j| queued.contains(&j.0));
|
||||
let jobs = &self.jobs;
|
||||
self.list.retain(|s| jobs.iter().any(|j| j.1 == s.id));
|
||||
}
|
||||
fn take(&mut self, drone: u32, now: u64) -> Slot {
|
||||
let Some(j) = self.jobs.iter().position(|j| j.0 == drone) else { return Slot::Free };
|
||||
let site = self.jobs[j].1;
|
||||
let Some(s) = self.list.iter_mut().find(|s| s.id == site) else {
|
||||
self.jobs.swap_remove(j);
|
||||
return Slot::Gone;
|
||||
};
|
||||
if !s.alive || s.stock == 0 {
|
||||
self.jobs.swap_remove(j);
|
||||
return Slot::Gone;
|
||||
}
|
||||
if s.next_free > now {
|
||||
return Slot::Busy;
|
||||
}
|
||||
s.stock -= 1;
|
||||
s.next_free = now + s.class.cadence();
|
||||
self.jobs.swap_remove(j);
|
||||
Slot::Go
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- choosing sides and places
|
||||
|
||||
/// The sides of a raid as bearings: one, two far apart (108° or more), or four to five spread round (at least
|
||||
/// 43° apart). The seed decides which.
|
||||
pub fn pick_sides(rng: &mut Rng, approach: Approach, bearing: f64) -> Vec<f64> {
|
||||
match approach {
|
||||
Approach::Sector | Approach::Waves => vec![bearing],
|
||||
Approach::TwoSectors => vec![bearing, bearing + std::f64::consts::PI * (0.6 + rng.f() * 0.6)],
|
||||
Approach::AllSides => {
|
||||
let n = if rng.chance(0.5) { 4 } else { 5 };
|
||||
let step = TAU / f64::from(n);
|
||||
(0..n).map(|k| bearing + f64::from(k) * step + rng.s() * 0.2 * step).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Search {
|
||||
Open,
|
||||
Found,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// A site wanted by a raid: its side and class, the places to try in order, and how the search stands.
|
||||
pub(super) struct Want {
|
||||
pub side: usize,
|
||||
pub bearing: f64,
|
||||
pub class: SiteClass,
|
||||
pub range: f64,
|
||||
/// The range ordered (`range` is where the site stood in the end).
|
||||
pub ordered: f64,
|
||||
cands: Vec<(DVec3, bool)>,
|
||||
cursor: usize,
|
||||
pub found: Option<DVec3>,
|
||||
pub water: bool,
|
||||
pub state: Search,
|
||||
pub jobs: u32,
|
||||
pub site: u32,
|
||||
}
|
||||
|
||||
/// The want of this side, class and range, made if there is none yet.
|
||||
pub(super) fn want_index(wants: &mut Vec<Want>, centre: DVec3, side: usize, bearing: f64, class: SiteClass, range: f64, lo: f64) -> usize {
|
||||
if let Some(i) = wants.iter().position(|w| w.side == side && w.class == class && (w.range - range).abs() < 1.0) {
|
||||
return i;
|
||||
}
|
||||
let out = DVec3::new(bearing.sin(), 0.0, bearing.cos());
|
||||
let lat = DVec3::new(bearing.cos(), 0.0, -bearing.sin());
|
||||
// In order: known land at the ordered range (round it for most, the ordered range for a ballistic battery),
|
||||
// known land nearer in steps, then a barge on open water at the ordered range, then on water nearer.
|
||||
let near = |from: f64| -> Vec<f64> {
|
||||
let min = if class == SiteClass::Battery { lo.min(from).max(80.0) } else { (from * 0.3).max(80.0) };
|
||||
(1..=6).map(|i| from - (from - min) * f64::from(i) / 6.0).collect()
|
||||
};
|
||||
let round: Vec<f64> = if class == SiteClass::Battery {
|
||||
vec![range]
|
||||
} else {
|
||||
[0.0, 24.0, -24.0, 48.0, -48.0, 72.0, -72.0].iter().map(|d| (range + d).max(KEEP_OUT)).collect()
|
||||
};
|
||||
let laterals: &[f64] = &[0.0, 20.0, -20.0];
|
||||
let mut cands = Vec::new();
|
||||
for water in [false, true] {
|
||||
for r in round.iter().copied().chain(near(range)) {
|
||||
for l in laterals {
|
||||
cands.push((centre + out * r + lat * *l, water));
|
||||
}
|
||||
}
|
||||
}
|
||||
cands.truncate(MAX_TRIES);
|
||||
wants.push(Want { side, bearing, class, range, ordered: range, cands, cursor: 0, found: None, water: false, state: Search::Open, jobs: 0, site: 0 });
|
||||
wants.len() - 1
|
||||
}
|
||||
|
||||
enum Verdict {
|
||||
/// The game hasn't told enough yet.
|
||||
Pending,
|
||||
Bad,
|
||||
/// Suitable: the surface at its centre, and whether it is water's.
|
||||
Good(DVec3, bool),
|
||||
}
|
||||
|
||||
/// Whether a site fits at `p` (x, z): every sample of its footprint is land the game told of and within
|
||||
/// `FLAT` blocks of the others, the sky above is open, the way ahead is clear, and nothing of the defence is near.
|
||||
fn judge(ground: &mut Ground, p: DVec3, inward: DVec3, keep: &[DVec3], now: u64) -> Verdict {
|
||||
if p.x.abs() > 29_000_000.0 || p.z.abs() > 29_000_000.0 || keep.iter().any(|k| horizontal(*k - p).length() < KEEP_OUT) {
|
||||
return Verdict::Bad;
|
||||
}
|
||||
let (mut lo, mut hi, mut pending) = (i32::MAX, i32::MIN, false);
|
||||
for (dx, dz) in FOOTPRINT {
|
||||
match ground.column((p.x + dx).floor() as i32, (p.z + dz).floor() as i32, 256) {
|
||||
None => pending = true,
|
||||
// No land to tell of (the sea, the void): never a site.
|
||||
Some(None) => return Verdict::Bad,
|
||||
Some(Some(h)) => {
|
||||
lo = lo.min(h as i32);
|
||||
hi = hi.max(h as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
if pending {
|
||||
return Verdict::Pending;
|
||||
}
|
||||
if hi - lo > FLAT {
|
||||
return Verdict::Bad;
|
||||
}
|
||||
let top = f64::from(hi);
|
||||
let at = DVec3::new(p.x.floor() + 0.5, top, p.z.floor() + 0.5);
|
||||
// The game's height is the top of what blocks motion *or holds a fluid*: the block under the surface is
|
||||
// solid at the footprint's centre and edges (land), or at none of them (water); a shore is neither.
|
||||
let mut solid = 0;
|
||||
for (dx, dz) in FOOTPRINT.iter().take(5) {
|
||||
let c = DVec3::new((p.x + dx).floor() + 0.5, top, (p.z + dz).floor() + 0.5);
|
||||
match ground.world.sweep(c + DVec3::Y * 0.5, c - DVec3::Y * 0.5, false, now) {
|
||||
world::Sweep::Hit { .. } => solid += 1,
|
||||
world::Sweep::Clear => {}
|
||||
world::Sweep::Unknown { pending: true, .. } | world::Sweep::Wait { .. } => return Verdict::Pending,
|
||||
world::Sweep::Unknown { .. } => return Verdict::Bad,
|
||||
}
|
||||
}
|
||||
if solid != 0 && solid != 5 {
|
||||
return Verdict::Bad;
|
||||
}
|
||||
let up = ground.world.sweep(at + DVec3::Y * 30.0, at + DVec3::Y * 1.0, false, now);
|
||||
let ahead = ground.world.sweep(at + DVec3::Y * 3.0, at + inward * 14.0 + DVec3::Y * 3.0, false, now);
|
||||
for s in [up, ahead] {
|
||||
match s {
|
||||
world::Sweep::Clear => {}
|
||||
world::Sweep::Unknown { pending: true, .. } | world::Sweep::Wait { .. } => return Verdict::Pending,
|
||||
world::Sweep::Unknown { .. } | world::Sweep::Hit { .. } => return Verdict::Bad,
|
||||
}
|
||||
}
|
||||
Verdict::Good(at, solid == 0)
|
||||
}
|
||||
|
||||
/// Works on the open wants: each tries its places in order, a place the game hasn't told enough of holds the
|
||||
/// search (the next few are asked about meanwhile), and a bad place is passed over. After `last` nothing waits.
|
||||
/// Returns whether every want is settled.
|
||||
pub(super) fn search(wants: &mut [Want], ground: &mut Ground, centre: DVec3, keep: &[DVec3], now: u64, last: bool) -> bool {
|
||||
let mut all = true;
|
||||
for k in 0..wants.len() {
|
||||
if wants[k].state != Search::Open {
|
||||
continue;
|
||||
}
|
||||
// Two sites never share a place: the others' kit is in the way.
|
||||
let mut keep: Vec<DVec3> = keep.to_vec();
|
||||
keep.extend(wants.iter().filter_map(|o| o.found));
|
||||
let w = &mut wants[k];
|
||||
let inward = DVec3::new(-w.bearing.sin(), 0.0, -w.bearing.cos());
|
||||
loop {
|
||||
if w.cursor >= w.cands.len() {
|
||||
w.state = Search::Failed;
|
||||
break;
|
||||
}
|
||||
let (cand, water) = w.cands[w.cursor];
|
||||
match judge(ground, cand, inward, &keep, now) {
|
||||
Verdict::Bad => w.cursor += 1,
|
||||
// Land when land was asked for, a barge when water was.
|
||||
Verdict::Good(_, got) if got != water => w.cursor += 1,
|
||||
Verdict::Good(at, _) => {
|
||||
w.found = Some(at);
|
||||
w.water = water;
|
||||
w.range = horizontal(at - centre).length();
|
||||
w.state = Search::Found;
|
||||
break;
|
||||
}
|
||||
Verdict::Pending => {
|
||||
for k in 1..=PREFETCH {
|
||||
if let Some(&(c, _)) = w.cands.get(w.cursor + k) {
|
||||
let _ = judge(ground, c, inward, &keep, now);
|
||||
}
|
||||
}
|
||||
if last {
|
||||
w.state = Search::Failed;
|
||||
} else {
|
||||
all = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- in the sim
|
||||
|
||||
/// Id base of the kit's models.
|
||||
const KIT: u32 = 0x7400_0000;
|
||||
/// The barge's deck over the water.
|
||||
pub const DECK: f64 = 0.3;
|
||||
|
||||
impl Sim {
|
||||
/// Takes site `id` out of play (destroyed): its stock is gone and its queued jobs are cancelled. Returns how
|
||||
/// many. (The counter-strike, #101, uses this.)
|
||||
pub fn destroy_site(&mut self, id: u32) -> u32 {
|
||||
let Some(s) = self.sites.list.iter_mut().find(|s| s.id == id) else { return 0 };
|
||||
s.alive = false;
|
||||
s.stock = 0;
|
||||
let ids: Vec<u32> = self.sites.jobs.iter().filter(|j| j.1 == id).map(|j| j.0).collect();
|
||||
self.queue.retain(|q| !ids.contains(&q.1.id));
|
||||
self.sites.jobs.retain(|j| j.1 != id);
|
||||
ids.len() as u32
|
||||
}
|
||||
|
||||
/// Asks a site whether a queued drone may leave now.
|
||||
pub(super) fn site_slot(&mut self, drone: u32) -> Slot {
|
||||
let now = self.tick;
|
||||
self.sites.take(drone, now)
|
||||
}
|
||||
|
||||
/// Draws every site's kit: a ramp or launcher per pad in use, turned to the way the launches go.
|
||||
pub(super) fn draw_sites(&self, out: &mut Out) {
|
||||
for s in self.sites.list.iter().filter(|s| s.alive) {
|
||||
let rot = DQuat::from_rotation_y(heading_of(s.inward()));
|
||||
let sc = s.class.scale();
|
||||
if s.water {
|
||||
let b = s.class.barge();
|
||||
out.model(KIT + (s.id * 8 + 7) * PARTS, "airdef:barge", s.pos - DVec3::Y * (DECK + 0.8 - 0.5 * b), rot, b, false);
|
||||
}
|
||||
for i in 0..s.planned.min(s.class.slots()) {
|
||||
let id = KIT + (s.id * 8 + i) * PARTS;
|
||||
out.model(id, s.class.model(), s.pad(i) + DVec3::Y * 0.5 * sc, rot, sc, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) mod testland {
|
||||
use super::*;
|
||||
|
||||
/// Land from a height function; blocks below it are solid, and the game's height map says the same.
|
||||
pub struct Land<F: Fn(i32, i32) -> i32>(pub F);
|
||||
impl<F: Fn(i32, i32) -> i32> world::WorldSource for Land<F> {
|
||||
fn solid(&self, x: i32, y: i32, z: i32) -> Option<bool> {
|
||||
Some(y < (self.0)(x, z))
|
||||
}
|
||||
}
|
||||
|
||||
/// What the game answers a tick after the sim asked: heights, and the sections.
|
||||
pub fn serve<F: Fn(i32, i32) -> i32>(ground: &mut Ground, land: &Land<F>, now: u64) {
|
||||
let columns = ground.take_wanted().into_iter().map(|(x, z)| (x, z, (land.0)(x, z))).collect();
|
||||
ground.answer(columns, now);
|
||||
let sections = ground.world.take_wanted(now);
|
||||
ground.world.serve(land, sections, now);
|
||||
}
|
||||
|
||||
pub fn step<F: Fn(i32, i32) -> i32>(sim: &mut Sim, ground: &mut Ground, land: &Land<F>, out: &mut Out) {
|
||||
serve(ground, land, sim.tick);
|
||||
sim.step(ground, None, out);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::testland::*;
|
||||
use super::*;
|
||||
|
||||
fn sep(a: f64, b: f64) -> f64 {
|
||||
angle_diff(a, b).abs().to_degrees()
|
||||
}
|
||||
|
||||
fn min_sep(sides: &[f64]) -> f64 {
|
||||
let mut m = 360.0f64;
|
||||
for i in 0..sides.len() {
|
||||
for j in 0..i {
|
||||
m = m.min(sep(sides[i], sides[j]));
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sector_modes_give_one_two_or_four_to_five_sides_apart() {
|
||||
let mut fours = 0;
|
||||
let mut fives = 0;
|
||||
for seed in 1..200u64 {
|
||||
for approach in Approach::ALL {
|
||||
let mut rng = Rng::new(seed);
|
||||
let b = rng.f() * TAU;
|
||||
let sides = pick_sides(&mut rng, approach, b);
|
||||
match approach {
|
||||
Approach::Sector | Approach::Waves => assert_eq!(sides.len(), 1),
|
||||
Approach::TwoSectors => {
|
||||
assert_eq!(sides.len(), 2);
|
||||
assert!(min_sep(&sides) >= 100.0, "two sectors only {:.0} deg apart", min_sep(&sides));
|
||||
}
|
||||
Approach::AllSides => {
|
||||
assert!((4..=5).contains(&sides.len()), "{} sides", sides.len());
|
||||
assert!(min_sep(&sides) >= 40.0, "all sides: two only {:.0} deg apart", min_sep(&sides));
|
||||
if sides.len() == 4 {
|
||||
fours += 1;
|
||||
} else {
|
||||
fives += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(fours > 20 && fives > 20, "the seed should pick both 4 and 5: {fours} / {fives}");
|
||||
}
|
||||
|
||||
fn flat() -> Land<impl Fn(i32, i32) -> i32> {
|
||||
Land(|_, _| 64)
|
||||
}
|
||||
|
||||
fn order(approach: Approach, dist: f64) -> RaidOrder {
|
||||
RaidOrder {
|
||||
kinds: vec![(DroneKind::Shahed, 6), (DroneKind::Gerbera, 3), (DroneKind::Kh101, 3), (DroneKind::Iskander, 2)],
|
||||
approach,
|
||||
dist,
|
||||
aim: Aim::Objects,
|
||||
}
|
||||
}
|
||||
|
||||
/// Orders a raid over `land` and plans it to the end; returns the ticks it took.
|
||||
fn plan<F: Fn(i32, i32) -> i32>(sim: &mut Sim, ground: &mut Ground, land: &Land<F>, o: &RaidOrder, out: &mut Out) -> u32 {
|
||||
sim.order_raid(o, &[DVec3::new(0.0, 70.0, 0.0)], out);
|
||||
let mut n = 0;
|
||||
while sim.planning() {
|
||||
step(sim, ground, land, out);
|
||||
n += 1;
|
||||
assert!(n < 1000, "the raid never finished planning");
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sites_per_sector_mode_launch_every_drone_from_a_site_on_a_side() {
|
||||
for (approach, sides) in [(Approach::Sector, 1usize), (Approach::TwoSectors, 2), (Approach::AllSides, 5), (Approach::Waves, 1)] {
|
||||
let (land, mut ground, mut out) = (flat(), Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(21);
|
||||
plan(&mut sim, &mut ground, &land, &order(approach, 300.0), &mut out);
|
||||
let mut sides_used: Vec<u8> = sim.sites.list.iter().map(|s| s.side).collect();
|
||||
sides_used.sort_unstable();
|
||||
sides_used.dedup();
|
||||
match approach {
|
||||
Approach::AllSides => assert!((4..=5).contains(&sides_used.len()), "{} sides", sides_used.len()),
|
||||
_ => assert_eq!(sides_used.len(), sides, "{approach:?}"),
|
||||
}
|
||||
// No hidden extra position: at most a site per class on a side (ramp, launcher, one battery per range).
|
||||
assert!(sim.sites.list.len() <= sides_used.len() * 3, "{} sites", sim.sites.list.len());
|
||||
// Every drone has a site that supports its kind.
|
||||
assert_eq!(sim.queue.len() + sim.drones.len(), 14, "{:?}", out.messages);
|
||||
for (_, d) in &sim.queue {
|
||||
let site = sim.sites.site_of(d.id).and_then(|id| sim.sites.get(id)).expect("a drone without a source site");
|
||||
assert!(site.supports(d.kind), "{:?} from a {:?}", d.kind, site.class);
|
||||
// It waits on the site's pad, over its ground.
|
||||
assert!(horizontal(d.pos - site.pos).length() < 25.0, "{:?} is {:.0} away from its site", d.kind, horizontal(d.pos - site.pos).length());
|
||||
assert!(d.pos.y > site.pos.y && d.pos.y < site.pos.y + 6.0);
|
||||
}
|
||||
for (i, a) in sim.sites.list.iter().enumerate() {
|
||||
for b in &sim.sites.list[..i] {
|
||||
assert!(horizontal(a.pos - b.pos).length() >= KEEP_OUT, "two sites on one place");
|
||||
}
|
||||
}
|
||||
// Stock and jobs agree; ground is real: flat at 64.
|
||||
for s in &sim.sites.list {
|
||||
assert_eq!(s.stock as usize, sim.sites.pending(s.id));
|
||||
assert_eq!(s.pos.y, 64.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ballistic_sites_keep_their_own_range_and_ramps_the_ordered_one() {
|
||||
let (land, mut ground, mut out) = (flat(), Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(4);
|
||||
plan(&mut sim, &mut ground, &land, &order(Approach::Sector, 300.0), &mut out);
|
||||
let centre = DVec3::new(0.0, 70.0, 0.0);
|
||||
for s in &sim.sites.list {
|
||||
let r = horizontal(s.pos - centre).length();
|
||||
match s.class {
|
||||
SiteClass::Battery => assert!(r > 990.0, "a battery only {r:.0} out"),
|
||||
_ => assert!((200.0..=400.0).contains(&r), "{:?} {r:.0} out", s.class),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launches_start_at_the_site_with_a_real_climb_and_in_turn() {
|
||||
let (land, mut ground, mut out) = (flat(), Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(9);
|
||||
plan(&mut sim, &mut ground, &land, &order(Approach::TwoSectors, 300.0), &mut out);
|
||||
let jobs: HashMap<u32, u32> = sim.queue.iter().map(|(_, d)| (d.id, sim.sites.site_of(d.id).unwrap())).collect();
|
||||
// One or two may have left in the tick the plan was ready.
|
||||
let early = sim.drones.len();
|
||||
let stock: u32 = sim.sites.list.iter().map(|s| s.stock).sum();
|
||||
assert_eq!(stock as usize + early, 14);
|
||||
let mut seen = HashMap::new();
|
||||
let mut last_launch: HashMap<u32, u64> = HashMap::new();
|
||||
for _ in 0..4000 {
|
||||
step(&mut sim, &mut ground, &land, &mut out);
|
||||
for d in sim.drones.iter().filter(|d| d.age == 1) {
|
||||
let site = sim.sites.get(jobs[&d.id]).unwrap().clone();
|
||||
// Its first tick is at the kit, not in the air somewhere.
|
||||
assert!(horizontal(d.pos - site.pos).length() < 25.0, "{:?} first seen {:.0} from its site", d.kind, horizontal(d.pos - site.pos).length());
|
||||
assert!(d.pos.y - site.pos.y < 12.0, "{:?} first seen {:.1} above the ground", d.kind, d.pos.y - site.pos.y);
|
||||
if d.kind.spec().ballistic.is_none() {
|
||||
assert!(d.vel.y > 0.0, "{:?} leaves level, not climbing", d.kind);
|
||||
}
|
||||
// One launch at a time from a site.
|
||||
if let Some(t) = last_launch.insert(site.id, sim.tick) {
|
||||
assert!(sim.tick - t >= site.class.cadence(), "two launches {} ticks apart", sim.tick - t);
|
||||
}
|
||||
seen.insert(d.id, d.pos.y);
|
||||
}
|
||||
}
|
||||
assert_eq!(seen.len() + early, 14, "not every drone left a site");
|
||||
assert_eq!(sim.sites.list.iter().map(|s| s.stock).sum::<u32>(), 0, "stock left after the raid");
|
||||
// The climb is real: an early drone is higher a second later, and clear of the ground.
|
||||
assert!(sim.queue.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_site_launches_only_what_it_holds() {
|
||||
let mut sim = Sim::new(2);
|
||||
let mut ground = Ground::default();
|
||||
let mut out = Out::default();
|
||||
let id = sim.sites.add(LaunchSite {
|
||||
id: 0,
|
||||
side: 0,
|
||||
sector: 0,
|
||||
bearing: 0.0,
|
||||
class: SiteClass::Ramp,
|
||||
pos: DVec3::new(0.0, 64.0, 300.0),
|
||||
range: 300.0,
|
||||
water: false,
|
||||
stock: 3,
|
||||
planned: 5,
|
||||
alive: true,
|
||||
next_free: 0,
|
||||
});
|
||||
for i in 0..5 {
|
||||
let site = sim.sites.get(id).unwrap().clone();
|
||||
let d = sim.make_drone(DroneKind::Shahed, site.muzzle(i), -DVec3::Z, DVec3::new(0.0, 70.0, 0.0));
|
||||
sim.sites.job(d.id, id);
|
||||
sim.queue.push((1, d));
|
||||
}
|
||||
let mut times = Vec::new();
|
||||
for _ in 0..80 {
|
||||
let before = sim.launched;
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
if sim.launched > before {
|
||||
times.push(sim.tick);
|
||||
}
|
||||
}
|
||||
assert_eq!(sim.launched, 3, "launched {:?}", times);
|
||||
assert!(sim.queue.is_empty(), "the unlaunchable jobs stay in the queue");
|
||||
assert_eq!(sim.sites.get(id).unwrap().stock, 0);
|
||||
assert!(times.windows(2).all(|w| w[1] - w[0] >= SiteClass::Ramp.cadence()), "{times:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_destroyed_site_cancels_its_future_jobs() {
|
||||
let (land, mut ground, mut out) = (flat(), Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(5);
|
||||
plan(&mut sim, &mut ground, &land, &order(Approach::Sector, 300.0), &mut out);
|
||||
let ramp = sim.sites.list.iter().find(|s| s.class == SiteClass::Ramp).unwrap().id;
|
||||
let early = sim.launched as usize;
|
||||
let n = sim.sites.pending(ramp);
|
||||
assert!(n >= 8 && n <= 9, "{n}");
|
||||
assert_eq!(sim.destroy_site(ramp) as usize, n);
|
||||
assert_eq!(sim.queue.len() + early + n, 14);
|
||||
for _ in 0..3000 {
|
||||
step(&mut sim, &mut ground, &land, &mut out);
|
||||
}
|
||||
assert_eq!(sim.launched as usize + n, 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suitable_ground_means_a_clear_refusal() {
|
||||
// Never answered: unknown ground is not suitable, and nothing is launched.
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(3);
|
||||
sim.order_raid(&order(Approach::TwoSectors, 300.0), &[DVec3::new(0.0, 70.0, 0.0)], &mut out);
|
||||
let mut n = 0;
|
||||
while sim.planning() {
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
n += 1;
|
||||
assert!(n <= SEARCH_TICKS + 5, "a raid waits for ever");
|
||||
}
|
||||
assert!(sim.queue.is_empty() && sim.sites.list.is_empty());
|
||||
assert!(out.messages.iter().any(|m| m.contains("площад")), "{:?}", out.messages);
|
||||
assert!(!sim.drones.iter().any(|d| d.alive()));
|
||||
// A cliff face everywhere (too steep to stand on) is refused as well, and a sea (no land) too.
|
||||
for steep in [true, false] {
|
||||
let land = Land(move |x, _| if steep { x * 4 } else { -100 });
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(3);
|
||||
plan(&mut sim, &mut ground, &land, &order(Approach::Sector, 300.0), &mut out);
|
||||
assert!(sim.queue.is_empty() && sim.sites.list.is_empty(), "steep {steep}");
|
||||
assert!(out.messages.iter().any(|m| m.contains("площад")));
|
||||
}
|
||||
}
|
||||
|
||||
/// Land inside a radius of the origin (surface 64), open water outside it (surface 63 over a floor at 50):
|
||||
/// the game's height over water is the surface, and the blocks under it are not solid.
|
||||
struct Island(f64);
|
||||
impl Island {
|
||||
fn land(&self, x: i32, z: i32) -> bool {
|
||||
f64::from(x).hypot(f64::from(z)) < self.0
|
||||
}
|
||||
}
|
||||
impl world::WorldSource for Island {
|
||||
fn solid(&self, x: i32, y: i32, z: i32) -> Option<bool> {
|
||||
Some(y < if self.land(x, z) { 64 } else { 50 })
|
||||
}
|
||||
}
|
||||
|
||||
fn run_island(radius: f64, seed: u64, o: &RaidOrder) -> (Sim, Out) {
|
||||
let (mut ground, mut out, island) = (Ground::default(), Out::default(), Island(radius));
|
||||
let mut sim = Sim::new(seed);
|
||||
sim.order_raid(o, &[DVec3::new(0.0, 70.0, 0.0)], &mut out);
|
||||
for _ in 0..SEARCH_TICKS + 5 {
|
||||
let cols = ground.take_wanted().into_iter().map(|(x, z)| (x, z, if island.land(x, z) { 64 } else { 63 })).collect();
|
||||
ground.answer(cols, sim.tick);
|
||||
let secs = ground.world.take_wanted(sim.tick);
|
||||
ground.world.serve(&island, secs, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
}
|
||||
for _ in 0..3000 {
|
||||
let cols = ground.take_wanted().into_iter().map(|(x, z)| (x, z, if island.land(x, z) { 64 } else { 63 })).collect();
|
||||
ground.answer(cols, sim.tick);
|
||||
let secs = ground.world.take_wanted(sim.tick);
|
||||
ground.world.serve(&island, secs, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
}
|
||||
(sim, out)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_all_water_map_gives_barges_and_every_drone_launches() {
|
||||
let o = RaidOrder { kinds: vec![(DroneKind::Shahed, 6), (DroneKind::Kh101, 3)], approach: Approach::TwoSectors, dist: 200.0, aim: Aim::Objects };
|
||||
// Water from the first block: nothing but the sea round the raid (its centre is the player's slab, not read).
|
||||
let (sim, out) = run_island(0.0, 6, &o);
|
||||
assert!(!sim.sites.list.is_empty(), "no barge: {:?}", out.messages);
|
||||
assert!(sim.sites.list.iter().all(|s| s.water && s.pos.y > 63.0 && s.pos.y < 64.0), "a site that is not on the surface");
|
||||
assert_eq!(sim.launched, 9, "{:?}", out.messages);
|
||||
let mut o2 = Out::default();
|
||||
sim.draw_sites(&mut o2);
|
||||
assert!(o2.models.iter().any(|m| m.name == "airdef:barge"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn land_is_preferred_over_water_when_both_are_known() {
|
||||
// An island of radius 260 in the sea; the raid is ordered 300 out: its sites stand on the island, nearer.
|
||||
let o = RaidOrder { kinds: vec![(DroneKind::Shahed, 6), (DroneKind::Kh101, 3)], approach: Approach::TwoSectors, dist: 300.0, aim: Aim::Objects };
|
||||
let (sim, out) = run_island(260.0, 6, &o);
|
||||
assert!(!sim.sites.list.is_empty());
|
||||
assert!(sim.sites.list.iter().all(|s| !s.water), "a barge where land was known: {:?} {:?}", out.messages, sim.sites.list.iter().map(|s| (s.class, s.water, s.pos, s.range)).collect::<Vec<_>>());
|
||||
assert_eq!(sim.launched, 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_site_never_stands_in_a_hill_or_under_a_roof() {
|
||||
// A hill 40 blocks high round the raid's ring at 300 out (radius 100): the sites find flat land elsewhere.
|
||||
let hill = |x: i32, z: i32| {
|
||||
let (dx, dz) = (f64::from(x) - 0.0, f64::from(z) - 300.0);
|
||||
if dx * dx + dz * dz < 100.0 * 100.0 { 64 + 40 } else { 64 }
|
||||
};
|
||||
let land = Land(hill);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(8);
|
||||
let o = RaidOrder { kinds: vec![(DroneKind::Shahed, 5)], approach: Approach::Sector, dist: 300.0, aim: Aim::Objects };
|
||||
plan(&mut sim, &mut ground, &land, &o, &mut out);
|
||||
for s in &sim.sites.list {
|
||||
for (dx, dz) in FOOTPRINT {
|
||||
let h = hill((s.pos.x + dx).floor() as i32, (s.pos.z + dz).floor() as i32);
|
||||
assert!((f64::from(h) - s.pos.y).abs() <= f64::from(FLAT), "a site over a slope");
|
||||
}
|
||||
}
|
||||
assert!(!sim.sites.list.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_raids_do_not_pile_up_sites_and_clear_removes_them() {
|
||||
let (land, mut ground, mut out) = (flat(), Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(12);
|
||||
plan(&mut sim, &mut ground, &land, &order(Approach::AllSides, 300.0), &mut out);
|
||||
let first: Vec<u32> = sim.sites.list.iter().map(|s| s.id).collect();
|
||||
for _ in 0..3000 {
|
||||
step(&mut sim, &mut ground, &land, &mut out);
|
||||
}
|
||||
plan(&mut sim, &mut ground, &land, &order(Approach::TwoSectors, 300.0), &mut out);
|
||||
assert!(sim.sites.list.iter().all(|s| !first.contains(&s.id)), "old sites stayed");
|
||||
assert!(sim.sites.list.len() <= 6);
|
||||
sim.clear();
|
||||
assert!(sim.sites.list.is_empty() && sim.queue.is_empty());
|
||||
let mut o = Out::default();
|
||||
sim.draw_sites(&mut o);
|
||||
assert!(o.models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_seed_picks_the_same_sites_and_graphics_do_not_move_them() {
|
||||
let run = |draw: bool| {
|
||||
let (land, mut ground, mut out) = (flat(), Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(33);
|
||||
plan(&mut sim, &mut ground, &land, &order(Approach::AllSides, 300.0), &mut out);
|
||||
if draw {
|
||||
// Rendering must not spend the combat randomness.
|
||||
let mut o = Out::default();
|
||||
sim.draw_sites(&mut o);
|
||||
assert!(!o.models.is_empty());
|
||||
}
|
||||
(sim.sites.list.iter().map(|s| (s.id, s.pos.x.to_bits(), s.pos.z.to_bits(), s.stock)).collect::<Vec<_>>(), sim.queue.iter().map(|q| q.0).collect::<Vec<_>>(), sim.rng.state())
|
||||
};
|
||||
assert_eq!(run(false), run(true));
|
||||
}
|
||||
}
|
||||
@@ -1193,6 +1193,36 @@ def ew_truck():
|
||||
return p
|
||||
|
||||
|
||||
def launch_ramp():
|
||||
"""Рампа-катапульта для дронов: основание, наклонные рельсы (нос на север поднят), опоры и ложе."""
|
||||
p = [el((1, 0, -6), (15, 2, 30), "dark_metal"), el((6, 2, 0), (10, 4, 26), "metal")]
|
||||
p += both(el((3, 9, -14), (5, 11, 30), "metal", rot=("x", 22.5)))
|
||||
p += both(el((3, 0, -1), (5, 12, 1), "dark_metal")) + both(el((3, 0, 17), (5, 6, 19), "dark_metal"))
|
||||
p += [el((4, 10, 24), (12, 14, 30), "dark_metal"), el((1, 2, 28), (15, 6, 32), "ru_khaki")]
|
||||
return p
|
||||
|
||||
|
||||
def launcher_truck():
|
||||
"""Пусковая машина ракет: шасси МАЗ и наклонный пакет труб, смотрящий вперёд (на север) вверх."""
|
||||
p = maz_chassis("ru_khaki")
|
||||
for x in (4, 10):
|
||||
p.append(el((x, 15, -4), (x + 5, 20, 30), "tube", {"north": "tube_cap", "south": "tube_cap"}, rot=("x", 45)))
|
||||
p += [el((3, 13, 22), (13, 17, 28), "dark_metal")]
|
||||
return p
|
||||
|
||||
|
||||
def barge():
|
||||
"""Баржа под пусковой комплект: плоская палуба 3×3 блока, корпус, борта, кнехты и скошенный нос."""
|
||||
p = [el((-16, 0, -16), (32, 3, 32), "dark_metal"),
|
||||
el((-16, 3, -16), (32, 4, 32), "metal"),
|
||||
el((-15, 3, -15), (31, 4.4, 31), "ru_khaki")]
|
||||
p += both(el((-16, 4, -16), (-14, 6, 32), "dark_metal"))
|
||||
p += [el((-16, 4, 30), (32, 6, 32), "dark_metal"), el((-16, 4, -16), (32, 6, -14), "dark_metal")]
|
||||
for z in (-10, 8, 26):
|
||||
p += both(el((-12, 4.4, z), (-9, 6.5, z + 3), "metal"))
|
||||
return p
|
||||
|
||||
|
||||
MODELS = {
|
||||
"shahed": (shahed, "Шахед-136"),
|
||||
"shahed_prop": (shahed_prop, "Винт"),
|
||||
@@ -1251,6 +1281,9 @@ MODELS = {
|
||||
"radar_truck": (radar_truck, "Шасси РЛС"),
|
||||
"depot": (depot, "Склад боеприпасов"),
|
||||
"tzm_truck": (tzm_truck, "Транспортно-заряжающая машина"),
|
||||
"barge": (barge, "Баржа"),
|
||||
"launch_ramp": (launch_ramp, "Рампа запуска дронов"),
|
||||
"launcher_truck": (launcher_truck, "Пусковая машина ракет"),
|
||||
"ew_truck": (ew_truck, "Станция РЭБ"),
|
||||
"searchlight": (searchlight, "Прожектор"),
|
||||
"searchlight_head": (searchlight_head, "Голова прожектора"),
|
||||
|
||||
Reference in New Issue
Block a user