diff --git a/art/blockbench/block/solar_panel_t1.bbmodel b/art/blockbench/block/solar_panel_t1.bbmodel new file mode 100644 index 0000000..fa331c1 --- /dev/null +++ b/art/blockbench/block/solar_panel_t1.bbmodel @@ -0,0 +1,136 @@ +{ + "meta": { + "format_version": "4.10", + "model_format": "java_block", + "box_uv": false + }, + "name": "solar_panel_t1", + "model_identifier": "", + "visible_box": [ + 1, + 1, + 0 + ], + "variable_placeholders": "", + "variable_placeholder_buttons": [], + "timeline_setups": [], + "unhandled_root_fields": {}, + "resolution": { + "width": 16, + "height": 16 + }, + "elements": [ + { + "name": "solar_panel_t1", + "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": "f2b7e1c9-6545-4663-ae28-a0b0b99dc36e" + } + ], + "outliner": [ + "f2b7e1c9-6545-4663-ae28-a0b0b99dc36e" + ], + "textures": [ + { + "path": "C:\\Users\\dario\\Documents\\Github\\nerospace\\src\\main\\resources\\assets\\nerospace\\textures\\block\\solar_panel_t1.png", + "name": "solar_panel_t1.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": "f9547efe-6be0-4f37-86d8-61dc5cd3b666", + "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/solar_panel_t1.png", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAsklEQVR4nGOct23W/2k98xiySpIYSAEwPYwmThb/SdKJBphgNrtsCmG494yJ4RWLFkNWSRIDj00aTr7LphCEATDGLJN1DDxyGgxfHt1gYGBgYLi0ag5OvtohPkwDYJI8chpE8WGABcZoKLBgYGCwYCCWj2HAohMscGc2FFgQ5GN4AZ+fsfGJDoMV17soC4Mn268N9TAglA4YYZnJZVMISgrDB27ZfWLY47cGYgDVMhO5AACeE58fVfWNpAAAAABJRU5ErkJggg==" + } + ] +} \ No newline at end of file diff --git a/art/blockbench/block/solar_panel_t1_base.bbmodel b/art/blockbench/block/solar_panel_t1_base.bbmodel new file mode 100644 index 0000000..c8afb41 --- /dev/null +++ b/art/blockbench/block/solar_panel_t1_base.bbmodel @@ -0,0 +1,136 @@ +{ + "meta": { + "format_version": "4.10", + "model_format": "java_block", + "box_uv": false + }, + "name": "solar_panel_t1_base", + "model_identifier": "", + "visible_box": [ + 1, + 1, + 0 + ], + "variable_placeholders": "", + "variable_placeholder_buttons": [], + "timeline_setups": [], + "unhandled_root_fields": {}, + "resolution": { + "width": 16, + "height": 16 + }, + "elements": [ + { + "name": "solar_panel_t1_base", + "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": "1b221b73-c516-4cf5-a27c-984623d4368b" + } + ], + "outliner": [ + "1b221b73-c516-4cf5-a27c-984623d4368b" + ], + "textures": [ + { + "path": "C:\\Users\\dario\\Documents\\Github\\nerospace\\src\\main\\resources\\assets\\nerospace\\textures\\block\\solar_panel_t1_base.png", + "name": "solar_panel_t1_base.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": "4774353d-9cf1-401b-9b4c-d3393f3118f7", + "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/solar_panel_t1_base.png", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAoUlEQVR4nGOct23WfwYKAAsDAwPDkQOnyNJ86cwVBiYYx8bBjGiNyGpZYIwjB04xXDpzheHLpy8MPHw8eGlkwITMIUYzDx8PigEsyBwePh6GrJIkvM7vqpuE24Avn74wTOuZR5ILULxAjPOpHgYUuwAjDMqa8nAGIAMDA8O0nnm4DeDh42Hoqps0QOmA1KQMyz9wF5CSoZDVsjAwQHIVuQAA6XKZ9XjO6eUAAAAASUVORK5CYII=" + } + ] +} \ No newline at end of file diff --git a/art/blockbench/block/solar_panel_t2.bbmodel b/art/blockbench/block/solar_panel_t2.bbmodel new file mode 100644 index 0000000..5836566 --- /dev/null +++ b/art/blockbench/block/solar_panel_t2.bbmodel @@ -0,0 +1,136 @@ +{ + "meta": { + "format_version": "4.10", + "model_format": "java_block", + "box_uv": false + }, + "name": "solar_panel_t2", + "model_identifier": "", + "visible_box": [ + 1, + 1, + 0 + ], + "variable_placeholders": "", + "variable_placeholder_buttons": [], + "timeline_setups": [], + "unhandled_root_fields": {}, + "resolution": { + "width": 16, + "height": 16 + }, + "elements": [ + { + "name": "solar_panel_t2", + "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": "0ed38d1a-e717-4f76-ac27-dfd3f73fe58a" + } + ], + "outliner": [ + "0ed38d1a-e717-4f76-ac27-dfd3f73fe58a" + ], + "textures": [ + { + "path": "C:\\Users\\dario\\Documents\\Github\\nerospace\\src\\main\\resources\\assets\\nerospace\\textures\\block\\solar_panel_t2.png", + "name": "solar_panel_t2.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": "73ab08d3-8fa0-4abc-8a1a-e55c0a77a1e9", + "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAr0lEQVR4nGOct23W/2k98xiySpIYSAEwPYwmThb/SdKJBphgNs95VMFw7xkTwysWLYaskiQGHps0nPw5jyoQBsAYTixdDDxyGgxfHt1gYGBgYLi0ag5O/rlJrzANgEnyyGkQxYcBFhijocCCgYHBgoFYPoYBi06wwJ3ZUGBBkI/hBXx+xsYnOgxWXO+iLAyebL821MOA6HSw6Z4fw7lJxKUDozwxBgY5iDj1MhO5AACqnZ1/oDq2pQAAAABJRU5ErkJggg==" + } + ] +} \ No newline at end of file diff --git a/art/blockbench/block/solar_panel_t2_base.bbmodel b/art/blockbench/block/solar_panel_t2_base.bbmodel new file mode 100644 index 0000000..981f068 --- /dev/null +++ b/art/blockbench/block/solar_panel_t2_base.bbmodel @@ -0,0 +1,136 @@ +{ + "meta": { + "format_version": "4.10", + "model_format": "java_block", + "box_uv": false + }, + "name": "solar_panel_t2_base", + "model_identifier": "", + "visible_box": [ + 1, + 1, + 0 + ], + "variable_placeholders": "", + "variable_placeholder_buttons": [], + "timeline_setups": [], + "unhandled_root_fields": {}, + "resolution": { + "width": 16, + "height": 16 + }, + "elements": [ + { + "name": "solar_panel_t2_base", + "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": "11cb466e-1102-40bf-8b26-de3b4f67fe6b" + } + ], + "outliner": [ + "11cb466e-1102-40bf-8b26-de3b4f67fe6b" + ], + "textures": [ + { + "path": "C:\\Users\\dario\\Documents\\Github\\nerospace\\src\\main\\resources\\assets\\nerospace\\textures\\block\\solar_panel_t2_base.png", + "name": "solar_panel_t2_base.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": "da883001-3a73-4bc8-ab97-852a9d568fe8", + "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAtklEQVR4nGOct23WfwYKAAsDAwPDkQOnyNJ86cwVBiYYJ2+dD9EakdWywBiTgrYwXDpzheHLpy8MPHw8eGmGIIRhTMgmE6OZh48HxTUsyBwePh6GrJIkvM7vqpuE24Avn74wTOuZR5ILULxAjPO/fPqC2wBywoBiF2CEQVlTHs4AZGBgYJjWMw+3ATx8PAxddZPoGwZwF+St80FJYfhA3jofhklBW1BdABMgBiCrZWFggOQqcgEAqtKamSBkq3YAAAAASUVORK5CYII=" + } + ] +} \ No newline at end of file diff --git a/art/blockbench/block/solar_panel_t3.bbmodel b/art/blockbench/block/solar_panel_t3.bbmodel new file mode 100644 index 0000000..e95aa0a --- /dev/null +++ b/art/blockbench/block/solar_panel_t3.bbmodel @@ -0,0 +1,136 @@ +{ + "meta": { + "format_version": "4.10", + "model_format": "java_block", + "box_uv": false + }, + "name": "solar_panel_t3", + "model_identifier": "", + "visible_box": [ + 1, + 1, + 0 + ], + "variable_placeholders": "", + "variable_placeholder_buttons": [], + "timeline_setups": [], + "unhandled_root_fields": {}, + "resolution": { + "width": 16, + "height": 16 + }, + "elements": [ + { + "name": "solar_panel_t3", + "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": "2468cb7e-3b2b-40bb-ad92-f96ca64652a0" + } + ], + "outliner": [ + "2468cb7e-3b2b-40bb-ad92-f96ca64652a0" + ], + "textures": [ + { + "path": "C:\\Users\\dario\\Documents\\Github\\nerospace\\src\\main\\resources\\assets\\nerospace\\textures\\block\\solar_panel_t3.png", + "name": "solar_panel_t3.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": "49bdf4a0-2607-41eb-864a-a383ec20a5f7", + "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAsklEQVR4nGOct23W/2k98xiySpIYSAEwPYwmThb/SdKJBphgNp/oE2C494yJ4RWLFkNWSRIDj00aTv6JPgGEATCGWMQnBh45DYYvj24wMDAwMFxaNQcnf9GzIEwDYJI8chpE8WGABcZoKLBgYGCwYCCWj2HAohMscGc2FFgQ5GN4AZ+fsfGJDoMV17soC4Mn268N9TAglA4YYZnpRJ8ASgrDB+Kk1jFYFH2AGEC1zEQuAADtWKAf+1rFgAAAAABJRU5ErkJggg==" + } + ] +} \ No newline at end of file diff --git a/art/blockbench/block/solar_panel_t3_base.bbmodel b/art/blockbench/block/solar_panel_t3_base.bbmodel new file mode 100644 index 0000000..dae814a --- /dev/null +++ b/art/blockbench/block/solar_panel_t3_base.bbmodel @@ -0,0 +1,136 @@ +{ + "meta": { + "format_version": "4.10", + "model_format": "java_block", + "box_uv": false + }, + "name": "solar_panel_t3_base", + "model_identifier": "", + "visible_box": [ + 1, + 1, + 0 + ], + "variable_placeholders": "", + "variable_placeholder_buttons": [], + "timeline_setups": [], + "unhandled_root_fields": {}, + "resolution": { + "width": 16, + "height": 16 + }, + "elements": [ + { + "name": "solar_panel_t3_base", + "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": "2989722a-6534-4fa5-9c01-bc0cad3c6d54" + } + ], + "outliner": [ + "2989722a-6534-4fa5-9c01-bc0cad3c6d54" + ], + "textures": [ + { + "path": "C:\\Users\\dario\\Documents\\Github\\nerospace\\src\\main\\resources\\assets\\nerospace\\textures\\block\\solar_panel_t3_base.png", + "name": "solar_panel_t3_base.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": "69704e92-e523-4cc2-b6de-d971de8f656e", + "relative_path": "../../../src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAt0lEQVR4nGOct23WfwYKAAsDAwPDkQOnyNJ86cwVBiYYZ1bUE6I1IqtlgTHSlskwXDpzheHLpy8MPHw8eOk0Bgu4AUzIJhOjmYePB8U1LMgcHj4ehqySJLzO76qbhNuAL5++MEzrmUeSC1C8QIzzv3z6gtsAcsKAYhdghEFZUx7OAGRgYGCY1jMPtwE8fDwMXXWT6BsGcBfMinqCksLwgVlRTxjSlsmgugAmQAxAVsvCwADJVeQCAFvLmzGP509xAAAAAElFTkSuQmCC" + } + ] +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/blockstates/solar_panel_t1.json b/src/generated/resources/assets/nerospace/blockstates/solar_panel_t1.json new file mode 100644 index 0000000..ff57fb2 --- /dev/null +++ b/src/generated/resources/assets/nerospace/blockstates/solar_panel_t1.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/solar_panel_t1" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/blockstates/solar_panel_t2.json b/src/generated/resources/assets/nerospace/blockstates/solar_panel_t2.json new file mode 100644 index 0000000..3267b5e --- /dev/null +++ b/src/generated/resources/assets/nerospace/blockstates/solar_panel_t2.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/solar_panel_t2" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/blockstates/solar_panel_t3.json b/src/generated/resources/assets/nerospace/blockstates/solar_panel_t3.json new file mode 100644 index 0000000..1424b53 --- /dev/null +++ b/src/generated/resources/assets/nerospace/blockstates/solar_panel_t3.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/solar_panel_t3" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/items/solar_panel_t1.json b/src/generated/resources/assets/nerospace/items/solar_panel_t1.json new file mode 100644 index 0000000..ea49974 --- /dev/null +++ b/src/generated/resources/assets/nerospace/items/solar_panel_t1.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/solar_panel_t1" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/items/solar_panel_t2.json b/src/generated/resources/assets/nerospace/items/solar_panel_t2.json new file mode 100644 index 0000000..abb427f --- /dev/null +++ b/src/generated/resources/assets/nerospace/items/solar_panel_t2.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/solar_panel_t2" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/items/solar_panel_t3.json b/src/generated/resources/assets/nerospace/items/solar_panel_t3.json new file mode 100644 index 0000000..20d00b8 --- /dev/null +++ b/src/generated/resources/assets/nerospace/items/solar_panel_t3.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/solar_panel_t3" + } +} \ 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 c7b69a0..791225e 100644 --- a/src/generated/resources/assets/nerospace/lang/en_us.json +++ b/src/generated/resources/assets/nerospace/lang/en_us.json @@ -51,6 +51,10 @@ "block.nerospace.rocket_launch_pad.report.t3_not_ready": "Tier 3 needs a Station Wall ring or a Heavy Launch Complex", "block.nerospace.rocket_launch_pad.report.t3_ready": "Tier 3 ready: Station Wall ring or Heavy complex present", "block.nerospace.sentry_test": "Sentry Test Block", + "block.nerospace.solar_panel.readout": "Solar panel: %s / %s FE (array of %s)", + "block.nerospace.solar_panel_t1": "Tier 1 Solar Panel", + "block.nerospace.solar_panel_t2": "Tier 2 Solar Panel", + "block.nerospace.solar_panel_t3": "Tier 3 Solar Panel", "block.nerospace.star_guide": "Star Guide", "block.nerospace.station_core": "Station Core", "block.nerospace.station_core.bound": "Station Core: %s", diff --git a/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json b/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json new file mode 100644 index 0000000..9202496 --- /dev/null +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json @@ -0,0 +1,104 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 3, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 7, + 3, + 7 + ], + "to": [ + 9, + 7, + 9 + ] + }, + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 7, + 7, + 4 + ], + "to": [ + 9, + 8, + 12 + ] + } + ], + "textures": { + "all": "nerospace:block/solar_panel_t1_base", + "particle": "nerospace:block/solar_panel_t1_base" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json b/src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json new file mode 100644 index 0000000..7035a31 --- /dev/null +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json @@ -0,0 +1,40 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 3, + 16 + ] + } + ], + "textures": { + "all": "nerospace:block/solar_panel_t2_base", + "particle": "nerospace:block/solar_panel_t2_base" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json b/src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json new file mode 100644 index 0000000..0b01a96 --- /dev/null +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json @@ -0,0 +1,40 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 3, + 16 + ] + } + ], + "textures": { + "all": "nerospace:block/solar_panel_t3_base", + "particle": "nerospace:block/solar_panel_t3_base" + } +} \ 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 872d627..9018ee7 100644 --- a/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -23,6 +23,9 @@ "nerospace:universal_pipe", "nerospace:combustion_generator", "nerospace:passive_generator", + "nerospace:solar_panel_t1", + "nerospace:solar_panel_t2", + "nerospace:solar_panel_t3", "nerospace:battery", "nerospace:fluid_tank", "nerospace:gas_tank", diff --git a/src/generated/resources/data/minecraft/tags/block/needs_iron_tool.json b/src/generated/resources/data/minecraft/tags/block/needs_iron_tool.json index a0847ce..5888595 100644 --- a/src/generated/resources/data/minecraft/tags/block/needs_iron_tool.json +++ b/src/generated/resources/data/minecraft/tags/block/needs_iron_tool.json @@ -20,6 +20,9 @@ "nerospace:universal_pipe", "nerospace:combustion_generator", "nerospace:passive_generator", + "nerospace:solar_panel_t1", + "nerospace:solar_panel_t2", + "nerospace:solar_panel_t3", "nerospace:quarry_controller" ] } \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t1.json b/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t1.json new file mode 100644 index 0000000..ada9e12 --- /dev/null +++ b/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t1.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "nerospace:solar_panel_t1" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_xertz_quartz": { + "conditions": { + "items": [ + { + "items": "nerospace:xertz_quartz" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_xertz_quartz" + ] + ], + "rewards": { + "recipes": [ + "nerospace:solar_panel_t1" + ] + } +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t2.json b/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t2.json new file mode 100644 index 0000000..34ba01d --- /dev/null +++ b/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t2.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_solar_panel_t1": { + "conditions": { + "items": [ + { + "items": "nerospace:solar_panel_t1" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "nerospace:solar_panel_t2" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_solar_panel_t1" + ] + ], + "rewards": { + "recipes": [ + "nerospace:solar_panel_t2" + ] + } +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t3.json b/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t3.json new file mode 100644 index 0000000..d9eec8d --- /dev/null +++ b/src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t3.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_solar_panel_t2": { + "conditions": { + "items": [ + { + "items": "nerospace:solar_panel_t2" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "nerospace:solar_panel_t3" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_solar_panel_t2" + ] + ], + "rewards": { + "recipes": [ + "nerospace:solar_panel_t3" + ] + } +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t1.json b/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t1.json new file mode 100644 index 0000000..a5adbd9 --- /dev/null +++ b/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t1.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:solar_panel_t1" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/solar_panel_t1" +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json b/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json new file mode 100644 index 0000000..3142364 --- /dev/null +++ b/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json @@ -0,0 +1,4 @@ +{ + "type": "minecraft:block", + "random_sequence": "nerospace:blocks/solar_panel_t2" +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json b/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json new file mode 100644 index 0000000..88206f4 --- /dev/null +++ b/src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json @@ -0,0 +1,4 @@ +{ + "type": "minecraft:block", + "random_sequence": "nerospace:blocks/solar_panel_t3" +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/recipe/solar_panel_t1.json b/src/generated/resources/data/nerospace/recipe/solar_panel_t1.json new file mode 100644 index 0000000..75a468c --- /dev/null +++ b/src/generated/resources/data/nerospace/recipe/solar_panel_t1.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "redstone", + "key": { + "C": "minecraft:copper_ingot", + "G": "minecraft:glass", + "N": "#c:ingots/nerosteel", + "Q": "nerospace:xertz_quartz" + }, + "pattern": [ + "GGG", + "QCQ", + "NNN" + ], + "result": { + "id": "nerospace:solar_panel_t1" + } +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/recipe/solar_panel_t2.json b/src/generated/resources/data/nerospace/recipe/solar_panel_t2.json new file mode 100644 index 0000000..b8c135a --- /dev/null +++ b/src/generated/resources/data/nerospace/recipe/solar_panel_t2.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "redstone", + "key": { + "N": "nerospace:nerosium_block", + "P": "nerospace:solar_panel_t1" + }, + "pattern": [ + " P ", + "PNP", + " P " + ], + "result": { + "id": "nerospace:solar_panel_t2" + } +} \ No newline at end of file diff --git a/src/generated/resources/data/nerospace/recipe/solar_panel_t3.json b/src/generated/resources/data/nerospace/recipe/solar_panel_t3.json new file mode 100644 index 0000000..b53cc4f --- /dev/null +++ b/src/generated/resources/data/nerospace/recipe/solar_panel_t3.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "redstone", + "key": { + "G": "minecraft:gold_block", + "Q": "nerospace:solar_panel_t2" + }, + "pattern": [ + " Q ", + "QGQ", + " Q " + ], + "result": { + "id": "nerospace:solar_panel_t3" + } +} \ No newline at end of file diff --git a/src/main/java/za/co/neroland/nerospace/NerospaceClient.java b/src/main/java/za/co/neroland/nerospace/NerospaceClient.java index 204361c..31a875a 100644 --- a/src/main/java/za/co/neroland/nerospace/NerospaceClient.java +++ b/src/main/java/za/co/neroland/nerospace/NerospaceClient.java @@ -174,6 +174,11 @@ static void onRegisterEntityRenderers(EntityRenderersEvent.RegisterRenderers eve za.co.neroland.nerospace.registry.ModBlockEntities.UNIVERSAL_PIPE.get(), context -> new UniversalPipeRenderer()); + // Solar panel: the sun-tracking, night-folding deck drawn above the housing. + event.registerBlockEntityRenderer( + za.co.neroland.nerospace.registry.ModBlockEntities.SOLAR_PANEL.get(), + context -> new za.co.neroland.nerospace.client.SolarPanelRenderer()); + // Star Guide pedestal: the floating next-step hologram. event.registerBlockEntityRenderer( za.co.neroland.nerospace.registry.ModBlockEntities.STAR_GUIDE.get(), diff --git a/src/main/java/za/co/neroland/nerospace/Tuning.java b/src/main/java/za/co/neroland/nerospace/Tuning.java index 2427e2c..eff431a 100644 --- a/src/main/java/za/co/neroland/nerospace/Tuning.java +++ b/src/main/java/za/co/neroland/nerospace/Tuning.java @@ -50,6 +50,18 @@ private Tuning() { public static final int BASE_COMBUSTION_GENERATOR_FE_PER_TICK = 60; public static final int BASE_PASSIVE_GENERATOR_FE_PER_TICK = 10; + /** + * Peak FE/tick a SINGLE solar panel adds at full sun (noon, clear sky). The "punchy per-panel" + * curve (SOLAR_PANEL_DESIGN): even a small array matters, big arrays are strong. An array's output + * is the sum across its panels; storage is the sum of their buffers (see below). + */ + public static final int BASE_SOLAR_PANEL_T1_FE_PER_TICK = 20; + public static final int BASE_SOLAR_PANEL_T2_FE_PER_TICK = 100; + public static final int BASE_SOLAR_PANEL_T3_FE_PER_TICK = 400; + /** Per-panel FE buffer; an array's total storage scales with how many panels it contains. */ + public static final int BASE_SOLAR_PANEL_T1_BUFFER = 50_000; + public static final int BASE_SOLAR_PANEL_T2_BUFFER = 250_000; + public static final int BASE_SOLAR_PANEL_T3_BUFFER = 1_000_000; public static final int BASE_ENERGY_PIPE_THROUGHPUT = 4_000; public static final int BASE_ENERGY_PIPE_CAPACITY = 8_000; public static final int BASE_FLUID_PIPE_CAPACITY = 4_000; @@ -218,6 +230,26 @@ public static int passiveGeneratorFePerTick() { return scale(BASE_PASSIVE_GENERATOR_FE_PER_TICK, Config.ENERGY_RATE_MULTIPLIER.get()); } + /** Peak FE/tick for one solar panel of {@code tier} (1-3), config-scaled. */ + public static int solarPanelFePerTick(int tier) { + int base = switch (tier) { + case 2 -> BASE_SOLAR_PANEL_T2_FE_PER_TICK; + case 3 -> BASE_SOLAR_PANEL_T3_FE_PER_TICK; + default -> BASE_SOLAR_PANEL_T1_FE_PER_TICK; + }; + return scale(base, Config.ENERGY_RATE_MULTIPLIER.get()); + } + + /** Per-panel FE buffer for one solar panel of {@code tier} (1-3), config-scaled. */ + public static int solarPanelBuffer(int tier) { + int base = switch (tier) { + case 2 -> BASE_SOLAR_PANEL_T2_BUFFER; + case 3 -> BASE_SOLAR_PANEL_T3_BUFFER; + default -> BASE_SOLAR_PANEL_T1_BUFFER; + }; + return scale(base, Config.ENERGY_RATE_MULTIPLIER.get()); + } + public static int energyPipeThroughput() { return scale(BASE_ENERGY_PIPE_THROUGHPUT, Config.ENERGY_RATE_MULTIPLIER.get()); } 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 5019edd..fd07f82 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))); + // 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, + new Vec3(ox - 42, oy + 9, oz + 28), new Vec3(ox - 42, oy + 1, oz + 42))); return shots; } diff --git a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java new file mode 100644 index 0000000..7d8e79b --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java @@ -0,0 +1,25 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; + +/** + * Render state for a single solar panel: the tilt angle of its deck (sun-tracking by day, folded flat + * at night) plus which horizontal faces have a power hookup (so a connector stub is drawn there). + */ +public class SolarPanelRenderState extends BlockEntityRenderState { + + /** Surface tilt in degrees about the east-west (Z) axis: 0 = flat (folded / noon), +-tilt by day. */ + public float angle; + + /** Energy hookup (cable/machine, any mod) present on a face, indexed N=0, E=1, S=2, W=3. */ + public final boolean[] connector = new boolean[4]; + + /** 1-based tier (selects the surface texture). */ + public int tier = 1; + + /** Footprint edge length (1 = T1 single tracker, >1 = N×N multiblock — one big deck on a mast). */ + public int footprint = 1; + + /** Whether this cell is its unit's anchor — only the anchor draws the deck. */ + public boolean anchor = true; +} diff --git a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java new file mode 100644 index 0000000..28a9ddc --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java @@ -0,0 +1,277 @@ +package za.co.neroland.nerospace.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; + +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.capabilities.Capabilities; + +import za.co.neroland.nerospace.Nerospace; +import za.co.neroland.nerospace.registry.ModDimensionTypes; +import za.co.neroland.nerospace.solar.SolarPanelBlock; +import za.co.neroland.nerospace.solar.SolarPanelBlockEntity; + +/** + * Draws the moving solar-panel deck above its static housing model. EVERY tier uses the SAME animation: + * a deck that pivots on a T-pole and pitches east-west to track the sun. Tier 1 is a single 1×1 tracker + * on its model's pole, with seam joining ({@link SolarPanelRenderState#connect}) so adjacent T1 units + * read as one field. Tier 2/3 are N×N multiblocks drawn as ONE big panel: only the anchor (min-corner) + * cell renders, drawing a single N×N deck on a central mast that pitches east-west like Tier 1 (the + * tilt is scaled down as the footprint grows so the wider deck's descending edge never dips below the + * housings). All panels read the same world time, so an array moves in lockstep. On faces touching a + * power cable or any other energy block (this or another mod), a connector stub is drawn so the hookup + * butts up against the cable arm with no blank gap ({@link SolarPanelRenderState#connector}). + */ +public class SolarPanelRenderer + implements BlockEntityRenderer { + + /** Pivot height = the cross-bar top (the T-pole). */ + private static final float POLE_TOP = 9.0F / 16.0F; + /** Static housing top (3px) — the floor the deck must stay clear of when tilted. */ + private static final float HOUSING_TOP = 3.0F / 16.0F; + /** Deck thickness: a real 1px slab. */ + private static final float THICK = 1.0F / 16.0F; + /** Max east-west tracking tilt; capped so the deck clears the torque tube. */ + private static final float MAX_TILT = 40.0F; + /** Central-mast dimensions for the multiblock deck (post 3..7px, N-S torque tube 7..8px). */ + private static final float MAST_HALF = 1.0F / 16.0F; + private static final float POST_TOP = 7.0F / 16.0F; + private static final float TUBE_TOP = 8.0F / 16.0F; + private static final float TUBE_HALF = 4.0F / 16.0F; + /** Connector stub cross-section (4..12px) — matches the cable arm so the joint reads as continuous. */ + private static final float CONN_LO = 4.0F / 16.0F; + private static final float CONN_HI = 12.0F / 16.0F; + /** How far the connector reaches in from a face (4px) to meet the cable arm at the shared face. */ + private static final float CONN_REACH = 4.0F / 16.0F; + + @Override + public SolarPanelRenderState createRenderState() { + return new SolarPanelRenderState(); + } + + @Override + public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderState state, + float partialTick, Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(panel, state, partialTick, cameraPos, breakProgress); + Level level = panel.getLevel(); + if (level == null) { + return; + } + state.tier = panel.tier().tier; + state.footprint = panel.tier().footprint; + state.anchor = panel.getBlockState().getValue(SolarPanelBlock.ANCHOR); + // Every cell renders its own tracker (Tier 1 animation at every tier) — no anchor-only branch. + + float openness; + float track; + if (level.dimensionTypeRegistration().is(ModDimensionTypes.SPACE)) { + openness = 1.0F; // permanent sun in orbit / on an airless moon + track = 0.0F; + } else { + // Use this dimension's OWN clock (getDefaultClockTime), not the overworld's: the deck must + // fold to match the sky the player actually sees. getOverworldClockTime() reads the overworld + // clock, so a panel in another dimension (e.g. the gallery's capture dim) stayed at its last + // overworld-daytime angle and never folded when the local sky went dark. + long tod = level.getDefaultClockTime() % 24000L; // 0 sunrise, 6000 noon, 18000 midnight + float sun = Mth.cos((float) ((tod - 6000L) / 24000.0 * 2.0 * Math.PI)); // +1 noon, -1 midnight + openness = Mth.clamp((sun + 0.05F) / 0.3F, 0.0F, 1.0F); // eases to 0 at night → folds flat + track = (float) ((tod - 6000L) / 24000.0) * 360.0F; // -90 sunrise .. 0 noon .. +90 sunset + } + // East-west sun tracking for ALL tiers; cells move in lockstep on the same clock and fold flat + // (angle → 0) at night because openness eases to 0. + state.angle = openness * Mth.clamp(track, -MAX_TILT, MAX_TILT); + + BlockPos pos = panel.getBlockPos(); + // Connector stubs: any horizontal neighbour exposing an energy capability that ISN'T another + // solar panel — a Nerospace universal cable, a battery/machine, or any other mod's power cable. + state.connector[0] = energyHookup(level, pos.relative(Direction.NORTH), Direction.NORTH); + state.connector[1] = energyHookup(level, pos.relative(Direction.EAST), Direction.EAST); + state.connector[2] = energyHookup(level, pos.relative(Direction.SOUTH), Direction.SOUTH); + state.connector[3] = energyHookup(level, pos.relative(Direction.WEST), Direction.WEST); + } + + /** + * True when {@code pos} (the neighbour on {@code face}) accepts/provides energy and is not itself a + * solar panel — i.e. a power cable or machine to hook up to. Capability-based, so it lights up for + * any mod's energy block, making the connector "dynamic for all mods that have power cables". + */ + private static boolean energyHookup(Level level, BlockPos pos, Direction face) { + if (level.getBlockEntity(pos) instanceof SolarPanelBlockEntity) { + return false; // don't grow a port between two adjacent panels + } + return Capabilities.Energy.BLOCK.getCapability(level, pos, null, null, face.getOpposite()) != null; + } + + @Override + public void submit(SolarPanelRenderState state, PoseStack poseStack, SubmitNodeCollector collector, + CameraRenderState cameraState) { + int light = state.lightCoords; + + // Power connector stubs are per-cell (each cell meets cables on its own faces) — drawn for EVERY + // cell, including the filler cells of a multiblock whose perimeter touches a cable. + drawConnectors(state, poseStack, collector, light); + + Identifier texture = Identifier.fromNamespaceAndPath( + Nerospace.MODID, "textures/block/solar_panel_t" + state.tier + ".png"); + + if (state.footprint > 1) { + if (!state.anchor) { + return; // one big panel per multiblock — only the anchor (min-corner) draws the deck + } + submitMultiblockDeck(state, poseStack, collector, light, texture); + return; + } + + // Tier 1: a centred 1×1 deck on the model's T-pole, pitching east-west to follow the sun. The + // deck fills the full block edge-to-edge (pixel-perfect — the tier-coloured ring sits in the + // texture's outer pixels, with no geometry padding) and maps the whole sprite. + poseStack.pushPose(); + poseStack.translate(0.5F, POLE_TOP, 0.5F); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.angle)); + collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), + (pose, consumer) -> box(consumer, pose, light, + -0.5F, -THICK / 2.0F, -0.5F, 0.5F, THICK / 2.0F, 0.5F, + 0.0F, 0.0F, 1.0F, 1.0F)); + poseStack.popPose(); + } + + /** + * Tier 2/3: ONE big N×N photovoltaic deck on a central mast, pitching east-west to track the sun — + * the same animation as Tier 1, just a single panel instead of many. Drawn in the anchor's + * (min-corner) space, so the deck and mast centre on the footprint. The tilt is reduced as the + * footprint grows ({@link #maxTiltFor}) so the wider deck's descending edge clears the housings. + */ + private void submitMultiblockDeck(SolarPanelRenderState state, PoseStack poseStack, + SubmitNodeCollector collector, int light, Identifier texture) { + int n = state.footprint; + float centre = n / 2.0F; + float cap = maxTiltFor(n); + float angle = Mth.clamp(state.angle, -cap, cap); + + // Central mast (post + N-S torque tube) on the `_base` sprite, supporting the deck at the centre. + Identifier baseTexture = Identifier.fromNamespaceAndPath( + Nerospace.MODID, "textures/block/solar_panel_t" + state.tier + "_base.png"); + var baseRt = RenderTypes.entityCutout(baseTexture); + collector.order(1).submitCustomGeometry(poseStack, baseRt, (pose, consumer) -> { + box(consumer, pose, light, centre - MAST_HALF, HOUSING_TOP, centre - MAST_HALF, + centre + MAST_HALF, POST_TOP, centre + MAST_HALF, 0.25F, 0.25F, 0.75F, 0.75F); + box(consumer, pose, light, centre - MAST_HALF, POST_TOP, centre - TUBE_HALF, + centre + MAST_HALF, TUBE_TOP, centre + TUBE_HALF, 0.25F, 0.25F, 0.75F, 0.75F); + }); + + // The single deck, pivoting east-west about the central mast; fills the footprint edge-to-edge + // (pixel-perfect — the tier-coloured ring lives in the texture's outer pixels, no geometry gap). + float half = centre; + poseStack.pushPose(); + poseStack.translate(centre, POLE_TOP, centre); + poseStack.mulPose(Axis.ZP.rotationDegrees(angle)); + collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), + (pose, consumer) -> box(consumer, pose, light, + -half, -THICK / 2.0F, -half, half, THICK / 2.0F, half, + 0.0F, 0.0F, 1.0F, 1.0F)); + poseStack.popPose(); + } + + /** + * The east-west tilt cap for a footprint. Tier 1 gets the full {@link #MAX_TILT}; wider multiblock + * decks are capped so the descending edge's underside never drops below the housing top (a bigger + * panel physically can't swing as far before hitting the ground). + */ + private static float maxTiltFor(int footprint) { + if (footprint <= 1) { + return MAX_TILT; + } + double half = footprint / 2.0; + double sin = (POLE_TOP - HOUSING_TOP - THICK / 2.0F) / half; + return (float) Math.toDegrees(Math.asin(Math.min(1.0, sin))); + } + + /** Draw the per-cell power connector stubs (block space, no deck rotation) on the `_base` sprite. */ + private void drawConnectors(SolarPanelRenderState state, PoseStack poseStack, + SubmitNodeCollector collector, int light) { + if (!(state.connector[0] || state.connector[1] || state.connector[2] || state.connector[3])) { + return; + } + Identifier baseTexture = Identifier.fromNamespaceAndPath( + Nerospace.MODID, "textures/block/solar_panel_t" + state.tier + "_base.png"); + var rt = RenderTypes.entityCutout(baseTexture); + if (state.connector[0]) { // NORTH (−Z) + connector(collector, poseStack, rt, light, CONN_LO, CONN_LO, 0.0F, CONN_HI, CONN_HI, CONN_REACH); + } + if (state.connector[2]) { // SOUTH (+Z) + connector(collector, poseStack, rt, light, CONN_LO, CONN_LO, 1.0F - CONN_REACH, CONN_HI, CONN_HI, 1.0F); + } + if (state.connector[1]) { // EAST (+X) + connector(collector, poseStack, rt, light, 1.0F - CONN_REACH, CONN_LO, CONN_LO, 1.0F, CONN_HI, CONN_HI); + } + if (state.connector[3]) { // WEST (−X) + connector(collector, poseStack, rt, light, 0.0F, CONN_LO, CONN_LO, CONN_REACH, CONN_HI, CONN_HI); + } + } + + /** A small box stub from the housing out to a face, mapping a centre patch of the base sprite. */ + private static void connector(SubmitNodeCollector collector, PoseStack poseStack, + RenderType rt, int light, + float x0, float y0, float z0, float x1, float y1, float z1) { + collector.order(1).submitCustomGeometry(poseStack, rt, + (pose, consumer) -> box(consumer, pose, light, x0, y0, z0, x1, y1, z1, + 0.25F, 0.25F, 0.75F, 0.75F)); + } + + /** + * A 1px-thick textured deck box. The PV sprite maps across the top/bottom by {@code x -> [u0,u1]} and + * {@code z -> [v0,v1]}; every face is double-sided so it shows from any angle through the cutout cull. + */ + private static void box(VertexConsumer c, PoseStack.Pose pose, int light, + float x0, float y0, float z0, float x1, float y1, float z1, + float u0, float v0, float u1, float v1) { + // top (+Y) / bottom (-Y) + face(c, pose, light, 0, 1, 0, x0, y1, z0, u0, v0, x0, y1, z1, u0, v1, x1, y1, z1, u1, v1, x1, y1, z0, u1, v0); + face(c, pose, light, 0, -1, 0, x0, y0, z0, u0, v0, x1, y0, z0, u1, v0, x1, y0, z1, u1, v1, x0, y0, z1, u0, v1); + // north (-Z) / south (+Z) + face(c, pose, light, 0, 0, -1, x0, y0, z0, u0, v0, x0, y1, z0, u0, v0, x1, y1, z0, u1, v0, x1, y0, z0, u1, v0); + face(c, pose, light, 0, 0, 1, x1, y0, z1, u1, v1, x1, y1, z1, u1, v1, x0, y1, z1, u0, v1, x0, y0, z1, u0, v1); + // west (-X) / east (+X) + face(c, pose, light, -1, 0, 0, x0, y0, z1, u0, v1, x0, y1, z1, u0, v1, x0, y1, z0, u0, v0, x0, y0, z0, u0, v0); + face(c, pose, light, 1, 0, 0, x1, y0, z0, u1, v0, x1, y1, z0, u1, v0, x1, y1, z1, u1, v1, x1, y0, z1, u1, v1); + } + + /** Emit a quad both ways (front with the given normal, back reversed) so it shows from both sides. */ + private static void face(VertexConsumer c, PoseStack.Pose pose, int light, float nx, float ny, float nz, + float ax, float ay, float az, float au, float av, + float bx, float by, float bz, float bu, float bv, + float cx, float cy, float cz, float cu, float cv, + float dx, float dy, float dz, float du, float dv) { + vertex(c, pose, ax, ay, az, au, av, light, nx, ny, nz); + vertex(c, pose, bx, by, bz, bu, bv, light, nx, ny, nz); + vertex(c, pose, cx, cy, cz, cu, cv, light, nx, ny, nz); + vertex(c, pose, dx, dy, dz, du, dv, light, nx, ny, nz); + vertex(c, pose, dx, dy, dz, du, dv, light, -nx, -ny, -nz); + vertex(c, pose, cx, cy, cz, cu, cv, light, -nx, -ny, -nz); + vertex(c, pose, bx, by, bz, bu, bv, light, -nx, -ny, -nz); + vertex(c, pose, ax, ay, az, au, av, light, -nx, -ny, -nz); + } + + private static void vertex(VertexConsumer c, PoseStack.Pose pose, float x, float y, float z, + float u, float v, int light, float nx, float ny, float nz) { + c.addVertex(pose, x, y, z) + .setColor(255, 255, 255, 255) + .setUv(u, v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(light) + .setNormal(pose, nx, ny, nz); + } +} 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 f2b079c..d101a99 100644 --- a/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java +++ b/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java @@ -350,12 +350,19 @@ private static int buildGallery(CommandSourceStack source) { buildGalleryQuarry(level, floor, origin.getX() + 42, origin.getZ() - 40, fy, 8, 8); buildGalleryQuarry(level, floor, origin.getX() + 64, origin.getZ() - 56, fy, 16, 12); + // SOLAR ARRAYS (SOLAR_PANEL_DESIGN, SW bearing): one unit per tier, then a multi-unit seam-joined + // field per tier (so the per-cell trackers reading as one surface is visible), plus a + // battery → universal cable → panel hookup that lights the panel's power connector. + buildSolarArrays(level, floor, origin.getX() - 50, origin.getZ() + 36, fy); + source.sendSuccess(() -> Component.literal("Built the Nerospace gallery: " + blocks.size() + " blocks, 4 RUNNING machine clusters (grinder line, fuel refinery " + "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), and 8 creatures (frozen for clean shots)."), false); + + "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); return Command.SINGLE_SUCCESS; } @@ -420,6 +427,56 @@ private static int clearGallery(CommandSourceStack source) { return Command.SINGLE_SUCCESS; } + /** + * Solar showcase (SW). Front row: one of each tier as a single unit — a 1×1 T1, a 2×2 T2 (one big + * panel) and a 3×3 T3 (one big panel). Behind it: several units of each tier side by side — nine T1 + * panels (a seam-joined 3×3 field), four T2 units and two T3 units — so multiple arrays tiling is + * visible. A Creative Battery → Universal Pipe → T1 panel line shows the dynamic power connector (the + * panel grows a stub toward the cable so the hookup butts up with no gap). Built at {@code (baseX, + * baseZ)}, extending east (+X) and south (+Z); panels sit on the floor with the tracking deck above. + */ + private static void buildSolarArrays(ServerLevel level, BlockState floor, int baseX, int baseZ, int fy) { + int sy = fy + 1; + for (int dx = -2; dx <= 20; dx++) { + for (int dz = -2; dz <= 10; dz++) { + level.setBlockAndUpdate(new BlockPos(baseX + dx, fy, baseZ + dz), floor); + } + } + + // Front row: one of each tier (multiblock anchors auto-fill their N×N footprint via onPlace). + placeSolar(level, ModBlocks.SOLAR_PANEL_T1.get(), baseX, sy, baseZ); + placeSolar(level, ModBlocks.SOLAR_PANEL_T2.get(), baseX + 2, sy, baseZ); // fills +2..3 + placeSolar(level, ModBlocks.SOLAR_PANEL_T3.get(), baseX + 5, sy, baseZ); // fills +5..7 + + // Cable hookup: Creative Battery → Universal Pipe → T1 panel (lights the panel's west connector). + level.setBlockAndUpdate(new BlockPos(baseX + 10, sy, baseZ), + ModBlocks.CREATIVE_BATTERY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(baseX + 11, sy, baseZ), + ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + placeSolar(level, ModBlocks.SOLAR_PANEL_T1.get(), baseX + 12, sy, baseZ); + + // Multi-unit seam-joined fields, set back (+Z) so footprints don't touch the front row. + // T1: a 3x3 field of nine single panels → one continuous tracking surface. + for (int dx = 0; dx <= 2; dx++) { + for (int dz = 4; dz <= 6; dz++) { + placeSolar(level, ModBlocks.SOLAR_PANEL_T1.get(), baseX + dx, sy, baseZ + dz); + } + } + // T2: four 2x2 units → a 4x4 field. + placeSolar(level, ModBlocks.SOLAR_PANEL_T2.get(), baseX + 5, sy, baseZ + 4); + placeSolar(level, ModBlocks.SOLAR_PANEL_T2.get(), baseX + 7, sy, baseZ + 4); + placeSolar(level, ModBlocks.SOLAR_PANEL_T2.get(), baseX + 5, sy, baseZ + 6); + placeSolar(level, ModBlocks.SOLAR_PANEL_T2.get(), baseX + 7, sy, baseZ + 6); + // T3: two 3x3 units → a 6x3 field. + placeSolar(level, ModBlocks.SOLAR_PANEL_T3.get(), baseX + 11, sy, baseZ + 4); // fills +11..13 + placeSolar(level, ModBlocks.SOLAR_PANEL_T3.get(), baseX + 14, sy, baseZ + 4); // fills +14..16 + } + + /** Place a solar panel anchor; multiblock tiers auto-expand their footprint in {@code onPlace}. */ + private static void placeSolar(ServerLevel level, Block block, int x, int y, int z) { + level.setBlockAndUpdate(new BlockPos(x, y, z), block.defaultBlockState()); + } + /** * Build one staged, fully-powered gallery quarry: a {@code (side+1) x (side+1)} region with its * frame ring, a west-side creative battery + pipe feed, an interior-only pre-carved pit diff --git a/src/main/java/za/co/neroland/nerospace/datagen/ModBlockLootSubProvider.java b/src/main/java/za/co/neroland/nerospace/datagen/ModBlockLootSubProvider.java index c0f6117..fa7d170 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModBlockLootSubProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModBlockLootSubProvider.java @@ -26,6 +26,12 @@ protected void generate() { dropSelf(ModBlocks.RAW_NEROSIUM_BLOCK.get()); dropSelf(ModBlocks.NEROSIUM_GRINDER.get()); + // Solar panels (SOLAR_PANEL_DESIGN). Tier 1 drops itself; the Tier 2/3 multiblocks drop nothing + // per cell — the block returns exactly one item for the whole unit on break (playerWillDestroy). + dropSelf(ModBlocks.SOLAR_PANEL_T1.get()); + add(ModBlocks.SOLAR_PANEL_T2.get(), noDrop()); + add(ModBlocks.SOLAR_PANEL_T3.get(), noDrop()); + add(ModBlocks.NEROSIUM_ORE.get(), block -> createOreDrop(block, ModItems.RAW_NEROSIUM.get())); add(ModBlocks.DEEPSLATE_NEROSIUM_ORE.get(), diff --git a/src/main/java/za/co/neroland/nerospace/datagen/ModBlockTagProvider.java b/src/main/java/za/co/neroland/nerospace/datagen/ModBlockTagProvider.java index 9dc5c9d..56dd8cd 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModBlockTagProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModBlockTagProvider.java @@ -49,6 +49,9 @@ protected void addTags(HolderLookup.Provider provider) { ModBlocks.UNIVERSAL_PIPE.get(), ModBlocks.COMBUSTION_GENERATOR.get(), ModBlocks.PASSIVE_GENERATOR.get(), + ModBlocks.SOLAR_PANEL_T1.get(), + ModBlocks.SOLAR_PANEL_T2.get(), + ModBlocks.SOLAR_PANEL_T3.get(), ModBlocks.BATTERY.get(), ModBlocks.FLUID_TANK.get(), ModBlocks.GAS_TANK.get(), @@ -84,6 +87,9 @@ protected void addTags(HolderLookup.Provider provider) { ModBlocks.UNIVERSAL_PIPE.get(), ModBlocks.COMBUSTION_GENERATOR.get(), ModBlocks.PASSIVE_GENERATOR.get(), + ModBlocks.SOLAR_PANEL_T1.get(), + ModBlocks.SOLAR_PANEL_T2.get(), + ModBlocks.SOLAR_PANEL_T3.get(), ModBlocks.QUARRY_CONTROLLER.get()); this.tag(Tags.Blocks.ORES) diff --git a/src/main/java/za/co/neroland/nerospace/datagen/ModLanguageProvider.java b/src/main/java/za/co/neroland/nerospace/datagen/ModLanguageProvider.java index 7e1c387..4a02fdd 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModLanguageProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModLanguageProvider.java @@ -56,6 +56,10 @@ protected void addTranslations() { add(ModBlocks.UNIVERSAL_PIPE.get(), "Universal Pipe"); add(ModBlocks.COMBUSTION_GENERATOR.get(), "Combustion Generator"); add(ModBlocks.PASSIVE_GENERATOR.get(), "Passive Generator"); + add(ModBlocks.SOLAR_PANEL_T1.get(), "Tier 1 Solar Panel"); + add(ModBlocks.SOLAR_PANEL_T2.get(), "Tier 2 Solar Panel"); + add(ModBlocks.SOLAR_PANEL_T3.get(), "Tier 3 Solar Panel"); + add("block.nerospace.solar_panel.readout", "Solar panel: %s / %s FE (array of %s)"); add(ModItems.CONFIGURATOR.get(), "Configurator"); add("block.nerospace.universal_pipe.energy", "Pipe energy: %s FE"); add("block.nerospace.universal_pipe.fluid", "Pipe fluid: %s mB of %s"); diff --git a/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java b/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java index 9d26a30..8db076e 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java @@ -72,6 +72,29 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat blockModels.blockStateOutput.accept( BlockModelGenerators.createSimpleBlock(pad, BlockModelGenerators.plainVariant(padModel))); + // Solar Panel (T1): the static steel mount — a 3px housing, a central post and a N-S cross-bar + // (the "T-pole") on its `_base` sprite. The sun-tracking PV deck pivots on it (renderer-drawn). + registerSolarHousing(blockModels, ModBlocks.SOLAR_PANEL_T1.get()); + + // Tier 2/3 Solar Panels: each cell is just a flat 3px housing on its own `_base` sprite. The + // single big N×N tilting deck (ONE panel) and its central support mast are drawn by the anchor's + // renderer. The "" variant covers both ANCHOR states. + for (Block multi : new Block[] {ModBlocks.SOLAR_PANEL_T2.get(), ModBlocks.SOLAR_PANEL_T3.get()}) { + var baseTex = TextureMapping.getBlockTexture(multi, "_base"); + TextureMapping mapping = new TextureMapping() + .put(TextureSlot.ALL, baseTex).put(TextureSlot.PARTICLE, baseTex); + ExtendedModelTemplate template = ExtendedModelTemplateBuilder.builder() + .requiredTextureSlot(TextureSlot.ALL) + .requiredTextureSlot(TextureSlot.PARTICLE) + .element(e -> e.from(0, 0, 0).to(16, 3, 16) + .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) + .build(); + Identifier model = template.create( + ModelLocationUtils.getModelLocation(multi), mapping, blockModels.modelOutput); + blockModels.blockStateOutput.accept( + BlockModelGenerators.createSimpleBlock(multi, BlockModelGenerators.plainVariant(model))); + } + // Launch Gantry — shaped tower in registerShapedMachines (art overhaul §3). // Power grid — connection-aware translucent pipe (multipart: core + one arm per connected @@ -441,6 +464,31 @@ private void registerTank(BlockModelGenerators blockModels, Block block) { shapedBlock(blockModels, block, builder.build(), mapping, false); } + /** + * One solar-panel housing model (used by every tier): a 3px housing, a central post, and a N-S + * cross-bar (the "T-pole"), all on the block's own {@code _base} sprite. The torque tube tops out at + * 8px — JUST BELOW the deck's 9px pivot — so it never rises through the deck at the 40° tracking cap. + */ + private void registerSolarHousing(BlockModelGenerators blockModels, Block solar) { + var baseTexture = TextureMapping.getBlockTexture(solar, "_base"); + TextureMapping mapping = new TextureMapping() + .put(TextureSlot.ALL, baseTexture).put(TextureSlot.PARTICLE, baseTexture); + ExtendedModelTemplate template = ExtendedModelTemplateBuilder.builder() + .requiredTextureSlot(TextureSlot.ALL) + .requiredTextureSlot(TextureSlot.PARTICLE) + .element(e -> e.from(0, 0, 0).to(16, 3, 16) // housing + .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) + .element(e -> e.from(7, 3, 7).to(9, 7, 9) // vertical post + .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) + .element(e -> e.from(7, 7, 4).to(9, 8, 12) // N-S torque tube under the deck swing + .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) + .build(); + Identifier model = template.create( + ModelLocationUtils.getModelLocation(solar), mapping, blockModels.modelOutput); + blockModels.blockStateOutput.accept( + BlockModelGenerators.createSimpleBlock(solar, BlockModelGenerators.plainVariant(model))); + } + /** * The Universal Pipe: a multipart blockstate — always the translucent core (4..12 cube), plus one * arm per connected face (the north arm model rotated for the other five). Translucency comes from diff --git a/src/main/java/za/co/neroland/nerospace/datagen/ModRecipeProvider.java b/src/main/java/za/co/neroland/nerospace/datagen/ModRecipeProvider.java index ef79398..9058f1f 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModRecipeProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModRecipeProvider.java @@ -46,6 +46,43 @@ private void moduleRecipe(net.minecraft.world.item.Item result, net.minecraft.wo @Override protected void buildRecipes() { + // --- Solar Panel T1 (SOLAR_PANEL_DESIGN): glass deck, quartz/copper cells, nerosteel housing. + // Higher tiers will fold the previous panel into their recipe (a Tier 1 panel inside Tier 2). + ShapedRecipeBuilder.shaped(this.registries.lookupOrThrow(Registries.ITEM), + RecipeCategory.REDSTONE, ModBlocks.SOLAR_PANEL_T1.get()) + .pattern("GGG") + .pattern("QCQ") + .pattern("NNN") + .define('G', Items.GLASS) + .define('Q', ModItems.XERTZ_QUARTZ) + .define('C', Items.COPPER_INGOT) + .define('N', ModTags.Items.INGOTS_NEROSTEEL) + .unlockedBy("has_xertz_quartz", this.has(ModItems.XERTZ_QUARTZ)) + .save(this.output); + + // Tier 2 (2x2): four Tier 1 panels around a Block of Nerosium core. Each tier folds in the + // previous panel, so building up the tiers reuses your existing array. + ShapedRecipeBuilder.shaped(this.registries.lookupOrThrow(Registries.ITEM), + RecipeCategory.REDSTONE, ModBlocks.SOLAR_PANEL_T2.get()) + .pattern(" P ") + .pattern("PNP") + .pattern(" P ") + .define('P', ModBlocks.SOLAR_PANEL_T1.get()) + .define('N', ModBlocks.NEROSIUM_BLOCK.get()) + .unlockedBy("has_solar_panel_t1", this.has(ModBlocks.SOLAR_PANEL_T1.get())) + .save(this.output); + + // Tier 3 (3x3): four Tier 2 panels around a Block of Gold core. + ShapedRecipeBuilder.shaped(this.registries.lookupOrThrow(Registries.ITEM), + RecipeCategory.REDSTONE, ModBlocks.SOLAR_PANEL_T3.get()) + .pattern(" Q ") + .pattern("QGQ") + .pattern(" Q ") + .define('Q', ModBlocks.SOLAR_PANEL_T2.get()) + .define('G', Items.GOLD_BLOCK) + .unlockedBy("has_solar_panel_t2", this.has(ModBlocks.SOLAR_PANEL_T2.get())) + .save(this.output); + // --- Smelting & blasting: raw nerosium -> ingot --------------------- SimpleCookingRecipeBuilder.smelting(Ingredient.of(ModItems.RAW_NEROSIUM), RecipeCategory.MISC, CookingBookCategory.MISC, ModItems.NEROSIUM_INGOT, 0.7F, 200) 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 03622d3..4238e0c 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -119,6 +119,12 @@ public final class ModBlockEntities { () -> new BlockEntityType<>( PassiveGeneratorBlockEntity::new, false, ModBlocks.PASSIVE_GENERATOR.get())); + public static final Supplier> SOLAR_PANEL = + BLOCK_ENTITY_TYPES.register("solar_panel", + () -> new BlockEntityType<>(za.co.neroland.nerospace.solar.SolarPanelBlockEntity::new, + false, ModBlocks.SOLAR_PANEL_T1.get(), + ModBlocks.SOLAR_PANEL_T2.get(), ModBlocks.SOLAR_PANEL_T3.get())); + // Storage endpoints + creative sources. public static final Supplier> BATTERY = BLOCK_ENTITY_TYPES.register("battery", 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 a50553b..5a054d2 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -389,6 +389,46 @@ public final class ModBlocks { .sound(SoundType.METAL) .noOcclusion()); // pedestal-and-panel model (art overhaul §3) + /** + * Tier 1 Solar Panel: a sun-tracking generator that pools with adjacent same-tier panels into one + * array (SOLAR_PANEL_DESIGN). noOcclusion — its flat housing must not cull neighbours, and the + * tilting deck is renderer-drawn above the block. + */ + public static final DeferredBlock SOLAR_PANEL_T1 = + BLOCKS.registerBlock("solar_panel_t1", + props -> new za.co.neroland.nerospace.solar.SolarPanelBlock( + za.co.neroland.nerospace.solar.SolarTier.TIER_1, props), + props -> props + .mapColor(MapColor.METAL) + .strength(3.0F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL) + .noOcclusion()); + + /** Tier 2 Solar Panel: a 2x2 multiblock array (placing one item fills the footprint). */ + public static final DeferredBlock SOLAR_PANEL_T2 = + BLOCKS.registerBlock("solar_panel_t2", + props -> new za.co.neroland.nerospace.solar.SolarPanelBlock( + za.co.neroland.nerospace.solar.SolarTier.TIER_2, props), + props -> props + .mapColor(MapColor.METAL) + .strength(3.0F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL) + .noOcclusion()); + + /** Tier 3 Solar Panel: a 3x3 multiblock array (placing one item fills the footprint). */ + public static final DeferredBlock SOLAR_PANEL_T3 = + BLOCKS.registerBlock("solar_panel_t3", + props -> new za.co.neroland.nerospace.solar.SolarPanelBlock( + za.co.neroland.nerospace.solar.SolarTier.TIER_3, props), + props -> props + .mapColor(MapColor.METAL) + .strength(3.0F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL) + .noOcclusion()); + // --- Storage endpoints (battery / tanks / item store + creative sources) --- // All storage endpoints carry shaped models (art overhaul §3) — noOcclusion stops the renderer diff --git a/src/main/java/za/co/neroland/nerospace/registry/ModCapabilities.java b/src/main/java/za/co/neroland/nerospace/registry/ModCapabilities.java index 0892924..7ed7c44 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModCapabilities.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModCapabilities.java @@ -109,6 +109,13 @@ public static void registerCapabilities(RegisterCapabilitiesEvent event) { ModBlockEntities.PASSIVE_GENERATOR.get(), (blockEntity, side) -> blockEntity.getEnergyHandler()); + // Solar panels: the pooled array energy is extractable on every side (output ports). The + // buffer's external receive is 0, so this never accepts a push — only generators feed it. + event.registerBlockEntity( + Capabilities.Energy.BLOCK, + ModBlockEntities.SOLAR_PANEL.get(), + (blockEntity, side) -> blockEntity.getEnergyHandler()); + // Generators expose their fuel/core slot so hoppers and pipes can feed them. event.registerBlockEntity( Capabilities.Item.BLOCK, 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 7a92c8b..87bdc86 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java @@ -71,6 +71,9 @@ public final class ModCreativeModeTabs { output.accept(ModBlocks.TERRAFORM_MONITOR.get()); output.accept(ModBlocks.COMBUSTION_GENERATOR.get()); output.accept(ModBlocks.PASSIVE_GENERATOR.get()); + output.accept(ModBlocks.SOLAR_PANEL_T1.get()); + output.accept(ModBlocks.SOLAR_PANEL_T2.get()); + output.accept(ModBlocks.SOLAR_PANEL_T3.get()); output.accept(ModBlocks.UNIVERSAL_PIPE.get()); // Quarry / Miner (MINER_DESIGN). 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 cb7bbf8..d598244 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -347,6 +347,12 @@ public final class ModItems { ITEMS.registerSimpleBlockItem(ModBlocks.COMBUSTION_GENERATOR); public static final DeferredItem PASSIVE_GENERATOR_ITEM = ITEMS.registerSimpleBlockItem(ModBlocks.PASSIVE_GENERATOR); + public static final DeferredItem SOLAR_PANEL_T1_ITEM = + ITEMS.registerSimpleBlockItem(ModBlocks.SOLAR_PANEL_T1); + public static final DeferredItem SOLAR_PANEL_T2_ITEM = + ITEMS.registerSimpleBlockItem(ModBlocks.SOLAR_PANEL_T2); + public static final DeferredItem SOLAR_PANEL_T3_ITEM = + ITEMS.registerSimpleBlockItem(ModBlocks.SOLAR_PANEL_T3); // Storage endpoints + creative sources. public static final DeferredItem BATTERY_ITEM = diff --git a/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java b/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java new file mode 100644 index 0000000..06d3729 --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java @@ -0,0 +1,128 @@ +package za.co.neroland.nerospace.solar; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; + +/** + * A connected run of same-tier solar panel units treated as ONE machine: total storage is the + * sum of every unit's buffer and total generation the sum of every unit's (sky-/weather-/dimension- + * scaled) output. The pooled energy is kept balanced across the unit anchors' buffers, so a pipe + * pulling from ANY panel face (forwarded by filler cells to their anchor) drains the whole array. + * + *

