<(common.inc.plp)><: use List::Util qw(max sum); my %scver = ( id => 'bw', name => 'Brood War', title => 'starcraft', game => 'StarCraft', major => 1, ); if ($ENV{PATH_INFO} and $ENV{PATH_INFO} eq '/2') { %scver = ( id => 'hots', name => 'Heart of the Swarm', title => 'starcraft2', game => 'StarCraft II', major => 2, ); } my $datafile = "sc-units-$scver{id}.inc.pl"; Html({ title => "$scver{title} unit cheat sheet", version => 'v1.1', description => [ "Reference of $scver{game} unit properties," . " comparing various statistics of all the units in $scver{name}" . ' including costs, damage, defense, speed, ranges, and abilities.', ], keywords => [ qw' starcraft game unit statistics stats comparison table sheet cheat reference software attributes properties ', $scver{major} < 2 ? qw' bw broodwar brood war ' : qw' starcraft2 hots ', ], stylesheet => [qw'light'], raw => '', data => [$datafile], }); print "

$scver{game} units

\n\n"; my $units = do $datafile; die "Cannot open unit data: $_\n" for $@ || $! || (); my $patch = shift @{$units} or die "Cannot open unit data: metadata not found\n"; print "

Unit properties as seen or measured in $scver{name}\n$patch.\n"; print "Also see the $_ table.\n" for join(', ', ('StarCraft 2: HotS') x ($scver{major} < 2), ('original SC: Brood War') x ($scver{major} > 1), ); print "

