Smart Cannon Shells

by smcpeak

Provides cannon shells that pass through friendly forces.

Content
3 years ago
0.17 - 1.1
518
Combat

i Exclude rails from area damage

4 years ago
(updated 4 years ago)

Hi! While I'm exploring and laying rail tracks to new outposts, I usually have a tank following me (using Autodrive) for protection. While the tank usually does a good job of keeping the big bugs at bay, the explosive cannon shells unfortunately often destroy the tracks.

I think it would be a good idea if railway infrastructure (rails and signals, perhaps even trainstops -- my smart train network depends circuit network signals to enable/disable waypoint-stations) would not be afffected by explosive damage from my own cannon shells. Would you consider implementing this, please?

It should be pretty straightforward: Listen to on_entity_damaged, filtered by damage-type for explosive damage. If the damaged entity is of type "curved-rail", "straight-rail", "rail-signal" or "rail-chain-signal", AND if it is of the same force as the entity that caused the damage, AND if the damage-causing entity has smart cannon shells in its inventory, just add the final damage to the damaged entity's health again.

4 years ago

I'll take a look within the next few days. I'm surprised this is an issue because the essential feature of smart shells is they do not (are not supposed to) damage entities of the same or an allied force due to the force_condition on the projectile. I assume you're using Factorio 0.18?

4 years ago
(updated 4 years ago)

Yes. But coming to think of it, last time this happened I also had used Armored Train (which only uses the non-explosive vanilla ammo). I'm pretty sure, however, that the tank with smart ammo did the damage.

EDIT: From your info page (under "Usage"):

Smart shells pass through allies. However, the explosive variants cause splash damage on impact that damages everything, including allies, so they are still pretty dangerous.

This would be in line with what I believe to have seen. :-)

4 years ago

Yes, I'd forgotten that I noticed the problem with splash damage and already noted it!

I implemented what you suggested, but it doesn't quite work. The problem is I don't know the original health of the damaged entity, and the "final damage" does not account for the entity's original health, so the effect is the explosive shells now heal friendly entities! That seems a bit overpowered, especially as I'd like to do this for all entities, not just rail. Any clever ideas on how to fix that?

Here is what I have currently. You can drop this into the mod directory (after unzipping) as control.lua to play with it:

-- SmartCannonShells control.lua

-- When true, prevent friendly explosive splash damage.
local prevent_friendly_explosive_damage = true;

-- When true, only prevent friendly explosive damage to rail pieces.
local only_prevent_friendly_explosive_damage_to_rail = true;


