From 4c422c513292402346aef62ac8410a149dc30658 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:13:33 +0800 Subject: [PATCH 1/6] Add Tier 1 solar panel block, BE, and renderer Introduce a new Tier 1 Solar Panel: block, block entity, energy handler and pooled-array logic. Adds SolarPanelBlock/SolarPanelBlockEntity, SolarArray (flood-fill pooled storage/generation), SolarTier constants and tuning, and an extract-only SolarEnergy handler. Client: SolarPanelRenderState & SolarPanelRenderer to draw a sun-tracking, night-folding deck that joins with same-tier neighbours. Resources and data: models, blockstate, item model, texture, loot table, recipe, advancement, language entry, and data-gen updates (loot/tags/models/recipes). Registration: block, item, block entity, capability exposure, creative tab and datagen wiring. Behavior: generation scales with sun/weather/dimension, arrays balance buffers across members, comparator/readout support, and server-side ticking for pooled generation. --- art/blockbench/block/solar_panel_t1.bbmodel | 136 ++++++++++++++ .../nerospace/blockstates/solar_panel_t1.json | 7 + .../nerospace/items/solar_panel_t1.json | 6 + .../assets/nerospace/lang/en_us.json | 2 + .../models/block/solar_panel_t1.json | 72 ++++++++ .../tags/block/mineable/pickaxe.json | 1 + .../minecraft/tags/block/needs_iron_tool.json | 1 + .../recipes/redstone/solar_panel_t1.json | 32 ++++ .../loot_table/blocks/solar_panel_t1.json | 21 +++ .../data/nerospace/recipe/solar_panel_t1.json | 18 ++ .../neroland/nerospace/NerospaceClient.java | 5 + .../java/za/co/neroland/nerospace/Tuning.java | 32 ++++ .../client/SolarPanelRenderState.java | 20 ++ .../nerospace/client/SolarPanelRenderer.java | 129 +++++++++++++ .../datagen/ModBlockLootSubProvider.java | 3 + .../datagen/ModBlockTagProvider.java | 2 + .../datagen/ModLanguageProvider.java | 2 + .../nerospace/datagen/ModModelProvider.java | 19 ++ .../nerospace/datagen/ModRecipeProvider.java | 14 ++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 16 ++ .../nerospace/registry/ModCapabilities.java | 7 + .../registry/ModCreativeModeTabs.java | 1 + .../neroland/nerospace/registry/ModItems.java | 2 + .../neroland/nerospace/solar/SolarArray.java | 125 +++++++++++++ .../nerospace/solar/SolarPanelBlock.java | 104 +++++++++++ .../solar/SolarPanelBlockEntity.java | 173 ++++++++++++++++++ .../neroland/nerospace/solar/SolarTier.java | 51 ++++++ .../textures/block/solar_panel_t1.png | Bin 0 -> 235 bytes tools/gen_bbmodels.py | 3 +- tools/gen_textures.py | 35 ++++ 31 files changed, 1043 insertions(+), 1 deletion(-) create mode 100644 art/blockbench/block/solar_panel_t1.bbmodel create mode 100644 src/generated/resources/assets/nerospace/blockstates/solar_panel_t1.json create mode 100644 src/generated/resources/assets/nerospace/items/solar_panel_t1.json create mode 100644 src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json create mode 100644 src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t1.json create mode 100644 src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t1.json create mode 100644 src/generated/resources/data/nerospace/recipe/solar_panel_t1.json create mode 100644 src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java create mode 100644 src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java create mode 100644 src/main/java/za/co/neroland/nerospace/solar/SolarArray.java create mode 100644 src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlock.java create mode 100644 src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java create mode 100644 src/main/java/za/co/neroland/nerospace/solar/SolarTier.java create mode 100644 src/main/resources/assets/nerospace/textures/block/solar_panel_t1.png 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/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/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/lang/en_us.json b/src/generated/resources/assets/nerospace/lang/en_us.json index 965d950..c0b2a82 100644 --- a/src/generated/resources/assets/nerospace/lang/en_us.json +++ b/src/generated/resources/assets/nerospace/lang/en_us.json @@ -51,6 +51,8 @@ "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": "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..70cf711 --- /dev/null +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json @@ -0,0 +1,72 @@ +{ + "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, + 2, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 1, + 2, + 1 + ], + "to": [ + 15, + 4, + 15 + ] + } + ], + "textures": { + "all": "nerospace:block/solar_panel_t1", + "particle": "nerospace:block/solar_panel_t1" + } +} \ 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..52bd34c 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,7 @@ "nerospace:universal_pipe", "nerospace:combustion_generator", "nerospace:passive_generator", + "nerospace:solar_panel_t1", "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..b8f4f00 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,7 @@ "nerospace:universal_pipe", "nerospace:combustion_generator", "nerospace:passive_generator", + "nerospace:solar_panel_t1", "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/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/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/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 efdf68c..5a9514a 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; @@ -212,6 +224,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/SolarPanelRenderState.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java new file mode 100644 index 0000000..1f6e8fd --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; + +/** + * Render state for a single solar panel: the blended tilt angle of its surface (sun-tracking by day, + * folded flat at night) plus which horizontal neighbours are same-tier panels, so an array of them + * reads as one continuous, seam-joined surface. + */ +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; + + /** Same-tier neighbour present, indexed N=0, E=1, S=2, W=3 (drives edge-to-edge seam joining). */ + public final boolean[] connect = new boolean[4]; + + /** 1-based tier (selects the surface texture). */ + public int tier = 1; +} 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..31eb711 --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java @@ -0,0 +1,129 @@ +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.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 za.co.neroland.nerospace.Nerospace; +import za.co.neroland.nerospace.registry.ModDimensionTypes; +import za.co.neroland.nerospace.solar.SolarPanelBlockEntity; +import za.co.neroland.nerospace.solar.SolarTier; + +/** + * Draws the moving solar-panel surface above its (static) housing model: a textured deck that tilts to + * track the sun across the day and folds flat at night. Every panel reads the SAME world time, so a + * whole array stays in lockstep, and connected same-tier neighbours extend their decks edge-to-edge so + * a row joins into one continuous, seamless surface. + */ +public class SolarPanelRenderer + implements BlockEntityRenderer { + + /** Hinge height (just above the 4px housing) so the tilted deck clears the block top. */ + private static final float PIVOT_Y = 0.27F; + /** Tracking tilt is clamped so the array stays low-profile (a tilting field, not standing poles). */ + private static final float MAX_TILT = 35.0F; + /** Half-extent of a deck edge that has NO neighbour (leaves a thin frame gap between arrays). */ + private static final float INSET = 0.46F; + /** Half-extent of a deck edge that DOES touch a same-tier neighbour (meets at the block border). */ + private static final float EDGE = 0.5F; + + @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; + + boolean space = level.dimensionTypeRegistration().is(ModDimensionTypes.SPACE); + if (space) { + // Permanent sun in orbit / on an airless moon: stay open, facing up. + state.angle = 0.0F; + } else { + long tod = level.getOverworldClockTime() % 24000L; // 0 sunrise, 6000 noon, 18000 midnight + float sun = Mth.cos((float) ((tod - 6000L) / 24000.0 * 2.0 * Math.PI)); // +1 noon, -1 midnight + float openness = Mth.clamp((sun + 0.02F) / 0.25F, 0.0F, 1.0F); // 0 at night -> folds flat + float deg = (float) ((tod - 6000L) / 24000.0) * 360.0F; // 0 noon, -90 sunrise, +90 sunset + state.angle = openness * Mth.clamp(deg, -MAX_TILT, MAX_TILT); + } + + SolarTier tier = panel.tier(); + BlockPos pos = panel.getBlockPos(); + state.connect[0] = sameTier(level, pos.relative(Direction.NORTH), tier); + state.connect[1] = sameTier(level, pos.relative(Direction.EAST), tier); + state.connect[2] = sameTier(level, pos.relative(Direction.SOUTH), tier); + state.connect[3] = sameTier(level, pos.relative(Direction.WEST), tier); + } + + private static boolean sameTier(Level level, BlockPos pos, SolarTier tier) { + return level.getBlockEntity(pos) instanceof SolarPanelBlockEntity neighbour && neighbour.tier() == tier; + } + + @Override + public void submit(SolarPanelRenderState state, PoseStack poseStack, SubmitNodeCollector collector, + CameraRenderState cameraState) { + Identifier texture = Identifier.fromNamespaceAndPath( + Nerospace.MODID, "textures/block/solar_panel_t" + state.tier + ".png"); + poseStack.pushPose(); + poseStack.translate(0.5F, PIVOT_Y, 0.5F); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.angle)); + collector.order(0).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), + (pose, consumer) -> drawDeck(state, pose, consumer)); + poseStack.popPose(); + } + + private static void drawDeck(SolarPanelRenderState state, PoseStack.Pose pose, VertexConsumer consumer) { + float no = state.connect[0] ? EDGE : INSET; // north (-Z) + float ee = state.connect[1] ? EDGE : INSET; // east (+X) + float so = state.connect[2] ? EDGE : INSET; // south (+Z) + float we = state.connect[3] ? EDGE : INSET; // west (-X) + int light = state.lightCoords; + + // Front face (normal +Y). Back face (normal -Y, reversed winding) so the steeply-tilted deck is + // visible from below too. The render type is no-cull, so both always draw. + vertex(consumer, pose, -we, -no, light, 1.0F); + vertex(consumer, pose, ee, -no, light, 1.0F); + vertex(consumer, pose, ee, so, light, 1.0F); + vertex(consumer, pose, -we, so, light, 1.0F); + + vertex(consumer, pose, -we, so, light, -1.0F); + vertex(consumer, pose, ee, so, light, -1.0F); + vertex(consumer, pose, ee, -no, light, -1.0F); + vertex(consumer, pose, -we, -no, light, -1.0F); + } + + /** + * One deck vertex. {@code x}/{@code z} are pivot-local half-offsets (the block square is + * [-0.5,0.5]); UVs are derived from them so each panel shows the full sprite. {@code ny} is the + * face normal's Y sign. + */ + private static void vertex(VertexConsumer consumer, PoseStack.Pose pose, float x, float z, int light, + float ny) { + consumer.addVertex(pose, x, 0.0F, z) + .setColor(255, 255, 255, 255) + .setUv(x + 0.5F, z + 0.5F) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(light) + .setNormal(pose, 0.0F, ny, 0.0F); + } +} 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..9e2db4f 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,9 @@ protected void generate() { dropSelf(ModBlocks.RAW_NEROSIUM_BLOCK.get()); dropSelf(ModBlocks.NEROSIUM_GRINDER.get()); + // Solar panel (SOLAR_PANEL_DESIGN). + dropSelf(ModBlocks.SOLAR_PANEL_T1.get()); + 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..c2b15af 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,7 @@ protected void addTags(HolderLookup.Provider provider) { ModBlocks.UNIVERSAL_PIPE.get(), ModBlocks.COMBUSTION_GENERATOR.get(), ModBlocks.PASSIVE_GENERATOR.get(), + ModBlocks.SOLAR_PANEL_T1.get(), ModBlocks.BATTERY.get(), ModBlocks.FLUID_TANK.get(), ModBlocks.GAS_TANK.get(), @@ -84,6 +85,7 @@ protected void addTags(HolderLookup.Provider provider) { ModBlocks.UNIVERSAL_PIPE.get(), ModBlocks.COMBUSTION_GENERATOR.get(), ModBlocks.PASSIVE_GENERATOR.get(), + ModBlocks.SOLAR_PANEL_T1.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 d723439..ced8ac8 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,8 @@ 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(), "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 65dd0c0..08dec14 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,25 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat blockModels.blockStateOutput.accept( BlockModelGenerators.createSimpleBlock(pad, BlockModelGenerators.plainVariant(padModel))); + // Solar Panel (T1): a flat 2px housing under a 4px deck slab (the moving, tilting surface is + // drawn by the block-entity renderer above this). Single texture via the ALL slot. + Block solar = ModBlocks.SOLAR_PANEL_T1.get(); + var solarTexture = TextureMapping.getBlockTexture(solar); + TextureMapping solarMapping = new TextureMapping() + .put(TextureSlot.ALL, solarTexture).put(TextureSlot.PARTICLE, solarTexture); + ExtendedModelTemplate solarTemplate = ExtendedModelTemplateBuilder.builder() + .requiredTextureSlot(TextureSlot.ALL) + .requiredTextureSlot(TextureSlot.PARTICLE) + .element(e -> e.from(0, 0, 0).to(16, 2, 16) + .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) + .element(e -> e.from(1, 2, 1).to(15, 4, 15) + .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) + .build(); + Identifier solarModel = solarTemplate.create( + ModelLocationUtils.getModelLocation(solar), solarMapping, blockModels.modelOutput); + blockModels.blockStateOutput.accept( + BlockModelGenerators.createSimpleBlock(solar, BlockModelGenerators.plainVariant(solarModel))); + // Launch Gantry — shaped tower in registerShapedMachines (art overhaul §3). // Power grid — connection-aware translucent pipe (multipart: core + one arm per connected 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..ae73c1c 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,20 @@ 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); + // --- 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..3a273a3 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,11 @@ 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())); + // 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..322aca9 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,22 @@ 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()); + // --- 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..f09b31b 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,7 @@ 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.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..aace97a 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,8 @@ 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); // 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..9c6ff94 --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java @@ -0,0 +1,125 @@ +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 panels treated as ONE machine: total storage is the sum of every + * member's buffer and total generation is the sum of every member's (sky-/weather-/dimension-scaled) + * output. The pooled energy is kept balanced evenly across the members' buffers, so a pipe pulling + * from ANY panel's output face effectively drains the whole array (every face is an output port). + * + *

Built by flood-fill from a seed panel, exactly like {@link za.co.neroland.nerospace.pipe.PipeNetwork}: + * membership is rebuilt lazily so placing or breaking a panel (merging/splitting arrays) needs no + * explicit hooks. Only neighbours of the SAME {@link SolarTier} are adopted — different tiers stay + * separate arrays.

+ */ +public final class SolarArray { + + private static final int MAX_MEMBERS = 4096; + + private final SolarTier tier; + private final List members; + private final LongOpenHashSet memberSet; + private boolean valid = true; + private long lastTick = -1L; + + private SolarArray(SolarTier tier, List members, LongOpenHashSet memberSet) { + this.tier = tier; + this.members = members; + this.memberSet = memberSet; + } + + public boolean isValid() { + return this.valid; + } + + public SolarTier tier() { + return this.tier; + } + + public int size() { + return this.members.size(); + } + + /** Flood-fill the connected same-tier panels from {@code seed}, build the array, adopt every member. */ + public static SolarArray getOrBuild(ServerLevel level, BlockPos seed, SolarTier tier) { + List members = new ArrayList<>(); + LongOpenHashSet seen = new LongOpenHashSet(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(seed); + seen.add(seed.asLong()); + + while (!queue.isEmpty() && members.size() < MAX_MEMBERS) { + BlockPos pos = queue.poll(); + if (!(level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel) || panel.tier() != tier) { + continue; + } + members.add(pos); + 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); + } + } + } + + LongOpenHashSet memberSet = new LongOpenHashSet(members.size()); + for (BlockPos pos : members) { + memberSet.add(pos.asLong()); + } + SolarArray array = new SolarArray(tier, members, memberSet); + for (BlockPos pos : members) { + if (level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel) { + panel.adopt(array); + } + } + return array; + } + + /** Generate this tick's pooled energy and re-balance the buffers. Runs at most once per game tick. */ + public void tick(ServerLevel level) { + long gameTime = level.getGameTime(); + if (gameTime == this.lastTick) { + return; + } + this.lastTick = gameTime; + + List panels = new ArrayList<>(this.members.size()); + for (BlockPos pos : this.members) { + if (level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel && panel.tier() == this.tier) { + panels.add(panel); + } else { + this.valid = false; // a member vanished/changed — members rebuild next tick + return; + } + } + if (panels.isEmpty()) { + this.valid = false; + return; + } + + // Each panel contributes its own daylight-scaled output (a shaded panel adds less); the sum is + // the array's generation. Add into the per-panel buffers, then balance them into one pool. + long total = 0L; + for (SolarPanelBlockEntity panel : panels) { + panel.generate(panel.generationThisTick(level)); + total += panel.energy().getAmountAsInt(); + } + int n = panels.size(); + int base = (int) (total / n); + int remainder = (int) (total % n); + for (int i = 0; i < n; i++) { + panels.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..cdb04bd --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlock.java @@ -0,0 +1,104 @@ +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.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.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * A solar panel: a low, sun-tracking power generator that pools with adjacent same-tier panels into a + * {@link SolarArray}. The block itself is a flat slab (collision + base model); the tilting, + * sun-following, night-folding panel surface is drawn by the block-entity renderer. 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)); + + /** 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); + + private final SolarTier tier; + + public SolarPanelBlock(SolarTier tier, Properties properties) { + super(properties); + this.tier = tier; + } + + public SolarTier tier() { + return this.tier; + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @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)); + } + + @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..97abb9e --- /dev/null +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java @@ -0,0 +1,173 @@ +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 physical solar panel. It carries its own FE buffer and, every tick, contributes daylight-scaled + * energy to its {@link SolarArray} — the connected run of same-tier panels that behaves as a single + * pooled machine. The buffer is extract-only on every face (it is a generator, not a sink), which makes + * each side an output port a pipe or machine can pull from. + * + *

Generation scales with the sun's height (peaks at noon, zero at night), requires a clear view of + * the sky, is cut by rain/thunder, and is doubled in the mod's space/airless dimensions (which have a + * permanent sun). The night-time zero falls straight out of the daylight curve, matching the renderer + * folding the panel flat.

+ */ +public class SolarPanelBlockEntity extends BlockEntity { + + private final SolarTier tier; + private final SolarEnergy energy; + + /** Transient: the array this panel belongs to, lazily (re)built and shared with all members. */ + @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()); + } + + public SolarTier tier() { + return this.tier; + } + + /** Exposed to {@code Capabilities.Energy.BLOCK} (every side) so pipes/machines pull the pooled power. */ + public EnergyHandler getEnergyHandler() { + return this.energy; + } + + SolarEnergy energy() { + return this.energy; + } + + /** Number of panels 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 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() { + int cap = this.energy.getCapacityAsInt(); + int stored = this.energy.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)) { + return; + } + 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 panel adds this tick = 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 + } + long tod = level.getOverworldClockTime() % 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")); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.deserialize(input.childOrEmpty("Energy")); + } + + /** 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 0000000000000000000000000000000000000000..9973736ecc1e7ef0a5b2e0dfde7a04781acf78ab GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`n><|{Ln`JZ&)J@P?SH22XNgT- zlO#MC4QzYV{KWoyUg~6^AHgQu&X%Q~loCIE;eRQ~_~ literal 0 HcmV?d00001 diff --git a/tools/gen_bbmodels.py b/tools/gen_bbmodels.py index da29bad..dbb5dde 100644 --- a/tools/gen_bbmodels.py +++ b/tools/gen_bbmodels.py @@ -44,7 +44,8 @@ "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"] 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 2eb5245..789221c 100644 --- a/tools/gen_textures.py +++ b/tools/gen_textures.py @@ -2809,7 +2809,42 @@ def gen_trash_can(): save(img, os.path.join(BLOCK_DIR, "trash_can.png")) +def gen_solar_panel(name, accent): + """A steel-framed photovoltaic panel: deep blue-teal cells in a silver grid, accent tier corners. + Greenxertz/steel palette (green steel housing); the tilting deck is drawn by the renderer.""" + img = new_img() + px = img.load() + cell_d = (18, 40, 58, 255) + cell = (30, 70, 98, 255) + cell_hi = (64, 126, 158, 255) + for y in range(S): + for x in range(S): + px[x, y] = G_STEEL + bevel(img, G_STEEL_L, G_STEEL_D) + for i in range(S): + px[1, i] = G_STEEL_D + px[i, 1] = G_STEEL_D + px[S - 2, i] = G_STEEL_D + px[i, S - 2] = G_STEEL_D + # Photovoltaic cells with a silver grid wire every 4px. + for y in range(2, 14): + for x in range(2, 14): + if (x - 2) % 4 == 3 or (y - 2) % 4 == 3: + px[x, y] = G_STEEL_L + elif (x - 2) % 4 == 0 and (y - 2) % 4 == 0: + px[x, y] = cell_hi + else: + px[x, y] = cell if (x + y) % 2 == 0 else cell_d + # Tier accent corners + a centre glow node. + for (gx, gy) in ((2, 2), (13, 2), (2, 13), (13, 13)): + px[gx, gy] = accent + px[8, 8] = G_GLOW + save(img, os.path.join(BLOCK_DIR, name + ".png")) + + if __name__ == "__main__": + # Solar panels (SOLAR_PANEL_DESIGN): T1 green accent (Greenxertz/steel family). + gen_solar_panel("solar_panel_t1", G_GREEN_L) # Trash Can (logistics void sink). gen_trash_can() # Quarry / Miner (MINER_DESIGN). From 5dc26b25ff4dcbfc07cf9a78bfd05aa2cb6c4eb2 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:50:01 +0800 Subject: [PATCH 2/6] Add solar_panel_t1_base and update renderer Introduce a distinct static base for the T1 solar panel and adapt rendering/data generation to match. Added a Blockbench model (.bbmodel) and a base texture (solar_panel_t1_base.png), updated the generated block model to use the _base texture and to include a post and torque tube element. Refactored SolarPanelRenderer to pivot on the pole top, compute openness/track separately, cap tilt, and draw the photovoltaic deck as a 1px-thick box (double-sided faces, proper UVs and lighting). Updated data-gen (ModModelProvider) to reference the base texture/model and modified tooling (gen_bbmodels.py, gen_textures.py) to generate the new base assets. --- .../block/solar_panel_t1_base.bbmodel | 136 ++++++++++++++++++ .../models/block/solar_panel_t1.json | 50 +++++-- .../nerospace/client/SolarPanelRenderer.java | 114 +++++++++------ .../nerospace/datagen/ModModelProvider.java | 18 ++- .../textures/block/solar_panel_t1_base.png | Bin 0 -> 218 bytes tools/gen_bbmodels.py | 2 +- tools/gen_textures.py | 24 ++++ 7 files changed, 284 insertions(+), 60 deletions(-) create mode 100644 art/blockbench/block/solar_panel_t1_base.bbmodel create mode 100644 src/main/resources/assets/nerospace/textures/block/solar_panel_t1_base.png 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/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json b/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json index 70cf711..9202496 100644 --- a/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t1.json @@ -28,7 +28,7 @@ ], "to": [ 16, - 2, + 3, 16 ] }, @@ -54,19 +54,51 @@ } }, "from": [ - 1, - 2, - 1 + 7, + 3, + 7 ], "to": [ - 15, - 4, - 15 + 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", - "particle": "nerospace:block/solar_panel_t1" + "all": "nerospace:block/solar_panel_t1_base", + "particle": "nerospace:block/solar_panel_t1_base" } } \ No newline at end of file diff --git a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java index 31eb711..3518d93 100644 --- a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java @@ -23,18 +23,21 @@ import za.co.neroland.nerospace.solar.SolarTier; /** - * Draws the moving solar-panel surface above its (static) housing model: a textured deck that tilts to - * track the sun across the day and folds flat at night. Every panel reads the SAME world time, so a - * whole array stays in lockstep, and connected same-tier neighbours extend their decks edge-to-edge so - * a row joins into one continuous, seamless surface. + * Draws the moving solar-panel deck above its static housing model: a 1px-thick photovoltaic slab + * hinged along its NORTH edge at the housing top. It tilts up to face the sky during the day and folds + * flat onto the housing at night — the hinge means it only ever rotates UP, so it never dips into the + * base or ground (no clipping). Every panel reads the SAME world time, so a whole array opens and folds + * in lockstep, and connected same-tier neighbours extend their decks edge-to-edge into one surface. */ public class SolarPanelRenderer implements BlockEntityRenderer { - /** Hinge height (just above the 4px housing) so the tilted deck clears the block top. */ - private static final float PIVOT_Y = 0.27F; - /** Tracking tilt is clamped so the array stays low-profile (a tilting field, not standing poles). */ - private static final float MAX_TILT = 35.0F; + /** Pivot height = centre of the cross-bar (the T-pole top); the deck tilts about it on the post. */ + private static final float POLE_TOP = 9.0F / 16.0F; + /** Deck thickness: a real 1px slab (not a zero-width plane). */ + private static final float THICK = 1.0F / 16.0F; + /** Max tracking tilt either side of flat; capped so the deck's dip stays clear of the torque tube. */ + private static final float MAX_TILT = 40.0F; /** Half-extent of a deck edge that has NO neighbour (leaves a thin frame gap between arrays). */ private static final float INSET = 0.46F; /** Half-extent of a deck edge that DOES touch a same-tier neighbour (meets at the block border). */ @@ -55,17 +58,20 @@ public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderStat } state.tier = panel.tier().tier; - boolean space = level.dimensionTypeRegistration().is(ModDimensionTypes.SPACE); - if (space) { - // Permanent sun in orbit / on an airless moon: stay open, facing up. - state.angle = 0.0F; + // Sun tracking: the deck pitches to follow the sun's east-west arc (flat at noon, tilted toward + // the low sun at dawn/dusk), scaled by openness so it eases back to flat and parks at night. + float openness; + float track; + if (level.dimensionTypeRegistration().is(ModDimensionTypes.SPACE)) { + openness = 1.0F; // permanent sun in orbit / on an airless moon + track = 0.0F; // no celestial cycle: rest facing straight up } else { long tod = level.getOverworldClockTime() % 24000L; // 0 sunrise, 6000 noon, 18000 midnight float sun = Mth.cos((float) ((tod - 6000L) / 24000.0 * 2.0 * Math.PI)); // +1 noon, -1 midnight - float openness = Mth.clamp((sun + 0.02F) / 0.25F, 0.0F, 1.0F); // 0 at night -> folds flat - float deg = (float) ((tod - 6000L) / 24000.0) * 360.0F; // 0 noon, -90 sunrise, +90 sunset - state.angle = openness * Mth.clamp(deg, -MAX_TILT, MAX_TILT); + openness = Mth.clamp((sun + 0.05F) / 0.3F, 0.0F, 1.0F); // eases to 0 at night + track = (float) ((tod - 6000L) / 24000.0) * 360.0F; // -90 sunrise .. 0 noon .. +90 sunset } + state.angle = openness * Mth.clamp(track, -MAX_TILT, MAX_TILT); SolarTier tier = panel.tier(); BlockPos pos = panel.getBlockPos(); @@ -82,48 +88,68 @@ private static boolean sameTier(Level level, BlockPos pos, SolarTier tier) { @Override public void submit(SolarPanelRenderState state, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState cameraState) { + float we = state.connect[3] ? EDGE : INSET; // west (-X) + float ee = state.connect[1] ? EDGE : INSET; // east (+X) + float no = state.connect[0] ? EDGE : INSET; // north (-Z) + float so = state.connect[2] ? EDGE : INSET; // south (+Z) Identifier texture = Identifier.fromNamespaceAndPath( Nerospace.MODID, "textures/block/solar_panel_t" + state.tier + ".png"); + poseStack.pushPose(); - poseStack.translate(0.5F, PIVOT_Y, 0.5F); + // Pivot on the cross-bar; rotating about Z pitches the deck east-west to follow the sun. + poseStack.translate(0.5F, POLE_TOP, 0.5F); poseStack.mulPose(Axis.ZP.rotationDegrees(state.angle)); - collector.order(0).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), - (pose, consumer) -> drawDeck(state, pose, consumer)); + float w = we; + float e = ee; + float n = no; + float s = so; + int light = state.lightCoords; + collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), + (pose, consumer) -> drawDeck(pose, consumer, w, e, n, s, light)); poseStack.popPose(); } - private static void drawDeck(SolarPanelRenderState state, PoseStack.Pose pose, VertexConsumer consumer) { - float no = state.connect[0] ? EDGE : INSET; // north (-Z) - float ee = state.connect[1] ? EDGE : INSET; // east (+X) - float so = state.connect[2] ? EDGE : INSET; // south (+Z) - float we = state.connect[3] ? EDGE : INSET; // west (-X) - int light = state.lightCoords; - - // Front face (normal +Y). Back face (normal -Y, reversed winding) so the steeply-tilted deck is - // visible from below too. The render type is no-cull, so both always draw. - vertex(consumer, pose, -we, -no, light, 1.0F); - vertex(consumer, pose, ee, -no, light, 1.0F); - vertex(consumer, pose, ee, so, light, 1.0F); - vertex(consumer, pose, -we, so, light, 1.0F); + /** A 1px-thick deck box centred on the pivot: x in [-w,e], z in [-n,s], y straddling 0. */ + private static void drawDeck(PoseStack.Pose pose, VertexConsumer consumer, + float w, float e, float n, float s, int light) { + float x0 = -w; + float x1 = e; + float y0 = -THICK / 2.0F; + float y1 = THICK / 2.0F; + float z0 = -n; + float z1 = s; + // Six faces, each double-sided so the slab shows from any angle through the cutout's culling. + face(consumer, pose, light, x0, y1, z0, x0, y1, z1, x1, y1, z1, x1, y1, z0, 0, 1, 0); // top + face(consumer, pose, light, x0, y0, z1, x0, y0, z0, x1, y0, z0, x1, y0, z1, 0, -1, 0); // bottom + face(consumer, pose, light, x0, y0, z0, x0, y1, z0, x1, y1, z0, x1, y0, z0, 0, 0, -1); // north + face(consumer, pose, light, x1, y0, z1, x1, y1, z1, x0, y1, z1, x0, y0, z1, 0, 0, 1); // south + face(consumer, pose, light, x0, y0, z1, x0, y1, z1, x0, y1, z0, x0, y0, z0, -1, 0, 0); // west + face(consumer, pose, light, x1, y0, z0, x1, y1, z0, x1, y1, z1, x1, y0, z1, 1, 0, 0); // east + } - vertex(consumer, pose, -we, so, light, -1.0F); - vertex(consumer, pose, ee, so, light, -1.0F); - vertex(consumer, pose, ee, -no, light, -1.0F); - vertex(consumer, pose, -we, -no, light, -1.0F); + /** Emit a quad both ways (front with the given normal, back reversed) so it's visible from both sides. */ + private static void face(VertexConsumer consumer, PoseStack.Pose pose, int light, + float ax, float ay, float az, float bx, float by, float bz, + float cx, float cy, float cz, float dx, float dy, float dz, + float nx, float ny, float nz) { + vertex(consumer, pose, ax, ay, az, light, nx, ny, nz); + vertex(consumer, pose, bx, by, bz, light, nx, ny, nz); + vertex(consumer, pose, cx, cy, cz, light, nx, ny, nz); + vertex(consumer, pose, dx, dy, dz, light, nx, ny, nz); + vertex(consumer, pose, dx, dy, dz, light, -nx, -ny, -nz); + vertex(consumer, pose, cx, cy, cz, light, -nx, -ny, -nz); + vertex(consumer, pose, bx, by, bz, light, -nx, -ny, -nz); + vertex(consumer, pose, ax, ay, az, light, -nx, -ny, -nz); } - /** - * One deck vertex. {@code x}/{@code z} are pivot-local half-offsets (the block square is - * [-0.5,0.5]); UVs are derived from them so each panel shows the full sprite. {@code ny} is the - * face normal's Y sign. - */ - private static void vertex(VertexConsumer consumer, PoseStack.Pose pose, float x, float z, int light, - float ny) { - consumer.addVertex(pose, x, 0.0F, z) + /** One vertex; UVs project the PV sprite onto the deck (u from x, v from z along the panel). */ + private static void vertex(VertexConsumer consumer, PoseStack.Pose pose, float x, float y, float z, + int light, float nx, float ny, float nz) { + consumer.addVertex(pose, x, y, z) .setColor(255, 255, 255, 255) .setUv(x + 0.5F, z + 0.5F) .setOverlay(OverlayTexture.NO_OVERLAY) .setLight(light) - .setNormal(pose, 0.0F, ny, 0.0F); + .setNormal(pose, nx, ny, nz); } } 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 08dec14..c795ef9 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java @@ -72,18 +72,24 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat blockModels.blockStateOutput.accept( BlockModelGenerators.createSimpleBlock(pad, BlockModelGenerators.plainVariant(padModel))); - // Solar Panel (T1): a flat 2px housing under a 4px deck slab (the moving, tilting surface is - // drawn by the block-entity renderer above this). Single texture via the ALL slot. + // Solar Panel (T1): the static steel mount — a 3px housing, a central post, and a north-south + // cross-bar on top (a "T-pole"), all on the `_base` sprite (distinct from the PV deck). The + // sun-tracking photovoltaic deck pivots on the cross-bar and is drawn by the block-entity renderer. Block solar = ModBlocks.SOLAR_PANEL_T1.get(); - var solarTexture = TextureMapping.getBlockTexture(solar); + var solarBaseTexture = TextureMapping.getBlockTexture(solar, "_base"); TextureMapping solarMapping = new TextureMapping() - .put(TextureSlot.ALL, solarTexture).put(TextureSlot.PARTICLE, solarTexture); + .put(TextureSlot.ALL, solarBaseTexture).put(TextureSlot.PARTICLE, solarBaseTexture); ExtendedModelTemplate solarTemplate = ExtendedModelTemplateBuilder.builder() .requiredTextureSlot(TextureSlot.ALL) .requiredTextureSlot(TextureSlot.PARTICLE) - .element(e -> e.from(0, 0, 0).to(16, 2, 16) + .element(e -> e.from(0, 0, 0).to(16, 3, 16) // housing .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) - .element(e -> e.from(1, 2, 1).to(15, 4, 15) + .element(e -> e.from(7, 3, 7).to(9, 7, 9) // vertical post + .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) + // Torque tube along the N-S pivot axis, topped at 8px — JUST BELOW the deck's swing (the + // deck pivots at 9px and dips < 1px over the tube's 2px width at the 40deg cap), so the + // tube never rises through the deck; it stays tucked underneath at every tracking angle. + .element(e -> e.from(7, 7, 4).to(9, 8, 12) .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) .build(); Identifier solarModel = solarTemplate.create( 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 0000000000000000000000000000000000000000..6a2415d8b162d9520ad2a6ed1d887abb22d5cd94 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3q4&NLn`JZ&)J@Pt)7jGft#6` z`S3*Msj;cZOgYeUM$yff zU!K2P Date: Sun, 14 Jun 2026 21:52:29 +0800 Subject: [PATCH 3/6] Add Solar Panel page and sidebar/roadmap links Add a new Solar-Panel wiki page detailing crafting, mechanics (sun-tracking generation, array pooling, tiers, buffer/output, weather and off-world bonuses), and implementation details. Also update Roadmap.md to list Solar Panels in upcoming features and add a link to the Solar Panel page in the sidebar for navigation. --- wiki/Roadmap.md | 2 ++ wiki/Solar-Panel.md | 59 +++++++++++++++++++++++++++++++++++++++++++++ wiki/_Sidebar.md | 1 + 3 files changed, 62 insertions(+) create mode 100644 wiki/Solar-Panel.md diff --git a/wiki/Roadmap.md b/wiki/Roadmap.md index e74ae8c..d7c31e1 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. Tier 1 (1×1) is in; + Tier 2 (2×2) and Tier 3 (3×3) are on the way. - **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..f7c7ba3 --- /dev/null +++ b/wiki/Solar-Panel.md @@ -0,0 +1,59 @@ +# Solar Panel + +A sun-tracking generator that pools with its neighbours into one big solar array. + +## Obtaining + +**Craft** (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 + +## 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. +- A single Tier 1 panel makes **20 FE/t** at noon into a **50,000 FE** buffer (both scale with the + `energyRateMultiplier` config). The buffer is **extract-only** — it never accepts a push. + +### Arrays + +- Place same-tier panels **next to each other** and they automatically merge into one **array**: the + array's storage is the **sum of every panel's buffer** and its generation is the **sum of every + panel'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* panel 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 panels + of the *same* tier pool together. + +### Appearance + +The photovoltaic deck sits on a **T-pole mount** above its steel base. By day it **tilts to track the +sun** across the sky; at night it **folds flat** and parks. Panels in a row line their decks up +edge-to-edge into one continuous, good-looking array — all of them moving in sync. + +## Tiers + +| Tier | Footprint | Status | +|---|---|---| +| **Tier 1** | 1×1 | Available | +| Tier 2 | 2×2 | Planned | +| Tier 3 | 3×3 | Planned | + +Higher tiers generate more per panel and **fold the previous tier's panel into their recipe** (a Tier 1 +panel is an ingredient for Tier 2, and so on). + +## Details + +- ID: `nerospace:solar_panel_t1` · Tool: pickaxe, iron tier · Drops: itself +- 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) From e8207e834ddea37e028510e5cde9af3b9f7b7420 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:19:11 +0800 Subject: [PATCH 4/6] Add Tier 2/3 solar panels and multiblock rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce Tier 2 and Tier 3 solar panels: adds block/item models, blockstates, textures, blockbench sources, loot tables, recipes and recipe advancements. Update datagen (models, tags, loot, recipes, language) to include new tiers and mark T2/T3 as mineable/need iron. Rendering: extend SolarPanelRenderState with footprint and anchor flags and update SolarPanelRenderer to support T1 tracking (east-west on a T-pole) and T2/T3 N×N multiblock lids (single-anchor rendering, hinged at housing top). Adjust block loot so multiblock filler cells drop nothing (anchor handles unit drop). Misc: minor updates to generated resources and docs. --- art/blockbench/block/solar_panel_t2.bbmodel | 136 +++++++++++++++++ .../block/solar_panel_t2_base.bbmodel | 136 +++++++++++++++++ art/blockbench/block/solar_panel_t3.bbmodel | 136 +++++++++++++++++ .../block/solar_panel_t3_base.bbmodel | 136 +++++++++++++++++ .../nerospace/blockstates/solar_panel_t2.json | 7 + .../nerospace/blockstates/solar_panel_t3.json | 7 + .../nerospace/items/solar_panel_t2.json | 6 + .../nerospace/items/solar_panel_t3.json | 6 + .../assets/nerospace/lang/en_us.json | 4 +- .../models/block/solar_panel_t2.json | 40 +++++ .../models/block/solar_panel_t3.json | 40 +++++ .../tags/block/mineable/pickaxe.json | 2 + .../minecraft/tags/block/needs_iron_tool.json | 2 + .../recipes/redstone/solar_panel_t2.json | 32 ++++ .../recipes/redstone/solar_panel_t3.json | 32 ++++ .../loot_table/blocks/solar_panel_t2.json | 4 + .../loot_table/blocks/solar_panel_t3.json | 4 + .../data/nerospace/recipe/solar_panel_t2.json | 16 ++ .../data/nerospace/recipe/solar_panel_t3.json | 16 ++ .../client/SolarPanelRenderState.java | 6 + .../nerospace/client/SolarPanelRenderer.java | 141 ++++++++++-------- .../datagen/ModBlockLootSubProvider.java | 5 +- .../datagen/ModBlockTagProvider.java | 4 + .../datagen/ModLanguageProvider.java | 4 +- .../nerospace/datagen/ModModelProvider.java | 18 +++ .../nerospace/datagen/ModRecipeProvider.java | 23 +++ .../nerospace/registry/ModBlockEntities.java | 3 +- .../nerospace/registry/ModBlocks.java | 24 +++ .../registry/ModCreativeModeTabs.java | 2 + .../neroland/nerospace/registry/ModItems.java | 4 + .../neroland/nerospace/solar/SolarArray.java | 89 +++++------ .../nerospace/solar/SolarPanelBlock.java | 111 +++++++++++++- .../solar/SolarPanelBlockEntity.java | 73 ++++++--- .../textures/block/solar_panel_t2.png | Bin 0 -> 232 bytes .../textures/block/solar_panel_t2_base.png | Bin 0 -> 239 bytes .../textures/block/solar_panel_t3.png | Bin 0 -> 235 bytes .../textures/block/solar_panel_t3_base.png | Bin 0 -> 240 bytes tools/gen_bbmodels.py | 4 +- tools/gen_textures.py | 18 ++- wiki/Roadmap.md | 4 +- wiki/Solar-Panel.md | 79 +++++++--- 41 files changed, 1214 insertions(+), 160 deletions(-) create mode 100644 art/blockbench/block/solar_panel_t2.bbmodel create mode 100644 art/blockbench/block/solar_panel_t2_base.bbmodel create mode 100644 art/blockbench/block/solar_panel_t3.bbmodel create mode 100644 art/blockbench/block/solar_panel_t3_base.bbmodel create mode 100644 src/generated/resources/assets/nerospace/blockstates/solar_panel_t2.json create mode 100644 src/generated/resources/assets/nerospace/blockstates/solar_panel_t3.json create mode 100644 src/generated/resources/assets/nerospace/items/solar_panel_t2.json create mode 100644 src/generated/resources/assets/nerospace/items/solar_panel_t3.json create mode 100644 src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json create mode 100644 src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json create mode 100644 src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t2.json create mode 100644 src/generated/resources/data/nerospace/advancement/recipes/redstone/solar_panel_t3.json create mode 100644 src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json create mode 100644 src/generated/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json create mode 100644 src/generated/resources/data/nerospace/recipe/solar_panel_t2.json create mode 100644 src/generated/resources/data/nerospace/recipe/solar_panel_t3.json create mode 100644 src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png create mode 100644 src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png create mode 100644 src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png create mode 100644 src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png 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_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_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 0d92e0b..791225e 100644 --- a/src/generated/resources/assets/nerospace/lang/en_us.json +++ b/src/generated/resources/assets/nerospace/lang/en_us.json @@ -52,7 +52,9 @@ "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": "Solar Panel", + "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_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 52bd34c..9018ee7 100644 --- a/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/src/generated/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -24,6 +24,8 @@ "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 b8f4f00..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 @@ -21,6 +21,8 @@ "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_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_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_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/client/SolarPanelRenderState.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java index 1f6e8fd..2348736 100644 --- a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java @@ -17,4 +17,10 @@ public class SolarPanelRenderState extends BlockEntityRenderState { /** 1-based tier (selects the surface texture). */ public int tier = 1; + + /** Footprint edge length (1 = T1 pole tracker, >1 = N×N multiblock lid). */ + 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 index 3518d93..e427607 100644 --- a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java @@ -19,28 +19,32 @@ 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; import za.co.neroland.nerospace.solar.SolarTier; /** - * Draws the moving solar-panel deck above its static housing model: a 1px-thick photovoltaic slab - * hinged along its NORTH edge at the housing top. It tilts up to face the sky during the day and folds - * flat onto the housing at night — the hinge means it only ever rotates UP, so it never dips into the - * base or ground (no clipping). Every panel reads the SAME world time, so a whole array opens and folds - * in lockstep, and connected same-tier neighbours extend their decks edge-to-edge into one surface. + * Draws the moving solar-panel deck above its static housing model. Tier 1 is a 1×1 deck that pivots on + * its T-pole and pitches east-west to track the sun. Tier 2/3 are N×N multiblocks: ONLY the anchor cell + * renders, drawing one big deck hinged along the footprint's north edge that tilts up to face the sky by + * day and folds flat onto the housings at night (the hinge means it never dips into the base — no + * clipping at any size). All panels read the same world time, so an array moves in lockstep. */ public class SolarPanelRenderer implements BlockEntityRenderer { - /** Pivot height = centre of the cross-bar (the T-pole top); the deck tilts about it on the post. */ + /** T1 pivot height = the cross-bar top (the T-pole). */ private static final float POLE_TOP = 9.0F / 16.0F; - /** Deck thickness: a real 1px slab (not a zero-width plane). */ + /** Multiblock lid hinge height = the housing top (3px). */ + 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 tracking tilt either side of flat; capped so the deck's dip stays clear of the torque tube. */ + /** T1 max east-west tracking tilt; capped so the deck clears the torque tube. */ private static final float MAX_TILT = 40.0F; - /** Half-extent of a deck edge that has NO neighbour (leaves a thin frame gap between arrays). */ + /** Multiblock open (daytime) tilt; folds to 0 (flat on the housings) at night. */ + private static final float OPEN_TILT = 40.0F; + /** Half-extent of a T1 deck edge with no neighbour (thin frame gap) / touching a neighbour. */ private static final float INSET = 0.46F; - /** Half-extent of a deck edge that DOES touch a same-tier neighbour (meets at the block border). */ private static final float EDGE = 0.5F; @Override @@ -57,21 +61,26 @@ public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderStat return; } state.tier = panel.tier().tier; + state.footprint = panel.tier().footprint; + state.anchor = panel.getBlockState().getValue(SolarPanelBlock.ANCHOR); + if (!state.anchor) { + return; // filler cells of a multiblock render nothing — the anchor draws the whole deck + } - // Sun tracking: the deck pitches to follow the sun's east-west arc (flat at noon, tilted toward - // the low sun at dawn/dusk), scaled by openness so it eases back to flat and parks at night. float openness; float track; if (level.dimensionTypeRegistration().is(ModDimensionTypes.SPACE)) { openness = 1.0F; // permanent sun in orbit / on an airless moon - track = 0.0F; // no celestial cycle: rest facing straight up + track = 0.0F; } else { long tod = level.getOverworldClockTime() % 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 track = (float) ((tod - 6000L) / 24000.0) * 360.0F; // -90 sunrise .. 0 noon .. +90 sunset } - state.angle = openness * Mth.clamp(track, -MAX_TILT, MAX_TILT); + state.angle = state.footprint > 1 + ? openness * OPEN_TILT // multiblock lid: open / fold + : openness * Mth.clamp(track, -MAX_TILT, MAX_TILT); // T1: east-west sun tracking SolarTier tier = panel.tier(); BlockPos pos = panel.getBlockPos(); @@ -88,66 +97,80 @@ private static boolean sameTier(Level level, BlockPos pos, SolarTier tier) { @Override public void submit(SolarPanelRenderState state, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState cameraState) { - float we = state.connect[3] ? EDGE : INSET; // west (-X) - float ee = state.connect[1] ? EDGE : INSET; // east (+X) - float no = state.connect[0] ? EDGE : INSET; // north (-Z) - float so = state.connect[2] ? EDGE : INSET; // south (+Z) + if (!state.anchor) { + return; + } Identifier texture = Identifier.fromNamespaceAndPath( Nerospace.MODID, "textures/block/solar_panel_t" + state.tier + ".png"); + int light = state.lightCoords; + if (state.footprint > 1) { + // Multiblock: one deck hinged at the footprint's north edge, lid-lifting about X. + int n = state.footprint; + poseStack.pushPose(); + poseStack.translate(0.0F, HOUSING_TOP, 0.0F); + poseStack.mulPose(Axis.XP.rotationDegrees(-state.angle)); + collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), + (pose, consumer) -> box(consumer, pose, light, + 0.0F, 0.0F, 0.0F, n, THICK, n, 0.0F, 0.0F, 1.0F, 1.0F)); + poseStack.popPose(); + return; + } + + // Tier 1: a centred deck on the T-pole, pitching east-west to follow the sun. + float we = state.connect[3] ? EDGE : INSET; + float ee = state.connect[1] ? EDGE : INSET; + float no = state.connect[0] ? EDGE : INSET; + float so = state.connect[2] ? EDGE : INSET; poseStack.pushPose(); - // Pivot on the cross-bar; rotating about Z pitches the deck east-west to follow the sun. poseStack.translate(0.5F, POLE_TOP, 0.5F); poseStack.mulPose(Axis.ZP.rotationDegrees(state.angle)); - float w = we; - float e = ee; - float n = no; - float s = so; - int light = state.lightCoords; collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), - (pose, consumer) -> drawDeck(pose, consumer, w, e, n, s, light)); + (pose, consumer) -> box(consumer, pose, light, + -we, -THICK / 2.0F, -no, ee, THICK / 2.0F, so, + -we + 0.5F, -no + 0.5F, ee + 0.5F, so + 0.5F)); poseStack.popPose(); } - /** A 1px-thick deck box centred on the pivot: x in [-w,e], z in [-n,s], y straddling 0. */ - private static void drawDeck(PoseStack.Pose pose, VertexConsumer consumer, - float w, float e, float n, float s, int light) { - float x0 = -w; - float x1 = e; - float y0 = -THICK / 2.0F; - float y1 = THICK / 2.0F; - float z0 = -n; - float z1 = s; - // Six faces, each double-sided so the slab shows from any angle through the cutout's culling. - face(consumer, pose, light, x0, y1, z0, x0, y1, z1, x1, y1, z1, x1, y1, z0, 0, 1, 0); // top - face(consumer, pose, light, x0, y0, z1, x0, y0, z0, x1, y0, z0, x1, y0, z1, 0, -1, 0); // bottom - face(consumer, pose, light, x0, y0, z0, x0, y1, z0, x1, y1, z0, x1, y0, z0, 0, 0, -1); // north - face(consumer, pose, light, x1, y0, z1, x1, y1, z1, x0, y1, z1, x0, y0, z1, 0, 0, 1); // south - face(consumer, pose, light, x0, y0, z1, x0, y1, z1, x0, y1, z0, x0, y0, z0, -1, 0, 0); // west - face(consumer, pose, light, x1, y0, z0, x1, y1, z0, x1, y1, z1, x1, y0, z1, 1, 0, 0); // east + /** + * 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's visible from both sides. */ - private static void face(VertexConsumer consumer, PoseStack.Pose pose, int light, - float ax, float ay, float az, float bx, float by, float bz, - float cx, float cy, float cz, float dx, float dy, float dz, - float nx, float ny, float nz) { - vertex(consumer, pose, ax, ay, az, light, nx, ny, nz); - vertex(consumer, pose, bx, by, bz, light, nx, ny, nz); - vertex(consumer, pose, cx, cy, cz, light, nx, ny, nz); - vertex(consumer, pose, dx, dy, dz, light, nx, ny, nz); - vertex(consumer, pose, dx, dy, dz, light, -nx, -ny, -nz); - vertex(consumer, pose, cx, cy, cz, light, -nx, -ny, -nz); - vertex(consumer, pose, bx, by, bz, light, -nx, -ny, -nz); - vertex(consumer, pose, ax, ay, az, light, -nx, -ny, -nz); + /** 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); } - /** One vertex; UVs project the PV sprite onto the deck (u from x, v from z along the panel). */ - private static void vertex(VertexConsumer consumer, PoseStack.Pose pose, float x, float y, float z, - int light, float nx, float ny, float nz) { - consumer.addVertex(pose, x, y, z) + 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(x + 0.5F, z + 0.5F) + .setUv(u, v) .setOverlay(OverlayTexture.NO_OVERLAY) .setLight(light) .setNormal(pose, nx, ny, nz); 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 9e2db4f..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,8 +26,11 @@ protected void generate() { dropSelf(ModBlocks.RAW_NEROSIUM_BLOCK.get()); dropSelf(ModBlocks.NEROSIUM_GRINDER.get()); - // Solar panel (SOLAR_PANEL_DESIGN). + // 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())); 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 c2b15af..56dd8cd 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModBlockTagProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModBlockTagProvider.java @@ -50,6 +50,8 @@ protected void addTags(HolderLookup.Provider provider) { 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(), @@ -86,6 +88,8 @@ protected void addTags(HolderLookup.Provider provider) { 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 1d4be7c..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,7 +56,9 @@ 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(), "Solar Panel"); + 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"); 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 3c0afa4..a939f58 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java @@ -97,6 +97,24 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat blockModels.blockStateOutput.accept( BlockModelGenerators.createSimpleBlock(solar, BlockModelGenerators.plainVariant(solarModel))); + // Tier 2/3 Solar Panels: each cell is just a flat 3px housing on its own `_base` sprite; the big + // N×N tilting deck is 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 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 ae73c1c..9058f1f 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModRecipeProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModRecipeProvider.java @@ -60,6 +60,29 @@ protected void buildRecipes() { .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 3a273a3..4238e0c 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -122,7 +122,8 @@ public final class ModBlockEntities { 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())); + 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 = 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 322aca9..5a054d2 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -405,6 +405,30 @@ public final class ModBlocks { .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/ModCreativeModeTabs.java b/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java index f09b31b..87bdc86 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModCreativeModeTabs.java @@ -72,6 +72,8 @@ public final class ModCreativeModeTabs { 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 aace97a..d598244 100644 --- a/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -349,6 +349,10 @@ public final class ModItems { 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 index 9c6ff94..767b7b0 100644 --- a/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java @@ -11,30 +11,31 @@ import net.minecraft.server.level.ServerLevel; /** - * A connected run of same-tier solar panels treated as ONE machine: total storage is the sum of every - * member's buffer and total generation is the sum of every member's (sky-/weather-/dimension-scaled) - * output. The pooled energy is kept balanced evenly across the members' buffers, so a pipe pulling - * from ANY panel's output face effectively drains the whole array (every face is an output port). + * 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 from a seed panel, exactly like {@link za.co.neroland.nerospace.pipe.PipeNetwork}: - * membership is rebuilt lazily so placing or breaking a panel (merging/splitting arrays) needs no - * explicit hooks. Only neighbours of the SAME {@link SolarTier} are adopted — different tiers stay - * separate arrays.

+ *

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_MEMBERS = 4096; + private static final int MAX_CELLS = 16_384; private final SolarTier tier; - private final List members; - private final LongOpenHashSet memberSet; + /** Member anchor positions (one per unit). */ + private final List anchors; + private final LongOpenHashSet anchorSet; private boolean valid = true; private long lastTick = -1L; - private SolarArray(SolarTier tier, List members, LongOpenHashSet memberSet) { + private SolarArray(SolarTier tier, List anchors, LongOpenHashSet anchorSet) { this.tier = tier; - this.members = members; - this.memberSet = memberSet; + this.anchors = anchors; + this.anchorSet = anchorSet; } public boolean isValid() { @@ -45,24 +46,31 @@ public SolarTier tier() { return this.tier; } + /** Number of pooled units (multiblocks) in the array. */ public int size() { - return this.members.size(); + return this.anchors.size(); } - /** Flood-fill the connected same-tier panels from {@code seed}, build the array, adopt every member. */ + /** 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 members = new ArrayList<>(); + List anchors = new ArrayList<>(); + LongOpenHashSet anchorSet = new LongOpenHashSet(); LongOpenHashSet seen = new LongOpenHashSet(); ArrayDeque queue = new ArrayDeque<>(); queue.add(seed); seen.add(seed.asLong()); - while (!queue.isEmpty() && members.size() < MAX_MEMBERS) { + int visited = 0; + while (!queue.isEmpty() && visited < MAX_CELLS) { BlockPos pos = queue.poll(); - if (!(level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel) || panel.tier() != tier) { + if (!(level.getBlockEntity(pos) instanceof SolarPanelBlockEntity cell) || cell.tier() != tier) { continue; } - members.add(pos); + 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()) @@ -73,20 +81,16 @@ public static SolarArray getOrBuild(ServerLevel level, BlockPos seed, SolarTier } } - LongOpenHashSet memberSet = new LongOpenHashSet(members.size()); - for (BlockPos pos : members) { - memberSet.add(pos.asLong()); - } - SolarArray array = new SolarArray(tier, members, memberSet); - for (BlockPos pos : members) { - if (level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel) { - panel.adopt(array); + SolarArray array = new SolarArray(tier, anchors, anchorSet); + 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 buffers. Runs at most once per game tick. */ + /** 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) { @@ -94,32 +98,33 @@ public void tick(ServerLevel level) { } this.lastTick = gameTime; - List panels = new ArrayList<>(this.members.size()); - for (BlockPos pos : this.members) { - if (level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel && panel.tier() == this.tier) { - panels.add(panel); + 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 member vanished/changed — members rebuild next tick + this.valid = false; // a unit anchor vanished/changed — members rebuild next tick return; } } - if (panels.isEmpty()) { + if (units.isEmpty()) { this.valid = false; return; } - // Each panel contributes its own daylight-scaled output (a shaded panel adds less); the sum is - // the array's generation. Add into the per-panel buffers, then balance them into one pool. + // 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 panel : panels) { - panel.generate(panel.generationThisTick(level)); - total += panel.energy().getAmountAsInt(); + for (SolarPanelBlockEntity unit : units) { + unit.generate(unit.generationThisTick(level)); + total += unit.energy().getAmountAsInt(); } - int n = panels.size(); + int n = units.size(); int base = (int) (total / n); int remainder = (int) (total % n); for (int i = 0; i < n; i++) { - panels.get(i).energy().setStored(base + (i < remainder ? 1 : 0)); + 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 index cdb04bd..864b497 100644 --- a/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlock.java +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlock.java @@ -9,6 +9,8 @@ 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; @@ -18,17 +20,25 @@ 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: a low, sun-tracking power generator that pools with adjacent same-tier panels into a - * {@link SolarArray}. The block itself is a flat slab (collision + base model); the tilting, - * sun-following, night-folding panel surface is drawn by the block-entity renderer. Energy is exposed - * on every side (output ports), so any face feeds a pipe or machine from the shared array pool. + * 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 { @@ -38,14 +48,24 @@ public class SolarPanelBlock extends BaseEntityBlock { 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() { @@ -57,6 +77,11 @@ protected MapCodec codec() { return CODEC; } + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ANCHOR); + } + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; @@ -81,6 +106,84 @@ public BlockEntityTicker getTicker(Level level, Block (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 diff --git a/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java index 97abb9e..b7bb86b 100644 --- a/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java @@ -18,22 +18,25 @@ import za.co.neroland.nerospace.registry.ModDimensionTypes; /** - * One physical solar panel. It carries its own FE buffer and, every tick, contributes daylight-scaled - * energy to its {@link SolarArray} — the connected run of same-tier panels that behaves as a single - * pooled machine. The buffer is extract-only on every face (it is a generator, not a sink), which makes - * each side an output port a pipe or machine can pull from. + * 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. * - *

Generation scales with the sun's height (peaks at noon, zero at night), requires a clear view of - * the sky, is cut by rain/thunder, and is doubled in the mod's space/airless dimensions (which have a - * permanent sun). The night-time zero falls straight out of the daylight curve, matching the renderer - * folding the panel flat.

+ *

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; - /** Transient: the array this panel belongs to, lazily (re)built and shared with all members. */ + /** 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; @@ -41,28 +44,59 @@ 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; } - /** Exposed to {@code Capabilities.Energy.BLOCK} (every side) so pipes/machines pull the pooled power. */ + /** 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() { - return this.energy; + SolarPanelBlockEntity anchor = anchorEntity(); + return anchor != null ? anchor.energy : this.energy; } SolarEnergy energy() { return this.energy; } - /** Number of panels in this panel's array (1 if not yet resolved). */ + /** 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 shares the one array instance. */ + /** Called by {@link SolarArray#getOrBuild} so every member anchor shares the one array instance. */ void adopt(SolarArray net) { this.array = net; } @@ -78,14 +112,15 @@ void generate(int amount) { } public int comparatorSignal() { - int cap = this.energy.getCapacityAsInt(); - int stored = this.energy.getAmountAsInt(); + 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)) { - return; + 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()) { @@ -95,7 +130,7 @@ public void tick(Level level, BlockPos pos, BlockState state) { net.tick(server); } - /** FE this panel adds this tick = peak output x the daylight/weather/dimension factor. */ + /** 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)); } @@ -137,12 +172,14 @@ private static float solarFactor(ServerLevel level, BlockPos pos) { 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. */ 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 0000000000000000000000000000000000000000..af32f315beafd2b3edae7350256182ee9cfa032c GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>pfi@Ln`JZ&)J@P?SH22XNgT- zlO#MC4QzYV{KWoyUg~6|F>^Hk|xZy>~hL`i?5AXCmvXptTjUgMGWrxLA dBjKG446EkOtzTfZZ7I;{44$rjF6*2UngEc0SY7}C literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8504f4ff0cc89d836cee6e383b765b9279183151 GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+dN$yLn`JZ&)J@Pt)7jGft#6` z`S3*M& zh9vQX7RSh7xpNFD2?+@^t{L-hX9Swa6I(j@g*01%_>;At9X%6&INY<6Z}W7F4v1wC mR$x<|{Ln`JZ&)J@P?SH22XNgT- zlO#MC4QzYV{KWoyUg~6b@-?>(IlMZ3kE!#stU`+cSS{&m_hwlEsBMI2FQ zle6S))9=qS0D+h&Q+DQAZnqu4nLc^>bP0l+XkKinLQF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7d6451494ae6dafeacca108699d7b0594765cbbc GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+dW+zLn`JZ&)J@Pt)7jGft#6` z`S3*M-e2>p={7^r zp@t;!gcirhV7YS)DG3P)Gp-r)Z)XG=$rD>T`GquFf%uuOs-{VYJ~T!K`bz-qU9gKm m&4I@u?DU}n3@1aMFfl}*o^9Cw+`ka$1qM%7KbLh*2~7ZLp Date: Tue, 16 Jun 2026 16:45:25 +0800 Subject: [PATCH 5/6] Solar panels: connectors and unified tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render and data changes for solar panels: each panel cell now renders the same sun-tracking deck (lockstep across multiblock fields) and draws small connector stubs toward any adjacent energy-capable neighbour (capability-based, skipping other solar panels). Added SolarPanelRenderState.connector flags, energyHookup detection and connector geometry in SolarPanelRenderer, and minor import/constant updates. Model/datagen changes consolidate the solar housing model for all tiers and update generated JSON to include the housing elements. Gallery command and harness were extended to build and capture a solar-array showcase (including a battery→pipe→panel hookup). SolarArray no longer stores an anchorSet (constructor/field simplified). --- .../models/block/solar_panel_t2.json | 64 +++++++++++ .../models/block/solar_panel_t3.json | 64 +++++++++++ .../client/GalleryCaptureHarness.java | 4 + .../client/SolarPanelRenderState.java | 3 + .../nerospace/client/SolarPanelRenderer.java | 106 ++++++++++++------ .../nerospace/command/NerospaceCommands.java | 59 +++++++++- .../nerospace/datagen/ModModelProvider.java | 74 ++++++------ .../neroland/nerospace/solar/SolarArray.java | 6 +- 8 files changed, 298 insertions(+), 82 deletions(-) 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 index 7035a31..4cb2686 100644 --- a/src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json @@ -31,6 +31,70 @@ 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": { 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 index 0b01a96..946c898 100644 --- a/src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json @@ -31,6 +31,70 @@ 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": { 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 index 2348736..6852695 100644 --- a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java @@ -15,6 +15,9 @@ public class SolarPanelRenderState extends BlockEntityRenderState { /** Same-tier neighbour present, indexed N=0, E=1, S=2, W=3 (drives edge-to-edge seam joining). */ public final boolean[] connect = new boolean[4]; + /** 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; diff --git a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java index e427607..d137fda 100644 --- a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java @@ -7,6 +7,7 @@ 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; @@ -16,6 +17,7 @@ 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; @@ -24,28 +26,31 @@ import za.co.neroland.nerospace.solar.SolarTier; /** - * Draws the moving solar-panel deck above its static housing model. Tier 1 is a 1×1 deck that pivots on - * its T-pole and pitches east-west to track the sun. Tier 2/3 are N×N multiblocks: ONLY the anchor cell - * renders, drawing one big deck hinged along the footprint's north edge that tilts up to face the sky by - * day and folds flat onto the housings at night (the hinge means it never dips into the base — no - * clipping at any size). All panels read the same world time, so an array moves in lockstep. + * Draws the moving solar-panel deck above its static housing model. EVERY tier uses the SAME animation: + * a deck that pivots on its T-pole and pitches east-west to track the sun. Tier 1 is a single 1×1 + * tracker; Tier 2/3 are N×N multiblocks where each cell renders its own identical tracker, and the + * edge-to-edge seam joining ({@link SolarPanelRenderState#connect}) merges them into one continuous, + * lockstep-moving tracking field (all cells read the same world time). On faces touching a power cable + * or any other energy block (this or another mod), a small 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 { - /** T1 pivot height = the cross-bar top (the T-pole). */ + /** Pivot height = the cross-bar top (the T-pole). */ private static final float POLE_TOP = 9.0F / 16.0F; - /** Multiblock lid hinge height = the housing top (3px). */ - 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; - /** T1 max east-west tracking tilt; capped so the deck clears the torque tube. */ + /** Max east-west tracking tilt; capped so the deck clears the torque tube. */ private static final float MAX_TILT = 40.0F; - /** Multiblock open (daytime) tilt; folds to 0 (flat on the housings) at night. */ - private static final float OPEN_TILT = 40.0F; - /** Half-extent of a T1 deck edge with no neighbour (thin frame gap) / touching a neighbour. */ + /** Half-extent of a deck edge with no neighbour (thin frame gap) / touching a neighbour. */ private static final float INSET = 0.46F; private static final float EDGE = 0.5F; + /** 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() { @@ -63,9 +68,7 @@ public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderStat state.tier = panel.tier().tier; state.footprint = panel.tier().footprint; state.anchor = panel.getBlockState().getValue(SolarPanelBlock.ANCHOR); - if (!state.anchor) { - return; // filler cells of a multiblock render nothing — the anchor draws the whole deck - } + // Every cell renders its own tracker (Tier 1 animation at every tier) — no anchor-only branch. float openness; float track; @@ -78,9 +81,8 @@ public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderStat openness = Mth.clamp((sun + 0.05F) / 0.3F, 0.0F, 1.0F); // eases to 0 at night track = (float) ((tod - 6000L) / 24000.0) * 360.0F; // -90 sunrise .. 0 noon .. +90 sunset } - state.angle = state.footprint > 1 - ? openness * OPEN_TILT // multiblock lid: open / fold - : openness * Mth.clamp(track, -MAX_TILT, MAX_TILT); // T1: east-west sun tracking + // East-west sun tracking for ALL tiers; cells move in lockstep on the same clock. + state.angle = openness * Mth.clamp(track, -MAX_TILT, MAX_TILT); SolarTier tier = panel.tier(); BlockPos pos = panel.getBlockPos(); @@ -88,36 +90,40 @@ public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderStat state.connect[1] = sameTier(level, pos.relative(Direction.EAST), tier); state.connect[2] = sameTier(level, pos.relative(Direction.SOUTH), tier); state.connect[3] = sameTier(level, pos.relative(Direction.WEST), tier); + + // 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); } private static boolean sameTier(Level level, BlockPos pos, SolarTier tier) { return level.getBlockEntity(pos) instanceof SolarPanelBlockEntity neighbour && neighbour.tier() == tier; } + /** + * 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) { - if (!state.anchor) { - return; - } + int light = state.lightCoords; Identifier texture = Identifier.fromNamespaceAndPath( Nerospace.MODID, "textures/block/solar_panel_t" + state.tier + ".png"); - int light = state.lightCoords; - - if (state.footprint > 1) { - // Multiblock: one deck hinged at the footprint's north edge, lid-lifting about X. - int n = state.footprint; - poseStack.pushPose(); - poseStack.translate(0.0F, HOUSING_TOP, 0.0F); - poseStack.mulPose(Axis.XP.rotationDegrees(-state.angle)); - collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), - (pose, consumer) -> box(consumer, pose, light, - 0.0F, 0.0F, 0.0F, n, THICK, n, 0.0F, 0.0F, 1.0F, 1.0F)); - poseStack.popPose(); - return; - } - // Tier 1: a centred deck on the T-pole, pitching east-west to follow the sun. + // A centred deck on the T-pole, pitching east-west to follow the sun. Every cell (T1 and each + // cell of a T2/T3 multiblock) draws this identically; the seam insets join neighbours edge-to-edge. float we = state.connect[3] ? EDGE : INSET; float ee = state.connect[1] ? EDGE : INSET; float no = state.connect[0] ? EDGE : INSET; @@ -130,6 +136,34 @@ public void submit(SolarPanelRenderState state, PoseStack poseStack, SubmitNodeC -we, -THICK / 2.0F, -no, ee, THICK / 2.0F, so, -we + 0.5F, -no + 0.5F, ee + 0.5F, so + 0.5F)); poseStack.popPose(); + + // Power connector stubs (drawn in block space, no deck rotation) on the `_base` housing sprite. + if (state.connector[0] || state.connector[1] || state.connector[2] || state.connector[3]) { + 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)); } /** 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..6756936 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. Behind it: a multi-unit field + * per tier — nine T1 panels (3x3), four T2 units (a 4x4 field) and two T3 units (a 6x3 field) — so + * the per-cell trackers seam-joining into one continuous, lockstep-tracking surface 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 drawn 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/ModModelProvider.java b/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java index a939f58..f1c72ba 100644 --- a/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java +++ b/src/main/java/za/co/neroland/nerospace/datagen/ModModelProvider.java @@ -72,47 +72,14 @@ 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 north-south - // cross-bar on top (a "T-pole"), all on the `_base` sprite (distinct from the PV deck). The - // sun-tracking photovoltaic deck pivots on the cross-bar and is drawn by the block-entity renderer. - Block solar = ModBlocks.SOLAR_PANEL_T1.get(); - var solarBaseTexture = TextureMapping.getBlockTexture(solar, "_base"); - TextureMapping solarMapping = new TextureMapping() - .put(TextureSlot.ALL, solarBaseTexture).put(TextureSlot.PARTICLE, solarBaseTexture); - ExtendedModelTemplate solarTemplate = 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))) - // Torque tube along the N-S pivot axis, topped at 8px — JUST BELOW the deck's swing (the - // deck pivots at 9px and dips < 1px over the tube's 2px width at the 40deg cap), so the - // tube never rises through the deck; it stays tucked underneath at every tracking angle. - .element(e -> e.from(7, 7, 4).to(9, 8, 12) - .allFaces((dir, face) -> face.texture(TextureSlot.ALL))) - .build(); - Identifier solarModel = solarTemplate.create( - ModelLocationUtils.getModelLocation(solar), solarMapping, blockModels.modelOutput); - blockModels.blockStateOutput.accept( - BlockModelGenerators.createSimpleBlock(solar, BlockModelGenerators.plainVariant(solarModel))); - - // Tier 2/3 Solar Panels: each cell is just a flat 3px housing on its own `_base` sprite; the big - // N×N tilting deck is 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))); + // Solar Panels (ALL tiers): the static steel mount — a 3px housing, a central post, and a + // north-south cross-bar on top (a "T-pole"), all on each block's own `_base` sprite (distinct + // from the PV deck). The sun-tracking photovoltaic deck pivots on the cross-bar and is drawn by + // the block-entity renderer. T2/T3 are N×N multiblocks where every cell carries this same mount, + // so each cell renders its own tracker and they seam-join into one field. + for (Block solar : new Block[] {ModBlocks.SOLAR_PANEL_T1.get(), + ModBlocks.SOLAR_PANEL_T2.get(), ModBlocks.SOLAR_PANEL_T3.get()}) { + registerSolarHousing(blockModels, solar); } // Launch Gantry — shaped tower in registerShapedMachines (art overhaul §3). @@ -484,6 +451,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/solar/SolarArray.java b/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java index 767b7b0..06d3729 100644 --- a/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarArray.java @@ -28,14 +28,12 @@ public final class SolarArray { private final SolarTier tier; /** Member anchor positions (one per unit). */ private final List anchors; - private final LongOpenHashSet anchorSet; private boolean valid = true; private long lastTick = -1L; - private SolarArray(SolarTier tier, List anchors, LongOpenHashSet anchorSet) { + private SolarArray(SolarTier tier, List anchors) { this.tier = tier; this.anchors = anchors; - this.anchorSet = anchorSet; } public boolean isValid() { @@ -81,7 +79,7 @@ public static SolarArray getOrBuild(ServerLevel level, BlockPos seed, SolarTier } } - SolarArray array = new SolarArray(tier, anchors, anchorSet); + SolarArray array = new SolarArray(tier, anchors); for (BlockPos anchor : anchors) { if (level.getBlockEntity(anchor) instanceof SolarPanelBlockEntity a) { a.adopt(array); From 6d13aafe553f0cb074c8485a19c8c73bc7d87ad8 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:49:21 +0800 Subject: [PATCH 6/6] Refactor solar panel rendering and textures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overhaul solar-panel rendering and assets: use the local dimension clock (getDefaultClockTime) in both renderer and block entity so panels fold to the sky the player actually sees. Replace per-cell seam-join geometry with a simpler scheme: T1 remains a 1×1 pitching deck, T2/T3 are drawn as a single N×N deck by the multiblock anchor (only the min-corner draws the deck and central mast). Connector stubs are now per-cell and always drawn from the base sprite; added mast/base geometry, tilt caps for larger footprints, and helper methods to submit multiblock decks. Data-gen/model changes produce flat 3px bases for multiblocks and keep the moving deck renderer-driven. Texture generator updated to a unified blue/green PV design with tier-coloured edge ring; updated PNG assets accordingly. Misc: cleaned up unused neighbor logic and improved comments. --- .../models/block/solar_panel_t2.json | 64 ------- .../models/block/solar_panel_t3.json | 64 ------- .../client/SolarPanelRenderState.java | 10 +- .../nerospace/client/SolarPanelRenderer.java | 161 ++++++++++++------ .../nerospace/command/NerospaceCommands.java | 12 +- .../nerospace/datagen/ModModelProvider.java | 29 +++- .../solar/SolarPanelBlockEntity.java | 4 +- .../textures/block/solar_panel_t1.png | Bin 235 -> 262 bytes .../textures/block/solar_panel_t1_base.png | Bin 218 -> 237 bytes .../textures/block/solar_panel_t2.png | Bin 232 -> 271 bytes .../textures/block/solar_panel_t2_base.png | Bin 239 -> 237 bytes .../textures/block/solar_panel_t3.png | Bin 235 -> 262 bytes .../textures/block/solar_panel_t3_base.png | Bin 240 -> 237 bytes tools/gen_textures.py | 89 +++++----- 14 files changed, 193 insertions(+), 240 deletions(-) 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 index 4cb2686..7035a31 100644 --- a/src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t2.json @@ -31,70 +31,6 @@ 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": { 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 index 946c898..0b01a96 100644 --- a/src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json +++ b/src/generated/resources/assets/nerospace/models/block/solar_panel_t3.json @@ -31,70 +31,6 @@ 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": { diff --git a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java index 6852695..7d8e79b 100644 --- a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java @@ -3,25 +3,21 @@ import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; /** - * Render state for a single solar panel: the blended tilt angle of its surface (sun-tracking by day, - * folded flat at night) plus which horizontal neighbours are same-tier panels, so an array of them - * reads as one continuous, seam-joined surface. + * 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; - /** Same-tier neighbour present, indexed N=0, E=1, S=2, W=3 (drives edge-to-edge seam joining). */ - public final boolean[] connect = new boolean[4]; - /** 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 pole tracker, >1 = N×N multiblock lid). */ + /** 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. */ diff --git a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java index d137fda..28a9ddc 100644 --- a/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java +++ b/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java @@ -23,29 +23,34 @@ import za.co.neroland.nerospace.registry.ModDimensionTypes; import za.co.neroland.nerospace.solar.SolarPanelBlock; import za.co.neroland.nerospace.solar.SolarPanelBlockEntity; -import za.co.neroland.nerospace.solar.SolarTier; /** * Draws the moving solar-panel deck above its static housing model. EVERY tier uses the SAME animation: - * a deck that pivots on its T-pole and pitches east-west to track the sun. Tier 1 is a single 1×1 - * tracker; Tier 2/3 are N×N multiblocks where each cell renders its own identical tracker, and the - * edge-to-edge seam joining ({@link SolarPanelRenderState#connect}) merges them into one continuous, - * lockstep-moving tracking field (all cells read the same world time). On faces touching a power cable - * or any other energy block (this or another mod), a small connector stub is drawn so the hookup butts - * up against the cable arm with no blank gap ({@link SolarPanelRenderState#connector}). + * 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; - /** Half-extent of a deck edge with no neighbour (thin frame gap) / touching a neighbour. */ - private static final float INSET = 0.46F; - private static final float EDGE = 0.5F; + /** 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; @@ -76,21 +81,20 @@ public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderStat openness = 1.0F; // permanent sun in orbit / on an airless moon track = 0.0F; } else { - long tod = level.getOverworldClockTime() % 24000L; // 0 sunrise, 6000 noon, 18000 midnight + // 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 + 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. + // 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); - SolarTier tier = panel.tier(); BlockPos pos = panel.getBlockPos(); - state.connect[0] = sameTier(level, pos.relative(Direction.NORTH), tier); - state.connect[1] = sameTier(level, pos.relative(Direction.EAST), tier); - state.connect[2] = sameTier(level, pos.relative(Direction.SOUTH), tier); - state.connect[3] = sameTier(level, pos.relative(Direction.WEST), tier); - // 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); @@ -99,10 +103,6 @@ public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderStat state.connector[3] = energyHookup(level, pos.relative(Direction.WEST), Direction.WEST); } - private static boolean sameTier(Level level, BlockPos pos, SolarTier tier) { - return level.getBlockEntity(pos) instanceof SolarPanelBlockEntity neighbour && neighbour.tier() == tier; - } - /** * 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 @@ -119,41 +119,106 @@ private static boolean energyHookup(Level level, BlockPos pos, Direction face) { 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"); - // A centred deck on the T-pole, pitching east-west to follow the sun. Every cell (T1 and each - // cell of a T2/T3 multiblock) draws this identically; the seam insets join neighbours edge-to-edge. - float we = state.connect[3] ? EDGE : INSET; - float ee = state.connect[1] ? EDGE : INSET; - float no = state.connect[0] ? EDGE : INSET; - float so = state.connect[2] ? EDGE : INSET; + 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, - -we, -THICK / 2.0F, -no, ee, THICK / 2.0F, so, - -we + 0.5F, -no + 0.5F, ee + 0.5F, so + 0.5F)); + -0.5F, -THICK / 2.0F, -0.5F, 0.5F, THICK / 2.0F, 0.5F, + 0.0F, 0.0F, 1.0F, 1.0F)); poseStack.popPose(); + } - // Power connector stubs (drawn in block space, no deck rotation) on the `_base` housing sprite. - if (state.connector[0] || state.connector[1] || state.connector[2] || state.connector[3]) { - 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); - } + /** + * 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); } } 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 6756936..d101a99 100644 --- a/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java +++ b/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java @@ -428,12 +428,12 @@ private static int clearGallery(CommandSourceStack source) { } /** - * Solar showcase (SW). Front row: one of each tier as a single unit. Behind it: a multi-unit field - * per tier — nine T1 panels (3x3), four T2 units (a 4x4 field) and two T3 units (a 6x3 field) — so - * the per-cell trackers seam-joining into one continuous, lockstep-tracking surface 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 drawn above. + * 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; 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 f1c72ba..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,14 +72,27 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat blockModels.blockStateOutput.accept( BlockModelGenerators.createSimpleBlock(pad, BlockModelGenerators.plainVariant(padModel))); - // Solar Panels (ALL tiers): the static steel mount — a 3px housing, a central post, and a - // north-south cross-bar on top (a "T-pole"), all on each block's own `_base` sprite (distinct - // from the PV deck). The sun-tracking photovoltaic deck pivots on the cross-bar and is drawn by - // the block-entity renderer. T2/T3 are N×N multiblocks where every cell carries this same mount, - // so each cell renders its own tracker and they seam-join into one field. - for (Block solar : new Block[] {ModBlocks.SOLAR_PANEL_T1.get(), - ModBlocks.SOLAR_PANEL_T2.get(), ModBlocks.SOLAR_PANEL_T3.get()}) { - registerSolarHousing(blockModels, solar); + // 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). diff --git a/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java index b7bb86b..82c1a18 100644 --- a/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java +++ b/src/main/java/za/co/neroland/nerospace/solar/SolarPanelBlockEntity.java @@ -150,7 +150,9 @@ private static float solarFactor(ServerLevel level, BlockPos pos) { if (!level.canSeeSky(above)) { return 0.0F; // roofed over — no sun reaches the panel } - long tod = level.getOverworldClockTime() % 24000L; // 0 sunrise, 6000 noon, 18000 midnight + // 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); } 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 index 9973736ecc1e7ef0a5b2e0dfde7a04781acf78ab..33fc25c6266680d4be66aeffe5dbf81452b51f8f 100644 GIT binary patch delta 234 zcmaFO*v2$LrT(m^i(^Q|oa8@=4?oOrWIAx-lf5dkc z_Uk;~e>tFOheL9_K=ZME3Tz;-v&D{0PF5ex2GPkNffCabk9?k3NJq}GiaYT1&8qK{ zH&{tW+Pdi|Sm)pU$s94acg;MjI0dyW6M?KY&byU7JU5)B!;==*IVl~xDY4OE)n>z_ dMxF1@j0}B#4F?o<>gg~5fv2mV%Q~loCIE+aU{L@7 delta 207 zcmZo;dd)aNrGAsAi(^Q|oa8y%bFcl+wEZlx$!n5?2cv;)kD8y@f6q&uY%>zfcIR*< z?)cu*`B1c5%sI?ch`HZa%5UH4WdbdX2FG+Jm-5&YN%6$X-Aw?29b2;a{Px{awDVz3 zm;{t?`Iy+idMra@15j>5g2J@^!~=_jbqdnWL?Q&9cqCUa&Y!>WV<%s;==I$i+|BB@ z*YkcgP?*9ekvQ{YYA}Zv*J1X&o3%+d9&cw$Vz_D~ypw@pp74D6(67Bq7=Xaj)z4*} HQ$iB}MOspE 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 index 6a2415d8b162d9520ad2a6ed1d887abb22d5cd94..1416158743de6d0da02ea84dfb00e648e719dcc3 100644 GIT binary patch delta 209 zcmV;?051R90qp^hB!9F?L_t(|oMT+D;nIHw3IGcO0|Ud7ITOgz{OZLc1|}G1%bR;d zYeZJd0%Q1yC^5Wx@rZ$khmYaQmrrXAx1L~9}kKFEXW)lJ|2dwk|ey&@2Z=G z+kh{hzA#MdnMaZfP`p5l^Qj&N6uST;49c4Fa0Va)0|Udfo;P{84dCJ9Vd$!xG(hQq s)T9i{KX4b2lA(~*vM?|}auvA%0O@j>^?1(d@~ 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 index af32f315beafd2b3edae7350256182ee9cfa032c..80f6b25159ff489f7070a6e5a00eef49efe3a02c 100644 GIT binary patch delta 243 zcmaFC*v~XUrT&_yi(^Q|oa8+VHa)awW^HI~Y;06K^ql|jZsTf+eK9wWE!e2v0TgR| z*ml{9ZD)(!4vYRi@#YDg+=mylx)zvzJ|rVCLjnly&AoZ}LFi>RbJG(S8X03_P8?}H zc<3;vGw);8sl5N!-g`3f>7Qh!$qITGZius#Pm6JqX9gOS!wdo-6F~yIq*4miistm{ z_{r)&C@Q=9+cRQr?;1H}u?cgxzx%_u=A2uYzpTE1@U0U-*5Q_SHf(Hf7WQgy4767i oe4Nu0p>Xx4#72i{zZWtvINj}N5Ps))m;nepUHx3vIVCg!05^DQApigX delta 204 zcmV;-05kuO0_XvdB!90-L_t(|oMW7~ZPx#3J@Xi{N|G2z0Wdv`CQcUrNz#c1m|!-~ zc~rr0?->)rD;6z=tdb-K9-B0r{5g**5C*^)PApvvJaQTgUmo3HU|?WixU^~xPX4(` zuP_aON%F{P5XFZ978s+Uz=DB+!GeKUK868{PO!k8*-&6XkVTK-1$=(S#wR)qR$T9* zEDWA(zc$pvfar9v>7F0Mxk<#O1EV}K1_n8ZIQ23Uxc~sFot=N6I<}<%00004B;*Lv$VnM)0CMsq1d!FTFfc%J6}bQasC}9hiFz>J00000 LNkvXXu0mjfW|LTH delta 211 zcmV;^04)FQ0q+5jB!9L^L_t(|oMW7~ZPtGV3IGcO0|Ud61EOHfMOS5gh5$T9!$;vWME)mnAS57w*fqS zJPciRljs`;a2MpQ^Fc{r_(0w|ABIUmTTop96Ceg4t7TzefaEH20RXDfnwcPEt9AeY N002ovPDHLkV1haAS4sc? 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 index ccbe99abcd257ba3f499fcfabc6c800b795a9519..2c2bd35bec8e3b7fb128954824b0ead6651b6a9f 100644 GIT binary patch delta 234 zcmaFO*v2$LrT(m^i(^Q|oa8^n2Orxrvo>@#Ha0q@J90IL^f58dKWxgjxn7DV?Z1h{ z51DlfJG`H27`)K&a13NNe{grEEl-?jZb5J}kF3OnqpdzM5;`_>46Ydb+xg(*{PPc` zBG3IVao~Q$)LZMwBlWvU0tB82{gS9C*#l;S=wy(glQY(3GXpq348`IhQi?dTAJ^2XJ%5%p$ eSU26_XJrr&YhCcz&G`}o5O})!xvXgB!99=L_t(|oMW7~ZPx#3J@Xi{N|G2z0Wdv`CQcUrNz#c1m|!-a z=o4VL_l$|*6^j-_R!I^Ak4+j*{)s*TgaI&ySi~m=9ytw$FOP08FfcGMTv{~;Cx6ki zAWQ>bl00%6MDbyO1;%J7uwYrzfEq~Xps%F}B@8fP zVb+9(0Sbd1EzcOZcmx>4B;*Lv$VnM)0CMsq1d!FTFfc%J6}bQa8cdoM$Euy800000 LNkvXXu0mjf6%AeN delta 212 zcmV;_04x9P0q_BkB!9O_L_t(|oMW7~ZPtGV3IGcO0|Ud61EQ!tjAvQBN4smdT*H046{TKvv7bzyQfr