Files
darktide-AutoMark/AutoMark/scripts/mods/AutoMark/targeting/custom_targeting.lua
T

1106 lines
48 KiB
Lua

---@class AutoMarkMod:DMFMod
local mod = get_mod("AutoMark")
local context = mod.context
local mark_context = mod.mark_context
local TAG_NAMES = mod.TAG_NAMES
local mod_settings = mod.settings
local companion_cancel_mark_breed_settings = mod.companion_cancel_mark_breed_settings
local noospheric_command_breed_settings = mod.noospheric_command_breed_settings
local visibility_cache = mod.visibility_cache
local visibility_check_frame = mod.visibility_check_frame
local servo_skull_visibility_cache = mod.servo_skull_visibility_cache
local servo_skull_visibility_check_frame = mod.servo_skull_visibility_check_frame
-- Imports
local Breed = require("scripts/utilities/breed")
local SpecialRulesSettings = require("scripts/settings/ability/special_rules_settings")
local Breed_height = Breed.height
local special_rules = SpecialRulesSettings.special_rules
-- Global Cache
local CLASS = CLASS
local HEALTH_ALIVE = HEALTH_ALIVE
local Managers = Managers
local get_game_object_field = GameSession.game_object_field
local Actor_unit = Actor.unit
local Actor_world_bounds = Actor.world_bounds
local Unit_box = Unit.box
local Unit_node = Unit.node
local Unit_world_position = Unit.world_position
local PhysicsWorld_raycast = PhysicsWorld.raycast
local PhysicsWorld_make_raycast = PhysicsWorld.make_raycast
local PhysicsWorld_linear_sphere_sweep = PhysicsWorld.linear_sphere_sweep
local Raycast_cast = Raycast.cast
local ScriptUnit_extension = ScriptUnit.extension
local math_abs = math.abs
local math_max = math.max
local math_cos = math.cos
local math_rad = math.rad
local Vector3_dot = Vector3.dot
local Vector3_normalize = Vector3.normalize
local Vector3_length = Vector3.length
local Vector3_distance = Vector3.distance
local Vector3_distance_squared = Vector3.distance_squared
local Vector3_flat = Vector3.flat
local Matrix4x4_right = Matrix4x4.right
local Matrix4x4_forward = Matrix4x4.forward
local Quaternion_forward = Quaternion.forward
-- Constants
local INDEX_POSITION = 1
local INDEX_DISTANCE = 2
local INDEX_NORMAL = 3
local INDEX_ACTOR = 4
local COLLISION_FILTER = "filter_player_ping_target_selection"
local EMPTY_TABLE = {}
local MAX_VISIBILITY_CHECKS_PER_FRAME = 16
local VIEW_CHECK_MIN_DISTANCE = 3
local VIEW_CHECK_MIN_DOT = 0.75
local AIM_CHECK_FILTER = "filter_player_ping_target_selection"
local AIM_CHECK_MAX_HITS = 64
local AIM_CHECK_MIN_RADIUS = 1
local AIM_CHECK_RADIUS_PER_METER = 0.052
local omni_rejects = nil
local function omni_reject(reason)
if omni_rejects then
omni_rejects[reason] = (omni_rejects[reason] or 0) + 1
end
end
-- Params
local visibility_raycast_object = nil
function mod:init_visibility_raycast_objects()
local smart_targeting_extension = context.smart_targeting_extension
local physics_world = smart_targeting_extension and smart_targeting_extension._physics_world
if physics_world then
visibility_raycast_object = PhysicsWorld_make_raycast(physics_world, "closest", "types", "both", "collision_filter", "filter_minion_line_of_sight_check")
end
end
function mod:destroy_visibility_raycast_objects()
visibility_raycast_object = nil
end
local function is_daemonhost_aggroed(unit)
local game_session = Managers.state.game_session:game_session()
local game_object_id = Managers.state.unit_spawner:game_object_id(unit)
local stage = get_game_object_field(game_session, game_object_id, "stage")
return type(stage) == "number" and stage == 6
end
local function is_target_aggroed(unit)
local game_session = Managers.state.game_session:game_session()
local game_object_id = Managers.state.unit_spawner:game_object_id(unit)
local attacking_unit_id = get_game_object_field(game_session, game_object_id, "target_unit_id")
return attacking_unit_id ~= -1
end
function mod:is_dormant_daemonhost(target_unit)
local unit_data_extension = ScriptUnit_extension(target_unit, "unit_data_system")
local breed_data = unit_data_extension and unit_data_extension._breed
if breed_data and breed_data.tags.witch and not is_target_aggroed(target_unit) then
return true
end
return false
end
local function get_breed_priority(breed_name, breed_data, breed_priorities, attacking_unit_id)
if breed_data and breed_data.tags.witch then
if attacking_unit_id == -1 then
return breed_priorities[breed_name .. "_passive"] or 0, true
else
return breed_priorities[breed_name] or 0, false
end
else
return breed_priorities[breed_name] or 0, false
end
end
-- Check if Target Unit's Breed is Valid for Auto-Mark
local function is_breed_group_valid(breed_data, class_settings)
if not breed_data or not class_settings then
return false
end
-- toggle enemy by type
if breed_data.tags.elite then
return class_settings.toggle_elite
elseif breed_data.tags.special then
return class_settings.toggle_special
elseif breed_data.is_boss then
return class_settings.toggle_boss
else
return class_settings.toggle_other
end
end
function mod:can_focus_target_overwrite(target_unit, target_tag)
if not target_tag then
return true
end
local target_buff_extension = ScriptUnit_extension(target_unit, "buff_system")
if not target_buff_extension then
return false
end
local focus_target_debuff = target_buff_extension._stacking_buffs["veteran_improved_tag_debuff"]
local target_stack_count = focus_target_debuff and focus_target_debuff:stack_count() or 0
local target_tag_name = target_tag and target_tag._template.name
-- target does not have focus target debuff
if target_tag_name ~= TAG_NAMES.VETERAN_TAG and target_stack_count <= 0 then
return true
end
-- latency bug
if target_tag_name == TAG_NAMES.VETERAN_TAG and target_stack_count <= 0 then
return false
end
-- focus_target_overwrite not enabled
if not mod_settings.focus_target_overwrite then
return false
end
local talent_resource_component = context.talent_resource_component
if not talent_resource_component then
return false
end
-- check if player's buff stacks greater than target's debuff stacks
local player_stack_count = talent_resource_component.current_resource or 0
if player_stack_count <= target_stack_count then
return false
end
return player_stack_count == context.focus_target_max_stacks
or player_stack_count - target_stack_count >= mod_settings.focus_target_overwrite_delta
end
local function is_focus_target_tag_target_valid(target_unit, target_tag, target_breed_name, target_attacking_unit_id, removed_units)
local target_tag_name = target_tag and target_tag._template.name
if target_tag_name == TAG_NAMES.COMPANION_TAG or target_tag_name == TAG_NAMES.SERVO_SKULL_TAG then
if removed_units[target_unit] then
return false
end
end
if target_breed_name ~= "cultist_ritualist" and mod_settings.focus_target_ignore_unaggroed and target_attacking_unit_id == -1 then
return false
end
if not mod:can_focus_target_overwrite(target_unit, target_tag) then
return false
end
return true
end
local function is_ritual_started(daemonhost_id, game_session)
if not daemonhost_id or daemonhost_id == -1 then
return false
end
local stage = get_game_object_field(game_session, daemonhost_id, "stage")
return type(stage) == "number" and stage > 1
end
local function is_cyber_mastiff_tag_target_valid(target_unit, target_tag, target_breed_name, target_breed_data, target_attacking_unit_id, game_session, removed_units, distance, companion_position, target_position)
-- do not target unit tagged by focus target
local target_tag_name = target_tag and target_tag._template.name
if target_tag_name == TAG_NAMES.VETERAN_TAG then
if removed_units[target_unit] then
return false
end
-- do not target pounceble unit marked by other dog
elseif target_tag_name == TAG_NAMES.COMPANION_TAG then
local pounce_setting = target_breed_data.companion_pounce_setting
local pounce_action = pounce_setting and pounce_setting.companion_pounce_action
if pounce_action == "human" then
return false
end
end
-- check if target is aggroed or if ritual is started
if target_breed_name == "chaos_mutator_ritualist" then
if not is_ritual_started(target_attacking_unit_id, game_session) then
return false
end
elseif target_breed_name == "cultist_ritualist" then
-- do nothing
else
if mod_settings.companion_mark_ignore_unaggroed and target_attacking_unit_id == -1 then
return false
end
end
-- check if target is within range limitation
local breed_settings = companion_cancel_mark_breed_settings[target_breed_name]
local player_max_distance
local companion_range_limitation
if breed_settings and breed_settings.override then
player_max_distance = breed_settings.max_distance or 0
companion_range_limitation = breed_settings.range_limitation or 0
else
player_max_distance = mod_settings.companion_mark_max_distance or 0
companion_range_limitation = mod_settings.companion_range_limitation or 0
end
if player_max_distance > 0 then
if distance > player_max_distance then
return false
end
end
if companion_range_limitation > 0 then
if not companion_position or not target_position then
return false
end
if Vector3_distance_squared(companion_position, target_position) > companion_range_limitation * companion_range_limitation then
return false
end
end
return true
end
local function has_enough_capacitance(breed_settings, breed_data, max_ability_charges, remaining_capacitance)
if not mod_settings.capacitance_retention or not context.has_noospheric_command then
return true
end
if not breed_data or not max_ability_charges or not remaining_capacitance then
return false
end
local capacitance_retention_threshold
if breed_settings and breed_settings.override then
capacitance_retention_threshold = breed_settings.threshold or 0
else
if breed_data.is_boss then
capacitance_retention_threshold = mod_settings.capacitance_retention_boss_threshold
elseif breed_data.tags.special then
capacitance_retention_threshold = mod_settings.capacitance_retention_special_threshold
else
capacitance_retention_threshold = mod_settings.capacitance_retention_elite_threshold
end
end
if 1 / capacitance_retention_threshold < 0 then
capacitance_retention_threshold = max_ability_charges + capacitance_retention_threshold
end
return remaining_capacitance >= capacitance_retention_threshold
end
local function get_remaining_ability_capacitance()
local player_ability_extension = context.player_ability_extension
if not player_ability_extension then
return 0
end
local remaining_ability_resource = player_ability_extension:remaining_ability_resource("combat_ability")
local resource_cost_per_charge = player_ability_extension:get_ability_resource_cost_per_charge("combat_ability")
return remaining_ability_resource / resource_cost_per_charge
end
function mod:has_enough_capacitance(target_unit)
local unit_data_extension = ScriptUnit_extension(target_unit, "unit_data_system")
local breed_data = unit_data_extension and unit_data_extension._breed
local breed_name = breed_data and breed_data.name
local breed_settings = noospheric_command_breed_settings[breed_name]
local player_ability_extension = context.player_ability_extension
local max_ability_charges = player_ability_extension and player_ability_extension:max_ability_charges("combat_ability")
local remaining_capacitance = get_remaining_ability_capacitance()
return has_enough_capacitance(breed_settings, breed_data, max_ability_charges, remaining_capacitance)
end
local function is_servo_skull_tag_target_valid(target_unit, target_tag, target_breed_name, target_breed_data, target_attacking_unit_id, game_session, removed_units, companion_position, target_position, max_ability_charges, remaining_capacitance)
-- do not target unit tagged by focus target
local target_tag_name = target_tag and target_tag._template.name
if target_tag_name == TAG_NAMES.VETERAN_TAG then
if removed_units[target_unit] then
return false
end
end
-- check if target is aggroed or if ritual is started
if target_breed_name == "chaos_mutator_ritualist" then
if not is_ritual_started(target_attacking_unit_id, game_session) then
return false
end
elseif target_breed_name == "cultist_ritualist" then
-- do nothing
else
if mod_settings.servo_skull_mark_ignore_unaggroed and target_attacking_unit_id == -1 then
return false
end
end
-- check if target is within range limitation
local breed_settings = noospheric_command_breed_settings[target_breed_name]
local servo_skull_range_limitation
if breed_settings and breed_settings.override then
servo_skull_range_limitation = breed_settings.range_limitation or 0
else
servo_skull_range_limitation = mod_settings.servo_skull_range_limitation or 0
end
if servo_skull_range_limitation > 0 then
if not companion_position or not target_position then
return false
end
if Vector3_distance_squared(companion_position, target_position) > servo_skull_range_limitation * servo_skull_range_limitation then
return false
end
end
-- check if capacitance is sufficient
if not has_enough_capacitance(breed_settings, target_breed_data, max_ability_charges, remaining_capacitance) then
return false
end
return true
end
local function is_sticky_targeting(tag_name, tag_context, breed_name, breed_data)
if tag_name == TAG_NAMES.COMPANION_TAG then
if not mod_settings.companion_mark_sticky_targeting then
return false
end
if not tag_context.pounce_start_time then
return false
end
if not breed_data then
return false
end
local breed_settings = companion_cancel_mark_breed_settings[breed_name]
if breed_settings and breed_settings.override then
return breed_settings.sticky_targeting
end
if breed_data.is_boss then
return mod_settings.companion_mark_sticky_targeting_boss
elseif breed_data.tags.special then
return mod_settings.companion_mark_sticky_targeting_special
else
return mod_settings.companion_mark_sticky_targeting_elite
end
elseif tag_name == TAG_NAMES.SERVO_SKULL_TAG then
if not mod_settings.servo_skull_mark_sticky_targeting then
return false
end
if not tag_context.shoot_start_time then
return false
end
if not breed_data then
return false
end
local breed_settings = noospheric_command_breed_settings[breed_name]
if breed_settings and breed_settings.override then
return breed_settings.sticky_targeting
end
if breed_data.is_boss then
return mod_settings.servo_skull_mark_sticky_targeting_boss
elseif breed_data.tags.special then
return mod_settings.servo_skull_mark_sticky_targeting_special
else
return mod_settings.servo_skull_mark_sticky_targeting_elite
end
else
return false
end
end
local function is_target_visible(ray_origin, up, target_unit_center_pos, half_height, target_unit, fixed_frame)
if not visibility_raycast_object then
return false
end
local cached_visibility = visibility_cache[target_unit]
local last_check_frame = visibility_check_frame[target_unit]
if cached_visibility ~= nil and fixed_frame - last_check_frame <= 5 then
return cached_visibility
end
mod.num_visibility_checks_this_frame = mod.num_visibility_checks_this_frame + 1
if mod.num_visibility_checks_this_frame > MAX_VISIBILITY_CHECKS_PER_FRAME then
return false
end
local to_target_center = target_unit_center_pos - ray_origin
local to_target_top = to_target_center + up * half_height
local hit_top = Raycast_cast(visibility_raycast_object, ray_origin, Vector3_normalize(to_target_top), Vector3_length(to_target_top))
if not hit_top then
visibility_cache[target_unit] = true
visibility_check_frame[target_unit] = fixed_frame
return true
end
local hit_center = Raycast_cast(visibility_raycast_object, ray_origin, Vector3_normalize(to_target_center), Vector3_length(to_target_center))
visibility_cache[target_unit] = not hit_center
visibility_check_frame[target_unit] = fixed_frame
return not hit_center
end
local function is_cyber_mastiff_target_visible(ray_origin, up, target_unit_center_pos, half_height, target_unit, fixed_frame, target_tag, target_unit_marked_by_execution_order)
if mod_settings.companion_mark_tagged_always_visible and (target_tag or target_unit_marked_by_execution_order) then
return true
end
return is_target_visible(ray_origin, up, target_unit_center_pos, half_height, target_unit, fixed_frame)
end
local function is_servo_skull_target_visible(ray_origin, target_unit, fixed_frame, force_check)
if not visibility_raycast_object then
return false
end
local cached_visibility = servo_skull_visibility_cache[target_unit]
local last_check_frame = servo_skull_visibility_check_frame[target_unit]
if cached_visibility ~= nil and fixed_frame - last_check_frame <= 5 then
return cached_visibility
end
if not force_check then
mod.num_visibility_checks_this_frame = mod.num_visibility_checks_this_frame + 1
if mod.num_visibility_checks_this_frame > MAX_VISIBILITY_CHECKS_PER_FRAME then
return false
end
end
local companion_spawner_extension = context.companion_spawner_extension
local servo_skull_unit = companion_spawner_extension and companion_spawner_extension:spawned_unit_lookup(special_rules.cryptic_servo_skull_hack)
if not servo_skull_unit then
return false
end
local target_los_node = Unit_node(target_unit, "enemy_aim_target_03")
local target_position = Unit_world_position(target_unit, target_los_node)
local to_target_position = target_position - ray_origin
local hit = Raycast_cast(visibility_raycast_object, ray_origin, Vector3_normalize(to_target_position), Vector3_length(to_target_position))
servo_skull_visibility_cache[target_unit] = not hit
servo_skull_visibility_check_frame[target_unit] = fixed_frame
return not hit
end
function mod:is_servo_skull_target_visible(target_unit, fixed_frame, force_check)
local companion_spawner_extension = context.companion_spawner_extension
local servo_skull_unit = companion_spawner_extension and companion_spawner_extension:spawned_unit_lookup(special_rules.cryptic_servo_skull_hack)
if not servo_skull_unit then
return false
end
local servo_skull_los_node = Unit_node(servo_skull_unit, "skull_aim_center")
local servo_skull_ray_origin = Unit_world_position(servo_skull_unit, servo_skull_los_node)
return is_servo_skull_target_visible(servo_skull_ray_origin, target_unit, fixed_frame, force_check)
end
function mod:is_target_pingable(target_unit)
local player = context.player
local player_unit = player and player.player_unit
if not player_unit then
return false
end
local player_unit_data_extension = ScriptUnit_extension(player_unit, "unit_data_system")
local first_person_component = player_unit_data_extension and player_unit_data_extension:read_component("first_person")
if not first_person_component then
return false
end
local target_position = Unit_world_position(target_unit, 1)
local to_target = target_position - Unit_world_position(player_unit, 1)
local distance = Vector3_length(to_target)
if distance < VIEW_CHECK_MIN_DISTANCE then
return true
end
local forward = Vector3_flat(Quaternion_forward(first_person_component.rotation))
local dot = Vector3_dot(Vector3_normalize(forward), Vector3_normalize(to_target))
if dot < VIEW_CHECK_MIN_DOT then
return mod_settings.omni_mark == true
end
local first_person_extension = ScriptUnit_extension(player_unit, "first_person_system")
local first_person_unit = first_person_extension and first_person_extension:first_person_unit()
if not first_person_unit then
return false
end
local smart_targeting_extension = context.smart_targeting_extension
local physics_world = smart_targeting_extension and smart_targeting_extension._physics_world
if not physics_world then
return false
end
local eye_position = Unit_world_position(first_person_unit, 1)
local test_distance = Vector3_distance(eye_position, target_position)
local radius = math.max(AIM_CHECK_MIN_RADIUS, test_distance * AIM_CHECK_RADIUS_PER_METER)
local to = eye_position + forward * (test_distance + radius)
local hits = PhysicsWorld_linear_sphere_sweep(physics_world, eye_position, to, radius, AIM_CHECK_MAX_HITS, "types", "both", "collision_filter", AIM_CHECK_FILTER, "report_initial_overlap", true)
for i = 1, hits and #hits or 0 do
if Actor_unit(hits[i].actor) == target_unit then
return true
end
end
return false
end
local IGNORE_EXECUTION_ORDER_FORCE_MARK_BREED_NAMES = {
chaos_poxwalker_bomber = true,
chaos_ogryn_houndmaster = true,
}
local IGNORE_EXECUTION_ORDER_PRIORITY_BREED_NAMES = {
cultist_ritualist = true,
chaos_mutator_ritualist = true,
renegade_netgunner = true,
}
local IGNORE_THREAT_PRIORITY_BREED_NAMES = {
cultist_grenadier = true,
renegade_grenadier = true,
cultist_ritualist = true,
chaos_mutator_ritualist = true,
}
function mod:find_auto_mark_target_unit(min_range, max_range, tag_name, tag_context, class_settings, marked_tag)
local player = context.player
local player_unit = player and player.player_unit
local smart_targeting_extension = context.smart_targeting_extension
local smart_tag_system = context.smart_tag_system
if not player_unit or not smart_targeting_extension or not smart_tag_system then
return
end
-- hit unit list: forward raycast, or full-sphere sweep when omni mark is enabled
local omni_mark = mod_settings.omni_mark
if mod_settings.debug_mode and not mod.omni_diag_settings_logged then
mod.omni_diag_settings_logged = true
printf("[MOD][AutoMark][DIAG] %s", string.format("OMNI-DIAG settings: omni_mark=%s ignore_los=%s debug=%s", tostring(omni_mark), tostring(mod_settings.omni_mark_ignore_los), tostring(mod_settings.debug_mode)))
end
local ray_origin, forward, right, up = smart_targeting_extension:_targeting_parameters()
local hits, num_hits, hits_are_units
if omni_mark then
-- enumerate smart-taggable units directly; physics sphere sweeps get swamped by world geometry actors
hits = {}
num_hits = 0
local unit_extension_data = smart_tag_system._unit_extension_data
local max_range_squared = max_range * max_range
if unit_extension_data then
for unit, smart_tag_extension in pairs(unit_extension_data) do
if HEALTH_ALIVE[unit] and smart_tag_extension._target_type == "breed" then
local position = Unit_world_position(unit, 1)
if Vector3_distance_squared(position, ray_origin) <= max_range_squared then
num_hits = num_hits + 1
hits[num_hits] = { unit = unit, position = position }
end
end
end
end
hits_are_units = true
omni_diag_counter = (omni_diag_counter or 0) + 1
omni_rejects = mod_settings.debug_mode and omni_diag_counter % 10 == 1 and {} or nil
if omni_rejects then
local counts, ordered_names = {}, {}
for i = 1, num_hits do
local unit_data_extension = ScriptUnit_extension(hits[i].unit, "unit_data_system")
local breed = unit_data_extension and unit_data_extension._breed
local key = breed and breed.name or "?"
if counts[key] == nil then
counts[key] = 0
ordered_names[#ordered_names + 1] = key
end
counts[key] = counts[key] + 1
end
local name_parts = {}
for _, n in ipairs(ordered_names) do
name_parts[#name_parts + 1] = n .. "x" .. counts[n]
end
printf("[MOD][AutoMark][DIAG] %s", string.format("OMNI-DIAG candidates=%d breeds=[%s]", num_hits, table.concat(name_parts, ",")))
end
else
hits, num_hits = PhysicsWorld_raycast(smart_targeting_extension._physics_world, ray_origin, forward, max_range, "all", "collision_filter", COLLISION_FILTER)
end
if not hits or num_hits <= 0 then
return
end
-- cache func
local game_session = Managers.state.game_session:game_session()
local unit_spawner_manager = Managers.state.unit_spawner
local get_game_object_id = unit_spawner_manager.game_object_id
-- cache params
local fixed_frame = smart_targeting_extension._latest_fixed_frame
local canceled_units = tag_context and tag_context.canceled_units or EMPTY_TABLE
local removed_units = tag_context and tag_context.removed_units or EMPTY_TABLE
local execution_order_units = mark_context.execution_order_units or EMPTY_TABLE
local breed_priorities = class_settings and class_settings.breed_priorities or EMPTY_TABLE
local companion_position, max_ability_charges, remaining_capacitance
if tag_name == TAG_NAMES.COMPANION_TAG then
local companion_spawner_extension = context.companion_spawner_extension
local companion_units = companion_spawner_extension and companion_spawner_extension:companion_units()
local companion_unit = companion_units and companion_units[1]
companion_position = companion_unit and Unit_world_position(companion_unit, 1)
elseif tag_name == TAG_NAMES.SERVO_SKULL_TAG then
local companion_spawner_extension = context.companion_spawner_extension
local servo_skull_unit = companion_spawner_extension and companion_spawner_extension:spawned_unit_lookup(special_rules.cryptic_servo_skull_hack)
local servo_skull_los_node = servo_skull_unit and Unit_node(servo_skull_unit, "skull_aim_center")
companion_position = servo_skull_los_node and Unit_world_position(servo_skull_unit, servo_skull_los_node)
local player_ability_extension = context.player_ability_extension
max_ability_charges = player_ability_extension and player_ability_extension:max_ability_charges("combat_ability")
remaining_capacitance = get_remaining_ability_capacitance()
end
local threat_priority = tag_name == TAG_NAMES.COMPANION_TAG and mod_settings.companion_mark_threat_priority or tag_name == TAG_NAMES.SERVO_SKULL_TAG and mod_settings.servo_skull_mark_threat_priority
local execution_order_priority = tag_name == TAG_NAMES.COMPANION_TAG and mod_settings.execution_order_priority and context.has_execution_order
local execution_order_force_mark = tag_name == TAG_NAMES.COMPANION_TAG and mod_settings.execution_order_force_mark and context.has_execution_order
local player_unit_id = get_game_object_id(unit_spawner_manager, player_unit)
-- init best unit for switch logic
local best_unit = nil
local best_unit_tag = nil
local best_unit_priority = -math.huge
local best_unit_breed_name = nil
local best_unit_is_attacking_player = false
local best_unit_is_dormant_daemonhost = false
local best_unit_marked_by_execution_order = false
local best_unit_is_sticky_targeting = false
local best_unit_distance = math.huge
local marked_unit = marked_tag and marked_tag._target_unit
if marked_unit then
best_unit = marked_unit
best_unit_distance = Vector3_length(Unit_world_position(best_unit, 1) - ray_origin)
local best_unit_data_extension = ScriptUnit_extension(best_unit, "unit_data_system")
local best_unit_breed_data = best_unit_data_extension and best_unit_data_extension._breed
best_unit_breed_name = best_unit_breed_data and best_unit_breed_data.name
local best_unit_game_object_id = get_game_object_id(unit_spawner_manager, marked_unit)
local best_unit_attacking_unit_id = get_game_object_field(game_session, best_unit_game_object_id, "target_unit_id")
best_unit_priority, best_unit_is_dormant_daemonhost = get_breed_priority(best_unit_breed_name, best_unit_breed_data, breed_priorities, best_unit_attacking_unit_id)
best_unit_marked_by_execution_order = execution_order_units[best_unit]
best_unit_is_attacking_player = best_unit_attacking_unit_id == player_unit_id
best_unit_is_sticky_targeting = is_sticky_targeting(tag_name, tag_context, best_unit_breed_name, best_unit_breed_data)
end
for i = 1, num_hits do
local hit = hits[i]
local hit_unit, hit_unit_center_pos
if hits_are_units then
hit_unit = hit.unit
hit_unit_center_pos = hit.position
else
local hit_actor = hit[INDEX_ACTOR]
if not hit_actor then
omni_reject("no_actor")
goto continue
end
hit_unit = Actor_unit(hit_actor)
end
-- ignore player unit, already marked unit and dead unit
if hit_unit == player_unit or hit_unit == marked_unit or canceled_units[hit_unit] or not HEALTH_ALIVE[hit_unit] then
omni_reject("skip_player_dead")
goto continue
end
local hit_unit_data_extension = ScriptUnit_extension(hit_unit, "unit_data_system")
local hit_unit_breed_data = hit_unit_data_extension and hit_unit_data_extension._breed
-- ignore untaggable unit
if not hit_unit_breed_data or hit_unit_breed_data.smart_tag_target_type ~= "breed" or hit_unit_breed_data.faction_name == "imperium" or hit_unit_breed_data.unit_template_name ~= "minion" then
omni_reject("untaggable")
goto continue
end
local hit_unit_breed_name = hit_unit_breed_data.name
if best_unit_is_sticky_targeting and hit_unit_breed_name ~= "renegade_netgunner" then
omni_reject("sticky1")
goto continue
end
local hit_unit_game_object_id = get_game_object_id(unit_spawner_manager, hit_unit)
local hit_unit_attacking_unit_id = get_game_object_field(game_session, hit_unit_game_object_id, "target_unit_id")
local hit_unit_priority, hit_unit_is_dormant_daemonhost = get_breed_priority(hit_unit_breed_name, hit_unit_breed_data, breed_priorities, hit_unit_attacking_unit_id)
local hit_unit_marked_by_execution_order = execution_order_units[hit_unit]
-- filter unit by type and priority
if hit_unit_priority <= 0 or not is_breed_group_valid(hit_unit_breed_data, class_settings) then
if not execution_order_force_mark
or not hit_unit_marked_by_execution_order
or IGNORE_EXECUTION_ORDER_FORCE_MARK_BREED_NAMES[hit_unit_breed_name]
or hit_unit_is_dormant_daemonhost
then
omni_reject("priority_or_group")
goto continue
end
end
local hit_unit_is_attacking_player = hit_unit_attacking_unit_id == player_unit_id
if best_unit_is_sticky_targeting and not hit_unit_is_attacking_player then
omni_reject("sticky2")
goto continue
end
local hit_unit_score = hit_unit_priority
local best_unit_score = best_unit_priority
if execution_order_priority and not IGNORE_EXECUTION_ORDER_PRIORITY_BREED_NAMES[hit_unit_breed_name] and not IGNORE_EXECUTION_ORDER_PRIORITY_BREED_NAMES[best_unit_breed_name] then
hit_unit_score = hit_unit_score + (hit_unit_marked_by_execution_order and 1000 or 0)
best_unit_score = best_unit_score + (best_unit_marked_by_execution_order and 1000 or 0)
end
if threat_priority and not IGNORE_THREAT_PRIORITY_BREED_NAMES[hit_unit_breed_name] and not IGNORE_THREAT_PRIORITY_BREED_NAMES[best_unit_breed_name] then
hit_unit_score = hit_unit_score + (hit_unit_is_attacking_player and 100 or 0)
best_unit_score = best_unit_score + (best_unit_is_attacking_player and 100 or 0)
end
if hit_unit_score < best_unit_score then
omni_reject("lower_score")
goto continue
end
if not hit_unit_center_pos then
hit_unit_center_pos = Actor_world_bounds(hit[INDEX_ACTOR])
end
local to_hit_unit_center = hit_unit_center_pos - ray_origin
local distance = Vector3_length(to_hit_unit_center)
-- filter unit by range
if distance < min_range or distance > max_range then
omni_reject("out_of_range")
goto continue
end
-- prefer the closer unit on score ties (hit order is arbitrary for the omni sweep)
if hit_unit_score == best_unit_score and distance >= best_unit_distance then
omni_reject("tie_distance")
goto continue
end
local hit_unit_tag = smart_tag_system:unit_tag(hit_unit)
if tag_name == TAG_NAMES.ENEMY_TAG then
if hit_unit_tag then
omni_reject("already_tagged")
goto continue
end
elseif tag_name == TAG_NAMES.VETERAN_TAG then
if hit_unit_is_dormant_daemonhost then
if hit_unit_tag then
omni_reject("vet_tagged")
goto continue
end
else
if not is_focus_target_tag_target_valid(hit_unit, hit_unit_tag, hit_unit_breed_name, hit_unit_attacking_unit_id, removed_units) then
omni_reject("vet_invalid")
goto continue
end
end
elseif tag_name == TAG_NAMES.COMPANION_TAG then
if not is_cyber_mastiff_tag_target_valid(hit_unit, hit_unit_tag, hit_unit_breed_name, hit_unit_breed_data, hit_unit_attacking_unit_id, game_session, removed_units, distance, companion_position, hit_unit_center_pos) then
omni_reject("companion_invalid")
goto continue
end
elseif tag_name == TAG_NAMES.SERVO_SKULL_TAG then
if not is_servo_skull_tag_target_valid(hit_unit, hit_unit_tag, hit_unit_breed_name, hit_unit_breed_data, hit_unit_attacking_unit_id, game_session, removed_units, companion_position, hit_unit_center_pos, max_ability_charges, remaining_capacitance) then
omni_reject("skull_invalid")
goto continue
end
end
local visible
if omni_mark and mod_settings.omni_mark_ignore_los then
visible = true
elseif tag_name == TAG_NAMES.SERVO_SKULL_TAG then
visible = is_servo_skull_target_visible(companion_position, hit_unit, fixed_frame)
elseif tag_name == TAG_NAMES.COMPANION_TAG then
local half_height = Breed_height(hit_unit, hit_unit_breed_data) * 0.5
visible = is_cyber_mastiff_target_visible(ray_origin, up, hit_unit_center_pos, half_height, hit_unit, fixed_frame, hit_unit_tag, hit_unit_marked_by_execution_order)
else
local half_height = Breed_height(hit_unit, hit_unit_breed_data) * 0.5
visible = is_target_visible(ray_origin, up, hit_unit_center_pos, half_height, hit_unit, fixed_frame)
end
if not visible then
omni_reject("not_visible")
goto continue
end
best_unit = hit_unit
best_unit_tag = hit_unit_tag
best_unit_priority = hit_unit_priority
best_unit_breed_name = hit_unit_breed_name
best_unit_is_attacking_player = hit_unit_is_attacking_player
best_unit_is_dormant_daemonhost = hit_unit_is_dormant_daemonhost
best_unit_marked_by_execution_order = hit_unit_marked_by_execution_order
best_unit_is_sticky_targeting = false
best_unit_distance = distance
::continue::
end
if omni_rejects then
local parts = {}
for k, v in pairs(omni_rejects) do
parts[#parts + 1] = k .. "=" .. v
end
table.sort(parts)
printf("[MOD][AutoMark][DIAG] %s", string.format("OMNI-DIAG pass[%s] best=%s rejects=[%s]", tostring(tag_name), tostring(best_unit_breed_name), table.concat(parts, ",")))
omni_rejects = nil
end
if best_unit ~= marked_unit then
return best_unit, best_unit_tag, best_unit_is_dormant_daemonhost
end
end
local ENEMY_EXTENTS = {
chaos_beast_of_nurgle = {
half_extent_forward = 1.1,
half_extent_right = 1.5,
},
chaos_ogryn_bulwark = {
half_extent_right = 0.8,
},
chaos_ogryn_executor = {
half_extent_right = 0.9,
},
chaos_ogryn_gunner = {
half_extent_right = 0.8,
},
chaos_ogryn_houndmaster = {
half_extent_right = 1.4,
},
chaos_plague_ogryn = {
half_extent_right = 1.6,
},
chaos_spawn = {
half_extent_right = 1.2,
},
cultist_berzerker = {
half_extent_right = 0.5,
},
cultist_mutant = {
half_extent_right = 1,
},
cultist_mutant_mutator = {
half_extent_right = 1,
},
renegade_berzerker = {
half_extent_right = 0.5,
},
}
local DISTANCE_EPSILON = 0.4
local MIN_COSINE = math_cos(math_rad(35))
function mod:find_focus_target_switch_melee_target_unit(max_range)
local player = context.player
local player_unit = player and player.player_unit
local smart_targeting_extension = context.smart_targeting_extension
local smart_tag_system = context.smart_tag_system
if not player_unit or not smart_targeting_extension or not smart_tag_system then
return
end
local ray_origin, forward, right, up = smart_targeting_extension:_targeting_parameters()
local hits, num_hits = PhysicsWorld_raycast(smart_targeting_extension._physics_world, ray_origin, forward, max_range, "all", "collision_filter", COLLISION_FILTER)
if not hits or num_hits <= 0 then
return
end
local fixed_frame = smart_targeting_extension._latest_fixed_frame
local best_unit
local best_unit_dot = -math.huge
local best_unit_distance = math.huge
local best_unit_aim_strict = false
local best_unit_aim_lenient = false
local best_unit_aim_outer = false
local best_unit_x_diff_edge = math.huge
local best_unit_y_diff_edge = math.huge
for i = 1, num_hits do
local hit = hits[i]
local hit_actor = hit[INDEX_ACTOR]
if not hit_actor then
goto continue
end
local hit_unit = Actor_unit(hit_actor)
local hit_position = hit[INDEX_POSITION]
if hit_unit == player_unit or not HEALTH_ALIVE[hit_unit] then
goto continue
end
local unit_data_extension = ScriptUnit_extension(hit_unit, "unit_data_system")
local breed_data = unit_data_extension and unit_data_extension._breed
-- ignore untaggable unit
if not breed_data or breed_data.smart_tag_target_type ~= "breed" or breed_data.faction_name == "imperium" or breed_data.unit_template_name ~= "minion" then
goto continue
end
local hit_unit_pose, _ = Unit_box(hit_unit, true)
local object_right = Matrix4x4_right(hit_unit_pose)
local object_forward = Matrix4x4_forward(hit_unit_pose)
local enemy_extents = ENEMY_EXTENTS[breed_data.name] or EMPTY_TABLE
local half_extent_right = enemy_extents.half_extent_right or breed_data.half_extent_right or 0.3
local half_extent_forward = enemy_extents.half_extent_forward or breed_data.half_extent_forward or 0.3
local world_extents_right = object_right * half_extent_right
local world_extents_forward = object_forward * half_extent_forward
local hit_unit_center_pos = Actor_world_bounds(hit_actor)
local to_hit_unit_center = hit_unit_center_pos - ray_origin
local to_hit_unit_center_direction = Vector3_normalize(hit_unit_center_pos - ray_origin)
local half_length = math_max(
math_abs(Vector3_dot(to_hit_unit_center_direction, world_extents_right + world_extents_forward)),
math_abs(Vector3_dot(to_hit_unit_center_direction, world_extents_right - world_extents_forward))
)
local hit_unit_distance = Vector3_length(to_hit_unit_center) - half_length
if hit_unit_distance > max_range then
goto continue
end
local hit_dot = Vector3_dot(forward, to_hit_unit_center_direction)
if hit_dot < MIN_COSINE then
goto continue
end
local hit_offset = hit_position - hit_unit_center_pos
local x_diff = math_abs(Vector3_dot(hit_offset, right))
local y_diff = math_abs(Vector3_dot(hit_offset, up))
local half_width = math_max(
math_abs(Vector3_dot(right, world_extents_right + world_extents_forward)),
math_abs(Vector3_dot(right, world_extents_right - world_extents_forward))
)
local half_height = Breed_height(hit_unit, breed_data) * 0.5
local x_diff_edge = x_diff - half_width
local y_diff_edge = y_diff - half_height
local hit_unit_aim_outer = x_diff_edge <= 0.8 and y_diff_edge <= 0.8
if not hit_unit_aim_outer then
goto continue
end
local hit_unit_aim_strict = x_diff <= half_width and y_diff <= half_height
if hit_unit_distance > 3.5 then
if not hit_unit_aim_strict then
goto continue
end
if best_unit_aim_lenient then
if best_unit_distance <= 3.5 then
goto continue
end
elseif best_unit_aim_outer then
if best_unit_distance <= 2.5 then
goto continue
end
end
end
local hit_unit_aim_lenient = x_diff_edge <= 0.4 and y_diff_edge <= 0.4
local distance_diff = math_abs(hit_unit_distance - best_unit_distance)
if best_unit_aim_strict then
if best_unit_distance > 3.5 then
if hit_unit_aim_strict then
if hit_unit_distance > 3.5 then
if distance_diff <= DISTANCE_EPSILON then
if hit_dot <= best_unit_dot then
goto continue
end
elseif hit_unit_distance > best_unit_distance then
goto continue
end
end
elseif hit_unit_aim_lenient then
-- Nothing
elseif hit_unit_aim_outer then
if hit_unit_distance > 2.5 then
goto continue
end
end
else
if distance_diff <= DISTANCE_EPSILON then
if hit_unit_aim_strict then
if hit_dot <= best_unit_dot then
goto continue
end
else
goto continue
end
elseif hit_unit_distance < best_unit_distance then
if not hit_unit_aim_lenient then
goto continue
end
elseif hit_unit_distance > best_unit_distance then
goto continue
end
end
elseif best_unit_aim_lenient then
if distance_diff <= DISTANCE_EPSILON then
if hit_unit_aim_strict then
-- Nothing
elseif hit_unit_aim_lenient then
if x_diff_edge >= best_unit_x_diff_edge and y_diff_edge >= best_unit_y_diff_edge then
goto continue
end
if hit_dot <= best_unit_dot then
goto continue
end
elseif hit_unit_aim_outer then
goto continue
end
elseif hit_unit_distance < best_unit_distance then
-- Nothing
elseif hit_unit_distance > best_unit_distance then
goto continue
end
elseif best_unit_aim_outer then
if distance_diff <= DISTANCE_EPSILON then
if hit_unit_aim_lenient then
-- Nothing
elseif hit_unit_aim_outer then
if x_diff_edge >= best_unit_x_diff_edge and y_diff_edge >= best_unit_y_diff_edge then
goto continue
end
if hit_dot <= best_unit_dot then
goto continue
end
end
elseif hit_unit_distance < best_unit_distance then
-- Nothing
elseif hit_unit_distance > best_unit_distance then
if not hit_unit_aim_strict then
goto continue
end
end
end
if not is_target_visible(ray_origin, up, hit_unit_center_pos, half_height, hit_unit, fixed_frame) then
goto continue
end
best_unit = hit_unit
best_unit_dot = hit_dot
best_unit_distance = hit_unit_distance
best_unit_aim_strict = hit_unit_aim_strict
best_unit_aim_lenient = hit_unit_aim_lenient
best_unit_aim_outer = hit_unit_aim_outer
best_unit_x_diff_edge = x_diff_edge
best_unit_y_diff_edge = y_diff_edge
::continue::
end
if best_unit then
local best_unit_tag = smart_tag_system:unit_tag(best_unit)
return best_unit, best_unit_tag
end
end
mod:hook_safe(CLASS.PrecisionTargetFinder, "init",
function(self, is_server, is_local_unit, player, physics_world, unit)
visibility_raycast_object = PhysicsWorld_make_raycast(physics_world, "closest", "types", "both", "collision_filter", "filter_minion_line_of_sight_check")
end)