#!/usr/bin/perl require 5.004; use FileHandle; use strict 'vars'; # Directory containing the *_info.txt files. Modify this if you don't # have the $ANGBAND_PATH env variable set. my $ANGBAND_PATH = $ENV{ANGBAND_PATH} || '/var/lib/games/angband'; sub usage () { print <print_self(); next BLOB; } } close($fh); } sub do_monster ($@) { my ($auto, @keys) = @_; my $filename = "${ANGBAND_PATH}/edit/r_info.txt"; my $fh = new FileHandle $filename; die "cannot open $filename: $!\n" if !defined($fh); Monster::init; BLOB: while (my $blob = Util::read_blob $fh) { my ($name) = ($blob =~ /^N:\d+:(.*)$/m); KEY: for my $key (@keys) { next KEY if $name !~ /$key/i; if (!$auto) { my $ynq = Util::ynq $name; last BLOB if !defined($ynq); next BLOB if !$ynq; } my $mon = new Monster($blob); $mon->print_self(); next BLOB; } } } sub do_stats (@) { Stats::init; my $race = RaceClass::get_race @_; my $class = RaceClass::get_class @_; my @stats = map { $Stats::race_mods[$race][$_] + $Stats::class_mods[$class][$_] } 0..5; my @names = qw(STR INT WIS DEX CON CHR); @stats = Util::add_plus @stats; print "Total stat mods: " . join(', ', map { "$names[$_] $stats[$_]" } 0..5) . "\n"; } sub do_wpn (@) { my ($class, $method, $str, $dex, $wgt) = (-1) x 5; my ($i, $n) = (0, 0); Weapon::init; $class = RaceClass::get_class @_; foreach (@_) { if (m|^S(\d+(/\d+)?)$|) { $str = Stats::index $1; $n++ if $str >= 0; } if (m|^D(\d+(/\d+)?)$|) { $dex = Stats::index $1; $n++ if $dex >= 0; } if (m|^W(\d+(\.\d*)?)$|) { $wgt = $1 * 10; $n++; } } print "\n"; if ($n == 2) { $method = 1 if $str >= 0 && $dex >= 0; $method = 2 if $str >= 0 && $wgt >= 0; $method = 3 if $dex >= 0 && $wgt >= 0; } elsif ($n != 0) { print < 11; $P0 = 11 if $P0 > 11; $n = Weapon::blows $str, $dex, $S*$M+1, $class; unshift @lines, sprintf 'Otherwise, %d blow%s', $n, Util::s $n; for $P (1..$P0) { next if ($Weapon::blows_tbl[$P][$D] == $Weapon::blows_tbl[$P-1][$D]); $wgt = int($S * $M / $P); $wgt = $W if $wgt < $W; $n = Weapon::blows $str, $dex, $wgt, $class; unshift @lines, sprintf 'For %d blow%s: <= %s lb', $n, Util::s $n, Util::fix_weight $wgt; last if $n == $Weapon::max_blows[$class] || $wgt <= $W; } $lines[0] =~ s/Otherwise, // if scalar @lines == 1; map { print "$_\n" } @lines; } if ($method == 2) { for $dex (0..37) { $n = Weapon::blows $str, $dex, $wgt, $class; if ($n > $i) { printf 'For %d blow%s: DEX %s'."\n", $n, Util::s $n, Stats::rev_index $dex; $i = $n; last if $n == $Weapon::max_blows[$class]; } } } if ($method == 3) { for $str (0..37) { $n = Weapon::blows $str, $dex, $wgt, $class; if ($n > $i) { printf 'For %d blow%s: STR %s'."\n", $n, Util::s $n, Stats::rev_index $str; $i = $n; last if $n == $Weapon::max_blows[$class]; } } } } sub do_wgt (@) { my $str = -1; my ($wgt, $step); $str = Stats::index $_[0] if scalar @_; while ($str < 0) { $str = Stats::index Util::prompt_val "Your STR:"; } $wgt = $str + 5; $wgt += $str - 15 if $str > 15; $wgt = 30 if $wgt > 30; $step = $wgt; $wgt = $wgt*6 - 0.1; print "Max unencumbered weight $wgt lb;" . " -1 speed per additional $step lb.\n"; } package Attr; use vars qw(%color); sub init () { %color = Util::split_hash < \&art_name, 'e' => \&ego_name, 'k' => \&obj_name); } sub art_name { my $blob = shift; $blob =~ /^N:\d+:(.*)\nI:(\d+:\d+)/ or return ''; return "The $tsvals{$2} $1"; } sub ego_name { my $blob = shift; $blob =~ /^N:\d+:(.*)\nX:(\d+)/ or return ''; return "$slots{$2} $1"; } sub obj_name { my $blob = shift; my $name; $blob =~ /^N:\d+:(.*)\nG:.:.\nI:(\d+):(\d+)/ or return ''; $name = $1; $name =~ s/\&/A/; $name =~ s/\~//; return $name if !$tvals{$2} || ($2 == 80 && $3 >= 32); return "$tvals{$2} $name"; } sub new ($$$$) { my ($class, $name, $blob, $which) = @_; my $self = { NAME => $name, IDX => 0, ATTRCHAR => '', PVAL => '', LEVEL => 0, RARITY => 0, WEIGHT => 0, COST => 0, APPEAR => '', BONUS => [], EGO_BONUS => [], PVAL_BOOST => [], SLAYS => [], BRANDS => [], SUSTAINS => [], IMMUNES => [], RESISTS => [], IGNORES => [], GENERAL => [], ADMIN => [] }; study $blob; my ($idx) = ($blob =~ /^N:(\d+)/); ($$self{PVAL}) = ($blob =~ /^I:\d+:\d+:(-?\d+)/m); $$self{ATTRCHAR} = "$Attr::color{$2} '$1'" if ($blob =~ /^G:(.):(.)/m); @$self{qw(LEVEL RARITY WEIGHT COST)} = ($blob =~ /^W:(\d+):(\d+):(\d+):(\d+)/m); $$self{APPEAR} = $1 if ($blob =~ /^A:(.*)$/m); $$self{BONUS} = [ $blob =~ /^P:(\d+):(\d+d\d+):(-?\d+):(-?\d+):(-?\d+)/m ]; $$self{EGO_BONUS} = [ $blob =~ /^C:(-?\d+):(-?\d+):(-?\d+):(-?\d+)/m ]; for my $i (split /\s*\|\s*/, join '', ($blob =~ /^F:(.*)$/mg)) { push @{$$self{PVAL_BOOST}}, $pval_boost{$i} if $pval_boost{$i}; push @{$$self{SLAYS}}, $slays{$i} if $slays{$i}; push @{$$self{BRANDS}}, $brands{$i} if $brands{$i}; push @{$$self{SUSTAINS}}, $sustains{$i} if $sustains{$i}; push @{$$self{IMMUNES}}, $immune{$i} if $immune{$i}; push @{$$self{RESISTS}}, $resists{$i} if $resists{$i}; push @{$$self{IGNORES}}, $ignores{$i} if $ignores{$i}; push @{$$self{GENERAL}}, $general{$i} if $general{$i}; push @{$$self{ADMIN}}, $admin{$i} if $admin{$i}; } $$self{PVAL} = Util::add_plus $$self{PVAL}; $$self{WEIGHT} = Util::fix_weight $$self{WEIGHT}; $$self{SPECIAL} = $ego_defns[$ego_random{$idx}] if $which eq 'e' && $ego_random{$idx}; $$self{SPECIAL} = "Activates for $activate{$idx}" if $which eq 'a' && $activate{$idx}; for my $i (@{$$self{APPEAR}}) { $i =~ s/(\d+):(\d:+)/level $1 (1 in $2)/; } if (@{$$self{BONUS}}) { @{$$self{BONUS}}[2..4] = Util::add_plus @{$$self{BONUS}}[2..4]; $$self{BONUS} = [ @{$$self{BONUS}}[0,4,1,2,3] ]; } if (@{$$self{EGO_BONUS}}) { $$self{PVAL} = Util::add_plus2 $$self{EGO_BONUS}[3]; $$self{EGO_BONUS} = [ Util::add_plus2 @{$$self{EGO_BONUS}}[0..2] ]; } return bless $self, $class; } sub print_self ($) { my $self = shift; print "\n$$self{NAME}"; printf ' [%s,%s] (%s) (%s,%s)', @{$$self{BONUS}} if @{$$self{BONUS}}; printf ' (%s,%s) [%s]', @{$$self{EGO_BONUS}} if @{$$self{EGO_BONUS}}; printf "\n".'Level %s, rarity %s, weight %s lb, cost %s'."\n", @$self{qw(LEVEL RARITY WEIGHT COST)}; print Util::comma_list 'Appearing on', $$self{APPEARING}; print Util::comma_list "$$self{PVAL}", $$self{PVAL_BOOST}; print Util::comma_list 'Slay', $$self{SLAYS}; print Util::comma_list '', $$self{BRANDS}; print Util::comma_list 'Sustain', $$self{SUSTAINS}; print Util::comma_list 'Immune', $$self{IMMUNES}; print Util::comma_list 'Resist', $$self{RESISTS}; print Util::comma_list 'Unharmed by', $$self{IGNORES}; print Util::comma_list '', $$self{GENERAL}; print "$$self{SPECIAL}\n" if $$self{SPECIAL}; print Util::comma_list '###', $$self{ADMIN}; print "\n"; } package Monster; use vars qw(%attack_forms %damage_forms %is %doesnt %treasure %can %hurt_by %resists %shoots %breathes %spells %rand_move @drop_type @drop_worth); sub init () { Attr::init; %attack_forms = Util::split_hash < '', 25 => ' (slightly random)', 50 => ' (random)', 75 => ' (very random)'); @drop_type = ('drops', 'treasures', 'items'); @drop_worth = ('', 'good ', 'great ', 'useful ', 'chosen '); } sub treasure_desc ($) { my $listref = $_[0]; my ($min, $max, $treas, $item) = (0, 0, 0, 0); my $n; return '' if scalar @$listref == 0; foreach $n (@$listref) { ($item = $n/1000) && next if $n >= 1000; ($treas = $n/100) && next if $n >= 100; ($min += $n/2) && ($max += $n) && next if $n > 1; $max++ && next if $n == 1; } return "$min-$max $drop_worth[$item]$drop_type[$treas]\n"; } sub new ($$) { my ($class, $blob) = @_; my $self = { NAME => '', ATTRCHAR => '', HP => '', SPEED => 0, AC => 0, SLEEPY => 0, NOTICE => 0, LEVEL => 0, RARITY => 0, EXP => 0, MOVE => 0, SPELLRATE => 0, ATTACKS => [], FLAGS => { FORCE_MAXHP => 0, POWERFUL => 0 }, IS => [], DOESNT => [], TREASURE => [], CAN => [], HURT => [], RESISTS => [], SHOOTS => [], BREATHES => [], SPELLS => [] }; study $blob; ($$self{NAME}) = ($blob =~ /^N:\d+:(.*)$/m); $$self{ATTRCHAR} = "$Attr::color{$2} '$1'" if ($blob =~ /^G:(.):(.)/m); @$self{qw(SPEED HP NOTICE AC SLEEPY)} = ($blob =~ /^I:(\d+):(\d+d\d+):(\d+):(\d+):(\d+)/m); @$self{qw(LEVEL RARITY EXP)} = ($blob =~ /^W:(\d+):(\d+):\d+:(\d+)/m); $$self{ATTACKS} = [ $blob =~ /^B:(.*)$/mg ]; my @flags = grep { $_ } map { split /\s*\|\s*/ } ($blob =~ /^F:(.*)$/mg); for my $i (@flags) { $$self{FLAGS}{$i} = 1 if defined $$self{FLAGS}{$i}; $$self{MOVE} += $1 if $i =~ /RAND_(\d+)/; push @{$$self{IS}}, $is{$i} if $is{$i}; push @{$$self{DOESNT}}, $doesnt{$i} if $doesnt{$i}; push @{$$self{TREASURE}}, $treasure{$i} if $treasure{$i}; push @{$$self{CAN}}, $can{$i} if $can{$i}; push @{$$self{HURT}}, $hurt_by{$i} if $hurt_by{$i}; push @{$$self{RESISTS}}, $resists{$i} if $resists{$i}; } my @spflags = grep { $_ } map { split /\s*\|\s*/ } ($blob =~ /^S:(.*)$/mg); for my $i (@spflags) { $$self{SPELLRATE} = $1 if $i =~ /1_IN_(\d+)/; push @{$$self{SHOOTS}}, $shoots{$i} if $shoots{$i}; push @{$$self{BREATHES}}, $breathes{$i} if $breathes{$i}; push @{$$self{SPELLS}}, $spells{$i} if $spells{$i}; } $$self{SPEED} = Util::add_plus ($$self{SPEED} - 110); $$self{MOVE} = $rand_move{$$self{MOVE}}; for my $i (@{$$self{ATTACKS}}) { $i = "$attack_forms{$1} for $damage_forms{$2} ($3)" if $i =~ /^(\w+):(\w+):(\d+d\d+)$/; $i = "$attack_forms{$1} for $damage_forms{$2}" if $i =~ /^(\w+):(\w+)$/; $i = "$attack_forms{$1}" if $i =~ /^(\w+)$/; } return bless $self, $class; } sub print_self ($) { my $self = shift; my $scratch = ''; if ($$self{FLAGS}{FORCE_MAXHP} || $$self{HP} =~ /d1$/) { $$self{HP} = $1 * $2 if $$self{HP} =~ /(\d+)d(\d+)/; } print "\n$$self{NAME} ($$self{ATTRCHAR})\n"; printf '%s HP, speed %s%s, AC %s, "sleepiness" %s, notice range %s'."\n", @$self{qw(HP SPEED MOVE AC SLEEPY NOTICE)}; printf 'Level %s, rarity %s, exp %s'."\n", @$self{qw(LEVEL RARITY EXP)}; print Util::comma_list 'Monster is', $$self{IS}; print "Doesn't ", join(' or ', @{$$self{DOESNT}}), "\n" if @{$$self{DOESNT}}; print Util::comma_list 'Attacks: ', $$self{ATTACKS}; print Util::comma_list 'Can', $$self{CAN}; print Util::comma_list 'Hurt by', $$self{HURT}; print Util::comma_list 'Resists', $$self{RESISTS}; print Util::comma_list 'Shoots', $$self{SHOOTS}; $scratch = ($$self{FLAGS}{POWERFUL} ? '*Breathes*' : 'Breathes'); $scratch = "$scratch (1/$$self{SPELLRATE})"; print Util::comma_list $scratch, $$self{BREATHES}; print Util::comma_list "Spells (1/$$self{SPELLRATE}): ", $$self{SPELLS}; print treasure_desc $$self{TREASURE}; print "\n"; } package RaceClass; use vars qw($racelist $classlist %race_names %class_names); sub init () { $racelist = < 18+220; # This formula is correct for 2.7.9v6 return $val - 3 if $val <= 18; return (15 + int(($val-18) / 10)) if $val <= 18+220; return 37; } sub rev_index ($) { my $idx = shift; # This formula is correct for 2.7.9v6 return $idx + 3 if $idx <= 15; return sprintf('18/%d0', ($idx-15)) if $idx <= 37; return 'N/A'; } package Weapon; use vars qw(@max_blows @mult @min_weight @str_tbl @dex_tbl @blows_tbl $which_method); sub init () { # War Mag Pri Rog Ran Pal @max_blows = ( 6, 4, 5, 5, 5, 5 ); @mult = ( 5, 2, 3, 3, 4, 4 ); @min_weight = ( 30, 40, 35, 30, 35, 30 ); @str_tbl = ( 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, # 3-17 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, # 18-18/99 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, # 18/100-18/199 220, 230, 240 # 18/200-18/220 ); @dex_tbl = ( 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, # 3-17 1, 2, 2, 2, 2, 3, 3, 4, 4, 5, # 18-18/99 6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 20, 20, 20 # 18/100-18/220 ); @blows_tbl = ( # Dex 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11+ / Str [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3], # 0 [1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 4], # 1 [1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5], # 2 [1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 5], # 3 [1, 2, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5], # 4 [2, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5, 6], # 5 [2, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5, 6], # 6 [2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 6], # 7 [3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6], # 8 [3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6], # 9 [3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6], # 10 [3, 3, 4, 4, 4, 4, 5, 5, 6, 6, 6, 6], # 11+ ); $which_method = < Wpn weight 2) STR, Wpn weight ==> DEX 3) DEX, Wpn weight ==> STR EOF RaceClass::init; } sub blows ($$$$) { my ($str, $dex, $wgt, $class) = @_; my $n; $wgt = $min_weight[$class] if $wgt < $min_weight[$class]; $str = int($str_tbl[$str] * $mult[$class] / $wgt); $dex = $dex_tbl[$dex]; $str = 11 if $str > 11; $dex = 11 if $dex > 11; $n = $blows_tbl[$str][$dex]; $n = $max_blows[$class] if $n > $max_blows[$class]; return $n; } package Util; sub read_blob ($) { my $fh = $_[0]; my $lines = ''; local($_); while (<$fh>) { return $lines if /^$/ && $lines; $lines .= $_ if !(/^$/ || /^[#VD]/); } return $lines; } sub add_plus (@) { return ($_[0] < 0 ? $_[0] : "+$_[0]") if !wantarray; return map { $_ < 0 ? $_ : "+$_" } @_; } sub add_plus2 (@) { return ($_[0] < 0 ? $_[0] : $_[0] == 0 ? "+0" : "+1-$_[0]") if !wantarray; return map { $_ < 0 ? $_ : $_ == 0 ? "+0" : "+1-$_" } @_; } sub s ($) { return ($_[0] == 1 ? ' ' : 's') } sub fix_weight ($) { my $wgt = $_[0]; return "0.$wgt" if $wgt >0 && $wgt < 10; return $wgt/10 if $wgt%10 == 0; return "${\( int($wgt/10) )}.${\( $wgt%10 )}"; } sub ynq ($) { my $prompt = $_[0]; my $ans; print "$prompt? [y/N/q] "; $ans = ; return ($ans =~ /^[Qq]/ ? undef : $ans =~ /^[Yy]/ ? 1 : 0); } sub prompt_val ($;$) { my ($prompt, $pattern) = @_; my $line; if (defined $pattern) { do { print "$prompt "; $line = ; } until $line =~ /^$pattern$/; } else { print "$prompt "; $line = ; } return $line; } sub wrap_line ($) { my $line = $_[0]; my ($full_lines, $part_line) = ('', ''); for my $w (split(/\s+/, $line)) { if (length($part_line) + length($w) > 72) { $full_lines = "$full_lines$part_line\n"; $part_line = " $w"; } else { $part_line = ($part_line ? "$part_line $w" : $w); } } return "$full_lines$part_line\n"; } sub comma_list ($$) { my ($first, $listref) = @_; return if ref $listref ne 'ARRAY' || scalar @$listref == 0; my $line = join ', ', @$listref; $line = "$first $line" if $first; return Util::wrap_line ucfirst $line; } sub split_hash ($) { return ($_[0] =~ /^\s*([^,]*), (.*)$/mg); }