-- Re-read the configuration settings.  (This is not hooked up yet
-- because I'm still trying to get the main code to work.)
local function read_configuration_settings()
  --prevent_friendly_explosive_damage =
  --  settings.global["smart-cannon-shells-prevent-friendly-explosive-damage"].value;

  --only_prevent_friendly_explosive_damage_to_rail =
  --  settings.global["smart-cannon-shells-only-prevent-friendly-explosive-damage-to-rail"].value;
end;

-- Do it once on startup, then afterward in response to the
-- on_runtime_mod_setting_changed event.
read_configuration_settings();
script.on_event(defines.events.on_runtime_mod_setting_changed, read_configuration_settings);


-- Return true if 'on_entity_damaged' event 'event' corresponds to a
-- situation where a friendly unit was damaged by the explosive splash
-- of a smart cannon shell.
local function smart_explosive_damaged_friendly_rail(event)
  -- Check that event damaged and was caused by an entity.
  if (event.entity == nil or event.cause == nil) then
    return false;
  end;

  -- Check that entity forces are the same.
  if (event.force ~= event.entity.force) then
    return false;
  end;

  -- Check that the damage is exposive.  Explosive damage is not
  -- prevented by the 'force_condition' associated with smart ammo.
  if (event.damage_type.name ~= "explosion") then
    return false;
  end;

  -- Check that the attacker is a vehicle.
  if (event.cause.type ~= "car") then
    return false;
  end;

  -- Get its ammo inventory.
  local inv = event.cause.get_inventory(defines.inventory.car_ammo);
  if (inv == nil) then
    return false;
  end;

  -- Check that it has smart explosives equipped as ammo.  We can't tell
  -- if those shells were actually used, but given that vehicles
  -- generally only have one slot compatible with explosive ammo, if
  -- they are equipped then they were probably what was used.
  has_smart_explosives =
    (inv.get_item_count("smart-explosive-cannon-shell-item") > 0) or
    (inv.get_item_count("smart-explosive-uranium-cannon-item") > 0);
  if (not has_smart_explosives) then
    return false;
  end;

  if (only_prevent_friendly_explosive_damage_to_rail) then
    -- Check that the damaged entity is a piece of rail equipment.
    local is_rail = (event.entity.type == "straight-rail" or
                     event.entity.type == "curved-rail" or
                     event.entity.type == "rail-signal" or
                     event.entity.type == "rail-chain-signal");
    if (not is_rail) then
      return false;
    end;
  end;

  return true;
end;

if (prevent_friendly_explosive_damage) then
  -- TODO: Use an event filter rather than doing all filtering
  -- after receiving the event.
  script.on_event({defines.events.on_entity_damaged},
    function(event)
      if (smart_explosive_damaged_friendly_rail(event)) then
        if (true) then
          log("Entity num=" .. tostring(event.entity.unit_number) ..
              " type=" .. event.entity.type ..
              " name=" .. event.entity.name ..
              " health=" .. tostring(event.entity.health) ..
              " took " .. event.final_damage_amount ..
              " friendly explosive damage" ..
              " from attacker={num=" .. event.cause.unit_number ..
                " name=" .. event.cause.name ..
                " type=" .. event.cause.type ..
              "}.  Restoring its health.");
        end;

        -- This works even if the entity would have died.
        --
        -- TODO: This heals entities too.  I don't have access to
        -- the original health.
        event.entity.health =
          event.entity.health + event.final_damage_amount;
      end;
    end
  );
end;

-- EOF
4 years ago

Be aware I've only tested the above with smart shells loaded into a manually controlled tank. The ammo check will not work for RoboTanks yet.

4 years ago

Sorry it has taken me so long to answer -- working on my own mod took way longer than expected (and it's not even ready yet).

I implemented what you suggested, but it doesn't quite work. The problem is I don't know the original health of the damaged entity, and the "final damage" does not account for the entity's original health, so the effect is the explosive shells now heal friendly entities! That seems a bit overpowered, especially as I'd like to do this for all entities, not just rail. Any clever ideas on how to fix that?

You're right, if something was damaged before, you will heal it completely because health will never be smaller than 0. I think nothing can be done about it currently. Any of these would help:

  • Allow "entity.health < 0", so adding final_damage_amount to it won't automatically restore all health.
  • Get access to the value of entity.health before the damage was applied, perhaps as "entity.original_health" or "entity.health_before_damage".
  • Provide access to the current value of entity.lifetime/entity.time_to_live. (That wouldn't affect your mod, but it's something I need for playing with fire, or rather, the "fire" prototype.)

I've just made an interface request. Not sure it will get us any further, as similar requests (e.g. for a new event triggering before damage is applied) have been turned down.

-- TODO: Use an event filter rather than doing all filtering
-- after receiving the event.

There's really nothing much you can filter by except damage type:

script.on_event(defines.events.on_entity_damaged, function(event) … end, 
                              {filter = "damage-type", type = "explosion"}
})

You could register the event in "if … then" and be more precise if only_prevent_friendly_explosive_damage_to_rail is true:

if only_prevent_friendly_explosive_damage_to_rail then     
    script.on_event(defines.events.on_entity_damaged, function(event) … end, 
                              {filter = "damage-type", type = "explosion"},
                              {filter = "rail", mode = "and"},
                              {filter = "rail-signal"},
    })
else
    script.on_event(defines.events.on_entity_damaged, function(event) … end, 
                              {filter = "damage-type", type = "explosion"}
    })
end

The filters will be ORed by default, therefore 'mode = "and"' is needed.

4 years ago

Following Klonan's suggestion in the request you filed, I found the right spot to put a force damage condition, and now all friendlies are excluded from explosive splash damage. Uploaded version 0.2.1.

4 years ago

Great, thank you! :-)

New response