Skip to content
Binary file added .gitattributes
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import minecrafttransportsimulator.entities.instances.APart;
import minecrafttransportsimulator.entities.instances.EntityBullet;
import minecrafttransportsimulator.entities.instances.EntityBullet.HitType;
import minecrafttransportsimulator.jsondefs.JSONBullet.BulletType;
import minecrafttransportsimulator.entities.instances.EntityPlacedPart;
import minecrafttransportsimulator.items.components.AItemBase;
import minecrafttransportsimulator.items.components.AItemPart;
Expand All @@ -42,6 +43,7 @@
import minecrafttransportsimulator.mcinterface.InterfaceManager;
import minecrafttransportsimulator.packets.instances.PacketEntityBulletHitCollision;
import minecrafttransportsimulator.packets.instances.PacketEntityBulletHitEntity;
import minecrafttransportsimulator.packets.instances.PacketEntityBulletHitExternalEntity;
import minecrafttransportsimulator.packets.instances.PacketEntityBulletHitGeneric;
import minecrafttransportsimulator.packets.instances.PacketPartChange_Add;
import minecrafttransportsimulator.packets.instances.PacketPartChange_Remove;
Expand Down Expand Up @@ -358,20 +360,103 @@ public EntityBullet.HitType attackProjectile(Damage damage, EntityBullet bullet,
if (hitEntry.box.groupDef != null && (hitEntry.box.groupDef.armorThickness != 0 || hitEntry.box.groupDef.heatArmorThickness != 0)) {
hitOperationalHitbox = true;
if (bullet != null) {
double armorThickness = hitEntry.box.definition != null ? (bullet.definition.bullet.isHeat && hitEntry.box.groupDef.heatArmorThickness != 0 ? hitEntry.box.groupDef.heatArmorThickness : hitEntry.box.groupDef.armorThickness) : 0;
double penetrationPotential = bullet.definition.bullet.isHeat ? bullet.definition.bullet.armorPenetration : (bullet.definition.bullet.armorPenetration * bullet.velocity / bullet.initialVelocity);
boolean bulletIsHeat = bullet.definition.bullet.types.contains(BulletType.HEAT);
double armorThickness = hitEntry.box.definition != null ? (bulletIsHeat && hitEntry.box.groupDef.heatArmorThickness != 0 ? hitEntry.box.groupDef.heatArmorThickness : hitEntry.box.groupDef.armorThickness) : 0;
double penetrationPotential = bulletIsHeat ? bullet.definition.bullet.armorPenetration : (bullet.definition.bullet.armorPenetration * bullet.velocity / bullet.initialVelocity);

//Armor-piercing and sub-caliber projectiles lose some of their armor penetration when penetrating the armor boxes.
//Heat projectiles use their base penetration directly without scaling.
bullet.armorPenetrated += armorThickness;
bullet.displayDebugMessage("HIT ARMOR OF: " + (int) armorThickness);
bullet.displayDebugMessage("HIT ARMOR: " + (int) armorThickness + " TOTAL ARMOR HIT: " + (int) bullet.armorPenetrated + " / PENETRATED " + (int) penetrationPotential);

if (bullet.armorPenetrated > penetrationPotential) {
//Bullet hit too much armor.
if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(bullet.gun, bullet.bulletNumber, hitEntry.position, hitEntry.side, HitType.ARMOR));
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(bullet.gun, bullet.bulletNumber, bullet.position, hitEntry.side, HitType.ARMOR));
bullet.waitingOnActionPacket = true;
} else {
EntityBullet.performGenericHitLogic(bullet.gun, bullet.bulletNumber, bullet.position, hitEntry.side, HitType.ARMOR);
}
if (bulletIsHeat) {
bullet.displayDebugMessage("HEAT FAILED TO PENETRATE ARMOR. MAX PEN: " + (int) penetrationPotential);
} else {
bullet.displayDebugMessage("HIT TOO MUCH ARMOR. MAX PEN: " + (int) penetrationPotential);
}
return EntityBullet.HitType.ARMOR;
}

//If the bullet has FRAG type and penetrated armor, deal fragmentation damage to internals.
if (bullet.definition.bullet.types.contains(BulletType.FRAG) && bullet.definition.bullet.fragDamage > 0) {
int fragPartsHit = 0;
int fragEntitiesHit = 0;
float coneAngle = bullet.definition.bullet.fragConeAngle > 0 ? bullet.definition.bullet.fragConeAngle : 45.0f;
float hitProbability = bullet.definition.bullet.fragHitProbability > 0 ? bullet.definition.bullet.fragHitProbability : 0.5f;
float fragDmg = bullet.definition.bullet.fragDamage;
double coneAngleRad = Math.toRadians(coneAngle / 2.0);
double coneRange = bullet.definition.bullet.fragRange > 0 ? bullet.definition.bullet.fragRange : bullet.definition.bullet.diameter / 10.0;

//Check all parts on this entity for fragmentation hits within the cone.
for (APart part : allParts) {
double distToPart = bullet.position.distanceTo(part.position);
if (distToPart <= coneRange) {
Point3D toPartVector = part.position.copy().subtract(bullet.position);
double toPartLen = toPartVector.length();
if (toPartLen > 0) {
double angleToPart = Math.acos(toPartVector.dotProduct(bullet.motion, false) / (toPartLen * bullet.motion.length()));
if (angleToPart <= coneAngleRad) {
if (hitProbability >= 1.0f || Math.random() <= hitProbability) {
fragPartsHit++;
Damage fragDamage = new Damage(fragDmg, part.boundingBox, bullet.gun, bullet.gun.lastController, null);
fragDamage.ignoreCooldown = true;
if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitEntity(bullet.gun, part, fragDamage));
} else {
EntityBullet.performEntityHitLogic(part, fragDamage);
}
}
}
}
}
}