\n\n"; sub addupgrade { my ($ref, $increase, $org) = @_; if (ref $increase eq 'HASH') { addupgrade(\${$ref}->{$_}, $increase->{$_}, $org->{$_}) for keys %{$increase}; } elsif (ref $increase eq 'ARRAY') { addupgrade(\${$ref}->[$_], $increase->[$_], $org->[$_]) for 0 .. $#{$increase}; } ${$ref} //= $org; ${$ref} += $increase if $increase =~ /^-?[0-9.]+/; } for my $unit (@{$units}) { for my $upgrade (@{ $unit->{upgrade} }) { while (my ($col, $increase) = each %{$upgrade}) { defined $unit->{$col} or next; addupgrade(\$unit->{upgraded}->{$col}, $increase, $unit->{$col}); } } for my $special (@{ $unit->{special} }) { for my $upgrade (@{ $special->{upgrade} }) { while (my ($col, $increase) = each %{$upgrade}) { defined $special->{$col} or next; addupgrade(\$special->{upgraded}->{$col}, $increase, $special->{$col}); } } } } sub coltoggle { my ($name, $id, $nolink) = @_; return sprintf( (defined $get{order} ? $get{order} eq $id : !$id) ? '%2$s ▼' : $nolink ? '%2$s' : '%s', $id && "order=$id", $name ); } :> <: sub showrange { my ($min, $max) = @_; return '' if not defined $min; return $min || '-' if !$max or $min == $max; return "$min-$max"; } sub showrangeint { $_ &&= int($_ + .5) for @_; # round halves up return showrange(@_); } sub showcost { my ($row, $unit) = @_; return join(' ', sprintf('cost %s%%', join '-', map { $_ && sprintf '%.0f', 100 * $row->{cost} / $_ } grep { defined $_ } $unit->{energy}, $unit->{upgraded}->{energy}, $unit->{capacity}, $unit->{upgraded}->{capacity}, ), !defined $row->{maint} ? () : sprintf('+%s%%/s', join '-', map { sprintf '%.1f', 100 * $row->{maint} / $_ } grep $_, $unit->{capacity}, $unit->{upgraded}->{capacity}, ), ); } sub showattack { my ($row, $area) = @_; my $attack = $row->{attack}->[$area] or return '', '
<:= coltoggle('name', '') :> cost gas <:= coltoggle(qw'build cost') :> <:= coltoggle(qw'size size') :> attr HP shield attack <:= coltoggle(qw'dps attack 1') :> range sight speed specials
'; my $upattack = $row->{upgraded}->{attack}->[$area]; my $damage = $attack->{damage}; my $maxdamage = $upattack->{damage} // $damage; $maxdamage += ($upattack->{upgrade} // $attack->{upgrade}) * 3; my $out = ''; $out .= sprintf '¤ ', showcost($attack, $row) if $attack->{cost}; $out .= sprintf('%s× ', showrangeint($attack->{count}, $upattack->{count}), ) if $attack->{count} > 1; $out .= '*' if $attack->{type} eq 'explosive'; $out .= '~' if $attack->{type} eq 'implosive'; if (my @bonus = sort grep { !/^-/ } keys %{ $attack->{bonus} }) { $out .= sprintf('', (map { $_ eq 'light' ? 'unit-s' : $_ eq 'armored' ? 'unit-l' : $_ eq 'organic' ? 'unit-o' : $_ eq 'massive' ? 'unit-h' : $_ eq 'shields' ? 'unit-shield' : '', } join '_', @bonus), join(', ', map {( sprintf('+%s vs %s', showrangeint( $attack->{bonus}->{$_}, $attack->{bonus}->{$_} + $attack->{bonus}->{"-$_"} * 3, ), $_, ), )} @bonus), ); } $out .= '' if $attack->{type} eq 'projectile'; $out .= sprintf '', $attack->{name} if $attack->{name}; $out .= showrangeint($damage, $maxdamage); $out .= '' if $attack->{name}; $out .= sprintf('%s', $attack->{splash} eq 'line' ? ('linear', '+') : ('splash', '⁜') ) if $attack->{splash}; $out .= ''; if ($attack->{dps}) { # precalculated dps, do not touch $out .= showrangeint($attack->{dps}->[0], $upattack->{dps}->[-1] // $attack->{dps}->[-1] ); } elsif ($attack->{cooldown}) { if (my $type = $attack->{type}) { if ($type eq 'explosive') { $damage /= 2; } elsif ($type eq 'implosive') { $damage /= 4; } } $damage *= ($attack->{count} // 1) / $attack->{cooldown}; if (my $bonus = $upattack->{bonus} // $attack->{bonus}) { $maxdamage += $_ for max( map { $bonus->{$_} + $bonus->{"-$_"} * 3 } grep { !/^-/ } keys %{$bonus} ); } $maxdamage *= ($upattack->{count} // $attack->{count} // 1) / ($upattack->{cooldown} // $attack->{cooldown}); $out .= showrangeint($damage, $maxdamage); } $out .= '' . '▽' x !!($attack->{anti} & 1); $out .= '' . '△' x !!($attack->{anti} & 2); $out .= '' . showrangeint($attack->{range}, $upattack->{range}); return $out; } sub showmagic { my ($row) = @_; my $specials = $row->{special} or return ''; return join ' ', map { sprintf '%s', $_->{duration} < 0 && ' class="magic-perma"', join('', $_->{name}, $_->{desc} ? ": $_->{desc}" : '', (map { $_ && " ($_)" } join ', ', #TODO: apply upgrades $_->{range} ? "range $_->{range}" : (), $_->{cost} ? showcost($_, $row) : $_->{cooldown} ? "cooldown $_->{cooldown}s" : (), ), ), sprintf($_->{build} ? '(%s)' : '%s', $_->{abbr}), } grep { defined $_->{abbr} } @{$specials}; } sub showunitcols { my ($row) = @_; local $_ = $row; $_->{hp} += $_->{shield} if $_->{shield}; return ( '' . ($_->{min} // ''), '' . ($_->{gas} || ''), !defined $_->{build} ? '' : sprintf('%s%.0f', !!$_->{base} && '+', $_->{build} || '0', ), sprintf('%s', $_-> {cargo} < 0 ? ('supply', T => 'transport') : $_->{upgraded}->{cargo} < 0 ? ('supply magic-opt', T => 'optional transport') : $_->{attr}->{flying} ? ('air', F => 'flying') : $_->{attr}->{structure} ? ('x', B => 'building') : ( [qw( x s m l l h h h h )]->[ $_->{cargo} ], $_->{cargo} || '-', $_->{cargo} ? 'transportable' : 'untransportable', ), defined $_->{size} && sprintf('⌀%.1f ', $_->{size}), ), sprintf('%s', defined $_->{pop} && $_->{pop} < 0 && ' unit-supply', defined $_->{pop} && $_->{pop} == .5 ? '½' : $_->{pop}, ), '' . join('', grep { $_ } (defined $_->{organic} ? !$_->{organic} : $_->{attr}->{mech}) && 'm', ($_->{organic} || $_->{attr}->{organic}) && 'o', $_->{attr}->{psionic} && 'ψ', ), '' . join('', grep { $_ } $_->{attr}->{armored} && 'A', $_->{attr}->{light} && 'L', $_->{suit} && sprintf( '%s', map { @{$_} } [ [qw( x ? unknown )], [qw( s S small )], [qw( m M medium )], [qw( l L large )], ]->[ $_->{suit} ], ), $_->{attr}->{massive} && '', ), '' . $_->{hp} // '', $_->{shield} ? sprintf('%.0f%%{shield} / $_->{hp} ) : '' . showrangeint($_->{armor}, $_->{upgraded}->{armor}), showattack($_, 0), '' . sprintf( $_->{detect} ? '%s' : '%s', showrangeint($_->{sight}, $_->{upgraded}->{sight}) ), sprintf('%s', showrange( map { $_ && sprintf '%.1f', $_ } $_->{speed}, $_->{upgraded}->{speed} ), defined $_->{creep} && sprintf(' title="%s on creep"', $_->{creep} == 1 ? 'same' : showrange( map { $_ && sprintf '%.1f', $_ } $_->{speed} * $_->{creep}, $_->{upgraded}->{speed} && $_->{upgraded}->{speed} * ($_->{upgraded}->{creep} // $_->{creep}), ), ), ), $_->{attr}->{jump} && qq'', '' . showmagic($_), !$_->{attack}->[1] ? () : ( '
', showattack($_, 1), '' ), !$_->{attack}->[2] ? () : ( '
', showattack($_, 2), '' ), "\n" ); } my $grouped = 1; # race headers if (exists $get{order}) { $grouped = 0; $get{order} ||= ''; if ($get{order} eq 'size') { $_->{order} = ( $_->{pop}*16 + ($_->{size} // $_->{suit}) + $_->{cargo}/8 + $_->{hp}/512 + $_->{min}/8192 ) for @$units; } elsif ($get{order} eq 'cost') { $_->{order} = ( $_->{gas}*1.5 + $_->{min} + $_->{pop}/8 + $_->{build}/256/8 ) for @$units; } elsif ($get{order} eq 'attack') { $_->{order} = $_->{hp} / 1024 + $_->{shield} / 1008 + max( map { ($_->{damage} + $_->{upgrade} * 3) * ($_->{count} // 1) / ($_->{cooldown} // 1) * ($_->{splash} ? 1.01 : 1) * ($_->{type} eq 'implosive' ? .96 : 1) * ($_->{type} eq 'explosive' ? .98 : 1) } @{ $_->{attack} } ) for @$units; } else { $units->[$_]->{order} = $_ for 0 .. $#$units; } } my @rows = @{$units}; @rows = sort {$a->{order} <=> $b->{order}} @rows unless $grouped; my ($race, $cat) = ('', ''); for (@rows) { if ($grouped) { printf '

%s

'."\n", $race = $_->{race}, ucfirst $race unless $race eq $_->{race}; } else { $_->{cat} = $_->{race}; } print( '
', $cat ne $_->{cat} && ($cat = $_->{cat}), '', $_->{name}, showunitcols($_), ); for my $subrow (@{ $_->{special} }) { $subrow->{alt} or next; print( '
', $subrow->{alt}, showunitcols($subrow), ); } } :>

Legend

cost
minerals and gas required to create one unit
includes total expenses if based on existing units
build
relative time needed to create at least one unit
excludes construction of dependencies such as buildings and +parent units
size
Transports can fit upto 8 non-Flying cargo units
number of command points taken while alive
<: if ($scver{major} > 1) { :>received damage depends on organic, mechanic, ψ(ps)ionic, Light, and Armored attributes
massive units cannot be lifted or slowed and can break force fields<: } else { :>abilities may hit only organic or mechanic targets
affected by Small, Medium, or Large damage<: } :>
HP
total number of hitpoints (including shields)
shield
percentage of HP in shields
shields always take full damage, irrelevant of unit size
does not take armor bonuses, but upgrades can decrease damage to any shield hit by upto 3
armor
base unit armor
can be increased by upto 3 at various facilities
each point decreases damage per hit by one, upto a minimum of ½
reduction applies to initial damage, before size penalties (so a plasma hit of 12 to 4 armor large deals 2 damage, not ½)
attack
damage per single hit
some weapons fire multiple × times, multiplying armor penalties
splash damage hits all objects nearby or in a straight line +.
<: if ($scver{major} > 1) { :>does not include bonus damage dealt to susceptible unit types
projectile shots are negated by Point Defense Drones<: } else { :>*explosive damage does only 50% damage to Small units, 75% to Medium, 100% to Large
~concussive/plasma damage does 25% to Large, 50% Medium, 100% to Small units<: } :>
dps indicates relative total amount of damage done in 1 second of <:= $scver{major} > 1 ? 'Normal in-game time' : 'time on Fast game speed' :>
targets  ground and/or  air
range
maximum hex distance a weapon can fire (note Sieged Tank also has a minimum)
sight
range in which the unit detects other units
emphasis indicates ability to detect cloaked units
speed
top movement speed in hex per second
acceleration and deceleration ignored
specials
parentheses () indicate that it needs to be researched first
passive abilities are always enabled
hover for description
range is maximum distance allowed to activate
cost describes energy loss percentage on spawn and when fully charged

When two values are given (1-2), the second value indicates the attribute after all possible upgrades.