forked from elanthia-online/dr-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathforge.lic
More file actions
1622 lines (1401 loc) · 51.5 KB
/
forge.lic
File metadata and controls
1622 lines (1401 loc) · 51.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# frozen_string_literal: true
=begin
Documentation: https://elanthipedia.play.net/Lich_script_repository#forge
=end
# Blacksmithing, armorsmithing, and weaponsmithing automation.
#
# Handles the complete forging workflow including:
# - Crafting new items from recipes (book or instructions)
# - Tempering finished weapons/armor
# - Enhancements: honing, balancing, lightening, reinforcing
# - Automatic forge rental renewal
# - Assembly of multi-part items (hilts, handles, padding)
#
# @example Craft a new item from a book recipe
# ;forge weaponsmithing 4 "steel throwing hammer" steel hammer
#
# @example Craft using instructions
# ;forge instructions steel sword
#
# @example Temper an existing weapon
# ;forge temper sword
#
# @example Hone a weapon for better damage
# ;forge hone sword
#
# @example Resume interrupted work
# ;forge resume weaponsmithing hammer
#
# @example Craft and log to engineering logbook
# ;forge log weaponsmithing 4 "steel throwing hammer" steel hammer
#
# @see https://elanthipedia.play.net/Lich_script_repository#forge
#
# @author Elanthia Online
# @since 2025-12-29 Refactored with finish option and rental renewal
class Forge
# @!group Rental Patterns
# Pattern to extract rental expiration time
# @return [Regexp]
RENTAL_EXPIRE_PATTERN = /It will expire (?<expire_time>.+)\./
# Patterns indicating rental notice not found
# @return [Array<String>]
RENTAL_NOT_FOUND_PATTERNS = ['I could not find', 'What were you referring to'].freeze
# Patterns indicating successful rental renewal
# @return [Array<String>]
RENTAL_RENEW_SUCCESS_PATTERNS = ['You mark the notice', 'renewed your rental', 'extends your rental'].freeze
# Patterns indicating failed rental renewal
# @return [Array<String>]
RENTAL_RENEW_FAILURE_PATTERNS = ["You don't have enough", 'I could not find'].freeze
# @!endgroup
# @!group Tool Patterns
# Patterns indicating tongs are required but missing
# @return [Array<String>]
TONGS_ERROR_PATTERNS = ['You must be holding some metal tongs', 'That tool does not seem'].freeze
# Pattern indicating tool is not suitable for current state
# @return [String]
TOOL_NOT_SUITABLE_PATTERN = "doesn't appear suitable"
# @!endgroup
# @!group Temper Patterns
# Pattern indicating tempering should begin
# @return [String]
TEMPER_START_PATTERN = 'You glance down at the hot coals of the forge'
# Pattern indicating tempering should continue with turning
# @return [String]
TEMPER_CONTINUE_PATTERN = 'ensure even heating in the forge'
# Pattern indicating item has already been tempered
# @return [String]
TEMPER_ALREADY_DONE_PATTERN = 'has already been tempered'
# @!endgroup
# @!group Forge State Patterns
# Patterns indicating forge needs more fuel (use shovel)
# @return [Array<String>]
FUEL_NEEDED_PATTERNS = ['needs more fuel', 'need some more fuel', 'Almost all of the coal has been consumed'].freeze
# Patterns indicating forge fire needs bellows
# @return [Array<String>]
BELLOWS_NEEDED_PATTERNS = ['fire dims and produces less heat', 'fire flickers and is unable to consume its fuel', 'The forge fire has died down'].freeze
# Patterns indicating item needs turning with tongs
# @return [Array<String>]
TONGS_TURN_PATTERNS = [
'straightening along the horn of the anvil',
'would benefit from some soft reworking.',
'set using tongs',
'sets using tongs',
'into wire using a mandrel or mold set',
'metal is in need of some gentle bending'
].freeze
# Patterns indicating item needs cooling in slack tub
# @return [Array<String>]
COOLING_PATTERNS = ['in the slack tub', 'The metal is ready to be cooled'].freeze
# @!endgroup
# @!group Finishing Patterns
# Pattern indicating wire brush is needed after grinding
# @return [String]
WIRE_BRUSH_PATTERN = 'The grinding has left many nicks and burs'
# Patterns indicating item needs pounding with hammer
# @return [Array<String>]
POUND_PATTERNS = [
'must be pounded free',
'the armor now needs reassembly with a hammer',
'looks ready to be pounded',
'appears ready for more pounding',
'anything that would obstruct pounding of the metal',
'appears ready for pounding the assembled handle'
].freeze
# Patterns indicating grindstone work is needed
# @return [Array<String>]
GRINDSTONE_PATTERNS = [
'ready for grinding away of the excess metal',
'now appears ready for grinding and polishing',
"thinning the armor's metal at a grindstone",
'The armor is ready to be lightened',
'ready to be ground away',
'You think adjusting the armor'
].freeze
# Patterns indicating pliers work is needed
# @return [Array<String>]
PLIERS_PATTERNS = [
'Some pliers are now required',
'appear ready to be woven',
'using a pair of pliers',
'using pliers',
'ready for more bending of links and plates'
].freeze
# Patterns indicating oil is needed to preserve metal
# @return [Array<String>]
OIL_PATTERNS = [
'in need of some oil to preserve',
'protection by pouring oil on it',
'metal will quickly rust',
'to be cleaned of the clay'
].freeze
# @!endgroup
# @!group Assembly Patterns
# Pattern indicating handle assembly is needed
# @return [String]
HANDLE_ASSEMBLY_PATTERN = 'now needs the handle assembled and pounded into place'
# Pattern indicating ingredients can be added
# @return [String]
INGREDIENTS_PATTERN = 'Ingredients can be added'
# Patterns indicating successful assembly
# @return [Array<String>]
ASSEMBLE_SUCCESS_PATTERNS = [
'affix it securely in place',
'and tighten the pommel to secure it',
'carefully mark where it will attach when you continue crafting',
'You layer the leather strips'
].freeze
# Pattern indicating assembly part is not required
# @return [String]
ASSEMBLE_NOT_REQUIRED_PATTERN = 'is not required to continue crafting'
# @!endgroup
# @!group Error Patterns
# Pattern indicating ingot is too small for recipe
# @return [String]
INGOT_TOO_SMALL_PATTERN = 'You need a larger volume of metal'
# Pattern indicating item was not found
# @return [String]
ITEM_NOT_FOUND_PATTERN = 'I could not find what you were referring to'
# Pattern indicating oil container is empty
# @return [String]
OIL_EMPTY_PATTERN = 'Pour what'
# @!endgroup
# @!group Completion Patterns
# Pattern indicating crafting is complete
# @return [String]
FINAL_TOUCHES_PATTERN = 'Applying the final touches'
# Pattern indicating roundtime occurred
# @return [String]
ROUNDTIME_PATTERN = 'Roundtime'
# Pattern indicating grindstone is too slow
# @return [String]
GRINDSTONE_SLOW_PATTERN = 'not spinning fast enough'
# @!endgroup
# @!group Grindstone Patterns
# Patterns indicating grindstone spin was successful
# @return [Array<String>]
SPIN_SUCCESS_PATTERNS = ['keeping it spinning fast', 'making it spin even faster'].freeze
# Patterns indicating grindstone needs more spinning
# @return [Array<String>]
SPIN_FAILURE_PATTERNS = ['not spinning fast enough', 'Roundtime'].freeze
# Pattern indicating grindstone is missing
# @return [String]
SPIN_MISSING_PATTERN = 'Turn what'
# @!endgroup
# @!group Item Manipulation Patterns
# Pattern indicating successful get
# @return [String]
GET_SUCCESS_PATTERN = 'You get'
# Pattern indicating failed get
# @return [String]
GET_FAILURE_PATTERN = 'What were you referring to?'
# Pattern indicating successful put
# @return [String]
PUT_SUCCESS_PATTERN = 'You put'
# Patterns for stamping finished items
# @return [Array<String>]
STAMP_PATTERNS = ['carefully hammer the stamp', 'You cannot figure out how to do that', 'too badly damaged'].freeze
# Patterns for swapping items between hands
# @return [Array<String>]
SWAP_PATTERNS = ['You move', 'You have nothing'].freeze
# @!endgroup
# @!group Location Patterns
# Pattern indicating anvil has an item on it
# @return [String]
ANVIL_HAS_ITEM_PATTERN = 'anvil you see'
# Pattern indicating anvil is empty
# @return [String]
ANVIL_EMPTY_PATTERN = 'clean and ready'
# Pattern indicating forge has an item on it
# @return [String]
FORGE_HAS_ITEM_PATTERN = 'forge you see'
# Pattern indicating forge is empty
# @return [String]
FORGE_EMPTY_PATTERN = 'There is nothing'
# @!endgroup
# @!group Progress Patterns
# Pattern to extract completion percentage from analyze
# @return [Regexp]
ANALYZE_PROGRESS_PATTERN = /is in the final stage of completion|is approximately (?<percent>\d+)% complete|is practically finished/
# Pattern to extract quality assessment from analyze
# @return [Regexp]
ANALYZE_QUALITY_PATTERN = /shows signs of (?<quality>[\w\s]+) craftsmanship/
# @!endgroup
# @!group Grindstone Constants
# Maximum retry attempts for spinning grindstone
# @return [Integer]
SPIN_MAX_RETRIES = 10
# @!endgroup
# @!group Private Forge Constants
# Default cost for private forge rental in copper
# @return [Integer]
DEFAULT_PRIVATE_FORGE_COST = 5000
# Patterns for successful private forge entry
# @return [Array<String>]
PRIVATE_FORGE_ENTRY_SUCCESS = ['You head through', 'You walk', 'You go', 'Obvious exits'].freeze
# Patterns for blocked private forge entry
# @return [Array<String>]
PRIVATE_FORGE_ENTRY_BLOCKED = ["You don't have enough", 'The sentry blocks', 'cannot enter', 'You need to pay'].freeze
# @!endgroup
# Initialize and run the forge script.
#
# Parses command-line arguments, validates settings, and orchestrates
# the complete forging workflow from setup through completion.
#
# @note Exits on validation failures or missing required settings
#
# @example Argument pattern 1: Book recipe
# ;forge [finish] <book_type> <chapter> <recipe_name> <metal> <noun> [skip] [debug]
#
# @example Argument pattern 2: Instructions
# ;forge [finish] instructions <metal> <noun> [skip] [debug]
#
# @example Argument pattern 3: Enhancement
# ;forge [finish] <enhancement> <noun> [skip] [debug]
#
# @example Argument pattern 4: Resume
# ;forge resume <book_type> <noun> [debug]
#
# @return [void]
def initialize
arg_definitions = [
[
{ name: 'finish', options: %w[hold log stow trash], optional: true, description: 'What to do with the finished item (default: hold).' },
{ name: 'book_type', display: 'book type', options: %w[blacksmithing armorsmithing weaponsmithing], description: 'What smithing type is this item.' },
{ name: 'chapter', regex: /\d+/i, variable: true, description: 'Chapter containing the item.' },
{ name: 'recipe_name', display: 'recipe name', regex: /^[A-z\s\-\']+$/i, variable: true, description: 'Name of the recipe, wrap in double quotes if this is multiple words.' },
{ name: 'metal', regex: /\w+/i, variable: true, description: 'Type of metal ingot to use.' },
{ name: 'noun', regex: /\w+/i, variable: true, description: 'name of item being crafted' },
{ name: 'skip', regex: /skip/i, optional: true, description: 'Optional setting to skip restocking consumables if low (oil)' },
{ name: 'debug', regex: /debug/i, optional: true, description: 'displays debug information' }
],
[
{ name: 'finish', options: %w[hold log stow trash], optional: true, description: 'What to do with the finished item (default: hold).' },
{ name: 'instructions', regex: /instructions/i, description: 'Instructions if using instructions' },
{ name: 'metal', regex: /\w+/i, variable: true, description: 'Type of metal ingot to use.' },
{ name: 'noun', regex: /\w+/i, variable: true, description: 'Noun of item being crafted' },
{ name: 'skip', regex: /skip/i, optional: true, description: 'Optional setting to skip restocking consumables if low (oil)' },
{ name: 'debug', regex: /debug/i, optional: true, description: 'displays debug information' }
],
[
{ name: 'recipe_name', display: 'Enhancement', options: %w[temper balance hone lighten reinforce], description: 'Enhancements applied to finished weapons and armor' },
{ name: 'noun', regex: /\w+/i, variable: true, description: 'Noun of item to enhance.' },
{ name: 'skip', regex: /skip/i, optional: true, description: 'Optional setting to skip restocking consumables if low (oil)' },
{ name: 'debug', regex: /debug/i, optional: true, description: 'displays debug information' }
],
[
{ name: 'resume', regex: /resume/i },
{ name: 'book_type', display: 'book type', options: %w[blacksmithing armorsmithing weaponsmithing], description: 'What smithing type is this item.' },
{ name: 'noun', regex: /\w+/i, variable: true, description: 'Noun of item to resume.' },
{ name: 'debug', regex: /debug/i, optional: true, description: 'displays debug information' }
]
]
args = parse_args(arg_definitions)
setup_flags
load_settings(args)
validate_setup(args)
execute_workflow(args)
end
private
# @!group Setup Methods
# Register Flags for async pattern matching during crafting.
#
# Sets up the following flags:
# - forge-assembly: Detects when assembly parts are needed
# - work-done: Detects crafting completion
# - ingot-restow: Handles leftover ingot metal
# - forge-rental-warning: Triggers auto-renewal
#
# @return [void]
def setup_flags
Flags.add('forge-assembly', 'another finished \S+ shield (handle)', 'another finished wooden (hilt|haft)', 'another finished (long|short|small|large) leather (cord|backing)', 'another finished (small|large) cloth (padding)', 'another finished (long|short) wooden (pole)', 'appears ready to be reinforced with some (leather strips)')
Flags.add('work-done', 'from the successful .* process', 'shows a slightly reduced weight', 'shows improved protection', 'Applying the final touches', /^The .* was successfully/)
Flags.add('ingot-restow', /^You realize .* will not require as much metal as you have, and so you split the ingot and leave the portion you won't be using in your (.*)\./)
Flags.add('forge-rental-warning', 'Your rental time is almost up')
end
# Load settings from YAML configuration and command-line arguments.
#
# @param args [OpenStruct] parsed command-line arguments
# @return [void]
def load_settings(args)
settings = get_settings
@hometown = settings.hometown
@stamp = settings.mark_crafted_goods
@cube = settings.cube_armor_piece
@bag = settings.crafting_container
@bag_items = settings.crafting_items_in_container
@forging_belt = settings.forging_belt
@hammer = settings.forging_tools.find { |item| /hammer|mallet/ =~ item }
@book_type = args.book_type
@chapter = args.chapter
@resume = args.resume
@instruction = args.instructions
@item = args.noun
@finish = args.finish || 'hold'
@info = get_data('crafting')['blacksmithing'][@hometown]
@recipe_name = resolve_recipe_name(args.recipe_name)
@metal = args.metal
@next_spin = Time.now
@debug = args.debug || settings.debug_mode
@use_private_forge = settings.forge_use_private_forge
@private_forge_cost = settings.forge_private_forge_cost || DEFAULT_PRIVATE_FORGE_COST
@show_progress = settings.forge_show_progress
@settings = settings
end
# Validate required settings and preconditions.
#
# Checks:
# - Crafting container is configured
# - Hammer/mallet exists in forging_tools
# - At least one hand is free (holding target item is OK)
# - Adjustable tongs setup if configured
#
# @param args [OpenStruct] parsed command-line arguments
# @return [void]
# @raise [SystemExit] if validation fails
def validate_setup(args)
validate_crafting_container
validate_hammer
DRC.wait_for_script_to_complete('buff', ['forge'])
validate_free_hand
setup_adjustable_tongs if @settings.adjustable_tongs && @item != 'tongs'
find_item if args.resume
end
# Validate that a crafting container is configured.
#
# @return [void]
# @raise [SystemExit] if no crafting container configured
def validate_crafting_container
return if @bag
cleanup_and_exit('No crafting_container configured in your YAML settings.')
end
# Validate that a hammer or mallet is configured in forging_tools.
#
# @return [void]
# @raise [SystemExit] if no hammer found
def validate_hammer
return if @hammer
cleanup_and_exit('No hammer or mallet found in forging_tools setting.')
end
# Validate that at least one hand is free for forging.
#
# Holding the target item is acceptable; other items must be stowed.
#
# @return [void]
# @raise [SystemExit] if both hands are occupied with non-target items
def validate_free_hand
right = DRC.right_hand
left = DRC.left_hand
right_ok = right.nil? || right.include?(@item)
left_ok = left.nil? || left.include?(@item)
free_hand = right.nil? || left.nil?
return if free_hand && right_ok && left_ok
held_items = [right, left].compact.reject { |item| item.include?(@item) }
error_log("Need a free hand to forge. Currently holding: #{held_items.join(' and ')}")
cleanup_and_exit('Please stow extra items and try again.')
end
# Set up adjustable tongs if configured.
#
# Tests that tongs can be adjusted and stows them for later use.
#
# @return [void]
def setup_adjustable_tongs
@adjustable_tongs = DRCC.get_adjust_tongs?('reset tongs', @bag, @bag_items, @forging_belt)
debug_log("Tongs adjustable? :: #{@adjustable_tongs}")
DRCC.stow_crafting_item('tongs', @bag, @forging_belt)
end
# Execute the main forging workflow.
#
# Handles navigation, consumable checks, and the prep/work cycle.
#
# @param args [OpenStruct] parsed command-line arguments
# @return [void]
def execute_workflow(args)
go_to_private_forge if @use_private_forge && !args.resume
check_rental_status
check_all_consumables unless args.skip
set_defaults
work if args.resume
prep
touch_cube if @cube
work
end
# @!endgroup
# @!group Helper Methods
# Resolve enhancement shortcut names to full recipe names.
#
# @param recipe_arg [String, nil] the recipe argument from command line
# @return [String, nil] the full recipe name
#
# @example
# resolve_recipe_name('hone') #=> 'metal weapon honing'
# resolve_recipe_name('balance') #=> 'metal weapon balancing'
def resolve_recipe_name(recipe_arg)
case recipe_arg
when 'hone'
'metal weapon honing'
when 'balance'
'metal weapon balancing'
when 'lighten'
'metal armor lightening'
when 'reinforce'
'metal armor reinforcing'
else
recipe_arg
end
end
# Log a debug message if debug mode is enabled.
#
# @param message [String] the message to log
# @return [void]
def debug_log(message)
Lich::Messaging.msg('plain', "Forge: #{message}") if @debug
end
# Log an error message (always displayed, bold).
#
# @param message [String] the message to log
# @return [void]
def error_log(message)
Lich::Messaging.msg('bold', "Forge: #{message}")
end
# Log an informational message (always displayed, plain).
#
# Use for success confirmations and status updates that aren't errors.
#
# @param message [String] the message to log
# @return [void]
def info_log(message)
Lich::Messaging.msg('plain', "Forge: #{message}")
end
# Stow whatever is in both hands.
#
# Uses DRCC.stow_crafting_item for proper tool/container handling.
#
# @return [void]
def stow_both_hands
DRCC.stow_crafting_item(DRC.right_hand, @bag, @forging_belt)
DRCC.stow_crafting_item(DRC.left_hand, @bag, @forging_belt)
end
# Stow hammer and tongs if they are currently held.
#
# Common cleanup before retrieving item from anvil/forge.
#
# @return [void]
def stow_hammer_and_tongs
DRCC.stow_crafting_item(@hammer, @bag, @forging_belt) if DRCI.in_hands?(@hammer)
DRCC.stow_crafting_item('tongs', @bag, @forging_belt) if DRCI.in_hands?('tongs')
end
# Ready hammer and tongs for pounding work.
#
# Swaps to hammer first, then to tongs (tongs in right hand, hammer accessible).
#
# @return [void]
def ready_hammer_with_tongs
swap_tool(@hammer)
swap_tool('tongs')
end
# Unified error handler that cleans up and exits.
#
# Logs the error, stows items in both hands, releases magic, and exits.
#
# @param message [String] the error message to display
# @return [void]
# @raise [SystemExit] always exits after cleanup
def cleanup_and_exit(message)
error_log(message)
stow_both_hands
magic_cleanup
exit
end
# Retrieve item from a location and ensure it's in the left hand.
#
# Stows hammer and tongs if held, gets item from specified location,
# and swaps to left hand if needed.
#
# @param location [String] where to get item from ('on anvil', 'on forge', etc.)
# @param context [String] description of why item is needed (for error messages)
# @return [Boolean] true if item is in left hand
def prepare_item_in_left_hand(location = 'on anvil', context = 'work')
return true if DRCI.in_left_hand?(@item)
stow_hammer_and_tongs
result = DRC.bput("get #{@item} #{location}", GET_SUCCESS_PATTERN, GET_FAILURE_PATTERN)
if result == GET_FAILURE_PATTERN
error_log("Failed to get #{@item} from #{location} for #{context}.")
end
ensure_item_in_left_hand
true
end
# Ensure item is in left hand, swapping or exiting as needed.
#
# If item is in right hand, swaps it to left. If item is not found
# in either hand, logs error and exits.
#
# @param item [String] the item to check for (defaults to @item)
# @return [void]
# @raise [SystemExit] if item cannot be found
def ensure_item_in_left_hand(item = @item)
return if DRCI.in_left_hand?(item)
if DRCI.in_right_hand?(item)
DRC.bput('swap', *SWAP_PATTERNS)
else
cleanup_and_exit("MISSING #{item}. Please find it and restart.")
end
end
# Touch the cube armor piece for crafting bonus.
#
# @return [void]
def touch_cube
DRC.bput("touch my #{@cube}", /^Warm vapor swirls around your head in a misty halo/, /^A thin cloud of vapor manifests with no particular effect./, /^Touch what/)
end
# Check all consumables (oil and wire brush).
#
# @return [void]
def check_all_consumables
DRCC.check_consumables('oil', @info['finisher-room'], @info['finisher-number'], @bag, @bag_items, @forging_belt) unless Script.running?('smith')
# Wire brush is always order #10 in all forging society tool stands
wire_brush_number = @info['wire-brush-number'] || 10
DRCC.check_consumables('wire brush', @info['finisher-room'], wire_brush_number, @bag, @bag_items, @forging_belt)
end
# @!endgroup
# @!group Navigation Methods
# Navigate to and enter the private forge.
#
# Ensures sufficient funds, walks to forge room, and attempts entry.
# Falls back to current location if entry fails.
#
# @return [void]
# @raise [SystemExit] if blocked from entering and no fallback available
def go_to_private_forge
private_forge_room = @info['private_forge']
unless private_forge_room
error_log("No private forge defined for #{@hometown}. Using public forge.")
return
end
debug_log("Private forge room: #{private_forge_room}")
debug_log("Private forge cost: #{@private_forge_cost} copper")
unless DRCM.ensure_copper_on_hand(@private_forge_cost, @settings, @hometown)
error_log("Unable to get #{@private_forge_cost} copper for private forge rental.")
cleanup_and_exit('Check your bank balance and try again.')
end
debug_log('Navigating to private forge...')
DRCT.walk_to(private_forge_room)
if Room.current.id == private_forge_room
info_log('Arrived at private forge.')
return
end
attempt_private_forge_entry
end
# Attempt manual entry into private forge.
#
# @return [void]
# @raise [SystemExit] if blocked from entering
def attempt_private_forge_entry
debug_log('Not in private forge room - trying to enter manually...')
result = DRC.bput('go door', *PRIVATE_FORGE_ENTRY_SUCCESS, *PRIVATE_FORGE_ENTRY_BLOCKED, 'What were you')
case result
when *PRIVATE_FORGE_ENTRY_BLOCKED
error_log('BLOCKED from entering private forge!')
error_log("The sentry won't let you in. Possible reasons:")
error_log(' - Not enough money on hand (need ~5 plat)')
error_log(' - Forge is already rented by someone else')
error_log(' - Other restriction')
cleanup_and_exit('Use a public forge or resolve the issue.')
when 'What were you'
error_log('No door found to enter private forge. Check room setup.')
error_log('Falling back to current location.')
else
info_log('Entered private forge.')
end
end
# @!endgroup
# @!group Rental Management
# Check current rental status and renew if expiring soon.
#
# Reads the rental notice, parses expiration time, and triggers
# renewal if less than 10 minutes remain.
#
# @return [void]
def check_rental_status
result = DRC.bput('read notice', RENTAL_EXPIRE_PATTERN, *RENTAL_NOT_FOUND_PATTERNS)
match = result.match(RENTAL_EXPIRE_PATTERN)
return unless match
expire_str = match[:expire_time]
process_rental_expiration(expire_str)
end
# Process rental expiration time and renew if needed.
#
# @param expire_str [String] the expiration time string from game
# @return [void]
def process_rental_expiration(expire_str)
# Parse format: "Sun Dec 28 23:39:15 ET 2025"
# Game always uses US Eastern Time -- determine EST (-0500) vs EDT (-0400)
expire_str_normalized = expire_str.sub(' ET ', " #{eastern_utc_offset} ")
expire_time = Time.parse(expire_str_normalized)
minutes_remaining = ((expire_time - Time.now) / 60).to_i
debug_log("Rental expires: #{expire_str} (#{minutes_remaining} minutes remaining)")
if minutes_remaining < 10
error_log("RENTAL LOW (#{minutes_remaining} min) - PRE-EMPTIVELY RENEWING")
renew_forge_rental
elsif minutes_remaining < 20
info_log("Rental has #{minutes_remaining} minutes remaining")
end
rescue ArgumentError => e
error_log("Could not parse rental time: #{expire_str} - #{e.message}")
end
# Determine the current US Eastern timezone UTC offset.
#
# DragonRealms always uses US Eastern Time (ET). This method
# determines whether EST (-0500) or EDT (-0400) is currently
# in effect based on US DST rules (second Sunday of March
# to first Sunday of November).
#
# @return [String] UTC offset string ('-0500' or '-0400')
def eastern_utc_offset
now = Time.now.utc
year = now.year
march_day = (8..14).find { |d| Time.utc(year, 3, d).sunday? }
november_day = (1..7).find { |d| Time.utc(year, 11, d).sunday? }
dst_start = Time.utc(year, 3, march_day, 7) # 2am EST = 7:00 UTC
dst_end = Time.utc(year, 11, november_day, 6) # 2am EDT = 6:00 UTC
(now >= dst_start && now < dst_end) ? '-0400' : '-0500'
end
# Renew the private forge rental.
#
# Marks the rental notice to extend the rental period.
#
# @return [void]
def renew_forge_rental
error_log('FORGE RENTAL EXPIRING - AUTO-RENEWING')
Flags.reset('forge-rental-warning')
result = DRC.bput('mark notice', *RENTAL_RENEW_SUCCESS_PATTERNS, *RENTAL_RENEW_FAILURE_PATTERNS)
case result
when "You don't have enough"
error_log('INSUFFICIENT FUNDS TO RENEW RENTAL')
when 'I could not find'
error_log('COULD NOT FIND NOTICE - CHECK LOCATION')
else
info_log('RENTAL RENEWED')
end
end
# @!endgroup
# @!group Tool Management
# Set default tool and command based on recipe type.
#
# Configures @home_tool, @home_command, @location, and @chapter
# based on whether crafting, tempering, or enhancing.
#
# @return [void]
def set_defaults
case @recipe_name
when 'temper'
setup_temper_defaults
when 'metal weapon honing', 'metal weapon balancing'
setup_weapon_enhancement_defaults
when 'metal armor lightening', 'metal armor reinforcing'
setup_armor_enhancement_defaults
else
setup_crafting_defaults
end
log_defaults
end
# Configure defaults for tempering.
#
# @return [void]
def setup_temper_defaults
@home_tool = 'tongs'
@home_command = "turn #{@item} on forge with my tongs"
@stamp = false
swap_tool('tongs')
ensure_item_in_left_hand('tongs') unless DRCI.in_left_hand?('tongs')
@location = 'on forge'
@command ||= "put my #{@item} on the forge"
end
# Configure defaults for weapon honing/balancing.
#
# @return [void]
def setup_weapon_enhancement_defaults
@home_tool = 'wire brush'
@home_command = "push grindstone with my #{@item}"
ensure_item_in_left_hand unless DRCI.in_left_hand?(@item)
spin_grindstone unless @resume
@chapter = 10
@book_type = 'weaponsmithing'
@stamp = false
end
# Configure defaults for armor lightening/reinforcing.
#
# @return [void]
def setup_armor_enhancement_defaults
@home_tool = 'pliers'
@home_command = "push grindstone with my #{@item}"
@command ||= "pull my #{@item} with my pliers"
ensure_item_in_left_hand unless DRCI.in_left_hand?(@item)
@chapter = 5
@book_type = 'armorsmithing'
@stamp = false
end
# Configure defaults for crafting new items.
#
# @return [void]
def setup_crafting_defaults
@location = 'on anvil'
@home_tool = @hammer
@home_command = "pound #{@item} on anvil with my #{@hammer}"
end
# Log the configured defaults for debugging.
#
# @return [void]
def log_defaults
debug_log("Recipe :: #{@recipe_name}")
debug_log("Default tool for this routine :: #{@home_tool}")
debug_log("Default command for this routine :: #{@home_command}")
debug_log("Item is where? Empty if held :: #{@location}")
debug_log("Item's name? :: #{@item}")
end
# Swap current tool for the next required tool.
#
# Handles adjustable tongs that can become a shovel, stowing the
# current tool, and verifying the new tool is in hand.
#
# @param next_tool [String] the tool to swap to ('tongs', 'shovel', 'hammer', etc.)
# @param skip [Boolean] passed to get_crafting_item for optional tools (default: false)
# @return [void]
# @raise [SystemExit] if tool cannot be obtained
def swap_tool(next_tool, skip = false)
debug_log("What is the next tool? :: #{next_tool}")
debug_log("Are we holding the next tool? :: #{DRCI.in_hands?(next_tool)}")
debug_log("Is the next tool tongs? :: #{next_tool.include?('tongs')}")
debug_log("Are we adjusting tongs (to tongs)? :: #{(next_tool == 'tongs') && @adjustable_tongs}")
debug_log("Are we adjusting tongs (to shovel)? :: #{(next_tool == 'shovel') && @adjustable_tongs}")
if next_tool.include?('tongs') && @adjustable_tongs
debug_log('Making tongs tongs again :: NOW')
DRCC.get_adjust_tongs?('tongs', @bag, @bag_items, @forging_belt, @adjustable_tongs)
elsif next_tool.include?('shovel') && @adjustable_tongs
debug_log('Making tongs shovel again :: NOW')
DRCC.get_adjust_tongs?('shovel', @bag, @bag_items, @forging_belt, @adjustable_tongs)
elsif !DRCI.in_hands?(next_tool)
get_tool(next_tool, skip)
end
verify_tool_in_hand(next_tool)
end
# Get a tool from storage.
#
# @param next_tool [String] the tool to get
# @param skip [Boolean] whether to skip if not found
# @return [void]
def get_tool(next_tool, skip)
if next_tool == 'tongs'
debug_log('Getting tongs (AjT false) :: NOW')
DRCC.get_crafting_item(next_tool, @bag, @bag_items, @forging_belt)
else
DRCC.stow_crafting_item(DRC.right_hand, @bag, @forging_belt)
DRCC.get_crafting_item(next_tool, @bag, @bag_items, @forging_belt, skip)
end
end
# Verify that the expected tool is in hand after swap.
#
# For adjustable tongs as shovel, checks for 'tongs' since the
# physical item is still tongs.
#
# @param next_tool [String] the tool that should be in hand
# @return [void]
# @raise [SystemExit] if tool is not in hand
def verify_tool_in_hand(next_tool)
tool_to_verify = (next_tool == 'shovel' && @adjustable_tongs) ? 'tongs' : next_tool
return if DRCI.in_hands?(tool_to_verify)
cleanup_and_exit("Failed to get #{next_tool}.")
end
# @!endgroup
# @!group Preparation
# Prepare for crafting by studying recipe and getting materials.
#
# Studies instructions or finds recipe in book, then places ingot
# on anvil if crafting a new item.
#
# @return [void]
def prep
DRCA.crafting_magic_routine(@settings)
if @instruction
prep_with_instructions
else
prep_with_book unless @recipe_name.include?('temper')
end
swap_tool(@home_tool)
prep_ingot if @metal
end
# Prepare using instructions (loose paper).
#
# @return [void]
# @raise [SystemExit] if instructions cannot be found
def prep_with_instructions
DRCC.get_crafting_item("#{@item} instructions", @bag, @bag_items, @forging_belt)
unless DRCI.in_hands?('instructions')
cleanup_and_exit("Failed to get #{@item} instructions.")
end
study_instructions
DRCC.stow_crafting_item("#{@item} instructions", @bag, @forging_belt)
end
# Study instructions, repeating if needed.
#
# @return [void]
def study_instructions
if /again/ =~ DRC.bput('study my instructions', 'Roundtime', 'Study them again')
DRC.bput('study my instructions', 'Roundtime', 'Study them again')
end
end
# Prepare using a recipe book.
#
# @return [void]
# @raise [SystemExit] if book cannot be found
def prep_with_book
if @settings.master_crafting_book