diff --git a/.devcontainer/install-deps.sh b/.devcontainer/install-deps.sh new file mode 100755 index 0000000..17c3e34 --- /dev/null +++ b/.devcontainer/install-deps.sh @@ -0,0 +1,9 @@ +env +echo `pwd` + +# Ruby requirements. Why won't mise install these for me? +sudo apt update && sudo apt install -y zlib1g-dev libssl-dev libffi-dev libyaml-dev + +curl https://mise.run | sh +echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc +mise install \ No newline at end of file diff --git a/public/examples/deathknight/frost.rb b/public/examples/deathknight/frost.rb new file mode 100644 index 0000000..9129b19 --- /dev/null +++ b/public/examples/deathknight/frost.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# --- +# title: 'Death Knight: Frost (11.2)' +# --- + +title 'Frost Death Knight WhackAura' +load spec: :frost_death_knight +hide_ooc! + +dynamic_group 'Frost DK Rotation' do + offset y: -100 + + action_usable 'Obliterate', if_stacks: { 'Killing Machine' => '>= 2' } do + glow! + end + + action_usable 'Howling Blast', requires: { auras: ['Rime'] } do + glow! + end + + action_usable 'Frost Strike', if_stacks: { 'Razorice' => '>= 5' } + action_usable 'Frost Strike' + action_usable 'Obliterate', if_stacks: { 'Killing Machine' => '1' } + action_usable 'Obliterate' + action_usable 'Empower Rune Weapon' +end + +dynamic_group 'Frost DK AoE' do + offset y: -140 + + action_usable 'Frostscythe', if_stacks: { 'Killing Machine' => '>= 1' } + action_usable 'Glacial Advance' +end + +dynamic_group 'Frost DK Cooldowns' do + offset y: -40 + + action_usable 'Pillar of Frost' do + glow! + end + action_usable "Reaper's Mark" + action_usable "Frostwyrm's Fury" + action_usable 'Breath of Sindragosa' + action_usable 'Abomination Limb' +end \ No newline at end of file diff --git a/public/examples/mage/frost.rb b/public/examples/mage/frost.rb index 582a26e..98b918b 100644 --- a/public/examples/mage/frost.rb +++ b/public/examples/mage/frost.rb @@ -9,6 +9,20 @@ hide_ooc! dynamic_group 'Frost Mage WhackAuras' do + icon 'Ray of Frost' do + action_usable! do + aura 'Cryopathy' do + stacks '>= 2' do + glow! + end + end + end + end + + icon 'Ring of Fire' do + action_usable! + end + action_usable 'Comet Storm' action_usable 'Glacial Spike' do glow! diff --git a/public/examples/paladin/retribution.rb b/public/examples/paladin/retribution.rb index 10134f1..8bbad81 100644 --- a/public/examples/paladin/retribution.rb +++ b/public/examples/paladin/retribution.rb @@ -13,7 +13,12 @@ scale 0.8 action_usable 'Wake of Ashes' action_usable 'Judgement' - action_usable 'Blade of Justice' + icon 'Blade of Justice' do + action_usable + action_usable spell_count: '>= 2' do + glow! + end + end action_usable 'Final Verdict' action_usable 'Bladestorm' action_usable 'Divine Toll' diff --git a/public/examples/priest/shadow.rb b/public/examples/priest/shadow.rb index 696f2ba..37ebaf9 100644 --- a/public/examples/priest/shadow.rb +++ b/public/examples/priest/shadow.rb @@ -8,17 +8,37 @@ load spec: :shadow_priest hide_ooc! -dynamic_group 'WhackAuras' do +dynamic_group 'Shadow Stay Big' do + scale 0.6 + offset y: -40, x: 80 + + action_usable 'Void Eruption' + action_usable 'Power Infusion' + action_usable 'Shadowfiend' +end + +dynamic_group 'Shadow Stay Small' do + scale 0.6 + offset y: -40, x: -80 + + action_usable 'Psyfiend' + action_usable 'Void Torrent' + action_usable 'Halo' +end + +dynamic_group 'Shadow WhackAuras' do + scale 0.8 + offset y: -70 + debuff_missing 'Shadow Word: Pain' debuff_missing 'Vampiric Touch' + action_usable 'Shadow Crash' do + glow! + end action_usable 'Mind Blast' - action_usable 'Void Torrent' action_usable 'Mindbender' - action_usable 'Halo' - action_usable 'Void Eruption' action_usable 'Shadow Word: Death' - action_usable 'Shadow Crash' action_usable 'Devouring Plague' action_usable 'Mind Flay: Insanity' end diff --git a/public/examples/warrior/arms.rb b/public/examples/warrior/arms.rb index a561242..2695c72 100644 --- a/public/examples/warrior/arms.rb +++ b/public/examples/warrior/arms.rb @@ -4,32 +4,41 @@ # title: 'Warrior: Arms' # --- +title 'Arms Warrior' load spec: :arms_warrior hide_ooc! -dynamic_group 'Arms WhackAuras' do - action_usable 'Colossus Smash' - # action_usable 'Warbreaker' - action_usable 'Execute' +dynamic_group 'Arms Stay Big' do + scale 0.7 + offset y: -40, x: 60 + + action_usable 'Avatar' action_usable 'Bladestorm' - # TODO: cleave instead of MS display when more than N targets? - # action_usable 'Cleave' - # action_usable 'Whirlwind' +end + +dynamic_group 'Arms Stay Small' do + scale 0.7 + offset y: -40, x: -60 + + action_usable 'Recklessness' action_usable 'Thunderous Roar' + action_usable 'Colossus Smash' +end - # TODO: add `stacks` to glow! instead - # Min-maxing OP>MS is not recommended. - # action_usable 'Mortal Strike', if_stacks: { 'Overpower' => 2 } do - # glow! - # end - # action_usable 'Overpower' do - # glow! charges: 2 - # end +dynamic_group 'Arms WhackAuras' do + scale 0.8 + offset y: -80 + + action_usable 'Skullsplitter' + action_usable 'Colossus Smash' + action_usable 'Execute' do + glow! # todo: glow on sudden death only + end + action_usable 'Bladestorm' + action_usable 'Wrecking Throw' + action_usable 'Cleave' action_usable ['Mortal Strike', 'Overpower'] - action_usable 'Thunder Clap', requires: { target_debuffs_missing: ['Rend'] } + action_usable 'Rend', requires: { target_debuffs_missing: ['Rend'] } action_usable 'Sweeping Strikes' - action_usable 'Avatar' do - glow! - end -end +end \ No newline at end of file diff --git a/public/examples/warrior/protection.rb b/public/examples/warrior/protection.rb index abaf113..ce34b1c 100644 --- a/public/examples/warrior/protection.rb +++ b/public/examples/warrior/protection.rb @@ -8,7 +8,28 @@ load spec: :protection_warrior hide_ooc! +dynamic_group 'Prot Stay Big' do + scale 0.7 + offset y: -40, x: 60 + + action_usable 'Avatar' + action_usable "Champion's Spear" + action_usable 'Shield Wall' + action_usable 'Last Stand' +end + +dynamic_group 'Prot Stay Small' do + scale 0.7 + offset y: -40, x: -60 + + action_usable 'Thunderous Roar' + action_usable 'Demolish' +end + dynamic_group 'Prot WhackAuras' do + scale 0.8 + offset y: -80 + action_usable 'Revenge' action_usable 'Shield Slam' action_usable 'Shield Block' diff --git a/public/index.json b/public/index.json index cda15ec..d0e9bfe 100644 --- a/public/index.json +++ b/public/index.json @@ -15,12 +15,12 @@ "/examples/shaman/elemental.rb": { "title": "Shaman: Elemental" }, - "/examples/priest/shadow.rb": { - "title": "Priest: Shadow" - }, "/examples/rogue/outlaw.rb": { "title": "Rogue: Outlaw" }, + "/examples/priest/shadow.rb": { + "title": "Priest: Shadow" + }, "/examples/paladin/retribution.rb": { "title": "Paladin: Retribution" }, @@ -35,6 +35,9 @@ }, "/examples/demonhunter/havoc.rb": { "title": "Demon Hunter: Havoc" + }, + "/examples/deathknight/frost.rb": { + "title": "Death Knight: Frost (11.2)" } }, "examples": [ @@ -43,13 +46,14 @@ "/examples/warrior/arms.rb", "/examples/shaman/restoration.rb", "/examples/shaman/elemental.rb", - "/examples/priest/shadow.rb", "/examples/rogue/outlaw.rb", + "/examples/priest/shadow.rb", "/examples/paladin/retribution.rb", "/examples/paladin/protection.rb", "/examples/mage/frost.rb", "/examples/hunter/beastmastery.rb", - "/examples/demonhunter/havoc.rb" + "/examples/demonhunter/havoc.rb", + "/examples/deathknight/frost.rb" ], "lua": [ "/lua/inspect.lua", diff --git a/public/node.rb b/public/node.rb index ed5e818..a3ea42c 100644 --- a/public/node.rb +++ b/public/node.rb @@ -58,16 +58,20 @@ class Node # rubocop:disable Style/Documentation,Metrics/ClassLength include Casting::Client delegate_missing_methods - attr_accessor :uid, :children, :controlled_children, :parent, :triggers, :actions, :type, :options + attr_accessor :uid, :children, :controlled_children, :parent, :triggers, :trigger_options, :actions, :type, :options attr_reader :conditions - def initialize(id: nil, type: nil, parent: nil, triggers: [], actions: { start: [], init: [], finish: [] }, &block) # rubocop:disable Metrics/MethodLength + def initialize(id: nil, type: nil, parent: nil, triggers: [], trigger_options: nil, actions: { start: [], init: [], finish: [] }, &block) # rubocop:disable Metrics/MethodLength,Metrics/ParameterLists,Layout/LineLength @uid = Digest::SHA1.hexdigest([id, parent, triggers, actions].to_json)[0..10] - @id = "#{id} (#{@uid})" + @id = id @parent = parent @children = [] @controlled_children = [] @triggers = triggers + @trigger_options = trigger_options || { + disjunctive: 'any', + activeTriggerMode: -10 + } @actions = actions @conditions = [] @type = type @@ -138,7 +142,9 @@ def make_triggers(requires, if_missing: [], if_stacks: {}, triggers: []) # ruboc end def map_triggers(triggers) - Hash[*triggers.each_with_index.to_h { |trigger, index| [index + 1, trigger.as_json] }.flatten] + Hash[*triggers.each_with_index.to_h do |trigger, index| + [index + 1, trigger.as_json] + end.flatten].merge(trigger_options) end def load(spec: nil) # rubocop:disable Metrics/MethodLength @@ -182,8 +188,9 @@ def dynamic_group(name, **kwargs, &block) end def icon(*args, **kwargs, &block) - kwargs = { parent: self, type: type }.merge(kwargs) - icon = WeakAura::Icon.new(*args, **kwargs, &block) + args = { id: args[0] } if args[0].is_a?(String) + kwargs = { parent: self, type: type }.merge(args).merge(kwargs) + icon = WeakAura::Icon.new(**kwargs, &block) add_node(icon) end @@ -195,7 +202,7 @@ def add_node(node) node end - def glow!(options = {}) # rubocop:disable Metrics/MethodLength + def glow!(**options) # rubocop:disable Metrics/MethodLength raise 'glow! only supports a single check, use multiple `glow!` calls for multiple checks.' if options.keys.size > 1 check = [] @@ -208,15 +215,16 @@ def glow!(options = {}) # rubocop:disable Metrics/MethodLength end if options[:charges] + charges_value, charges_op = parse_operator(options[:charges]) check = { - "variable": 'charges', - "op": '==', - "value": options[:charges].to_s, - "trigger": 1 + 'variable' => 'charges', + 'op' => charges_op, + 'value' => charges_value.to_s, + 'trigger' => 1 } end - @conditions ||= {} + @conditions ||= [] @conditions << { check: check, changes: [ @@ -228,8 +236,28 @@ def glow!(options = {}) # rubocop:disable Metrics/MethodLength } end + def aura(name, **options, &block) + # Adds an aura trigger for conditional logic + options[:parent_node] = self + trigger = Trigger::Auras.new(aura_names: name, **options) + triggers << trigger + + # Executes block in context of trigger for nested conditions + trigger.instance_eval(&block) if block_given? + trigger + end + + def parse_operator(value) + return [value, '=='] if value.is_a?(Integer) + + value_str = value.to_s + operator = value_str.match(/^[<>!=]+/)&.[](0) || '==' + parsed_value = value_str.gsub(/^[<>!=]+\s*/, '').to_i + [parsed_value, operator] + end + def hide_ooc! # rubocop:disable Metrics/MethodLength - @conditions ||= {} + @conditions ||= [] @conditions << { check: { trigger: -1, @@ -245,7 +273,11 @@ def hide_ooc! # rubocop:disable Metrics/MethodLength end def as_json - { load: load, triggers: triggers, actions: actions, conditions: conditions, + { id: "#{id} (#{@uid})", + load: load, + triggers: triggers.is_a?(Hash) ? triggers : map_triggers(triggers), + actions: actions, + conditions: conditions, tocversion: TOC_VERSION } end end diff --git a/public/node_spec.rb b/public/node_spec.rb index 30d2c23..43f3af4 100644 --- a/public/node_spec.rb +++ b/public/node_spec.rb @@ -3,6 +3,97 @@ require './spec/spec_helper' RSpec.describe Node do + describe '#as_json' do + it 'maps triggers to a hash if they are still an array' do + node = Node.new + trigger = { test: 'test' } + expect(trigger).to receive(:as_json).and_return(trigger) + node.triggers = [trigger] + hash = node.as_json + expect(hash[:triggers]).to be_a(Hash) + expect(hash[:triggers][1]).to eq(trigger) + end + end + + describe '#icon' do + it 'should accept a string and default id to it' do + node = Node.new + icon = node.icon 'Test' + expect(icon.id).to eq('Test') + end + end + + describe '#parse_operator' do + it 'parses operators from string values' do + node = Node.new + expect(node.parse_operator('>= 5')).to eq([5, '>=']) + expect(node.parse_operator('< 3')).to eq([3, '<']) + expect(node.parse_operator('== 2')).to eq([2, '==']) + expect(node.parse_operator('!= 4')).to eq([4, '!=']) + expect(node.parse_operator('10')).to eq([10, '==']) + expect(node.parse_operator(7)).to eq([7, '==']) + end + end + + describe '#aura' do + it 'creates an aura trigger and adds it to triggers' do + node = Node.new + trigger = node.aura('Shadow Word: Pain') + expect(node.triggers).to include(trigger) + expect(trigger).to be_a(Trigger::Auras) + end + + it 'passes parent_node context to the trigger' do + node = Node.new + trigger = node.aura('Shadow Word: Pain') + expect(trigger.options[:parent_node]).to eq(node) + end + + it 'executes block in trigger context' do + node = Node.new + block_executed = false + node.aura('Shadow Word: Pain') do + block_executed = true + end + expect(block_executed).to be true + end + end + + describe '#glow!' do + it 'adds a condition for glowing' do + node = Node.new + node.glow! + expect(node.conditions).not_to be_empty + expect(node.conditions.first[:changes]).to include( + hash_including(property: 'sub.3.glow', value: true) + ) + end + + it 'supports charges condition' do + node = Node.new + node.glow!(charges: '>= 2') + condition = node.conditions.first[:check] + expect(condition['variable']).to eq('charges') + expect(condition['op']).to eq('>=') + expect(condition['value']).to eq('2') + end + end + + describe '#hide_ooc!' do + it 'adds a condition to hide out of combat' do + node = Node.new + node.hide_ooc! + expect(node.conditions).not_to be_empty + condition = node.conditions.first + expect(condition[:check][:trigger]).to eq(-1) + expect(condition[:check][:variable]).to eq('incombat') + expect(condition[:check][:value]).to eq(0) + expect(condition[:changes]).to include( + hash_including(property: 'alpha') + ) + end + end + describe 'option' do it 'allows setting and modifying the default' do Node.option :foo, default: 'bar' diff --git a/public/weak_aura/icon.rb b/public/weak_aura/icon.rb index 4e57d37..e1c65b4 100644 --- a/public/weak_aura/icon.rb +++ b/public/weak_aura/icon.rb @@ -2,11 +2,22 @@ class WeakAura class Icon < Node # rubocop:disable Metrics/ClassLength,Style/Documentation - def as_json # rubocop:disable Metrics/MethodLength + def all_triggers! + trigger_options.merge!({ disjunctive: 'all' }) + end + + def action_usable!(**kwargs, &block) + kwargs = { spell: id, parent_node: self }.merge(kwargs) + trigger = Trigger::ActionUsable.new(**kwargs) + triggers << trigger + trigger.instance_eval(&block) if block_given? + end + + def as_json # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity super.merge( { - width: parent.options[:icon_width] || 64, - height: parent.options[:icon_height] || parent.options[:icon_width] || 64, + width: parent&.options&.[](:icon_width) || 64, + height: parent&.options&.[](:icon_height) || parent&.options&.[](:icon_width) || 64, iconSource: -1, authorOptions: [], yOffset: 0, @@ -116,7 +127,7 @@ def as_json # rubocop:disable Metrics/MethodLength xOffset: 0, uid: uid, inverse: false, - parent: parent.id, + parent: parent&.id, conditions: conditions, information: [] } diff --git a/public/weak_aura/icon_spec.rb b/public/weak_aura/icon_spec.rb new file mode 100644 index 0000000..6b4df40 --- /dev/null +++ b/public/weak_aura/icon_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require './spec/spec_helper' + +RSpec.describe WeakAura::Icon do + describe '#all_triggers!' do + it 'sets trigger disjunctive to all' do + icon = WeakAura::Icon.new(id: 'Test') + icon.all_triggers! + expect(icon.trigger_options[:disjunctive]).to eq('all') + end + end + + describe 'action_usable!' do + it 'adds an ActionUsable trigger that defaults to the icons name' do + icon = WeakAura::Icon.new(id: 'Rampage') do + action_usable! + end.as_json + trigger = icon[:triggers][1][:trigger] + expect(trigger[:spellName]).to eq('Rampage') + end + + it 'passes on named arguments' do + icon = WeakAura::Icon.new(id: 'Rampage') do + action_usable! spell_count: '>= 2' do + glow! + end + end.as_json + trigger = icon[:triggers][1][:trigger] + expect(trigger[:spellCount]).to eq('2') + end + + it 'passes parent_node context to trigger' do + icon = WeakAura::Icon.new(id: 'Test') + icon.action_usable! + trigger = icon.triggers.last + expect(trigger.options[:parent_node]).to eq(icon) + end + + it 'executes block in trigger context' do + icon = WeakAura::Icon.new(id: 'Test') + block_executed = false + icon.action_usable! do + block_executed = true + end + expect(block_executed).to be true + end + end +end diff --git a/public/weak_aura/triggers.rb b/public/weak_aura/triggers.rb index b632180..cae1f98 100644 --- a/public/weak_aura/triggers.rb +++ b/public/weak_aura/triggers.rb @@ -9,6 +9,7 @@ def initialize(**options) event: 'Action Usable', spell_name: options[:spell] }.merge(options) + @parent_node = @options[:parent_node] end def parse_count_operator(count, default_operator = '==') @@ -18,6 +19,34 @@ def parse_count_operator(count, default_operator = '==') count = count.to_s.gsub(/^[<>!=]+/, '').to_i [count, operator] end + + def charges(count_op, &block) + @options[:charges] = count_op + + # Create a context for conditional logic + if block_given? + instance_eval(&block) + end + end + + def stacks(count_op, &block) + @options[:stacks] = count_op + + # Create a context for conditional logic + if block_given? + instance_eval(&block) + end + end + + def glow!(**options) + # Forward glow! to parent node if available + @parent_node.glow!(**options) if @parent_node&.respond_to?(:glow!) + end + + def remaining_time(count_op, &block) + @options[:remaining_time] = count_op + block.call if block_given? + end end end diff --git a/public/weak_aura/triggers/action_usable.rb b/public/weak_aura/triggers/action_usable.rb index 38b74cb..e099b9a 100644 --- a/public/weak_aura/triggers/action_usable.rb +++ b/public/weak_aura/triggers/action_usable.rb @@ -30,24 +30,29 @@ def as_json # rubocop:disable Metrics/MethodLength } if options[:spell_count] - spell_count_operator = options[:spell_count].to_s.match(/[<>=]+/)&.[](0) || '==' - spell_count = if options[:spell_count].is_a?(Numeric) - options[:spell_count] - else - options[:spell_count] - .match(/[0-9]+/)&.[](0) - end.to_i - + spell_count, spell_count_operator = parse_count_operator(options[:spell_count], '==') if spell_count trigger .merge!({ - spellCount: spell_count, + spellCount: spell_count.to_s, use_spellCount: true, spellCount_operator: spell_count_operator }) end end + if options[:charges] + charges, charges_operator = parse_count_operator(options[:charges], '==') + if charges + trigger + .merge!({ + charges: charges.to_s, + use_charges: true, + charges_operator: charges_operator + }) + end + end + { trigger: trigger } diff --git a/public/weak_aura/triggers/action_usable_spec.rb b/public/weak_aura/triggers/action_usable_spec.rb index 7007dfc..bfc902b 100644 --- a/public/weak_aura/triggers/action_usable_spec.rb +++ b/public/weak_aura/triggers/action_usable_spec.rb @@ -3,15 +3,105 @@ require './spec/spec_helper' RSpec.describe Trigger::ActionUsable do - it 'should accept spell_count and default to the equality operator' do - trigger = Trigger::ActionUsable.new(spell_count: 1).as_json[:trigger] - expect(trigger[:spellCount]).to eq(1) - expect(trigger[:spellCount_operator]).to eq('==') + describe '#initialize' do + it 'sets spell_name from spell option' do + trigger = Trigger::ActionUsable.new(spell: 'Fireball') + expect(trigger.options[:spell_name]).to eq('Fireball') + expect(trigger.options[:spell]).to eq('Fireball') + end + + it 'preserves spell_name if explicitly provided' do + trigger = Trigger::ActionUsable.new(spell: 'Fireball', spell_name: 'Custom Name') + expect(trigger.options[:spell_name]).to eq('Custom Name') + expect(trigger.options[:spell]).to eq('Fireball') + end + + it 'defaults exact to false' do + trigger = Trigger::ActionUsable.new(spell: 'Fireball') + expect(trigger.options[:exact]).to eq(false) + end + + it 'allows overriding exact option' do + trigger = Trigger::ActionUsable.new(spell: 'Fireball', exact: true) + expect(trigger.options[:exact]).to eq(true) + end end - it 'should accept spell_count w/ gte operator' do - trigger = Trigger::ActionUsable.new(spell_count: '>= 1').as_json[:trigger] - expect(trigger[:spellCount]).to eq(1) - expect(trigger[:spellCount_operator]).to eq('>=') + describe '#as_json' do + it 'generates correct base trigger structure' do + trigger = Trigger::ActionUsable.new(spell: 'Mortal Strike').as_json[:trigger] + + expect(trigger[:type]).to eq('spell') + expect(trigger[:event]).to eq('Action Usable') + expect(trigger[:spellName]).to eq('Mortal Strike') + expect(trigger[:realSpellName]).to eq('Mortal Strike') + expect(trigger[:use_spellName]).to eq(true) + expect(trigger[:use_exact_spellName]).to eq(false) + expect(trigger[:use_genericShowOn]).to eq(true) + expect(trigger[:genericShowOn]).to eq('showOnCooldown') + expect(trigger[:unit]).to eq('player') + expect(trigger[:use_track]).to eq(true) + expect(trigger[:debuffType]).to eq('HELPFUL') + end + + it 'respects exact option for spell name matching' do + trigger = Trigger::ActionUsable.new(spell: 'Mortal Strike', exact: true).as_json[:trigger] + expect(trigger[:use_exact_spellName]).to eq(true) + end + + it 'handles spell_count with default equality operator' do + trigger = Trigger::ActionUsable.new(spell_count: 1).as_json[:trigger] + expect(trigger[:spellCount]).to eq('1') + expect(trigger[:use_spellCount]).to eq(true) + expect(trigger[:spellCount_operator]).to eq('==') + end + + it 'handles spell_count with custom operator' do + trigger = Trigger::ActionUsable.new(spell_count: '>= 1').as_json[:trigger] + expect(trigger[:spellCount]).to eq('1') + expect(trigger[:use_spellCount]).to eq(true) + expect(trigger[:spellCount_operator]).to eq('>=') + end + + it 'handles charges with default equality operator' do + trigger = Trigger::ActionUsable.new(charges: 2).as_json[:trigger] + expect(trigger[:charges]).to eq('2') + expect(trigger[:use_charges]).to eq(true) + expect(trigger[:charges_operator]).to eq('==') + end + + it 'handles charges with custom operator' do + trigger = Trigger::ActionUsable.new(charges: '< 3').as_json[:trigger] + expect(trigger[:charges]).to eq('3') + expect(trigger[:use_charges]).to eq(true) + expect(trigger[:charges_operator]).to eq('<') + end + + it 'omits spell_count fields when not provided' do + trigger = Trigger::ActionUsable.new(spell: 'Test').as_json[:trigger] + expect(trigger).not_to have_key(:spellCount) + expect(trigger).not_to have_key(:use_spellCount) + expect(trigger).not_to have_key(:spellCount_operator) + end + + it 'omits charges fields when not provided' do + trigger = Trigger::ActionUsable.new(spell: 'Test').as_json[:trigger] + expect(trigger).not_to have_key(:charges) + expect(trigger).not_to have_key(:use_charges) + expect(trigger).not_to have_key(:charges_operator) + end + + it 'handles both spell_count and charges together' do + trigger = Trigger::ActionUsable.new( + spell: 'Test', + spell_count: '>= 2', + charges: '< 3' + ).as_json[:trigger] + + expect(trigger[:spellCount]).to eq('2') + expect(trigger[:spellCount_operator]).to eq('>=') + expect(trigger[:charges]).to eq('3') + expect(trigger[:charges_operator]).to eq('<') + end end end diff --git a/public/weak_aura/triggers_spec.rb b/public/weak_aura/triggers_spec.rb new file mode 100644 index 0000000..522857f --- /dev/null +++ b/public/weak_aura/triggers_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require './spec/spec_helper' + +RSpec.describe Trigger::Base do + describe '#charges' do + it 'sets charges option' do + trigger = Trigger::Base.new + trigger.charges('>= 2') + expect(trigger.options[:charges]).to eq('>= 2') + end + + it 'executes block in trigger context' do + trigger = Trigger::Base.new + block_executed = false + trigger.charges(2) do + block_executed = true + end + expect(block_executed).to be true + end + end + + describe '#stacks' do + it 'sets stacks option' do + trigger = Trigger::Base.new + trigger.stacks('>= 5') + expect(trigger.options[:stacks]).to eq('>= 5') + end + + it 'executes block in trigger context' do + trigger = Trigger::Base.new + block_executed = false + trigger.stacks(3) do + block_executed = true + end + expect(block_executed).to be true + end + end + + describe '#glow!' do + it 'forwards glow to parent node if available' do + parent = Node.new + trigger = Trigger::Base.new(parent_node: parent) + + expect(parent).to receive(:glow!).with(charges: 2) + trigger.glow!(charges: 2) + end + + it 'does not error when no parent node' do + trigger = Trigger::Base.new + expect { trigger.glow! }.not_to raise_error + end + end + + describe '#remaining_time' do + it 'sets remaining_time option' do + trigger = Trigger::Base.new + trigger.remaining_time('<= 5') + expect(trigger.options[:remaining_time]).to eq('<= 5') + end + + it 'executes block when provided' do + trigger = Trigger::Base.new + block_executed = false + trigger.remaining_time(10) do + block_executed = true + end + expect(block_executed).to be true + end + + it 'works without a block' do + trigger = Trigger::Base.new + expect { trigger.remaining_time(5) }.not_to raise_error + expect(trigger.options[:remaining_time]).to eq(5) + end + end +end