Zero-K_Custom_Widgets/unit_tables_analysis.py

409 lines
16 KiB
Python
Raw Permalink Normal View History

# This is not a ZK Lua Widget, don't put it in your widgets folder.
# This is a companion script to export_UnitDefs.lua which will parse the lua output and generate something nicer.
# Currently, it shortlists the units and properties we care about and spits those out as JSON.
import pandas as pd
import slpp # https://github.com/SirAnthony/slpp
FILENAME = 'SPR_104.0.1-1510-g89bb8e3 maintenance_ZK_v1.8.5.3_unitdefs.lua' # TODO: pass it in as an arg or something
with open(FILENAME, 'r') as file:
unitdefs_text = file.read().lstrip('return')
unitdefs_full = {v['name']:v for v in slpp.slpp.decode(unitdefs_text).values()}
unit_shortlist = [
'cloakaa', 'cloakarty', 'cloakassault', 'cloakbomb', 'cloakcon', 'cloakheavyraid', 'cloakjammer', 'cloakraid', 'cloakriot', 'cloakskirm', 'cloaksnipe',
'shieldaa', 'shieldarty', 'shieldassault', 'shieldbomb', 'shieldcon', 'shieldfelon', 'shieldraid', 'shieldriot', 'shieldscout', 'shieldshield', 'shieldskirm',
'vehaa', 'veharty', 'wolverine_mine', 'vehassault', 'vehcapture', 'vehcon', 'vehheavyarty', 'vehraid', 'vehriot', 'vehscout', 'vehsupport',
'hoveraa', 'hoverarty', 'hoverassault', 'hovercon', 'hoverdepthcharge', 'hoverheavyraid', 'hoverminer', 'hoverraid', 'hoverriot', 'hovershotgun', 'hoverskirm', 'hoverskirm2', 'hoversonic',
'gunshipaa', 'gunshipassault', 'gunshipbomb', 'gunshipcon', 'gunshipemp', 'gunshipheavyskirm', 'gunshipheavytrans', 'gunshipkrow', 'gunshipraid', 'gunshipskirm', 'gunshiptrans', 'nebula',
'planecon', 'planefighter', 'planeheavyfighter', 'planelightscout', 'planescout',
'bomberassault', 'bomberdisarm', 'bomberheavy', 'bomberprec', 'bomberriot', 'bomberstrike',
'spideraa', 'spideranarchid', 'spiderantiheavy', 'spiderassault', 'spidercon', 'spidercrabe', 'spideremp', 'spiderriot', 'spiderscout', 'spiderskirm',
'jumpaa', 'jumparty', 'jumpassault', 'jumpblackhole', 'jumpbomb', 'jumpcon', 'jumpraid', 'jumpscout', 'jumpskirm', 'jumpsumo',
'tankaa', 'tankarty', 'tankassault', 'tankcon', 'tankheavyarty', 'tankheavyassault', 'tankheavyraid', 'tankraid', 'tankriot',
'amphaa', 'amphassault', 'amphbomb', 'amphcon', 'amphfloater', 'amphimpulse', 'amphlaunch', 'amphraid', 'amphriot', 'amphtele', 'tele_beacon', 'grebe',
'shipaa', 'shiparty', 'shipassault', 'shipcarrier', 'shipcon', 'shipheavyarty', 'shipriot', 'shipscout', 'shipskirm', 'shiptorpraider', 'assaultcruiser',
'subraider', 'subscout', 'subtacmissile',
'striderantiheavy', 'striderarty', 'striderbantha', 'striderdante', 'striderdetriment', 'striderfunnelweb', 'striderscorpion', 'athena',
'energyfusion', 'energygeo', 'energyheavygeo', 'energypylon', 'energysingu', 'energysolar', 'energywind',
'starlight_satellite', 'staticantinuke', 'staticarty', 'staticcon', 'staticheavyarty', 'staticheavyradar', 'staticjammer', 'staticmex', 'staticmissilesilo', 'staticnuke', 'staticradar', 'staticrearm', 'staticshield', 'staticsonar', 'staticstorage',
'turretaaclose', 'turretaafar', 'turretaaflak', 'turretaaheavy', 'turretaalaser', 'turretantiheavy', 'turretemp', 'turretgauss', 'turretheavy', 'turretheavylaser', 'turretimpulse', 'turretlaser', 'turretmissile', 'turretriot', 'turretsunlance', 'turrettorp',
'mahlazer', 'tacnuke', 'empmissile', 'napalmmissile', 'seismic', 'zenith', 'raveparty',
'dronecarry', 'dronefighter', 'droneheavyslow', 'dronelight',
'factoryamph', 'factorycloak', 'factorygunship', 'factoryhover', 'factoryjump', 'factoryplane', 'factoryshield', 'factoryship', 'factoryspider', 'factorytank', 'factoryveh', 'striderhub',
'roost',
'chicken', 'chicken_blimpy', 'chicken_digger', 'chicken_digger_b', 'chicken_dodo', 'chicken_dragon', 'chicken_drone', 'chicken_drone_starter', 'chicken_leaper', 'chicken_listener', 'chicken_listener_b', 'chicken_pigeon', 'chicken_rafflesia', 'chicken_roc', 'chicken_shield', 'chicken_spidermonkey', 'chicken_sporeshooter', 'chicken_tiamat', 'chickena', 'chickenblobber', 'chickenbroodqueen', 'chickenc', 'chickend', 'chickenf', 'chickenflyerqueen', 'chickenlandqueen', 'chickenr', 'chickens', 'chickenspire', 'chickenwurm',
]
unitdefkeys_shortlist = [
'name',
'humanName',
'tooltip',
'metalCost', #'energyCost', 'buildTime', # These should always be identical
'unitsPer10k', # Fake key calculated from metalCost for convenience
#'cost', # Seems to be slightly higher than the metalCost/energyCost/buildTime
'metalStorage', #'metalMake', 'metalUpkeep', # Make and Upkeep all 0 for some reason, handled elsewhere in ZK
'energyStorage', #'energyMake', 'energyUpkeep', # See above
'customParams',
'health',
'healthPerMetal', # Fake key calculated from health and metalCost for convenience
'speed', #'speedToFront',
'turnDegPerSec', 'turnSecPer180', # Fake keys calculated from turnRate for convenience
'turnRate', 'turnRadius', # turnRate*0.16 = degrees per second
'turnInPlaceSpeedLimit',
'hasShield', 'shieldPower', 'shieldWeaponDef',
'shieldRate', 'shieldRadius', 'shieldPowerPerMetal', 'shieldRatePerMetal', # Fake keys for convenience
'canCloak', 'cloakCost', 'cloakCostMoving',
'decloakDistance', 'decloakOnFire',
'startCloaked', 'stealth',
'airLosRadius', 'losHeight', 'losRadius',
'radarRadius', 'jammerRadius',
'sonarRadius',
'airStrafe',
'armorType',
'armoredMultiple',
'bankingAllowed',
'buildDistance',
'buildOptions',
'canReclaim', 'canRepair', 'canResurrect', # Builders should have first two, last is for Athenas only
'buildSpeed', #'reclaimSpeed', 'repairSpeed', 'resurrectSpeed', # Should all be identical in ZK
'terraformSpeed',
'idleTime',
'modCategories',
'springCategories',
'mass',
'transportSize', # 0, 4, 25
'onOffable',
'power',
'rSpeed',
'strafeToAttack',
'maxWeaponRange',
'primaryWeapon',
'stockpileWeaponDef',
'reloadTime',
'weapons',
#'sonarJamRadius', # All 0
#'sonarStealth', # All false
#'decloakSpherical', # All True
#'extractRange', # All 0
#'extractsMetal', # All 0
#'idleAutoHeal', # All 0, strangely. Seems to be handled in customParams.
#'turnInPlace', # All False
#'isAirUnit', 'isBomberAirUnit',
#'isBuilder', 'isBuilding',
#'isExtractor', # All False
#'isFactory',
#'isFirePlatform', # All False
#'isFighterAirUnit', 'isGroundUnit', 'isHoveringAirUnit', 'isImmobile', 'isMobileBuilder', 'isStaticBuilder', 'isStrafingAirUnit', 'isTransport',
#'canSelfRepair', # All false
#'buildRange3D', # All False
#'autoHeal', # All 0
#'activateWhenBuilt',
#'buildingDecalDeaySpeed', 'buildingDecalSizeX', 'buildingDecalSizeY', 'buildingDecalType', 'buildpicname',
#'canAssist', 'canBeAssisted', 'canCapture',
#'canAttack', 'canAttackWater', 'canFight', 'canFireControl', 'canManualFire', 'canLoopbackAttack',
#'canFly', 'canGuard', 'canKamikaze',
#'canMove', 'canPatrol',
#'canParalyze', 'canRepeat', 'canRestore', 'canSelfD',
#'canStockpile', 'canSubmerge', 'cantBeTransported',
#'capturable', 'captureSpeed',
#'cobID',
#'collide', 'collisionVolume',
#'crashDrag', 'deathExplosion',
#'dlHoverFactor',
#'factoryHeadingTakeoff',
#'fallSpeed',
#'fireState',
#'canDropFlare', 'flankingBonusDirX', 'flankingBonusDirY', 'flankingBonusDirZ', 'flankingBonusMax', 'flankingBonusMin', 'flankingBonusMobilityAdd', 'flankingBonusMode',
#'flareDelay', 'flareDropVectorX', 'flareDropVectorY', 'flareDropVectorZ', 'flareEfficiency', 'flareReloadTime', 'flareSalvoDelay', 'flareSalvoSize', 'flareTime',
#'floatOnWater',
#'frontToSpeed',
#'fullHealthFactory',
#'height',
#'hideDamage',
#'highTrajectoryType',
#'holdSteady',
#'hoverAttack',
#'iconType',
#'id',
#'isFeature',
#'kamikazeDist', 'kamikazeUseLOS',
#'leaveTracks',
#'levelGround',
#'loadingRadius',
#'makesMetal', # All 0
#'maxAcc', 'maxDec',
#'maxAileron', 'maxBank', 'maxElevator', 'maxHeightDif', 'maxPitch', 'maxRudder',
#'maxCoverage',
#'maxRepairSpeed',
#'maxThisUnit',
#'maxWaterDepth',
#'minCollisionSpeed',
#'minWaterDepth',
#'model', 'modelname', 'modelpath', 'modeltype',
#'moveDef', #'moveState',
#'myGravity',
#'showNanoFrame', 'showNanoSpray', 'nanoColorB', 'nanoColorG', 'nanoColorR',
#'needGeo',
#'noChaseCategories',
#'radius',
#'reclaimable', # Almost all True
#'releaseHeld',
#'repairable', # True for all shortlist
#'scriptName', 'scriptPath',
#'seismicRadius', 'seismicSignature',
#'selectionVolume',
#'selfDCountdown', 'selfDExplosion',
#'showPlayerName',
#'slideTolerance',
#'sounds',
#'stopToAttack',
#'targfac', # All False
#'totalEnergyOut', # All 0
#'trackOffset',
#'trackStrength',
#'trackStretch',
#'trackType',
#'trackWidth',
#'transportByEnemy', # All True
#'transportCapacity', # All 0 or 1
#'transportMass', # All 100000
#'transportUnloadMethod', # All 0
#'unitFallSpeed',
#'upright',
#'useBuildingGroundDecal',
#'useSmoothMesh',
#'verticalSpeed',
#'wantedHeight',
#'waterline',
#'tidalGenerator',
#'windGenerator',
#'wingAngle', 'wingDrag',
#'wreckName',
#'xsize', 'zsize'
]
weaponkeys_shortlist = [
'name',
'id',
'description',
'salvoDamage',
'salvoDuration',
'salvoDPS',
'sustainedDPS',
'range',
'reload',
'customParams',
'damages',
'paralyzer',
'isShield',
'salvoDelay',
'salvoSize',
'accuracy',
'tracks',
'badTargets', 'onlyTargets',
'canAttackGround',
'cegTag',
#'coverageRange', # All 0 or 100000
'craterAreaOfEffect',
'cylinderTargeting', 'cylinderTargetting',
'damageAreaOfEffect',
'duration',
'dynDamageExp', # 0, 1
#'dynDamageInverted', 'dynDamageMin', # All False, 0 respectively
'dynDamageRange',
'edgeEffectiveness',
'explosionSpeed',
'fireStarter',
'flightTime',
'manualFire',
'impactOnly',
'movingAccuracy',
'noAutoTarget',
'noExplode',
'noSelfDamage',
'numbounce',
'onlyForward',
'projectiles', 'projectilespeed',
'proximityPriority',
'selfExplode',
'sprayAngle',
'startvelocity',
'stockpile', 'stockpileTime',
'metalCost', 'energyCost',
'turnRate',
'turret',
'type',
'uptime',
'weaponAcceleration',
'weaponDef',
'wobble'
#'avoidFeature', 'avoidFriendly', 'avoidNeutral',
#'beamTTL', 'beamburst', 'beamtime',
#'bouncerebound',
#'collisionSize',
#'dance',
#'gravityAffected',
#'groundbounce', 'groundslip',
#'heightBoostFactor', 'heightMod',
#'highTrajectory',
#'intensity', 'minIntensity',
#'interceptSolo',
#'interceptedByShieldType',
#'interceptor',
#'largeBeamLaser',
#'laserHardStop',
#'leadBonus',
#'leadLimit',
#'mainDirX', 'mainDirY', 'mainDirZ',
#'maxAngle', 'maxAngleDif',
#'myGravity',
#'noEnemyCollide', 'noFeatureCollide', 'noFireBaseCollide', 'noFriendlyCollide', 'noGroundCollide', 'noNeutralCollide', 'noNonTargetCollide',
#'predictBoost',
#'fireSound', 'hitSound', 'soundTrigger',
#'shieldAlpha',
#'shieldBadColorA', 'shieldBadColorB', 'shieldBadColorG', 'shieldBadColorR',
#'shieldGoodColorA', 'shieldGoodColorB', 'shieldGoodColorG', 'shieldGoodColorR',
#'shieldEnergyUse',
#'shieldPower', 'shieldPowerRegen', 'shieldPowerRegenEnergy', 'shieldRadius', 'shieldRechargeDelay',
#'shieldForce',
#'shieldInterceptType',
#'shieldMaxSpeed',
#'shieldRepulser',
#'smartShield',
#'exteriorShield',
#'visibleShield', 'visibleShieldHitFrames', 'visibleShieldRepulse',
#'size',
#'sizeGrowth',
#'slavedTo', # All 0 on shortlist
#'sweepFire', # All false on shortlist
#'targetBorder', 'targetMoveError', 'targetable',
#'tdfId', # All 0 on shortlist
#'trajectoryHeight',
#'visuals',
#'waterWeapon', 'waterbounce',
]
#udefs_short = {u:{k:v for k,v in unitdefs_full[u].items() if k in unitdefkeys_shortlist} for u in unit_shortlist}
#udefs_short = {
#u: {
#k: (unitdefs_full[u][k] if k!='weapons' else
#[{wk: wep[wk] for wk in weaponkeys_shortlist if wk in wep} for wep in unitdefs_full[u]['weapons'].values()])
#for k in unitdefkeys_shortlist if k in unitdefs_full[u]
#}
#for u in unit_shortlist
#}
def wdef_salvoDamage(wdef):
if 'customParams' in wdef and 'shot_damage' in wdef['customParams']:
return float(wdef['customParams']['shot_damage']) # Yes, there are strings in the dataset for no reason
return float(wdef.get('salvoSize', 1)) * float(wdef.get('damages', {}).get(0, 0))
def wdef_salvoDuration(wdef):
return float(wdef.get('salvoSize', 1)) * float(wdef.get('salvoDelay', 0))
def wdef_salvoDPS(wdef):
dmg = wdef_salvoDamage(wdef)
if float(wdef.get('salvoSize', 1)) > 1:
duration = max(min(wdef_salvoDuration(wdef), wdef.get('reload', 1)), 0.000001) # Don't want divide-by-zero silliness
else:
duration = max(wdef.get('reload', 1), 0.000001) # Don't want divide-by-zero silliness
return dmg/duration
def wdef_sustainedDPS(wdef):
dmg = wdef_salvoDamage(wdef)
duration = max(wdef.get('reload', 1), 0.000001) # Don't want divide-by-zero silliness
return dmg/duration
wdef_xforms = {
'badTargets': lambda wdef: [i for i in wdef['badTargets'].keys()],
'onlyTargets': lambda wdef: [i for i in wdef['onlyTargets'].keys()],
'salvoDamage': wdef_salvoDamage,
'salvoDuration': wdef_salvoDuration,
'salvoDPS': wdef_salvoDPS,
'sustainedDPS': wdef_sustainedDPS
}
udef_xforms = {
#'weapons': lambda udef: [{wk: (wep[wk] if wk not in wdef_xforms else wdef_xforms[wk](wep)) for wk in weaponkeys_shortlist if wk in wep} for wep in udef['weapons'].values()],
'weapons': lambda udef: [{wk: wdef_xforms.get(wk, lambda x:x[wk])(wep) for wk in weaponkeys_shortlist if (wk in wep or wk in wdef_xforms)} for wep in udef['weapons'].values()],
'buildOptions': lambda udef: [i for i in udef['buildOptions'].values()],
'modCategories': lambda udef: [i for i in udef['modCategories'].keys()],
'springCategories': lambda udef: [i for i in udef['springCategories'].keys()],
'turnDegPerSec': lambda udef: udef['turnRate']*0.16, # Fake keys calculated from turnRate for convenience
'turnSecPer180': lambda udef: 180/((udef['turnRate'] if udef['turnRate']>0 else 1)*0.16),
'healthPerMetal': lambda udef: udef['health']/(1 if udef['metalCost']<1 else udef['metalCost']), # Fake key calculated from health and metalCost for convenience
'unitsPer10k': lambda udef: 10000/udef.get('metalCost', 1), # Fake key calculated from metalCost for convenience
'shieldRadius': lambda udef: float(udef.get('customParams', {}).get('shield_radius', 0)),
'shieldRate': lambda udef: float(udef.get('customParams', {}).get('shield_rate', 0)),
'shieldPowerPerMetal': lambda udef: float(udef.get('shieldPower', 0))/udef.get('metalCost', 1),
'shieldRatePerMetal': lambda udef: float(udef.get('customParams', {}).get('shield_rate', 0))/udef.get('metalCost', 1),
}
udefs_short = {
u: {
#k: (unitdefs_full[u][k] if k not in udef_xforms else udef_xforms[k](unitdefs_full[u]))
k: udef_xforms.get(k, lambda x:x[k])(unitdefs_full[u]) for k in unitdefkeys_shortlist if (k in unitdefs_full[u] or k in udef_xforms)
}
for u in unit_shortlist
}
for uname, udef in udefs_short.items():
wdefs = udef.get('weapons', [])
if len(wdefs) > 0:
cost = 1 if udef['metalCost']<1 else udef['metalCost']
udefs_short[uname]['max_salvoDPS'] = max([wdef.get('salvoDPS', 0) for wdef in wdefs])
udefs_short[uname]['max_sustainedDPS'] = max([wdef.get('sustainedDPS', 0) for wdef in wdefs])
udefs_short[uname]['max_salvoDPS_perMetal'] = udefs_short[uname]['max_salvoDPS']/cost
udefs_short[uname]['max_sustainedDPS_perMetal'] = udefs_short[uname]['max_sustainedDPS']/cost
df_udefs = pd.DataFrame.from_dict(udefs_short)
df_udefs.to_json(FILENAME.rpartition('.')[0] + '.json', orient='columns', force_ascii=False, indent=2)
udef_spreadsheet_keys = [
'name',
'humanName',
'tooltip',
'metalCost',
'unitsPer10k',
'health',
'shieldPower',
'shieldRate',
'max_salvoDPS',
'max_sustainedDPS',
'maxWeaponRange',
'reloadTime',
'healthPerMetal',
'max_salvoDPS_perMetal',
'max_sustainedDPS_perMetal',
'shieldPowerPerMetal',
'shieldRatePerMetal',
'speed',
'turnDegPerSec', 'turnSecPer180',
'shieldRadius',
'metalStorage',
'energyStorage',
'canCloak', 'startCloaked', 'stealth', 'cloakCost', 'cloakCostMoving', 'decloakDistance',
'airLosRadius', 'losHeight', 'losRadius',
'radarRadius', 'jammerRadius', 'sonarRadius',
'armorType', 'armoredMultiple',
'buildDistance', 'buildOptions',
'canReclaim', 'canRepair', 'canResurrect', # Builders should have first two, last is for Athenas only
'buildSpeed', 'terraformSpeed',
'idleTime',
'mass',
'transportSize', # 0, 4, 25
'onOffable',
'power',
'modCategories',
'springCategories',
]
udef_spreadsheet_dict = {unit_k:{k:udefs_short[unit_k].get(k,'') for k in udef_spreadsheet_keys} for unit_k in udefs_short.keys()}
udef_spreadsheet_df = pd.DataFrame.from_dict(udef_spreadsheet_dict)
import csv
udef_spreadsheet_df.T.to_csv(FILENAME.rpartition('.')[0] + '.tsv', sep='\t', index=False, quoting=csv.QUOTE_NONE)