diff --git a/Fuyutsui/core/block.lua b/Fuyutsui/core/block.lua index c225a260..7a747473 100644 --- a/Fuyutsui/core/block.lua +++ b/Fuyutsui/core/block.lua @@ -608,6 +608,16 @@ local function SetupPermanentAuraPixel(button, index) bg:SetColorTexture(r, g, 1, 1) end +--- 职责覆盖槽:光环存在时保留职责像素的索引通道,并把数值通道覆盖为 0。 +local function SetupZeroAuraPixel(button, index) + AnchorAuraPixelButton(button, index) + button:SetFrameLevel((button:GetFrameLevel() or 0) + 1) + local bg = button:CreateTexture(nil, "ARTWORK") + bg:SetAllPoints(button) + local r, g = EncodeBlockChannels(index) + bg:SetColorTexture(r, g, 0, 1) +end + --- 限时光环槽:底层 b=0,█ 用剩余时间曲线;叠在永久槽之上 local function SetupTimedAuraDuration(button, index) AnchorAuraPixelButton(button, index) @@ -646,6 +656,12 @@ local function MakeTimedSlotInitializer(index) end end +local function MakeZeroSlotInitializer(index) + return function(button) + SetupZeroAuraPixel(button, index) + end +end + local function AddDurationAuraSlotPair(container, slotKeyPrefix, filter, includeSpellIDs, index) -- 先限时后永久:若容器对 auraInstance 互斥分配,避免限时光环被永久槽抢走。 -- 限时槽:maxDuration 排除永久;底层 b=0 + █ 曲线 @@ -1120,7 +1136,9 @@ end 驱散蓝通道:Magic=1 Curse=2 Disease=3 Poison=4 Bleed=11(/255) ============================================================================]] -local groupAuraContainers = {} -- [memberIndex] = AuraContainer +local GROUP_ROLE_ZERO_AURA_ID = 27827 +local groupAuraContainers = {} -- [memberIndex] = AuraContainer +local groupRoleOverlayContainers = {} -- [memberIndex] = AuraContainer local function CollectGroupAuraDefs(auraTable) local defs = {} @@ -1173,6 +1191,39 @@ function Fuyutsui:ReleaseGroupAuraContainers() ReleaseFrame(container) groupAuraContainers[memberIndex] = nil end + for memberIndex, container in pairs(groupRoleOverlayContainers) do + ReleaseFrame(container) + groupRoleOverlayContainers[memberIndex] = nil + end +end + +local function CreateGroupMemberRoleOverlayContainer(memberIndex, groups) + EnsureAuraContainerLoaded() + + local container = CreateFrame("AuraContainer", "FuyutsuiGroupRoleOverlaySlots_" .. memberIndex, UIParent, + "CustomAuraContainerTemplate") + container:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0) + container:SetEnabled(true) + container:SetFrameStrata(AURA_DURATION_STRATA) + container:SetFrameLevel(AURA_DURATION_LEVEL + 1) + + local pixelIndex = GroupAuraPixelIndex(groups, memberIndex, groups.role) + local slotKey = "group_" .. memberIndex .. "_role_zero" + local includeSpellIDs = { [GROUP_ROLE_ZERO_AURA_ID] = true } + container:AddAuraSlot(slotKey, "HELPFUL", { + candidateFilters = AuraSlotFilters(includeSpellIDs), + sortMethod = AuraContainerSortMethod.Expiration, + sortDirection = AuraContainerSortDirection.Normal, + initializeFrame = MakeZeroSlotInitializer(pixelIndex), + }) + container.fuyutsuiSpellIdSlots = { + { + key = slotKey, + includeSpellIDs = includeSpellIDs, + }, + } + + return container end local function CreateGroupMemberAuraContainer(memberIndex, groups, auraDefs, includeDispelTypes) @@ -1237,34 +1288,57 @@ function Fuyutsui:RefreshGroupAuraContainers() local auraDefs = CollectGroupAuraDefs(groups.aura) local includeDispelTypes = groups.dispel and CopyIncludeDispelTypes() or nil - if #auraDefs == 0 and not includeDispelTypes then + local needsAuraContainer = #auraDefs > 0 or includeDispelTypes ~= nil + local needsRoleOverlay = groups.role ~= nil + if not needsAuraContainer and not needsRoleOverlay then self:ReleaseGroupAuraContainers() return end local groupList = Fuyutsui.groupList or {} local group = Fuyutsui.group or {} - local used = {} + local usedAuraContainers = {} + local usedRoleOverlayContainers = {} for _, unit in ipairs(groupList) do local obj = group[unit] if obj and obj.index then local memberIndex = obj.index - used[memberIndex] = true - local container = groupAuraContainers[memberIndex] - if not container then - container = CreateGroupMemberAuraContainer(memberIndex, groups, auraDefs, includeDispelTypes) - groupAuraContainers[memberIndex] = container + if needsAuraContainer then + usedAuraContainers[memberIndex] = true + local container = groupAuraContainers[memberIndex] + if not container then + container = CreateGroupMemberAuraContainer(memberIndex, groups, auraDefs, includeDispelTypes) + groupAuraContainers[memberIndex] = container + end + container.fuyutsuiUnit = unit + container:SetUnit(unit) + container:SetEnabled(true) + container:Show() + end + if needsRoleOverlay then + usedRoleOverlayContainers[memberIndex] = true + local roleContainer = groupRoleOverlayContainers[memberIndex] + if not roleContainer then + roleContainer = CreateGroupMemberRoleOverlayContainer(memberIndex, groups) + groupRoleOverlayContainers[memberIndex] = roleContainer + end + roleContainer.fuyutsuiUnit = unit + RebindContainerSpellFilters(roleContainer, unit) + roleContainer:SetEnabled(true) + roleContainer:Show() end - container.fuyutsuiUnit = unit - container:SetUnit(unit) - container:SetEnabled(true) - container:Show() end end for memberIndex, container in pairs(groupAuraContainers) do - if not used[memberIndex] then + if not usedAuraContainers[memberIndex] then + container:SetEnabled(false) + container:Hide() + end + end + for memberIndex, container in pairs(groupRoleOverlayContainers) do + if not usedRoleOverlayContainers[memberIndex] then container:SetEnabled(false) container:Hide() end @@ -1317,4 +1391,14 @@ function Fuyutsui:RebindAuraSpellFilters() RebindContainerSpellFilters(container, container.fuyutsuiUnit) end + local roleOverlayIndices = {} + for memberIndex in pairs(groupRoleOverlayContainers) do + tinsert(roleOverlayIndices, memberIndex) + end + table.sort(roleOverlayIndices) + for _, memberIndex in ipairs(roleOverlayIndices) do + local container = groupRoleOverlayContainers[memberIndex] + RebindContainerSpellFilters(container, container.fuyutsuiUnit) + end + end diff --git a/Fuyutsui/core/group.lua b/Fuyutsui/core/group.lua index 9cdcd56b..ce619b33 100644 --- a/Fuyutsui/core/group.lua +++ b/Fuyutsui/core/group.lua @@ -6,6 +6,7 @@ local state = Fuyutsui.state local roleMap = Fuyutsui.roleMap local ColorValue0 = CreateColor(0, 0, 0, 1) local updateIndex = 1 +local GROUP_MAX_MEMBERS = 40 -- 施法治疗预估偏移(近似秒数/权重,写入生命曲线) local helpfulSpells = { @@ -60,30 +61,39 @@ function Fuyutsui:UpdateGroupInRangeAndHealth() local groupList = self.groupList if not blocks or not blocks.groups then return end local numUnits = #groupList - if numUnits >= 1 then - local unit = groupList[updateIndex] - local obj = group[unit] - if not obj then return end - self:UpdateUnitHealthInfo(unit) - local index = blocks.groups.start + (obj.index - 1) * blocks.groups.num + blocks.groups.role - obj.isDead = UnitIsDeadOrGhost(unit) - obj.canAssist = UnitCanAssist("player", unit) - obj.valid = not obj.isDead and obj.canAssist and obj.inSight - if obj.valid then - local inRange = UnitIsUnit(unit, "player") and true or UnitInRange(unit) - local roleValue = roleMap[obj.role] and roleMap[obj.role] / 255 or 5 / 255 - local trueValue = CreateColor(0, 0, roleValue, 1) - local booleanValue = EvaluateColorFromBoolean(inRange, trueValue, ColorValue0) - local _, _, b = booleanValue:GetRGB() - self:CreateTexture(index, b) - else - self:CreateTexture(index, 0) - end - updateIndex = updateIndex + 1 - if updateIndex > numUnits then - updateIndex = 1 - end + if numUnits < 1 then + updateIndex = 1 + return end + + if updateIndex < 1 or updateIndex > numUnits then + updateIndex = 1 + end + + local unit = groupList[updateIndex] + local obj = unit and group[unit] or nil + if not obj then + updateIndex = 1 + return + end + + self:UpdateUnitHealthInfo(unit) + local index = blocks.groups.start + (obj.index - 1) * blocks.groups.num + blocks.groups.role + obj.isDead = UnitIsDeadOrGhost(unit) + obj.canAssist = UnitCanAssist("player", unit) + obj.valid = not obj.isDead and obj.canAssist and obj.inSight + if obj.valid then + local inRange = UnitIsUnit(unit, "player") and true or UnitInRange(unit) + local roleValue = roleMap[obj.role] and roleMap[obj.role] / 255 or 5 / 255 + local trueValue = CreateColor(0, 0, roleValue, 1) + local booleanValue = EvaluateColorFromBoolean(inRange, trueValue, ColorValue0) + local _, _, b = booleanValue:GetRGB() + self:CreateTexture(index, b) + else + self:CreateTexture(index, 0) + end + + updateIndex = updateIndex % numUnits + 1 end --- source: "guid" | "health" | nil @@ -140,15 +150,20 @@ end function Fuyutsui:ClearGroupBlocks() local blocks = self.blocks - if blocks.groups and blocks.groups.start then - local startIndex = blocks.groups.start - for index = startIndex, 255 do - self:CreateTexture(index, 0) - end + local groups = blocks and blocks.groups or nil + if not groups or not groups.start or not groups.num then + return + end + + local endIndex = groups.start + GROUP_MAX_MEMBERS * groups.num + for index = groups.start, endIndex do + self:CreateTexture(index, 0) end end function Fuyutsui:UpdateGroup() + self:ClearGroupBlocks() + updateIndex = 1 self.group = {} self.groupList = {} local group = self.group diff --git a/Infrastructure/StartupNotice.cs b/Infrastructure/StartupNotice.cs index 9064216f..a0e1bc27 100644 --- a/Infrastructure/StartupNotice.cs +++ b/Infrastructure/StartupNotice.cs @@ -4,17 +4,10 @@ internal static class StartupNotice { // 在这里填写首次打开或版本升级时展示的信息文本。 private const string NoticeText = """ - 1.2.1.11 - - 添加了首领战的编号 - - 添加了常用字段的参考 - - 添加了鼠标单位的信息显示 + 1.2.1.12 + - 修复了在某些情况下无法正确加载配置文件的问题。 + - 不再需要检测"救赎之魂"buff, 现在可以在不使用该buff的情况下正常筛选单位 - 1.2.1.10 - - 重要: 修改了施法时间和引导时间的比例, 1秒 = 10, 所有打断都需要修改。 - - 施法修改为:施法(倒计时), 添加了:施法(正计时)。 - - 可以监控boss的一些信息。 - - 优化了UI界面。 - """; public static void ShowIfNeeded() diff --git a/Modules/UnitSelector.cs b/Modules/UnitSelector.cs index b791425e..bc5ca8f0 100644 --- a/Modules/UnitSelector.cs +++ b/Modules/UnitSelector.cs @@ -4,8 +4,8 @@ namespace Shigure; /// 把 / 定义在当前 group 状态下解析为 /// 单位槽位或数量。逻辑忠实移植自旧 Python 项目 utils.py: /// - 只考虑职责 != 0 的单位; -/// - 生命值 0 视为死亡跳过; -/// - 阈值表示只考虑 0 < 生命值 < 阈值; +/// - 最低生命值选择器保留并比较 0 和负生命值; +/// - 生命值数量字段仍只统计 0 < 生命值 < 阈值; /// - 按 "1".."30" 升序遍历, 保证首/末语义稳定。 /// public static class UnitSelector @@ -134,7 +134,7 @@ public static class UnitSelector }; } - /// 在职责 != 0 的单位里, 取 0 < 生命值 < 阈值 且满足 predicate 的最低血量单位。 + /// 在职责 != 0 的单位里, 取生命值 < 阈值且满足 predicate 的最低血量单位(含 0 和负数)。 private static string? LowestHealth( IReadOnlyDictionary> group, int threshold, @@ -155,7 +155,7 @@ public static class UnitSelector continue; } - if (pct > 0 && pct < threshold && pct < lowestPct) + if (pct < threshold && pct < lowestPct) { lowestUnit = key; lowestPct = pct; diff --git a/Obsidian/20-Fuyutsui/07-Fuyutsui-队伍与治疗吸收.md b/Obsidian/20-Fuyutsui/07-Fuyutsui-队伍与治疗吸收.md index 4a41c32e..987d489b 100644 --- a/Obsidian/20-Fuyutsui/07-Fuyutsui-队伍与治疗吸收.md +++ b/Obsidian/20-Fuyutsui/07-Fuyutsui-队伍与治疗吸收.md @@ -30,7 +30,7 @@ source_symbols: - Fuyutsui:UpdateUnitHealthInfo - Fuyutsui:RefreshGroupHealAbsorbBars - Fuyutsui:UpdateGroupHealAbsorbBar -verified_at: 2026-08-09 +verified_at: 2026-08-28 --- # Fuyutsui 队伍与治疗吸收 @@ -100,6 +100,7 @@ GROUP_ROSTER_UPDATE / 专精重建 - 健康使用 `curve100`,并可叠加 `inComingHeals` 的近似偏移;这是预测值,不是纯 `UnitHealthPercent`。 - `valid` 由“未死亡、可协助、在视线内”组成。 - 职责来自 `UnitGroupRolesAssigned()`;玩家用当前专精职责覆盖 API 结果。 +- 每名成员另有一个独立职责覆盖 AuraContainer:存在 `27827`(救赎之魂)时,在职责像素上覆盖 `B=0`;光环消失后覆盖槽隐藏,重新显示底层原职责。 - `inSight` 在收到中文 `UI_ERROR_MESSAGE`“目标不在视野中”时对最近施法目标置为 false,1.5 秒后恢复 true;它是错误消息驱动的启发式,不等价于射线检测。 - 死亡既可按 unit token 更新,也可由战斗日志 GUID 标记。 @@ -107,6 +108,8 @@ GROUP_ROSTER_UPDATE / 专精重建 如果 `group.aura` 存在,每个 offset 建一个 `HELPFUL|PLAYER` 槽;如果 `group.dispel` 存在,则按玩家当前实际会驱散的类型建立一个减益槽。两者都落在成员块地址公式内。 +职责覆盖使用独立容器和 `HELPFUL` 过滤,因此 `27827` 可以来自队伍中的其他牧师,也不会与配置中已有的“救赎之魂”普通光环槽争用同一个 aura 实例。 + 光环持续时间使用顶部像素 `B`,驱散槽使用固定类型色。定义与刷新细节见 [[20-Fuyutsui/08-Fuyutsui-光环容器本地集成]]。 ## 治疗吸收网格协议 @@ -130,20 +133,21 @@ GROUP_ROSTER_UPDATE / 专精重建 - 对 30 名外部消费者,最后一个成员地址必须不超过顶部索引 510。 - 顶部队伍块和吸收网格是两套坐标系,不可用同一个 index 解释。 - Shigure 当前只构建 30 名成员,而 WoW 团队可达到 40 人;30 是当前跨项目契约上限,不是游戏上限。 -- roster 变化后应重置轮转游标并清除旧成员像素,避免上一阵容泄漏。 +- roster 变化后会先清除完整 40 人成员区并重置轮转游标,避免上一阵容泄漏。 +- roster 重建后的职责像素先保持为 0,再由每帧轮转逐个恢复;未重新确认有效性的成员不会提前进入 Shigure 的目标选择。 ## 失败模式与当前风险 -1. **缩编后分帧轮转可永久卡住。** `updateIndex` 是文件级局部变量,`UpdateGroup()` 不重置它。若旧索引大于新 `#groupList`,`groupList[updateIndex]` 为 `nil`,函数在递增前返回,此后每帧都停在同一无效索引(`group.lua:8,57-85,151-190`)。 -2. **旧成员顶部像素残留。** 重建 roster 没有清除超出新人数的旧槽;`ClearGroupBlocks()` 未见调用,而且只清到 255,无法覆盖两段协议的 256..510。 -3. **40 人被截断或未消费。** Lua 成员表可能包含 40 人,但吸收网格和 Shigure 固定 30;需要明确选择截断策略。 -4. **角色偏移缺失。** 职业 group 表若没有 `role` 或 `healthPercent`,更新器做索引算术时可能报错。 -5. **异步 roster 竞态。** roster 更新使用延迟回调;光环容器和吸收槽在短窗口内可能仍绑定旧 unit。 -6. **视线语义是启发式。** 将 `valid` 当作权威 LoS 会导致错误治疗决策。 +1. **40 人被截断或未消费。** Lua 成员表可能包含 40 人,但吸收网格和 Shigure 固定 30;需要明确选择截断策略。 +2. **角色偏移缺失。** 职业 group 表若没有 `role` 或 `healthPercent`,更新器做索引算术时可能报错。 +3. **异步 roster 竞态。** roster 更新使用延迟回调;光环容器和吸收槽在短窗口内可能仍绑定旧 unit。 +4. **视线语义是启发式。** 将 `valid` 当作权威 LoS 会导致错误治疗决策。 + +已修复:`UpdateGroup()` 现在会在重建前清空完整 40 人顶部成员区并把 `updateIndex` 重置为 1;每帧更新也会主动归一越界或异常游标,不再因缩编停在无效槽位。 ## 修改影响 -- 修复缩编时应在 `UpdateGroup()` 把 `updateIndex` 归一为 1,并清除旧成员实际占用的完整 1..510 范围。 +- 队伍重建必须继续保持“先清成员块、再重置游标、最后重建 roster”的顺序;不要在新 roster 写入后再清理。 - 改成员顺序、最大人数或字段偏移,必须同步 [[30-Shigure/03-Shigure-配置合并与GameState构建]]。 - 改吸收网格的列数、槽宽、锚点 RGB 或终点色,必须同步 [[30-Shigure/02-Shigure-像素扫描与协议解码]]。 - 新增 group aura/dispel 字段时检查 `group.num` 和第 30 人末索引。 @@ -151,11 +155,11 @@ GROUP_ROSTER_UPDATE / 专精重建 ## 源码索引 - `Fuyutsui/main.lua:171-180`:成员块起点与字段定义。 -- `Fuyutsui/core/group.lua:21-87`:成员遍历、健康与每帧轮转。 -- `Fuyutsui/core/group.lua:90-139`:死亡、视线与治疗预估。 -- `Fuyutsui/core/group.lua:141-190`:清理和 roster 重建。 +- `Fuyutsui/core/group.lua:22-97`:成员遍历、健康与每帧轮转。 +- `Fuyutsui/core/group.lua:100-149`:死亡、视线与治疗预估。 +- `Fuyutsui/core/group.lua:151-202`:清理和 roster 重建。 - `Fuyutsui/core/block.lua:299-457`:治疗吸收网格、绑定和事件。 -- `Fuyutsui/core/block.lua:946-1090`:队伍光环/驱散成员槽。 +- `Fuyutsui/core/block.lua:1136-1404`:队伍光环、职责覆盖与驱散成员槽。 ## 知识图谱 diff --git a/Obsidian/30-Shigure/03-Shigure-配置合并与GameState构建.md b/Obsidian/30-Shigure/03-Shigure-配置合并与GameState构建.md index 3a248e62..4ddcb3f3 100644 --- a/Obsidian/30-Shigure/03-Shigure-配置合并与GameState构建.md +++ b/Obsidian/30-Shigure/03-Shigure-配置合并与GameState构建.md @@ -28,7 +28,7 @@ source_symbols: - StateBuilder.Build - StateBuilder.BuildGroup - GameState.GetValue -verified_at: 2026-08-09 +verified_at: 2026-08-28 --- # Shigure 配置合并与 GameState 构建 @@ -92,7 +92,7 @@ RowData[2,3] - 组员配置默认 `start=26`、`num=5`,但以生成的配置为准。 - 固定生成 30 个单位;第 `i` 个单位某字段的绝对步骤为 `start + (i-1)*num + relativeStep`。 - 组员字段若 `step: "bar"`,读取的是全局动作条索引,不是组员步长。 -- `HealAbsorbData[i]` 注入组员;因插件生命值包含吸收,构建器从 `生命值` 中减去吸收,并把结果下限钳为 0。 +- `HealAbsorbData[i]` 注入组员;因插件生命值包含吸收,构建器从 `生命值` 中减去吸收。结果不设下限,可以为 0 或负数,供最低生命值选择器比较。 ### GameState 查询 diff --git a/Obsidian/30-Shigure/07-Shigure-动态单位数量与公式.md b/Obsidian/30-Shigure/07-Shigure-动态单位数量与公式.md index 7636ba5b..5442d179 100644 --- a/Obsidian/30-Shigure/07-Shigure-动态单位数量与公式.md +++ b/Obsidian/30-Shigure/07-Shigure-动态单位数量与公式.md @@ -29,7 +29,7 @@ source_symbols: - UnitSelector.Resolve - ModuleLogic.ResolveDynamicFields - FormulaEvaluator.TryEvaluateInt -verified_at: 2026-08-09 +verified_at: 2026-08-28 --- # Shigure 动态单位、数量统计与公式 @@ -75,8 +75,8 @@ verified_at: 2026-08-09 选择器根据 `ModuleUnit.Kind` 分派,核心类别包括最低生命/低于阈值、按角色选取、按光环/驱散、按治疗吸收等。常见但容易遗漏的细节: -- 最低生命要求 `0 < health < threshold`;默认阈值是 100,所以满血 100 不参与。若要包含满血,应把阈值设为 101。 -- 低生命/最低生命路径会跳过生命 0,但光环、角色、驱散、吸收路径没有统一的生命 0 过滤。 +- 最低生命要求 `health < threshold`,会保留并比较 0 和负数;默认阈值是 100,所以满血 100 不参与。若要包含满血,应把阈值设为 101。 +- 最低生命路径不会跳过生命 0;扣除治疗吸收后的生命值也不设下限,因此负数同样参与选择。光环、角色、驱散、吸收路径也没有统一的生命 0 过滤。 - 角色选择的 first/last 由 `Reverse` 控制;它本身不排除生命 0。 - 光环选择比较光环数值,取最大值;驱散选择取首个满足项。 - 吸收选择使用严格 `absorb > threshold`,默认阈值 0。 @@ -89,6 +89,8 @@ verified_at: 2026-08-09 `ModuleCountField` 使用与单位选择相近的筛选维度,但输出数量而非单位编号。数量可在条件中直接按名字读取,也会物化为 `$counts.`。若 ValueAdjustment 目标是已有 count 或 unit-health 名称,调整器可覆盖其派生数值;否则可能写到原状态或 `$dynamicvalues`。 +低生命数量字段仍要求 `0 < health < threshold`,所以 0 和负数不会计入数量;这与最低生命动态单位的候选范围不同。 + ## ValueAdjustment 每项调整有 `Enabled`、Condition、Field、Delta 和 Formula: @@ -122,7 +124,7 @@ verified_at: 2026-08-09 | 症状 | 优先检查 | |---|---| | 满血单位从不被最低生命选中 | 默认阈值 100 且比较是严格 `<`;设 101 | -| 离线/死亡单位仍被光环或角色规则选中 | 这些 Kind 没有统一检查 health > 0 | +| 离线/死亡单位仍被最低生命、光环或角色规则选中 | 这些 Kind 没有统一检查 health > 0;最低生命明确允许 0 和负数 | | 角色缺失的单位意外参与 | `RoleNotZero` 对不可解析值返回合格 | | 数值调整没影响目标选择 | 该字段未被归类为选择阈值的早期调整,或 Condition 未命中 | | 公式小数结果少 1 | 最终强转 int 向 0 截断 | diff --git a/Obsidian/40-跨项目/01-Shigure-像素生产消费契约.md b/Obsidian/40-跨项目/01-Shigure-像素生产消费契约.md index d4e380df..4663b690 100644 --- a/Obsidian/40-跨项目/01-Shigure-像素生产消费契约.md +++ b/Obsidian/40-跨项目/01-Shigure-像素生产消费契约.md @@ -32,7 +32,7 @@ source_symbols: - "Fuyutsui:RefreshGroupHealAbsorbBars" - "PixelScanner.ScanScreenData" - "StateBuilder.Build" -verified_at: "2026-08-09" +verified_at: "2026-08-28" --- # Shigure 像素生产消费契约 @@ -117,7 +117,7 @@ Shigure 从客户区左边缘向下寻找红色标记字节 `(1,0,0)`,以该 y - `G - 1` 是治疗吸收值。 - `B` 是单位编号 `1..30`。 -`StateBuilder` 将该值写入每个 `group` 成员的 `治疗吸收`,并在值非零时从扫描到的 `生命值` 中扣除吸收,最低为零。因此这里的协议变化不仅影响显示字段,也会改变模块实际使用的队伍生命值。 +`StateBuilder` 将该值写入每个 `group` 成员的 `治疗吸收`,并在值非零时从扫描到的 `生命值` 中扣除吸收。扣除结果不设下限,可以为 0 或负数,并会参与最低生命值动态单位的比较。因此这里的协议变化不仅影响显示字段,也会改变模块实际使用的队伍生命值。 ## 关键不变量 diff --git a/README.md b/README.md index 64b16e59..9eb1ed74 100644 --- a/README.md +++ b/README.md @@ -241,10 +241,10 @@ Tools\ 辅助脚本 ### 动态单位、数量与动态数值 -这些字段按 `group` 状态每帧重新计算。队伍统计会跳过 `职责 == 0` 或 `生命值 == 0` 的单位。 +这些字段按 `group` 状态每帧重新计算。多数动态选择与数量统计会跳过 `职责 == 0` 的单位;具体生命值过滤取决于字段类别。 -- 动态单位:可按最低生命值、最高治疗吸收、职责、光环、光环值或驱散类型选择 `1`~`30` 的队伍槽位。可在规则目标中直接使用,也可用裸名称判断是否成功解析。 -- 数量字段:统计低于生命值阈值、高于治疗吸收阈值或带/不带指定光环的单位数,只能用于条件。 +- 动态单位:可按最低生命值、最高治疗吸收、职责、光环、光环值或驱散类型选择 `1`~`30` 的队伍槽位。最低生命值使用“扫描生命值减去治疗吸收”的结果,允许结果为 0 或负数,并在所有 `生命值 < 阈值` 的候选中取最低值。动态单位可在规则目标中直接使用,也可用裸名称判断是否成功解析。 +- 数量字段:统计低于生命值阈值、高于治疗吸收阈值或带/不带指定光环的单位数,只能用于条件。其中低生命数量仍只统计 `0 < 生命值 < 阈值` 的单位。 - 条件动态数值:条件成立时对已有状态、光环、技能或动态字段加减一个整数。 - 公式动态数值:使用字段和算术表达式生成命名数值,支持 `+ - * /`、括号、一元正负号和 `int`、`round`、`floor`、`ceil`、`min`、`max`。 diff --git a/Runtime/StateBuilder.cs b/Runtime/StateBuilder.cs index 6b93c01f..056f2fc2 100644 --- a/Runtime/StateBuilder.cs +++ b/Runtime/StateBuilder.cs @@ -110,11 +110,12 @@ public sealed class StateBuilder : IRuntimeStateBuilder // 治疗吸收来自网格扫描:白块右侧像素的 B=单位编号,G-1=吸收值。 // 插件像素里的生命值含吸收盾,这里折算为真实生命:生命值 -= 治疗吸收。 + // 保留 0 和负数,供最低生命值选择器比较治疗吸收后的有效生命值。 var absorb = healAbsorbData.TryGetValue(i, out var absorbValue) ? absorbValue : 0; sub["治疗吸收"] = absorb; if (absorb != 0 && sub.TryGetValue("生命值", out var healthObj) && healthObj is int health) { - sub["生命值"] = Math.Max(0, health - absorb); + sub["生命值"] = health - absorb; } group[i.ToString()] = sub;