2026/6/4 チャットログから制御するロジックを共通ファイルから削除しアドオン化しました

EasyFarmが使えなくなっていたので似たようなアドオンを作った

久しぶりにミシックを作っているので、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()

コメント

コメントする

目次