#!/usr/bin/perl -w # Changes: # # 2006-11-18: # - Oops, missed the SENS_FIRE flag... # - Add Galleon and Machine to the list of special level map files to # scan for info. # # 2006-05-29: # Mention special drops of dungeon guardian monsters where known. # # 2005-09-23: # Fix name construction for artifact trap kits. # # 2005-05-17: # Allow for negative base armor class when parsing objects (cf. The Robe # of Great Luck). # # 2005-05-15: # Display starting inventory in player entries. This doesn't include # equipment added in C or via Lua hooks. # # 2005-03-18: # - Show underlying XdY "hit dice" definition along with fixed hp for # FORCE_MAXHP monsters, since the information is still relevant for # monster leveling and the Morgoth Curse spell. # - New line "Concealed by" in monster description to cover the # CHAR_CLEAR and CHAR_MULTI flags; describing them as "floor-hidden" # and "item-hidden" was just awkward. # # 2005-01-17: # Oops, missed the ABOMINATION damage type... # # 2004-10-09: # - R_ELEM means a random element resistance, not all elemental # resistances... :-} # - Handle the 'r:[NF]:' lines in e_info.txt that specify # required/forbidden flags on the base object. # # 2004-06-19: # - Display mana adjustment for classes. # - Display body-part deltas for classes, just in case any future classes # ever use them. # # 2004-04-18: # Rearrange Item::init() so that Item::init_sets() gets called on later # invocations if necessary. Again, we needed this for the Tk interface. # # 2004-01-30: # Add category ratios for monster drops, from the O: lines in r_info.txt. # # 2003-11-17: # Handle the XTRA_MIGHT_{BOW,SLING,XBOW} player flags, which are # implemented as of v2.2.4. # # 2003-10-12: # New pattern char:C which matches objects and monsters displayed with # that character. # # 2003-10-02: # New search type -P to list info for player races, subraces and classes. # # 2003-09-27: # - Add symbiote to the body-parts listing. # - Tweak the docs for 't' and 's'. # - Fix capitalization in monster drain-stat attack descriptions. # # 2003-09-09: # - Handle new object flags WIELD_CAST and IMMOVABLE. # - Catch tval/sval 14:60 for ego horns. # # 2003-06-07: # Rearranged Dungeon::init() so that initializations that weren't done # when called from Monster::init() would get done when later called again # by Object::init(). Didn't matter for command-line mode, but was # missing info from the Tk interface. # # 2003-06-01: # - New 's' modifier to -A prints set info for artifacts that are members # of artifact sets. # - Add artifact pattern set:string which match artifacts in an artifact # set whose name contains the string. # - Fix all the dates in the change log, which for no obvious reason I # had entered as last year... :-} # # 2003-05-24: # Added optional Tk interface; select with option -T. # # 2003-04-17: # Fix object name display for trap sets. # # 2003-04-13: # - Corrected flag description for weapons of Slaying. # - New 't' modifier to -O prints {tval:sval} for objects. # # 2003-04-07: # Fix object name display for Runestones. # # 2003-03-29: # Individual flags in a flags:... pattern can now be preceded with ! to # specify that they shouldn't be present. # # 2003-03-25: # Search d_info.txt and *.map for special locations of monsters, objects # and artifacts. Doesn't catch things done in code. # # 2003-02-22: # - New 'd' modifier to -[AEOM] prints text description of target. # - Add object/ego/artifact/monster pattern flags:FLAG,FLAG which matches # targets with all listed flags. # # 2003-02-19: # - Add object patterns tval:NN, tsval:NN:MM and tsval:NN:MM-RR. # - Show attr/char for objects. # - Properly reflect game-imposed maximum light radius of 5. # - Fix displayed object name for essences. # - Work around a typo in the War Hammer object entry. # # 2003-02-15: # - Oops, that damage type flag should be INSANITY, not SANITY. :-} # - Check for no args before examining the first argument. # # 2003-02-09: # - Don't add tval names for INSTA_ART objects. # - New 'p' modifier to -M prints body parts for Possessors. use strict; use FileHandle; use Text::ParseWords; use vars qw($auto_yes $show_tsval $poss_info $show_desc $show_set); # Directory containing the *_info.txt files. my $TOME_LIB = '/usr/local/lib/tome/edit'; my $usage = < 0, prevmode => 0, args => '', matches => [], help => undef); my $tk_help = <. A list of all entries matching one or more of the given keys will be shown; you can select one for display with the mouse or the up/down arrow keys. The buttons in the middle toggle the display of additional information for the selected entry: Show in-game description Includes the description shown by the game when you 'I'nspect an object or recall a monster's information from the 'l'ook command. Show artifact set info For query type 'Artifacts', show extra information for artifacts that are members of an artifact set. Show possessor info For query type 'Monsters', includes corpse weight and the list of equipment a Possessor could wield while possessing this monster. Show tval/sval For query type 'Objects', shows the internal tval and sval of the selected object. The following types of search keys are recognized: string Matches entries whose name contains the given string. Case-insensitive. If the string contains spaces, you must put it in quotes. flags:FLAG,FLAG For query types 'Objects', 'Monsters', 'Ego items' and 'Artifacts', matches entries with all the flags specified in the comma-separated list; for query type 'Player info', matches entries that start with or gain the specified flags.. Flags must be spelled and capitalized exactly as they appear in the info files. A flag may be preceded with a ! to select entries that do not have that flag. char:C For query types 'Objects' and 'Monsters', matches things that are displayed on screen with the specified character. tval:NN For query type 'Objects', matches objects with tval NN. tsval:NN:MM For query type 'Objects', matches the oject with tval NN and sval MM. tsval:NN:MM-RR For query type 'Objects', matches objects with tval NN and svals MM-RR. ess:string For query type 'Alchemy', matches alchemist recipes that use the specified essence. Note that essence of Light must be spelled 'lite' and essence of Extra Life must be spelled 'extralife'. Case-insensitive. use:string For query type 'Alchemy', matches alchemist artifact flags whose material component matches the given string. Case-insensitive. set:string For query type 'Artifacts', match artifacts that are members of an artifact set whose name contains the given string. Case-insensitive. Some examples: shadow With query type 'Objects', matches Shadow Cloak and Shadow Blade; with query type 'Monsters', matches a half dozen or so monsters; with query type 'Ego items', matches Light of the Shadows; with query type 'Artifacts', matches the two artifact Shadow Cloaks; and with query type 'Alchemy', matches the alchemist recipe for Lights of the Shadows. 'hill orc' 'cave orc' half-orc uruk With query type 'Monsters', matches various types of orcs, including some uniques. gloves gauntlets handwear With query type 'Ego items', matches all ego handwear. tval:21 With query type 'Objects', matchs all hafted weapons. tsval:21:2-5 With query type 'Objects, matches Whip, Quarterstaff and Mace. char:- With query type 'Objects', matches all wands and rods. flags:RES_ACID,RES_ELEC,RES_COLD,RES_FIRE With query type 'Objects', 'Ego items' or 'Artifacts', matches all corresponding entries that provide all the elemental resistances. flags:DEMON With query type 'Monsters', matches all demons. flags:NEVER_MOVE,!UNIQUE With query type 'Monsters', matches all creatures than can be charmed and worn by Symbiants (never-moving and non-unique). char:v With query type 'Monsters', matches all vortex-like monsters -- that is, monsters displayed as a 'v'. vampiric With query type 'Ego items' matches Vampiric swords; with query type 'Alchemy', matches the alchemist recipe for Vampiric swords and the Vampiric artifact flag. ess:extralife With query type 'Alchemy', matches all alchemist recipes that use Extra Life essences (that is, all objects from which extra life essences can be extracted). use:ring With query type 'Alchemy', matches all alchemist artifact flags that require a ring as the material component. set:elven With query type 'Artifacts', matches Sting and the Phial of Galadriel, the members of the artifact set 'Elven Gifts'. yeek hermit sorceror With query type 'Player info', matches the Sorceror class, the Yeek race, and the Hermit subrace. flags:RES_POIS With query type 'Player info', matches all races and subraces that gain or start with poison resistance. EOF sub change_mode () { return if $W{mode} == $W{prevmode}; $W{prevmode} = $W{mode}; $W{list}->delete(0, 'end'); $W{match}->delete(0, 'end'); $W{descr}->delete('1.0', 'end'); $W{matches} = []; } sub populate () { my $data; my @keys = shellwords($W{args}); $W{list}->delete(0, 'end'); $W{descr}->delete('1.0', 'end'); $data = do_item 'k', @keys if $W{mode} == 0; $data = do_monster @keys if $W{mode} == 1; $data = do_item 'e', @keys if $W{mode} == 2; $data = do_item 'a', @keys if $W{mode} == 3; $data = do_recipe @keys if $W{mode} == 4; $data = do_player @keys if $W{mode} == 5; $W{matches} = $$data[1]; if (scalar @{$W{matches}} > 0) { $W{list}->insert(0, @{$$data[0]}); $W{list}->activate(0); $W{list}->selectionSet(0); $W{descr}->insert('1.0', $W{matches}[0]->description()); } else { $W{descr}->insert('1.0', 'No matches found'); } $W{list}->focus(); } sub check_active () { return if scalar @{$W{matches}} == 0; my $i = $W{list}->index('active'); $W{descr}->delete('1.0', 'end'); $W{descr}->insert('1.0', $W{matches}[$i]->description()); } sub close_help_window () { $W{help}->destroy; $W{help} = undef; } sub help_window () { return if defined $W{help}; $W{help} = new MainWindow; my $text = $W{help}->Scrolled('ROText', -setgrid => 1, -relief => 'sunken', -height => 20, -scrollbars => 'e') ->pack(-side => 'top', -fill => 'both'); $W{help}->bind('Tk::ROText', '' => ''); $W{help}->bind('Tk::ROText', '' => ''); $W{help}->Button(-text => 'Close', -command => \&close_help_window) ->pack(-side => 'right'); $text->insert('1.0', $tk_help); } sub exit_all () { $W{main}->destroy(); close_help_window if defined $W{help}; } sub setup_window () { $W{main} = new MainWindow; my $font = $W{main}->Font(-family => 'sans-serif', -size => 10); my $bold = $W{main}->Font(-family => 'sans-serif', -size => 10, -weight => 'bold'); my @modes = ('Objects', 'Monsters', 'Ego items', 'Artifacts', 'Alchemy', 'Player info'); my $modes = $W{main}->Frame->pack(-side => 'top', -fill => 'both'); Tk::grid($modes->Label(-text => 'Query type:', -font => $bold), -row => 0, -column => 0); for my $i (0..$#modes) { my $btn = $modes->Radiobutton(-text => $modes[$i], -font => $bold, -variable => \$W{mode}, -value => $i, -command => \&change_mode); Tk::grid($btn, -row => 0, -column => 1+$i); } $W{list} = $W{main}->Scrolled('Listbox', -setgrid => 1, -height => 10, -scrollbars => 'e', -font => $font) ->pack(-side => 'top', -fill => 'both'); my $entry = $W{main}->Frame->pack(-side => 'top', -fill => 'both'); $entry->Label(-text => 'Search keys:', -font => $bold) ->pack(-side => 'left'); $W{match} = $entry->Entry(-textvariable => \$W{args}, -font => $font, -relief => 'sunken') ->pack(-side => 'right', -expand => 1, -fill => 'both'); $W{match}->bind('' => \&populate); my $opts = $W{main}->Frame->pack(-side => 'top', -fill => 'both'); Tk::grid($opts->Checkbutton(-text => 'Show in-game description', -font => $bold, -variable => \$show_desc, -command => \&check_active), -row => 0, -column => 0); Tk::grid($opts->Checkbutton(-text => 'Show artifact set info', -font => $bold, -variable => \$show_set, -command => \&check_active), -row => 0, -column => 1); Tk::grid($opts->Checkbutton(-text => 'Show possessor info', -font => $bold, -variable => \$poss_info, -command => \&check_active), -row => 1, -column => 0); Tk::grid($opts->Checkbutton(-text => 'Show tval/sval', -font => $bold, -variable => \$show_tsval, -command => \&check_active), -row => 1, -column => 1); $W{descr} = $W{main}->Scrolled('ROText', -setgrid => 1, -relief => 'sunken', -height => 12, -scrollbars => 'e') ->pack(-side => 'top', -fill => 'both'); $W{main}->bind('Tk::ROText', '' => ''); $W{main}->bind('Tk::ROText', '' => ''); $W{main}->Button(-text => 'Exit', -command => \&exit_all) ->pack(-side => 'right'); $W{main}->Button(-text => 'Help', -command => \&help_window) ->pack(-side => 'right'); $W{list}->bind('', \&check_active); $W{list}->bind('', \&check_active); $W{list}->focus(); } setup_window; Tk::MainLoop(); } } else { # The command-line interface. die $usage if scalar @ARGV == 0 || $ARGV[0] !~ /^(-[AEOMRP])([ytpds]*)$/; my ($category, $flags) = ($1, $2); my $data; $auto_yes = 1 if $flags =~ /y/; $show_tsval = 1 if $flags =~ /t/; $poss_info = 1 if $flags =~ /p/; $show_desc = 1 if $flags =~ /d/; $show_set = 1 if $flags =~ /s/; shift @ARGV; if ($category =~ /-([AEO])/) { $data = do_item lc($1), @ARGV; } elsif ($category eq '-M') { $data = do_monster @ARGV; } elsif ($category eq '-R') { $data = do_recipe @ARGV; } elsif ($category eq '-P') { $data = do_player @ARGV; } else { # Shouldn't be possible, but just in case... die $usage; } my ($names, $objs) = @$data; for my $i (0..$#$names) { if (!$auto_yes) { my $ynq = Util::ynq $$names[$i]; last if !defined($ynq); next if !$ynq; } my $descr = $$objs[$i]->description(); print "$descr\n\n"; } } sub do_item ($@) { my ($which, @keys) = @_; $which = 'k' if $which eq 'o'; my $fh = new FileHandle "$TOME_LIB/${which}_info.txt" or die "Cannot open $TOME_LIB/${which}_info.txt: $!\n"; my @names = (); my @objs = (); Item::init $which; BLOB: while (my $blob = Util::read_blob $fh) { my $name = &{$Item::getname{$which}}($blob); KEY: for my $key (@keys) { next KEY if !Item::matches($blob, $name, $key, $which); push @names, $name; push @objs, new Item $name, $blob, $which; next BLOB; } } close($fh); return [ \@names, \@objs ]; } sub do_monster (@) { my @keys = @_; my $fh = new FileHandle "$TOME_LIB/r_info.txt" or die "Cannot open $TOME_LIB/r_info.txt: $!\n"; my @names = (); my @objs = (); Monster::init; BLOB: while (my $blob = Util::read_blob $fh) { my ($name) = ($blob =~ /^N:\d+:(.*)$/m); KEY: for my $key (@keys) { next KEY if !Monster::matches($blob, $name, $key); push @names, $name; push @objs, new Monster($blob); next BLOB; } } close($fh); return [ \@names, \@objs ]; } sub do_recipe (@) { my @keys = @_; my @names = (); my @objs = (); Recipe::init; RECIPE: for my $name (sort keys %Recipe::recipes) { KEY: for my $key (@keys) { next KEY if !Recipe::matches($name, $key); push @names, $name; push @objs, new Recipe $name; next RECIPE; } } return [ \@names, \@objs ]; } sub do_player (@) { my @keys = @_; my @names = (); my @objs = (); Player::init; ENTRY: for my $entry (@Player::entries) { KEY: for my $key (@keys) { next KEY if !Player::matches($entry, $key); push @names, $$entry{NAME}; push @objs, $entry; next ENTRY; } } return [ \@names, \@objs ]; } package Item; use vars qw(%getname %tsvals %sets %pval_boost %slays %brands %sustains %immune %resists %vuln %provides %drains %ignores %esp %lite %trap_target %general %admin %categories %tvals %tvals_plural %ego_tvals @ego_defns); %tsvals = (); sub init_tsvals () { local $_; my $name = ''; open IN, "<$TOME_LIB/k_info.txt" or die "Cannot open $TOME_LIB/k_info.txt: $!\n"; while () { if (/^N:\d+:(.*)$/) { $name = $1; $name =~ s/^\& //; $name =~ s/\~//; next; } if (/^I:(46:\d+):/) { $tsvals{$1} = "$name Trap Kit"; } elsif (/^I:(\d+:\d+):/) { $tsvals{$1} = $name; next; } } close IN; } %sets = (); sub set_flag ($$) { my ($flag, $pval) = @_; return Util::add_plus($pval) . " $pval_boost{$flag}" if exists $pval_boost{$flag}; return "slay $slays{$flag}" if exists $slays{$flag}; return $brands{$flag} if exists $brands{$flag}; return "sustain $sustains{$flag}" if exists $sustains{$flag}; return "immune $immune{$flag}" if exists $immune{$flag}; return "resist $resists{$flag}" if exists $resists{$flag}; return $provides{$flag} if exists $provides{$flag}; return "drain $drains{$flag}" if exists $drains{$flag}; return "sense $esp{$flag}" if exists $esp{$flag}; return $general{$flag} if exists $general{$flag}; return "<< ??? $flag >>"; } sub init_sets () { local $_; my $name = ''; my ($art, $pval, $n_worn) = (0, 0, 0); open IN, "<$TOME_LIB/set_info.txt" or die "Cannot open $TOME_LIB/set_info.txt: $!\n"; while () { if (/^N:\d+:(.*)$/) { $name = $1; $sets{$name} = { descr => '', num => 0 }; next; } if (/^D:(.*)$/) { if ($sets{$name}{descr}) { $sets{$name}{descr} .= " $1"; } else { $sets{$name}{descr} = $1; } next; } if (/^P:(\d+):(\d+):(\d+)$/) { ($art, $n_worn, $pval) = ($1, $2, $3); if (!exists($sets{$art})) { $sets{$name}{num}++; $sets{$art} = { set => $name }; } next; } if (/^F:(.*)$/) { my $flags = $1; my @flags = split /\s*\|\s*/, $flags; @flags = map { set_flag $_, $pval } @flags; $sets{$art}{$n_worn} ||= []; push @{$sets{$art}{$n_worn}}, @flags; } } close IN; } sub init_flags () { %pval_boost = Util::split_hash < \%pval_boost, LITE => \%lite, SLAYS => \%slays, BRANDS => \%brands, SUSTAINS => \%sustains, IMMUNES => \%immune, RESISTS => \%resists, VULN => \%vuln, PROVIDES => \%provides, DRAINS => \%drains, IGNORES => \%ignores, ESP => \%esp, TRAP_TARGET => \%trap_target, GENERAL => \%general, ADMIN => \%admin, ); @ego_defns = ( '', 'Random high resist [blindness, confusion, sound, shards, nether, ' . "nexus, chaos,\n disenchantment, poison]", 'Random ability [feather fall, light, see invisible, telepathy, slow ' . "digestion,\n regeneration, free action, hold life]", 'Sustain random stat' ); %tvals = Util::split_hash < \&art_name, 'e' => \&ego_name, 'k' => \&obj_name); } sub describe_flag ($$) { my ($flag, $pval) = @_; return Util::add_plus($pval) . " $pval_boost{$flag}" if exists $pval_boost{$flag}; return "resist $resists{$flag}" if exists $resists{$flag}; return "immunity to $immune{$flag}" if exists $immune{$flag}; return "sustain $sustains{$flag}" if exists $sustains{$flag}; return "drain $drains{$flag}" if exists $drains{$flag}; return $provides{$flag} if exists $provides{$flag}; return 'ESP' if $flag eq 'ESP_ALL'; if (exists $esp{$flag}) { my $ret = $esp{$flag}; $ret =~ s/(?: creature)?s$//; return "sense $ret"; } return 'immobility' if $flag eq 'IMMOVABLE'; return 'aggravation' if $flag eq 'AGGRAVATE'; return 'vulnerability to fire' if $flag eq 'SENS_FIRE'; return $flag; } sub art_name { my $blob = shift; $blob =~ /^N:\d+:(.*)\nI:(\d+:\d+)/ or return ''; my ($name, $tsval) = ($1, $2); $tsval = $tsvals{$tsval} if exists $tsvals{$tsval}; return "The $tsval $name"; } sub numerically { return $a <=> $b } sub ego_name { my $blob = shift; my $name = ($blob =~ /^N:\d+:(.*)\n/ && $1) or return ''; my $pos = ($blob =~ /^X:([AB]):(\d+)/m && $1) or return ''; my @tvals = map { m/^125:/ ? () : exists $ego_tvals{$_} ? $ego_tvals{$_} : (m/(\d+):\d+:\d+/ && exists $ego_tvals{$1}) ? $ego_tvals{$1} : $_ } ($blob =~ /^T:(\d+:\d+:\d+)/mg); return '' if scalar @tvals == 0; my $tvals = join '/', sort keys %{{ map { ($_ => 1) } @tvals }}; $tvals =~ s#Hard armor/Soft armor#Body armor#; $tvals =~ s#Large Metal Shield/Shield of Deflection/Small Metal Shield#Metal Shield#; $tvals =~ s#Crown/Demonhorn/Helm#Headwear#; $tvals =~ s#Axe/(.*)/Hafted weapon/Polearm/Sword#Weapon/$1#; $tvals =~ s#Axe/Hafted weapon/Polearm/Sword#Weapon#; $tvals =~ s#Arrows/Bolts/Shots#Ammo#; $name =~ s/^(.*) of $/of $1/ if $tvals eq 'Rod (main)'; return ($pos eq 'A' ? "$tvals $name" : "$name $tvals"); } sub obj_name { my $blob = shift; $blob =~ /^N:\d+:(.*)\nG:.:..?\nI:(\d+):(\d+):(-?\d+)(?::(-?\d+))?/ or return ''; my ($name, $tval, $sval, $pval, $pval2) = ($1, $2, $3, $4, $5); $name =~ s/\& ([AEIOU])/An $1/; $name =~ s/\&/A/; $name =~ s/\~//; if ($blob !~ /\b(FULL_NAME|INSTA_ART)\b/ && exists $tvals{$tval}) { if ($tvals{$tval} eq 'A Rune') { $name = "A Rune [$name]" if $sval != 255; } elsif ($tvals{$tval} ne 'A Mushroom of' || $sval < 32) { $name = "$tvals{$tval} $name"; } } elsif ($tval == 35 && defined $pval2) { $name = "A Cloak of Mimicry $name"; } elsif ($tval == 46) { $name = "A $name Trap Kit"; $name =~ s/^A ([AEIOU])/An $1/; } $name =~ s/^An? /The / if $blob =~ /\bNORM_ART\b/; return $name; } sub obj_name_plural { my $blob = shift; $blob =~ /^N:\d+:(.*)\nG:.:..?\nI:(\d+):(\d+):(-?\d+)(?::(-?\d+))?/ or return ''; my ($name, $tval, $sval, $pval, $pval2) = ($1, $2, $3, $4, $5); $name =~ s/\& //; $name =~ s/\~/s/; if ($blob !~ /\b(FULL_NAME|INSTA_ART)\b/ && exists $tvals_plural{$tval}) { if ($tvals_plural{$tval} eq 'Runes') { $name = "Runes [$name]" if $sval != 255; } elsif ($tvals_plural{$tval} ne 'Mushrooms of' || $sval < 32) { $name = "$tvals_plural{$tval} $name"; } } elsif ($tval == 35 && defined $pval2) { $name = "Cloaks of Mimicry $name"; } elsif ($tval == 46) { $name = "$name Trap Kits"; } return $name; } sub matches ($$$$) { my ($blob, $name, $key, $which) = @_; return 1 if $name =~ /$key/i; if ($key =~ /^char:(.)$/) { my $char = $1; return 1 if $blob =~ /^G:${char}:/m; } if ($key =~ /^tval:(\d+)$/) { my $tval = $1; return 1 if $blob =~ /^I:${tval}:/m; } if ($key =~ /^tsval:(\d+):(\d+)$/) { my ($tval, $sval) = ($1, $2); return 1 if $blob =~ /^I:${tval}:${sval}:/m; } if ($key =~ /^tsval:(\d+):(\d+)-(\d+)$/) { my ($tval, $sval1, $sval2) = ($1, $2, $3); return 1 if $blob =~ /^I:${tval}:(\d+):/m && $1 >= $sval1 && $1 <= $sval2; } if ($key =~ /^flags:(.*)$/) { my @flags = split ',', $1; my @neg = map { s/^!// || 0 } @flags; for my $i (0..$#flags) { return 0 if ( $neg[$i] && $blob =~ /\b$flags[$i]\b/) || (!$neg[$i] && $blob !~ /\b$flags[$i]\b/) } return 1; } if ($key =~ /^set:(.*)$/ && $which eq 'a') { my $set = $1; return 0 if $blob !~ /^N:(\d+):/m; my $idx = $1; return 1 if exists $sets{$idx} && $sets{$idx}{set} =~ /$set/i; } return 0; } sub new ($$$$) { local $_; my ($class, $name, $blob, $which) = @_; my $self = { NAME => $name, ATTR => ($which eq 'k' ? '<>' : ''), CHAR => ($which eq 'k' ? '<>' : ''), IDX => 0, CHECK_SET => 0, PVAL => '', LEVEL => 0, RARITY => 0, WEIGHT => 0, COST => 0, BONUS => [], EGO_BONUS => [], PVAL_BOOST => [], SLAYS => [], BRANDS => [], SUSTAINS => [], IMMUNES => [], RESISTS => [], VULN => [], PROVIDES => [], DRAINS => [], IGNORES => [], ESP => [], TRAP_TARGET => [], GENERAL => [], REQUIRED => [], FORBIDDEN => [], LITE => [], FUEL_LITE => 0, ADMIN => [], ACTIVATE => '', POWER => '', SPEC_GEN => '', LEFTOVERS => [], DESCR => [], }; for (split "\n", $blob) { if (/^N:(\d+):/) { $$self{IDX} = $1; $$self{CHECK_SET} = 1 if $which eq 'a' && exists $sets{$1}; next; } if (/^G:(.):(.)/) { @$self{qw(ATTR CHAR)} = ($Attr::color{$2}, "'$1'"); next; } if (/^I:(\d+):(\d+):(-?\d+)/) { @$self{qw(TVAL SVAL)} = ($1, $2) if $which eq 'k'; $$self{PVAL} = Util::add_plus $3; next; } if (/^R:(\d+)$/) { for (keys %categories) { push @{$$self{$_}}, [ $1 ] if $_ ne 'LEFTOVERS'; } next; } if (/^W:(\d+):(\d+):(\d+):(\d+)/) { @$self{qw(LEVEL RARITY WEIGHT COST)} = ($1, $2, $3, $4); next } if (/^C:(-?\d+):(-?\d+):(-?\d+):(-?\d+)/) { $$self{PVAL} = Util::add_plus2 $4; $$self{EGO_BONUS} = [ map { Util::add_plus2 $_ } ($1, $2, $3) ]; next; } if (/^P:(-?\d+):(\d+d\d+):(-?\d+):(-?\d+):(-?\d+)/) { $$self{BONUS} = [ $1, Util::add_plus($5), $2, Util::add_plus($3, $4) ]; next; } if (/^a:HARDCORE=(.*)$/) { $$self{ACTIVATE} = "effect '$1'"; next } if (/^a:SPELL=(.*)$/) { $$self{ACTIVATE} = "spell '$1'"; next } if (/^Z:(.*)$/) { $$self{POWER} = $1; next } if (/^(?:F|r:[NF]):(.*\S)\s*$/) { my $flags = $1; my @flags = split /\s*\|\s*/, $flags; for my $flag (@flags) { next if $flag eq 'FULL_NAME'; ($$self{ATTR} = $Attr::attr{$flag}), next if exists $Attr::attr{$flag}; ($$self{FUEL_LITE} = 1), next if $flag eq 'FUEL_LITE'; #($$self{LITE} += $lite{$flag}), next if exists $lite{$flag}; if ($flag eq 'ACTIVATE') { $$self{ACTIVATE} ||= 'unspecified effect'; next } if ($flag eq 'SPECIAL_GENE') { Dungeon::init; Dungeon::init_stage2; my $dgn = Dungeon::find($which, $$self{IDX}); if ($dgn) { $$self{SPEC_GEN} = Dungeon::describe($dgn, 1); next; } } for my $cat ((keys(%categories), 'LEFTOVERS')) { next if $cat ne 'LEFTOVERS' && !exists($categories{$cat}{$flag}); my $flag_p = $flag; $flag_p = $categories{$cat}{$flag} if $cat ne 'LEFTOVERS'; Util::push $$self{$cat}, $flag_p if /^F:/; Util::push $$self{REQUIRED}, $flag_p if /^r:N:/; Util::push $$self{FORBIDDEN}, $flag_p if /^r:F:/; last; } } next; } if (/^D:(.*\S)\s*$/) { push @{$$self{DESCR}}, $1; next } } $$self{WEIGHT} /= 10; $$self{RARITY} = -1 if $which eq 'k'; my @lite = (0); for my $lite (@{$$self{LITE}}) { if (! ref $lite) { $lite[0] += $lite } elsif ($$lite[0] == 100) { for (@$lite[1..$#$lite]) { $lite[0] += $_ } } else { my $r = 0; for (@$lite[1..$#$lite]) { $r += $_ } push @lite, "$$lite[0]\%: +$r" if $r > 0; } } $lite[0] = 5 if $lite[0] > 5; $$self{LITE} = shift @lite; $$self{LITE} .= ' (' . join(', ', @lite) . ')' if scalar @lite > 0; return bless $self, $class; } sub description ($) { my $self = shift; local $_ = $$self{NAME}; $_ .= " {$$self{TVAL}:$$self{SVAL}}" if $main::show_tsval && exists $$self{TVAL}; $_ .= sprintf ' [%s,%s] (%s) (%s,%s)', @{$$self{BONUS}} if @{$$self{BONUS}}; $_ .= sprintf ' (%s,%s) [%s]', @{$$self{EGO_BONUS}} if @{$$self{EGO_BONUS}}; $_ .= " ($$self{PVAL})" if $$self{PVAL} && $$self{PVAL} ne '+0'; $_ .= " ($$self{ATTR} $$self{CHAR})" if $$self{ATTR}; $_ .= "\nLevel $$self{LEVEL}, " . ($$self{RARITY} >= 0 ? "rarity $$self{RARITY}, " : '') . "weight $$self{WEIGHT} lb, cost $$self{COST}\n"; $_ .= "Activates for $$self{ACTIVATE}\n" if $$self{ACTIVATE}; $_ .= "Grants power of $$self{POWER}\n" if $$self{POWER}; if ($$self{LITE}) { if ($$self{FUEL_LITE}) { $_ .= "Light (radius $$self{LITE}), when fueled\n"; } else { $_ .= "Permanent light (radius $$self{LITE})\n"; } } $_ .= Util::comma_list "$$self{PVAL}", $$self{PVAL_BOOST}; $_ .= Util::comma_list 'Slay', $$self{SLAYS}; $_ .= Util::comma_list '', $$self{BRANDS}; $_ .= Util::comma_list 'Sustain', $$self{SUSTAINS}; $_ .= Util::comma_list 'Immune', $$self{IMMUNES}; $_ .= Util::comma_list 'Resist', $$self{RESISTS}; $_ .= Util::comma_list 'Vulnerable to ', $$self{VULN}; $_ .= Util::comma_list 'Provides ', $$self{PROVIDES}; $_ .= Util::comma_list 'Drains', $$self{DRAINS}; $_ .= Util::comma_list 'Unharmed by', $$self{IGNORES}; $_ .= Util::comma_list 'Only affects', $$self{TRAP_TARGET}; $_ .= Util::comma_list 'ESP for', $$self{ESP}; $_ .= Util::comma_list '', $$self{GENERAL}; $_ .= Util::comma_list 'Required on base object:', $$self{REQUIRED}; $_ .= Util::comma_list 'Forbidden on base object:', $$self{FORBIDDEN}; $_ .= $$self{SPEC_GEN} if $$self{SPEC_GEN}; $_ .= Util::comma_list 'Description: ', $$self{DESCR}, 1 if $main::show_desc; if ($main::show_set && $$self{CHECK_SET}) { my $set = $sets{$$self{IDX}}; my $setg = $sets{$$set{set}}; my $line = "Member of artifact set '$$set{set}'"; $line .= " (\"$$setg{descr}\")" if $main::show_desc; $_ .= Util::wrap_line $line; for my $n (sort numerically grep /^\d+$/, keys %$set) { my $leader = ($n >= $$setg{num} ? "all $n" : "$n or more"); $leader = 'both' if $leader eq 'all 2'; $leader = "Extra flags if $leader set members equipped: "; $_ .= Util::comma_list $leader, $$set{$n}; } } $_ .= Util::comma_list '###', $$self{ADMIN}; $_ .= Util::comma_list '[unhandled]', $$self{LEFTOVERS}; chomp $_; return $_; } package Monster; use vars qw(%mon_type %appears %attack_forms %damage_forms %is %doesnt %treasure %can %cant %hurt_by %resists %suscep %hide %general %shoots %breathes %spells %rand_move @drop_type @drop_worth @drop_grps); sub init () { Attr::init; return if scalar keys %mon_type > 0; %mon_type = Util::split_hash < '', 25 => ' (slightly random)', 50 => ' (random)', 75 => ' (very random)'); @drop_type = qw(drops treasures items); @drop_worth = ('', 'good ', 'great ', 'useful ', 'chosen '); @drop_grps = qw(treasure combat magic tools); } sub treasure_desc ($$) { my ($flags, $ratios) = @_; return '' if scalar @$flags == 0; my ($min, $max, $treas, $item, $also) = (0, 0, 0, 0, ''); my @drops = (); my $n; my $rat = join ', ', map { "$$ratios[$_]\% $drop_grps[$_]" } grep { $$ratios[$_] } 0..$#$ratios; $rat &&= " ($rat)"; foreach $n (@$flags) { push @drops, $n and next if $n !~ /^\d/; ($item = int($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; } unshift @drops, "$min-$max $drop_worth[$item]$drop_type[$treas]$rat" if $max > 0; my $drops = join ', ', @drops; $drops =~ s/, ([^,]+)$/ and $1/ if scalar @drops > 1; $drops = "Drops $drops" if $max == 0; return "$drops\n"; } sub matches ($$$) { my ($blob, $name, $key) = @_; return 1 if $name =~ /$key/i; if ($key =~ /^char:(.)$/) { my $char = $1; return 1 if $blob =~ /^G:${char}:/m; } if ($key =~ /^flags:(.*)$/) { my @flags = split ',', $1; my @neg = map { s/^!// || 0 } @flags; for my $i (0..$#flags) { return 0 if ( $neg[$i] && $blob =~ /\b$flags[$i]\b/) || (!$neg[$i] && $blob !~ /\b$flags[$i]\b/) } return 1; } return 0; } sub new ($$) { my ($class, $blob) = @_; my $self = { NAME => '', ATTR => '<>', CHAR => '<>', TYPE => '<>', HP => '?d?', SPEED => 0, AC => 0, SLEEPY => 0, NOTICE => 0, LEVEL => 0, RARITY => 0, EXP => 0, MOVE => 0, SPELLRATE => 0, APPEARS => ['in dungeon'], WILD_TOO => 0, ONLY_DEPTH => 0, SPEC_GEN => '', UNIQUE => 0, ATTACKS => [], FLAGS => { FORCE_MAXHP => 0, POWERFUL => 0 }, IS => [], DOESNT => [], TREASURE => [], TRSR_RATIOS => [0, 0, 0, 0], CAN => [], CANT => [], HURT => [], RESISTS => [], SUSCEP => [], HIDE => [], GENERAL => [], SHOOTS => [], BREATHES => [], SPELLS => [], PARTS => [ 1, 1, 1, 2, 1, 1 ], CORPSE_WGT => '???', LEFTOVERS => [], DESCR => [], }; study $blob; (@$self{qw(IDX NAME)}) = ($blob =~ /^N:(\d+):(.*)$/m); @$self{qw(ATTR CHAR)} = ($Attr::color{$2}, "'$1'") if ($blob =~ /^G:(.):(.)/m); @$self{qw(SPEED HP NOTICE AC SLEEPY)} = ($1, $2, $3, $4, $5) if $blob =~ /^I:(\d+):(\d+d\d+):(\d+):(\d+):(\d+)/m; @$self{qw(LEVEL RARITY CORPSE_WGT EXP)} = ($1, $2, $3/10, $4) if $blob =~ /^W:(\d+):(\d+):(\d+):(\d+)/m; $$self{ATTACKS} = [ $blob =~ /^B:(.*)$/mg ]; $$self{DESCR} = [ $blob =~ /^D:(.*\S)\s*$/mg ]; $$self{PARTS} = [ split ':', $1 ] if $blob =~ /^E:(\d+:\d+:\d+:\d+:\d+:\d+)$/m; $$self{TRSR_RATIOS} = [ split ':', $1 ] if $blob =~ /^O:(\d+:\d+:\d+:\d+)$/m; my @flags = grep { $_ } map { split /\s*\|\s*/ } ($blob =~ /^F:(.*\S)\s*$/mg); for my $i (@flags) { ($$self{TYPE} = $mon_type{$i}), next if $mon_type{$i}; ($$self{ATTR} = $Attr::attr{$i}), next if $Attr::attr{$i}; ($$self{FLAGS}{$i} = 1), next if defined $$self{FLAGS}{$i}; ($$self{MOVE} += $1), next if $i =~ /RAND_(\d+)/; push @{$$self{APPEARS}}, $appears{$i} and next if $appears{$i}; shift @{$$self{APPEARS}} and next if $i eq 'WILD_ONLY'; $$self{UNIQUE} = 1 if $i eq 'UNIQUE'; ($$self{WILD_TOO} = 1), next if $i eq 'WILD_TOO'; ($$self{ONLY_DEPTH} = 1), next if $i eq 'ONLY_DEPTH'; ($$self{CHAR} = 'random symbol'), next if $i eq 'SHAPECHANGER'; if ($i eq 'SPECIAL_GENE') { Dungeon::init; Dungeon::init_stage2a; my $dgn = Dungeon::find('m', $$self{IDX}); if ($dgn) { $$self{SPEC_GEN} = Dungeon::describe($dgn, 0); push @{$$self{TREASURE}}, $$dgn{grdn_drop} if exists $$dgn{grdn_drop}; next; } } push @{$$self{IS}}, $is{$i} and next if $is{$i}; push @{$$self{DOESNT}}, $doesnt{$i} and next if $doesnt{$i}; push @{$$self{TREASURE}}, $treasure{$i} and next if $treasure{$i}; push @{$$self{CAN}}, $can{$i} and next if $can{$i}; push @{$$self{CANT}}, $cant{$i} and next if $cant{$i}; push @{$$self{HURT}}, $hurt_by{$i} and next if $hurt_by{$i}; push @{$$self{RESISTS}}, $resists{$i} and next if $resists{$i}; push @{$$self{SUSCEP}}, $suscep{$i} and next if $suscep{$i}; push @{$$self{HIDE}}, $hide{$i} and next if $hide{$i}; push @{$$self{GENERAL}}, $general{$i} and next if $general{$i}; push @{$$self{LEFTOVERS}}, "F:$i"; } my @spflags = grep { $_ } map { split /\s*\|\s*/ } ($blob =~ /^S:(.*\S)\s*$/mg); for my $i (@spflags) { ($$self{SPELLRATE} = $1), next if $i =~ /1_IN_(\d+)/; push @{$$self{CAN}}, $can{$i} and next if $can{$i}; push @{$$self{SHOOTS}}, $shoots{$i} and next if $shoots{$i}; push @{$$self{BREATHES}}, $breathes{$i} and next if $breathes{$i}; push @{$$self{SPELLS}}, $spells{$i} and next if $spells{$i}; push @{$$self{LEFTOVERS}}, "S:$i"; } $$self{SPEED} = Util::add_plus ($$self{SPEED} - 110); $$self{MOVE} = $rand_move{$$self{MOVE}}; for my $i (@{$$self{ATTACKS}}) { my ($atyp, $dtyp, $dam) = ($i =~ /^(\w+)(?::(\w+|\*)(?::(\d+d\d+))?)?$/); next if !$atyp; $atyp = $attack_forms{$atyp} if exists $attack_forms{$atyp}; $dtyp = '' if $dtyp eq '*'; $dtyp = $damage_forms{$dtyp} if $dtyp && exists $damage_forms{$dtyp}; $i = $atyp; $i .= " for $dtyp" if $dtyp; $i .= " ($dam)" if $dam; } if ($$self{WILD_TOO}) { if (scalar @{$$self{APPEARS}} == 0 || $$self{APPEARS}[0] ne 'in dungeon') { warn "[WILD_TOO and WILD_ONLY ($$self{NAME})]\n"; } elsif (scalar @{$$self{APPEARS}} == 1) { push @{$$self{APPEARS}}, map { $appears{$_} } qw(WILD_TOWN WILD_WOOD WILD_GRASS WILD_WASTE); } } elsif (scalar @{$$self{APPEARS}} == 0) { warn "[WILD_ONLY with no other WILD_* ($$self{NAME})]\n"; } # Slight Hack(TM): If monster doesn't have the SPECIAL_GENE flag but is # unique and appears in %Dungeon::dgn_mon, add a special-gen description. if ($$self{UNIQUE} && !$$self{SPEC_GEN}) { Dungeon::init; Dungeon::init_stage2a; my $dgn = Dungeon::find('m', $$self{IDX}); if ($dgn) { $$self{SPEC_GEN} = Dungeon::describe($dgn, 0); $$self{SPEC_GEN} =~ s/\n$/ (?)\n/; push @{$$self{TREASURE}}, $$dgn{grdn_drop} if exists $$dgn{grdn_drop}; } } return bless $self, $class; } my $body_delta = 0; sub body_part ($$;$) { my ($num, $descr, $flags) = @_; my $singular = $flags && ($flags & 1); my $zero_chop = $flags && ($flags & 2); return undef if $body_delta && $num == 0; return $descr if $singular && !$body_delta && $num > 0; $descr =~ s/S// if $num == 1; $descr =~ s/S/s/; $descr =~ s/^.*:// if $zero_chop && $num == 0; $descr =~ s/://; $num = 'no' if $num == 0; $num = Util::add_plus $num if $body_delta; return "$num $descr"; } sub body_parts ($;$) { my $P = $_[0]; my @parts = (); $body_delta = ($_[1] ? 1 : 0); push @parts, body_part $$P[0], 'weaponS'; push @parts, body_part $$P[0], 'bow', 1; push @parts, body_part $$P[1], 'body armor', 1; push @parts, body_part $$P[1], 'cloak', 1; push @parts, body_part $$P[1], 'light', 1; push @parts, body_part $$P[1], 'quiver', 1; push @parts, body_part $$P[3], 'ringS'; push @parts, body_part $$P[4], 'helmS'; push @parts, body_part $$P[4], 'amuletS'; push @parts, body_part $$P[2], 'shieldS'; push @parts, body_part $$P[2], 'pairS of :gloves', 2; push @parts, body_part $$P[2], 'digger/tool', 1; push @parts, body_part $$P[5], 'pairS of :boots', 2; push @parts, body_part $$P[1], 'symbiote', 1; @parts = grep { defined } @parts if $body_delta; return @parts; } sub description ($) { my ($self) = @_; my $scratch = ''; local $_; if ($$self{FLAGS}{FORCE_MAXHP} || $$self{HP} =~ /d1$/) { die "$$self{NAME} has $$self{HP} hp?\n" unless $$self{HP} =~ /(\d+)d(\d+)/; my ($d, $s) = ($1, $2); my $hp = $d*$s; $$self{HP} = "$hp (${d}d$s)"; } $_ = "$$self{NAME} ($$self{ATTR} $$self{CHAR}, $$self{TYPE})\n"; $_ .= sprintf '%s HP, speed %s%s, AC %s, "sleepiness" %s, notice range %s', @$self{qw(HP SPEED MOVE AC SLEEPY NOTICE)}; $_ .= sprintf "\n".'Level %s, rarity %s, exp %s'."\n", @$self{qw(LEVEL RARITY EXP)}; if ($$self{SPEC_GEN}) { $_ .= $$self{SPEC_GEN}; } elsif ($$self{ONLY_DEPTH}) { $_ .= "Only appears at dungeon level $$self{LEVEL}\n"; } else { $_ .= Util::comma_list 'Can appear', $$self{APPEARS}; } $_ .= Util::comma_list 'Monster is', $$self{IS}; $_ .= "Doesn't ".join(' or ', @{$$self{DOESNT}})."\n" if @{$$self{DOESNT}}; $_ .= "Can't be ".join(' or ', @{$$self{CANT}})."\n" if @{$$self{CANT}}; $_ .= Util::comma_list 'Attacks: ', $$self{ATTACKS}; $_ .= Util::comma_list 'Can', $$self{CAN}; $_ .= Util::comma_list 'Hurt by', $$self{HURT}; $_ .= Util::comma_list 'Resists', $$self{RESISTS}; $_ .= Util::comma_list 'Susceptible to', $$self{SUSCEP}; $_ .= "Concealed by ".join(' or ', @{$$self{HIDE}})."\n" if @{$$self{HIDE}}; $_ .= Util::comma_list '', $$self{GENERAL}; $_ .= Util::comma_list 'Shoots', $$self{SHOOTS}; $scratch = ($$self{FLAGS}{POWERFUL} ? '*Breathes*' : 'Breathes'); $scratch = "$scratch (1/$$self{SPELLRATE})"; $_ .= Util::comma_list $scratch, $$self{BREATHES}; $_ .= Util::comma_list "Spells (1/$$self{SPELLRATE}): ", $$self{SPELLS}; $_ .= treasure_desc $$self{TREASURE}, $$self{TRSR_RATIOS}; $_ .= Util::comma_list 'Description: ', $$self{DESCR}, 1 if $main::show_desc; if ($main::poss_info) { my @parts = body_parts $$self{PARTS}; $_ .= Util::comma_list 'Can wear/wield', \@parts; $_ .= "Corpse weight $$self{CORPSE_WGT} lbs.\n"; } $_ .= Util::comma_list '[unhandled]', $$self{LEFTOVERS}; chomp $_; return $_; } package Recipe; use vars qw(%recipes %monsters %monflags %objects %egos %essences %remains); sub init () { return if scalar keys %recipes > 0; local $_; my $a_pfx = ''; my $a_cost = ''; my $a_flag = ''; Item::init('e'); %recipes = (); %monsters = (); %objects = (); %egos = (); %essences = ( EXTRALIFE => 'Extra Life' ); %monflags = Util::split_hash <<'EOF'; S_HI_UNDEAD, summoner of greater undead TROLL, troll ANIMAL, animal DEMON, demon DRAGON, dragon EVIL, evil creature GIANT, giant ORC, orc UNDEAD, undead creature EOF %remains = Util::split_hash <<'EOF'; -1, remains 1, corpse 2, skeleton 3, head 4, skull 5, meat EOF # Read the recipes. open IN, "<$TOME_LIB/al_info.txt" or die "Cannot open $TOME_LIB/al_info.txt: $!\n"; while () { chomp; if (/^I:(\d+):(\d+):(\d+):(\w+)/) { # Essence needed for item or ego type. my ($tval, $sval, $amt, $ess) = ($1, $2, $3, $4); my $tsval = "${tval}:$sval"; if ($tval == 1) { $egos{$sval} = 1; $tsval = "E:$sval"; } $essences{$ess} = ucfirst lc $ess if !exists($essences{$ess}); $recipes{$tsval} ||= {}; $recipes{$tsval}{$ess} ||= 0; $recipes{$tsval}{$ess} += $amt; next; } if (/^A:\d+:(\d+:-?\d+:-?\d+:[01]:\d+:-?\d+)$/) { $a_cost = $1; $a_flag = $a_pfx = ''; next; } if (/^F:/) { $a_pfx = 'Artifact flag'; next } if (/^x:/) { $a_pfx = 'Artifact activation'; next } if (/^f:(.*)$/) { $a_flag = $1; next } if (/^D:(.*)$/) { # Artifact flag or activation. my $flag = $1; warn "No artifact entry for '$flag'" and next if !$a_cost; warn "No artifact prefix for '$flag'" and next if !$a_pfx; $flag = "${a_pfx}: $flag"; warn "Duplicate entries for $flag" if exists $recipes{$flag}; $recipes{$flag} = {}; my ($tval, $sval, $pval, $level, $xp) = ($a_cost =~ /^(\d+):(-?\d+):(-?\d+):[01]:(\d+):(-?\d+)$/); @{$recipes{$flag}}{qw(level xp)} = ($level, $xp); if ($tval == 9) { $recipes{$flag}{remains} = $remains{$sval}; if ($pval == 0) { $recipes{$flag}{monflag} = $a_flag; } else { $recipes{$flag}{monidx} = $pval; $monsters{$pval} = 1; } } elsif ($sval < 0) { $recipes{$flag}{tval} = $tval; } elsif ($tval > 0) { my $tsval = "${tval}:$sval"; $recipes{$flag}{tsval} = $tsval; $objects{$tsval} = 1; } } } close IN; # Read monster data, to fill in corpse information. open IN, "<$TOME_LIB/r_info.txt" or die "Cannot open $TOME_LIB/r_info.txt: $!\n"; while () { chomp; next if !/^N:(\d+):(.*)$/; $monsters{$1} = $2 if exists $monsters{$1}; } close IN; # Read object data, to get object and component names. my $fh = new FileHandle "$TOME_LIB/k_info.txt" or die "Cannot open $TOME_LIB/k_info.txt: $!\n"; while (my $blob = Util::read_blob $fh) { next if $blob !~ /^I:(\d+:\d+):/m; my $tsval = $1; next if !exists($recipes{$1}) && !exists($objects{$1}); my $o_name = Item::obj_name($blob); $objects{$tsval} = $o_name if exists $objects{$tsval}; if (exists $recipes{$tsval}) { $recipes{$o_name} = $recipes{$tsval}; delete $recipes{$tsval}; } } close IN; # Read ego data, to get names of ego types. $fh = new FileHandle "$TOME_LIB/e_info.txt" or die "Cannot open $TOME_LIB/e_info.txt: $!\n"; while (my $blob = Util::read_blob $fh) { next if $blob !~ /^N:(\d+):/ || !exists($egos{$1}); my $key = "E:$1"; die "BUGCHK ($key)" if !exists($recipes{$key}); my $e_name = Item::ego_name($blob); $recipes{$e_name} = $recipes{$key}; delete $recipes{$key}; } close($fh); } sub describe_remains ($) { my $recipe = $_[0]; return '' if !exists($$recipe{remains}); my $remains = "$$recipe{remains} of "; if (exists $$recipe{monflag}) { my $F = $$recipe{monflag}; $remains .= (exists $monflags{$F} ? $monflags{$F} : "monster with flag $F"); } elsif (exists $$recipe{monidx}) { $remains .= $monsters{$$recipe{monidx}}; } else { $remains .= "any monster"; } return $remains; } sub describe_component ($) { my $recipe = $_[0]; if (exists $$recipe{tval}) { my $tval = $$recipe{tval}; return exists $Item::ego_tvals{$tval} ? $Item::ego_tvals{$tval} : "tval#$tval"; } return $objects{$$recipe{tsval}} if exists $$recipe{tsval}; return ''; } sub matches ($$) { my ($name, $key) = @_; return 1 if $name =~ /\Q$key\E/i; if ($key =~ /^ess:(.*)/) { my $essence = $1; for (keys %{$recipes{$name}}) { next if !exists($essences{$_}); # Hack: Don't let pattern 'life' match essence 'Extra life'. next if $_ eq 'EXTRALIFE' && lc $essence eq 'life'; # Hack: Don't let pattern 'light' match essence 'Lightning'. next if $_ eq 'LIGHTNING' && lc $essence eq 'light'; return 1 if /$essence/i || $essences{$_} =~ /$essence/i; } } if ($key =~ /^use:(.*)/) { my $use = $1; return 1 if describe_remains($recipes{$name}) =~ /$use/i || describe_component($recipes{$name}) =~ /$use/i; } return 0; } sub new ($$) { my ($class, $name) = @_; my $self = { name => $name }; return bless $self, $class; } sub description ($) { my $self = $_[0]; my $name = $$self{name}; return '' if !exists($recipes{$name}); my $recipe = $recipes{$name}; my $remains = describe_remains $recipe; my $component = describe_component $recipe; my @parts = (); local $_ = $name; $_ .= " (available at skill $$recipe{level})" if exists $$recipe{level}; push @parts, "$$recipe{xp}#exp" if exists $$recipe{xp}; push @parts, map { "$$recipe{$_}#$essences{$_}" } sort grep { /^[A-Z]+$/ } keys %$recipe; push @parts, $component if $component; push @parts, $remains if $remains; my $parts = Util::comma_list '', \@parts; $parts =~ s/#/ /g; return $_."\n $parts"; } package Dungeon; my %dgn_mon = (); my %dgn_art = (); my %dgn_obj = (); my %dgns; my $done_stage2 = 0; my $done_stage2a = 0; sub init () { return if scalar keys %dgn_mon > 0; # Grmbl... for some reason this doesn't take if I try to initialize it # outside the function. %dgns = ( m => \%dgn_mon, a => \%dgn_art, k => \%dgn_obj ); # Read dungeon descriptions. my $fh = new FileHandle "$TOME_LIB/d_info.txt" or die "Cannot open $TOME_LIB/d_info.txt: $!\n"; while (my $blob = Util::read_blob $fh) { next if $blob !~ /\bFINAL_/; my $dgn = { name => 'unknown dungeon', bottom => '???', splev => 0 }; $$dgn{name} = $1 if $blob =~ /^N:\d+:(.*)$/m; $$dgn{bottom} = $1 if $blob =~ /^W:\d+:(\d+):/m; $dgn_mon{$1} = $dgn if $blob =~ /\bFINAL_GUARDIAN_(\d+)\b/; $dgn_art{$1} = $dgn if $blob =~ /\bFINAL_ARTIFACT_(\d+)\b/; $dgn_obj{$1} = $dgn if $blob =~ /\bFINAL_OBJECT_(\d+)\b/; } # Read special level map files. We do this after getting guardian names # so that the special levels don't accidentally get a guardian. my %maps = Util::split_hash < $maps{$map}, splev => 1 }; $fh = new FileHandle "$TOME_LIB/$map" or die "Cannot open $TOME_LIB/${map}: $!\n"; while (<$fh>) { next if !/^F:/; $dgn_mon{$1} = $lev if /^F:\*?.:\d+:\d+:(\d+)/; $dgn_art{$1} = $lev if /^F:\*?.:\d+:\d+:\d+:\d+:\d+:(\d+)/; } } } sub init_stage2 () { return if $done_stage2; $done_stage2 = 1; # Get names of dungeon guardians for item display purposes. We don't do # this until we need to, since it's an extra read of r_info.txt. Don't # put guardian monsters on the special levels, or it will say that # artifacts on those levels are dropped by those monsters. my @mon = sort { $a <=> $b } grep { !$dgn_mon{$_}{splev} } keys %dgn_mon; my $fh = new FileHandle "$TOME_LIB/r_info.txt" or die "Cannot open $TOME_LIB/r_info.txt: $!\n"; while (my $blob = Util::read_blob $fh) { last if scalar @mon == 0; next if $blob !~ /^N:$mon[0]:(.*)\n/; $dgn_mon{$mon[0]}{guardian} = $1; shift @mon; } } sub init_stage2a () { return if $done_stage2a; $done_stage2a = 1; Item::init 'a'; # Get names of dungeon guardian drops for monster display purposes. We # don't do this until we need to, since it's an extra read of r_info.txt # and k_info.txt. Don't put guardian drops on the special levels, or # it will say that monsters generated on that level drop them. my @art = sort { $a <=> $b } grep { !$dgn_art{$_}{splev} } keys %dgn_art; my $fh = new FileHandle "$TOME_LIB/a_info.txt" or die "Cannot open $TOME_LIB/a_info.txt: $!\n"; my $blob; while ($blob = Util::read_blob $fh) { last if scalar @art == 0; next if $blob !~ /^N:$art[0]:.*\n/; $dgn_art{$art[0]}{grdn_drop} = Item::art_name $blob; shift @art; } my @obj = sort { $a <=> $b } grep { !$dgn_obj{$_}{splev} } keys %dgn_obj; $fh = new FileHandle "$TOME_LIB/k_info.txt" or die "Cannot open $TOME_LIB/k_info.txt: $!\n"; while ($blob = Util::read_blob $fh) { last if scalar @obj == 0; next if $blob !~ /^N:$obj[0]:.*\n/; $dgn_obj{$obj[0]}{grdn_drop} = Item::obj_name $blob; shift @obj; } } sub find ($$) { my ($type, $idx) = @_; return undef if !exists($dgns{$type}) || !exists($dgns{$type}{$idx}); return $dgns{$type}{$idx}; } sub describe ($$) { my ($dgn, $is_item) = @_; my $descr = 'Appears'; $descr = "Dropped by $$dgn{guardian}" if $is_item && exists $$dgn{guardian}; if ($$dgn{splev}) { $descr .= " in $$dgn{name}"; } else { $descr .= " on level $$dgn{bottom} of $$dgn{name}"; } return Util::wrap_line $descr; } package Player; use vars qw(@entries); @entries = (); my %pl_flags = (); my %skill_tree = (); my $class_tmpl; my %start_inven = (); sub numerically { return $a <=> $b } sub copy_tmpl ($); sub copy_tmpl ($) { my $T = $_[0]; return $T if !ref($T); return [ map { copy_tmpl $_ } @$T ] if ref $T eq 'ARRAY'; return { map { ($_ => copy_tmpl $$T{$_}) } keys %$T } if ref $T eq 'HASH' || ref $T eq 'Player'; die "Can't copy template element $T\n"; }; sub new ($$) { my ($class, $blob) = @_; return undef if $blob !~ /^[RSC]:/; local $_; my $self = { NAME => '', CATEGORY => '?', DESCR => [], STATS => [], BASEABILS => [], HD => 0, XPMULT => 0, HIST => 0, SKILLS => {}, GODS => [], FLAGS => [], GAINFLAG => {}, GAINABIL => {}, GAINLEV => [], PARTS => [], RACES => [], CLASSES => [], NOTCLASS => [], INVEN => [], POWER => '', ATTRCHAR => undef, }; $self = copy_tmpl $class_tmpl if $blob =~ /^C:a:N:/; my $gains; for (split "\n", $blob) { if (/^([RS]):N:\d+:(.*)/) { @$self{qw(CATEGORY NAME)} = ($1, $2); if ($$self{CATEGORY} eq 'S') { return undef if $$self{NAME} =~ /^\s*$/ || $$self{NAME} =~ /^x+$/; } next; } if (/^C:N:\d+:(.*)/) { @$self{qw(CATEGORY CLASSGRP)} = ('c', $1); next; } if (/^C:a:N:(.*)/) { @$self{qw(CATEGORY NAME)} = ('C', $1); return undef if $$self{NAME} =~ /\btest\b/i; next; } if (/^(?:R:D|S:D:[AB]|C:a:D):(.*\S)\s*$/) { push @{$$self{DESCR}}, $1; next; } next if /^C:D:/; if (/^[RSC]:S:((?:-?\d+:){6,8}-?\d+)$/) { my @stats = split ':', $1; $$self{BLOWS} = pop @stats if $$self{CATEGORY} eq 'c'; $$self{MANA} = pop @stats if $$self{CATEGORY} =~ /[Sc]/; $$self{MANA} -= 100 if $$self{CATEGORY} eq 'S'; $$self{LUCK} = pop @stats if $$self{CATEGORY} =~ /[RS]/; $$self{STATS} = \@stats; next; } if (/^(?:[RSC]:K|C:X):((?:-?\d+:){7}-?\d+)$/) { $$self{BASEABILS} = [ split ':', $1 ]; next; } if (/^[RSC]:P:(-?\d+):(\d+)(?::(\d+))?/) { @$self{qw(HD XPMULT)} = ($1, $2); $$self{INFRA} = $3 if defined $3; next; } if (/^[RS]:M:/) { next } if (/^[RSC]:E:((?:\d+:){5}\d+)$/) { $$self{PARTS} = [ split ':', $1 ]; next; } if (/^(?:R:C|S:C:A):(.*)$/) { $$self{CLASSES} = [ split /\s*\|\s*/, $1 ]; next; } if (/^C:(?:a:)?g:(.*)/) { push @{$$self{GODS}}, $1; next } if (/^S:C:F:(.*)$/) { $$self{NOTCLASS} = [ split /\s*\|\s*/, $1 ]; next } if (/^S:A:(.*)$/) { push @{$$self{RACES}}, split /\s*\|\s*/, $1; next } if (/^[RSC]:k:(.*)$/ || /^C:a:k:(.*)$/) { my ($dval, $dmult, $skill) = split ':', $1; if ($dval eq '=0' && $dmult eq '=0') { delete $$self{SKILLS}{$skill} if exists $$self{SKILLS}{$skill}; } else { $$self{SKILLS}{$skill} ||= [ 0, 0 ]; $$self{SKILLS}{$skill}[0] += $dval; if ($dmult =~ /\%(\d+)/) { $$self{SKILLS}{$skill}[1] = " x$1\%"; } else { $$self{SKILLS}{$skill}[1] += $dmult; } } next; } if (/^[RC]:Z:(.*)$/) { $$self{POWER} = $1; next } if (/^[RS]:g:(.):(.)$/) { next if $1 eq '*' && $2 eq '*'; my ($attr, $char) = ('default-colored', "'$1'"); $attr = $Attr::color{$2} if exists $Attr::color{$2}; $char = 'default symbol' if $char eq "'*'"; $$self{ATTRCHAR} = [ $attr, $char ]; next; } if (/^[RSC]:R:(\d+):(-?\d+)$/) { $gains = $$self{GAINFLAG}{$1} = [ $2 ]; next; } if (/^[RSC]:G:(.*)$/ || /^C:a:G:(.*)$/) { my $flags = $1; my @flags = split /\s*\|\s*/, $flags; push @{$$self{FLAGS}}, @flags; next; } if (/^[RSC]:F:(.*)$/) { die "*:F: before *:R: in entry '$$self{NAME}'\n" if !defined($gains); push @$gains, split /\s*\|\s*/, $1; next; } if (/^[RSC]:b:(\d+):(.*)$/ || /^C:a:b:(\d+):(.*)$/) { my ($lev, $abil) = ($1, $2); $$self{GAINABIL}{$lev} ||= []; push @{$$self{GAINABIL}{$lev}}, $abil; next; } if (/^[RSC]:O:(.*)/ || /^C:a:O:(.*)/) { my ($tval, $sval, $pval, $num) = split ':', $1; ($num, $pval) = ($pval, 0) if !defined($num); push @{$$self{INVEN}}, [ $tval, $sval, $num, $pval ]; next; } if (/^C:B:(\d+):(\d+):(\d+)$/) { next } if (/^C:C:([HL]):([HL]):(\d+):(\d+):(\d+)$/) { next } print STDERR "[??? '$_']\n"; } if (exists $$self{BLOWS} && $$self{CATEGORY} ne 'c') { for (my $i = 1; $i <= $$self{BLOWS}; $i++) { my $lev = $i*50/$$self{BLOWS}; $lev = 1 + int $lev if $lev > int $lev; if (!exists($$self{GAINFLAG}{$lev})) { $$self{GAINFLAG}{$lev} = [ 1, 'BLOWS' ]; } elsif ($$self{GAINFLAG}{$lev}[0] == 1) { push @{$$self{GAINFLAG}{$lev}}, 'BLOWS'; } else { die "eep ($lev)\n" } } } my %gl = map { ($_ => 1) } keys(%{$$self{GAINFLAG}}), keys(%{$$self{GAINABIL}}); $$self{GAINLEV} = [ sort numerically keys %gl ]; return bless $self, $class; } sub init () { return if scalar @entries > 0; Item::init 'k'; %pl_flags = Util::split_hash < [] ); $fh = new FileHandle "$TOME_LIB/s_info.txt" or die "Cannot open $TOME_LIB/s_info.txt: $!\n"; while (<$fh>) { chomp; next if !/^T:([^:]+):([^:]+)$/; my ($parent, $child) = ($1, $2); die "Dangling skill parent '$parent'\n" if !exists($skill_tree{$parent}); push @{$skill_tree{$parent}}, $child; $skill_tree{$child} = []; } # Read k_info.txt to get object names for starting inventory. for my $entry (@entries) { for (@{$$entry{INVEN}}) { $start_inven{"$$_[0]:$$_[1]"} = '' } } # Annoying hack for ToME 2.2.x Mimics. $start_inven{'35:102'} = [ 'A Mouse Fur', 'Mouse Furs' ]; $fh = new FileHandle "$TOME_LIB/k_info.txt" or die "Cannot open $TOME_LIB/k_info.txt: $!\n"; while (my $blob = Util::read_blob $fh) { next if $blob !~ /^I:(\d+:\d+):/m; my $tsval = $1; next if !exists($start_inven{$1}) || $start_inven{$1}; my $o_name = Item::obj_name($blob); my $o_names = Item::obj_name_plural($blob); $start_inven{$tsval} = [ $o_name, $o_names ]; } for my $entry (@entries) { for my $inv (@{$$entry{INVEN}}) { my $tsval = "$$inv[0]:$$inv[1]"; # die "What is a $tsval?\n" if !$start_inven{$tsval}; ($inv = $tsval), next if !$start_inven{$tsval}; if ($$inv[2] eq '1d1') { $inv = $start_inven{$tsval}[0]; } else { $$inv[2] =~ s/d1$//; $inv = "$$inv[2] $start_inven{$tsval}[1]"; } } } } sub matches ($$) { my ($entry, $key) = @_; return 1 if $$entry{NAME} =~ /$key/i; if ($key =~ /^flags:(.*)$/) { my @flags = split ',', $1; my @neg = map { s/^!// || 0 } @flags; my $flags = join ',', @{$$entry{FLAGS}}, map { @$_ } values %{$$entry{GAINFLAG}}; for my $i (0..$#flags) { return 0 if ( $neg[$i] && $flags =~ /\b$flags[$i]\b/) || (!$neg[$i] && $flags !~ /\b$flags[$i]\b/) } return 1; } return 0; } sub fmt_skill_tree ($;$); sub fmt_skill_tree ($;$) { my ($skills, $parent) = @_; $parent ||= 'Main'; my $ret = ''; die "Skill $parent not in skill tree\n" if !exists($skill_tree{$parent}); for my $skill (@{$skill_tree{$parent}}) { if (exists $$skills{$skill}) { my ($lev, $mult) = @{$$skills{$skill}}; $lev = sprintf '%+5.3f', $lev/1000; $mult = sprintf '%+5.3f', $mult/1000 unless $mult =~ /\%$/; $ret .= sprintf " \%-20s $lev [\%s]\n", $skill, $mult; } $ret .= fmt_skill_tree $skills, $skill; } return $ret; } sub description ($) { my $self = shift; my $cat = $$self{CATEGORY}; my @stats = qw(STR INT WIS DEX CON CHR); local $_ = $$self{NAME}; my $xp = Util::add_plus($$self{XPMULT} - ($cat eq 'R' ? 100 : 0)); my $hd = ($cat eq 'R' ? "Base hit die 1d$$self{HD}" : Util::add_plus($$self{HD}) . ' hit die'); my $infra = ''; if (exists $$self{INFRA}) { if ($cat eq 'S') { $infra = 'infravision ' . Util::add_plus($$self{INFRA}*10) . ' feet'; } elsif ($$self{INFRA}) { $infra = "infravision $$self{INFRA}0 feet"; } else { $infra = 'no infravision'; } } $_ .= " (subclass of $$self{CLASSGRP})" if exists $$self{CLASSGRP}; $_ .= " (@{$$self{ATTRCHAR}})" if defined $$self{ATTRCHAR}; $_ .= "\n$hd, $xp\% exp"; $_ .= ", $infra" if $infra; $_ .= "\n"; $_ .= join ', ', map { Util::add_plus($$self{STATS}[$_]) . " $stats[$_]" } 0..$#stats; $_ .= ', ' . Util::add_plus($$self{LUCK}) . ' luck' if exists $$self{LUCK}; $_ .= ', ' . Util::add_plus($$self{MANA}) . '% mana' if exists $$self{MANA}; $_ .= "\n"; $_ .= Util::comma_list '', [ map { $pl_flags{$_} || $_ } @{$$self{FLAGS}} ]; if (scalar keys %{$$self{SKILLS}} > 0) { my $len = 0; for my $skill (keys %{$$self{SKILLS}}) { $len = length $skill if $len < length $skill; } $_ .= "Skill modifiers:\n"; $_ .= fmt_skill_tree $$self{SKILLS}; } for my $lev (@{$$self{GAINLEV}}) { my @parts = (); push @parts, @{$$self{GAINABIL}{$lev}} if exists $$self{GAINABIL}{$lev}; if (exists $$self{GAINFLAG}{$lev}) { my @z = @{$$self{GAINFLAG}{$lev}}; my $pval = shift @z; push @parts, map { Item::describe_flag($_, $pval) } @z; } $_ .= Util::comma_list 'Gains', \@parts, 0, "at level $lev"; } $_ .= Util::comma_list 'Starts with', $$self{INVEN}; my $A = ($cat eq 'S' ? 'Additional a' : 'A'); $_ .= Util::comma_list "${A}llowed class groups: ", $$self{CLASSES}; $_ .= Util::comma_list 'Forbidden class groups: ', $$self{NOTCLASS}; $_ .= Util::comma_list 'Can worship', $$self{GODS}; my $rc = ($cat eq 'R' ? 'Race' : 'Class'); $_ .= "$rc power '$$self{POWER}'\n" if $$self{POWER}; $_ .= Util::comma_list 'Allowed for races', $$self{RACES}; $_ .= Util::comma_list 'Description: ', $$self{DESCR}, 1 if $main::show_desc; if ($main::poss_info && $cat =~ /[Rc]/) { my @parts = Monster::body_parts $$self{PARTS}, ($cat eq 'c'); $_ .= Util::comma_list 'Can wear/wield', \@parts; } chomp $_; return $_; } package Attr; use vars qw(%color %attr); sub init () { return if scalar keys %color > 0; %color = Util::split_hash <) { return $lines if /^$/ && $lines; $lines .= $_ if !(/^$/ || /^[#V]/); } return $lines; } sub ynq ($) { my $prompt = $_[0]; my $ans; print "$prompt? [y/N/q] "; $ans = ; return ($ans =~ /^[Qq]/ ? undef : $ans =~ /^[Yy]/ ? 1 : 0); } sub split_hash ($) { return ($_[0] =~ /^\s*([^,]*), (.*)$/mg); } sub add_plus (@) { return ($_[0] < 0 ? $_[0] : "+$_[0]") if !wantarray; return map { $_ < 0 ? $_ : "+$_" } @_; } sub add_plus2 (@) { return ($_[0] < 0 ? $_[0] : $_[0] <= 1 ? "+$_[0]" : "+1-$_[0]") if !wantarray; return map { $_ < 0 ? $_ : $_ <= 1 ? "+$_" : "+1-$_" } @_; } 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 push ($$) { my ($listref, $entry) = @_; if (scalar @$listref > 0 && ref $$listref[-1]) { CORE::push @{$$listref[-1]}, $entry; } else { CORE::push @$listref, $entry } } sub prob_group (@) { return map { ! ref $_ ? $_ : scalar @$_ < 2 ? () : $$_[0] == 100 ? @$_[1..$#$_] : "($$_[0]\%: " . join(', ', @$_[1..$#$_]) . ')' } @_; } sub comma_list ($$;$$) { my ($first, $listref, $no_comma, $suffix) = @_; return '' if ref $listref ne 'ARRAY'; my @list = prob_group @$listref; return '' if scalar @list == 0; my $join = ($no_comma ? ' ' : ', '); my $line = join $join, @list; $line = "$first $line" if $first; $line = "$line $suffix" if $suffix; return Util::wrap_line ucfirst $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"; }