diff --git a/mccore/src/main/java/minecrafttransportsimulator/entities/components/AEntityF_Multipart.java b/mccore/src/main/java/minecrafttransportsimulator/entities/components/AEntityF_Multipart.java index 890b10653..24f0ba546 100644 --- a/mccore/src/main/java/minecrafttransportsimulator/entities/components/AEntityF_Multipart.java +++ b/mccore/src/main/java/minecrafttransportsimulator/entities/components/AEntityF_Multipart.java @@ -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; @@ -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; @@ -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) { @@ -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; + 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); + } + 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; diff --git a/mccore/src/main/java/minecrafttransportsimulator/entities/instances/EntityBullet.java b/mccore/src/main/java/minecrafttransportsimulator/entities/instances/EntityBullet.java index b4060595b..7678a5d7e 100644 --- a/mccore/src/main/java/minecrafttransportsimulator/entities/instances/EntityBullet.java +++ b/mccore/src/main/java/minecrafttransportsimulator/entities/instances/EntityBullet.java @@ -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. diff --git a/mccore/src/main/java/minecrafttransportsimulator/jsondefs/JSONBullet.java b/mccore/src/main/java/minecrafttransportsimulator/jsondefs/JSONBullet.java index 889d5a7d7..e2a24ba36 100644 --- a/mccore/src/main/java/minecrafttransportsimulator/jsondefs/JSONBullet.java +++ b/mccore/src/main/java/minecrafttransportsimulator/jsondefs/JSONBullet.java @@ -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; @@ -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 }