RyanHub – file viewer
filename: common/src/main/java/rearth/oritech/client/ui/ReactorScreen.java
branch: 1.21
back to repo
package rearth.oritech.client.ui;

import io.wispforest.owo.ui.base.BaseOwoHandledScreen;
import io.wispforest.owo.ui.component.Components;
import io.wispforest.owo.ui.component.LabelComponent;
import io.wispforest.owo.ui.component.TextureComponent;
import io.wispforest.owo.ui.container.Containers;
import io.wispforest.owo.ui.container.FlowLayout;
import io.wispforest.owo.ui.core.*;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Pair;
import net.minecraft.util.math.BlockPos;
import org.jetbrains.annotations.NotNull;
import rearth.oritech.block.blocks.reactor.ReactorAbsorberBlock;
import rearth.oritech.block.blocks.reactor.ReactorHeatPipeBlock;
import rearth.oritech.block.blocks.reactor.ReactorHeatVentBlock;
import rearth.oritech.block.blocks.reactor.ReactorRodBlock;
import rearth.oritech.block.entity.reactor.ReactorAbsorberPortEntity;
import rearth.oritech.block.entity.reactor.ReactorControllerBlockEntity;
import rearth.oritech.block.entity.reactor.ReactorFuelPortEntity;
import rearth.oritech.client.ui.components.ReactorBlockRenderComponent;
import rearth.oritech.client.ui.components.ReactorPreviewContainer;
import rearth.oritech.init.BlockContent;
import rearth.oritech.util.ScreenProvider;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;

import static rearth.oritech.client.ui.BasicMachineScreen.ORITECH_PANEL;

public class ReactorScreen extends BaseOwoHandledScreen<FlowLayout, ReactorScreenHandler> {
    
    private ArrayList<Pair<Integer, ReactorBlockRenderComponent>> activeComponents;
    private HashSet<ReactorBlockRenderComponent> activeOverlays;
    private LabelComponent tooltipTitle;
    private FlowLayout tooltipContainer;
    private ReactorBlockRenderComponent selectedBlockOverlay;
    private TextureComponent energyIndicator;
    private LabelComponent productionLabel;
    private LabelComponent hottestLabel;
    private LabelComponent sumHeatLabel;
    private LabelComponent statusLabel;
    
    public ReactorScreen(ReactorScreenHandler handler, PlayerInventory inventory, Text title) {
        super(handler, inventory, title);
    }
    
    @Override
    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {
        return OwoUIAdapter.create(this, Containers::verticalFlow);
    }
    
    @Override
    protected void build(FlowLayout rootComponent) {
        rootComponent
          .surface(Surface.VANILLA_TRANSLUCENT)
          .horizontalAlignment(HorizontalAlignment.CENTER)
          .verticalAlignment(VerticalAlignment.CENTER);
        
        tooltipContainer = Containers.verticalFlow(Sizing.content(2), Sizing.content(2));
        tooltipContainer.surface(Surface.VANILLA_TRANSLUCENT);
        tooltipTitle = Components.label(Text.literal("My title!"));
        tooltipContainer.child(tooltipTitle.margins(Insets.of(6)));
        tooltipContainer.zIndex(3000);
        tooltipContainer.padding(Insets.of(3));
        tooltipTitle.zIndex(3001);
        
        var overlay = Containers.horizontalFlow(Sizing.fixed(329), Sizing.fixed(200));
        rootComponent.child(overlay.surface(ORITECH_PANEL));
        
        if (handler.reactorEntity.uiData != null) {
            addReactorComponentPreview(overlay);
            addReactorStats(overlay);
            addEnergyBar(overlay);
            addReactorStatus(overlay);
        }
        
        addTitle(overlay);
        rootComponent.child(tooltipContainer.positioning(Positioning.absolute(0, 0)));
    }
    
    private void addReactorStats(FlowLayout overlay) {
        var container = Containers.verticalFlow(Sizing.fixed(131), Sizing.content(0));
        
        productionLabel = Components.label(Text.translatable("RF Production: %s RF/t", "50").formatted(Formatting.WHITE));
        sumHeatLabel = Components.label(Text.translatable("Heat Production: %s RF/t", "50").formatted(Formatting.WHITE));
        hottestLabel = Components.label(Text.translatable("Hottest Part: %s RF/t", "50").formatted(Formatting.WHITE));
        
        container.child(productionLabel.margins(Insets.of(4)));
        container.child(hottestLabel.margins(Insets.of(4)));
        container.child(sumHeatLabel.margins(Insets.of(4)));
        
        overlay.child(container.margins(Insets.of(8)).surface(Surface.PANEL_INSET).positioning(Positioning.absolute(183, 16)));
    }
    