//Also check riders on all parts for fragmentation damage.
for (APart part : allParts) {
if (part.rider != null) {
IWrapperEntity partRider = part.rider;
double distToRider = bullet.position.distanceTo(partRider.getPosition());
if (distToRider <= coneRange) {
Point3D toRiderVector = partRider.getPosition().copy().subtract(bullet.position);
double toRiderLen = toRiderVector.length();
if (toRiderLen > 0) {
double angleToRider = Math.acos(toRiderVector.dotProduct(bullet.motion, false) / (toRiderLen * bullet.motion.length()));
if (angleToRider <= coneAngleRad) {
if (hitProbability >= 1.0f || Math.random() <= hitProbability) {
fragEntitiesHit++;
Damage fragDamage = new Damage(fragDmg, part.boundingBox, bullet.gun, bullet.gun.lastController, null);
fragDamage.ignoreCooldown = true;
if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitExternalEntity(partRider, fragDamage));
} else {
EntityBullet.performExternalEntityHitLogic(partRider, fragDamage);
}
}
}
}
}
}
}
bullet.displayDebugMessage("FRAG HIT " + fragPartsHit + " PARTS AND " + fragEntitiesHit + " ENTITIES");
}

//HEAT shells detonate on armor contact and do not continue flying.
if (bulletIsHeat) {
bullet.displayDebugMessage("HEAT DETONATED ON ARMOR.");
if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(bullet.gun, bullet.bulletNumber, bullet.position, hitEntry.side, HitType.ARMOR));
bullet.waitingOnActionPacket = true;
} else {
EntityBullet.performGenericHitLogic(bullet.gun, bullet.bulletNumber, hitEntry.position, hitEntry.side, HitType.ARMOR);
EntityBullet.performGenericHitLogic(bullet.gun, bullet.bulletNumber, bullet.position, hitEntry.side, HitType.ARMOR);
}
bullet.displayDebugMessage("HIT TOO MUCH ARMOR. MAX PEN: " + (int) penetrationPotential);
return EntityBullet.HitType.ARMOR;
}
} else {
Expand Down Expand Up @@ -404,14 +489,14 @@ public EntityBullet.HitType attackProjectile(Damage damage, EntityBullet bullet,
if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitEntity(bullet.gun, hitEntity, damage));
if (removeAfterDamage) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(bullet.gun, bullet.bulletNumber, hitEntry.position, hitEntry.side, HitType.VEHICLE));
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(bullet.gun, bullet.bulletNumber, bullet.position, hitEntry.side, HitType.VEHICLE));
bullet.waitingOnActionPacket = true;
return EntityBullet.HitType.VEHICLE;
}
} else {
EntityBullet.performEntityHitLogic(hitEntity, damage);
if (removeAfterDamage) {
EntityBullet.performGenericHitLogic(bullet.gun, bullet.bulletNumber, hitEntry.position, hitEntry.side, HitType.VEHICLE);
EntityBullet.performGenericHitLogic(bullet.gun, bullet.bulletNumber, bullet.position, hitEntry.side, HitType.VEHICLE);
return EntityBullet.HitType.VEHICLE;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@ public void update() {
if (((definition.bullet.isLongRange || !(gun.lastController instanceof IWrapperPlayer)) ^ world.isClient()) && (!world.isClient() || InterfaceManager.clientInterface.getClientPlayer().getID().equals(gun.lastController.getID()))) {
//Now that we have an accurate motion, check for collisions.
//First get a damage object to try to attack entities with.
double amount = definition.bullet.isHeat ? definition.bullet.damage : (velocity / initialVelocity) * definition.bullet.damage * ConfigSystem.settings.damage.bulletDamageFactor.value * ConfigSystem.settings.damage.packBulletDamageFactors.value.get(gun.lastLoadedBullet.definition.packID);
boolean bulletIsHeat = definition.bullet.types.contains(BulletType.HEAT);
double amount = bulletIsHeat ? definition.bullet.damage : (velocity / initialVelocity) * definition.bullet.damage * ConfigSystem.settings.damage.bulletDamageFactor.value * ConfigSystem.settings.damage.packBulletDamageFactors.value.get(gun.lastLoadedBullet.definition.packID);
Damage damage = new Damage(gun, boundingBox, amount);

//Declare variables that may be used for hit logic.
Expand Down Expand Up @@ -417,11 +418,11 @@ public void update() {
if (hitExternalEntity != null) {
if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitExternalEntity(hitExternalEntity, damage));
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(gun, bulletNumber, hitExternalEntity.getPosition(), Axis.getFromVector(motion), HitType.ENTITY));
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(gun, bulletNumber, position, Axis.getFromVector(motion), HitType.ENTITY));
waitingOnActionPacket = true;
} else {
performExternalEntityHitLogic(hitExternalEntity, damage);
performGenericHitLogic(gun, bulletNumber, hitExternalEntity.getPosition(), Axis.getFromVector(motion), HitType.ENTITY);
performGenericHitLogic(gun, bulletNumber, position, Axis.getFromVector(motion), HitType.ENTITY);
}
displayDebugMessage("HIT MC ENTITY " + hitExternalEntity.getName());
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,17 @@ public static class Bullet {
@JSONDescription("Set this to true to make this bullet not spawn, but consume ammo.")
public boolean isBlank;

@JSONDescription("If true, then this bullet will be considered a HEAT bullet and will use the HEAT armor value on any collision boxes it finds. If that value isn't defined, it will just use the normal armor value.")
public boolean isHeat;
@JSONDescription("The cone angle, in degrees, for fragmentation spread when a FRAG-type bullet penetrates armor. Fragments are scattered within this cone behind the point of penetration.")
public float fragConeAngle;

@JSONDescription("The maximum distance, in meters (blocks), that fragments can travel from the point of armor penetration.")
public float fragRange;

@JSONDescription("The probability (0.0 to 1.0) that a fragment will hit each individual component or rider within the fragmentation cone. If 1.0, every target in the cone is guaranteed to be hit.")
public float fragHitProbability;

@JSONDescription("The damage each fragment deals when it hits an internal component or rider.")
public float fragDamage;

@JSONDescription("Normally, bullet checks are handled only on the client that spawned them. This client then sends the info to the server when it sees a hit. This works best for most bullets, since it prevents the firing player from 'missing' something they hit due to lag. However, this prevents bullets from hitting things that aren't loaded. Setting this to true will make the bullet do checks on the server, which will let them hit anything loaded on the server, but will result in de-syncs between hit position seen and actual hit position if the gun is moving at any significant speed when fired.")
public boolean isLongRange;
Expand Down Expand Up @@ -154,6 +163,9 @@ public static class Bullet {

@JSONDescription("The radius (in blocks) within which full armor penetration is applied regardless of distance.")
public float maxPenRadius;

@Deprecated
public boolean isHeat;
}

public enum BulletType {
Expand All @@ -165,6 +177,16 @@ public enum BulletType {
WATER,
@JSONDescription("A bullet that pierces player armor. Useful for pesky super-suits.")
ARMOR_PIERCING,
@JSONDescription("If defined, then this bullet will be considered a HEAT bullet and will use the HEAT armor value on any collision boxes it finds. If that value isn't defined, it will just use the normal armor value.")
HEAT,
@JSONDescription("Like armor-piercing ones, but used for categorization purposes.")
SUBCALIBER,
@JSONDescription("Used when fragmentation need to be used for bullets.")
FRAG,
@JSONDescription("Identifies the bullet type as a missile. Used for categorization.")
MISSILE,
@JSONDescription("Identifies the bullet type as a bomb. Used for categorization.")
BOMB,
@JSONDescription("A bullet that has a custom function defined in code. Useful for integration with a variety of mods, regardless of version.")
CUSTOM
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import minecrafttransportsimulator.jsondefs.JSONAnimationDefinition;
import minecrafttransportsimulator.jsondefs.JSONAnimationDefinition.AnimationComponentType;
import minecrafttransportsimulator.jsondefs.JSONBullet;
import minecrafttransportsimulator.jsondefs.JSONBullet.BulletType;
import minecrafttransportsimulator.jsondefs.JSONCollisionBox;
import minecrafttransportsimulator.jsondefs.JSONCollisionGroup;
import minecrafttransportsimulator.jsondefs.JSONCollisionGroup.CollisionType;
Expand Down Expand Up @@ -2007,6 +2008,15 @@ private static void performBulletLegacyCompats(JSONBullet definition) {
if (definition.bullet.damage == 0) {
definition.bullet.damage = definition.bullet.diameter / 5F;
}
if (definition.bullet.isHeat) {
if (definition.bullet.types == null) {
definition.bullet.types = new ArrayList<>();
}
if (!definition.bullet.types.contains(BulletType.HEAT)) {
definition.bullet.types.add(BulletType.HEAT);
}
definition.bullet.isHeat = false;
}
//Make guided bullets default to active guidance
if (definition.bullet.guidanceType == null && definition.bullet.turnRate > 0) {
definition.bullet.guidanceType = JSONBullet.GuidanceType.ACTIVE;
Expand Down
Loading
Loading