File Coverage

blib/lib/Games/Lacuna/Task/Role/Ships.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Games::Lacuna::Task::Role::Ships;
2              
3 1     1   1649 use 5.010;
  1         4  
  1         63  
4             our $VERSION = $Games::Lacuna::Task::VERSION;
5              
6 1     1   517 use Moose::Role;
  0            
  0            
7              
8             use List::Util qw(min sum max first);
9             use Games::Lacuna::Task::Utils qw(parse_ship_type);
10             use Games::Lacuna::Client::Types qw(ship_tags);
11              
12             sub name_ship {
13             my ($self, %params) = @_;
14            
15             my $spaceport = $params{spaceport};
16             my $name = $params{name};
17             my $ignore = $params{ignore};
18             my $prefix = $params{prefix};
19             my $ship = $params{ship};
20            
21             my ($old_name,$old_prefix,$old_ignore);
22             $ship->{name} ||= $ship->{type_human};
23             $old_name = $ship->{name} ;
24             if ($old_name =~ s/!//g) {
25             $old_ignore = 1;
26             } else {
27             $old_ignore = 0;
28             }
29              
30             if ($old_name =~ m/^([^:]+):(.+)$/) {
31             $old_prefix = $1;
32             $old_name = $2;
33             }
34            
35             $prefix ||= $old_prefix; # not defined or!
36             $ignore //= $old_ignore;
37             $name //= $old_name;
38            
39             $prefix = join(',', grep { defined $_ && $_ !~ /^\s*$/ } @{$prefix} )
40             if ref $prefix eq 'ARRAY';
41            
42             # Normalize name
43             $prefix = Games::Lacuna::Task::Utils::clean_name($prefix);
44             $name = Games::Lacuna::Task::Utils::clean_name($name);
45            
46             # Get max name length
47             my $max_length = 30;
48             $max_length -= 1
49             if $ignore;
50             $max_length -= ( 1 + length($prefix))
51             if $prefix;
52            
53             # Build new name
54             my $new_name = '';
55             $new_name .= $prefix.':'
56             if defined $prefix;
57             $new_name .= substr($name,0,$max_length);
58             $new_name .= '!'
59             if $ignore;
60            
61            
62             if ($new_name ne $ship->{name}) {
63             $self->log('notice',"Renaming ship from '%s' to '%s'",$ship->{name},$new_name);
64             $ship->{name} = $new_name;
65             $self->request(
66             object => $spaceport,
67             method => 'name_ship',
68             params => [$ship->{id},$new_name],
69             );
70             }
71             }
72              
73             sub push_ships {
74             my ($self,$form_id,$to_id,$ships) = @_;
75            
76             my $trade_object = $self->get_building_object($form_id,'Trade');
77             my $spaceport_object = $self->get_building_object($form_id,'SpacePort');
78             my $target_spaceport_object = $self->get_building_object($to_id,'SpacePort');
79            
80             return 0
81             unless $trade_object
82             && $spaceport_object
83             && $target_spaceport_object;
84            
85             my $docks_available = $self->request(
86             object => $target_spaceport_object,
87             method => 'view',
88             )->{docks_available};
89            
90             if (scalar @{$ships} > $docks_available) {
91             $ships = [ @{$ships}[0..$docks_available-1] ];
92             }
93            
94             # Loop all ships
95             my (@cargo_ships,@other_ships);
96             foreach my $ship (@{$ships}) {
97             if ($ship->{type} ~~ [qw(galleon hulk cargo freighter hulk smuggler barge dory)]) {
98             push(@cargo_ships,$ship);
99             } else {
100             push(@other_ships,$ship);
101             }
102            
103             $self->name_ship(
104             spaceport => $spaceport_object,
105             ship => $ship,
106             ignore => 0
107             );
108             }
109            
110             # Loop all cargo ships to be sent
111             foreach my $cargo_ship (sort { $b->{speed} <=> $a->{speed} } @cargo_ships) {
112             my $available_hold_size = $cargo_ship->{hold_size};
113             my @cargo;
114            
115             while (scalar @other_ships
116             && $available_hold_size >= $Games::Lacuna::Task::Constants::CARGO{ship}) {
117             my $ship = shift @other_ships;
118             push (@cargo,{
119             "type" => "ship",
120             "ship_id" => $ship->{id},
121             });
122             $available_hold_size -= $Games::Lacuna::Task::Constants::CARGO{ship}
123             }
124            
125             # Add minimum cargo
126             push(@cargo,{
127             "type" => "water",
128             "quantity" => 1,
129             }) unless scalar(@cargo);
130            
131             $self->request(
132             object => $trade_object,
133             method => 'push_items',
134             params => [
135             $to_id,
136             \@cargo,
137             {
138             ship_id => $cargo_ship->{id},
139             stay => 1,
140             }
141             ]
142             );
143             }
144              
145             # We have non-cargo ships left
146             if (scalar @other_ships) {
147             my @cargo;
148             foreach my $other_ship (@other_ships) {
149             push(@cargo,{
150             type => 'ship',
151             ship_id => $other_ship->{id},
152             });
153             }
154            
155             my $trade_ships = $self->trade_ships($form_id,\@cargo);
156            
157             foreach my $ship_id (keys %{$trade_ships}) {
158             $self->request(
159             object => $trade_object,
160             method => 'push_items',
161             params => [
162             $to_id,
163             $trade_ships->{$ship_id},
164             {
165             ship_id => $ship_id,
166             stay => 0,
167             }
168             ]
169             );
170             }
171             }
172            
173             return;
174             }
175              
176             sub trade_ships {
177             my ($self,$body_id,$cargo) = @_;
178            
179             my $trade = $self->find_building($body_id,'Trade');
180             return
181             unless defined $trade;
182             my $trade_object = $self->build_object($trade);
183            
184             # Calculate cargo capacity
185             my $required_hold_size = 0;
186             foreach my $position (@$cargo) {
187             $position->{hold_size_per_item} = $Games::Lacuna::Task::Constants::CARGO{$position->{type}};
188             $position->{quantity} //= 1;
189             $required_hold_size += $position->{hold_size_per_item} * $position->{quantity};
190             }
191            
192             # Get all trade ships
193             my $trade_ships = $self->request(
194             object => $trade_object,
195             method => 'get_trade_ships',
196             )->{ships};
197            
198             # Get max hold size
199             my $max_hold_size = max map { $_->{hold_size} } @{$trade_ships};
200            
201             my $return = {};
202            
203             # One cargo ship is enough
204             if ($max_hold_size > $required_hold_size) {
205             foreach my $cargo_ship (sort { $b->{speed} <=> $a->{speed} } @{$trade_ships}) {
206             next
207             if $cargo_ship->{name} =~ m/!/;
208             if ($cargo_ship->{hold_size} > $required_hold_size) {
209             $return->{$cargo_ship->{id}} = $cargo;
210             last;
211             }
212             }
213             # We need multiple cargo ships
214             } else {
215             foreach my $cargo_ship (sort { $b->{hold_size} <=> $a->{hold_size} } @{$trade_ships}) {
216             next
217             if $cargo_ship->{name} =~ m/!/;
218            
219             my $available_hold_size = $cargo_ship->{hold_size};
220             my @cargo_for_ship;
221            
222             foreach my $position (sort { $b->{hold_size_per_item} <=> $a->{hold_size_per_item} } @{$cargo}) {
223             next
224             if $position->{quantity} == 0;
225             if ($available_hold_size > $position->{hold_size_per_item}) {
226             my $this_position = \%{$position}; # shallow copy
227             $this_position->{quantity} = min( ($available_hold_size/$position->{hold_size_per_item}), $position->{quantity} );
228             $position->{quantity} -= $this_position->{quantity};
229             $available_hold_size -= $this_position->{quantity} * $position->{hold_size_per_item};
230             push(@cargo_for_ship,$this_position);
231             }
232             }
233            
234             last
235             if scalar @cargo_for_ship == 0;
236            
237             $return->{$cargo_ship->{id}} = \@cargo_for_ship;
238             }
239             }
240            
241             # # Remove temporary values from return value
242             # foreach my $cargo_for_ship (values %{$return}) {
243             # foreach my $position (@$cargo_for_ship) {
244             # delete $cargo_for_ship->{hold_size_per_item};
245             # }
246             # }
247              
248             return $return;
249             }
250              
251             sub spaceport_slots {
252             my ($self,$planet_id) = @_;
253            
254             my $spaceport = $self->find_building($planet_id,'SpacePort');
255            
256             return (0,0)
257             unless $spaceport;
258            
259             my $spaceport_data = $self->request(
260             object => $self->build_object($spaceport),
261             method => 'view',
262             );
263            
264             return ($spaceport_data->{docks_available},$spaceport_data->{max_ships});
265             }
266              
267             sub shipyard_slots {
268             my ($self,$planet_id) = @_;
269            
270             my @shipyards = $self->find_building($planet_id,'Shipyard');
271            
272             return (0,{})
273             unless (scalar @shipyards);
274            
275             my $total_current_queue_size = 0;
276             my $total_max_queue_size = 0;
277             my $available_shipyards = {};
278            
279             SHIPYARDS:
280             foreach my $shipyard (@shipyards) {
281             my $shipyard_id = $shipyard->{id};
282             my $shipyard_object = $self->build_object($shipyard);
283            
284             # Get build queue
285             my $shipyard_queue_data = $self->request(
286             object => $shipyard_object,
287             method => 'view_build_queue',
288             params => [1],
289             );
290            
291             my $shipyard_queue_size = $shipyard_queue_data->{number_of_ships_building} // 0;
292             $total_max_queue_size += $shipyard->{level};
293             $total_current_queue_size += $shipyard_queue_size;
294            
295             # Check available build slots
296             next SHIPYARDS
297             if $shipyard->{level} <= $shipyard_queue_size;
298            
299             $available_shipyards->{$shipyard_id} = {
300             id => $shipyard_id,
301             object => $shipyard_object,
302             level => $shipyard->{level},
303             seconds_remaining => ($shipyard_queue_data->{building}{work}{seconds_remaining} // 0),
304             available => ($shipyard->{level} - $shipyard_queue_size),
305             };
306             }
307            
308             return ( ($total_max_queue_size - $total_current_queue_size) , $available_shipyards );
309             }
310              
311             sub get_ships {
312             my ($self,%params) = @_;
313            
314             # Get params
315             my $planet_stats = $params{planet};
316             my $type = parse_ship_type($params{type});
317             my $name_prefix = $params{name_prefix};
318             my $quantity = $params{quantity};
319             my $travelling = $params{travelling} // 0;
320             my $build = $params{build} // 1;
321            
322             # Initialize vars
323             my @known_ships;
324             my @avaliable_ships;
325             my $building_ships = 0;
326             my $travelling_ships = 0;
327            
328             return
329             unless defined $type && defined $planet_stats;
330            
331             # Get space port
332             my @spaceports = $self->find_building($planet_stats->{id},'SpacePort');
333             return
334             unless scalar @spaceports;
335            
336             my $spaceport_object = $self->build_object($spaceports[0]);
337            
338             # Get all available ships
339             my $ships_data = $self->request(
340             object => $spaceport_object,
341             method => 'view_all_ships',
342             params => [ { no_paging => 1 } ],
343             );
344            
345             # Get available slots
346             my $max_spaceport_slots = sum map { $_->{level} * 2 } @spaceports;
347             my $available_spaceport_slots = max($max_spaceport_slots - $ships_data->{number_of_ships},0);
348            
349             # Find all avaliable and buildings ships
350             SHIPS:
351             foreach my $ship (@{$ships_data->{ships}}) {
352             next
353             unless $type eq $ship->{type};
354            
355             push(@known_ships,$ship->{id});
356            
357             # Check ship prefix and flags
358             if (defined $name_prefix) {
359             next SHIPS
360             unless $ship->{name} =~ m/^$name_prefix/i;
361             } else {
362             next SHIPS
363             if $ship->{name} =~ m/\!/; # Indicates reserved ship
364             }
365            
366             # Get ship activity
367             if ($ship->{task} eq 'Docked') {
368             push(@avaliable_ships,$ship->{id});
369             } elsif ($ship->{task} eq 'Building') {
370             $building_ships ++;
371             } elsif ($ship->{task} eq 'Travelling') {
372             $travelling_ships ++;
373             }
374            
375             # Check if we have enough ships
376             return @avaliable_ships
377             if defined $quantity
378             && $quantity > 0
379             && scalar(@avaliable_ships) >= $quantity;
380             }
381            
382             # Check if we should build new ships
383             return @avaliable_ships
384             unless $build;
385            
386             if (defined $quantity
387             && $quantity > 0) {
388             $quantity -= $building_ships;
389             $quantity -= $travelling_ships
390             if $travelling;
391             $quantity -= scalar(@avaliable_ships);
392             $quantity = max($quantity,0);
393             }
394            
395            
396             return @avaliable_ships
397             if ! defined $quantity || $quantity <= 0 || ! defined $type;
398            
399             my $new_building = $self->build_ships(
400             planet => $planet_stats,
401             quantity => $quantity,
402             type => $type,
403             spaceports_slots => $available_spaceport_slots,
404             (defined $name_prefix ? (name_prefix => $name_prefix):()),
405             );
406            
407             # # Rename new ships
408             # if ($new_building > 0
409             # && defined $name_prefix) {
410             #
411             # # Get all available ships
412             # my $ships_data = $self->request(
413             # object => $spaceport_object,
414             # method => 'view_all_ships',
415             # params => [ { no_paging => 1 } ],
416             # );
417             #
418             # NEW_SHIPS:
419             # foreach my $ship (@{$ships_data->{ships}}) {
420             # next NEW_SHIPS
421             # if $ship->{id} ~~ \@known_ships;
422             # next NEW_SHIPS
423             # unless $ship->{type} eq $type;
424             #
425             # my $name = $name_prefix .': '.$ship->{name}.'!';
426             #
427             # $self->log('notice',"Renaming new ship to %s on %s",$name,$planet_stats->{name});
428             #
429             # # Rename ship
430             # $self->request(
431             # object => $spaceport_object,
432             # method => 'name_ship',
433             # params => [$ship->{id},$name],
434             # );
435             # }
436             # }
437            
438             return @avaliable_ships;
439             }
440              
441             sub build_ships {
442             my ($self,%params) = @_;
443            
444             # Get params
445             my $planet_stats = $params{planet};
446             my $quantity = $params{quantity};
447             my $type = parse_ship_type($params{type});
448             my $available_spaceport_slots = $params{spaceports_slots};
449             my $available_shipyard_slots = $params{shipyard_slots};
450             my $available_shipyards = $params{shipyards};
451             my $name_prefix = $params{name_prefix};
452            
453             # Initialize vars
454             my $max_build_quantity = 0;
455             my $new_building = 0;
456            
457             # Get Buildings
458             my @spaceports = $self->find_building($planet_stats->{id},'SpacePort');
459             return 0
460             unless scalar @spaceports;
461            
462             my $spaceport_object = $self->build_object($spaceports[0]);
463            
464             # Get shipyard slots
465             unless (defined $available_shipyard_slots && defined $available_shipyard_slots) {
466             ($available_shipyard_slots,$available_shipyards) = $self->shipyard_slots($planet_stats->{id});
467             }
468            
469             # Get spaceport slots
470             unless (defined $available_spaceport_slots) {
471             ($available_spaceport_slots,undef) = $self->spaceport_slots($planet_stats->{id});
472             }
473            
474             # Calc max spaceport capacity
475             my $max_ships_possible = sum map { $_->{level} * 2 } @spaceports;
476            
477             # Quantity is defined as free-spaceport slots
478             if ($quantity < 0) {
479             $max_build_quantity = max($max_ships_possible - $available_spaceport_slots + $quantity,0);
480             # Quantity is defined as number of ships
481             } else {
482             $max_build_quantity = min($max_ships_possible - $available_spaceport_slots,$quantity);
483             $max_build_quantity = max($max_build_quantity,0);
484             }
485            
486             # Check max build queue size
487             $max_build_quantity = min($available_shipyard_slots,$max_build_quantity);
488            
489            
490             # Check if we can build new ships
491             return 0
492             unless ($max_build_quantity > 0);
493            
494             my @ships_building;
495            
496             # Repeat until we have enough ships
497             BUILD_QUEUE:
498             while ($new_building < $max_build_quantity) {
499            
500             my $shipyard =
501             first { $_->{available} > 0 }
502             sort { $a->{seconds_remaining} <=> $b->{seconds_remaining} }
503             values %{$available_shipyards};
504            
505             last BUILD_QUEUE
506             unless defined $shipyard;
507            
508             # Get build quantity
509             my $build_per_shipyard = int(($max_build_quantity - $new_building) / scalar (keys %{$available_shipyards}) / 1.5) || 1;
510             my $build_quantity = min($shipyard->{available},$max_build_quantity,$build_per_shipyard);
511            
512             eval {
513             # Build ship
514             my $response = $self->request(
515             object => $shipyard->{object},
516             method => 'build_ship',
517             params => [$type,$build_quantity],
518             );
519            
520             $shipyard->{seconds_remaining} = $response->{building}{work}{seconds_remaining};
521            
522             $self->log('notice',"Building %i %s(s) on %s at shipyard level %i",$build_quantity,$type,$planet_stats->{name},$shipyard->{level});
523            
524             # Remove shipyard slot
525             $shipyard->{available} -= $build_quantity;
526            
527             # Remove from available shipyards
528             delete $available_shipyards->{$shipyard->{id}}
529             if $shipyard->{available} <= 0;
530            
531             # Add ship to list and rename
532             for (1..$build_quantity) {
533             my $ship_building = pop(@{$response->{ships_building}});
534             push(@ships_building,$ship_building);
535            
536             if (defined $name_prefix) {
537             $self->name_ship(
538             spaceport => $spaceport_object,
539             ship => $ship_building,
540             prefix => $name_prefix,
541             name => $ship_building->{type_human},
542             ignore => 1,
543             );
544             }
545             }
546            
547             };
548             if ($@) {
549             $self->log('warn','Could not build %s: %s',$type,$@);
550             last BUILD_QUEUE;
551             }
552            
553             $new_building += $build_quantity;
554             }
555            
556             return wantarray ? @ships_building : scalar @ships_building;
557             }
558              
559             no Moose::Role;
560             1;
561              
562             =encoding utf8
563              
564             =head1 NAME
565              
566             Games::Lacuna::Task::Role::Ships - Helper methods for fetching and building ships
567              
568             =head1 SYNOPSIS
569              
570             package Games::Lacuna::Task::Action::MyTask;
571             use Moose;
572             extends qw(Games::Lacuna::Task::Action);
573             with qw(Games::Lacuna::Task::Role::Ships);
574            
575             =head1 DESCRIPTION
576              
577             This role provides ship-related helper methods.
578              
579             =head1 METHODS
580              
581             =head2 get_ships
582              
583             my @avaliable_scows = $self->get_ships(
584             planet => $planet_stats,
585             ships_needed => 3, # get three
586             ship_type => 'scow',
587             );
588              
589             Tries to fetch the given number of available ships. If there are not enough
590             ships available then the required number of ships are built.
591              
592             The following arguments are accepted
593              
594             =over
595              
596             =item * planet
597              
598             Planet data has [Required]
599              
600             =item * ships_needed
601              
602             Number of required ships. If ships_needed is a negative number it will return
603             all matching ships and build as many new ships as possible while keeping
604             ships_needed * -1 space port slots free [Required]
605              
606             =item * ship_type
607              
608             Ship type [Required]
609              
610             =item * travelling
611              
612             If true will not build new ships if there are matchig ships currently
613             travelling
614              
615             =item * name_prefix
616              
617             Will only return ships with the given prefix in their names. Newly built ships
618             will be renamed to add the prefix.
619              
620             =back
621              
622             =head2 trade_ships
623              
624             my $trade_ships = $self->trade_ships($body_id,$cargo_list);
625              
626             Returns a hashref with cargo ship ids as keys and cargo lists as values.
627              
628             =head2 push_ships
629              
630             $self->push_ships($from_body_id,$to_body_id,\@ships);
631              
632             Pushes the selected ships from one body to another
633              
634             =cut