    private void addReactorStatus(FlowLayout overlay) {
        
        var container = Containers.verticalFlow(Sizing.fixed(90), Sizing.content(1));
        
        statusLabel = Components.label(Text.translatable("Stable").formatted(Formatting.WHITE, Formatting.BOLD));
        
        container.child(statusLabel.horizontalTextAlignment(HorizontalAlignment.CENTER).horizontalSizing(Sizing.fill()).margins(Insets.of(4)));
        
        overlay.child(container.margins(Insets.of(4)).surface(Surface.PANEL_INSET).positioning(Positioning.absolute(187, 75)));
        
    }
    
    private void addReactorComponentPreview(FlowLayout overlay) {
        
        var holoPreviewContainer = new ReactorPreviewContainer(Sizing.fixed(180), Sizing.fixed(164), FlowLayout.Algorithm.HORIZONTAL, this::onContainerMouseMove);
        holoPreviewContainer.surface(Surface.PANEL_INSET);
        holoPreviewContainer.margins(Insets.of(8));
        
        var uiData = handler.reactorEntity.uiData;
        if (uiData == null) return;
        
        var totalSize = uiData.previewMax().subtract(uiData.min());
        var leftCount = totalSize.getZ();
        var rightCount = totalSize.getX();
        var totalWidth = leftCount + rightCount + 3;
        var middlePercentage = leftCount / (float) totalWidth;
        var xOffset = middlePercentage * 170 + 10;
        
        var size = (int) (170 / (float) totalWidth * 2.2f);
        
        activeComponents = new ArrayList<>();
        activeOverlays = new HashSet<>();
        
        BlockPos.stream(uiData.min(), uiData.previewMax()).forEach(pos -> {
            var state = handler.world.getBlockState(pos);
            if (state.isAir()) return;
            var offset = pos.subtract(uiData.min());
            var projectedPosX = offset.getX() * 0.43f - offset.getZ() * 0.43f;
            var projectedPosY = offset.getX() * 0.224f + offset.getZ() * 0.224f + offset.getY() * 0.5f;
            var zIndex = offset.getY() - offset.getX() - offset.getZ();
            var preview = new ReactorBlockRenderComponent(null, handler.world.getBlockEntity(pos), zIndex, pos.toImmutable())
                            .sizing(Sizing.fixed(size))
                            .positioning(Positioning.absolute((int) (projectedPosX * size + xOffset), (int) (-projectedPosY * size) + 100));
            if (offset.getY() == 1) {
                activeComponents.add(new Pair<>(-zIndex, (ReactorBlockRenderComponent) preview));
            }
            holoPreviewContainer.child(preview);
            
            if (state.getBlock() instanceof ReactorRodBlock || state.getBlock() instanceof ReactorHeatPipeBlock) {
                var heatOverlay = new ReactorBlockRenderComponent(Blocks.AIR.getDefaultState(), null, zIndex + 0.5f, pos.toImmutable())
                                    .sizing(Sizing.fixed(size))
                                    .positioning(Positioning.absolute((int) (projectedPosX * size + xOffset), (int) (-projectedPosY * size) + 100));
                
                holoPreviewContainer.child(heatOverlay);
                activeOverlays.add((ReactorBlockRenderComponent) heatOverlay);
            }
            
        });
        
        selectedBlockOverlay = (ReactorBlockRenderComponent) new ReactorBlockRenderComponent(Blocks.AIR.getDefaultState(), null, 10 + 0.5f, BlockPos.ORIGIN)
                                                               .sizing(Sizing.fixed(size))
                                                               .positioning(Positioning.absolute(0, 0));
        holoPreviewContainer.child(selectedBlockOverlay);
        
        activeComponents.sort(Comparator.comparingInt(Pair::getLeft));
        
        overlay.child(holoPreviewContainer.positioning(Positioning.absolute(0, 16)));
        
    }
    
