X-Git-Url: http://git.shiar.nl/sheet.git/blobdiff_plain/e487749bf1899655bb0c7b7fe9c2d4cbff3b8021..HEAD:/sc.plp diff --git a/sc.plp b/sc.plp index cf2155f..e784cbd 100644 --- a/sc.plp +++ b/sc.plp @@ -1,27 +1,44 @@ <(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', +my %scvers = ( + bw => { + name => 'Brood War', + title => 'starcraft', + game => 'StarCraft BW', + major => 1, + }, + hots => { name => 'Heart of the Swarm', - title => 'starcraft2', - game => 'StarCraft II', + title => 'starcraft2 hots', + game => 'StarCraft II HotS', + major => 2, + }, + lotv => { + name => 'Legacy of the Void', + title => 'starcraft2 lotv', + game => 'StarCraft II LotV', major => 2, - ); + }, + index => 'bw', + 1 => 'bw', + 2 => 'lotv', +); + +my $requestver = $scvers{$Request ||= 'index'} + or Html(), Abort("Requested version $Request not available", '404 request not found'); + +if (ref $requestver ne 'HASH') { + $header{Location} = "/sc/$requestver"; + Abort("Canonical URL for $Request is at $requestver", '302 subpage alias'); } -my $datafile = "sc-units-$scver{id}.inc.pl"; + +my %scver = %{$requestver}; +my $datafile = "sc-units-$Request"; Html({ title => "$scver{title} unit cheat sheet", - version => 'v1.1', + version => '1.4', description => [ "Reference of $scver{game} unit properties," . " comparing various statistics of all the units in $scver{name}" @@ -30,127 +47,203 @@ Html({ keywords => [ qw' starcraft game unit statistics stats comparison table sheet cheat - reference software attributes properties + reference software attributes properties patch attribute multiplayer ', - $scver{major} < 2 ? qw' bw broodwar brood war ' : qw' starcraft2 hots ', + $scver{major} < 2 ? qw' bw broodwar brood war ' : + qw' starcraft2 lotv hots wol ', ], - stylesheet => [qw'light'], - raw => '', - data => [$datafile], + stylesheet => [qw( light dark )], + raw => '', + data => ["$datafile.inc.pl"], }); -print "

$scver{game} units

\n\n"; +say "

$scver{game} units

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

Unit properties as seen or measured in $scver{name}\n$patch.\n

\n\n"; +say "

Unit properties as seen or measured in $scver{name}\n$patch."; +say "Also see the $_ tables." for join(' and ', + (showlink('StarCraft 2: LotV', '/sc/lotv')) x ($Request ne 'lotv'), + (showlink( 'HotS', '/sc/hots')) x ($Request ne 'hots'), + (showlink('original SC: Brood War', '/sc/bw')) x ($Request ne 'bw'), +); +say "

