use strict;
use warnings;
use CGI ();
use URI::Escape;
use Text::CSV;
use Data::Dumper;
use constant DATA_FILE => 'Monsters.csv';
use constant ATTRIBUTES => (
'Climate/Terrain', 'Frequency', 'Organization',
'Activity cycle', 'Diet', 'Intelligence',
'Treasure', 'Alignment', 'No. Appearing',
'Armor Class', 'Movement', 'Hit Dice',
'THAC0', 'No. of Attacks', 'Damage/Attack',
'Special Attacks', 'Special Defenses', 'Magic Resistance',
'Size', 'Morale', 'XP Value',
);
use constant SECTIONS => (
'Appearance', 'Combat',
'Habitat/Society', 'Ecology',
'Variants', 'Note',
);
use constant FIELDS => (
'Monster',
ATTRIBUTES,
SECTIONS,
);
use constant SUMMARY_FIELDS => (
'Monster',
'Climate/Terrain', 'Frequency', 'Organization',
'Activity cycle', 'Diet', 'Intelligence',
'Treasure', 'Alignment', 'No. Appearing',
'Hit Dice',
);
my $q = CGI->new;
my @monsters = $q->param( 'name' );
print @monsters == 1
? monster_detail($q, $monsters[0])
: monster_summary($q, @monsters );
# --------------------------------
sub page_start {
# Generate the standard HTML for the start of a page.
# Returns html text.
my $q = shift; # CGI Object
my $title = shift; # Title of the page.
my $html = <
$title
$title
END_HTML
return $html;
}
sub page_end {
# Generate the standard HTML for the end of a page.
# Returns html text.
my $q = shift; # CGI Object
my $url = $q->url(-relative=>1);
my $html = <Monster List
END_HTML
return $html;
}
sub monster_detail {
# Format a monster detail page
# Returns html text.
my $q = shift; # CGI object
my $monster = shift; # Name of monster to display
my $monster_data = load_monsters( $monster );
return error_page( $q, "'$monster': Monster not found.")
unless @$monster_data;
my $html = join '',
$q->header,
page_start($q, $monster_data->[0]{Monster} ),
gen_detail( $monster_data->[0], [ ATTRIBUTES ], [ SECTIONS ] ),
page_end( $q );
return $html;
}
sub monster_summary {
# Format a monster summary page.
# Returns html text.
my $q = shift; # CGI object
my @monsters = @_; # List of monsters to summarize.
my $monster_data = load_monsters( @monsters );
return error_page( $q, "No matching monsters found (@monsters)." )
unless @$monster_data;
my $html = join '',
$q->header,
page_start($q, 'Monster Summary'),
gen_table( $monster_data, [ SUMMARY_FIELDS ], {
Monster => sub {
my ($attr, $item) = @_;
my $name = $item->{$attr};
my $esc = uri_escape( $name );
return "$name";
},
}),
page_end( $q );
return $html;
}
=head3 load_monsters
Accepts a list of monster names to load. If no list is provided, all monsters will be loaded from the data file.
Returns an array ref containing hash refs, each containing data from one monster.
=cut
sub load_monsters {
my %wanted_monster; @wanted_monster{@_} = ();
my $csv = Text::CSV_XS->new ({
binary => 1,
quote_char => '~',
sep_char => '|',
}) or die "Error creating CSV parser: ".Text::CSV->error_diag;
open my $fh, "<:encoding(utf8)", DATA_FILE
or die "Error opening data file: $!";
my @monsters;
while (my $row = $csv->getline ($fh)) {
my %monster;
@monster{ FIELDS() } = @$row;
push @monsters, \%monster
if !%wanted_monster || exists $wanted_monster{ $monster{Monster} };
}
$csv->eof or $csv->error_diag ();
close $fh;
return \@monsters;
}
sub gen_table {
# Generate monster summary table.
# Returns html text.
my $data = shift; # Array of Monster hashes
my $fields = shift; # Array of fields to use
my $formatter = shift || {}; # Hash of subs used to preprocess fields
my $html = '';
$html .= join '', map "$_ | ", @$fields;
$html .= '
';
for my $item ( @$data ) {
$html .= '';
$html .= join '',
map "$_ | ",
map {
exists $formatter->{$_}
? $formatter->{$_}( $_, $item )
: $item->{$_};
} @$fields;
$html .= '
';
}
$html .= '
';
}
sub gen_detail {
# Generate monster detail view
# Returns html text.
my $data = shift;
my $attr = shift;
my $sections = shift;
my $html = '';
$html .= join '', map "- $_
- $data->{$_}
", @$attr;
$html .= '
';
for my $section ( @$sections ) {
my @paras = split /\\/, $data->{$section};
$html .= "$section
";
$html .= join '', map "$_
", @paras;
}
return $html;
}