    @Override
    protected void handledScreenTick() {
        super.handledScreenTick();
        
        if (handler.reactorEntity.uiSyncData == null) return;
        
        for (var overlay : activeOverlays) {
            var data = getStatsAtPosition(overlay.pos);
            
            var isEmpty = data.storedHeat() <= 10;
            if (isEmpty) {
                overlay.state = Blocks.AIR.getDefaultState();
                continue;
            }
            
            var res = BlockContent.REACTOR_COLD_INDICATOR_BLOCK.getDefaultState();
            
            if (data.storedHeat() > 1000) {
                res = BlockContent.REACTOR_HOT_INDICATOR_BLOCK.getDefaultState();
            } else if (data.storedHeat() > 200) {
                res = BlockContent.REACTOR_MEDIUM_INDICATOR_BLOCK.getDefaultState();
            }
            
            overlay.state = res;
        }
        
        var stackHeight = handler.reactorEntity.uiData.max().getY() - handler.reactorEntity.uiData.min().getY() - 1;
        
        // gather stats
        var sumProducedEnergy = handler.reactorEntity.uiSyncData.componentHeats().stream()
                                  .mapToInt(data -> data.receivedPulses() * ReactorControllerBlockEntity.RF_PER_PULSE * stackHeight).sum();
        
        var sumProducedHeat = handler.reactorEntity.uiSyncData.componentHeats().stream()
                                .filter(elem -> elem.receivedPulses() > 0)
                                .mapToInt(ReactorControllerBlockEntity.ComponentStatistics::heatChanged).sum();
        
        var hottestComponent = handler.reactorEntity.uiSyncData.componentHeats().stream()
                                 .mapToInt(ReactorControllerBlockEntity.ComponentStatistics::storedHeat)
                                 .max().orElse(0);
        
        productionLabel.text(Text.translatable("text.oritech.reactor.rf_production", sumProducedEnergy));
        hottestLabel.text(Text.translatable("text.oritech.reactor.hottest_part", hottestComponent));
        sumHeatLabel.text(Text.translatable("text.oritech.reactor.heat_production", sumProducedHeat));
        
        // update status
        var isActive = sumProducedEnergy + sumProducedHeat > 0;
        var activeLabel = "idle";
        var color = Formatting.WHITE;
        
        if (isActive) {
            if (hottestComponent < 100) {
                activeLabel = "stable";
            } else if (hottestComponent < 1200) {
                activeLabel = "heating_up";
                color = Formatting.YELLOW;
            } else if (hottestComponent < 1700) {
                activeLabel = "unstable";
                color = Formatting.RED;
            } else {
                activeLabel = "explosion_imminent";
                color = Formatting.DARK_RED;
            }
        }
        
        statusLabel.text(Text.translatable("text.oritech.reactor." + activeLabel).formatted(Formatting.BOLD).formatted(color));
        
        updateEnergyBar();
        
    }
    
    private void onContainerMouseMove(int mouseX, int mouseY) {
        
        var posX = mouseX;
        var posY = mouseY;
        
        // check if self is on top of activeComponents
        for (var component : activeComponents) {
            var hit = component.getRight().isInBoundingBox(mouseX, mouseY);
            if (hit) {
                var pos = component.getRight().pos;
                addStatsToTooltip(pos, handler.world.getBlockState(pos), tooltipContainer);
                posX = component.getRight().x();
                posY = component.getRight().y();
                
                selectedBlockOverlay.state = BlockContent.ADDON_INDICATOR_BLOCK.getDefaultState();
                selectedBlockOverlay.pos = pos;
                selectedBlockOverlay.zIndex = component.getRight().zIndex + 0.6f;
                selectedBlockOverlay.positioning(component.getRight().positioning().get());
                
                break;
            }
        }
        
        if (posX == mouseX) {   // move out of visible area
            tooltipContainer.positioning(Positioning.absolute(-100, -500));
            selectedBlockOverlay.state = Blocks.AIR.getDefaultState();
            return;
        }
        
        var containerHeight = tooltipContainer.height();
        
        tooltipContainer.positioning(Positioning.absolute(posX - 30, posY - 5 - containerHeight));
        
    }
    
