diff --git a/client/generated/move-metadata.json b/client/generated/move-metadata.json deleted file mode 100644 index 8cd343c..0000000 --- a/client/generated/move-metadata.json +++ /dev/null @@ -1,1768 +0,0 @@ -{ - "generatedAt": "2026-01-25T19:41:26.403Z", - "totalMoves": 44, - "movesByMon": { - "inutia": [ - { - "contractName": "BigBite", - "filePath": "mons/inutia/BigBite.sol", - "inheritsFrom": "StandardAttack", - "name": "Big Bite", - "basePower": 85, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Wild", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "ChainExpansion", - "filePath": "mons/inutia/ChainExpansion.sol", - "inheritsFrom": "IMoveSet", - "name": "Chain Expansion", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "CHARGES": 4, - "HEAL_DENOM": 8, - "DAMAGE_1_DENOM": 16, - "DAMAGE_2_DENOM": 8, - "DAMAGE_3_DENOM": 4 - }, - "customBehavior": "stat-modification, applies-effect, healing" - }, - { - "contractName": "HitAndDip", - "filePath": "mons/inutia/HitAndDip.sol", - "inheritsFrom": "StandardAttack", - "name": "Hit And Dip", - "basePower": 30, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customBehavior": "force-switch" - }, - { - "contractName": "Initialize", - "filePath": "mons/inutia/Initialize.sol", - "inheritsFrom": "IMoveSet", - "name": "Initialize", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "ATTACK_BUFF_PERCENT": 50, - "SP_ATTACK_BUFF_PERCENT": 50 - }, - "customBehavior": "applies-effect" - } - ], - "gorillax": [ - { - "contractName": "Blow", - "filePath": "mons/gorillax/Blow.sol", - "inheritsFrom": "StandardAttack", - "name": "Blow", - "basePower": 70, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Air", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "PoundGround", - "filePath": "mons/gorillax/PoundGround.sol", - "inheritsFrom": "StandardAttack", - "name": "Pound Ground", - "basePower": 95, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Earth", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "RockPull", - "filePath": "mons/gorillax/RockPull.sol", - "inheritsFrom": "IMoveSet", - "name": "Rock Pull", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "dynamic", - "moveType": "Earth", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "OPPONENT_BASE_POWER": 80, - "SELF_DAMAGE_BASE_POWER": 30 - }, - "customBehavior": "self-damage" - }, - { - "contractName": "ThrowPebble", - "filePath": "mons/gorillax/ThrowPebble.sol", - "inheritsFrom": "StandardAttack", - "name": "Throw Pebble", - "basePower": 40, - "staminaCost": 1, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Earth", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - } - ], - "iblivion": [ - { - "contractName": "Brightback", - "filePath": "mons/iblivion/Brightback.sol", - "inheritsFrom": "IMoveSet", - "name": "Brightback", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yang", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_POWER": 70 - }, - "customBehavior": "stat-modification" - }, - { - "contractName": "Loop", - "filePath": "mons/iblivion/Loop.sol", - "inheritsFrom": "IMoveSet", - "name": "Loop", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yang", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BOOST_PERCENT_LEVEL_1": 15, - "BOOST_PERCENT_LEVEL_2": 30, - "BOOST_PERCENT_LEVEL_3": 40 - } - }, - { - "contractName": "Renormalize", - "filePath": "mons/iblivion/Renormalize.sol", - "inheritsFrom": "IMoveSet", - "name": "Renormalize", - "basePower": "dynamic", - "staminaCost": 0, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yang", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "consumes-stacks" - }, - { - "contractName": "UnboundedStrike", - "filePath": "mons/iblivion/UnboundedStrike.sol", - "inheritsFrom": "IMoveSet", - "name": "Unbounded Strike", - "basePower": "dynamic", - "staminaCost": "dynamic", - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Air", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_POWER": 80, - "EMPOWERED_POWER": 130, - "BASE_STAMINA": 2, - "EMPOWERED_STAMINA": 1, - "REQUIRED_STACKS": 3 - }, - "customBehavior": "conditional-power, dynamic-stamina, consumes-stacks" - } - ], - "aurox": [ - { - "contractName": "BullRush", - "filePath": "mons/aurox/BullRush.sol", - "inheritsFrom": "StandardAttack", - "name": "Bull Rush", - "basePower": 120, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": null, - "extraDataType": "None", - "customConstants": { - "SELF_DAMAGE_PERCENT": 20 - }, - "customBehavior": "self-damage" - }, - { - "contractName": "GildedRecovery", - "filePath": "mons/aurox/GildedRecovery.sol", - "inheritsFrom": "IMoveSet", - "name": "Gilded Recovery", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customConstants": { - "HEAL_PERCENT": 50, - "STAMINA_BONUS": 1 - }, - "customBehavior": "stat-modification, healing" - }, - { - "contractName": "IronWall", - "filePath": "mons/aurox/IronWall.sol", - "inheritsFrom": "IMoveSet", - "name": "Iron Wall", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "HEAL_PERCENT": 50, - "INITIAL_HEAL_PERCENT": 20 - }, - "customBehavior": "stat-modification, applies-effect, healing" - }, - { - "contractName": "VolatilePunch", - "filePath": "mons/aurox/VolatilePunch.sol", - "inheritsFrom": "StandardAttack", - "name": "Volatile Punch", - "basePower": 40, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "STATUS_EFFECT_CHANCE": 50 - }, - "customBehavior": "applies-effect" - } - ], - "pengym": [ - { - "contractName": "ChillOut", - "filePath": "mons/pengym/ChillOut.sol", - "inheritsFrom": "StandardAttack", - "name": "Chill Out", - "basePower": 0, - "staminaCost": 0, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Ice", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": "FROSTBITE_STATUS", - "extraDataType": "None" - }, - { - "contractName": "Deadlift", - "filePath": "mons/pengym/Deadlift.sol", - "inheritsFrom": "IMoveSet", - "name": "Deadlift", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "ATTACK_BUFF_PERCENT": 50, - "DEF_BUFF_PERCENT": 50 - } - }, - { - "contractName": "DeepFreeze", - "filePath": "mons/pengym/DeepFreeze.sol", - "inheritsFrom": "IMoveSet", - "name": "Deep Freeze", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Ice", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_POWER": 90 - } - }, - { - "contractName": "PistolSquat", - "filePath": "mons/pengym/PistolSquat.sol", - "inheritsFrom": "StandardAttack", - "name": "Pistol Squat", - "basePower": 80, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY - 1", - "moveType": "Metal", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "force-switch" - } - ], - "xmon": [ - { - "contractName": "ContagiousSlumber", - "filePath": "mons/xmon/ContagiousSlumber.sol", - "inheritsFrom": "IMoveSet", - "name": "Contagious Slumber", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "applies-effect" - }, - { - "contractName": "NightTerrors", - "filePath": "mons/xmon/NightTerrors.sol", - "inheritsFrom": "IMoveSet", - "name": "Night Terrors", - "basePower": "dynamic", - "staminaCost": 0, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_DAMAGE_PER_STACK": 20, - "ASLEEP_DAMAGE_PER_STACK": 30 - }, - "customBehavior": "stat-modification, applies-effect" - }, - { - "contractName": "Somniphobia", - "filePath": "mons/xmon/Somniphobia.sol", - "inheritsFrom": "IMoveSet", - "name": "Somniphobia", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DURATION": 6, - "DAMAGE_DENOM": 16 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "VitalSiphon", - "filePath": "mons/xmon/VitalSiphon.sol", - "inheritsFrom": "StandardAttack", - "name": "Vital Siphon", - "basePower": 40, - "staminaCost": 2, - "accuracy": 90, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "STAMINA_STEAL_PERCENT": 50 - }, - "customBehavior": "stat-modification" - } - ], - "volthare": [ - { - "contractName": "DualShock", - "filePath": "mons/volthare/DualShock.sol", - "inheritsFrom": "StandardAttack", - "name": "Dual Shock", - "basePower": 60, - "staminaCost": 0, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "applies-effect" - }, - { - "contractName": "Electrocute", - "filePath": "mons/volthare/Electrocute.sol", - "inheritsFrom": "StandardAttack", - "name": "Electrocute", - "basePower": 90, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Lightning", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 10, - "effect": "ZAP_STATUS", - "extraDataType": "None" - }, - { - "contractName": "MegaStarBlast", - "filePath": "mons/volthare/MegaStarBlast.sol", - "inheritsFrom": "IMoveSet", - "name": "Mega Star Blast", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Lightning", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_ACCURACY": 50, - "ZAP_ACCURACY": 30, - "BASE_POWER": 150 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "RoundTrip", - "filePath": "mons/volthare/RoundTrip.sol", - "inheritsFrom": "StandardAttack", - "name": "Round Trip", - "basePower": 30, - "staminaCost": 1, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Lightning", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customBehavior": "force-switch" - } - ], - "ghouliath": [ - { - "contractName": "EternalGrudge", - "filePath": "mons/ghouliath/EternalGrudge.sol", - "inheritsFrom": "IMoveSet", - "name": "Eternal Grudge", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yin", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "ATTACK_DEBUFF_PERCENT": 50, - "SP_ATTACK_DEBUFF_PERCENT": 50 - } - }, - { - "contractName": "InfernalFlame", - "filePath": "mons/ghouliath/InfernalFlame.sol", - "inheritsFrom": "StandardAttack", - "name": "Infernal Flame", - "basePower": 120, - "staminaCost": 2, - "accuracy": 85, - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 30, - "effect": "BURN_STATUS", - "extraDataType": "None" - }, - { - "contractName": "Osteoporosis", - "filePath": "mons/ghouliath/Osteoporosis.sol", - "inheritsFrom": "StandardAttack", - "name": "Osteoporosis", - "basePower": 90, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Yin", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "WitherAway", - "filePath": "mons/ghouliath/WitherAway.sol", - "inheritsFrom": "StandardAttack", - "name": "Wither Away", - "basePower": 60, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Yin", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": "PANIC_STATUS", - "extraDataType": "None", - "customBehavior": "applies-effect" - } - ], - "malalien": [ - { - "contractName": "FederalInvestigation", - "filePath": "mons/malalien/FederalInvestigation.sol", - "inheritsFrom": "StandardAttack", - "name": "Federal Investigation", - "basePower": 100, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "InfiniteLove", - "filePath": "mons/malalien/InfiniteLove.sol", - "inheritsFrom": "StandardAttack", - "name": "Infinite Love", - "basePower": 90, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 10, - "effect": "_SLEEP_STATUS", - "extraDataType": "None" - }, - { - "contractName": "NegativeThoughts", - "filePath": "mons/malalien/NegativeThoughts.sol", - "inheritsFrom": "StandardAttack", - "name": "Infinite Love", - "basePower": 80, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Math", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 10, - "effect": "_PANIC_STATUS", - "extraDataType": "None" - }, - { - "contractName": "TripleThink", - "filePath": "mons/malalien/TripleThink.sol", - "inheritsFrom": "IMoveSet", - "name": "Triple Think", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Math", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "SP_ATTACK_BUFF_PERCENT": 75 - } - } - ], - "sofabbi": [ - { - "contractName": "Gachachacha", - "filePath": "mons/sofabbi/Gachachacha.sol", - "inheritsFrom": "IMoveSet", - "name": "Gachachacha", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "MIN_BASE_POWER": 1, - "MAX_BASE_POWER": 200, - "SELF_KO_CHANCE": 5, - "OPP_KO_CHANCE": 5 - }, - "customBehavior": "random-power" - }, - { - "contractName": "GuestFeature", - "filePath": "mons/sofabbi/GuestFeature.sol", - "inheritsFrom": "IMoveSet", - "name": "Guest Feature", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customConstants": { - "BASE_POWER": 75 - } - }, - { - "contractName": "SnackBreak", - "filePath": "mons/sofabbi/SnackBreak.sol", - "inheritsFrom": "IMoveSet", - "name": "Snack Break", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Nature", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DEFAULT_HEAL_DENOM": 2, - "MAX_DIVISOR": 3 - }, - "customBehavior": "stat-modification, healing" - }, - { - "contractName": "UnexpectedCarrot", - "filePath": "mons/sofabbi/UnexpectedCarrot.sol", - "inheritsFrom": "StandardAttack", - "name": "Unexpected Carrot", - "basePower": 120, - "staminaCost": 4, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Nature", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - } - ], - "embursa": [ - { - "contractName": "HeatBeacon", - "filePath": "mons/embursa/HeatBeacon.sol", - "inheritsFrom": "IMoveSet", - "name": "Heat Beacon", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "applies-effect" - }, - { - "contractName": "HoneyBribe", - "filePath": "mons/embursa/HoneyBribe.sol", - "inheritsFrom": "IMoveSet", - "name": "Honey Bribe", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Nature", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DEFAULT_HEAL_DENOM": 2, - "MAX_DIVISOR": 3, - "SP_DEF_PERCENT": 50 - }, - "customBehavior": "stat-modification, healing" - }, - { - "contractName": "Q5", - "filePath": "mons/embursa/Q5.sol", - "inheritsFrom": "IMoveSet", - "name": "Q5", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DELAY": 5, - "BASE_POWER": 150 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "SetAblaze", - "filePath": "mons/embursa/SetAblaze.sol", - "inheritsFrom": "StandardAttack", - "name": "Set Ablaze", - "basePower": 90, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 30, - "effect": "BURN_STATUS", - "extraDataType": "None" - } - ] - }, - "allMoves": [ - { - "contractName": "BigBite", - "filePath": "mons/inutia/BigBite.sol", - "inheritsFrom": "StandardAttack", - "name": "Big Bite", - "basePower": 85, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Wild", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "Blow", - "filePath": "mons/gorillax/Blow.sol", - "inheritsFrom": "StandardAttack", - "name": "Blow", - "basePower": 70, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Air", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "Brightback", - "filePath": "mons/iblivion/Brightback.sol", - "inheritsFrom": "IMoveSet", - "name": "Brightback", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yang", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_POWER": 70 - }, - "customBehavior": "stat-modification" - }, - { - "contractName": "BullRush", - "filePath": "mons/aurox/BullRush.sol", - "inheritsFrom": "StandardAttack", - "name": "Bull Rush", - "basePower": 120, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": null, - "extraDataType": "None", - "customConstants": { - "SELF_DAMAGE_PERCENT": 20 - }, - "customBehavior": "self-damage" - }, - { - "contractName": "ChainExpansion", - "filePath": "mons/inutia/ChainExpansion.sol", - "inheritsFrom": "IMoveSet", - "name": "Chain Expansion", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "CHARGES": 4, - "HEAL_DENOM": 8, - "DAMAGE_1_DENOM": 16, - "DAMAGE_2_DENOM": 8, - "DAMAGE_3_DENOM": 4 - }, - "customBehavior": "stat-modification, applies-effect, healing" - }, - { - "contractName": "ChillOut", - "filePath": "mons/pengym/ChillOut.sol", - "inheritsFrom": "StandardAttack", - "name": "Chill Out", - "basePower": 0, - "staminaCost": 0, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Ice", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": "FROSTBITE_STATUS", - "extraDataType": "None" - }, - { - "contractName": "ContagiousSlumber", - "filePath": "mons/xmon/ContagiousSlumber.sol", - "inheritsFrom": "IMoveSet", - "name": "Contagious Slumber", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "applies-effect" - }, - { - "contractName": "Deadlift", - "filePath": "mons/pengym/Deadlift.sol", - "inheritsFrom": "IMoveSet", - "name": "Deadlift", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "ATTACK_BUFF_PERCENT": 50, - "DEF_BUFF_PERCENT": 50 - } - }, - { - "contractName": "DeepFreeze", - "filePath": "mons/pengym/DeepFreeze.sol", - "inheritsFrom": "IMoveSet", - "name": "Deep Freeze", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Ice", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_POWER": 90 - } - }, - { - "contractName": "DualShock", - "filePath": "mons/volthare/DualShock.sol", - "inheritsFrom": "StandardAttack", - "name": "Dual Shock", - "basePower": 60, - "staminaCost": 0, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "applies-effect" - }, - { - "contractName": "Electrocute", - "filePath": "mons/volthare/Electrocute.sol", - "inheritsFrom": "StandardAttack", - "name": "Electrocute", - "basePower": 90, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Lightning", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 10, - "effect": "ZAP_STATUS", - "extraDataType": "None" - }, - { - "contractName": "EternalGrudge", - "filePath": "mons/ghouliath/EternalGrudge.sol", - "inheritsFrom": "IMoveSet", - "name": "Eternal Grudge", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yin", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "ATTACK_DEBUFF_PERCENT": 50, - "SP_ATTACK_DEBUFF_PERCENT": 50 - } - }, - { - "contractName": "FederalInvestigation", - "filePath": "mons/malalien/FederalInvestigation.sol", - "inheritsFrom": "StandardAttack", - "name": "Federal Investigation", - "basePower": 100, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "Gachachacha", - "filePath": "mons/sofabbi/Gachachacha.sol", - "inheritsFrom": "IMoveSet", - "name": "Gachachacha", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "MIN_BASE_POWER": 1, - "MAX_BASE_POWER": 200, - "SELF_KO_CHANCE": 5, - "OPP_KO_CHANCE": 5 - }, - "customBehavior": "random-power" - }, - { - "contractName": "GildedRecovery", - "filePath": "mons/aurox/GildedRecovery.sol", - "inheritsFrom": "IMoveSet", - "name": "Gilded Recovery", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customConstants": { - "HEAL_PERCENT": 50, - "STAMINA_BONUS": 1 - }, - "customBehavior": "stat-modification, healing" - }, - { - "contractName": "GuestFeature", - "filePath": "mons/sofabbi/GuestFeature.sol", - "inheritsFrom": "IMoveSet", - "name": "Guest Feature", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cyber", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customConstants": { - "BASE_POWER": 75 - } - }, - { - "contractName": "HeatBeacon", - "filePath": "mons/embursa/HeatBeacon.sol", - "inheritsFrom": "IMoveSet", - "name": "Heat Beacon", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "applies-effect" - }, - { - "contractName": "HitAndDip", - "filePath": "mons/inutia/HitAndDip.sol", - "inheritsFrom": "StandardAttack", - "name": "Hit And Dip", - "basePower": 30, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customBehavior": "force-switch" - }, - { - "contractName": "HoneyBribe", - "filePath": "mons/embursa/HoneyBribe.sol", - "inheritsFrom": "IMoveSet", - "name": "Honey Bribe", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Nature", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DEFAULT_HEAL_DENOM": 2, - "MAX_DIVISOR": 3, - "SP_DEF_PERCENT": 50 - }, - "customBehavior": "stat-modification, healing" - }, - { - "contractName": "InfernalFlame", - "filePath": "mons/ghouliath/InfernalFlame.sol", - "inheritsFrom": "StandardAttack", - "name": "Infernal Flame", - "basePower": 120, - "staminaCost": 2, - "accuracy": 85, - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 30, - "effect": "BURN_STATUS", - "extraDataType": "None" - }, - { - "contractName": "InfiniteLove", - "filePath": "mons/malalien/InfiniteLove.sol", - "inheritsFrom": "StandardAttack", - "name": "Infinite Love", - "basePower": 90, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 10, - "effect": "_SLEEP_STATUS", - "extraDataType": "None" - }, - { - "contractName": "Initialize", - "filePath": "mons/inutia/Initialize.sol", - "inheritsFrom": "IMoveSet", - "name": "Initialize", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Mythic", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "ATTACK_BUFF_PERCENT": 50, - "SP_ATTACK_BUFF_PERCENT": 50 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "IronWall", - "filePath": "mons/aurox/IronWall.sol", - "inheritsFrom": "IMoveSet", - "name": "Iron Wall", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "HEAL_PERCENT": 50, - "INITIAL_HEAL_PERCENT": 20 - }, - "customBehavior": "stat-modification, applies-effect, healing" - }, - { - "contractName": "Loop", - "filePath": "mons/iblivion/Loop.sol", - "inheritsFrom": "IMoveSet", - "name": "Loop", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yang", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BOOST_PERCENT_LEVEL_1": 15, - "BOOST_PERCENT_LEVEL_2": 30, - "BOOST_PERCENT_LEVEL_3": 40 - } - }, - { - "contractName": "MegaStarBlast", - "filePath": "mons/volthare/MegaStarBlast.sol", - "inheritsFrom": "IMoveSet", - "name": "Mega Star Blast", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Lightning", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_ACCURACY": 50, - "ZAP_ACCURACY": 30, - "BASE_POWER": 150 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "NegativeThoughts", - "filePath": "mons/malalien/NegativeThoughts.sol", - "inheritsFrom": "StandardAttack", - "name": "Infinite Love", - "basePower": 80, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Math", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 10, - "effect": "_PANIC_STATUS", - "extraDataType": "None" - }, - { - "contractName": "NightTerrors", - "filePath": "mons/xmon/NightTerrors.sol", - "inheritsFrom": "IMoveSet", - "name": "Night Terrors", - "basePower": "dynamic", - "staminaCost": 0, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_DAMAGE_PER_STACK": 20, - "ASLEEP_DAMAGE_PER_STACK": 30 - }, - "customBehavior": "stat-modification, applies-effect" - }, - { - "contractName": "Osteoporosis", - "filePath": "mons/ghouliath/Osteoporosis.sol", - "inheritsFrom": "StandardAttack", - "name": "Osteoporosis", - "basePower": 90, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Yin", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "PistolSquat", - "filePath": "mons/pengym/PistolSquat.sol", - "inheritsFrom": "StandardAttack", - "name": "Pistol Squat", - "basePower": 80, - "staminaCost": 2, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY - 1", - "moveType": "Metal", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "force-switch" - }, - { - "contractName": "PoundGround", - "filePath": "mons/gorillax/PoundGround.sol", - "inheritsFrom": "StandardAttack", - "name": "Pound Ground", - "basePower": 95, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Earth", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "Q5", - "filePath": "mons/embursa/Q5.sol", - "inheritsFrom": "IMoveSet", - "name": "Q5", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DELAY": 5, - "BASE_POWER": 150 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "Renormalize", - "filePath": "mons/iblivion/Renormalize.sol", - "inheritsFrom": "IMoveSet", - "name": "Renormalize", - "basePower": "dynamic", - "staminaCost": 0, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Yang", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customBehavior": "consumes-stacks" - }, - { - "contractName": "RockPull", - "filePath": "mons/gorillax/RockPull.sol", - "inheritsFrom": "IMoveSet", - "name": "Rock Pull", - "basePower": "dynamic", - "staminaCost": 3, - "accuracy": "DEFAULT_ACCURACY", - "priority": "dynamic", - "moveType": "Earth", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "OPPONENT_BASE_POWER": 80, - "SELF_DAMAGE_BASE_POWER": 30 - }, - "customBehavior": "self-damage" - }, - { - "contractName": "RoundTrip", - "filePath": "mons/volthare/RoundTrip.sol", - "inheritsFrom": "StandardAttack", - "name": "Round Trip", - "basePower": 30, - "staminaCost": 1, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Lightning", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "SelfTeamIndex", - "customBehavior": "force-switch" - }, - { - "contractName": "SetAblaze", - "filePath": "mons/embursa/SetAblaze.sol", - "inheritsFrom": "StandardAttack", - "name": "Set Ablaze", - "basePower": 90, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Fire", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 30, - "effect": "BURN_STATUS", - "extraDataType": "None" - }, - { - "contractName": "SnackBreak", - "filePath": "mons/sofabbi/SnackBreak.sol", - "inheritsFrom": "IMoveSet", - "name": "Snack Break", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Nature", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DEFAULT_HEAL_DENOM": 2, - "MAX_DIVISOR": 3 - }, - "customBehavior": "stat-modification, healing" - }, - { - "contractName": "Somniphobia", - "filePath": "mons/xmon/Somniphobia.sol", - "inheritsFrom": "IMoveSet", - "name": "Somniphobia", - "basePower": "dynamic", - "staminaCost": 1, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Other", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "DURATION": 6, - "DAMAGE_DENOM": 16 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "ThrowPebble", - "filePath": "mons/gorillax/ThrowPebble.sol", - "inheritsFrom": "StandardAttack", - "name": "Throw Pebble", - "basePower": 40, - "staminaCost": 1, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Earth", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "TripleThink", - "filePath": "mons/malalien/TripleThink.sol", - "inheritsFrom": "IMoveSet", - "name": "Triple Think", - "basePower": "dynamic", - "staminaCost": 2, - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Math", - "moveClass": "Self", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "SP_ATTACK_BUFF_PERCENT": 75 - } - }, - { - "contractName": "UnboundedStrike", - "filePath": "mons/iblivion/UnboundedStrike.sol", - "inheritsFrom": "IMoveSet", - "name": "Unbounded Strike", - "basePower": "dynamic", - "staminaCost": "dynamic", - "accuracy": "DEFAULT_ACCURACY", - "priority": "DEFAULT_PRIORITY", - "moveType": "Air", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "BASE_POWER": 80, - "EMPOWERED_POWER": 130, - "BASE_STAMINA": 2, - "EMPOWERED_STAMINA": 1, - "REQUIRED_STACKS": 3 - }, - "customBehavior": "conditional-power, dynamic-stamina, consumes-stacks" - }, - { - "contractName": "UnexpectedCarrot", - "filePath": "mons/sofabbi/UnexpectedCarrot.sol", - "inheritsFrom": "StandardAttack", - "name": "Unexpected Carrot", - "basePower": 120, - "staminaCost": 4, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Nature", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None" - }, - { - "contractName": "VitalSiphon", - "filePath": "mons/xmon/VitalSiphon.sol", - "inheritsFrom": "StandardAttack", - "name": "Vital Siphon", - "basePower": 40, - "staminaCost": 2, - "accuracy": 90, - "priority": "DEFAULT_PRIORITY", - "moveType": "Cosmic", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "STAMINA_STEAL_PERCENT": 50 - }, - "customBehavior": "stat-modification" - }, - { - "contractName": "VolatilePunch", - "filePath": "mons/aurox/VolatilePunch.sol", - "inheritsFrom": "StandardAttack", - "name": "Volatile Punch", - "basePower": 40, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Metal", - "moveClass": "Physical", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 0, - "effect": null, - "extraDataType": "None", - "customConstants": { - "STATUS_EFFECT_CHANCE": 50 - }, - "customBehavior": "applies-effect" - }, - { - "contractName": "WitherAway", - "filePath": "mons/ghouliath/WitherAway.sol", - "inheritsFrom": "StandardAttack", - "name": "Wither Away", - "basePower": 60, - "staminaCost": 3, - "accuracy": 100, - "priority": "DEFAULT_PRIORITY", - "moveType": "Yin", - "moveClass": "Special", - "critRate": "DEFAULT_CRIT_RATE", - "volatility": "DEFAULT_VOL", - "effectAccuracy": 100, - "effect": "PANIC_STATUS", - "extraDataType": "None", - "customBehavior": "applies-effect" - } - ] -} \ No newline at end of file diff --git a/client/lib/metadata-converter.ts b/client/lib/metadata-converter.ts index a83b89b..84a077e 100644 --- a/client/lib/metadata-converter.ts +++ b/client/lib/metadata-converter.ts @@ -171,9 +171,9 @@ export function createMoveMap(moves: MoveMetadata[]): Map } /** - * Loads and converts move metadata from JSON file + * Loads and converts move metadata from JSON data * - * @param jsonData - Parsed JSON data from move-metadata.json + * @param jsonData - Parsed JSON data (from transpiler's dependency-manifest.json or other source) * @returns Converted metadata with moves indexed by name */ export function loadMoveMetadata(jsonData: { diff --git a/client/package.json b/client/package.json index 052bd7f..d0ad61a 100644 --- a/client/package.json +++ b/client/package.json @@ -5,7 +5,6 @@ "main": "index.ts", "types": "index.ts", "scripts": { - "extract-metadata": "npx tsx scripts/extract-move-metadata.ts", "build": "tsc", "test": "vitest" }, diff --git a/client/scripts/extract-move-metadata.ts b/client/scripts/extract-move-metadata.ts deleted file mode 100644 index 0109065..0000000 --- a/client/scripts/extract-move-metadata.ts +++ /dev/null @@ -1,500 +0,0 @@ -#!/usr/bin/env npx tsx -/** - * Move Metadata Extractor - * - * Parses Solidity move files and extracts metadata from: - * 1. StandardAttack-based moves (ATTACK_PARAMS in constructor) - * 2. IMoveSet direct implementations (values from getter methods) - * - * Usage: npx tsx extract-move-metadata.ts [--output ./generated/move-metadata.json] - */ - -import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; -import { join, relative, basename } from 'path'; - -interface RawMoveMetadata { - contractName: string; - filePath: string; - inheritsFrom: string; - name: string; - basePower: string | number; - staminaCost: string | number; - accuracy: string | number; - priority: string | number; - moveType: string; - moveClass: string; - critRate: string | number; - volatility: string | number; - effectAccuracy: string | number; - effect: string | null; - extraDataType: string; - customConstants?: Record; - customBehavior?: string; -} - -const SRC_DIR = join(__dirname, '../../src'); -const MONS_DIR = join(SRC_DIR, 'mons'); - -/** - * Recursively find all .sol files in a directory - */ -function findSolidityFiles(dir: string): string[] { - const results: string[] = []; - const items = readdirSync(dir); - - for (const item of items) { - const fullPath = join(dir, item); - const stat = statSync(fullPath); - - if (stat.isDirectory()) { - results.push(...findSolidityFiles(fullPath)); - } else if (item.endsWith('.sol') && !item.includes('Lib')) { - results.push(fullPath); - } - } - - return results; -} - -/** - * Parse a single ATTACK_PARAMS struct literal from source - */ -function parseAttackParams(content: string): Partial | null { - // Match ATTACK_PARAMS({ ... }) - const paramsMatch = content.match(/ATTACK_PARAMS\s*\(\s*\{([\s\S]*?)\}\s*\)/); - if (!paramsMatch) return null; - - const paramsBlock = paramsMatch[1]; - const result: Partial = {}; - - // Parse each field - const fieldPatterns: Record = { - 'NAME': 'name', - 'BASE_POWER': 'basePower', - 'STAMINA_COST': 'staminaCost', - 'ACCURACY': 'accuracy', - 'PRIORITY': 'priority', - 'MOVE_TYPE': 'moveType', - 'MOVE_CLASS': 'moveClass', - 'CRIT_RATE': 'critRate', - 'VOLATILITY': 'volatility', - 'EFFECT_ACCURACY': 'effectAccuracy', - 'EFFECT': 'effect', - }; - - for (const [solidityField, metadataField] of Object.entries(fieldPatterns)) { - // Match patterns like: NAME: "Bull Rush" or BASE_POWER: 120 or MOVE_TYPE: Type.Metal - const regex = new RegExp(`${solidityField}\\s*:\\s*([^,}]+)`, 'i'); - const match = paramsBlock.match(regex); - - if (match) { - let value: string | number = match[1].trim(); - - // Handle string literals - if (value.startsWith('"') && value.endsWith('"')) { - value = value.slice(1, -1); - } - // Handle numeric literals - else if (/^\d+$/.test(value)) { - value = parseInt(value, 10); - } - // Handle Type.* and MoveClass.* enums - else if (value.startsWith('Type.')) { - value = value.replace('Type.', ''); - } - else if (value.startsWith('MoveClass.')) { - value = value.replace('MoveClass.', ''); - } - // Handle IEffect(address(0)) as null - else if (value.includes('address(0)')) { - value = 'null'; - } - // Keep constant references as strings (e.g., DEFAULT_PRIORITY) - - (result as Record)[metadataField] = value === 'null' ? null : value; - } - } - - return result; -} - -/** - * Extract custom constants from a contract (public constant declarations) - */ -function extractCustomConstants(content: string): Record { - const constants: Record = {}; - - // Match patterns like: uint256 public constant SELF_DAMAGE_PERCENT = 20; - const constantRegex = /(?:uint\d*|int\d*)\s+public\s+constant\s+(\w+)\s*=\s*(\d+)/g; - let match; - - while ((match = constantRegex.exec(content)) !== null) { - constants[match[1]] = parseInt(match[2], 10); - } - - return constants; -} - -/** - * Extract extraDataType override from a contract - */ -function extractExtraDataType(content: string): string { - // Match: function extraDataType() ... returns (ExtraDataType) { return ExtraDataType.X; } - const match = content.match(/function\s+extraDataType\s*\([^)]*\)[^{]*\{[^}]*return\s+ExtraDataType\.(\w+)/); - return match ? match[1] : 'None'; -} - -/** - * Check if contract has custom move() behavior - */ -function hasCustomMoveBehavior(content: string): boolean { - // Get the move function body - const moveBody = extractFunctionBody(content, 'move'); - if (!moveBody) return false; - - // If it just calls _move() and nothing else, it's not custom - const hasOnlyMoveCall = /^\s*_move\s*\([^)]*\)\s*;?\s*$/.test(moveBody.trim()); - if (hasOnlyMoveCall) return false; - - // If the body has conditional logic, calculations, or multiple statements, it's custom - const hasComplexLogic = hasDynamicLogic(moveBody) || - moveBody.includes('_calculateDamage') || - moveBody.includes('setBaselightLevel') || - moveBody.includes('addEffect') || - moveBody.includes('updateMonState') || - /\w+\s*=\s*[^;]+;/.test(moveBody); // Has variable assignments - - return hasComplexLogic; -} - -/** - * Describe custom behavior based on contract analysis - */ -function describeCustomBehavior(content: string, contractName: string): string | undefined { - const behaviors: string[] = []; - - // Check for self-damage - if (content.includes('SELF_DAMAGE') || content.includes('selfDamage')) { - behaviors.push('self-damage'); - } - - // Check for switch - if (content.includes('switchActiveMon')) { - behaviors.push('force-switch'); - } - - // Check for stat modification - if (content.includes('updateMonState')) { - behaviors.push('stat-modification'); - } - - // Check for effect application - if (content.includes('addEffect')) { - behaviors.push('applies-effect'); - } - - // Check for healing - if (content.includes('healDamage') || content.includes('HEAL')) { - behaviors.push('healing'); - } - - // Check for random base power - if (content.includes('rng') && content.includes('basePower')) { - behaviors.push('random-power'); - } - - // Check for dynamic/conditional power (based on stacks, level, etc.) - const moveBody = extractFunctionBody(content, 'move'); - if (moveBody && hasDynamicLogic(moveBody) && /power\s*=/.test(moveBody)) { - behaviors.push('conditional-power'); - } - - // Check for dynamic stamina cost - const staminaBody = extractFunctionBody(content, 'stamina'); - if (staminaBody && hasDynamicLogic(staminaBody)) { - behaviors.push('dynamic-stamina'); - } - - // Check for stack consumption (like Baselight) - if (content.includes('setBaselightLevel') || content.includes('consumeStacks') || - /set\w+Level\s*\([^)]*,\s*0\s*\)/.test(content)) { - behaviors.push('consumes-stacks'); - } - - return behaviors.length > 0 ? behaviors.join(', ') : undefined; -} - -/** - * Extract the body of a function from Solidity source - */ -function extractFunctionBody(content: string, functionName: string): string | null { - // Match function definition and its body (handles multi-line with balanced braces) - const funcStart = content.search(new RegExp(`function\\s+${functionName}\\s*\\(`)); - if (funcStart === -1) return null; - - // Find the opening brace - const braceStart = content.indexOf('{', funcStart); - if (braceStart === -1) return null; - - // Find matching closing brace (handle nested braces) - let depth = 1; - let pos = braceStart + 1; - while (depth > 0 && pos < content.length) { - if (content[pos] === '{') depth++; - else if (content[pos] === '}') depth--; - pos++; - } - - return content.slice(braceStart + 1, pos - 1); -} - -/** - * Check if a function has conditional/dynamic logic (if statements, ternary, etc.) - */ -function hasDynamicLogic(functionBody: string | null): boolean { - if (!functionBody) return false; - // Check for if statements or ternary operators indicating conditional returns - return /\bif\s*\(/.test(functionBody) || /\?.*:/.test(functionBody); -} - -/** - * Extract a simple return value from function body (handles single return case) - */ -function extractSimpleReturn( - functionBody: string | null, - pattern: RegExp -): string | number | null { - if (!functionBody) return null; - const match = functionBody.match(pattern); - if (!match) return null; - - const value = match[1].trim(); - if (/^\d+$/.test(value)) { - return parseInt(value, 10); - } - return value; -} - -/** - * Parse a move contract that directly implements IMoveSet - */ -function parseIMoveSetImplementation(content: string): Partial | null { - const result: Partial = {}; - - // Extract name from name() function - const nameMatch = content.match(/function\s+name\s*\([^)]*\)[^{]*\{[^}]*return\s*"([^"]+)"/); - if (nameMatch) { - result.name = nameMatch[1]; - } - - // Extract stamina - check for dynamic logic first - const staminaBody = extractFunctionBody(content, 'stamina'); - if (staminaBody) { - if (hasDynamicLogic(staminaBody)) { - // Dynamic stamina - mark as such - result.staminaCost = 'dynamic'; - } else { - // Try to extract simple return value - const staminaValue = extractSimpleReturn(staminaBody, /return\s+(\d+|DEFAULT_STAMINA|\w+_STAMINA)/); - if (staminaValue !== null) { - result.staminaCost = staminaValue; - } - } - } - - // Extract priority - check for dynamic logic - const priorityBody = extractFunctionBody(content, 'priority'); - if (priorityBody) { - if (hasDynamicLogic(priorityBody)) { - result.priority = 'dynamic'; - } else { - const priorityValue = extractSimpleReturn(priorityBody, /return\s+(\d+|DEFAULT_PRIORITY|\w+_PRIORITY)/); - if (priorityValue !== null) { - result.priority = priorityValue; - } - } - } - - // Extract moveType - const typeMatch = content.match(/function\s+moveType\s*\([^)]*\)[^{]*\{[^}]*return\s+Type\.(\w+)/); - if (typeMatch) { - result.moveType = typeMatch[1]; - } - - // Extract moveClass - const classMatch = content.match(/function\s+moveClass\s*\([^)]*\)[^{]*\{[^}]*return\s+MoveClass\.(\w+)/); - if (classMatch) { - result.moveClass = classMatch[1]; - } - - // Check if any values were found - if (Object.keys(result).length === 0) return null; - - // Set defaults for missing values (indicates special move logic) - result.basePower = result.basePower ?? 'dynamic'; - result.staminaCost = result.staminaCost ?? 'DEFAULT_STAMINA'; - result.accuracy = result.accuracy ?? 'DEFAULT_ACCURACY'; - result.critRate = result.critRate ?? 'DEFAULT_CRIT_RATE'; - result.volatility = result.volatility ?? 'DEFAULT_VOL'; - result.effectAccuracy = result.effectAccuracy ?? 0; - result.effect = null; - - return result; -} - -/** - * Extract contract name and inheritance from source - */ -function extractContractInfo(content: string): { name: string; inheritsFrom: string } | null { - // Match: contract ContractName is Parent1, Parent2 { - const match = content.match(/contract\s+(\w+)\s+is\s+([^{]+)\s*\{/); - if (!match) return null; - - const name = match[1]; - const inheritsList = match[2].split(',').map(s => s.trim()); - - // Determine primary parent - let inheritsFrom = 'unknown'; - if (inheritsList.includes('StandardAttack')) { - inheritsFrom = 'StandardAttack'; - } else if (inheritsList.includes('IMoveSet')) { - inheritsFrom = 'IMoveSet'; - } else if (inheritsList.some(i => i.includes('Effect') || i.includes('Ability'))) { - inheritsFrom = 'Effect/Ability'; - } - - return { name, inheritsFrom }; -} - -/** - * Parse a single Solidity file and extract move metadata - */ -function parseMoveFile(filePath: string): RawMoveMetadata | null { - const content = readFileSync(filePath, 'utf-8'); - const relativePath = relative(SRC_DIR, filePath); - - const contractInfo = extractContractInfo(content); - if (!contractInfo) return null; - - // Skip non-move contracts - if (contractInfo.inheritsFrom === 'Effect/Ability') { - return null; - } - - let metadata: Partial; - - if (contractInfo.inheritsFrom === 'StandardAttack') { - const params = parseAttackParams(content); - if (!params) return null; - metadata = params; - } else if (contractInfo.inheritsFrom === 'IMoveSet') { - const params = parseIMoveSetImplementation(content); - if (!params) return null; - metadata = params; - } else { - return null; - } - - // Extract additional info - const customConstants = extractCustomConstants(content); - const extraDataType = extractExtraDataType(content); - const hasCustomBehavior = hasCustomMoveBehavior(content); - const customBehavior = hasCustomBehavior - ? describeCustomBehavior(content, contractInfo.name) - : undefined; - - return { - contractName: contractInfo.name, - filePath: relativePath, - inheritsFrom: contractInfo.inheritsFrom, - name: metadata.name ?? contractInfo.name, - basePower: metadata.basePower ?? 0, - staminaCost: metadata.staminaCost ?? 'DEFAULT_STAMINA', - accuracy: metadata.accuracy ?? 'DEFAULT_ACCURACY', - priority: metadata.priority ?? 'DEFAULT_PRIORITY', - moveType: metadata.moveType ?? 'None', - moveClass: metadata.moveClass ?? 'Other', - critRate: metadata.critRate ?? 'DEFAULT_CRIT_RATE', - volatility: metadata.volatility ?? 'DEFAULT_VOL', - effectAccuracy: metadata.effectAccuracy ?? 0, - effect: metadata.effect ?? null, - extraDataType, - ...(Object.keys(customConstants).length > 0 && { customConstants }), - ...(customBehavior && { customBehavior }), - }; -} - -/** - * Main extraction function - */ -function extractAllMoveMetadata(): RawMoveMetadata[] { - const moveFiles = findSolidityFiles(MONS_DIR); - const metadata: RawMoveMetadata[] = []; - - for (const filePath of moveFiles) { - try { - const moveMetadata = parseMoveFile(filePath); - if (moveMetadata) { - metadata.push(moveMetadata); - } - } catch (error) { - console.error(`Error parsing ${filePath}:`, error); - } - } - - // Sort by contract name - metadata.sort((a, b) => a.contractName.localeCompare(b.contractName)); - - return metadata; -} - -/** - * Group moves by mon (based on file path) - */ -function groupByMon(moves: RawMoveMetadata[]): Record { - const groups: Record = {}; - - for (const move of moves) { - // Extract mon name from path (e.g., "mons/aurox/BullRush.sol" -> "aurox") - const pathParts = move.filePath.split('/'); - const monIndex = pathParts.indexOf('mons'); - const monName = monIndex >= 0 && pathParts[monIndex + 1] - ? pathParts[monIndex + 1] - : 'unknown'; - - if (!groups[monName]) { - groups[monName] = []; - } - groups[monName].push(move); - } - - return groups; -} - -// Main execution -const args = process.argv.slice(2); -const outputIndex = args.indexOf('--output'); -const outputPath = outputIndex >= 0 && args[outputIndex + 1] - ? args[outputIndex + 1] - : join(__dirname, '../generated/move-metadata.json'); - -console.log('Extracting move metadata from Solidity files...'); -console.log(`Source directory: ${MONS_DIR}`); - -const allMoves = extractAllMoveMetadata(); -const groupedMoves = groupByMon(allMoves); - -const output = { - generatedAt: new Date().toISOString(), - totalMoves: allMoves.length, - movesByMon: groupedMoves, - allMoves, -}; - -writeFileSync(outputPath, JSON.stringify(output, null, 2)); - -console.log(`\nExtracted ${allMoves.length} moves:`); -for (const [mon, moves] of Object.entries(groupedMoves)) { - console.log(` ${mon}: ${moves.map(m => m.contractName).join(', ')}`); -} -console.log(`\nOutput written to: ${outputPath}`); diff --git a/transpiler/CHANGELOG.md b/transpiler/CHANGELOG.md index 14bee6e..af284ab 100644 --- a/transpiler/CHANGELOG.md +++ b/transpiler/CHANGELOG.md @@ -6,52 +6,63 @@ A transpiler that converts Solidity contracts to TypeScript for local battle sim 1. [Architecture Overview](#architecture-overview) 2. [How the Transpiler Works](#how-the-transpiler-works) -3. [Adding New Solidity Files](#adding-new-solidity-files) -4. [Angular Integration](#angular-integration) -5. [Contract Address System](#contract-address-system) -6. [Supported Features](#supported-features) -7. [Known Limitations](#known-limitations) -8. [Future Work](#future-work) -9. [Test Coverage](#test-coverage) +3. [Metadata and Dependency Injection](#metadata-and-dependency-injection) +4. [Adding New Solidity Files](#adding-new-solidity-files) +5. [Angular Integration](#angular-integration) +6. [Contract Address System](#contract-address-system) +7. [Supported Features](#supported-features) +8. [Known Limitations](#known-limitations) +9. [Future Work](#future-work) +10. [Test Coverage](#test-coverage) --- ## Architecture Overview ``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Transpilation Pipeline │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ src/*.sol ──► sol2ts.py ──► ts-output/*.ts ──► Angular Battle Service │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │ Solidity │───►│ Lexer │───►│ Parser │───►│ Code Generator │ │ -│ │ Source │ │ (Tokens) │ │ (AST) │ │ (TypeScript) │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │ -│ │ -│ Type Discovery: Scans src/ to build enum, struct, constant registries │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────────────┐ -│ Runtime Architecture │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Engine.ts │────►│ Effects │────►│ Moves │ │ -│ │ (Battle Core) │ │ (StatBoosts, │ │ (StandardAttack │ │ -│ │ │ │ StatusEffects) │ │ + custom) │ │ -│ └────────┬────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ runtime.ts │ │ Structs.ts │ │ Enums.ts │ │ -│ │ (Contract base, │ │ (Mon, Battle, │ │ (Type, MoveClass│ │ -│ │ Storage, Utils)│ │ MonStats, etc) │ │ EffectStep) │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Transpilation Pipeline │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ src/*.sol ──► sol2ts.py ──► ts-output/*.ts ──► Angular Battle Service │ +│ │ │ +│ └──► dependency-manifest.json (optional) │ +│ └──► factories.ts (optional) │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ +│ │ Solidity │───►│ Lexer │───►│ Parser │───►│ Code Generator │ │ +│ │ Source │ │ (Tokens) │ │ (AST) │ │ (TypeScript) │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ Metadata Extractor│ (--emit-metadata) │ +│ └──────────────────┘ │ +│ │ +│ Type Discovery: Scans src/ to build enum, struct, constant registries │ +│ Optimizations: Qualified name caching for O(1) type lookups │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Runtime Architecture │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Engine.ts │────►│ Effects │────►│ Moves │ │ +│ │ (Battle Core) │ │ (StatBoosts, │ │ (StandardAttack │ │ +│ │ │ │ StatusEffects) │ │ + custom) │ │ +│ └────────┬────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ runtime.ts │ │ Structs.ts │ │ Enums.ts │ │ +│ │ (Contract base, │ │ (Mon, Battle, │ │ (Type, MoveClass│ │ +│ │ Storage, Utils,│ │ MonStats, etc) │ │ EffectStep) │ │ +│ │ ContractCont.) │ └─────────────────┘ └─────────────────┘ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ ``` ### Key Design Principles @@ -64,6 +75,8 @@ A transpiler that converts Solidity contracts to TypeScript for local battle sim 4. **Storage Simulation**: The `Storage` class simulates Solidity's storage model with slot-based access. +5. **Dependency Injection**: The `ContractContainer` class provides automatic dependency resolution for contract instantiation. + --- ## How the Transpiler Works @@ -77,6 +90,8 @@ Before transpiling any file, the transpiler scans the source directory to discov - **Constants**: Collected into `Constants.ts` - **Contract/Library Names**: Used for import resolution +The type registry builds a **qualified name cache** for O(1) lookups, avoiding repeated set membership checks during code generation. + ```bash python3 transpiler/sol2ts.py src/moves/MyMove.sol -o transpiler/ts-output -d src # ^^^^^^ @@ -101,7 +116,7 @@ ContractDefinition ├── base_contracts: ["Bar", "IBaz"] ├── state_variables: [...] ├── functions: [...] -└── ... +└── constructor: {...} ``` ### Phase 4: Code Generation @@ -132,6 +147,140 @@ import * as Enums from './Enums'; import * as Constants from './Constants'; ``` +### Phase 6: Metadata Extraction (Optional) + +When `--emit-metadata` is specified, the transpiler also extracts: + +- **Dependencies**: Constructor parameters that are contract/interface types +- **Constants**: Constant values declared in the contract +- **Move Properties**: For contracts implementing `IMoveSet`, extracts name, power, etc. +- **Dependency Graph**: Maps each contract to its required dependencies + +--- + +## Metadata and Dependency Injection + +### Generating Metadata + +The transpiler can emit metadata for dependency injection and UI purposes: + +```bash +# Emit metadata alongside TypeScript +python3 sol2ts.py src/ -o ts-output -d src --emit-metadata + +# Only emit metadata (skip TypeScript generation) +python3 sol2ts.py src/ --metadata-only -d src +``` + +This generates: + +#### `dependency-manifest.json` + +```json +{ + "contracts": { + "UnboundedStrike": { + "name": "UnboundedStrike", + "filePath": "mons/iblivion/UnboundedStrike.sol", + "inheritsFrom": ["IMoveSet"], + "dependencies": [ + { "name": "_ENGINE", "typeName": "IEngine", "isInterface": true }, + { "name": "_TYPE_CALCULATOR", "typeName": "ITypeCalculator", "isInterface": true }, + { "name": "_BASELIGHT", "typeName": "Baselight", "isInterface": false } + ], + "constants": { + "BASE_POWER": 80, + "EMPOWERED_POWER": 130 + }, + "isMove": true, + "isEffect": false, + "moveProperties": { + "name": "Unbounded Strike", + "BASE_POWER": 80 + } + } + }, + "moves": { ... }, + "effects": { ... }, + "dependencyGraph": { + "UnboundedStrike": ["IEngine", "ITypeCalculator", "Baselight"] + } +} +``` + +#### `factories.ts` + +Auto-generated factory functions for each contract: + +```typescript +export function createUnboundedStrike( + _ENGINE: IEngine, + _TYPE_CALCULATOR: ITypeCalculator, + _BASELIGHT: Baselight +): UnboundedStrike { + return new UnboundedStrike(_ENGINE, _TYPE_CALCULATOR, _BASELIGHT); +} + +export function setupContainer(container: ContractContainer): void { + container.registerFactory('UnboundedStrike', + ['IEngine', 'ITypeCalculator', 'Baselight'], + (_ENGINE, _TYPE_CALCULATOR, _BASELIGHT) => + new UnboundedStrike(_ENGINE, _TYPE_CALCULATOR, _BASELIGHT) + ); +} +``` + +### Using the Dependency Injection Container + +The runtime includes a `ContractContainer` for managing contract instances: + +```typescript +import { ContractContainer, globalContainer } from './runtime'; + +// Create a container +const container = new ContractContainer(); + +// Register singletons (shared instances) +container.registerSingleton('Engine', new Engine()); +container.registerSingleton('TypeCalculator', new TypeCalculator()); + +// Register factories with dependencies +container.registerFactory( + 'UnboundedStrike', + ['Engine', 'TypeCalculator', 'Baselight'], + (engine, typeCalc, baselight) => new UnboundedStrike(engine, typeCalc, baselight) +); + +// Register lazy singletons (created on first resolve) +container.registerLazySingleton( + 'Baselight', + ['Engine'], + (engine) => new Baselight(engine) +); + +// Resolve with automatic dependency injection +const move = container.resolve('UnboundedStrike'); + +// The container automatically: +// 1. Resolves Engine (singleton) +// 2. Resolves TypeCalculator (singleton) +// 3. Resolves Baselight (lazy singleton, creates Engine dependency) +// 4. Creates UnboundedStrike with all dependencies +``` + +### Bulk Registration from Manifest + +```typescript +import manifest from './dependency-manifest.json'; +import { factories } from './factories'; + +// Register all contracts from the manifest +container.registerFromManifest(manifest.dependencyGraph, factories); + +// Now resolve any contract +const move = container.resolve('UnboundedStrike'); +``` + --- ## Adding New Solidity Files @@ -168,10 +317,17 @@ python3 transpiler/sol2ts.py src/moves/mymove/CoolMove.sol \ -o transpiler/ts-output \ -d src -# Or transpile an entire directory +# Transpile with metadata +python3 transpiler/sol2ts.py src/moves/mymove/CoolMove.sol \ + -o transpiler/ts-output \ + -d src \ + --emit-metadata + +# Transpile an entire directory python3 transpiler/sol2ts.py src/moves/mymove/ \ -o transpiler/ts-output \ - -d src + -d src \ + --emit-metadata ``` ### Step 3: Review the Output @@ -185,13 +341,30 @@ Check `transpiler/ts-output/CoolMove.ts` for: ### Step 4: Handle Dependencies -If your move uses other contracts (e.g., StatBoosts), you'll need to inject them: +Use the dependency injection container: ```typescript -// In your test or Angular service -const statBoosts = new StatBoosts(engine); -const coolMove = new CoolMove(engine); -(coolMove as any).STAT_BOOSTS = statBoosts; // Inject dependency +// Register core singletons +container.registerSingleton('Engine', engine); + +// Register the move with its dependencies +container.registerFactory( + 'CoolMove', + ['Engine'], + (engine) => new CoolMove(engine) +); + +// Resolve +const coolMove = container.resolve('CoolMove'); +``` + +Or use the generated factories if `--emit-metadata` was used: + +```typescript +import { setupContainer } from './factories'; + +setupContainer(container); +const coolMove = container.resolve('CoolMove'); ``` ### Common Transpilation Patterns @@ -210,17 +383,18 @@ const coolMove = new CoolMove(engine); ## Angular Integration -### Setting Up the Battle Service - -The `BattleService` in Angular dynamically imports transpiled modules and sets up the simulation: +### Setting Up the Battle Service with Dependency Injection ```typescript // client/lib/battle.service.ts +import { Injectable, signal, computed } from '@angular/core'; +import { ContractContainer } from '../../transpiler/ts-output/runtime'; + @Injectable({ providedIn: 'root' }) export class BattleService { - private localEngine: any; - private localTypeCalculator: any; + private container = new ContractContainer(); + private initialized = signal(false); async initializeLocalSimulation(): Promise { // Dynamic imports from transpiler output @@ -228,6 +402,7 @@ export class BattleService { { Engine }, { TypeCalculator }, { StandardAttack }, + { StatBoosts }, Structs, Enums, Constants, @@ -235,38 +410,72 @@ export class BattleService { import('../../transpiler/ts-output/Engine'), import('../../transpiler/ts-output/TypeCalculator'), import('../../transpiler/ts-output/StandardAttack'), + import('../../transpiler/ts-output/StatBoosts'), import('../../transpiler/ts-output/Structs'), import('../../transpiler/ts-output/Enums'), import('../../transpiler/ts-output/Constants'), ]); - // Create engine instance - this.localEngine = new Engine(); - this.localTypeCalculator = new TypeCalculator(); + // Register core singletons + const engine = new Engine(); + const typeCalculator = new TypeCalculator(); + const statBoosts = new StatBoosts(engine); + + this.container.registerSingleton('Engine', engine); + this.container.registerSingleton('IEngine', engine); // Interface alias + this.container.registerSingleton('TypeCalculator', typeCalculator); + this.container.registerSingleton('ITypeCalculator', typeCalculator); + this.container.registerSingleton('StatBoosts', statBoosts); + + // Load move factories from generated manifest (optional) + // Or register moves manually as needed + + this.initialized.set(true); + } + + // Get a move instance with all dependencies resolved + async getMove(moveName: string): Promise { + if (!this.initialized()) { + await this.initializeLocalSimulation(); + } + return this.container.resolve(moveName); + } - // Initialize battle state storage - (this.localEngine as any).battleConfig = {}; - (this.localEngine as any).battleData = {}; + // Register a move dynamically + registerMove( + name: string, + dependencies: string[], + factory: (...deps: any[]) => any + ): void { + this.container.registerFactory(name, dependencies, factory); } } ``` -### Configuring Contract Addresses - -If you need specific addresses for contracts (e.g., for on-chain verification): +### Loading Moves Dynamically ```typescript -import { contractAddresses } from '../../transpiler/ts-output/runtime'; - -// Before creating contract instances -contractAddresses.setAddresses({ - 'StatBoosts': '0x1234567890abcdef...', - 'BurnStatus': '0xfedcba0987654321...', - 'Engine': '0xabcdef1234567890...', -}); - -// Now created instances will use these addresses -const engine = new Engine(); // engine._contractAddress === '0xabcdef...' +async loadMovesForMon(monName: string): Promise { + // Import moves for this mon + const moveModules = await Promise.all([ + import(`../../transpiler/ts-output/mons/${monName}/Move1`), + import(`../../transpiler/ts-output/mons/${monName}/Move2`), + // ... + ]); + + // Register each move with the container + for (const module of moveModules) { + const MoveCtor = Object.values(module)[0] as any; + const moveName = MoveCtor.name; + + // Parse dependencies from the manifest or constructor + const deps = this.getDependencies(moveName); + + this.container.registerFactory(moveName, deps, (...resolvedDeps) => + new MoveCtor(...resolvedDeps) + ); + } +} ``` ### Running a Local Battle Simulation @@ -275,31 +484,51 @@ const engine = new Engine(); // engine._contractAddress === '0xabcdef...' async simulateBattle(team1: Mon[], team2: Mon[]): Promise { await this.initializeLocalSimulation(); + const engine = this.container.resolve('Engine'); + // Set up battle configuration - const battleKey = this.localEngine.computeBattleKey( - player1Address, - player2Address - ); + const battleKey = engine.computeBattleKey(player1Address, player2Address); // Initialize teams - this.localEngine.initializeBattle(battleKey, { + engine.initializeBattle(battleKey, { p0Team: team1, p1Team: team2, - // ... other config }); - // Execute moves + // Get move instances + const move = this.container.resolve('BigBite'); + + // Execute move const damage = move.move( battleKey, attackerIndex, defenderIndex, - // ... other params + extraData, + rng ); return { damage, /* ... */ }; } ``` +### Configuring Contract Addresses + +If you need specific addresses for contracts (e.g., for on-chain verification): + +```typescript +import { contractAddresses } from '../../transpiler/ts-output/runtime'; + +// Before creating contract instances +contractAddresses.setAddresses({ + 'StatBoosts': '0x1234567890abcdef...', + 'BurnStatus': '0xfedcba0987654321...', + 'Engine': '0xabcdef1234567890...', +}); + +// Now created instances will use these addresses +const engine = new Engine(); // engine._contractAddress === '0xabcdef...' +``` + ### Handling Effects and Abilities Effects need to be registered and can be looked up by address: @@ -308,8 +537,8 @@ Effects need to be registered and can be looked up by address: import { registry } from '../../transpiler/ts-output/runtime'; // Register effects -const burnStatus = new BurnStatus(engine); -const statBoosts = new StatBoosts(engine); +const burnStatus = container.resolve('BurnStatus'); +const statBoosts = container.resolve('StatBoosts'); registry.registerEffect(burnStatus._contractAddress, burnStatus); registry.registerEffect(statBoosts._contractAddress, statBoosts); @@ -389,6 +618,14 @@ const myContract = new MyContract(); // Address derived from class name - ✅ `type(uint256).max`, `type(int256).min` - ✅ `msg.sender`, `block.timestamp`, `tx.origin` +### Metadata & DI +- ✅ Dependency extraction from constructors +- ✅ Constant value extraction +- ✅ Move property extraction +- ✅ Dependency graph generation +- ✅ Factory function generation +- ✅ ContractContainer with automatic resolution + --- ## Known Limitations @@ -413,13 +650,8 @@ const myContract = new MyContract(); // Address derived from class name ### Dependency Injection -Contracts that reference other contracts need manual injection: - -```typescript -// Solidity: STAT_BOOSTS is set via constructor or immutable -// TypeScript: May need manual assignment -(myMove as any).STAT_BOOSTS = statBoostsInstance; -``` +- Circular dependencies are detected and throw errors +- Interface types should be registered with the concrete implementation name --- @@ -427,17 +659,18 @@ Contracts that reference other contracts need manual injection: ### High Priority -1. **Cross-Contract Dependency Detection** - - Auto-detect when a contract uses another contract (e.g., StatBoosts) - - Generate constructor parameters or injection helpers +1. **Enhanced Move Metadata Extraction** + - Extract `moveType()`, `moveClass()`, `priority()` return values + - Support dynamic values (functions that compute based on state) + - Generate UI-compatible metadata format 2. **Modifier Support** - Parse and inline modifier logic into functions - Currently modifiers are stripped -3. **Better Type Inference for abi.encode** - - Detect return types of function calls used as arguments - - Currently assumes uint256 for non-literal arguments +3. **Automatic Container Setup** + - Generate a complete `setupContainer()` function that registers all contracts + - Topological sort for correct initialization order ### Medium Priority @@ -448,8 +681,9 @@ Contracts that reference other contracts need manual injection: 5. **Source Maps** - Map TypeScript lines back to Solidity for debugging -6. **Function Overloading** - - Handle multiple functions with same name but different signatures +6. **Inheritance-Aware Dependency Resolution** + - Traverse inheritance tree to find all required dependencies + - Handle diamond inheritance patterns ### Low Priority @@ -460,11 +694,24 @@ Contracts that reference other contracts need manual injection: 8. **Fixed-Point Math** - Support `ufixed` and `fixed` types +9. **Custom Metadata Plugins** + - Allow users to define custom metadata extractors + - Support for game-specific metadata formats + --- ## Test Coverage -### Unit Tests (`test/run.ts`) +### Unit Tests (`test_transpiler.py`) + +```bash +cd transpiler && python3 test_transpiler.py +``` + +- ABI encode type inference (string, uint, address, mixed) +- Contract type imports (state variables, constructor params) + +### Runtime Tests (`test/run.ts`) ```bash cd transpiler && npm test @@ -500,6 +747,9 @@ cd transpiler && npm test ### Tests to Add +- [ ] ContractContainer circular dependency detection +- [ ] Factory function generation validation +- [ ] Metadata extraction accuracy - [ ] Negative number handling (signed integers) - [ ] Overflow behavior verification - [ ] Complex nested struct construction @@ -520,8 +770,14 @@ cd transpiler && npm test # Single file python3 transpiler/sol2ts.py src/path/to/File.sol -o transpiler/ts-output -d src -# Directory -python3 transpiler/sol2ts.py src/moves/ -o transpiler/ts-output -d src +# Single file with metadata +python3 transpiler/sol2ts.py src/path/to/File.sol -o transpiler/ts-output -d src --emit-metadata + +# Directory with metadata +python3 transpiler/sol2ts.py src/moves/ -o transpiler/ts-output -d src --emit-metadata + +# Metadata only (no TypeScript) +python3 transpiler/sol2ts.py src/moves/ --metadata-only -d src # Print to stdout (for debugging) python3 transpiler/sol2ts.py src/path/to/File.sol --stdout -d src @@ -534,6 +790,11 @@ python3 transpiler/sol2ts.py src/path/to/File.sol -o transpiler/ts-output -d src ```bash cd transpiler + +# Python unit tests +python3 test_transpiler.py + +# TypeScript runtime tests npm install npm test ``` @@ -542,20 +803,47 @@ npm test ``` transpiler/ -├── sol2ts.py # Main transpiler script +├── sol2ts.py # Main transpiler script +├── test_transpiler.py # Python unit tests ├── runtime/ -│ └── index.ts # Runtime library source (copy to ts-output as needed) -├── ts-output/ # Generated TypeScript files -│ ├── runtime.ts # Runtime library -│ ├── Structs.ts # All struct definitions -│ ├── Enums.ts # All enum definitions -│ ├── Constants.ts # All constants -│ ├── Engine.ts # Battle engine -│ └── *.ts # Transpiled contracts +│ └── index.ts # Runtime library (Contract, Storage, ContractContainer) +├── ts-output/ # Generated TypeScript files +│ ├── runtime.ts # Runtime library (copied from runtime/) +│ ├── Structs.ts # All struct definitions +│ ├── Enums.ts # All enum definitions +│ ├── Constants.ts # All constants +│ ├── Engine.ts # Battle engine +│ ├── dependency-manifest.json # Contract metadata (--emit-metadata) +│ ├── factories.ts # Factory functions (--emit-metadata) +│ └── *.ts # Transpiled contracts ├── test/ -│ ├── run.ts # Test runner -│ ├── e2e.ts # End-to-end tests -│ ├── engine-e2e.ts # Engine-specific tests +│ ├── run.ts # Test runner +│ ├── test-utils.ts # Test utilities +│ ├── e2e.ts # End-to-end tests +│ ├── engine-e2e.ts # Engine-specific tests │ └── battle-simulation.ts # Battle scenario tests └── package.json ``` + +### Key Runtime Exports + +```typescript +// Core classes +export class Contract { ... } // Base class for all contracts +export class Storage { ... } // EVM storage simulation +export class ContractContainer { ... } // Dependency injection container +export class Registry { ... } // Move/Effect registry +export class EventStream { ... } // Event logging + +// Global instances +export const globalContainer: ContractContainer; +export const globalEventStream: EventStream; +export const registry: Registry; + +// Utilities +export const ADDRESS_ZERO: string; +export function addressToUint(addr: string): bigint; +export function keccak256(...): string; +export function encodePacked(...): string; +export function encodeAbiParameters(...): string; +``` diff --git a/transpiler/runtime/index.ts b/transpiler/runtime/index.ts index d4debf9..2324702 100644 --- a/transpiler/runtime/index.ts +++ b/transpiler/runtime/index.ts @@ -597,3 +597,201 @@ export class Registry { // Global registry instance export const registry = new Registry(); + +// ============================================================================= +// DEPENDENCY INJECTION CONTAINER +// ============================================================================= + +/** + * Factory function type for creating contract instances + */ +export type ContractFactory = (...deps: any[]) => T; + +/** + * Container registration entry + */ +interface ContainerEntry { + instance?: any; + factory?: ContractFactory; + dependencies?: string[]; + singleton: boolean; +} + +/** + * Dependency injection container for managing contract instances and their dependencies. + * + * Supports: + * - Singleton instances (register once, resolve same instance) + * - Factory functions (create new instance on each resolve) + * - Automatic dependency resolution + * - Lazy instantiation + * + * Example usage: + * ```typescript + * const container = new ContractContainer(); + * + * // Register singletons (shared instances) + * container.registerSingleton('Engine', new Engine()); + * container.registerSingleton('TypeCalculator', new TypeCalculator()); + * + * // Register factory with dependencies + * container.registerFactory('UnboundedStrike', + * ['Engine', 'TypeCalculator', 'Baselight'], + * (engine, typeCalc, baselight) => new UnboundedStrike(engine, typeCalc, baselight) + * ); + * + * // Resolve with automatic dependency injection + * const move = container.resolve('UnboundedStrike'); + * ``` + */ +export class ContractContainer { + private entries: Map = new Map(); + private resolving: Set = new Set(); // For circular dependency detection + + /** + * Register a singleton instance + */ + registerSingleton(name: string, instance: T): void { + this.entries.set(name, { + instance, + singleton: true, + }); + } + + /** + * Register a factory function with dependencies + */ + registerFactory( + name: string, + dependencies: string[], + factory: ContractFactory + ): void { + this.entries.set(name, { + factory, + dependencies, + singleton: false, + }); + } + + /** + * Register a lazy singleton (created on first resolve) + */ + registerLazySingleton( + name: string, + dependencies: string[], + factory: ContractFactory + ): void { + this.entries.set(name, { + factory, + dependencies, + singleton: true, + }); + } + + /** + * Check if a name is registered + */ + has(name: string): boolean { + return this.entries.has(name); + } + + /** + * Resolve an instance by name + */ + resolve(name: string): T { + const entry = this.entries.get(name); + if (!entry) { + throw new Error(`ContractContainer: '${name}' is not registered`); + } + + // Return existing singleton instance + if (entry.singleton && entry.instance !== undefined) { + return entry.instance; + } + + // Check for circular dependencies + if (this.resolving.has(name)) { + const cycle = Array.from(this.resolving).join(' -> ') + ' -> ' + name; + throw new Error(`ContractContainer: Circular dependency detected: ${cycle}`); + } + + // Create new instance using factory + if (entry.factory) { + this.resolving.add(name); + try { + // Resolve dependencies + const deps = (entry.dependencies || []).map(dep => this.resolve(dep)); + const instance = entry.factory(...deps); + + // Store singleton instances + if (entry.singleton) { + entry.instance = instance; + } + + return instance; + } finally { + this.resolving.delete(name); + } + } + + throw new Error(`ContractContainer: '${name}' has no instance or factory`); + } + + /** + * Try to resolve an instance, returning undefined if not found + */ + tryResolve(name: string): T | undefined { + try { + return this.resolve(name); + } catch { + return undefined; + } + } + + /** + * Get all registered names + */ + getRegisteredNames(): string[] { + return Array.from(this.entries.keys()); + } + + /** + * Create a child container that inherits from this one + */ + createChild(): ContractContainer { + const child = new ContractContainer(); + // Copy all entries from parent + for (const [name, entry] of this.entries) { + child.entries.set(name, { ...entry }); + } + return child; + } + + /** + * Clear all registrations + */ + clear(): void { + this.entries.clear(); + this.resolving.clear(); + } + + /** + * Bulk register from a dependency manifest + */ + registerFromManifest( + manifest: Record, + factories: Record + ): void { + for (const [name, dependencies] of Object.entries(manifest)) { + const factory = factories[name]; + if (factory) { + this.registerFactory(name, dependencies, factory); + } + } + } +} + +/** + * Global container instance for convenience + */ +export const globalContainer = new ContractContainer(); diff --git a/transpiler/sol2ts.py b/transpiler/sol2ts.py index b02a811..afe7acc 100644 --- a/transpiler/sol2ts.py +++ b/transpiler/sol2ts.py @@ -15,10 +15,12 @@ import re import sys +import json from dataclasses import dataclass, field from typing import Optional, List, Dict, Any, Tuple, Set from enum import Enum, auto from pathlib import Path +from io import StringIO # ============================================================================= @@ -2144,6 +2146,31 @@ def merge(self, other: 'TypeRegistry') -> None: else: self.method_return_types[name] = ret_types.copy() + def build_qualified_name_cache(self, current_file_type: str = '') -> Dict[str, str]: + """Build a cached lookup dictionary for qualified names. + + This optimization avoids repeated set lookups in get_qualified_name(). + Returns a dict mapping name -> qualified name (with prefix if needed). + """ + cache: Dict[str, str] = {} + + # Add structs with Structs. prefix (unless current file is Structs) + if current_file_type != 'Structs': + for name in self.structs: + cache[name] = f'Structs.{name}' + + # Add enums with Enums. prefix (unless current file is Enums) + if current_file_type != 'Enums': + for name in self.enums: + cache[name] = f'Enums.{name}' + + # Add constants with Constants. prefix (unless current file is Constants) + if current_file_type != 'Constants': + for name in self.constants: + cache[name] = f'Constants.{name}' + + return cache + # ============================================================================= # CODE GENERATOR @@ -2166,6 +2193,9 @@ def __init__(self, registry: Optional[TypeRegistry] = None): # Type registry: maps variable names to their TypeName for array/mapping detection self.var_types: Dict[str, 'TypeName'] = {} + # Store the registry reference for later use + self._registry = registry + # Use provided registry or create empty one if registry: self.known_structs = registry.structs @@ -2200,6 +2230,9 @@ def __init__(self, registry: Optional[TypeRegistry] = None): # Current file type (to avoid self-referencing prefixes) self.current_file_type = '' + # OPTIMIZATION: Cached qualified name lookup (built lazily per file) + self._qualified_name_cache: Dict[str, str] = {} + def indent(self) -> str: return self.indent_str * self.indent_level @@ -2207,14 +2240,10 @@ def get_qualified_name(self, name: str) -> str: """Get the qualified name for a type, adding appropriate prefix if needed. Handles Structs., Enums., Constants. prefixes based on the current file context. + Uses cached lookup for performance optimization. """ - if name in self.known_structs and self.current_file_type != 'Structs': - return f'Structs.{name}' - if name in self.known_enums and self.current_file_type != 'Enums': - return f'Enums.{name}' - if name in self.known_constants and self.current_file_type != 'Constants': - return f'Constants.{name}' - return name + # OPTIMIZATION: Use cached lookup instead of repeated set membership checks + return self._qualified_name_cache.get(name, name) def _to_padded_address(self, val: str) -> str: """Convert a numeric or hex value to a 40-char padded hex address string.""" @@ -2255,6 +2284,22 @@ def generate(self, ast: SourceUnit) -> str: else: self.current_file_type = contract_name + # OPTIMIZATION: Build qualified name cache for this file + if self._registry: + self._qualified_name_cache = self._registry.build_qualified_name_cache(self.current_file_type) + else: + # Build cache manually from current sets + self._qualified_name_cache = {} + if self.current_file_type != 'Structs': + for name in self.known_structs: + self._qualified_name_cache[name] = f'Structs.{name}' + if self.current_file_type != 'Enums': + for name in self.known_enums: + self._qualified_name_cache[name] = f'Enums.{name}' + if self.current_file_type != 'Constants': + for name in self.known_constants: + self._qualified_name_cache[name] = f'Constants.{name}' + # Add header output.append('// Auto-generated by sol2ts transpiler') output.append('// Do not edit manually\n') @@ -4063,12 +4108,18 @@ class SolidityToTypeScriptTranspiler: def __init__(self, source_dir: str = '.', output_dir: str = './ts-output', discovery_dirs: Optional[List[str]] = None, - stubbed_contracts: Optional[List[str]] = None): + stubbed_contracts: Optional[List[str]] = None, + emit_metadata: bool = False): self.source_dir = Path(source_dir) self.output_dir = Path(output_dir) self.parsed_files: Dict[str, SourceUnit] = {} self.registry = TypeRegistry() self.stubbed_contracts = set(stubbed_contracts or []) + self.emit_metadata = emit_metadata + + # Metadata and dependency tracking + self.metadata_extractor: Optional[MetadataExtractor] = None + self.dependency_manifest = DependencyManifest() # Run type discovery on specified directories if discovery_dirs: @@ -4098,6 +4149,22 @@ def transpile_file(self, filepath: str, use_registry: bool = True) -> str: # Also discover types from this file if not already done self.registry.discover_from_ast(ast) + # Extract metadata if enabled + if self.emit_metadata: + if self.metadata_extractor is None: + self.metadata_extractor = MetadataExtractor(self.registry) + # Get relative path if possible, otherwise use filename + try: + if self.source_dir.exists() and Path(filepath).is_relative_to(self.source_dir): + rel_path = str(Path(filepath).relative_to(self.source_dir)) + else: + rel_path = Path(filepath).name + except (ValueError, TypeError): + rel_path = Path(filepath).name + metadata_list = self.metadata_extractor.extract_from_ast(ast, rel_path) + for metadata in metadata_list: + self.dependency_manifest.add_metadata(metadata) + # Check if any contract in this file is stubbed contract_name = Path(filepath).stem if contract_name in self.stubbed_contracts: @@ -4172,6 +4239,315 @@ def write_output(self, results: Dict[str, str]): f.write(content) print(f"Written: {filepath}") + def write_metadata(self, output_path: Optional[str] = None): + """Write the dependency manifest and metadata to JSON files.""" + if not self.emit_metadata: + return + + output_dir = Path(output_path) if output_path else self.output_dir + + # Write dependency manifest + manifest_path = output_dir / 'dependency-manifest.json' + manifest_path.parent.mkdir(parents=True, exist_ok=True) + with open(manifest_path, 'w') as f: + json.dump(self.dependency_manifest.to_dict(), f, indent=2) + print(f"Written: {manifest_path}") + + # Write factory functions + factories_path = output_dir / 'factories.ts' + with open(factories_path, 'w') as f: + f.write(self.dependency_manifest.generate_factories_ts()) + print(f"Written: {factories_path}") + + def get_metadata_json(self) -> str: + """Get the dependency manifest as a JSON string.""" + return json.dumps(self.dependency_manifest.to_dict(), indent=2) + + +# ============================================================================= +# METADATA AND DEPENDENCY EXTRACTION +# ============================================================================= + +@dataclass +class ContractDependency: + """Represents a dependency required by a contract's constructor.""" + name: str # Parameter name + type_name: str # Type name (e.g., 'IEngine', 'Baselight') + is_interface: bool # Whether the type is an interface + + def to_dict(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'typeName': self.type_name, + 'isInterface': self.is_interface + } + + +@dataclass +class ContractMetadata: + """Metadata extracted from a contract for dependency injection and UI purposes.""" + name: str + file_path: str + inherits_from: List[str] + dependencies: List[ContractDependency] + constants: Dict[str, Any] + public_methods: List[str] + is_move: bool # Implements IMoveSet + is_effect: bool # Implements IEffect + move_properties: Optional[Dict[str, Any]] = None # Extracted move metadata if is_move + + def to_dict(self) -> Dict[str, Any]: + result = { + 'name': self.name, + 'filePath': self.file_path, + 'inheritsFrom': self.inherits_from, + 'dependencies': [d.to_dict() for d in self.dependencies], + 'constants': self.constants, + 'publicMethods': self.public_methods, + 'isMove': self.is_move, + 'isEffect': self.is_effect, + } + if self.move_properties: + result['moveProperties'] = self.move_properties + return result + + +class MetadataExtractor: + """Extracts metadata from parsed Solidity ASTs for dependency injection and UI purposes.""" + + def __init__(self, registry: TypeRegistry): + self.registry = registry + self.move_interfaces = {'IMoveSet'} + self.effect_interfaces = {'IEffect'} + self.standard_attack_bases = {'StandardAttack'} + + def extract_from_ast(self, ast: 'SourceUnit', file_path: str) -> List[ContractMetadata]: + """Extract metadata from all contracts in an AST.""" + results = [] + for contract in ast.contracts: + if contract.kind != 'interface': + metadata = self._extract_contract_metadata(contract, file_path) + results.append(metadata) + return results + + def _extract_contract_metadata(self, contract: 'ContractDefinition', file_path: str) -> ContractMetadata: + """Extract metadata from a single contract.""" + # Determine if this is a move or effect + is_move = self._implements_interface(contract, self.move_interfaces) + is_effect = self._implements_interface(contract, self.effect_interfaces) + + # Extract dependencies from constructor + dependencies = self._extract_constructor_dependencies(contract) + + # Extract constants + constants = self._extract_constants(contract) + + # Extract public methods + public_methods = [ + f.name for f in contract.functions + if f.name and f.visibility in ('public', 'external') + ] + + # Extract move properties if applicable + move_properties = None + if is_move: + move_properties = self._extract_move_properties(contract) + + return ContractMetadata( + name=contract.name, + file_path=file_path, + inherits_from=contract.base_contracts or [], + dependencies=dependencies, + constants=constants, + public_methods=public_methods, + is_move=is_move, + is_effect=is_effect, + move_properties=move_properties + ) + + def _implements_interface(self, contract: 'ContractDefinition', interfaces: Set[str]) -> bool: + """Check if a contract implements any of the given interfaces.""" + if not contract.base_contracts: + return False + + for base in contract.base_contracts: + if base in interfaces: + return True + # Check if base contract is a known move base (like StandardAttack) + if base in self.standard_attack_bases: + return True + # Recursively check if base implements the interface + if base in self.registry.contracts: + # Check if the base contract's bases include the interface + if base in self.registry.contract_methods: + # This is a simplified check - a full implementation would + # traverse the inheritance tree + pass + return False + + def _extract_constructor_dependencies(self, contract: 'ContractDefinition') -> List[ContractDependency]: + """Extract dependencies from constructor parameters.""" + dependencies = [] + if not contract.constructor: + return dependencies + + for param in contract.constructor.parameters: + type_name = param.type_name.name if param.type_name else 'unknown' + + # Skip basic types + if type_name in ('uint256', 'uint128', 'uint64', 'uint32', 'uint16', 'uint8', + 'int256', 'int128', 'int64', 'int32', 'int16', 'int8', + 'bool', 'address', 'bytes32', 'bytes', 'string'): + continue + + is_interface = (type_name.startswith('I') and len(type_name) > 1 and + type_name[1].isupper()) or type_name in self.registry.interfaces + + dependencies.append(ContractDependency( + name=param.name, + type_name=type_name, + is_interface=is_interface + )) + + return dependencies + + def _extract_constants(self, contract: 'ContractDefinition') -> Dict[str, Any]: + """Extract constant values from a contract.""" + constants = {} + for var in contract.state_variables: + if var.mutability == 'constant' and var.initial_value: + value = self._extract_literal_value(var.initial_value) + if value is not None: + constants[var.name] = value + return constants + + def _extract_literal_value(self, expr: 'Expression') -> Any: + """Extract a literal value from an expression.""" + if isinstance(expr, Literal): + if expr.kind == 'number': + try: + return int(expr.value) + except ValueError: + return expr.value + elif expr.kind == 'hex': + return expr.value + elif expr.kind == 'string': + # Remove surrounding quotes + return expr.value[1:-1] if expr.value.startswith('"') else expr.value + elif expr.kind == 'bool': + return expr.value == 'true' + return None + + def _extract_move_properties(self, contract: 'ContractDefinition') -> Dict[str, Any]: + """Extract move-specific properties from a contract.""" + properties: Dict[str, Any] = {} + + # Extract from constants + constants = self._extract_constants(contract) + for name, value in constants.items(): + properties[name] = value + + # Try to extract properties from getter functions + for func in contract.functions: + if not func.name or func.visibility not in ('public', 'external', 'internal'): + continue + + # Check for pure/view functions that return single values + if func.mutability not in ('pure', 'view'): + continue + + if func.return_parameters and len(func.return_parameters) == 1: + # Check for simple return statements + if func.body and func.body.statements: + for stmt in func.body.statements: + if isinstance(stmt, ReturnStatement) and stmt.expression: + value = self._extract_literal_value(stmt.expression) + if value is not None: + properties[func.name] = value + + return properties + + +class DependencyManifest: + """Generates a dependency manifest for all contracts.""" + + def __init__(self): + self.contracts: Dict[str, ContractMetadata] = {} + + def add_metadata(self, metadata: ContractMetadata) -> None: + """Add contract metadata to the manifest.""" + self.contracts[metadata.name] = metadata + + def to_dict(self) -> Dict[str, Any]: + """Convert to a dictionary for JSON serialization.""" + return { + 'contracts': {name: m.to_dict() for name, m in self.contracts.items()}, + 'moves': {name: m.to_dict() for name, m in self.contracts.items() if m.is_move}, + 'effects': {name: m.to_dict() for name, m in self.contracts.items() if m.is_effect}, + 'dependencyGraph': self._build_dependency_graph() + } + + def _build_dependency_graph(self) -> Dict[str, List[str]]: + """Build a graph of contract dependencies.""" + graph = {} + for name, metadata in self.contracts.items(): + deps = [d.type_name for d in metadata.dependencies] + if deps: + graph[name] = deps + return graph + + def generate_factories_ts(self) -> str: + """Generate TypeScript factory functions for dependency injection.""" + lines = [ + '// Auto-generated by sol2ts transpiler', + '// Factory functions for dependency injection', + '', + "import { ContractContainer } from './runtime';", + '' + ] + + # Import all contracts + for name in sorted(self.contracts.keys()): + lines.append(f"import {{ {name} }} from './{name}';") + + lines.append('') + lines.append('// Dependency manifest') + lines.append('export const dependencyManifest = {') + + for name, metadata in sorted(self.contracts.items()): + deps = [d.type_name for d in metadata.dependencies] + if deps: + lines.append(f" '{name}': {deps},") + + lines.append('};') + lines.append('') + + # Generate factory functions + lines.append('// Factory functions') + for name, metadata in sorted(self.contracts.items()): + if metadata.dependencies: + params = ', '.join([ + f'{d.name}: {d.type_name}' + for d in metadata.dependencies + ]) + args = ', '.join([d.name for d in metadata.dependencies]) + lines.append(f'export function create{name}({params}): {name} {{') + lines.append(f' return new {name}({args});') + lines.append('}') + lines.append('') + + # Generate container setup function + lines.append('// Container setup') + lines.append('export function setupContainer(container: ContractContainer): void {') + for name, metadata in sorted(self.contracts.items()): + if metadata.dependencies: + dep_types = ', '.join([f"'{d.type_name}'" for d in metadata.dependencies]) + lines.append(f" container.registerFactory('{name}', [{dep_types}], ({', '.join([d.name for d in metadata.dependencies])}) => new {name}({', '.join([d.name for d in metadata.dependencies])}));") + lines.append('}') + lines.append('') + + return '\n'.join(lines) + # ============================================================================= # CLI INTERFACE @@ -4188,6 +4564,10 @@ def main(): help='Directory to scan for type discovery (can be specified multiple times)') parser.add_argument('--stub', action='append', metavar='CONTRACT', help='Contract name to generate as minimal stub (can be specified multiple times)') + parser.add_argument('--emit-metadata', action='store_true', + help='Emit dependency manifest and factory functions') + parser.add_argument('--metadata-only', action='store_true', + help='Only emit metadata, skip TypeScript generation') args = parser.parse_args() @@ -4196,9 +4576,14 @@ def main(): # Collect discovery directories and stubbed contracts discovery_dirs = args.discover or [] stubbed_contracts = args.stub or [] + emit_metadata = args.emit_metadata or args.metadata_only if input_path.is_file(): - transpiler = SolidityToTypeScriptTranspiler(discovery_dirs=discovery_dirs, stubbed_contracts=stubbed_contracts) + transpiler = SolidityToTypeScriptTranspiler( + discovery_dirs=discovery_dirs, + stubbed_contracts=stubbed_contracts, + emit_metadata=emit_metadata + ) # If no discovery dirs specified, try to find the project root # by looking for common Solidity project directories @@ -4216,7 +4601,10 @@ def main(): ts_code = transpiler.transpile_file(str(input_path)) - if args.stdout: + if args.metadata_only: + # Only output metadata + print(transpiler.get_metadata_json()) + elif args.stdout: print(ts_code) else: output_path = Path(args.output) / input_path.with_suffix('.ts').name @@ -4225,12 +4613,23 @@ def main(): f.write(ts_code) print(f"Written: {output_path}") + if emit_metadata: + transpiler.write_metadata(args.output) + elif input_path.is_dir(): - transpiler = SolidityToTypeScriptTranspiler(str(input_path), args.output, discovery_dirs, stubbed_contracts) + transpiler = SolidityToTypeScriptTranspiler( + str(input_path), args.output, discovery_dirs, stubbed_contracts, + emit_metadata=emit_metadata + ) # Also discover from the input directory itself transpiler.discover_types(str(input_path)) - results = transpiler.transpile_directory() - transpiler.write_output(results) + + if not args.metadata_only: + results = transpiler.transpile_directory() + transpiler.write_output(results) + + if emit_metadata: + transpiler.write_metadata(args.output) else: print(f"Error: {args.input} is not a valid file or directory")