Add flaps: flappable tag, flap lever on the control node, flap physics (0.4.0)
Shift-right-click a block in improved_aerodynamics:flappable to make it a flap. G/H step the node's flap lever UP/TAKEOFF/LANDING; flaps slew to the configured angle, add lift and drag and raise the stall angle, and retract above a max speed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
18 files changed
+525
-13
No files matched your search
@@ -54,6 +54,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double roll;
|
||||
double trimIntegral;
|
||||
double trimOutput;
|
||||
// Deflection of this node's flaps in radians, moves towards what the flap lever and airspeed allow
|
||||
double flapAngle;
|
||||
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
|
||||
final Vector3d pitchResponse = new Vector3d();
|
||||
final Vector3d pitchResponseBuilding = new Vector3d();
|
||||
@@ -67,6 +69,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
trimIntegral = data.autoTrim() ? data.storedTrim() : 0.0;
|
||||
trimOutput = trimIntegral;
|
||||
savedTrim = trimIntegral;
|
||||
flapAngle = Math.toRadians(FlapSetting.of(data.flapSetting()).degrees());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,9 +225,10 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null;
|
||||
NodeState state = node != null ? nodeStates.get(node.key()) : null;
|
||||
double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0;
|
||||
double flap = s.flap() && state != null ? state.flapAngle : 0.0;
|
||||
|
||||
if (improved) {
|
||||
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
|
||||
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, flap, weight, tuning, stalled[i], out);
|
||||
if (commit) {
|
||||
stalled[i] = nowStalled;
|
||||
wingStalled |= nowStalled && s.type() == SurfaceType.WING;
|
||||
@@ -239,7 +243,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
probe.clear();
|
||||
double base = 0.0;
|
||||
if (improved) {
|
||||
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
|
||||
ImprovedAero.evaluate(s, v, w, r, input, flap, weight, tuning, stalled[i], probe);
|
||||
} else {
|
||||
WariumAero.evaluate(s, v, w, r, input, weight, probe);
|
||||
}
|
||||
@@ -247,7 +251,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
probe.clear();
|
||||
double nudged = input(s, node, state, delta);
|
||||
if (improved) {
|
||||
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
|
||||
ImprovedAero.evaluate(s, v, w, r, nudged, flap, weight, tuning, stalled[i], probe);
|
||||
} else {
|
||||
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
||||
}
|
||||
@@ -296,10 +300,25 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
state.pitch = moveTowards(state.pitch, node.pitch(), step);
|
||||
state.yaw = moveTowards(state.yaw, node.yaw(), step);
|
||||
state.roll = moveTowards(state.roll, node.roll(), step);
|
||||
updateFlaps(state, node, worldVelocity.length(), dt);
|
||||
AutoTrim.update(state, node, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a node's flaps towards the angle its lever asks for. Above the setting's maximum speed the air pushes them
|
||||
* back in, so a craft that is too fast for its flaps has them half retracted, and all the way at 25% more.
|
||||
*/
|
||||
private static void updateFlaps(NodeState state, NodeData node, double speed, double dt) {
|
||||
FlapSetting setting = FlapSetting.of(node.flapSetting());
|
||||
double target = Math.toRadians(setting.degrees());
|
||||
double limit = setting.maxSpeed();
|
||||
if (speed > limit) {
|
||||
target *= Math.max(0.0, 1.0 - (speed - limit) / (0.25 * limit));
|
||||
}
|
||||
state.flapAngle = moveTowards(state.flapAngle, target, Math.toRadians(Config.FLAP_SLEW_DEGREES.get()) * dt);
|
||||
}
|
||||
|
||||
static double moveTowards(double value, double target, double maxStep) {
|
||||
if (Math.abs(target - value) <= maxStep) {
|
||||
return target;
|
||||
|
||||
@@ -14,16 +14,17 @@ package dev.lumareth.improvedAerodynamics.flight;
|
||||
* @param brakeInput air brakes only: Warium's input value, they ignore smoothing and trim
|
||||
* @param aspectRatio span squared over area of the wing the block belongs to, 0 if not known yet
|
||||
* @param wingGroup whether the wing the block belongs to contains WING blocks, not only control surfaces or stabilizers
|
||||
* @param flap whether the block was made a flap with shift-right-click, it then also follows its node's flap setting
|
||||
*/
|
||||
public record AeroSurface(long key, SurfaceType type, boolean slab, double x, double y, double z, int facingX, int facingZ,
|
||||
double pressure, double fluidFactor, double speedMultiplier,
|
||||
long node, ControlAxis axis1, double multiplier1, ControlAxis axis2, double multiplier2,
|
||||
double brakeInput, double aspectRatio, boolean wingGroup) {
|
||||
double brakeInput, double aspectRatio, boolean wingGroup, boolean flap) {
|
||||
public static final long NO_NODE = Long.MIN_VALUE;
|
||||
|
||||
AeroSurface withWingShape(double aspectRatio, boolean wingGroup) {
|
||||
return new AeroSurface(key, type, slab, x, y, z, facingX, facingZ, pressure, fluidFactor, speedMultiplier,
|
||||
node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio, wingGroup);
|
||||
node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio, wingGroup, flap);
|
||||
}
|
||||
|
||||
/** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
|
||||
/** Server side of the flap keys: moves the flap lever of the control node the pilot's seat is linked to. */
|
||||
public final class FlapControl {
|
||||
private FlapControl() {
|
||||
}
|
||||
|
||||
/** @param step +1 for more flaps, -1 for fewer */
|
||||
public static void step(ServerPlayer player, int step) {
|
||||
Entity vehicle = player.getVehicle();
|
||||
if (!(vehicle instanceof SeatEntityEntity)) {
|
||||
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps.no_seat"), true);
|
||||
return;
|
||||
}
|
||||
BlockEntity node = AutoTrimToggle.findNode(vehicle.level(), vehicle.blockPosition());
|
||||
if (node == null) {
|
||||
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps.no_node"), true);
|
||||
return;
|
||||
}
|
||||
CompoundTag tag = node.getPersistentData();
|
||||
int current = FlapSetting.of(tag.getInt(WariumCapture.FLAP_SETTING_TAG)).ordinal();
|
||||
int next = Math.max(0, Math.min(FlapSetting.values().length - 1, current + (step > 0 ? 1 : -1)));
|
||||
if (next != current) {
|
||||
tag.putInt(WariumCapture.FLAP_SETTING_TAG, next);
|
||||
node.setChanged();
|
||||
}
|
||||
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps",
|
||||
Component.translatable(FlapSetting.of(next).translationKey())), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
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.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
|
||||
/**
|
||||
* Shift-right-click on a block in the {@code improved_aerodynamics:flappable} tag (Warium's horizontal control
|
||||
* surfaces) with empty hands makes it a flap, or a normal control surface again. A flap follows the flap lever of the
|
||||
* control node it is linked to, on top of the pilot's input.
|
||||
*/
|
||||
public final class FlapInteraction {
|
||||
public static final TagKey<Block> FLAPPABLE = TagKey.create(Registries.BLOCK,
|
||||
new ResourceLocation(ImprovedAerodynamics.MOD_ID, "flappable"));
|
||||
|
||||
private FlapInteraction() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
Player player = event.getEntity();
|
||||
if (event.getHand() != InteractionHand.MAIN_HAND || !player.isShiftKeyDown() || player.isPassenger()
|
||||
|| !player.getMainHandItem().isEmpty() || !player.getOffhandItem().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Level level = event.getLevel();
|
||||
BlockPos pos = event.getPos();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (!state.is(FLAPPABLE)) {
|
||||
return;
|
||||
}
|
||||
// Handled on both sides so the block's own right-click action does not run on either
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide));
|
||||
if (level.isClientSide) {
|
||||
return;
|
||||
}
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity == null) {
|
||||
return;
|
||||
}
|
||||
CompoundTag tag = blockEntity.getPersistentData();
|
||||
boolean flap = !tag.getBoolean(WariumCapture.FLAP_TAG);
|
||||
tag.putBoolean(WariumCapture.FLAP_TAG, flap);
|
||||
blockEntity.setChanged();
|
||||
if (flap && !tag.contains("ControlX")) {
|
||||
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flap.unlinked"), true);
|
||||
} else {
|
||||
player.displayClientMessage(Component.translatable(flap
|
||||
? "message.improved_aerodynamics.flap.on" : "message.improved_aerodynamics.flap.off"), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.Config;
|
||||
|
||||
/** Positions of a control node's flap lever. */
|
||||
public enum FlapSetting {
|
||||
UP,
|
||||
TAKEOFF,
|
||||
LANDING;
|
||||
|
||||
/** The setting for a stored number, UP for anything that is not one. */
|
||||
public static FlapSetting of(int ordinal) {
|
||||
FlapSetting[] values = values();
|
||||
return ordinal >= 0 && ordinal < values.length ? values[ordinal] : UP;
|
||||
}
|
||||
|
||||
/** Deflection in degrees the flaps move to at this setting. */
|
||||
double degrees() {
|
||||
return switch (this) {
|
||||
case UP -> 0.0;
|
||||
case TAKEOFF -> Config.FLAP_TAKEOFF_DEGREES.get();
|
||||
case LANDING -> Config.FLAP_LANDING_DEGREES.get();
|
||||
};
|
||||
}
|
||||
|
||||
/** Airspeed in blocks per second above which the air starts to push the flaps back in. Infinite when they are up. */
|
||||
double maxSpeed() {
|
||||
return switch (this) {
|
||||
case UP -> Double.POSITIVE_INFINITY;
|
||||
case TAKEOFF -> Config.FLAP_TAKEOFF_MAX_SPEED.get();
|
||||
case LANDING -> Config.FLAP_LANDING_MAX_SPEED.get();
|
||||
};
|
||||
}
|
||||
|
||||
public String translationKey() {
|
||||
return "message.improved_aerodynamics.flaps." + name().toLowerCase(java.util.Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ final class ImprovedAero {
|
||||
private static final double MAX_DEFLECTION = Math.toRadians(20.0);
|
||||
// Share of the deflection that acts as angle of attack, gives Warium's control authority at the design speed
|
||||
private static final double FLAP_EFFECTIVENESS = 0.75;
|
||||
// How much of the lift a flap adds raises the stall angle, and the parasitic drag coefficient per radian² of flap
|
||||
// Share of the flap's deflection that acts as angle of attack: 35 degrees of landing flap adds about 1.1 to the
|
||||
// lift coefficient, like a real plain or slotted flap
|
||||
private static final double FLAP_ALPHA_FACTOR = 0.4;
|
||||
private static final double FLAP_STALL_GAIN = 0.7;
|
||||
private static final double FLAP_DRAG = 0.35;
|
||||
// Angle of attack of a level cambered wing, where K is calibrated
|
||||
private static final double REFERENCE_ANGLE = Math.toRadians(4.0);
|
||||
private static final double REFERENCE_SPEED = 40.0;
|
||||
@@ -62,10 +68,11 @@ final class ImprovedAero {
|
||||
* @param w ship angular velocity, ship axes
|
||||
* @param r block center relative to the center of mass, ship axes
|
||||
* @param input control input of the surface, -1 to 1
|
||||
* @param flap flap deflection in radians, 0 for surfaces that are not flaps
|
||||
* @param stalled whether the surface was stalled in the previous substep
|
||||
* @return whether the surface is stalled now
|
||||
*/
|
||||
static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double weight,
|
||||
static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double flap, double weight,
|
||||
Tuning tuning, boolean stalled, Wrench out) {
|
||||
// Airflow at the block: ship velocity plus w x r
|
||||
double vx = v[0] + w[1] * r[2] - w[2] * r[1];
|
||||
@@ -114,6 +121,10 @@ final class ImprovedAero {
|
||||
// Same sign convention as Warium: positive input pushes along n while moving against the facing
|
||||
alpha += FLAP_EFFECTIVENESS * MAX_DEFLECTION * input * (u <= 0.0 ? 1.0 : -1.0);
|
||||
}
|
||||
// A flap bends the airfoil's camber whichever way the air comes, so it always adds lift, and it moves the stall
|
||||
// up with it: a flap raises the highest lift a wing can make
|
||||
double flapShift = FLAP_ALPHA_FACTOR * flap;
|
||||
alpha += flapShift;
|
||||
alpha = Math.max(-Math.PI / 2.0, Math.min(Math.PI / 2.0, alpha));
|
||||
|
||||
// Low aspect ratio wings keep their flow attached longer thanks to leading-edge vortices. Control surfaces
|
||||
@@ -124,6 +135,9 @@ final class ImprovedAero {
|
||||
: s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle;
|
||||
double critical = Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
|
||||
double absAlpha = Math.abs(alpha);
|
||||
if (alpha >= 0.0) {
|
||||
critical += FLAP_STALL_GAIN * flapShift;
|
||||
}
|
||||
// Flips only where both blends agree, so the hysteresis never makes the force jump
|
||||
if (!stalled && absAlpha > critical + STALL_BLEND) {
|
||||
stalled = true;
|
||||
@@ -156,7 +170,7 @@ final class ImprovedAero {
|
||||
double dy = -(vn * ny) / planeSpeed;
|
||||
double dz = -(u * cz + vn * nz) / planeSpeed;
|
||||
double liftForce = q * lift * tuning.liftScale();
|
||||
double dragForce = q * drag / m;
|
||||
double dragForce = q * (drag + FLAP_DRAG * flap * flap) / m;
|
||||
out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce);
|
||||
return stalled;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package dev.lumareth.improvedAerodynamics.flight;
|
||||
* trim state.
|
||||
*
|
||||
* @param storedTrim trim saved in the node, used when the physics thread first sees the node
|
||||
* @param flapSetting the node's flap lever, see {@link FlapSetting#ordinal()}
|
||||
*/
|
||||
public record NodeData(long key, double pitch, double yaw, double roll, double throttle, boolean autoTrim, double storedTrim) {
|
||||
public record NodeData(long key, double pitch, double yaw, double roll, double throttle, boolean autoTrim, double storedTrim, int flapSetting) {
|
||||
}
|
||||
@@ -31,6 +31,9 @@ import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
public final class WariumCapture {
|
||||
public static final String AUTO_TRIM_TAG = "improved_aerodynamics_autotrim";
|
||||
public static final String TRIM_TAG = "improved_aerodynamics_trim";
|
||||
/** Block entity flag of a surface that is a flap, and the flap lever of a control node. */
|
||||
public static final String FLAP_TAG = "improved_aerodynamics_flap";
|
||||
public static final String FLAP_SETTING_TAG = "improved_aerodynamics_flap_setting";
|
||||
private static final int TRIM_SAVE_INTERVAL = 20;
|
||||
|
||||
// The procedure currently running, procedures run one at a time on the server thread
|
||||
@@ -77,10 +80,12 @@ public final class WariumCapture {
|
||||
String key = "Null";
|
||||
String key2 = "Null";
|
||||
double brakeInput = 0.0;
|
||||
boolean flap = false;
|
||||
if (type.controllable || type == SurfaceType.AIR_BRAKE) {
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity != null) {
|
||||
CompoundTag tag = blockEntity.getPersistentData();
|
||||
flap = type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(FLAP_TAG);
|
||||
key = tag.getString("Key");
|
||||
key2 = type == SurfaceType.AIR_BRAKE ? "Null" : tag.getString("Key2");
|
||||
BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ"));
|
||||
@@ -97,7 +102,7 @@ public final class WariumCapture {
|
||||
return new AeroSurface(pos.asLong(), type, slab, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5,
|
||||
facing.getStepX(), facing.getStepZ(), pressure, fluidFactor(level), speedMultiplier,
|
||||
node, ControlAxis.of(key), ControlAxis.multiplier(key), ControlAxis.of(key2), ControlAxis.multiplier(key2),
|
||||
brakeInput, 0.0, false);
|
||||
brakeInput, 0.0, false, flap);
|
||||
}
|
||||
|
||||
/** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */
|
||||
@@ -119,7 +124,8 @@ public final class WariumCapture {
|
||||
}
|
||||
}
|
||||
aero.addNode(new NodeData(nodeKey, tag.getDouble("Pitch"), tag.getDouble("Yaw"), tag.getDouble("Roll"),
|
||||
tag.getDouble("Throttle"), autoTrim, storedTrim));
|
||||
tag.getDouble("Throttle"), autoTrim, storedTrim,
|
||||
FlapSetting.of(tag.getInt(FLAP_SETTING_TAG)).ordinal()));
|
||||
}
|
||||
|
||||
/** Raw node value for a key like "Pitch+", as Warium's air brakes read it. */
|
||||
|
||||
Reference in new issue
Block a user