Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -357,9 +359,19 @@ 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);
bullet.armorPenetrated += armorThickness;
boolean bulletIsHeat = bullet.definition.bullet.types.contains(BulletType.HEAT) || bullet.definition.bullet.isHeat;
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);

//For non-HEAT kinetic rounds (AP and subcaliber), reduce remaining penetration based on armor thickness.
//Penetration loss is proportional to (armorThickness / penetrationPotential)^2, simulating energy loss.
if (!bulletIsHeat && armorThickness > 0 && penetrationPotential > 0) {
double ratio = armorThickness / penetrationPotential;
double penetrationLoss = armorThickness * (1.0 + ratio);
bullet.armorPenetrated += penetrationLoss;
} else {
bullet.armorPenetrated += armorThickness;
}
bullet.displayDebugMessage("HIT ARMOR OF: " + (int) armorThickness);

if (bullet.armorPenetrated > penetrationPotential) {
Expand All @@ -373,6 +385,105 @@ public EntityBullet.HitType attackProjectile(Damage damage, EntityBullet bullet,
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) {
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect explicit zero frag hit probability

Using > 0 to decide whether to apply a default makes fragHitProbability: 0.0 impossible: it is treated as "unset" and replaced with 0.5, so fragments still hit 50% of the time. That breaks the documented 0.01.0 behavior and can cause unexpected internal/crew damage when pack authors intentionally configure zero chance. Defaulting should use a distinct sentinel (for example negative values) or clamp the provided value directly.

Useful? React with 👍 / 👎.

float fragDmg = bullet.definition.bullet.fragDamage;
double coneAngleRad = Math.toRadians(coneAngle / 2.0);
double coneRange = bullet.definition.bullet.diameter / 10.0;
bullet.displayDebugMessage("FRAG ACTIVATED");

//Check all parts on this entity for fragmentation hits within the cone.
for (APart part : allParts) {
double distToPart = hitEntry.position.distanceTo(part.position);
if (distToPart <= coneRange) {
//Check if part is within the cone angle from bullet's motion direction.
Point3D toPartVector = part.position.copy().subtract(hitEntry.position);
double toPartLen = toPartVector.length();
if (toPartLen > 0) {
double angleToPart = Math.acos(toPartVector.dotProduct(bullet.motion, false) / (toPartLen * bullet.motion.length()));
if (angleToPart <= coneAngleRad) {
//Roll for hit probability.
if (Math.random() <= hitProbability) {
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);
}
Comment on lines +413 to +416
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid mutating allParts during FRAG iteration

This branch applies damage to part while attackProjectile is iterating allParts, and APart.attack() can call removePart(), which rebuilds allParts. In server-side bullet simulation (for isLongRange bullets or NPC-fired bullets), a FRAG hit that destroys a part will mutate the collection mid-iteration and can raise ConcurrentModificationException, aborting hit processing. Iterate over a snapshot/list copy (or pre-collect targets) before applying fragment damage.

Useful? React with 👍 / 👎.

bullet.displayDebugMessage("FRAG HIT PART: " + part.definition.systemName + " FOR " + (int) fragDmg);
}
}
}
}
}

//Also check riders (crew) on all parts for fragmentation damage.
for (APart part : allParts) {
if (part.rider != null) {
IWrapperEntity partRider = part.rider;
double distToRider = hitEntry.position.distanceTo(partRider.getPosition());
if (distToRider <= coneRange) {
Point3D toRiderVector = partRider.getPosition().copy().subtract(hitEntry.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 (Math.random() <= hitProbability) {
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 CREW: " + partRider.getName() + " FOR " + (int) fragDmg);
}
}
}
}
}
}
}

//HEAT shells detonate on armor contact and do not continue flying.
//If penetration was successful, apply post-penetration damage to the vehicle and crew
//proportional to the residual penetration potential.
if (bulletIsHeat) {
double residualRatio = (penetrationPotential - bullet.armorPenetrated) / penetrationPotential;
if (residualRatio > 0) {
double postPenDamage = bullet.definition.bullet.damage * residualRatio;
Damage heatDamage = new Damage(bullet.gun, hitEntry.box, postPenDamage);
if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitEntity(bullet.gun, hitEntity, heatDamage));
for (APart part : allParts) {
if (part.rider != null) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitExternalEntity(part.rider, heatDamage));
}
}
} else {
EntityBullet.performEntityHitLogic(hitEntity, heatDamage);
for (APart part : allParts) {
if (part.rider != null) {
EntityBullet.performExternalEntityHitLogic(part.rider, heatDamage);
}
}
}
bullet.displayDebugMessage("HEAT POST-PEN DAMAGE: " + (int) postPenDamage);
}

if (world.isClient()) {
InterfaceManager.packetInterface.sendToServer(new PacketEntityBulletHitGeneric(bullet.gun, bullet.bulletNumber, hitEntry.position, hitEntry.side, HitType.ARMOR));
bullet.waitingOnActionPacket = true;
} else {
EntityBullet.performGenericHitLogic(bullet.gun, bullet.bulletNumber, hitEntry.position, hitEntry.side, HitType.ARMOR);
}
bullet.displayDebugMessage("HEAT DETONATED ON ARMOR");
return EntityBullet.HitType.ARMOR;
}
} else {
//Not a bullet, but hit armor, 100% stopping power with no damage.
return EntityBullet.HitType.ARMOR;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,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) || definition.bullet.isHeat;
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,18 @@ 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.")
@Deprecated
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 probability (0.0 to 1.0) that a fragment will hit an internal component or crew member within the fragmentation cone.")
public float fragHitProbability;

@JSONDescription("The damage each fragment deals when it hits an internal component or crew member.")
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 @@ -120,6 +129,16 @@ public enum BulletType {
WATER,
@JSONDescription("A bullet that pierces player armor. Useful for pesky super-suits.")
ARMOR_PIERCING,
@JSONDescription("A HEAT (High-Explosive Anti-Tank) bullet. Uses the HEAT armor value on collision boxes if defined, otherwise uses normal armor. Penetration is not affected by velocity loss.")
HEAT,
@JSONDescription("A subcaliber (sabot) round. Uses the same penetration mechanics as armor-piercing rounds, but is a distinct type for categorization purposes.")
SUBCALIBER,
@JSONDescription("A bullet that produces fragmentation upon penetrating armor. Fragments deal damage to internal components and crew based on the bullet's fragConeAngle, fragHitProbability, and fragDamage parameters.")
FRAG,
@JSONDescription("A guided missile. Uses guidance and turn rate parameters for tracking targets.")
MISSILE,
@JSONDescription("An unguided bomb. Dropped from a gun with zero muzzle velocity, affected by gravity.")
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
Loading