add 飞行状态、上马状态、跟随状态监控
@@ -4,9 +4,9 @@
|
|||||||
供外部 Python 等脚本截取小区域即可获取完整状态机。
|
供外部 Python 等脚本截取小区域即可获取完整状态机。
|
||||||
]]
|
]]
|
||||||
|
|
||||||
-- 配置:8个状态位(1个白色锚点 + 7个数据位)
|
-- 配置:10个状态位(1个白色锚点 + 9个数据位)
|
||||||
local PIXEL_SIZE = 10
|
local PIXEL_SIZE = 10
|
||||||
local STATUS_COUNT = 8
|
local STATUS_COUNT = 10
|
||||||
local ADDON_NAME = "LogicBeacon"
|
local ADDON_NAME = "LogicBeacon"
|
||||||
|
|
||||||
-- 默认配置(可被 SavedVariables 覆盖)
|
-- 默认配置(可被 SavedVariables 覆盖)
|
||||||
@@ -27,8 +27,8 @@ for k, v in pairs(defaults) do
|
|||||||
LogicBeaconDB[k] = v
|
LogicBeaconDB[k] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- 升级:至少 8 个状态位(含目标位+后勤位+坐标朝向)
|
-- 升级:至少 10 个状态位(含跟随信号)
|
||||||
LogicBeaconDB.statusCount = math.max(LogicBeaconDB.statusCount or 0, 8)
|
LogicBeaconDB.statusCount = math.max(LogicBeaconDB.statusCount or 0, 10)
|
||||||
|
|
||||||
local db = LogicBeaconDB
|
local db = LogicBeaconDB
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ local bg = f:CreateTexture(nil, "BACKGROUND")
|
|||||||
bg:SetAllPoints()
|
bg:SetAllPoints()
|
||||||
bg:SetColorTexture(0, 0, 0, 1)
|
bg:SetColorTexture(0, 0, 0, 1)
|
||||||
|
|
||||||
-- 像素纹理([1]=锚点 [2~8]=数据位)
|
-- 像素纹理([1]=锚点 [2~10]=数据位)
|
||||||
local pixels = {}
|
local pixels = {}
|
||||||
for i = 1, db.statusCount do
|
for i = 1, db.statusCount do
|
||||||
local tex = f:CreateTexture(nil, "OVERLAY")
|
local tex = f:CreateTexture(nil, "OVERLAY")
|
||||||
@@ -98,6 +98,47 @@ local function GetPlayerCoords()
|
|||||||
return 0, 0
|
return 0, 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 专门用于计算飞行信号的函数(R 通道:0 未上马 / 0.5 可起飞需按空格 / 1.0 已在飞)
|
||||||
|
local function GetFlightSignal()
|
||||||
|
local isMounted = IsMounted()
|
||||||
|
local isFlying = IsFlying()
|
||||||
|
local canFly = IsFlyableArea()
|
||||||
|
|
||||||
|
if not isMounted then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
if isMounted and canFly and not isFlying then
|
||||||
|
if not UnitAffectingCombat("player") and IsOutdoors() then
|
||||||
|
return 0.5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if isMounted and isFlying then
|
||||||
|
return 1.0
|
||||||
|
end
|
||||||
|
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 专门用于计算跟随信号的函数(R 通道:0 未组队 / 0.5 需补跟随 / 1.0 跟随中)
|
||||||
|
local function GetFollowSignal()
|
||||||
|
if not IsInGroup() then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local leader = "party1"
|
||||||
|
|
||||||
|
if IsFollowing() then
|
||||||
|
return 1.0
|
||||||
|
else
|
||||||
|
if UnitExists(leader) and UnitIsVisible(leader) then
|
||||||
|
return 0.5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
f:SetScript("OnUpdate", function()
|
f:SetScript("OnUpdate", function()
|
||||||
-- [1] 锚点:永远纯白 (255, 255, 255)
|
-- [1] 锚点:永远纯白 (255, 255, 255)
|
||||||
pixels[1]:SetColorTexture(1, 1, 1)
|
pixels[1]:SetColorTexture(1, 1, 1)
|
||||||
@@ -150,6 +191,16 @@ f:SetScript("OnUpdate", function()
|
|||||||
math.floor((mapY * 10000) % 100) / 255,
|
math.floor((mapY * 10000) % 100) / 255,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
-- [9] R=飞行信号 GetFlightSignal(),G=上马(1/0),B=可飞区域 IsFlyableArea()
|
||||||
|
local fSignal = GetFlightSignal()
|
||||||
|
local mountedSig = IsMounted() and 1 or 0
|
||||||
|
local canFlySig = IsFlyableArea() and 1 or 0
|
||||||
|
pixels[9]:SetColorTexture(fSignal, mountedSig, canFlySig, 1)
|
||||||
|
|
||||||
|
-- [10] R=跟随信号(0 / 0.5 需补跟随 / 1.0 跟随中)
|
||||||
|
local followSig = GetFollowSignal()
|
||||||
|
pixels[10]:SetColorTexture(followSig, 0, 0, 1)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- 可拖动
|
-- 可拖动
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 默认布局参数
|
# 默认布局参数(与 LogicBeacon:锚点 + 9 个数据块 对齐,需盖住第 9、10 格)
|
||||||
_DEFAULTS = {
|
_DEFAULTS = {
|
||||||
"pixel_size": 17,
|
"pixel_size": 17,
|
||||||
"block_start_x": 30,
|
"block_start_x": 30,
|
||||||
"scan_region_width": 155,
|
"scan_region_width": 190,
|
||||||
"scan_region_height": 15,
|
"scan_region_height": 15,
|
||||||
"offset_left": 20,
|
"offset_left": 20,
|
||||||
"offset_top": 45,
|
"offset_top": 45,
|
||||||
@@ -62,6 +62,51 @@ def load_layout_config():
|
|||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def _tri_state_from_r(channel_byte):
|
||||||
|
"""Lua SetColorTexture 的 R 通道 0 / 0.5 / 1.0 → 与最近档位对齐(抗截图量化误差)"""
|
||||||
|
v = channel_byte / 255.0
|
||||||
|
return min((0.0, 0.5, 1.0), key=lambda t: abs(t - v))
|
||||||
|
|
||||||
|
|
||||||
|
def _tri_state_label(value, low_mid_high):
|
||||||
|
"""将 0 / 0.5 / 1.0 三档转为对应中文说明(抗浮点误差)"""
|
||||||
|
v = float(value) if value is not None else 0.0
|
||||||
|
if v < 0.25:
|
||||||
|
return low_mid_high[0]
|
||||||
|
if v < 0.75:
|
||||||
|
return low_mid_high[1]
|
||||||
|
return low_mid_high[2]
|
||||||
|
|
||||||
|
|
||||||
|
def format_game_state_line(state):
|
||||||
|
"""单行状态文字(中文),供 CLI 与 GUI 共用。"""
|
||||||
|
death_txt = ('存活', '尸体', '灵魂')[state.get('death_state', 0)]
|
||||||
|
target_hp = state.get('target_hp')
|
||||||
|
if target_hp is not None:
|
||||||
|
hp_part = f"血:{state['hp']}% 目标血:{target_hp}% 法:{state['mp']}%"
|
||||||
|
else:
|
||||||
|
hp_part = f"血:{state['hp']}% 法:{state['mp']}%"
|
||||||
|
combat_txt = '战斗中' if state['combat'] else '未战斗'
|
||||||
|
target_txt = '有敌对目标' if state['target'] else '无有效目标'
|
||||||
|
flight_txt = _tri_state_label(
|
||||||
|
state.get('flight', 0),
|
||||||
|
('地面', '待起飞', '飞行中'),
|
||||||
|
)
|
||||||
|
mounted_txt = '已上马' if state.get('mounted') else '未上马'
|
||||||
|
flyable_txt = '区域可飞' if state.get('flyable') else '区域不可飞'
|
||||||
|
follow_txt = _tri_state_label(
|
||||||
|
state.get('follow', 0),
|
||||||
|
('未跟随', '需补跟随', '跟随中'),
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"{hp_part} | "
|
||||||
|
f"{combat_txt} {target_txt} | "
|
||||||
|
f"空格:{state.get('free_slots', 0)} 耐久:{state.get('durability', 0):.0%} {death_txt} | "
|
||||||
|
f"x:{state['x']} y:{state['y']} 朝向:{state['facing']:.1f}° | "
|
||||||
|
f"{flight_txt} {mounted_txt} {flyable_txt} {follow_txt}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_layout_config(cfg):
|
def save_layout_config(cfg):
|
||||||
"""保存布局配置到 game_state_config.json"""
|
"""保存布局配置到 game_state_config.json"""
|
||||||
path = _get_config_path()
|
path = _get_config_path()
|
||||||
@@ -101,8 +146,11 @@ def parse_game_state():
|
|||||||
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
||||||
screenshot.save(os.path.join(SCREENSHOT_DIR, 'game_state.png'))
|
screenshot.save(os.path.join(SCREENSHOT_DIR, 'game_state.png'))
|
||||||
|
|
||||||
# 按截图像素分段:每 pixel_size 一格 → hp, mp, combat, target, logistics_death, x, y
|
# 每 pixel_size 一格 → … x, y, 飞行块(pixels[9]), 跟随(pixels[10])
|
||||||
CHANNEL_NAMES = ('hp', 'mp', 'combat', 'target', 'logistics_death', 'x', 'y')
|
CHANNEL_NAMES = (
|
||||||
|
'hp', 'mp', 'combat', 'target', 'logistics_death', 'x', 'y',
|
||||||
|
'flight_block', 'follow',
|
||||||
|
)
|
||||||
for idx, name in enumerate(CHANNEL_NAMES):
|
for idx, name in enumerate(CHANNEL_NAMES):
|
||||||
left = block_start_x + idx * pixel_size
|
left = block_start_x + idx * pixel_size
|
||||||
box = (left, 0, left + pixel_size, pixel_size)
|
box = (left, 0, left + pixel_size, pixel_size)
|
||||||
@@ -146,6 +194,16 @@ def parse_game_state():
|
|||||||
p7 = get_val(6)
|
p7 = get_val(6)
|
||||||
state['y'] = round((p7[0] + p7[1]/100), 2)
|
state['y'] = round((p7[0] + p7[1]/100), 2)
|
||||||
|
|
||||||
|
# 第 9 像素(Lua pixels[9]):R=飞行信号,G=上马,B=可飞区域
|
||||||
|
p9 = get_val(7)
|
||||||
|
state['flight'] = _tri_state_from_r(p9[0])
|
||||||
|
state['mounted'] = p9[1] > 127
|
||||||
|
state['flyable'] = p9[2] > 127
|
||||||
|
|
||||||
|
# 第 10 像素(Lua pixels[10]):R=跟随信号
|
||||||
|
p10 = get_val(8)
|
||||||
|
state['follow'] = _tri_state_from_r(p10[0])
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -154,14 +212,7 @@ if __name__ == "__main__":
|
|||||||
while True:
|
while True:
|
||||||
state = parse_game_state()
|
state = parse_game_state()
|
||||||
if state:
|
if state:
|
||||||
death_txt = ('存活', '尸体', '灵魂')[state.get('death_state', 0)]
|
print(f"\r[状态] {format_game_state_line(state)}", end="")
|
||||||
print(
|
|
||||||
f"\r[状态] 血:{state['hp']}% 法:{state['mp']}% 目标血:{state.get('target_hp', 0)}% | "
|
|
||||||
f"战斗:{'Y' if state['combat'] else 'N'} 目标:{'Y' if state['target'] else 'N'} | "
|
|
||||||
f"空格:{state.get('free_slots', 0)} 耐久:{state.get('durability', 0):.0%} {death_txt} | "
|
|
||||||
f"x:{state['x']} y:{state['y']} 朝向:{state['facing']:.1f}°",
|
|
||||||
end=""
|
|
||||||
)
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n已停止。")
|
print("\n已停止。")
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pixel_size": 17,
|
"pixel_size": 17,
|
||||||
"block_start_x": 30,
|
"block_start_x": 30,
|
||||||
"scan_region_width": 155,
|
"scan_region_width": 190,
|
||||||
"scan_region_height": 15,
|
"scan_region_height": 15,
|
||||||
"offset_left": 20,
|
"offset_left": 20,
|
||||||
"offset_top": 45
|
"offset_top": 45
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 88 B |
BIN
screenshot/game_state_flight_block.png
Normal file
|
After Width: | Height: | Size: 74 B |
BIN
screenshot/game_state_follow.png
Normal file
|
After Width: | Height: | Size: 74 B |
|
Before Width: | Height: | Size: 88 B After Width: | Height: | Size: 74 B |
|
Before Width: | Height: | Size: 83 B After Width: | Height: | Size: 74 B |
|
Before Width: | Height: | Size: 90 B After Width: | Height: | Size: 74 B |
|
Before Width: | Height: | Size: 86 B After Width: | Height: | Size: 74 B |
|
Before Width: | Height: | Size: 84 B After Width: | Height: | Size: 74 B |
|
Before Width: | Height: | Size: 83 B After Width: | Height: | Size: 74 B |
@@ -331,7 +331,7 @@ class GameLoopWorker(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
from game_state import parse_game_state
|
from game_state import parse_game_state, format_game_state_line
|
||||||
except ImportError:
|
except ImportError:
|
||||||
self.log_signal.emit("❌ 无法导入 game_state 模块")
|
self.log_signal.emit("❌ 无法导入 game_state 模块")
|
||||||
return
|
return
|
||||||
@@ -395,19 +395,7 @@ class GameLoopWorker(QThread):
|
|||||||
while self.running:
|
while self.running:
|
||||||
state = parse_game_state()
|
state = parse_game_state()
|
||||||
if state:
|
if state:
|
||||||
death_txt = ('存活', '尸体', '灵魂')[state.get('death_state', 0)]
|
self.state_signal.emit(format_game_state_line(state))
|
||||||
target_hp = state.get('target_hp')
|
|
||||||
if target_hp is not None:
|
|
||||||
hp_part = f"血:{state['hp']}% 目标血:{target_hp}% 法:{state['mp']}%"
|
|
||||||
else:
|
|
||||||
hp_part = f"血:{state['hp']}% 法:{state['mp']}%"
|
|
||||||
state_str = (
|
|
||||||
f"{hp_part} | "
|
|
||||||
f"战斗:{'Y' if state['combat'] else 'N'} 目标:{'Y' if state['target'] else 'N'} | "
|
|
||||||
f"空格:{state.get('free_slots', 0)} 耐久:{state.get('durability', 0):.0%} {death_txt} | "
|
|
||||||
f"x:{state['x']} y:{state['y']} 朝向:{state['facing']:.1f}°"
|
|
||||||
)
|
|
||||||
self.state_signal.emit(state_str)
|
|
||||||
|
|
||||||
if self.mode == 'patrol' and self.bot_move:
|
if self.mode == 'patrol' and self.bot_move:
|
||||||
self.bot_move.execute_logic(state)
|
self.bot_move.execute_logic(state)
|
||||||
|
|||||||