久しぶりにミシックを作っているので、EasyFarmでノックしようと思ったら、起動しない・・・
何をやっても動かないので、似たようなアドオンを作った。というか、AIに作ってもらった。アビセアでメイジャンノックくらいなら問題なくできているので、とりあえず満足です。
_addon.name = 'AutoRime'
_addon.version = '1.1.0'
_addon.commands = {'autorime', 'ar'}
--[[
指定した名前の敵を探す → 近づく → 戦闘開始 → TPが溜まったらモーダントライム。
敵を倒したら次の敵を探す。
コマンド:
//ar on | off | toggle … 全体の有効/無効
//ar hunt … タゲ取り(索敵)のON/OFF。OFFならWSだけ撃つ
//ar addt … 今ターゲットしている敵の名前をリストに追加(確実)
//ar add <敵名> … 敵名を追加(部分一致)
//ar del <番号> … リストから削除
//ar list … 対象リストを表示
//ar clear … 対象リストを空にする
//ar range 20 … 索敵範囲(ヤルム)
//ar tp 1000 … 撃つTPの閾値
//ar ws Mordant Rime … 撃つWS(英語名)
//ar dist 5.5 … WSを撃てる最大距離
//ar status … 設定を表示
対象リストは data/targets.txt に1行1名で保存される(UTF-8、直接編集も可)。
]]
require('logger')
local res = require('resources')
local packets = require('packets')
local settings = {
enabled = false,
hunt = true,
tp = 1000,
ws_name = 'モーダントライム',
max_dist = 5.5, -- WSを撃つ最大距離
engage_dist = 4.0, -- この距離まで近づいてから戦闘開始
stop_dist = 2.5, -- 戦闘中、これより離れていたら詰める
range = 50, -- 索敵範囲
max_z = 6, -- 高低差がこれ以上の敵は無視(崖上・崖下対策)
interval = 0.25, -- 判定間隔(秒)
cooldown = 2.5, -- WS送信後の待ち(秒)
engage_wait = 3.0, -- 戦闘開始を送ってから再送するまでの待ち(秒)
stuck_time = 8.0, -- これだけ近づけなかったら別の敵を探す(秒)
}
local blocking_buffs = {
[0]=true, [2]=true, [7]=true, [10]=true, [14]=true,
[16]=true, [19]=true, [28]=true,
}
local targets = {}
local target_file = windower.addon_path .. 'data/targets.txt'
local ws = nil
local last_check = 0
local last_send = 0
local last_engage = 0
local running = false
local cur_id = nil -- 今狙っている敵のID
local cur_since = 0 -- 狙い始めた時刻
local ignore = {} -- 近づけなかった敵 [id] = 解除時刻
------------------------------------------------------------------------
-- 対象リストの保存・読み込み
------------------------------------------------------------------------
local function save_targets()
windower.create_dir(windower.addon_path .. 'data')
local f = io.open(target_file, 'w')
if not f then return end
for _, n in ipairs(targets) do f:write(n, '\n') end
f:close()
end
local function load_targets()
targets = {}
local f = io.open(target_file, 'r')
if not f then return end
for raw in f:lines() do
local line = raw:gsub('^%s+', ''):gsub('%s+$', '')
if line ~= '' then targets[#targets + 1] = line end
end
f:close()
end
-- 文字コードの差を吸収するため、元の名前と変換後の両方で比較する
local function name_variants(name)
local v = {name}
local ok, conv = pcall(windower.from_shift_jis, name)
if ok and conv and conv ~= name then v[#v + 1] = conv end
return v
end
local function name_matches(mob_name)
if #targets == 0 then return false end
for _, n in ipairs(name_variants(mob_name)) do
local ln = n:lower()
for _, t in ipairs(targets) do
if ln:find(t:lower(), 1, true) then return true end
end
end
return false
end
------------------------------------------------------------------------
-- WS
------------------------------------------------------------------------
local function find_ws(name)
local lname = name:lower()
for _, v in pairs(res.weapon_skills) do
if v.en:lower() == lname or v.ja == name then return v end
end
end
local function load_ws()
ws = find_ws(settings.ws_name)
if not ws then
error('WSが見つかりません: ' .. settings.ws_name)
settings.enabled = false
end
end
local function ws_available()
local abil = windower.ffxi.get_abilities()
if not abil or not abil.weapon_skills then return false end
for _, id in ipairs(abil.weapon_skills) do
if id == ws.id then return true end
end
return false
end
local function is_blocked(player)
for _, b in ipairs(player.buffs or {}) do
if blocking_buffs[b] then return true end
end
return false
end
local function send_ws()
local lang = (windower.ffxi.get_info().language or 'English'):lower()
if lang == 'japanese' then
windower.chat.input(windower.to_shift_jis('/ws "' .. ws.ja .. '" <t>'))
else
windower.chat.input('/ws "' .. ws.en .. '" <t>')
end
end
------------------------------------------------------------------------
-- 移動・向き
------------------------------------------------------------------------
local function dist(mob)
return math.sqrt(mob.distance)
end
local function face(me, mob)
windower.ffxi.turn(-math.atan2(mob.y - me.y, mob.x - me.x))
end
local function run_to(me, mob)
windower.ffxi.run(mob.x - me.x, mob.y - me.y)
running = true
end
local function stop()
if running then
windower.ffxi.run(false)
running = false
end
end
------------------------------------------------------------------------
-- 索敵
------------------------------------------------------------------------
local function party_ids()
local ids = {}
local party = windower.ffxi.get_party()
if not party then return ids end
for k, m in pairs(party) do
if type(m) == 'table' and m.mob then ids[m.mob.id] = true end
end
return ids
end
local function find_target(me)
local pids = party_ids()
local now = os.clock()
local best, best_d = nil, settings.range
for _, mob in pairs(windower.ffxi.get_mob_array()) do
if mob.valid_target and mob.is_npc and mob.spawn_type == 16
and mob.hpp > 0
and (ignore[mob.id] or 0) < now
and (mob.claim_id == 0 or pids[mob.claim_id])
and math.abs((mob.z or 0) - (me.z or 0)) < settings.max_z
and name_matches(mob.name) then
local d = dist(mob)
if d < best_d then best, best_d = mob, d end
end
end
return best
end
local function engage(mob)
local p = packets.new('outgoing', 0x01A, {
['Target'] = mob.id,
['Target Index'] = mob.index,
['Category'] = 2, -- 戦闘開始
})
packets.inject(p)
last_engage = os.clock()
end
------------------------------------------------------------------------
-- メインループ
------------------------------------------------------------------------
local function tick()
local player = windower.ffxi.get_player()
local me = windower.ffxi.get_mob_by_target('me')
if not player or not me then return end
if is_blocked(player) then stop() return end
local now = os.clock()
-- 戦闘中:距離を詰めて、TPが溜まったらWS
if player.status == 1 then
local mob = windower.ffxi.get_mob_by_target('t')
if not mob or mob.hpp == 0 then stop() return end
cur_id = mob.id
face(me, mob)
if settings.hunt and dist(mob) > settings.stop_dist then
run_to(me, mob)
else
stop()
end
if player.vitals.tp >= settings.tp
and now - last_send >= settings.cooldown
and dist(mob) <= settings.max_dist
and ws_available() then
send_ws()
last_send = now
end
return
end
-- 待機中:索敵して近づき、戦闘開始
if player.status ~= 0 or not settings.hunt then stop() return end
local mob = cur_id and windower.ffxi.get_mob_by_id(cur_id)
if not (mob and mob.valid_target and mob.hpp > 0
and (mob.claim_id == 0 or party_ids()[mob.claim_id])) then
mob = find_target(me)
if not mob then stop() cur_id = nil return end
cur_id, cur_since = mob.id, now
end
-- 長く近づけない敵(地形で詰まった等)は一定時間無視
if now - cur_since > settings.stuck_time then
ignore[mob.id] = now + 30
cur_id = nil
stop()
return
end
face(me, mob)
if dist(mob) > settings.engage_dist then
run_to(me, mob)
else
stop()
if now - last_engage >= settings.engage_wait then
engage(mob)
end
end
end
windower.register_event('prerender', function()
if not settings.enabled or not ws then return end
local now = os.clock()
if now - last_check < settings.interval then return end
last_check = now
tick()
end)
windower.register_event('action', function(act)
local player = windower.ffxi.get_player()
if player and act.actor_id == player.id and act.category == 3 then
last_send = os.clock()
end
end)
local function turn_off(msg)
if settings.enabled then
settings.enabled = false
stop()
cur_id = nil
if msg then log(msg) end
end
end
windower.register_event('zone change', function()
turn_off('ゾーン移動したのでOFFにしました')
end)
windower.register_event('status change', function(new)
if new == 2 or new == 3 then
turn_off('戦闘不能になったのでOFFにしました')
end
end)
-- チャット入力など、手動操作したいときは //ar off で止める
windower.register_event('unload', function() stop() end)
------------------------------------------------------------------------
-- コマンド
------------------------------------------------------------------------
local function show_status()
log(('状態:%s 索敵:%s WS:%s TP:%d 範囲:%dy 対象:%d件'):format(
settings.enabled and 'ON' or 'OFF',
settings.hunt and 'ON' or 'OFF',
ws and (ws.ja or ws.en) or '?',
settings.tp, settings.range, #targets))
end
local function show_list()
if #targets == 0 then log('対象リストは空です(//ar addt で追加)') return end
for i, n in ipairs(targets) do log(('%d: %s'):format(i, n)) end
end
local function add_target(name)
if not name or name == '' then return end
for _, n in ipairs(targets) do if n == name then return end end
targets[#targets + 1] = name
save_targets()
log('追加: ' .. name)
end
windower.register_event('addon command', function(cmd, ...)
cmd = (cmd or 'toggle'):lower()
local args = {...}
local arg = table.concat(args, ' ')
if cmd == 'on' then
settings.enabled = true
elseif cmd == 'off' then
turn_off()
elseif cmd == 'toggle' then
if settings.enabled then turn_off() else settings.enabled = true end
elseif cmd == 'hunt' then
settings.hunt = not settings.hunt
if not settings.hunt then stop() end
elseif cmd == 'addt' then
local t = windower.ffxi.get_mob_by_target('t')
if t then add_target(name_variants(t.name)[#name_variants(t.name)])
else log('ターゲットしていません') end
return
elseif cmd == 'add' and arg ~= '' then
local vs = name_variants(arg)
add_target(vs[#vs])
return
elseif cmd == 'del' and tonumber(args[1]) then
local n = table.remove(targets, tonumber(args[1]))
if n then save_targets() log('削除: ' .. n) end
return
elseif cmd == 'list' then
show_list() return
elseif cmd == 'clear' then
targets = {} save_targets() log('対象リストを空にしました') return
elseif cmd == 'range' and tonumber(args[1]) then
settings.range = tonumber(args[1])
elseif cmd == 'tp' and tonumber(args[1]) then
settings.tp = math.max(1000, math.min(3000, tonumber(args[1])))
elseif cmd == 'dist' and tonumber(args[1]) then
settings.max_dist = tonumber(args[1])
elseif cmd == 'ws' and arg ~= '' then
settings.ws_name = arg
load_ws()
elseif cmd ~= 'status' then
log('//ar on|off|hunt|addt|add|del|list|clear|range|tp|ws|dist|status')
return
end
show_status()
end)
load_targets()
load_ws()
コメント