-- Monster Target Indicator(仇恨连线)—— 每只大怪用红线连接当前锁定的猎人 / 随从猎人 / 猫狗。 -- 数据:snow.enemy.EnemyCharacterBase.getCurrentTargetCharacter()。招式之间目标为空时保留 1.5 秒,换目标立即切线。 -- 目标与世界坐标在 PlayerManager.update 前置钩子内读取;re.on_frame 只投影、绘制坐标快照。 -- 使用 REFramework 背景绘图列表(旧版回退到 draw),不依赖 d2d,也不修改游戏内的名字。 local CONFIG_FILE = "TargetLines.json" local config = { enabled = true } -- 菜单只有一个开关(2026-10-31 用户:"只需要 On/off 就行,英文版") local hasCfg = false pcall(function() hasCfg = fs.glob("TargetLines%.json$")[1] ~= nil end) if hasCfg then pcall(function() local c = json.load_file(CONFIG_FILE) if type(c) == "table" then for k, v in pairs(c) do if config[k] ~= nil then config[k] = v end end end end) end local function saveConfig() pcall(json.dump_file, CONFIG_FILE, config) end if not hasCfg then saveConfig() end -- 第一次启动写一份默认配置(免得日志里每次报"读不到") local HOLD = 1.5 local MAX_TARGET_DISTANCE = 50.0 local MONSTER_Y = 1.0 local PLAYER_Y = 0.9 local OTOMO_Y = 0.5 -- 从外到内叠加静态荧光;线宽以 1080p 为基准,颜色使用 ABGR。 local LINE_LAYERS = { { width = 7, color = 0x183030FF }, { width = 5, color = 0x383030FF }, { width = 3, color = 0xFF3030FF }, { width = 1, color = 0xFFD0D0FF }, } local HEAD_NAMES = { "Head_00", "Head", "Head_01", "head" } local lines = {} -- 仅保存复制后的世界坐标,不包含游戏对象 local bossLast = {} -- 怪物地址 → { target = 托管对象引用, kind, t } local headCache = {} -- 怪物地址 → { mesh, joint };joint = false 表示没有找到骨骼 local kindCache = {} local lastStatus local diag = { hook = false, err = nil } local function clearState() lines, bossLast, headCache, kindCache = {}, {}, {}, {} lastStatus = nil end local function kindOf(ch) local addr = ch:get_address() local kind = kindCache[addr] if kind == nil then local name = ch:get_type_definition():get_full_name() if name:find("^snow%.player%.") then kind = "player" elseif name:find("^snow%.otomo%.") then kind = "otomo" else kind = false end kindCache[addr] = kind end return kind end local function offsetPosition(pos, height) if not pos then return nil end return Vector3f.new(pos.x, pos.y + height, pos.z) end local function monsterPosition(enemy, addr, newHeads, rootPos) -- 头部骨骼的查找或读取失败不影响连线,改用怪物根位置。 local ok, pos = pcall(function() local mesh = enemy:call("get_RefMesh") if not mesh then return nil end local rec = headCache[addr] if not rec or rec.mesh:get_address() ~= mesh:get_address() then local go = mesh:call("get_GameObject") local transform = go and go:call("get_Transform") local joint if transform then for _, name in ipairs(HEAD_NAMES) do joint = transform:call("getJointByName", name) if joint then break end end end rec = { mesh = mesh, joint = joint or false } end newHeads[addr] = rec if rec.joint and sdk.is_managed_object(rec.joint) then return rec.joint:call("get_Position") end end) if ok and pos then return offsetPosition(pos, 0) end return offsetPosition(rootPos, MONSTER_Y) end local function collectBoss(enemy, addr, now, newHeads) if enemy:call("checkDie") == true or enemy:call("get_IsCombatMode") ~= true then return end local last = bossLast[addr] local target = enemy:call("getCurrentTargetCharacter") local hasCurrentTarget = target ~= nil local kind if target then if not sdk.is_managed_object(target) then return end kind = kindOf(target) if not kind then return end -- 明确锁定其他对象时,不保留上一目标 elseif not last or now - last.t >= HOLD then return else target, kind = last.target, last.kind end -- 当前目标和缓冲目标每帧都用原始根位置检查距离,超距清理优先于 1.5 秒缓冲。 -- 不返回 last 即同时丢弃该怪物的连线与缓冲;读取抛错由外层逐怪 pcall 隔离。 if not sdk.is_managed_object(target) then return end local rootPos = enemy:call("get_Pos") local targetPos = target:call("get_Pos") if not rootPos or not targetPos then return end local dx, dy, dz = rootPos.x - targetPos.x, rootPos.y - targetPos.y, rootPos.z - targetPos.z if dx * dx + dy * dy + dz * dz > MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE then return end -- 仅游戏返回的有效目标能建立或刷新缓冲;沿用旧目标时不延长缓冲时间。 if hasCurrentTarget then last = { target = target, kind = kind, t = now } end local to = offsetPosition(targetPos, kind == "otomo" and OTOMO_Y or PLAYER_Y) local from = monsterPosition(enemy, addr, newHeads, rootPos) if not from then return end return last, { from = from, to = to } end -- ===== 每帧采集(PlayerManager.update 前置钩子)===== local function tick() if not config.enabled then clearState(); return end local em = sdk.get_managed_singleton("snow.enemy.EnemyManager") local pm = sdk.get_managed_singleton("snow.player.PlayerManager") local qm = sdk.get_managed_singleton("snow.QuestManager") if not em or not pm or not qm then clearState(); return end local status = qm:get_field("_QuestStatus") if status ~= lastStatus then clearState() lastStatus = status end local now = os.clock() local newLines, newLast, newHeads = {}, {}, {} for i = 0, (em:call("getBossEnemyCount") or 0) - 1 do local enemy = em:call("getBossEnemy", i) if enemy then local addr = enemy:get_address() local ok, last, line = pcall(collectBoss, enemy, addr, now, newHeads) if ok then newLast[addr] = last if line then newLines[#newLines + 1] = line end else -- 单只怪读取失败时丢弃它的旧状态,其他怪照常更新。 newHeads[addr] = nil diag.err = tostring(last) end end end bossLast, headCache, lines = newLast, newHeads, newLines end do local ok, err = pcall(function() local td = sdk.find_type_definition("snow.player.PlayerManager") local method = td and td:get_method("update") if not method then error("PlayerManager.update is unavailable") end sdk.hook(method, function() local updated, updateErr = pcall(tick) if not updated then clearState() diag.err = tostring(updateErr) end end, function(retval) return retval end) end) diag.hook = ok if not ok then diag.err = tostring(err) end end re.on_script_reset(clearState) -- ===== 内置绘图:只读取世界坐标快照 ===== local function backgroundDrawList() if type(imgui.get_background_draw_list) ~= "function" then return nil end local ok, list = pcall(function() local drawList = imgui.get_background_draw_list() if drawList and type(drawList.add_line) == "function" then return drawList end end) if ok then return list end end local function drawGlowLine(from, to, drawList, scale) local dx, dy = to.x - from.x, to.y - from.y local lengthSquared = dx * dx + dy * dy if lengthSquared == 0 then return end if drawList then for _, layer in ipairs(LINE_LAYERS) do drawList:add_line(from, to, layer.color, layer.width * scale) end return end -- 旧版没有背景绘图接口时,以四边形和端点圆形模拟同样的分层粗线。 local length = math.sqrt(lengthSquared) local nx, ny = -dy / length, dx / length for _, layer in ipairs(LINE_LAYERS) do local radius = layer.width * scale * 0.5 local ox, oy = nx * radius, ny * radius draw.filled_quad(from.x + ox, from.y + oy, from.x - ox, from.y - oy, to.x - ox, to.y - oy, to.x + ox, to.y + oy, layer.color) draw.filled_circle(from.x, from.y, radius, layer.color, 16) draw.filled_circle(to.x, to.y, radius, layer.color, 16) end end re.on_frame(function() if not config.enabled then return end local snapshot = lines local ok, err = pcall(function() local display = imgui.get_display_size() if not display or display.y <= 0 then return end local scale = display.y / 1080.0 local drawList = backgroundDrawList() for _, line in ipairs(snapshot) do local from = draw.world_to_screen(line.from) local to = draw.world_to_screen(line.to) if from and to then drawGlowLine(from, to, drawList, scale) end end end) if not ok then lines = {} diag.err = tostring(err) end end) -- ===== 菜单 ===== re.on_draw_ui(function() if not imgui.tree_node("Monster Target Indicator") then return end local changed, enabled = imgui.checkbox("Enabled", config.enabled) if changed then config.enabled = enabled clearState() saveConfig() end if not diag.hook then imgui.text("Hook failed (different game version?)") end if diag.err then imgui.text("Error: " .. diag.err) end imgui.tree_pop() end)