File Coverage

blib/lib/WWW/MetaForge/ArcRaiders.pm
Criterion Covered Total %
statement 238 297 80.1
branch 37 70 52.8
condition 22 59 37.2
subroutine 50 58 86.2
pod 29 31 93.5
total 376 515 73.0


line stmt bran cond sub pod time code
1             package WWW::MetaForge::ArcRaiders;
2             our $AUTHORITY = 'cpan:GETTY';
3             # ABSTRACT: Perl client for the MetaForge ARC Raiders API
4             our $VERSION = '0.002';
5              
6 5     5   1398429 use Moo;
  5         23647  
  5         33  
7 5     5   8430 use LWP::UserAgent;
  5         220943  
  5         267  
8 5     5   2038 use JSON::MaybeXS;
  5         36442  
  5         517  
9 5     5   42 use Carp qw(croak);
  5         11  
  5         253  
10 5     5   1995 use namespace::clean;
  5         59211  
  5         36  
11              
12             our $DEBUG = $ENV{WWW_METAFORGE_ARCRAIDERS_DEBUG};
13              
14 5     5   4288 use WWW::MetaForge::Cache;
  5         25  
  5         285  
15 5     5   3150 use WWW::MetaForge::GameMapData;
  5         29  
  5         289  
16 5     5   3920 use WWW::MetaForge::ArcRaiders::Request;
  5         24  
  5         298  
17 5     5   5316 use WWW::MetaForge::ArcRaiders::Result::Item;
  5         28  
  5         406  
18 5     5   3933 use WWW::MetaForge::ArcRaiders::Result::Arc;
  5         30  
  5         286  
19 5     5   4030 use WWW::MetaForge::ArcRaiders::Result::Quest;
  5         28  
  5         257  
20 5     5   3974 use WWW::MetaForge::ArcRaiders::Result::Trader;
  5         26  
  5         344  
21 5     5   9098 use WWW::MetaForge::ArcRaiders::Result::EventTimer;
  5         31  
  5         297  
22 5     5   3631 use WWW::MetaForge::ArcRaiders::Result::MapMarker;
  5         23  
  5         22499  
