List of spells/parse-spl-data

From CrawlWiki
Revision as of 14:38, 30 January 2013 by CommanderC (talk | contribs) (Import Neil's script)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

#! /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/index.php?title=User:Neil/parse-spl-data

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

our ($PROGNAME, $DATE, $CRAWL_VERSION);
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 {
	# 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 5 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]]";
	} @_;
}


# Add a spell if it exists
sub maybe_add_spell($) {
	my $spell = shift;
	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;
	$by_name{$spell->{name}} = $spell;
	push @{$by_level{$spell->{level}}}, $spell;

	my $let = uc(substr $spell->{name}, 0, 1);
	push @{$by_letter{$let}}, $spell;
	

	for my $school (@{$spell->{schools}}) {
		push @{$by_school{$school}}, $spell;
	}
	for my $flag (@{$spell->{flags}}) {
		push @{$by_flag{$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

my $sortkey = "alpha";
my $sortfn = \&sort_by_name;
GetOptions(
	"a|alphabetic" => sub { $sortkey = "alpha"; $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 },
	"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.
  -l, --level       Arrange by level, then schools.
  -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;
($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.
	next if /^#if/../^#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
#   {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 $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);
	} 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;

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";
==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
     on $DATE
  -->
{| class="prettytable"
!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

my %by_key;
if ($sortkey eq "alpha") {
	%by_key = %by_letter
} elsif ($sortkey eq "book") {
	%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====";

	# 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=8|$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";
|----
|style="padding-left:1em"|[[$spell->{name}]]
|$schools
|$spell->{level}
|$spell->{cap}
|$spell->{minrange}
|$spell->{maxrange}
|$flags
|$books
EOF
	}
}

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