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 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 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 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)