Create/src/main/java/com/simibubi/create/content/trains/track/TrackPlacement.java
IThundxr 7d3d4a1e7c
feat: Track Placement breaks flowers (#5821)
* feat: tracks break flowers during placement

* chore: cleanup import
2024-03-21 12:09:24 +01:00

777 lines
25 KiB
Java

package com.simibubi.create.content.trains.track;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.jozufozu.flywheel.util.Color;
import com.simibubi.create.AllSpecialTextures;
import com.simibubi.create.AllTags;
import com.simibubi.create.CreateClient;
import com.simibubi.create.content.equipment.blueprint.BlueprintOverlayRenderer;
import com.simibubi.create.foundation.block.ProperWaterloggedBlock;
import com.simibubi.create.foundation.utility.AngleHelper;
import com.simibubi.create.foundation.utility.BlockHelper;
import com.simibubi.create.foundation.utility.Couple;
import com.simibubi.create.foundation.utility.Iterate;
import com.simibubi.create.foundation.utility.Lang;
import com.simibubi.create.foundation.utility.Pair;
import com.simibubi.create.foundation.utility.VecHelper;
import com.simibubi.create.foundation.utility.animation.LerpedFloat;
import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser;
import com.simibubi.create.infrastructure.config.AllConfigs;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Direction.Axis;
import net.minecraft.core.Direction.AxisDirection;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.Mth;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.HitResult.Type;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.items.ItemHandlerHelper;
public class TrackPlacement {
public static class PlacementInfo {
public PlacementInfo(TrackMaterial material) {
this.trackMaterial = material;
}
BezierConnection curve = null;
boolean valid = false;
int end1Extent = 0;
int end2Extent = 0;
String message = null;
public int requiredTracks = 0;
public boolean hasRequiredTracks = false;
public int requiredPavement = 0;
public boolean hasRequiredPavement = false;
public final TrackMaterial trackMaterial;
// for visualisation
Vec3 end1;
Vec3 end2;
Vec3 normal1;
Vec3 normal2;
Vec3 axis1;
Vec3 axis2;
BlockPos pos1;
BlockPos pos2;
public PlacementInfo withMessage(String message) {
this.message = "track." + message;
return this;
}
public PlacementInfo tooJumbly() {
curve = null;
return this;
}
}
public static PlacementInfo cached;
static BlockPos hoveringPos;
static boolean hoveringMaxed;
static int hoveringAngle;
static ItemStack lastItem;
static int extraTipWarmup;
public static PlacementInfo tryConnect(Level level, Player player, BlockPos pos2, BlockState state2,
ItemStack stack, boolean girder, boolean maximiseTurn) {
Vec3 lookVec = player.getLookAngle();
int lookAngle = (int) (22.5 + AngleHelper.deg(Mth.atan2(lookVec.z, lookVec.x)) % 360) / 8;
int maxLength = AllConfigs.server().trains.maxTrackPlacementLength.get();
if (level.isClientSide && cached != null && pos2.equals(hoveringPos) && stack.equals(lastItem)
&& hoveringMaxed == maximiseTurn && lookAngle == hoveringAngle)
return cached;
PlacementInfo info = new PlacementInfo(TrackMaterial.fromItem(stack.getItem()));
hoveringMaxed = maximiseTurn;
hoveringAngle = lookAngle;
hoveringPos = pos2;
lastItem = stack;
cached = info;
ITrackBlock track = (ITrackBlock) state2.getBlock();
Pair<Vec3, AxisDirection> nearestTrackAxis = track.getNearestTrackAxis(level, pos2, state2, lookVec);
Vec3 axis2 = nearestTrackAxis.getFirst()
.scale(nearestTrackAxis.getSecond() == AxisDirection.POSITIVE ? -1 : 1);
Vec3 normal2 = track.getUpNormal(level, pos2, state2)
.normalize();
Vec3 normedAxis2 = axis2.normalize();
Vec3 end2 = track.getCurveStart(level, pos2, state2, axis2);
CompoundTag itemTag = stack.getTag();
CompoundTag selectionTag = itemTag.getCompound("ConnectingFrom");
BlockPos pos1 = NbtUtils.readBlockPos(selectionTag.getCompound("Pos"));
Vec3 axis1 = VecHelper.readNBT(selectionTag.getList("Axis", Tag.TAG_DOUBLE));
Vec3 normedAxis1 = axis1.normalize();
Vec3 end1 = VecHelper.readNBT(selectionTag.getList("End", Tag.TAG_DOUBLE));
Vec3 normal1 = VecHelper.readNBT(selectionTag.getList("Normal", Tag.TAG_DOUBLE));
boolean front1 = selectionTag.getBoolean("Front");
BlockState state1 = level.getBlockState(pos1);
if (level.isClientSide) {
info.end1 = end1;
info.end2 = end2;
info.normal1 = normal1;
info.normal2 = normal2;
info.axis1 = axis1;
info.axis2 = axis2;
}
if (pos1.equals(pos2))
return info.withMessage("second_point");
if (pos1.distSqr(pos2) > maxLength * maxLength)
return info.withMessage("too_far")
.tooJumbly();
if (!state1.hasProperty(TrackBlock.HAS_BE))
return info.withMessage("original_missing");
if (level.getBlockEntity(pos2) instanceof TrackBlockEntity tbe && tbe.isTilted())
return info.withMessage("turn_start");
if (axis1.dot(end2.subtract(end1)) < 0) {
axis1 = axis1.scale(-1);
normedAxis1 = normedAxis1.scale(-1);
front1 = !front1;
end1 = track.getCurveStart(level, pos1, state1, axis1);
if (level.isClientSide) {
info.end1 = end1;
info.axis1 = axis1;
}
}
double[] intersect = VecHelper.intersect(end1, end2, normedAxis1, normedAxis2, Axis.Y);
boolean parallel = intersect == null;
boolean skipCurve = false;
if ((parallel && normedAxis1.dot(normedAxis2) > 0) || (!parallel && (intersect[0] < 0 || intersect[1] < 0))) {
axis2 = axis2.scale(-1);
normedAxis2 = normedAxis2.scale(-1);
end2 = track.getCurveStart(level, pos2, state2, axis2);
if (level.isClientSide) {
info.end2 = end2;
info.axis2 = axis2;
}
}
Vec3 cross2 = normedAxis2.cross(new Vec3(0, 1, 0));
double a1 = Mth.atan2(normedAxis2.z, normedAxis2.x);
double a2 = Mth.atan2(normedAxis1.z, normedAxis1.x);
double angle = a1 - a2;
double ascend = end2.subtract(end1).y;
double absAscend = Math.abs(ascend);
boolean slope = !normal1.equals(normal2);
if (level.isClientSide) {
Vec3 offset1 = axis1.scale(info.end1Extent);
Vec3 offset2 = axis2.scale(info.end2Extent);
BlockPos targetPos1 = pos1.offset(offset1.x, offset1.y, offset1.z);
BlockPos targetPos2 = pos2.offset(offset2.x, offset2.y, offset2.z);
info.curve = new BezierConnection(Couple.create(targetPos1, targetPos2),
Couple.create(end1.add(offset1), end2.add(offset2)), Couple.create(normedAxis1, normedAxis2),
Couple.create(normal1, normal2), true, girder, TrackMaterial.fromItem(stack.getItem()));
}
// S curve or Straight
double dist = 0;
if (parallel) {
double[] sTest = VecHelper.intersect(end1, end2, normedAxis1, cross2, Axis.Y);
if (sTest != null) {
double t = Math.abs(sTest[0]);
double u = Math.abs(sTest[1]);
skipCurve = Mth.equal(u, 0);
if (!skipCurve && sTest[0] < 0)
return info.withMessage("perpendicular")
.tooJumbly();
if (skipCurve) {
dist = VecHelper.getCenterOf(pos1)
.distanceTo(VecHelper.getCenterOf(pos2));
info.end1Extent = (int) Math.round((dist + 1) / axis1.length());
} else {
if (!Mth.equal(ascend, 0) || normedAxis1.y != 0)
return info.withMessage("ascending_s_curve");
double targetT = u <= 1 ? 3 : u * 2;
if (t < targetT)
return info.withMessage("too_sharp");
// This is for standardising s curve sizes
if (t > targetT) {
int correction = (int) ((t - targetT) / axis1.length());
info.end1Extent = maximiseTurn ? 0 : correction / 2 + (correction % 2);
info.end2Extent = maximiseTurn ? 0 : correction / 2;
}
}
}
}
// Slope
if (slope) {
if (!skipCurve)
return info.withMessage("slope_turn");
if (Mth.equal(normal1.dot(normal2), 0))
return info.withMessage("opposing_slopes");
if ((axis1.y < 0 || axis2.y > 0) && ascend > 0)
return info.withMessage("leave_slope_ascending");
if ((axis1.y > 0 || axis2.y < 0) && ascend < 0)
return info.withMessage("leave_slope_descending");
skipCurve = false;
info.end1Extent = 0;
info.end2Extent = 0;
Axis plane = Mth.equal(axis1.x, 0) ? Axis.X : Axis.Z;
intersect = VecHelper.intersect(end1, end2, normedAxis1, normedAxis2, plane);
double dist1 = Math.abs(intersect[0] / axis1.length());
double dist2 = Math.abs(intersect[1] / axis2.length());
if (dist1 > dist2)
info.end1Extent = (int) Math.round(dist1 - dist2);
if (dist2 > dist1)
info.end2Extent = (int) Math.round(dist2 - dist1);
double turnSize = Math.min(dist1, dist2);
if (intersect[0] < 0 || intersect[1] < 0)
return info.withMessage("too_sharp")
.tooJumbly();
if (turnSize < 2)
return info.withMessage("too_sharp");
// This is for standardising curve sizes
if (turnSize > 2 && !maximiseTurn) {
info.end1Extent += turnSize - 2;
info.end2Extent += turnSize - 2;
turnSize = 2;
}
}
// Straight ascend
if (skipCurve && !Mth.equal(ascend, 0)) {
int hDistance = info.end1Extent;
if (axis1.y == 0 || !Mth.equal(absAscend + 1, dist / axis1.length())) {
if (axis1.y != 0 && axis1.y == -axis2.y)
return info.withMessage("ascending_s_curve");
info.end1Extent = 0;
double minHDistance = Math.max(absAscend < 4 ? absAscend * 4 : absAscend * 3, 6) / axis1.length();
if (hDistance < minHDistance)
return info.withMessage("too_steep");
if (hDistance > minHDistance) {
int correction = (int) (hDistance - minHDistance);
info.end1Extent = maximiseTurn ? 0 : correction / 2 + (correction % 2);
info.end2Extent = maximiseTurn ? 0 : correction / 2;
}
skipCurve = false;
}
}
// Turn
if (!parallel) {
float absAngle = Math.abs(AngleHelper.deg(angle));
if (absAngle < 60 || absAngle > 300)
return info.withMessage("turn_90")
.tooJumbly();
intersect = VecHelper.intersect(end1, end2, normedAxis1, normedAxis2, Axis.Y);
double dist1 = Math.abs(intersect[0]);
double dist2 = Math.abs(intersect[1]);
float ex1 = 0;
float ex2 = 0;
if (dist1 > dist2)
ex1 = (float) ((dist1 - dist2) / axis1.length());
if (dist2 > dist1)
ex2 = (float) ((dist2 - dist1) / axis2.length());
double turnSize = Math.min(dist1, dist2) - .1d;
boolean ninety = (absAngle + .25f) % 90 < 1;
if (intersect[0] < 0 || intersect[1] < 0)
return info.withMessage("too_sharp")
.tooJumbly();
double minTurnSize = ninety ? 7 : 3.25;
double turnSizeToFitAscend =
minTurnSize + (ninety ? Math.max(0, absAscend - 3) * 2f : Math.max(0, absAscend - 1.5f) * 1.5f);
if (turnSize < minTurnSize)
return info.withMessage("too_sharp");
if (turnSize < turnSizeToFitAscend)
return info.withMessage("too_steep");
// This is for standardising curve sizes
if (!maximiseTurn) {
ex1 += (turnSize - turnSizeToFitAscend) / axis1.length();
ex2 += (turnSize - turnSizeToFitAscend) / axis2.length();
}
info.end1Extent = Mth.floor(ex1);
info.end2Extent = Mth.floor(ex2);
turnSize = turnSizeToFitAscend;
}
Vec3 offset1 = axis1.scale(info.end1Extent);
Vec3 offset2 = axis2.scale(info.end2Extent);
BlockPos targetPos1 = pos1.offset(offset1.x, offset1.y, offset1.z);
BlockPos targetPos2 = pos2.offset(offset2.x, offset2.y, offset2.z);
info.curve = skipCurve ? null
: new BezierConnection(Couple.create(targetPos1, targetPos2),
Couple.create(end1.add(offset1), end2.add(offset2)), Couple.create(normedAxis1, normedAxis2),
Couple.create(normal1, normal2), true, girder, TrackMaterial.fromItem(stack.getItem()));
info.valid = true;
info.pos1 = pos1;
info.pos2 = pos2;
info.axis1 = axis1;
info.axis2 = axis2;
placeTracks(level, info, state1, state2, targetPos1, targetPos2, true);
ItemStack offhandItem = player.getOffhandItem()
.copy();
boolean shouldPave = offhandItem.getItem() instanceof BlockItem;
if (shouldPave) {
BlockItem paveItem = (BlockItem) offhandItem.getItem();
paveTracks(level, info, paveItem, true);
info.hasRequiredPavement = true;
}
info.hasRequiredTracks = true;
if (!player.isCreative()) {
for (boolean simulate : Iterate.trueAndFalse) {
if (level.isClientSide && !simulate)
break;
int tracks = info.requiredTracks;
int pavement = info.requiredPavement;
int foundTracks = 0;
int foundPavement = 0;
Inventory inv = player.getInventory();
int size = inv.items.size();
for (int j = 0; j <= size + 1; j++) {
int i = j;
boolean offhand = j == size + 1;
if (j == size)
i = inv.selected;
else if (offhand)
i = 0;
else if (j == inv.selected)
continue;
ItemStack stackInSlot = (offhand ? inv.offhand : inv.items).get(i);
boolean isTrack = AllTags.AllBlockTags.TRACKS.matches(stackInSlot) && stackInSlot.is(stack.getItem());
if (!isTrack && (!shouldPave || offhandItem.getItem() != stackInSlot.getItem()))
continue;
if (isTrack ? foundTracks >= tracks : foundPavement >= pavement)
continue;
int count = stackInSlot.getCount();
if (!simulate) {
int remainingItems =
count - Math.min(isTrack ? tracks - foundTracks : pavement - foundPavement, count);
if (i == inv.selected)
stackInSlot.setTag(null);
ItemStack newItem = ItemHandlerHelper.copyStackWithSize(stackInSlot, remainingItems);
if (offhand)
player.setItemInHand(InteractionHand.OFF_HAND, newItem);
else
inv.setItem(i, newItem);
}
if (isTrack)
foundTracks += count;
else
foundPavement += count;
}
if (simulate && foundTracks < tracks) {
info.valid = false;
info.tooJumbly();
info.hasRequiredTracks = false;
return info.withMessage("not_enough_tracks");
}
if (simulate && foundPavement < pavement) {
info.valid = false;
info.tooJumbly();
info.hasRequiredPavement = false;
return info.withMessage("not_enough_pavement");
}
}
}
if (level.isClientSide())
return info;
if (shouldPave) {
BlockItem paveItem = (BlockItem) offhandItem.getItem();
paveTracks(level, info, paveItem, false);
}
return placeTracks(level, info, state1, state2, targetPos1, targetPos2, false);
}
private static void paveTracks(Level level, PlacementInfo info, BlockItem blockItem, boolean simulate) {
Block block = blockItem.getBlock();
info.requiredPavement = 0;
if (block == null || block instanceof EntityBlock || block.defaultBlockState()
.getCollisionShape(level, info.pos1)
.isEmpty())
return;
Set<BlockPos> visited = new HashSet<>();
for (boolean first : Iterate.trueAndFalse) {
int extent = (first ? info.end1Extent : info.end2Extent) + (info.curve != null ? 1 : 0);
Vec3 axis = first ? info.axis1 : info.axis2;
BlockPos pavePos = first ? info.pos1 : info.pos2;
info.requiredPavement +=
TrackPaver.paveStraight(level, pavePos.below(), axis, extent, block, simulate, visited);
}
if (info.curve != null)
info.requiredPavement += TrackPaver.paveCurve(level, info.curve, block, simulate, visited);
}
private static PlacementInfo placeTracks(Level level, PlacementInfo info, BlockState state1, BlockState state2,
BlockPos targetPos1, BlockPos targetPos2, boolean simulate) {
info.requiredTracks = 0;
for (boolean first : Iterate.trueAndFalse) {
int extent = first ? info.end1Extent : info.end2Extent;
Vec3 axis = first ? info.axis1 : info.axis2;
BlockPos pos = first ? info.pos1 : info.pos2;
BlockState state = first ? state1 : state2;
if (state.hasProperty(TrackBlock.HAS_BE) && !simulate)
state = state.setValue(TrackBlock.HAS_BE, false);
switch (state.getValue(TrackBlock.SHAPE)) {
case TE, TW:
state = state.setValue(TrackBlock.SHAPE, TrackShape.XO);
break;
case TN, TS:
state = state.setValue(TrackBlock.SHAPE, TrackShape.ZO);
break;
default:
break;
}
for (int i = 0; i < (info.curve != null ? extent + 1 : extent); i++) {
Vec3 offset = axis.scale(i);
BlockPos offsetPos = pos.offset(offset.x, offset.y, offset.z);
BlockState stateAtPos = level.getBlockState(offsetPos);
// copy over all shared properties from the shaped state to the correct track material block
BlockState toPlace = BlockHelper.copyProperties(state, info.trackMaterial.getBlock().defaultBlockState());
boolean canPlace = stateAtPos.getMaterial().isReplaceable() || stateAtPos.is(BlockTags.FLOWERS);
if (canPlace)
info.requiredTracks++;
if (simulate)
continue;
if (stateAtPos.getBlock() instanceof ITrackBlock trackAtPos) {
toPlace = trackAtPos.overlay(level, offsetPos, stateAtPos, toPlace);
canPlace = true;
}
if (canPlace)
level.setBlock(offsetPos, ProperWaterloggedBlock.withWater(level, toPlace, offsetPos), 3);
}
}
if (info.curve == null)
return info;
if (!simulate) {
BlockState onto = info.trackMaterial.getBlock().defaultBlockState();
BlockState stateAtPos = level.getBlockState(targetPos1);
level.setBlock(targetPos1, ProperWaterloggedBlock.withWater(level,
(AllTags.AllBlockTags.TRACKS.matches(stateAtPos) ? stateAtPos : BlockHelper.copyProperties(state1, onto))
.setValue(TrackBlock.HAS_BE, true), targetPos1), 3);
stateAtPos = level.getBlockState(targetPos2);
level.setBlock(targetPos2, ProperWaterloggedBlock.withWater(level,
(AllTags.AllBlockTags.TRACKS.matches(stateAtPos) ? stateAtPos : BlockHelper.copyProperties(state2, onto))
.setValue(TrackBlock.HAS_BE, true), targetPos2), 3);
}
BlockEntity te1 = level.getBlockEntity(targetPos1);
BlockEntity te2 = level.getBlockEntity(targetPos2);
int requiredTracksForTurn = (info.curve.getSegmentCount() + 1) / 2;
if (!(te1 instanceof TrackBlockEntity) || !(te2 instanceof TrackBlockEntity)) {
info.requiredTracks += requiredTracksForTurn;
return info;
}
TrackBlockEntity tte1 = (TrackBlockEntity) te1;
TrackBlockEntity tte2 = (TrackBlockEntity) te2;
if (!tte1.getConnections()
.containsKey(tte2.getBlockPos()))
info.requiredTracks += requiredTracksForTurn;
if (simulate)
return info;
tte1.addConnection(info.curve);
tte2.addConnection(info.curve.secondary());
tte1.tilt.tryApplySmoothing();
tte2.tilt.tryApplySmoothing();
return info;
}
static LerpedFloat animation = LerpedFloat.linear()
.startWithValue(0);
static int lastLineCount = 0;
static BlockPos hintPos;
static int hintAngle;
static Couple<List<BlockPos>> hints;
@OnlyIn(Dist.CLIENT)
public static void clientTick() {
LocalPlayer player = Minecraft.getInstance().player;
ItemStack stack = player.getMainHandItem();
HitResult hitResult = Minecraft.getInstance().hitResult;
int restoreWarmup = extraTipWarmup;
extraTipWarmup = 0;
if (hitResult == null)
return;
if (hitResult.getType() != Type.BLOCK)
return;
InteractionHand hand = InteractionHand.MAIN_HAND;
if (!AllTags.AllBlockTags.TRACKS.matches(stack)) {
stack = player.getOffhandItem();
hand = InteractionHand.OFF_HAND;
if (!AllTags.AllBlockTags.TRACKS.matches(stack))
return;
}
if (!stack.hasFoil())
return;
TrackBlockItem blockItem = (TrackBlockItem) stack.getItem();
Level level = player.level;
BlockHitResult bhr = (BlockHitResult) hitResult;
BlockPos pos = bhr.getBlockPos();
BlockState hitState = level.getBlockState(pos);
if (!(hitState.getBlock() instanceof TrackBlock) && !hitState.getMaterial()
.isReplaceable()) {
pos = pos.relative(bhr.getDirection());
hitState = blockItem.getPlacementState(new UseOnContext(player, hand, bhr));
if (hitState == null)
return;
}
if (!(hitState.getBlock() instanceof TrackBlock))
return;
extraTipWarmup = restoreWarmup;
boolean maxTurns = Minecraft.getInstance().options.keySprint.isDown();
PlacementInfo info = tryConnect(level, player, pos, hitState, stack, false, maxTurns);
if (extraTipWarmup < 20)
extraTipWarmup++;
if (!info.valid || !hoveringMaxed && (info.end1Extent == 0 || info.end2Extent == 0))
extraTipWarmup = 0;
if (!player.isCreative() && (info.valid || !info.hasRequiredTracks || !info.hasRequiredPavement))
BlueprintOverlayRenderer.displayTrackRequirements(info, player.getOffhandItem());
if (info.valid)
player.displayClientMessage(Lang.translateDirect("track.valid_connection")
.withStyle(ChatFormatting.GREEN), true);
else if (info.message != null)
player.displayClientMessage(Lang.translateDirect(info.message)
.withStyle(info.message.equals("track.second_point") ? ChatFormatting.WHITE : ChatFormatting.RED),
true);
if (bhr.getDirection() == Direction.UP) {
Vec3 lookVec = player.getLookAngle();
int lookAngle = (int) (22.5 + AngleHelper.deg(Mth.atan2(lookVec.z, lookVec.x)) % 360) / 8;
if (!pos.equals(hintPos) || lookAngle != hintAngle) {
hints = Couple.create(ArrayList::new);
hintAngle = lookAngle;
hintPos = pos;
for (int xOffset = -2; xOffset <= 2; xOffset++) {
for (int zOffset = -2; zOffset <= 2; zOffset++) {
BlockPos offset = pos.offset(xOffset, 0, zOffset);
PlacementInfo adjInfo = tryConnect(level, player, offset, hitState, stack, false, maxTurns);
hints.get(adjInfo.valid)
.add(offset.below());
}
}
}
if (hints != null && !hints.either(Collection::isEmpty)) {
CreateClient.OUTLINER.showCluster("track_valid", hints.getFirst())
.withFaceTexture(AllSpecialTextures.THIN_CHECKERED)
.colored(0x95CD41)
.lineWidth(0);
CreateClient.OUTLINER.showCluster("track_invalid", hints.getSecond())
.withFaceTexture(AllSpecialTextures.THIN_CHECKERED)
.colored(0xEA5C2B)
.lineWidth(0);
}
}
animation.chase(info.valid ? 1 : 0, 0.25, Chaser.EXP);
animation.tickChaser();
if (!info.valid) {
info.end1Extent = 0;
info.end2Extent = 0;
}
int color = Color.mixColors(0xEA5C2B, 0x95CD41, animation.getValue());
Vec3 up = new Vec3(0, 4 / 16f, 0);
{
Vec3 v1 = info.end1;
Vec3 a1 = info.axis1.normalize();
Vec3 n1 = info.normal1.cross(a1)
.scale(15 / 16f);
Vec3 o1 = a1.scale(0.125f);
Vec3 ex1 =
a1.scale((info.end1Extent - (info.curve == null && info.end1Extent > 0 ? 2 : 0)) * info.axis1.length());
line(1, v1.add(n1)
.add(up), o1, ex1);
line(2, v1.subtract(n1)
.add(up), o1, ex1);
Vec3 v2 = info.end2;
Vec3 a2 = info.axis2.normalize();
Vec3 n2 = info.normal2.cross(a2)
.scale(15 / 16f);
Vec3 o2 = a2.scale(0.125f);
Vec3 ex2 = a2.scale(info.end2Extent * info.axis2.length());
line(3, v2.add(n2)
.add(up), o2, ex2);
line(4, v2.subtract(n2)
.add(up), o2, ex2);
}
BezierConnection bc = info.curve;
if (bc == null)
return;
Vec3 previous1 = null;
Vec3 previous2 = null;
int railcolor = color;
int segCount = bc.getSegmentCount();
float s = animation.getValue() * 7 / 8f + 1 / 8f;
float lw = animation.getValue() * 1 / 16f + 1 / 16f;
Vec3 end1 = bc.starts.getFirst();
Vec3 end2 = bc.starts.getSecond();
Vec3 finish1 = end1.add(bc.axes.getFirst()
.scale(bc.getHandleLength()));
Vec3 finish2 = end2.add(bc.axes.getSecond()
.scale(bc.getHandleLength()));
String key = "curve";
for (int i = 0; i <= segCount; i++) {
float t = i / (float) segCount;
Vec3 result = VecHelper.bezier(end1, end2, finish1, finish2, t);
Vec3 derivative = VecHelper.bezierDerivative(end1, end2, finish1, finish2, t)
.normalize();
Vec3 normal = bc.getNormal(t)
.cross(derivative)
.scale(15 / 16f);
Vec3 rail1 = result.add(normal)
.add(up);
Vec3 rail2 = result.subtract(normal)
.add(up);
if (previous1 != null) {
Vec3 middle1 = rail1.add(previous1)
.scale(0.5f);
Vec3 middle2 = rail2.add(previous2)
.scale(0.5f);
CreateClient.OUTLINER
.showLine(Pair.of(key, i * 2), VecHelper.lerp(s, middle1, previous1),
VecHelper.lerp(s, middle1, rail1))
.colored(railcolor)
.disableLineNormals()
.lineWidth(lw);
CreateClient.OUTLINER
.showLine(Pair.of(key, i * 2 + 1), VecHelper.lerp(s, middle2, previous2),
VecHelper.lerp(s, middle2, rail2))
.colored(railcolor)
.disableLineNormals()
.lineWidth(lw);
}
previous1 = rail1;
previous2 = rail2;
}
for (int i = segCount + 1; i <= lastLineCount; i++) {
CreateClient.OUTLINER.remove(Pair.of(key, i * 2));
CreateClient.OUTLINER.remove(Pair.of(key, i * 2 + 1));
}
lastLineCount = segCount;
}
@OnlyIn(Dist.CLIENT)
private static void line(int id, Vec3 v1, Vec3 o1, Vec3 ex) {
int color = Color.mixColors(0xEA5C2B, 0x95CD41, animation.getValue());
CreateClient.OUTLINER.showLine(Pair.of("start", id), v1.subtract(o1), v1.add(ex))
.lineWidth(1 / 8f)
.disableLineNormals()
.colored(color);
}
}