Talk:List of default ship variants

From Hearts of Iron 4 Wiki
Jump to navigation Jump to search

Script[edit]

These tables have been generated by the following script. Feel free to update the script here if you make changes.

import re
from decimal import Decimal
from pyradox import parse_merge, parse_file, get_localisation
from functools import partial

GAME_VERSION = '1.9'

get_localisation = partial(
    get_localisation, game='HoI4', process_substitutions=False)

# mapping stat names to l10n keys
EQUIPMENT_STATS = {stat: f'STAT_NAVY_{stat.upper()}' for stat in [
    "surface_detection", "sub_detection", "surface_visibility",
    "sub_visibility", "lg_attack", "lg_armor_piercing", "hg_attack",
    "hg_armor_piercing", "torpedo_attack", "sub_attack", "anti_air_attack",
]}
EQUIPMENT_STATS.update({
    "naval_speed": 'STAT_NAVY_MAXIMUM_SPEED',
    "naval_range": 'STAT_NAVY_RANGE',
    "max_strength": 'STAT_COMMON_MAX_STRENGTH',
    "armor_value": 'STAT_COMMON_ARMOR',
    "reliability": 'STAT_COMMON_RELIABILITY',
    "fuel_consumption": 'STAT_COMMON_FUEL_CONSUMPTION',
    "carrier_size": 'STAT_CARRIER_SIZE',
    "build_cost_ic": 'STAT_COMMON_BUILD_COST_IC',
})

equipmentDB = parse_merge('common/units/equipment', game='HoI4', merge_levels=1)
equipments = equipmentDB['equipments']

def right(value):
    return f'<span style="float:right">{value}</span>'

# insert number into l10n string placeholder
def replace(number, tmpl, key='VALUE'):
    m = re.search(r'\$'+key.upper()+r'\b([^$]*)\$', tmpl)
    if not m:
        raise Exception(tmpl)
    result = ''
    judgement = 0
    options = m.group(1)
    if '+' in options:
        judgement = 1
    if '-' in options:
        judgement = -1
    if number == 0:
        judgement = 0
    elif number < 0:
        judgement = -judgement

    use_percent = '%' in options
    if judgement > 0:
        result += '{{green|'
    elif judgement < 0:
        result += '{{red|'
    if use_percent:
        number *= 100
        number = number.quantize(Decimal('0.1'))
    if number == number.to_integral():
        number = number.quantize(Decimal(1))
    else:
        number = number.normalize()
    result += f'{number:+}' if judgement else f'{number}'
    result += '%' if use_percent else ''
    if judgement:
        result += '}}'
    return re.sub(r'\$'+key.upper()+r'\b([^$]*)\$', result, tmpl)

moduleDB = parse_merge(
    'common/units/equipment/modules',
    game='HoI4',
    merge_levels=1)

P = Decimal('0.001')  # precision of game calculations is limited to this

def get_attr(name, a):  # recursively find the first value
    if name is None:
        return None
    e = equipments[name]
    return e.find(a) or get_attr(e['parent'], a) or get_attr(e['archetype'], a)

def get_stat(name, attr, archetype=False):
    if name is None:
        return Decimal().quantize(P)
    e = equipments[name]
    if attr in e:
        return Decimal(e[attr]).quantize(P)
    if archetype or 'archetype' not in e:
        return Decimal().quantize(P)
    return get_stat(e['archetype'], attr, archetype=True)

def stat_l10n(label):
    label = get_localisation(EQUIPMENT_STATS[stat]).rstrip(': ')
    label = re.sub(r'£production_cost', '{{icon|production cost}}', label)
    return label

# collect all equipment names unlocked by naval techs into sections
sections = {
    'screen_ship': 'Screen ships',
    'capital_ship': 'Capital ships',
    'carrier': 'Carriers',
    'submarine': 'Submarines',
}
variants = { section: [] for section in sections }
techDB = parse_file('common/technologies/naval.txt', game='HoI4')
for tech in techDB['technologies'].values():
    for equipment_name in tech.find_all('enable_equipments'):
        variant = { 'name': equipment_name }
        # guess comparable hull name from archetype and equipment name
        quirks = {
            'torpedo_cruiser': 'ship_hull_torpedo_cruiser',
            'battle_cruiser_2': 'ship_hull_heavy_3',
            'carrier_1': 'ship_hull_carrier_conversion_bb',
            'carrier_2': 'ship_hull_carrier_1',
            'carrier_3': 'ship_hull_carrier_2',
            'carrier_4': 'ship_hull_carrier_3',
        }
        variant['hull_name'] = equipments[equipment_name]['archetype'] + equipment_name[-2:]
        if equipment_name in quirks:
            variant['hull_name'] = quirks[equipment_name]
        variants[get_attr(equipment_name, 'type')].append(variant)

