# 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)