package BDSM::Map; use strict; use warnings; use include; use BDSM::Util; # Returns a new tilemap with height * width tiles initialized as a blank tile. sub new { my $class = shift; die "new() called off an existing map!\n" if ref $class; my $height = shift; my $width = shift; my $blank = shift; die "No default key for the tile!\n" if ref $blank eq "HASH" and !(my $key = shift); my $map = []; # TODO: offset stuff foreach my $y (0 .. $height) { foreach my $x (0 .. $width) { # We don't want a ton of tiles with references to one blank tile! $map->[$y][$x] = (ref $blank eq "HASH") ? { %$blank } : $blank; } } my $self = bless { Map => $map, OnChange => sub { # The placeholder handler for changing a tile and (usually) updating the # UI. my ($mapobj, $y, $x, $new) = @_; }, Flyweight => { # Each hash entry here allows for flyweighting. So if a tile should # possibly have a certain reference to a unique object like, say, an # inventory container... instead of creating tons of useless containers, # create them as they're needed. # Example entry: # method1 => sub { undef }, # method2 => sub { @{[]} }, # _handler => $ref # _tilekey => "key" # See AUTOLOAD for details. } }, $class; $self->{TileKey} = $key if ref $blank; return $self; } # Returns the height of the map. sub height { my $self = shift; return $#{ $self->{Map} }; } # Returns the width of the map. sub width { my $self = shift; # All maps are rectangular; thus, each row is the same width. return $#{ $self->{Map}[0] }; } # Change a tile on the map. sub mod { my ($self, $y, $x, $new) = @_; die "Out of bounds ($y, $x)!\n" if $y < 0 or $x < 0 or $y > $self->height or $x > $self->width; # Each tile is either a scalar or a hash. If it's a hash and we pass in a # scalar, then (to make it nice) just assume they mean some default key. if (ref $self->{Map}[$y][$x] eq "HASH") { # "Blanking" a tile is only possible by passing in a hash with all of the # keys "blanked" or by manually resetting the tile to a new hash. my $tile = $self->{Map}[$y][$x]; if (ref $new eq "HASH") { %$tile = (%$tile, %$new); } else { $tile->{ $self->{TileKey} } = $new; } } else { $self->{Map}[$y][$x] = $new; } # Let arbitrary things happen, like updating the UI. $self->{OnChange}->($self, $y, $x, $new); return $new; } # Quick and simple dump to STDERR in a format suitable for viewing with most(1) sub dump { my $self = shift; foreach my $y (0 .. $self->height) { print STDERR join("", map { (ref $_ eq "HASH") ? $_->{ $self->{TileKey } } : $_; } @{ $self->{Map}[$y] }), "\n"; } return 1; } # Takes in the source and destination coordinates (both y, x) and a hash of # options. Returns the path if possible, -1 if impossible, or 0 if already # there. sub pathfind { # Real simple A*, actually. my $self = shift; my $y1 = shift; my $x1 = shift; my $y2 = shift; my $x2 = shift; my $opts = shift || {}; die "Need a barrier checker!\n" unless ref $opts->{Pass} eq "CODE"; # Save some time. return 0 if $y1 == $y2 and $x1 == $x2; # Destination itself cannot be traversed. return -1 unless $opts->{Pass}->($self->{Map}[$y2][$x2]); my $open = new_pqueue; $open->add({ P => abs($y1 - $y2) + abs($x1 - $x2), Y => $y1, X => $x1 }); # Set up a temp copy of the map to hold data. my $map = []; foreach my $y (0 .. $self->height) { foreach my $x (0 .. $self->width) { $map->[$y][$x] = {}; } } $map->[$y1][$x1]{Used} = 1; $map->[$y1][$x1]{Backref} = "Start"; my $node; while ($node = $open->lowest) { if ($node->{Y} == $y2 and $node->{X} == $x2) { # We're here! my $y = $node->{Y}; my $x = $node->{X}; my @path = ([$y, $x]); # From the destination node, trace back to Start. while (ref $map->[$y][$x]{Backref} eq "ARRAY") { my $backref = $map->[$y][$x]{Backref}; ($y, $x) = @$backref; unshift @path, $backref; } return @path; } $map->[ $node->{Y} ][ $node->{X} ]{Used} = 1; foreach my $dir (vi_dirs) { next if $opts->{Diags} == 0 and is_diag($dir); my ($y, $x) = keycoord($dir, $node->{Y}, $node->{X}); next if $map->[$y][$x]{Used}; # Pointless trails, dude. next unless $opts->{Pass}->($self->{Map}[$y][$x]); my $h = abs($y - $y2) + abs($x - $x2); $map->[$y][$x]{Used} = 1; $map->[$y][$x]{Backref} = [ $node->{Y}, $node->{X} ]; $open->add({ P => $h, Y => $y, X => $x }); } } return -1; } # Change the form of each tile. sub retile { my ($self, $change) = @_; foreach my $y (0 .. $self->height) { foreach my $x (0 .. $self->width) { my $new = $change->($self->{Map}[$y][$x]); delete $self->{TileKey} unless ref $new eq "HASH"; $self->{Map}[$y][$x] = $new; } } } # Finds a random tile on the map. sub getpt { my ($self, $match) = @_; my ($y, $x); while (1) { $y = CONFIG->random(0, $self->height); $x = CONFIG->random(0, $self->width); my $tile = $self->{Map}[$y][$x]; if ($self->{TileKey}) { last if $tile->{ $self->{TileKey} } eq $match; } else { last if $tile eq $match; } } return ($y, $x); } # Finds all tiles matching a specification. sub find { my ($self, $match) = @_; my @ls; foreach my $y (0 .. $self->height) { foreach my $x (0 .. $self->width) { my $tile = $self->{Map}[$y][$x]; if ($self->{TileKey}) { push @ls, [$y, $x] if $tile->{ $self->{TileKey} } eq $match; } else { push @ls, [$y, $x] if $tile eq $match; } } } return @ls; } our $AUTOLOAD; # We handle the tile flyweighting. sub AUTOLOAD { my ($self, $y, $x, $method, @args) = @_; my $do = $AUTOLOAD; $do =~ s/^.*:://; return if $do eq "DESTROY"; die "This map can't flyweight $do!\n" unless $self->{Flyweight}{$do}; my $key = $self->{Flyweight}{$do}{_tilekey}; my $tile = $self->{Map}[$y][$x]; if (my $handler = $tile->{$key}) { # No need to flyweight; a handler exists for this tile. $handler->$method(@args); } elsif (my $blank = $self->{Flyweight}{$do}{$method}) { # A blank method! return $blank->(); } else { # An authentic method. "Autovivify" a handler and delegate. $tile->{$key} = $self->{Flyweight}{$do}{_handler}->new($tile); $tile->{$key}->$method(@args); } } 42;