aliens

Author Topic: Melee hits vs large units handler  (Read 68 times)

Offline CrazedHarpooner

  • Colonel
  • ****
  • Posts: 148
    • View Profile
Melee hits vs large units handler
« on: January 27, 2025, 04:23:04 pm »
I've discovered through scripting that OXCE (and possibly OXC, needs testing) that melee hits vs large units will always strike from the top corner (NW) of the target facing downwards (SE) regardless of the attackers facing and position. The only factor that is considered at this point is which way the target is facing. More details in the bug report here: https://openxcom.org/forum/index.php?topic=12433.0

I decided to give scripting a try and create my own melee handler for these particular cases and here it is:
Code: [Select]
extended:
  scripts:
    hitUnit:
# Improved override that takes into account attacker position and facing.
      - new: CORRECT_POWER_SIDE_MELEE_V2
        offset: -90
        code: |
          # Check if this is a melee attack
          if eq battle_action BA_HIT;
            var ptr RuleArmor rule_armor;
            var int unit_size;

            unit.getRuleArmor rule_armor;
            rule_armor.getSize unit_size;

            # Check if the target is a large unit (size 2)
            if eq unit_size 2;
              var int relPosX;
              var int relPosY;
              var int facing_attacker;
              var int facing_target;
              var int facing_modifier_A;
              var int facing_modifier_B;
              var int pos_modifier;
              var int cw8_side;

              # Calculate relative X and Y position between attacker and target (we don't need Z)
              begin;
                var Position position;
                var int temp;

                attacker.getPosition position;
                position.getX relPosX;
                position.getY relPosY;
                unit.getPosition position;
                position.getX temp;
                sub relPosX temp;
                position.getY temp;
                sub relPosY temp;

                attacker.getDirection facing_attacker;
                unit.getDirection facing_target;

                # We shift values 0 or lower for small attackers, large attackers remain the same.
                attacker.getRuleArmor rule_armor;
                rule_armor.getSize unit_size;
                if eq unit_size 1;
                  if le relPosX 0;
                    sub relPosX 1;
                  end;
                  if le relPosY 0;
                    sub relPosY 1;
                  end;

                # Handle 'oddball' diagonal hits from size 2 attackers. We shift values as needed to match a non-diagonal facing in an adjacent position.
                else;
                  var int tempA;
                  var int tempB;
                  var int tempC;

                  set tempA relPosX;
                  mul tempA relPosY;
                  set tempB facing_attacker;
                  mod tempB 2;
                  if and eq tempA 0 eq tempB 1;
                    set tempB relPosY;
                    abs tempB;
                    div tempB 2;
                    set tempC -1;
                    pow tempC tempB;

                    set tempA facing_attacker;
                    div tempA 2;
                    mod tempA 2;
                    set tempB -1;
                    pow tempB tempA;

                    set tempA relPosX;
                    add tempA relPosY;
                    div tempA 2;

                    if eq relPosX 0;
                      aggregate relPosX tempA tempB;
                    else eq relPosY 0;
                      aggregate relPosY tempA tempB;
                    end;
                    abs tempA;
                    mul tempB tempC;
                    aggregate facing_attacker tempA tempB;
                  end;
                end;
              end;

              # The constant is +8 circular shift in facing to avoid negative numbers and +1 to invert the modulo 2 calculation.
              # This facing modifier is needed when diagonal attacks are performed to non-corners of the target. Final valid values for facing_modifier_A = [0,1]
              set facing_modifier_A 9;
              add facing_modifier_A relPosX;
              add facing_modifier_A relPosY;
              add facing_modifier_A facing_attacker;
              mod facing_modifier_A 2;

              # This is used for diagonal corner attacks.
              # This modifier will only ever have a value of 1 when both relative positions have an absolute value of 2, it'll be 0 otherwise.
              # Final valid values for facing_modifier_B = [0,1]
              set facing_modifier_B relPosX;
              mul facing_modifier_B relPosY;
              abs facing_modifier_B;
              div facing_modifier_B 4;

              # Determine additional modifier based on position combinations.
              # This modifier is based on the sign of the 2 relative positions but additionally by relPosX absolute value.
              # If the absolute value of relPosX is 2 then the final sign will be changed, if it's 1 no change will be made.
              # Since the only options for the products of the relative positions are: [-4,-2,2,4] we divide by 2 to reduce and module by 2 to limit the pos_modifier to [-1,0,1].
              begin;
                var int tempA;
                var int tempB -1;

                set pos_modifier relPosX;
                mul pos_modifier relPosY;
                set tempA relPosX;
                abs tempA;
                add tempA 1;
                mod tempA 2;
                pow tempB tempA;
                mul pos_modifier tempB;
                div pos_modifier 2;
                mod pos_modifier 2;
              end;

              # After determining all modifiers we now actually perform the calculations for the side being hit.
              # Constant is composed by +8 direction shift (to avoid negatives) and +4 from attacker facing 0 and target facing 0, strike side would be 4 as base reference.
              set cw8_side facing_modifier_A;
              mul cw8_side pos_modifier;
              add cw8_side 12;
              add cw8_side facing_attacker;
              sub cw8_side facing_target;

              # Section that handles RNG rolls for cases were 2 possible sides could be hit.
              begin;
                var int temp;
                set temp cw8_side;
                mod temp 2;
                if eq temp 1;
                  if or eq facing_modifier_A 1 eq facing_modifier_B 1;
                    var ptre GeoscapeGame geo_game;
                    var ptre RandomState rand_state;
                    var int coin;
                    battle_game.getGeoscapeGame geo_game;
                    geo_game.getRandomState rand_state;
                    rand_state.randomRange coin 0 1;
                    add cw8_side coin;

                  # handling of cases were the target's 2 possible sides that can be hit are the same.
                  else;
                    sub cw8_side pos_modifier;
                  end;
                end;
              end;

              # Calculation is done and now we proceed to covert to OXCE enumeration.
              # We reduce the value of the clock-wise 8-side option to the 0-7 range.
              mod cw8_side 8;

              # We then trim it to a 4-side clock-wise selection of 0-3.
              div cw8_side 2;

              # Here we remap the side hit from our clock-wise numbering to OXCE enumeration. Note that we never need to consider SIDE_UNDER.
              if eq cw8_side 3;
                set side SIDE_LEFT;
              else eq cw8_side 2;
                set side SIDE_REAR;
              else eq cw8_side 1;
                set side SIDE_RIGHT;
              else;
                set side SIDE_FRONT;
              end;

              # Based on the side finally obtained we also set the part hit.
              if eq side SIDE_LEFT;
                set part BODYPART_LEFTARM;
              else eq side SIDE_RIGHT;
                set part BODYPART_RIGHTARM;
              else;
                set part BODYPART_TORSO;
              end;
            end;
          end;
          return power part side;
I'm also attaching the script in mod form that should be compatible with most existing mods out there and a logging script that tracks all melee hits.
« Last Edit: January 27, 2025, 04:38:01 pm by CrazedHarpooner »