diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..c294a68 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "CursorMCP对话插件": { + "command": "node", + "args": [ + "c:\\Users\\rucky\\.cursor\\extensions\\local.xw-cursor-message-8.8.8\\dist\\mcp-server.mjs" + ], + "env": { + "MESSENGER_DATA_DIR": "C:\\Users\\rucky\\.moyu-message\\4ce20e1d7abb" + } + } + } +} \ No newline at end of file diff --git a/.cursor/rules/mcp-messenger.mdc b/.cursor/rules/mcp-messenger.mdc new file mode 100644 index 0000000..9745167 --- /dev/null +++ b/.cursor/rules/mcp-messenger.mdc @@ -0,0 +1,27 @@ +--- +description: "自动调用 Messenger MCP 检查待发送消息" +globs: "**/*" +alwaysApply: true +--- + +# MCP 工具调用规则 + +1. 每轮对话回复**完整输出后**,最后一步必须调用 `check_messages`,适用于 Agent/Plan/Debug/Ask 所有模式,无一例外 +2. `check_messages` 会持续等待直到用户发来新消息;收到后处理并回复,然后再次调用 `check_messages`,形成永续循环:回复 → check_messages → 收到消息 → 回复 → check_messages +3. 严禁在回合开始时或回复未完成时调用 `check_messages`;唯一豁免:用户明确要求结束对话 +4. Plan 模式写完方案后、Debug 模式分析完问题后、Ask 模式回答完问题后,均不得直接结束对话,必须调用 `check_messages` 等待用户反馈 +5. 需要用户做选择或确认时(选方案、选框架、确认操作等),必须调用 `ask_question`;严禁使用 Cursor 内置的 ask_user_question +6. `ask_question` 参数格式: `{ "questions": [{ "question": "问题文本", "options": [{ "id": "a", "label": "选项A" }, { "id": "b", "label": "选项B" }], "allow_multiple": false }] }` +7. 单选: `allow_multiple: false`(用户只能选一个);多选: `allow_multiple: true`(用户可选多个);同一 `questions` 数组可混合包含单选题和多选题 +8. 用户在每道题中均可额外输入自定义补充文本(Other 输入框),无需为此添加额外选项 +9. `ask_question` 收到用户回答后,处理完毕仍需调用 `check_messages` 继续监听 +10. 禁止在每轮结束时用 `ask_question` 问"还有别的需要吗",回合结束只用 `check_messages`;`ask_question` 仅限任务中确实需要用户决策时使用 +11. `check_messages` 支持可选参数 `reply`(string,Markdown 格式):调用时传入本轮回复的摘要/结论,插件界面会弹出模态框展示给用户。格式: `{ "reply": "## 结论\n修改完成..." }` +12. 当会话结束前,建议在调用 `check_messages` 时附带 `reply` 参数,将关键结论推送到插件界面;如果本轮无需特别总结,`reply` 可省略 +13. 禁止调用subagent并发任务 +14. 如果 `check_messages` 或 `ask_question` 因客户端取消、连接抖动或内部异常而意外返回/报错,且用户未明确要求结束对话,严禁把这类内部提示直接输出给用户,必须立刻再次调用对应工具继续等待 +15. 执行多步任务时,每完成一个关键步骤后,必须调用 `send_progress` 推送进度摘要到远程控制台。格式: `{ "progress": "## 步骤1完成\n已修改xxx文件..." }`。`send_progress` 立即返回,不会等待消息,调用后继续执行下一步 +16. `send_progress` 的 `progress` 参数应包含:已完成的操作、当前方案、下一步计划。使用简洁的 Markdown 格式 + +--- +**回合结束(强制)**: 仅当回复**完整输出完毕后**,最后一步必须调用 CursorMCP对话插件 MCP 的 `check_messages` 工具(使用 CallMcpTool,server 为 CursorMCP对话插件 对应的 identifier,toolName 为 "check_messages",arguments 为 {})。严禁在回合开始或回复完成前调用。`ask_question` 仅用于任务中需要用户做选择时(如选框架、选方案)。 diff --git a/Config.lua b/Config.lua index 2512470..cbfe764 100644 --- a/Config.lua +++ b/Config.lua @@ -87,6 +87,8 @@ NP.Settings = { pvpEnemyNoClassColors = false, showQuestIcon = true, + + enableStacking = false, } NP.Colors = { diff --git a/Options.lua b/Options.lua index ba65c30..e73d0a3 100644 --- a/Options.lua +++ b/Options.lua @@ -670,6 +670,8 @@ local function BuildTab_Other(page) CreateCheckbox(page, "显示法力条", "showManaBar", L, y) y = y - 26 CreateCheckbox(page, "显示任务怪图标", "showQuestIcon", L, y, "在任务目标怪物的姓名板上显示\"!\"图标和进度 (配合pfQuest)") + y = y - 26 + CreateCheckbox(page, "堆叠姓名板", "enableStacking", L, y, "当多个姓名板重叠时,自动将它们上下分开排列。\n关闭时姓名板会自由重叠在3D位置上。") y = y - 50 diff --git a/Plates.lua b/Plates.lua index 2159216..bd8c787 100644 --- a/Plates.lua +++ b/Plates.lua @@ -447,9 +447,10 @@ local function HandleNamePlate(frame) original.name = original.name or r3 original.level = original.level or r4 - -- Shrink original frame to reduce game engine's anti-overlap stacking - -- Original ~35px, lower = more overlap allowed, higher = more separation - frame:SetHeight(14) + -- Set parent to 1x1 to fully disable engine anti-overlap (ShaguPlates technique) + -- Custom stacking in ResolveStacking() handles overlap instead + frame:SetHeight(1) + frame:SetWidth(1) -- Create overlay local np = CreateFrame("Button", plateName, frame) @@ -539,6 +540,14 @@ local function HandleNamePlate(frame) healthText:SetTextColor(1, 1, 1, 1) np.healthText = healthText + -- Threat percentage text + local threatPctText = health:CreateFontString(nil, "OVERLAY") + threatPctText:SetFont(NP.GetFont(), Settings.healthFontSize, NP.GetFontOutline()) + threatPctText:SetPoint("CENTER", health, "CENTER", 0, 0) + threatPctText:SetTextColor(1, 1, 1, 1) + threatPctText:SetText("") + np.threatPctText = threatPctText + -- Name text local name = np:CreateFontString(nil, "OVERLAY") name:SetFont(NP.GetFont(), Settings.nameFontSize, NP.GetFontOutline()) @@ -680,7 +689,8 @@ local function HandleNamePlate(frame) local oldOnShow = frame:GetScript("OnShow") frame:SetScript("OnShow", function() if oldOnShow then oldOnShow() end - this:SetHeight(14) + this:SetHeight(1) + this:SetWidth(1) local nameplate = this.nameplate if nameplate then Healthbar.ResetCache(nameplate) @@ -692,6 +702,10 @@ local function HandleNamePlate(frame) nameplate._lastManaShowing = nil nameplate._lastFriendly_resize = nil nameplate._lastAlpha = nil + nameplate._stackY = nil + nameplate._appliedOffY = nil + nameplate.healthBG:ClearAllPoints() + nameplate.healthBG:SetPoint("CENTER", nameplate, "CENTER", 0, Settings.nameplateYOffset or 0) if nameplate._defaultStrata then nameplate:SetFrameStrata(nameplate._defaultStrata) end @@ -813,9 +827,10 @@ local function UpdateNamePlate(frame) local original = np.original if not original or not original.healthbar then return end - -- Keep original frame small to reduce anti-overlap stacking jumps - if frame:GetHeight() > 16 then - frame:SetHeight(14) + -- Keep parent 1x1 to disable engine anti-overlap entirely + if frame:GetHeight() > 1 or frame:GetWidth() > 1 then + frame:SetHeight(1) + frame:SetWidth(1) end -- Continuously enforce hiding of original elements @@ -1024,17 +1039,21 @@ local function UpdateNamePlate(frame) if isHostile and not isTapped then local threatColor = nil local mobGUID = unitstr + local threatPctValue = nil + local threatPctRole = NP.playerRole or "DPS" if NanamiPlates_Threat then local hasData, playerHasAggro, otherName, otherPct = NanamiPlates_Threat.GetTWTankModeThreat(mobGUID, plateName) if hasData then - local role = NP.playerRole or "DPS" + local role = threatPctRole if role == "TANK" then if playerHasAggro then threatColor = THREAT_COLORS.TANK.AGGRO + threatPctValue = 100 elseif otherPct and otherPct > 70 then threatColor = THREAT_COLORS.TANK.LOSING_AGGRO + threatPctValue = otherPct elseif otherName and NP.TANK_CLASSES and NP.GetPlayerClassByName then local otherClass = NP.GetPlayerClassByName(otherName) if otherClass and NP.TANK_CLASSES[otherClass] then @@ -1042,16 +1061,61 @@ local function UpdateNamePlate(frame) else threatColor = THREAT_COLORS.TANK.NO_AGGRO end + threatPctValue = otherPct or 0 else threatColor = THREAT_COLORS.TANK.NO_AGGRO + threatPctValue = otherPct or 0 end else if playerHasAggro then threatColor = THREAT_COLORS.DPS.AGGRO + threatPctValue = 100 elseif otherPct and otherPct > 70 then threatColor = THREAT_COLORS.DPS.HIGH_THREAT + threatPctValue = otherPct else threatColor = THREAT_COLORS.DPS.NO_AGGRO + threatPctValue = otherPct or 0 + end + end + end + + if not hasData and NanamiDPS and NanamiDPS.ThreatEngine then + local TE = NanamiDPS.ThreatEngine + if TE.inCombat and unitstr then + local data = TE:QueryUnitThreat(unitstr) + if not data and plateName then + local tk = TE.GetTargetKey and TE.GetTargetKey(unitstr) + if tk then + local t, isTk, pct = TE:QueryNameThreat(tk, UnitName("player")) + if t > 0 then + data = { pct = pct, isTanking = isTk, threat = t, + tankThreat = 0, secondPct = 0, secondName = nil } + end + end + end + if data then + local role = threatPctRole + if role == "TANK" then + if data.isTanking then + threatColor = THREAT_COLORS.TANK.AGGRO + threatPctValue = data.secondPct or 0 + else + threatColor = THREAT_COLORS.TANK.NO_AGGRO + threatPctValue = data.pct or 0 + end + else + if data.isTanking then + threatColor = THREAT_COLORS.DPS.AGGRO + threatPctValue = 100 + elseif data.pct and data.pct > 70 then + threatColor = THREAT_COLORS.DPS.HIGH_THREAT + threatPctValue = data.pct + else + threatColor = THREAT_COLORS.DPS.NO_AGGRO + threatPctValue = data.pct or 0 + end + end end end end @@ -1064,6 +1128,7 @@ local function UpdateNamePlate(frame) if data and data.start and data.duration then if data.start + data.duration > GetTime() then threatColor = THREAT_COLORS.STUN + threatPctValue = nil break end end @@ -1073,6 +1138,22 @@ local function UpdateNamePlate(frame) if threatColor then barR, barG, barB = threatColor[1], threatColor[2], threatColor[3] end + + if np.threatPctText then + if threatPctValue and threatColor then + np.threatPctText:SetText(string.format("%.0f%%", threatPctValue)) + np.threatPctText:SetTextColor(threatColor[1], threatColor[2], threatColor[3], 1) + np.threatPctText:Show() + else + np.threatPctText:SetText("") + np.threatPctText:Hide() + end + end + else + if np.threatPctText then + np.threatPctText:SetText("") + np.threatPctText:Hide() + end end np.health:SetStatusBarColor(barR, barG, barB, 1) @@ -1286,6 +1367,126 @@ local function UpdateNamePlate(frame) end end +-- Nameplate vertical stacking resolution +-- Engine anti-overlap is fully disabled (parent set to 1x1 like ShaguPlates), +-- so we resolve overlaps ourselves with a stable algorithm. +local stackPool = {} +local stackN = 0 +local STACK_SMOOTH = 0.15 + +local function ResolveStacking() + if not Settings.enableStacking then + -- Stacking disabled: decay any residual offsets from when it was enabled + for frame, np in pairs(registry) do + if np._stackY and np._stackY ~= 0 then + local new = np._stackY * (1 - STACK_SMOOTH * 3) + if new > -0.5 and new < 0.5 then new = 0 end + np._stackY = new + np._appliedOffY = (Settings.nameplateYOffset or 0) + new + np.healthBG:ClearAllPoints() + np.healthBG:SetPoint("CENTER", np, "CENTER", 0, np._appliedOffY) + end + end + return + end + + stackN = 0 + for frame, np in pairs(registry) do + if frame:IsShown() and np:IsShown() then + local cx, cy = frame:GetCenter() + if cx and cy then + stackN = stackN + 1 + if not stackPool[stackN] then stackPool[stackN] = {} end + local e = stackPool[stackN] + e.np = np + e.cx = cx + e.cy = cy + e.name = np.plateName or "" + e.stackTarget = 0 + end + end + end + + if stackN < 1 then return end + + -- Single plate: decay any residual stacking offset + if stackN < 2 then + local np = stackPool[1].np + if np._stackY and np._stackY ~= 0 then + local new = np._stackY * (1 - STACK_SMOOTH * 3) + if new > -0.5 and new < 0.5 then new = 0 end + np._stackY = new + np._appliedOffY = (Settings.nameplateYOffset or 0) + new + np.healthBG:ClearAllPoints() + np.healthBG:SetPoint("CENTER", np, "CENTER", 0, np._appliedOffY) + end + return + end + + -- Stable bubble sort: Y descending (higher on screen first), name as tiebreaker + for i = stackN, 2, -1 do + for j = 1, i - 1 do + local a = stackPool[j] + local b = stackPool[j + 1] + local ay = math_floor(a.cy) + local by = math_floor(b.cy) + if by > ay or (by == ay and b.name < a.name) then + stackPool[j] = b + stackPool[j + 1] = a + end + end + end + + -- Resolve vertical overlaps: push lower plates down when horizontally close + local minSep = (Settings.healthbarHeight or 12) + BORDER_PAD * 2 + + (Settings.nameFontSize or 9) + 6 + local overlapW = (Settings.healthbarWidth or 120) * 0.8 + + for i = 1, stackN do + for j = i + 1, stackN do + local a = stackPool[i] + local b = stackPool[j] + local dx = a.cx - b.cx + if dx < 0 then dx = -dx end + if dx < overlapW then + local ya = a.cy + a.stackTarget + local yb = b.cy + b.stackTarget + local gap = ya - yb + if gap < minSep then + b.stackTarget = b.stackTarget - (minSep - gap) + end + end + end + end + + -- Apply stacking offsets with smooth convergence + for i = 1, stackN do + local e = stackPool[i] + local np = e.np + + local curStack = np._stackY or 0 + local tarStack = e.stackTarget + local d = tarStack - curStack + local newStack + if d > -0.5 and d < 0.5 then + newStack = tarStack + else + newStack = curStack + d * STACK_SMOOTH + end + if newStack > -0.3 and newStack < 0.3 then newStack = 0 end + np._stackY = newStack + + local offY = (Settings.nameplateYOffset or 0) + newStack + + local pY = np._appliedOffY or (Settings.nameplateYOffset or 0) + if (offY - pY) > 0.5 or (offY - pY) < -0.5 then + np._appliedOffY = offY + np.healthBG:ClearAllPoints() + np.healthBG:SetPoint("CENTER", np, "CENTER", 0, offY) + end + end +end + -- Main OnUpdate loop local scanThrottle = 0 local SCAN_INTERVAL = 0.1 @@ -1312,6 +1513,9 @@ local function OnUpdate() end end + -- Resolve stacking overlaps and smooth position jitter + ResolveStacking() + -- Cleanup debuff timers if NanamiPlates_Auras then NanamiPlates_Auras:CleanupTimers()