create-dubhe 1.2.0-pre.98 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +16 -16
  4. package/templates/101/sui-template/docker-compose.testnet.yml +73 -0
  5. package/templates/101/sui-template/packages/client/src/app/page.tsx +545 -41
  6. package/templates/101/sui-template/packages/client/src/app/providers.tsx +12 -4
  7. package/templates/101/sui-template/packages/contracts/Pub.localnet.toml +4 -4
  8. package/templates/101/sui-template/packages/contracts/counter.lock.json +20 -0
  9. package/templates/101/sui-template/packages/contracts/deployment.ts +8 -3
  10. package/templates/101/sui-template/packages/contracts/dubhe.config.json +59 -13
  11. package/templates/101/sui-template/packages/contracts/dubhe.config.ts +1 -4
  12. package/templates/101/sui-template/packages/contracts/metadata.json +1015 -68
  13. package/templates/101/sui-template/packages/contracts/package.json +7 -4
  14. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/dapp_key.move +45 -31
  15. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/error.move +8 -0
  16. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/genesis.move +30 -13
  17. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/init_test.move +19 -14
  18. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2.move +198 -0
  19. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2withkey.move +90 -0
  20. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/value.move +30 -20
  21. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
  22. package/templates/101/sui-template/packages/contracts/src/counter/sources/scripts/deploy_hook.move +6 -11
  23. package/templates/101/sui-template/packages/contracts/src/counter/sources/systems/counter.move +23 -11
  24. package/templates/101/sui-template/packages/contracts/src/counter/sources/tests/counter.move +15 -14
  25. package/templates/101/sui-template/packages/contracts/src/dubhe/Move.lock +33 -30
  26. package/templates/101/sui-template/packages/contracts/src/dubhe/Move.toml +8 -3
  27. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/dapp_key.move +45 -31
  28. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/error.move +170 -0
  29. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/genesis.move +16 -13
  30. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/init_test.move +8 -14
  31. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/account_key.move +1 -3
  32. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/dapp_service.move +1776 -137
  33. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/data_key.move +1 -20
  34. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/events.move +864 -25
  35. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/scripts/deploy_hook.move +15 -16
  36. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/scripts/migrate.move +0 -3
  37. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/address_system.move +25 -17
  38. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/dapp_system.move +2802 -343
  39. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/utils.move +1 -1
  40. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/address.move +173 -55
  41. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/dapp_test.move +769 -0
  42. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/fee_test.move +2965 -0
  43. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/integration_test.move +303 -0
  44. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/listing_test.move +1422 -0
  45. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/scene_session_test.move +269 -0
  46. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/session_test.move +624 -0
  47. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/storage_test.move +700 -0
  48. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/typed_object_test.move +177 -0
  49. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/typed_scene_test.move +452 -0
  50. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/user_storage_test.move +160 -0
  51. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/utils/math.move +4 -3
  52. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/utils/type_info.move +2 -2
  53. package/templates/contract/sui-template/dubhe.config.ts +0 -1
  54. package/templates/contract/sui-template/package.json +3 -3
  55. package/templates/contract/sui-template/src/dubhe/Move.lock +33 -30
  56. package/templates/contract/sui-template/src/dubhe/Move.toml +8 -3
  57. package/templates/contract/sui-template/src/dubhe/sources/codegen/dapp_key.move +45 -31
  58. package/templates/contract/sui-template/src/dubhe/sources/codegen/error.move +170 -0
  59. package/templates/contract/sui-template/src/dubhe/sources/codegen/genesis.move +16 -13
  60. package/templates/contract/sui-template/src/dubhe/sources/codegen/init_test.move +8 -14
  61. package/templates/contract/sui-template/src/dubhe/sources/core/account_key.move +1 -3
  62. package/templates/contract/sui-template/src/dubhe/sources/core/dapp_service.move +1776 -137
  63. package/templates/contract/sui-template/src/dubhe/sources/core/data_key.move +1 -20
  64. package/templates/contract/sui-template/src/dubhe/sources/core/events.move +864 -25
  65. package/templates/contract/sui-template/src/dubhe/sources/scripts/deploy_hook.move +15 -16
  66. package/templates/contract/sui-template/src/dubhe/sources/scripts/migrate.move +0 -3
  67. package/templates/contract/sui-template/src/dubhe/sources/systems/address_system.move +25 -17
  68. package/templates/contract/sui-template/src/dubhe/sources/systems/dapp_system.move +2802 -343
  69. package/templates/contract/sui-template/src/dubhe/sources/systems/utils.move +1 -1
  70. package/templates/contract/sui-template/src/dubhe/sources/tests/address.move +173 -55
  71. package/templates/contract/sui-template/src/dubhe/sources/tests/dapp_test.move +769 -0
  72. package/templates/contract/sui-template/src/dubhe/sources/tests/fee_test.move +2965 -0
  73. package/templates/contract/sui-template/src/dubhe/sources/tests/integration_test.move +303 -0
  74. package/templates/contract/sui-template/src/dubhe/sources/tests/listing_test.move +1422 -0
  75. package/templates/contract/sui-template/src/dubhe/sources/tests/scene_session_test.move +269 -0
  76. package/templates/contract/sui-template/src/dubhe/sources/tests/session_test.move +624 -0
  77. package/templates/contract/sui-template/src/dubhe/sources/tests/storage_test.move +700 -0
  78. package/templates/contract/sui-template/src/dubhe/sources/tests/typed_object_test.move +177 -0
  79. package/templates/contract/sui-template/src/dubhe/sources/tests/typed_scene_test.move +452 -0
  80. package/templates/contract/sui-template/src/dubhe/sources/tests/user_storage_test.move +160 -0
  81. package/templates/contract/sui-template/src/dubhe/sources/utils/math.move +4 -3
  82. package/templates/contract/sui-template/src/dubhe/sources/utils/type_info.move +2 -2
  83. package/templates/contract/sui-template/src/template/Move.toml +12 -0
  84. package/templates/contract/sui-template/src/template/sources/codegen/dapp_key.move +47 -0
  85. package/templates/contract/sui-template/src/template/sources/codegen/genesis.move +34 -0
  86. package/templates/contract/sui-template/src/template/sources/codegen/init_test.move +23 -0
  87. package/templates/contract/sui-template/src/template/sources/codegen/user_storage_init.move +27 -0
  88. package/templates/contract/sui-template/src/template/sources/scripts/deploy_hook.move +13 -0
  89. package/templates/contract/sui-template/src/template/sources/scripts/migrate.move +7 -0
  90. package/templates/nextjs/sui-card-duel/.gitignore_ +51 -0
  91. package/templates/nextjs/sui-card-duel/README.md +213 -0
  92. package/templates/nextjs/sui-card-duel/docker-compose.yml +34 -0
  93. package/templates/nextjs/sui-card-duel/mprocs.yaml +17 -0
  94. package/templates/nextjs/sui-card-duel/package.json +43 -0
  95. package/templates/nextjs/sui-card-duel/packages/client/.gitignore_ +48 -0
  96. package/templates/nextjs/sui-card-duel/packages/client/.prettierignore +1 -0
  97. package/templates/nextjs/sui-card-duel/packages/client/.prettierrc.json +8 -0
  98. package/templates/nextjs/sui-card-duel/packages/client/eslint.config.mjs +22 -0
  99. package/templates/nextjs/sui-card-duel/packages/client/next.config.mjs +8 -0
  100. package/templates/nextjs/sui-card-duel/packages/client/package.json +47 -0
  101. package/templates/nextjs/sui-card-duel/packages/client/postcss.config.mjs +8 -0
  102. package/templates/nextjs/sui-card-duel/packages/client/src/app/admin/ArenaPanel.tsx +307 -0
  103. package/templates/nextjs/sui-card-duel/packages/client/src/app/admin/page.tsx +622 -0
  104. package/templates/nextjs/sui-card-duel/packages/client/src/app/brawl/[id]/page.tsx +559 -0
  105. package/templates/nextjs/sui-card-duel/packages/client/src/app/collection/page.tsx +202 -0
  106. package/templates/nextjs/sui-card-duel/packages/client/src/app/components/CardView.tsx +66 -0
  107. package/templates/nextjs/sui-card-duel/packages/client/src/app/components/GameShell.tsx +294 -0
  108. package/templates/nextjs/sui-card-duel/packages/client/src/app/components/icons.tsx +101 -0
  109. package/templates/nextjs/sui-card-duel/packages/client/src/app/duel/[id]/page.tsx +477 -0
  110. package/templates/nextjs/sui-card-duel/packages/client/src/app/favicon.ico +0 -0
  111. package/templates/nextjs/sui-card-duel/packages/client/src/app/globals.css +85 -0
  112. package/templates/nextjs/sui-card-duel/packages/client/src/app/hooks/useGame.ts +313 -0
  113. package/templates/nextjs/sui-card-duel/packages/client/src/app/layout.tsx +24 -0
  114. package/templates/nextjs/sui-card-duel/packages/client/src/app/leaderboard/page.tsx +123 -0
  115. package/templates/nextjs/sui-card-duel/packages/client/src/app/lib/game.ts +77 -0
  116. package/templates/nextjs/sui-card-duel/packages/client/src/app/lib/scenes.ts +226 -0
  117. package/templates/nextjs/sui-card-duel/packages/client/src/app/market/page.tsx +418 -0
  118. package/templates/nextjs/sui-card-duel/packages/client/src/app/page.tsx +427 -0
  119. package/templates/nextjs/sui-card-duel/packages/client/src/app/providers.tsx +69 -0
  120. package/templates/nextjs/sui-card-duel/packages/client/tailwind.config.ts +21 -0
  121. package/templates/nextjs/sui-card-duel/packages/client/tsconfig.json +30 -0
  122. package/templates/nextjs/sui-card-duel/packages/contracts/.gitignore_ +51 -0
  123. package/templates/nextjs/sui-card-duel/packages/contracts/.prettierignore +1 -0
  124. package/templates/nextjs/sui-card-duel/packages/contracts/.prettierrc.json +8 -0
  125. package/templates/nextjs/sui-card-duel/packages/contracts/Dockerfile +5 -0
  126. package/templates/nextjs/sui-card-duel/packages/contracts/Pub.localnet.toml +13 -0
  127. package/templates/nextjs/sui-card-duel/packages/contracts/card_duel.lock.json +66 -0
  128. package/templates/nextjs/sui-card-duel/packages/contracts/docker-compose.yml +34 -0
  129. package/templates/nextjs/sui-card-duel/packages/contracts/dubhe.config.ts +150 -0
  130. package/templates/nextjs/sui-card-duel/packages/contracts/logs/combined.log +31383 -0
  131. package/templates/nextjs/sui-card-duel/packages/contracts/package.json +49 -0
  132. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/config.ts +17 -0
  133. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-brawl.ts +270 -0
  134. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-duel.ts +272 -0
  135. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-lobby.ts +177 -0
  136. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-market.ts +219 -0
  137. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-session.ts +342 -0
  138. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/query-dapp-revenue.ts +95 -0
  139. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/seed.ts +92 -0
  140. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/withdraw-dapp-revenue.ts +56 -0
  141. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/Move.toml +15 -0
  142. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/dapp_key.move +46 -0
  143. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/card_kind.move +47 -0
  144. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/match_state.move +37 -0
  145. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/rarity.move +37 -0
  146. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/error.move +126 -0
  147. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/genesis.move +33 -0
  148. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/init_test.move +23 -0
  149. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/objects/arena.move +137 -0
  150. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/permits/brawl_permit.move +125 -0
  151. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/permits/duel_permit.move +125 -0
  152. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/battle_state.move +255 -0
  153. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/card.move +282 -0
  154. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/deck.move +64 -0
  155. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/game_config.move +267 -0
  156. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/gold.move +205 -0
  157. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/profile.move +255 -0
  158. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/scenes/brawl.move +313 -0
  159. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/scenes/duel.move +313 -0
  160. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/user_storage_init.move +26 -0
  161. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/scripts/deploy_hook.move +32 -0
  162. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/scripts/migrate.move +7 -0
  163. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/arena_system.move +57 -0
  164. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/brawl_system.move +559 -0
  165. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/card_system.move +146 -0
  166. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/duel_system.move +515 -0
  167. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/market_system.move +115 -0
  168. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/player_system.move +37 -0
  169. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/brawl_tests.move +385 -0
  170. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/duel_tests.move +656 -0
  171. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/test_helpers.move +31 -0
  172. package/templates/nextjs/sui-card-duel/packages/contracts/sui-indexer-schema.graphql +5973 -0
  173. package/templates/nextjs/sui-card-duel/pnpm-workspace.yaml +2 -0
  174. package/templates/nextjs/sui-farm/.gitignore_ +51 -0
  175. package/templates/nextjs/sui-farm/README.md +15 -0
  176. package/templates/nextjs/sui-farm/UPGRADE_NOTES.md +97 -0
  177. package/templates/nextjs/sui-farm/docker-compose.yml +34 -0
  178. package/templates/nextjs/sui-farm/mprocs.yaml +17 -0
  179. package/templates/nextjs/sui-farm/package.json +43 -0
  180. package/templates/nextjs/sui-farm/packages/client/.gitignore_ +48 -0
  181. package/templates/nextjs/sui-farm/packages/client/.prettierignore +1 -0
  182. package/templates/nextjs/sui-farm/packages/client/.prettierrc.json +8 -0
  183. package/templates/nextjs/sui-farm/packages/client/eslint.config.mjs +22 -0
  184. package/templates/nextjs/sui-farm/packages/client/next.config.mjs +8 -0
  185. package/templates/nextjs/sui-farm/packages/client/package.json +47 -0
  186. package/templates/nextjs/sui-farm/packages/client/postcss.config.mjs +8 -0
  187. package/templates/nextjs/sui-farm/packages/client/src/app/admin/page.tsx +618 -0
  188. package/templates/nextjs/sui-farm/packages/client/src/app/components/FarmLand.tsx +526 -0
  189. package/templates/nextjs/sui-farm/packages/client/src/app/components/FarmPlotCard.tsx +215 -0
  190. package/templates/nextjs/sui-farm/packages/client/src/app/components/PetAvatar.tsx +868 -0
  191. package/templates/nextjs/sui-farm/packages/client/src/app/components/PetPanel.tsx +1360 -0
  192. package/templates/nextjs/sui-farm/packages/client/src/app/components/PlantModal.tsx +147 -0
  193. package/templates/nextjs/sui-farm/packages/client/src/app/components/ProxyCard.tsx +684 -0
  194. package/templates/nextjs/sui-farm/packages/client/src/app/components/RanchLand.tsx +822 -0
  195. package/templates/nextjs/sui-farm/packages/client/src/app/components/ResourceHUD.tsx +125 -0
  196. package/templates/nextjs/sui-farm/packages/client/src/app/components/ShopPanel.tsx +232 -0
  197. package/templates/nextjs/sui-farm/packages/client/src/app/components/icons/GameIcons.tsx +574 -0
  198. package/templates/nextjs/sui-farm/packages/client/src/app/favicon.ico +0 -0
  199. package/templates/nextjs/sui-farm/packages/client/src/app/globals.css +423 -0
  200. package/templates/nextjs/sui-farm/packages/client/src/app/hooks/useWorldPermitId.ts +66 -0
  201. package/templates/nextjs/sui-farm/packages/client/src/app/layout.tsx +23 -0
  202. package/templates/nextjs/sui-farm/packages/client/src/app/leaderboard/page.tsx +185 -0
  203. package/templates/nextjs/sui-farm/packages/client/src/app/lib/crops.ts +97 -0
  204. package/templates/nextjs/sui-farm/packages/client/src/app/market/page.tsx +763 -0
  205. package/templates/nextjs/sui-farm/packages/client/src/app/page.tsx +1173 -0
  206. package/templates/nextjs/sui-farm/packages/client/src/app/providers.tsx +69 -0
  207. package/templates/nextjs/sui-farm/packages/client/src/app/state/index.tsx +5 -0
  208. package/templates/nextjs/sui-farm/packages/client/tailwind.config.ts +25 -0
  209. package/templates/nextjs/sui-farm/packages/client/tsconfig.json +30 -0
  210. package/templates/nextjs/sui-farm/packages/contracts/.gitignore_ +51 -0
  211. package/templates/nextjs/sui-farm/packages/contracts/.prettierignore +1 -0
  212. package/templates/nextjs/sui-farm/packages/contracts/.prettierrc.json +8 -0
  213. package/templates/nextjs/sui-farm/packages/contracts/Dockerfile +5 -0
  214. package/templates/nextjs/sui-farm/packages/contracts/Pub.localnet.toml +13 -0
  215. package/templates/nextjs/sui-farm/packages/contracts/counter.lock.json +14 -0
  216. package/templates/nextjs/sui-farm/packages/contracts/docker-compose.yml +34 -0
  217. package/templates/nextjs/sui-farm/packages/contracts/dubhe.config.ts +178 -0
  218. package/templates/nextjs/sui-farm/packages/contracts/harvest.lock.json +120 -0
  219. package/templates/nextjs/sui-farm/packages/contracts/package.json +48 -0
  220. package/templates/nextjs/sui-farm/packages/contracts/scripts/config.ts +17 -0
  221. package/templates/nextjs/sui-farm/packages/contracts/scripts/query-dapp-revenue.ts +95 -0
  222. package/templates/nextjs/sui-farm/packages/contracts/scripts/read-permit.ts +72 -0
  223. package/templates/nextjs/sui-farm/packages/contracts/scripts/seed.ts +85 -0
  224. package/templates/nextjs/sui-farm/packages/contracts/scripts/withdraw-dapp-revenue.ts +56 -0
  225. package/templates/nextjs/sui-farm/packages/contracts/src/counter/Move.toml +14 -0
  226. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/dapp_key.move +31 -0
  227. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/error.move +10 -0
  228. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/genesis.move +33 -0
  229. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/init_test.move +23 -0
  230. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/resources/counter1.move +64 -0
  231. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/resources/counter2.move +70 -0
  232. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
  233. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/scripts/deploy_hook.move +8 -0
  234. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/scripts/migrate.move +8 -0
  235. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/systems/counter.move +13 -0
  236. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/tests/counter.move +28 -0
  237. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/Move.toml +14 -0
  238. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/dapp_key.move +46 -0
  239. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/crop_type.move +47 -0
  240. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/egg_type.move +42 -0
  241. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/pet_rarity.move +37 -0
  242. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/pet_species.move +52 -0
  243. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/error.move +110 -0
  244. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/genesis.move +33 -0
  245. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/init_test.move +23 -0
  246. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/permits/world.move +125 -0
  247. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/carrot.move +136 -0
  248. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/carrot_seed.move +80 -0
  249. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/common_egg.move +136 -0
  250. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/corn.move +136 -0
  251. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/corn_seed.move +80 -0
  252. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/crow_charges.move +162 -0
  253. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/crow_damage.move +207 -0
  254. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/farm_plot.move +261 -0
  255. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/gold.move +80 -0
  256. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet.move +467 -0
  257. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_config.move +372 -0
  258. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_hatch.move +162 -0
  259. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_slot_index.move +83 -0
  260. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_slots.move +64 -0
  261. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/profile.move +162 -0
  262. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pumpkin.move +136 -0
  263. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pumpkin_seed.move +80 -0
  264. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/rare_egg.move +136 -0
  265. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/scarecrow.move +64 -0
  266. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/season_config.move +197 -0
  267. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/season_stats.move +64 -0
  268. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/seasonal_egg.move +136 -0
  269. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/shop_config.move +267 -0
  270. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/trophy.move +245 -0
  271. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/wheat.move +136 -0
  272. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/wheat_seed.move +80 -0
  273. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/world_permit_id.move +64 -0
  274. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/user_storage_init.move +26 -0
  275. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/scripts/deploy_hook.move +45 -0
  276. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/scripts/migrate.move +7 -0
  277. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/crow_system.move +96 -0
  278. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/farm_system.move +182 -0
  279. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/market_system.move +172 -0
  280. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/pet_system.move +495 -0
  281. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/season_system.move +70 -0
  282. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/shop_system.move +112 -0
  283. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/world_system.move +34 -0
  284. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/tests/farm_tests.move +1192 -0
  285. package/templates/nextjs/sui-farm/pnpm-workspace.yaml +2 -0
  286. package/templates/nextjs/sui-template/packages/client/src/app/components/ProxyCard.tsx +683 -0
  287. package/templates/nextjs/sui-template/packages/client/src/app/page.tsx +510 -5
  288. package/templates/nextjs/sui-template/packages/client/src/app/providers.tsx +13 -5
  289. package/templates/nextjs/sui-template/packages/contracts/Pub.localnet.toml +4 -4
  290. package/templates/nextjs/sui-template/packages/contracts/counter.lock.json +14 -0
  291. package/templates/nextjs/sui-template/packages/contracts/deployment.ts +8 -3
  292. package/templates/nextjs/sui-template/packages/contracts/dubhe.config.json +9 -9
  293. package/templates/nextjs/sui-template/packages/contracts/dubhe.config.ts +0 -4
  294. package/templates/nextjs/sui-template/packages/contracts/metadata.json +180 -124
  295. package/templates/nextjs/sui-template/packages/contracts/package.json +8 -4
  296. package/templates/nextjs/sui-template/packages/contracts/src/counter/Move.toml +4 -0
  297. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/dapp_key.move +45 -31
  298. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/error.move +10 -0
  299. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/genesis.move +30 -13
  300. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/init_test.move +19 -14
  301. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter1.move +30 -20
  302. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2.move +35 -24
  303. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
  304. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/scripts/deploy_hook.move +6 -10
  305. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/systems/counter.move +7 -20
  306. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/tests/counter.move +14 -27
  307. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/Move.lock +33 -30
  308. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/Move.toml +8 -3
  309. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/dapp_key.move +45 -31
  310. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/error.move +170 -0
  311. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/genesis.move +16 -13
  312. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/init_test.move +8 -14
  313. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/account_key.move +1 -3
  314. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/dapp_service.move +1776 -137
  315. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/data_key.move +1 -20
  316. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/events.move +864 -25
  317. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/scripts/deploy_hook.move +15 -16
  318. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/scripts/migrate.move +0 -3
  319. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/address_system.move +25 -17
  320. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/dapp_system.move +2802 -343
  321. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/utils.move +1 -1
  322. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/address.move +173 -55
  323. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/dapp_test.move +769 -0
  324. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/fee_test.move +2965 -0
  325. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/integration_test.move +303 -0
  326. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/listing_test.move +1422 -0
  327. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/scene_session_test.move +269 -0
  328. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/session_test.move +624 -0
  329. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/storage_test.move +700 -0
  330. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/typed_object_test.move +177 -0
  331. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/typed_scene_test.move +452 -0
  332. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/user_storage_test.move +160 -0
  333. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/utils/math.move +4 -3
  334. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/utils/type_info.move +2 -2
  335. package/templates/101/sui-template/packages/contracts/src/counter/Move.lock +0 -41
  336. package/templates/101/sui-template/packages/contracts/src/counter/Published.toml +0 -9
  337. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/errors.move +0 -12
  338. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/errors.move +0 -140
  339. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
  340. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
  341. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
  342. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
  343. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/dapp_store.move +0 -29
  344. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/session_system.move +0 -44
  345. package/templates/contract/sui-template/src/dubhe/sources/codegen/errors.move +0 -140
  346. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
  347. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
  348. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
  349. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
  350. package/templates/contract/sui-template/src/dubhe/sources/core/dapp_store.move +0 -29
  351. package/templates/contract/sui-template/src/dubhe/sources/systems/session_system.move +0 -44
  352. package/templates/nextjs/sui-template/packages/contracts/src/counter/Move.lock +0 -47
  353. package/templates/nextjs/sui-template/packages/contracts/src/counter/Published.toml +0 -9
  354. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/errors.move +0 -12
  355. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/errors.move +0 -140
  356. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
  357. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
  358. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
  359. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
  360. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/dapp_store.move +0 -29
  361. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/session_system.move +0 -44
