From ecab5df07f9d3cbc755b8d9b3e712c384cb5ed9d Mon Sep 17 00:00:00 2001 From: Mahtra <93822896+MahtraDR@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:20:27 +1300 Subject: [PATCH] refactor(dr): workorders.lic comprehensive refactor Bug fixes: - Fix NPC walking away during turn-in not handled (add "What is it you're trying to give" pattern to retry loop) - find_npc now returns boolean and verifies NPC presence after walking Code improvements: - Add frozen_string_literal: true - Extract 15 frozen pattern constants (GIVE_LOGBOOK_*, REPAIR_*, BUNDLE_*, WORK_ORDER_*, READ_LOGBOOK_*, COUNT_*, TAP_HERB_*, MATERIAL_NOUNS) - Replace all Regexp.last_match with named captures (WORK_ORDER_ITEM_PATTERN, LOGBOOK_REMAINING_PATTERN, POLISH_COUNT_PATTERN, TAP_HERB_PATTERN, HERB_COUNT_PATTERN, REMEDY_COUNT_PATTERN) - Replace all echo/DRC.message with Lich::Messaging.msg and WorkOrders: prefix - Add verbose messaging on all exit paths - Nil-safe iteration with &. operator in buy_parts/order_parts - Remove unused reget call in enchanting_items Testing: - Add 41 comprehensive specs covering constants, find_npc, complete_work_order, bundle_item, find_recipe, repair_items, buy_parts, order_parts, and pattern matching Co-Authored-By: Claude Opus 4.5 --- spec/workorders_spec.rb | 657 ++++++++++++++++++++++++++++++++++++++++ workorders.lic | 229 +++++++++----- 2 files changed, 811 insertions(+), 75 deletions(-) create mode 100644 spec/workorders_spec.rb diff --git a/spec/workorders_spec.rb b/spec/workorders_spec.rb new file mode 100644 index 0000000000..6b9b66e6b3 --- /dev/null +++ b/spec/workorders_spec.rb @@ -0,0 +1,657 @@ +# frozen_string_literal: true + +require 'ostruct' +require 'time' + +# Load test harness which provides mock game objects +load File.join(File.dirname(__FILE__), '..', 'test', 'test_harness.rb') +include Harness + +# Extract and eval a class from a .lic file without executing top-level code +def load_lic_class(filename, class_name) + return if Object.const_defined?(class_name) + + filepath = File.join(File.dirname(__FILE__), '..', filename) + lines = File.readlines(filepath) + + start_idx = lines.index { |l| l =~ /^class\s+#{class_name}\b/ } + raise "Could not find 'class #{class_name}' in #{filename}" unless start_idx + + # Find the matching 'end' at column 0 (same level as class definition) + end_idx = nil + (start_idx + 1...lines.size).each do |i| + if lines[i] =~ /^end\s*$/ + end_idx = i + break + end + end + raise "Could not find matching end for 'class #{class_name}' in #{filename}" unless end_idx + + class_source = lines[start_idx..end_idx].join + eval(class_source, TOPLEVEL_BINDING, filepath, start_idx + 1) +end + +# Minimal stub modules for game interaction +module DRC + def self.right_hand + $right_hand + end + + def self.left_hand + $left_hand + end + + def self.bput(*_args) + 'Roundtime' + end + + def self.message(*_args); end + + def self.release_invisibility; end + + def self.wait_for_script_to_complete(*_args); end + + def self.fix_standing; end +end + +module DRCC + def self.stow_crafting_item(*_args) + true + end + + def self.get_crafting_item(*_args); end + + def self.find_shaping_room(*_args); end + + def self.find_sewing_room(*_args); end + + def self.find_enchanting_room(*_args); end + + def self.find_empty_crucible(*_args); end + + def self.check_for_existing_sigil?(*_args) + true + end + + def self.order_enchant(*_args); end + + def self.fount(*_args); end + + def self.repair_own_tools(*_args); end +end + +module DRCI + def self.stow_hands; end + + def self.dispose_trash(*_args); end + + def self.get_item(*_args); end + + def self.get_item_if_not_held?(*_args) + true + end + + def self.count_items_in_container(*_args) + 0 + end + + def self.exists?(*_args) + false + end +end + +module DRCT + def self.walk_to(*_args); end + + def self.order_item(*_args); end + + def self.buy_item(*_args); end + + def self.dispose(*_args); end +end + +module DRCM + def self.ensure_copper_on_hand(*_args); end +end + +module DRSkill + def self.getxp(*_args) + 0 + end +end + +module DRRoom + def self.npcs + $room_npcs || [] + end +end + +class Room + def self.current + OpenStruct.new(id: $room_id || 1) + end +end + +module XMLData + def self.room_title + $room_title || '' + end +end + +module Flags + def self.add(*_args); end + def self.delete(*_args); end +end + +module Lich + module Messaging + def self.msg(*_args); end + end + + module Util + def self.issue_command(*_args) + [] + end + end +end + +# Stub for script before_dying +def before_dying(&block) + # No-op for testing +end + +# Global ordinals used by workorders +$ORDINALS = %w[first second third fourth fifth sixth seventh eighth ninth tenth eleventh twelfth thirteenth fourteenth fifteenth sixteenth seventeenth eighteenth nineteenth twentieth].freeze + +# Load WorkOrders class definition (without executing top-level code) +load_lic_class('workorders.lic', 'WorkOrders') + +RSpec.configure do |config| + config.before(:each) do + reset_data if defined?(reset_data) + $right_hand = nil + $left_hand = nil + $room_npcs = [] + $room_id = 1 + $room_title = '' + end +end + +RSpec.describe WorkOrders do + # Allocate a bare instance without calling initialize + let(:workorders) { WorkOrders.allocate } + + before(:each) do + workorders.instance_variable_set(:@settings, OpenStruct.new( + crafting_container: 'backpack', + crafting_items_in_container: [], + hometown: 'Crossing', + default_container: 'backpack', + workorder_min_items: 1, + workorder_max_items: 10 + )) + workorders.instance_variable_set(:@bag, 'backpack') + workorders.instance_variable_set(:@bag_items, []) + workorders.instance_variable_set(:@belt, nil) + workorders.instance_variable_set(:@hometown, 'Crossing') + workorders.instance_variable_set(:@worn_trashcan, nil) + workorders.instance_variable_set(:@worn_trashcan_verb, nil) + workorders.instance_variable_set(:@min_items, 1) + workorders.instance_variable_set(:@max_items, 10) + + # Stub methods that would exit or interact with game + allow(workorders).to receive(:exit) + allow(workorders).to receive(:fput) + allow(workorders).to receive(:pause) + end + + # =========================================================================== + # Constants - verify frozen pattern constants + # =========================================================================== + describe 'constants' do + it 'defines GIVE_LOGBOOK_SUCCESS_PATTERNS as frozen array' do + expect(WorkOrders::GIVE_LOGBOOK_SUCCESS_PATTERNS).to be_frozen + expect(WorkOrders::GIVE_LOGBOOK_SUCCESS_PATTERNS).to include('You hand') + end + + it 'defines GIVE_LOGBOOK_RETRY_PATTERNS as frozen array' do + expect(WorkOrders::GIVE_LOGBOOK_RETRY_PATTERNS).to be_frozen + expect(WorkOrders::GIVE_LOGBOOK_RETRY_PATTERNS).to include("What is it you're trying to give") + end + + it 'defines NPC_NOT_FOUND_PATTERN as frozen string' do + expect(WorkOrders::NPC_NOT_FOUND_PATTERN).to be_frozen + expect(WorkOrders::NPC_NOT_FOUND_PATTERN).to eq("What is it you're trying to give") + end + + it 'defines REPAIR_GIVE_PATTERNS as frozen array' do + expect(WorkOrders::REPAIR_GIVE_PATTERNS).to be_frozen + expect(WorkOrders::REPAIR_GIVE_PATTERNS.length).to eq(6) + end + + it 'defines BUNDLE_SUCCESS_PATTERNS as frozen array' do + expect(WorkOrders::BUNDLE_SUCCESS_PATTERNS).to be_frozen + expect(WorkOrders::BUNDLE_SUCCESS_PATTERNS).to include('You notate the') + end + + it 'defines BUNDLE_FAILURE_PATTERN as frozen regex' do + expect(WorkOrders::BUNDLE_FAILURE_PATTERN).to be_frozen + expect(WorkOrders::BUNDLE_FAILURE_PATTERN).to match('requires items of') + end + + it 'defines WORK_ORDER_ITEM_PATTERN with named captures' do + pattern = WorkOrders::WORK_ORDER_ITEM_PATTERN + match = 'order for leather gloves. I need 3 '.match(pattern) + expect(match).not_to be_nil + expect(match[:item]).to eq('leather gloves') + expect(match[:quantity]).to eq('3') + end + + it 'defines WORK_ORDER_STACKS_PATTERN with named captures' do + pattern = WorkOrders::WORK_ORDER_STACKS_PATTERN + match = 'order for healing salve. I need 2 stacks (5 uses each) of fine quality'.match(pattern) + expect(match).not_to be_nil + expect(match[:item]).to eq('healing salve') + expect(match[:quantity]).to eq('2') + end + + it 'defines LOGBOOK_REMAINING_PATTERN with named capture' do + pattern = WorkOrders::LOGBOOK_REMAINING_PATTERN + match = 'You must bundle and deliver 3 more'.match(pattern) + expect(match).not_to be_nil + expect(match[:remaining]).to eq('3') + end + + it 'defines POLISH_COUNT_PATTERN with named capture' do + pattern = WorkOrders::POLISH_COUNT_PATTERN + match = 'The surface polish has 15 uses remaining'.match(pattern) + expect(match).not_to be_nil + expect(match[:count]).to eq('15') + end + + it 'defines TAP_HERB_PATTERN with named capture' do + pattern = WorkOrders::TAP_HERB_PATTERN + match = 'You tap a jadice flower inside your'.match(pattern) + expect(match).not_to be_nil + expect(match[:item]).to eq('a jadice flower') + end + + it 'defines HERB_COUNT_PATTERN with named capture' do + pattern = WorkOrders::HERB_COUNT_PATTERN + match = 'You count out 25 pieces.'.match(pattern) + expect(match).not_to be_nil + expect(match[:count]).to eq('25') + end + + it 'defines REMEDY_COUNT_PATTERN with named capture' do + pattern = WorkOrders::REMEDY_COUNT_PATTERN + match = 'You count out 5 uses remaining.'.match(pattern) + expect(match).not_to be_nil + expect(match[:count]).to eq('5') + end + + it 'defines MATERIAL_NOUNS as frozen array' do + expect(WorkOrders::MATERIAL_NOUNS).to be_frozen + expect(WorkOrders::MATERIAL_NOUNS).to eq(%w[deed pebble stone rock rock boulder]) + end + + it 'defines READ_LOGBOOK_PATTERNS as frozen array' do + expect(WorkOrders::READ_LOGBOOK_PATTERNS).to be_frozen + expect(WorkOrders::READ_LOGBOOK_PATTERNS.length).to eq(2) + end + end + + # =========================================================================== + # #find_npc - NPC location with proper verification + # =========================================================================== + describe '#find_npc' do + let(:room_list) { [100, 101, 102] } + + context 'when NPC is in current room' do + before { $room_npcs = ['Jakke'] } + + it 'returns true without walking' do + expect(DRCT).not_to receive(:walk_to) + result = workorders.send(:find_npc, room_list, 'Jakke') + expect(result).to be true + end + end + + context 'when NPC is in second room' do + it 'walks to rooms until NPC is found' do + call_count = 0 + allow(DRRoom).to receive(:npcs) do + call_count += 1 + call_count >= 2 ? ['Jakke'] : [] + end + + expect(DRCT).to receive(:walk_to).with(100).once + result = workorders.send(:find_npc, room_list, 'Jakke') + expect(result).to be true + end + end + + context 'when NPC is not in any room' do + before { $room_npcs = [] } + + it 'walks to all rooms and returns false' do + expect(DRCT).to receive(:walk_to).exactly(3).times + result = workorders.send(:find_npc, room_list, 'Jakke') + expect(result).to be false + end + end + end + + # =========================================================================== + # #complete_work_order - handles NPC walking away + # =========================================================================== + describe '#complete_work_order' do + let(:info) do + { + 'npc-rooms' => [100, 101], + 'npc_last_name' => 'Jakke', + 'npc' => 'Jakke', + 'logbook' => 'engineering' + } + end + + before do + allow(workorders).to receive(:find_npc).and_return(true) + allow(workorders).to receive(:stow_tool) + end + + context 'when give succeeds on first try' do + it 'gives logbook and stows it' do + expect(DRC).to receive(:bput).with('get my engineering logbook', 'You get').once + expect(DRC).to receive(:release_invisibility).once + expect(DRC).to receive(:bput).with('give log to Jakke', any_args).and_return('You hand') + expect(workorders).to receive(:stow_tool).with('logbook') + + workorders.send(:complete_work_order, info) + end + end + + context 'when NPC walks away (bug fix scenario)' do + it 'retries finding NPC and giving again' do + call_count = 0 + allow(DRC).to receive(:bput) do |cmd, *_patterns| + if cmd.include?('give log') + call_count += 1 + call_count == 1 ? "What is it you're trying to give" : 'You hand' + else + 'You get' + end + end + allow(DRC).to receive(:release_invisibility) + + expect(workorders).to receive(:find_npc).twice.and_return(true) + expect(workorders).to receive(:stow_tool).with('logbook') + + workorders.send(:complete_work_order, info) + end + end + + context 'when NPC cannot be found' do + it 'logs error and returns without crashing' do + allow(workorders).to receive(:find_npc).and_return(false) + expect(Lich::Messaging).to receive(:msg).with('bold', /Could not find NPC/) + + workorders.send(:complete_work_order, info) + end + end + end + + # =========================================================================== + # #bundle_item - pattern matching for success/failure + # =========================================================================== + describe '#bundle_item' do + before do + allow(DRC).to receive(:bput).and_return('You notate the') + end + + context 'when bundling succeeds' do + it 'gets logbook and bundles item' do + expect(DRC).to receive(:bput).with('get my engineering logbook', 'You get') + expect(DRC).to receive(:bput).with('bundle my gloves with my logbook', *WorkOrders::BUNDLE_SUCCESS_PATTERNS).and_return('You notate the') + expect(DRCI).to receive(:stow_hands) + expect(DRCI).not_to receive(:dispose_trash) + + workorders.send(:bundle_item, 'gloves', 'engineering') + end + end + + context 'when item quality is too low' do + it 'disposes the item and logs message' do + expect(DRC).to receive(:bput).with('get my engineering logbook', 'You get') + expect(DRC).to receive(:bput).with('bundle my gloves with my logbook', any_args).and_return('The work order requires items of a higher quality') + expect(Lich::Messaging).to receive(:msg).with('bold', /Bundle failed/) + expect(DRCI).to receive(:dispose_trash).with('gloves', nil, nil) + expect(DRCI).to receive(:stow_hands) + + workorders.send(:bundle_item, 'gloves', 'engineering') + end + end + + context 'when item is damaged enchanted' do + it 'disposes the item and logs message' do + expect(DRC).to receive(:bput).with('get my engineering logbook', 'You get') + expect(DRC).to receive(:bput).with('bundle my sphere with my logbook', any_args).and_return('Only undamaged enchanted items may be used with workorders.') + expect(Lich::Messaging).to receive(:msg).with('bold', /Bundle failed/) + expect(DRCI).to receive(:dispose_trash).with('sphere', nil, nil) + expect(DRCI).to receive(:stow_hands) + + workorders.send(:bundle_item, 'sphere', 'engineering') + end + end + + context 'when noun is small sphere (fount)' do + it 'converts noun to fount' do + expect(DRC).to receive(:bput).with('get my enchanting logbook', 'You get') + expect(DRC).to receive(:bput).with('bundle my fount with my logbook', any_args).and_return('You notate the') + expect(DRCI).to receive(:stow_hands) + + workorders.send(:bundle_item, 'small sphere', 'enchanting') + end + end + end + + # =========================================================================== + # #find_recipe - pure calculation method + # =========================================================================== + describe '#find_recipe' do + let(:materials_info) { { 'stock-volume' => 100 } } + + context 'with recipe volume that divides evenly' do + let(:recipe) { { 'volume' => 25 } } + + it 'returns correct items per stock' do + result = workorders.send(:find_recipe, materials_info, recipe, 4) + _recipe, items_per_stock, spare_stock, scrap = result + + expect(items_per_stock).to eq(4) + expect(spare_stock).to be_nil + expect(scrap).to be_nil + end + end + + context 'with recipe volume that leaves remainder' do + let(:recipe) { { 'volume' => 30 } } + + it 'calculates spare stock correctly' do + result = workorders.send(:find_recipe, materials_info, recipe, 3) + _recipe, items_per_stock, spare_stock, scrap = result + + expect(items_per_stock).to eq(3) + expect(spare_stock).to eq(10) # 100 % 30 = 10 + expect(scrap).to be_truthy + end + end + + context 'when quantity causes scrap' do + let(:recipe) { { 'volume' => 25 } } + + it 'detects scrap from quantity mismatch' do + result = workorders.send(:find_recipe, materials_info, recipe, 5) + _recipe, items_per_stock, _spare_stock, scrap = result + + expect(items_per_stock).to eq(4) + expect(scrap).to be_truthy # 5 % 4 = 1 + end + end + end + + # =========================================================================== + # #get_tool / #stow_tool - delegates to DRCC + # =========================================================================== + describe '#get_tool' do + it 'delegates to DRCC.get_crafting_item with correct args' do + expect(DRCC).to receive(:get_crafting_item).with('scissors', 'backpack', [], nil, true) + workorders.send(:get_tool, 'scissors') + end + end + + describe '#stow_tool' do + it 'delegates to DRCC.stow_crafting_item with correct args' do + expect(DRCC).to receive(:stow_crafting_item).with('scissors', 'backpack', nil) + workorders.send(:stow_tool, 'scissors') + end + end + + # =========================================================================== + # #repair_items - tool repair workflow + # =========================================================================== + describe '#repair_items' do + let(:info) do + { + 'repair-room' => 200, + 'repair-npc' => 'Rangu' + } + end + let(:tools) { ['hammer', 'tongs'] } + + before do + workorders.instance_variable_set(:@settings, OpenStruct.new(workorders_repair_own_tools: false)) + end + + context 'when tool needs no repair' do + it 'stows tool when repair not needed' do + # Mock sequence: give hammer (no scratch), give tongs (no scratch), get ticket (none) + allow(DRC).to receive(:bput) do |cmd, *_patterns| + if cmd.include?('give') + "There isn't a scratch on that" + elsif cmd.include?('get my') + 'What were' + else + '' + end + end + expect(workorders).to receive(:get_tool).with('hammer') + expect(workorders).to receive(:get_tool).with('tongs') + expect(workorders).to receive(:stow_tool).with('hammer') + expect(workorders).to receive(:stow_tool).with('tongs') + + workorders.send(:repair_items, info, tools) + end + end + end + + # =========================================================================== + # #buy_parts / #order_parts - nil-safe iteration + # =========================================================================== + describe '#buy_parts' do + context 'when parts is nil' do + it 'does not crash' do + expect { workorders.send(:buy_parts, nil, 100) }.not_to raise_error + end + end + + context 'when parts is empty' do + it 'does not call buy_item' do + expect(DRCT).not_to receive(:buy_item) + workorders.send(:buy_parts, [], 100) + end + end + + context 'when parts has items' do + it 'buys and stows each part' do + expect(DRCT).to receive(:buy_item).with(100, 'clasp') + expect(workorders).to receive(:stow_tool).with('clasp') + workorders.send(:buy_parts, ['clasp'], 100) + end + end + end + + describe '#order_parts' do + before do + workorders.instance_variable_set(:@recipe_parts, { + 'clasp' => { + 'Crossing' => { 'part-room' => 100, 'part-number' => 5 } + } + }) + end + + context 'when parts is nil' do + it 'does not crash' do + expect { workorders.send(:order_parts, nil, 2) }.not_to raise_error + end + end + + context 'when part has part-number' do + it 'orders from room with number' do + expect(DRCT).to receive(:order_item).with(100, 5).twice + expect(workorders).to receive(:stow_tool).with('clasp').twice + workorders.send(:order_parts, ['clasp'], 2) + end + end + end + + # =========================================================================== + # #gather_process_herb - messaging update + # =========================================================================== + describe '#gather_process_herb' do + it 'logs message with WorkOrders prefix' do + expect(Lich::Messaging).to receive(:msg).with('plain', 'WorkOrders: Gathering herb: jadice flower') + expect(DRC).to receive(:wait_for_script_to_complete).with('alchemy', ['jadice flower', 'forage', 25]) + expect(DRC).to receive(:wait_for_script_to_complete).with('alchemy', ['jadice flower', 'prepare']) + + workorders.send(:gather_process_herb, 'jadice flower', 25) + end + end + + # =========================================================================== + # Pattern matching tests for named captures + # =========================================================================== + describe 'pattern matching' do + describe 'WORK_ORDER_ITEM_PATTERN' do + it 'captures item name with spaces' do + result = 'order for leather gloves. I need 5 ' + match = result.match(WorkOrders::WORK_ORDER_ITEM_PATTERN) + expect(match[:item]).to eq('leather gloves') + expect(match[:quantity]).to eq('5') + end + + it 'captures single word items' do + result = 'order for gloves. I need 3 ' + match = result.match(WorkOrders::WORK_ORDER_ITEM_PATTERN) + expect(match[:item]).to eq('gloves') + expect(match[:quantity]).to eq('3') + end + end + + describe 'LOGBOOK_REMAINING_PATTERN' do + it 'captures remaining count' do + result = 'You must bundle and deliver 7 more items' + match = result.match(WorkOrders::LOGBOOK_REMAINING_PATTERN) + expect(match[:remaining]).to eq('7') + end + end + + describe 'TAP_HERB_PATTERN' do + it 'captures full herb name including adjectives' do + result = 'You tap a dried jadice flower inside your backpack' + match = result.match(WorkOrders::TAP_HERB_PATTERN) + expect(match[:item]).to eq('a dried jadice flower') + end + end + end +end diff --git a/workorders.lic b/workorders.lic index 4cd26909d9..5ca9d70602 100644 --- a/workorders.lic +++ b/workorders.lic @@ -1,8 +1,76 @@ +# frozen_string_literal: true + =begin Documentation: https://elanthipedia.play.net/Lich_script_repository#workorders =end class WorkOrders + # Pattern constants for game responses + GIVE_LOGBOOK_SUCCESS_PATTERNS = [ + 'You hand', + 'You can', + 'What were you', + 'Apparently the work order time limit has expired', + "The work order isn't yet complete" + ].freeze + + GIVE_LOGBOOK_RETRY_PATTERNS = [ + 'What were you', + 'You can', + "What is it you're trying to give" + ].freeze + + NPC_NOT_FOUND_PATTERN = "What is it you're trying to give".freeze + + REPAIR_GIVE_PATTERNS = [ + "I don't repair those here", + 'What is it', + "There isn't a scratch on that", + 'Just give it to me again', + 'I will not', + "I can't fix those. They only have so many uses and then you must buy another." + ].freeze + + REPAIR_NO_NEED_PATTERNS = [/scratch/, /I will not/, /They only have so many uses/].freeze + + BUNDLE_SUCCESS_PATTERNS = [ + 'You notate the', + 'This work order has expired', + 'The work order requires items of a higher quality', + 'Only undamaged enchanted items may be used with workorders.', + "That's not going to work" + ].freeze + + BUNDLE_FAILURE_PATTERN = /requires items of|Only undamaged enchanted/.freeze + + WORK_ORDER_REQUEST_PATTERNS = [ + '^To whom', + 'order for .* I need \d+ ', + 'order for .* I need \d+ stacks \(5 uses each\) of .* quality', + 'You realize you have items bundled with the logbook', + 'You want to ask about shadowlings' + ].freeze + + WORK_ORDER_ITEM_PATTERN = /order for (?.*)\. I need (?\d+) /.freeze + WORK_ORDER_STACKS_PATTERN = /order for (?.*)\. I need (?\d+) stacks \(5 uses each\) of .* quality/.freeze + + READ_LOGBOOK_PATTERNS = [ + 'This work order appears to be complete.', + 'You must bundle and deliver \d+ more' + ].freeze + + LOGBOOK_REMAINING_PATTERN = /You must bundle and deliver (?\d+) more/.freeze + + COUNT_PATTERN = /(?\d+)/.freeze + POLISH_COUNT_PATTERN = /The surface polish has (?\d+) uses remaining/.freeze + + TAP_HERB_PATTERN = /You tap (?.*) inside your/.freeze + HERB_COUNT_PATTERN = /You count out (?\d+) pieces\./.freeze + + REMEDY_COUNT_PATTERN = /You count out (?\d+) uses remaining\./.freeze + + MATERIAL_NOUNS = %w[deed pebble stone rock rock boulder].freeze + def initialize arg_definitions = [ [ @@ -57,7 +125,7 @@ class WorkOrders recipes = recipes.select { |x| x['material'] == @carving_type } if discipline == 'carving' unless info - echo("No crafting settings found for discipline: #{discipline}") + Lich::Messaging.msg('bold', "WorkOrders: No crafting settings found for discipline: #{discipline}") exit end @@ -97,7 +165,7 @@ class WorkOrders craft_method = :knit_items else if item['chapter'] - echo("UNKNOWN CHAPTER FOR TAILORING ITEM #{item}") + Lich::Messaging.msg('bold', "WorkOrders: Unknown chapter for tailoring item: #{item}") exit end end @@ -132,14 +200,14 @@ class WorkOrders @belt = @settings.enchanting_belt craft_method = :enchanting_items else - echo 'No discipline found?' + Lich::Messaging.msg('bold', 'WorkOrders: No discipline found') return end return repair_items(info, tools) if repair if DRSkill.getxp(skill) > @craft_max_mindstate - echo("Exiting because your current mindstate for #{skill} over the set maximum craft_max_mindstate:#{@craft_max_mindstate}") + Lich::Messaging.msg('bold', "WorkOrders: Exiting because your current mindstate for #{skill} (#{DRSkill.getxp(skill)}) is over the set maximum craft_max_mindstate: #{@craft_max_mindstate}") exit end @@ -160,11 +228,14 @@ class WorkOrders def complete_work_order(info) DRCI.stow_hands loop do - find_npc(info['npc-rooms'], info['npc_last_name']) + unless find_npc(info['npc-rooms'], info['npc_last_name']) + Lich::Messaging.msg('bold', "WorkOrders: Could not find NPC #{info['npc_last_name']} in any of the expected rooms") + return + end DRC.bput("get my #{info['logbook']} logbook", 'You get') DRC.release_invisibility - result = DRC.bput("give log to #{info['npc']}", 'You hand', 'You can', 'What were you', 'Apparently the work order time limit has expired', 'The work order isn\'t yet complete') - break unless ['What were you', 'You can'].include?(result) + result = DRC.bput("give log to #{info['npc']}", *GIVE_LOGBOOK_SUCCESS_PATTERNS, NPC_NOT_FOUND_PATTERN) + break unless GIVE_LOGBOOK_RETRY_PATTERNS.any? { |pattern| pattern.is_a?(Regexp) ? pattern.match?(result) : result.include?(pattern) } end stow_tool('logbook') end @@ -190,10 +261,10 @@ class WorkOrders tools.each do |tool_name| get_tool(tool_name) - case DRC.bput("give #{info['repair-npc']}", "I don't repair those here", 'What is it', "There isn't a scratch on that", 'Just give it to me again', 'I will not', "I can't fix those. They only have so many uses and then you must buy another.") - when /scratch/, /I will not/, /They only have so many uses/ + result = DRC.bput("give #{info['repair-npc']}", *REPAIR_GIVE_PATTERNS) + if REPAIR_NO_NEED_PATTERNS.any? { |pat| pat.match?(result) } stow_tool(tool_name) - when /give/ + elsif result.include?('give') DRC.bput("give #{info['repair-npc']}", 'repair ticket') DRC.bput('stow ticket', 'You put') end @@ -226,15 +297,14 @@ class WorkOrders def carve_items(info, materials_info, item, quantity) DRCM.ensure_copper_on_hand(@cash_on_hand || 5000, @settings, @hometown) recipe, items_per_stock, spare_stock, scrap = find_recipe(materials_info, item, quantity) - material_noun = %w[deed pebble stone rock rock boulder] material_volume = 0 bone_carving = recipe['material'] == 'bone' case DRC.bput('get my surface polish', 'You get', 'What were') when 'You get' - /(\d+)/ =~ DRC.bput('count my polish', 'The surface polish has \d+ uses remaining') - if Regexp.last_match(1).to_i < 3 - # stow_tool('polish') + count_result = DRC.bput('count my polish', 'The surface polish has \d+ uses remaining') + match = count_result.match(POLISH_COUNT_PATTERN) + if match && match[:count].to_i < 3 DRCI.dispose_trash('polish', @worn_trashcan, @worn_trashcan_verb) DRCT.order_item(info['polish-room'], info['polish-number']) end @@ -246,7 +316,7 @@ class WorkOrders order_parts(recipe['part'], quantity) if recipe['part'] quantity.times do |count| - DRCI.dispose_trash("#{materials_info['stock-name']} #{material_noun[material_volume]}", @worn_trashcan, @worn_trashcan_verb) if count.positive? && spare_stock + DRCI.dispose_trash("#{materials_info['stock-name']} #{MATERIAL_NOUNS[material_volume]}", @worn_trashcan, @worn_trashcan_verb) if count.positive? && spare_stock if items_per_stock.zero? || (count % items_per_stock).zero? if count.positive? go_door if XMLData.room_title.include?('Workshop') @@ -271,14 +341,14 @@ class WorkOrders end if !bone_carving - rock_result = DRC.bput("get #{materials_info['stock-name']} #{material_noun[material_volume]}", 'You get', 'What were', 'You are not strong', 'You pick up', 'but can\'t quite lift it') + rock_result = DRC.bput("get #{materials_info['stock-name']} #{MATERIAL_NOUNS[material_volume]}", 'You get', 'What were', 'You are not strong', 'You pick up', "but can't quite lift it") DRCC.find_shaping_room(@hometown, @engineering_room) unless rock_result =~ /You are not strong|but can't quite lift it/i else DRCC.find_shaping_room(@hometown, @engineering_room) end DRC.bput('swap', 'You move') if DRC.right_hand =~ /#{@noun}/i - DRC.wait_for_script_to_complete('carve', [recipe['chapter'], recipe['name'], materials_info['stock-name'], bone_carving ? 'stack' : material_noun[material_volume], recipe['noun']]) + DRC.wait_for_script_to_complete('carve', [recipe['chapter'], recipe['name'], materials_info['stock-name'], bone_carving ? 'stack' : MATERIAL_NOUNS[material_volume], recipe['noun']]) material_volume = materials_info['stock-volume'] if material_volume.zero? material_volume -= recipe['volume'] @@ -292,7 +362,7 @@ class WorkOrders DRCI.dispose_trash("#{materials_info['stock-name']} stack", @worn_trashcan, @worn_trashcan_verb) unless @retain_crafting_materials end elsif scrap - DRCI.dispose_trash("#{materials_info['stock-name']} #{material_noun[material_volume]}", @worn_trashcan, @worn_trashcan_verb) + DRCI.dispose_trash("#{materials_info['stock-name']} #{MATERIAL_NOUNS[material_volume]}", @worn_trashcan, @worn_trashcan_verb) end go_door if XMLData.room_title.include?('Workshop') end @@ -330,9 +400,10 @@ class WorkOrders end DRC.wait_for_script_to_complete('shape', ['log', recipe['chapter'], recipe['name'], materials_info['stock-name'], recipe['noun']]) - case DRC.bput('read my engineering logbook', 'This work order appears to be complete.', 'You must bundle and deliver \d+ more') - when /You must bundle and deliver \d+ more / - log_num = Regexp.last_match(1).to_i + result = DRC.bput('read my engineering logbook', *READ_LOGBOOK_PATTERNS) + match = result.match(LOGBOOK_REMAINING_PATTERN) + if match + log_num = match[:remaining].to_i break if count + 1 + log_num != quantity end end @@ -354,14 +425,14 @@ class WorkOrders end def buy_parts(parts, partroom) - parts.each do |part| + parts&.each do |part| DRCT.buy_item(partroom, part) stow_tool(part) end end def order_parts(parts, quantity) - parts.each do |part| + parts&.each do |part| data = @recipe_parts[part][@hometown] quantity.times do if data['part-number'] @@ -402,9 +473,10 @@ class WorkOrders quantity.times do |count| DRC.wait_for_script_to_complete('sew', ['log', 'sewing', recipe['chapter'], recipe['name'], materials_info['stock-name'], recipe['noun']]) - case DRC.bput('read my outfitting logbook', 'This work order appears to be complete.', 'You must bundle and deliver \d+ more') - when /You must bundle and deliver \d+ more / - log_num = Regexp.last_match(1).to_i + result = DRC.bput('read my outfitting logbook', *READ_LOGBOOK_PATTERNS) + match = result.match(LOGBOOK_REMAINING_PATTERN) + if match + log_num = match[:remaining].to_i break if count + 1 + log_num != quantity end end @@ -430,9 +502,10 @@ class WorkOrders quantity.times do |count| DRC.wait_for_script_to_complete('sew', ['log', 'knitting', recipe['chapter'], recipe['name'], materials_info['stock-name'], recipe['noun']]) - case DRC.bput('read my outfitting logbook', 'This work order appears to be complete.', 'You must bundle and deliver \d+ more') - when /You must bundle and deliver \d+ more / - log_num = Regexp.last_match(1).to_i + result = DRC.bput('read my outfitting logbook', *READ_LOGBOOK_PATTERNS) + match = result.match(LOGBOOK_REMAINING_PATTERN) + if match + log_num = match[:remaining].to_i break if count + 1 + log_num != quantity end end @@ -452,7 +525,7 @@ class WorkOrders end def gather_process_herb(herb, herb_volume_to_purchase) - echo herb + Lich::Messaging.msg('plain', "WorkOrders: Gathering herb: #{herb}") DRC.wait_for_script_to_complete('alchemy', [herb, 'forage', herb_volume_to_purchase]) DRC.wait_for_script_to_complete('alchemy', [herb, 'prepare']) end @@ -464,7 +537,6 @@ class WorkOrders herb_volume_total = 0 last_herb_volume = 0 last_descriptor = '' - stack_descriptor = '' # We have to only ever use the last word in a multi-word herb. We will need to be careful when counting because of this. # This is to resolve "red flower" vs "blue flower" because "count third red flower in my bag" fails. @@ -485,17 +557,20 @@ class WorkOrders # example: # [workorders]>tap first flower in my haversack # You lightly tap Inkin on the shoulder. - /You tap (.*) inside your|I could not find|You lightly tap/ =~ DRC.bput("tap #{stack_descriptor} #{herb_for_tapping} in my #{@bag}", 'You tap (.*) inside your', 'I could not find', 'You lightly tap') - tap_result = Regexp.last_match(1) - if tap_result.nil? + tap_result = DRC.bput("tap #{stack_descriptor} #{herb_for_tapping} in my #{@bag}", 'You tap (.*) inside your', 'I could not find', 'You lightly tap') + match = tap_result.match(TAP_HERB_PATTERN) + if match.nil? herb_volume = 0 found_stack = false else + tap_item = match[:item] # Check to see if the generic item we just tapped matches the exact item we are looking for. - herb_volume = if tap_result.include? herb - DRC.bput("count #{stack_descriptor} #{herb_for_tapping} in my #{@bag}", 'I could not find', 'You count out \d+ pieces.').scan(/\d+/).first.to_i + herb_volume = if tap_item.include?(herb) + count_result = DRC.bput("count #{stack_descriptor} #{herb_for_tapping} in my #{@bag}", 'I could not find', 'You count out \d+ pieces.') + count_match = count_result.match(HERB_COUNT_PATTERN) + count_match ? count_match[:count].to_i : 0 else - # Since this looks like the wrong item, we can't count it's volume, and we just move on. + # Since this looks like the wrong item, we can't count its volume, and we just move on. 0 end end @@ -566,8 +641,10 @@ class WorkOrders stow_tool(DRC.right_hand) DRC.bput("Mark my #{recipe['noun']} at 5", 'You measure') DRC.bput("Break my #{recipe['noun']}", 'You carefully') - /(\d+)/ =~ DRC.bput("count my first #{recipe['noun']}", 'You count out \d+ uses remaining.') - if Regexp.last_match(1).to_i == 5 + count_result = DRC.bput("count my first #{recipe['noun']}", 'You count out \d+ uses remaining.') + match = count_result.match(REMEDY_COUNT_PATTERN) + count_value = match ? match[:count].to_i : 0 + if count_value == 5 DRC.bput("stow my second #{recipe['noun']}", 'You put', 'You combine') else DRC.bput("stow my first #{recipe['noun']}", 'You put', 'You combine') @@ -577,6 +654,7 @@ class WorkOrders when 'You notate', 'You put' DRC.bput('stow my logbook', 'You put') when 'The work order requires items of a higher quality' + Lich::Messaging.msg('bold', 'WorkOrders: Work order requires higher quality items. Disposing and stopping.') DRCI.dispose_trash(recipe['noun'], @worn_trashcan, @worn_trashcan_verb) unless @retain_crafting_materials DRCI.dispose_trash(recipe['herb1'], @worn_trashcan, @worn_trashcan_verb) while recipe['herb1'] && DRCI.exists?(recipe['herb1']) @@ -633,7 +711,7 @@ class WorkOrders if DRC.bput("get my #{@use_own_ingot_type} ingot", 'You get', 'What were') == 'What were' if DRC.bput("get my #{@use_own_ingot_type} deed", 'You get', 'What were') == 'What were' - echo('out of material/deeds') + Lich::Messaging.msg('bold', 'WorkOrders: Out of material/deeds for forging') exit else volume = deed_ingot_volume @@ -651,7 +729,7 @@ class WorkOrders if volume < quantity * recipe['volume'] smelt = true if DRC.bput("get my #{@use_own_ingot_type} deed", 'You get', 'What were') == 'What were' - echo('out of material/deeds') + Lich::Messaging.msg('bold', 'WorkOrders: Out of material/deeds for forging (need more volume)') DRCI.stow_hands exit else @@ -666,7 +744,7 @@ class WorkOrders DRCI.stow_hands if volume < quantity * recipe['volume'] - echo('out of material/deeds') + Lich::Messaging.msg('bold', "WorkOrders: Insufficient material volume (have #{volume}, need #{quantity * recipe['volume']})") exit end @@ -746,35 +824,31 @@ class WorkOrders end # Exit script if one of the four sigil checks is false. - exit if tally >= 1 + if tally >= 1 + Lich::Messaging.msg('bold', "WorkOrders: Missing #{tally} required sigil type(s) for enchanting") + exit + end # Enchant component #1 tmp_comp_count = 0 - need_comp = 0 if recipe['item'] tmp_comp_count = DRCI.count_items_in_container(recipe['noun'].split.last, @bag) if tmp_comp_count < quantity need_comp = quantity - tmp_comp_count - # Found a weird challenge that made the temp_part_count equal 1 even though no "component" was in container - need_comp += 1 if reget(3, 'but there is nothing in there like that') && (need_comp + tmp_comp_count) != quantity - DRC.message("need_comp with potiental plus 1 is #{need_comp}.") + Lich::Messaging.msg('plain', "WorkOrders: Need #{need_comp} more enchanting components") DRCC.order_enchant(info['stock-room'], need_comp, recipe['item'], @bag, @belt) end end # Parts # - tmp_part_count = 0 - need_part = 0 if recipe['part'] - (recipe['part']).each do |p| - p.to_s - + recipe['part'].each do |p| tmp_part_count = DRCI.count_items_in_container(p, @bag) next unless tmp_part_count < quantity need_part = quantity - tmp_part_count - DRC.message("need_part is #{need_part}.") + Lich::Messaging.msg('plain', "WorkOrders: Need #{need_part} more parts: #{p}") # parts are listed by only their noun order_parts([p.split.last], need_part) @@ -797,10 +871,10 @@ class WorkOrders recipe['noun'].split.last end bundle_item(product, info['logbook']) - case DRC.bput('read my enchanting logbook', 'This work order appears to be complete.',\ - 'You must bundle and deliver \d+ more') - when /You must bundle and deliver (\d+) more/ - log_num = Regexp.last_match(1).to_i + result = DRC.bput('read my enchanting logbook', *READ_LOGBOOK_PATTERNS) + match = result.match(LOGBOOK_REMAINING_PATTERN) + if match + log_num = match[:remaining].to_i break if count + 1 + log_num != quantity end end @@ -809,7 +883,9 @@ class WorkOrders def bundle_item(noun, logbook) noun = 'fount' if noun == 'small sphere' DRC.bput("get my #{logbook} logbook", 'You get') - if /requires items of|Only undamaged enchanted/ =~ DRC.bput("bundle my #{noun} with my logbook", 'You notate the', 'This work order has expired', 'The work order requires items of a higher quality', 'Only undamaged enchanted items may be used with workorders.', 'That\'s not going to work') + result = DRC.bput("bundle my #{noun} with my logbook", *BUNDLE_SUCCESS_PATTERNS) + if BUNDLE_FAILURE_PATTERN.match?(result) + Lich::Messaging.msg('bold', "WorkOrders: Bundle failed - #{result}. Disposing item.") DRCI.dispose_trash(noun, @worn_trashcan, @worn_trashcan_verb) end DRCI.stow_hands @@ -820,29 +896,29 @@ class WorkOrders diff ||= 'challenging' DRCI.stow_hands 500.times do - find_npc(npc_rooms, npc_last_name) + unless find_npc(npc_rooms, npc_last_name) + Lich::Messaging.msg('bold', "WorkOrders: Could not find NPC #{npc_last_name}") + next + end DRC.bput("get my #{logbook} logbook", 'You get') unless DRC.left_hand || DRC.right_hand - case DRC.bput("ask #{npc} for #{diff} #{discipline} work", '^To whom', 'order for .* I need \d+ ', 'order for .* I need \d+ stacks \(5 uses each\) of .* quality', 'You realize you have items bundled with the logbook', 'You want to ask about shadowlings') + result = DRC.bput("ask #{npc} for #{diff} #{discipline} work", *WORK_ORDER_REQUEST_PATTERNS) + case result when 'You want to ask about shadowlings' pause 10 fput('say Hmm.') - when /order for (.*)\. I need (\d+) / - item = Regexp.last_match(1) - quantity = Regexp.last_match(2).to_i - if @min_items <= quantity && quantity <= @max_items && match_names.include?(item) - stow_tool('logbook') - return [item, quantity] - end - when /order for (.*)\. I need (\d+) stacks \(5 uses each\) of .* quality/ - item = Regexp.last_match(1) - quantity = Regexp.last_match(2).to_i - if @min_items <= quantity && quantity <= @max_items && match_names.include?(item) - stow_tool('logbook') - return [item, quantity] + when WORK_ORDER_ITEM_PATTERN, WORK_ORDER_STACKS_PATTERN + match = result.match(WORK_ORDER_ITEM_PATTERN) || result.match(WORK_ORDER_STACKS_PATTERN) + if match + item = match[:item] + quantity = match[:quantity].to_i + if @min_items <= quantity && quantity <= @max_items && match_names.include?(item) + stow_tool('logbook') + return [item, quantity] + end end when 'You realize you have items bundled with the logbook' DRC.bput('untie my logbook', 'You untie') - if DRC.left_hand.include?('logbook') + if DRC.left_hand&.include?('logbook') DRCI.dispose_trash(DRC.right_hand, @worn_trashcan, @worn_trashcan_verb) else DRCI.dispose_trash(DRC.left_hand, @worn_trashcan, @worn_trashcan_verb) @@ -851,15 +927,18 @@ class WorkOrders end end stow_tool('logbook') + Lich::Messaging.msg('bold', 'WorkOrders: Failed to get a suitable work order after 500 attempts') exit end def find_npc(room_list, npc) room_list.each do |room_id| - break if DRRoom.npcs.include?(npc) + return true if DRRoom.npcs.include?(npc) DRCT.walk_to(room_id) end + # Final check after walking to all rooms + DRRoom.npcs.include?(npc) end end