Files
VS--Improved-Aerodynamics/src/main/java/dev/lumareth/improvedAerodynamics/exhaust/NozzleBlockEntity.java
T
richard 70eb237d70 Add vectored afterburner nozzle with afterburner key and exhaust style
- Add vectored afterburner nozzle block with AzureLib model, deflection, throttle and afterburner state
- Drive nozzles from the vehicle control node and map pitch, roll and yaw to differential deflection
- Add afterburner toggle key and packet, linked to the control node
- Replace Warium exhaust effect entities with improved flames, particles and sounds when exhaustStyle is improved
- Add /aero seedgen to measure the air ceiling for the air placeholder
- Upgrade Warium VS to 1.1.7 and make the lift use horizontal speed
2026-11-11 18:00:46 +01:00

282 lines
10 KiB
Java

package dev.lumareth.improvedAerodynamics.exhaust;
import dev.lumareth.improvedAerodynamics.Config;
import dev.lumareth.improvedAerodynamics.exhaust.client.NozzleClientEffects;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.DirectionalBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.Nullable;
/**
* State of one vectored afterburner nozzle. The server owns it: whatever drives the nozzle (a debug command now, flight
* logic later) calls {@link #setDeflectionTarget}, {@link #setThrottle} and {@link #setAfterburner}, the rest follows
* from them. Everything the client needs to draw the nozzle is in the update tag, so players that join or reload the
* chunk see the right state.
*/
public class NozzleBlockEntity extends BlockEntity {
// Warium's throttle goes from 0 to 10
private static final float MAX_NODE_THROTTLE = 10.0F;
private static final boolean WARIUM_LOADED = net.minecraftforge.fml.ModList.get().isLoaded("valkyrien_warium");
// What changes are worth an update packet: a quarter degree, two percent throttle
private static final float DEFLECTION_STEP = (float) Math.toRadians(0.25);
private static final float THROTTLE_STEP = 0.02F;
private float deflectionTarget;
private float deflectionPrevious;
private float deflectionCurrent;
private float throttle;
private boolean afterburnerRequested;
private boolean afterburnerAuto;
private boolean afterburner;
private boolean rightEngine;
// Last values sent to the clients
private int sentDeflection;
private int sentThrottle;
private boolean sentAfterburner;
private boolean sentRightEngine;
/** The client's sound and particle state, owned by {@link NozzleClientEffects}. */
@Nullable
private Object clientEffects;
public NozzleBlockEntity(BlockPos pos, BlockState state) {
super(NozzleContent.NOZZLE_ENTITY.get(), pos, state);
}
// ---- API ----
/** Sets where the nozzle should swing to, in radians, positive turns the nozzle's rotation bone positively. */
public void setDeflectionTarget(float radians) {
float limit = (float) Math.toRadians(Config.NOZZLE_MAX_DEFLECTION.get());
deflectionTarget = Math.max(-limit, Math.min(limit, radians));
changed();
}
/** Sets the engine throttle, 0 to 1. Only matters for a nozzle that is not linked to a control node. */
public void setThrottle(float throttle) {
this.throttle = Math.max(0.0F, Math.min(1.0F, throttle));
changed();
}
/** Forces the afterburner on, independent of the control node. Switching it off leaves it to the afterburner key again. */
public void setAfterburner(boolean on) {
afterburnerRequested = on;
changed();
}
public void setRightEngine(boolean right) {
rightEngine = right;
changed();
}
public float getDeflectionTarget() {
return deflectionTarget;
}
/** The deflection being drawn, eased towards the target, interpolated between ticks. */
public float getDeflection(float partialTick) {
return deflectionPrevious + (deflectionCurrent - deflectionPrevious) * partialTick;
}
public float getThrottle() {
return throttle;
}
public boolean isAfterburner() {
return afterburner;
}
public boolean isRightEngine() {
return rightEngine;
}
public Direction getFacing() {
return getBlockState().getValue(DirectionalBlock.FACING);
}
// ---- Ticking ----
public static void tick(Level level, BlockPos pos, BlockState state, NozzleBlockEntity nozzle) {
nozzle.tick(level);
}
private void tick(Level level) {
if (!level.isClientSide) {
drive(level);
}
float maxStep = (float) Math.toRadians(Config.NOZZLE_SLEW_RATE.get() / 20.0);
deflectionPrevious = deflectionCurrent;
deflectionCurrent += Math.max(-maxStep, Math.min(maxStep, deflectionTarget - deflectionCurrent));
if (level.isClientSide) {
NozzleClientEffects.tick(this);
return;
}
boolean lit = afterburnerRequested || afterburnerAuto;
if (lit != afterburner) {
afterburner = lit;
setChanged();
}
sync();
boolean light = lit && Config.NOZZLE_LIGHT.get();
BlockState state = getBlockState();
if (state.getValue(NozzleBlock.LIT) != light) {
level.setBlock(worldPosition, state.setValue(NozzleBlock.LIT, light), 3);
}
if (WARIUM_LOADED && level instanceof ServerLevel serverLevel) {
ExhaustThrust.apply(serverLevel, worldPosition, getFacing(),
NozzleGeometry.exhaustDirection(getFacing(), deflectionCurrent, rightEngine));
}
}
/**
* Takes throttle, deflection and afterburner from the linked control node. A nozzle without a link keeps what the
* API or the debug command gave it.
*/
private void drive(Level level) {
NozzleControl.Inputs inputs = NozzleControl.read(level, this);
if (inputs == null) {
afterburnerAuto = false;
return;
}
// No flame without a running engine behind the nozzle: the jet exhaust there hands over its power
boolean running = !WARIUM_LOADED || ExhaustThrust.power(level, worldPosition, getFacing()) > 0.0;
throttle = running ? Math.min(1.0F, (float) inputs.throttle() / MAX_NODE_THROTTLE) : 0.0F;
float pitch = (float) inputs.pitchUp();
float roll = (float) inputs.rollRight();
deflectionTarget = NozzleMixing.deflection(pitch, roll, (float) inputs.yawRight(), rightEngine);
double minimum = Config.AFTERBURNER_MIN_THROTTLE.get();
if (inputs.afterburnerSwitch() && inputs.throttle() < minimum) {
// Too little throttle: out, and it stays out until the key is pressed again
inputs.node().getPersistentData().putBoolean(AfterburnerToggle.TAG, false);
inputs.node().setChanged();
}
afterburnerAuto = running && inputs.afterburnerSwitch() && inputs.throttle() >= minimum;
}
/** Saves the state, and tells the clients if something they can see changed. */
private void changed() {
setChanged();
sync();
}
private void sync() {
if (level == null || level.isClientSide) {
return;
}
int deflection = Math.round(deflectionTarget / DEFLECTION_STEP);
int throttleStep = Math.round(throttle / THROTTLE_STEP);
if (deflection != sentDeflection || throttleStep != sentThrottle || afterburner != sentAfterburner || rightEngine != sentRightEngine) {
sentDeflection = deflection;
sentThrottle = throttleStep;
sentAfterburner = afterburner;
sentRightEngine = rightEngine;
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), UPDATE_CLIENTS);
}
}
private static final int UPDATE_CLIENTS = 2;
// ---- Saving and syncing ----
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
tag.putFloat("Deflection", deflectionTarget);
tag.putFloat("Throttle", throttle);
tag.putBoolean("Afterburner", afterburner);
tag.putBoolean("AfterburnerRequested", afterburnerRequested);
tag.putBoolean("RightEngine", rightEngine);
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
deflectionTarget = tag.getFloat("Deflection");
throttle = tag.getFloat("Throttle");
afterburner = tag.getBoolean("Afterburner");
afterburnerRequested = tag.getBoolean("AfterburnerRequested");
afterburnerAuto = afterburner && !afterburnerRequested;
rightEngine = tag.getBoolean("RightEngine");
if (level == null || !level.isClientSide || deflectionCurrent == 0.0F && deflectionPrevious == 0.0F) {
// Loaded fresh: start at the target instead of swinging in from zero
deflectionCurrent = deflectionTarget;
deflectionPrevious = deflectionTarget;
}
sentDeflection = Math.round(deflectionTarget / DEFLECTION_STEP);
sentThrottle = Math.round(throttle / THROTTLE_STEP);
sentAfterburner = afterburner;
sentRightEngine = rightEngine;
}
@Override
public CompoundTag getUpdateTag() {
return saveWithoutMetadata();
}
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public void onDataPacket(Connection connection, ClientboundBlockEntityDataPacket packet) {
CompoundTag tag = packet.getTag();
if (tag != null) {
// Warium's key configuration screen reads the keys from the block's persistent data on the client, so that
// has to arrive too. Not load() as a whole: the deflection on screen must keep easing from where it is
super.load(tag);
deflectionTarget = tag.getFloat("Deflection");
throttle = tag.getFloat("Throttle");
afterburner = tag.getBoolean("Afterburner");
rightEngine = tag.getBoolean("RightEngine");
}
}
// ---- Client ----
@Nullable
public Object getClientEffects() {
return clientEffects;
}
public void setClientEffects(@Nullable Object clientEffects) {
this.clientEffects = clientEffects;
}
@Override
public AABB getRenderBoundingBox() {
// The nozzle swings out of the block and the flame trails behind it
return new AABB(worldPosition).inflate(2.0);
}
@Override
public void setRemoved() {
super.setRemoved();
stopEffects();
}
@Override
public void onChunkUnloaded() {
super.onChunkUnloaded();
stopEffects();
}
private void stopEffects() {
if (level != null && level.isClientSide) {
NozzleClientEffects.stop(this);
}
}
}