From 3e1154347ed0dff92aef69b7dca54ebd7ce1ade7 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Thu, 18 Jun 2020 22:44:15 +0930 Subject: [PATCH] Python script for generating nice json and tsv from the lua unitdefs dump --- unit_tables_analysis.py | 408 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 unit_tables_analysis.py diff --git a/unit_tables_analysis.py b/unit_tables_analysis.py new file mode 100644 index 0000000..cec94d6 --- /dev/null +++ b/unit_tables_analysis.py @@ -0,0 +1,408 @@ +# 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)