--[[ Copyright © 2017, Ivaar All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of AutoDNC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL IVAAR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ]] _addon.name = 'AutoDNC' _addon.version = '1.20.05.19' _addon.author = 'Ivaar' _addon.commands = {'AutoDNC','DNC'} CLAUDE_BUILD = 'claude-build-75' res = require('resources') config = require('config') packets = require('packets') texts = require('texts') _static = { default = { actions = 1, ja = 1, ws = 1, setws = 'Rudra\'s Storm', setws2 = 'Cyclone', distance = 4.0, waltzhp = 75, minwshp = 1, maxwshp = 100, samba = 1, --waltz = {p0=true,p1=true,p2=true,p3=true,p4=true,p5=true} waltz = 1, waltzpt = 1, na = 1, proc = 0, posx = 10, posy = 200, statusposx = 100, statusposy = 100, wstp = 2500, saberwstp = 1000, useflourish = 1, autostance = 1, stancehp = 65, fmmax = 6, healmode = 0, attackmode = 0, tankmode = 0, autocancel = 1, cancelhp = 40, flourish3 = 'ternary', flourish1 = 'violent', saberpattern = 1, ternarytp = 1000 }, base = T{['off']=0,['on']=1}, samba = T{['off']=0,['haste']=1,['drain']=2}, proc = T{['off']=0,['ja']=1,['ws']=2}, dual_toggle = S{'actions','ja','ws','waltz','waltzpt','na','silent','useflourish','autostance','healmode','tankmode','autocancel','attackmode'} } setting = config.load(_static.default) clock = os.clock() time = clock delay = 0 JA_del = 1.2 WS_del = 5 fm_block_until = 0 stun_window_until = 0 last_status = nil pendingWS = false pendingWS_tp = nil pendingWS_ws = nil tank_tp3000_since = nil saber_wait_since = nil step_use_count = {} step_rotation = { {en = 'Box Step'}, {en = 'Feather Step'} } STEP_SWITCH_EVERY = 3 -- 同じステップをこの回数使ったら次の種類に切り替える function get_next_step(mob) local name = (mob and mob.name) or '' local idx = (step_use_count[name..':idx'] or 0) local used = (step_use_count[name..':used'] or 0) + 1 if used > STEP_SWITCH_EVERY then used = 1 idx = (idx + 1) % #step_rotation end step_use_count[name..':idx'] = idx step_use_count[name..':used'] = used return step_rotation[idx + 1].en end -- get_next_stepを実行(カウンタを進める)せずに、次に使われるはずのステップ名だけを覗き見る function peek_next_step(mob) local name = (mob and mob.name) or '' local idx = (step_use_count[name..':idx'] or 0) local used = (step_use_count[name..':used'] or 0) + 1 if used > STEP_SWITCH_EVERY then idx = (idx + 1) % #step_rotation end return step_rotation[idx + 1].en end -- ステップ技名から、そのステップ自身のリキャストIDを返す(ボックスステップとフェザーステップは別ID) function get_step_recast_id(step_name) if step_name == 'Box Step' then return RID_STEP_SUB end return RID_STEP_MAIN end -- 剣の舞い中の連携パターン。文字列はWS名、テーブルは{flourish='技名'}でフラリッシュを挟む指示 -- パターン①: エクゼンレイター→ルドラストーム→エヴィセレイション→ルドラストーム -- パターン②: Wフラリッシュ+エヴィセレイション→ピリックレオス→エヴィセレイション→ルドラストーム -- (ピリックレオス未取得でも、取得後そのまま使えるように組み込み済み) saber_patterns = { [1] = {'Exenterator', "Rudra's Storm", 'Evisceration', "Rudra's Storm"}, [2] = {{flourish = 'Wild Flourish'}, 'Evisceration', 'Pyrrhic Kleos', 'Evisceration', "Rudra's Storm"} } saber_pattern_index = 1 saber_gap_until = nil saber_combo_started = false saber_last_action = nil SABER_COMBO_GAP = 30 -- 連携を1周し終えてから、次の1周を始めるまでの休み時間(秒) function get_active_pattern() return saber_patterns[setting.saberpattern] or saber_patterns[1] end function saber_pattern_advance() local pattern = get_active_pattern() saber_pattern_index = saber_pattern_index + 1 if saber_pattern_index > #pattern then saber_pattern_index = 1 saber_gap_until = os.clock() + SABER_COMBO_GAP saber_combo_started = false end end function get_flourish_recast_id(name) if name == 'Climactic Flourish' then return RID_CLIMACTIC elseif name == 'Boost Flourish' then return RID_BOOST elseif name == 'Wild Flourish' then return RID_WILD elseif name == 'Ternary Flourish' then return RID_TERNARY end return nil end function next_ws(mode) if mode == 'saber' then local pattern = get_active_pattern() local entry = pattern[saber_pattern_index] if type(entry) == 'string' then return entry end end return setting.setws end function target_change() local mob = windower.ffxi.get_mob_by_target('t') local targ = windower.ffxi.get_mob_by_target('st', 't') if not setting.silent and setting.proc ~= 0 then windower.text.set_visibility('proc_box', false) if targ then for i,v in ipairs(staggered) do if v == targ.id then windower.text.set_visibility('proc_box', true) windower.text.set_text('proc_box',' Mob Staggered '..targ.id) end end end end if setting.proc == 1 then if mob then for i,v in ipairs(staggered) do if v == mob.id then setting.ja = 0 setting.ws = 1 return true end end end setting.ja = 1 setting.ws = 0 elseif setting.proc == 2 then if mob then for i,v in ipairs(staggered) do if v == mob.id then setting.ws = 0 return true end end end setting.ws = 1 end end windower.register_event('target change', target_change) function zen2han_digits(str) -- 全角スペース(U+3000)を半角スペースに、全角数字を半角数字に正規化する str = str:gsub('\227\128\128', ' ') return (str:gsub('\239\188[\144-\153]', function(c) return string.char(48 + (c:byte(3) - 0x90)) end)) end windower.register_event('incoming text', function(old,new,color,newcolor) if string.find(old,'フィニシングムーブがMAX') or string.find(old,'Finishing Move') and string.find(old,'unable') then fm_block_until = os.clock() + 3 end -- 敵の技の構え(予備動作)を検知:Vフラでのスタン狙いに使うウィンドウを開く local mob = windower.ffxi.get_mob_by_target('t') if mob and mob.name and string.find(old, mob.name, 1, true) and string.find(old, 'の構え。') then stun_window_until = os.clock() + 4 end if proc ~= 0 then local play = windower.ffxi.get_player() if play and string.find(old,(play.name).. '\'s attack staggers the fiend%p') then local targ = windower.ffxi.get_mob_by_target('t') local ltarg = windower.ffxi.get_mob_by_target('lastst') if targ then for k, v in ipairs(staggered) do if v == targ.id and ltarg then targ = ltarg end end elseif ltarg then targ = ltarg end table.insert(staggered, targ.id) target_change() windower.send_ipc_message('add'..targ.id) end end end) function get_FM(buffs) local max = 0 for k,v in pairs(buffs) do local n = k:match('^finishing move %((%d+)%+?%)$') if n then local num = tonumber(n) if num and num > max then max = num end end end return max end function buff_name(id) local b = res.buffs[id] if not b then return nil end local n = b.en or b.english or b.name return n and n:lower() or nil end function to_buff_set(id_list) local set = S{} if not id_list then return set end for _,id in ipairs(id_list) do local n = buff_name(id) if n then set[n] = true end end return set end function find_buff_id(play, name) if not (play and play.buffs) then return nil end for _,id in ipairs(play.buffs) do if buff_name(id) == name then return id end end return nil end windower.register_event('prerender',function () local now = os.clock() if clock + delay <= now then time = clock clock = now delay = 0.1 local play = windower.ffxi.get_player() local mob = windower.ffxi.get_mob_by_target('t') local recast = windower.ffxi.get_ability_recasts() if not play then return end local buffs = to_buff_set(play.buffs) -- 踊り子がメイン職・サブ職どちらでもない時は表示しない if play.main_job ~= 'DNC' and play.sub_job ~= 'DNC' then if status_box then status_box:hide() end else if status_box then status_box:show() end -- AutoDNCのON/OFF・スタンス・主要な個別設定をまとめて画面隅に常時表示する(チャットログは汚さない) local status_stance = buffs['saber dance'] and 'Saber' or (buffs['fan dance'] and 'Fan' or 'None') local function onoff(v) return (v == 1) and 'On' or 'Off' end local status_lines = { 'AutoDNC: '..(setting.actions == 1 and 'ON' or 'OFF')..' ['..status_stance..']', 'WS:'..onoff(setting.ws)..' JA:'..onoff(setting.ja)..' Stance:'..onoff(setting.autostance)..' Cancel:'..onoff(setting.autocancel), 'Heal:'..onoff(setting.healmode)..' Attack:'..onoff(setting.attackmode)..' Tank:'..onoff(setting.tankmode), 'Waltz:'..onoff(setting.waltz)..' Party:'..onoff(setting.waltzpt)..' NA:'..onoff(setting.na)..' Samba:'..(setting.samba == 0 and 'Off' or setting.samba == 1 and 'Haste' or 'Drain') } status_box:text(table.concat(status_lines, '\n')) end -- 【剣の舞い自動キャンセル】外部のCancelアドオンに頼らず、AutoDNC自身でバフを直接キャンセルする -- 自分だけでなくパーティメンバーのHPも見て、誰かがcancelhpを下回ったら剣の舞いを解除する -- 回復専念モード(healmode)中は、HPに関わらず即座に剣の舞い・扇の舞いを両方解除する if setting.healmode == 1 and play.main_job == 'DNC' and (buffs['saber dance'] or buffs['fan dance']) then local bname = buffs['saber dance'] and 'saber dance' or 'fan dance' local bid = find_buff_id(play, bname) if bid then windower.ffxi.cancel_buff(bid) return end end if setting.autocancel == 1 and play.main_job == 'DNC' and buffs['saber dance'] then local worst_hpp = play.vitals.hpp if setting.waltzpt == 1 then for i,member in pairs(windower.ffxi.get_party()) do if type(member) == 'table' and member.hpp and member.hpp ~= 0 and member.hpp < worst_hpp then worst_hpp = member.hpp end end end if worst_hpp <= setting.cancelhp then local bid = find_buff_id(play, 'saber dance') if bid then windower.ffxi.cancel_buff(bid) return end end end if buffs.sleep or buffs.petrification or buffs.stun or buffs.charm or buffs.amnesia or buffs.terror or buffs.lullaby or buffs.impairment then return end if play.status == 1 and setting.actions == 1 and mob and mob.valid_target and mob.is_npc then Engaged = true else Engaged = false end if play.status <= 1 and waltz(play, recast, buffs) or play.vitals.hpp < 50 then return end if Engaged then local FM = get_FM(buffs) -- 【スタンス自動切替】扇の舞い(Fan Dance)は盾専念モード専用、剣の舞い(Saber Dance)はautostanceでの通常維持 -- HPを見た自動切替(回復のために扇の舞いへ逃げる)は廃止。回復したい時はCancelアドオン等で手動解除する運用 -- 普段(回復が必要な状況でなければ)はリキャストが明け次第すぐ剣の舞いに入る。TPを待つ必要はない if play.main_job == 'DNC' then if setting.tankmode == 1 and not buffs['fan dance'] and FAN_ID and recast[FAN_ID] == 0 then return autoJA('Fan Dance', '') elseif setting.autostance == 1 and setting.tankmode == 0 and setting.healmode == 0 and not buffs['saber dance'] and SABER_ID and recast[SABER_ID] == 0 then -- 新しく剣の舞いに入る時は、前回の連携状態を引きずらないようリセットする saber_pattern_index = 1 saber_gap_until = nil saber_combo_started = false return autoJA('Saber Dance', '') end end -- 剣の舞い(Saber Dance)/扇の舞い(Fan Dance)/どちらもなし、を自動判定 -- 攻撃専念モード中は実際のバフ状態に関わらず強制的にsaber扱いにして、攻撃パターンを常時実行する -- 回復専念モード中は実際のバフ状態に関わらず強制的にfan扱いにして、回復以外の行動を完全に封印する -- (両方onの場合は安全のため回復専念を優先する) local mode = buffs['saber dance'] and 'saber' or (buffs['fan dance'] and 'fan' or 'normal') if setting.attackmode == 1 then mode = 'saber' end if setting.healmode == 1 then mode = 'fan' end -- 盾専念モード以外で、剣の舞いでない状態(扇の舞い中・無スタンス)は基本的に回復専念モードと同じ扱い -- (攻撃は行わず、ステップ→RフラでTPだけ確保して、いつでもワルツが撃てる状態を維持する) local passive = setting.tankmode == 0 and mode ~= 'saber' -- 【最優先(回復専念相当の間は無効)】直前のティックでWフラ/Bフラ/Tフラを撃った直後は、 -- 効果が乗っている今のうちに必ずWSを撃つ(1テンポ遅れると効果が乗らないため) if pendingWS then pendingWS = false if (not passive) and play.vitals.tp >= (pendingWS_tp or setting.wstp) then return autoWS(pendingWS_ws or setting.setws) end end -- 剣の舞いの連携が実際に進行中か(=休み時間でも、まだ連携開始前でもないか)を判定。 -- 連携中はWS以外一切禁止にするため、サンバ・ステップ・フラリッシュなど全ての寄り道行動をこのフラグでガードする -- saber_combo_started は「この周回で一度でもパターンのWS/フラリッシュを撃ったか」を表す。 -- 剣の舞いに入った直後(まだ1発も撃っていない)は休み時間中と同じ扱いで準備行動を許可する -- 安全装置:連携中のはずなのに一定時間(15秒)何も撃てていない場合(敵のHP/距離条件で -- WSが弾かれ続けている等)は、詰みを防ぐため強制的に休み時間扱いにして準備行動を再開する local saber_gap_now = (mode == 'saber') and saber_gap_until and os.clock() < saber_gap_until local saber_stalled = (mode == 'saber') and saber_combo_started and (os.clock() - (saber_last_action or 0) > 15) local saber_combo_running = (mode == 'saber') and saber_combo_started and not saber_gap_now and not saber_stalled -- 【サンバ維持】ヘイスト/ドレインサンバが切れていたら再発動(剣の舞いの連携実行中は行わない。 -- 休み時間・回復専念モード・扇の舞い中・無スタンス中はOK。passiveの早期リターンより前に置くことで -- 回復専念モード中もちゃんとサンバ維持が働くようにしている) if setting.samba ~= 0 and not saber_combo_running then if not buffs['haste samba'] and play.vitals.tp >= 350 and setting.samba == 1 and RID_HASTESAMBA and recast[RID_HASTESAMBA] == 0 then return autoJA('Haste Samba', '') elseif setting.samba == 2 and not buffs['drain samba'] and RID_DRAINSAMBA and recast[RID_DRAINSAMBA] == 0 then if play.main_job == 'DNC' and play.vitals.tp >= 400 then return autoJA('Drain Samba III', '') elseif play.sub_job == 'DNC' and play.vitals.tp >= 250 then return autoJA('Drain Samba II', '') end end end -- 【扇の舞い中 / 無スタンス中 / 回復専念モード中】WS・攻撃連携は封印するが、 -- ワルツを撃つためのTPが枯渇しないよう、ステップ→RフラでTPだけは素早く確保する -- メイン職踊り子はボックス/フェザーの2択ローテーション、サブ職踊り子はフェザーステップが使えないため -- ボックスステップ固定にする if passive then if play.main_job == 'DNC' then local ns1 = peek_next_step(mob) local ns1_rid = get_step_recast_id(ns1) if ns1_rid and recast[ns1_rid] == 0 and play.vitals.tp >= 100 and FM < setting.fmmax and os.clock() >= fm_block_until then autoJA(get_next_step(mob), '') elseif RID_REVERSE and recast[RID_REVERSE] == 0 and FM >= 1 then -- 回復専念中はコンボを考えず、貯まったFMは即TPに変換して構わない autoJA('Reverse Flourish', '') end elseif play.sub_job == 'DNC' then if RID_STEP_SUB and recast[RID_STEP_SUB] == 0 and play.vitals.tp >= 100 and FM < setting.fmmax and os.clock() >= fm_block_until then autoJA('Box Step', '') elseif RID_REVERSE and recast[RID_REVERSE] == 0 and FM >= 1 then autoJA('Reverse Flourish', '') end end return end -- 【盾専念モード】TPが3000に到達してから20秒その水準を維持できたらWSを撃つ。 -- 待っている間はAフラリッシュで敵対心を稼ぐ。通常のTフラ/wstpによるWS発動はこのモード中は行わない if setting.tankmode == 1 and play.main_job == 'DNC' then if play.vitals.tp >= 3000 then tank_tp3000_since = tank_tp3000_since or os.clock() if os.clock() - tank_tp3000_since >= 20 and setting.ws == 1 and mob.hpp >= setting.minwshp and mob.hpp <= setting.maxwshp and math.sqrt(mob.distance) <= setting.distance then tank_tp3000_since = nil return autoWS(setting.setws) end else tank_tp3000_since = nil end if FM >= 1 and RID_ANIMATED and recast[RID_ANIMATED] == 0 then return autoJA('Animated Flourish', '') end end -- 【Tフラ→WS専用トリガー】TP1000以上・FM3以上が揃った時点で、通常のwstp閾値を待たずに即発動 -- (Tフラは次の一撃を確定トリプルアタックにする効果。Rフラとのフラリッシュ枠は別カテゴリなので競合しない) -- 盾専念モード中は上のTP3000保持ロジックがWS発動を管理するので、こちらは動かさない -- 剣の舞い中はパターンエンジンに全てのWS選択を任せるため、このトリガーは動かさない -- (このトリガーはパターンの位置を進めないので、剣の舞い中に発動するとパターンとズレて二重発動などの原因になる) if setting.tankmode == 0 and mode ~= 'saber' and play.main_job == 'DNC' and setting.flourish3 == 'ternary' and setting.ws == 1 and play.vitals.tp >= setting.ternarytp and FM >= 3 and RID_TERNARY and recast[RID_TERNARY] == 0 and mob.hpp >= setting.minwshp and mob.hpp <= setting.maxwshp and math.sqrt(mob.distance) <= setting.distance then pendingWS = true pendingWS_tp = setting.ternarytp pendingWS_ws = next_ws(mode) return autoJA('Ternary Flourish', '') end -- 【WS発動フェーズ】TPが閾値に到達し、対象HP・距離が条件を満たす場合 -- 剣の舞い中は専用の低い閾値(saberwstp)、それ以外は通常のwstpを使う -- 盾専念モード中はTP3000保持ロジックが専用で管理するため、こちらは動かさない -- 剣の舞い中は、1周し終えた後の休み時間(saber_gap_until)中はWS発動フェーズ自体を止める -- (休み時間中はTPが閾値に達していてもステップ側のTP/FM構築に回す) local ws_threshold = (mode == 'saber') and setting.saberwstp or setting.wstp local saber_in_gap = (mode == 'saber') and saber_gap_until and os.clock() < saber_gap_until if not saber_in_gap and setting.tankmode == 0 and play.vitals.tp >= ws_threshold and setting.ws == 1 and mob.hpp >= setting.minwshp and mob.hpp <= setting.maxwshp and math.sqrt(mob.distance) <= setting.distance then if play.main_job == 'DNC' and mode == 'saber' then -- 剣の舞い中:連携パターンに従って、フラリッシュ→WS or 通常WSを順番に実行 saber_combo_started = true saber_last_action = os.clock() local entry = get_active_pattern()[saber_pattern_index] if type(entry) == 'table' and entry.flourish then local rid = get_flourish_recast_id(entry.flourish) if rid and recast[rid] == 0 and FM >= 1 then saber_pattern_advance() pendingWS = true pendingWS_tp = ws_threshold pendingWS_ws = next_ws(mode) local target = (entry.flourish == 'Climactic Flourish' or entry.flourish == 'Boost Flourish') and '' or '' return autoJA(entry.flourish, target) else -- フラリッシュが使えない時(リキャスト中/FM不足)は、この位置を諦めて -- パターンを次に進め、次のエントリ(WS名のはず)をそのまま撃つ saber_pattern_advance() local next_entry = get_active_pattern()[saber_pattern_index] local fallback_ws = (type(next_entry) == 'string' and next_entry) or setting.setws if type(next_entry) == 'string' then saber_pattern_advance() end return autoWS(fallback_ws) end elseif type(entry) == 'string' then saber_pattern_advance() return autoWS(entry) end elseif play.main_job == 'DNC' and FM >= 1 and setting.flourish3 == 'climactic' and RID_CLIMACTIC and recast[RID_CLIMACTIC] == 0 then -- C.フラリッシュ(剣の舞い以外での単発運用、Lv75+専用) return autoJA('Climactic Flourish', '') elseif play.main_job == 'DNC' and setting.useflourish == 1 and FM >= 2 and RID_WILD and recast[RID_WILD] == 0 then -- Wフラリッシュ:次のWSに連携効果を付与してから撃つ pendingWS = true pendingWS_tp = setting.wstp pendingWS_ws = next_ws(mode) return autoJA('Wild Flourish', '') else return autoWS(next_ws(mode)) end elseif play.sub_job == 'DNC' and setting.ja == 1 then -- サブ職踊り子:FM構築→(FMがMAX-1以上でTPが心もとなければ)TP変換、が基本 if RID_STEP_SUB and recast[RID_STEP_SUB] == 0 and play.vitals.tp >= 100 and FM < setting.fmmax and os.clock() >= fm_block_until then autoJA('Box Step', '') elseif RID_REVERSE and recast[RID_REVERSE] == 0 and play.vitals.tp < setting.wstp and FM >= 1 then -- Rフラリッシュ:WS閾値に届いていない間、FMが1以上あればリキャスト明け次第すぐTPに変換 autoJA('Reverse Flourish', '') elseif RID_VIOLENT and recast[RID_VIOLENT] == 0 and FM >= 2 then autoJA('Violent Flourish', '') elseif play.main_job == 'THF' and RID_BULLY and recast[RID_BULLY] == 0 then autoJA('Bully', '') end elseif play.main_job == 'DNC' and setting.ja == 1 and mode == 'saber' then -- 【剣の舞い専用ロジック】優先順位: 敵の技に反応してVフラ(スタン) > Rフラでためる > ステップでFM構築 -- 休み時間中・まだ連携未開始・連携が詰まっている(15秒不動)、のいずれかの間だけ -- Vフラ/Rフラ/ステップ/ノーフットライズを使ってTP・FM・敵対心を整える。 -- 連携実行中はこれらを一切使わず、TPは通常攻撃だけで自然に貯める(WS以外は本当に何もしない) if not saber_combo_running then local peeked_step = peek_next_step(mob) local peeked_step_rid = get_step_recast_id(peeked_step) if RID_VIOLENT and recast[RID_VIOLENT] == 0 and FM >= 2 and setting.flourish1 == 'violent' then -- Vフラ:敵の技の構え検知に頼らず、休み時間中にリキャストが空いていれば使う -- (チャットテキストでの構え検知が文字コードの都合で機能していない可能性が高いため撤廃) autoJA('Violent Flourish', '') elseif RID_REVERSE and recast[RID_REVERSE] == 0 and play.vitals.tp < setting.wstp and FM >= 1 then -- それ以外はできるだけRフラでTPをためる -- (剣の舞いの連携パターンはもうTフラ/Bフラを使わないので、FMを温存する必要はない) autoJA('Reverse Flourish', '') elseif peeked_step_rid and recast[peeked_step_rid] == 0 and play.vitals.tp >= 100 and FM < setting.fmmax and os.clock() >= fm_block_until then -- FMがない/使い道がない間はステップで構築(ボックス/フェザーそれぞれ自分自身のリキャストで判定) if RID_PRESTO and recast[RID_PRESTO] == 0 and FM < 3 then autoJA('Presto', '') end autoJA(get_next_step(mob), '') elseif RID_NOFOOT and recast[RID_NOFOOT] == 0 then autoJA('No Foot Rise', '') end end elseif play.main_job == 'DNC' and setting.ja == 1 then -- メイン職踊り子(剣の舞い以外。主に盾専念モードのTP待機中):TPがWS閾値未満の間はFMを構築し続ける local peeked_step2 = peek_next_step(mob) local peeked_step2_rid = get_step_recast_id(peeked_step2) if peeked_step2_rid and recast[peeked_step2_rid] == 0 and play.vitals.tp >= 100 and FM < setting.fmmax and os.clock() >= fm_block_until then if RID_PRESTO and recast[RID_PRESTO] == 0 and FM < 3 then autoJA('Presto', '') end autoJA(get_next_step(mob), '') elseif RID_NOFOOT and recast[RID_NOFOOT] == 0 then -- ノーフットライズ:リキャストが明けたら条件を付けずすぐ使う autoJA('No Foot Rise', '') elseif RID_REVERSE and recast[RID_REVERSE] == 0 and play.vitals.tp < setting.wstp and FM >= 1 then -- Rフラリッシュ:WS閾値に届いていない間、FMが1以上あればリキャスト明け次第すぐTPに変換 autoJA('Reverse Flourish', '') elseif FM >= 1 and play.vitals.tp < setting.wstp and setting.flourish1 == 'violent' and RID_VIOLENT and recast[RID_VIOLENT] == 0 then -- Vフラリッシュ:スタンを狙う(剣の舞い中でも使用、制限なし) autoJA('Violent Flourish', '') elseif FM >= 1 and play.vitals.tp < setting.wstp and setting.flourish1 == 'animated' and RID_ANIMATED and recast[RID_ANIMATED] == 0 then -- Aフラリッシュ:自分に敵対心を向けさせる autoJA('Animated Flourish', '') elseif FM >= 1 and play.vitals.tp < setting.wstp and setting.flourish1 == 'desperate' and RID_DESPERATE and recast[RID_DESPERATE] == 0 then -- Dフラリッシュ:対象をヘヴィ状態にする autoJA('Desperate Flourish', '') end end end end end) function get_member_buffs(member) if not (member.mob and member.mob.buffs) then return S{} end return to_buff_set(member.mob.buffs) end local ailment_names = S{'paralysis','blind','disease','slow','poison','silence','plague','curse'} function has_ailment(b) for ailment in ailment_names:it() do if b[ailment] then return true end end return false end function waltz(play, recast, buffs) if (play.main_job ~= 'DNC' and play.sub_job ~= 'DNC') or (buffs.invisible) then return false else end -- 剣の舞い(Saber Dance)効果中はワルツが一切使用できない if buffs['saber dance'] then return false end -- 扇の舞い(Fan Dance)中は防御優先:早めのタイミングで自己ワルツを撃つ local effective_waltzhp = setting.waltzhp if buffs['fan dance'] then effective_waltzhp = math.min(setting.waltzhp + 15, 95) end if setting.waltz == 1 and play.vitals.hpp < effective_waltzhp and play.vitals.tp >= 500 and (RID_CURINGWALTZ and recast[RID_CURINGWALTZ]) == 0 then autoJA('Curing Waltz III', '') return true elseif setting.waltzpt == 1 then -- 何人が回復対象になっているか数える。2人以上いるならディバインワルツ(範囲回復)の方がTP効率が良い local hurt_count = 0 for i,member in pairs(windower.ffxi.get_party()) do if type(member) == 'table' and member.mob and member.hpp ~= 0 and member.hpp < setting.waltzhp then hurt_count = hurt_count + 1 end end if hurt_count >= 2 then if play.vitals.tp >= 80 and RID_DIVINEWALTZ2 and recast[RID_DIVINEWALTZ2] == 0 then autoJA('Divine Waltz II', '') return true elseif play.vitals.tp >= 40 and RID_DIVINEWALTZ and recast[RID_DIVINEWALTZ] == 0 then autoJA('Divine Waltz', '') return true end end for i,member in pairs(windower.ffxi.get_party()) do if type(member) == 'table' and member.mob and member.hpp ~= 0 then -- HPに応じた最適な段階から、リキャスト中なら1段階ずつ下げて空いている技を探す -- (2019年1月の仕様変更で各段階は独立したリキャストを持つため、決め打ちで同じ段階を連打しない) if play.main_job == 'DNC' and member.hpp < 30 and play.vitals.tp >= 800 and RID_CW5 and recast[RID_CW5] == 0 then autoJA('Curing Waltz V', member.mob.name) return true elseif play.main_job == 'DNC' and member.hpp < 50 and play.vitals.tp >= 650 and RID_CW4 and recast[RID_CW4] == 0 then autoJA('Curing Waltz IV', member.mob.name) return true elseif member.hpp < setting.waltzhp and play.vitals.tp >= 500 and RID_CURINGWALTZ and recast[RID_CURINGWALTZ] == 0 then autoJA('Curing Waltz III', member.mob.name) return true elseif member.hpp < setting.waltzhp and play.vitals.tp >= 350 and RID_CW2 and recast[RID_CW2] == 0 then autoJA('Curing Waltz II', member.mob.name) return true elseif member.hpp < setting.waltzhp and play.vitals.tp >= 200 and RID_CURINGWALTZ and recast[RID_CURINGWALTZ] == 0 then autoJA('Curing Waltz', member.mob.name) return true end end end end -- ヒーリングワルツ:自分優先、次にパーティメンバー(同一PC上の複垢キャラなど、 -- バフ情報が取得できる相手のみ状態異常を検知可能) if play.vitals.tp >= 200 and setting.na == 1 and (RID_HEALINGWALTZ and recast[RID_HEALINGWALTZ]) == 0 then if has_ailment(buffs) then autoJA('Healing Waltz', '') return true end for i,member in pairs(windower.ffxi.get_party()) do if type(member) == 'table' and member.mob and member.hpp ~= 0 and member.mob.name ~= play.name then local mbuffs = get_member_buffs(member) if has_ailment(mbuffs) then autoJA('Healing Waltz', member.mob.name) return true end end end end return false end function autoJA(str,ta) windower.send_command('input /ja "%s" %s':format(str,ta)) delay = JA_del end function autoWS(str) windower.send_command('input /ws "%s" ':format(str)) delay = WS_del end windower.register_event('incoming chunk', function(id, data) if id == 0x029 then action_message = packets.parse('incoming', data) if (action_message['Message'] == 6) or (action_message['Message'] == 20) then for k, v in ipairs(staggered) do if v == action_message['Target'] then windower.send_ipc_message('rem'..action_message['Target']) table.remove(staggered, k) target_change() end end end end end) function proc_table(msg) local id = msg:slice(4) local msg = tonumber(msg:slice(1, 3)) if msg == 'add' then for k,v in ipairs(staggered) do if v == id then return end end table.insert(staggered, id) elseif msg == 'rem' then for k,v in ipairs(staggered) do if v == id then table.remove(staggered, k) end end end target_change() end windower.register_event('ipc message', proc_table) function get_recast_id(name) local ja = res.job_abilities:with('en', name) return ja and ja.recast_id end windower.register_event('load',function () staggered = {} SABER_ID = get_recast_id('Saber Dance') FAN_ID = get_recast_id('Fan Dance') RID_HASTESAMBA = get_recast_id('Haste Samba') RID_DRAINSAMBA = get_recast_id('Drain Samba III') or get_recast_id('Drain Samba') RID_CLIMACTIC = get_recast_id('Climactic Flourish') RID_TERNARY = get_recast_id('Ternary Flourish') RID_WILD = get_recast_id('Wild Flourish') RID_BOOST = get_recast_id('Boost Flourish') RID_REVERSE = get_recast_id('Reverse Flourish') RID_VIOLENT = get_recast_id('Violent Flourish') RID_ANIMATED = get_recast_id('Animated Flourish') RID_DESPERATE = get_recast_id('Desperate Flourish') RID_STEP_MAIN = get_recast_id('Feather Step') RID_STEP_SUB = get_recast_id('Box Step') RID_PRESTO = get_recast_id('Presto') RID_NOFOOT = get_recast_id('No Foot Rise') RID_BULLY = get_recast_id('Bully') RID_CURINGWALTZ = get_recast_id('Curing Waltz') RID_CW2 = get_recast_id('Curing Waltz II') RID_CW3 = get_recast_id('Curing Waltz III') RID_CW4 = get_recast_id('Curing Waltz IV') RID_CW5 = get_recast_id('Curing Waltz V') RID_DIVINEWALTZ = get_recast_id('Divine Waltz') RID_DIVINEWALTZ2 = get_recast_id('Divine Waltz II') RID_HEALINGWALTZ = get_recast_id('Healing Waltz') windower.text.create('proc_box') windower.text.set_bg_color('proc_box',200,30,30,30) windower.text.set_color('proc_box',255,200,200,200) windower.text.set_location('proc_box',setting.posx,setting.posy) windower.text.set_bg_visibility('proc_box',1) windower.text.set_font('proc_box','Arial') windower.text.set_font_size('proc_box',10) -- 表示位置も、既存のsettings.xmlに古い座標が残っていてもここで強制的に初期値へ戻す -- (動作確認が取れている座標から始めて、あとはドラッグで好きな位置に動かしてもらう想定) setting.statusposx = 100 setting.statusposy = 100 -- AutoDNCのON/OFF状態を常時画面表示する枠(textsライブラリ使用、ドラッグで移動可能) status_box = texts.new('', { pos = {x = setting.statusposx, y = setting.statusposy}, text = {size = 10, font = 'Arial', red = 255, green = 255, blue = 255}, bg = {visible = true, alpha = 120, red = 0, green = 0, blue = 0}, flags = {draggable = true, bold = false} }) status_box:text('AutoDNC status box test') status_box:show() -- 大元のスイッチは、既存のsettings.xmlに古い値(Off)が残っていてもここで強制的にONにする -- (デフォルト値の変更はまだ設定ファイルが無い場合にしか効かないため、既存キャラにも確実に反映させる) setting.actions = 1 setting.ja = 1 setting.ws = 1 setting.autostance = 1 setting:save() windower.add_to_chat(207, 'AutoDNC ['..CLAUDE_BUILD..'] loaded') end) windower.register_event('unload',function () windower.text.delete('proc_box') if status_box then status_box:hide() end end) windower.register_event('zone change',function() staggered = {} end) function addon_message(msg) windower.add_to_chat(207,'%s: %s':format(_addon.name,msg)) end windower.register_event('addon command', function(...) local commands = {...} if not commands[1] then return end commands[1] = commands[1]:lower() if commands[1] == 'on' then setting.actions = 1 addon_message('Actions On') elseif commands[1] == 'off' then setting.actions = 0 addon_message('Actions Off') elseif commands[1] == 'load' then setting = config.load(defaults) addon_message('Global Settings Loaded.') elseif commands[1]:lower() == 'save' then setting:save() addon_message('Settings Saved.') --config.save(setting, 'all') --addon_message('Global Settings Saved.') elseif setting[commands[1]] then if not commands[2] then if _static.dual_toggle[commands[1]] then if setting[commands[1]] == 0 then setting[commands[1]] = 1 addon_message(commands[1]..' is now set to on') else setting[commands[1]] = 0 addon_message(commands[1]..' is now set to off') end elseif _static[commands[1]] then if _static[commands[1]]:find(setting[commands[1]]+1) then setting[commands[1]] = setting[commands[1]]+1 else setting[commands[1]] = 0 end addon_message(commands[1]..' is now set to '..tostring(_static[commands[1]]:find(setting[commands[1]]))) end else commands[2] = tonumber(commands[2]) or commands[2]:lower() if _static[commands[1]] or _static.dual_toggle[commands[1]] then if _static.base[commands[2]] then setting[commands[1]] = _static.base[commands[2]] addon_message(commands[1]..' is now set to '..commands[2]) elseif _static[commands[1]] and _static[commands[1]][commands[2]] then setting[commands[1]] = _static[commands[1]][commands[2]] addon_message(commands[1]..' is now set to '..commands[2]) end elseif type(setting[commands[1]]) == type(commands[2]) then setting[commands[1]] = tonumber(commands[2]) or table.concat(commands, ' ',2):lower() addon_message(commands[1]..' is now set to '..setting[commands[1]]) end -- statusposx/statusposyはリロードしなくてもその場で位置を反映する if (commands[1] == 'statusposx' or commands[1] == 'statusposy') and status_box then status_box:pos(setting.statusposx, setting.statusposy) end if commands[1] == 'proc' then if (commands[2]:lower() == 'ja' or commands[2]:lower() == 'on') then setting.ja = 1 setting.ws = 0 elseif commands[2]:lower() == 'ws' then setting.ws = 1 setting.ja = 0 elseif commands[2]:lower() == 'off' then setting.ja = 0 end end end elseif commands[1] == 'active' then addon_message('['..CLAUDE_BUILD..'] '..(setting.actions == 1 and '[On]' or '[Off]')..' Weapon Skill: '..(setting.ws == 1 and '[On]' or '[Off]')..' Job Ability: ' .. (setting.ja == 1 and '[On]' or '[Off]')..' Proc Mode: '..(setting.proc == 1 and '[Job Ability]' or setting.proc == 2 and '[Weapon Skill]' or setting.proc == 0 and '[Off]')..'\nWeapon Skill: ['..(setting.setws)..'] WS TP: [>'..(setting.wstp)..'] Saber WS TP: [>'..(setting.saberwstp)..'] Wild Flourish: '..(setting.useflourish == 1 and '[On]' or '[Off]')..' Flourish3: ['..(setting.flourish3)..'] Ternary TP: [>'..(setting.ternarytp)..'] HP: [>'..(setting.minwshp)..'% <'..(setting.maxwshp)..'%] Distance: ['..(setting.distance)..']\nWaltz Self: '..(setting.waltz == 1 and '[On]' or '[Off]')..' Party: '..(setting.waltzpt== 1 and '[On]' or '[Off]')..' HP: [<'..(setting.waltzhp)..'%] Healing Waltz: '..(setting.na== 1 and '[On]' or '[Off]')..'\nSamba: '..(setting.samba == 1 and '[Haste Samba]' or setting.samba == 2 and '[Drain Samba]' or setting.samba == 0 and '[Off]')..'\nAuto Stance: '..(setting.autostance == 1 and '[On]' or '[Off]')..' (Saber Dance when not Tank Mode) Heal Mode: '..(setting.healmode == 1 and '[On]' or '[Off]')..' Tank Mode: '..(setting.tankmode == 1 and '[On]' or '[Off]')..' (Fan Dance while Tank Mode) Auto Cancel: '..(setting.autocancel == 1 and '[On]' or '[Off]')..' (HP<='..(setting.cancelhp)..'%: cancel Saber Dance) Saber Pattern: ['..(setting.saberpattern)..'] Attack Mode: '..(setting.attackmode == 1 and '[On]' or '[Off]')) elseif commands[1] == 'eval' then assert(loadstring(table.concat(commands, ' ',2)))() elseif commands[1] == 'help' then if not commands[2] then addon_message('//dnc help command.\nCommands: on | off | actions | ws | ja | samba | waltz | distance\nCommands: minwshp | maxwshp | setws | proc | active | save | load') elseif commands[2]:lower() == 'on' then addon_message('//dnc on - Turn Local Actions On') elseif commands[2]:lower() == 'off' then addon_message('//dnc off - Turn Local Actions Off') elseif commands[2]:lower() == 'actions' then addon_message('actions [ on | off ] - Toggle Global Actions') elseif commands[2]:lower() == 'ws' then addon_message('ws [ on | off ] - Toggle Weapon Skill\n//dnc ws | //dnc ws on //dnc ws off') elseif commands[2]:lower() == 'ja' then addon_message('ja [ on | off ] - Toggle Job Abilities\n//dnc ja | //dnc ja on | //dnc ja off') elseif commands[2]:lower() == 'samba' then addon_message('samba [ haste | drain | off ] -- Toggle samba Mode. Default: Haste samba') elseif commands[2]:lower() == 'waltz' then addon_message('waltz [ on | off | me | pt | na | hp [number]--]\nwaltz me [ on | off ] - Toggle waltz for self\nwaltz pt [ on | off ] - Toggle waltz for party\nwaltz na [ on | off ] - Toggle Healing waltz\nwaltz [ hp ] - Set HP% Threshold for waltzes\n//dnc waltzhp 75 - Default: 75') elseif commands[2]:lower() == 'set' then addon_message('setws [ weapon skill name ] - Set Weapon Skill\n//dnc setws Rudra\'s Storm') elseif commands[2]:lower() == 'maxwshp' then addon_message('maxwshp [ hpp ] - //dnc maxwshp 100 - Sets Max Mob WS HP%.') elseif commands[2]:lower() == 'minwshp' then addon_message('minwshp [ hpp ] - //dnc minwshp 30 - Sets Min Mob WS HP%.') elseif commands[2]:lower() == 'proc' then addon_message('proc [ ja | ws | off ] - Sets automatic stagger mode') elseif commands[2]:lower() == 'active' then addon_message('active [ actions | ws | ja | samba | min/maxwshp | setws | waltz | proc | silent ]- Displays Active setting.') end end end)