List of spells/parse-spl-data: Difference between revisions
From Ashenzari's Archive
More actions
imported>CommanderC Import Neil's script |
imported>Medar Propose deletion. |
||
| (9 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/ | # 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"; | ||
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 | 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 = | 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; | ||
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; | return 1; | ||
} | } | ||
| Line 229: | Line 140: | ||
### MAIN | ### MAIN | ||
my $sortkey = "alpha"; | |||
my $sortfn = \&sort_by_name; | sub main { | ||
GetOptions( | # 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] | Usage: $0 [options] [directory] | ||
| Line 245: | Line 182: | ||
Options include: | Options include: | ||
-a, --alphabetic | -a, --alphabetic Arrange alphabetically (default). | ||
-b, --book | -b, --book Arrange by book, then level. | ||
-f, --flag | -f, --flag Arrange by flag, then level. | ||
-l, --level | -k, --module-spell Generate a Lua table of spells. | ||
-s, --school | -l, --level Arrange by level, then schools. | ||
-h, --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; | |||
}, | |||
); | ); | ||
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 ( | # 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; | |||
} | } | ||
# 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) = @_; | ||
my $spell = {}; | |||
while (<SPELLS>) { | 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 | # 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 | |||
} else { | |||
push @{$spell->{data}}, $1; | |||
} | |||
} | |||
} | |||
} | |||
close SPELLS; | |||
} | } | ||
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 | ||
--> | --> | ||
{| 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 | ||
# TODO: allow sorting by other criteria | |||
for my $key (sort keys %{$by_key}) { | |||
my @spells = @{$by_key->{$key}}; | |||
@spells = sort $sortfn @spells if $sortfn; | |||
# TODO: allow sorting by other criteria | |||
for my $key (sort keys %by_key) { | |||
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}]] | |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 10: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;