Built by flood-fill across all same-tier cells (anchors AND fillers, so multiblock footprints + * bridge), collecting the distinct anchors as members. Membership is rebuilt lazily so placing + * or breaking a panel needs no explicit hooks. Only the SAME {@link SolarTier} is adopted — different + * tiers stay separate arrays.

+ */ +public final class SolarArray { + + private static final int MAX_CELLS = 16_384; + + private final SolarTier tier; + /** Member anchor positions (one per unit). */ + private final List anchors; + private boolean valid = true; + private long lastTick = -1L; + + private SolarArray(SolarTier tier, List anchors) { + this.tier = tier; + this.anchors = anchors; + } + + public boolean isValid() { + return this.valid; + } + + public SolarTier tier() { + return this.tier; + } + + /** Number of pooled units (multiblocks) in the array. */ + public int size() { + return this.anchors.size(); + } + + /** Flood-fill the connected same-tier cells from {@code seed}, collect the distinct unit anchors. */ + public static SolarArray getOrBuild(ServerLevel level, BlockPos seed, SolarTier tier) { + List anchors = new ArrayList<>(); + LongOpenHashSet anchorSet = new LongOpenHashSet(); + LongOpenHashSet seen = new LongOpenHashSet(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(seed); + seen.add(seed.asLong()); + + int visited = 0; + while (!queue.isEmpty() && visited < MAX_CELLS) { + BlockPos pos = queue.poll(); + if (!(level.getBlockEntity(pos) instanceof SolarPanelBlockEntity cell) || cell.tier() != tier) { + continue; + } + visited++; + BlockPos anchor = cell.anchorPos(); + if (anchorSet.add(anchor.asLong())) { + anchors.add(anchor); + } + for (Direction dir : Direction.values()) { + BlockPos np = pos.relative(dir); + if (seen.add(np.asLong()) + && level.getBlockEntity(np) instanceof SolarPanelBlockEntity neighbour + && neighbour.tier() == tier) { + queue.add(np); + } + } + } + + SolarArray array = new SolarArray(tier, anchors); + for (BlockPos anchor : anchors) { + if (level.getBlockEntity(anchor) instanceof SolarPanelBlockEntity a) { + a.adopt(array); + } + } + return array; + } + + /** Generate this tick's pooled energy and re-balance the anchors' buffers. Runs once per game tick. */ + public void tick(ServerLevel level) { + long gameTime = level.getGameTime(); + if (gameTime == this.lastTick) { + return; + } + this.lastTick = gameTime; + + List units = new ArrayList<>(this.anchors.size()); + for (BlockPos anchor : this.anchors) { + if (level.getBlockEntity(anchor) instanceof SolarPanelBlockEntity a + && a.isAnchor() && a.tier() == this.tier) { + units.add(a); + } else { + this.valid = false; // a unit anchor vanished/changed — members rebuild next tick + return; + } + } + if (units.isEmpty()) { + this.valid = false; + return; + } + + // Each unit contributes its own daylight-scaled output; the sum is the array's generation. Add + // into the per-unit buffers, then balance them into one pool. + long total = 0L; + for (SolarPanelBlockEntity unit : units) { + unit.generate(unit.generationThisTick(level)); + total += unit.energy().getAmountAsInt(); + } + int n = units.size(); + int base = (int) (total / n); + int remainder = (int) (total % n); + for (int i = 0; i < n; i++) { + units.get(i).energy().setStored(base + (i < remainder ? 1 : 0)); + } + } +} diff --git a/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlock.java b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlock.java new file mode 100644 index 0000000..864b497 --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlock.java @@ -0,0 +1,207 @@ +package za.co.neroland.nerospace.solar; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * A solar panel that pools with adjacent same-tier panels into a {@link SolarArray}. Tier 1 is a single + * 1×1 block; Tier 2 (2×2) and Tier 3 (3×3) are multiblocks: placing the item fills the whole N×N + * footprint with one {@link #ANCHOR} cell at the clicked min-corner plus filler cells, all powered as a + * single unit. Breaking any cell tears the whole unit down and returns one item. + * + *

The block is a flat slab (collision + base model); the tilting, sun-tracking, night-folding deck is + * drawn by the block-entity renderer (only on the anchor for multiblocks). Energy is exposed on every + * side (output ports), so any face feeds a pipe or machine from the shared array pool.

+ */ +public class SolarPanelBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + instance.group( + SolarTier.CODEC.fieldOf("tier").forGetter(SolarPanelBlock::tier), + propertiesCodec() + ).apply(instance, SolarPanelBlock::new)); + + /** True on the unit's min-corner cell — the only cell that drops the item and renders the deck. */ + public static final BooleanProperty ANCHOR = BooleanProperty.create("anchor"); + + /** Flat 4px slab — the panel housing; the tilting surface above it is renderer-only. */ + private static final VoxelShape SHAPE = Block.box(0.0, 0.0, 0.0, 16.0, 4.0, 16.0); + + /** Re-entrancy guard so cascading a multiblock teardown doesn't recurse or double-drop. */ + private static boolean tearingDown = false; + + private final SolarTier tier; + + @SuppressWarnings("this-escape") // registerDefaultState is the idiomatic constructor wiring + public SolarPanelBlock(SolarTier tier, Properties properties) { + super(properties); + this.tier = tier; + // Default to an anchor so a raw setBlock (e.g. the /nerospace gallery) places a working unit; + // filler cells are explicitly set to false during multiblock placement. + registerDefaultState(this.stateDefinition.any().setValue(ANCHOR, true)); + } + + public SolarTier tier() { + return this.tier; + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ANCHOR); + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new SolarPanelBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.SOLAR_PANEL.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + // --- Multiblock placement ------------------------------------------------- + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + BlockState anchor = defaultBlockState().setValue(ANCHOR, true); + int n = this.tier.footprint; + if (n <= 1) { + return anchor; + } + // The clicked cell is already validated by the item; require the rest of the N×N footprint clear. + Level level = context.getLevel(); + BlockPos origin = context.getClickedPos(); + for (int dx = 0; dx < n; dx++) { + for (int dz = 0; dz < n; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + if (!level.getBlockState(origin.offset(dx, 0, dz)).canBeReplaced(context)) { + return null; // footprint blocked — cancel the placement + } + } + } + return anchor; + } + + @Override + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { + super.onPlace(state, level, pos, oldState, movedByPiston); + if (level.isClientSide() || this.tier.footprint <= 1 || !state.getValue(ANCHOR)) { + return; + } + int n = this.tier.footprint; + BlockState part = defaultBlockState().setValue(ANCHOR, false); + for (int dx = 0; dx < n; dx++) { + for (int dz = 0; dz < n; dz++) { + BlockPos cell = pos.offset(dx, 0, dz); + if (!cell.equals(pos)) { + BlockState existing = level.getBlockState(cell); + if (existing.getBlock() != this && existing.canBeReplaced()) { + level.setBlock(cell, part, Block.UPDATE_CLIENTS); + } + } + if (level.getBlockEntity(cell) instanceof SolarPanelBlockEntity be) { + be.setAnchor(pos); // anchor cell -> self; fillers -> the anchor + } + } + } + } + + // --- Multiblock teardown -------------------------------------------------- + + @Override + public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + if (this.tier.footprint > 1 && !level.isClientSide() && !tearingDown) { + BlockPos anchor = level.getBlockEntity(pos) instanceof SolarPanelBlockEntity be ? be.anchorPos() : pos; + tearingDown = true; + try { + int n = this.tier.footprint; + for (int dx = 0; dx < n; dx++) { + for (int dz = 0; dz < n; dz++) { + BlockPos cell = anchor.offset(dx, 0, dz); + if (!cell.equals(pos) && level.getBlockState(cell).getBlock() == this) { + level.removeBlock(cell, false); // sibling cell — no drops + } + } + } + // One item back for the whole unit (the broken cell's own loot is empty for multiblocks). + if (player == null || !player.getAbilities().instabuild) { + Block.popResource(level, anchor, new ItemStack(this)); + } + } finally { + tearingDown = false; + } + } + return super.playerWillDestroy(level, pos, state, player); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel) { + serverPlayer.sendSystemMessage(Component.translatable("block.nerospace.solar_panel.readout", + panel.getEnergyHandler().getAmountAsInt(), panel.getEnergyHandler().getCapacityAsInt(), + panel.arraySize())); + } + return InteractionResult.SUCCESS; + } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel ? panel.comparatorSignal() : 0; + } +} diff --git a/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java new file mode 100644 index 0000000..82c1a18 --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java @@ -0,0 +1,212 @@ +package za.co.neroland.nerospace.solar; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +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 net.neoforged.neoforge.transfer.energy.EnergyHandler; +import net.neoforged.neoforge.transfer.energy.SimpleEnergyHandler; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.Tuning; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModDimensionTypes; + +/** + * One cell of a solar panel. A Tier 1 panel is a single 1×1 cell; Tier 2/3 are N×N multiblocks made of + * one anchor cell (min-corner) plus filler cells that all point back to it ({@link #anchorPos}). + * Only the anchor ticks, generates, and holds the unit's FE buffer; filler cells forward their energy + * capability to the anchor, so a pipe on ANY face of the multiblock pulls from the one pooled buffer. + * + *

Adjacent same-tier units pool further into a {@link SolarArray}: total storage and generation are + * the sums across every unit's anchor. Generation scales with the sun's height (peaks at noon, zero at + * night), needs a clear view of the sky, is cut by rain/thunder, and is doubled in the mod's + * space/airless dimensions (permanent sun).

+ */ +public class SolarPanelBlockEntity extends BlockEntity { + + private final SolarTier tier; + private final SolarEnergy energy; + + /** This cell's unit anchor (the multiblock min-corner). Defaults to self — every T1 cell is its own. */ + private BlockPos anchorPos; + + /** Transient: the array this unit belongs to, lazily (re)built and shared with all member anchors. */ + @Nullable + private SolarArray array; + + public SolarPanelBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.SOLAR_PANEL.get(), pos, state); + this.tier = state.getBlock() instanceof SolarPanelBlock panel ? panel.tier() : SolarTier.TIER_1; + this.energy = new SolarEnergy(this.tier.buffer()); + this.anchorPos = pos; + } + + public SolarTier tier() { + return this.tier; + } + + /** The multiblock anchor (min-corner) this cell belongs to. */ + public BlockPos anchorPos() { + return this.anchorPos; + } + + /** True when this cell is its unit's anchor — the only cell that ticks, generates and renders. */ + public boolean isAnchor() { + return this.anchorPos.equals(this.worldPosition); + } + + /** Point a filler cell at its anchor (set during placement). */ + public void setAnchor(BlockPos anchor) { + this.anchorPos = anchor; + setChanged(); + } + + /** The anchor's BE (this one if it is the anchor), or {@code null} if the anchor is gone. */ + @Nullable + private SolarPanelBlockEntity anchorEntity() { + if (isAnchor()) { + return this; + } + return this.level != null && this.level.getBlockEntity(this.anchorPos) instanceof SolarPanelBlockEntity a + ? a : null; + } + + /** + * Exposed to {@code Capabilities.Energy.BLOCK} on every face. Filler cells forward to the anchor's + * buffer, so the whole multiblock reads as one extract-only pool from any side. + */ + public EnergyHandler getEnergyHandler() { + SolarPanelBlockEntity anchor = anchorEntity(); + return anchor != null ? anchor.energy : this.energy; + } + + SolarEnergy energy() { + return this.energy; + } + + /** Number of unit anchors in this panel's array (1 if not yet resolved). */ + public int arraySize() { + SolarArray net = this.array; + return net == null ? 1 : net.size(); + } + + /** Called by {@link SolarArray#getOrBuild} so every member anchor shares the one array instance. */ + void adopt(SolarArray net) { + this.array = net; + } + + /** Drop the cached array so the next tick rebuilds it (placement / break / neighbour change). */ + public void invalidateArray() { + this.array = null; + } + + /** Raise the buffer by {@code amount} (generation path; bypasses the zero external receive limit). */ + void generate(int amount) { + this.energy.generate(amount); + } + + public int comparatorSignal() { + EnergyHandler handler = getEnergyHandler(); + int cap = handler.getCapacityAsInt(); + int stored = handler.getAmountAsInt(); + return (cap <= 0 || stored <= 0) ? 0 : 1 + (int) (stored / (double) cap * 14.0D); + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (!(level instanceof ServerLevel server) || !isAnchor()) { + return; // only the anchor drives the unit; filler cells are passive forwarders + } + SolarArray net = this.array; // local so the null/valid check holds for the analyzer + if (net == null || !net.isValid()) { + net = SolarArray.getOrBuild(server, pos, this.tier); + this.array = net; + } + net.tick(server); + } + + /** FE this whole unit adds this tick = its tier's peak output x the daylight/weather/dimension factor. */ + public int generationThisTick(ServerLevel level) { + return Math.round(this.tier.fePerTick() * solarFactor(level, this.worldPosition)); + } + + /** + * Daylight factor in [0, 2]: the sun-height curve (0 at night), gated on sky access, cut by weather, + * and doubled in space/airless dimensions (which have a permanent sun and no weather). + */ + private static float solarFactor(ServerLevel level, BlockPos pos) { + boolean space = level.dimensionTypeRegistration().is(ModDimensionTypes.SPACE); + BlockPos above = pos.above(); + + float daylight; + if (space) { + daylight = 1.0F; // permanent sun in orbit / on an airless moon + } else { + if (!level.canSeeSky(above)) { + return 0.0F; // roofed over — no sun reaches the panel + } + // This dimension's own clock (matches the renderer's fold + the sky the player sees); in the + // overworld this is identical to the overworld clock, so survival balance is unchanged. + long tod = level.getDefaultClockTime() % 24000L; // 0 sunrise, 6000 noon, 18000 midnight + float sun = Mth.cos((float) ((tod - 6000L) / 24000.0 * 2.0 * Math.PI)); // +1 noon, -1 midnight + daylight = Math.max(0.0F, sun); + } + + float weather = 1.0F; + if (!space) { + if (level.isThundering()) { + weather = 0.25F; + } else if (level.isRaining() && level.isRainingAt(above)) { + weather = 0.4F; + } + } + + float dimensionBonus = space ? 2.0F : 1.0F; + return daylight * weather * dimensionBonus; + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + this.energy.serialize(output.child("Energy")); + output.putLong("Anchor", this.anchorPos.asLong()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.deserialize(input.childOrEmpty("Energy")); + this.anchorPos = BlockPos.of(input.getLongOr("Anchor", this.worldPosition.asLong())); + } + + /** Extract-only FE buffer (external receive = 0); {@link #generate}/{@link #setStored} are internal. */ + final class SolarEnergy extends SimpleEnergyHandler { + SolarEnergy(int capacity) { + super(capacity, 0, Tuning.energyPipeThroughput()); + } + + @Override + protected void onEnergyChanged(int previousAmount) { + SolarPanelBlockEntity.this.setChanged(); + } + + void generate(int amount) { + if (amount <= 0) { + return; + } + int next = Math.min(getCapacityAsInt(), getAmountAsInt() + amount); + if (next != getAmountAsInt()) { + set(next); + } + } + + void setStored(int value) { + set(Math.max(0, Math.min(getCapacityAsInt(), value))); + } + } +} diff --git a/src/main/java/za/co/neroland/nerospace/solar/SolarTier.java b/src/main/java/za/co/neroland/nerospace/solar/SolarTier.java new file mode 100644 index 0000000..f576eec --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarTier.java @@ -0,0 +1,51 @@ +package za.co.neroland.nerospace.solar; + +import com.mojang.serialization.Codec; + +import net.minecraft.util.StringRepresentable; + +import za.co.neroland.nerospace.Tuning; + +/** + * The three solar-panel tiers. Each tier is its own registered block, occupies an {@code NxN} + * horizontal footprint, and only ever pools energy with panels of the SAME tier (a Tier 1 array and a + * Tier 2 array never merge — see {@link SolarArray}). Generation and storage come from {@link Tuning} + * (config-scaled), so modpacks tune the whole family through {@code energyRateMultiplier}. + * + *

VERTICAL SLICE: only {@link #TIER_1} is registered/wired so far; the Tier 2/3 entries exist so the + * balance numbers, recipes and renderer are already tier-aware when those blocks are added.

+ */ +public enum SolarTier implements StringRepresentable { + TIER_1("tier_1", 1, 1), + TIER_2("tier_2", 2, 2), + TIER_3("tier_3", 3, 3); + + public static final Codec CODEC = StringRepresentable.fromEnum(SolarTier::values); + + private final String name; + /** 1-based tier number (drives the Tuning lookups). */ + public final int tier; + /** Footprint edge length in blocks: T1 = 1 (1x1), T2 = 2 (2x2), T3 = 3 (3x3). */ + public final int footprint; + + SolarTier(String name, int tier, int footprint) { + this.name = name; + this.tier = tier; + this.footprint = footprint; + } + + /** Peak FE/tick this single panel adds at full sun (config-scaled). */ + public int fePerTick() { + return Tuning.solarPanelFePerTick(this.tier); + } + + /** This panel's own FE buffer; an array's total storage is the sum across its members. */ + public int buffer() { + return Tuning.solarPanelBuffer(this.tier); + } + + @Override + public String getSerializedName() { + return this.name; + } +} diff --git a/src/main/resources/assets/nerospace/textures/block/solar_panel_t1.png b/src/main/resources/assets/nerospace/textures/block/solar_panel_t1.png new file mode 100644 index 0000000..33fc25c Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/solar_panel_t1.png differ diff --git a/src/main/resources/assets/nerospace/textures/block/solar_panel_t1_base.png b/src/main/resources/assets/nerospace/textures/block/solar_panel_t1_base.png new file mode 100644 index 0000000..1416158 Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/solar_panel_t1_base.png differ diff --git a/src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png b/src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png new file mode 100644 index 0000000..80f6b25 Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png differ diff --git a/src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png b/src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png new file mode 100644 index 0000000..f82e4ca Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png differ diff --git a/src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png b/src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png new file mode 100644 index 0000000..2c2bd35 Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png differ diff --git a/src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png b/src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png new file mode 100644 index 0000000..81d869a Binary files /dev/null and b/src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png differ diff --git a/tools/gen_bbmodels.py b/tools/gen_bbmodels.py index da29bad..5115da4 100644 --- a/tools/gen_bbmodels.py +++ b/tools/gen_bbmodels.py @@ -44,7 +44,10 @@ "battery", "creative_battery", "fluid_tank", "creative_fluid_tank", "gas_tank", "creative_gas_tank", "item_store", "creative_item_store", "star_guide", "launch_gantry", "sentry_test", - "quarry_controller", "quarry_landmark", "quarry_frame", "trash_can"] + "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"] 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", diff --git a/tools/gen_textures.py b/tools/gen_textures.py index 817b54f..c1564ae 100644 --- a/tools/gen_textures.py +++ b/tools/gen_textures.py @@ -2823,7 +2823,77 @@ def gen_trash_can(): save(img, os.path.join(BLOCK_DIR, "trash_can.png")) +def gen_solar_panel(name, edge): + """Futuristic photovoltaic deck: neon blue-and-green cells on a near-black lattice, filling the + sprite edge-to-edge (pixel-perfect — no steel padding), wrapped in a single tier-coloured pixel + border. The {@code edge} colour codes the tier (T1/T2/T3); the cells are the same for every tier.""" + img = new_img() + px = img.load() + sub = (10, 16, 30, 255) # near-black blue substrate = the grid wires/gaps + blue_d = (22, 60, 104, 255) + blue = (40, 112, 172, 255) + blue_hi = (96, 184, 238, 255) + grn = (50, 206, 112, 255) # bright green mixed in with the blue cells + grn_hi = (132, 255, 176, 255) + for y in range(S): + for x in range(S): + if x % 3 == 0 or y % 3 == 0: + px[x, y] = sub # thin dark lattice between cells + else: + cx, cy = x // 3, y // 3 + node = (x % 3 == 2 and y % 3 == 2) # cell centre = highlight node + if (cx + cy) % 3 == 1: # ~a third of cells are bright green + px[x, y] = grn_hi if node else grn + else: + px[x, y] = blue_hi if node else (blue if (x + y) % 2 == 0 else blue_d) + # A brighter green core node so the deck reads as "alive". + px[7, 7] = grn_hi + px[8, 8] = grn_hi + # Tier-coloured edge ring: the outermost pixels, all four sides (pixel-perfect, no padding). + for i in range(S): + px[0, i] = edge + px[S - 1, i] = edge + px[i, 0] = edge + px[i, S - 1] = edge + save(img, os.path.join(BLOCK_DIR, name + ".png")) + + +def gen_solar_panel_base(name, edge): + """The housing the deck sits on — a neutral blued-steel plate (NOT the green steel, NOT the PV + sprite) with a recessed bay and tier-coloured corner bolts, so the static base reads as a distinct + futuristic mount and still identifies its tier.""" + img = new_img() + px = img.load() + base = METAL[1] + dk = METAL[2] + lt = METAL_L + recess = METAL[0] + for y in range(S): + for x in range(S): + px[x, y] = base + bevel(img, lt, dk) + # Recessed central bay (where the deck rests when folded flat). + for y in range(3, 13): + for x in range(3, 13): + px[x, y] = dk if (x + y) % 2 == 0 else recess + # Two vent ridges + tier-coloured corner bolts. + for x in range(4, 12): + px[x, 5] = lt + px[x, 10] = lt + for (bx, by) in ((2, 2), (13, 2), (2, 13), (13, 13)): + px[bx, by] = edge + save(img, os.path.join(BLOCK_DIR, name + ".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. + gen_solar_panel("solar_panel_t1", N_RED) + gen_solar_panel_base("solar_panel_t1_base", N_RED) + gen_solar_panel("solar_panel_t2", N_MAG) + gen_solar_panel_base("solar_panel_t2_base", N_MAG) + gen_solar_panel("solar_panel_t3", GOLD) + gen_solar_panel_base("solar_panel_t3_base", GOLD) # Trash Can (logistics void sink). gen_trash_can() # Quarry / Miner (MINER_DESIGN). diff --git a/wiki/Roadmap.md b/wiki/Roadmap.md index e74ae8c..8ee118c 100644 --- a/wiki/Roadmap.md +++ b/wiki/Roadmap.md @@ -43,6 +43,8 @@ release** — the complete progression from the first nerosium ore to a terrafor ## 🛠️ Next up (first post-1.0 updates) +- **[Solar Panels](Solar-Panel)** — sun-tracking, array-pooling power generation across **three tiers** + (Tier 1 1×1, Tier 2 2×2, Tier 3 3×3 multiblocks), each folding the previous panel into its recipe. - **EMI integration** as soon as it reaches 26.1. - **Balance tuning from player feedback** — the config multipliers make this cheap. - **Bespoke audio** to replace the vanilla-alias placeholders. diff --git a/wiki/Solar-Panel.md b/wiki/Solar-Panel.md new file mode 100644 index 0000000..eb8d87a --- /dev/null +++ b/wiki/Solar-Panel.md @@ -0,0 +1,90 @@ +# Solar Panel + +Sun-tracking generators that pool with their neighbours into one big solar array. Comes in **three +tiers** — Tier 1 is a single block; Tier 2 and Tier 3 are 2×2 and 3×3 **multiblocks** that you place +from a single item. + +## Obtaining + +**Tier 1** (shaped): a glass deck over quartz/copper cells on a nerosteel housing — + +```text +G G G +Q C Q +N N N +``` + +`G` = Glass · `Q` = Xertz Quartz · `C` = Copper Ingot · `N` = Nerosteel Ingot + +**Tier 2** — four Tier 1 panels around a Block of Nerosium: + +```text +. P . +P N P +. P . +``` + +`P` = Tier 1 Solar Panel · `N` = Block of Nerosium + +**Tier 3** — four Tier 2 panels around a Block of Gold: + +```text +. Q . +Q G Q +. Q . +``` + +`Q` = Tier 2 Solar Panel · `G` = Block of Gold + +Each tier folds the **previous panel** into its recipe, so upgrading reuses what you already built. + +## How it works + +- **Sunlight in, FE out.** Output follows the sun: full at noon, tapering to **nothing at night**. + The panel needs a **clear view of the sky** — anything solid directly above it stops generation. +- **Weather** cuts output: rain/snow drops it to ~40%, a thunderstorm to ~25%. +- **Airless dimensions** (Orbital Station, Greenxertz, Cindara, Glacira, founded stations) have a + permanent sun, so panels there run at full **and earn a ×2 bonus** — solar is the natural off-world + power source. +- Output and storage scale by tier (all values × the `energyRateMultiplier` config). The buffer is + **extract-only** — it never accepts a push. + +| Tier | Footprint | Output @ noon | Storage | +|---|---|---|---| +| **Tier 1** | 1×1 | 20 FE/t | 50,000 FE | +| **Tier 2** | 2×2 | 100 FE/t | 250,000 FE | +| **Tier 3** | 3×3 | 400 FE/t | 1,000,000 FE | + +Each higher tier is stronger **per area** than tiling the lower one, and carries a much bigger buffer. + +### Multiblocks (Tier 2 & 3) + +- A Tier 2 / Tier 3 panel is a **single item that fills its whole N×N footprint** when placed — you need + that flat area clear (it won't place into an obstructed footprint). +- It behaves as **one unit**: one big tilting deck, one pooled buffer, energy on every outer face. +- **Breaking any cell returns the whole panel as one item** — no duplication, and you get your panel + back wherever you mined it. + +### Arrays + +- Place same-tier units **next to each other** and they automatically merge into one **array**: the + array's storage is the **sum of every unit's buffer** and its generation the **sum of every unit's + output**. Build wider for more of both — arrays can be almost any size. +- **Every side is an output port.** A Universal Pipe (energy layer) — or any machine/Battery — touching + *any* face pulls from the shared array pool, so one pipe drains the whole array. +- **Tiers never mix.** A Tier 1 array and a Tier 2 array placed side by side stay separate; only units + of the *same* tier pool together. + +### Appearance + +The tiers are **visibly distinct** — Tier 1 has a green accent and pivots on a **T-pole**, tracking the +sun east-to-west; Tier 2 (nerosium magenta) and Tier 3 (gold) are flat N×N arrays whose decks **tilt up +to face the sky by day and fold flat onto their housings at night**. All panels read the same world +time, so a field of them moves in lockstep. + +## Details + +- IDs: `nerospace:solar_panel_t1` / `_t2` / `_t3` · Tool: pickaxe, iron tier +- Tier 1 drops itself; Tier 2/3 return one item for the whole multiblock when any cell is broken. +- Emits a comparator signal from the array's charge. +- Config: `energyRateMultiplier` (scales both output and storage). See **[Configuration](Configuration)**. diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 51cb7f4..f082946 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -43,6 +43,7 @@ - [Pipe Filters and Upgrades](Pipe-Filters-and-Upgrades) - [Combustion Generator](Combustion-Generator) - [Passive Generator](Passive-Generator) +- [Solar Panel](Solar-Panel) - [Battery](Battery) - [Fluid Tank](Fluid-Tank) - [Gas Tank](Gas-Tank)