From 5cee22db2a8c6db68ff58a7d57540dc2dbda956f Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:39:21 +0300 Subject: [PATCH 1/9] refactor: scr_roman string fallback --- scripts/scr_roman/scr_roman.gml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/scr_roman/scr_roman.gml b/scripts/scr_roman/scr_roman.gml index 7a311e2252..2c8a7c4e8d 100644 --- a/scripts/scr_roman/scr_roman.gml +++ b/scripts/scr_roman/scr_roman.gml @@ -16,4 +16,6 @@ function scr_roman(argument0) { if (argument0 == 5) { return "V"; } + + return "??" } From 5e5832d8343e53a491c4f3e7e2bcffea972aa5d5 Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:40:19 +0300 Subject: [PATCH 2/9] fix: Fleet inspection error --- .../scr_inquisition_inspection.gml | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/scripts/scr_inquisition_inspection/scr_inquisition_inspection.gml b/scripts/scr_inquisition_inspection/scr_inquisition_inspection.gml index ac4f99ce3f..1d93b08dd4 100644 --- a/scripts/scr_inquisition_inspection/scr_inquisition_inspection.gml +++ b/scripts/scr_inquisition_inspection/scr_inquisition_inspection.gml @@ -60,9 +60,10 @@ function inquisitor_inspection_structure() constructor { }; ships = -1; - planets = 0; // can be single integer or an array of planet indices + planets = undefined; // can be single integer or an array of planet indices location = ""; // location string for collect_role_group - star = -1; // star instance + /// @type {Id.Instance.obj_star} + star = noone; // star instance units = []; // collected units for inspection // convenience flags populated during inspection @@ -83,7 +84,7 @@ function inquisitor_inspection_structure() constructor { }; static planet_heresys = function() { - if (instance_exists(star)) { + if (instance_exists(star) && planets != undefined) { if (is_array(planets)) { for (var i = 0; i < array_length(planets); i++) { var _p = planets[i]; @@ -199,7 +200,7 @@ function inquisitor_inspection_structure() constructor { // ----- Inspection modules that use internal star & planets ----- static inspect_secret_base = function() { - if (!instance_exists(star)) { + if (!instance_exists(star) || planets == undefined) { return; } @@ -259,7 +260,7 @@ function inquisitor_inspection_structure() constructor { }; static inspect_arsenal = function() { - if (!instance_exists(star)) { + if (!instance_exists(star) || planets == undefined) { return; } @@ -324,7 +325,7 @@ function inquisitor_inspection_structure() constructor { }; static inspect_gene_vault = function() { - if (!instance_exists(star)) { + if (!instance_exists(star) || planets == undefined) { return; } @@ -389,12 +390,15 @@ function inquisitor_inspection_structure() constructor { } var planet_label = ""; - if (is_array(planets)) { - // show first planet for display purposes - planet_label = scr_roman(planets[0]); - } else { - planet_label = scr_roman(planets); + if (planets != undefined) { + if (is_array(planets)) { + // show first planet for display purposes + planet_label = scr_roman(planets[0]); + } else { + planet_label = scr_roman(planets); + } } + var star_planet = star.name + planet_label; // Logging @@ -522,25 +526,27 @@ function inquisitor_inspection_structure() constructor { }; } +/// @self Id.Instance.obj_en_fleet function inquisition_inspection_loyalty(inspection_type) { if ((inspection_type == "inspect_world") || (inspection_type == "inspect_fleet")) { var _inspect_results = new inquisitor_inspection_structure(); - that = instance_nearest(x, y, obj_star); + /// @type {Id.Instance.obj_star} + var _star = instance_nearest(x, y, obj_star); if (inspection_type == "inspect_world") { - var _monestary_planet = scr_get_planet_with_feature(that, eP_FEATURES.MONASTERY); + var _monestary_planet = scr_get_planet_with_feature(_star, eP_FEATURES.MONASTERY); if (_monestary_planet != -1) { _inspect_results.planets = _monestary_planet; } else { var _plans = []; - for (var i = 1; i <= that.planets; i++) { + for (var i = 1; i <= _star.planets; i++) { array_push(_plans, i); } _inspect_results.planets = _plans; } - _inspect_results.star = that; + _inspect_results.star = _star; _inspect_results.planet_heresys(); @@ -720,6 +726,7 @@ function inquisition_inspection_loyalty(inspection_type) { } } +/// @param {Id.Instance.obj_star} cur_star function inquisitor_contraband_take_popup(cur_star, planet) { var _inspect = new inquisitor_inspection_structure(); @@ -747,6 +754,7 @@ function inquisitor_contraband_take_popup(cur_star, planet) { _inspect.finalize_contraband_popup(); } +/// @self Id.Instance.obj_en_fleet function inquisition_inspection_logic() { var inspec_alert_string = ""; var cur_star = instance_nearest(x, y, obj_star); From 450223922e35bcd574a728dded9c542225d67cce Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:07:34 +0300 Subject: [PATCH 3/9] fix: Melee in raids; Refactor player targeting --- objects/obj_pnunit/Alarm_0.gml | 391 +++++------------- objects/obj_pnunit/Create_0.gml | 42 +- .../scr_player_combat_weapon_stacks.gml | 7 +- .../scr_punit_combat_heplers.gml | 53 +++ scripts/scr_target/scr_target.gml | 103 ++--- 5 files changed, 240 insertions(+), 356 deletions(-) diff --git a/objects/obj_pnunit/Alarm_0.gml b/objects/obj_pnunit/Alarm_0.gml index 61aac367c8..9bf15aba4a 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -1,13 +1,20 @@ try { obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SYSTEM, $"Player block {obj_ncombat.combat_debugger.resolve_label(id)} at x={x} is picking a target"); - enemy = instance_nearest(0, y, obj_enunit); // Left most enemy - if (!instance_exists(enemy)) { + if (!instance_exists(obj_enunit)) { obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"no enemy exists, exiting"); + enemy = noone; engaged = false; + push_held_fire(); exit; } + if (obj_ncombat.dropping || (!obj_ncombat.defending && obj_ncombat.formation_set != 2)) { + move_unit_block("east"); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"moved east"); + } + + enemy = instance_nearest(0, y, obj_enunit); obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"initial target {obj_ncombat.combat_debugger.resolve_label(enemy)} at x={enemy.x}"); if (instance_number(obj_enunit) != 1) { @@ -19,306 +26,120 @@ try { } } - if (obj_ncombat.dropping || (!obj_ncombat.defending && obj_ncombat.formation_set != 2)) { - move_unit_block("east"); - } + engaged = collision_point(x - 10, y, obj_enunit, 0, 1) != noone || collision_point(x + 10, y, obj_enunit, 0, 1) != noone; + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"engaged = {engaged}"); - engaged = collision_point(x - 14, y, obj_enunit, 0, 1) != noone || collision_point(x + 14, y, obj_enunit, 0, 1) != noone; + //* Psychic power buffs + tick_psychic_buffs(); - var once_only = 0; - var range_shoot = ""; - var dist = point_distance(x, y, enemy.x, enemy.y) / 10; + obj_ncombat.ctally_target = undefined; + obj_ncombat.ctally_bounce = []; + obj_ncombat.ctally_injure = []; - //* Psychic power buffs - for (var i = 0; i < array_length(unit_struct); i++) { - if (marine_mshield[i] > 0) { - marine_mshield[i] -= 1; - } - if (marine_quick[i] > 0) { - marine_quick[i] -= 1; - } - if (marine_might[i] > 0) { - marine_might[i] -= 1; + for (var i = 0; i < array_length(wep); i++) { + if (!instance_exists(obj_enunit)) { + push_held_fire(i); + break; } - if (marine_fiery[i] > 0) { - marine_fiery[i] -= 1; - } - if (marine_fshield[i] > 0) { - marine_fshield[i] -= 1; - } - if (marine_dome[i] > 0) { - marine_dome[i] -= 1; - } - if (marine_spatial[i] > 0) { - marine_spatial[i] -= 1; + + if (wep[i] == "" || ammo[i] == 0 || att[i] <= 0) { + continue; } - } - if (instance_exists(obj_enunit)) { - obj_ncombat.ctally_target = undefined; - obj_ncombat.ctally_bounce = []; - obj_ncombat.ctally_injure = []; - for (var i = 0; i < array_length(wep); i++) { - // Enemies wiped before every weapon got to fire (e.g. spill-over cleared the line). - // Report who held fire and stop, rather than swinging at empty air. - if (!instance_exists(obj_enunit)) { - var _held_fire = []; - for (var hf = i; hf < array_length(wep); hf++) { - // Only ranged weapons "hold fire"; melee (range 1) never shoots, so skip it. - // Mirror the firing ammo gate (ammo != 0) so out-of-ammo weapons that could not - // have fired aren't reported as having held fire. - if (wep[hf] != "" && wep_num[hf] > 0 && range[hf] > 1 && ammo[hf] != 0) { - array_push(_held_fire, wep[hf]); - } - } - report_held_fire(_held_fire); + enemy = instance_nearest(0, y, obj_enunit); + + if (enemy.men + enemy.veh + enemy.medi <= 0) { + with (enemy) { + instance_destroy(); + } + + enemy = instance_nearest(0, y, obj_enunit); + if (!instance_exists(enemy)) { + engaged = false; break; } - if (wep[i] == "") { + } + + var _is_ap = apa[i] > 2; + var _dist = point_distance(x, y, enemy.x, enemy.y) / 10; + var _mode = engaged ? "melee" : "ranged"; + + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon slot [{i}] is attacking ({wep_num[i]}x{wep[i]} ATTK:{att[i]} AP:{apa[i]}, RNG:{range[i]}, AMM:{ammo[i]})"); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"distance to the enemy at x={enemy.x} is {_dist}"); + + if (_mode == "ranged") { + if (range[i] < _dist) { continue; } - once_only = 0; - enemy = instance_nearest(0, y, obj_enunit); - if (enemy.men + enemy.veh + enemy.medi <= 0) { - var x5 = enemy.x; - with (enemy) { - instance_destroy(); - } - enemy = instance_nearest(0, y, obj_enunit); - if (!instance_exists(enemy)) { - engaged = false; - break; - } - } - if ((range[i] >= dist) && (ammo[i] != 0 || range[i] == 1)) { - if ((range[i] != 1) && (engaged == 0)) { - range_shoot = "ranged"; - } - if ((range[i] != floor(range[i]) || floor(range[i]) == 1) && engaged == 1) { - range_shoot = "melee"; - } + // Solo vehicle columns are always valid AP targets + if (instance_number(obj_enunit) == 1 && enemy.men == 0 && enemy.medi == 0 && enemy.veh > 0) { + _is_ap = true; } - if ((range_shoot == "ranged") && (range[i] >= dist)) { - // Weapon meets preliminary checks - var ap = 0; - var good = 0; - if (apa[i] > att[i]) { - ap = 1; - } // Determines if it is AP or not - if (wep[i] == "Missile Launcher") { - ap = 1; - } - if (string_count("Lascan", wep[i]) > 0) { - ap = 1; - } - if ((instance_number(obj_enunit) == 1) && (obj_enunit.men == 0) && (obj_enunit.veh > 0)) { - ap = 1; - } - - if ((obj_enunit.veh > 0) && (obj_enunit.men == 0) && (apa[i] > 10)) { - ap = 1; - } - - if ((ap == 1) && (once_only == 0)) { - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] ({wep[i]}) AP -> targeting vehicles"); - // Check for vehicles - if (enemy.veh > 0) { - good = scr_target(enemy, "veh"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] scr_target(veh) -> {enemy.dudes_vehicle[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy)}"); - scr_shoot(i, enemy, good, "arp", "ranged"); - } - if ((good == 0) && (instance_number(obj_enunit) > 1)) { - // First target does not have vehicles, cycle through objects to find one that has vehicles - var x2 = enemy.x; - repeat (instance_number(obj_enunit) - 1) { - if (good == 0) { - x2 += 10; - var enemy2 = instance_nearest(x2, y, obj_enunit); - if ((enemy2.veh > 0) && (good == 0)) { - good = scr_target(enemy2, "veh"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] scr_target(veh) at next column -> {enemy2.dudes_vehicle[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy2)}"); - scr_shoot(i, enemy2, good, "arp", "ranged"); - once_only = 1; - } - } - } - } - if (good == 0) { - ap = 0; - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] ({wep[i]}) AP -> no vehicles found, falling back to infantry"); - } - if (!instance_exists(enemy)) { - engaged = false; - continue; - } - } - - if (once_only == 0) { - if ((enemy.medi > 0) && (enemy.veh == 0)) { - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] ({wep[i]}) medi -> targeting monsters"); - good = scr_target(enemy, "medi"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] scr_target(medi) -> {enemy.dudes[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy)}"); - scr_shoot(i, enemy, good, "medi", "ranged"); - - if ((good == 0) && (instance_number(obj_enunit) > 1)) { - var x2 = enemy.x; - repeat (instance_number(obj_enunit) - 1) { - if (good == 0) { - x2 += 10; - var enemy2 = instance_nearest(x2, y, obj_enunit); - if ((enemy2.veh > 0) && (good == 0)) { - good = scr_target(enemy2, "medi"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] scr_target(medi) at next column -> {enemy2.dudes[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy2)}"); - scr_shoot(i, enemy2, good, "medi", "ranged"); - once_only = 1; - } - } - } - } - if (good == 0) { - ap = 0; - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] ({wep[i]}) medi -> no monsters found, falling back to infantry"); - } - } - if (!instance_exists(enemy)) { - engaged = false; - continue; - } - } - - if ((ap == 0) && (once_only == 0)) { - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] ({wep[i]}) -> targeting infantry"); - good = 0; - - if (enemy.men + enemy.medi > 0) { - good = scr_target(enemy, "men"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] scr_target(men) -> {enemy.dudes[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy)}"); - scr_shoot(i, enemy, good, "att", "ranged"); - } - if ((good == 0) && (instance_number(obj_enunit) > 1)) { - var x2 = enemy.x; - repeat (instance_number(obj_enunit) - 1) { - if (good == 0) { - x2 += 10; - var enemy2 = instance_nearest(x2, y, obj_enunit); - if ((enemy2.men > 0) && (good == 0)) { - good = scr_target(enemy2, "men"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"weapon[{i}] scr_target(men) at next column -> {enemy2.dudes[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy2)}"); - scr_shoot(i, enemy2, good, "att", "ranged"); - once_only = 1; - } - } - } - } - if (!instance_exists(enemy)) { - engaged = false; - continue; - } - } - } else if ((range_shoot == "melee") && ((range[i] == 1) || (range[i] != floor(range[i])))) { - // Weapon meets preliminary checks - var ap = 0; - if (apa[i] == 1) { - ap = 1; - } // Determines if it is AP or not - - if ((enemy.men == 0) && (apa[i] == 0) && (att[i] >= 80)) { - apa[i] = floor(att[i] / 2); - ap = 1; - } - - if ((apa[i] == 1) && (once_only == 0)) { - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee weapon[{i}] ({wep[i]}) AP -> targeting vehicles"); - // Check for vehicles - var g = 0, good = 0; - - if (enemy.veh > 0) { - good = scr_target(enemy, "veh"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee scr_target(veh) -> {enemy.dudes_vehicle[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy)}"); - if (range[i] == 1) { - scr_shoot(i, enemy, good, "arp", "melee"); - } - } - if (good != 0) { - once_only = 1; - } - if ((good == 0) && (att[i] > 0)) { - ap = 0; - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee weapon[{i}] AP -> no vehicles, fallback to infantry"); - } - if (!instance_exists(enemy)) { - engaged = false; - continue; - } - } - - if ((enemy.veh == 0) && (enemy.medi > 0) && (once_only == 0)) { - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee weapon[{i}] ({wep[i]}) -> targeting monsters"); - var g = 0, good = 0; - - if (enemy.medi > 0) { - good = scr_target(enemy, "medi"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee scr_target(medi) -> {enemy.dudes[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy)}"); - if (range[i] == 1) { - scr_shoot(i, enemy, good, "medi", "melee"); - } - } - if (good != 0) { - once_only = 1; - } - if ((good == 0) && (att[i] > 0)) { - ap = 0; - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee weapon[{i}] medi -> no monsters, fallback to infantry"); - } - if (!instance_exists(enemy)) { - engaged = false; - continue; - } - } - - if ((ap == 0) && (once_only == 0)) { - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee weapon[{i}] ({wep[i]}) -> targeting infantry"); - var good = 0; - - if ((enemy.men > 0) && (once_only == 0)) { - good = scr_target(enemy, "men"); - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"melee scr_target(men) -> {enemy.dudes[good]} in {obj_ncombat.combat_debugger.resolve_label(enemy)}"); - if (range[i] == 1) { - scr_shoot(i, enemy, good, "att", "melee"); - } - } - if (good != 0) { - once_only = 1; - } - if (!instance_exists(enemy)) { - engaged = false; - continue; - } - } + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"shooting"); + } else { + var _can_melee = (range[i] <= 2) || (floor(range[i]) != range[i]); + if (!_can_melee) { + continue; } + + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"attacking in melee"); } - } else { - // The field was already clear when this block's turn came up - its whole arsenal holds fire. - var _skipped_fire = []; - for (var s = 0; s < array_length(wep); s++) { - // Only ranged weapons "hold fire"; melee (range 1) never shoots, so skip it. - // Mirror the firing ammo gate (ammo != 0) so out-of-ammo weapons that could not have - // fired aren't reported as having held fire. - if (wep[s] != "" && wep_num[s] > 0 && range[s] > 1 && ammo[s] != 0) { - array_push(_skipped_fire, wep[s]); + + var _priorities = [ + { + guard: _is_ap && enemy.veh > 0, + type: "veh", + damage: "arp", + label: "vehicles", + }, + { + guard: enemy.medi > 0, + type: "medi", + damage: "medi", + label: "monsters", + }, + { + guard: enemy.men > 0, + type: "men", + damage: "att", + label: "infantry", + }, + ]; + + for (var p = 0; p < array_length(_priorities); p++) { + var _priority_data = _priorities[p]; + if (!_priority_data.guard) { + continue; } + + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"targeting {_priority_data.label}"); + + var _column = pick_target_column(_priority_data.type, _mode); + if (_column == undefined) { + continue; + } + + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"column {obj_ncombat.combat_debugger.resolve_label(_column)}"); + + var _unit_i = scr_target(_column, _priority_data.type); + if (_unit_i == undefined) { + continue + } + + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"unit {_column.dudes[_unit_i]}"); + + scr_shoot(i, _column, _unit_i, _priority_data.damage, _mode); + break; } - report_held_fire(_skipped_fire); } combat_tally_flush(); instance_activate_object(obj_enunit); - // Safety net: drop empty/zombie formations the firing loop never reached, so a lingering corpse - // can't keep the battle alive. + // Safety net: drop empty/zombie formations the firing loop never reached with (obj_enunit) { var _alive = 0; for (var _rr = 1; _rr < array_length(dudes_num); _rr++) { @@ -326,24 +147,22 @@ try { _alive += dudes_num[_rr]; } } - if ((_alive == 0) && (owner != 1)) { + + if (_alive == 0 && owner != 1) { instance_destroy(); } } if (instance_exists(obj_enunit)) { - // Accumulate this formation's attack casts, then emit one summary line per power instead - // of one line per Librarian (see flush_psychic_summary in scr_powers). var _psy_log = {}; for (var i = 0; i < array_length(unit_struct); i++) { if (marine_dead[i] == 0 && marine_casting[i] == true) { - var caster_id = i; - scr_powers(caster_id, _psy_log); + scr_powers(i, _psy_log); } } + flush_psychic_summary(_psy_log); } -} -catch (_exception) { +} catch (_exception) { ERROR_HANDLER.handle_exception(_exception); } diff --git a/objects/obj_pnunit/Create_0.gml b/objects/obj_pnunit/Create_0.gml index 9f6388fa39..9fdee7229d 100644 --- a/objects/obj_pnunit/Create_0.gml +++ b/objects/obj_pnunit/Create_0.gml @@ -9,7 +9,8 @@ attacked_dudes = 0; dreads = 0; jetpack_destroy = 0; defenses = 0; -enemy = instance_nearest(x, y, obj_enunit); +/// @type {Id.Instance.obj_enunit} +enemy = noone; unit_count = 0; unit_count_old = 0; @@ -28,6 +29,7 @@ y2 = 450 + (draw_size / 2); // x determines column; maybe every 10 or so? // For fortified locations maybe create a wall unit for the player? +/// @type {Array 0 && range[i] > 1 && ammo[i] != 0) { + array_push(_skipped_fire, wep[i]); + } + } + report_held_fire(_skipped_fire); +} + +/// @description Tick down psychic buff durations for all marines in the calling player column. +tick_psychic_buffs = function() { + try { + var _buffs = [ + "marine_mshield", + "marine_quick", + "marine_might", + "marine_fiery", + "marine_fshield", + "marine_dome", + "marine_spatial", + "marine_iron", + "marine_dementia", + ]; + for (var i = 0; i < array_length(unit_struct); i++) { + for (var b = 0; b < array_length(_buffs); b++) { + var _name = _buffs[b]; + if (self[$ _name][i] > 0) { + self[$ _name][i] -= 1; + } + } + } + } catch (_exception) { + ERROR_HANDLER.handle_exception(_exception); + } +} 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 3ad455a594..0b0b665e8b 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 @@ -59,7 +59,7 @@ function add_data_to_stack(stack_index, weapon, unit_damage = false, head_role = /// @self Asset.GMObject.obj_pnunit function find_stack_index(weapon_name, head_role = false, unit = "none") { - final_index = -1; + var final_index = -1; var allow = false; for (var stack_index = 1; stack_index < array_length(wep); stack_index++) { allow = false; @@ -242,15 +242,14 @@ function scr_player_combat_weapon_stacks() { var primary_melee = marine_attack[g][3]; //collect unit melee data weapon_stack_index = find_stack_index(primary_melee.name, head_role, unit); if (weapon_stack_index > -1) { - if (range[weapon_stack_index] > 1.9) { + if (range[weapon_stack_index] >= 2) { continue; } //creates secondary weapon stack for close combat ranged weaponry use - primary_melee.range = 1; add_data_to_stack(weapon_stack_index, primary_melee, marine_attack[g][0], head_role, unit); if (head_role) { player_head_role_stack(weapon_stack_index, unit); } - if (floor(primary_melee.range) <= 1 && primary_melee.ammo == 0) { + if (floor(primary_melee.range) < 2 && primary_melee.ammo == 0) { ammo[weapon_stack_index] = -1; //no ammo limit } } diff --git a/scripts/scr_punit_combat_heplers/scr_punit_combat_heplers.gml b/scripts/scr_punit_combat_heplers/scr_punit_combat_heplers.gml index 0364003fcc..55912ec98f 100644 --- a/scripts/scr_punit_combat_heplers/scr_punit_combat_heplers.gml +++ b/scripts/scr_punit_combat_heplers/scr_punit_combat_heplers.gml @@ -270,3 +270,56 @@ function update_block_size() { function update_block_unit_count() { unit_count = men + medi + dreads + veh; } + +/// @description Check if a column has the given target type. +/// @param {Id.Instance.obj_enunit} column Enemy battle block to check +/// @param {string} target_type "veh", "medi", or "men" +/// @param {string} mode "ranged" or "melee" +/// @returns {bool} true if the column has units matching the target type +function has_target_type(column, target_type, mode = "ranged") { + try { + switch (target_type) { + case "veh": + return column.veh > 0; + case "medi": + return column.medi > 0; + case "men": + return mode == "ranged" ? column.men + column.medi > 0 : column.men > 0; + } + + return false; + } catch (_exception) { + ERROR_HANDLER.handle_exception(_exception); + return false; + } +} + +/// @description Find an enemy column that has the given target type. Checks the current enemy column first, then cycles through other columns in ranged mode. +/// @param {string} target_type "veh", "medi", or "men" +/// @param {string} mode "ranged" or "melee" +/// @returns {Id.Instance|undefined} The column instance, or undefined if none found +/// @self Asset.GMObject.obj_pnunit +function pick_target_column(target_type, mode) { + try { + if (has_target_type(enemy, target_type, mode)) { + return enemy; + } + + if (mode == "ranged" && instance_number(obj_enunit) > 1) { + var _x2 = enemy.x; + repeat (instance_number(obj_enunit) - 1) { + _x2 += 10; + var _enemy2 = instance_nearest(_x2, y, obj_enunit); + + if (has_target_type(_enemy2, target_type, mode)) { + return _enemy2; + } + } + } + + return undefined; + } catch (_exception) { + ERROR_HANDLER.handle_exception(_exception); + return undefined; + } +} diff --git a/scripts/scr_target/scr_target.gml b/scripts/scr_target/scr_target.gml index 690fa730b3..cf0cec97e2 100644 --- a/scripts/scr_target/scr_target.gml +++ b/scripts/scr_target/scr_target.gml @@ -1,77 +1,50 @@ -function scr_target(battle_block, man_or_vehicle) { - // battle_block : instance - // man_or_vehicle : "veh" or "men" +/// @description Selects a weighted-random target slot from a battle block. +/// @param {Id.Instance.obj_enunit} battle_block The enemy column instance +/// @param {String} target_type "veh", "men", or "medi" +/// @returns {Real|Undefined} The target slot index, or undefined if no valid target found +function scr_target(battle_block, target_type) { + var _total = 0; + + for (var f = 0; f <= 30; f++) { + if (battle_block.dudes[f] == "") { + continue; + } - // This naughty, sexy code is used to return a target for a battle block's any particular weapon + if (target_type == "veh" && battle_block.dudes_vehicle[f] != 1) { + continue; + } - // wih(ob_ennit){showmeage("targ#"+string(dudes[1])+"|"+string(dudes_num[1])+"|"+string(men+medi)+"|"+string(dudes_hp[1]));} + if (target_type == "men" && battle_block.dudes_vehicle[f] != 0) { + continue; + } - var final_target = 0; + _total += battle_block.dudes_num[f]; + } - repeat (1) { - if (final_target == 0) { - var target_num, target_id, target_min, target_max; - var targets = 0, roll = 0, dice = 0; - var valid = false; + if (_total <= 0) { + return undefined; + } - var f = -1; - repeat (31) { - f += 1; - target_num[f] = 0; - target_id[f] = 0; - target_min[f] = 0; - target_max[f] = 0; - } + var _roll = floor(random(_total)) + 1; + var _cumulative = 0; + for (var f = 0; f <= 30; f++) { + if (battle_block.dudes[f] == "") { + continue; + } - f = 0; + if (target_type == "veh" && battle_block.dudes_vehicle[f] != 1) { + continue; + } - for (f = 1; f <= 30; f++) { - valid = false; - if (battle_block.dudes[f] == "") { - continue; - } - if (man_or_vehicle == "veh") { - if (battle_block.dudes_vehicle[f] == 1) { - valid = true; - } - } else if (man_or_vehicle == "men") { - if (battle_block.dudes_vehicle[f] == 0) { - valid = true; - } - } else if ((man_or_vehicle == "medi") && (battle_block.dudes_vehicle[f] == 1 || battle_block.dudes_vehicle[f] == 0)) { - valid = true; - } - if (valid) { - targets += 1; - target_id[targets] = f; - target_num[targets] = battle_block.dudes_num[f]; - dice += target_num[targets]; - target_max[targets] = dice; - if (targets == 1) { - target_min[targets] = 0; - } - if (targets > 1) { - target_min[targets] = target_max[targets - 1] + 1; - } - } - } + if (target_type == "men" && battle_block.dudes_vehicle[f] != 0) { + continue; + } - roll = floor(random(dice)) + 1; - f = 0; - repeat (targets) { - f += 1; - if ((roll >= target_min[f]) && (roll <= target_max[f]) && (final_target == 0)) { - final_target = f; - break; - } - } + _cumulative += battle_block.dudes_num[f]; + if (_roll <= _cumulative) { + return f; } } - if (final_target != 0) { - return final_target; - } - if (final_target == 0) { - return 1; - } + return undefined; } From f490de79ca4166768c2b870521b26abb39306a2b Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Sun, 12 Jul 2026 04:27:58 +0300 Subject: [PATCH 4/9] chore(enunit/Alarm_0): Remove dead code --- objects/obj_enunit/Alarm_0.gml | 109 --------------------------------- 1 file changed, 109 deletions(-) diff --git a/objects/obj_enunit/Alarm_0.gml b/objects/obj_enunit/Alarm_0.gml index 644b3f3621..52dc31ca13 100644 --- a/objects/obj_enunit/Alarm_0.gml +++ b/objects/obj_enunit/Alarm_0.gml @@ -273,112 +273,3 @@ if (!engaged) { } instance_activate_object(obj_pnunit); - -//TODO: Everything bellow has to be scrapped and reworked; -//! Commented out stuff bellow, until I understand why it exists; -/*if (image_index == -500) { - var leftest, charge = 0, - enemy2 = 0; - - with(obj_pnunit) { - if (x < -4000) { - instance_deactivate_object(id); - } - } - - if (flank == 0) { - move_unit_block("west"); - // instance_activate_object(obj_cursor); - } - if (flank == 1) { - enemy = instance_nearest(x, y, obj_pnunit); // Right most enemy - enemy2 = enemy; - // if (collision_point(x+10,y,obj_pnunit,0,1)) then engaged=1; - // if (!collision_point(x+10,y,obj_pnunit,0,1)) then engaged=0; - move_unit_block(); - - if (!position_empty(x + 10, y)) { - engaged = 1; - } // Quick smash - // instance_activate_object(obj_cursor); - } - -if (!collision_point(x+10,y,obj_pnunit,0,1)) and (!collision_point(x-10,y,obj_pnunit,0,1)) then engaged=0; -if (collision_point(x+10,y,obj_pnunit,0,1)) or (collision_point(x-10,y,obj_pnunit,0,1)) then engaged=1; - - - -var range_shooti; - - i=0; - - - repeat(30){i+=1; - - - - dist=floor(point_distance(enemy2.x,enemy2.y,x,y)/10); - - - - - - range_shoot=""; - - if (wep[i]!="") and (range[i]>=dist) and (ammo[i]!=0){ - if (range[i]!=1) and (engaged=0) then range_shoot="ranged"; - if ((range[i]!=floor(range[i])) or (range[i]=1)) and (engaged=1) then range_shoot="melee"; - } - - - - - - - - if (wep[i]!="") and (range_shoot="ranged") and (range[i]>=dist){// Weapon meets preliminary checks - var _armour_piercing;_armour_piercing=0;if (apa[i]>att[i]) then _armour_piercing=1;// Determines if it is _armour_piercing or not - - // if (wep[i]="Missile Launcher") then _armour_piercing=1; - - if (string_count("Gauss",wep[i])>0) then _armour_piercing=1; - - if (wep[i]="Missile Launcher") or (wep[i]="Rokkit Launcha") or (wep[i]="Kannon") then _armour_piercing=1; - if (wep[i]="Big Shoota") then _armour_piercing=0;if (wep[i]="Devourer") then _armour_piercing=0; - if (wep[i]="Gauss Particle Cannon") or (wep[i]="Overcharged Gauss Cannon") or (wep[i]="Particle Whip") then _armour_piercing=1; - - - if (instance_exists(enemy2)){ - if (enemy2.veh+enemy2.dreads>0) and (enemy2.men=0) and (apa[i]>10) then _armour_piercing=1; - - if (_armour_piercing=1) and (once_only=0){// Check for vehicles - var g,good;g=0;good=0; - - if (enemy.veh>0){ - // good=scr_target(enemy,"veh");// First target has vehicles, blow it to hell - scr_shoot(i,enemy2,good,"arp","ranged"); - } - if (good=0) and (instance_number(obj_pnunit)>1){// First target does not have vehicles, cycle through objects to find one that has vehicles - var x2;x2=enemy2.x; - repeat(instance_number(obj_enunit)-1){ - if (good=0){ - x2+=10;enemy2=instance_nearest(x2,y,obj_pnunit); - if (enemy2.veh+enemy2.dreads>0) and (good=0){ - good=scr_target(enemy2,"veh");// This target has vehicles, blow it to hell - scr_shoot(i,enemy2,good,"arp","ranged");once_only=1; - } - } - } - } - if (good=0) then _armour_piercing=0;// Fuck it, shoot at infantry - } - } - - } - - - -} - - instance_activate_object(obj_pnunit); -} */ From 656574a10e7ad2e89ed51b5aca5dbf799ceb91d5 Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Sun, 12 Jul 2026 07:00:00 +0300 Subject: [PATCH 5/9] refactor(flavor2): Interpolation via template strings --- scripts/scr_flavor2/scr_flavor2.gml | 142 ++++++++++++++-------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/scripts/scr_flavor2/scr_flavor2.gml b/scripts/scr_flavor2/scr_flavor2.gml index 58a78bf8eb..7c0b08fcf1 100644 --- a/scripts/scr_flavor2/scr_flavor2.gml +++ b/scripts/scr_flavor2/scr_flavor2.gml @@ -42,101 +42,101 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo if (_hostile_weapon == "Daemonette Melee") { flavor = 1; if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " Daemonettes rake and claw at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Daemonettes rake and claw at {target_type}. "; } if (_hostile_shots == 1) { - m1 = "A Daemonette rakes and claws at " + string(target_type) + ". "; + m1 = $"A Daemonette rakes and claws at {target_type}. "; } } if (_hostile_weapon == "Plaguebearer Melee") { flavor = 1; if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " Plague Swords slash into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Plague Swords slash into {target_type}. "; } if (_hostile_shots == 1) { - m1 = "A Plaguesword is swung into " + string(target_type) + ". "; + m1 = $"A Plaguesword is swung into {target_type}. "; } } if (_hostile_weapon == "Bloodletter Melee") { flavor = 1; if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " Hellblades hiss and slash into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Hellblades hiss and slash into {target_type}. "; } if (_hostile_shots == 1) { - m1 = "A Bloodletter swings a Hellblade into " + string(target_type) + ". "; + m1 = $"A Bloodletter swings a Hellblade into {target_type}. "; } } if (_hostile_weapon == "Nurgle Vomit") { flavor = 1; if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " putrid, corrosive streams of Daemonic vomit spew into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} putrid, corrosive streams of Daemonic vomit spew into {target_type}. "; } if (_hostile_shots == 1) { - m1 = "A putrid, corrosive stream of Daemonic vomit spews into " + string(target_type) + ". "; + m1 = $"A putrid, corrosive stream of Daemonic vomit spews into {target_type}. "; } } if (_hostile_weapon == "Maulerfiend Claws") { flavor = 1; if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " Maulerfiends advance, wrenching and smashing their claws into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Maulerfiends advance, wrenching and smashing their claws into {target_type}. "; } if (_hostile_shots == 1) { - m1 = "A Maulerfiend advances, wrenching and smashing its claws into " + string(target_type) + ". "; + m1 = $"A Maulerfiend advances, wrenching and smashing its claws into {target_type}. "; } } if (hostile_range > 1) { if (_hostile_weapon == "Big Shoota") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z roar and blast away at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z roar and blast away at {target_type}. "; flavor = 1; } if (_hostile_weapon == "Dakkagun") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z scream and rattle, blasting into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z scream and rattle, blasting into {target_type}. "; flavor = 1; } if (_hostile_weapon == "Deffgun") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z scream and rattle, blasting into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z scream and rattle, blasting into {target_type}. "; flavor = 1; } if (_hostile_weapon == "Snazzgun") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z scream and rattle, blasting into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z scream and rattle, blasting into {target_type}. "; flavor = 1; } if (_hostile_weapon == "Grot Blasta") { - m1 = "The Gretchin fire their shoddy weapons and club at your " + string(target_type) + ". "; + m1 = $"The Gretchin fire their shoddy weapons and club at your {target_type}. "; flavor = 1; } if (_hostile_weapon == "Kannon") { flavor = 1; if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z belch out large caliber shells. "; + m1 = $"{_hostile_shots} {_hostile_weapon}z belch out large caliber shells. "; } if (_hostile_shots == 1) { - m1 = "A " + string(_hostile_weapon) + "z belches out a large caliber shell. "; + m1 = $"A {_hostile_weapon}z belches out a large caliber shell. "; } } if (_hostile_weapon == "Shoota") { flavor = 1; var ranz = choose(1, 2, 3, 4); if (ranz == 1) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z fire away at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z fire away at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z spit lead at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z spit lead at {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z blast at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z blast at {target_type}. "; } if (ranz == 4) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z roar and fire at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z roar and fire at {target_type}. "; } } if (_hostile_weapon == "Burna") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z spray napalm into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z spray napalm into {target_type}. "; flavor = 1; } if (_hostile_weapon == "Skorcha") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z spray huge gouts of napalm into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z spray huge gouts of napalm into {target_type}. "; flavor = 1; } if (_hostile_weapon == "Rokkit Launcha") { @@ -144,22 +144,22 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo var ranz; ranz = choose(1, 2, 2, 3, 3); if (ranz == 1) { - m1 = string(_hostile_shots) + " rokkitz shoot at " + string(target_type) + ", the explosions disrupting. "; + m1 = $"{_hostile_shots} rokkitz shoot at {target_type}, the explosions disrupting. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " rokkitz scream upward and then fall upon " + string(target_type) + ". "; + m1 = $"{_hostile_shots} rokkitz scream upward and then fall upon {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z roar and fire their payloads. "; + m1 = $"{_hostile_shots} {_hostile_weapon}z roar and fire their payloads. "; } } if ((_hostile_weapon == "Staff of Light Shooting") && (_hostile_shots == 1)) { - m1 = "A Staff of Light crackles with energy and fires upon " + string(target_type) + ". "; + m1 = $"A Staff of Light crackles with energy and fires upon {target_type}. "; flavor = 1; } if ((_hostile_weapon == "Staff of Light Shooting") && (_hostile_shots > 1)) { - m1 = string(_hostile_shots) + " Staves of Light crackle with energy and fire upon " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Staves of Light crackle with energy and fire upon {target_type}. "; flavor = 1; } if ((_hostile_weapon == "Gauss Flayer") || (_hostile_weapon == "Gauss Blaster") || (_hostile_weapon == "Gauss Flayer Array")) { @@ -167,16 +167,16 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo var ranz; ranz = choose(1, 2, 3, 4); if (ranz == 1) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s shoot at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s shoot at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s crackle and fire at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s crackle and fire at {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s discharge upon " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s discharge upon {target_type}. "; } if (ranz == 4) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s spew green energy at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s spew green energy at {target_type}. "; } } if ((_hostile_weapon == "Gauss Cannon") || (_hostile_weapon == "Overcharged Gauss Cannon") || (_hostile_weapon == "Gauss Flux Arc")) { @@ -184,65 +184,65 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo var ranz; ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s charge and then blast at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s charge and then blast at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s crackle with a sick amount of energy before firing at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s crackle with a sick amount of energy before firing at {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s pulse with energy and then discharge upon " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s pulse with energy and then discharge upon {target_type}. "; } } if (_hostile_weapon == "Gauss Particle Cannon") { flavor = 1; - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "s shine a sick green, pulsing with energy, and then blast solid beams of energy into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}s shine a sick green, pulsing with energy, and then blast solid beams of energy into {target_type}. "; } if (_hostile_weapon == "Particle Whip") { flavor = 1; if (_hostile_shots == 1) { - m1 = "The apex of the Monolith pulses with energy. An instant layer it fires, the solid beam of energy crashing into " + string(target_type) + ". "; + m1 = $"The apex of the Monolith pulses with energy. An instant layer it fires, the solid beam of energy crashing into {target_type}. "; } if (_hostile_shots > 1) { - m1 = "The apex of " + string(_hostile_shots) + " Monoliths pulse with energy. An instant later they fire, the solid beams of energy crashing into " + string(target_type) + ". "; + m1 = $"The apex of {_hostile_shots} Monoliths pulse with energy. An instant later they fire, the solid beams of energy crashing into {target_type}. "; } } if (_hostile_weapon == "Doomsday Cannon") { flavor = 1; if (_hostile_shots == 1) { - m1 = "A Doomsday Arc crackles with energy and then fires. The resulting blast is blinding in intensity, the ground shaking before its might. "; + m1 = $"A Doomsday Arc crackles with energy and then fires. The resulting blast is blinding in intensity, the ground shaking before its might. "; } if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " Doomsday Arcs crackle with energy and then fire. The resulting blasts are blinding in intensity, the ground shaking. "; + m1 = $"{_hostile_shots} Doomsday Arcs crackle with energy and then fire. The resulting blasts are blinding in intensity, the ground shaking. "; } } if (_hostile_weapon == "Eldritch Fire") { flavor = 1; if (_hostile_shots == 1) { - m1 = "A Pink Horror spits out a globlet of bright energy. The bolt smashes into " + string(target_type) + ". "; + m1 = $"A Pink Horror spits out a globlet of bright energy. The bolt smashes into {target_type}. "; } if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " Pink Horrors spit and throw bolts of warp energy into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Pink Horrors spit and throw bolts of warp energy into {target_type}. "; } } } if (_hostile_shots > 0) { if (_hostile_weapon == "Choppa") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z cleave into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z cleave into {target_type}. "; flavor = 1; } if (_hostile_weapon == "Power Klaw") { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z rip and tear at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z rip and tear at {target_type}. "; flavor = 1; } if (_hostile_weapon == "Venom Claws") { if (_hostile_shots > 1) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + " rake at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon} rake at {target_type}. "; } flavor = 1; if (_hostile_shots == 1) { - m1 = "The Spyrer rakes at " + string(target_type) + " with his " + string(_hostile_weapon) + ". "; + m1 = $"The Spyrer rakes at {target_type} with his {_hostile_weapon}. "; } flavor = 1; } @@ -250,16 +250,16 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo flavor = 1; var ranz = choose(1, 2, 3, 4); if (ranz == 1) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z fire away at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z fire away at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z spit lead at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z spit lead at {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z blast at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z blast at {target_type}. "; } if (ranz == 4) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z roar and fire at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z roar and fire at {target_type}. "; } } if (_hostile_weapon == "Tankbusta Bomb") { @@ -267,26 +267,26 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo var ranz; ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z are attached to " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z are attached to {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z are clamped onto " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z are clamped onto {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " " + string(_hostile_weapon) + "z are flung into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} {_hostile_weapon}z are flung into {target_type}. "; } } if ((_hostile_weapon == "Melee1") && (enemy == eFACTION.ORK)) { flavor = 1; var ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = string(_hostile_shots) + " Orks club and smash at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Orks club and smash at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " Orks shoot their Slugas and smash gunbarrels into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Orks shoot their Slugas and smash gunbarrels into {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " Orks claw and punch at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Orks claw and punch at {target_type}. "; } } @@ -295,25 +295,25 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo if (_hostile_shots == 1) { var ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = "A " + string(_hostile_weapon) + " crackles and is swung into " + string(target_type) + ". "; + m1 = $"A {_hostile_weapon} crackles and is swung into {target_type}. "; } if (ranz == 2) { - m1 = "A " + string(_hostile_weapon) + " pulses and smashes through " + string(target_type) + ". "; + m1 = $"A {_hostile_weapon} pulses and smashes through {target_type}. "; } if (ranz == 3) { - m1 = "A " + string(_hostile_weapon) + " crackles and smashes into " + string(target_type) + ". "; + m1 = $"A {_hostile_weapon} crackles and smashes into {target_type}. "; } } if (_hostile_shots > 1) { var ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = string(_hostile_shots) + " Staves of Light strike at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Staves of Light strike at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " Staves of Light smash at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Staves of Light smash at {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " Staves of Light swing into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Staves of Light swing into {target_type}. "; } } } @@ -321,13 +321,13 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo flavor = 1; var ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = string(_hostile_shots) + " Warscythes strike at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Warscythes strike at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " Warscythes of Light slice into " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Warscythes of Light slice into {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " Warscythes of Light hew " + string(target_type) + ". "; + m1 = $"{_hostile_shots} Warscythes of Light hew {target_type}. "; } } if (_hostile_weapon == "Claws") { @@ -335,25 +335,25 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo if (_hostile_shots == 1) { var ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = "A massive claw slices through " + string(target_type) + ". "; + m1 = $"A massive claw slices through {target_type}. "; } if (ranz == 2) { - m1 = "A razor-sharp claw slashes into " + string(target_type) + ". "; + m1 = $"A razor-sharp claw slashes into {target_type}. "; } if (ranz == 3) { - m1 = "A large necron claw strikes at " + string(target_type) + ". "; + m1 = $"A large necron claw strikes at {target_type}. "; } } if (_hostile_shots > 1) { var ranz = choose(1, 2, 3); if (ranz == 1) { - m1 = string(_hostile_shots) + " massive claws strike and slice at " + string(target_type) + ". "; + m1 = $"{_hostile_shots} massive claws strike and slice at {target_type}. "; } if (ranz == 2) { - m1 = string(_hostile_shots) + " razor-sharp claws assault " + string(target_type) + ". "; + m1 = $"{_hostile_shots} razor-sharp claws assault {target_type}. "; } if (ranz == 3) { - m1 = string(_hostile_shots) + " large necron claws strike at and shred " + string(target_type) + ". "; + m1 = $"{_hostile_shots} large necron claws strike at and shred {target_type}. "; } } } From d03fb1e54482e83067b98b8552c89861aa9ab92d Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Sun, 12 Jul 2026 07:01:42 +0300 Subject: [PATCH 6/9] fix: Add flavor targets to some enemy weapons --- scripts/scr_flavor2/scr_flavor2.gml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/scr_flavor2/scr_flavor2.gml b/scripts/scr_flavor2/scr_flavor2.gml index 7c0b08fcf1..e899263a49 100644 --- a/scripts/scr_flavor2/scr_flavor2.gml +++ b/scripts/scr_flavor2/scr_flavor2.gml @@ -109,10 +109,10 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo if (_hostile_weapon == "Kannon") { flavor = 1; if (_hostile_shots > 1) { - m1 = $"{_hostile_shots} {_hostile_weapon}z belch out large caliber shells. "; + m1 = $"{_hostile_shots} {_hostile_weapon}z belch out large caliber shells at {target_type}. "; } if (_hostile_shots == 1) { - m1 = $"A {_hostile_weapon}z belches out a large caliber shell. "; + m1 = $"A {_hostile_weapon}z belches out a large caliber shell at {target_type}. "; } } if (_hostile_weapon == "Shoota") { @@ -150,7 +150,7 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo m1 = $"{_hostile_shots} rokkitz scream upward and then fall upon {target_type}. "; } if (ranz == 3) { - m1 = $"{_hostile_shots} {_hostile_weapon}z roar and fire their payloads. "; + m1 = $"{_hostile_shots} {_hostile_weapon}z roar and fire their payloads at {target_type}. "; } } @@ -209,10 +209,10 @@ function scr_flavor2(lost_units_count, target_type, hostile_range, hostile_weapo if (_hostile_weapon == "Doomsday Cannon") { flavor = 1; if (_hostile_shots == 1) { - m1 = $"A Doomsday Arc crackles with energy and then fires. The resulting blast is blinding in intensity, the ground shaking before its might. "; + m1 = $"A Doomsday Arc crackles with energy and then fires at {target_type}. The resulting blast is blinding in intensity, the ground shaking before its might. "; } if (_hostile_shots > 1) { - m1 = $"{_hostile_shots} Doomsday Arcs crackle with energy and then fire. The resulting blasts are blinding in intensity, the ground shaking. "; + m1 = $"{_hostile_shots} Doomsday Arcs crackle with energy and then fire at {target_type}. The resulting blasts are blinding in intensity, the ground shaking. "; } } From 074350c2f3aaca54da8d3cf94cb2dc4e9ead83a1 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:04:32 +0300 Subject: [PATCH 7/9] feat: Combat spillover; Speed force --- objects/obj_pnunit/Alarm_0.gml | 6 + .../scr_player_combat_weapon_stacks.gml | 10 + scripts/scr_shoot/scr_shoot.gml | 483 ++++++++++-------- 3 files changed, 290 insertions(+), 209 deletions(-) diff --git a/objects/obj_pnunit/Alarm_0.gml b/objects/obj_pnunit/Alarm_0.gml index 9bf15aba4a..2f5f3f1574 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -60,6 +60,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; + } + var _is_ap = apa[i] > 2; var _dist = point_distance(x, y, enemy.x, enemy.y) / 10; var _mode = engaged ? "melee" : "ranged"; 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 0b0b665e8b..d472250a79 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 4ba3f2a3d0..c01e603886 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -7,12 +7,6 @@ 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; @@ -239,8 +233,6 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_dat 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) { @@ -253,254 +245,327 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_dat } if (that_works == true) { - var damage_per_weapon = 0, c = 0, target_armour_value = 0, ap = 0, wii = ""; - var attack_count_mod = 0; + var damage_per_weapon = 0; + 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]; + attack_count_mod = max(1, splash[weapon_index_position]); - // 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; + // Armour multiplier indexed by AP rating (1..4); any AP outside that range + // leaves armour untouched. Infantry and vehicles scale differently. + var _inf_ap = [1, 3, 2, 1.5, 0]; + var _veh_ap = [1, 6, 4, 2, 0]; + var _ap_valid = (armour_pierce >= 1) && (armour_pierce <= 4); + + // Never open fire on a dead rank/formation. Stale men/veh/medi (only refreshed on + // the enemy's own alarm) and scr_target's rank-1 fallback can aim us at corpses; + // snap to a living rank instead, or clean up the empty formation and bail. + if (!instance_exists(target_object)) { + exit; + } + 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 (armour_pierce == 3) { - target_armour_value = target_armour_value * 2; + target_type = _alive_rank; + } + + // Damage spills across ranks and, once a formation is spent, into the + // formation behind it. Every target actually fired upon gets its own flavour + // line with its own casualty count. The loop always terminates: shots_left + // strictly shrinks on every iteration that continues. + var spill_block = target_object; + var spill_rank = target_type; + var shots_left = shots_fired; + var touched_blocks = []; // Spill-over formations only; target_object is cleaned below. + + // The whole volley posts ONE battle-log line: the first target gets the rich + // weapon flavour (deferred), and every later target the overflow kills is + // gathered into a kill list appended to it (see emit_volley_flavour). + var _first_target = true; + var _primary_flavour = undefined; + var _spill_kills = []; // [{ name, count }] for targets killed after the first. + + while (shots_left > 0) { + // This target's armour against our AP rating. + var _armour = spill_block.dudes_ac[spill_rank]; + if (_ap_valid) { + var _ap_table = spill_block.dudes_vehicle[spill_rank] ? _veh_ap : _inf_ap; + _armour *= _ap_table[armour_pierce]; } - if (armour_pierce == 2) { - target_armour_value = target_armour_value * 4; + var final_hit = max(0, (damage_per_weapon - (_armour * attack_count_mod)) * spill_block.dudes_dr[spill_rank]); + + var rank_num = spill_block.dudes_num[spill_rank]; + var rank_hp = spill_block.dudes_hp[spill_rank]; + var total_damage = shots_left * final_hit; + var raw_kills = floor(total_damage / rank_hp); + var casualties = min(raw_kills, rank_num, shots_left * attack_count_mod); + + // Surplus damage only spills once this rank is actually wiped out. + var next_shots = 0; + if ((casualties >= rank_num) && (rank_num > 0) && (raw_kills > rank_num)) { + next_shots = max(0, shots_left - ceil((rank_num * rank_hp) / final_hit)); } - if (armour_pierce == 1) { - target_armour_value = target_armour_value * 6; + + // Gather flavour. The first target carries the rich phrasing (deferred, with + // the full weapon count); later targets just contribute to the kill list. + // final_hit <= 0 means armour stopped the shots cold (AP too low). + if (_first_target) { + _primary_flavour = scr_flavor(weapon_index_position, spill_block, spill_rank, shots_fired, casualties, final_hit <= 0, true); + _first_target = false; + } else if (casualties > 0) { + array_push(_spill_kills, { name: spill_block.dudes[spill_rank], count: casualties }); } - } else { - if (armour_pierce == 4) { - target_armour_value = 0; + + if ((rank_num == 1) && (casualties == 0) && (total_damage > 0)) { + spill_block.dudes_hp[spill_rank] -= total_damage; // Chip a lone survivor + if (spill_block.dudes_hp[spill_rank] <= 0) { + // Chipped to death: remove it now and drop the force count. Otherwise + // dudes_num stays 1 at dudes_hp <= 0 - a "zombie" that find_next_alive_rank + // skips, so it's never finished off and keeps inflating enemy_forces. + spill_block.dudes_num[spill_rank] = 0; + obj_ncombat.enemy_forces -= 1; + } } - if (armour_pierce == 3) { - target_armour_value = target_armour_value * 1.5; + if (casualties >= 1) { + spill_block.dudes_num[spill_rank] -= casualties; + obj_ncombat.enemy_forces -= casualties; } - if (armour_pierce == 2) { - target_armour_value = target_armour_value * 2; + + shots_left = next_shots; + if (shots_left <= 0) { + break; } - if (armour_pierce == 1) { - target_armour_value = target_armour_value * 3; + + // Next target: a living rank in this formation, else the formation behind. + var next_rank = find_next_alive_rank(spill_block, spill_block.dudes_vehicle[spill_rank]); + if (next_rank == -1) { + spill_block = get_next_enemy_formation(spill_block); + if (spill_block == noone) { + break; + } + array_push(touched_blocks, spill_block); + next_rank = find_next_alive_rank(spill_block, -1); + if (next_rank == -1) { + break; + } } + spill_rank = next_rank; } - attack_count_mod = max(1, splash[weapon_index_position]); - - final_hit_damage_value = max(0, damage_per_weapon - target_armour_value); //damage armour reduction + // Post the single consolidated line for the whole volley. + emit_volley_flavour(_primary_flavour, _spill_kills); - final_hit_damage_value *= target_object.dudes_dr[target_type]; //damage_resistance mod + // Clean up the spill-over formations (target_object is handled below). + for (var _tb = 0; _tb < array_length(touched_blocks); _tb++) { + if (instance_exists(touched_blocks[_tb])) { + compress_enemy_array(touched_blocks[_tb]); + destroy_empty_column(touched_blocks[_tb]); + } + } + } + } - c = shots_fired * final_hit_damage_value; // New damage + if (stop == 0) { + compress_enemy_array(target_object); + destroy_empty_column(target_object); + } + } + } catch (_exception) { + ERROR_HANDLER.handle_exception(_exception); + } +} - var casualties, onceh = 0, ponies = 0; +/// @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} _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; +} - casualties = min(floor(c / target_object.dudes_hp[target_type]), shots_fired * attack_count_mod); +/// @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; +} - 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; - } +/// @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; + } - 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]); + var _shots = wep_num[weapon_index_position]; + var _ap = apa[weapon_index_position]; + var _dpw = att[weapon_index_position] / _shots; // per-bike damage + var _mod = max(1, splash[weapon_index_position]); + if (ammo[weapon_index_position] > 0) { + ammo[weapon_index_position] -= 1; + } - shots_remaining = round(damage_remaining / damage_per_weapon); - } + // Armour multiplier indexed by AP rating (1..4), matching scr_shoot's normal path. + var _inf_ap = [1, 3, 2, 1.5, 0]; + var _veh_ap = [1, 6, 4, 2, 0]; + var _ap_valid = (_ap >= 1) && (_ap <= 4); + + // Total living models across every formation on the field. + var _formations = []; + var _total = 0; + with (obj_enunit) { + array_push(_formations, id); + for (var r = 1; r <= 30; r++) { + // Skip "zombie" ranks (dudes_num > 0 but dudes_hp <= 0): they would dilute _total and + // cause a divide-by-zero in the per-rank damage loop below. + if (dudes[r] != "" && dudes_num[r] > 0 && dudes_hp[r] > 0) { + _total += dudes_num[r]; + } + } + } + if (_total <= 0) { + exit; + } - if (target_object.dudes_num[target_type] - casualties < 0) { - casualties = ponies; - } - if (casualties < 0) { - casualties = 0; - } + // Apply damage proportionally to each rank's share of the field; record every rank that lost models. + var _hits = []; // [{ name, kills, bounced }] + var _wounded = undefined; // first rank that took fire but lost no models (wound/bounce fallback) + for (var fi = 0; fi < array_length(_formations); fi++) { + var _f = _formations[fi]; + if (!instance_exists(_f)) { + continue; + } + for (var r = 1; r <= 30; r++) { + // Mirror the _total guard: skip empty/empty-ranked and "zombie" (hp <= 0) ranks so the + // proportional-damage division below can never divide by zero/negative HP. + if (_f.dudes[r] == "" || _f.dudes_num[r] <= 0 || _f.dudes_hp[r] <= 0) { + continue; + } - 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 _armour = _f.dudes_ac[r]; + if (_ap_valid) { + var _ap_table = _f.dudes_vehicle[r] ? _veh_ap : _inf_ap; + _armour *= _ap_table[_ap]; + } - 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); + var _rank_shots = _shots * (_f.dudes_num[r] / _total); + var _final_hit = max(0, (_dpw - (_armour * _mod)) * _f.dudes_dr[r]); + var _kills = min(floor((_rank_shots * _final_hit) / _f.dudes_hp[r]), _f.dudes_num[r]); + if (_kills < 0) { + _kills = 0; + } - 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 (_kills > 0) { + _f.dudes_num[r] -= _kills; + obj_ncombat.enemy_forces -= _kills; + array_push(_hits, { name: _f.dudes[r], kills: _kills, bounced: (_final_hit <= 0), block: _f, rank: r }); + } else if (_wounded == undefined) { + // Volley spent ammo but killed no-one here; remember the first such rank so a + // non-killing sweep still reports a wound/bounce instead of going silent. + _wounded = { bounced: (_final_hit <= 0), block: _f, rank: r }; + } + } + } - if (casualties >= 1) { - target_object.dudes_num[target_type] -= casualties; - obj_ncombat.enemy_forces -= casualties; - } + // Primary = the rank with the most kills (rich deferred flavour); the rest form the kill list. + 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; } } + for (var i = 0; i < array_length(_hits); i++) { + if (i == _best) { + continue; + } + array_push(_spill, { name: _hits[i].name, count: _hits[i].kills }); + } + 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)) { + // Nothing died but the volley still landed: report a single wounded/bounced target so the + // consolidated flavour log records the shot (casualties = 0 -> injured or bounced). + _primary = scr_flavor(weapon_index_position, _wounded.block, _wounded.rank, _shots, 0, _wounded.bounced, true); + } + emit_volley_flavour(_primary, _spill); - if (stop == 0) { - compress_enemy_array(target_object); - destroy_empty_column(target_object); + // Clean up spent ranks/formations (mirrors scr_shoot). + for (var fi = 0; fi < array_length(_formations); fi++) { + if (instance_exists(_formations[fi])) { + compress_enemy_array(_formations[fi]); + destroy_empty_column(_formations[fi]); } } } catch (_exception) { From 9d6ed243411cbc627d73601d852e33fc39fd70c2 Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:23:31 +0300 Subject: [PATCH 8/9] refactor: scr_shoot cleanup --- scripts/scr_clean/scr_clean.gml | 46 +- scripts/scr_shoot/scr_shoot.gml | 889 ++++++++++++++++---------------- 2 files changed, 443 insertions(+), 492 deletions(-) diff --git a/scripts/scr_clean/scr_clean.gml b/scripts/scr_clean/scr_clean.gml index 259f43671e..8abf61fa81 100644 --- a/scripts/scr_clean/scr_clean.gml +++ b/scripts/scr_clean/scr_clean.gml @@ -196,26 +196,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++) { @@ -243,7 +224,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) { @@ -305,26 +286,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 @@ -349,7 +311,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_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index c01e603886..63db092ca7 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,452 +159,311 @@ /// @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 - 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 ((obj_ncombat.enemy == eFACTION.CHAOS) && (obj_ncombat.threat == 7)) { - doom = 1; - } + if (!instance_exists(target_object)) { + exit; + } - var damage_type = ""; - var stop = 0; + var _aggregate_damage = att[weapon_index_position]; + var _armour_pierce = apa[weapon_index_position]; - if (ammo[weapon_index_position] > 0) { - ammo[weapon_index_position] -= 1; - } + 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 (damage_data == "medi") { - damage_type = "att"; - if (aggregate_damage < armour_pierce) { - damage_type = "arp"; - } - } else { - damage_type = damage_data; - } - if (wep[weapon_index_position] == "Web Spinner") { - damage_type = "status"; - } + 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); + } +} - var attack_count_mod = max(1, splash[weapon_index_position]); +/// @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; + } - 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; + 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; + } + } - damage_per_weapon = aggregate_damage; + // Heretic damage boost + if (obj_ncombat.enemy == eFACTION.HERETICS) { + aggregate_damage = round(aggregate_damage * 1.15); + } - 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; - } - } + // Resolve damage type + if (damage_data == "medi") { + _damage_type = (aggregate_damage < armour_pierce) ? "arp" : "att"; + } else { + _damage_type = damage_data; + } - hit_number = shots_fired; + if (_weapon_name == "Web Spinner") { + _damage_type = "status"; + } - 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 _is_assorted = wep_owner[weapon_index_position] == "assorted"; - 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 (_damage_type == "status") { + if (melee_or_ranged != "wall") { + _hit_count *= _weapon_splash; - if ((doom != 0) && (shots_fired > 1)) { - damage_per_weapon = floor((doom * damage_per_weapon)); - hit_number = floor(hit_number * doom); - } + target_object.hostile_shooters = _is_assorted ? 999 : 1; + _damage = 0; + _hostile_type = 1; - if (damage_per_weapon == 0) { - damage_per_weapon = shots_fired * doom; - } + 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 (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); - } - } + 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 (instance_exists(target_object) && (owner == eFACTION.PLAYER)) { - var shots_fired = 0; - var stop = 0; - var damage_type = ""; + if (_hit_count > 1) { + _damage_per_weapon = floor(_doom_mod * _damage_per_weapon); + _hit_count = floor(_hit_count * _doom_mod); + } - if (weapon_index_position >= 0) { - shots_fired = wep_num[weapon_index_position]; - } + if (melee_or_ranged != "wall") { + _hit_count *= _weapon_splash; - if (shots_fired == 0) { - exit; - } + if (_hit_count > 0) { + target_object.hostile_shooters = _is_assorted ? 999 : 1; + _damage = _damage_per_weapon / _hit_count; + _hostile_type = 1; - 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; - } + 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; - if (weapon_index_position >= 0) { - if (ammo[weapon_index_position] == 0) { - stop = 1; - } - if (ammo[weapon_index_position] > 0) { - ammo[weapon_index_position] -= 1; - } - } - if (wep[weapon_index_position] == "Missile Silo") { - obj_ncombat.player_silos -= min(obj_ncombat.player_silos, 30); - } + if (melee_or_ranged != "wall") { + _hit_count *= _weapon_splash; + } - if (damage_data != "highest") { - damage_type = damage_data; - } - if ((damage_data == "highest") && (weapon_index_position >= 0)) { - damage_type = "att"; - if ((aggregate_damage >= 100) && (armour_pierce > 0)) { - damage_type = "arp"; - } - } - if (damage_data == "highest") { - if (weapon_index_position == -51 || weapon_index_position == -52 || weapon_index_position == -53) { - damage_type = "att"; - } + 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 ((weapon_index_position >= 0) || (weapon_index_position < -40)) { - // Normal shooting - var that_works = false; + if (_hit_count > 1) { + _damage_per_weapon = floor(_doom_mod * _damage_per_weapon); + _hit_count = floor(_hit_count * _doom_mod); + } - if (weapon_index_position >= 0) { - if ((aggregate_damage > 0) && (stop == 0)) { - that_works = true; - } - } - if ((weapon_index_position < -40) && (stop == 0)) { - that_works = true; - } + 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; + + _damage -= target_object.ac; + _damage = max(0, _damage); + _damage = round(_damage) * _hit_count; + target_object.hp -= _damage; - if (that_works == true) { - var damage_per_weapon = 0; - attack_count_mod = 0; - - if (weapon_index_position >= 0) { - damage_per_weapon = aggregate_damage / wep_num[weapon_index_position]; - } // Average damage - if (weapon_index_position < -40) { - attack_count_mod = 3; - - if (weapon_index_position == -51) { - at = 160; - armour_pierce = 0; - } - if (weapon_index_position == -52) { - at = 200; - armour_pierce = -1; - } - if (weapon_index_position == -53) { - at = 250; - armour_pierce = 0; - } - } - - attack_count_mod = max(1, splash[weapon_index_position]); - - // Armour multiplier indexed by AP rating (1..4); any AP outside that range - // leaves armour untouched. Infantry and vehicles scale differently. - var _inf_ap = [1, 3, 2, 1.5, 0]; - var _veh_ap = [1, 6, 4, 2, 0]; - var _ap_valid = (armour_pierce >= 1) && (armour_pierce <= 4); - - // Never open fire on a dead rank/formation. Stale men/veh/medi (only refreshed on - // the enemy's own alarm) and scr_target's rank-1 fallback can aim us at corpses; - // snap to a living rank instead, or clean up the empty formation and bail. - if (!instance_exists(target_object)) { - exit; - } - 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; - } - target_type = _alive_rank; - } - - // Damage spills across ranks and, once a formation is spent, into the - // formation behind it. Every target actually fired upon gets its own flavour - // line with its own casualty count. The loop always terminates: shots_left - // strictly shrinks on every iteration that continues. - var spill_block = target_object; - var spill_rank = target_type; - var shots_left = shots_fired; - var touched_blocks = []; // Spill-over formations only; target_object is cleaned below. - - // The whole volley posts ONE battle-log line: the first target gets the rich - // weapon flavour (deferred), and every later target the overflow kills is - // gathered into a kill list appended to it (see emit_volley_flavour). - var _first_target = true; - var _primary_flavour = undefined; - var _spill_kills = []; // [{ name, count }] for targets killed after the first. - - while (shots_left > 0) { - // This target's armour against our AP rating. - var _armour = spill_block.dudes_ac[spill_rank]; - if (_ap_valid) { - var _ap_table = spill_block.dudes_vehicle[spill_rank] ? _veh_ap : _inf_ap; - _armour *= _ap_table[armour_pierce]; - } - var final_hit = max(0, (damage_per_weapon - (_armour * attack_count_mod)) * spill_block.dudes_dr[spill_rank]); - - var rank_num = spill_block.dudes_num[spill_rank]; - var rank_hp = spill_block.dudes_hp[spill_rank]; - var total_damage = shots_left * final_hit; - var raw_kills = floor(total_damage / rank_hp); - var casualties = min(raw_kills, rank_num, shots_left * attack_count_mod); - - // Surplus damage only spills once this rank is actually wiped out. - var next_shots = 0; - if ((casualties >= rank_num) && (rank_num > 0) && (raw_kills > rank_num)) { - next_shots = max(0, shots_left - ceil((rank_num * rank_hp) / final_hit)); - } - - // Gather flavour. The first target carries the rich phrasing (deferred, with - // the full weapon count); later targets just contribute to the kill list. - // final_hit <= 0 means armour stopped the shots cold (AP too low). - if (_first_target) { - _primary_flavour = scr_flavor(weapon_index_position, spill_block, spill_rank, shots_fired, casualties, final_hit <= 0, true); - _first_target = false; - } else if (casualties > 0) { - array_push(_spill_kills, { name: spill_block.dudes[spill_rank], count: casualties }); - } - - if ((rank_num == 1) && (casualties == 0) && (total_damage > 0)) { - spill_block.dudes_hp[spill_rank] -= total_damage; // Chip a lone survivor - if (spill_block.dudes_hp[spill_rank] <= 0) { - // Chipped to death: remove it now and drop the force count. Otherwise - // dudes_num stays 1 at dudes_hp <= 0 - a "zombie" that find_next_alive_rank - // skips, so it's never finished off and keeps inflating enemy_forces. - spill_block.dudes_num[spill_rank] = 0; - obj_ncombat.enemy_forces -= 1; - } - } - if (casualties >= 1) { - spill_block.dudes_num[spill_rank] -= casualties; - obj_ncombat.enemy_forces -= casualties; - } - - shots_left = next_shots; - if (shots_left <= 0) { - break; - } - - // Next target: a living rank in this formation, else the formation behind. - var next_rank = find_next_alive_rank(spill_block, spill_block.dudes_vehicle[spill_rank]); - if (next_rank == -1) { - spill_block = get_next_enemy_formation(spill_block); - if (spill_block == noone) { - break; - } - array_push(touched_blocks, spill_block); - next_rank = find_next_alive_rank(spill_block, -1); - if (next_rank == -1) { - break; - } - } - spill_rank = next_rank; - } - - // Post the single consolidated line for the whole volley. - emit_volley_flavour(_primary_flavour, _spill_kills); - - // Clean up the spill-over formations (target_object is handled below). - for (var _tb = 0; _tb < array_length(touched_blocks); _tb++) { - if (instance_exists(touched_blocks[_tb])) { - compress_enemy_array(touched_blocks[_tb]); - destroy_empty_column(touched_blocks[_tb]); - } - } + if (target_object.hp <= 0) { + dest = 1; } - } - if (stop == 0) { - compress_enemy_array(target_object); - destroy_empty_column(target_object); + 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 { + 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); } } - } catch (_exception) { - ERROR_HANDLER.handle_exception(_exception); } } -/// @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} _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; +/// @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; + } + + 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; } - var _fallback = -1; - for (var f = 1; f <= 30; f++) { - if (_block.dudes_num[f] <= 0 || _block.dudes_hp[f] <= 0) { - continue; + + if (shots_fired == 0) { + exit; + } + + var _hp_count = array_length(target_object.dudes_hp); + + while (target_type < _hp_count && target_object.dudes_hp[target_type] <= 0) { + target_type++; + } + + if (target_type >= _hp_count) { + exit; + } + + var damage_per_weapon = 0; + var attack_count_mod = 1; + + if (weapon_index_position >= 0) { + var _ammo = ammo[weapon_index_position]; + if (_ammo == 0) { + exit; + } else if (_ammo != -1) { + ammo[weapon_index_position]--; } - if (_prefer_vehicle == -1 || _block.dudes_vehicle[f] == _prefer_vehicle) { - return f; + + //? 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 (_fallback == -1) { - _fallback = f; + + 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; } } - 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; + // 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; + } + + target_type = _alive_rank; } - var _bx = _block.x; - var _bid = _block.id; - var _best = noone; - var _best_x = 0; - with (obj_enunit) { - if (id == _bid) { - continue; + + var _target_block = target_object; + var _target_rank = target_type; + var _shots_left = shots_fired; + var _touched_blocks = [_target_block]; + + 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 (x <= _bx) { - continue; + + 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}); } - if (find_next_alive_rank(id, -1) == -1) { - continue; + + _shots_left = _next_shots; + if (_shots_left <= 0) { + break; } - if (_best == noone || x < _best_x) { - _best = id; - _best_x = x; + + 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; + } + + array_push(_touched_blocks, _target_block); + _next_rank = find_next_alive_rank(_target_block, -1); + if (_next_rank == -1) { + break; + } } + + _target_rank = _next_rank; } - return _best; + + emit_volley_flavour(_primary_flavour, _spill_kills); + combat_cleanup_formations(_touched_blocks); } /// @self Asset.GMObject.obj_pnunit @@ -466,75 +478,57 @@ function scr_shoot_spread(weapon_index_position) { var _shots = wep_num[weapon_index_position]; var _ap = apa[weapon_index_position]; - var _dpw = att[weapon_index_position] / _shots; // per-bike damage - var _mod = max(1, splash[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; } - // Armour multiplier indexed by AP rating (1..4), matching scr_shoot's normal path. - var _inf_ap = [1, 3, 2, 1.5, 0]; - var _veh_ap = [1, 6, 4, 2, 0]; - var _ap_valid = (_ap >= 1) && (_ap <= 4); - - // Total living models across every formation on the field. var _formations = []; var _total = 0; with (obj_enunit) { array_push(_formations, id); - for (var r = 1; r <= 30; r++) { - // Skip "zombie" ranks (dudes_num > 0 but dudes_hp <= 0): they would dilute _total and - // cause a divide-by-zero in the per-rank damage loop below. - if (dudes[r] != "" && dudes_num[r] > 0 && dudes_hp[r] > 0) { - _total += dudes_num[r]; + for (var _rank = 1; _rank <= 30; _rank++) { + if (dudes[_rank] != "" && dudes_num[_rank] > 0 && dudes_hp[_rank] > 0) { + _total += dudes_num[_rank]; } } } + if (_total <= 0) { exit; } - // Apply damage proportionally to each rank's share of the field; record every rank that lost models. - var _hits = []; // [{ name, kills, bounced }] - var _wounded = undefined; // first rank that took fire but lost no models (wound/bounce fallback) + var _hits = []; + var _wounded = undefined; for (var fi = 0; fi < array_length(_formations); fi++) { - var _f = _formations[fi]; - if (!instance_exists(_f)) { + var _block = _formations[fi]; + if (!instance_exists(_block)) { continue; } - for (var r = 1; r <= 30; r++) { - // Mirror the _total guard: skip empty/empty-ranked and "zombie" (hp <= 0) ranks so the - // proportional-damage division below can never divide by zero/negative HP. - if (_f.dudes[r] == "" || _f.dudes_num[r] <= 0 || _f.dudes_hp[r] <= 0) { - continue; - } - var _armour = _f.dudes_ac[r]; - if (_ap_valid) { - var _ap_table = _f.dudes_vehicle[r] ? _veh_ap : _inf_ap; - _armour *= _ap_table[_ap]; + for (var _rank = 1; _rank <= 30; _rank++) { + if (_block.dudes[_rank] == "" || _block.dudes_num[_rank] <= 0 || _block.dudes_hp[_rank] <= 0) { + continue; } - var _rank_shots = _shots * (_f.dudes_num[r] / _total); - var _final_hit = max(0, (_dpw - (_armour * _mod)) * _f.dudes_dr[r]); - var _kills = min(floor((_rank_shots * _final_hit) / _f.dudes_hp[r]), _f.dudes_num[r]); - if (_kills < 0) { - _kills = 0; - } + 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 (_kills > 0) { - _f.dudes_num[r] -= _kills; - obj_ncombat.enemy_forces -= _kills; - array_push(_hits, { name: _f.dudes[r], kills: _kills, bounced: (_final_hit <= 0), block: _f, rank: r }); + 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) { - // Volley spent ammo but killed no-one here; remember the first such rank so a - // non-killing sweep still reports a wound/bounce instead of going silent. - _wounded = { bounced: (_final_hit <= 0), block: _f, rank: r }; + _wounded = { + bounced: _results.bounced, + block: _block, + rank: _rank, + }; } } } - // Primary = the rank with the most kills (rich deferred flavour); the rest form the kill list. + // Generate and emit flavor text var _primary = undefined; var _spill = []; if (array_length(_hits) > 0) { @@ -544,30 +538,25 @@ function scr_shoot_spread(weapon_index_position) { _best = i; } } + for (var i = 0; i < array_length(_hits); i++) { if (i == _best) { continue; } - array_push(_spill, { name: _hits[i].name, count: _hits[i].kills }); + + array_push(_spill, {name: _hits[i].name, count: _hits[i].kills}); } + 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)) { - // Nothing died but the volley still landed: report a single wounded/bounced target so the - // consolidated flavour log records the shot (casualties = 0 -> injured or bounced). _primary = scr_flavor(weapon_index_position, _wounded.block, _wounded.rank, _shots, 0, _wounded.bounced, true); } - emit_volley_flavour(_primary, _spill); - // Clean up spent ranks/formations (mirrors scr_shoot). - for (var fi = 0; fi < array_length(_formations); fi++) { - if (instance_exists(_formations[fi])) { - compress_enemy_array(_formations[fi]); - destroy_empty_column(_formations[fi]); - } - } + emit_volley_flavour(_primary, _spill); + combat_cleanup_formations(_formations); } catch (_exception) { ERROR_HANDLER.handle_exception(_exception); } From fd22bbf4eee6a3eeb47bf5246c53a94fbee7de43 Mon Sep 17 00:00:00 2001 From: EttyKitty <20323032+EttyKitty@users.noreply.github.com> Date: Fri, 10 Jul 2026 05:17:40 +0300 Subject: [PATCH 9/9] refactor: Wire-in CombatDebugger (test) --- scripts/scr_clean/scr_clean.gml | 9 +++++---- scripts/scr_shoot/scr_shoot.gml | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/scr_clean/scr_clean.gml b/scripts/scr_clean/scr_clean.gml index 8abf61fa81..bff46605f3 100644 --- a/scripts/scr_clean/scr_clean.gml +++ b/scripts/scr_clean/scr_clean.gml @@ -206,7 +206,7 @@ function damage_infantry(_damage_data, _shots, _damage, _armour_pierce) { } } - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"damage_infantry valid_marines={array_length(valid_marines)} shots={_shots} dmg={_damage} ap={_hostile_armour_pierce}"); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"damage_infantry valid_marines={array_length(valid_marines)} shots={_shots} dmg={_damage} ap={_armour_pierce}"); // Apply damage for each shot for (var shot = 0; shot < _shots; shot++) { @@ -297,7 +297,7 @@ function damage_vehicles(_damage_data, _shots, _damage, _armour_pierce) { } } - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"damage_vehicles valid_vehicles={array_length(valid_vehicles)} shots={_shots} dmg={_damage} ap={_hostile_armour_pierce}"); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"damage_vehicles valid_vehicles={array_length(valid_vehicles)} shots={_shots} dmg={_damage} ap={_armour_pierce}"); // Apply damage for each hostile shot, until we run out of targets for (var shot = 0; shot < _shots; shot++) { @@ -311,7 +311,8 @@ function damage_vehicles(_damage_data, _shots, _damage, _armour_pierce) { veh_index = array_random_element(valid_vehicles); // Apply damage - var _modified_damage = _damage - combat_rank_armour(veh_ac[veh_index], _armour_pierce, false); + var _veh_armour = combat_rank_armour(veh_ac[veh_index], _armour_pierce, false); + var _modified_damage = _damage - _veh_armour; if (_modified_damage < 0) { _modified_damage = 0; } @@ -327,7 +328,7 @@ function damage_vehicles(_damage_data, _shots, _damage, _armour_pierce) { veh_dead[veh_index] = 1; _damage_data.units_lost++; obj_ncombat.player_forces -= 1; - obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"damage_vehicles veh[{veh_index}] ({_damage_data.unit_type}) DESTROYED: armour={veh_ac[veh_index] * _armour_mod} raw_dmg={_damage} mod_dmg={_modified_damage} hp_before={_hp_before}"); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"damage_vehicles veh[{veh_index}] ({_damage_data.unit_type}) DESTROYED: armour={_veh_armour} raw_dmg={_damage} mod_dmg={_modified_damage} hp_before={_hp_before}"); // Record loss var existing_index = array_get_index(lost, veh_type[veh_index]); diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index 63db092ca7..156b12e0c6 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -129,6 +129,8 @@ function combat_apply_rank_damage(block, rank, dmg_per_weapon, ap, splash, shots var _casualties = min(_raw_kills, _rank_num); _casualties = min(_casualties, ceil(shots_fired * splash)); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"combat_apply_rank_damage block={obj_ncombat.combat_debugger.resolve_label(block)} rank={rank} dmg_per_weapon={dmg_per_weapon} ap={ap} splash={splash} shots_fired={shots_fired} -> armour={_rank_armour} final_hit={_final_hit} raw_kills={_raw_kills} casualties={_casualties} bounced={_final_hit <= 0} num_before={_rank_num} hp_before={_rank_hp}"); + // Apply casualties and update overall combat status if (_casualties > 0) { block.dudes_num[rank] -= _casualties; @@ -191,6 +193,8 @@ function scr_shoot_enemy(weapon_index_position, target_object, damage_data, mele exit; } + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_enemy self={obj_ncombat.combat_debugger.resolve_label(id)} weapon_idx={weapon_index_position} weapon={wep[weapon_index_position]} target={obj_ncombat.combat_debugger.resolve_label(target_object)} dmg_data={damage_data} mode={melee_or_ranged} agg_dmg={aggregate_damage} ap={armour_pierce} hits={_hit_count} ammo_before={ammo[weapon_index_position]}"); + ammo[weapon_index_position] -= 1; var _hostile_type = 0; @@ -249,6 +253,7 @@ function scr_shoot_enemy(weapon_index_position, target_object, damage_data, mele _damage = 0; _hostile_type = 1; + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_enemy -> scr_clean(status) target={obj_ncombat.combat_debugger.resolve_label(target_object)} hostile_type={_hostile_type} hits={_hit_count} dmg={_damage} weapon={_weapon_name}"); 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) { @@ -274,6 +279,7 @@ function scr_shoot_enemy(weapon_index_position, target_object, damage_data, mele _damage = _damage_per_weapon / _hit_count; _hostile_type = 1; + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_enemy -> scr_clean(att) target={obj_ncombat.combat_debugger.resolve_label(target_object)} hostile_type={_hostile_type} hits={_hit_count} dmg_per_shot={_damage} weapon={_weapon_name} ap={armour_pierce}"); scr_clean(target_object, _hostile_type, _hit_count, _damage, _weapon_name, _weapon_range, _weapon_splash, armour_pierce); } } @@ -324,6 +330,7 @@ function scr_shoot_enemy(weapon_index_position, target_object, damage_data, mele target_object.hostile_shooters = _is_assorted ? 999 : 1; _hostile_type = 0; + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_enemy -> scr_clean(arp) target={obj_ncombat.combat_debugger.resolve_label(target_object)} hostile_type={_hostile_type} hits={_hit_count} dmg_per_shot={_damage} weapon={_weapon_name} ap={armour_pierce}"); scr_clean(target_object, _hostile_type, _hit_count, _damage, _weapon_name, _weapon_range, _weapon_splash, armour_pierce); } } @@ -334,6 +341,8 @@ function scr_shoot_enemy(weapon_index_position, target_object, damage_data, mele /// @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) { + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_player self={obj_ncombat.combat_debugger.resolve_label(id)} weapon_idx={weapon_index_position} target={obj_ncombat.combat_debugger.resolve_label(target_object)} target_type={target_type} agg_dmg={aggregate_damage} ap={armour_pierce}"); + // Resolve shots fired var shots_fired = 0; if (weapon_index_position >= 0) { @@ -398,15 +407,19 @@ function scr_shoot_player(weapon_index_position, target_object, target_type, agg } } + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"scr_shoot_player shots={shots_fired} dmg_per_weapon={damage_per_weapon} attack_count_mod={attack_count_mod}"); + // 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) { + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.DAMAGE, $"scr_shoot_player target rank dead, no alive ranks -> destroying column"); destroy_empty_column(target_object); exit; } target_type = _alive_rank; + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.TARGETING, $"scr_shoot_player target rank was dead, switched to rank {_alive_rank}"); } var _target_block = target_object; @@ -447,22 +460,28 @@ function scr_shoot_player(weapon_index_position, target_object, target_type, agg var _next_rank = find_next_alive_rank(_target_block, _target_block.dudes_vehicle[_target_rank]); if (_next_rank == -1) { + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_player spillover: no more alive ranks in block {obj_ncombat.combat_debugger.resolve_label(_target_block)}, searching next formation"); _target_block = get_next_enemy_formation(_target_block); if (_target_block == noone) { + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_player spillover: no next formation found, done"); break; } array_push(_touched_blocks, _target_block); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_player spillover: switched to formation {obj_ncombat.combat_debugger.resolve_label(_target_block)}"); _next_rank = find_next_alive_rank(_target_block, -1); if (_next_rank == -1) { + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_player spillover: new formation has no alive ranks"); break; } } _target_rank = _next_rank; + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_player spillover: continuing at block={obj_ncombat.combat_debugger.resolve_label(_target_block)} rank={_target_rank} shots_left={_shots_left}"); } emit_volley_flavour(_primary_flavour, _spill_kills); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.CLEANUP, $"scr_shoot_player cleanup {array_length(_touched_blocks)} touched blocks, spill_kills={array_length(_spill_kills)}"); combat_cleanup_formations(_touched_blocks); } @@ -481,6 +500,8 @@ function scr_shoot_spread(weapon_index_position) { var _damage_per_weapon = att[weapon_index_position] / _shots; var _splash = max(1, splash[weapon_index_position]); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.SHOOTING, $"scr_shoot_spread self={obj_ncombat.combat_debugger.resolve_label(id)} weapon_idx={weapon_index_position} weapon={wep[weapon_index_position]} shots={_shots} ap={_ap} dmg_per_weapon={_damage_per_weapon} splash={_splash}"); + if (ammo[weapon_index_position] > 0) { ammo[weapon_index_position] -= 1; } @@ -556,6 +577,7 @@ function scr_shoot_spread(weapon_index_position) { } emit_volley_flavour(_primary, _spill); + obj_ncombat.combat_debugger.add(eCOMBAT_CATEGORY.CLEANUP, $"scr_shoot_spread cleanup {array_length(_formations)} formations, hits={array_length(_hits)}"); combat_cleanup_formations(_formations); } catch (_exception) { ERROR_HANDLER.handle_exception(_exception);