mirror of
https://github.com/XevianLight/Aphelion.git
synced 2026-05-10 17:40:56 +01:00
Basic crude GUI implementation for StationFlightComputerBlock
This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cd /tmp)",
|
||||
"Bash(jar -xf \"C:/Users/Xevian/.gradle/caches/modules-2/files-2.1/net.neoforged/neoforge/21.1.217/f8798213b260c83be365a3d8ec5537d36dd44d1c/neoforge-21.1.217-sources.jar\" \"net/neoforged/neoforge/common/extensions/IPlayerExtension.java\")",
|
||||
"Read(//tmp/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import net.xevianlight.aphelion.fluid.ModFluids;
|
||||
import net.xevianlight.aphelion.recipe.ModRecipes;
|
||||
import net.xevianlight.aphelion.screen.ElectricArcFurnaceScreen;
|
||||
import net.xevianlight.aphelion.screen.ModMenuTypes;
|
||||
import net.xevianlight.aphelion.screen.StationFlightComputerScreen;
|
||||
import net.xevianlight.aphelion.screen.TestBlockScreen;
|
||||
import net.xevianlight.aphelion.screen.VacuumArcFurnaceScreen;
|
||||
import org.slf4j.Logger;
|
||||
@@ -152,6 +153,7 @@ public class Aphelion {
|
||||
event.register(ModMenuTypes.TEST_BLOCK_MENU.get(), TestBlockScreen::new);
|
||||
event.register(ModMenuTypes.ELECTRIC_ARC_FURNACE_MENU.get(), ElectricArcFurnaceScreen::new);
|
||||
event.register(ModMenuTypes.VACUUM_ARC_FURNACE_MENU.get(), VacuumArcFurnaceScreen::new);
|
||||
event.register(ModMenuTypes.STATION_FLIGHT_COMPUTER_MENU.get(), StationFlightComputerScreen::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
||||
@@ -2,16 +2,27 @@ package net.xevianlight.aphelion.block.custom;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
import net.xevianlight.aphelion.block.custom.base.BasicHorizontalEntityBlock;
|
||||
import net.xevianlight.aphelion.block.entity.custom.StationFlightComputerBlockEntity;
|
||||
import net.xevianlight.aphelion.core.saveddata.types.PartitionData;
|
||||
import net.xevianlight.aphelion.network.packet.AvailableDestinationsPayload;
|
||||
import net.xevianlight.aphelion.network.packet.PlanetInfo;
|
||||
import net.xevianlight.aphelion.planet.PlanetCache;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StationFlightComputerBlock extends BasicHorizontalEntityBlock {
|
||||
|
||||
public static final MapCodec<StationFlightComputerBlock> CODEC = simpleCodec(StationFlightComputerBlock::new);
|
||||
@@ -30,9 +41,31 @@ public class StationFlightComputerBlock extends BasicHorizontalEntityBlock {
|
||||
return new StationFlightComputerBlockEntity(blockPos, blockState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull InteractionResult useWithoutItem(@NotNull BlockState state, @NotNull Level level,
|
||||
@NotNull BlockPos pos, @NotNull Player player,
|
||||
@NotNull BlockHitResult hitResult) {
|
||||
if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) {
|
||||
if (level.getBlockEntity(pos) instanceof StationFlightComputerBlockEntity be) {
|
||||
List<PlanetInfo> planets = PlanetCache.PLANETS.entrySet().stream()
|
||||
.map(e -> new PlanetInfo(
|
||||
e.getKey(),
|
||||
e.getValue().orbit().location(),
|
||||
e.getValue().orbitDistance(),
|
||||
e.getValue().parentPlanet().map(k -> k.location())))
|
||||
.collect(Collectors.toList());
|
||||
// Send planet list before opening the menu so DestinationClientCache is populated when the screen opens.
|
||||
PacketDistributor.sendToPlayer(serverPlayer, new AvailableDestinationsPayload(planets));
|
||||
serverPlayer.openMenu(be, be.getBlockPos());
|
||||
}
|
||||
}
|
||||
return InteractionResult.sidedSuccess(level.isClientSide());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemove(BlockState state, @NotNull Level level, @NotNull BlockPos pos, BlockState newState, boolean movedByPiston) {
|
||||
super.onRemove(state, level, pos, newState, movedByPiston);
|
||||
// Breaking the flight computer aborts travel — no computer, no navigation.
|
||||
if (level.getBlockEntity(pos) instanceof StationFlightComputerBlockEntity computerBE) {
|
||||
PartitionData data = computerBE.getData();
|
||||
if (data != null) {
|
||||
@@ -44,11 +77,5 @@ public class StationFlightComputerBlock extends BasicHorizontalEntityBlock {
|
||||
@Override
|
||||
protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) {
|
||||
super.onPlace(state, level, pos, oldState, movedByPiston);
|
||||
if (level.getBlockEntity(pos) instanceof StationFlightComputerBlockEntity computerBE) {
|
||||
PartitionData data = computerBE.getData();
|
||||
if (data != null) {
|
||||
data.setTraveling(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ package net.xevianlight.aphelion.block.entity.custom;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerData;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
@@ -11,28 +18,41 @@ import net.xevianlight.aphelion.core.init.ModBlockEntities;
|
||||
import net.xevianlight.aphelion.core.init.ModDimensions;
|
||||
import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData;
|
||||
import net.xevianlight.aphelion.core.saveddata.types.PartitionData;
|
||||
import net.xevianlight.aphelion.screen.StationFlightComputerMenu;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class StationFlightComputerBlockEntity extends BlockEntity implements TickableBlockEntity {
|
||||
public StationFlightComputerBlockEntity(BlockPos pos, BlockState blockState) {
|
||||
super(ModBlockEntities.STATION_FLIGHT_COMPUTER_BLOCK_ENTITY.get(), pos, blockState);
|
||||
}
|
||||
public class StationFlightComputerBlockEntity extends BlockEntity implements TickableBlockEntity, MenuProvider {
|
||||
|
||||
protected PartitionData data;
|
||||
private boolean isInitialized = false;
|
||||
|
||||
@Override
|
||||
public void clientTick(ClientLevel level, long time, BlockState state, BlockPos pos) {
|
||||
private final ContainerData containerData = new ContainerData() {
|
||||
@Override
|
||||
public int get(int index) {
|
||||
if (data == null) return 0;
|
||||
return switch (index) {
|
||||
case StationFlightComputerMenu.DATA_TRAVELING -> data.isTraveling() ? 1 : 0;
|
||||
case StationFlightComputerMenu.DATA_ENGINE_COUNT -> data.getEngines().size();
|
||||
case StationFlightComputerMenu.DATA_PAD_COUNT -> data.getLandingPadControllers().size();
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
// set() is intentionally a no-op: client writes go through explicit network packets, not ContainerData
|
||||
@Override public void set(int index, int value) {}
|
||||
@Override public int getCount() { return StationFlightComputerMenu.DATA_COUNT; }
|
||||
};
|
||||
|
||||
public StationFlightComputerBlockEntity(BlockPos pos, BlockState blockState) {
|
||||
super(ModBlockEntities.STATION_FLIGHT_COMPUTER_BLOCK_ENTITY.get(), pos, blockState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) {
|
||||
}
|
||||
public void clientTick(ClientLevel level, long time, BlockState state, BlockPos pos) {}
|
||||
|
||||
public @Nullable PartitionData getData() {
|
||||
return data;
|
||||
}
|
||||
@Override
|
||||
public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) {}
|
||||
|
||||
public @Nullable PartitionData getData() { return data; }
|
||||
|
||||
@Override
|
||||
public void firstTick(Level level, BlockState state, BlockPos pos) {
|
||||
@@ -40,20 +60,34 @@ public class StationFlightComputerBlockEntity extends BlockEntity implements Tic
|
||||
if (level instanceof ServerLevel serverLevel) {
|
||||
if (serverLevel.dimension() == ModDimensions.SPACE) {
|
||||
data = SpacePartitionSavedData.get(serverLevel).getDataForBlockPos(pos);
|
||||
setTraveling(true);
|
||||
}
|
||||
}
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
protected boolean setTraveling(boolean value) {
|
||||
public boolean setTraveling(boolean value) {
|
||||
if (data == null) return false;
|
||||
data.setTraveling(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() {
|
||||
return isInitialized;
|
||||
public void setDestination(@Nullable ResourceLocation destination) {
|
||||
if (data == null) return;
|
||||
data.setDestination(destination);
|
||||
}
|
||||
|
||||
public ContainerData getContainerData() { return containerData; }
|
||||
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return Component.translatable("block.aphelion.station_flight_computer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable AbstractContainerMenu createMenu(int windowId, Inventory inventory, Player player) {
|
||||
return new StationFlightComputerMenu(windowId, inventory, this, containerData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() { return isInitialized; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package net.xevianlight.aphelion.client;
|
||||
|
||||
import net.xevianlight.aphelion.network.packet.PlanetInfo;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class DestinationClientCache {
|
||||
// volatile: written from the netty network thread, read from the render thread
|
||||
private static volatile List<PlanetInfo> planets = Collections.emptyList();
|
||||
|
||||
public static void set(List<PlanetInfo> list) { planets = List.copyOf(list); }
|
||||
public static List<PlanetInfo> get() { return planets; }
|
||||
}
|
||||
@@ -15,10 +15,14 @@ import net.xevianlight.aphelion.block.entity.custom.TestBlockEntity;
|
||||
import net.xevianlight.aphelion.block.entity.custom.VacuumArcFurnaceControllerEntity;
|
||||
import net.xevianlight.aphelion.core.init.ModBlockEntities;
|
||||
import net.xevianlight.aphelion.core.init.ModEntities;
|
||||
import net.xevianlight.aphelion.network.FlightComputerPayloadHandler;
|
||||
import net.xevianlight.aphelion.network.RocketPayloadHandlers;
|
||||
import net.xevianlight.aphelion.network.PartitionPayloadHandler;
|
||||
import net.xevianlight.aphelion.network.packet.AvailableDestinationsPayload;
|
||||
import net.xevianlight.aphelion.network.packet.PartitionPayload;
|
||||
import net.xevianlight.aphelion.network.packet.RocketLaunchPayload;
|
||||
import net.xevianlight.aphelion.network.packet.SetDestinationPayload;
|
||||
import net.xevianlight.aphelion.network.packet.SetTravelingPayload;
|
||||
|
||||
@EventBusSubscriber(modid = Aphelion.MOD_ID)
|
||||
public class ModBusEvents {
|
||||
@@ -58,5 +62,23 @@ public class ModBusEvents {
|
||||
RocketPayloadHandlers::handleRocketLaunch
|
||||
);
|
||||
|
||||
registrar.playToClient(
|
||||
AvailableDestinationsPayload.TYPE,
|
||||
AvailableDestinationsPayload.STREAM_CODEC,
|
||||
FlightComputerPayloadHandler::handleAvailableDestinations
|
||||
);
|
||||
|
||||
registrar.playToServer(
|
||||
SetDestinationPayload.TYPE,
|
||||
SetDestinationPayload.STREAM_CODEC,
|
||||
FlightComputerPayloadHandler::handleSetDestination
|
||||
);
|
||||
|
||||
registrar.playToServer(
|
||||
SetTravelingPayload.TYPE,
|
||||
SetTravelingPayload.STREAM_CODEC,
|
||||
FlightComputerPayloadHandler::handleSetTraveling
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.xevianlight.aphelion.network;
|
||||
|
||||
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||
import net.xevianlight.aphelion.block.entity.custom.StationFlightComputerBlockEntity;
|
||||
import net.xevianlight.aphelion.client.DestinationClientCache;
|
||||
import net.xevianlight.aphelion.network.packet.AvailableDestinationsPayload;
|
||||
import net.xevianlight.aphelion.network.packet.SetDestinationPayload;
|
||||
import net.xevianlight.aphelion.network.packet.SetTravelingPayload;
|
||||
|
||||
public class FlightComputerPayloadHandler {
|
||||
|
||||
// Runs on the CLIENT: caches the planet list so the screen has it immediately on open.
|
||||
public static void handleAvailableDestinations(AvailableDestinationsPayload payload, IPayloadContext context) {
|
||||
DestinationClientCache.set(payload.planets());
|
||||
}
|
||||
|
||||
// Runs on the SERVER: client-side button sends this; server commits it to PartitionData.
|
||||
public static void handleSetDestination(SetDestinationPayload payload, IPayloadContext context) {
|
||||
var level = context.player().level();
|
||||
if (level.getBlockEntity(payload.computerPos()) instanceof StationFlightComputerBlockEntity be) {
|
||||
be.setDestination(payload.destination().orElse(null));
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on the SERVER: the Launch/Abort button toggles traveling via this packet.
|
||||
public static void handleSetTraveling(SetTravelingPayload payload, IPayloadContext context) {
|
||||
var level = context.player().level();
|
||||
if (level.getBlockEntity(payload.computerPos()) instanceof StationFlightComputerBlockEntity be) {
|
||||
be.setTraveling(payload.traveling());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.xevianlight.aphelion.network.packet;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.xevianlight.aphelion.Aphelion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record AvailableDestinationsPayload(List<PlanetInfo> planets) implements CustomPacketPayload {
|
||||
public static final Type<AvailableDestinationsPayload> TYPE =
|
||||
new Type<>(ResourceLocation.fromNamespaceAndPath(Aphelion.MOD_ID, "available_destinations"));
|
||||
|
||||
// 256 is a hard cap on the list codec to prevent oversized packets; the solar system has ~8 planets
|
||||
public static final StreamCodec<ByteBuf, AvailableDestinationsPayload> STREAM_CODEC =
|
||||
PlanetInfo.STREAM_CODEC.apply(ByteBufCodecs.list(256))
|
||||
.map(AvailableDestinationsPayload::new, AvailableDestinationsPayload::planets);
|
||||
|
||||
@Override
|
||||
public @NotNull Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package net.xevianlight.aphelion.network.packet;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record PlanetInfo(ResourceLocation id, ResourceLocation orbit, double orbitDistance, Optional<ResourceLocation> parentPlanet) {
|
||||
|
||||
public static final StreamCodec<ByteBuf, PlanetInfo> STREAM_CODEC = StreamCodec.composite(
|
||||
ResourceLocation.STREAM_CODEC, PlanetInfo::id,
|
||||
ResourceLocation.STREAM_CODEC, PlanetInfo::orbit,
|
||||
ByteBufCodecs.DOUBLE, PlanetInfo::orbitDistance,
|
||||
ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), PlanetInfo::parentPlanet,
|
||||
(id, orb, dist, parent) -> new PlanetInfo(id, orb, dist, parent)
|
||||
);
|
||||
|
||||
public boolean isMoon() {
|
||||
return parentPlanet.isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.xevianlight.aphelion.network.packet;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.xevianlight.aphelion.Aphelion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record SetDestinationPayload(BlockPos computerPos, Optional<ResourceLocation> destination) implements CustomPacketPayload {
|
||||
public static final Type<SetDestinationPayload> TYPE =
|
||||
new Type<>(ResourceLocation.fromNamespaceAndPath(Aphelion.MOD_ID, "set_destination"));
|
||||
|
||||
public static final StreamCodec<ByteBuf, SetDestinationPayload> STREAM_CODEC =
|
||||
StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC,
|
||||
SetDestinationPayload::computerPos,
|
||||
ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC),
|
||||
SetDestinationPayload::destination,
|
||||
SetDestinationPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public @NotNull Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.xevianlight.aphelion.network.packet;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.xevianlight.aphelion.Aphelion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record SetTravelingPayload(BlockPos computerPos, boolean traveling) implements CustomPacketPayload {
|
||||
public static final Type<SetTravelingPayload> TYPE =
|
||||
new Type<>(ResourceLocation.fromNamespaceAndPath(Aphelion.MOD_ID, "set_traveling"));
|
||||
|
||||
public static final StreamCodec<ByteBuf, SetTravelingPayload> STREAM_CODEC =
|
||||
StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC,
|
||||
SetTravelingPayload::computerPos,
|
||||
ByteBufCodecs.BOOL,
|
||||
SetTravelingPayload::traveling,
|
||||
SetTravelingPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public @NotNull Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ public class ModMenuTypes {
|
||||
public static DeferredHolder<MenuType<?>,MenuType<VacuumArcFurnaceMenu>> VACUUM_ARC_FURNACE_MENU =
|
||||
registerMenuType("vacuum_arc_furnace_menu", VacuumArcFurnaceMenu::new);
|
||||
|
||||
public static DeferredHolder<MenuType<?>,MenuType<StationFlightComputerMenu>> STATION_FLIGHT_COMPUTER_MENU =
|
||||
registerMenuType("station_flight_computer_menu", StationFlightComputerMenu::new);
|
||||
|
||||
private static <T extends AbstractContainerMenu>DeferredHolder<MenuType<?>, MenuType<T>> registerMenuType(String name,
|
||||
IContainerFactory<T> factory) {
|
||||
return MENUS.register(name, () -> IMenuTypeExtension.create(factory));
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.xevianlight.aphelion.screen;
|
||||
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerData;
|
||||
import net.minecraft.world.inventory.SimpleContainerData;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.xevianlight.aphelion.block.entity.custom.StationFlightComputerBlockEntity;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class StationFlightComputerMenu extends AbstractContainerMenu {
|
||||
public final StationFlightComputerBlockEntity blockEntity;
|
||||
|
||||
public static final int DATA_TRAVELING = 0;
|
||||
public static final int DATA_ENGINE_COUNT = 1;
|
||||
public static final int DATA_PAD_COUNT = 2;
|
||||
public static final int DATA_COUNT = 3;
|
||||
|
||||
private final ContainerData data;
|
||||
|
||||
public StationFlightComputerMenu(int windowId, Inventory inv, FriendlyByteBuf buf) {
|
||||
this(windowId, inv, inv.player.level().getBlockEntity(buf.readBlockPos()), new SimpleContainerData(DATA_COUNT));
|
||||
}
|
||||
|
||||
public StationFlightComputerMenu(int windowId, Inventory inv, BlockEntity be, ContainerData data) {
|
||||
super(ModMenuTypes.STATION_FLIGHT_COMPUTER_MENU.get(), windowId);
|
||||
this.blockEntity = (StationFlightComputerBlockEntity) be;
|
||||
this.data = data;
|
||||
addDataSlots(data);
|
||||
}
|
||||
|
||||
public boolean isTraveling() { return data.get(DATA_TRAVELING) != 0; }
|
||||
public int getEngineCount() { return data.get(DATA_ENGINE_COUNT); }
|
||||
public int getPadCount() { return data.get(DATA_PAD_COUNT); }
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stillValid(@NotNull Player player) {
|
||||
// Always valid — players can interact with the computer from anywhere on the station.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
package net.xevianlight.aphelion.screen;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
import net.xevianlight.aphelion.client.DestinationClientCache;
|
||||
import net.xevianlight.aphelion.client.PartitionClientState;
|
||||
import net.xevianlight.aphelion.core.saveddata.types.PartitionData;
|
||||
import net.xevianlight.aphelion.network.packet.PlanetInfo;
|
||||
import net.xevianlight.aphelion.network.packet.SetDestinationPayload;
|
||||
import net.xevianlight.aphelion.network.packet.SetTravelingPayload;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class StationFlightComputerScreen extends AbstractContainerScreen<StationFlightComputerMenu> {
|
||||
|
||||
// ── Orbital animation ──────────────────────────────────────────────────
|
||||
/** Shared epoch so planet positions persist across screen open/close. */
|
||||
private static final long EPOCH_MS = System.currentTimeMillis();
|
||||
/** Real-world ms for one Earth orbit. Inner planets scale by AU^1.5. */
|
||||
private static final double EARTH_PERIOD_MS = 120_000.0; // 15 seconds
|
||||
/** Fixed screen-space orbit radius for moons around their parent dot (system view). */
|
||||
private static final int MOON_ORBIT_R = 13;
|
||||
/** Screen-space orbit radius for moons when the orrery is zoomed into a subsystem. */
|
||||
private static final int ZOOM_MOON_ORBIT_R = 45;
|
||||
/** Fixed animation period for all moons — actual AU is too small for Keplerian scaling. */
|
||||
private static final double MOON_PERIOD_MS = 18_000.0;
|
||||
|
||||
// ── Layout ─────────────────────────────────────────────────────────────
|
||||
private static final int ORRERY_W = 176;
|
||||
private static final int ORRERY_H = 186;
|
||||
private static final int ORRERY_CX = ORRERY_W / 2; // 88
|
||||
private static final int ORRERY_CY = ORRERY_H / 2; // 93
|
||||
private static final int MAX_ORBIT_R = 82;
|
||||
private static final int MIN_ORBIT_R = 18;
|
||||
private static final int INFO_X = 180; // relative to leftPos
|
||||
private static final int INFO_W = 98;
|
||||
|
||||
// ── Colors ─────────────────────────────────────────────────────────────
|
||||
private static final int C_BG = 0xFF0D0D1A;
|
||||
private static final int C_SPACE = 0xFF030308;
|
||||
private static final int C_BORDER = 0xFF2A2A4A;
|
||||
private static final int C_PANEL = 0xFF0A0A16;
|
||||
private static final int C_GOLD = 0xFFFFD700;
|
||||
private static final int C_WHITE = 0xFFE8E8E8;
|
||||
private static final int C_GRAY = 0xFF888899;
|
||||
private static final int C_ORBIT = 0xFF1A1A30;
|
||||
private static final int C_ORBIT_CUR = 0xFF334466;
|
||||
private static final int C_ORBIT_DEST = 0xFF2A4A2A;
|
||||
private static final int C_ORBIT_SEL = 0xFF2A3A6A;
|
||||
private static final int C_STAR = 0xFFFFE866;
|
||||
private static final int C_STATION = 0xFFFFFFFF;
|
||||
private static final int C_TRAVEL_LINE = 0xFF446644;
|
||||
private static final int C_PROG_BG = 0xFF0A1A0A;
|
||||
private static final int C_PROG_FILL = 0xFF00BB44;
|
||||
|
||||
private static final int[] PLANET_COLORS = {
|
||||
0xFFE8A060, 0xFF88CC44, 0xFF4499FF, 0xFFCC8844,
|
||||
0xFF88AACC, 0xFFCC44CC, 0xFFFFCC44, 0xFF99FFAA,
|
||||
};
|
||||
|
||||
// ── Background stars (fixed, deterministic) ────────────────────────────
|
||||
private static final int STAR_COUNT = 60;
|
||||
private static final int[] STAR_PX = new int[STAR_COUNT];
|
||||
private static final int[] STAR_PY = new int[STAR_COUNT];
|
||||
private static final int[] STAR_COL = new int[STAR_COUNT];
|
||||
static {
|
||||
long s = 0x5DEECE66DL;
|
||||
for (int i = 0; i < STAR_COUNT; i++) {
|
||||
s = (s * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFL;
|
||||
STAR_PX[i] = (int)(s % ORRERY_W);
|
||||
s = (s * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFL;
|
||||
STAR_PY[i] = (int)(s % ORRERY_H);
|
||||
int alpha = 0x50 + (i % 4) * 0x18;
|
||||
STAR_COL[i] = (alpha << 24) | 0xCCCCCC;
|
||||
}
|
||||
}
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────
|
||||
@Nullable private ResourceLocation selectedPlanet = null;
|
||||
/** Non-null when the orrery is zoomed into a planet's local subsystem. */
|
||||
@Nullable private ResourceLocation zoomedSystem = null;
|
||||
|
||||
/** Cached each frame in renderBg; read in mouseClicked to stay in sync. */
|
||||
private final List<int[]> planetDrawCache = new ArrayList<>(); // [absX, absY, screenR, colorIdx]
|
||||
private final List<PlanetInfo> planetCache = new ArrayList<>();
|
||||
|
||||
private Button setDestButton;
|
||||
private Button travelButton;
|
||||
|
||||
public StationFlightComputerScreen(StationFlightComputerMenu menu, Inventory inv, Component title) {
|
||||
super(menu, inv, title);
|
||||
this.imageWidth = 280;
|
||||
this.imageHeight = 190;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
this.titleLabelY = 10000;
|
||||
this.inventoryLabelY = 10000;
|
||||
|
||||
int bx = leftPos + INFO_X + 4;
|
||||
int bw = INFO_W - 8;
|
||||
|
||||
setDestButton = addRenderableWidget(Button.builder(
|
||||
Component.literal("Set Destination"),
|
||||
btn -> {
|
||||
if (selectedPlanet != null) {
|
||||
PacketDistributor.sendToServer(new SetDestinationPayload(
|
||||
menu.blockEntity.getBlockPos(),
|
||||
Optional.of(selectedPlanet)));
|
||||
}
|
||||
})
|
||||
.pos(bx, topPos + 152)
|
||||
.size(bw, 16)
|
||||
.build());
|
||||
|
||||
travelButton = addRenderableWidget(Button.builder(
|
||||
Component.literal("Launch"),
|
||||
btn -> PacketDistributor.sendToServer(new SetTravelingPayload(
|
||||
menu.blockEntity.getBlockPos(),
|
||||
!menu.isTraveling())))
|
||||
.pos(bx, topPos + 170)
|
||||
.size(bw, 16)
|
||||
.build());
|
||||
}
|
||||
|
||||
// ── Rendering ──────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void render(GuiGraphics g, int mouseX, int mouseY, float partialTick) {
|
||||
renderBackground(g, mouseX, mouseY, partialTick);
|
||||
super.render(g, mouseX, mouseY, partialTick);
|
||||
renderTooltip(g, mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics g, float partialTick, int mouseX, int mouseY) {
|
||||
// Outer frame
|
||||
g.fill(leftPos, topPos, leftPos + imageWidth, topPos + imageHeight, C_BG);
|
||||
|
||||
drawOrrery(g, mouseX, mouseY);
|
||||
drawInfoPanel(g, mouseX, mouseY);
|
||||
|
||||
// Dynamic button labels / states
|
||||
travelButton.setMessage(menu.isTraveling()
|
||||
? Component.literal("Abort Travel")
|
||||
: Component.literal("Launch"));
|
||||
setDestButton.active = selectedPlanet != null;
|
||||
}
|
||||
|
||||
// ── Orrery ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void drawOrrery(GuiGraphics g, int mouseX, int mouseY) {
|
||||
int orrX = leftPos + 2;
|
||||
int orrY = topPos + 2;
|
||||
int cx = orrX + ORRERY_CX;
|
||||
int cy = orrY + ORRERY_CY;
|
||||
|
||||
// Space background
|
||||
g.fill(orrX, orrY, orrX + ORRERY_W, orrY + ORRERY_H, C_SPACE);
|
||||
|
||||
// Panel border
|
||||
g.fill(orrX, orrY, orrX + ORRERY_W, orrY + 1, C_BORDER);
|
||||
g.fill(orrX, orrY + ORRERY_H - 1, orrX + ORRERY_W, orrY + ORRERY_H, C_BORDER);
|
||||
g.fill(orrX, orrY, orrX + 1, orrY + ORRERY_H, C_BORDER);
|
||||
g.fill(orrX + ORRERY_W - 1, orrY, orrX + ORRERY_W, orrY + ORRERY_H, C_BORDER);
|
||||
|
||||
// Background stars
|
||||
for (int i = 0; i < STAR_COUNT; i++) {
|
||||
g.fill(orrX + STAR_PX[i], orrY + STAR_PY[i],
|
||||
orrX + STAR_PX[i] + 1, orrY + STAR_PY[i] + 1, STAR_COL[i]);
|
||||
}
|
||||
|
||||
List<PlanetInfo> allPlanets = DestinationClientCache.get();
|
||||
PartitionData data = PartitionClientState.get().map(p -> p.partitionData()).orElse(null);
|
||||
|
||||
// Filter to the station's star system. Resolved client-side so the server
|
||||
// can send all planets without needing to know the station's current system.
|
||||
ResourceLocation currentSystem = null;
|
||||
if (data != null && data.getOrbit() != null) {
|
||||
var currentPlanet = net.xevianlight.aphelion.planet.PlanetCache.getByOrbitOrNull(data.getOrbit());
|
||||
if (currentPlanet != null) currentSystem = currentPlanet.system().location();
|
||||
}
|
||||
final ResourceLocation systemFilter = currentSystem;
|
||||
List<PlanetInfo> planets = systemFilter == null ? allPlanets : allPlanets.stream()
|
||||
.filter(p -> {
|
||||
var planet = net.xevianlight.aphelion.planet.PlanetCache.getOrNull(p.id());
|
||||
return planet == null || planet.system().location().equals(systemFilter);
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
// Rebuild cached positions
|
||||
planetDrawCache.clear();
|
||||
planetCache.clear();
|
||||
|
||||
if (planets.isEmpty()) {
|
||||
var font = Minecraft.getInstance().font;
|
||||
String msg = "No planet data";
|
||||
g.drawString(font, msg, cx - font.width(msg) / 2, cy - 4, C_GRAY, false);
|
||||
drawDot(g, cx, cy, 4, C_STAR);
|
||||
return;
|
||||
}
|
||||
|
||||
long elapsed = System.currentTimeMillis() - EPOCH_MS;
|
||||
|
||||
if (zoomedSystem == null) {
|
||||
// ── SYSTEM VIEW ────────────────────────────────────────────────
|
||||
// Exclude moons from the star-scale min/max so inner planets aren't crushed.
|
||||
double maxOrbit = planets.stream().filter(p -> !p.isMoon()).mapToDouble(PlanetInfo::orbitDistance).max().orElse(1);
|
||||
double minOrbit = planets.stream().filter(p -> !p.isMoon()).mapToDouble(PlanetInfo::orbitDistance).min().orElse(0);
|
||||
|
||||
// Pass 1 — non-moon bodies: positions relative to the central star.
|
||||
int nonMoonCount = (int) planets.stream().filter(p -> !p.isMoon()).count();
|
||||
int phaseSlot = 0;
|
||||
for (int i = 0; i < planets.size(); i++) {
|
||||
PlanetInfo p = planets.get(i);
|
||||
if (p.isMoon()) {
|
||||
planetDrawCache.add(null); // placeholder; filled in pass 2
|
||||
planetCache.add(p);
|
||||
continue;
|
||||
}
|
||||
double phase = (2.0 * Math.PI * phaseSlot / Math.max(1, nonMoonCount));
|
||||
double period = EARTH_PERIOD_MS * Math.pow(p.orbitDistance(), 1.5);
|
||||
double angle = phase + elapsed * 2.0 * Math.PI / period;
|
||||
int screenR = orbitRadius(p.orbitDistance(), minOrbit, maxOrbit);
|
||||
int px = cx + (int)(screenR * Math.cos(angle));
|
||||
int py = cy + (int)(screenR * Math.sin(angle));
|
||||
planetDrawCache.add(new int[]{px, py, screenR, i % PLANET_COLORS.length});
|
||||
planetCache.add(p);
|
||||
phaseSlot++;
|
||||
}
|
||||
|
||||
// Pass 2 — moons: positions relative to their parent's draw position.
|
||||
// moonSlotByParent tracks per-parent slot so phase offsets spread siblings evenly.
|
||||
java.util.Map<java.util.Optional<ResourceLocation>, Integer> moonSlotByParent = new java.util.HashMap<>();
|
||||
for (int i = 0; i < planets.size(); i++) {
|
||||
PlanetInfo p = planets.get(i);
|
||||
if (!p.isMoon()) continue;
|
||||
int parentIdx = indexById(p.parentPlanet().orElse(null));
|
||||
int[] parentPos = (parentIdx >= 0) ? planetDrawCache.get(parentIdx) : null;
|
||||
int ocx = (parentPos != null) ? parentPos[0] : cx;
|
||||
int ocy = (parentPos != null) ? parentPos[1] : cy;
|
||||
int slot = moonSlotByParent.getOrDefault(p.parentPlanet(), 0);
|
||||
int siblings = (int) planets.stream().filter(q -> q.isMoon() && q.parentPlanet().equals(p.parentPlanet())).count();
|
||||
double phase = (2.0 * Math.PI * slot / Math.max(1, siblings));
|
||||
double angle = phase + elapsed * 2.0 * Math.PI / MOON_PERIOD_MS;
|
||||
int px = ocx + (int)(MOON_ORBIT_R * Math.cos(angle));
|
||||
int py = ocy + (int)(MOON_ORBIT_R * Math.sin(angle));
|
||||
planetDrawCache.set(i, new int[]{px, py, MOON_ORBIT_R, i % PLANET_COLORS.length});
|
||||
moonSlotByParent.put(p.parentPlanet(), slot + 1);
|
||||
}
|
||||
} else {
|
||||
// ── SUBSYSTEM VIEW ─────────────────────────────────────────────
|
||||
// Focused planet sits at orrery centre; its moons orbit it at ZOOM_MOON_ORBIT_R.
|
||||
// Every other body is hidden (null entry → unclickable, not drawn).
|
||||
java.util.Map<java.util.Optional<ResourceLocation>, Integer> moonSlotByParent = new java.util.HashMap<>();
|
||||
for (int i = 0; i < planets.size(); i++) {
|
||||
PlanetInfo p = planets.get(i);
|
||||
planetCache.add(p);
|
||||
if (p.id().equals(zoomedSystem)) {
|
||||
// The focused planet itself: place it at the centre.
|
||||
planetDrawCache.add(new int[]{cx, cy, 0, i % PLANET_COLORS.length});
|
||||
} else if (p.isMoon() && p.parentPlanet().map(zoomedSystem::equals).orElse(false)) {
|
||||
// Moon of the focused planet: orbit around centre.
|
||||
int slot = moonSlotByParent.getOrDefault(p.parentPlanet(), 0);
|
||||
int siblings = (int) planets.stream().filter(q -> q.isMoon() && q.parentPlanet().equals(p.parentPlanet())).count();
|
||||
double phase = (2.0 * Math.PI * slot / Math.max(1, siblings));
|
||||
double angle = phase + elapsed * 2.0 * Math.PI / MOON_PERIOD_MS;
|
||||
int px = cx + (int)(ZOOM_MOON_ORBIT_R * Math.cos(angle));
|
||||
int py = cy + (int)(ZOOM_MOON_ORBIT_R * Math.sin(angle));
|
||||
planetDrawCache.add(new int[]{px, py, ZOOM_MOON_ORBIT_R, i % PLANET_COLORS.length});
|
||||
moonSlotByParent.put(p.parentPlanet(), slot + 1);
|
||||
} else {
|
||||
planetDrawCache.add(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Identify current and destination planets
|
||||
ResourceLocation curOrbit = data != null ? data.getOrbit() : null;
|
||||
ResourceLocation destId = data != null ? data.getDestination() : null;
|
||||
|
||||
int curIdx = indexByOrbit(curOrbit);
|
||||
int destIdx = indexById(destId);
|
||||
|
||||
// ── Orbit rings ────────────────────────────────────────────────────
|
||||
for (int i = 0; i < planets.size(); i++) {
|
||||
int[] pos = planetDrawCache.get(i);
|
||||
if (pos == null) continue;
|
||||
PlanetInfo p = planets.get(i);
|
||||
boolean isCur = i == curIdx;
|
||||
boolean isDest = i == destIdx;
|
||||
boolean isSel = p.id().equals(selectedPlanet);
|
||||
int col = isCur ? C_ORBIT_CUR : isDest ? C_ORBIT_DEST : isSel ? C_ORBIT_SEL : C_ORBIT;
|
||||
if (p.isMoon()) {
|
||||
int parentIdx = indexById(p.parentPlanet().orElse(null));
|
||||
int[] parentPos = (parentIdx >= 0) ? planetDrawCache.get(parentIdx) : null;
|
||||
int ocx = (parentPos != null) ? parentPos[0] : cx;
|
||||
int ocy = (parentPos != null) ? parentPos[1] : cy;
|
||||
drawOrbitRing(g, ocx, ocy, pos[2], col); // pos[2] holds the orbit radius (system vs zoom)
|
||||
} else if (pos[2] > 0) {
|
||||
drawOrbitRing(g, cx, cy, pos[2], col); // pos[2] == 0 for the focused planet in zoom mode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Travel arc ─────────────────────────────────────────────────────
|
||||
if (data != null && data.isTraveling() && curIdx >= 0 && destIdx >= 0) {
|
||||
int[] sp = planetDrawCache.get(curIdx);
|
||||
int[] dp = planetDrawCache.get(destIdx);
|
||||
if (sp != null && dp != null)
|
||||
drawDashedLine(g, sp[0], sp[1], dp[0], dp[1], C_TRAVEL_LINE);
|
||||
}
|
||||
|
||||
// ── Central body (star in system view; focused planet in subsystem view) ──
|
||||
if (zoomedSystem == null) {
|
||||
drawOrbitRing(g, cx, cy, 7, 0x20FFE866);
|
||||
drawDot(g, cx, cy, 4, C_STAR);
|
||||
} else {
|
||||
// Draw focused planet at centre with a glow ring, and show a back hint.
|
||||
int focusedIdx = indexById(zoomedSystem);
|
||||
int focusedColor = (focusedIdx >= 0) ? PLANET_COLORS[planetDrawCache.get(focusedIdx)[3]] : C_WHITE;
|
||||
drawOrbitRing(g, cx, cy, 9, focusedColor & 0x40FFFFFF);
|
||||
drawDot(g, cx, cy, 5, focusedColor);
|
||||
var font = Minecraft.getInstance().font;
|
||||
String hint = "↩ system view";
|
||||
g.drawString(font, hint, orrX + 4, orrY + ORRERY_H - 10, C_GRAY, false);
|
||||
}
|
||||
|
||||
// ── Planet dots ────────────────────────────────────────────────────
|
||||
var font = Minecraft.getInstance().font;
|
||||
for (int i = 0; i < planets.size(); i++) {
|
||||
PlanetInfo p = planets.get(i);
|
||||
int[] pos = planetDrawCache.get(i);
|
||||
if (pos == null) continue;
|
||||
int px = pos[0], py = pos[1];
|
||||
|
||||
boolean isCur = i == curIdx;
|
||||
boolean isDest = i == destIdx;
|
||||
boolean isSel = p.id().equals(selectedPlanet);
|
||||
boolean isHover = distSq(mouseX, mouseY, px, py) <= 64;
|
||||
|
||||
int baseColor = PLANET_COLORS[pos[3]];
|
||||
int dotColor = isSel ? blend(baseColor, 0xFFFFFFFF, 0.5f)
|
||||
: isDest ? blend(baseColor, 0xFF44FF44, 0.5f)
|
||||
: isCur ? blend(baseColor, 0xFFFFFFFF, 0.3f)
|
||||
: baseColor;
|
||||
int dotR = (isSel || isCur || isDest) ? 4 : 3;
|
||||
drawDot(g, px, py, dotR, dotColor);
|
||||
|
||||
// Label: always for current/dest/selected, on hover otherwise
|
||||
if (isCur || isDest || isSel || isHover) {
|
||||
String name = formatId(p.id());
|
||||
int labelX = px + dotR + 3;
|
||||
int labelY = py - 4;
|
||||
if (labelX + font.width(name) > orrX + ORRERY_W - 4)
|
||||
labelX = px - font.width(name) - dotR - 2;
|
||||
int labelCol = isCur ? 0xFFAAAAFF : isDest ? 0xFF88FF88 : isSel ? C_WHITE : C_GRAY;
|
||||
g.drawString(font, name, labelX, labelY, labelCol, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Station marker ─────────────────────────────────────────────────
|
||||
if (data != null) {
|
||||
if (data.isTraveling() && curIdx >= 0 && destIdx >= 0) {
|
||||
int[] sp = planetDrawCache.get(curIdx);
|
||||
int[] dp = planetDrawCache.get(destIdx);
|
||||
if (sp != null && dp != null) {
|
||||
double tripDist = Math.abs(data.getTripDistanceAU());
|
||||
float progress = tripDist > 0
|
||||
? (float)(Math.abs(data.getDistanceTraveledAU()) / tripDist) : 0f;
|
||||
progress = Math.max(0f, Math.min(1f, progress));
|
||||
int sx = (int)(sp[0] + (dp[0] - sp[0]) * progress);
|
||||
int sy = (int)(sp[1] + (dp[1] - sp[1]) * progress);
|
||||
drawStation(g, sx, sy, C_STATION);
|
||||
}
|
||||
} else if (curIdx >= 0) {
|
||||
int[] pos = planetDrawCache.get(curIdx);
|
||||
if (pos != null) {
|
||||
PlanetInfo curPlanet = planetCache.get(curIdx);
|
||||
int ocx = cx, ocy = cy;
|
||||
if (curPlanet.isMoon()) {
|
||||
int parentIdx = indexById(curPlanet.parentPlanet().orElse(null));
|
||||
int[] parentPos = (parentIdx >= 0) ? planetDrawCache.get(parentIdx) : null;
|
||||
if (parentPos != null) { ocx = parentPos[0]; ocy = parentPos[1]; }
|
||||
}
|
||||
// Offset slightly along the orbit ring so station doesn't overlap the planet dot
|
||||
double baseAngle = Math.atan2(pos[1] - ocy, pos[0] - ocx);
|
||||
double stAngle = baseAngle + 0.35;
|
||||
int sx = ocx + (int)(pos[2] * Math.cos(stAngle));
|
||||
int sy = ocy + (int)(pos[2] * Math.sin(stAngle));
|
||||
drawStation(g, sx, sy, C_STATION);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Info panel ─────────────────────────────────────────────────────────
|
||||
|
||||
private void drawInfoPanel(GuiGraphics g, int mouseX, int mouseY) {
|
||||
var font = Minecraft.getInstance().font;
|
||||
int ix = leftPos + INFO_X;
|
||||
int iy = topPos + 2;
|
||||
|
||||
g.fill(ix, iy, ix + INFO_W, iy + ORRERY_H, C_PANEL);
|
||||
// Divider from orrery
|
||||
g.fill(ix - 2, iy + 6, ix - 1, iy + ORRERY_H - 6, C_BORDER);
|
||||
|
||||
int tx = ix + 5;
|
||||
int ty = iy + 6;
|
||||
|
||||
// Title
|
||||
String title = "COMPUTER";
|
||||
g.drawString(font, title, ix + (INFO_W - font.width(title)) / 2, ty, C_GOLD, false);
|
||||
ty += 10;
|
||||
g.fill(tx, ty, ix + INFO_W - 5, ty + 1, C_BORDER);
|
||||
ty += 5;
|
||||
|
||||
PartitionData data = PartitionClientState.get().map(p -> p.partitionData()).orElse(null);
|
||||
|
||||
// Resolve current planet for system + orbit display
|
||||
var currentPlanet = (data != null && data.getOrbit() != null)
|
||||
? net.xevianlight.aphelion.planet.PlanetCache.getByOrbitOrNull(data.getOrbit())
|
||||
: null;
|
||||
|
||||
// System
|
||||
String systemName = currentPlanet != null ? formatId(currentPlanet.system().location()) : "—";
|
||||
g.drawString(font, "System:", tx, ty, C_GRAY, false);
|
||||
ty += 9;
|
||||
g.drawString(font, systemName, tx + 2, ty, 0xFFCCCCFF, false);
|
||||
ty += 11;
|
||||
|
||||
// Orbit (last segment only — strips the "orbit/" prefix)
|
||||
String orbitName = (data != null && data.getOrbit() != null) ? formatLastSegment(data.getOrbit()) : "—";
|
||||
g.drawString(font, "Orbit:", tx, ty, C_GRAY, false);
|
||||
ty += 9;
|
||||
g.drawString(font, orbitName, tx + 2, ty, 0xFFAAAAFF, false);
|
||||
ty += 11;
|
||||
|
||||
// Destination (committed from PartitionData, or preview from selection)
|
||||
boolean hasDest = data != null && data.getDestination() != null;
|
||||
ResourceLocation destId = hasDest ? data.getDestination() : selectedPlanet;
|
||||
String destName = destId != null ? formatId(destId) : "—";
|
||||
int destColor = hasDest ? 0xFF88FF88 : selectedPlanet != null ? C_GOLD : C_GRAY;
|
||||
g.drawString(font, "Dest:", tx, ty, C_GRAY, false);
|
||||
ty += 9;
|
||||
g.drawString(font, destName, tx + 2, ty, destColor, false);
|
||||
ty += 10;
|
||||
|
||||
// Distance to destination (shown whenever a destination is selected or committed)
|
||||
if (destId != null && data != null) {
|
||||
double currentAU = data.getOrbitDistance();
|
||||
double destAU = currentAU; // fallback
|
||||
for (PlanetInfo pi : planetCache) {
|
||||
if (pi.id().equals(destId)) { destAU = pi.orbitDistance(); break; }
|
||||
}
|
||||
double dist = Math.abs(destAU - currentAU);
|
||||
String distLabel = dist < 0.01
|
||||
? "%.4f AU to dest".formatted(dist)
|
||||
: "%.2f AU to dest".formatted(dist);
|
||||
g.drawString(font, distLabel, tx + 2, ty, C_GRAY, false);
|
||||
ty += 10;
|
||||
}
|
||||
ty += 2;
|
||||
|
||||
g.fill(tx, ty, ix + INFO_W - 5, ty + 1, C_BORDER);
|
||||
ty += 4;
|
||||
|
||||
// Engines & Pads
|
||||
g.drawString(font, "Engines: " + menu.getEngineCount(), tx, ty, C_GRAY, false);
|
||||
ty += 10;
|
||||
g.drawString(font, "Pads: " + menu.getPadCount(), tx, ty, C_GRAY, false);
|
||||
ty += 13;
|
||||
|
||||
g.fill(tx, ty, ix + INFO_W - 5, ty + 1, C_BORDER);
|
||||
ty += 4;
|
||||
|
||||
// Travel status
|
||||
boolean traveling = data != null && data.isTraveling();
|
||||
g.drawString(font, traveling ? "TRAVELING" : "IDLE", tx, ty, traveling ? C_GOLD : C_GRAY, false);
|
||||
ty += 11;
|
||||
|
||||
if (traveling) {
|
||||
double tripDist = Math.abs(data.getTripDistanceAU());
|
||||
float progress = tripDist > 0
|
||||
? (float)(Math.abs(data.getDistanceTraveledAU()) / tripDist) : 0f;
|
||||
progress = Math.max(0f, Math.min(1f, progress));
|
||||
|
||||
int bw = INFO_W - 10;
|
||||
g.fill(tx, ty, tx + bw, ty + 7, C_PROG_BG);
|
||||
if (progress > 0) g.fill(tx, ty, tx + (int)(bw * progress), ty + 7, C_PROG_FILL);
|
||||
g.fill(tx, ty, tx + bw, ty + 1, C_BORDER);
|
||||
g.fill(tx, ty, tx + 1, ty + 7, C_BORDER);
|
||||
ty += 10;
|
||||
|
||||
String progStr = "%.2f / %.2f AU".formatted(
|
||||
Math.abs(data.getDistanceTraveledAU()),
|
||||
Math.abs(data.getTripDistanceAU()));
|
||||
g.drawString(font, progStr, tx, ty, C_GRAY, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Input ──────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
int orrX = leftPos + 2;
|
||||
int orrY = topPos + 2;
|
||||
|
||||
if (button == 0 && mouseX >= orrX && mouseX < orrX + ORRERY_W
|
||||
&& mouseY >= orrY && mouseY < orrY + ORRERY_H) {
|
||||
// Find nearest body within 10px
|
||||
ResourceLocation nearest = null;
|
||||
int nearestDist2 = 100;
|
||||
for (int i = 0; i < planetDrawCache.size(); i++) {
|
||||
int[] pos = planetDrawCache.get(i);
|
||||
if (pos == null) continue;
|
||||
int d2 = distSq((int)mouseX, (int)mouseY, pos[0], pos[1]);
|
||||
if (d2 < nearestDist2) { nearestDist2 = d2; nearest = planetCache.get(i).id(); }
|
||||
}
|
||||
|
||||
if (nearest == null) {
|
||||
// Clicked empty space: zoom out if zoomed in.
|
||||
zoomedSystem = null;
|
||||
} else if (zoomedSystem != null) {
|
||||
// In subsystem view: clicking any body selects it.
|
||||
selectedPlanet = nearest.equals(selectedPlanet) ? null : nearest;
|
||||
} else {
|
||||
// In system view: check if this body has moons.
|
||||
final ResourceLocation nearestFinal = nearest;
|
||||
boolean hasMoons = planetCache.stream().anyMatch(p ->
|
||||
p.isMoon() && p.parentPlanet().map(nearestFinal::equals).orElse(false));
|
||||
if (hasMoons) {
|
||||
// Zoom into this planet's subsystem; clear selection to avoid confusion.
|
||||
zoomedSystem = nearest;
|
||||
selectedPlanet = null;
|
||||
} else {
|
||||
selectedPlanet = nearest.equals(selectedPlanet) ? null : nearest;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.mouseClicked(mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
// ── Drawing helpers ────────────────────────────────────────────────────
|
||||
|
||||
private static void drawOrbitRing(GuiGraphics g, int cx, int cy, int r, int color) {
|
||||
int steps = Math.max(64, r * 6);
|
||||
for (int i = 0; i < steps; i++) {
|
||||
double a = 2.0 * Math.PI * i / steps;
|
||||
int x = cx + (int)Math.round(r * Math.cos(a));
|
||||
int y = cy + (int)Math.round(r * Math.sin(a));
|
||||
g.fill(x, y, x + 1, y + 1, color);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawDot(GuiGraphics g, int cx, int cy, int r, int color) {
|
||||
int r2 = r * r + r;
|
||||
for (int dy = -r; dy <= r; dy++)
|
||||
for (int dx = -r; dx <= r; dx++)
|
||||
if (dx * dx + dy * dy <= r2)
|
||||
g.fill(cx + dx, cy + dy, cx + dx + 1, cy + dy + 1, color);
|
||||
}
|
||||
|
||||
/** Small cross (station marker). */
|
||||
private static void drawStation(GuiGraphics g, int cx, int cy, int color) {
|
||||
g.fill(cx - 4, cy, cx + 5, cy + 1, color); // horizontal
|
||||
g.fill(cx, cy - 4, cx + 1, cy + 5, color); // vertical
|
||||
// Hollow center for clarity
|
||||
g.fill(cx - 1, cy - 1, cx + 2, cy + 2, 0xFF000000);
|
||||
g.fill(cx, cy, cx + 1, cy + 1, color);
|
||||
}
|
||||
|
||||
private static void drawDashedLine(GuiGraphics g, int x1, int y1, int x2, int y2, int color) {
|
||||
float dx = x2 - x1, dy = y2 - y1;
|
||||
int steps = (int)Math.sqrt(dx * dx + dy * dy);
|
||||
if (steps == 0) return;
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
if ((i / 3) % 2 == 0) {
|
||||
int x = x1 + (int)(dx * i / steps);
|
||||
int y = y1 + (int)(dy * i / steps);
|
||||
g.fill(x, y, x + 1, y + 1, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Utilities ──────────────────────────────────────────────────────────
|
||||
|
||||
// Log scale so inner planets (Mercury, Venus) aren't crushed to the center on a linear AU axis.
|
||||
private int orbitRadius(double orbitDist, double minDist, double maxDist) {
|
||||
if (maxDist <= minDist) return (MIN_ORBIT_R + MAX_ORBIT_R) / 2;
|
||||
double logMin = Math.log(Math.max(minDist, 0.001));
|
||||
double logMax = Math.log(Math.max(maxDist, 0.001));
|
||||
double logDist = Math.log(Math.max(orbitDist, 0.001));
|
||||
double t = (logDist - logMin) / (logMax - logMin);
|
||||
return (int)(MIN_ORBIT_R + t * (MAX_ORBIT_R - MIN_ORBIT_R));
|
||||
}
|
||||
|
||||
private int indexByOrbit(@Nullable ResourceLocation orbitRl) {
|
||||
if (orbitRl == null) return -1;
|
||||
for (int i = 0; i < planetCache.size(); i++)
|
||||
if (planetCache.get(i).orbit().equals(orbitRl)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int indexById(@Nullable ResourceLocation planetId) {
|
||||
if (planetId == null) return -1;
|
||||
for (int i = 0; i < planetCache.size(); i++)
|
||||
if (planetCache.get(i).id().equals(planetId)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int distSq(int x1, int y1, int x2, int y2) {
|
||||
int dx = x1 - x2, dy = y1 - y2;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
private static int blend(int base, int target, float t) {
|
||||
int ba = (base >> 24) & 0xFF, br = (base >> 16) & 0xFF,
|
||||
bg = (base >> 8) & 0xFF, bb = base & 0xFF;
|
||||
int ta = (target >> 24) & 0xFF, tr = (target >> 16) & 0xFF,
|
||||
tg = (target >> 8) & 0xFF, tb = target & 0xFF;
|
||||
return ((int)(ba + (ta - ba) * t) << 24)
|
||||
| ((int)(br + (tr - br) * t) << 16)
|
||||
| ((int)(bg + (tg - bg) * t) << 8)
|
||||
| (int)(bb + (tb - bb) * t);
|
||||
}
|
||||
|
||||
private static String formatId(ResourceLocation id) {
|
||||
String[] parts = id.getPath().split("[_/]");
|
||||
var sb = new StringBuilder();
|
||||
for (String p : parts) {
|
||||
if (p.isEmpty()) continue;
|
||||
if (sb.length() > 0) sb.append(' ');
|
||||
sb.append(Character.toUpperCase(p.charAt(0))).append(p.substring(1));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** Like formatId but only uses the last path segment, so orbit/earth → "Earth". */
|
||||
private static String formatLastSegment(ResourceLocation id) {
|
||||
String path = id.getPath();
|
||||
int slash = path.lastIndexOf('/');
|
||||
String name = slash >= 0 ? path.substring(slash + 1) : path;
|
||||
String[] parts = name.split("_");
|
||||
var sb = new StringBuilder();
|
||||
for (String p : parts) {
|
||||
if (p.isEmpty()) continue;
|
||||
if (sb.length() > 0) sb.append(' ');
|
||||
sb.append(Character.toUpperCase(p.charAt(0))).append(p.substring(1));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"custom_clouds": false,
|
||||
"custom_sky": true,
|
||||
"custom_weather": false,
|
||||
"dimension": "aphelion:space",
|
||||
"has_fog": false,
|
||||
"has_thick_fog": false,
|
||||
"render_in_rain": false,
|
||||
"sunrise_angle": 0,
|
||||
"sunrise_color": 14180147,
|
||||
"horizon_height": -128,
|
||||
"clear_color_scale": 1.0
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"custom_clouds": false,
|
||||
"custom_sky": true,
|
||||
"custom_weather": false,
|
||||
"dimension": "aphelion:space",
|
||||
"has_fog": false,
|
||||
"has_thick_fog": false,
|
||||
"render_in_rain": false,
|
||||
"sunrise_angle": 0,
|
||||
"sunrise_color": 14180147,
|
||||
"horizon_height": -128,
|
||||
"clear_color_scale": 1.0
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"custom_clouds": false,
|
||||
"custom_sky": true,
|
||||
"custom_weather": false,
|
||||
"dimension": "aphelion:space",
|
||||
"has_fog": false,
|
||||
"has_thick_fog": false,
|
||||
"render_in_rain": false,
|
||||
"sunrise_angle": 0,
|
||||
"sunrise_color": 14180147,
|
||||
"horizon_height": -128,
|
||||
"clear_color_scale": 1.0
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
"entity.aphelion.rocket": "Rocket",
|
||||
|
||||
"fluid_type.aphelion.oil": "Oil",
|
||||
"fluid_type.aphelion.rocket_fuel": "Rocket Fuel",
|
||||
|
||||
|
||||
"command.aphelion.station.orbit.set": "Set station (%s, %s)'s orbit to %s",
|
||||
@@ -59,5 +60,7 @@
|
||||
"command.aphelion.station.owner.unset": "Station has no owner",
|
||||
"command.aphelion.station.owner.get": "Station (%s %s) belongs to %s",
|
||||
"command.aphelion.station.owner.set.success": "Set station (%s %s)'s owner to %s",
|
||||
"command.aphelion.player.invalid": "Player is invalid"
|
||||
"command.aphelion.player.invalid": "Player is invalid",
|
||||
|
||||
"block.aphelion.station_flight_computer": "Station Flight Computer"
|
||||
}
|
||||
|
||||
9
src/main/resources/data/aphelion/planet/deimos.json
Normal file
9
src/main/resources/data/aphelion/planet/deimos.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"dimension": "aphelion:space",
|
||||
"orbit": "aphelion:orbit/deimos",
|
||||
"orbit_distance": 0.000157,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 0.003,
|
||||
"oxygen": false,
|
||||
"parent_planet": "aphelion:mars"
|
||||
}
|
||||
@@ -4,5 +4,5 @@
|
||||
"orbit_distance": 1,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 1,
|
||||
"oxygen": false
|
||||
"oxygen": true
|
||||
}
|
||||
8
src/main/resources/data/aphelion/planet/jupiter.json
Normal file
8
src/main/resources/data/aphelion/planet/jupiter.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:jupiter",
|
||||
"orbit": "aphelion:orbit/jupiter",
|
||||
"orbit_distance": 5.203,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 2.528,
|
||||
"oxygen": false
|
||||
}
|
||||
9
src/main/resources/data/aphelion/planet/luna.json
Normal file
9
src/main/resources/data/aphelion/planet/luna.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"dimension": "aphelion:space",
|
||||
"orbit": "aphelion:orbit/luna",
|
||||
"orbit_distance": 0.00257,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 0.165,
|
||||
"oxygen": false,
|
||||
"parent_planet": "aphelion:earth"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:mars",
|
||||
"orbit": "aphelion:orbit/mars",
|
||||
"orbit_distance": 1.5,
|
||||
"orbit_distance": 1.524,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 1,
|
||||
"gravity": 0.379,
|
||||
"oxygen": false
|
||||
}
|
||||
8
src/main/resources/data/aphelion/planet/mercury.json
Normal file
8
src/main/resources/data/aphelion/planet/mercury.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:mercury",
|
||||
"orbit": "aphelion:orbit/mercury",
|
||||
"orbit_distance": 0.387,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 0.38,
|
||||
"oxygen": false
|
||||
}
|
||||
8
src/main/resources/data/aphelion/planet/neptune.json
Normal file
8
src/main/resources/data/aphelion/planet/neptune.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:neptune",
|
||||
"orbit": "aphelion:orbit/neptune",
|
||||
"orbit_distance": 30.069,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 1.148,
|
||||
"oxygen": false
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"dimension": "minecraft:overworld",
|
||||
"orbit": "aphelion:orbit/overworld",
|
||||
"orbit_distance": 1,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 1,
|
||||
"oxygen": false
|
||||
}
|
||||
9
src/main/resources/data/aphelion/planet/phobos.json
Normal file
9
src/main/resources/data/aphelion/planet/phobos.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"dimension": "aphelion:space",
|
||||
"orbit": "aphelion:orbit/phobos",
|
||||
"orbit_distance": 0.0000627,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 0.0057,
|
||||
"oxygen": false,
|
||||
"parent_planet": "aphelion:mars"
|
||||
}
|
||||
8
src/main/resources/data/aphelion/planet/pluto.json
Normal file
8
src/main/resources/data/aphelion/planet/pluto.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:pluto",
|
||||
"orbit": "aphelion:orbit/pluto",
|
||||
"orbit_distance": 39.482,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 0.063,
|
||||
"oxygen": false
|
||||
}
|
||||
8
src/main/resources/data/aphelion/planet/saturn.json
Normal file
8
src/main/resources/data/aphelion/planet/saturn.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:saturn",
|
||||
"orbit": "aphelion:orbit/saturn",
|
||||
"orbit_distance": 9.537,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 1.065,
|
||||
"oxygen": false
|
||||
}
|
||||
8
src/main/resources/data/aphelion/planet/uranus.json
Normal file
8
src/main/resources/data/aphelion/planet/uranus.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:uranus",
|
||||
"orbit": "aphelion:orbit/uranus",
|
||||
"orbit_distance": 19.191,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 0.886,
|
||||
"oxygen": false
|
||||
}
|
||||
8
src/main/resources/data/aphelion/planet/venus.json
Normal file
8
src/main/resources/data/aphelion/planet/venus.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dimension": "aphelion:venus",
|
||||
"orbit": "aphelion:orbit/venus",
|
||||
"orbit_distance": 0.723,
|
||||
"star_system": "aphelion:sol",
|
||||
"gravity": 0.905,
|
||||
"oxygen": false
|
||||
}
|
||||
3
src/main/resources/data/aphelion/star_system/sol.json
Normal file
3
src/main/resources/data/aphelion/star_system/sol.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"temp": 5778
|
||||
}
|
||||
Reference in New Issue
Block a user