    public void addStatsToTooltip(BlockPos pos, BlockState state, FlowLayout container) {
        
        container.clearChildren();
        
        var blockname = state.getBlock().getName();
        container.child(Components.label(blockname.formatted(Formatting.WHITE, Formatting.BOLD)).margins(Insets.of(0, 3, 0, 0)));
        
        var stats = getStatsAtPosition(pos);
        if (stats.storedHeat() == -1) return;
        
        var stackHeight = handler.reactorEntity.uiData.max().getY() - handler.reactorEntity.uiData.min().getY() - 1;
        var portPosition = pos.add(0, stackHeight, 0);
        var portEntity = handler.world.getBlockEntity(portPosition);
        if (portEntity != null && portEntity.isRemoved()) return;
        
        
        if (state.getBlock() instanceof ReactorRodBlock rodBlock) {
            var rodCount = rodBlock.getRodCount();
            var totalPulses = stats.receivedPulses();
            var createdPulses = rodBlock.getInternalPulseCount();
            var externalPulses = totalPulses - createdPulses;
            var generatedEnergy = ReactorControllerBlockEntity.RF_PER_PULSE * totalPulses;
            var generatedHeat = stats.heatChanged();
            var heat = stats.storedHeat();
            
            if (totalPulses == 0) { // probably no fuel
                createdPulses = 0;
                externalPulses = 0;
            }
            
            if (!(portEntity instanceof ReactorFuelPortEntity fuelPortEntity)) return;
            var availableFuel = fuelPortEntity.availableFuel;
            var maxFuel = fuelPortEntity.currentFuelOriginalCapacity;
            
            container.child(Components.label(Text.translatable("text.oritech.reactor.rod_count", rodCount).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.generated_pulses", createdPulses).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.received_pulses", externalPulses).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.generated_heat", generatedHeat).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.generated_energy", generatedEnergy).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.heat", heat).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.fuel", availableFuel, maxFuel).formatted(Formatting.WHITE)));
        } else if (state.getBlock() instanceof ReactorHeatPipeBlock pipeBlock) {
            container.child(Components.label(Text.translatable("text.oritech.reactor.collected_heat", stats.heatChanged()).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.heat", stats.storedHeat()).formatted(Formatting.WHITE)));
        } else if (state.getBlock() instanceof ReactorHeatVentBlock pipeBlock) {
            container.child(Components.label(Text.translatable("text.oritech.reactor.removed_heat", stats.heatChanged()).formatted(Formatting.WHITE)));
        } else if (state.getBlock() instanceof ReactorAbsorberBlock absorberBlock) {
            
            if (!(portEntity instanceof ReactorAbsorberPortEntity absorberPortEntity)) return;
            var availableFuel = absorberPortEntity.availableFuel;
            var maxFuel = absorberPortEntity.currentFuelOriginalCapacity;
            
            container.child(Components.label(Text.translatable("text.oritech.reactor.absorbed_heat", stats.heatChanged()).formatted(Formatting.WHITE)));
            container.child(Components.label(Text.translatable("text.oritech.reactor.absorbant", availableFuel, maxFuel).formatted(Formatting.WHITE)));
        }
        
    }
    
    public ReactorControllerBlockEntity.ComponentStatistics getStatsAtPosition(BlockPos pos) {
        
        if (handler.reactorEntity.uiSyncData == null) return ReactorControllerBlockEntity.ComponentStatistics.EMPTY;
        
        var componentPositions = handler.reactorEntity.uiSyncData.componentPositions();
        for (int i = 0; i < componentPositions.size(); i++) {
            var candidate = componentPositions.get(i);
            if (!candidate.equals(pos)) continue;
            return handler.reactorEntity.uiSyncData.componentHeats().get(i);
        }
        
        return ReactorControllerBlockEntity.ComponentStatistics.EMPTY;
    }
    
    private void addTitle(FlowLayout overlay) {
        var blockTitle = handler.reactorEntity.getCachedState().getBlock().getName();
        var label = Components.label(blockTitle);
        label.color(new Color(64 / 255f, 64 / 255f, 64 / 255f));
        label.sizing(Sizing.fixed(176), Sizing.content(2));
        label.horizontalTextAlignment(HorizontalAlignment.CENTER);
        label.zIndex(1);
        overlay.child(label.positioning(Positioning.relative(50, 3)));
    }
    
    private void addEnergyBar(FlowLayout panel) {
        
        var config = new ScreenProvider.BarConfiguration(285, 80, 36, 108);
        var insetSize = 1;
        var tooltipText = Text.translatable("tooltip.oritech.energy_indicator", 10, 50);
        
        var frame = Containers.horizontalFlow(Sizing.fixed(config.width() + insetSize * 2), Sizing.fixed(config.height() + insetSize * 2));
        frame.surface(Surface.PANEL_INSET);
        frame.padding(Insets.of(insetSize));
        frame.positioning(Positioning.absolute(config.x() - insetSize, config.y() - insetSize));
        panel.child(frame);
        
        var indicator_background = Components.texture(BasicMachineScreen.GUI_COMPONENTS, 24, 0, 24, 96, 98, 96);
        indicator_background.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height()));
        
        energyIndicator = Components.texture(BasicMachineScreen.GUI_COMPONENTS, 0, 0, 24, (96), 98, 96);
        energyIndicator.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height()));
        energyIndicator.positioning(Positioning.absolute(0, 0));
        energyIndicator.tooltip(tooltipText);
        
        frame
          .child(indicator_background)
          .child(energyIndicator);
    }
    
    protected void updateEnergyBar() {
        
        var capacity = handler.reactorEntity.energyStorage.getCapacity();
        var amount = handler.reactorEntity.energyStorage.getAmount();
        
        var fillAmount = (float) amount / capacity;
        var tooltipText = BasicMachineScreen.getEnergyTooltip(amount, capacity, 0, 0);
        
        energyIndicator.tooltip(tooltipText);
        energyIndicator.visibleArea(PositionedRectangle.of(0, 96 - ((int) (96 * (fillAmount))), 24, (int) (96 * fillAmount)));
    }
}