@@ -0,0 +1,1360 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import {
6
+ PetAvatar,
7
+ EggIcon,
8
+ IconPaw,
9
+ IconFeed,
10
+ IconTag,
11
+ IconWarning,
12
+ IconHeart,
13
+ IconStar
14
+ } from './PetAvatar';
15
+ import { IconGold } from './icons/GameIcons';
16
+
17
+ // ── Constants ─────────────────────────────────────────────────────────────────
18
+
19
+ export const SPECIES_NAME: Record<number, string> = {
20
+ 0: 'Unknown',
21
+ 1: 'Bunny',
22
+ 2: 'Chick',
23
+ 3: 'Fox',
24
+ 4: 'Deer',
25
+ 5: 'Dragon'
26
+ };
27
+ export const RARITY_LABEL: Record<number, string> = {
28
+ 0: 'Common',
29
+ 1: 'Uncommon',
30
+ 2: 'Rare'
31
+ };
32
+ export const RARITY_COLOR: Record<number, string> = {
33
+ 0: 'text-gray-400',
34
+ 1: 'text-blue-400',
35
+ 2: 'text-purple-400'
36
+ };
37
+ export const FAVORITE_FOOD: Record<number, string> = {
38
+ 1: 'Carrot',
39
+ 2: 'Wheat',
40
+ 3: 'Pumpkin',
41
+ 4: 'Corn',
42
+ 5: 'Any'
43
+ };
44
+ export const CROP_SHORT: Record<number, string> = { 1: 'W', 2: 'C', 3: 'Ca', 4: 'P' };
45
+ export const CROP_FULL: Record<number, string> = {
46
+ 1: 'Wheat',
47
+ 2: 'Corn',
48
+ 3: 'Carrot',
49
+ 4: 'Pumpkin'
50
+ };
51
+
52
+ const EGG_NAME: Record<number, string> = { 1: 'Common', 2: 'Rare', 3: 'Seasonal' };
53
+ const EGG_PRICE: Record<number, number> = { 1: 80, 2: 300, 3: 500 };
54
+ const EGG_RARITY_COLOR: Record<number, string> = {
55
+ 1: 'border-amber-700/60 bg-amber-900/30',
56
+ 2: 'border-purple-700/60 bg-purple-900/20',
57
+ 3: 'border-yellow-600/60 bg-yellow-900/20'
58
+ };
59
+
60
+ const XP_PER_LEVEL = 100;
61
+ const MAX_LEVEL = 10;
62
+ const SATIETY_DRAIN_INTERVAL_MS = 4 * 60 * 60 * 1000;
63
+ const SATIETY_DRAIN_AMOUNT = 20;
64
+
65
+ // ── Types ─────────────────────────────────────────────────────────────────────
66
+
67
+ export interface PetData {
68
+ petId: string;
69
+ species: number;
70
+ rarity: number;
71
+ level: number;
72
+ xp: number;
73
+ happiness: number;
74
+ satiety: number;
75
+ fedAt: number;
76
+ bornAt: number;
77
+ }
78
+
79
+ export interface PetHatchData {
80
+ eggType: number;
81
+ hatchAt: number;
82
+ }
83
+
84
+ export interface PetInventory {
85
+ commonEgg: bigint;
86
+ rareEgg: bigint;
87
+ seasonalEgg: bigint;
88
+ slotsOwned: number;
89
+ hatch: PetHatchData | null;
90
+ activeSlots: (PetData | null)[];
91
+ ranchPets: PetData[];
92
+ }
93
+
94
+ interface PetPanelProps {
95
+ gold: bigint;
96
+ inventory: PetInventory;
97
+ cropInventory: Record<number, bigint>;
98
+ isLoading: boolean;
99
+ onBuyEgg: (eggType: number) => void;
100
+ onStartHatch: (eggType: number) => void;
101
+ onOpenEgg: () => void;
102
+ onFeedPet: (petId: string, cropType: number, amount: number) => void;
103
+ onBuySlot: () => void;
104
+ onDismissPet: (petId: string) => void;
105
+ onListPet: (petId: string, price: bigint) => void;
106
+ onAssignSlot: (petId: string, slot: number) => void;
107
+ onUnassignSlot: (slot: number) => void;
108
+ }
109
+
110
+ // ── Selection state ────────────────────────────────────────────────────────────
111
+
112
+ interface Selection {
113
+ pet: PetData;
114
+ context: 'active' | 'ranch';
115
+ slot?: number;
116
+ }
117
+
118
+ // ── Helpers ───────────────────────────────────────────────────────────────────
119
+
120
+ function computeCurrentSatiety(pet: PetData, now: number): number {
121
+ const elapsed = now - pet.fedAt;
122
+ const drained = Math.floor(elapsed / SATIETY_DRAIN_INTERVAL_MS) * SATIETY_DRAIN_AMOUNT;
123
+ return Math.max(0, pet.satiety - drained);
124
+ }
125
+
126
+ function satietyBarColor(s: number) {
127
+ if (s >= 60) return 'bg-green-500';
128
+ if (s >= 30) return 'bg-yellow-500';
129
+ return 'bg-red-500';
130
+ }
131
+ function happinessBarColor(h: number) {
132
+ if (h >= 60) return 'bg-pink-400';
133
+ if (h >= 30) return 'bg-yellow-400';
134
+ return 'bg-gray-500';
135
+ }
136
+
137
+ function stageLabel(level: number) {
138
+ if (level >= 7) return { label: 'Adult', color: 'text-purple-400' };
139
+ if (level >= 4) return { label: 'Young', color: 'text-blue-400' };
140
+ return { label: 'Baby', color: 'text-green-400' };
141
+ }
142
+
143
+ function petBonus(species: number, level: number): string {
144
+ const tier = level >= 7 ? 2 : 1;
145
+ const pct = tier === 2 ? 20 : 10;
146
+ if (species === 1) return `Carrot yield +${pct}%`;
147
+ if (species === 2) return `Wheat grows ${pct}% faster`;
148
+ if (species === 3) return `Sell price +${pct}%${tier === 2 ? ' + Scarecrow +2h' : ''}`;
149
+ if (species === 4) return `All crop yield +${Math.round(pct * 0.75)}%`;
150
+ if (species === 5) return `All bonuses +${pct + 5}%${tier === 2 ? ' + Season x1.5' : ''}`;
151
+ return '';
152
+ }
153
+
154
+ /** Returns the species-specific idle CSS animation class, modified by satiety. */
155
+ function petIdleClass(species: number, satiety: number): string {
156
+ if (satiety < 30) return 'pet-anim-starving';
157
+ const base: Record<number, string> = {
158
+ 1: 'pet-anim-bunny',
159
+ 2: 'pet-anim-chick',
160
+ 3: 'pet-anim-fox',
161
+ 4: 'pet-anim-deer',
162
+ 5: 'pet-anim-dragon'
163
+ };
164
+ const cls = base[species] ?? '';
165
+ return satiety < 60 ? `${cls} pet-anim-slow` : cls;
166
+ }
167
+
168
+ // ── Particle types ────────────────────────────────────────────────────────────
169
+
170
+ interface Particle {
171
+ id: number;
172
+ x: number;
173
+ y: number;
174
+ type: 'heart' | 'star';
175
+ sx?: number;
176
+ sy?: number;
177
+ }
178
+
179
+ // ── StatBar ───────────────────────────────────────────────────────────────────
180
+
181
+ function StatBar({
182
+ label,
183
+ value,
184
+ colorClass
185
+ }: {
186
+ label: string;
187
+ value: number;
188
+ colorClass: string;
189
+ }) {
190
+ return (
191
+ <div className="flex items-center gap-2">
192
+ <span className="text-xs text-amber-600 w-14 shrink-0 font-pixel">{label}</span>
193
+ <div className="flex-1 bg-gray-800 rounded-full h-2 overflow-hidden">
194
+ <motion.div
195
+ className={`h-2 rounded-full ${colorClass}`}
196
+ initial={false}
197
+ animate={{ width: `${value}%` }}
198
+ transition={{ duration: 0.6, ease: 'easeOut' }}
199
+ />
200
+ </div>
201
+ <span className="text-xs text-amber-500 w-8 text-right tabular-nums">{value}</span>
202
+ </div>
203
+ );
204
+ }
205
+
206
+ // ── SlotCell — large visual habitat box ───────────────────────────────────────
207
+
208
+ function SlotCell({
209
+ pet,
210
+ slot,
211
+ isOwned,
212
+ isSelected,
213
+ isLoading,
214
+ slotsOwned,
215
+ onSelect,
216
+ onBuySlot
217
+ }: {
218
+ pet: PetData | null;
219
+ slot: number;
220
+ isOwned: boolean;
221
+ isSelected: boolean;
222
+ isLoading: boolean;
223
+ slotsOwned: number;
224
+ onSelect: (sel: Selection) => void;
225
+ onBuySlot: () => void;
226
+ }) {
227
+ const now = Date.now();
228
+
229
+ // Locked slot
230
+ if (!isOwned) {
231
+ return (
232
+ <motion.button
233
+ onClick={onBuySlot}
234
+ disabled={isLoading}
235
+ whileHover={{ scale: 1.03 }}
236
+ whileTap={{ scale: 0.97 }}
237
+ className="flex-1 h-32 border-2 border-dashed border-amber-800/30 rounded-xl
238
+ flex flex-col items-center justify-center gap-1
239
+ bg-amber-950/20 hover:bg-amber-950/40 hover:border-amber-700/50
240
+ transition-all disabled:opacity-40"
241
+ >
242
+ <span className="text-2xl text-amber-800">+</span>
243
+ <span className="text-amber-700 text-xs font-pixel">Slot {slot + 1}</span>
244
+ <span className="text-amber-800 text-xs">{slotsOwned === 1 ? '200g' : '500g'}</span>
245
+ </motion.button>
246
+ );
247
+ }
248
+
249
+ // Owned but empty
250
+ if (!pet) {
251
+ return (
252
+ <div
253
+ className="flex-1 h-32 border-2 border-dashed border-amber-800/30 rounded-xl
254
+ flex flex-col items-center justify-center gap-1
255
+ bg-gradient-to-b from-slate-900/30 to-amber-950/30"
256
+ >
257
+ <span className="text-amber-800 text-xs font-pixel">Slot {slot + 1}</span>
258
+ <span className="text-amber-900 text-xs">Empty</span>
259
+ </div>
260
+ );
261
+ }
262
+
263
+ const satiety = computeCurrentSatiety(pet, now);
264
+ const hungry = satiety < 30;
265
+
266
+ return (
267
+ <motion.div
268
+ onClick={() => onSelect({ pet, context: 'active', slot })}
269
+ whileHover={{ scale: 1.04 }}
270
+ whileTap={{ scale: 0.96 }}
271
+ className={`flex-1 h-32 rounded-xl overflow-hidden cursor-pointer relative
272
+ bg-gradient-to-b from-slate-900/60 via-green-950/40 to-amber-950/50
273
+ border-2 transition-all select-none
274
+ ${
275
+ isSelected
276
+ ? 'pet-selected'
277
+ : hungry
278
+ ? 'border-red-700/50'
279
+ : 'border-amber-800/30 hover:border-amber-700/50'
280
+ }`}
281
+ >
282
+ {/* Sky gradient top */}
283
+ <div className="absolute inset-0 bg-gradient-to-b from-blue-950/20 to-transparent pointer-events-none" />
284
+
285
+ {/* Slot number */}
286
+ <div className="absolute top-1.5 left-2 text-amber-700/60 text-xs font-pixel">{slot + 1}</div>
287
+
288
+ {/* Hunger warning badge */}
289
+ {hungry && (
290
+ <motion.div
291
+ className="absolute top-1 right-1.5 z-10"
292
+ animate={{ opacity: [1, 0.25, 1] }}
293
+ transition={{ duration: 0.7, repeat: Infinity }}
294
+ >
295
+ <IconWarning size={13} />
296
+ </motion.div>
297
+ )}
298
+
299
+ {/* Pet sprite with species animation */}
300
+ <div
301
+ className={`flex items-center justify-center h-20 relative z-10 ${petIdleClass(
302
+ pet.species,
303
+ satiety
304
+ )}`}
305
+ >
306
+ <PetAvatar species={pet.species} level={pet.level} size={56} />
307
+ </div>
308
+
309
+ {/* Ground strip */}
310
+ <div className="absolute bottom-5 left-0 right-0 h-2 bg-green-900/60 ground-shimmer pointer-events-none" />
311
+ <div className="absolute bottom-3 left-0 right-0 h-2 bg-green-950/50 pointer-events-none" />
312
+
313
+ {/* Species + level label */}
314
+ <div className="absolute bottom-5 left-0 right-0 flex justify-center">
315
+ <span className="text-amber-600/70 text-xs font-pixel leading-none">
316
+ {SPECIES_NAME[pet.species]} Lv.{pet.level}
317
+ </span>
318
+ </div>
319
+
320
+ {/* Satiety bar */}
321
+ <div className="absolute bottom-0 left-0 right-0 h-2 bg-gray-900/70">
322
+ <motion.div
323
+ className={`h-full ${satietyBarColor(satiety)}`}
324
+ initial={false}
325
+ animate={{ width: `${satiety}%` }}
326
+ transition={{ duration: 0.8, ease: 'easeOut' }}
327
+ />
328
+ </div>
329
+ </motion.div>
330
+ );
331
+ }
332
+
333
+ // ── RanchCell — compact grid portrait ─────────────────────────────────────────
334
+
335
+ function RanchCell({
336
+ pet,
337
+ isSelected,
338
+ onSelect
339
+ }: {
340
+ pet: PetData;
341
+ isSelected: boolean;
342
+ onSelect: (sel: Selection) => void;
343
+ }) {
344
+ const now = Date.now();
345
+ const satiety = computeCurrentSatiety(pet, now);
346
+ const hungry = satiety < 30;
347
+
348
+ return (
349
+ <motion.div
350
+ onClick={() => onSelect({ pet, context: 'ranch' })}
351
+ whileHover={{ scale: 1.08 }}
352
+ whileTap={{ scale: 0.94 }}
353
+ className={`w-16 h-20 rounded-xl overflow-hidden cursor-pointer relative
354
+ bg-gradient-to-b from-slate-900/50 to-amber-950/60
355
+ border-2 transition-all select-none
356
+ ${
357
+ isSelected
358
+ ? 'pet-selected'
359
+ : hungry
360
+ ? 'border-red-700/50'
361
+ : 'border-amber-800/30 hover:border-amber-600/50'
362
+ }`}
363
+ >
364
+ {/* Hunger badge */}
365
+ {hungry && (
366
+ <motion.div
367
+ className="absolute top-0.5 right-0.5 z-10"
368
+ animate={{ opacity: [1, 0.2, 1] }}
369
+ transition={{ duration: 0.7, repeat: Infinity }}
370
+ >
371
+ <IconWarning size={10} />
372
+ </motion.div>
373
+ )}
374
+
375
+ {/* Pet sprite */}
376
+ <div
377
+ className={`flex items-center justify-center h-12 pt-1 ${petIdleClass(
378
+ pet.species,
379
+ satiety
380
+ )}`}
381
+ >
382
+ <PetAvatar species={pet.species} level={pet.level} size={38} />
383
+ </div>
384
+
385
+ {/* Level label */}
386
+ <div className="text-center">
387
+ <span className="text-amber-700 text-xs font-pixel">Lv.{pet.level}</span>
388
+ </div>
389
+
390
+ {/* Satiety bar */}
391
+ <div className="absolute bottom-0 left-0 right-0 h-1.5 bg-gray-900/70">
392
+ <div
393
+ className={`h-full transition-all duration-700 ${satietyBarColor(satiety)}`}
394
+ style={{ width: `${satiety}%` }}
395
+ />
396
+ </div>
397
+ </motion.div>
398
+ );
399
+ }
400
+
401
+ // ── PetDetailPane — inline interactive detail view ────────────────────────────
402
+
403
+ function PetDetailPane({
404
+ selection,
405
+ isLoading,
406
+ cropInventory,
407
+ emptySlots,
408
+ onFeed,
409
+ onDismiss,
410
+ onList,
411
+ onAssignSlot,
412
+ onUnassign,
413
+ onClose
414
+ }: {
415
+ selection: Selection;
416
+ isLoading: boolean;
417
+ cropInventory: Record<number, bigint>;
418
+ emptySlots: number[];
419
+ onFeed: (cropType: number, amount: number) => void;
420
+ onDismiss: () => void;
421
+ onList: (price: bigint) => void;
422
+ onAssignSlot: (slot: number) => void;
423
+ onUnassign: () => void;
424
+ onClose: () => void;
425
+ }) {
426
+ const { pet, context } = selection;
427
+
428
+ const [showFeed, setShowFeed] = useState(false);
429
+ const [showList, setShowList] = useState(false);
430
+ const [showAssign, setShowAssign] = useState(false);
431
+ const [feedCrop, setFeedCrop] = useState(3);
432
+ const [feedAmt, setFeedAmt] = useState(1);
433
+ const [listPrice, setListPrice] = useState('');
434
+ const [isEating, setIsEating] = useState(false);
435
+ const [particles, setParticles] = useState<Particle[]>([]);
436
+ const [levelBanner, setLevelBanner] = useState<string | null>(null);
437
+ const [glowing, setGlowing] = useState(false);
438
+ const particleId = useRef(0);
439
+ const prevLevel = useRef(pet.level);
440
+
441
+ const now = Date.now();
442
+ const satiety = computeCurrentSatiety(pet, now);
443
+ const xpPct = pet.level >= MAX_LEVEL ? 100 : Math.floor((pet.xp / XP_PER_LEVEL) * 100);
444
+ const stage = stageLabel(pet.level);
445
+
446
+ // Level-up detection
447
+ useEffect(() => {
448
+ if (pet.level > prevLevel.current) {
449
+ setLevelBanner(`Lv.${prevLevel.current} → Lv.${pet.level}!`);
450
+ setGlowing(true);
451
+ const id = ++particleId.current;
452
+ const stars: Particle[] = Array.from({ length: 8 }, (_, i) => {
453
+ const angle = (i / 8) * Math.PI * 2;
454
+ return {
455
+ id: id * 100 + i,
456
+ x: 0,
457
+ y: 0,
458
+ type: 'star' as const,
459
+ sx: Math.round(Math.cos(angle) * 44),
460
+ sy: Math.round(Math.sin(angle) * 44) - 10
461
+ };
462
+ });
463
+ setParticles((p) => [...p, ...stars]);
464
+ setTimeout(
465
+ () => setParticles((p) => p.filter((x) => !stars.find((s) => s.id === x.id))),
466
+ 900
467
+ );
468
+ setTimeout(() => setLevelBanner(null), 2200);
469
+ setTimeout(() => setGlowing(false), 1800);
470
+ }
471
+ prevLevel.current = pet.level;
472
+ }, [pet.level]);
473
+
474
+ const triggerFeedFx = useCallback(() => {
475
+ setIsEating(true);
476
+ setTimeout(() => setIsEating(false), 400);
477
+ const id = ++particleId.current;
478
+ const hearts: Particle[] = Array.from({ length: 5 }, (_, i) => ({
479
+ id: id * 10 + i,
480
+ x: (Math.random() - 0.5) * 40,
481
+ y: 0,
482
+ type: 'heart' as const
483
+ }));
484
+ setParticles((p) => [...p, ...hearts]);
485
+ setTimeout(
486
+ () => setParticles((p) => p.filter((h) => !hearts.find((n) => n.id === h.id))),
487
+ 1000
488
+ );
489
+ }, []);
490
+
491
+ const handleFeed = () => {
492
+ onFeed(feedCrop, feedAmt);
493
+ triggerFeedFx();
494
+ setShowFeed(false);
495
+ };
496
+
497
+ // Re-open with clean state when pet changes
498
+ useEffect(() => {
499
+ setShowFeed(false);
500
+ setShowList(false);
501
+ setShowAssign(false);
502
+ setListPrice('');
503
+ setFeedAmt(1);
504
+ }, [pet.petId]);
505
+
506
+ const canFeed = !isLoading && Number(cropInventory[feedCrop] ?? 0) >= feedAmt;
507
+
508
+ return (
509
+ <motion.div
510
+ key={pet.petId}
511
+ initial={{ opacity: 0, y: 12 }}
512
+ animate={{ opacity: 1, y: 0 }}
513
+ exit={{ opacity: 0, y: 8 }}
514
+ transition={{ duration: 0.2 }}
515
+ className="bg-amber-950/80 border border-amber-600/40 rounded-xl p-3 space-y-3"
516
+ >
517
+ {/* ── Header row ── */}
518
+ <div className="flex items-start gap-3 relative">
519
+ {/* Particle layer over avatar */}
520
+ <div className="absolute top-0 left-4 pointer-events-none z-20">
521
+ {particles.map((p) => (
522
+ <span
523
+ key={p.id}
524
+ className="absolute"
525
+ style={
526
+ { '--sx': `${p.sx ?? p.x}px`, '--sy': `${p.sy ?? p.y}px` } as React.CSSProperties
527
+ }
528
+ >
529
+ <span
530
+ className={
531
+ p.type === 'star' ? 'star-burst inline-block' : 'heart-float inline-block'
532
+ }
533
+ >
534
+ {p.type === 'star' ? <IconStar size={11} /> : <IconHeart size={9} />}
535
+ </span>
536
+ </span>
537
+ ))}
538
+ </div>
539
+
540
+ {/* Large animated avatar */}
541
+ <div
542
+ className={`shrink-0 relative ${petIdleClass(pet.species, satiety)}
543
+ ${isEating ? 'pet-eating' : ''} ${glowing ? 'level-up-glow' : ''}`}
544
+ >
545
+ <PetAvatar species={pet.species} level={pet.level} size={72} />
546
+ {satiety < 30 && (
547
+ <motion.div
548
+ className="absolute -top-1 -right-1"
549
+ animate={{ opacity: [1, 0.2, 1] }}
550
+ transition={{ duration: 0.7, repeat: Infinity }}
551
+ >
552
+ <IconWarning size={14} />
553
+ </motion.div>
554
+ )}
555
+ </div>
556
+
557
+ {/* Identity block */}
558
+ <div className="flex-1 min-w-0">
559
+ {/* Level-up banner */}
560
+ <AnimatePresence>
561
+ {levelBanner && (
562
+ <motion.div
563
+ initial={{ y: -8, opacity: 0 }}
564
+ animate={{ y: 0, opacity: 1 }}
565
+ exit={{ y: -6, opacity: 0 }}
566
+ className="font-pixel text-yellow-400 text-xs mb-1"
567
+ >
568
+ {levelBanner}
569
+ </motion.div>
570
+ )}
571
+ </AnimatePresence>
572
+
573
+ <div className="flex items-center gap-1.5 flex-wrap">
574
+ <span className="font-pixel text-amber-200 text-sm">{SPECIES_NAME[pet.species]}</span>
575
+ <span className={`text-xs ${RARITY_COLOR[pet.rarity]}`}>
576
+ {RARITY_LABEL[pet.rarity]}
577
+ </span>
578
+ <span className={`text-xs font-pixel ${stage.color}`}>{stage.label}</span>
579
+ </div>
580
+ <p className="text-amber-600 text-xs mt-0.5">
581
+ Lv.{pet.level} · Fav: {FAVORITE_FOOD[pet.species]}
582
+ </p>
583
+
584
+ {/* XP bar */}
585
+ {pet.level < MAX_LEVEL && (
586
+ <div className="flex items-center gap-1.5 mt-1.5">
587
+ <span className="text-xs text-amber-800 w-5">XP</span>
588
+ <div className="flex-1 bg-gray-800 rounded-full h-1.5 overflow-hidden">
589
+ <motion.div
590
+ className="h-full rounded-full bg-blue-500"
591
+ initial={false}
592
+ animate={{ width: `${xpPct}%` }}
593
+ transition={{ duration: 0.6 }}
594
+ />
595
+ </div>
596
+ <span className="text-xs text-amber-700 tabular-nums">
597
+ {pet.xp}/{XP_PER_LEVEL}
598
+ </span>
599
+ </div>
600
+ )}
601
+
602
+ {/* Farm bonus (active only, lv4+) */}
603
+ {context === 'active' && pet.level >= 4 && (
604
+ <p className="text-xs text-emerald-400 font-pixel mt-1">
605
+ + {petBonus(pet.species, pet.level)}
606
+ </p>
607
+ )}
608
+ </div>
609
+
610
+ {/* Close button */}
611
+ <button
612
+ onClick={onClose}
613
+ className="shrink-0 text-amber-700 hover:text-amber-400 text-sm leading-none transition-colors"
614
+ >
615
+ x
616
+ </button>
617
+ </div>
618
+
619
+ {/* ── Stat bars ── */}
620
+ <div className="space-y-1.5">
621
+ <StatBar label="Satiety" value={satiety} colorClass={satietyBarColor(satiety)} />
622
+ <StatBar
623
+ label="Happy"
624
+ value={pet.happiness}
625
+ colorClass={happinessBarColor(pet.happiness)}
626
+ />
627
+ </div>
628
+
629
+ {/* ── Action buttons row ── */}
630
+ <div className="flex flex-wrap gap-1.5">
631
+ <motion.button
632
+ onClick={() => {
633
+ setShowFeed(!showFeed);
634
+ setShowList(false);
635
+ setShowAssign(false);
636
+ }}
637
+ disabled={isLoading}
638
+ whileHover={{ scale: 1.05 }}
639
+ whileTap={{ scale: 0.95 }}
640
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-pixel
641
+ disabled:opacity-50 transition-colors
642
+ ${
643
+ showFeed
644
+ ? 'bg-green-700 text-white'
645
+ : 'bg-green-900/60 hover:bg-green-800/80 text-green-300'
646
+ }`}
647
+ >
648
+ <IconFeed size={12} /> Feed
649
+ </motion.button>
650
+
651
+ {context === 'active' && (
652
+ <motion.button
653
+ onClick={() => {
654
+ onUnassign();
655
+ onClose();
656
+ }}
657
+ disabled={isLoading}
658
+ whileHover={{ scale: 1.05 }}
659
+ whileTap={{ scale: 0.95 }}
660
+ className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-pixel
661
+ bg-amber-900/60 hover:bg-amber-800/80 text-amber-300 disabled:opacity-50 transition-colors"
662
+ >
663
+ Ranch
664
+ </motion.button>
665
+ )}
666
+ {context === 'ranch' && (
667
+ <motion.button
668
+ onClick={() => {
669
+ setShowAssign(!showAssign);
670
+ setShowFeed(false);
671
+ setShowList(false);
672
+ }}
673
+ disabled={isLoading || emptySlots.length === 0}
674
+ whileHover={{ scale: 1.05 }}
675
+ whileTap={{ scale: 0.95 }}
676
+ className={`flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-pixel
677
+ disabled:opacity-50 transition-colors
678
+ ${
679
+ showAssign
680
+ ? 'bg-emerald-700 text-white'
681
+ : 'bg-emerald-900/60 hover:bg-emerald-800/80 text-emerald-300'
682
+ }`}
683
+ title={emptySlots.length === 0 ? 'All slots occupied' : 'Put in active slot'}
684
+ >
685
+ Activate
686
+ </motion.button>
687
+ )}
688
+
689
+ <motion.button
690
+ onClick={() => {
691
+ setShowList(!showList);
692
+ setShowFeed(false);
693
+ setShowAssign(false);
694
+ }}
695
+ disabled={isLoading}
696
+ whileHover={{ scale: 1.05 }}
697
+ whileTap={{ scale: 0.95 }}
698
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-pixel
699
+ disabled:opacity-50 transition-colors
700
+ ${
701
+ showList
702
+ ? 'bg-blue-700 text-white'
703
+ : 'bg-blue-900/60 hover:bg-blue-800/80 text-blue-300'
704
+ }`}
705
+ >
706
+ <IconTag size={12} /> List
707
+ </motion.button>
708
+
709
+ <motion.button
710
+ onClick={() => {
711
+ onDismiss();
712
+ onClose();
713
+ }}
714
+ disabled={isLoading}
715
+ whileHover={{ scale: 1.05 }}
716
+ whileTap={{ scale: 0.95 }}
717
+ className="px-2.5 py-1.5 rounded-lg text-xs font-pixel
718
+ bg-red-950/60 hover:bg-red-900/60 text-red-400 disabled:opacity-50 transition-colors"
719
+ title="Permanently release this pet"
720
+ >
721
+ Dismiss
722
+ </motion.button>
723
+ </div>
724
+
725
+ {/* ── Feed panel ── */}
726
+ <AnimatePresence>
727
+ {showFeed && (
728
+ <motion.div
729
+ initial={{ height: 0, opacity: 0 }}
730
+ animate={{ height: 'auto', opacity: 1 }}
731
+ exit={{ height: 0, opacity: 0 }}
732
+ className="overflow-hidden"
733
+ >
734
+ <div className="pt-1 space-y-2 border-t border-amber-800/30">
735
+ {/* Crop type selector */}
736
+ <div className="grid grid-cols-4 gap-1 pt-2">
737
+ {[1, 2, 3, 4].map((ct) => {
738
+ const amt = Number(cropInventory[ct] ?? 0);
739
+ const isFav = FAVORITE_FOOD[pet.species] === CROP_FULL[ct];
740
+ return (
741
+ <button
742
+ key={ct}
743
+ onClick={() => setFeedCrop(ct)}
744
+ className={`py-1.5 rounded-lg text-xs font-pixel transition-colors relative
745
+ ${
746
+ feedCrop === ct
747
+ ? 'bg-amber-700 text-white'
748
+ : 'bg-amber-900/40 text-amber-400 hover:bg-amber-800/50'
749
+ }`}
750
+ >
751
+ {isFav && (
752
+ <span className="absolute -top-1 -right-1">
753
+ <IconHeart size={8} />
754
+ </span>
755
+ )}
756
+ <div>{CROP_SHORT[ct]}</div>
757
+ <div className="text-amber-600 text-xs opacity-70">{amt}</div>
758
+ </button>
759
+ );
760
+ })}
761
+ </div>
762
+ {/* Amount + feed button */}
763
+ <div className="flex gap-2 items-center">
764
+ <input
765
+ type="number"
766
+ min={1}
767
+ max={Number(cropInventory[feedCrop] ?? 0)}
768
+ value={feedAmt}
769
+ onChange={(e) => setFeedAmt(Math.max(1, Number(e.target.value)))}
770
+ className="w-16 bg-gray-800/80 text-amber-200 text-xs rounded-lg px-2 py-1.5 border border-amber-700/40"
771
+ />
772
+ <motion.button
773
+ onClick={handleFeed}
774
+ disabled={!canFeed}
775
+ whileHover={{ scale: 1.05 }}
776
+ whileTap={{ scale: 0.95 }}
777
+ className="flex-1 bg-green-700 hover:bg-green-600 text-white text-xs font-pixel
778
+ py-1.5 rounded-lg disabled:opacity-50 transition-colors"
779
+ >
780
+ Feed x{feedAmt}
781
+ {FAVORITE_FOOD[pet.species] === CROP_FULL[feedCrop] ? ' (Fav!)' : ''}
782
+ </motion.button>
783
+ </div>
784
+ </div>
785
+ </motion.div>
786
+ )}
787
+ </AnimatePresence>
788
+
789
+ {/* ── Assign slot picker ── */}
790
+ <AnimatePresence>
791
+ {showAssign && context === 'ranch' && emptySlots.length > 0 && (
792
+ <motion.div
793
+ initial={{ height: 0, opacity: 0 }}
794
+ animate={{ height: 'auto', opacity: 1 }}
795
+ exit={{ height: 0, opacity: 0 }}
796
+ className="overflow-hidden"
797
+ >
798
+ <div className="pt-1 border-t border-amber-800/30">
799
+ <p className="text-amber-600 text-xs font-pixel mb-2 pt-1">
800
+ Pick a slot to activate:
801
+ </p>
802
+ <div className="flex gap-2">
803
+ {emptySlots.map((s) => (
804
+ <motion.button
805
+ key={s}
806
+ onClick={() => {
807
+ onAssignSlot(s);
808
+ onClose();
809
+ }}
810
+ disabled={isLoading}
811
+ whileHover={{ scale: 1.06 }}
812
+ whileTap={{ scale: 0.94 }}
813
+ className="flex-1 bg-emerald-700 hover:bg-emerald-600 text-white
814
+ text-xs font-pixel py-2 rounded-lg disabled:opacity-50"
815
+ >
816
+ Slot {s + 1}
817
+ </motion.button>
818
+ ))}
819
+ </div>
820
+ </div>
821
+ </motion.div>
822
+ )}
823
+ </AnimatePresence>
824
+
825
+ {/* ── List price panel ── */}
826
+ <AnimatePresence>
827
+ {showList && (
828
+ <motion.div
829
+ initial={{ height: 0, opacity: 0 }}
830
+ animate={{ height: 'auto', opacity: 1 }}
831
+ exit={{ height: 0, opacity: 0 }}
832
+ className="overflow-hidden"
833
+ >
834
+ <div className="pt-1 border-t border-amber-800/30">
835
+ <div className="flex gap-2 items-center pt-2">
836
+ <input
837
+ type="number"
838
+ min={1}
839
+ placeholder="Price (MIST)"
840
+ value={listPrice}
841
+ onChange={(e) => setListPrice(e.target.value)}
842
+ className="flex-1 bg-gray-800/80 text-amber-200 text-xs rounded-lg px-2 py-1.5 border border-amber-700/40"
843
+ />
844
+ <motion.button
845
+ onClick={() => {
846
+ const p = BigInt(listPrice || '0');
847
+ if (p > 0n) {
848
+ onList(p);
849
+ setShowList(false);
850
+ setListPrice('');
851
+ onClose();
852
+ }
853
+ }}
854
+ disabled={isLoading || !listPrice || BigInt(listPrice || '0') <= 0n}
855
+ whileHover={{ scale: 1.05 }}
856
+ whileTap={{ scale: 0.95 }}
857
+ className="bg-blue-700 hover:bg-blue-600 text-white text-xs font-pixel
858
+ px-3 py-1.5 rounded-lg disabled:opacity-50 transition-colors"
859
+ >
860
+ List
861
+ </motion.button>
862
+ </div>
863
+ </div>
864
+ </motion.div>
865
+ )}
866
+ </AnimatePresence>
867
+ </motion.div>
868
+ );
869
+ }
870
+
871
+ // ── EggIncubator ──────────────────────────────────────────────────────────────
872
+
873
+ function EggIncubator({
874
+ hatch,
875
+ isLoading,
876
+ onOpenEgg
877
+ }: {
878
+ hatch: PetHatchData;
879
+ isLoading: boolean;
880
+ onOpenEgg: () => void;
881
+ }) {
882
+ const [now, setNow] = useState(Date.now());
883
+ const [cracking, setCracking] = useState(false);
884
+ // Tracks whether we've already fired the tx; cleared only on tx failure (isLoading → false).
885
+ const [submitted, setSubmitted] = useState(false);
886
+
887
+ useEffect(() => {
888
+ const t = setInterval(() => setNow(Date.now()), 1000);
889
+ return () => clearInterval(t);
890
+ }, []);
891
+
892
+ // If loading stopped but the hatch record still exists (tx failed), reset all local state.
893
+ useEffect(() => {
894
+ if (!isLoading && (cracking || submitted)) {
895
+ setCracking(false);
896
+ setSubmitted(false);
897
+ }
898
+ }, [isLoading]);
899
+
900
+ const ready = now >= hatch.hatchAt;
901
+ const refMs = 5 * 60 * 1000;
902
+ const startMs = hatch.hatchAt - refMs;
903
+ const elapsed = now - startMs;
904
+ const progress = Math.min(100, Math.round((elapsed / refMs) * 100));
905
+ const secsLeft = Math.max(0, Math.ceil((hatch.hatchAt - now) / 1000));
906
+ const eggClass = ready ? 'egg-ready egg-glow' : progress > 80 ? 'egg-rocking' : '';
907
+
908
+ const handleOpen = () => {
909
+ setCracking(true);
910
+ setSubmitted(true);
911
+ setTimeout(() => onOpenEgg(), 450);
912
+ };
913
+
914
+ // Show "Hatching..." while transaction is in flight OR after it succeeds
915
+ // (submitted=true stays true until parent clears hatch via optimistic update).
916
+ const showHatching = submitted && (isLoading || cracking);
917
+
918
+ return (
919
+ <div className="bg-amber-900/40 border border-amber-700/50 rounded-xl p-4 text-center space-y-3">
920
+ {showHatching ? (
921
+ // Transaction pending — animated loading state
922
+ <div className="py-4 space-y-3">
923
+ <div className="inline-block pet-pop-in">
924
+ <IconPaw size={40} />
925
+ </div>
926
+ <p className="font-pixel text-emerald-300 text-xs animate-pulse">Hatching...</p>
927
+ <p className="text-amber-700 text-xs">Waiting for confirmation...</p>
928
+ </div>
929
+ ) : (
930
+ <>
931
+ <div className="relative inline-block">
932
+ {cracking && !showHatching ? (
933
+ // Crack animation (450ms window before tx fires)
934
+ <div className="relative w-12 h-12 mx-auto">
935
+ <span className="absolute inset-0 flex items-center justify-center egg-crack-left">
936
+ <EggIcon eggType={hatch.eggType} size={48} />
937
+ </span>
938
+ <span className="absolute inset-0 flex items-center justify-center egg-crack-right">
939
+ <EggIcon eggType={hatch.eggType} size={48} />
940
+ </span>
941
+ </div>
942
+ ) : (
943
+ <span className={`inline-block ${eggClass}`}>
944
+ <EggIcon eggType={hatch.eggType} size={48} />
945
+ </span>
946
+ )}
947
+ </div>
948
+
949
+ <p className="font-pixel text-amber-300 text-xs">{EGG_NAME[hatch.eggType]} Egg</p>
950
+
951
+ <div className="w-full bg-gray-800 rounded-full h-2 overflow-hidden">
952
+ <motion.div
953
+ className="h-2 rounded-full bg-emerald-500"
954
+ initial={false}
955
+ animate={{ width: `${progress}%` }}
956
+ transition={{ duration: 1, ease: 'linear' }}
957
+ />
958
+ </div>
959
+
960
+ {ready ? (
961
+ <>
962
+ <p className="text-emerald-400 text-xs font-pixel animate-pulse">Ready to hatch!</p>
963
+ <p className="text-amber-700 text-[10px] leading-snug px-1">
964
+ Starting incubation was step 1. This button sends the on-chain transaction that
965
+ creates your pet.
966
+ </p>
967
+ <motion.button
968
+ onClick={handleOpen}
969
+ disabled={isLoading || cracking || submitted}
970
+ whileHover={{ scale: 1.08 }}
971
+ whileTap={{ scale: 0.92 }}
972
+ className="w-full bg-emerald-700 hover:bg-emerald-600 text-white
973
+ font-pixel text-xs py-2 rounded-xl disabled:opacity-50 transition-colors"
974
+ >
975
+ Open Egg
976
+ </motion.button>
977
+ </>
978
+ ) : (
979
+ <>
980
+ <p className="text-amber-700 text-[10px] px-1">
981
+ When the bar fills, tap Open Egg below to claim your pet (second on-chain step).
982
+ </p>
983
+ <p className="text-amber-500 text-xs tabular-nums">
984
+ {Math.floor(secsLeft / 60)}m {secsLeft % 60}s remaining ({progress}%)
985
+ </p>
986
+ </>
987
+ )}
988
+ </>
989
+ )}
990
+ </div>
991
+ );
992
+ }
993
+
994
+ // ── Main PetPanel ─────────────────────────────────────────────────────────────
995
+
996
+ export function PetPanel({
997
+ gold,
998
+ inventory,
999
+ cropInventory,
1000
+ isLoading,
1001
+ onBuyEgg,
1002
+ onStartHatch,
1003
+ onOpenEgg,
1004
+ onFeedPet,
1005
+ onBuySlot,
1006
+ onDismissPet,
1007
+ onListPet,
1008
+ onAssignSlot,
1009
+ onUnassignSlot
1010
+ }: PetPanelProps) {
1011
+ const [tab, setTab] = useState<'pets' | 'hatch' | 'shop'>('pets');
1012
+ const [selection, setSelection] = useState<Selection | null>(null);
1013
+
1014
+ const { activeSlots, ranchPets, slotsOwned } = inventory;
1015
+
1016
+ const emptySlots = Array.from({ length: slotsOwned }, (_, i) => i).filter(
1017
+ (i) => activeSlots[i] === null
1018
+ );
1019
+
1020
+ const totalPets = activeSlots.filter(Boolean).length + ranchPets.length;
1021
+
1022
+ const availableEggs = [
1023
+ { type: 1, count: inventory.commonEgg },
1024
+ { type: 2, count: inventory.rareEgg },
1025
+ { type: 3, count: inventory.seasonalEgg }
1026
+ ].filter((e) => e.count > 0n);
1027
+
1028
+ const hatchTabLabel = inventory.hatch
1029
+ ? Date.now() >= inventory.hatch.hatchAt
1030
+ ? 'Hatch [!]'
1031
+ : 'Hatch ...'
1032
+ : 'Hatch';
1033
+
1034
+ // Deselect if selected pet no longer exists
1035
+ useEffect(() => {
1036
+ if (!selection) return;
1037
+ const allIds = [
1038
+ ...activeSlots.filter(Boolean).map((p) => p!.petId),
1039
+ ...ranchPets.map((p) => p.petId)
1040
+ ];
1041
+ if (!allIds.includes(selection.pet.petId)) setSelection(null);
1042
+ }, [activeSlots, ranchPets]);
1043
+
1044
+ // Keep selection in sync with latest pet data
1045
+ const syncedSelection = selection
1046
+ ? (() => {
1047
+ const allPets = [
1048
+ ...activeSlots.flatMap((p, i) =>
1049
+ p ? [{ pet: p, context: 'active' as const, slot: i }] : []
1050
+ ),
1051
+ ...ranchPets.map((p) => ({ pet: p, context: 'ranch' as const, slot: undefined }))
1052
+ ];
1053
+ const found = allPets.find((x) => x.pet.petId === selection.pet.petId);
1054
+ return found ?? null;
1055
+ })()
1056
+ : null;
1057
+
1058
+ const handleSelect = (sel: Selection) => {
1059
+ setSelection((prev) => (prev?.pet.petId === sel.pet.petId ? null : sel));
1060
+ };
1061
+
1062
+ return (
1063
+ <div className="bg-amber-950/40 border border-amber-700/30 rounded-2xl p-4 space-y-4">
1064
+ {/* Header */}
1065
+ <div className="flex items-center justify-between">
1066
+ <div className="flex items-center gap-2">
1067
+ <IconPaw size={16} />
1068
+ <h2 className="font-pixel text-amber-300 text-xs">PETS</h2>
1069
+ </div>
1070
+ <div className="flex items-center gap-3">
1071
+ <span className="text-xs text-amber-600 font-pixel">Slots {slotsOwned}/3</span>
1072
+ <span className="text-xs text-amber-700 font-pixel">Owned {totalPets}</span>
1073
+ </div>
1074
+ </div>
1075
+
1076
+ {/* Tab bar */}
1077
+ <div className="flex gap-1">
1078
+ {(
1079
+ [
1080
+ { key: 'pets', label: 'My Pets' },
1081
+ { key: 'hatch', label: hatchTabLabel },
1082
+ { key: 'shop', label: 'Shop' }
1083
+ ] as const
1084
+ ).map(({ key, label }) => (
1085
+ <button
1086
+ key={key}
1087
+ onClick={() => setTab(key)}
1088
+ className={`flex-1 py-1.5 text-xs font-pixel rounded-lg transition-colors
1089
+ ${
1090
+ tab === key
1091
+ ? 'bg-amber-700 text-amber-100'
1092
+ : 'bg-amber-900/30 text-amber-500 hover:bg-amber-800/30'
1093
+ }`}
1094
+ >
1095
+ {label}
1096
+ </button>
1097
+ ))}
1098
+ </div>
1099
+
1100
+ {/* ── My Pets tab ── */}
1101
+ {tab === 'pets' && (
1102
+ <div className="space-y-3">
1103
+ {/* Ranch view hint */}
1104
+ <div className="flex items-center gap-2 bg-green-950/50 border border-green-700/30 rounded-xl px-3 py-2">
1105
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
1106
+ <path d="M2 11 L7 3 L12 11 L2 11Z" fill="#4A8A28" opacity="0.8" />
1107
+ <path d="M4 11 L7 6 L10 11" fill="#3A7018" />
1108
+ </svg>
1109
+ <p className="text-xs text-green-500 font-pixel">
1110
+ Switch to Ranch view to see your pets move
1111
+ </p>
1112
+ </div>
1113
+
1114
+ {/* Slot overview bar */}
1115
+ <div className="flex gap-1.5">
1116
+ {Array.from({ length: 3 }, (_, i) => {
1117
+ const pet = activeSlots[i];
1118
+ const owned = i < slotsOwned;
1119
+ return (
1120
+ <div
1121
+ key={i}
1122
+ className={`flex-1 rounded-lg border px-1.5 py-1.5 text-center
1123
+ ${
1124
+ pet
1125
+ ? 'border-emerald-700/50 bg-emerald-950/30'
1126
+ : owned
1127
+ ? 'border-amber-700/30 bg-amber-950/20'
1128
+ : 'border-gray-700/30 bg-gray-900/20'
1129
+ }`}
1130
+ >
1131
+ {pet ? (
1132
+ <div className="flex flex-col items-center gap-0.5">
1133
+ <PetAvatar species={pet.species} level={pet.level} size={22} />
1134
+ <span className="text-emerald-400 font-pixel" style={{ fontSize: '7px' }}>
1135
+ Slot {i + 1}
1136
+ </span>
1137
+ </div>
1138
+ ) : owned ? (
1139
+ <div className="flex flex-col items-center gap-0.5 opacity-40">
1140
+ <div className="w-5 h-5 rounded-full border border-amber-600/40 flex items-center justify-center">
1141
+ <span className="text-amber-600 text-xs">+</span>
1142
+ </div>
1143
+ <span className="text-amber-700 font-pixel" style={{ fontSize: '7px' }}>
1144
+ empty
1145
+ </span>
1146
+ </div>
1147
+ ) : (
1148
+ <div className="flex flex-col items-center gap-0.5 opacity-30">
1149
+ <div className="w-5 h-5 rounded-full border border-gray-600/40 flex items-center justify-center">
1150
+ <span className="text-gray-600 text-xs">+</span>
1151
+ </div>
1152
+ <span className="text-gray-700 font-pixel" style={{ fontSize: '7px' }}>
1153
+ {200}g
1154
+ </span>
1155
+ </div>
1156
+ )}
1157
+ </div>
1158
+ );
1159
+ })}
1160
+ </div>
1161
+
1162
+ {/* Pet list: all pets compact */}
1163
+ {totalPets === 0 ? (
1164
+ <div className="text-amber-800 text-xs text-center font-pixel py-4 space-y-1 px-1">
1165
+ {inventory.hatch ? (
1166
+ Date.now() >= inventory.hatch.hatchAt ? (
1167
+ <>
1168
+ <p className="text-emerald-500">
1169
+ Pet ready — go to Hatch tab and tap Open Egg.
1170
+ </p>
1171
+ <p className="text-amber-700 text-[10px] opacity-90">
1172
+ (Starting incubation only places the egg; Open Egg mints your pet on-chain.)
1173
+ </p>
1174
+ </>
1175
+ ) : (
1176
+ <>
1177
+ <p>Egg is incubating — wait for the timer, then use Open Egg.</p>
1178
+ <p className="text-amber-700 text-[10px] opacity-90">
1179
+ Ranch stays empty until Open Egg succeeds.
1180
+ </p>
1181
+ </>
1182
+ )
1183
+ ) : (
1184
+ <p>No pets yet — buy an egg in Shop, then Hatch tab to incubate.</p>
1185
+ )}
1186
+ </div>
1187
+ ) : (
1188
+ <div className="space-y-1 max-h-48 overflow-y-auto pr-0.5">
1189
+ {[
1190
+ ...activeSlots.flatMap((p, i) =>
1191
+ p ? [{ pet: p, context: 'active' as const, slot: i }] : []
1192
+ ),
1193
+ ...ranchPets.map((p) => ({ pet: p, context: 'ranch' as const, slot: undefined }))
1194
+ ].map(({ pet, context, slot }) => {
1195
+ const isSelected = syncedSelection?.pet.petId === pet.petId;
1196
+ return (
1197
+ <button
1198
+ key={pet.petId}
1199
+ onClick={() => handleSelect({ pet, context, slot })}
1200
+ className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-xl border
1201
+ text-left transition-colors
1202
+ ${
1203
+ isSelected
1204
+ ? 'border-amber-500/60 bg-amber-900/40'
1205
+ : 'border-amber-800/20 bg-amber-950/20 hover:bg-amber-900/20'
1206
+ }`}
1207
+ >
1208
+ <PetAvatar species={pet.species} level={pet.level} size={26} />
1209
+ <div className="flex-1 min-w-0">
1210
+ <div className="flex items-center gap-1">
1211
+ <span className="text-amber-200 font-pixel text-xs">
1212
+ {SPECIES_NAME[pet.species]}
1213
+ </span>
1214
+ <span className={`text-xs ${RARITY_COLOR[pet.rarity]}`}>
1215
+ {RARITY_LABEL[pet.rarity]}
1216
+ </span>
1217
+ {context === 'active' && (
1218
+ <span className="text-emerald-500 font-pixel" style={{ fontSize: '7px' }}>
1219
+ S{(slot ?? 0) + 1}
1220
+ </span>
1221
+ )}
1222
+ </div>
1223
+ <div className="flex items-center gap-1 mt-0.5">
1224
+ <span className="text-amber-700 font-pixel" style={{ fontSize: '9px' }}>
1225
+ Lv.{pet.level}
1226
+ </span>
1227
+ <div className="flex-1 bg-gray-800 rounded-full h-1 overflow-hidden">
1228
+ <div
1229
+ className={`h-full rounded-full ${
1230
+ pet.satiety >= 60
1231
+ ? 'bg-green-600'
1232
+ : pet.satiety >= 30
1233
+ ? 'bg-yellow-500'
1234
+ : 'bg-red-500'
1235
+ }`}
1236
+ style={{ width: `${pet.satiety}%` }}
1237
+ />
1238
+ </div>
1239
+ <span className="text-amber-700" style={{ fontSize: '9px' }}>
1240
+ {pet.satiety}
1241
+ </span>
1242
+ </div>
1243
+ </div>
1244
+ </button>
1245
+ );
1246
+ })}
1247
+ </div>
1248
+ )}
1249
+
1250
+ {/* Detail pane (inline, animated) */}
1251
+ <AnimatePresence>
1252
+ {syncedSelection && (
1253
+ <PetDetailPane
1254
+ key={syncedSelection.pet.petId}
1255
+ selection={syncedSelection}
1256
+ isLoading={isLoading}
1257
+ cropInventory={cropInventory}
1258
+ emptySlots={emptySlots}
1259
+ onFeed={(ct, amt) => onFeedPet(syncedSelection.pet.petId, ct, amt)}
1260
+ onDismiss={() => onDismissPet(syncedSelection.pet.petId)}
1261
+ onList={(price) => onListPet(syncedSelection.pet.petId, price)}
1262
+ onAssignSlot={(slot) => onAssignSlot(syncedSelection.pet.petId, slot)}
1263
+ onUnassign={() => onUnassignSlot(syncedSelection.slot!)}
1264
+ onClose={() => setSelection(null)}
1265
+ />
1266
+ )}
1267
+ </AnimatePresence>
1268
+ </div>
1269
+ )}
1270
+
1271
+ {/* ── Hatch tab ── */}
1272
+ {tab === 'hatch' && (
1273
+ <div className="space-y-3">
1274
+ {inventory.hatch ? (
1275
+ <EggIncubator hatch={inventory.hatch} isLoading={isLoading} onOpenEgg={onOpenEgg} />
1276
+ ) : (
1277
+ <>
1278
+ <p className="text-amber-600 text-xs text-center font-pixel">No egg incubating.</p>
1279
+ {availableEggs.length > 0 ? (
1280
+ <div className="space-y-2">
1281
+ {availableEggs.map(({ type, count }) => (
1282
+ <motion.button
1283
+ key={type}
1284
+ onClick={() => onStartHatch(type)}
1285
+ disabled={isLoading}
1286
+ whileHover={{ scale: 1.02, x: 2 }}
1287
+ whileTap={{ scale: 0.98 }}
1288
+ className={`w-full flex items-center justify-between px-3 py-2
1289
+ border rounded-xl hover:bg-amber-800/30
1290
+ transition-colors disabled:opacity-50 ${EGG_RARITY_COLOR[type]}`}
1291
+ >
1292
+ <div className="flex items-center gap-2">
1293
+ <EggIcon eggType={type} size={24} />
1294
+ <span className="font-pixel text-amber-200 text-xs">
1295
+ {EGG_NAME[type]} x{String(count)}
1296
+ </span>
1297
+ </div>
1298
+ <span className="text-amber-500 text-xs">Incubate</span>
1299
+ </motion.button>
1300
+ ))}
1301
+ </div>
1302
+ ) : (
1303
+ <p className="text-amber-700 text-xs text-center font-pixel">
1304
+ No eggs — buy from the Shop tab.
1305
+ </p>
1306
+ )}
1307
+ </>
1308
+ )}
1309
+ </div>
1310
+ )}
1311
+
1312
+ {/* ── Shop tab ── */}
1313
+ {tab === 'shop' && (
1314
+ <div className="space-y-2">
1315
+ {[
1316
+ { type: 1, pool: '70% Bunny 30% Chick' },
1317
+ { type: 2, pool: '60% Fox 35% Deer 5% Dragon' },
1318
+ { type: 3, pool: 'Guaranteed Uncommon+ Seasonal look' }
1319
+ ].map(({ type, pool }) => {
1320
+ const price = EGG_PRICE[type];
1321
+ return (
1322
+ <div
1323
+ key={type}
1324
+ className={`border rounded-xl p-3 space-y-2 ${EGG_RARITY_COLOR[type]}`}
1325
+ >
1326
+ <div className="flex items-center gap-3">
1327
+ <EggIcon eggType={type} size={32} />
1328
+ <div className="flex-1">
1329
+ <p className="font-pixel text-amber-200 text-xs">{EGG_NAME[type]} Egg</p>
1330
+ <p className="text-amber-600 text-xs mt-0.5">{pool}</p>
1331
+ </div>
1332
+ <div className="flex items-center gap-1">
1333
+ <IconGold size={12} />
1334
+ <span className="font-pixel text-amber-400 text-xs">{price}</span>
1335
+ </div>
1336
+ </div>
1337
+ <div className="flex gap-2">
1338
+ {[1, 3, 5].map((qty) => (
1339
+ <motion.button
1340
+ key={qty}
1341
+ onClick={() => onBuyEgg(type)}
1342
+ disabled={isLoading || gold < BigInt(price * qty)}
1343
+ whileHover={{ scale: 1.05 }}
1344
+ whileTap={{ scale: 0.93 }}
1345
+ className="flex-1 bg-amber-700 hover:bg-amber-600 text-white
1346
+ text-xs font-pixel py-1.5 rounded-lg
1347
+ disabled:opacity-50 transition-colors"
1348
+ >
1349
+ x{qty}
1350
+ </motion.button>
1351
+ ))}
1352
+ </div>
1353
+ </div>
1354
+ );
1355
+ })}
1356
+ </div>
1357
+ )}
1358
+ </div>
1359
+ );
1360
+ }