diff --git a/objects/obj_pnunit/Alarm_0.gml b/objects/obj_pnunit/Alarm_0.gml index a10457062..52400fa31 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -87,6 +87,12 @@ try { } } + // Speed Force sweeps the whole field - bypass normal targeting/range. + if (wep[i] == "Speed Force" || wep[i] == "Speed Force (Ranged)") { + scr_shoot_spread(i); + continue; + } + if ((range[i] >= dist) && (ammo[i] != 0 || range[i] == 1)) { if ((range[i] != 1) && (engaged == 0)) { range_shoot = "ranged"; diff --git a/scripts/scr_clean/scr_clean.gml b/scripts/scr_clean/scr_clean.gml index e5ef92106..82ab06e28 100644 --- a/scripts/scr_clean/scr_clean.gml +++ b/scripts/scr_clean/scr_clean.gml @@ -191,26 +191,7 @@ function scr_clean(target_object, target_is_infantry, hostile_shots, hostile_dam } /// @self Asset.GMObject.obj_pnunit -function damage_infantry(_damage_data, _shots, _damage, _hostile_armour_pierce) { - var _armour_mod = 0; - switch (_hostile_armour_pierce) { - case 4: - _armour_mod = 0; - break; - case 3: - _armour_mod = 1.5; - break; - case 2: - _armour_mod = 2; - break; - case 1: - _armour_mod = 3; - break; - default: - _armour_mod = 3; - break; - } - +function damage_infantry(_damage_data, _shots, _damage, _armour_pierce) { // Find valid infantry targets var valid_marines = []; for (var m = 0, l = array_length(unit_struct); m < l; m++) { @@ -236,7 +217,7 @@ function damage_infantry(_damage_data, _shots, _damage, _hostile_armour_pierce) // Apply damage var _shot_luck = roll_dice_chapter(1, 100, "low"); var _modified_damage = 0; - var _marine_armour = marine_ac[marine_index] * _armour_mod; + var _marine_armour = combat_rank_armour(marine_ac[marine_index], _armour_pierce, false); if (_shot_luck == 1) { _modified_damage = _damage - (2 * _marine_armour); } else if (_shot_luck == 100) { @@ -293,26 +274,7 @@ function damage_infantry(_damage_data, _shots, _damage, _hostile_armour_pierce) } /// @self Asset.GMObject.obj_pnunit -function damage_vehicles(_damage_data, _shots, _damage, _hostile_armour_pierce) { - var _armour_mod = 0; - switch (_hostile_armour_pierce) { - case 4: - _armour_mod = 0; - break; - case 3: - _armour_mod = 2; - break; - case 2: - _armour_mod = 4; - break; - case 1: - _armour_mod = 6; - break; - default: - _armour_mod = 6; - break; - } - +function damage_vehicles(_damage_data, _shots, _damage, _armour_pierce) { var veh_index = -1; // Find valid vehicle targets @@ -335,7 +297,7 @@ function damage_vehicles(_damage_data, _shots, _damage, _hostile_armour_pierce) veh_index = array_random_element(valid_vehicles); // Apply damage - var _modified_damage = _damage - veh_ac[veh_index] * _armour_mod; + var _modified_damage = _damage - combat_rank_armour(veh_ac[veh_index], _armour_pierce, false); if (_modified_damage < 0) { _modified_damage = 0; } diff --git a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml index 3ad455a59..9c42f036b 100644 --- a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml +++ b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml @@ -182,6 +182,16 @@ function scr_player_combat_weapon_stacks() { } } } + if (is_struct(mobi_item) && mobi_item.has_tag("bike")) { + var _speed_force = unit.speed_force(mobi_item.has_tag("sf_ranged")); + var stack_index = find_stack_index(_speed_force.name, head_role, unit); + if (stack_index > -1) { + add_data_to_stack(stack_index, _speed_force, false, head_role, unit); + if (head_role) { + player_head_role_stack(stack_index, unit); + } + } + } if (is_struct(mobi_item)) { add_second_profiles_to_stack(mobi_item); diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index 4ba3f2a3d..63db092ca 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -1,3 +1,156 @@ +/// @function find_next_alive_rank +/// @description Returns the index of the next living rank (dudes_num > 0 and dudes_hp > 0) in a +/// formation, preferring ranks that match the requested vehicle flag. Returns -1 if +/// none. The dudes_hp > 0 check keeps callers safe from dividing by a rank's HP. +/// @param {Id.Instance.obj_enunit} _block The obj_enunit formation to search. +/// @param {Real} _prefer_vehicle 0/1 to prefer that category, or -1 for any living rank. +/// @returns {Real} +function find_next_alive_rank(_block, _prefer_vehicle) { + if (!instance_exists(_block)) { + return -1; + } + + var _fallback = -1; + for (var f = 1; f <= 30; f++) { + if (_block.dudes_num[f] <= 0 || _block.dudes_hp[f] <= 0) { + continue; + } + + if (_prefer_vehicle == -1 || _block.dudes_vehicle[f] == _prefer_vehicle) { + return f; + } + + if (_fallback == -1) { + _fallback = f; + } + } + + return _fallback; +} + +/// @function get_next_enemy_formation +/// @description Returns the nearest enemy formation (obj_enunit) sitting behind the given one +/// that still contains at least one living rank, or noone if there isn't one. +/// @param {Id.Instance} _block The formation we are spilling out of. +/// @returns {Id.Instance} +function get_next_enemy_formation(_block) { + if (!instance_exists(_block)) { + return noone; + } + + var _bx = _block.x; + var _bid = _block.id; + var _best = noone; + var _best_x = 0; + with (obj_enunit) { + if (id == _bid) { + continue; + } + + if (x <= _bx) { + continue; + } + + if (find_next_alive_rank(id, -1) == -1) { + continue; + } + + if (_best == noone || x < _best_x) { + _best = id; + _best_x = x; + } + } + + return _best; +} + +/// @param {Array|Id.Instance} _list Formation ID or array of such to compress & destroy if empty +function combat_cleanup_formations(_list) { + if (is_array(_list)) { + for (var i = 0; i < array_length(_list); i++) { + if (instance_exists(_list[i])) { + compress_enemy_array(_list[i]); + destroy_empty_column(_list[i]); + } + } + } else if (instance_exists(_list)) { + compress_enemy_array(_list); + destroy_empty_column(_list); + } +} + +/// @param {Real} _ac Armour points value +/// @param {Real} _ap Armour-pierce value +/// @param {Bool} _is_vehicle Vehicle or not +/// @returns {Real} Armour value after AP multiplier +function combat_rank_armour(_ac, _ap, _is_vehicle) { + if (_ap < 1 || _ap > 4) { + return _ac; + } + + static _inf = [ + 1, + 3, + 2, + 1.5, + 0, + ]; + + static _veh = [ + 1, + 6, + 4, + 2, + 0, + ]; + + var _armor_mod = _is_vehicle ? _veh[_ap] : _inf[_ap]; + _ac *= _armor_mod; + return round(_ac); +} + +/// @description Computes damage against a rank, applies casualties, and returns the combat outcome. +/// @param {Id.Instance.obj_enunit} block +/// @param {Real} rank +/// @param {Real} dmg_per_weapon +/// @param {Real} ap +/// @param {Real} splash +/// @param {Real} shots_fired +/// @returns {Struct} +function combat_apply_rank_damage(block, rank, dmg_per_weapon, ap, splash, shots_fired) { + var _rank_armour = combat_rank_armour(block.dudes_ac[rank], ap, block.dudes_vehicle[rank]); + var _rank_num = block.dudes_num[rank]; + var _rank_hp = block.dudes_hp[rank]; + var _rank_dr = block.dudes_dr[rank]; + + var _final_hit = max(0, (dmg_per_weapon - (_rank_armour)) * _rank_dr); + var _total_damage = shots_fired * _final_hit; + var _raw_kills = floor(_total_damage / _rank_hp); + var _casualties = min(_raw_kills, _rank_num); + _casualties = min(_casualties, ceil(shots_fired * splash)); + + // Apply casualties and update overall combat status + if (_casualties > 0) { + block.dudes_num[rank] -= _casualties; + obj_ncombat.enemy_forces -= _casualties; + } else if (_rank_num == 1 && _total_damage > 0) { + // Handle partial damage for single unit remains + block.dudes_hp[rank] -= _total_damage; + if (block.dudes_hp[rank] <= 0) { + block.dudes_num[rank] = 0; + obj_ncombat.enemy_forces -= 1; + _casualties = 1; // Count as casualty for tracking purposes + } + } + + return { + casualties: _casualties, + raw_kills: _raw_kills, + final_hit: _final_hit, + bounced: (_final_hit <= 0), + }; +} + /// @self Id.Instance.obj_enunit|Id.Instance.obj_pnunit /// @param {Real} weapon_index_position Weapon number /// @param {Id.Instance.obj_enunit|Id.Instance.obj_pnunit} target_object Target object @@ -6,503 +159,404 @@ /// @param {String} melee_or_ranged melee or ranged function scr_shoot(weapon_index_position, target_object, target_type, damage_data, melee_or_ranged) { try { - // This massive clusterfuck of a script uses the newly determined weapon and target data to attack and assign damage - for (var j = 1; j <= 100; j++) { - obj_ncombat.dead_ene[j] = ""; - obj_ncombat.dead_ene_n[j] = 0; - } - obj_ncombat.dead_enemies = 0; - - var hostile_type; - var hostile_damage; - var hostile_weapon; - var hostile_range; - var hostile_splash; - var aggregate_damage = att[weapon_index_position]; - var armour_pierce = apa[weapon_index_position]; if (obj_ncombat.wall_destroyed == 1) { exit; } - if ((weapon_index_position >= 0) && instance_exists(target_object) && (owner == 2)) { - var shots_fired = wep_num[weapon_index_position]; - if (shots_fired == 0 || ammo[weapon_index_position] == 0) { - exit; - } - var doom = 0; - if ((shots_fired != 1) && (melee_or_ranged != "melee")) { - switch (obj_ncombat.enemy) { - case eFACTION.ECCLESIARCHY: - doom = 0.3; - break; - case eFACTION.ELDAR: - doom = 0.4; - break; - case eFACTION.ORK: - doom = 0.2; - break; - case eFACTION.TAU: - doom = 0.4; - break; - case eFACTION.TYRANIDS: - doom = 0.4; - break; - } - } - if (obj_ncombat.enemy == eFACTION.HERETICS) { - aggregate_damage = round(aggregate_damage * 1.15); + if (!instance_exists(target_object)) { + exit; + } + + var _aggregate_damage = att[weapon_index_position]; + var _armour_pierce = apa[weapon_index_position]; + + if ((owner == 2) && (weapon_index_position >= 0)) { + scr_shoot_enemy(weapon_index_position, target_object, damage_data, melee_or_ranged, _aggregate_damage, _armour_pierce); + } + + if (owner == eFACTION.PLAYER) { + scr_shoot_player(weapon_index_position, target_object, target_type, _aggregate_damage, _armour_pierce); + } + } catch (_exception) { + ERROR_HANDLER.handle_exception(_exception); + } +} + +/// @self Id.Instance.obj_enunit +/// @description Handles enemy-side shooting (owner == 2). +/// @param {Id.Instance.obj_pnunit} target_object +function scr_shoot_enemy(weapon_index_position, target_object, damage_data, melee_or_ranged, aggregate_damage, armour_pierce) { + var _hit_count = wep_num[weapon_index_position]; + if (_hit_count == 0 || ammo[weapon_index_position] == 0) { + exit; + } + + ammo[weapon_index_position] -= 1; + + var _hostile_type = 0; + var _damage = 0; + var _damage_per_weapon = 0; + var _damage_type = ""; + var _weapon_name = wep[weapon_index_position]; + var _weapon_splash = max(1, splash[weapon_index_position]); + var _weapon_range = range[weapon_index_position]; + var _doom_mod = 1; + + // Determine doom modifier based on faction + if (_hit_count > 1 && melee_or_ranged != "melee") { + switch (obj_ncombat.enemy) { + case eFACTION.ECCLESIARCHY: + _doom_mod = 0.3; + break; + case eFACTION.ELDAR: + _doom_mod = 0.4; + break; + case eFACTION.ORK: + _doom_mod = 0.2; + break; + case eFACTION.TAU: + _doom_mod = 0.4; + break; + case eFACTION.TYRANIDS: + _doom_mod = 0.4; + break; + } + } + + // Heretic damage boost + if (obj_ncombat.enemy == eFACTION.HERETICS) { + aggregate_damage = round(aggregate_damage * 1.15); + } + + // Resolve damage type + if (damage_data == "medi") { + _damage_type = (aggregate_damage < armour_pierce) ? "arp" : "att"; + } else { + _damage_type = damage_data; + } + + if (_weapon_name == "Web Spinner") { + _damage_type = "status"; + } + + var _is_assorted = wep_owner[weapon_index_position] == "assorted"; + + if (_damage_type == "status") { + if (melee_or_ranged != "wall") { + _hit_count *= _weapon_splash; + + target_object.hostile_shooters = _is_assorted ? 999 : 1; + _damage = 0; + _hostile_type = 1; + + scr_clean(target_object, _hostile_type, _hit_count, _damage, _weapon_name, _weapon_range, _weapon_splash, armour_pierce); + } + } else if (_damage_type == "att" && aggregate_damage > 0) { + _damage_per_weapon = aggregate_damage; + + if (melee_or_ranged == "melee") { + var _men_limit = (target_object.men - target_object.dreads) * 2; + if (_hit_count > _men_limit) { + _doom_mod = _men_limit / _hit_count; } - if ((obj_ncombat.enemy == eFACTION.CHAOS) && (obj_ncombat.threat == 7)) { - doom = 1; + } + + if (_hit_count > 1) { + _damage_per_weapon = floor(_doom_mod * _damage_per_weapon); + _hit_count = floor(_hit_count * _doom_mod); + } + + if (melee_or_ranged != "wall") { + _hit_count *= _weapon_splash; + + if (_hit_count > 0) { + target_object.hostile_shooters = _is_assorted ? 999 : 1; + _damage = _damage_per_weapon / _hit_count; + _hostile_type = 1; + + scr_clean(target_object, _hostile_type, _hit_count, _damage, _weapon_name, _weapon_range, _weapon_splash, armour_pierce); } + } + } else if ((_damage_type == "arp" || _damage_type == "dread") && armour_pierce > 0) { + _damage_per_weapon = (aggregate_damage == 0) ? _hit_count : aggregate_damage; - var damage_type = ""; - var stop = 0; + if (melee_or_ranged != "wall") { + _hit_count *= _weapon_splash; + } - if (ammo[weapon_index_position] > 0) { - ammo[weapon_index_position] -= 1; + if (melee_or_ranged == "melee") { + var _veh_limit = (target_object.veh + target_object.dreads) * 5; + if (_hit_count > _veh_limit) { + _doom_mod = _veh_limit / _hit_count; } + } + + if (_hit_count > 1) { + _damage_per_weapon = floor(_doom_mod * _damage_per_weapon); + _hit_count = floor(_hit_count * _doom_mod); + } + + if (_damage_per_weapon == 0) { + _damage_per_weapon = _hit_count * _doom_mod; + } + + if (_hit_count > 0) { + _damage = _damage_per_weapon / _hit_count; + + if (melee_or_ranged == "wall") { + var dest = 0; - if (damage_data == "medi") { - damage_type = "att"; - if (aggregate_damage < armour_pierce) { - damage_type = "arp"; + _damage -= target_object.ac; + _damage = max(0, _damage); + _damage = round(_damage) * _hit_count; + target_object.hp -= _damage; + + if (target_object.hp <= 0) { + dest = 1; } + + obj_nfort.hostile_weapons = _weapon_name; + obj_nfort.hostile_shots = _hit_count; + obj_nfort.hostile_damage = _damage; + + scr_flavor2(dest, "wall", _weapon_range, _weapon_name, _hit_count, _weapon_splash); } else { - damage_type = damage_data; - } - if (wep[weapon_index_position] == "Web Spinner") { - damage_type = "status"; + target_object.hostile_shooters = _is_assorted ? 999 : 1; + _hostile_type = 0; + + scr_clean(target_object, _hostile_type, _hit_count, _damage, _weapon_name, _weapon_range, _weapon_splash, armour_pierce); } + } + } +} - var attack_count_mod = max(1, splash[weapon_index_position]); +/// @self Id.Instance.obj_pnunit +/// @description Handles player-side shooting (owner == eFACTION.PLAYER). +/// @param {Id.Instance.obj_enunit} target_object +function scr_shoot_player(weapon_index_position, target_object, target_type, aggregate_damage, armour_pierce) { + // Resolve shots fired + var shots_fired = 0; + if (weapon_index_position >= 0) { + if (aggregate_damage <= 0) { + exit; + } - if ((damage_type == "status") && (stop == 0) && (shots_fired > 0)) { - var damage_per_weapon = 0, hit_number = shots_fired; - if (melee_or_ranged != "wall") { - shots_fired *= attack_count_mod; - } - if ((hit_number > 0) && (melee_or_ranged != "wall") && instance_exists(target_object)) { - if (wep_owner[weapon_index_position] == "assorted") { - target_object.hostile_shooters = 999; - } else if (wep_owner[weapon_index_position] != "assorted") { - target_object.hostile_shooters = 1; - } - hostile_damage = 0; - hostile_weapon = wep[weapon_index_position]; - hostile_type = 1; - hostile_range = range[weapon_index_position]; - hostile_splash = attack_count_mod; - - scr_clean(target_object, hostile_type, hit_number, hostile_damage, hostile_weapon, hostile_range, hostile_splash, armour_pierce); - } - } else if ((damage_type == "att") && (aggregate_damage > 0) && (stop == 0) && (shots_fired > 0)) { - var damage_per_weapon, hit_number; + shots_fired = wep_num[weapon_index_position]; + } else if (weapon_index_position < -40) { + // Fixes legacy bug where negative weapon indices would always exit early with 0 shots + shots_fired = 1; + } - damage_per_weapon = aggregate_damage; + if (shots_fired == 0) { + exit; + } - if (melee_or_ranged == "melee") { - if (shots_fired > (target_object.men - target_object.dreads) * 2) { - doom = ((target_object.men - target_object.dreads) * 2) / shots_fired; - } - } + var _hp_count = array_length(target_object.dudes_hp); - hit_number = shots_fired; + while (target_type < _hp_count && target_object.dudes_hp[target_type] <= 0) { + target_type++; + } + + if (target_type >= _hp_count) { + exit; + } - if ((doom != 0) && (shots_fired > 1)) { - damage_per_weapon = floor((doom * damage_per_weapon)); - hit_number = floor(hit_number * doom); - } - if (melee_or_ranged != "wall") { - shots_fired *= attack_count_mod; - } + var damage_per_weapon = 0; + var attack_count_mod = 1; - if ((hit_number > 0) && (melee_or_ranged != "wall") && instance_exists(target_object)) { - if (wep_owner[weapon_index_position] == "assorted") { - target_object.hostile_shooters = 999; - } - if (wep_owner[weapon_index_position] != "assorted") { - target_object.hostile_shooters = 1; - } - hostile_damage = damage_per_weapon / hit_number; - hostile_weapon = wep[weapon_index_position]; - hostile_type = 1; - hostile_range = range[weapon_index_position]; - hostile_splash = attack_count_mod; - - scr_clean(target_object, hostile_type, hit_number, hostile_damage, hostile_weapon, hostile_range, hostile_splash, armour_pierce); - } - } else if (((damage_type == "arp") || (damage_type == "dread")) && (armour_pierce > 0) && (stop == 0) && (shots_fired > 0)) { - var damage_per_weapon, hit_number; - damage_per_weapon = aggregate_damage; - if (aggregate_damage == 0) { - damage_per_weapon = shots_fired; - } - if (melee_or_ranged != "wall") { - shots_fired *= attack_count_mod; - } - if (melee_or_ranged == "melee") { - if (shots_fired > ((target_object.veh + target_object.dreads) * 5)) { - doom = ((target_object.veh + target_object.dreads) * 5) / shots_fired; - } - } - hit_number = shots_fired; + if (weapon_index_position >= 0) { + var _ammo = ammo[weapon_index_position]; + if (_ammo == 0) { + exit; + } else if (_ammo != -1) { + ammo[weapon_index_position]--; + } - if ((doom != 0) && (shots_fired > 1)) { - damage_per_weapon = floor((doom * damage_per_weapon)); - hit_number = floor(hit_number * doom); - } + //? Probably dead code, because `player_silos` seems to never be assigned to after initialization to 0; + if (wep[weapon_index_position] == "Missile Silo") { + obj_ncombat.player_silos -= min(obj_ncombat.player_silos, 30); + } - if (damage_per_weapon == 0) { - damage_per_weapon = shots_fired * doom; - } + damage_per_weapon = aggregate_damage / wep_num[weapon_index_position]; + attack_count_mod = max(1, splash[weapon_index_position]); + } else if (weapon_index_position < -40) { + attack_count_mod = 3; + + switch (weapon_index_position) { + case -51: + damage_per_weapon = 160; + armour_pierce = 0; + break; + case -52: + damage_per_weapon = 200; + armour_pierce = -1; + break; + case -53: + damage_per_weapon = 250; + armour_pierce = 0; + break; + } + } - if (hit_number > 0 && instance_exists(target_object)) { - hostile_weapon = wep[weapon_index_position]; - hostile_range = range[weapon_index_position]; - hostile_splash = attack_count_mod; - hostile_damage = damage_per_weapon / hit_number; - if (melee_or_ranged == "wall") { - var dest = 0; - - hostile_damage -= target_object.ac; - hostile_damage = max(0, hostile_damage); - hostile_damage = round(hostile_damage) * hit_number; - target_object.hp -= hostile_damage; - if (target_object.hp <= 0) { - dest = 1; - } - obj_nfort.hostile_weapons = hostile_weapon; - obj_nfort.hostile_shots = hit_number; - obj_nfort.hostile_damage = hostile_damage; - - scr_flavor2(dest, "wall", hostile_range, hostile_weapon, hit_number, hostile_splash); - } else { - target_object.hostile_shooters = (wep_owner[weapon_index_position] == "assorted") ? 999 : 1; - hostile_type = 0; - - scr_clean(target_object, hostile_type, hit_number, hostile_damage, hostile_weapon, hostile_range, hostile_splash, armour_pierce); - } - } - } + // Verify current target rank status + if (target_object.dudes_num[target_type] <= 0) { + var _alive_rank = find_next_alive_rank(target_object, -1); + if (_alive_rank == -1) { + destroy_empty_column(target_object); + exit; } - if (instance_exists(target_object) && (owner == eFACTION.PLAYER)) { - var shots_fired = 0; - var stop = 0; - var damage_type = ""; + target_type = _alive_rank; + } - if (weapon_index_position >= 0) { - shots_fired = wep_num[weapon_index_position]; - } + var _target_block = target_object; + var _target_rank = target_type; + var _shots_left = shots_fired; + var _touched_blocks = [_target_block]; - if (shots_fired == 0) { - exit; + var _first_target = true; + var _primary_flavour = undefined; + var _spill_kills = []; + + // Distribute damage spillover across units + while (_shots_left > 0) { + var _rank_num = _target_block.dudes_num[_target_rank]; + var _rank_hp = _target_block.dudes_hp[_target_rank]; + + var _results = combat_apply_rank_damage(_target_block, _target_rank, damage_per_weapon, armour_pierce, attack_count_mod, _shots_left); + + var _casualties = _results.casualties; + var _final_hit = _results.final_hit; + + var _next_shots = 0; + if (_casualties >= _rank_num && _rank_num > 0 && _results.raw_kills > _rank_num) { + _next_shots = max(0, _shots_left - ceil((_rank_num * _rank_hp) / _final_hit)); + } + + if (_first_target) { + _primary_flavour = scr_flavor(weapon_index_position, _target_block, _target_rank, shots_fired, _casualties, _final_hit <= 0, true); + _first_target = false; + } else if (_casualties > 0) { + array_push(_spill_kills, {name: _target_block.dudes[_target_rank], count: _casualties}); + } + + _shots_left = _next_shots; + if (_shots_left <= 0) { + break; + } + + var _next_rank = find_next_alive_rank(_target_block, _target_block.dudes_vehicle[_target_rank]); + if (_next_rank == -1) { + _target_block = get_next_enemy_formation(_target_block); + if (_target_block == noone) { + break; } - while (target_type < array_length(target_object.dudes_hp)) { - if (target_object.dudes_hp[target_type] == 0) { - target_type++; - stop = 1; - } else { - stop = 0; - break; - } + array_push(_touched_blocks, _target_block); + _next_rank = find_next_alive_rank(_target_block, -1); + if (_next_rank == -1) { + break; } + } - if (weapon_index_position >= 0) { - if (ammo[weapon_index_position] == 0) { - stop = 1; - } - if (ammo[weapon_index_position] > 0) { - ammo[weapon_index_position] -= 1; + _target_rank = _next_rank; + } + + emit_volley_flavour(_primary_flavour, _spill_kills); + combat_cleanup_formations(_touched_blocks); +} + +/// @self Asset.GMObject.obj_pnunit +/// @description Speed Force: sweep the whole enemy force, dividing damage proportionally to rank +/// size, and report it as ONE consolidated volley line (see emit_volley_flavour). +/// @param {Real} weapon_index_position The Speed Force weapon stack index. +function scr_shoot_spread(weapon_index_position) { + try { + if (wep_num[weapon_index_position] <= 0 || ammo[weapon_index_position] == 0) { + exit; + } + + var _shots = wep_num[weapon_index_position]; + var _ap = apa[weapon_index_position]; + var _damage_per_weapon = att[weapon_index_position] / _shots; + var _splash = max(1, splash[weapon_index_position]); + + if (ammo[weapon_index_position] > 0) { + ammo[weapon_index_position] -= 1; + } + + var _formations = []; + var _total = 0; + with (obj_enunit) { + array_push(_formations, id); + for (var _rank = 1; _rank <= 30; _rank++) { + if (dudes[_rank] != "" && dudes_num[_rank] > 0 && dudes_hp[_rank] > 0) { + _total += dudes_num[_rank]; } } - if (wep[weapon_index_position] == "Missile Silo") { - obj_ncombat.player_silos -= min(obj_ncombat.player_silos, 30); - } + } - if (damage_data != "highest") { - damage_type = damage_data; + if (_total <= 0) { + exit; + } + + var _hits = []; + var _wounded = undefined; + for (var fi = 0; fi < array_length(_formations); fi++) { + var _block = _formations[fi]; + if (!instance_exists(_block)) { + continue; } - if ((damage_data == "highest") && (weapon_index_position >= 0)) { - damage_type = "att"; - if ((aggregate_damage >= 100) && (armour_pierce > 0)) { - damage_type = "arp"; + + for (var _rank = 1; _rank <= 30; _rank++) { + if (_block.dudes[_rank] == "" || _block.dudes_num[_rank] <= 0 || _block.dudes_hp[_rank] <= 0) { + continue; } - } - if (damage_data == "highest") { - if (weapon_index_position == -51 || weapon_index_position == -52 || weapon_index_position == -53) { - damage_type = "att"; + + var _rank_shots = _shots * (_block.dudes_num[_rank] / _total); + var _results = combat_apply_rank_damage(_block, _rank, _damage_per_weapon, _ap, _splash, _rank_shots); + + if (_results.casualties > 0) { + array_push(_hits, {name: _block.dudes[_rank], kills: _results.casualties, bounced: _results.bounced, block: _block, rank: _rank}); + } else if (_wounded == undefined) { + _wounded = { + bounced: _results.bounced, + block: _block, + rank: _rank, + }; } } + } - if ((weapon_index_position >= 0) || (weapon_index_position < -40)) { - // Normal shooting - var overkill = 0, damage_remaining = 0, shots_remaining = 0; - - var that_works = false; - - if (weapon_index_position >= 0) { - if ((aggregate_damage > 0) && (stop == 0)) { - that_works = true; - } - } - if ((weapon_index_position < -40) && (stop == 0)) { - that_works = true; + // Generate and emit flavor text + var _primary = undefined; + var _spill = []; + if (array_length(_hits) > 0) { + var _best = 0; + for (var i = 1; i < array_length(_hits); i++) { + if (_hits[i].kills > _hits[_best].kills) { + _best = i; } + } - if (that_works == true) { - var damage_per_weapon = 0, c = 0, target_armour_value = 0, ap = 0, wii = ""; - var attack_count_mod = 0; - - if (weapon_index_position >= 0) { - damage_per_weapon = aggregate_damage / wep_num[weapon_index_position]; - ap = armour_pierce; - } // Average damage - if (weapon_index_position < -40) { - wii = ""; - attack_count_mod = 3; - - if (weapon_index_position == -51) { - wii = "Heavy Bolter Emplacement"; - at = 160; - armour_pierce = 0; - } - if (weapon_index_position == -52) { - wii = "Missile Launcher Emplacement"; - at = 200; - armour_pierce = -1; - } - if (weapon_index_position == -53) { - wii = "Missile Silo"; - at = 250; - armour_pierce = 0; - } - } - - target_armour_value = target_object.dudes_ac[target_type]; - - // Calculate final armor value based on armor piercing (AP) rating against target type - if (target_object.dudes_vehicle[target_type]) { - if (armour_pierce == 4) { - target_armour_value = 0; - } - if (armour_pierce == 3) { - target_armour_value = target_armour_value * 2; - } - if (armour_pierce == 2) { - target_armour_value = target_armour_value * 4; - } - if (armour_pierce == 1) { - target_armour_value = target_armour_value * 6; - } - } else { - if (armour_pierce == 4) { - target_armour_value = 0; - } - if (armour_pierce == 3) { - target_armour_value = target_armour_value * 1.5; - } - if (armour_pierce == 2) { - target_armour_value = target_armour_value * 2; - } - if (armour_pierce == 1) { - target_armour_value = target_armour_value * 3; - } - } - - attack_count_mod = max(1, splash[weapon_index_position]); - - final_hit_damage_value = max(0, damage_per_weapon - target_armour_value); //damage armour reduction - - final_hit_damage_value *= target_object.dudes_dr[target_type]; //damage_resistance mod - - c = shots_fired * final_hit_damage_value; // New damage - - var casualties, onceh = 0, ponies = 0; - - casualties = min(floor(c / target_object.dudes_hp[target_type]), shots_fired * attack_count_mod); - - ponies = target_object.dudes_num[target_type]; - if ((target_object.dudes_num[target_type] == 1) && ((target_object.dudes_hp[target_type] - c) <= 0)) { - casualties = 1; - } - - if (target_object.dudes_num[target_type] - casualties < 0) { - overkill = casualties - target_object.dudes_num[target_type]; - damage_remaining = c - (overkill * target_object.dudes_hp[target_type]); - - shots_remaining = round(damage_remaining / damage_per_weapon); - } - - if (target_object.dudes_num[target_type] - casualties < 0) { - casualties = ponies; - } - if (casualties < 0) { - casualties = 0; - } - - if (casualties >= 1) { - var iii = 0, found = 0, openz = 0; - for (iii = 0; iii <= 40; iii++) { - iii += 1; - if (found == 0) { - if ((obj_ncombat.dead_ene[iii] == "") && (openz == 0)) { - openz = iii; - } - if ((obj_ncombat.dead_ene[iii] == target_object.dudes[target_type]) && (found == 0)) { - found = iii; - obj_ncombat.dead_ene_n[obj_ncombat.dead_enemies] += casualties; - } - } - } - if (found == 0) { - obj_ncombat.dead_enemies += 1; - obj_ncombat.dead_ene[openz] = string(target_object.dudes[target_type]); - obj_ncombat.dead_ene_n[openz] = casualties; - } - } - - var k = 0; - if ((damage_remaining > 0) && (shots_remaining > 0)) { - repeat (10) { - if ((damage_remaining > 0) && (shots_remaining > 0)) { - var godd; - godd = 0; - k = target_type; - - // Find similar target in this same group - repeat (10) { - k += 1; - if (godd == 0) { - if ((target_object.dudes_num[k] > 0) && (target_object.dudes_vehicle[k] == target_object.dudes_vehicle[target_type])) { - godd = k; - } - } - } - k = target_type; - if (godd == 0) { - repeat (10) { - k -= 1; - if ((godd == 0) && (k >= 1)) { - if ((target_object.dudes_num[k] > 0) && (target_object.dudes_vehicle[k] == target_object.dudes_vehicle[target_type])) { - godd = k; - } - } - } - } - - // Found damage_per_weapon similar target to get the damage - if ((godd > 0) && (damage_remaining > 0) && (shots_remaining > 0)) { - var a2, b2, c2, target_armour_value2, ap2; - ap2 = damage_remaining; - a2 = damage_per_weapon; // Average damage - - target_armour_value2 = target_object.dudes_ac[godd]; - if (target_object.dudes_vehicle[godd] == 0) { - if (ap2 == 1) { - target_armour_value2 = target_armour_value2 * 3; - } - if (ap2 == 2) { - target_armour_value2 = target_armour_value2 * 2; - } - if (ap2 == 3) { - target_armour_value2 = target_armour_value2 * 1.5; - } - if (ap2 == 4) { - target_armour_value2 = 0; - } - } - if (target_object.dudes_vehicle[godd] == 1) { - if (ap2 == 1) { - target_armour_value2 = target_armour_value2 * 6; - } - if (ap2 == 2) { - target_armour_value2 = target_armour_value2 * 4; - } - if (ap2 == 3) { - target_armour_value2 = target_armour_value2 * 2; - } - } - b2 = a2 - target_armour_value2; - if (b2 <= 0) { - b2 = 0; - } // Average after armour - - c2 = b2 * shots_remaining; // New damage - - var casualties2 = 0; - var onceh2 = 0; - var ponies2 = 0; - if (attack_count_mod <= 1) { - casualties2 = min(floor(c2 / target_object.dudes_hp[godd]), shots_remaining); - } - - if (attack_count_mod > 1) { - casualties2 = floor(c2 / target_object.dudes_hp[godd]); - } - ponies2 = target_object.dudes_num[godd]; - if ((target_object.dudes_num[godd] == 1) && ((target_object.dudes_hp[godd] - c2) <= 0)) { - casualties2 = 1; - } - if (target_object.dudes_num[godd] < casualties2) { - casualties2 = target_object.dudes_num[godd]; - } - if (casualties2 < 1) { - casualties2 = 0; - damage_remaining = 0; - overkill = 0; - shots_remaining = 0; - } - - if ((casualties2 >= 1) && (shots_fired > 0)) { - var iii = 0; - var found = 0; - var openz = 0; - repeat (40) { - iii += 1; - if (found == 0) { - if ((obj_ncombat.dead_ene[iii] == "") && (openz == 0)) { - openz = iii; - } - if ((obj_ncombat.dead_ene[iii] == target_object.dudes[godd]) && (found == 0)) { - found = iii; - obj_ncombat.dead_ene_n[obj_ncombat.dead_enemies] += casualties; - } - } - } - if (found == 0) { - obj_ncombat.dead_enemies += 1; - obj_ncombat.dead_ene[openz] = string(target_object.dudes[godd]); - obj_ncombat.dead_ene_n[openz] = casualties; - } - - target_object.dudes_num[godd] -= casualties2; - obj_ncombat.enemy_forces -= casualties2; - } - } - } - } - } // End repeat 10 - scr_flavor(weapon_index_position, target_object, target_type, shots_fired - wep_rnum[weapon_index_position], casualties); - - if ((target_object.dudes_num[target_type] == 1) && (c > 0)) { - target_object.dudes_hp[target_type] -= c; - } // Need special flavor here for just damaging - - if (casualties >= 1) { - target_object.dudes_num[target_type] -= casualties; - obj_ncombat.enemy_forces -= casualties; - } + for (var i = 0; i < array_length(_hits); i++) { + if (i == _best) { + continue; } + + array_push(_spill, {name: _hits[i].name, count: _hits[i].kills}); } - if (stop == 0) { - compress_enemy_array(target_object); - destroy_empty_column(target_object); + var _p = _hits[_best]; + if (instance_exists(_p.block)) { + _primary = scr_flavor(weapon_index_position, _p.block, _p.rank, _shots, _p.kills, _p.bounced, true); } + } else if (_wounded != undefined && instance_exists(_wounded.block)) { + _primary = scr_flavor(weapon_index_position, _wounded.block, _wounded.rank, _shots, 0, _wounded.bounced, true); } + + emit_volley_flavour(_primary, _spill); + combat_cleanup_formations(_formations); } catch (_exception) { ERROR_HANDLER.handle_exception(_exception); }