diff --git a/art/blockbench/block/meteor_core.bbmodel b/art/blockbench/block/meteor_core.bbmodel
new file mode 100644
index 0000000..1227cf5
--- /dev/null
+++ b/art/blockbench/block/meteor_core.bbmodel
@@ -0,0 +1,136 @@
+{
+ "meta": {
+ "format_version": "4.10",
+ "model_format": "java_block",
+ "box_uv": false
+ },
+ "name": "meteor_core",
+ "model_identifier": "",
+ "visible_box": [
+ 1,
+ 1,
+ 0
+ ],
+ "variable_placeholders": "",
+ "variable_placeholder_buttons": [],
+ "timeline_setups": [],
+ "unhandled_root_fields": {},
+ "resolution": {
+ "width": 16,
+ "height": 16
+ },
+ "elements": [
+ {
+ "name": "meteor_core",
+ "box_uv": false,
+ "rescale": false,
+ "locked": false,
+ "render_order": "default",
+ "allow_mirror_modeling": true,
+ "from": [
+ 0,
+ 0,
+ 0
+ ],
+ "to": [
+ 16,
+ 16,
+ 16
+ ],
+ "autouv": 0,
+ "color": 0,
+ "origin": [
+ 8,
+ 8,
+ 8
+ ],
+ "uv_offset": [
+ 0,
+ 0
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "east": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "south": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "west": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "up": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "down": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ }
+ },
+ "type": "cube",
+ "uuid": "a8025f2e-56a9-4b8a-8e12-3cd4a1f22250"
+ }
+ ],
+ "outliner": [
+ "a8025f2e-56a9-4b8a-8e12-3cd4a1f22250"
+ ],
+ "textures": [
+ {
+ "path": "/sessions/serene-clever-noether/mnt/nerospace/src/main/resources/assets/nerospace/textures/block/meteor_core.png",
+ "name": "meteor_core.png",
+ "folder": "block",
+ "namespace": "nerospace",
+ "id": "0",
+ "particle": true,
+ "render_mode": "default",
+ "render_sides": "auto",
+ "frame_time": 1,
+ "frame_order_type": "loop",
+ "frame_order": "",
+ "frame_interpolate": false,
+ "visible": true,
+ "mode": "bitmap",
+ "saved": true,
+ "uuid": "63753f54-3fc0-46c8-abd6-1e0f12a10ba4",
+ "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/meteor_core.png",
+ "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABxElEQVR4nG2TvYrbQBSFPwtDygxiEBMXWyxDkGEK1WHSqEhtQqoUYas8wrKPsPgR9gHSpMkTuNIDqBBYhQipvMIMYlKmcgrnDtKSU12ke879mXNX5dvqAuBcRdMc8L6m61q0LhiGHmtLhqFHqZwYJ6wtCeFMjBPe16w+fby78A/jeALAmA1Nc8DaUn4xDD0ASuXpm9YFWdMc6Lo2EYXsfZ1Ic6LWBVoXOFcBXDsYxxMhnJFRgCQa4wSA9zXjeFqIAqwBQjjjXEXXtXRdi3MVx72S2gC8358SScRCOLPuupYYpzR/jBPfv/xiax4XlY73D2zvIy+RaV1gbYkxG2KceH66TeRdWbErryNtzSPHvcL7mqY5pGKZMRuGoU8zC3ZlxefXr1IskE5DOGNtSTaOJ5TKkU7m+Pb7TxIRiDckzoahT5s2ZpMSf/RtEpEYrs8p4wKsTHFzka2KyH+XOD7w5utPgIU7V/7dh4tYc97a89PtQmB7HxdWFuuvTHFz4QXmuwjhjNZFElYqT2RrSzLva7yvUSpPRLG0VBqGfrGfufUzeZoYp5QkCVJJqXxhNOeqdLVrUZp7XOsinbT39WK8+bE5V/EX3If3A13KBkAAAAAASUVORK5CYII="
+ }
+ ]
+}
\ No newline at end of file
diff --git a/art/blockbench/block/meteor_rock.bbmodel b/art/blockbench/block/meteor_rock.bbmodel
new file mode 100644
index 0000000..2df7a77
--- /dev/null
+++ b/art/blockbench/block/meteor_rock.bbmodel
@@ -0,0 +1,136 @@
+{
+ "meta": {
+ "format_version": "4.10",
+ "model_format": "java_block",
+ "box_uv": false
+ },
+ "name": "meteor_rock",
+ "model_identifier": "",
+ "visible_box": [
+ 1,
+ 1,
+ 0
+ ],
+ "variable_placeholders": "",
+ "variable_placeholder_buttons": [],
+ "timeline_setups": [],
+ "unhandled_root_fields": {},
+ "resolution": {
+ "width": 16,
+ "height": 16
+ },
+ "elements": [
+ {
+ "name": "meteor_rock",
+ "box_uv": false,
+ "rescale": false,
+ "locked": false,
+ "render_order": "default",
+ "allow_mirror_modeling": true,
+ "from": [
+ 0,
+ 0,
+ 0
+ ],
+ "to": [
+ 16,
+ 16,
+ 16
+ ],
+ "autouv": 0,
+ "color": 0,
+ "origin": [
+ 8,
+ 8,
+ 8
+ ],
+ "uv_offset": [
+ 0,
+ 0
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "east": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "south": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "west": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "up": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "down": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ }
+ },
+ "type": "cube",
+ "uuid": "46bc8506-936d-4d65-951f-38e973732dee"
+ }
+ ],
+ "outliner": [
+ "46bc8506-936d-4d65-951f-38e973732dee"
+ ],
+ "textures": [
+ {
+ "path": "/sessions/serene-clever-noether/mnt/nerospace/src/main/resources/assets/nerospace/textures/block/meteor_rock.png",
+ "name": "meteor_rock.png",
+ "folder": "block",
+ "namespace": "nerospace",
+ "id": "0",
+ "particle": true,
+ "render_mode": "default",
+ "render_sides": "auto",
+ "frame_time": 1,
+ "frame_order_type": "loop",
+ "frame_order": "",
+ "frame_interpolate": false,
+ "visible": true,
+ "mode": "bitmap",
+ "saved": true,
+ "uuid": "c8e34ca8-318e-49f1-9a96-2099cb051722",
+ "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/meteor_rock.png",
+ "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAByklEQVR4nHWSIYscQRCFvxtCdLM0Q2fkMhwTaDE/YGNORoQjRB8hIqyOWBkRuSL6WBVi4yIiR7U4Tq0YyIohRM0OQzP0P7gTu1W3G0ipprpe1av36uLd2/cPw9AT4whAShNlWeFcQdtu4fst3Cw5i2PO+5oMoOt2eF8DsFhcARBCA4D99Bnva6zNsTYnpQm/vsXanLbdkrXtFmNmtO32kFwtca44gG2uzbpuR4zjoXa1VMYX1WX9AOB9TQgNxsz4vTa8+PgHY2a6koSs5n3NMPRkQqttt1ok4NOIX78Q40gIDSlNhNDgXPHE4JTyv9NO884VyjSliUzEszan63aIqFLofU3X7fRvGHp1y5jZQcSUprMJ4kRZVoTQUJYVZVmpcKKJtTnPBBzjqEBpasxMgVIn64hGmUyWVYah13dKE4tfP7E2V4BzBTGOWJvjXEEmUwV8SrMsK7oPT55LE2EzDD2ZXNyPm796QGrdEagXeGS238y1Rm0USoAK979m8gbIUprUwhAa9Rg4s0+mO1dwfX+nrLKyrLRov5mzWFyp0vvNHGNmdN2OYei5vr+jbbeE12+URXa696v1cz0UgJerpJNjHPl2eanMxJlH8IoCIxFyA1kAAAAASUVORK5CYII="
+ }
+ ]
+}
\ No newline at end of file
diff --git a/art/blockbench/item/alien_core.bbmodel b/art/blockbench/item/alien_core.bbmodel
new file mode 100644
index 0000000..d16d40e
--- /dev/null
+++ b/art/blockbench/item/alien_core.bbmodel
@@ -0,0 +1,136 @@
+{
+ "meta": {
+ "format_version": "4.10",
+ "model_format": "java_block",
+ "box_uv": false
+ },
+ "name": "alien_core",
+ "model_identifier": "",
+ "visible_box": [
+ 1,
+ 1,
+ 0
+ ],
+ "variable_placeholders": "",
+ "variable_placeholder_buttons": [],
+ "timeline_setups": [],
+ "unhandled_root_fields": {},
+ "resolution": {
+ "width": 16,
+ "height": 16
+ },
+ "elements": [
+ {
+ "name": "alien_core",
+ "box_uv": false,
+ "rescale": false,
+ "locked": false,
+ "render_order": "default",
+ "allow_mirror_modeling": true,
+ "from": [
+ 0,
+ 0,
+ 7.5
+ ],
+ "to": [
+ 16,
+ 16,
+ 8.5
+ ],
+ "autouv": 0,
+ "color": 0,
+ "origin": [
+ 8,
+ 8,
+ 8
+ ],
+ "uv_offset": [
+ 0,
+ 0
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "south": {
+ "uv": [
+ 16,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": 0
+ },
+ "east": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "west": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "up": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ },
+ "down": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ }
+ },
+ "type": "cube",
+ "uuid": "cca8d6e6-717f-4122-9442-1df0fc7a5060"
+ }
+ ],
+ "outliner": [
+ "cca8d6e6-717f-4122-9442-1df0fc7a5060"
+ ],
+ "textures": [
+ {
+ "path": "/sessions/serene-clever-noether/mnt/nerospace/src/main/resources/assets/nerospace/textures/item/alien_core.png",
+ "name": "alien_core.png",
+ "folder": "item",
+ "namespace": "nerospace",
+ "id": "0",
+ "particle": true,
+ "render_mode": "default",
+ "render_sides": "auto",
+ "frame_time": 1,
+ "frame_order_type": "loop",
+ "frame_order": "",
+ "frame_interpolate": false,
+ "visible": true,
+ "mode": "bitmap",
+ "saved": true,
+ "uuid": "b6677917-5311-46ee-b73a-d61e47ff5760",
+ "relative_path": "../../../src/main/resources/assets/nerospace/textures/item/alien_core.png",
+ "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAqUlEQVR4nGNgoBAwYhOUEJP7j038xatHGOoxBGCan89SQhGXTLuH1RAmYjQji6G7DsU0CTG5/zCFlQ47UAxoP+ABdwmyK1AM+L/B4T82zeiGMAYcgOtjQXc+DETxs6NoXvbxJ4pLYa5ACQNywMAbAA8DiJ+U4OGA7Gd0gBwLKC6AJRZYaCMD5GhEBhjpgIEBe0JC1ozTBTAJdFtwacZwAbpL0AG2zEQxAAACHExZBxfQawAAAABJRU5ErkJggg=="
+ }
+ ]
+}
\ No newline at end of file
diff --git a/art/blockbench/item/alien_fragment.bbmodel b/art/blockbench/item/alien_fragment.bbmodel
new file mode 100644
index 0000000..4e38df1
--- /dev/null
+++ b/art/blockbench/item/alien_fragment.bbmodel
@@ -0,0 +1,136 @@
+{
+ "meta": {
+ "format_version": "4.10",
+ "model_format": "java_block",
+ "box_uv": false
+ },
+ "name": "alien_fragment",
+ "model_identifier": "",
+ "visible_box": [
+ 1,
+ 1,
+ 0
+ ],
+ "variable_placeholders": "",
+ "variable_placeholder_buttons": [],
+ "timeline_setups": [],
+ "unhandled_root_fields": {},
+ "resolution": {
+ "width": 16,
+ "height": 16
+ },
+ "elements": [
+ {
+ "name": "alien_fragment",
+ "box_uv": false,
+ "rescale": false,
+ "locked": false,
+ "render_order": "default",
+ "allow_mirror_modeling": true,
+ "from": [
+ 0,
+ 0,
+ 7.5
+ ],
+ "to": [
+ 16,
+ 16,
+ 8.5
+ ],
+ "autouv": 0,
+ "color": 0,
+ "origin": [
+ 8,
+ 8,
+ 8
+ ],
+ "uv_offset": [
+ 0,
+ 0
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "south": {
+ "uv": [
+ 16,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": 0
+ },
+ "east": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "west": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "up": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ },
+ "down": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ }
+ },
+ "type": "cube",
+ "uuid": "25365464-6ce5-48a1-bcb1-a17deb5023de"
+ }
+ ],
+ "outliner": [
+ "25365464-6ce5-48a1-bcb1-a17deb5023de"
+ ],
+ "textures": [
+ {
+ "path": "/sessions/serene-clever-noether/mnt/nerospace/src/main/resources/assets/nerospace/textures/item/alien_fragment.png",
+ "name": "alien_fragment.png",
+ "folder": "item",
+ "namespace": "nerospace",
+ "id": "0",
+ "particle": true,
+ "render_mode": "default",
+ "render_sides": "auto",
+ "frame_time": 1,
+ "frame_order_type": "loop",
+ "frame_order": "",
+ "frame_interpolate": false,
+ "visible": true,
+ "mode": "bitmap",
+ "saved": true,
+ "uuid": "e4f13f59-d48b-41ee-b968-b48d6338c152",
+ "relative_path": "../../../src/main/resources/assets/nerospace/textures/item/alien_fragment.png",
+ "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAVElEQVR4nGNgGAUUA0ZcEhonTvxH5t+wsMCqlgmX5gANA7wG4jWAgYGBYcONCwzohhBtAMy5G25cYGBgYGC4/qICpwE4w4CBAdXZJIUBLheNAhoBAISQGEL85dGaAAAAAElFTkSuQmCC"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/art/blockbench/item/alien_tech_scrap.bbmodel b/art/blockbench/item/alien_tech_scrap.bbmodel
new file mode 100644
index 0000000..1b8e775
--- /dev/null
+++ b/art/blockbench/item/alien_tech_scrap.bbmodel
@@ -0,0 +1,136 @@
+{
+ "meta": {
+ "format_version": "4.10",
+ "model_format": "java_block",
+ "box_uv": false
+ },
+ "name": "alien_tech_scrap",
+ "model_identifier": "",
+ "visible_box": [
+ 1,
+ 1,
+ 0
+ ],
+ "variable_placeholders": "",
+ "variable_placeholder_buttons": [],
+ "timeline_setups": [],
+ "unhandled_root_fields": {},
+ "resolution": {
+ "width": 16,
+ "height": 16
+ },
+ "elements": [
+ {
+ "name": "alien_tech_scrap",
+ "box_uv": false,
+ "rescale": false,
+ "locked": false,
+ "render_order": "default",
+ "allow_mirror_modeling": true,
+ "from": [
+ 0,
+ 0,
+ 7.5
+ ],
+ "to": [
+ 16,
+ 16,
+ 8.5
+ ],
+ "autouv": 0,
+ "color": 0,
+ "origin": [
+ 8,
+ 8,
+ 8
+ ],
+ "uv_offset": [
+ 0,
+ 0
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "south": {
+ "uv": [
+ 16,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": 0
+ },
+ "east": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "west": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "up": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ },
+ "down": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ }
+ },
+ "type": "cube",
+ "uuid": "709ba01c-5178-4070-b641-0bbb27d45b85"
+ }
+ ],
+ "outliner": [
+ "709ba01c-5178-4070-b641-0bbb27d45b85"
+ ],
+ "textures": [
+ {
+ "path": "/sessions/serene-clever-noether/mnt/nerospace/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png",
+ "name": "alien_tech_scrap.png",
+ "folder": "item",
+ "namespace": "nerospace",
+ "id": "0",
+ "particle": true,
+ "render_mode": "default",
+ "render_sides": "auto",
+ "frame_time": 1,
+ "frame_order_type": "loop",
+ "frame_order": "",
+ "frame_interpolate": false,
+ "visible": true,
+ "mode": "bitmap",
+ "saved": true,
+ "uuid": "173a9c99-a0a6-4392-9182-87529a017fe6",
+ "relative_path": "../../../src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png",
+ "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAZ0lEQVR4nGNgGAUUA0Z0ARsrt/8SElIML148Y8BGr1m3AEUPE7oBMMU22zZhNYQoF7zpa8LqXJ2uGRguYMHmAoaiOqzOx+YCDC9gc/7hsl9wPkEDJCSkGI54+aHYbNvFhtMFo4AKAAC/s0Oh8evw/gAAAABJRU5ErkJggg=="
+ }
+ ]
+}
\ No newline at end of file
diff --git a/art/blockbench/item/meteor_caller.bbmodel b/art/blockbench/item/meteor_caller.bbmodel
new file mode 100644
index 0000000..ad7a268
--- /dev/null
+++ b/art/blockbench/item/meteor_caller.bbmodel
@@ -0,0 +1,136 @@
+{
+ "meta": {
+ "format_version": "4.10",
+ "model_format": "java_block",
+ "box_uv": false
+ },
+ "name": "meteor_caller",
+ "model_identifier": "",
+ "visible_box": [
+ 1,
+ 1,
+ 0
+ ],
+ "variable_placeholders": "",
+ "variable_placeholder_buttons": [],
+ "timeline_setups": [],
+ "unhandled_root_fields": {},
+ "resolution": {
+ "width": 16,
+ "height": 16
+ },
+ "elements": [
+ {
+ "name": "meteor_caller",
+ "box_uv": false,
+ "rescale": false,
+ "locked": false,
+ "render_order": "default",
+ "allow_mirror_modeling": true,
+ "from": [
+ 0,
+ 0,
+ 7.5
+ ],
+ "to": [
+ 16,
+ 16,
+ 8.5
+ ],
+ "autouv": 0,
+ "color": 0,
+ "origin": [
+ 8,
+ 8,
+ 8
+ ],
+ "uv_offset": [
+ 0,
+ 0
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "south": {
+ "uv": [
+ 16,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": 0
+ },
+ "east": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "west": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "up": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ },
+ "down": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ }
+ },
+ "type": "cube",
+ "uuid": "d9e6b61e-bc9e-435f-8fa9-fb080da83e17"
+ }
+ ],
+ "outliner": [
+ "d9e6b61e-bc9e-435f-8fa9-fb080da83e17"
+ ],
+ "textures": [
+ {
+ "path": "/sessions/serene-clever-noether/mnt/nerospace/src/main/resources/assets/nerospace/textures/item/meteor_caller.png",
+ "name": "meteor_caller.png",
+ "folder": "item",
+ "namespace": "nerospace",
+ "id": "0",
+ "particle": true,
+ "render_mode": "default",
+ "render_sides": "auto",
+ "frame_time": 1,
+ "frame_order_type": "loop",
+ "frame_order": "",
+ "frame_interpolate": false,
+ "visible": true,
+ "mode": "bitmap",
+ "saved": true,
+ "uuid": "aa690ee7-74f3-4047-a78f-46c47b9046b3",
+ "relative_path": "../../../src/main/resources/assets/nerospace/textures/item/meteor_caller.png",
+ "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAVUlEQVR4nGNgGJ5ARkHn/4INl/6j00QbANP0f4PD//8bHOCGkOwCmAFkuaDiww8Mb5DsAhgNM4wkF8go6Pyv+PCDchcgG0KyC6gSBhonTgyAC4YWAADNDo6b2xad3wAAAABJRU5ErkJggg=="
+ }
+ ]
+}
\ No newline at end of file
diff --git a/art/blockbench/item/meteor_tracker.bbmodel b/art/blockbench/item/meteor_tracker.bbmodel
new file mode 100644
index 0000000..d38d8aa
--- /dev/null
+++ b/art/blockbench/item/meteor_tracker.bbmodel
@@ -0,0 +1,136 @@
+{
+ "meta": {
+ "format_version": "4.10",
+ "model_format": "java_block",
+ "box_uv": false
+ },
+ "name": "meteor_tracker",
+ "model_identifier": "",
+ "visible_box": [
+ 1,
+ 1,
+ 0
+ ],
+ "variable_placeholders": "",
+ "variable_placeholder_buttons": [],
+ "timeline_setups": [],
+ "unhandled_root_fields": {},
+ "resolution": {
+ "width": 16,
+ "height": 16
+ },
+ "elements": [
+ {
+ "name": "meteor_tracker",
+ "box_uv": false,
+ "rescale": false,
+ "locked": false,
+ "render_order": "default",
+ "allow_mirror_modeling": true,
+ "from": [
+ 0,
+ 0,
+ 7.5
+ ],
+ "to": [
+ 16,
+ 16,
+ 8.5
+ ],
+ "autouv": 0,
+ "color": 0,
+ "origin": [
+ 8,
+ 8,
+ 8
+ ],
+ "uv_offset": [
+ 0,
+ 0
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "texture": 0
+ },
+ "south": {
+ "uv": [
+ 16,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": 0
+ },
+ "east": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "west": {
+ "uv": [
+ 0,
+ 0,
+ 0,
+ 16
+ ],
+ "texture": null
+ },
+ "up": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ },
+ "down": {
+ "uv": [
+ 0,
+ 0,
+ 16,
+ 0
+ ],
+ "texture": null
+ }
+ },
+ "type": "cube",
+ "uuid": "e7b648d1-a8ed-43c7-9114-9d42e47e70de"
+ }
+ ],
+ "outliner": [
+ "e7b648d1-a8ed-43c7-9114-9d42e47e70de"
+ ],
+ "textures": [
+ {
+ "path": "/sessions/serene-clever-noether/mnt/nerospace/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png",
+ "name": "meteor_tracker.png",
+ "folder": "item",
+ "namespace": "nerospace",
+ "id": "0",
+ "particle": true,
+ "render_mode": "default",
+ "render_sides": "auto",
+ "frame_time": 1,
+ "frame_order_type": "loop",
+ "frame_order": "",
+ "frame_interpolate": false,
+ "visible": true,
+ "mode": "bitmap",
+ "saved": true,
+ "uuid": "a877d92b-b204-4209-98f9-9479686c313a",
+ "relative_path": "../../../src/main/resources/assets/nerospace/textures/item/meteor_tracker.png",
+ "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAl0lEQVR4nGNgoBAwYhOUUdD5j038yYMrGOoxBGCaWyYsQxGvKYjCaQiK5gUbLv1fsOHSfwk5FRQME8flOrhmdI0VH35gGIRsCBM+PydcuoJCY/MqXtuxuQDdFUw4AwQKFujp4JUnaMCuh7cpM0B3oyNxBtQURDF0TFpHyDyGjknr4GkCbgDexIEDYNVDTkKiOClTnJkoBgDWcISiLh/RmwAAAABJRU5ErkJggg=="
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/blockstates/meteor_core.json b/src/generated/resources/assets/nerospace/blockstates/meteor_core.json
new file mode 100644
index 0000000..45319af
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/blockstates/meteor_core.json
@@ -0,0 +1,7 @@
+{
+ "variants": {
+ "": {
+ "model": "nerospace:block/meteor_core"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/blockstates/meteor_rock.json b/src/generated/resources/assets/nerospace/blockstates/meteor_rock.json
new file mode 100644
index 0000000..d27dcf2
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/blockstates/meteor_rock.json
@@ -0,0 +1,7 @@
+{
+ "variants": {
+ "": {
+ "model": "nerospace:block/meteor_rock"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/items/alien_core.json b/src/generated/resources/assets/nerospace/items/alien_core.json
new file mode 100644
index 0000000..90d9d61
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/items/alien_core.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "nerospace:item/alien_core"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/items/alien_fragment.json b/src/generated/resources/assets/nerospace/items/alien_fragment.json
new file mode 100644
index 0000000..4bacabf
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/items/alien_fragment.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "nerospace:item/alien_fragment"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/items/alien_tech_scrap.json b/src/generated/resources/assets/nerospace/items/alien_tech_scrap.json
new file mode 100644
index 0000000..b315c60
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/items/alien_tech_scrap.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "nerospace:item/alien_tech_scrap"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/items/meteor_caller.json b/src/generated/resources/assets/nerospace/items/meteor_caller.json
new file mode 100644
index 0000000..75db1c4
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/items/meteor_caller.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "nerospace:item/meteor_caller"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/items/meteor_core.json b/src/generated/resources/assets/nerospace/items/meteor_core.json
new file mode 100644
index 0000000..16b7b2c
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/items/meteor_core.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "nerospace:block/meteor_core"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/items/meteor_rock.json b/src/generated/resources/assets/nerospace/items/meteor_rock.json
new file mode 100644
index 0000000..fb59abb
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/items/meteor_rock.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "nerospace:block/meteor_rock"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/items/meteor_tracker.json b/src/generated/resources/assets/nerospace/items/meteor_tracker.json
new file mode 100644
index 0000000..5629eb7
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/items/meteor_tracker.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "nerospace:item/meteor_tracker"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/lang/en_us.json b/src/generated/resources/assets/nerospace/lang/en_us.json
index 791225e..b45a256 100644
--- a/src/generated/resources/assets/nerospace/lang/en_us.json
+++ b/src/generated/resources/assets/nerospace/lang/en_us.json
@@ -30,6 +30,8 @@
"block.nerospace.launch_gantry": "Launch Gantry",
"block.nerospace.launch_gantry.boarded": "Boarded the rocket — strap in",
"block.nerospace.launch_gantry.no_rocket": "No rocket on the pad to board",
+ "block.nerospace.meteor_core": "Meteor Core",
+ "block.nerospace.meteor_rock": "Meteor Rock",
"block.nerospace.nerosium_block": "Block of Nerosium",
"block.nerospace.nerosium_grinder": "Nerosium Grinder",
"block.nerospace.nerosium_ore": "Nerosium Ore",
@@ -89,6 +91,7 @@
"container.nerospace.terraformer": "Terraformer",
"entity.nerospace.cinder_stalker": "Cinder Stalker",
"entity.nerospace.ember_strutter": "Ember Strutter",
+ "entity.nerospace.falling_meteor": "Meteor",
"entity.nerospace.frost_strider": "Frost Strider",
"entity.nerospace.greenling": "Greenling",
"entity.nerospace.meadow_loper": "Meadow Loper",
@@ -135,6 +138,7 @@
"gui.nerospace.rocket.stations_none": "NO STATIONS",
"gui.nerospace.rocket.tier": "Tier %s Rocket",
"gui.nerospace.star_guide.chapter.machines": "Machines",
+ "gui.nerospace.star_guide.chapter.meteor_events": "Meteor Events",
"gui.nerospace.star_guide.chapter.mining": "Mining",
"gui.nerospace.star_guide.chapter.nerosium": "Nerosium",
"gui.nerospace.star_guide.chapter.new_worlds": "New Worlds",
@@ -143,6 +147,10 @@
"gui.nerospace.star_guide.chapter.terraforming": "Terraforming",
"gui.nerospace.star_guide.chapter.vacuum": "Vacuum",
"gui.nerospace.star_guide.complete": "COMPLETE",
+ "gui.nerospace.star_guide.step.alien_core": "Alien Core",
+ "gui.nerospace.star_guide.step.alien_core.text": "The rarest meteor prize. An intact Alien Core is the key to the deepest alien technology — the heart of a future upgrade tree.",
+ "gui.nerospace.star_guide.step.alien_tech": "Salvaged Tech",
+ "gui.nerospace.star_guide.step.alien_tech.text": "Rarer meteors carry Alien Tech Scrap — the raw material of the scanners and upgrades to come. Hoard it.",
"gui.nerospace.star_guide.step.battery": "Stored Potential",
"gui.nerospace.star_guide.step.battery.text": "Batteries buffer the grid: generators fill them, machines drain them through the pipe network. Build one before your first rocket launch window.",
"gui.nerospace.star_guide.step.cindara": "Into the Fire",
@@ -169,6 +177,8 @@
"gui.nerospace.star_guide.step.hydration_module.text": "A Hydration Module touching your Terraformer melts glacite into water for the Hydrated stage — basins fill into lakes behind the green frontier.",
"gui.nerospace.star_guide.step.living_world": "World Awake",
"gui.nerospace.star_guide.step.living_world.text": "Behind the water, the land matures: natural colour, trees, rain — and the first herds. Stand on Living ground and watch a world breathe on its own.",
+ "gui.nerospace.star_guide.step.meteor_site": "Visitor from Beyond",
+ "gui.nerospace.star_guide.step.meteor_site.text": "Meteors crash on the Overworld and the planets, leaving a small crater around a glowing Meteor Core. Break the core to claim Alien Fragments and a jump-start of off-world ores — hold a Meteor Tracker to find the way.",
"gui.nerospace.star_guide.step.nerosium_dust": "Finely Ground",
"gui.nerospace.star_guide.step.nerosium_dust.text": "Grind raw nerosium into dust, then smelt the dust into ingots — two for one.",
"gui.nerospace.star_guide.step.nerosium_grinder": "Industrial Revolution",
@@ -236,6 +246,9 @@
"gui.nerospace.terraformer.stages": "Radii: %s / %s / %s",
"gui.nerospace.terraformer.tier": "Tier %s",
"gui.nerospace.terraformer.working": "Terraforming",
+ "item.nerospace.alien_core": "Alien Core",
+ "item.nerospace.alien_fragment": "Alien Fragment",
+ "item.nerospace.alien_tech_scrap": "Alien Tech Scrap",
"item.nerospace.capacity_upgrade": "Capacity Upgrade",
"item.nerospace.cindara_compass": "Cindara Compass",
"item.nerospace.cinder_stalker_spawn_egg": "Cinder Stalker Spawn Egg",
@@ -259,6 +272,14 @@
"item.nerospace.greenxertz_navigator.travel": "Transported to Greenxertz",
"item.nerospace.loper_haunch": "Loper Haunch",
"item.nerospace.meadow_loper_spawn_egg": "Meadow Loper Spawn Egg",
+ "item.nerospace.meteor_caller": "Meteor Caller",
+ "item.nerospace.meteor_caller.called": "A meteor streaks down from the sky...",
+ "item.nerospace.meteor_caller.creative_only": "The Meteor Caller only works in Creative mode",
+ "item.nerospace.meteor_tracker": "Meteor Tracker",
+ "item.nerospace.meteor_tracker.incoming": "Incoming",
+ "item.nerospace.meteor_tracker.landed": "Landed",
+ "item.nerospace.meteor_tracker.none": "Meteor Tracker: no meteors detected",
+ "item.nerospace.meteor_tracker.readout": "☄ Meteor %s — %s, %sm",
"item.nerospace.nerosium_dust": "Nerosium Dust",
"item.nerospace.nerosium_ingot": "Nerosium Ingot",
"item.nerospace.nerosium_pickaxe": "Nerosium Pickaxe",
diff --git a/src/generated/resources/assets/nerospace/models/block/meteor_core.json b/src/generated/resources/assets/nerospace/models/block/meteor_core.json
new file mode 100644
index 0000000..84bfb33
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/models/block/meteor_core.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:block/cube_all",
+ "textures": {
+ "all": "nerospace:block/meteor_core"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/models/block/meteor_rock.json b/src/generated/resources/assets/nerospace/models/block/meteor_rock.json
new file mode 100644
index 0000000..a9bbcc8
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/models/block/meteor_rock.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:block/cube_all",
+ "textures": {
+ "all": "nerospace:block/meteor_rock"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/models/item/alien_core.json b/src/generated/resources/assets/nerospace/models/item/alien_core.json
new file mode 100644
index 0000000..2257cf0
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/models/item/alien_core.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "nerospace:item/alien_core"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/models/item/alien_fragment.json b/src/generated/resources/assets/nerospace/models/item/alien_fragment.json
new file mode 100644
index 0000000..5442743
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/models/item/alien_fragment.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "nerospace:item/alien_fragment"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/models/item/alien_tech_scrap.json b/src/generated/resources/assets/nerospace/models/item/alien_tech_scrap.json
new file mode 100644
index 0000000..894d2e5
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/models/item/alien_tech_scrap.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "nerospace:item/alien_tech_scrap"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/models/item/meteor_caller.json b/src/generated/resources/assets/nerospace/models/item/meteor_caller.json
new file mode 100644
index 0000000..fb2e87e
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/models/item/meteor_caller.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "nerospace:item/meteor_caller"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/assets/nerospace/models/item/meteor_tracker.json b/src/generated/resources/assets/nerospace/models/item/meteor_tracker.json
new file mode 100644
index 0000000..33d38e4
--- /dev/null
+++ b/src/generated/resources/assets/nerospace/models/item/meteor_tracker.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "nerospace:item/meteor_tracker"
+ }
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json b/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json
index 9018ee7..c803bf4 100644
--- a/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json
+++ b/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json
@@ -35,6 +35,8 @@
"nerospace:quarry_controller",
"nerospace:quarry_landmark",
"nerospace:quarry_frame",
- "nerospace:trash_can"
+ "nerospace:trash_can",
+ "nerospace:meteor_rock",
+ "nerospace:meteor_core"
]
}
\ No newline at end of file
diff --git a/src/generated/resources/data/nerospace/advancement/guide/alien_core.json b/src/generated/resources/data/nerospace/advancement/guide/alien_core.json
new file mode 100644
index 0000000..6a8b172
--- /dev/null
+++ b/src/generated/resources/data/nerospace/advancement/guide/alien_core.json
@@ -0,0 +1,28 @@
+{
+ "parent": "nerospace:guide/alien_tech_scrap",
+ "criteria": {
+ "has_item": {
+ "conditions": {
+ "items": [
+ {
+ "items": "nerospace:alien_core"
+ }
+ ]
+ },
+ "trigger": "minecraft:inventory_changed"
+ }
+ },
+ "display": {
+ "description": "Find the rare Alien Core inside a meteor",
+ "icon": {
+ "id": "nerospace:alien_core"
+ },
+ "title": "Alien Core"
+ },
+ "requirements": [
+ [
+ "has_item"
+ ]
+ ],
+ "sends_telemetry_event": true
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/nerospace/advancement/guide/alien_fragment.json b/src/generated/resources/data/nerospace/advancement/guide/alien_fragment.json
new file mode 100644
index 0000000..95eae67
--- /dev/null
+++ b/src/generated/resources/data/nerospace/advancement/guide/alien_fragment.json
@@ -0,0 +1,28 @@
+{
+ "parent": "nerospace:root",
+ "criteria": {
+ "has_item": {
+ "conditions": {
+ "items": [
+ {
+ "items": "nerospace:alien_fragment"
+ }
+ ]
+ },
+ "trigger": "minecraft:inventory_changed"
+ }
+ },
+ "display": {
+ "description": "Loot a fallen meteor for an Alien Fragment",
+ "icon": {
+ "id": "nerospace:alien_fragment"
+ },
+ "title": "Visitor from Beyond"
+ },
+ "requirements": [
+ [
+ "has_item"
+ ]
+ ],
+ "sends_telemetry_event": true
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/nerospace/advancement/guide/alien_tech_scrap.json b/src/generated/resources/data/nerospace/advancement/guide/alien_tech_scrap.json
new file mode 100644
index 0000000..e0e9715
--- /dev/null
+++ b/src/generated/resources/data/nerospace/advancement/guide/alien_tech_scrap.json
@@ -0,0 +1,28 @@
+{
+ "parent": "nerospace:guide/alien_fragment",
+ "criteria": {
+ "has_item": {
+ "conditions": {
+ "items": [
+ {
+ "items": "nerospace:alien_tech_scrap"
+ }
+ ]
+ },
+ "trigger": "minecraft:inventory_changed"
+ }
+ },
+ "display": {
+ "description": "Recover Alien Tech Scrap from a meteor",
+ "icon": {
+ "id": "nerospace:alien_tech_scrap"
+ },
+ "title": "Salvaged Tech"
+ },
+ "requirements": [
+ [
+ "has_item"
+ ]
+ ],
+ "sends_telemetry_event": true
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/nerospace/loot_table/blocks/meteor_rock.json b/src/generated/resources/data/nerospace/loot_table/blocks/meteor_rock.json
new file mode 100644
index 0000000..c637909
--- /dev/null
+++ b/src/generated/resources/data/nerospace/loot_table/blocks/meteor_rock.json
@@ -0,0 +1,21 @@
+{
+ "type": "minecraft:block",
+ "pools": [
+ {
+ "bonus_rolls": 0.0,
+ "conditions": [
+ {
+ "condition": "minecraft:survives_explosion"
+ }
+ ],
+ "entries": [
+ {
+ "type": "minecraft:item",
+ "name": "nerospace:meteor_rock"
+ }
+ ],
+ "rolls": 1.0
+ }
+ ],
+ "random_sequence": "nerospace:blocks/meteor_rock"
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/nerospace/tags/item/alien_materials.json b/src/generated/resources/data/nerospace/tags/item/alien_materials.json
new file mode 100644
index 0000000..6574dee
--- /dev/null
+++ b/src/generated/resources/data/nerospace/tags/item/alien_materials.json
@@ -0,0 +1,7 @@
+{
+ "values": [
+ "nerospace:alien_fragment",
+ "nerospace:alien_tech_scrap",
+ "nerospace:alien_core"
+ ]
+}
\ No newline at end of file
diff --git a/src/main/java/za/co/neroland/nerospace/Config.java b/src/main/java/za/co/neroland/nerospace/Config.java
index 89ea83d..fb8ed49 100644
--- a/src/main/java/za/co/neroland/nerospace/Config.java
+++ b/src/main/java/za/co/neroland/nerospace/Config.java
@@ -206,6 +206,48 @@ public class Config {
.comment("Guard on how many chunks active terraforming may force-load at once.")
.defineInRange("terraformMaxForcedChunks", 16, 0, 256);
+ // --- Meteor events (meteor-events-design.md) -----------------------------
+ // Spawn pacing + loot tunables. Defaults give roughly one natural meteor every ~2-3 play-hours
+ // per active level; tune for busier or quieter skies.
+
+ public static final ModConfigSpec.BooleanValue METEOR_NATURAL_SPAWN = BUILDER
+ .comment("Whether meteors fall naturally near players (the creative Meteor Caller works either way).")
+ .define("meteorNaturalSpawn", true);
+
+ public static final ModConfigSpec.IntValue METEOR_AVG_INTERVAL_SECONDS = BUILDER
+ .comment("Average seconds between natural meteor impacts on an eligible dimension with players online.",
+ "Default 9000 (~2.5 hours). Each interval is randomised 0.66x..1.33x so impacts feel irregular.")
+ .defineInRange("meteorAvgIntervalSeconds", 9000, 60, 1_000_000);
+
+ public static final ModConfigSpec.IntValue METEOR_WARNING_SECONDS = BUILDER
+ .comment("Warning window: seconds a meteor is tracked as 'incoming' before it actually falls.")
+ .defineInRange("meteorWarningSeconds", 30, 0, 600);
+
+ public static final ModConfigSpec.IntValue METEOR_MIN_DISTANCE = BUILDER
+ .comment("Minimum horizontal distance (blocks) from the anchor player a meteor targets.")
+ .defineInRange("meteorMinDistance", 200, 0, 2000);
+
+ public static final ModConfigSpec.IntValue METEOR_MAX_DISTANCE = BUILDER
+ .comment("Maximum horizontal distance (blocks) from the anchor player a meteor targets.")
+ .defineInRange("meteorMaxDistance", 500, 16, 4000);
+
+ public static final ModConfigSpec.IntValue METEOR_CRATER_RADIUS = BUILDER
+ .comment("Radius (blocks) of the small crater a meteor carves. Kept modest to avoid griefing builds.")
+ .defineInRange("meteorCraterRadius", 3, 1, 8);
+
+ public static final ModConfigSpec.IntValue METEOR_MAX_ACTIVE_SITES = BUILDER
+ .comment("Max simultaneous scheduled/falling meteors tracked per dimension.")
+ .defineInRange("meteorMaxActiveSites", 4, 1, 64);
+
+ public static final ModConfigSpec.IntValue METEOR_LOOT_BONUS_ROLLS = BUILDER
+ .comment("Weighted bonus loot rolls in a meteor core, on top of its guaranteed alien fragments.")
+ .defineInRange("meteorLootBonusRolls", 3, 0, 32);
+
+ public static final ModConfigSpec.BooleanValue METEOR_DEBUG_LOG = BUILDER
+ .comment("Verbose, NON-personal meteor logging (dimension + coordinates only, never player",
+ "identifiers). Off by shipped default (POPIA/GDPR).")
+ .define("meteorDebugLog", false);
+
static final ModConfigSpec SPEC = BUILDER.build();
/** Client oxygen-visual quality tiers. */
diff --git a/src/main/java/za/co/neroland/nerospace/NerospaceClient.java b/src/main/java/za/co/neroland/nerospace/NerospaceClient.java
index 31a875a..b3b9431 100644
--- a/src/main/java/za/co/neroland/nerospace/NerospaceClient.java
+++ b/src/main/java/za/co/neroland/nerospace/NerospaceClient.java
@@ -9,7 +9,9 @@
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
+import net.minecraft.world.phys.Vec3;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
@@ -41,8 +43,13 @@
import za.co.neroland.nerospace.client.OxygenGeneratorScreen;
import za.co.neroland.nerospace.client.CombustionGeneratorScreen;
import za.co.neroland.nerospace.client.PassiveGeneratorScreen;
+import za.co.neroland.nerospace.client.ClientMeteorTracker;
+import za.co.neroland.nerospace.client.FallingMeteorModel;
+import za.co.neroland.nerospace.client.FallingMeteorRenderer;
import za.co.neroland.nerospace.client.RocketModel;
import za.co.neroland.nerospace.client.RocketRenderer;
+import za.co.neroland.nerospace.meteor.MeteorSite;
+import za.co.neroland.nerospace.registry.ModItems;
import za.co.neroland.nerospace.client.UniversalPipeRenderer;
import za.co.neroland.nerospace.client.RocketScreen;
import za.co.neroland.nerospace.client.TerraformerScreen;
@@ -131,6 +138,8 @@ static void onRenderGuiLayer(RenderGuiLayerEvent.Pre event) {
@SubscribeEvent
static void onRegisterEntityRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(ModEntities.ROCKET.get(), RocketRenderer::new);
+ // Meteor events (meteor-events-design.md): the tumbling, trailing falling meteor.
+ event.registerEntityRenderer(ModEntities.FALLING_METEOR.get(), FallingMeteorRenderer::new);
// Each creature now has its own model geometry; the scale just fine-tunes size.
event.registerEntityRenderer(ModEntities.XERTZ_STALKER.get(),
context -> new GreenxertzCreatureRenderer(context,
@@ -204,6 +213,7 @@ static void onRegisterLayerDefinitions(EntityRenderersEvent.RegisterLayerDefinit
event.registerLayerDefinition(za.co.neroland.nerospace.client.WoollyDriftModel.LAYER,
za.co.neroland.nerospace.client.WoollyDriftModel::createBodyLayer);
event.registerLayerDefinition(RocketModel.LAYER, RocketModel::createBodyLayer);
+ event.registerLayerDefinition(FallingMeteorModel.LAYER, FallingMeteorModel::createBodyLayer);
// Per-tier rocket geometry (ART_OVERHAUL_DESIGN.md §4.2).
event.registerLayerDefinition(za.co.neroland.nerospace.client.RocketT2Model.LAYER,
za.co.neroland.nerospace.client.RocketT2Model::createBodyLayer);
@@ -326,6 +336,44 @@ private static float lerp(float from, float to, float t) {
return from + (to - from) * t;
}
+ private static final String[] COMPASS_8 = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
+
+ /**
+ * Meteor Tracker readout (meteor-events design §6): while the player holds a tracker, show the
+ * nearest meteor's state (incoming / landed), compass heading and distance in the action bar.
+ * Server-authoritative — the data arrives via {@link ClientMeteorTracker}; this only presents it.
+ */
+ @SubscribeEvent
+ static void onMeteorTrackerTick(ClientTickEvent.Post event) {
+ Minecraft mc = Minecraft.getInstance();
+ if (mc.player == null || mc.level == null || mc.isPaused()) {
+ return;
+ }
+ boolean holding = mc.player.getMainHandItem().is(ModItems.METEOR_TRACKER.get())
+ || mc.player.getOffhandItem().is(ModItems.METEOR_TRACKER.get());
+ if (!holding) {
+ return;
+ }
+ if (!ClientMeteorTracker.isPresent()) {
+ mc.gui.setOverlayMessage(
+ Component.translatable("item.nerospace.meteor_tracker.none"), false);
+ return;
+ }
+ BlockPos target = ClientMeteorTracker.pos();
+ Vec3 p = mc.player.position();
+ double dx = target.getX() + 0.5D - p.x;
+ double dz = target.getZ() + 0.5D - p.z;
+ int dist = (int) Math.round(Math.sqrt(dx * dx + dz * dz));
+ // Bearing where North = -Z, East = +X (Minecraft convention).
+ double deg = (Math.toDegrees(Math.atan2(dx, -dz)) + 360.0D) % 360.0D;
+ String heading = COMPASS_8[(int) Math.round(deg / 45.0D) & 7];
+ Component state = Component.translatable(ClientMeteorTracker.state() == MeteorSite.LANDED
+ ? "item.nerospace.meteor_tracker.landed"
+ : "item.nerospace.meteor_tracker.incoming");
+ mc.gui.setOverlayMessage(
+ Component.translatable("item.nerospace.meteor_tracker.readout", state, heading, dist), false);
+ }
+
private static Identifier entityTexture(String name) {
return Identifier.fromNamespaceAndPath(Nerospace.MODID, "textures/entity/" + name + ".png");
}
diff --git a/src/main/java/za/co/neroland/nerospace/client/ClientMeteorTracker.java b/src/main/java/za/co/neroland/nerospace/client/ClientMeteorTracker.java
new file mode 100644
index 0000000..2b6b912
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/client/ClientMeteorTracker.java
@@ -0,0 +1,41 @@
+package za.co.neroland.nerospace.client;
+
+import javax.annotation.Nullable;
+
+import net.minecraft.core.BlockPos;
+
+import za.co.neroland.nerospace.network.MeteorSyncPayload;
+
+/**
+ * Client-side holder for the latest nearest-meteor snapshot (meteor-events design §6). Fed by
+ * {@link MeteorSyncPayload}; read by the tracker readout in {@code NerospaceClient.onClientTick}.
+ */
+public final class ClientMeteorTracker {
+
+ private static boolean present;
+ @Nullable
+ private static BlockPos pos;
+ private static int state;
+
+ private ClientMeteorTracker() {
+ }
+
+ public static void accept(MeteorSyncPayload payload) {
+ present = payload.present();
+ pos = payload.present() ? BlockPos.of(payload.pos()) : null;
+ state = payload.state();
+ }
+
+ public static boolean isPresent() {
+ return present && pos != null;
+ }
+
+ @Nullable
+ public static BlockPos pos() {
+ return pos;
+ }
+
+ public static int state() {
+ return state;
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/client/FallingMeteorModel.java b/src/main/java/za/co/neroland/nerospace/client/FallingMeteorModel.java
new file mode 100644
index 0000000..2956caf
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/client/FallingMeteorModel.java
@@ -0,0 +1,44 @@
+package za.co.neroland.nerospace.client;
+
+import net.minecraft.client.model.EntityModel;
+import net.minecraft.client.model.geom.ModelLayerLocation;
+import net.minecraft.client.model.geom.ModelPart;
+import net.minecraft.client.model.geom.PartPose;
+import net.minecraft.client.model.geom.builders.CubeListBuilder;
+import net.minecraft.client.model.geom.builders.LayerDefinition;
+import net.minecraft.client.model.geom.builders.MeshDefinition;
+import net.minecraft.client.model.geom.builders.PartDefinition;
+import net.minecraft.client.renderer.entity.state.EntityRenderState;
+import net.minecraft.resources.Identifier;
+
+import za.co.neroland.nerospace.Nerospace;
+
+/**
+ * A lumpy meteor: a chunky charred core with a couple of bumps for an irregular silhouette. Built
+ * with the 26.1 {@code LayerDefinition} mesh API; the renderer tumbles it and the entity trails fire.
+ * Authored purely in Java (not registered with {@code model_sync.py}), so the build never depends on
+ * a Blockbench source for it.
+ */
+public class FallingMeteorModel extends EntityModel {
+
+ public static final ModelLayerLocation LAYER = new ModelLayerLocation(
+ Identifier.fromNamespaceAndPath(Nerospace.MODID, "falling_meteor"), "main");
+
+ public FallingMeteorModel(ModelPart root) {
+ super(root);
+ }
+
+ public static LayerDefinition createBodyLayer() {
+ MeshDefinition mesh = new MeshDefinition();
+ PartDefinition root = mesh.getRoot();
+
+ root.addOrReplaceChild("core",
+ CubeListBuilder.create().texOffs(0, 0).addBox(-6F, -6F, -6F, 12F, 12F, 12F),
+ PartPose.offset(0.0F, 0.0F, 0.0F));
+ root.addOrReplaceChild("bump",
+ CubeListBuilder.create().texOffs(0, 28).addBox(3F, -8F, -2F, 6F, 6F, 6F),
+ PartPose.offset(0.0F, 0.0F, 0.0F));
+
+ return LayerDefinition.create(mesh, 64, 64);
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderState.java b/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderState.java
new file mode 100644
index 0000000..41cc3f5
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderState.java
@@ -0,0 +1,10 @@
+package za.co.neroland.nerospace.client;
+
+import net.minecraft.client.renderer.entity.state.EntityRenderState;
+
+/** Render state for the falling meteor: an age value used to spin the rock. */
+public class FallingMeteorRenderState extends EntityRenderState {
+
+ /** Entity age (ticks + partial) — drives the tumble rotation. */
+ public float ticks;
+}
diff --git a/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderer.java b/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderer.java
new file mode 100644
index 0000000..aa980ea
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderer.java
@@ -0,0 +1,64 @@
+package za.co.neroland.nerospace.client;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.math.Axis;
+
+import net.minecraft.client.renderer.SubmitNodeCollector;
+import net.minecraft.client.renderer.entity.EntityRenderer;
+import net.minecraft.client.renderer.entity.EntityRendererProvider;
+import net.minecraft.client.renderer.rendertype.RenderType;
+import net.minecraft.client.renderer.state.level.CameraRenderState;
+import net.minecraft.client.renderer.texture.OverlayTexture;
+import net.minecraft.resources.Identifier;
+
+import za.co.neroland.nerospace.Nerospace;
+import za.co.neroland.nerospace.meteor.FallingMeteorEntity;
+
+/**
+ * Entity renderer for the falling meteor (meteor-events design §4). Draws {@link FallingMeteorModel}
+ * via the 26.1 submit pipeline, tumbling it on its age, at full brightness so the molten rock glows
+ * against the sky. The flame/smoke trail is spawned by the entity itself.
+ */
+public class FallingMeteorRenderer extends EntityRenderer {
+
+ private static final Identifier TEXTURE =
+ Identifier.fromNamespaceAndPath(Nerospace.MODID, "textures/entity/falling_meteor.png");
+ private static final int FULL_BRIGHT = 0x00F000F0;
+
+ private final FallingMeteorModel model;
+
+ public FallingMeteorRenderer(EntityRendererProvider.Context context) {
+ super(context);
+ this.model = new FallingMeteorModel(context.bakeLayer(FallingMeteorModel.LAYER));
+ }
+
+ @Override
+ public FallingMeteorRenderState createRenderState() {
+ return new FallingMeteorRenderState();
+ }
+
+ @Override
+ public void extractRenderState(FallingMeteorEntity meteor, FallingMeteorRenderState state, float partialTick) {
+ super.extractRenderState(meteor, state, partialTick);
+ state.ticks = meteor.tickCount + partialTick;
+ }
+
+ @Override
+ public void submit(FallingMeteorRenderState state, PoseStack poseStack, SubmitNodeCollector collector,
+ CameraRenderState cameraState) {
+ poseStack.pushPose();
+ // Standard entity-model orientation (flip into model space), then tumble on the age so the
+ // rock spins as it falls.
+ poseStack.scale(-1.0F, -1.0F, 1.0F);
+ poseStack.translate(0.0F, -0.7F, 0.0F);
+ poseStack.mulPose(Axis.YP.rotationDegrees(state.ticks * 7.0F));
+ poseStack.mulPose(Axis.XP.rotationDegrees(state.ticks * 5.0F));
+
+ RenderType renderType = this.model.renderType(TEXTURE);
+ collector.order(0).submitModel(this.model, state, poseStack, renderType,
+ FULL_BRIGHT, OverlayTexture.NO_OVERLAY, -1, null, 0, null);
+
+ poseStack.popPose();
+ super.submit(state, poseStack, collector, cameraState);
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/client/GalleryCaptureHarness.java b/src/main/java/za/co/neroland/nerospace/client/GalleryCaptureHarness.java
index fd07f82..b5b664d 100644
--- a/src/main/java/za/co/neroland/nerospace/client/GalleryCaptureHarness.java
+++ b/src/main/java/za/co/neroland/nerospace/client/GalleryCaptureHarness.java
@@ -329,6 +329,10 @@ private static java.util.List buildShots(int ox, int oy, int oz) {
// glowing frame, the moving drill head and the power hookup.
shots.add(new Shot("quarry_operating", none, none, 0,
new Vec3(ox + 44, oy + 9, oz - 28), new Vec3(ox + 42, oy - 2, oz - 39)));
+ // Meteor crash site (SW, mirrors buildMeteorSite at ox-28, oz+30): low angle so the crater +
+ // glowing core read, with the hovering meteor (fy+11) and its trail filling the upper frame.
+ shots.add(new Shot("meteor_site", none, none, 0,
+ new Vec3(ox - 20, oy + 4, oz + 24), new Vec3(ox - 28, oy + 4, oz + 30)));
// Solar arrays (SW): raised, looking south down the cluster so the front-row single units AND
// the seam-joined multi-unit fields behind them (plus the cabled connector) read together.
shots.add(new Shot("solar", none, none, 0,
diff --git a/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java b/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java
index d101a99..f3be841 100644
--- a/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java
+++ b/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java
@@ -36,6 +36,8 @@
import net.neoforged.neoforge.transfer.item.ItemResource;
import za.co.neroland.nerospace.Nerospace;
+import za.co.neroland.nerospace.meteor.FallingMeteorEntity;
+import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity;
import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity;
import za.co.neroland.nerospace.machine.FuelRefineryBlockEntity;
import za.co.neroland.nerospace.machine.HydrationModuleBlockEntity;
@@ -329,6 +331,10 @@ private static int buildGallery(CommandSourceStack source) {
spawnShowcase(level, creatures.get(i), new BlockPos(mx + i * 4, fy + 1, mz + 1), true);
}
+ // METEOR SITE (meteor-events-design.md): a small crater of meteor_rock around a loot-bearing
+ // meteor_core, with a frozen meteor hovering above it (spins + trails for the shot). SW spoke.
+ buildMeteorSite(level, floor, origin.getX() - 28, origin.getZ() + 30, fy);
+
// QUARRY (MINER_DESIGN): two NE displays.
// 1. Landmark-only — three landmarks in an L (shows the projected marker lasers).
// 2. Fully operating — a powered quarry mid-dig: frame ring, drill head, a real pit forming.
@@ -360,9 +366,10 @@ private static int buildGallery(CommandSourceStack source) {
+ "line, oxygen generator + lever, terraformer crew + lever — flip a lever to start "
+ "those two), 4 live pipe scenarios (energy/fluid/gas/items), all 4 suit variants, "
+ "a loaded Star Guide pedestal, all 4 rocket tiers on their required pads (3x3, "
- + "3x3, walled ring, Heavy Launch Complex), 8 creatures (frozen for clean shots), and "
- + "the solar arrays (T1/T2/T3 single units + a seam-joined field per tier + a cabled "
- + "hookup showing the power connector)."), false);
+ + "3x3, walled ring, Heavy Launch Complex), 8 creatures (frozen for clean shots), "
+ + "a meteor crash site (crater + loot core + hovering meteor), and the solar arrays "
+ + "(T1/T2/T3 single units + a seam-joined field per tier + a cabled hookup showing "
+ + "the power connector)."), false);
return Command.SINGLE_SUCCESS;
}
@@ -520,6 +527,34 @@ private static void buildGalleryQuarry(ServerLevel level, BlockState floor, int
}
}
+ /**
+ * A showcase meteor crash site: a 7x7 floor pad, a 5x5 {@code meteor_rock} crater floor with a
+ * raised rim, a loot-pre-rolled {@code meteor_core} nestled in the centre, and a frozen
+ * {@link FallingMeteorEntity} hovering above (spins + trails, but never falls — gallery only).
+ */
+ private static void buildMeteorSite(ServerLevel level, BlockState floor, int cx, int cz, int fy) {
+ for (int dx = -3; dx <= 3; dx++) {
+ for (int dz = -3; dz <= 3; dz++) {
+ level.setBlockAndUpdate(new BlockPos(cx + dx, fy, cz + dz), floor);
+ }
+ }
+ BlockState rock = ModBlocks.METEOR_ROCK.get().defaultBlockState();
+ for (int dx = -2; dx <= 2; dx++) {
+ for (int dz = -2; dz <= 2; dz++) {
+ level.setBlockAndUpdate(new BlockPos(cx + dx, fy + 1, cz + dz), rock); // crater floor
+ if (Math.abs(dx) == 2 || Math.abs(dz) == 2) {
+ level.setBlockAndUpdate(new BlockPos(cx + dx, fy + 2, cz + dz), rock); // raised rim
+ }
+ }
+ }
+ BlockPos corePos = new BlockPos(cx, fy + 2, cz);
+ level.setBlockAndUpdate(corePos, ModBlocks.METEOR_CORE.get().defaultBlockState());
+ if (level.getBlockEntity(corePos) instanceof MeteorCoreBlockEntity core) {
+ core.generateLoot(level.getRandom().nextLong());
+ }
+ FallingMeteorEntity.spawnFrozen(level, cx + 0.5D, fy + 11, cz + 0.5D);
+ }
+
/** A full {@code size x size} square of launch pads with min-corner {@code corner}. */
private static void fillPad(ServerLevel level, BlockPos corner, int size, BlockState pad) {
for (int dx = 0; dx < size; dx++) {
diff --git a/src/main/java/za/co/neroland/nerospace/datagen/ModAdvancements.java b/src/main/java/za/co/neroland/nerospace/datagen/ModAdvancements.java
index 47f9893..6c558aa 100644
--- a/src/main/java/za/co/neroland/nerospace/datagen/ModAdvancements.java
+++ b/src/main/java/za/co/neroland/nerospace/datagen/ModAdvancements.java
@@ -286,6 +286,18 @@ public void generate(HolderLookup.Provider registries, ConsumerMotion is recomputed each tick by aiming at the stored target at a fixed speed, so the meteor
+ * always lands exactly where it was scheduled and the descent survives a mid-flight save/reload
+ * without persisting velocity.
+ */
+public class FallingMeteorEntity extends Entity {
+
+ /** Blocks above the target the meteor spawns at. */
+ public static final int FALL_HEIGHT = 150;
+ /** Blocks travelled per tick (fast + dramatic). */
+ private static final double SPEED = 1.7D;
+
+ private int targetX;
+ private int targetY;
+ private int targetZ;
+ private long lootSeed;
+ /** Gallery/showcase only: hover in place (spin + trail) instead of falling. Not persisted. */
+ private boolean frozen;
+
+ private final InterpolationHandler interpolation = new InterpolationHandler(this);
+
+ @SuppressWarnings("this-escape")
+ public FallingMeteorEntity(EntityType extends FallingMeteorEntity> type, Level level) {
+ super(type, level);
+ this.setNoGravity(true);
+ this.noPhysics = true; // we step the position manually; no vanilla collision pushback
+ }
+
+ /**
+ * Spawns a meteor aimed at {@code target} (a surface block position) with RNG loot from
+ * {@code seed}. The spawn point is high above the target with a random horizontal offset so the
+ * descent reads as a diagonal arc. Server-side.
+ */
+ public static FallingMeteorEntity spawn(ServerLevel level, BlockPos target, long seed) {
+ FallingMeteorEntity meteor = new FallingMeteorEntity(ModEntities.FALLING_METEOR.get(), level);
+ meteor.targetX = target.getX();
+ meteor.targetY = target.getY();
+ meteor.targetZ = target.getZ();
+ meteor.lootSeed = seed;
+
+ double angle = level.getRandom().nextDouble() * Math.PI * 2.0D;
+ double offset = FALL_HEIGHT * 0.45D;
+ double sx = target.getX() + 0.5D + Math.cos(angle) * offset;
+ double sz = target.getZ() + 0.5D + Math.sin(angle) * offset;
+ meteor.setPos(sx, target.getY() + FALL_HEIGHT, sz);
+ level.addFreshEntity(meteor);
+ level.playSound(null, target, SoundEvents.FIREWORK_ROCKET_LARGE_BLAST_FAR, SoundSource.AMBIENT, 4.0F, 0.6F);
+ return meteor;
+ }
+
+ /** Spawns a non-falling meteor that hovers + spins + trails — for the gallery/showcase only. */
+ public static FallingMeteorEntity spawnFrozen(ServerLevel level, double x, double y, double z) {
+ FallingMeteorEntity meteor = new FallingMeteorEntity(ModEntities.FALLING_METEOR.get(), level);
+ meteor.frozen = true;
+ meteor.setPos(x, y, z);
+ level.addFreshEntity(meteor);
+ return meteor;
+ }
+
+ @Override
+ protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) {
+ // No synced data: the client renders from the tracked position + spins on tickCount.
+ }
+
+ @Override
+ public InterpolationHandler getInterpolation() {
+ return this.interpolation;
+ }
+
+ private Vec3 targetVec() {
+ return new Vec3(this.targetX + 0.5D, this.targetY + 0.5D, this.targetZ + 0.5D);
+ }
+
+ @Override
+ public void tick() {
+ super.tick();
+
+ if (level().isClientSide()) {
+ if (this.interpolation.hasActiveInterpolation()) {
+ this.interpolation.interpolate();
+ }
+ spawnTrail();
+ return;
+ }
+
+ if (this.frozen) {
+ return; // gallery/showcase: hover in place
+ }
+
+ Vec3 pos = position();
+ Vec3 target = targetVec();
+ Vec3 delta = target.subtract(pos);
+ double dist = delta.length();
+
+ // Impact when we reach the target column or drop to/below the crater surface.
+ if (dist <= SPEED || pos.y <= this.targetY + 0.5D) {
+ resolveImpact((ServerLevel) level());
+ return;
+ }
+
+ Vec3 step = delta.scale(SPEED / dist);
+ this.setDeltaMovement(step); // for client interpolation / rotation cues
+ this.setPos(pos.x + step.x, pos.y + step.y, pos.z + step.z);
+ }
+
+ /** Flame + smoke trail, denser as the meteor nears the ground (client-side, per the design §4). */
+ private void spawnTrail() {
+ double proximity = 1.0D - Math.min(1.0D, (getY() - this.targetY) / (double) FALL_HEIGHT);
+ int puffs = 2 + (int) (proximity * 4);
+ for (int i = 0; i < puffs; i++) {
+ double ox = (this.random.nextDouble() - 0.5D) * 0.8D;
+ double oy = (this.random.nextDouble() - 0.5D) * 0.8D;
+ double oz = (this.random.nextDouble() - 0.5D) * 0.8D;
+ level().addParticle(ParticleTypes.FLAME, getX() + ox, getY() + oy, getZ() + oz, 0.0D, 0.02D, 0.0D);
+ level().addParticle(ParticleTypes.LARGE_SMOKE, getX() + ox, getY() + 0.4D + oy, getZ() + oz,
+ 0.0D, 0.04D, 0.0D);
+ }
+ }
+
+ /**
+ * Carve a small bowl crater (meteor-events design §4): clears the bowl interior to air, lines the
+ * floor with {@code meteor_rock}, and seats a loot-bearing {@code meteor_core} at the deepest
+ * point. Non-destructive beyond the radius, never touches bedrock, no fire or wide explosion.
+ */
+ private void resolveImpact(ServerLevel level) {
+ int radius = Math.max(1, Config.METEOR_CRATER_RADIUS.get());
+ BlockPos center = new BlockPos(this.targetX, this.targetY, this.targetZ);
+ BlockState rock = ModBlocks.METEOR_ROCK.get().defaultBlockState();
+
+ for (int dx = -radius - 1; dx <= radius + 1; dx++) {
+ for (int dz = -radius - 1; dz <= radius + 1; dz++) {
+ double horiz = Math.sqrt(dx * dx + dz * dz);
+ if (horiz > radius + 0.6D) {
+ continue;
+ }
+ int depth = Math.max(1, Math.round((float) (radius - horiz) * 0.7F));
+ // Clear the open bowl above the floor.
+ for (int dy = -depth + 1; dy <= radius; dy++) {
+ if (Math.sqrt(dx * dx + dy * dy + dz * dz) > radius + 0.6D) {
+ continue;
+ }
+ BlockPos p = center.offset(dx, dy, dz);
+ if (!isProtected(level, p)) {
+ level.setBlock(p, Blocks.AIR.defaultBlockState(), 3);
+ }
+ }
+ // Line the bowl floor with meteor rock.
+ BlockPos floor = center.offset(dx, -depth, dz);
+ if (!isProtected(level, floor)) {
+ level.setBlock(floor, rock, 3);
+ }
+ }
+ }
+
+ // The loot core sits at the deepest centre point.
+ int centerDepth = Math.max(1, Math.round(radius * 0.7F));
+ BlockPos corePos = center.offset(0, -centerDepth, 0);
+ if (!isProtected(level, corePos)) {
+ level.setBlock(corePos, ModBlocks.METEOR_CORE.get().defaultBlockState(), 3);
+ if (level.getBlockEntity(corePos) instanceof MeteorCoreBlockEntity core) {
+ core.generateLoot(this.lootSeed);
+ }
+ }
+
+ // Impact feedback (no terrain-damaging explosion).
+ level.sendParticles(ParticleTypes.EXPLOSION_EMITTER, center.getX() + 0.5D, center.getY() + 1.0D,
+ center.getZ() + 0.5D, 1, 0.0D, 0.0D, 0.0D, 0.0D);
+ level.sendParticles(ParticleTypes.LARGE_SMOKE, center.getX() + 0.5D, center.getY() + 1.0D,
+ center.getZ() + 0.5D, 40, radius, 1.0D, radius, 0.02D);
+ level.playSound(null, center.getX() + 0.5D, center.getY() + 0.5D, center.getZ() + 0.5D,
+ SoundEvents.GENERIC_EXPLODE, SoundSource.BLOCKS, 6.0F, 0.7F);
+
+ MeteorEventManager.get(level).onImpact(center);
+
+ if (Config.METEOR_DEBUG_LOG.get()) {
+ // POPIA/GDPR: coordinates + dimension only, never player identifiers.
+ Nerospace.LOGGER.info("[meteor] impact dim={} pos={} radius={}", level.dimension(), center, radius);
+ }
+ discard();
+ }
+
+ /** Bedrock and other unbreakable blocks (destroy speed < 0) are left untouched. */
+ private static boolean isProtected(ServerLevel level, BlockPos pos) {
+ BlockState state = level.getBlockState(pos);
+ return state.is(Blocks.BEDROCK) || state.getDestroySpeed(level, pos) < 0.0F;
+ }
+
+ @Override
+ public boolean isPickable() {
+ return false;
+ }
+
+ @Override
+ public boolean hurtServer(ServerLevel level, net.minecraft.world.damagesource.DamageSource source, float amount) {
+ return false; // indestructible in flight
+ }
+
+ @Override
+ protected void readAdditionalSaveData(ValueInput input) {
+ this.targetX = input.getIntOr("TargetX", 0);
+ this.targetY = input.getIntOr("TargetY", 0);
+ this.targetZ = input.getIntOr("TargetZ", 0);
+ this.lootSeed = input.getLongOr("LootSeed", 0L);
+ }
+
+ @Override
+ protected void addAdditionalSaveData(ValueOutput output) {
+ output.putInt("TargetX", this.targetX);
+ output.putInt("TargetY", this.targetY);
+ output.putInt("TargetZ", this.targetZ);
+ output.putLong("LootSeed", this.lootSeed);
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/meteor/MeteorCallerItem.java b/src/main/java/za/co/neroland/nerospace/meteor/MeteorCallerItem.java
new file mode 100644
index 0000000..bd3c3a5
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/meteor/MeteorCallerItem.java
@@ -0,0 +1,39 @@
+package za.co.neroland.nerospace.meteor;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.context.UseOnContext;
+
+/**
+ * Creative-only Meteor Caller (meteor-events design §7): right-click a block to call a meteor down
+ * onto that spot with freshly rolled RNG loot — the same path natural spawning uses, on demand.
+ * Functions only for creative-mode players; in survival it does nothing (and says so).
+ */
+public class MeteorCallerItem extends Item {
+
+ public MeteorCallerItem(Properties properties) {
+ super(properties);
+ }
+
+ @Override
+ public InteractionResult useOn(UseOnContext context) {
+ Player player = context.getPlayer();
+ if (player == null || !player.getAbilities().instabuild) {
+ if (player != null && !context.getLevel().isClientSide()) {
+ player.sendSystemMessage(Component.translatable("item.nerospace.meteor_caller.creative_only"));
+ }
+ return InteractionResult.PASS;
+ }
+
+ if (context.getLevel() instanceof ServerLevel level) {
+ BlockPos target = context.getClickedPos();
+ FallingMeteorEntity.spawn(level, target, level.getRandom().nextLong());
+ player.sendSystemMessage(Component.translatable("item.nerospace.meteor_caller.called"));
+ }
+ return InteractionResult.SUCCESS;
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlock.java b/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlock.java
new file mode 100644
index 0000000..323ef47
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlock.java
@@ -0,0 +1,39 @@
+package za.co.neroland.nerospace.meteor;
+
+import com.mojang.serialization.MapCodec;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.block.BaseEntityBlock;
+import net.minecraft.world.level.block.RenderShape;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+
+/**
+ * The Meteor Core (meteor-events design §5): the glowing block at the centre of a crater that holds
+ * the meteor's RNG loot. Break-to-loot — the stored stacks spill when the core is removed, driven by
+ * {@link MeteorCoreBlockEntity#preRemoveSideEffects} (the block has no loot table, so the rolled
+ * contents survive rather than a fresh roll).
+ */
+public class MeteorCoreBlock extends BaseEntityBlock {
+
+ public static final MapCodec CODEC = simpleCodec(MeteorCoreBlock::new);
+
+ public MeteorCoreBlock(Properties properties) {
+ super(properties);
+ }
+
+ @Override
+ protected MapCodec codec() {
+ return CODEC;
+ }
+
+ @Override
+ protected RenderShape getRenderShape(BlockState state) {
+ return RenderShape.MODEL;
+ }
+
+ @Override
+ public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
+ return new MeteorCoreBlockEntity(pos, state);
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlockEntity.java b/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlockEntity.java
new file mode 100644
index 0000000..13c12eb
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlockEntity.java
@@ -0,0 +1,79 @@
+package za.co.neroland.nerospace.meteor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.sounds.SoundEvents;
+import net.minecraft.sounds.SoundSource;
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.Containers;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.storage.ValueInput;
+import net.minecraft.world.level.storage.ValueOutput;
+
+import za.co.neroland.nerospace.Config;
+import za.co.neroland.nerospace.registry.ModBlockEntities;
+
+/**
+ * The "box in the middle" of a meteor (meteor-events design §5): stores the loot rolled once when
+ * the meteor lands, so contents are fixed per meteor (no re-roll exploit) and identical for every
+ * player who reaches it. v1 is break-to-loot — {@link MeteorCoreBlock} spills these stacks when the
+ * core is broken; a clickable container GUI is a noted follow-up.
+ */
+public class MeteorCoreBlockEntity extends BlockEntity {
+
+ private final List loot = new ArrayList<>();
+
+ public MeteorCoreBlockEntity(BlockPos pos, BlockState state) {
+ super(ModBlockEntities.METEOR_CORE.get(), pos, state);
+ }
+
+ /** Rolls and stores loot from {@code seed} (idempotent — only the first non-empty roll sticks). */
+ public void generateLoot(long seed) {
+ if (!this.loot.isEmpty()) {
+ return;
+ }
+ this.loot.addAll(MeteorLoot.roll(RandomSource.create(seed), Config.METEOR_LOOT_BONUS_ROLLS.get()));
+ setChanged();
+ }
+
+ /**
+ * Break-to-loot: spill the stored stacks the moment the core is removed (mirrors the Station
+ * Core's charter pop). The block has no loot table, so this is the only drop path — the rolled
+ * contents survive rather than a fresh roll.
+ */
+ @Override
+ public void preRemoveSideEffects(BlockPos pos, BlockState state) {
+ super.preRemoveSideEffects(pos, state);
+ if (!(this.level instanceof ServerLevel serverLevel)) {
+ return;
+ }
+ for (ItemStack stack : this.loot) {
+ if (!stack.isEmpty()) {
+ Containers.dropItemStack(serverLevel,
+ pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, stack.copy());
+ }
+ }
+ if (!this.loot.isEmpty()) {
+ serverLevel.playSound(null, pos, SoundEvents.AMETHYST_BLOCK_BREAK, SoundSource.BLOCKS, 1.0F, 0.7F);
+ }
+ this.loot.clear();
+ }
+
+ @Override
+ protected void saveAdditional(ValueOutput output) {
+ super.saveAdditional(output);
+ output.store("Loot", ItemStack.OPTIONAL_CODEC.listOf(), this.loot);
+ }
+
+ @Override
+ protected void loadAdditional(ValueInput input) {
+ super.loadAdditional(input);
+ this.loot.clear();
+ this.loot.addAll(input.read("Loot", ItemStack.OPTIONAL_CODEC.listOf()).orElse(List.of()));
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/meteor/MeteorEventManager.java b/src/main/java/za/co/neroland/nerospace/meteor/MeteorEventManager.java
new file mode 100644
index 0000000..367d9ae
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/meteor/MeteorEventManager.java
@@ -0,0 +1,203 @@
+package za.co.neroland.nerospace.meteor;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.levelgen.Heightmap;
+import net.minecraft.world.level.saveddata.SavedData;
+import net.minecraft.world.level.saveddata.SavedDataType;
+
+import za.co.neroland.nerospace.Config;
+import za.co.neroland.nerospace.Nerospace;
+
+/**
+ * Per-{@link ServerLevel} driver + persistent state for natural meteor events (meteor-events design
+ * §3). Holds the live impact sites and a cooldown, schedules a rare meteor near a random online
+ * player when the cooldown elapses, advances each site (SCHEDULED → spawns the falling entity →
+ * LANDED), and answers "nearest site" for the tracker compass.
+ *
+ * Modelled on {@link za.co.neroland.nerospace.world.OxygenFieldManager}: only the sites + cooldown
+ * are persisted via the {@link SavedDataType} codec; everything reconverges from them on load.
+ */
+public final class MeteorEventManager extends SavedData {
+
+ public static final Identifier ID = Identifier.fromNamespaceAndPath(Nerospace.MODID, "meteor_events");
+
+ public static final SavedDataType TYPE =
+ new SavedDataType<>(ID, MeteorEventManager::new, codec());
+
+ /** Ticks a landed site lingers so the tracker can still lead players to a fresh crater (5 min). */
+ private static final int LANDED_EXPIRY_TICKS = 6000;
+ /** Failsafe: drop a FALLING site if its entity never reports impact (e.g. unloaded). */
+ private static final int FALLING_TIMEOUT_TICKS = 600;
+
+ private final List sites;
+ private int cooldown;
+
+ public MeteorEventManager() {
+ this(new ArrayList<>(), 0);
+ }
+
+ private MeteorEventManager(List sites, int cooldown) {
+ this.sites = new ArrayList<>(sites);
+ this.cooldown = cooldown;
+ }
+
+ private static Codec codec() {
+ return RecordCodecBuilder.create(inst -> inst.group(
+ MeteorSite.CODEC.listOf().fieldOf("sites").forGetter(m -> m.sites),
+ Codec.INT.fieldOf("cooldown").forGetter(m -> m.cooldown)
+ ).apply(inst, MeteorEventManager::new));
+ }
+
+ public static MeteorEventManager get(ServerLevel level) {
+ return level.getDataStorage().computeIfAbsent(TYPE);
+ }
+
+ // --- Tick driver --------------------------------------------------------
+
+ /** One server tick on an eligible dimension (called from {@link MeteorEvents}). */
+ public void tick(ServerLevel level) {
+ boolean dirty = scheduleIfDue(level);
+ dirty |= advanceSites(level);
+ if (dirty) {
+ setDirty();
+ }
+ }
+
+ private boolean scheduleIfDue(ServerLevel level) {
+ if (!Config.METEOR_NATURAL_SPAWN.get() || level.players().isEmpty()) {
+ return false;
+ }
+ if (this.cooldown > 0) {
+ this.cooldown--;
+ return this.cooldown % 200 == 0; // persist roughly once every 10s of countdown
+ }
+ // Cooldown elapsed: schedule one meteor near a random online player (rarity is global per level).
+ this.cooldown = nextInterval(level);
+ if (countByState(MeteorSite.SCHEDULED, MeteorSite.FALLING) >= Config.METEOR_MAX_ACTIVE_SITES.get()) {
+ return true;
+ }
+ ServerPlayer anchor = level.players().get(level.getRandom().nextInt(level.players().size()));
+ BlockPos target = pickTarget(level, anchor);
+ if (target != null) {
+ this.sites.add(new MeteorSite(target.asLong(), MeteorSite.SCHEDULED,
+ Math.max(1, Config.METEOR_WARNING_SECONDS.get() * 20)));
+ if (Config.METEOR_DEBUG_LOG.get()) {
+ Nerospace.LOGGER.info("[meteor] scheduled dim={} target={}", level.dimension(), target);
+ }
+ }
+ return true;
+ }
+
+ private boolean advanceSites(ServerLevel level) {
+ boolean dirty = false;
+ Iterator it = this.sites.iterator();
+ while (it.hasNext()) {
+ MeteorSite site = it.next();
+ switch (site.state) {
+ case MeteorSite.SCHEDULED -> {
+ if (--site.timer <= 0) {
+ FallingMeteorEntity.spawn(level, site.blockPos(), level.getRandom().nextLong());
+ site.state = MeteorSite.FALLING;
+ site.timer = FALLING_TIMEOUT_TICKS;
+ dirty = true;
+ }
+ }
+ case MeteorSite.FALLING -> {
+ if (--site.timer <= 0) {
+ it.remove(); // failsafe: entity never impacted
+ dirty = true;
+ }
+ }
+ case MeteorSite.LANDED -> {
+ if (--site.timer <= 0) {
+ it.remove();
+ dirty = true;
+ }
+ }
+ default -> it.remove();
+ }
+ }
+ return dirty;
+ }
+
+ /** Called by {@link FallingMeteorEntity} on impact: flip the matching site to LANDED (or add one). */
+ public void onImpact(BlockPos pos) {
+ for (MeteorSite site : this.sites) {
+ if (site.state != MeteorSite.LANDED && site.blockPos().closerThan(pos, 8.0D)) {
+ site.state = MeteorSite.LANDED;
+ site.timer = LANDED_EXPIRY_TICKS;
+ site.pos = pos.asLong();
+ setDirty();
+ return;
+ }
+ }
+ // Creative-spawned (or unscheduled) meteor: add a transient landed site for the tracker.
+ this.sites.add(new MeteorSite(pos.asLong(), MeteorSite.LANDED, LANDED_EXPIRY_TICKS));
+ setDirty();
+ }
+
+ /** Nearest tracked site to {@code from} (any state), or {@code null} if none. For the tracker. */
+ @Nullable
+ public MeteorSite nearestSite(BlockPos from) {
+ MeteorSite best = null;
+ double bestSq = Double.MAX_VALUE;
+ for (MeteorSite site : this.sites) {
+ double sq = site.blockPos().distSqr(from);
+ if (sq < bestSq) {
+ bestSq = sq;
+ best = site;
+ }
+ }
+ return best;
+ }
+
+ // --- Helpers ------------------------------------------------------------
+
+ private int countByState(int... states) {
+ int n = 0;
+ for (MeteorSite site : this.sites) {
+ for (int s : states) {
+ if (site.state == s) {
+ n++;
+ break;
+ }
+ }
+ }
+ return n;
+ }
+
+ private static int nextInterval(ServerLevel level) {
+ int avg = Math.max(1, Config.METEOR_AVG_INTERVAL_SECONDS.get()) * 20;
+ // Spread 0.66x .. 1.33x of the average so impacts feel irregular.
+ return (int) (avg * 0.66D) + level.getRandom().nextInt(Math.max(1, (int) (avg * 0.67D)));
+ }
+
+ @Nullable
+ private static BlockPos pickTarget(ServerLevel level, ServerPlayer anchor) {
+ int min = Math.max(0, Config.METEOR_MIN_DISTANCE.get());
+ int max = Math.max(min + 1, Config.METEOR_MAX_DISTANCE.get());
+ double angle = level.getRandom().nextDouble() * Math.PI * 2.0D;
+ double d = min + level.getRandom().nextDouble() * (max - min);
+ int x = (int) Math.floor(anchor.getX() + Math.cos(angle) * d);
+ int z = (int) Math.floor(anchor.getZ() + Math.sin(angle) * d);
+ // getHeight loads/generates the target chunk — acceptable for a rare event.
+ int surfaceAir = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z);
+ int groundY = surfaceAir - 1;
+ if (groundY <= level.getMinY() + 1) {
+ return null; // void / no terrain
+ }
+ return new BlockPos(x, groundY, z);
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java b/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java
new file mode 100644
index 0000000..6382ba4
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java
@@ -0,0 +1,69 @@
+package za.co.neroland.nerospace.meteor;
+
+import java.util.Set;
+
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.Level;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.fml.common.EventBusSubscriber;
+import net.neoforged.neoforge.event.tick.LevelTickEvent;
+import net.neoforged.neoforge.network.PacketDistributor;
+
+import za.co.neroland.nerospace.Nerospace;
+import za.co.neroland.nerospace.network.MeteorSyncPayload;
+import za.co.neroland.nerospace.registry.ModDimensions;
+import za.co.neroland.nerospace.registry.ModItems;
+
+/**
+ * Server-side driver for meteor events (meteor-events design §3/§6): ticks the per-level
+ * {@link MeteorEventManager} on eligible surface dimensions and pushes the nearest-site snapshot to
+ * any player holding a Meteor Tracker. Cheap when idle — the manager short-circuits with no players.
+ */
+@EventBusSubscriber(modid = Nerospace.MODID)
+public final class MeteorEvents {
+
+ /** Surface worlds meteors fall on (the void station is excluded — nothing to crater). */
+ public static final Set> METEOR_DIMENSIONS = Set.of(
+ Level.OVERWORLD,
+ ModDimensions.GREENXERTZ_LEVEL,
+ ModDimensions.CINDARA_LEVEL,
+ ModDimensions.GLACIRA_LEVEL);
+
+ /** How often the tracker snapshot is pushed to holders. */
+ private static final int SYNC_INTERVAL_TICKS = 10;
+
+ private MeteorEvents() {
+ }
+
+ @SubscribeEvent
+ public static void onLevelTick(LevelTickEvent.Post event) {
+ if (!(event.getLevel() instanceof ServerLevel level) || !METEOR_DIMENSIONS.contains(level.dimension())) {
+ return;
+ }
+ MeteorEventManager manager = MeteorEventManager.get(level);
+ manager.tick(level);
+
+ if (level.getGameTime() % SYNC_INTERVAL_TICKS == 0) {
+ for (ServerPlayer player : level.players()) {
+ if (!holdsTracker(player)) {
+ continue;
+ }
+ MeteorSite nearest = manager.nearestSite(player.blockPosition());
+ PacketDistributor.sendToPlayer(player, nearest == null
+ ? MeteorSyncPayload.ABSENT
+ : new MeteorSyncPayload(true, nearest.pos, nearest.state));
+ }
+ }
+ }
+
+ private static boolean holdsTracker(ServerPlayer player) {
+ return isTracker(player.getMainHandItem()) || isTracker(player.getOffhandItem());
+ }
+
+ private static boolean isTracker(ItemStack stack) {
+ return stack.is(ModItems.METEOR_TRACKER.get());
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/meteor/MeteorLoot.java b/src/main/java/za/co/neroland/nerospace/meteor/MeteorLoot.java
new file mode 100644
index 0000000..2d76f1b
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/meteor/MeteorLoot.java
@@ -0,0 +1,71 @@
+package za.co.neroland.nerospace.meteor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.ItemLike;
+
+import za.co.neroland.nerospace.registry.ModItems;
+
+/**
+ * RNG contents of a meteor core (meteor-events design §5). Rolling is deterministic for a given
+ * seed, so the {@link MeteorCoreBlockEntity} can roll once on placement and store the result — all
+ * players who reach the meteor see identical loot and there is no re-roll exploit.
+ *
+ * v1 keeps the loot table in code (sensible defaults) with a few config knobs in {@link
+ * za.co.neroland.nerospace.Config}; a data-driven loot table is a clean follow-up. Every meteor
+ * guarantees a handful of {@code alien_fragment} (the future scanner feedstock) plus a number of
+ * weighted bonus rolls drawn from existing raw ores and the rarer alien items.
+ */
+public final class MeteorLoot {
+
+ /** A single weighted entry: an item, how many to give, and its selection weight. */
+ private record Entry(ItemLike item, int min, int max, int weight) {
+ int roll(RandomSource rng) {
+ return this.min >= this.max ? this.min : this.min + rng.nextInt(this.max - this.min + 1);
+ }
+ }
+
+ private MeteorLoot() {
+ }
+
+ /** The weighted bonus pool (existing ores are common; alien tech/core are the rare prizes). */
+ private static List pool() {
+ List pool = new ArrayList<>();
+ pool.add(new Entry(ModItems.RAW_NEROSIUM.get(), 2, 5, 30));
+ pool.add(new Entry(ModItems.RAW_NEROSTEEL.get(), 2, 5, 24));
+ pool.add(new Entry(ModItems.XERTZ_QUARTZ.get(), 1, 4, 18));
+ pool.add(new Entry(ModItems.ALIEN_FRAGMENT.get(), 2, 4, 16));
+ pool.add(new Entry(ModItems.ALIEN_TECH_SCRAP.get(), 1, 2, 9));
+ pool.add(new Entry(ModItems.ALIEN_CORE.get(), 1, 1, 3));
+ return pool;
+ }
+
+ /**
+ * Rolls a fresh set of stacks for a meteor core.
+ *
+ * @param rng seeded source (use {@code RandomSource.create(seed)} for reproducibility)
+ * @param bonusRolls number of weighted bonus rolls on top of the guaranteed fragments
+ */
+ public static List roll(RandomSource rng, int bonusRolls) {
+ List out = new ArrayList<>();
+ // Guaranteed: a handful of alien fragments — every meteor seeds the scanner economy.
+ out.add(new ItemStack(ModItems.ALIEN_FRAGMENT.get(), 3 + rng.nextInt(4)));
+
+ List pool = pool();
+ int totalWeight = pool.stream().mapToInt(Entry::weight).sum();
+ for (int i = 0; i < bonusRolls; i++) {
+ int pick = rng.nextInt(totalWeight);
+ for (Entry e : pool) {
+ pick -= e.weight();
+ if (pick < 0) {
+ out.add(new ItemStack(e.item(), e.roll(rng)));
+ break;
+ }
+ }
+ }
+ return out;
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/meteor/MeteorSite.java b/src/main/java/za/co/neroland/nerospace/meteor/MeteorSite.java
new file mode 100644
index 0000000..3526533
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/meteor/MeteorSite.java
@@ -0,0 +1,41 @@
+package za.co.neroland.nerospace.meteor;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import net.minecraft.core.BlockPos;
+
+/**
+ * One tracked meteor impact site (meteor-events design §3). Mutable so the {@link MeteorEventManager}
+ * can advance its state/timer in place each tick. {@code timer} means "ticks until the fall" while
+ * {@link #SCHEDULED}, and "ticks until this record expires" once {@link #LANDED}.
+ */
+public final class MeteorSite {
+
+ /** Scheduled near a player; the tracker shows it as incoming during the warning window. */
+ public static final int SCHEDULED = 0;
+ /** The meteor entity is descending. */
+ public static final int FALLING = 1;
+ /** Landed — the crater + loot core exist; kept briefly so the tracker leads players in. */
+ public static final int LANDED = 2;
+
+ public static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group(
+ Codec.LONG.fieldOf("pos").forGetter(s -> s.pos),
+ Codec.INT.fieldOf("state").forGetter(s -> s.state),
+ Codec.INT.fieldOf("timer").forGetter(s -> s.timer)
+ ).apply(inst, MeteorSite::new));
+
+ public long pos;
+ public int state;
+ public int timer;
+
+ public MeteorSite(long pos, int state, int timer) {
+ this.pos = pos;
+ this.state = state;
+ this.timer = timer;
+ }
+
+ public BlockPos blockPos() {
+ return BlockPos.of(this.pos);
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/network/MeteorSyncPayload.java b/src/main/java/za/co/neroland/nerospace/network/MeteorSyncPayload.java
new file mode 100644
index 0000000..13307d0
--- /dev/null
+++ b/src/main/java/za/co/neroland/nerospace/network/MeteorSyncPayload.java
@@ -0,0 +1,34 @@
+package za.co.neroland.nerospace.network;
+
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import net.minecraft.network.codec.ByteBufCodecs;
+import net.minecraft.network.codec.StreamCodec;
+import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
+import net.minecraft.resources.Identifier;
+
+import za.co.neroland.nerospace.Nerospace;
+
+/**
+ * Server → client nearest-meteor snapshot for the Meteor Tracker (meteor-events design §6). Pushed
+ * only to players holding a tracker. {@code present} false means "no tracked meteor" (the needle
+ * idles); otherwise the packed position + {@link za.co.neroland.nerospace.meteor.MeteorSite} state
+ * let the client draw direction, distance and incoming/landed status.
+ */
+public record MeteorSyncPayload(boolean present, long pos, int state) implements CustomPacketPayload {
+
+ public static final Type TYPE =
+ new Type<>(Identifier.fromNamespaceAndPath(Nerospace.MODID, "meteor_sync"));
+
+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite(
+ ByteBufCodecs.BOOL, MeteorSyncPayload::present,
+ ByteBufCodecs.VAR_LONG, MeteorSyncPayload::pos,
+ ByteBufCodecs.VAR_INT, MeteorSyncPayload::state,
+ MeteorSyncPayload::new);
+
+ public static final MeteorSyncPayload ABSENT = new MeteorSyncPayload(false, 0L, 0);
+
+ @Override
+ public Type extends CustomPacketPayload> type() {
+ return TYPE;
+ }
+}
diff --git a/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java b/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java
index 4616710..e0afbbb 100644
--- a/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java
+++ b/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java
@@ -5,6 +5,7 @@
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import za.co.neroland.nerospace.Nerospace;
+import za.co.neroland.nerospace.client.ClientMeteorTracker;
import za.co.neroland.nerospace.client.ClientOxygenField;
/**
@@ -25,6 +26,10 @@ public static void register(RegisterPayloadHandlersEvent event) {
.playToClient(
OxygenFieldSyncPayload.TYPE,
OxygenFieldSyncPayload.STREAM_CODEC,
- (payload, context) -> context.enqueueWork(() -> ClientOxygenField.accept(payload)));
+ (payload, context) -> context.enqueueWork(() -> ClientOxygenField.accept(payload)))
+ .playToClient(
+ MeteorSyncPayload.TYPE,
+ MeteorSyncPayload.STREAM_CODEC,
+ (payload, context) -> context.enqueueWork(() -> ClientMeteorTracker.accept(payload)));
}
}
diff --git a/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java b/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java
index dc9eec9..5af9470 100644
--- a/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java
+++ b/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java
@@ -100,7 +100,13 @@ private static Step step(String id, Supplier extends ItemLike> icon, String ad
// Deeper terraforming (DEEPER_TERRAFORM_DESIGN.md §11): the staged finale.
step("hydration_module", () -> ModBlocks.HYDRATION_MODULE.get(), "guide/hydration_module"),
step("living_world", () -> ModItems.MEADOW_LOPER_SPAWN_EGG.get(), "guide/living_world"),
- step("new_life", () -> ModItems.LOPER_HAUNCH.get(), "guide/new_life"))));
+ step("new_life", () -> ModItems.LOPER_HAUNCH.get(), "guide/new_life"))),
+ // Meteor events (meteor-events-design.md): a parallel branch — meteors seed alien
+ // materials on the Overworld and the planets, no rocket required for the first taste.
+ new Chapter("meteor_events", List.of(
+ step("meteor_site", () -> ModItems.ALIEN_FRAGMENT.get(), "guide/alien_fragment"),
+ step("alien_tech", () -> ModItems.ALIEN_TECH_SCRAP.get(), "guide/alien_tech_scrap"),
+ step("alien_core", () -> ModItems.ALIEN_CORE.get(), "guide/alien_core"))));
public static final int CHAPTER_COUNT = CHAPTERS.size();
diff --git a/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java
index 4238e0c..9a61e5f 100644
--- a/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java
+++ b/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java
@@ -179,6 +179,14 @@ public final class ModBlockEntities {
false,
ModBlocks.STAR_GUIDE.get()));
+ // Meteor Core (meteor-events-design.md §5): stores the rolled loot, break-to-loot.
+ public static final Supplier> METEOR_CORE =
+ BLOCK_ENTITY_TYPES.register("meteor_core",
+ () -> new BlockEntityType<>(
+ za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity::new,
+ false,
+ ModBlocks.METEOR_CORE.get()));
+
private ModBlockEntities() {
}
diff --git a/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java
index 5a054d2..3f92627 100644
--- a/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java
+++ b/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java
@@ -506,6 +506,36 @@ public final class ModBlocks {
.strength(100.0F)
.noLootTable());
+ // --- Meteor events (meteor-events-design.md) ----------------------------
+
+ /**
+ * Meteor Rock: the charred crater body left by an impact. A mineable space-rock building block
+ * (any pickaxe), faintly glowing with alien heat. Drops itself.
+ */
+ public static final DeferredBlock METEOR_ROCK = BLOCKS.registerSimpleBlock(
+ "meteor_rock",
+ props -> props
+ .mapColor(MapColor.COLOR_BLACK)
+ .strength(3.0F, 4.0F)
+ .requiresCorrectToolForDrops()
+ .lightLevel(state -> 3)
+ .sound(SoundType.STONE));
+
+ /**
+ * Meteor Core: the loot-bearing block at a crater's centre (meteor-events design §5). Backed by
+ * {@link za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity}; break-to-loot, so it has no loot
+ * table — the rolled contents spill from the block entity on removal.
+ */
+ public static final DeferredBlock METEOR_CORE =
+ BLOCKS.registerBlock("meteor_core", za.co.neroland.nerospace.meteor.MeteorCoreBlock::new,
+ props -> props
+ .mapColor(MapColor.COLOR_CYAN)
+ .strength(4.0F, 6.0F)
+ .requiresCorrectToolForDrops()
+ .lightLevel(state -> 10)
+ .sound(SoundType.METAL)
+ .noLootTable());
+
// --- Developer diagnostics ----------------------------------------------
/**
diff --git a/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java b/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java
index 87bdc86..124239b 100644
--- a/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java
+++ b/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java
@@ -142,6 +142,15 @@ public final class ModCreativeModeTabs {
output.accept(ModItems.STRUTTER_DRUMSTICK.get());
output.accept(ModItems.DRIFT_FLEECE.get());
+ // Meteor events (meteor-events-design.md).
+ output.accept(ModItems.ALIEN_FRAGMENT.get());
+ output.accept(ModItems.ALIEN_TECH_SCRAP.get());
+ output.accept(ModItems.ALIEN_CORE.get());
+ output.accept(ModBlocks.METEOR_ROCK.get());
+ output.accept(ModBlocks.METEOR_CORE.get());
+ output.accept(ModItems.METEOR_TRACKER.get());
+ output.accept(ModItems.METEOR_CALLER.get());
+
// Creative-only travel devices (no survival recipe).
output.accept(ModItems.GREENXERTZ_NAVIGATOR.get());
output.accept(ModItems.STATION_COMPASS.get());
diff --git a/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java b/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java
index cbf12a0..9094c93 100644
--- a/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java
+++ b/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java
@@ -101,6 +101,18 @@ public final class ModEntities {
MobCategory.CREATURE,
builder -> builder.sized(0.9F, 1.2F).eyeHeight(1.0F).clientTrackingRange(8));
+ // --- Meteor events (meteor-events-design.md) ----------------------------
+
+ /** The falling meteor: a non-living projectile (like the rocket) that craters on impact. */
+ public static final Supplier> FALLING_METEOR =
+ ENTITY_TYPES.registerEntityType(
+ "falling_meteor",
+ za.co.neroland.nerospace.meteor.FallingMeteorEntity::new,
+ MobCategory.MISC,
+ builder -> builder.sized(1.4F, 1.4F).eyeHeight(0.7F)
+ // It spawns ~150 blocks up, so a generous tracking range lets players see it fall.
+ .clientTrackingRange(12).updateInterval(2));
+
private ModEntities() {
}
diff --git a/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/src/main/java/za/co/neroland/nerospace/registry/ModItems.java
index d598244..1f404ff 100644
--- a/src/main/java/za/co/neroland/nerospace/registry/ModItems.java
+++ b/src/main/java/za/co/neroland/nerospace/registry/ModItems.java
@@ -71,6 +71,23 @@ public final class ModItems {
/** Glacite gem — dropped by glacite ore on Glacira; feeds future suit variants/terraforming. */
public static final DeferredItem- GLACITE = ITEMS.registerSimpleItem("glacite");
+ // --- Meteor events (meteor-events-design.md) ----------------------------
+ // Tiered "alien" loot from meteor cores; the fragment is the future scanner feedstock.
+ /** Common meteor loot; future Scanner input. */
+ public static final DeferredItem
- ALIEN_FRAGMENT = ITEMS.registerSimpleItem("alien_fragment");
+ /** Uncommon meteor loot; future upgrade crafting. */
+ public static final DeferredItem
- ALIEN_TECH_SCRAP = ITEMS.registerSimpleItem("alien_tech_scrap");
+ /** Rare meteor loot; high-value scanner/upgrade gate. */
+ public static final DeferredItem
- ALIEN_CORE = ITEMS.registerSimpleItem("alien_core");
+
+ /** Meteor Tracker — points to the nearest tracked meteor (held readout; no survival recipe yet). */
+ public static final DeferredItem
- METEOR_TRACKER = ITEMS.registerItem(
+ "meteor_tracker", props -> new Item(props.stacksTo(1)));
+
+ /** Creative-only Meteor Caller — right-click a block to call a meteor down onto it. */
+ public static final DeferredItem
- METEOR_CALLER = ITEMS.registerItem(
+ "meteor_caller", props -> new za.co.neroland.nerospace.meteor.MeteorCallerItem(props.stacksTo(1)));
+
// --- Tools --------------------------------------------------------------
public static final DeferredItem
- NEROSIUM_PICKAXE = ITEMS.registerItem(
@@ -296,6 +313,12 @@ public final class ModItems {
public static final DeferredItem GLACITE_BLOCK_ITEM =
ITEMS.registerSimpleBlockItem(ModBlocks.GLACITE_BLOCK);
+ // Meteor events (meteor-events-design.md) block items.
+ public static final DeferredItem METEOR_ROCK_ITEM =
+ ITEMS.registerSimpleBlockItem(ModBlocks.METEOR_ROCK);
+ public static final DeferredItem METEOR_CORE_ITEM =
+ ITEMS.registerSimpleBlockItem(ModBlocks.METEOR_CORE);
+
// Phase 7c — station block items.
public static final DeferredItem STATION_FLOOR_ITEM =
ITEMS.registerSimpleBlockItem(ModBlocks.STATION_FLOOR);
diff --git a/src/main/java/za/co/neroland/nerospace/registry/ModTags.java b/src/main/java/za/co/neroland/nerospace/registry/ModTags.java
index 81426db..7bc43bd 100644
--- a/src/main/java/za/co/neroland/nerospace/registry/ModTags.java
+++ b/src/main/java/za/co/neroland/nerospace/registry/ModTags.java
@@ -106,5 +106,9 @@ private Items() {
* widen it.
*/
public static final TagKey
- HYDRATION_INPUT = itemTag("nerospace", "hydration_input");
+
+ // Meteor events (meteor-events-design.md): the tiered "alien" loot, grouped for the future
+ // scanner/upgrade system so recipes can accept the family.
+ public static final TagKey
- ALIEN_MATERIALS = itemTag("nerospace", "alien_materials");
}
}
diff --git a/src/main/resources/assets/nerospace/textures/block/meteor_core.png b/src/main/resources/assets/nerospace/textures/block/meteor_core.png
new file mode 100644
index 0000000..19d2727
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/meteor_core.png differ
diff --git a/src/main/resources/assets/nerospace/textures/block/meteor_rock.png b/src/main/resources/assets/nerospace/textures/block/meteor_rock.png
new file mode 100644
index 0000000..0df9be9
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/meteor_rock.png differ
diff --git a/src/main/resources/assets/nerospace/textures/entity/falling_meteor.png b/src/main/resources/assets/nerospace/textures/entity/falling_meteor.png
new file mode 100644
index 0000000..52a76a0
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/entity/falling_meteor.png differ
diff --git a/src/main/resources/assets/nerospace/textures/item/alien_core.png b/src/main/resources/assets/nerospace/textures/item/alien_core.png
new file mode 100644
index 0000000..0c0c061
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/item/alien_core.png differ
diff --git a/src/main/resources/assets/nerospace/textures/item/alien_fragment.png b/src/main/resources/assets/nerospace/textures/item/alien_fragment.png
new file mode 100644
index 0000000..36c19d1
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/item/alien_fragment.png differ
diff --git a/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png b/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png
new file mode 100644
index 0000000..2b422fc
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png differ
diff --git a/src/main/resources/assets/nerospace/textures/item/meteor_caller.png b/src/main/resources/assets/nerospace/textures/item/meteor_caller.png
new file mode 100644
index 0000000..5326867
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/item/meteor_caller.png differ
diff --git a/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png b/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png
new file mode 100644
index 0000000..59371e1
Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png differ
diff --git a/tools/gen_bbmodels.py b/tools/gen_bbmodels.py
index 5115da4..4f5b098 100644
--- a/tools/gen_bbmodels.py
+++ b/tools/gen_bbmodels.py
@@ -47,7 +47,8 @@
"quarry_controller", "quarry_landmark", "quarry_frame", "trash_can",
"solar_panel_t1", "solar_panel_t1_base",
"solar_panel_t2", "solar_panel_t2_base",
- "solar_panel_t3", "solar_panel_t3_base"]
+ "solar_panel_t3", "solar_panel_t3_base",
+ "meteor_rock", "meteor_core"]
ITEMS = ["nerosium_ingot", "nerosium_dust", "raw_nerosium", "nerosium_pickaxe",
"raw_nerosteel", "nerosteel_ingot", "xertz_quartz", "greenxertz_navigator",
"rocket_fuel_canister", "rocket_tier_1", "rocket_tier_2", "rocket_tier_3",
@@ -56,7 +57,8 @@
"loper_haunch", "strutter_drumstick", "drift_fleece",
"station_compass", "greenxertz_compass", "cindara_compass", "glacira_compass",
"star_guide_book", "station_charter",
- "frame_casing", "speed_module", "efficiency_module", "fortune_module", "silk_touch_module"]
+ "frame_casing", "speed_module", "efficiency_module", "fortune_module", "silk_touch_module",
+ "alien_fragment", "alien_tech_scrap", "alien_core", "meteor_tracker", "meteor_caller"]
# Entity bbmodels are NOT generated here any more (art overhaul A3): `tools/model_sync.py` owns
# every entity source bidirectionally from the per-model Java geometry. Generating them here too
diff --git a/tools/gen_textures.py b/tools/gen_textures.py
index c1564ae..d024463 100644
--- a/tools/gen_textures.py
+++ b/tools/gen_textures.py
@@ -2885,6 +2885,143 @@ def gen_solar_panel_base(name, edge):
save(img, os.path.join(BLOCK_DIR, name + ".png"))
+# ---------------- METEOR EVENTS (meteor-events-design.md) ----------------
+# Charred basalt body with a cyan/teal + amber "alien" glow — kept visually distinct from the
+# nerosium (red/purple) and Greenxertz (green) families.
+M_DARK = (24, 22, 30, 255)
+M_GREY = (60, 58, 70, 255)
+M_GREY2 = (84, 82, 96, 255)
+M_TEAL = (40, 200, 200, 255)
+M_CYAN = (120, 240, 248, 255)
+M_AMBER = (255, 176, 64, 255)
+M_GLOW = (210, 255, 255, 255)
+METEOR_STONE = [M_DARK, M_GREY, (40, 38, 48, 255), M_GREY2]
+
+
+def gen_meteor_rock():
+ rng = random.Random(int(hashlib.md5(b"meteor_rock").hexdigest(), 16) & 0xffffffff)
+ img = new_img()
+ noise_fill(img, METEOR_STONE, rng)
+ px = img.load()
+ for _ in range(10):
+ px[rng.randint(0, S - 1), rng.randint(0, S - 1)] = M_TEAL if rng.random() < 0.6 else M_AMBER
+ for _ in range(3):
+ px[rng.randint(1, S - 2), rng.randint(1, S - 2)] = M_CYAN
+ save(img, os.path.join(BLOCK_DIR, "meteor_rock.png"))
+
+
+def gen_meteor_core():
+ rng = random.Random(int(hashlib.md5(b"meteor_core").hexdigest(), 16) & 0xffffffff)
+ img = new_img()
+ noise_fill(img, METEOR_STONE, rng)
+ px = img.load()
+ for y in range(S):
+ for x in range(S):
+ d = ((x - 7.5) ** 2 + (y - 7.5) ** 2) ** 0.5
+ if d <= 3:
+ px[x, y] = M_CYAN if d <= 2 else M_TEAL
+ elif d <= 4:
+ px[x, y] = M_AMBER
+ px[7, 7] = M_GLOW
+ px[8, 8] = M_GLOW
+ save(img, os.path.join(BLOCK_DIR, "meteor_core.png"))
+
+
+def gen_alien_fragment():
+ img = new_img()
+ px = img.load()
+ shape = {5: (6, 10), 6: (5, 11), 7: (5, 11), 8: (6, 11), 9: (7, 10), 10: (7, 9)}
+ for y, (x0, x1) in shape.items():
+ for x in range(x0, x1):
+ px[x, y] = M_TEAL
+ px[6, 6] = M_CYAN
+ px[7, 7] = M_CYAN
+ px[9, 8] = M_AMBER
+ save(img, os.path.join(ITEM_DIR, "alien_fragment.png"))
+
+
+def gen_alien_tech_scrap():
+ img = new_img()
+ px = img.load()
+ for y in range(5, 11):
+ for x in range(4, 12):
+ px[x, y] = M_GREY2 if (x + y) % 2 == 0 else M_GREY
+ for x in range(5, 11):
+ px[x, 7] = M_TEAL
+ px[6, 6] = M_CYAN
+ px[9, 9] = M_AMBER
+ px[5, 9] = M_CYAN
+ save(img, os.path.join(ITEM_DIR, "alien_tech_scrap.png"))
+
+
+def gen_alien_core():
+ img = new_img()
+ px = img.load()
+ for y in range(S):
+ for x in range(S):
+ d = ((x - 7.5) ** 2 + ((y - 7.5) * 0.8) ** 2) ** 0.5
+ if d <= 2:
+ px[x, y] = M_GLOW
+ elif d <= 3.5:
+ px[x, y] = M_CYAN
+ elif d <= 5:
+ px[x, y] = M_AMBER
+ elif d <= 5.8:
+ px[x, y] = M_DARK
+ save(img, os.path.join(ITEM_DIR, "alien_core.png"))
+
+
+def gen_meteor_tracker():
+ img = new_img()
+ px = img.load()
+ cx = cy = 8
+ for y in range(S):
+ for x in range(S):
+ d = ((x - cx + 0.5) ** 2 + (y - cy + 0.5) ** 2) ** 0.5
+ if d <= 7:
+ if d > 6:
+ px[x, y] = METAL_D
+ elif d > 5:
+ px[x, y] = METAL_L
+ else:
+ px[x, y] = (24, 30, 36, 255)
+ for (x, y) in [(8, 4), (8, 5), (8, 6), (7, 5), (9, 5)]:
+ px[x, y] = M_CYAN
+ for (x, y) in [(8, 9), (8, 10), (8, 11)]:
+ px[x, y] = M_AMBER
+ px[8, 8] = M_GLOW
+ save(img, os.path.join(ITEM_DIR, "meteor_tracker.png"))
+
+
+def gen_meteor_caller():
+ img = new_img()
+ px = img.load()
+ for y in range(3, 13):
+ for x in range(5, 11):
+ px[x, y] = METAL_L if (x + y) % 2 else METAL_D
+ for (x, y) in [(7, 4), (8, 4), (7, 5), (8, 5)]:
+ px[x, y] = M_AMBER
+ for (x, y) in [(6, 6), (9, 7), (7, 8), (8, 9)]:
+ px[x, y] = M_CYAN
+ px[8, 11] = M_TEAL
+ save(img, os.path.join(ITEM_DIR, "meteor_caller.png"))
+
+
+def gen_falling_meteor_entity():
+ # 64x64 box-UV sheet: charred rock with glowing cyan/amber cracks (uniform-ish so every face reads).
+ rng = random.Random(4242)
+ img = Image.new("RGBA", (ES, ES), (0, 0, 0, 0))
+ px = img.load()
+ for y in range(ES):
+ for x in range(ES):
+ px[x, y] = rng.choice(METEOR_STONE)
+ for _ in range(120):
+ px[rng.randint(0, ES - 1), rng.randint(0, ES - 1)] = M_TEAL if rng.random() < 0.6 else M_AMBER
+ for _ in range(40):
+ px[rng.randint(0, ES - 1), rng.randint(0, ES - 1)] = M_CYAN
+ save(img, os.path.join(ENTITY_DIR, "falling_meteor.png"))
+
+
if __name__ == "__main__":
# Solar panels (SOLAR_PANEL_DESIGN): shared futuristic blue+green PV deck; the edge ring colour
# codes the tier — T1 signal red, T2 nerosium magenta, T3 gold.
@@ -2987,4 +3124,13 @@ def gen_solar_panel_base(name, edge):
gen_station_core()
gen_station_charter()
# Developer diagnostics — hidden Sentry test block: a steel panel with a red warning glyph.
- gen_panel_block("sentry_test", N_RED, SYM_BANG)
\ No newline at end of file
+ gen_panel_block("sentry_test", N_RED, SYM_BANG)
+ # Meteor events (meteor-events-design.md): crater rock/core, alien loot, tracker/caller, entity.
+ gen_meteor_rock()
+ gen_meteor_core()
+ gen_alien_fragment()
+ gen_alien_tech_scrap()
+ gen_alien_core()
+ gen_meteor_tracker()
+ gen_meteor_caller()
+ gen_falling_meteor_entity()
\ No newline at end of file
diff --git a/wiki/Configuration.md b/wiki/Configuration.md
index 648411f..d20325a 100644
--- a/wiki/Configuration.md
+++ b/wiki/Configuration.md
@@ -93,6 +93,23 @@ at defaults unless debugging server performance.
| `terraformForceLoadChunks` | `false` | — | Force-load a bounded arc around the working frontier (TPS footgun — off by default). |
| `terraformMaxForcedChunks` | `16` | 0–256 | Guard on force-loaded chunks. |
+### Meteor events
+
+Tunables for the meteor world-event (see **[Meteor Events](Meteor-Events)**). Defaults give roughly
+one natural meteor every 2–3 play-hours per active dimension; the Meteor Caller works regardless.
+
+| Key | Default | Range | Meaning |
+|---|---|---|---|
+| `meteorNaturalSpawn` | `true` | — | Whether meteors fall naturally near players. |
+| `meteorAvgIntervalSeconds` | `9000` | 60–1,000,000 | Average seconds between natural impacts (randomised 0.66×–1.33×). |
+| `meteorWarningSeconds` | `30` | 0–600 | Warning window a meteor is tracked as *incoming* before it falls. |
+| `meteorMinDistance` | `200` | 0–2,000 | Min horizontal distance from the anchor player a meteor targets. |
+| `meteorMaxDistance` | `500` | 16–4,000 | Max horizontal distance a meteor targets. |
+| `meteorCraterRadius` | `3` | 1–8 | Radius of the crater carved (kept modest to avoid griefing builds). |
+| `meteorMaxActiveSites` | `4` | 1–64 | Max simultaneous scheduled/falling meteors per dimension. |
+| `meteorLootBonusRolls` | `3` | 0–32 | Weighted bonus loot rolls on top of the guaranteed alien fragments. |
+| `meteorDebugLog` | `false` | — | Verbose, non-personal meteor logging (dimension + coordinates only — POPIA/GDPR). |
+
## Removed keys (for modpack authors migrating)
Folded into multipliers: `atmosphereDamage`, `oxygenMax`, `oxygenDrainPerTick`, `oxygenSuitDrain`,
diff --git a/wiki/Home.md b/wiki/Home.md
index be41332..641bb35 100644
--- a/wiki/Home.md
+++ b/wiki/Home.md
@@ -32,6 +32,8 @@ eventually **terraform** a dead planet into livable, rained-on ground.
the Star Guide.
- **[Creatures](Creatures)** — the native mobs of Greenxertz, Cindara, and Glacira, plus the
terraform livestock.
+- **[Meteor Events](Meteor-Events)** — rare meteor crashes that seed the world with alien materials
+ and off-world ores; track them with the Meteor Tracker.
- **[Star Guide](Star-Guide)** — the in-game progression guide.
- **[Configuration](Configuration)** — the five multiplier keys for server/modpack tuning.
- **[Roadmap](Roadmap)** — what shipped in 1.0 and what's next.
diff --git a/wiki/Items.md b/wiki/Items.md
index cc8dc0a..0691df9 100644
--- a/wiki/Items.md
+++ b/wiki/Items.md
@@ -14,6 +14,9 @@ A reference for Nerospace's non-block items. (Blocks have their own pages — se
| **Xertz Quartz** | Gem dropped by [Xertz Quartz Ore](Xertz-Quartz-Ore); used in upgrades and the Terraform Monitor. |
| **Cindrite** | Gem from [Cindrite Ore](Cindrite-Ore) on Cindara; Tier-3 Terraformer upgrade, Tier 2 + [Thermal Suit](Oxygen-Suit) pieces, Tier 4 rocket. |
| **Glacite** | Gem from [Glacite Ore](Glacite-Ore) on Glacira; [Cryo Suit](Oxygen-Suit) pieces and the [Hydration Module](Hydration-Module)'s water cycle. |
+| **Alien Fragment** | Common loot from a [Meteor Core](Meteor-Core); future scanner feedstock. |
+| **Alien Tech Scrap** | Uncommon meteor loot; future upgrade crafting. |
+| **Alien Core** | Rare meteor loot; high-value scanner/upgrade gate. |
## Tools
@@ -76,6 +79,13 @@ engage. See **[Oxygen Suit](Oxygen-Suit)** for the full page.
- **Drift Fleece** — dropped by the Woolly Drift; crafts into 4 String.
See **[Creatures](Creatures)** for the livestock themselves.
+## Meteor events
+
+- **Meteor Tracker** — points to the nearest meteor (held action-bar readout: state, heading,
+ distance). Creative for now; a survival craft comes with the scanner.
+- **Meteor Caller** — creative-only: right-click a block to call a meteor down onto it.
+ See **[Meteor Events](Meteor-Events)** for the full world-event, loot table and config.
+
## Travel devices (creative)
- **Greenxertz Navigator** and the **Station / Greenxertz / Cindara / Glacira Compasses** are
diff --git a/wiki/Meteor-Core.md b/wiki/Meteor-Core.md
new file mode 100644
index 0000000..52b578f
--- /dev/null
+++ b/wiki/Meteor-Core.md
@@ -0,0 +1,26 @@
+# Meteor Core
+
+The glowing block at the centre of a crater — the meteor's "box" of loot.
+
+## Overview
+
+Every fallen meteor seats a Meteor Core at the bottom of its crater (see
+**[Meteor Events](Meteor-Events)**). It holds the meteor's RNG loot, rolled **once** when the meteor
+lands and then fixed, so the contents are identical for everyone who reaches it.
+
+## Obtaining
+
+**Not craftable.** A Meteor Core is placed by a meteor impact. **Break it to claim the loot**
+(break-to-loot): the stored stacks spill out where it stood — a handful of **Alien Fragments** plus
+weighted bonus rolls of raw ores and the rarer **Alien Tech Scrap** / **Alien Core**.
+
+## Use
+
+- Smash it open for the meteor's contents — the jump-start of alien materials and off-world ores.
+- The alien items feed a future **scanner / upgrade** system (see [Roadmap](Roadmap)).
+
+## Details
+
+- ID: `nerospace:meteor_core` · Tool: pickaxe · No loot table — drops its stored contents on break.
+- Emits light (level 10). Loot is configurable via the **Meteor events** keys in
+ [Configuration](Configuration).
diff --git a/wiki/Meteor-Events.md b/wiki/Meteor-Events.md
new file mode 100644
index 0000000..2e8599d
--- /dev/null
+++ b/wiki/Meteor-Events.md
@@ -0,0 +1,64 @@
+# Meteor Events
+
+Falling meteors are a **world event**: rare crashes that seed the Overworld (and the planets) with
+alien materials and a jump-start of off-world ores — no rocket required to get your first taste of
+what's out there.
+
+## Overview
+
+Every so often, near an active player, a meteor is scheduled to fall. After a short warning window it
+streaks down from the sky on a fiery arc, craters into the ground, and leaves a small **crater of
+[Meteor Rock](Meteor-Rock)** around a glowing **[Meteor Core](Meteor-Core)** — the "box" that holds
+the RNG loot. The crash is deliberately modest (a few-block crater, no wide explosion, no fire) so it
+won't grief your builds.
+
+Impacts happen **as you get close** to the site: meteors are scheduled far out and only fall once the
+area is loaded, so following the warning toward the site is part of the hunt.
+
+## The Meteor Tracker
+
+The **Meteor Tracker** is an early-warning compass. While you hold it, it shows the nearest tracked
+meteor in the action bar:
+
+- **State** — *Incoming* (still falling / not yet landed) or *Landed* (the crater is waiting).
+- **Heading** — a compass direction (N, NE, E, …) toward the site.
+- **Distance** — how far off it is, in metres.
+
+It's a creative item for now (no survival recipe yet) — a survival craft is planned alongside the
+scanner system below.
+
+## Loot
+
+Breaking the [Meteor Core](Meteor-Core) spills the meteor's contents (break-to-loot). The loot is
+rolled **once** when the meteor lands and then fixed, so it's the same for everyone who reaches it —
+no re-roll exploit. A meteor always carries a handful of **Alien Fragments**, plus weighted bonus
+rolls of existing raw ores (Raw Nerosium, Raw Nerosteel, Xertz Quartz) and the rarer
+**Alien Tech Scrap** and **Alien Core**.
+
+| Item | Rarity | Role |
+|---|---|---|
+| **Alien Fragment** | common (guaranteed) | Future **scanner** feedstock. |
+| **Alien Tech Scrap** | uncommon | Future upgrade crafting. |
+| **Alien Core** | rare | High-value scanner/upgrade gate. |
+
+The three are grouped under the `nerospace:alien_materials` item tag for future recipes. The scanner
+that turns them into upgrades is on the **[Roadmap](Roadmap)** / **[Future Features](Future-Features)**.
+
+## Calling a meteor (creative)
+
+The **Meteor Caller** is a creative-only tool: right-click any block to call a meteor down onto that
+spot immediately, with freshly rolled loot — the same path natural spawning uses. It does nothing in
+survival.
+
+## Configuration
+
+Meteor frequency, the warning window, target distance, crater size and loot generosity are all tunable
+— see the **Meteor events** section of **[Configuration](Configuration)**. Natural spawning can be
+turned off entirely (the Meteor Caller still works).
+
+## Details
+
+- Trigger: rare timer near players (≈ one per 2–3 play-hours by default, configurable).
+- Dimensions: Overworld, Greenxertz, Cindara, Glacira (not the void station).
+- Blocks: [Meteor Rock](Meteor-Rock), [Meteor Core](Meteor-Core) · Entity: `nerospace:falling_meteor`.
+- Items: `alien_fragment`, `alien_tech_scrap`, `alien_core`, `meteor_tracker`, `meteor_caller`.
diff --git a/wiki/Meteor-Rock.md b/wiki/Meteor-Rock.md
new file mode 100644
index 0000000..725e310
--- /dev/null
+++ b/wiki/Meteor-Rock.md
@@ -0,0 +1,25 @@
+# Meteor Rock
+
+The charred, faintly glowing rock left behind by a meteor crash.
+
+## Overview
+
+Meteor Rock forms the small crater around a fallen meteor (see **[Meteor Events](Meteor-Events)**). It
+is a cosmetic space-rock building block — dark basalt veined with a cyan/teal alien glow — and the
+visible marker of a crash site from a distance.
+
+## Obtaining
+
+- **Mining:** any pickaxe; requires the correct tool to drop. Drops itself, so you can collect and
+ build with it.
+- **Generation:** placed by meteor impacts — it is not found in normal world generation.
+
+## Use
+
+- Decorative building block for space/crater builds.
+- The crater body around a **[Meteor Core](Meteor-Core)** — dig in to reach the loot.
+
+## Details
+
+- ID: `nerospace:meteor_rock` · Tool: pickaxe · Drops: itself
+- Emits a faint light (level 3).
diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md
index f082946..e53e847 100644
--- a/wiki/_Sidebar.md
+++ b/wiki/_Sidebar.md
@@ -59,6 +59,12 @@
- [Station Wall](Station-Wall)
- [Station Core](Station-Core)
+**World Events**
+
+- [Meteor Events](Meteor-Events)
+- [Meteor Rock](Meteor-Rock)
+- [Meteor Core](Meteor-Core)
+
**More**
- [Items](Items)