feat(client): gun pipper with ground impact point and radar lead marker

Draws where the shells of the first light autocannon come down and, for the
selected radar target, a lead pipper. Uses the same ballistics as Warium's
LACFireScript (muzzle speed, barrels, drag 0.99, gravity 0.05).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
Claude committed 2026-11-11 12:02:47 +01:00
1 parent 1e659816b8
commit 9e7aae5773
7 files changed
+692

No files matched your search

@@ -1,6 +1,7 @@
package dev.lumareth.improvedAerodynamics;
import com.mojang.logging.LogUtils;
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
import dev.lumareth.improvedAerodynamics.network.Network;
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
@@ -21,6 +22,7 @@ public class ImprovedAerodynamics {
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC);
Network.register();
}
}
@@ -0,0 +1,147 @@
package dev.lumareth.improvedAerodynamics.client.pipper;
/**
* Flight of a Warium shell, which is a vanilla arrow: every tick it moves by its velocity, the velocity is then scaled
* by the drag and gravity is subtracted. That is linear in the launch velocity, so the position after n ticks is
* <pre>M + D(n) * v0 - (0, G(n), 0)</pre>
* with D(n) the sum of the drag factors and G(n) the drop gravity causes on its own. Both are tabulated once, which
* makes both the impact point and the lead solution cheap enough to run every frame.
* <p>
* Plain doubles and no Minecraft classes, so it can be tested on its own. Units are blocks and ticks.
*/
public final class Ballistics {
public static final int MAX_TICKS = 400;
private final double[] d = new double[MAX_TICKS + 1];
private final double[] g = new double[MAX_TICKS + 1];
public Ballistics(double drag, double gravity) {
double y = 0.0;
double vy = 0.0;
double sum = 0.0;
double factor = 1.0;
for (int n = 1; n <= MAX_TICKS; n++) {
y += vy;
vy = vy * drag - gravity;
sum += factor;
factor *= drag;
d[n] = sum;
g[n] = -y;
}
}
/** Sum of the drag factors after n ticks, n may be fractional. */
public double d(double n) {
return interpolate(d, n);
}
/** Drop in blocks after n ticks, n may be fractional. */
public double g(double n) {
return interpolate(g, n);
}
private static double interpolate(double[] table, double n) {
if (n <= 0.0) {
return 0.0;
}
if (n >= MAX_TICKS) {
return table[MAX_TICKS];
}
int i = (int) n;
double f = n - i;
return table[i] + (table[i + 1] - table[i]) * f;
}
/** Height of the terrain a shell would hit at a column. */
public interface Ground {
double heightAt(double x, double z);
}
/**
* Where a shell fired from the muzzle with the given world velocity (blocks per tick, already including the
* speed of the shooter if shells inherit it) comes down. Returns the impact point as {x, y, z} or null if it
* stays in the air for the whole table.
*/
public double[] impact(double mx, double my, double mz, double vx, double vy, double vz, Ground ground) {
double px = mx;
double py = my;
double pz = mz;
for (int n = 1; n <= MAX_TICKS; n++) {
double nx = mx + d[n] * vx;
double ny = my + d[n] * vy - g[n];
double nz = mz + d[n] * vz;
double floor = ground.heightAt(nx, nz);
if (ny <= floor) {
// Linear crossing inside this step
double before = py - ground.heightAt(px, pz);
double after = ny - floor;
double f = before - after > 1.0E-9 ? Math.min(1.0, Math.max(0.0, before / (before - after))) : 1.0;
return new double[] {px + (nx - px) * f, py + (ny - py) * f, pz + (nz - pz) * f};
}
px = nx;
py = ny;
pz = nz;
}
return null;
}
/** Result of {@link #lead}: aim direction, time of flight in ticks and the range to the target's current position. */
public record Lead(double dirX, double dirY, double dirZ, double ticks, double range) {
}
/**
* Aim direction that makes a shell meet a moving target.
*
* @param speed muzzle speed in blocks per tick
* @param t* target position now
* @param tv* target velocity in blocks per tick
* @param sv* velocity the shell inherits from the shooter in blocks per tick, zero if it does not
* @return the solution or null if the shell cannot reach the target within the table
*/
public Lead lead(double mx, double my, double mz, double speed, double tx, double ty, double tz,
double tvx, double tvy, double tvz, double svx, double svy, double svz) {
double range = Math.sqrt(sq(tx - mx) + sq(ty - my) + sq(tz - mz));
double previousN = 0.0;
for (int n = 1; n <= MAX_TICKS; n++) {
double f = miss(n, mx, my, mz, speed, tx, ty, tz, tvx, tvy, tvz, svx, svy, svz);
if (f <= 0.0) {
double lo = previousN;
double hi = n;
for (int i = 0; i < 30; i++) {
double mid = 0.5 * (lo + hi);
if (miss(mid, mx, my, mz, speed, tx, ty, tz, tvx, tvy, tvz, svx, svy, svz) > 0.0) {
lo = mid;
} else {
hi = mid;
}
}
double t = 0.5 * (lo + hi);
double dn = d(t);
double rx = tx + tvx * t - mx - dn * svx;
double ry = ty + tvy * t - my - dn * svy + g(t);
double rz = tz + tvz * t - mz - dn * svz;
double length = Math.sqrt(rx * rx + ry * ry + rz * rz);
if (length < 1.0E-6) {
return null;
}
return new Lead(rx / length, ry / length, rz / length, t, range);
}
previousN = n;
}
return null;
}
/** Distance the shell can still cover short of where the target will be, positive means it has not got there yet. */
private double miss(double n, double mx, double my, double mz, double speed, double tx, double ty, double tz,
double tvx, double tvy, double tvz, double svx, double svy, double svz) {
double dn = d(n);
double rx = tx + tvx * n - mx - dn * svx;
double ry = ty + tvy * n - my - dn * svy + g(n);
double rz = tz + tvz * n - mz - dn * svz;
return Math.sqrt(rx * rx + ry * ry + rz * rz) - speed * dn;
}
private static double sq(double v) {
return v * v;
}
}
@@ -0,0 +1,155 @@
package dev.lumareth.improvedAerodynamics.client.pipper;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.registries.ForgeRegistries;
import org.joml.primitives.AABBic;
import java.util.ArrayList;
import java.util.List;
/**
* Finds the light autocannon of a ship and works out what it will shoot, the same way Warium's LACFireScript does:
* barrel count, muzzle, aim direction and muzzle speed.
*/
final class GunFinder {
private static final ResourceLocation LIGHT_AUTOCANNON = new ResourceLocation("crusty_chunks", "light_autocannon");
private static final TagKey<Block> BARREL = TagKey.create(Registries.BLOCK, new ResourceLocation("crusty_chunks", "acbarrel"));
private static final TagKey<Block> PASSTHROUGH = TagKey.create(Registries.BLOCK, new ResourceLocation("crusty_chunks", "barrelpassthrough"));
private static final int RESCAN_TICKS = 100;
private static final int MAX_GUNS = 16;
/** Muzzle and direction in ship space and the speed of the shell in blocks per tick. */
record Shot(double mx, double my, double mz, double dx, double dy, double dz, double speed) {
}
private long shipId = Long.MIN_VALUE;
private long lastScan = Long.MIN_VALUE;
private final List<BlockPos> guns = new ArrayList<>();
/** The gun closest to the seat, or null if the ship has none loaded. */
Shot find(ClientLevel level, long shipId, AABBic aabb, BlockPos seat) {
if (shipId != this.shipId || level.getGameTime() - lastScan >= RESCAN_TICKS || level.getGameTime() < lastScan) {
this.shipId = shipId;
lastScan = level.getGameTime();
scan(level, aabb);
}
BlockPos best = null;
double bestDistance = Double.MAX_VALUE;
for (BlockPos pos : guns) {
double distance = pos.distSqr(seat);
if (distance < bestDistance) {
bestDistance = distance;
best = pos;
}
}
return best == null ? null : shotOf(level, best);
}
private void scan(ClientLevel level, AABBic aabb) {
guns.clear();
Block gun = ForgeRegistries.BLOCKS.getValue(LIGHT_AUTOCANNON);
if (gun == null || aabb == null) {
return;
}
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int cx = aabb.minX() >> 4; cx <= aabb.maxX() >> 4; cx++) {
for (int cz = aabb.minZ() >> 4; cz <= aabb.maxZ() >> 4; cz++) {
ChunkAccess chunk = level.getChunk(cx, cz, ChunkStatus.FULL, false);
if (chunk == null) {
continue;
}
LevelChunkSection[] sections = chunk.getSections();
for (int i = 0; i < sections.length; i++) {
LevelChunkSection section = sections[i];
if (section.hasOnlyAir() || !section.maybeHas(state -> state.is(gun))) {
continue;
}
int baseY = level.getSectionYFromSectionIndex(i) << 4;
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
if (section.getBlockState(x, y, z).is(gun) && guns.size() < MAX_GUNS) {
guns.add(pos.set((cx << 4) + x, baseY + y, (cz << 4) + z).immutable());
}
}
}
}
}
}
}
}
private static Shot shotOf(ClientLevel level, BlockPos gun) {
BlockState state = level.getBlockState(gun);
Property<?> property = state.getBlock().getStateDefinition().getProperty("facing");
if (!(property instanceof DirectionProperty facingProperty)) {
return null;
}
Direction facing = state.getValue(facingProperty);
int fx = facing.getStepX();
int fy = facing.getStepY();
int fz = facing.getStepZ();
// Barrels, counted like Warium does: up to five more behind the first one
double barrels = 1.0;
for (int i = 0; i < 5 && level.getBlockState(gun.offset(fx * (int) (1 + barrels), fy * (int) (1 + barrels), fz * (int) (1 + barrels))).is(BARREL); i++) {
barrels++;
}
// Muzzle: past the barrels, skipping through blocks the shell passes
double position = barrels;
for (int i = 0; i < 10; i++) {
BlockState at = level.getBlockState(gun.offset(fx * (int) (1 + position), fy * (int) (1 + position), fz * (int) (1 + position)));
if ((!at.isAir() && !at.canOcclude()) || at.is(PASSTHROUGH)) {
position++;
} else {
break;
}
}
double mx = gun.getX() + fx * (1.5 + position) + 0.5;
double my = gun.getY() + fy * (1.5 + position) + 0.5;
double mz = gun.getZ() + fz * (1.5 + position) + 0.5;
// Aim, from the tags of the gun itself
double aimX = 0.0;
double aimY = 0.0;
double aimZ = 0.0;
BlockEntity entity = level.getBlockEntity(gun);
double gx = entity == null ? 0.0 : entity.getPersistentData().getDouble("X");
double gz = entity == null ? 0.0 : entity.getPersistentData().getDouble("Z");
double gpitch = entity == null ? 0.0 : entity.getPersistentData().getDouble("Pitch");
if (1.0 > Math.abs(fx - gx) || 1.0 > Math.abs(fz - gz)) {
aimY = gpitch;
aimX = gx + fx;
aimZ = gz + fz;
}
if (aimX * aimZ == 0.0) {
aimX = fx;
aimZ = fz;
}
double length = Math.sqrt(aimX * aimX + aimY * aimY + aimZ * aimZ);
if (length < 1.0E-6) {
return null;
}
double speed = PipperConfig.MUZZLE_SPEED_OVERRIDE.get();
if (speed <= 0.0) {
double multiplier = ModList.get().isLoaded("ritchiesprojectilelib") ? 2.0 : 1.0;
speed = (5.0 + barrels / 1.5) * multiplier;
}
return new Shot(mx, my, mz, aimX / length, aimY / length, aimZ / length, speed);
}
}
@@ -0,0 +1,237 @@
package dev.lumareth.improvedAerodynamics.client.pipper;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.BufferUploader;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.Tesselator;
import com.mojang.blaze3d.vertex.VertexFormat;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import org.joml.Matrix4f;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.valkyrienskies.core.api.ships.ClientShip;
import org.valkyrienskies.core.api.ships.Ship;
import org.valkyrienskies.mod.common.VSGameUtilsKt;
/**
* Draws two markers for the first light autocannon of the aircraft you sit in:
* an orange ring where its shells come down on the ground, and, for the radar target selected on the seat, a green
* ring you put on the target to hit it (the lead pipper). Both come from the same ballistics Warium's shells follow.
*/
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
public final class GunPipperRenderer {
private static final int IMPACT_COLOR = 0xFFFF9020;
private static final int LEAD_COLOR = 0xFF40FF60;
private static final float DISTANCE = 2.0F;
private static final GunFinder FINDER = new GunFinder();
private static final TargetTracker TRACKER = new TargetTracker();
private static Ballistics ballistics;
private static double ballisticsDrag = Double.NaN;
private static double ballisticsGravity = Double.NaN;
private GunPipperRenderer() {
}
@SubscribeEvent
public static void onRenderLevel(RenderLevelStageEvent event) {
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES || !PipperConfig.SPEC.isLoaded()
|| !PipperConfig.ENABLED.get() || !ModList.get().isLoaded("valkyrien_warium")) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
ClientLevel level = minecraft.level;
if (level == null || minecraft.player == null) {
return;
}
Entity seat = WariumSeat.seatOf(minecraft.player);
if (seat == null) {
TRACKER.reset();
return;
}
BlockPos seatPos = BlockPos.containing(seat.getX(), seat.getY(), seat.getZ());
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, seatPos);
if (ship == null) {
return;
}
GunFinder.Shot shot = FINDER.find(level, ship.getId(), ship.getShipAABB(), seatPos);
if (shot == null) {
return;
}
org.joml.Matrix4dc shipToWorld = ship instanceof ClientShip clientShip
? clientShip.getRenderTransform().getShipToWorld() : ship.getShipToWorld();
Vector3d muzzle = shipToWorld.transformPosition(new Vector3d(shot.mx(), shot.my(), shot.mz()));
Vector3d direction = shipToWorld.transformDirection(new Vector3d(shot.dx(), shot.dy(), shot.dz())).normalize();
double inherit = PipperConfig.INHERIT_SHIP_VELOCITY.get() ? 1.0 / 20.0 : 0.0;
org.joml.Vector3dc velocity = ship.getVelocity();
double svx = velocity.x() * inherit;
double svy = velocity.y() * inherit;
double svz = velocity.z() * inherit;
Ballistics table = table();
Camera camera = event.getCamera();
Matrix4f matrix = event.getPoseStack().last().pose();
BufferBuilder builder = Tesselator.getInstance().getBuilder();
RenderSystem.disableDepthTest();
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
RenderSystem.disableCull();
RenderSystem.setShader(GameRenderer::getPositionColorShader);
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
boolean drawn = false;
if (PipperConfig.SHOW_IMPACT.get()) {
double[] impact = table.impact(muzzle.x, muzzle.y, muzzle.z,
direction.x * shot.speed() + svx, direction.y * shot.speed() + svy, direction.z * shot.speed() + svz,
(x, z) -> level.getHeight(Heightmap.Types.MOTION_BLOCKING, Mth.floor(x), Mth.floor(z)));
if (impact != null) {
drawn |= marker(builder, matrix, camera, impact[0], impact[1], impact[2], IMPACT_COLOR, false);
}
}
if (PipperConfig.SHOW_LEAD.get()) {
drawn |= lead(builder, matrix, camera, level, seat, seatPos, table, muzzle, shot.speed(), svx, svy, svz);
} else {
TRACKER.reset();
}
if (drawn) {
BufferUploader.drawWithShader(builder.end());
} else {
builder.end().release();
}
RenderSystem.enableCull();
RenderSystem.enableDepthTest();
RenderSystem.disableBlend();
}
private static boolean lead(BufferBuilder builder, Matrix4f matrix, Camera camera, ClientLevel level, Entity seat, BlockPos seatPos,
Ballistics table, Vector3d muzzle, double speed, double svx, double svy, double svz) {
int selected = WariumSeat.selectedTarget(seat);
if (selected <= 0) {
TRACKER.reset();
return false;
}
BlockEntity seatEntity = level.getBlockEntity(seatPos);
if (seatEntity == null) {
return false;
}
var seatData = seatEntity.getPersistentData();
BlockEntity node = level.getBlockEntity(BlockPos.containing(seatData.getDouble("ControlX"), seatData.getDouble("ControlY"), seatData.getDouble("ControlZ")));
if (node == null) {
return false;
}
var data = node.getPersistentData();
// Warium builds these keys from a double, so they read TargetX1.0, TargetY1.0 and so on
double tx = data.getDouble("TargetX" + (double) selected);
double ty = data.getDouble("TargetY" + (double) selected);
double tz = data.getDouble("TargetZ" + (double) selected);
if (tx == 0.0 && ty == 0.0 && tz == 0.0) {
return false;
}
TRACKER.update(selected, tx, ty, tz, level.getGameTime());
Ballistics.Lead lead = table.lead(muzzle.x, muzzle.y, muzzle.z, speed, tx, ty, tz,
TRACKER.vx(), TRACKER.vy(), TRACKER.vz(), svx, svy, svz);
if (lead == null) {
return false;
}
return marker(builder, matrix, camera,
muzzle.x + lead.dirX() * lead.range(), muzzle.y + lead.dirY() * lead.range(), muzzle.z + lead.dirZ() * lead.range(),
LEAD_COLOR, true);
}
private static Ballistics table() {
double drag = PipperConfig.DRAG.get();
double gravity = PipperConfig.GRAVITY.get();
if (ballistics == null || drag != ballisticsDrag || gravity != ballisticsGravity) {
ballistics = new Ballistics(drag, gravity);
ballisticsDrag = drag;
ballisticsGravity = gravity;
}
return ballistics;
}
/** A ring on the screen in the direction of a world point, with ticks on it for the lead pipper. */
private static boolean marker(BufferBuilder builder, Matrix4f matrix, Camera camera, double x, double y, double z, int color, boolean ticks) {
Vec3 cameraPosition = camera.getPosition();
Vector3f toPoint = new Vector3f((float) (x - cameraPosition.x), (float) (y - cameraPosition.y), (float) (z - cameraPosition.z));
if (toPoint.lengthSquared() < 1.0E-6F) {
return false;
}
toPoint.normalize();
if (toPoint.dot(camera.getLookVector()) <= 0.05F) {
return false;
}
Vector3f center = new Vector3f(toPoint).mul(DISTANCE);
Vector3f left = new Vector3f(camera.getLeftVector());
Vector3f up = new Vector3f(camera.getUpVector());
float radius = (float) (PipperConfig.MARKER_SIZE.get() * DISTANCE);
float thickness = radius * 0.12F;
ring(builder, matrix, center, left, up, radius, thickness, color);
quad(builder, matrix, center, left, up, thickness * 1.6F, thickness * 1.6F, color);
if (ticks) {
for (int i = 0; i < 4; i++) {
double angle = i * Math.PI / 2.0;
float cx = (float) Math.cos(angle);
float cy = (float) Math.sin(angle);
Vector3f tickCenter = new Vector3f(center)
.fma(cx * radius * 1.45F, left).fma(cy * radius * 1.45F, up);
float halfX = Math.abs(cx) > 0.5F ? radius * 0.35F : thickness;
float halfY = Math.abs(cy) > 0.5F ? radius * 0.35F : thickness;
quad(builder, matrix, tickCenter, left, up, halfX, halfY, color);
}
}
return true;
}
private static void ring(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float radius, float thickness, int color) {
final int segments = 32;
float inner = radius - thickness;
float outer = radius + thickness;
for (int i = 0; i < segments; i++) {
double a0 = i * 2.0 * Math.PI / segments;
double a1 = (i + 1) * 2.0 * Math.PI / segments;
vertex(builder, matrix, center, left, up, (float) Math.cos(a0) * inner, (float) Math.sin(a0) * inner, color);
vertex(builder, matrix, center, left, up, (float) Math.cos(a0) * outer, (float) Math.sin(a0) * outer, color);
vertex(builder, matrix, center, left, up, (float) Math.cos(a1) * outer, (float) Math.sin(a1) * outer, color);
vertex(builder, matrix, center, left, up, (float) Math.cos(a1) * inner, (float) Math.sin(a1) * inner, color);
}
}
private static void quad(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float halfX, float halfY, int color) {
vertex(builder, matrix, center, left, up, -halfX, -halfY, color);
vertex(builder, matrix, center, left, up, halfX, -halfY, color);
vertex(builder, matrix, center, left, up, halfX, halfY, color);
vertex(builder, matrix, center, left, up, -halfX, halfY, color);
}
private static void vertex(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float a, float b, int color) {
// The left vector points to the left of the screen, so a positive offset along it is drawn to the left
float vx = center.x - left.x * a + up.x * b;
float vy = center.y - left.y * a + up.y * b;
float vz = center.z - left.z * a + up.z * b;
builder.vertex(matrix, vx, vy, vz)
.color(color >> 16 & 255, color >> 8 & 255, color & 255, color >>> 24)
.endVertex();
}
}
@@ -0,0 +1,44 @@
package dev.lumareth.improvedAerodynamics.client.pipper;
import net.minecraftforge.common.ForgeConfigSpec;
/** Client side settings of the gun pipper. Lives in improved_aerodynamics-client.toml. */
public final class PipperConfig {
public static final ForgeConfigSpec SPEC;
public static final ForgeConfigSpec.BooleanValue ENABLED;
public static final ForgeConfigSpec.BooleanValue SHOW_IMPACT;
public static final ForgeConfigSpec.BooleanValue SHOW_LEAD;
public static final ForgeConfigSpec.BooleanValue INHERIT_SHIP_VELOCITY;
public static final ForgeConfigSpec.DoubleValue MUZZLE_SPEED_OVERRIDE;
public static final ForgeConfigSpec.DoubleValue GRAVITY;
public static final ForgeConfigSpec.DoubleValue DRAG;
public static final ForgeConfigSpec.DoubleValue MARKER_SIZE;
static {
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
builder.comment("Gun pipper for Warium light autocannons, drawn while seated in a Warium seat").push("pipper");
ENABLED = builder.comment("Master switch")
.define("enabled", true);
SHOW_IMPACT = builder.comment("Show where the shells of the gun come down on the ground (orange ring)")
.define("showImpact", true);
SHOW_LEAD = builder.comment("Show the lead pipper for the radar target you selected (green ring)")
.define("showLead", true);
INHERIT_SHIP_VELOCITY = builder.comment("Add the speed of your aircraft to the shells. Warium does not do this itself, only turn it",
"on if a test shows your shells keep the speed of the jet (fire level at full speed, then hovering)")
.define("inheritShipVelocity", false);
MUZZLE_SPEED_OVERRIDE = builder.comment("Muzzle speed in blocks per tick, 0 works it out from the barrel length like Warium does")
.defineInRange("muzzleSpeedOverride", 0.0, 0.0, 100.0);
GRAVITY = builder.comment("Gravity of the shells in blocks per tick squared")
.defineInRange("gravity", 0.05, 0.0, 1.0);
DRAG = builder.comment("Speed the shells keep each tick, 0.99 is a vanilla arrow")
.defineInRange("drag", 0.99, 0.5, 1.0);
MARKER_SIZE = builder.comment("Radius of the markers on screen")
.defineInRange("markerSize", 0.03, 0.005, 0.2);
builder.pop();
SPEC = builder.build();
}
private PipperConfig() {
}
}
@@ -0,0 +1,87 @@
package dev.lumareth.improvedAerodynamics.client.pipper;
/**
* Target velocity from the radar. The node only stores positions, and they change when the radar updates, which is
* not every tick. So the velocity is the change between two distinct positions divided by the ticks between them,
* smoothed, and it is dropped once the position has not changed for a while.
*/
final class TargetTracker {
private static final long STALE_TICKS = 60;
private static final double SMOOTHING = 0.5;
private static final double JUMP_BLOCKS_PER_TICK = 15.0;
private int index = -1;
private double px;
private double py;
private double pz;
private long sampleTick;
private double vx;
private double vy;
private double vz;
private boolean hasVelocity;
void update(int selected, double x, double y, double z, long tick) {
if (selected != index || tick < sampleTick) {
index = selected;
px = x;
py = y;
pz = z;
sampleTick = tick;
hasVelocity = false;
vx = 0.0;
vy = 0.0;
vz = 0.0;
return;
}
if (x != px || y != py || z != pz) {
long dt = Math.max(1L, tick - sampleTick);
double nx = (x - px) / dt;
double ny = (y - py) / dt;
double nz = (z - pz) / dt;
if (Math.sqrt(nx * nx + ny * ny + nz * nz) > JUMP_BLOCKS_PER_TICK) {
// Teleport or a different contact reusing the slot
hasVelocity = false;
} else if (hasVelocity) {
vx += (nx - vx) * SMOOTHING;
vy += (ny - vy) * SMOOTHING;
vz += (nz - vz) * SMOOTHING;
} else {
vx = nx;
vy = ny;
vz = nz;
hasVelocity = true;
}
px = x;
py = y;
pz = z;
sampleTick = tick;
} else if (tick - sampleTick > STALE_TICKS) {
hasVelocity = false;
vx = 0.0;
vy = 0.0;
vz = 0.0;
}
}
void reset() {
index = -1;
hasVelocity = false;
}
/** Velocity in blocks per tick, zero when unknown. */
double vx() {
return vx;
}
double vy() {
return vy;
}
double vz() {
return vz;
}
boolean hasVelocity() {
return hasVelocity;
}
}
@@ -0,0 +1,20 @@
package dev.lumareth.improvedAerodynamics.client.pipper;
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
import net.minecraft.world.entity.Entity;
/** Everything that touches Warium VS classes, so nothing else loads them when Warium is missing. */
final class WariumSeat {
private WariumSeat() {
}
static Entity seatOf(Entity rider) {
Entity root = rider.getRootVehicle();
return root instanceof SeatEntityEntity ? root : null;
}
/** Index of the radar target selected on the seat, 1 based, 0 for none. */
static int selectedTarget(Entity seat) {
return seat.getEntityData().get(SeatEntityEntity.DATA_SelectedTarget);
}
}