print('{{Version|%s}}' % GAME_VERSION)
# create one table per section
for section, section_name in sections.items():
    # pre-calculate stats for all rows to filter out empty columns
    for v in variants[section]:
        v['manpower'] = get_attr(v['name'], 'manpower')
        module_add = { stat: Decimal() for stat in EQUIPMENT_STATS }
        module_mult = { stat: Decimal(1) for stat in EQUIPMENT_STATS }
        module_avg = { stat: Decimal() for stat in EQUIPMENT_STATS }
        module_avg_count = { stat: 0 for stat in EQUIPMENT_STATS }
        for module_name in equipments[v['name']]['default_modules'].values():
            if module_name == "empty":
                continue
            module = moduleDB['equipment_modules'][module_name]
            v['manpower'] += module['manpower'] or 0
            for stat, val in module.find('add_stats', {}).items():
                module_add[stat] += Decimal(val).quantize(P)
            for stat, val in module.find('multiply_stats', {}).items():
                module_mult[stat] += Decimal(val).quantize(P)
            for stat, val in module.find('add_average_stats', {}).items():
                module_avg[stat] += Decimal(val).quantize(P)
                module_avg_count[stat] += 1

        for stat in EQUIPMENT_STATS:
            base = get_stat(v['name'], stat)
            base += module_add[stat]
            if module_avg_count[stat]:
                base += (module_avg[stat] / module_avg_count[stat]).quantize(P)
            v[stat] = (base * module_mult[stat]).quantize(P)

    include_mp = any(v['manpower'] for v in variants[section])
    stat_columns = {
        stat: 1
        for stat in EQUIPMENT_STATS
        if any(v[stat] for v in variants[section])
    }

    print(f'== {section_name} ==')
    print('{{SVersion|%s}}' % GAME_VERSION)
    print('{| class="wikitable"\n! Equipment type\n! Equivalent hull in<br>'
          '{{icon|mtg|1}}\n! Extra stats<br>compared to hull\n! Modules')
    if include_mp:
        print('! <span style="writing-mode: vertical-lr">{{icon|manpower|1}}</span>')
    for stat in stat_columns:
        print('! <span style="writing-mode: vertical-lr">',
              stat_l10n(EQUIPMENT_STATS[stat]), '</span>')
    for v in variants[section]:
        equipment_l10n = get_localisation(v['name'])
        print(f'|- id="{equipment_l10n}"')
        print(f'| {equipment_l10n}')
        print('|', get_localisation(v['hull_name']))
        print('|')
        # compare base stats to hull
        if include_mp:
            mp_diff = get_attr(v['name'], 'manpower') - get_attr(v['hull_name'], 'manpower')
            if mp_diff:
                val = '{{%s|%d}}' % ('green' if mp_diff < 0 else 'red', mp_diff)
                print('* {{icon|manpower|1}}', right(val))
        for stat in EQUIPMENT_STATS:
            stat_diff = get_stat(v['name'], stat) - get_stat(v['hull_name'], stat)
            if stat_diff:
                diff_l10n = EQUIPMENT_STATS[stat] + '_DIFF'
                print(
                    '*',
                    stat_l10n(EQUIPMENT_STATS[stat]),
                    right(replace(stat_diff, get_localisation(diff_l10n))),
                )
        print('|')
        for name in equipments[v['name']]['default_modules'].values():
            if name == "empty":
                continue
            print('*', get_localisation(name + '_short') or get_localisation(name))
        if include_mp:
            print(f'| {right(v["manpower"])}')
        for stat in stat_columns:
            value_l10n = EQUIPMENT_STATS[stat] + '_VALUE'
            print(f'| {right(replace(v[stat], get_localisation(value_l10n)))}')
    print('|}')

Bitmode (talk) 00:26, 29 January 2020 (UTC)