Difference between revisions of "List of spells/parse-spl-data"

From CrawlWiki
Jump to: navigation, search
(Import Neil's script)
 
m (Propose deletion.)
 
(7 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 +
{{delete|No longer used}}
 
<pre>
 
<pre>
 
<nowiki>
 
<nowiki>
Line 10: Line 11:
 
#
 
#
 
# The latest version of this program may be found at:
 
# The latest version of this program may be found at:
#  http://crawl.chaosforge.org/index.php?title=User:Neil/parse-spl-data
+
#  http://crawl.chaosforge.org/List_of_spells/parse-spl-data
  
 
use strict;
 
use strict;
Line 18: Line 19:
 
our $VERSION = "0.1.0";
 
our $VERSION = "0.1.0";
  
# Input directory.
 
my $indir = "./";
 
 
# Map from letter, school, flag, book name, or level to list of spells.
 
my %by_letter = ();
 
my %by_school = ();
 
my %by_flag = ();
 
my %by_book = ();
 
my %by_level = ();
 
 
# Map from ID or name to spell
 
my %by_id = ();
 
my %by_name = ();
 
 
# Map from spell name to list of book names.
 
my %book_spells = ();
 
 
# Descriptions of spell flags
 
my %flag_descs = (
 
ALLOW_SELF => <<"EOF",
 
The spell is not helpful, but you will not receive a "Really target yourself?"
 
prompt.  You may still receive "That beam is likely to hit you." for
 
"[[#Dir or target|dir or target]]" spells such as Mephitic Cloud.
 
EOF
 
AREA => <<"EOF",
 
The spell harms an area.  Pacified fleeing monsters will not use emergency
 
spells with this flag.
 
EOF
 
BATTLE => <<"EOF",
 
The spell is a non-[[Conjuration]] spell disliked by [[Elyvilon]].  There is no
 
piety penalty for using such spells, but a randart spellbook containing one of
 
these spells will never have Elyvilon's name on it.
 
EOF
 
CHAOTIC => <<"EOF",
 
The spell is hated by [[Zin]], who will impose penance on any player casting
 
it, and reward killing monsters who can cast it.
 
EOF
 
CORPSE_VIOLATING => <<"EOF",
 
The spell is hated by [[Fedhas Madash]], who will impose penance on any player
 
casting it.
 
EOF
 
DIR => <<"EOF",
 
The spell requires choosing a direction (and not a target).
 
EOF
 
DIR_OR_TARGET => <<"EOF",
 
The spell requires choosing a direction or target, and is stopped by
 
interposing creatures.
 
EOF
 
ESCAPE => <<"EOF",
 
The spell helps you get out of trouble.  Xom considers such spells boring, and
 
will not gift spellbooks containing them.  Furthermore, the spell is an option
 
when control-clicking yourself in tiles mode.
 
EOF
 
GRID => <<"EOF",
 
The spell targets a grid square, disregarding any creatures in the way.  This
 
is a form of smite targeting that does not require a target.
 
EOF
 
HASTY => <<"EOF",
 
The spell is hated by [[Cheibriados]], who will impose penance on any player
 
casting it.
 
EOF
 
HELPFUL => <<"EOF",
 
The spell helps you or the target; if targeted, the targeting commands
 
cycle through friendlies rather than hostiles.  Xom considers such spells
 
boring, and will not gift spellbooks containing them.
 
EOF
 
NEUTRAL => <<"EOF",
 
The spell is neither harmful nor helpful; if targeted, the targeting
 
commands cycle through all creatures, not just hostiles.  Xom considers
 
such spells boring, and will not gift spellbooks containing them.
 
EOF
 
NONE => <<"EOF",
 
The spell has no special flags.  Such spells are always untargeted, except
 
for [[Projected Noise]].
 
EOF
 
NOT_SELF => <<"EOF",
 
The spell may not target you or your square.
 
EOF
 
RECOVERY => <<"EOF",
 
The spell helps you recover from ill effects.  Xom considers such spells
 
boring, and will not gift spellbooks containing them.  Furthermore, the spell
 
is an option when control-clicking yourself in tiles mode.
 
EOF
 
TARGET => <<"EOF",
 
The spell targets a creature, disregarding any other creatures in the way.
 
This is a form of smite targeting that requires a target creature.
 
EOF
 
TARG_OBJ => <<"EOF",
 
The spell targets an object, disregarding any other creatures in the way.
 
This is a form of smite targeting that requires a target object.
 
EOF
 
UNCLEAN => <<"EOF",
 
The spell is hated by [[Zin]], who will impose penance on any player casting
 
it, and reward killing monsters who can cast it.
 
EOF
 
UNHOLY => <<"EOF",
 
The spell is a non-necromantic unholy spell.  It is hated by [[good]] gods
 
([[Elyvilon]], [[The Shining One]], and [[Zin]]), who will impose penance on
 
any player casting it.
 
EOF
 
);
 
  
 
sub crawl_version {
 
sub crawl_version {
 +
        my ($indir) = @_;
 
# Look for util/release_ver first
 
# Look for util/release_ver first
 
if (open VERSION, "<", "${indir}util/release_ver") {
 
if (open VERSION, "<", "${indir}util/release_ver") {
Line 139: Line 40:
 
my $range = shift;
 
my $range = shift;
 
return "LOS" if $range eq "LOS_RADIUS";
 
return "LOS" if $range eq "LOS_RADIUS";
return 5 if $range eq "TORNADO_RADIUS";
+
return 6 if $range eq "TORNADO_RADIUS";
 
return "" if $range eq "-1";
 
return "" if $range eq "-1";
 
return $range;
 
return $range;
Line 166: Line 67:
 
}
 
}
  
 +
sub ucfirst_array {
 +
    my $items = join ",", map {
 +
        my $name = ucfirst(lc $_);
 +
        "\"$name\"";
 +
    } @_;
 +
    "{ $items }";
 +
}
  
 
# Add a spell if it exists
 
# Add a spell if it exists
sub maybe_add_spell($) {
+
sub maybe_add_spell {
my $spell = shift;
+
my ($spell, $by_id, $by_key, $sortkey) = @_;
 
return undef unless $spell->{name};
 
return undef unless $spell->{name};
  
Line 179: Line 87:
 
# Sanity checks: abort if we encounter one of these, as it means
 
# Sanity checks: abort if we encounter one of these, as it means
 
# something funny is up with our book-parsing.
 
# something funny is up with our book-parsing.
+
 
 
# Spells that belong to certain monster-only schools.
 
# Spells that belong to certain monster-only schools.
 
for my $school (@{$spell->{schools}}) {
 
for my $school (@{$spell->{schools}}) {
Line 194: Line 102:
  
 
# It's worth keeping.
 
# It's worth keeping.
$by_id{$spell->{id}} = $spell;
+
$by_id->{$spell->{id}} = $spell;
$by_name{$spell->{name}} = $spell;
+
        if ($sortkey eq "alpha") {
push @{$by_level{$spell->{level}}}, $spell;
+
                $by_key->{$spell->{name}} = $spell;
 
+
        } elsif ($sortkey eq "letter") {
my $let = uc(substr $spell->{name}, 0, 1);
+
                my $let = uc(substr $spell->{name}, 0, 1);
push @{$by_letter{$let}}, $spell;
+
                push @{$by_key->{$let}}, $spell;
+
        } elsif ($sortkey eq "level") {
 
+
                push @{$by_key->{$spell->{level}}}, $spell;
for my $school (@{$spell->{schools}}) {
+
        } elsif ($sortkey eq "school") {
push @{$by_school{$school}}, $spell;
+
                for my $school (@{$spell->{schools}}) {
}
+
                        push @{$by_key->{$school}}, $spell;
for my $flag (@{$spell->{flags}}) {
+
                }
push @{$by_flag{$flag}}, $spell;
+
        } elsif ($sortkey eq "flag") {
}
+
                for my $flag (@{$spell->{flags}}) {
 +
                        push @{$by_key->{$flag}}, $spell;
 +
                }
 +
        }
 
return 1;
 
return 1;
 
}
 
}
Line 229: Line 140:
 
### MAIN
 
### MAIN
  
my $sortkey = "alpha";
+
 
my $sortfn = \&sort_by_name;
+
sub main {
GetOptions(
+
        # Input directory.
"a|alphabetic" => sub { $sortkey = "alpha"; $sortfn = \&sort_by_name },
+
        my $indir = "./";
"b|book" => sub { $sortkey = "book"; $sortfn = undef },
+
 
"f|flag" => sub { $sortkey = "flag"; $sortfn = \&sort_by_level },
+
        # Map from letter, school, flag, book name, or level to list of spells.
"l|level" => sub { $sortkey = "level"; $sortfn = \&sort_by_school },
+
        my %by_key = ();
"s|school" => sub { $sortkey = "school"; $sortfn = \&sort_by_level },
+
        my %by_letter = ();
"h|help" => sub {
+
        my %by_school = ();
print <<"EOF";
+
        my %by_flag = ();
 +
        my %by_book = ();
 +
        my %by_level = ();
 +
 
 +
        # Map from ID or name to spell
 +
        my %by_id = ();
 +
        my %by_name = ();
 +
 
 +
        # Map from spell name to list of book names.
 +
        my %book_spells = ();
 +
 
 +
 
 +
 
 +
        my $sortkey = "alpha";
 +
        my $sortfn = \&sort_by_name;
 +
        my $module = "default";
 +
 
 +
        GetOptions(
 +
                  "a|alphabetic" => sub { $sortkey = "letter"; $sortfn = \&sort_by_name; },
 +
                  "b|book" => sub { $sortkey = "book"; $sortfn = undef; },
 +
                  "f|flag" => sub { $sortkey = "flag"; $sortfn = \&sort_by_level; },
 +
                  "l|level" => sub { $sortkey = "level"; $sortfn = \&sort_by_school; },
 +
                  "s|school" => sub { $sortkey = "school"; $sortfn = \&sort_by_level; },
 +
                  "m|module-book" => sub { $module = "book"; $sortkey = "book" ; $sortfn = undef; },
 +
                  "k|module-spell" => sub { $module = "spell"; $sortkey = "letter"; $sortfn = \&sort_by_name; },
 +
                  "h|help" => sub {
 +
                          print <<"EOF";
 
Usage: $0 [options] [directory]
 
Usage: $0 [options] [directory]
  
Line 245: Line 182:
  
 
Options include:
 
Options include:
   -a, --alphabetic Arrange alphabetically (default).
+
   -a, --alphabetic   Arrange alphabetically (default).
   -b, --book       Arrange by book, then level.
+
   -b, --book         Arrange by book, then level.
   -f, --flag       Arrange by flag, then level.
+
   -f, --flag         Arrange by flag, then level.
   -l, --level       Arrange by level, then schools.
+
  -k, --module-spell  Generate a Lua table of spells.
   -s, --school     Arrange by school, then level.
+
   -l, --level         Arrange by level, then schools.
   -h, --help       Display this help.
+
  -m, --module-book  Generate a Lua table of spellbooks.
 +
   -s, --school       Arrange by school, then level.
 +
   -h, --help         Display this help.
  
 
Spells of the same schools and level are sorted by name.  With the --book,
 
Spells of the same schools and level are sorted by name.  With the --book,
 
--school, and --flag option, spells may appear multiple times.
 
--school, and --flag option, spells may appear multiple times.
 
EOF
 
EOF
exit 0;
+
                          exit 0;
},
+
                  },
);
+
                  );
 +
 
 +
        if (@ARGV) {
 +
                $indir = shift @ARGV;
 +
                $indir .= "/" unless $indir eq "" or $indir =~ m!/$!;
 +
        }
 +
 
 +
        $DATE = gmtime;
 +
        $CRAWL_VERSION = crawl_version($indir);
 +
        ($PROGNAME = $0) =~ s!.*/!!;
 +
 
 +
        parse_book_data($indir, \%book_spells, \%by_book);
 +
        parse_spl_data($indir, \%book_spells, \%by_id, \%by_key, $sortkey);
 +
        for my $k (keys %by_book) {
 +
                # Convert the list of spell ids to a list of spells, but
 +
                # remove those that aren't in %by_id because maybe_add_spell
 +
                # skipped them.
 +
                my @spells =  map { $by_id{$_} || () } @{$by_book{$k}};
 +
                if (@spells) {
 +
                        $by_book{$k} = \@spells;
 +
                } else {
 +
                        # Remove the book if it has no spells.
 +
                        delete $by_book{$k};
 +
                }
 +
        }
  
if (@ARGV) {
+
        # Print data
$indir = shift @ARGV;
+
        if ($module eq "default") {
$indir .= "/" unless $indir eq "" or $indir =~ m!/$!;
+
                if ($sortkey eq "book") {
 +
                        module_default($sortkey, $sortfn, \%by_book);
 +
                } else {
 +
                        module_default($sortkey, $sortfn, \%by_key);
 +
                }
 +
        } elsif ($module eq "book") {
 +
                module_book(\%by_book);
 +
        } elsif ($module eq "spell") {
 +
                module_spell($sortfn, \%by_key);
 +
        }
 
}
 
}
  
$DATE = gmtime;
+
sub parse_book_data {
$CRAWL_VERSION = crawl_version;
+
        my ($indir, $book_spells, $by_book) = @_;
($PROGNAME = $0) =~ s!.*/!!;
+
        # Name of current book.
 
+
        my $book = "bug" ;
 +
        open BOOKS, "${indir}book-data.h"
 +
          or die "could not open ${indir}book-data.h: $!";
 +
        while (<BOOKS>) {
 +
                # Skip conditional sections.  Really we need to look at
 +
                # the condition, but that requires the C preprocessor and
 +
                # I'd rather avoid that.
  
# Name of current book.
+
                #next if /^#if/../^#endif/;
my $book = "bug" ;
+
                next if /^#if TAG_MAJOR_VERSION == 34/../^#endif/;
open BOOKS, "${indir}book-data.h"
 
or die "could not open ${indir}book-data.h: $!";
 
while (<BOOKS>) {
 
# Skip conditional sections.  Really we need to look at
 
# the condition, but that requires the C preprocessor and
 
# I'd rather avoid that.
 
next if /^#if/../^#endif/;
 
  
# Kind of hackish --- quit at the first rod
+
                # Kind of hackish --- quit at the first rod
last if m!// Rod!;
+
                last if m!// Rod!;
  
if (m!^{\s*// (.*)!) {
+
                if (m!^{\s*// (.*)!) {
# Get the spell name from the comment.
+
                        # Get the spell name from the comment.
$book = $1;
+
                        $book = $1;
# Disambiguation for Conjuration
+
                        # Disambiguation for Conjuration
my $extra = "";
+
                        my $extra = "";
  
# Remove parenthesized bits
+
                        # Remove parenthesized bits
$book =~ s/\s+\([^)]*\)//g;
+
                        $book =~ s/\s+\([^)]*\)//g;
  
# Remove extra description
+
                        # Remove extra description
if ($book =~ s/ - (.*)//) {
+
                        if ($book =~ s/ - (.*)//) {
# But remember it for Conjuration
+
                                # But remember it for Conjuration
if ($1 =~ /Fire and Earth/) {
+
                                if ($1 =~ /Fire and Earth/) {
$extra = " (fire+earth)";
+
                                        $extra = " (fire+earth)";
} elsif ($1 =~ /Air and Ice/) {
+
                                } elsif ($1 =~ /Air and Ice/) {
$extra = " (ice+air)";
+
                                        $extra = " (ice+air)";
}
+
                                }
}
+
                        }
  
# Remove roman numeral counter
+
                        # Remove roman numeral counter
$book =~ s/\s+[IVX]+$//;
+
                        $book =~ s/\s+[IVX]+$//;
  
# Replace "Tome of" with "Book of" (special case for Dragon)
+
                        # Replace "Tome of" with "Book of" (special case for Dragon)
$book =~ s/^Tome of/Book of/;
+
                        $book =~ s/^Tome of/Book of/;
  
# And special-case Minor Magic.
+
                        # And special-case Minor Magic.
$book =~ s/^Minor/Book of Minor/;
+
                        $book =~ s/^Minor/Book of Minor/;
  
# Append disambiguation
+
                        # Append disambiguation
$book .= $extra;
+
                        $book .= $extra;
} else {
+
                } else {
while (/SPELL_(\w+)/g) {
+
                        while (/SPELL_(\w+)/g) {
push @{$book_spells{$1}}, $book;
+
                                push @{$book_spells->{$1}}, $book;
push @{$by_book{$book}}, $1;
+
                                push @{$by_book->{$book}}, $1;
}
+
                        }
}
+
                }
 +
        }
 +
        close BOOKS;
 
}
 
}
close BOOKS;
 
  
# Current spell.  Members are
+
sub parse_spl_data {
#  {name}    : Spell name
+
        # Current spell.  Members are
#  {id}      : SPELL_* identifier (without the SPELL_)
+
        #  {name}    : Spell name
#  {schools} : List of SPTYP_* constants (without the SPTYP_)
+
        #  {id}      : SPELL_* identifier (without the SPELL_)
#  {flags}  : List of SPFLAG_* constants (without the SPFLAG_)
+
        #  {schools} : List of SPTYP_* constants (without the SPTYP_)
#  {books}  : List of book names.
+
        #  {flags}  : List of SPFLAG_* constants (without the SPFLAG_)
#  {level}  : Level
+
        #  {books}  : List of book names.
#  {cap}    : Power cap
+
        #  {level}  : Level
#  {minrange}: Minimum range, or "LOS" or ""
+
        #  {cap}    : Power cap
#  {maxrange}: Maximum range, or "LOS" or ""
+
        #  {minrange}: Minimum range, or "LOS" or ""
#  {noisy}  : Noise modifier
+
        #  {maxrange}: Maximum range, or "LOS" or ""
#
+
        #  {noisy}  : Noise modifier
#  {data}    : List of extra data; converted to {level}-{noise} at the end
+
        #
#              of the spell block.
+
        #  {data}    : List of extra data; converted to {level}-{noise} at the end
my $spell = {};
+
        #              of the spell block.
open SPELLS, "${indir}spl-data.h"
+
        my ($indir, $book_spells, $by_id, $by_key, $sortkey) = @_;
or die "could not open ${indir}spl-data.h: $!";
+
        my $spell = {};
while (<SPELLS>) {
+
        open SPELLS, "${indir}spl-data.h"
chomp;
+
          or die "could not open ${indir}spl-data.h: $!";
if (/^{/) {
+
        while (<SPELLS>) {
$spell = {};
+
                chomp;
} elsif (/^}/) {
+
                if (/^{/) {
# Unpack data
+
                        $spell = {};
my (
+
                } elsif (/^}/) {
$sch, $flag, $lev, $cap, $minr, $maxr, $nm, @rest
+
                        # Unpack data
) = @{$spell->{data}};
+
                        my (
 +
                            $sch, $flag, $lev, $cap, $minr, $maxr, $nm, @rest
 +
                          ) = @{$spell->{data}};
  
# Parse out schools and flags
+
                        # Parse out schools and flags
$spell->{schools} = [
+
                        $spell->{schools} = [
map { s/SPTYP_//; $_ } split /\s*\|\s*/, $sch
+
                                            map { s/SPTYP_//; $_ } split /\s*\|\s*/, $sch
];
+
                                            ];
$spell->{flags} = [
+
                        $spell->{flags} = [
map { s/SPFLAG_//; $_ } split /\s*\|\s*/, $flag
+
                                          map { s/SPFLAG_//; $_ } split /\s*\|\s*/, $flag
];
+
                                          ];
  
# Include the rest of the data
+
                        # Include the rest of the data
$spell->{level} = $lev;
+
                        $spell->{level} = $lev;
$spell->{cap} = $cap;
+
                        $spell->{cap} = $cap;
$spell->{minrange} = xlate_range $minr;
+
                        $spell->{minrange} = xlate_range $minr;
$spell->{maxrange} = xlate_range $maxr;
+
                        $spell->{maxrange} = xlate_range $maxr;
$spell->{noisy} = $nm;
+
                        $spell->{noisy} = $nm;
  
maybe_add_spell($spell);
+
                        maybe_add_spell($spell, $by_id, $by_key, $sortkey);
} elsif (/^\s*SPELL_(\w+),\s+"([^"]*)",/) {
+
                } elsif (/^\s*SPELL_(\w+),\s+"([^"]*)",/) {
$spell->{id} = $1;
+
                        $spell->{id} = $1;
$spell->{name} = $2;
+
                        $spell->{name} = $2;
if (exists $book_spells{$1}) {
+
                        if (exists $book_spells->{$1}) {
$spell->{books} = [ @{$book_spells{$1}} ];
+
                                $spell->{books} = [ @{$book_spells->{$1}} ];
} else {
+
                        } else {
$spell->{books} = [];
+
                                $spell->{books} = [];
}
+
                        }
} else {
+
                } else {
# Strip comments first.
+
                        # Strip comments first.
s!\s*//.*!!;
+
                        s!\s*//.*!!;
# Get comma-delimited sections
+
                        # Get comma-delimited sections
while (/\s*([^,]+)(,|$)/g) {
+
                        while (/\s*([^,]+)(,|$)/g) {
if (substr($1, 0, 1) eq "|") {
+
                                if (substr($1, 0, 1) eq "|") {
 
# Continuation line; really we should check
 
# Continuation line; really we should check
 
# whether the previous line ended with a
 
# whether the previous line ended with a
 
# comma, but this is probably good enough.
 
# comma, but this is probably good enough.
$spell->{data}[-1] .= $1
+
                                        $spell->{data}[-1] .= $1
} else {
+
                                } else {
push @{$spell->{data}}, $1;
+
                                        push @{$spell->{data}}, $1;
}
+
                                }
}
+
                        }
  
}
+
                }
 +
        }
 +
        close SPELLS;
 
}
 
}
close SPELLS;
 
  
for my $k (keys %by_book) {
 
# Convert the list of spell ids to a list of spells, but
 
# remove those that aren't in %by_id because maybe_add_spell
 
# skipped them.
 
my @spells =  map { $by_id{$_} || () } @{$by_book{$k}};
 
if (@spells) {
 
$by_book{$k} = \@spells;
 
} else {
 
# Remove the book if it has no spells.
 
delete $by_book{$k};
 
}
 
}
 
  
print <<"EOF";
+
sub module_default {
 +
        my ($sortkey, $sortfn, $by_key) = @_;
 +
        # Descriptions of spell flags
 +
        my %flag_descs = (
 +
                          ALLOW_SELF => <<"EOF",
 +
The spell is not helpful, but you will not receive a "Really target yourself?"
 +
prompt.  You may still receive "That beam is likely to hit you." for
 +
"[[#Dir or target|dir or target]]" spells such as Mephitic Cloud.
 +
EOF
 +
                          AREA => <<"EOF",
 +
The spell harms an area.  Pacified fleeing monsters will not use emergency
 +
spells with this flag.
 +
EOF
 +
                          BATTLE => <<"EOF",
 +
The spell is a non-[[Conjuration]] spell disliked by [[Elyvilon]].  There is no
 +
piety penalty for using such spells, but a randart spellbook containing one of
 +
these spells will never have Elyvilon's name on it.
 +
EOF
 +
                          CHAOTIC => <<"EOF",
 +
The spell is hated by [[Zin]], who will impose penance on any player casting
 +
it, and reward killing monsters who can cast it.
 +
EOF
 +
                          CORPSE_VIOLATING => <<"EOF",
 +
The spell is hated by [[Fedhas Madash]], who will impose penance on any player
 +
casting it.
 +
EOF
 +
                          DIR => <<"EOF",
 +
The spell requires choosing a direction (and not a target).
 +
EOF
 +
                          DIR_OR_TARGET => <<"EOF",
 +
The spell requires choosing a direction or target, and is stopped by
 +
interposing creatures.
 +
EOF
 +
                          ESCAPE => <<"EOF",
 +
The spell helps you get out of trouble.  Xom considers such spells boring, and
 +
will not gift spellbooks containing them.  Furthermore, the spell is an option
 +
when control-clicking yourself in tiles mode.
 +
EOF
 +
                          GRID => <<"EOF",
 +
The spell targets a grid square, disregarding any creatures in the way.  This
 +
is a form of smite targeting that does not require a target.
 +
EOF
 +
                          HASTY => <<"EOF",
 +
The spell is hated by [[Cheibriados]], who will impose penance on any player
 +
casting it.
 +
EOF
 +
                          HELPFUL => <<"EOF",
 +
The spell helps you or the target; if targeted, the targeting commands
 +
cycle through friendlies rather than hostiles.  Xom considers such spells
 +
boring, and will not gift spellbooks containing them.
 +
EOF
 +
                          NEUTRAL => <<"EOF",
 +
The spell is neither harmful nor helpful; if targeted, the targeting
 +
commands cycle through all creatures, not just hostiles.  Xom considers
 +
such spells boring, and will not gift spellbooks containing them.
 +
EOF
 +
                          NONE => <<"EOF",
 +
The spell has no special flags.  Such spells are always untargeted.
 +
EOF
 +
                          NOT_SELF => <<"EOF",
 +
The spell may not target you or your square.
 +
EOF
 +
                          RECOVERY => <<"EOF",
 +
The spell helps you recover from ill effects.  Xom considers such spells
 +
boring, and will not gift spellbooks containing them.  Furthermore, the spell
 +
is an option when control-clicking yourself in tiles mode.
 +
EOF
 +
                          TARGET => <<"EOF",
 +
The spell targets a creature, disregarding any other creatures in the way.
 +
This is a form of smite targeting that requires a target creature.
 +
EOF
 +
                          TARG_OBJ => <<"EOF",
 +
The spell targets an object, disregarding any other creatures in the way.
 +
This is a form of smite targeting that requires a target object.
 +
EOF
 +
                          UNCLEAN => <<"EOF",
 +
The spell is hated by [[Zin]], who will impose penance on any player casting
 +
it, and reward killing monsters who can cast it.
 +
EOF
 +
                          UNHOLY => <<"EOF",
 +
The spell is a non-necromantic unholy spell.  It is hated by [[good]] gods
 +
([[Elyvilon]], [[The Shining One]], and [[Zin]]), who will impose penance on
 +
any player casting it.
 +
EOF
 +
                        );
 +
        print <<"EOF";
 
==Spells== <!-- We *must* have a heading before the table, or the TOC will end up inside the table! -->
 
==Spells== <!-- We *must* have a heading before the table, or the TOC will end up inside the table! -->
  
 
<!-- Automatically generated by $PROGNAME $VERSION
 
<!-- Automatically generated by $PROGNAME $VERSION
 
     from Dungeon Crawl Stone Soup version $CRAWL_VERSION
 
     from Dungeon Crawl Stone Soup version $CRAWL_VERSION
    on $DATE
 
 
   -->
 
   -->
 
{| class="prettytable"
 
{| class="prettytable"
 +
!rowspan=2|Image
 
!rowspan=2|Name
 
!rowspan=2|Name
 
!rowspan=2|Schools
 
!rowspan=2|Schools
Line 424: Line 472:
 
EOF
 
EOF
  
my %by_key;
+
        # TODO: allow sorting by other criteria
if ($sortkey eq "alpha") {
+
        for my $key (sort keys %{$by_key}) {
%by_key = %by_letter
+
                my @spells = @{$by_key->{$key}};
} elsif ($sortkey eq "book") {
+
                @spells = sort $sortfn @spells if $sortfn;
%by_key = %by_book
 
} elsif ($sortkey eq "level") {
 
%by_key = %by_level
 
} elsif ($sortkey eq "school") {
 
%by_key = %by_school
 
} elsif ($sortkey eq "flag") {
 
%by_key = %by_flag
 
}
 
 
 
# TODO: allow sorting by other criteria
 
for my $key (sort keys %by_key) {
 
my @spells = @{$by_key{$key}};
 
@spells = sort $sortfn @spells if $sortfn;
 
  
print "|----\n! colspan=8 style=\"text-align:left\"|\n====";
+
                print "|----\n! colspan=9 style=\"text-align:left\"|\n====";
  
# Format and link the key appropriately
+
                # Format and link the key appropriately
if ($sortkey eq "book") {
+
                if ($sortkey eq "book") {
print "[[$key]]";
+
                        print "[[$key]]";
} elsif ($sortkey eq "school") {
+
                } elsif ($sortkey eq "school") {
print format_schools $key;
+
                        print format_schools $key;
} elsif ($sortkey eq "level") {
+
                } elsif ($sortkey eq "level") {
print "level $key";
+
                        print "level $key";
} elsif ($sortkey eq "flag") {
+
                } elsif ($sortkey eq "flag") {
my $fl = ucfirst lc $key;
+
                        my $fl = ucfirst lc $key;
$fl =~ s/_/ /g;
+
                        $fl =~ s/_/ /g;
print $fl;
+
                        print $fl;
} else {
+
                } else {
print $key;
+
                        print $key;
}
+
                }
print "====\n";
+
                print "====\n";
if ($sortkey eq "flag") {
+
                if ($sortkey eq "flag") {
my $desc = $flag_descs{$key};
+
                        my $desc = $flag_descs{$key};
if ($desc) {
+
                        if ($desc) {
$desc =~ s/\n/ /g;
+
                                $desc =~ s/\n/ /g;
print "|----\n| colspan=8|$desc\n";
+
                                print "|----\n| colspan=9|$desc\n";
}
+
                        }
}
+
                }
for my $spell (@spells) {
+
                for my $spell (@spells) {
# Format schools and flags
+
                        # Format schools and flags
my $schools = format_schools @{$spell->{schools}};
+
                        my $schools = format_schools @{$spell->{schools}};
  
my $flags = join ", ", map {
+
                        my $flags = join ", ", map {
s/_/ /g; lc $_  
+
                                s/_/ /g; lc $_
} @{$spell->{flags}}, ($spell->{noisy} ? "noise $spell->{noisy}" : ());
+
                        } @{$spell->{flags}}, ($spell->{noisy} ? "noise $spell->{noisy}" : ());
my $books = join "<br>", map { "[[$_]]" } @{$spell->{books}};
+
                        my $books = join "<br>", map { "[[$_]]" } @{$spell->{books}};
  
  
print <<"EOF";
+
                        print <<"EOF";
 
|----
 
|----
 +
|[[File:{{lc:$spell->{name}.png}}]]
 
|style="padding-left:1em"|[[$spell->{name}]]
 
|style="padding-left:1em"|[[$spell->{name}]]
 
|$schools
 
|$schools
Line 487: Line 523:
 
|$books
 
|$books
 
EOF
 
EOF
}
+
                }
}
+
        }
  
print <<"EOF";
+
        print <<"EOF";
 
|----
 
|----
 
|}
 
|}
 
EOF
 
EOF
 +
 +
}
 +
 +
 +
sub module_book {
 +
        my ($by_book) = @_;
 +
    print <<"EOF";
 +
--[=[
 +
    Table of spellbooks
 +
]=]--
 +
 +
local m = {}
 +
EOF
 +
 +
    my @letters = qw(a b c d e f g);
 +
    my $i = 0;
 +
    for my $key (sort keys %$by_book) {
 +
        print "m[\"$key\"] = {\n";
 +
        my @spells = @{$by_book->{$key}};
 +
        $i = 0;
 +
        for my $spell (@spells) {
 +
            my $schools = format_schools @{$spell->{schools}};
 +
            print "  {\n";
 +
            print "    [\"letter\"] = \"$letters[$i++]\", \n";
 +
            print "    [\"name\"] = \"$spell->{name}\", \n";
 +
    my $lc_name = lc($spell->{name});
 +
            print "    [\"image\"] = \"[[File:${lc_name}.png]]\", \n";
 +
            print "    [\"level\"] = \"$spell->{level}\", \n";
 +
            print "    [\"schools\"] = \"$schools\", \n";
 +
            print "  },\n";
 +
        }
 +
        print "}\n"
 +
    }
 +
    print "return m\n";
 +
}
 +
 +
sub module_spell {
 +
        my ($sortfn, $by_letter) = @_;
 +
        # Table of spells
 +
        print <<"EOF";
 +
--[=[
 +
    Table of spells
 +
]=]--
 +
 +
local m = {}
 +
EOF
 +
 +
        for my $key (sort keys %$by_letter) {
 +
            my @spells = @{$by_letter->{$key}};
 +
            @spells = sort $sortfn @spells if $sortfn;
 +
            for my $spell (@spells) {
 +
                my $schools = ucfirst_array @{$spell->{schools}};
 +
                my $flags = ucfirst_array @{$spell->{flags}};
 +
                $flags =~ s/_/ /g;
 +
                $flags =~ s/Mr check/MR check/; # special case
 +
                $flags =~ s/^{ \"None\" }$/nil/; # special case
 +
                my $books = join ",", map { "\"$_\"" } @{$spell->{books}};
 +
                my $range;
 +
                if ($spell->{minrange} eq "") {
 +
                    $range = "nil";
 +
                } elsif ($spell->{minrange} eq "LOS") {
 +
                    $range = "\"LOS\"";
 +
                } elsif ($spell->{minrange} eq $spell->{maxrange}) {
 +
                    $range = $spell->{minrange};
 +
                } else {
 +
                    $range = "{$spell->{minrange}, $spell->{maxrange}}";
 +
                }
 +
                print "m[\"$spell->{name}\"] = {\n";
 +
                print "    [\"schools\"] = $schools, \n";
 +
                print "    [\"flags\"] = $flags, \n";
 +
                print "    [\"books\"] = { $books }, \n";
 +
                print "    [\"level\"] = $spell->{level}, \n";
 +
                print "    [\"cap\"] = $spell->{cap}, \n";
 +
                print "    [\"range\"] = $range, \n";
 +
                print "    [\"noise\"] = $spell->{noisy}, \n";
 +
                print "}\n";
 +
            }
 +
        }
 +
        print "return m\n";
 +
}
 +
 +
main;
 +
 
</nowiki>
 
</nowiki>
 
</pre>
 
</pre>

Latest revision as of 11:31, 3 November 2016

A user has suggested the deletion of this page. Reason: No longer used


#! /usr/bin/perl -w
# parse-spl-data, by http://crawl.chaosforge.org/index.php?title=User:Neil
# Copyright (C) 2011.  No rights reserved.
#
# You may use, distribute, modify, study, fold, spindle, or mutilate this
# software as you see fit, but know that there is NO WARRANTY, EXPRESS
# OR IMPLIED (to the extent permitted by law).
#
# The latest version of this program may be found at:
#  http://crawl.chaosforge.org/List_of_spells/parse-spl-data

use strict;
use Getopt::Long qw(:config gnu_getopt);

our ($PROGNAME, $DATE, $CRAWL_VERSION);
our $VERSION = "0.1.0";


sub crawl_version {
        my ($indir) = @_;
	# Look for util/release_ver first
	if (open VERSION, "<", "${indir}util/release_ver") {
		my $ver = <VERSION>;
		chomp $ver;
		close VERSION;
		return $ver;
	}
	# TODO: maybe try running git
	return "<unknown>";
}

# Translate a range into something friendlier for display.  -1 (no range)
# becomes the empty string, while "LOS_RADIUS" becomes just LOS.
# TORNADO_RADIUS becomes 5 (its value), but maybe we should use "Tornado"
# instead.
sub xlate_range {
	my $range = shift;
	return "LOS" if $range eq "LOS_RADIUS";
	return 6 if $range eq "TORNADO_RADIUS";
	return "" if $range eq "-1";
	return $range;
}

# Wiki-format a list of spell schools, linking to the corresponding
# magic skill.
sub format_schools {
	join "/", map {
		my $school = ucfirst(lc $_);

		# Convert school names to skill names for linking
		my $skill = $school;
		if ($school =~ /^(Poison|Air|Fire|Ice|Earth)$/) {
			$skill = "$school Magic";
		} elsif ($school !~ /[ys]$/) {
			# "Necromancy" isn't pluralised as a skill,
			# and "Hexes" and "Charms" are already
			# pluralized as a magic school.  The others
			# are singular as a school, plural as a skill.
			$skill = "${school}s";
		}

		$skill eq $school ? "[[$school]]" : "[[$skill|$school]]";
	} @_;
}

sub ucfirst_array {
    my $items = join ",", map {
        my $name = ucfirst(lc $_);
        "\"$name\"";
    } @_;
    "{ $items }";
}

# Add a spell if it exists
sub maybe_add_spell {
	my ($spell, $by_id, $by_key, $sortkey) = @_;
	return undef unless $spell->{name};

	# Ignore spells that do not occur in any book
	return "" unless scalar @{$spell->{books}};
	# ..and NO_SPELL, which "occurs" in books but not really.
	return "" if $spell->{id} eq "NO_SPELL";

	# Sanity checks: abort if we encounter one of these, as it means
	# something funny is up with our book-parsing.

	# Spells that belong to certain monster-only schools.
	for my $school (@{$spell->{schools}}) {
		die "SPTYP_NONE for $spell->{name}" if $school eq "NONE";
	}
	# Spells that don't belong to a school at all, even SPTYP_NONE.
	die "No school for $spell->{name}" unless scalar @{$spell->{schools}};

	# Monster and testing spells.
	for my $flag (@{$spell->{flags}}) {
		die "Monster spell $spell->{name}" if $flag eq "MONSTER";
		die "Testing spell $spell->{name}" if $flag eq "TESTING";
	}

	# It's worth keeping.
	$by_id->{$spell->{id}} = $spell;
        if ($sortkey eq "alpha") {
                $by_key->{$spell->{name}} = $spell;
        } elsif ($sortkey eq "letter") {
                my $let = uc(substr $spell->{name}, 0, 1);
                push @{$by_key->{$let}}, $spell;
        } elsif ($sortkey eq "level") {
                push @{$by_key->{$spell->{level}}}, $spell;
        } elsif ($sortkey eq "school") {
                for my $school (@{$spell->{schools}}) {
                        push @{$by_key->{$school}}, $spell;
                }
        } elsif ($sortkey eq "flag") {
                for my $flag (@{$spell->{flags}}) {
                        push @{$by_key->{$flag}}, $spell;
                }
        }
	return 1;
}

# Sorting functions
sub sort_by_name {
	$a->{name} cmp $b->{name}
}

sub sort_by_level {
	$a->{level} <=> $b->{level} or sort_by_name
}

sub sort_by_school {
	# A Schwartzian transform would be more efficient, but we have
	# few enough spells that it's not necessary.
	join("/", @{$a->{schools}}) cmp join("/", @{$b->{schools}})
		or sort_by_name
}

### MAIN


sub main {
        # Input directory.
        my $indir = "./";

        # Map from letter, school, flag, book name, or level to list of spells.
        my %by_key = ();
        my %by_letter = ();
        my %by_school = ();
        my %by_flag = ();
        my %by_book = ();
        my %by_level = ();

        # Map from ID or name to spell
        my %by_id = ();
        my %by_name = ();

        # Map from spell name to list of book names.
        my %book_spells = ();



        my $sortkey = "alpha";
        my $sortfn = \&sort_by_name;
        my $module = "default";

        GetOptions(
                   "a|alphabetic" => sub { $sortkey = "letter"; $sortfn = \&sort_by_name; },
                   "b|book" => sub { $sortkey = "book"; $sortfn = undef; },
                   "f|flag" => sub { $sortkey = "flag"; $sortfn = \&sort_by_level; },
                   "l|level" => sub { $sortkey = "level"; $sortfn = \&sort_by_school; },
                   "s|school" => sub { $sortkey = "school"; $sortfn = \&sort_by_level; },
                   "m|module-book" => sub { $module = "book"; $sortkey = "book" ; $sortfn = undef; },
                   "k|module-spell" => sub { $module = "spell"; $sortkey = "letter"; $sortfn = \&sort_by_name; },
                   "h|help" => sub {
                           print <<"EOF";
Usage: $0 [options] [directory]

Produce a wiki table of DCSS spells.  The specified directory should contain
spl-data.h an book-data.h.  If omitted, the current directory is used.

Options include:
  -a, --alphabetic    Arrange alphabetically (default).
  -b, --book          Arrange by book, then level.
  -f, --flag          Arrange by flag, then level.
  -k, --module-spell  Generate a Lua table of spells.
  -l, --level         Arrange by level, then schools.
  -m, --module-book   Generate a Lua table of spellbooks.
  -s, --school        Arrange by school, then level.
  -h, --help          Display this help.

Spells of the same schools and level are sorted by name.  With the --book,
--school, and --flag option, spells may appear multiple times.
EOF
                           exit 0;
                   },
                  );

        if (@ARGV) {
                $indir = shift @ARGV;
                $indir .= "/" unless $indir eq "" or $indir =~ m!/$!;
        }

        $DATE = gmtime;
        $CRAWL_VERSION = crawl_version($indir);
        ($PROGNAME = $0) =~ s!.*/!!;

        parse_book_data($indir, \%book_spells, \%by_book);
        parse_spl_data($indir, \%book_spells, \%by_id, \%by_key, $sortkey);
        for my $k (keys %by_book) {
                # Convert the list of spell ids to a list of spells, but
                # remove those that aren't in %by_id because maybe_add_spell
                # skipped them.
                my @spells =  map { $by_id{$_} || () } @{$by_book{$k}};
                if (@spells) {
                        $by_book{$k} = \@spells;
                } else {
                        # Remove the book if it has no spells.
                        delete $by_book{$k};
                }
        }

        # Print data
        if ($module eq "default") {
                if ($sortkey eq "book") {
                        module_default($sortkey, $sortfn, \%by_book);
                } else {
                        module_default($sortkey, $sortfn, \%by_key);
                }
        } elsif ($module eq "book") {
                module_book(\%by_book);
        } elsif ($module eq "spell") {
                module_spell($sortfn, \%by_key);
        }
}

sub parse_book_data {
        my ($indir, $book_spells, $by_book) = @_;
        # Name of current book.
        my $book = "bug" ;
        open BOOKS, "${indir}book-data.h"
          or die "could not open ${indir}book-data.h: $!";
        while (<BOOKS>) {
                # Skip conditional sections.  Really we need to look at
                # the condition, but that requires the C preprocessor and
                # I'd rather avoid that.

                #next if /^#if/../^#endif/;
                next if /^#if TAG_MAJOR_VERSION == 34/../^#endif/;

                # Kind of hackish --- quit at the first rod
                last if m!// Rod!;

                if (m!^{\s*// (.*)!) {
                        # Get the spell name from the comment.
                        $book = $1;
                        # Disambiguation for Conjuration
                        my $extra = "";

                        # Remove parenthesized bits
                        $book =~ s/\s+\([^)]*\)//g;

                        # Remove extra description
                        if ($book =~ s/ - (.*)//) {
                                # But remember it for Conjuration
                                if ($1 =~ /Fire and Earth/) {
                                        $extra = " (fire+earth)";
                                } elsif ($1 =~ /Air and Ice/) {
                                        $extra = " (ice+air)";
                                }
                        }

                        # Remove roman numeral counter
                        $book =~ s/\s+[IVX]+$//;

                        # Replace "Tome of" with "Book of" (special case for Dragon)
                        $book =~ s/^Tome of/Book of/;

                        # And special-case Minor Magic.
                        $book =~ s/^Minor/Book of Minor/;

                        # Append disambiguation
                        $book .= $extra;
                } else {
                        while (/SPELL_(\w+)/g) {
                                push @{$book_spells->{$1}}, $book;
                                push @{$by_book->{$book}}, $1;
                        }
                }
        }
        close BOOKS;
}

sub parse_spl_data {
        # Current spell.  Members are
        #   {name}    : Spell name
        #   {id}      : SPELL_* identifier (without the SPELL_)
        #   {schools} : List of SPTYP_* constants (without the SPTYP_)
        #   {flags}   : List of SPFLAG_* constants (without the SPFLAG_)
        #   {books}   : List of book names.
        #   {level}   : Level
        #   {cap}     : Power cap
        #   {minrange}: Minimum range, or "LOS" or ""
        #   {maxrange}: Maximum range, or "LOS" or ""
        #   {noisy}   : Noise modifier
        #
        #   {data}    : List of extra data; converted to {level}-{noise} at the end
        #               of the spell block.
        my ($indir, $book_spells, $by_id, $by_key, $sortkey) = @_;
        my $spell = {};
        open SPELLS, "${indir}spl-data.h"
          or die "could not open ${indir}spl-data.h: $!";
        while (<SPELLS>) {
                chomp;
                if (/^{/) {
                        $spell = {};
                } elsif (/^}/) {
                        # Unpack data
                        my (
                            $sch, $flag, $lev, $cap, $minr, $maxr, $nm, @rest
                           ) = @{$spell->{data}};

                        # Parse out schools and flags
                        $spell->{schools} = [
                                             map { s/SPTYP_//; $_ } split /\s*\|\s*/, $sch
                                            ];
                        $spell->{flags} = [
                                           map { s/SPFLAG_//; $_ } split /\s*\|\s*/, $flag
                                          ];

                        # Include the rest of the data
                        $spell->{level} = $lev;
                        $spell->{cap} = $cap;
                        $spell->{minrange} = xlate_range $minr;
                        $spell->{maxrange} = xlate_range $maxr;
                        $spell->{noisy} = $nm;

                        maybe_add_spell($spell, $by_id, $by_key, $sortkey);
                } elsif (/^\s*SPELL_(\w+),\s+"([^"]*)",/) {
                        $spell->{id} = $1;
                        $spell->{name} = $2;
                        if (exists $book_spells->{$1}) {
                                $spell->{books} = [ @{$book_spells->{$1}} ];
                        } else {
                                $spell->{books} = [];
                        }
                } else {
                        # Strip comments first.
                        s!\s*//.*!!;
                        # Get comma-delimited sections
                        while (/\s*([^,]+)(,|$)/g) {
                                if (substr($1, 0, 1) eq "|") {
				# Continuation line; really we should check
				# whether the previous line ended with a
				# comma, but this is probably good enough.
                                        $spell->{data}[-1] .= $1
                                } else {
                                        push @{$spell->{data}}, $1;
                                }
                        }

                }
        }
        close SPELLS;
}


sub module_default {
        my ($sortkey, $sortfn, $by_key) = @_;
        # Descriptions of spell flags
        my %flag_descs = (
                          ALLOW_SELF => <<"EOF",
The spell is not helpful, but you will not receive a "Really target yourself?"
prompt.  You may still receive "That beam is likely to hit you." for
"[[#Dir or target|dir or target]]" spells such as Mephitic Cloud.
EOF
                          AREA => <<"EOF",
The spell harms an area.  Pacified fleeing monsters will not use emergency
spells with this flag.
EOF
                          BATTLE => <<"EOF",
The spell is a non-[[Conjuration]] spell disliked by [[Elyvilon]].  There is no
piety penalty for using such spells, but a randart spellbook containing one of
these spells will never have Elyvilon's name on it.
EOF
                          CHAOTIC => <<"EOF",
The spell is hated by [[Zin]], who will impose penance on any player casting
it, and reward killing monsters who can cast it.
EOF
                          CORPSE_VIOLATING => <<"EOF",
The spell is hated by [[Fedhas Madash]], who will impose penance on any player
casting it.
EOF
                          DIR => <<"EOF",
The spell requires choosing a direction (and not a target).
EOF
                          DIR_OR_TARGET => <<"EOF",
The spell requires choosing a direction or target, and is stopped by
interposing creatures.
EOF
                          ESCAPE => <<"EOF",
The spell helps you get out of trouble.  Xom considers such spells boring, and
will not gift spellbooks containing them.  Furthermore, the spell is an option
when control-clicking yourself in tiles mode.
EOF
                          GRID => <<"EOF",
The spell targets a grid square, disregarding any creatures in the way.  This
is a form of smite targeting that does not require a target.
EOF
                          HASTY => <<"EOF",
The spell is hated by [[Cheibriados]], who will impose penance on any player
casting it.
EOF
                          HELPFUL => <<"EOF",
The spell helps you or the target; if targeted, the targeting commands
cycle through friendlies rather than hostiles.  Xom considers such spells
boring, and will not gift spellbooks containing them.
EOF
                          NEUTRAL => <<"EOF",
The spell is neither harmful nor helpful; if targeted, the targeting
commands cycle through all creatures, not just hostiles.  Xom considers
such spells boring, and will not gift spellbooks containing them.
EOF
                          NONE => <<"EOF",
The spell has no special flags.  Such spells are always untargeted.
EOF
                          NOT_SELF => <<"EOF",
The spell may not target you or your square.
EOF
                          RECOVERY => <<"EOF",
The spell helps you recover from ill effects.  Xom considers such spells
boring, and will not gift spellbooks containing them.  Furthermore, the spell
is an option when control-clicking yourself in tiles mode.
EOF
                          TARGET => <<"EOF",
The spell targets a creature, disregarding any other creatures in the way.
This is a form of smite targeting that requires a target creature.
EOF
                          TARG_OBJ => <<"EOF",
The spell targets an object, disregarding any other creatures in the way.
This is a form of smite targeting that requires a target object.
EOF
                          UNCLEAN => <<"EOF",
The spell is hated by [[Zin]], who will impose penance on any player casting
it, and reward killing monsters who can cast it.
EOF
                          UNHOLY => <<"EOF",
The spell is a non-necromantic unholy spell.  It is hated by [[good]] gods
([[Elyvilon]], [[The Shining One]], and [[Zin]]), who will impose penance on
any player casting it.
EOF
                         );
        print <<"EOF";
==Spells== <!-- We *must* have a heading before the table, or the TOC will end up inside the table! -->

<!-- Automatically generated by $PROGNAME $VERSION
     from Dungeon Crawl Stone Soup version $CRAWL_VERSION
  -->
{| class="prettytable"
!rowspan=2|Image
!rowspan=2|Name
!rowspan=2|Schools
!rowspan=2|Level
!rowspan=2|Power<br>cap
! colspan=2 |Range
!rowspan=2|Flags
!rowspan=2|Books
|----
!min
!max
EOF

        # TODO: allow sorting by other criteria
        for my $key (sort keys %{$by_key}) {
                my @spells = @{$by_key->{$key}};
                @spells = sort $sortfn @spells if $sortfn;

                print "|----\n! colspan=9 style=\"text-align:left\"|\n====";

                # Format and link the key appropriately
                if ($sortkey eq "book") {
                        print "[[$key]]";
                } elsif ($sortkey eq "school") {
                        print format_schools $key;
                } elsif ($sortkey eq "level") {
                        print "level $key";
                } elsif ($sortkey eq "flag") {
                        my $fl = ucfirst lc $key;
                        $fl =~ s/_/ /g;
                        print $fl;
                } else {
                        print $key;
                }
                print "====\n";
                if ($sortkey eq "flag") {
                        my $desc = $flag_descs{$key};
                        if ($desc) {
                                $desc =~ s/\n/ /g;
                                print "|----\n| colspan=9|$desc\n";
                        }
                }
                for my $spell (@spells) {
                        # Format schools and flags
                        my $schools = format_schools @{$spell->{schools}};

                        my $flags = join ", ", map {
                                s/_/ /g; lc $_
                        } @{$spell->{flags}}, ($spell->{noisy} ? "noise $spell->{noisy}" : ());
                        my $books = join "<br>", map { "[[$_]]" } @{$spell->{books}};


                        print <<"EOF";
|----
|[[File:{{lc:$spell->{name}.png}}]]
|style="padding-left:1em"|[[$spell->{name}]]
|$schools
|$spell->{level}
|$spell->{cap}
|$spell->{minrange}
|$spell->{maxrange}
|$flags
|$books
EOF
                }
        }

        print <<"EOF";
|----
|}
EOF

}


sub module_book {
        my ($by_book) = @_;
    print <<"EOF";
--[=[
     Table of spellbooks
 ]=]--

local m = {}
EOF

    my @letters = qw(a b c d e f g);
    my $i = 0;
    for my $key (sort keys %$by_book) {
        print "m[\"$key\"] = {\n";
        my @spells = @{$by_book->{$key}};
        $i = 0;
        for my $spell (@spells) {
            my $schools = format_schools @{$spell->{schools}};
            print "  {\n";
            print "    [\"letter\"] = \"$letters[$i++]\", \n";
            print "    [\"name\"] = \"$spell->{name}\", \n";
	    my $lc_name = lc($spell->{name});
            print "    [\"image\"] = \"[[File:${lc_name}.png]]\", \n";
            print "    [\"level\"] = \"$spell->{level}\", \n";
            print "    [\"schools\"] = \"$schools\", \n";
            print "  },\n";
        }
        print "}\n"
    }
    print "return m\n";
}

sub module_spell {
        my ($sortfn, $by_letter) = @_;
        # Table of spells
        print <<"EOF";
--[=[
     Table of spells
 ]=]--

local m = {}
EOF

        for my $key (sort keys %$by_letter) {
            my @spells = @{$by_letter->{$key}};
            @spells = sort $sortfn @spells if $sortfn;
            for my $spell (@spells) {
                my $schools = ucfirst_array @{$spell->{schools}};
                my $flags = ucfirst_array @{$spell->{flags}};
                $flags =~ s/_/ /g;
                $flags =~ s/Mr check/MR check/; # special case
                $flags =~ s/^{ \"None\" }$/nil/; # special case
                my $books = join ",", map { "\"$_\"" } @{$spell->{books}};
                my $range;
                if ($spell->{minrange} eq "") {
                    $range = "nil";
                } elsif ($spell->{minrange} eq "LOS") {
                    $range = "\"LOS\"";
                } elsif ($spell->{minrange} eq $spell->{maxrange}) {
                    $range = $spell->{minrange};
                } else {
                    $range = "{$spell->{minrange}, $spell->{maxrange}}";
                }
                print "m[\"$spell->{name}\"] = {\n";
                print "    [\"schools\"] = $schools, \n";
                print "    [\"flags\"] = $flags, \n";
                print "    [\"books\"] = { $books }, \n";
                print "    [\"level\"] = $spell->{level}, \n";
                print "    [\"cap\"] = $spell->{cap}, \n";
                print "    [\"range\"] = $range, \n";
                print "    [\"noise\"] = $spell->{noisy}, \n";
                print "}\n";
            }
        }
        print "return m\n";
}

main;