#!/usr/bin/env perl # Simple perl script to update monster attacks. The damage should be split # into a 'physical' part and a secondary effect part, to which one could have # resistance. According to Timo on oook: # # "So "hit to burn" always does basic damage, if you don't have fire resistance # that is multiplied by three, if you have single resistance you get dam + # 2*dam/3, if double resistance dam + 2*dam/9, if immunity only dam. You can # extend the resistances to high-elements as well, just always make that part # of dam that gets reduced 2*dam and add the base dam to that." # # Can be called with "path/to/lib/edit/monster.txt", otherwise will default to # ./lib/edit/monster.txt # # Gorbad - 7th Nov, 2011 use strict; use warnings; # {{{ DAMAGE_TYPES # all HIT: attacks possible, the ones that shouldn't change (as they are not # resisted, so no extra damage in mon-power.c or mon-melee2.c) are # commented out. # Some attacks *might* be resisted, so I added some extra comments to them. # If it does count as resistable, uncomment, and add extra damage for that # flag. my %DAMAGE_TYPES = ( #'HURT' => 500, #'EAT_GOLD' => 2, # one can make a case that DEX 18/150+ counts as resist? #'SHATTER' => 6, 'ELEC' => 11, # RES_ELEC 'COLD' => 2, # RES_COLD 'EXP_40' => 2, # HOLD_LIFE 'BLIND' => 3, # RES_BLIND #'LOSE_STR' => 15, # SUST_STR #'LOSE_CHR' => 2, # SUST_CHR #'EAT_ITEM' => 1, # See EAT_GOLD 'POISON' => 15, # RES_POIS #'UN_POWER' => 6, 'EXP_80' => 7, # HOLD_LIFE 'PARALYZE' => 8, # FREE_ACTION 'CONFUSE' => 22, # RES_CONF #'LOSE_ALL' => 1, # All sustains needed? #'HALLU' => 1, # RES_CHAOS? #'LOSE_CON' => 1, # SUST_CON #'LOSE_DEX' => 1, # SUST_DEX 'FIRE' => 29, # RES_FIRE 'UN_BONUS' => 23 # RES_DISEN ); # }}} # {{{ ATTACK_TYPES # For each attack type, different handling might be required, for example # TOUCH should not provide _any_ damage besides the secondary effect. # It's a multiplier, so 1 keeps the original dmg dice, and 0 means no # dmg effect if you resist the secondary (the actual damage taken is # then determined purely by mon-power my %ATTACK_TYPES = ( 'SPIT' => 0, 'CRUSH' => 0.3, 'CRAWL' => 0, 'GAZE' => 0, 'CLAW' => 0.3, 'KICK' => 0.3, 'ENGULF' => 0.2, 'STING' => 0.3, 'TOUCH' => 0, 'WAIL' => 0, 'SPORE' => 0.1, 'BUTT' => 0.3, 'BITE' => 0.3, 'HIT' => 0.3, ); # }}} # {{{ Initialize some things my $monster_txt_file = $ARGV[0] || './lib/edit/monster.txt'; open my $monster_data, '<', $monster_txt_file or die "Cannot open ${monster_txt_file}: $!\n"; my %all_dice_combos = _get_ordered_averages(); # Add special 0d0 case for no damage $all_dice_combos{0} = '0d0'; # }}} # {{{ main my ($monster, $printed); while ( my $line = <$monster_data> ) { if ( $line =~ /^N:\d+:(.*)$/ ) { # Which monster are we dealing with? $monster = $1; $printed = 0; } elsif ( my ( $attack, $type, $damage ) = $line =~ /^B:(\w+):(\w+):(.*)/ ) { next unless exists $DAMAGE_TYPES{$type}; # Only print the monster name once if ( ! $printed && defined $monster) { print "$monster:\n"; $printed = 1; } # Find old average from dice, and recalculate according to the # ATTACK_TYPES multiplier my ( $dice, $sides ) = $damage =~ /^(\d)+d(\d+)$/; my $average = ( $sides * $dice + $dice ) / 2; my $new_damage = $average * $ATTACK_TYPES{$attack}; # Find which dice match the average my $nearest_average = _find_matching_dice($new_damage); # Uncomment to see more detailed proof # print "$attack:$type:$damage Avg: $average New Avg: $new_damage = $all_dice_combos{$nearest_average}\n"; print "B:$attack:$type:$all_dice_combos{$nearest_average}\n"; } } # }}} # {{{ Helper functions sub _get_ordered_averages { # Helper function to get a list of averages to compare with. # I'm not a math wiz, so there might be an algorithm to find # matching dice for a given average, but I don't know it. # Use a hash to discard the lower dice types in case of duplicates. my %averages; # Let's assume we only use 'natural' dice # In monsters.txt, the max dice type and max dice is 20 foreach my $sides (qw{ 1 2 4 6 8 10 12 20 }) { foreach my $dice (qw{ 1 2 3 4 5 6 7 8 9 10 11 12 20 }) { $averages{ ( $sides * $dice + $dice ) / 2 } = sprintf '%dd%d', $dice, $sides; } } return %averages; } sub _find_matching_dice { # Binary trees are faster, but we don't have too much # data anyway, so this is fine. my ($required_average) = @_; # Shortcut values of 1 and lower return 0 if $required_average == 0; return 1 if $required_average <= 1; # Work from high to low, to pick the dice just above the current max. # The drive is to make Angband more difficult, certainly not less. my $best_match = 1000; foreach my $average ( keys %all_dice_combos ) { $best_match = $average if $required_average <= $average and $average < $best_match; } return $best_match; } # }}} __END__