\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) = @_; - return sprintf( - (defined $get{order} ? $get{order} eq $id : !$id) ? '%2$s ▼' - : '%s', - $id && "order=$id", $name - ); + my ($name, $id, $nolink) = @_; + return "$name ▼" if defined $get{order} ? $get{order} eq $id : !$id; + return $name if $nolink; + return showlink($name, '?'.($id && "order=$id")); } :> - - - - - + + + + + - + + - - - - + + + + - + <: sub showrange { my ($min, $max) = @_; - $_ = int($_ + .5) for $min, $max; # round halves up - return $min == $max ? $min : "$min-$max"; + return '' if not defined $min; + return $min || '-' if !$max or $min == $max; + return "$min-$max"; } -sub showval { - my ($row, @elements) = @_; - my ($min, $max); +sub showrangeint { + $_ &&= int($_ + .5) for @_; # round halves up + return showrange(@_); +} - my $value = $row; - $value = ref $value eq 'HASH' && $value->{$_} or last for @elements; - if (ref $value eq 'ARRAY') { - $min = $value->[0]; - $max = $value->[-1]; - } - else { - $min = $max = $value; - } - defined $min or return ''; - - if ($row->{upgrade}) { - for (@{ $row->{upgrade} }) { - my $increase = $_ or next; - $increase = ref $increase eq 'HASH' && $increase->{$_} or last for @elements; - $increase = $increase->[-1] if ref $increase eq 'ARRAY'; - $max += $increase if $increase; - } + 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}, + ), + ); } - if ($elements[0] eq 'attack' and $elements[1] ne 'range' and $elements[2] eq 'dps') {{ - my $attack = $row->{ $elements[0] }->{ $elements[1] }; - ref $attack or $attack = $row->{ $elements[0] }->{$attack}; # follow - my $type = $attack->{type} or next; - if ($type eq 'explosive') { - $min /= 2; - } - elsif ($type eq 'implosive') { - $min /= 4; - } - }} - return showrange($min, $max); -} - sub showattack { my ($row, $area) = @_; - my $attack = $row->{attack}->{$area}; - if (not ref $attack) { - # reference to another area - $area = $attack; - $attack = $row->{attack}->{$area}; - } + my $attack = $row->{attack}->[$area] + or return '
<:= coltoggle('name', '') :>mingas<:= coltoggle(qw'build cost') :><:= coltoggle(exists $get{order} ? 'race' : 'source' => '') :><:= coltoggle(name => 'name') :><:= coltoggle(cost => 'cost') :>gas<:= coltoggle(build => 'build') :> <:= coltoggle(qw'size size') :>HPattr<:= coltoggle(HP => 'hp') :> shieldarmorgroundairrange⛨<:= coltoggle(attack => 'attack') :>dpsrange sightspeed<:= coltoggle(speed => 'speed') :> specials
'; - return '' unless $attack; + my $upattack = $row->{upgraded}->{attack}->[$area]; + my $damage = $attack->{damage}; + my $maxdamage = $upattack->{damage} // $damage; + $maxdamage += ($upattack->{upgrade} // $attack->{upgrade}) * 3; - my $tagbase = ''; + $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('', + ( + $_ eq 'light' ? 'unit-s' : + $_ eq 'armored' ? 'unit-l' : + $_ eq 'organic' ? 'unit-o' : + $_ eq 'massive' ? 'unit-h' : + $_ eq 'shields' ? 'unit-shield' : + $_ eq 'structure' ? 'unit-x' : + '', + ), + ( + sprintf('+%s vs %s', + showrangeint( + $attack->{bonus}->{$_}, + ($upattack->{bonus} // $attack->{bonus})->{$_} + + ($upattack->{bonus} // $attack->{bonus})->{"-$_"} * 3, + ), + $_, + ), + ), + ) for @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; + } } - elsif ($attack->{type} eq 'implosive') { - $tagbase .= ' unit-s'; + $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); } - $tagbase .= '">'; - - my $out = showval($row, 'attack', $area, 'damage'); - $out .= '+' if $attack->{splash}; - $attack->{dps} = $attack->{cooldown} && [ - map { 24 * $_ / $attack->{cooldown} * ($attack->{count} // 1) } - map { ref $_ ? @{$_} : $_ } - $attack->{damage} - #TODO: upgrade (zergling) - ]; - $out .= '' . showval($row, 'attack', $area, 'dps'); - return $tagbase . $out; + + $out .= '' . '▽' x !!($attack->{anti} & 1); + $out .= '' . '△' x !!($attack->{anti} & 2); + + $out .= '' . + showrangeint($attack->{range}, $upattack->{range}); + + return $out; } sub showmagic { @@ -158,16 +251,19 @@ sub showval { my $specials = $row->{special} or return ''; return join ' ', map { sprintf '%s', - $_->{duration} < 0 && ' class="magic-perma"', join('', - $_->{name}, + $_->{duration} < 0 && ' class="magic-perma"', + $_->{detect} && ' class="unit-detect"', + ), + join('', + $_->{name} // $_->{alt}, $_->{desc} ? ": $_->{desc}" : '', - $_->{range} || $_->{cost} ? sprintf(' (%s)', join ', ', + (map { $_ && " ($_)" } join ', ', + #TODO: apply upgrades $_->{range} ? "range $_->{range}" : (), - $_->{cost} ? sprintf('cost %.0f%%', - 100 * $_->{cost} / $row->{energy} - ) : (), - ) : '', + $_->{cost} ? showcost($_, $row) : + $_->{cooldown} ? "cooldown $_->{cooldown}s" : (), + ), ), sprintf($_->{build} ? '(%s)' : '%s', $_->{abbr}), } grep { defined $_->{abbr} } @{$specials}; @@ -177,73 +273,160 @@ sub showval { my ($row) = @_; local $_ = $row; $_->{hp} += $_->{shield} if $_->{shield}; - my $suitchar = ''; - if ($_->{cargo} > 0) { - $suitchar = [qw/? s m l l h h h h/]->[abs $_->{cargo}]; - } - elsif ($_->{size}) { - $suitchar = [qw/s m l h h h/]->[$_->{size}]; - } - elsif ($_->{suit}) { - $suitchar = [qw/? s m l/]->[$_->{suit}]; - } return ( - '' . ($_->{min} // ''), - '' . ($_->{gas} || ''), - !defined $_->{build} ? '' : sprintf('%s%.0f', - !!$_->{base} && '+', - $_->{build} || '0', + '' . ($_->{min} // ''), + '' . ($_->{gas} || ''), + defined $_->{transform} ? sprintf('%.0f', + $_->{transform}, + ) : + !defined $_->{build} ? '' : sprintf('%s%.0f', + defined $_->{warp} && sprintf(' title="%.0f without warpgate"', $_->{build}), + !!$_->{base} && sprintf( + '+', + 'from '.join('+', @{ $_->{base} }), + ), + $_->{warp} // $_->{build} || '0', ), - !$suitchar ? '' : sprintf('%s', $suitchar, ucfirst $suitchar), - '' . join('', - defined $_->{unit} && $_->{unit} == .5 ? '½' : $_->{unit}, - defined $_->{organic} && sprintf( - '%s', - $_->{organic} ? 'o' : 'u', - $_->{organic} ? 'organic' : 'mechanic', - $_->{organic} ? 'o' : 'm', + sprintf('%s', + $_-> {cargo} < 0 ? ('supply', T => 'transport') : + $_->{upgraded}->{cargo} < 0 ? ('supply magic-opt', T => 'optional transport') : + $_-> {attr}->{flying} ? ('air', F => 'flying') : + $_->{upgraded}->{attr}->{flying} ? ('air magic-opt', F => 'potentially 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}), ), - '' . $_->{hp} // '', - '' . ( - $_->{shield} ? sprintf('%.0f%%', 100 * $_->{shield} / $_->{hp}) : ' ' + sprintf('%s', + defined $_->{pop} && $_->{pop} < 0 && ' unit-supply', + defined $_->{pop} && $_->{pop} == .5 ? '½' : $_->{pop}, ), - '' . showval($_, 'armor'), - showattack($_, 'ground'), - showattack($_, 'air'), - '' . showval($_, 'attack', 'range'), + '' . 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} < 0 ? '∞' : + '' . showrangeint($_->{hp}, $_->{upgraded}->{hp}), + $_->{shield} ? sprintf('%.0f%%{shield} / $_->{hp} + ) : '' . + showrangeint($_->{armor}, $_->{upgraded}->{armor}), + showattack($_, 0), '' . sprintf( $_->{detect} ? '%s' : '%s', - showval($_, 'sight') + showrangeint($_->{sight}, $_->{upgraded}->{sight}) ), - '' . showval($_, 'speed'), + 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($_), + (map {( + '
', + showattack($row, $_), + '', + )} 1 .. $#{ $_->{attack} }), "\n" ); } + my @rows = @{$units}; my $grouped = 1; # race headers if (exists $get{order}) { $grouped = 0; $get{order} ||= ''; - if ($get{order} eq 'size') { - $_->{order} = $_->{unit}*8 + $_->{suit} + $_->{hp}/512 + $_->{min}/8192 for @$units; + if ($get{order} eq 'name') { + @rows = sort {$a->{name} cmp $b->{name}} @rows; } elsif ($get{order} eq 'cost') { - $_->{order} = $_->{gas}*1.5 + $_->{min} + $_->{unit}/8 + $_->{build}/256/8 for @$units; + $_->{order} = ( + $_->{gas}*1.5 + $_->{min} + $_->{pop}/8 + $_->{build}/256/8 + ) for @rows; } - else { - $units->[$_]->{order} = $_ for 0 .. $#$units; + elsif ($get{order} eq 'build') { + my %unittime = map { ($_->{name} => $_->{warp} // $_->{build}) } @rows; + $unittime{Templar} = $unittime{'High Templar'}; + $_->{order} = ( + ($_->{warp} // $_->{build}) + + ($_->{gas}*1.5 + $_->{min} + $_->{pop}/8)/1024 + + ($_->{base} ? ($unittime{$_->{base}->[0]} // 100) + 1 : 0) + ) for @rows; + } + elsif ($get{order} eq 'size') { + $_->{order} = ( + $_->{pop}*16 + ($_->{size} // $_->{suit}) + $_->{cargo}/8 + + $_->{hp}/512 + $_->{min}/8192 + ) for @rows; } + elsif ($get{order} eq 'hp') { + $_->{order} = ( + $_->{hp}*1.01 + $_->{armor} + $_->{shield} + $_->{size}/1024, + ) for @rows; + } + elsif ($get{order} eq 'attack') { + $_->{order} = $_->{hp} / 16384 + max( + map { + ($_->{dps} ? $_->{dps}->[-1] : + ($_->{damage} + $_->{upgrade} * 3) + * ($_->{count} // 1) / ($_->{cooldown} // 1) + ) + * ($_->{splash} ? 1.01 : 1) + * ($_->{type} eq 'implosive' ? .96 : 1) + * ($_->{type} eq 'explosive' ? .98 : 1) + } @{ $_->{attack} } + ) for @rows; + } + elsif ($get{order} eq 'speed') { + $_->{order} = ( + ($_->{upgraded}->{speed} // $_->{speed}*1.01) + + $_->{sight}/1024 + $_->{detect}/2048 + ) for @rows; + } + @rows = sort {$a->{order} <=> $b->{order}} @rows if exists $rows[0]->{order}; } - my @rows = @{$units}; - @rows = sort {$a->{order} <=> $b->{order}} @rows unless $grouped; my ($race, $cat) = ('', ''); for (@rows) { if ($grouped) { - printf '

%s

'."\n", + say sprintf '

%s

', $race = $_->{race}, ucfirst $race unless $race eq $_->{race}; } @@ -261,7 +444,7 @@ sub showval { for my $subrow (@{ $_->{special} }) { $subrow->{alt} or next; print( - '
', $subrow->{alt}, + '
', $subrow->{alt}, showunitcols($subrow), ); } @@ -274,64 +457,105 @@ sub showval {
cost -
minerals+gas required to create one unit +
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) { - :>transports can fit 8 Small, - 4 Medium, - 2 Large, - or a single Huge unit<: + :>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 { - :>affected by Small, - Medium, - or Large unit damage<: + :>abilities may hit only organic + or mechanic targets +
affected by Small, + Medium, or + Large damage<: } :> -
number of command points taken per unit -
organic/mechanic unit -
HP
- total number of hitpoints (including shields) +
HP +
total number of hitpoints (including shields) +
everything zerg (except for eggs) regenerates one point every + <:= $scver{major} == 1 ? '4½' : '3.7' :> seconds
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 +
<: +if ($scver{major} > 1) { + :>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 +
<: +if ($scver{major} > 1) { + :>after 10 seconds out of combat, 2 points are recharged per game second<: +} else { + :>recharges one point every 2½ seconds<: +} :>
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 ½) -
ground/air -
damage done per single attack against ground/air units -
2nd column indicates relative amount of damage done in - 1 second of fastest game time -
splash damage+ hits nearby objects as well -
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 +
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 -
range -
maximum range of weapon (note siege tank also has a minimum range)
speed -
relative speed of movement (when in full motion, startup speed ignored) +
top movement speed in hex per second +
acceleration and deceleration ignored
specials -
special abilities are usually casted manually, but some are always active
parentheses () indicate that it needs to be researched first +
passive abilities are always enabled
hover for description -
range is maximum range required to activate -
cost is percentage of total energy lost +
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), second value indicates attribute after all -possible upgrades. +When two values are given (1-2), the second value indicates the attribute +after all possible upgrades.