23              
24              
25             # Fixed list of ARC Raiders maps (API mapID format)
26             our @MAPS = qw(dam spaceport buried-city blue-gate stella-montis);
27             our %MAP_DISPLAY_NAMES = (
28             'dam' => 'Dam',
29             'spaceport' => 'Spaceport',
30             'buried-city' => 'Buried City',
31             'blue-gate' => 'Blue Gate',
32             'stella-montis' => 'Stella Montis',
33             );
34              
35 1     1 1 7483 sub maps { return @MAPS }
36              
37              
38 1     1 1 587 sub map_display_names { return %MAP_DISPLAY_NAMES }
39              
40              
41             sub map_display_name {
42 2     2 1 674 my ($self, $map_id) = @_;
43 2   66     13 return $MAP_DISPLAY_NAMES{$map_id} // $map_id;
44             }
45              
46              
47             has ua => (
48             is => 'ro',
49             lazy => 1,
50             builder => '_build_ua',
51             );
52              
53              
54             has request => (
55             is => 'ro',
56             lazy => 1,
57             default => sub { WWW::MetaForge::ArcRaiders::Request->new },
58             );
59              
60              
61             has cache => (
62             is => 'ro',
63             lazy => 1,
64             builder => '_build_cache',
65             );
66              
67              
68             has use_cache => (
69             is => 'ro',
70             default => 1,
71             );
72              
73              
74             has cache_dir => (
75             is => 'ro',
76             );
77              
78              
79             has json => (
80             is => 'ro',
81             lazy => 1,
82             default => sub { JSON::MaybeXS->new(utf8 => 1) },
83             );
84              
85              
86             has debug => (
87             is => 'ro',
88             default => sub { $DEBUG },
89             );
90              
91              
92             sub _debug {
93 125     125   1131 my ($self, $msg) = @_;
94 125 100       467 return unless $self->debug;
95 3         61 my $ts = localtime;
96 3         32 warn "[WWW::MetaForge::ArcRaiders $ts] $msg\n";
97             }
98              
99             sub _build_ua {
100 0     0   0 my ($self) = @_;
101 0   0     0 my $ua = LWP::UserAgent->new(
102             agent => 'WWW-MetaForge-ArcRaiders/' . ($WWW::MetaForge::ArcRaiders::VERSION // 'dev'),
103             timeout => 30,
104             );
105 0         0 return $ua;
106             }
107              
108             sub _build_cache {
109 2     2   14 my ($self) = @_;
110 2         4 my %args;
111 2 50       14 $args{cache_dir} = $self->cache_dir if defined $self->cache_dir;
112 2         24 return WWW::MetaForge::Cache->new(%args);
113             }
114              
115             has game_map_data => (
116             is => 'ro',
117             lazy => 1,
118             builder => '_build_game_map_data',
119             );
120              
121              
122             sub _build_game_map_data {
123 1     1   10 my ($self) = @_;
124 1         19 return WWW::MetaForge::GameMapData->new(
125             debug => $self->debug,
126             use_cache => $self->use_cache,
127             cache => $self->cache, # Share cache with ArcRaiders
128             marker_class => 'WWW::MetaForge::ArcRaiders::Result::MapMarker',
129             );
130             }
131              
132             sub _fetch {
133 41     41   120 my ($self, $endpoint, $http_request, %params) = @_;
134              
135 41         94 my $skip_cache = delete $params{_skip_cache};
136              
137 41 100 66     187 if ($self->use_cache && !$skip_cache) {
138 2         34 my $cached = $self->cache->get($endpoint, \%params);
139 2 100       68 if (defined $cached) {
140 1         5 $self->_debug("CACHE HIT: $endpoint");
141 1         2 return $cached;
142             }
143 1         5 $self->_debug("CACHE MISS: $endpoint");
144             }
145              
146 40         105 my $url = $http_request->uri;
147 40         449 $self->_debug("REQUEST: GET $url");
148              
149 40         1013 my $response = $self->ua->request($http_request);
150              
151 40         25238 $self->_debug("RESPONSE: " . $response->code . " " . $response->message);
152              
153 40 50       160 unless ($response->is_success) {
154 0         0 croak sprintf("API request failed: %s %s",
155             $response->code, $response->message);
156             }
157              
158 40         1041 my $data = eval { $self->json->decode($response->decoded_content) };
  40         1262  
159 40 50       8700 croak "Failed to parse JSON response: $@" if $@;
160              
161 40 50       146 my $count = ref $data eq 'ARRAY' ? scalar(@$data) : 1;
162 40         175 $self->_debug("PARSED: $count records");
163              
164 40 100 66     157 if ($self->use_cache && !$skip_cache) {
165 1         18 $self->cache->set($endpoint, \%params, $data);
166 1         6 $self->_debug("CACHE SET: $endpoint");
167             }
168              
169 40         248 return $data;
170             }
171              
172             sub _extract_data {
173 40     40   80 my ($self, $response) = @_;
174              
175 40 50       169 return $response unless ref $response eq 'HASH';
176              
177             # API returns {"data": ...} or {"success": true, "data": ...}
178 40 50       109 if (exists $response->{data}) {
179 40         98 return $response->{data};
180             }
181              
182 0         0 return $response;
183             }
184              
185             sub _to_objects {
186 32     32   71 my ($self, $data, $class) = @_;
187              
188 32 50       68 return [] unless defined $data;
189              
190 32 50       86 if (ref $data eq 'ARRAY') {
    0          
191 32         81 return [ map { $class->from_hashref($_) } @$data ];
  151         92348  
192             } elsif (ref $data eq 'HASH') {
193             # Single item from API (e.g., when querying by id=) - wrap in array for consistency
194 0         0 return [ $class->from_hashref($data) ];
195             }
196              
197 0         0 return $data;
198             }
199              
200             # Generic paginated fetch - returns {data => [...], pagination => {...}}
201             sub _fetch_paginated {
202 32     32   120 my ($self, $endpoint, $request_method, $result_class, %params) = @_;
203              
204 32         891 my $req = $self->request->$request_method(%params);
205 32         4604 my $response = $self->_fetch($endpoint, $req, %params);
206 32         97 my $data = $self->_extract_data($response);
207 32 50       91 my $pagination = ref $response eq 'HASH' ? $response->{pagination} : undef;
208              
209             return {
210 32         103 data => $self->_to_objects($data, $result_class),
211             pagination => $pagination,
212             };
213             }
214              
215             # Fetch all pages for a paginated endpoint
216             sub _fetch_all_pages {
217 4     4   14 my ($self, $endpoint, $request_method, $result_class, %params) = @_;
218              
219 4         5 my @all_data;
220 4         8 my $current_page = 1;
221              
222 4         69 while (1) {
223 4         12 $params{page} = $current_page;
224 4         16 my $result = $self->_fetch_paginated($endpoint, $request_method, $result_class, %params);
225 4         936 push @all_data, @{$result->{data}};
  4         15  
226              
227 4         9 my $pagination = $result->{pagination};
228 4 50 33     93 last unless $pagination && $pagination->{hasNextPage};
229 0         0 $current_page++;
230             }
231              
232 4         83 return \@all_data;
233             }
234              
235             sub items {
236 4     4 1 14451 my ($self, %params) = @_;
237 4         20 return $self->_fetch_paginated('items', 'items', 'WWW::MetaForge::ArcRaiders::Result::Item', %params)->{data};
238             }
239              
240              
241             sub items_paginated {
242 10     10 1 7684 my ($self, %params) = @_;
243 10         36 return $self->_fetch_paginated('items', 'items', 'WWW::MetaForge::ArcRaiders::Result::Item', %params);
244             }
245              
246              
247             sub items_all {
248 2     2 1 4736 my ($self, %params) = @_;
249 2         11 return $self->_fetch_all_pages('items', 'items', 'WWW::MetaForge::ArcRaiders::Result::Item', %params);
250             }
251              
252              
253             # Legacy alias
254 1     1 0 5758 sub items_with_pagination { shift->items_paginated(@_) }
255              
256             sub arcs {
257 2     2 1 10293 my ($self, %params) = @_;
258 2         8 return $self->_fetch_paginated('arcs', 'arcs', 'WWW::MetaForge::ArcRaiders::Result::Arc', %params)->{data};
259             }
260              
261              
262             sub arcs_paginated {
263 4     4 1 3785 my ($self, %params) = @_;
264 4         20 return $self->_fetch_paginated('arcs', 'arcs', 'WWW::MetaForge::ArcRaiders::Result::Arc', %params);
265             }
266              
267              
268             sub arcs_all {
269 1     1 1 4524 my ($self, %params) = @_;
270 1         5 return $self->_fetch_all_pages('arcs', 'arcs', 'WWW::MetaForge::ArcRaiders::Result::Arc', %params);
271             }
272              
273              
274             sub quests {
275 2     2 1 15734 my ($self, %params) = @_;
276 2         25 return $self->_fetch_paginated('quests', 'quests', 'WWW::MetaForge::ArcRaiders::Result::Quest', %params)->{data};
277             }
278              
279              
280             sub quests_paginated {
281 6     6 1 3569 my ($self, %params) = @_;
282 6         29 return $self->_fetch_paginated('quests', 'quests', 'WWW::MetaForge::ArcRaiders::Result::Quest', %params);
283             }
284              
285              
286             sub quests_all {
287 1     1 1 4503 my ($self, %params) = @_;
288 1         5 return $self->_fetch_all_pages('quests', 'quests', 'WWW::MetaForge::ArcRaiders::Result::Quest', %params);
289             }
290              
291              
292             # Legacy alias
293             sub quests_with_pagination {
294 1     1 0 3960 my ($self, %params) = @_;
295 1         5 my $result = $self->quests_paginated(%params);
296 1         372 return { quests => $result->{data}, pagination => $result->{pagination} };
297             }
298              
299             sub traders {
300 5     5 1 5727 my ($self, %params) = @_;
301 5         112 my $req = $self->request->traders(%params);
302 5         640 my $response = $self->_fetch('traders', $req, %params);
303 5         17 my $data = $self->_extract_data($response);
304              
305             # Traders API returns {"TraderName": [...items...], ...}
306             # Convert to array of trader objects
307 5 50 33     29 if (ref $data eq 'HASH' && !exists $data->{id}) {
308 5         8 my @traders;
309 5         22 for my $name (sort keys %$data) {
310             push @traders, WWW::MetaForge::ArcRaiders::Result::Trader->new(
311             name => $name,
312             inventory => $data->{$name},
313 10         10074 _raw => { name => $name, inventory => $data->{$name} },
314             );
315             }
316 5         349 return \@traders;
317             }
318              
319 0         0 return $self->_to_objects($data, 'WWW::MetaForge::ArcRaiders::Result::Trader');
320             }
321              
322              
323             # event_timers: always fresh (no cache) - time-critical data
324             sub event_timers {
325 3     3 1 9907 my ($self, %params) = @_;
326 3         83 my $req = $self->request->event_timers(%params);
327 3         505 my $response = $self->_fetch('event_timers', $req, %params, _skip_cache => 1);
328 3         13 my $data = $self->_extract_data($response);
329 3         15 return $self->_group_event_timers($data);
330             }
331              
332              
333             # Group flat event list by name+map into EventTimer objects with TimeSlots
334             sub _group_event_timers {
335 3     3   8 my ($self, $data) = @_;
336              
337 3 50 33     31 return [] unless $data && ref $data eq 'ARRAY';
338              
339             # Group by name+map
340 3         6 my %grouped;
341 3         9 for my $event (@$data) {
342 15   50     52 my $key = ($event->{name} // '') . '|' . ($event->{map} // '');
      50        
343 15         20 push @{ $grouped{$key} }, $event;
  15         38  
344             }
345              
346             # Build EventTimer objects from grouped data
347 3         7 my @timers;
348 3         16 for my $key (sort keys %grouped) {
349 6         7759 my $events = $grouped{$key};
350 6         30 my ($name, $map) = split /\|/, $key, 2;
351 6         57 push @timers, WWW::MetaForge::ArcRaiders::Result::EventTimer->from_grouped(
352             $name, $map, $events
353             );
354             }
355              
356 3         428 return \@timers;
357             }
358              
359             # event_timers_cached: use cache (for when you don't need live data)
360             sub event_timers_cached {
361 0     0 1 0 my ($self, %params) = @_;
362 0         0 my $req = $self->request->event_timers(%params);
363 0         0 my $response = $self->_fetch('event_timers', $req, %params);
364 0         0 my $data = $self->_extract_data($response);
365 0         0 return $self->_group_event_timers($data);
366             }
367              
368              
369             # event_timers_hourly: cached but invalidates at the start of each hour
370             sub event_timers_hourly {
371 0     0 1 0 my ($self, %params) = @_;
372              
373             # Calculate current hour boundary (epoch time at minute 0 of current hour)
374 0         0 my $now = time();
375 0         0 my $current_hour = int($now / 3600) * 3600;
376              
377             # Check if we have valid cached data from this hour
378 0         0 my $cache_key = 'event_timers_hourly';
379 0 0       0 if ($self->use_cache) {
380 0         0 my $cache_file = $self->cache->_cache_file($cache_key, \%params);
381 0 0       0 if ($cache_file->is_file) {
382 0         0 my $cached = eval { $self->json->decode($cache_file->slurp_utf8) };
  0         0  
383 0 0 0     0 if ($cached && ref $cached eq 'HASH') {
384 0   0     0 my $cached_time = $cached->{timestamp} // 0;
385             # Valid if cached in the same hour
386 0 0       0 if ($cached_time >= $current_hour) {
387 0         0 $self->_debug("CACHE HIT (hourly): $cache_key");
388 0         0 my $data = $cached->{data};
389 0         0 return $self->_group_event_timers($data);
390             }
391 0         0 $self->_debug("CACHE EXPIRED (new hour): $cache_key");
392             }
393             }
394             }
395              
396             # Fetch fresh data
397 0         0 my $req = $self->request->event_timers(%params);
398 0         0 my $response = $self->_fetch('event_timers', $req, %params, _skip_cache => 1);
399 0         0 my $data = $self->_extract_data($response);
400              
401             # Store in our hourly cache
402 0 0       0 if ($self->use_cache) {
403 0         0 $self->cache->set($cache_key, \%params, $data);
404 0         0 $self->_debug("CACHE SET (hourly): $cache_key");
405             }
406              
407 0         0 return $self->_group_event_timers($data);
408             }
409              
410              
411             sub map_data {
412 1     1 1 3296 my ($self, %params) = @_;
413 1         26 return $self->game_map_data->map_data(%params);
414             }
415              
416              
417             sub items_raw {
418 0     0 1 0 my ($self, %params) = @_;
419 0         0 my $req = $self->request->items(%params);
420 0         0 return $self->_fetch('items', $req, %params);
421             }
422              
423              
424             sub arcs_raw {
425 0     0 1 0 my ($self, %params) = @_;
426 0         0 my $req = $self->request->arcs(%params);
427 0         0 return $self->_fetch('arcs', $req, %params);
428             }
429              
430             sub quests_raw {
431 0     0 1 0 my ($self, %params) = @_;
432 0         0 my $req = $self->request->quests(%params);
433 0         0 return $self->_fetch('quests', $req, %params);
434             }
435              
436             sub traders_raw {
437 1     1 1 2730 my ($self, %params) = @_;
438 1         24 my $req = $self->request->traders(%params);
439 1         93 return $self->_fetch('traders', $req, %params);
440             }
441              
442             sub event_timers_raw {
443 0     0 1 0 my ($self, %params) = @_;
444 0         0 my $req = $self->request->event_timers(%params);
445 0         0 return $self->_fetch('event_timers', $req, %params);
446             }
447              
448             sub map_data_raw {
449 0     0 1 0 my ($self, %params) = @_;
450 0         0 return $self->game_map_data->map_data_raw(%params);
451             }
452              
453             sub clear_cache {
454 1     1 1 13 my ($self, $endpoint) = @_;
455 1         14 $self->cache->clear($endpoint);
456             }
457              
458              
459             # Internal cache for item lookups (populated on first use)
460             has _items_cache => (
461             is => 'rw',
462             default => sub { undef },
463             );
464              
465             sub _ensure_items_cache {
466 39     39   67 my ($self) = @_;
467 39 100       178 return $self->_items_cache if $self->_items_cache;
468              
469 1         6 $self->_debug("Loading all items for requirements calculation...");
470 1         7 my $items = $self->items_all(includeComponents => 'true');
471 1         4 my %by_name;
472             my %by_id;
473 1         4 for my $item (@$items) {
474 8         28 $by_name{lc($item->name)} = $item;
475 8         26 $by_id{$item->id} = $item;
476             }
477 1         10 $self->_items_cache({ by_name => \%by_name, by_id => \%by_id, list => $items });
478 1         8 $self->_debug("Loaded " . scalar(@$items) . " items");
479 1         5 return $self->_items_cache;
480             }
481              
482             sub find_item_by_name {
483 38     38 1 13872 my ($self, $name) = @_;
484 38         91 my $cache = $self->_ensure_items_cache;
485 38         144 return $cache->{by_name}{lc($name)};
486             }
487              
488              
489             sub find_item_by_id {
490 1     1 1 7958 my ($self, $id) = @_;
491 1         7 my $cache = $self->_ensure_items_cache;
492 1         6 return $cache->{by_id}{$id};
493             }
494              
495              
496             # Extract component name from crafting requirement
497             # Handles both string format and object format from API
498             sub _component_name {
499 19     19   35 my ($self, $component) = @_;
500 19 50       42 return unless defined $component;
501 19 50       48 return ref($component) eq 'HASH' ? $component->{name} : $component;
502             }
503              
504             sub calculate_requirements {
505 4     4 1 21513 my ($self, %args) = @_;
506 4   50     14 my $items = $args{items} // [];
507              
508 4         6 my %total;
509             my @missing;
510              
511 4         9 for my $req (@$items) {
512 4   33     11 my $name = $req->{item} // $req->{name};
513 4   33     10 my $count = $req->{count} // $req->{quantity} // 1;
      0        
514              
515 4         12 my $item = $self->find_item_by_name($name);
516 4 100       26 unless ($item) {
517 1         5 push @missing, { item => $name, count => $count, reason => 'not_found' };
518 1         4 next;
519             }
520              
521 3   50     11 my $crafting = $item->crafting_requirements // [];
522 3 100       8 if (@$crafting) {
523 2         4 for my $mat (@$crafting) {
524 6         11 my $mat_name = $self->_component_name($mat->{component});
525 6   50     10 my $mat_count = ($mat->{quantity} // 1) * $count;
526 6 50       16 $total{$mat_name} += $mat_count if $mat_name;
527             }
528             } else {
529             # Item has no crafting requirements - it's already a base material
530 1         4 push @missing, { item => $name, count => $count, reason => 'not_craftable' };
531             }
532             }
533              
534             # Resolve total to item objects
535 4         7 my @requirements;
536 4         13 for my $name (sort keys %total) {
537 6         9 my $item = $self->find_item_by_name($name);
538 6 50       9 if ($item) {
539 6         11 push @requirements, { item => $item, count => $total{$name} };
540             } else {
541 0         0 push @missing, { item => $name, count => $total{$name}, reason => 'material_not_found' };
542             }
543             }
544              
545             return {
546 4         23 requirements => \@requirements,
547             missing => \@missing,
548             };
549             }
550              
551              
552             sub calculate_base_requirements {
553 2     2 1 15946 my ($self, %args) = @_;
554 2   50     13 my $items = $args{items} // [];
555 2   50     14 my $max_depth = $args{max_depth} // 20; # Prevent infinite loops
556              
557 2         13 my %total;
558             my @missing;
559 2         0 my %seen; # Track items being processed to detect cycles
560              
561 2         0 my $resolve;
562             $resolve = sub {
563 16     16   38 my ($name, $count, $depth) = @_;
564              
565 16 50       40 if ($depth > $max_depth) {
566 0         0 push @missing, { item => $name, count => $count, reason => 'max_depth_exceeded' };
567 0         0 return;
568             }
569              
570 16         38 my $item = $self->find_item_by_name($name);
571 16 50       41 unless ($item) {
572 0         0 push @missing, { item => $name, count => $count, reason => 'not_found' };
573 0         0 return;
574             }
575              
576 16   50     48 my $crafting = $item->crafting_requirements // [];
577              
578             # Base material: no crafting requirements
579 16 100       42 if (!@$crafting) {
580 11         35 $total{lc($item->name)} += $count;
581 11         30 return;
582             }
583              
584             # Cycle detection
585 5         19 my $key = lc($item->name);
586 5 50       16 if ($seen{$key}) {
587 0         0 push @missing, { item => $name, count => $count, reason => 'cycle_detected' };
588 0         0 return;
589             }
590 5         27 $seen{$key} = 1;
591              
592             # Recursively resolve each material
593 5         14 for my $mat (@$crafting) {
594 13         35 my $mat_name = $self->_component_name($mat->{component});
595 13   50     41 my $mat_count = ($mat->{quantity} // 1) * $count;
596 13 50       56 $resolve->($mat_name, $mat_count, $depth + 1) if $mat_name;
597             }
598              
599 5         16 delete $seen{$key};
600 2         16 };
601              
602 2         7 for my $req (@$items) {
603 3   33     11 my $name = $req->{item} // $req->{name};
604 3   33     12 my $count = $req->{count} // $req->{quantity} // 1;
      0        
605 3         11 $resolve->($name, $count, 0);
606             }
607              
608             # Resolve total to item objects
609 2         7 my @requirements;
610 2         13 for my $name (sort keys %total) {
611 8         18 my $item = $self->find_item_by_name($name);
612 8 50       20 if ($item) {
613 8         32 push @requirements, { item => $item, count => $total{$name} };
614             } else {
615 0         0 push @missing, { item => $name, count => $total{$name}, reason => 'base_material_not_found' };
616             }
617             }
618              
619             return {
620 2         35 requirements => \@requirements,
621             missing => \@missing,
622             };
623             }
624              
625              
626             # Clear internal item cache (call after items data might have changed)
627             sub clear_items_cache {
628 1     1 1 535 my ($self) = @_;
629 1         96 $self->_items_cache(undef);
630             }
631              
632              
633             1;
634              
635             __END__
636              
637             =pod
638              
639             =encoding UTF-8
640              
641             =head1 NAME
642              
643             WWW::MetaForge::ArcRaiders - Perl client for the MetaForge ARC Raiders API
644              
645             =head1 VERSION
646              
647             version 0.002
648              
649             =head1 SYNOPSIS
650              
651             use WWW::MetaForge::ArcRaiders;
652              
653             my $api = WWW::MetaForge::ArcRaiders->new;
654              
655             # Get items
656             my $items = $api->items;
657             for my $item (@$items) {
658             say $item->name . " (" . $item->rarity . ")";
659             }
660              
661             # Search with parameters
662             my $ferro = $api->items(search => 'Ferro');
663              
664             # Event timers with helper methods
665             my $events = $api->event_timers;
666             for my $event (@$events) {
667             say $event->name;
668             say " Active!" if $event->is_active_now;
669             }
670              
671             # Disable caching
672             my $api = WWW::MetaForge::ArcRaiders->new(use_cache => 0);
673              
674             # For async usage (e.g., with WWW::Chain)
675             my $request = $api->request->items(search => 'Ferro');
676              
677             =head1 DESCRIPTION
678              
679             Perl interface to the MetaForge ARC Raiders API for game data
680             (items, ARCs, quests, traders, event timers, map data).
681              
682             =head2 maps
683              
684             my @maps = $api->maps;
685              
686             Returns list of available ARC Raiders map IDs (e.g., C<dam>, C<spaceport>,
687             C<buried-city>, C<blue-gate>, C<stella-montis>).
688              
689             =head2 map_display_names
690              
691             my %names = $api->map_display_names;
692              
693             Returns hash of map ID to display name (e.g., C<dam> => "Dam").
694              
695             =head2 map_display_name
696              
697             my $name = $api->map_display_name('dam'); # "Dam"
698              
699             Returns human-readable display name for a map ID. Falls back to the
700             ID itself if no display name is available.
701              
702             =head2 ua
703              
704             L<LWP::UserAgent> instance. Built lazily with sensible defaults.
705              
706             =head2 request
707              
708             L<WWW::MetaForge::ArcRaiders::Request> instance for creating
709             L<HTTP::Request> objects. Use for async framework integration.
710              
711             =head2 cache
712              
713             L<WWW::MetaForge::Cache> instance for response caching.
714              
715             =head2 use_cache
716              
717             Boolean, default true. Set to false to disable caching.
718              
719             =head2 cache_dir
720              
721             Optional L<Path::Tiny> path for cache directory. Defaults to
722             XDG cache dir on Unix, LOCALAPPDATA on Windows.
723              
724             =head2 json
725              
726             L<JSON::MaybeXS> instance for encoding/decoding JSON responses.
727              
728             =head2 debug
729              
730             Boolean. Enable debug output. Also settable via
731             C<$ENV{WWW_METAFORGE_ARCRAIDERS_DEBUG}>.
732              
733             =head2 game_map_data
734              
735             L<WWW::MetaForge::GameMapData> instance used for C<map_data> calls.
736             Configured automatically with ARC Raiders specific marker class.
737              
738             =head2 items
739              
740             my $items = $api->items(%params);
741              
742             Returns ArrayRef of L<WWW::MetaForge::ArcRaiders::Result::Item> from
743             first page. Supports C<search>, C<page>, C<limit> parameters.
744              
745             =head2 items_paginated
746              
747             my $result = $api->items_paginated(%params);
748             my $items = $result->{data};
749             my $pagination = $result->{pagination};
750              
751             Returns HashRef with C<data> (items ArrayRef) and C<pagination> info
752             (total, page, totalPages, hasNextPage).
753              
754             =head2 items_all
755              
756             my $items = $api->items_all(%params);
757              
758             Fetches all pages and returns complete ArrayRef of all items.
759             Use with caution on large datasets.
760              
761             =head2 arcs
762              
763             my $arcs = $api->arcs(%params);
764              
765             Returns ArrayRef of L<WWW::MetaForge::ArcRaiders::Result::Arc> from
766             first page. Supports C<includeLoot> parameter.
767              
768             =head2 arcs_paginated
769              
770             my $result = $api->arcs_paginated(%params);
771              
772             Returns HashRef with C<data> and C<pagination> info.
773              
774             =head2 arcs_all
775              
776             my $arcs = $api->arcs_all(%params);
777              
778             Fetches all pages and returns complete ArrayRef of all ARCs.
779              
780             =head2 quests
781              
782             my $quests = $api->quests(%params);
783              
784             Returns ArrayRef of L<WWW::MetaForge::ArcRaiders::Result::Quest> from
785             first page. Supports C<type> parameter.
786              
787             =head2 quests_paginated
788              
789             my $result = $api->quests_paginated(%params);
790              
791             Returns HashRef with C<data> and C<pagination> info.
792              
793             =head2 quests_all
794              
795             my $quests = $api->quests_all(%params);
796              
797             Fetches all pages and returns complete ArrayRef of all quests.
798              
799             =head2 traders
800              
801             my $traders = $api->traders(%params);
802              
803             Returns ArrayRef of L<WWW::MetaForge::ArcRaiders::Result::Trader>.
804              
805             =head2 event_timers
806              
807             my $events = $api->event_timers(%params);
808              
809             Returns ArrayRef of L<WWW::MetaForge::ArcRaiders::Result::EventTimer>.
810             Always fetches fresh data (bypasses cache) since event timers are time-sensitive.
811              
812             =head2 event_timers_cached
813              
814             my $events = $api->event_timers_cached(%params);
815              
816             Like C<event_timers> but uses cache. Only use when you don't need
817             real-time event status.
818              
819             =head2 event_timers_hourly
820              
821             my $events = $api->event_timers_hourly;
822              
823             Like C<event_timers_cached> but invalidates the cache at the start of
824             each hour (when minute becomes 0). Useful for scheduled data that
825             updates hourly.
826              
827             =head2 map_data
828              
829             my $markers = $api->map_data(%params);
830              
831             Returns ArrayRef of L<WWW::MetaForge::ArcRaiders::Result::MapMarker>.
832             Supports C<map> parameter.
833              
834             =head2 items_raw
835              
836             =head2 arcs_raw
837              
838             =head2 quests_raw
839              
840             =head2 traders_raw
841              
842             =head2 event_timers_raw
843              
844             =head2 map_data_raw
845              
846             Same as the corresponding methods but return raw HashRef/ArrayRef instead of result objects.
847              
848             =head2 clear_cache
849              
850             $api->clear_cache('items'); # Clear specific endpoint
851             $api->clear_cache; # Clear all
852              
853             Clear cached responses.
854              
855             =head2 find_item_by_name
856              
857             my $item = $api->find_item_by_name('Ferro I');
858              
859             Find an item by exact name (case-insensitive). Loads all items on first
860             call for fast subsequent lookups.
861              
862             =head2 find_item_by_id
863              
864             my $item = $api->find_item_by_id('ferro-i');
865              
866             Find an item by its ID.
867              
868             =head2 calculate_requirements
869              
870             my $result = $api->calculate_requirements(
871             items => [
872             { item => 'Ferro II', count => 2 },
873             { item => 'Advanced Circuit', count => 1 },
874             ]
875             );
876              
877             for my $req (@{$result->{requirements}}) {
878             say $req->{item}->name . " x" . $req->{count};
879             }
880              
881             Calculate the direct crafting materials needed to build the given items.
882             Returns a hashref with:
883              
884             =over
885              
886             =item requirements
887              
888             ArrayRef of C<< { item => $item_obj, count => N } >>
889              
890             =item missing
891              
892             ArrayRef of items that couldn't be resolved (not found, not craftable, etc.)
893              
894             =back
895              
896             =head2 calculate_base_requirements
897              
898             my $result = $api->calculate_base_requirements(
899             items => [{ item => 'Ferro III', count => 1 }],
900             max_depth => 10, # optional, default 20
901             );
902              
903             Like C<calculate_requirements> but recursively resolves all crafting
904             chains down to base materials (items with no crafting requirements).
905             Includes cycle detection and depth limiting.
906              
907             =head2 clear_items_cache
908              
909             $api->clear_items_cache;
910              
911             Clear the internal item lookup cache. Call this if item data may have
912             changed and you need fresh data for C<find_item_*> and C<calculate_*>
913             methods.
914              
915             =head1 ATTRIBUTION
916              
917             This module uses the MetaForge ARC Raiders API. Please respect their terms:
918              
919             Terms of Usage
920              
921             This API contains data maintained by our team and community
922             contributors. If you use this API in a public project, you must
923             include attribution and a link to metaforge.app/arc-raiders so
924             others know where the data comes from.
925              
926             Commercial/Paid Projects: If you plan to use this API in a paid
927             app, subscription service, or any product monetized in any way,
928             please contact us first via Discord.
929              
930             For (limited) support, visit our Discord.
931              
932             =over
933              
934             =item *
935              
936             MetaForge ARC Raiders: L<https://metaforge.app/arc-raiders>
937              
938             =item *
939              
940             MetaForge Discord: L<https://discord.gg/8UEK9TrQDs>
941              
942             =back
943              
944             =head1 SUPPORT
945              
946             =head2 Issues
947              
948             Please report bugs and feature requests on GitHub at
949             L<https://github.com/Getty/p5-www-metaforge/issues>.
950              
951             =head2 IRC
952              
953             You can reach Getty on C<irc.perl.org> for questions and support.
954              
955             =head1 CONTRIBUTING
956              
957             Contributions are welcome! Please fork the repository and submit a pull request.
958              
959             =head1 AUTHOR
960              
961             Torsten Raudssus <torsten@raudssus.de>
962              
963             =head1 COPYRIGHT AND LICENSE
964              
965             This software is copyright (c) 2026 by Torsten Raudssus.
966              
967             This is free software; you can redistribute it and/or modify it under
968             the same terms as the Perl 5 programming language system itself.
969              
970             =cut