@nice2dev/game-engine 1.3.15 → 1.3.16

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 (434) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/accessibility/Accessibility.js +152 -165
  3. package/dist/cjs/ai/AI.js +292 -294
  4. package/dist/cjs/ai/BehaviorTree.js +1029 -1113
  5. package/dist/cjs/ai/StateMachine.js +694 -756
  6. package/dist/cjs/animation/Animation.js +232 -241
  7. package/dist/cjs/audio/AudioAdvanced.js +221 -236
  8. package/dist/cjs/audio/AudioBridge.js +441 -435
  9. package/dist/cjs/audio/AudioManager.js +242 -259
  10. package/dist/cjs/build-cloud/BuildOrchestration.js +1059 -1098
  11. package/dist/cjs/build-cloud/GodotBuildCloud.js +641 -670
  12. package/dist/cjs/build-cloud/UnityBuildCloud.js +563 -610
  13. package/dist/cjs/build-cloud/UnrealBuildCloud.js +610 -635
  14. package/dist/cjs/core/EventBus.js +51 -56
  15. package/dist/cjs/core/GameClock.js +62 -66
  16. package/dist/cjs/core/GameConfig.js +72 -79
  17. package/dist/cjs/core/GameLoop.js +80 -89
  18. package/dist/cjs/core/ServiceLocator.js +61 -65
  19. package/dist/cjs/core/Validation.js +72 -82
  20. package/dist/cjs/core/math.js +108 -112
  21. package/dist/cjs/devtools/DevTools.js +542 -563
  22. package/dist/cjs/devtools/DeveloperExperience.js +521 -559
  23. package/dist/cjs/devtools/GameplayAnalytics.js +636 -646
  24. package/dist/cjs/dialogue/DialogueSystem.js +984 -986
  25. package/dist/cjs/docs/DocGenerator.js +339 -364
  26. package/dist/cjs/ecs/World.js +355 -371
  27. package/dist/cjs/editor/AchievementEditor.js +580 -636
  28. package/dist/cjs/editor/AdvancedEditor.js +184 -191
  29. package/dist/cjs/editor/AnimationRetargeting.js +696 -741
  30. package/dist/cjs/editor/AssetBrowser.js +1072 -1160
  31. package/dist/cjs/editor/AssetManager.js +187 -192
  32. package/dist/cjs/editor/ConsolePro.js +971 -1061
  33. package/dist/cjs/editor/CraftingEditor.js +680 -730
  34. package/dist/cjs/editor/DebugTools.js +187 -185
  35. package/dist/cjs/editor/DialogueSystem.js +844 -901
  36. package/dist/cjs/editor/LeaderboardSystem.js +429 -483
  37. package/dist/cjs/editor/NiceGameEditor.js +1903 -1201
  38. package/dist/cjs/editor/PrefabSystem.js +772 -838
  39. package/dist/cjs/editor/ProfilerPanel.js +649 -700
  40. package/dist/cjs/editor/QuestEditor.js +792 -839
  41. package/dist/cjs/editor/ReplaySystem.js +585 -634
  42. package/dist/cjs/editor/SceneEditor.js +176 -200
  43. package/dist/cjs/editor/ScreenshotMode.js +621 -688
  44. package/dist/cjs/editor/ShaderGraph.js +1556 -1582
  45. package/dist/cjs/editor/TimelineEditor.js +715 -778
  46. package/dist/cjs/editor/UndoRedoPro.js +830 -904
  47. package/dist/cjs/editor/VRARMode.js +582 -645
  48. package/dist/cjs/engine/NiceGameEngine.js +141 -164
  49. package/dist/cjs/engine/SaveSystem.js +441 -495
  50. package/dist/cjs/enterprise/Enterprise.js +254 -261
  51. package/dist/cjs/export/BevyExporter.js +927 -967
  52. package/dist/cjs/export/CocosExporter.js +644 -673
  53. package/dist/cjs/export/Construct3Exporter.js +783 -794
  54. package/dist/cjs/export/DefoldExporter.js +1028 -1069
  55. package/dist/cjs/export/GDevelopExporter.js +674 -708
  56. package/dist/cjs/export/GameMakerExporter.js +791 -805
  57. package/dist/cjs/export/GodotExporter.js +1003 -1045
  58. package/dist/cjs/export/MinecraftBedrockExporter.js +573 -578
  59. package/dist/cjs/export/MonoGameExporter.js +1265 -1293
  60. package/dist/cjs/export/Pico8Exporter.js +766 -812
  61. package/dist/cjs/export/PlatformExporter.js +188 -206
  62. package/dist/cjs/export/RPGMakerExporter.js +860 -879
  63. package/dist/cjs/export/RobloxExporter.js +883 -919
  64. package/dist/cjs/export/Solar2DExporter.js +1218 -1256
  65. package/dist/cjs/export/ThreeJSExporter.js +1006 -1073
  66. package/dist/cjs/export/Tic80Exporter.js +1210 -1267
  67. package/dist/cjs/export/UnityExporter.js +1116 -1131
  68. package/dist/cjs/export/UnrealExporter.js +889 -919
  69. package/dist/cjs/export/WebExporter.js +931 -970
  70. package/dist/cjs/export/index.js +42 -65
  71. package/dist/cjs/godot-integration/GodotAssetLibrary.js +659 -692
  72. package/dist/cjs/godot-integration/GodotImport.js +929 -1027
  73. package/dist/cjs/godot-integration/GodotLiveLink.js +915 -945
  74. package/dist/cjs/godot-integration/components/GodotProjectBrowser.js +911 -611
  75. package/dist/cjs/i18n/I18n.js +860 -900
  76. package/dist/cjs/i18n/useTranslation.js +84 -89
  77. package/dist/cjs/import/AsepriteImporter.js +658 -716
  78. package/dist/cjs/import/DragonBonesImporter.js +437 -461
  79. package/dist/cjs/import/GameMakerImporter.js +511 -521
  80. package/dist/cjs/import/GodotSceneImporter.js +788 -822
  81. package/dist/cjs/import/LDtkImporter.js +436 -454
  82. package/dist/cjs/import/Live2DImporter.js +497 -518
  83. package/dist/cjs/import/NdgFormat.js +424 -457
  84. package/dist/cjs/import/OgmoImporter.js +466 -493
  85. package/dist/cjs/import/RPGMakerImporter.js +451 -477
  86. package/dist/cjs/import/SceneImporter.js +378 -423
  87. package/dist/cjs/import/SpineImporter.js +511 -546
  88. package/dist/cjs/import/SpriterImporter.js +590 -613
  89. package/dist/cjs/import/TiledMapImporter.js +811 -846
  90. package/dist/cjs/import/UnitySceneImporter.js +687 -703
  91. package/dist/cjs/import/index.js +206 -251
  92. package/dist/cjs/index.js +275 -278
  93. package/dist/cjs/input/GamepadNavigation.js +185 -205
  94. package/dist/cjs/input/InputManager.js +397 -421
  95. package/dist/cjs/input/useGamepads.js +70 -81
  96. package/dist/cjs/integration/IconSprite.js +129 -256
  97. package/dist/cjs/inventory/InventorySystem.js +920 -995
  98. package/dist/cjs/kids/KidMode.js +997 -1022
  99. package/dist/cjs/kids/KidTools.js +570 -594
  100. package/dist/cjs/monetization/Monetization.js +170 -177
  101. package/dist/cjs/multiplayer/LocalMultiplayer.js +287 -287
  102. package/dist/cjs/multiplayer/MiniGameTypes.js +49 -59
  103. package/dist/cjs/native-engine/AnimationSystem.js +731 -806
  104. package/dist/cjs/native-engine/AssetPipeline.js +454 -515
  105. package/dist/cjs/native-engine/AudioEngine.js +763 -804
  106. package/dist/cjs/native-engine/InputSystem.js +1075 -1145
  107. package/dist/cjs/native-engine/NetworkingSystem.js +733 -793
  108. package/dist/cjs/native-engine/PhysicsEngine.js +583 -607
  109. package/dist/cjs/native-engine/RenderingEngine.js +302 -350
  110. package/dist/cjs/native-engine/SceneGraph.js +769 -808
  111. package/dist/cjs/network/MultiplayerTransport.js +97 -132
  112. package/dist/cjs/network/Networking.js +549 -564
  113. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AbortController.js +21 -29
  114. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js +40 -48
  115. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js +35 -43
  116. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js +10 -15
  117. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Errors.js +103 -135
  118. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js +131 -158
  119. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js +39 -53
  120. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js +5 -8
  121. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpClient.js +40 -48
  122. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js +494 -572
  123. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnection.js +826 -953
  124. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js +125 -146
  125. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js +13 -23
  126. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ILogger.js +11 -25
  127. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ITransport.js +13 -24
  128. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js +108 -121
  129. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Loggers.js +10 -14
  130. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js +150 -185
  131. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js +158 -196
  132. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js +90 -105
  133. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Subject.js +30 -35
  134. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js +16 -21
  135. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Utils.js +215 -248
  136. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js +135 -157
  137. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js +76 -85
  138. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/index.js +24 -27
  139. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/pkg-version.js +3 -4
  140. package/dist/cjs/pathfinding/Pathfinding.js +245 -263
  141. package/dist/cjs/performance/AssetStreaming.js +561 -635
  142. package/dist/cjs/performance/BenchmarkSuite.js +283 -311
  143. package/dist/cjs/performance/DeltaCompression.js +467 -526
  144. package/dist/cjs/performance/DeterministicPhysics.js +450 -498
  145. package/dist/cjs/performance/ECSOptimization.js +644 -695
  146. package/dist/cjs/performance/GameSaveSystem.js +509 -556
  147. package/dist/cjs/performance/HotReload.js +447 -500
  148. package/dist/cjs/performance/InputRecording.js +511 -561
  149. package/dist/cjs/performance/InterestManagement.js +453 -500
  150. package/dist/cjs/performance/LocalizationRuntime.js +376 -423
  151. package/dist/cjs/performance/MemoryManagement.js +410 -470
  152. package/dist/cjs/performance/ModSupport.js +445 -496
  153. package/dist/cjs/performance/MultiThreadedPhysics.js +400 -443
  154. package/dist/cjs/performance/NetworkPrediction.js +424 -482
  155. package/dist/cjs/performance/ObjectPooling.js +569 -617
  156. package/dist/cjs/performance/Performance.js +174 -190
  157. package/dist/cjs/performance/PerformanceAdvanced.js +614 -637
  158. package/dist/cjs/performance/WebGPURenderer.js +656 -683
  159. package/dist/cjs/physics/PhysicsAdvanced.js +345 -379
  160. package/dist/cjs/physics/PhysicsEngine2D.js +391 -427
  161. package/dist/cjs/plugins/PluginSDK.js +432 -466
  162. package/dist/cjs/plugins/PluginTestKit.js +307 -330
  163. package/dist/cjs/procedural/Procedural.js +243 -254
  164. package/dist/cjs/procedural/WorldBuilding.js +297 -314
  165. package/dist/cjs/quest/QuestSystem.js +975 -932
  166. package/dist/cjs/release/MobileExport.js +157 -163
  167. package/dist/cjs/release/Release.js +225 -238
  168. package/dist/cjs/rendering/Camera2D.js +121 -124
  169. package/dist/cjs/rendering/Renderer2D.js +321 -335
  170. package/dist/cjs/rendering/WebGPURenderPipeline.js +632 -651
  171. package/dist/cjs/runtime3d/Runtime3D.js +359 -378
  172. package/dist/cjs/runtime3d/SceneEditor3D.js +252 -263
  173. package/dist/cjs/scene/SceneManager.js +177 -193
  174. package/dist/cjs/scripting/GraphToAST.js +574 -547
  175. package/dist/cjs/scripting/LanguageExporter.js +254 -279
  176. package/dist/cjs/scripting/NodeGraph.js +546 -570
  177. package/dist/cjs/scripting/ScriptAST.js +31 -39
  178. package/dist/cjs/scripting/VisualScripting2.js +1016 -1065
  179. package/dist/cjs/scripting/exporters/CSharpExporter.js +464 -496
  180. package/dist/cjs/scripting/exporters/GDScriptExporter.js +390 -442
  181. package/dist/cjs/scripting/exporters/LuaExporter.js +406 -447
  182. package/dist/cjs/scripting/exporters/PythonExporter.js +484 -549
  183. package/dist/cjs/scripting/exporters/RustExporter.js +459 -512
  184. package/dist/cjs/scripting/exporters/TypeScriptExporter.js +505 -556
  185. package/dist/cjs/social/Social.js +72 -80
  186. package/dist/cjs/systems/LightingSystem.js +844 -931
  187. package/dist/cjs/systems/ParticleSystem2.js +1447 -1451
  188. package/dist/cjs/systems/PhysicsDebugSystem.js +1070 -1141
  189. package/dist/cjs/systems/TerrainSystem.js +985 -1098
  190. package/dist/cjs/templates/ActionTemplates.js +296 -309
  191. package/dist/cjs/templates/FightingTemplate.js +417 -463
  192. package/dist/cjs/templates/MetroidvaniaTemplate.js +353 -405
  193. package/dist/cjs/templates/PartyTemplates.js +190 -202
  194. package/dist/cjs/templates/PuzzleTemplates.js +245 -261
  195. package/dist/cjs/templates/RPGTemplates.js +160 -178
  196. package/dist/cjs/templates/SportsTemplates.js +237 -250
  197. package/dist/cjs/templates/StrategyTemplates.js +143 -153
  198. package/dist/cjs/templates/SurvivalTemplate.js +336 -380
  199. package/dist/cjs/templates/WaveDefense.js +499 -526
  200. package/dist/cjs/tilemap/Tilemap.js +207 -214
  201. package/dist/cjs/tutorials/TutorialBranching.js +396 -417
  202. package/dist/cjs/tutorials/TutorialEngine.js +888 -931
  203. package/dist/cjs/tutorials/TutorialOverlay.js +602 -640
  204. package/dist/cjs/tutorials/content/Tutorials_T05_T08.js +787 -809
  205. package/dist/cjs/tutorials/content/Tutorials_T09_T11.js +736 -756
  206. package/dist/cjs/tutorials/content/Tutorials_T12_T15.js +754 -778
  207. package/dist/cjs/ui/GameUI.js +295 -312
  208. package/dist/cjs/unity-integration/UnityAssetStore.js +700 -741
  209. package/dist/cjs/unity-integration/UnityImport.js +1066 -1176
  210. package/dist/cjs/unity-integration/UnityLiveLink.js +934 -981
  211. package/dist/cjs/unity-integration/components/UnityAssetStoreBrowser.js +1233 -757
  212. package/dist/cjs/unity-integration/components/UnityProjectBrowser.js +1240 -768
  213. package/dist/cjs/unreal-integration/UnrealImport.js +391 -412
  214. package/dist/cjs/unreal-integration/UnrealLiveLink.js +1102 -1137
  215. package/dist/cjs/unreal-integration/UnrealMarketplace.js +436 -473
  216. package/dist/cjs/unreal-integration/components/UnrealProjectBrowser.js +1150 -767
  217. package/dist/cjs/xr/ARVR.js +167 -175
  218. package/dist/esm/accessibility/Accessibility.js +160 -164
  219. package/dist/esm/ai/AI.js +310 -293
  220. package/dist/esm/ai/BehaviorTree.js +1057 -1112
  221. package/dist/esm/ai/StateMachine.js +709 -755
  222. package/dist/esm/animation/Animation.js +237 -240
  223. package/dist/esm/audio/AudioAdvanced.js +239 -235
  224. package/dist/esm/audio/AudioBridge.js +448 -434
  225. package/dist/esm/audio/AudioManager.js +243 -258
  226. package/dist/esm/build-cloud/BuildOrchestration.js +1071 -1097
  227. package/dist/esm/build-cloud/GodotBuildCloud.js +647 -669
  228. package/dist/esm/build-cloud/UnityBuildCloud.js +571 -609
  229. package/dist/esm/build-cloud/UnrealBuildCloud.js +615 -634
  230. package/dist/esm/core/EventBus.js +52 -55
  231. package/dist/esm/core/GameClock.js +63 -65
  232. package/dist/esm/core/GameConfig.js +77 -78
  233. package/dist/esm/core/GameLoop.js +81 -88
  234. package/dist/esm/core/ServiceLocator.js +63 -64
  235. package/dist/esm/core/Validation.js +82 -81
  236. package/dist/esm/core/math.js +109 -111
  237. package/dist/esm/devtools/DevTools.js +546 -562
  238. package/dist/esm/devtools/DeveloperExperience.js +535 -558
  239. package/dist/esm/devtools/GameplayAnalytics.js +647 -645
  240. package/dist/esm/dialogue/DialogueSystem.js +998 -985
  241. package/dist/esm/docs/DocGenerator.js +342 -363
  242. package/dist/esm/ecs/World.js +358 -370
  243. package/dist/esm/editor/AchievementEditor.js +584 -635
  244. package/dist/esm/editor/AdvancedEditor.js +191 -190
  245. package/dist/esm/editor/AnimationRetargeting.js +700 -740
  246. package/dist/esm/editor/AssetBrowser.js +1078 -1159
  247. package/dist/esm/editor/AssetManager.js +188 -191
  248. package/dist/esm/editor/ConsolePro.js +977 -1060
  249. package/dist/esm/editor/CraftingEditor.js +684 -729
  250. package/dist/esm/editor/DebugTools.js +194 -184
  251. package/dist/esm/editor/DialogueSystem.js +849 -900
  252. package/dist/esm/editor/LeaderboardSystem.js +433 -482
  253. package/dist/esm/editor/NiceGameEditor.js +1908 -1200
  254. package/dist/esm/editor/PrefabSystem.js +777 -837
  255. package/dist/esm/editor/ProfilerPanel.js +654 -699
  256. package/dist/esm/editor/QuestEditor.js +795 -838
  257. package/dist/esm/editor/ReplaySystem.js +591 -633
  258. package/dist/esm/editor/SceneEditor.js +189 -199
  259. package/dist/esm/editor/ScreenshotMode.js +628 -687
  260. package/dist/esm/editor/ShaderGraph.js +1565 -1581
  261. package/dist/esm/editor/TimelineEditor.js +733 -777
  262. package/dist/esm/editor/UndoRedoPro.js +835 -903
  263. package/dist/esm/editor/VRARMode.js +586 -644
  264. package/dist/esm/engine/NiceGameEngine.js +142 -163
  265. package/dist/esm/engine/SaveSystem.js +447 -494
  266. package/dist/esm/enterprise/Enterprise.js +265 -260
  267. package/dist/esm/export/BevyExporter.js +929 -966
  268. package/dist/esm/export/CocosExporter.js +646 -672
  269. package/dist/esm/export/Construct3Exporter.js +785 -793
  270. package/dist/esm/export/DefoldExporter.js +1030 -1068
  271. package/dist/esm/export/GDevelopExporter.js +676 -707
  272. package/dist/esm/export/GameMakerExporter.js +793 -804
  273. package/dist/esm/export/GodotExporter.js +1004 -1044
  274. package/dist/esm/export/MinecraftBedrockExporter.js +575 -577
  275. package/dist/esm/export/MonoGameExporter.js +1267 -1292
  276. package/dist/esm/export/Pico8Exporter.js +768 -811
  277. package/dist/esm/export/PlatformExporter.js +193 -205
  278. package/dist/esm/export/RPGMakerExporter.js +862 -878
  279. package/dist/esm/export/RobloxExporter.js +885 -918
  280. package/dist/esm/export/Solar2DExporter.js +1220 -1255
  281. package/dist/esm/export/ThreeJSExporter.js +1007 -1072
  282. package/dist/esm/export/Tic80Exporter.js +1212 -1266
  283. package/dist/esm/export/UnityExporter.js +1117 -1130
  284. package/dist/esm/export/UnrealExporter.js +891 -918
  285. package/dist/esm/export/WebExporter.js +933 -969
  286. package/dist/esm/export/index.js +86 -71
  287. package/dist/esm/godot-integration/GodotAssetLibrary.js +663 -691
  288. package/dist/esm/godot-integration/GodotImport.js +935 -1026
  289. package/dist/esm/godot-integration/GodotLiveLink.js +918 -944
  290. package/dist/esm/godot-integration/components/GodotProjectBrowser.js +912 -610
  291. package/dist/esm/i18n/I18n.js +871 -899
  292. package/dist/esm/i18n/useTranslation.js +88 -88
  293. package/dist/esm/import/AsepriteImporter.js +637 -715
  294. package/dist/esm/import/DragonBonesImporter.js +417 -460
  295. package/dist/esm/import/GameMakerImporter.js +491 -520
  296. package/dist/esm/import/GodotSceneImporter.js +767 -821
  297. package/dist/esm/import/LDtkImporter.js +415 -453
  298. package/dist/esm/import/Live2DImporter.js +477 -517
  299. package/dist/esm/import/NdgFormat.js +410 -456
  300. package/dist/esm/import/OgmoImporter.js +446 -492
  301. package/dist/esm/import/RPGMakerImporter.js +431 -476
  302. package/dist/esm/import/SceneImporter.js +385 -422
  303. package/dist/esm/import/SpineImporter.js +491 -545
  304. package/dist/esm/import/SpriterImporter.js +570 -612
  305. package/dist/esm/import/TiledMapImporter.js +790 -845
  306. package/dist/esm/import/UnitySceneImporter.js +666 -702
  307. package/dist/esm/import/index.js +247 -258
  308. package/dist/esm/index.js +1332 -188
  309. package/dist/esm/input/GamepadNavigation.js +187 -204
  310. package/dist/esm/input/InputManager.js +401 -420
  311. package/dist/esm/input/useGamepads.js +72 -80
  312. package/dist/esm/integration/IconSprite.js +143 -255
  313. package/dist/esm/inventory/InventorySystem.js +925 -994
  314. package/dist/esm/kids/KidMode.js +1003 -1021
  315. package/dist/esm/kids/KidTools.js +573 -593
  316. package/dist/esm/monetization/Monetization.js +180 -176
  317. package/dist/esm/multiplayer/LocalMultiplayer.js +290 -286
  318. package/dist/esm/multiplayer/MiniGameTypes.js +58 -58
  319. package/dist/esm/native-engine/AnimationSystem.js +735 -805
  320. package/dist/esm/native-engine/AssetPipeline.js +456 -514
  321. package/dist/esm/native-engine/AudioEngine.js +765 -803
  322. package/dist/esm/native-engine/InputSystem.js +1079 -1144
  323. package/dist/esm/native-engine/NetworkingSystem.js +740 -792
  324. package/dist/esm/native-engine/PhysicsEngine.js +585 -606
  325. package/dist/esm/native-engine/RenderingEngine.js +304 -349
  326. package/dist/esm/native-engine/SceneGraph.js +773 -807
  327. package/dist/esm/network/MultiplayerTransport.js +99 -131
  328. package/dist/esm/network/Networking.js +556 -563
  329. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AbortController.js +22 -28
  330. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js +41 -47
  331. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js +36 -42
  332. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js +11 -14
  333. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Errors.js +111 -134
  334. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js +132 -157
  335. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js +40 -52
  336. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js +6 -7
  337. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpClient.js +42 -47
  338. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js +496 -571
  339. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnection.js +828 -952
  340. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js +126 -145
  341. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js +14 -23
  342. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ILogger.js +12 -25
  343. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ITransport.js +15 -24
  344. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js +109 -120
  345. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Loggers.js +11 -13
  346. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js +151 -184
  347. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js +159 -195
  348. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js +91 -104
  349. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Subject.js +31 -34
  350. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js +17 -20
  351. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Utils.js +230 -248
  352. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js +136 -156
  353. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js +77 -84
  354. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/index.js +31 -12
  355. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/pkg-version.js +4 -3
  356. package/dist/esm/pathfinding/Pathfinding.js +249 -262
  357. package/dist/esm/performance/AssetStreaming.js +567 -634
  358. package/dist/esm/performance/BenchmarkSuite.js +285 -310
  359. package/dist/esm/performance/DeltaCompression.js +478 -525
  360. package/dist/esm/performance/DeterministicPhysics.js +479 -497
  361. package/dist/esm/performance/ECSOptimization.js +653 -694
  362. package/dist/esm/performance/GameSaveSystem.js +515 -555
  363. package/dist/esm/performance/HotReload.js +454 -499
  364. package/dist/esm/performance/InputRecording.js +520 -560
  365. package/dist/esm/performance/InterestManagement.js +460 -499
  366. package/dist/esm/performance/LocalizationRuntime.js +382 -422
  367. package/dist/esm/performance/MemoryManagement.js +415 -469
  368. package/dist/esm/performance/ModSupport.js +455 -495
  369. package/dist/esm/performance/MultiThreadedPhysics.js +408 -442
  370. package/dist/esm/performance/NetworkPrediction.js +436 -481
  371. package/dist/esm/performance/ObjectPooling.js +582 -616
  372. package/dist/esm/performance/Performance.js +182 -189
  373. package/dist/esm/performance/PerformanceAdvanced.js +621 -636
  374. package/dist/esm/performance/WebGPURenderer.js +660 -682
  375. package/dist/esm/physics/PhysicsAdvanced.js +359 -378
  376. package/dist/esm/physics/PhysicsEngine2D.js +393 -426
  377. package/dist/esm/plugins/PluginSDK.js +435 -465
  378. package/dist/esm/plugins/PluginTestKit.js +315 -329
  379. package/dist/esm/procedural/Procedural.js +248 -253
  380. package/dist/esm/procedural/WorldBuilding.js +316 -313
  381. package/dist/esm/quest/QuestSystem.js +982 -931
  382. package/dist/esm/release/MobileExport.js +163 -162
  383. package/dist/esm/release/Release.js +234 -237
  384. package/dist/esm/rendering/Camera2D.js +122 -123
  385. package/dist/esm/rendering/Renderer2D.js +323 -334
  386. package/dist/esm/rendering/WebGPURenderPipeline.js +647 -650
  387. package/dist/esm/runtime3d/Runtime3D.js +368 -377
  388. package/dist/esm/runtime3d/SceneEditor3D.js +268 -262
  389. package/dist/esm/scene/SceneManager.js +178 -192
  390. package/dist/esm/scripting/GraphToAST.js +576 -546
  391. package/dist/esm/scripting/LanguageExporter.js +263 -278
  392. package/dist/esm/scripting/NodeGraph.js +550 -569
  393. package/dist/esm/scripting/ScriptAST.js +45 -38
  394. package/dist/esm/scripting/VisualScripting2.js +1025 -1064
  395. package/dist/esm/scripting/exporters/CSharpExporter.js +465 -495
  396. package/dist/esm/scripting/exporters/GDScriptExporter.js +391 -441
  397. package/dist/esm/scripting/exporters/LuaExporter.js +407 -446
  398. package/dist/esm/scripting/exporters/PythonExporter.js +485 -548
  399. package/dist/esm/scripting/exporters/RustExporter.js +460 -511
  400. package/dist/esm/scripting/exporters/TypeScriptExporter.js +506 -555
  401. package/dist/esm/social/Social.js +77 -79
  402. package/dist/esm/systems/LightingSystem.js +855 -930
  403. package/dist/esm/systems/ParticleSystem2.js +1453 -1450
  404. package/dist/esm/systems/PhysicsDebugSystem.js +1078 -1140
  405. package/dist/esm/systems/TerrainSystem.js +996 -1097
  406. package/dist/esm/templates/ActionTemplates.js +307 -308
  407. package/dist/esm/templates/FightingTemplate.js +439 -462
  408. package/dist/esm/templates/MetroidvaniaTemplate.js +391 -404
  409. package/dist/esm/templates/PartyTemplates.js +206 -201
  410. package/dist/esm/templates/PuzzleTemplates.js +260 -260
  411. package/dist/esm/templates/RPGTemplates.js +172 -177
  412. package/dist/esm/templates/SportsTemplates.js +251 -249
  413. package/dist/esm/templates/StrategyTemplates.js +158 -152
  414. package/dist/esm/templates/SurvivalTemplate.js +367 -379
  415. package/dist/esm/templates/WaveDefense.js +501 -525
  416. package/dist/esm/tilemap/Tilemap.js +209 -213
  417. package/dist/esm/tutorials/TutorialBranching.js +399 -416
  418. package/dist/esm/tutorials/TutorialEngine.js +893 -930
  419. package/dist/esm/tutorials/TutorialOverlay.js +603 -639
  420. package/dist/esm/tutorials/content/Tutorials_T05_T08.js +792 -808
  421. package/dist/esm/tutorials/content/Tutorials_T09_T11.js +740 -755
  422. package/dist/esm/tutorials/content/Tutorials_T12_T15.js +759 -777
  423. package/dist/esm/ui/GameUI.js +296 -311
  424. package/dist/esm/unity-integration/UnityAssetStore.js +704 -740
  425. package/dist/esm/unity-integration/UnityImport.js +1071 -1175
  426. package/dist/esm/unity-integration/UnityLiveLink.js +938 -980
  427. package/dist/esm/unity-integration/components/UnityAssetStoreBrowser.js +1234 -756
  428. package/dist/esm/unity-integration/components/UnityProjectBrowser.js +1241 -767
  429. package/dist/esm/unreal-integration/UnrealImport.js +395 -411
  430. package/dist/esm/unreal-integration/UnrealLiveLink.js +1105 -1136
  431. package/dist/esm/unreal-integration/UnrealMarketplace.js +442 -472
  432. package/dist/esm/unreal-integration/components/UnrealProjectBrowser.js +1151 -766
  433. package/dist/esm/xr/ARVR.js +181 -174
  434. package/package.json +7 -12
@@ -1,1285 +1,1993 @@
1
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { NiceTutorialButton, resolveTutorial, NiceIcon, clickOnActivationKey } from '@nice2dev/ui';
3
- import React, { useReducer, useRef, useState, useCallback, useMemo, useContext, useEffect, createContext } from 'react';
4
- import { EventBus } from '../core/EventBus.js';
5
- import { World } from '../ecs/World.js';
6
- import { useTranslation } from '../i18n/useTranslation.js';
7
- import { SceneManager } from '../scene/SceneManager.js';
8
-
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { NiceTutorialButton, resolveTutorial, NiceIcon, clickOnActivationKey } from "@nice2dev/ui";
3
+ import React, { useReducer, useRef, useState, useCallback, useMemo, useContext, useEffect, createContext } from "react";
4
+ import { EventBus } from "../core/EventBus.js";
5
+ import { World } from "../ecs/World.js";
6
+ import { useTranslation } from "../i18n/useTranslation.js";
7
+ import { SceneManager } from "../scene/SceneManager.js";
9
8
  class UndoStack {
10
- constructor(maxSize = 100) {
11
- this.stack = [];
12
- this.pointer = -1;
13
- this.events = new EventBus();
14
- this.maxSize = maxSize;
9
+ constructor(maxSize = 100) {
10
+ this.stack = [];
11
+ this.pointer = -1;
12
+ this.events = new EventBus();
13
+ this.maxSize = maxSize;
14
+ }
15
+ push(entry) {
16
+ this.stack.splice(this.pointer + 1);
17
+ this.stack.push(entry);
18
+ if (this.stack.length > this.maxSize) {
19
+ this.stack.shift();
15
20
  }
16
- push(entry) {
17
- // Remove everything after current pointer
18
- this.stack.splice(this.pointer + 1);
19
- this.stack.push(entry);
20
- if (this.stack.length > this.maxSize) {
21
- this.stack.shift();
22
- }
23
- this.pointer = this.stack.length - 1;
24
- this.events.emit('change', null);
25
- }
26
- undo() {
27
- if (this.pointer < 0) {
28
- return null;
29
- }
30
- const entry = this.stack[this.pointer];
31
- entry.undo();
32
- this.pointer--;
33
- this.events.emit('change', null);
34
- return entry.description;
35
- }
36
- redo() {
37
- if (this.pointer >= this.stack.length - 1) {
38
- return null;
39
- }
40
- this.pointer++;
41
- const entry = this.stack[this.pointer];
42
- entry.redo();
43
- this.events.emit('change', null);
44
- return entry.description;
45
- }
46
- get canUndo() {
47
- return this.pointer >= 0;
48
- }
49
- get canRedo() {
50
- return this.pointer < this.stack.length - 1;
21
+ this.pointer = this.stack.length - 1;
22
+ this.events.emit("change", null);
23
+ }
24
+ undo() {
25
+ if (this.pointer < 0) {
26
+ return null;
51
27
  }
52
- clear() {
53
- this.stack.length = 0;
54
- this.pointer = -1;
55
- this.events.emit('change', null);
28
+ const entry = this.stack[this.pointer];
29
+ entry.undo();
30
+ this.pointer--;
31
+ this.events.emit("change", null);
32
+ return entry.description;
33
+ }
34
+ redo() {
35
+ if (this.pointer >= this.stack.length - 1) {
36
+ return null;
56
37
  }
38
+ this.pointer++;
39
+ const entry = this.stack[this.pointer];
40
+ entry.redo();
41
+ this.events.emit("change", null);
42
+ return entry.description;
43
+ }
44
+ get canUndo() {
45
+ return this.pointer >= 0;
46
+ }
47
+ get canRedo() {
48
+ return this.pointer < this.stack.length - 1;
49
+ }
50
+ clear() {
51
+ this.stack.length = 0;
52
+ this.pointer = -1;
53
+ this.events.emit("change", null);
54
+ }
57
55
  }
58
56
  const DEFAULT_PANELS = [
59
- { id: 'hierarchy', title: 'Hierarchy', visible: true, position: 'left', width: 250 },
60
- { id: 'inspector', title: 'Inspector', visible: true, position: 'right', width: 300 },
61
- { id: 'assets', title: 'Assets', visible: true, position: 'bottom', height: 200 },
62
- { id: 'console', title: 'Console', visible: true, position: 'bottom', height: 150 },
57
+ { id: "hierarchy", title: "Hierarchy", visible: true, position: "left", width: 250 },
58
+ { id: "inspector", title: "Inspector", visible: true, position: "right", width: 300 },
59
+ { id: "assets", title: "Assets", visible: true, position: "bottom", height: 200 },
60
+ { id: "console", title: "Console", visible: true, position: "bottom", height: 150 }
63
61
  ];
64
62
  const DEFAULT_STATE = {
65
- mode: 'edit',
66
- tool: 'select',
67
- selectedEntities: [],
68
- clipboard: [],
69
- gridSize: 16,
70
- snapToGrid: true,
71
- showGrid: true,
72
- showRulers: true,
73
- zoom: 1,
74
- panOffset: { x: 0, y: 0 },
75
- panels: DEFAULT_PANELS,
76
- currentSceneId: null,
77
- projectPath: null,
78
- dirty: false,
63
+ mode: "edit",
64
+ tool: "select",
65
+ selectedEntities: [],
66
+ clipboard: [],
67
+ gridSize: 16,
68
+ snapToGrid: true,
69
+ showGrid: true,
70
+ showRulers: true,
71
+ zoom: 1,
72
+ panOffset: { x: 0, y: 0 },
73
+ panels: DEFAULT_PANELS,
74
+ currentSceneId: null,
75
+ projectPath: null,
76
+ dirty: false
79
77
  };
80
78
  function editorReducer(state, action) {
81
- switch (action.type) {
82
- case 'SET_MODE':
83
- return { ...state, mode: action.mode };
84
- case 'SET_TOOL':
85
- return { ...state, tool: action.tool };
86
- case 'SELECT':
87
- return { ...state, selectedEntities: action.entities };
88
- case 'ADD_SELECTION':
89
- return {
90
- ...state,
91
- selectedEntities: [...new Set([...state.selectedEntities, ...action.entities])],
92
- };
93
- case 'DESELECT_ALL':
94
- return { ...state, selectedEntities: [] };
95
- case 'SET_CLIPBOARD':
96
- return { ...state, clipboard: action.entities };
97
- case 'SET_GRID_SIZE':
98
- return { ...state, gridSize: action.size };
99
- case 'TOGGLE_SNAP':
100
- return { ...state, snapToGrid: !state.snapToGrid };
101
- case 'TOGGLE_GRID':
102
- return { ...state, showGrid: !state.showGrid };
103
- case 'SET_ZOOM':
104
- return { ...state, zoom: Math.max(0.1, Math.min(10, action.zoom)) };
105
- case 'SET_PAN':
106
- return { ...state, panOffset: action.offset };
107
- case 'TOGGLE_PANEL':
108
- return {
109
- ...state,
110
- panels: state.panels.map((p) => p.id === action.panelId ? { ...p, visible: !p.visible } : p),
111
- };
112
- case 'SET_SCENE':
113
- return { ...state, currentSceneId: action.sceneId };
114
- case 'MARK_DIRTY':
115
- return { ...state, dirty: true };
116
- case 'MARK_CLEAN':
117
- return { ...state, dirty: false };
118
- default:
119
- return state;
120
- }
79
+ switch (action.type) {
80
+ case "SET_MODE":
81
+ return { ...state, mode: action.mode };
82
+ case "SET_TOOL":
83
+ return { ...state, tool: action.tool };
84
+ case "SELECT":
85
+ return { ...state, selectedEntities: action.entities };
86
+ case "ADD_SELECTION":
87
+ return {
88
+ ...state,
89
+ selectedEntities: [.../* @__PURE__ */ new Set([...state.selectedEntities, ...action.entities])]
90
+ };
91
+ case "DESELECT_ALL":
92
+ return { ...state, selectedEntities: [] };
93
+ case "SET_CLIPBOARD":
94
+ return { ...state, clipboard: action.entities };
95
+ case "SET_GRID_SIZE":
96
+ return { ...state, gridSize: action.size };
97
+ case "TOGGLE_SNAP":
98
+ return { ...state, snapToGrid: !state.snapToGrid };
99
+ case "TOGGLE_GRID":
100
+ return { ...state, showGrid: !state.showGrid };
101
+ case "SET_ZOOM":
102
+ return { ...state, zoom: Math.max(0.1, Math.min(10, action.zoom)) };
103
+ case "SET_PAN":
104
+ return { ...state, panOffset: action.offset };
105
+ case "TOGGLE_PANEL":
106
+ return {
107
+ ...state,
108
+ panels: state.panels.map(
109
+ (p) => p.id === action.panelId ? { ...p, visible: !p.visible } : p
110
+ )
111
+ };
112
+ case "SET_SCENE":
113
+ return { ...state, currentSceneId: action.sceneId };
114
+ case "MARK_DIRTY":
115
+ return { ...state, dirty: true };
116
+ case "MARK_CLEAN":
117
+ return { ...state, dirty: false };
118
+ default:
119
+ return state;
120
+ }
121
121
  }
122
122
  const EditorContext = /* @__PURE__ */ createContext(null);
123
123
  function useEditor() {
124
- const ctx = useContext(EditorContext);
125
- if (!ctx) {
126
- throw new Error('useEditor must be used within NiceGameEditor');
127
- }
128
- return ctx;
124
+ const ctx = useContext(EditorContext);
125
+ if (!ctx) {
126
+ throw new Error("useEditor must be used within NiceGameEditor");
127
+ }
128
+ return ctx;
129
129
  }
130
130
  function useKeyboardShortcuts(shortcuts) {
131
- useEffect(() => {
132
- const handler = (e) => {
133
- for (const s of shortcuts) {
134
- if (e.key.toLowerCase() === s.key.toLowerCase() &&
135
- !!e.ctrlKey === !!s.ctrl &&
136
- !!e.shiftKey === !!s.shift &&
137
- !!e.altKey === !!s.alt) {
138
- e.preventDefault();
139
- s.action();
140
- return;
141
- }
142
- }
143
- };
144
- window.addEventListener('keydown', handler);
145
- return () => window.removeEventListener('keydown', handler);
146
- }, [shortcuts]);
131
+ useEffect(() => {
132
+ const handler = (e) => {
133
+ for (const s of shortcuts) {
134
+ if (e.key.toLowerCase() === s.key.toLowerCase() && !!e.ctrlKey === !!s.ctrl && !!e.shiftKey === !!s.shift && !!e.altKey === !!s.alt) {
135
+ e.preventDefault();
136
+ s.action();
137
+ return;
138
+ }
139
+ }
140
+ };
141
+ window.addEventListener("keydown", handler);
142
+ return () => window.removeEventListener("keydown", handler);
143
+ }, [shortcuts]);
147
144
  }
148
- /* ── .ndg Project Format ──────────────────────────────────────── */
149
145
  function serializeProject(project) {
150
- return JSON.stringify(project, null, 2);
146
+ return JSON.stringify(project, null, 2);
151
147
  }
152
148
  function deserializeProject(json) {
153
- return JSON.parse(json);
149
+ return JSON.parse(json);
154
150
  }
155
- /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
156
151
  const GAME_EDITOR_TOUR = [
157
- {
158
- target: '.nice-game-editor',
159
- titleKey: 'tutorial.gameEditor.intro.title',
160
- title: 'Game editor',
161
- contentKey: 'tutorial.gameEditor.intro.content',
162
- content: 'Build your scene in the viewport using the hierarchy and asset panels, tweak entities in the inspector, and save the project.',
163
- placement: 'auto',
164
- },
152
+ {
153
+ target: ".nice-game-editor",
154
+ titleKey: "tutorial.gameEditor.intro.title",
155
+ title: "Game editor",
156
+ contentKey: "tutorial.gameEditor.intro.content",
157
+ content: "Build your scene in the viewport using the hierarchy and asset panels, tweak entities in the inspector, and save the project.",
158
+ placement: "auto"
159
+ }
165
160
  ];
166
- const NiceGameEditor = ({ project, onSave, className, style, tutorial, }) => {
167
- const { t: tr } = useTranslation('engine');
168
- const [state, dispatch] = useReducer(editorReducer, DEFAULT_STATE);
169
- const worldRef = useRef(new World());
170
- const undoRef = useRef(new UndoStack());
171
- const sceneManagerRef = useRef(new SceneManager());
172
- const eventsRef = useRef(new EventBus());
173
- const world = worldRef.current;
174
- const undoStack = undoRef.current;
175
- const sceneManager = sceneManagerRef.current;
176
- const events = eventsRef.current;
177
- const [worldVersion, setWorldVersion] = useState(0);
178
- const [consoleLogs, setConsoleLogs] = useState([]);
179
- const logIdRef = useRef(0);
180
- const refreshWorld = useCallback(() => setWorldVersion((v) => v + 1), []);
181
- const addLog = useCallback((level, message) => {
182
- setConsoleLogs((prev) => [
183
- ...prev.slice(-199),
184
- { id: logIdRef.current++, level, message, timestamp: Date.now() },
185
- ]);
186
- }, []);
187
- const createNewEntity = useCallback((name, components = ['Transform']) => {
188
- const entity = world.createEntity(name);
189
- for (const comp of components) {
190
- world.addComponent(entity.id, comp);
191
- }
192
- dispatch({ type: 'SELECT', entities: [entity.id] });
193
- dispatch({ type: 'MARK_DIRTY' });
194
- refreshWorld();
195
- addLog('info', `Created entity "${name}" (#${entity.id})`);
196
- return entity.id;
197
- }, [world, refreshWorld, addLog]);
198
- // Keyboard shortcuts. The list is built once; Save and Delete reach the handlers of the latest
199
- // render through a ref (they are declared below and change with the selection and the props) —
200
- // until 1.2.2 they called the first render's, so Delete never saw a selection.
201
- const shortcutActionsRef = useRef({ save: () => { }, deleteSelected: () => { } });
202
- const shortcuts = useMemo(() => [
203
- { key: 'z', ctrl: true, action: () => undoStack.undo(), description: 'Undo' },
204
- { key: 'z', ctrl: true, shift: true, action: () => undoStack.redo(), description: 'Redo' },
205
- { key: 'y', ctrl: true, action: () => undoStack.redo(), description: 'Redo' },
206
- {
207
- key: 's',
208
- ctrl: true,
209
- action: () => shortcutActionsRef.current.save(),
210
- description: 'Save',
211
- },
212
- {
213
- key: 'v',
214
- action: () => dispatch({ type: 'SET_TOOL', tool: 'select' }),
215
- description: 'Select tool',
216
- },
217
- {
218
- key: 'w',
219
- action: () => dispatch({ type: 'SET_TOOL', tool: 'move' }),
220
- description: 'Move tool',
221
- },
222
- {
223
- key: 'e',
224
- action: () => dispatch({ type: 'SET_TOOL', tool: 'rotate' }),
225
- description: 'Rotate tool',
226
- },
227
- {
228
- key: 'r',
229
- action: () => dispatch({ type: 'SET_TOOL', tool: 'scale' }),
230
- description: 'Scale tool',
231
- },
232
- {
233
- key: 'g',
234
- ctrl: true,
235
- action: () => dispatch({ type: 'TOGGLE_GRID' }),
236
- description: 'Toggle grid',
237
- },
238
- {
239
- key: 'Delete',
240
- action: () => shortcutActionsRef.current.deleteSelected(),
241
- description: 'Delete selected',
242
- },
243
- {
244
- key: 'Escape',
245
- action: () => dispatch({ type: 'DESELECT_ALL' }),
246
- description: 'Deselect all',
161
+ const NiceGameEditor = ({
162
+ project,
163
+ onSave,
164
+ className,
165
+ style,
166
+ tutorial
167
+ }) => {
168
+ const { t: tr } = useTranslation("engine");
169
+ const [state, dispatch] = useReducer(editorReducer, DEFAULT_STATE);
170
+ const worldRef = useRef(new World());
171
+ const undoRef = useRef(new UndoStack());
172
+ const sceneManagerRef = useRef(new SceneManager());
173
+ const eventsRef = useRef(new EventBus());
174
+ const world = worldRef.current;
175
+ const undoStack = undoRef.current;
176
+ const sceneManager = sceneManagerRef.current;
177
+ const events = eventsRef.current;
178
+ const [worldVersion, setWorldVersion] = useState(0);
179
+ const [consoleLogs, setConsoleLogs] = useState([]);
180
+ const logIdRef = useRef(0);
181
+ const refreshWorld = useCallback(() => setWorldVersion((v) => v + 1), []);
182
+ const addLog = useCallback((level, message) => {
183
+ setConsoleLogs((prev) => [
184
+ ...prev.slice(-199),
185
+ { id: logIdRef.current++, level, message, timestamp: Date.now() }
186
+ ]);
187
+ }, []);
188
+ const createNewEntity = useCallback(
189
+ (name, components = ["Transform"]) => {
190
+ const entity = world.createEntity(name);
191
+ for (const comp of components) {
192
+ world.addComponent(entity.id, comp);
193
+ }
194
+ dispatch({ type: "SELECT", entities: [entity.id] });
195
+ dispatch({ type: "MARK_DIRTY" });
196
+ refreshWorld();
197
+ addLog("info", `Created entity "${name}" (#${entity.id})`);
198
+ return entity.id;
199
+ },
200
+ [world, refreshWorld, addLog]
201
+ );
202
+ const shortcutActionsRef = useRef({ save: () => {
203
+ }, deleteSelected: () => {
204
+ } });
205
+ const shortcuts = useMemo(
206
+ () => [
207
+ { key: "z", ctrl: true, action: () => undoStack.undo(), description: "Undo" },
208
+ { key: "z", ctrl: true, shift: true, action: () => undoStack.redo(), description: "Redo" },
209
+ { key: "y", ctrl: true, action: () => undoStack.redo(), description: "Redo" },
210
+ {
211
+ key: "s",
212
+ ctrl: true,
213
+ action: () => shortcutActionsRef.current.save(),
214
+ description: "Save"
215
+ },
216
+ {
217
+ key: "v",
218
+ action: () => dispatch({ type: "SET_TOOL", tool: "select" }),
219
+ description: "Select tool"
220
+ },
221
+ {
222
+ key: "w",
223
+ action: () => dispatch({ type: "SET_TOOL", tool: "move" }),
224
+ description: "Move tool"
225
+ },
226
+ {
227
+ key: "e",
228
+ action: () => dispatch({ type: "SET_TOOL", tool: "rotate" }),
229
+ description: "Rotate tool"
230
+ },
231
+ {
232
+ key: "r",
233
+ action: () => dispatch({ type: "SET_TOOL", tool: "scale" }),
234
+ description: "Scale tool"
235
+ },
236
+ {
237
+ key: "g",
238
+ ctrl: true,
239
+ action: () => dispatch({ type: "TOGGLE_GRID" }),
240
+ description: "Toggle grid"
241
+ },
242
+ {
243
+ key: "Delete",
244
+ action: () => shortcutActionsRef.current.deleteSelected(),
245
+ description: "Delete selected"
246
+ },
247
+ {
248
+ key: "Escape",
249
+ action: () => dispatch({ type: "DESELECT_ALL" }),
250
+ description: "Deselect all"
251
+ }
252
+ ],
253
+ [undoStack]
254
+ );
255
+ useKeyboardShortcuts(shortcuts);
256
+ const handleSave = useCallback(() => {
257
+ if (!onSave) {
258
+ return;
259
+ }
260
+ const sceneData = sceneManager.serializeScene(
261
+ world,
262
+ state.currentSceneId ?? "main",
263
+ "Main Scene"
264
+ );
265
+ const proj = project ?? {
266
+ formatVersion: 1,
267
+ name: "Untitled",
268
+ version: "0.1.0",
269
+ author: "",
270
+ description: "",
271
+ settings: {
272
+ width: 1280,
273
+ height: 720,
274
+ targetFps: 60,
275
+ maxPlayers: 4,
276
+ physics: {
277
+ gravity: { x: 0, y: 980 },
278
+ fixedTimestep: 1 / 60,
279
+ velocityIterations: 8,
280
+ positionIterations: 3
247
281
  },
248
- ], [undoStack]);
249
- useKeyboardShortcuts(shortcuts);
250
- const handleSave = useCallback(() => {
251
- if (!onSave) {
252
- return;
282
+ audio: { masterVolume: 1, musicVolume: 0.7, sfxVolume: 1, voiceVolume: 1 },
283
+ rendering: {
284
+ pixelPerfect: false,
285
+ antiAlias: true,
286
+ backgroundColor: { r: 0.1, g: 0.1, b: 0.15, a: 1 }
253
287
  }
254
- const sceneData = sceneManager.serializeScene(world, state.currentSceneId ?? 'main', 'Main Scene');
255
- const proj = project ?? {
256
- formatVersion: 1,
257
- name: 'Untitled',
258
- version: '0.1.0',
259
- author: '',
260
- description: '',
261
- settings: {
262
- width: 1280,
263
- height: 720,
264
- targetFps: 60,
265
- maxPlayers: 4,
266
- physics: {
267
- gravity: { x: 0, y: 980 },
268
- fixedTimestep: 1 / 60,
269
- velocityIterations: 8,
270
- positionIterations: 3,
271
- },
272
- audio: { masterVolume: 1, musicVolume: 0.7, sfxVolume: 1, voiceVolume: 1 },
273
- rendering: {
274
- pixelPerfect: false,
275
- antiAlias: true,
276
- backgroundColor: { r: 0.1, g: 0.1, b: 0.15, a: 1 },
277
- },
278
- },
279
- scenes: [sceneData],
280
- startSceneId: sceneData.id,
281
- assets: { assets: [] },
282
- };
283
- onSave(proj);
284
- dispatch({ type: 'MARK_CLEAN' });
285
- }, [world, sceneManager, state.currentSceneId, project, onSave]);
286
- const deleteSelected = useCallback(() => {
287
- const toDelete = [...state.selectedEntities];
288
- if (toDelete.length === 0) {
289
- return;
290
- }
291
- // Store for undo
292
- const snapshots = toDelete.map((id) => ({
293
- entity: world.getEntity(id),
294
- id,
295
- }));
288
+ },
289
+ scenes: [sceneData],
290
+ startSceneId: sceneData.id,
291
+ assets: { assets: [] }
292
+ };
293
+ onSave(proj);
294
+ dispatch({ type: "MARK_CLEAN" });
295
+ }, [world, sceneManager, state.currentSceneId, project, onSave]);
296
+ const deleteSelected = useCallback(() => {
297
+ const toDelete = [...state.selectedEntities];
298
+ if (toDelete.length === 0) {
299
+ return;
300
+ }
301
+ const snapshots = toDelete.map((id) => ({
302
+ entity: world.getEntity(id),
303
+ id
304
+ }));
305
+ for (const id of toDelete) {
306
+ world.destroyEntity(id);
307
+ }
308
+ dispatch({ type: "DESELECT_ALL" });
309
+ dispatch({ type: "MARK_DIRTY" });
310
+ refreshWorld();
311
+ addLog("info", `Deleted ${toDelete.length} entity(ies)`);
312
+ undoStack.push({
313
+ description: `Delete ${toDelete.length} entity(ies)`,
314
+ undo: () => {
315
+ events.emit("editor:undo:delete", snapshots);
316
+ },
317
+ redo: () => {
296
318
  for (const id of toDelete) {
297
- world.destroyEntity(id);
319
+ world.destroyEntity(id);
298
320
  }
299
- dispatch({ type: 'DESELECT_ALL' });
300
- dispatch({ type: 'MARK_DIRTY' });
301
- refreshWorld();
302
- addLog('info', `Deleted ${toDelete.length} entity(ies)`);
303
- undoStack.push({
304
- description: `Delete ${toDelete.length} entity(ies)`,
305
- undo: () => {
306
- // Recreating entities from snapshots would be more complex in real impl
307
- events.emit('editor:undo:delete', snapshots);
308
- },
309
- redo: () => {
310
- for (const id of toDelete) {
311
- world.destroyEntity(id);
312
- }
313
- },
314
- });
315
- }, [state.selectedEntities, world, undoStack, refreshWorld, addLog, events]);
316
- shortcutActionsRef.current = { save: handleSave, deleteSelected };
317
- const ctxValue = useMemo(() => ({
318
- state,
319
- dispatch,
320
- world,
321
- undoStack,
322
- sceneManager,
323
- events,
324
- worldVersion,
325
- refreshWorld,
326
- createNewEntity,
327
- consoleLogs,
328
- addLog,
329
- }), [
330
- state,
331
- world,
332
- undoStack,
333
- sceneManager,
334
- events,
335
- worldVersion,
336
- refreshWorld,
337
- createNewEntity,
338
- consoleLogs,
339
- addLog,
340
- ]);
341
- return (jsx(EditorContext.Provider, { value: ctxValue, children: jsxs("div", { className: `nice-game-editor ${className ?? ''}`, style: {
342
- display: 'flex',
343
- flexDirection: 'column',
344
- width: '100%',
345
- height: '100%',
346
- overflow: 'hidden',
347
- backgroundColor: '#1e1e2e',
348
- color: '#cdd6f4',
349
- fontFamily: 'monospace',
350
- fontSize: 13,
351
- position: 'relative',
352
- ...style,
353
- }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, GAME_EDITOR_TOUR) }) }), jsx(EditorToolbar, {}), jsxs("div", { style: { display: 'flex', flex: 1, overflow: 'hidden' }, children: [state.panels.find((p) => p.id === 'hierarchy')?.visible && (jsx(EditorPanelContainer, { title: tr('editor.panels.hierarchy'), width: 250, position: "left", children: jsx(HierarchyPanel, {}) })), jsx("div", { style: { flex: 1, position: 'relative', overflow: 'hidden' }, children: jsx(EditorViewport, {}) }), state.panels.find((p) => p.id === 'inspector')?.visible && (jsx(EditorPanelContainer, { title: tr('editor.panels.inspector'), width: 300, position: "right", children: jsx(InspectorPanel, {}) }))] }), jsxs("div", { style: { display: 'flex', borderTop: '1px solid #45475a' }, children: [state.panels.find((p) => p.id === 'assets')?.visible && (jsx(EditorPanelContainer, { title: tr('editor.panels.assets'), height: 180, position: "bottom", children: jsx(AssetPanel, {}) })), state.panels.find((p) => p.id === 'console')?.visible && (jsx(EditorPanelContainer, { title: tr('editor.panels.console'), height: 180, position: "bottom", children: jsx(ConsolePanel, {}) }))] })] }) }));
321
+ }
322
+ });
323
+ }, [state.selectedEntities, world, undoStack, refreshWorld, addLog, events]);
324
+ shortcutActionsRef.current = { save: handleSave, deleteSelected };
325
+ const ctxValue = useMemo(
326
+ () => ({
327
+ state,
328
+ dispatch,
329
+ world,
330
+ undoStack,
331
+ sceneManager,
332
+ events,
333
+ worldVersion,
334
+ refreshWorld,
335
+ createNewEntity,
336
+ consoleLogs,
337
+ addLog
338
+ }),
339
+ [
340
+ state,
341
+ world,
342
+ undoStack,
343
+ sceneManager,
344
+ events,
345
+ worldVersion,
346
+ refreshWorld,
347
+ createNewEntity,
348
+ consoleLogs,
349
+ addLog
350
+ ]
351
+ );
352
+ return /* @__PURE__ */ jsx(EditorContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsxs(
353
+ "div",
354
+ {
355
+ className: `nice-game-editor ${className ?? ""}`,
356
+ style: {
357
+ display: "flex",
358
+ flexDirection: "column",
359
+ width: "100%",
360
+ height: "100%",
361
+ overflow: "hidden",
362
+ backgroundColor: "#1e1e2e",
363
+ color: "#cdd6f4",
364
+ fontFamily: "monospace",
365
+ fontSize: 13,
366
+ position: "relative",
367
+ ...style
368
+ },
369
+ children: [
370
+ /* @__PURE__ */ jsx("span", { style: { position: "absolute", top: 6, right: 6, zIndex: 30 }, children: /* @__PURE__ */ jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, GAME_EDITOR_TOUR) }) }),
371
+ /* @__PURE__ */ jsx(EditorToolbar, {}),
372
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flex: 1, overflow: "hidden" }, children: [
373
+ state.panels.find((p) => p.id === "hierarchy")?.visible && /* @__PURE__ */ jsx(EditorPanelContainer, { title: tr("editor.panels.hierarchy"), width: 250, position: "left", children: /* @__PURE__ */ jsx(HierarchyPanel, {}) }),
374
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, position: "relative", overflow: "hidden" }, children: /* @__PURE__ */ jsx(EditorViewport, {}) }),
375
+ state.panels.find((p) => p.id === "inspector")?.visible && /* @__PURE__ */ jsx(
376
+ EditorPanelContainer,
377
+ {
378
+ title: tr("editor.panels.inspector"),
379
+ width: 300,
380
+ position: "right",
381
+ children: /* @__PURE__ */ jsx(InspectorPanel, {})
382
+ }
383
+ )
384
+ ] }),
385
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", borderTop: "1px solid #45475a" }, children: [
386
+ state.panels.find((p) => p.id === "assets")?.visible && /* @__PURE__ */ jsx(EditorPanelContainer, { title: tr("editor.panels.assets"), height: 180, position: "bottom", children: /* @__PURE__ */ jsx(AssetPanel, {}) }),
387
+ state.panels.find((p) => p.id === "console")?.visible && /* @__PURE__ */ jsx(
388
+ EditorPanelContainer,
389
+ {
390
+ title: tr("editor.panels.console"),
391
+ height: 180,
392
+ position: "bottom",
393
+ children: /* @__PURE__ */ jsx(ConsolePanel, {})
394
+ }
395
+ )
396
+ ] })
397
+ ]
398
+ }
399
+ ) });
354
400
  };
355
- /* ── Helpers ───────────────────────────────────────────────────── */
356
401
  function getEntityBounds(entityId, world) {
357
- const shape = world.getComponent(entityId, 'Shape');
358
- if (shape) {
359
- if (shape.shape === 'circle') {
360
- return { x: -shape.radius, y: -shape.radius, w: shape.radius * 2, h: shape.radius * 2 };
361
- }
362
- return { x: -shape.width / 2, y: -shape.height / 2, w: shape.width, h: shape.height };
402
+ const shape = world.getComponent(entityId, "Shape");
403
+ if (shape) {
404
+ if (shape.shape === "circle") {
405
+ return { x: -shape.radius, y: -shape.radius, w: shape.radius * 2, h: shape.radius * 2 };
363
406
  }
364
- const sprite = world.getComponent(entityId, 'Sprite');
365
- if (sprite) {
366
- return {
367
- x: -sprite.width * sprite.anchor.x,
368
- y: -sprite.height * sprite.anchor.y,
369
- w: sprite.width,
370
- h: sprite.height,
371
- };
372
- }
373
- const text = world.getComponent(entityId, 'Text');
374
- if (text) {
375
- return { x: -30, y: -text.fontSize / 2, w: 60, h: text.fontSize };
376
- }
377
- return { x: -8, y: -8, w: 16, h: 16 };
407
+ return { x: -shape.width / 2, y: -shape.height / 2, w: shape.width, h: shape.height };
408
+ }
409
+ const sprite = world.getComponent(entityId, "Sprite");
410
+ if (sprite) {
411
+ return {
412
+ x: -sprite.width * sprite.anchor.x,
413
+ y: -sprite.height * sprite.anchor.y,
414
+ w: sprite.width,
415
+ h: sprite.height
416
+ };
417
+ }
418
+ const text = world.getComponent(entityId, "Text");
419
+ if (text) {
420
+ return { x: -30, y: -text.fontSize / 2, w: 60, h: text.fontSize };
421
+ }
422
+ return { x: -8, y: -8, w: 16, h: 16 };
378
423
  }
379
424
  function getEntityWorldBounds(entityId, world) {
380
- const transform = world.getComponent(entityId, 'Transform');
381
- if (!transform) {
382
- return null;
383
- }
384
- const b = getEntityBounds(entityId, world);
385
- return { x: transform.position.x + b.x, y: transform.position.y + b.y, w: b.w, h: b.h };
425
+ const transform = world.getComponent(entityId, "Transform");
426
+ if (!transform) {
427
+ return null;
428
+ }
429
+ const b = getEntityBounds(entityId, world);
430
+ return { x: transform.position.x + b.x, y: transform.position.y + b.y, w: b.w, h: b.h };
386
431
  }
387
432
  function getCursorForTool(tool) {
388
- switch (tool) {
389
- case 'move':
390
- return 'move';
391
- case 'rotate':
392
- return 'crosshair';
393
- case 'scale':
394
- return 'nwse-resize';
395
- case 'pan':
396
- return 'grab';
397
- default:
398
- return 'default';
399
- }
433
+ switch (tool) {
434
+ case "move":
435
+ return "move";
436
+ case "rotate":
437
+ return "crosshair";
438
+ case "scale":
439
+ return "nwse-resize";
440
+ case "pan":
441
+ return "grab";
442
+ default:
443
+ return "default";
444
+ }
400
445
  }
401
446
  function colorToCSS(c) {
402
- return `rgba(${Math.round(c.r * 255)},${Math.round(c.g * 255)},${Math.round(c.b * 255)},${c.a})`;
447
+ return `rgba(${Math.round(c.r * 255)},${Math.round(c.g * 255)},${Math.round(c.b * 255)},${c.a})`;
403
448
  }
404
449
  function cssToColor(hex) {
405
- const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
406
- if (!m) {
407
- return { r: 1, g: 1, b: 1, a: 1 };
408
- }
409
- return {
410
- r: parseInt(m[1], 16) / 255,
411
- g: parseInt(m[2], 16) / 255,
412
- b: parseInt(m[3], 16) / 255,
413
- a: 1,
414
- };
450
+ const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
451
+ if (!m) {
452
+ return { r: 1, g: 1, b: 1, a: 1 };
453
+ }
454
+ return {
455
+ r: parseInt(m[1], 16) / 255,
456
+ g: parseInt(m[2], 16) / 255,
457
+ b: parseInt(m[3], 16) / 255,
458
+ a: 1
459
+ };
415
460
  }
416
461
  function colorToHex(c) {
417
- const r = Math.round(c.r * 255)
418
- .toString(16)
419
- .padStart(2, '0');
420
- const g = Math.round(c.g * 255)
421
- .toString(16)
422
- .padStart(2, '0');
423
- const b = Math.round(c.b * 255)
424
- .toString(16)
425
- .padStart(2, '0');
426
- return `#${r}${g}${b}`;
462
+ const r = Math.round(c.r * 255).toString(16).padStart(2, "0");
463
+ const g = Math.round(c.g * 255).toString(16).padStart(2, "0");
464
+ const b = Math.round(c.b * 255).toString(16).padStart(2, "0");
465
+ return `#${r}${g}${b}`;
427
466
  }
428
- /** `label` is also the name given to a created entity, so it stays English (A3);
429
- * `key` is what the button shows. */
430
467
  const ENTITY_PRESETS = [
431
- { key: 'empty', label: 'Empty', icon: 'circle', components: ['Transform'] },
432
- { key: 'sprite', label: 'Sprite', icon: 'image', components: ['Transform', 'Sprite'] },
433
- { key: 'shape', label: 'Shape', icon: 'square', components: ['Transform', 'Shape'] },
434
- { key: 'text', label: 'Text', icon: 'type', components: ['Transform', 'Text'] },
435
- {
436
- key: 'physicsBody',
437
- label: 'Physics Body',
438
- icon: 'zap',
439
- components: ['Transform', 'Shape', 'Collider2D', 'RigidBody2D'],
440
- },
441
- { key: 'camera', label: 'Camera', icon: 'camera', components: ['Transform', 'CameraTarget'] },
468
+ { key: "empty", label: "Empty", icon: "circle", components: ["Transform"] },
469
+ { key: "sprite", label: "Sprite", icon: "image", components: ["Transform", "Sprite"] },
470
+ { key: "shape", label: "Shape", icon: "square", components: ["Transform", "Shape"] },
471
+ { key: "text", label: "Text", icon: "type", components: ["Transform", "Text"] },
472
+ {
473
+ key: "physicsBody",
474
+ label: "Physics Body",
475
+ icon: "zap",
476
+ components: ["Transform", "Shape", "Collider2D", "RigidBody2D"]
477
+ },
478
+ { key: "camera", label: "Camera", icon: "camera", components: ["Transform", "CameraTarget"] }
442
479
  ];
443
- /* ── Sub-components ───────────────────────────────────────────── */
444
480
  const EditorToolbar = () => {
445
- const { t: tr } = useTranslation('engine');
446
- const { state, dispatch, createNewEntity, undoStack } = useEditor();
447
- const [showAddMenu, setShowAddMenu] = useState(false);
448
- const tools = ['select', 'move', 'rotate', 'scale', 'pan'];
449
- const toolIcons = {
450
- select: 'cursor',
451
- move: 'move',
452
- rotate: 'refresh',
453
- scale: 'expand',
454
- pan: 'grip',
455
- };
456
- const btnStyle = (active) => ({
457
- background: active ? '#585b70' : 'transparent',
458
- border: 'none',
459
- color: '#cdd6f4',
460
- padding: '4px 8px',
461
- borderRadius: 4,
462
- cursor: 'pointer',
463
- fontSize: 14,
464
- });
465
- return (jsxs("div", { style: {
466
- display: 'flex',
467
- alignItems: 'center',
468
- gap: 4,
469
- padding: '4px 8px',
470
- borderBottom: '1px solid #45475a',
471
- backgroundColor: '#181825',
472
- }, children: [tools.map((t) => (jsx("button", { onClick: () => dispatch({ type: 'SET_TOOL', tool: t }), style: btnStyle(state.tool === t), title: `${tr(`editor.tools.${t}`)} (${t[0].toUpperCase()})`, children: jsx(NiceIcon, { name: toolIcons[t], size: 14 }) }, t))), jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsxs("div", { style: { position: 'relative' }, children: [jsx("button", { onClick: () => setShowAddMenu(!showAddMenu), style: {
473
- ...btnStyle(false),
474
- fontSize: 12,
475
- padding: '4px 10px',
476
- background: '#2f9e44',
477
- color: '#fff',
478
- }, children: "+ Entity" }), showAddMenu && (jsx("div", { style: {
479
- position: 'absolute',
480
- top: '100%',
481
- left: 0,
482
- zIndex: 100,
483
- background: '#1e1e2e',
484
- border: '1px solid #45475a',
485
- borderRadius: 4,
486
- padding: 4,
487
- minWidth: 150,
488
- marginTop: 2,
489
- }, children: ENTITY_PRESETS.map((p) => (jsxs("button", { onClick: () => {
490
- createNewEntity(p.label, p.components);
491
- setShowAddMenu(false);
492
- }, style: {
493
- display: 'block',
494
- width: '100%',
495
- textAlign: 'left',
496
- background: 'transparent',
497
- border: 'none',
498
- color: '#cdd6f4',
499
- padding: '4px 8px',
500
- cursor: 'pointer',
501
- borderRadius: 2,
502
- fontSize: 12,
503
- }, children: [jsx(NiceIcon, { name: p.icon, size: 12 }), " ", tr(`editor.entity.${p.key}`)] }, p.label))) }))] }), jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsx("button", { onClick: () => dispatch({ type: 'SET_MODE', mode: state.mode === 'play' ? 'edit' : 'play' }), style: btnStyle(state.mode === 'play'), title: tr('editor.view.playStop'), children: jsx(NiceIcon, { name: state.mode === 'play' ? 'stop' : 'play', size: 14 }) }), jsx("button", { onClick: () => dispatch({ type: 'SET_MODE', mode: state.mode === 'pause' ? 'play' : 'pause' }), style: btnStyle(state.mode === 'pause'), title: tr('editor.actions.pause'), disabled: state.mode === 'edit', children: jsx(NiceIcon, { name: "pause", size: 14 }) }), jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsx("button", { onClick: () => undoStack.undo(), style: btnStyle(false), title: `${tr('editor.actions.undo')} (Ctrl+Z)`, disabled: !undoStack.canUndo, children: jsx(NiceIcon, { name: "undo", size: 14 }) }), jsx("button", { onClick: () => undoStack.redo(), style: btnStyle(false), title: `${tr('editor.actions.redo')} (Ctrl+Y)`, disabled: !undoStack.canRedo, children: jsx(NiceIcon, { name: "redo", size: 14 }) }), jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsx("button", { onClick: () => dispatch({ type: 'SET_ZOOM', zoom: state.zoom * 0.8 }), style: btnStyle(false), title: tr('editor.view.zoomOut'), children: "\u2212" }), jsxs("span", { style: { fontSize: 11, minWidth: 40, textAlign: 'center' }, children: [(state.zoom * 100) | 0, "%"] }), jsx("button", { onClick: () => dispatch({ type: 'SET_ZOOM', zoom: state.zoom * 1.25 }), style: btnStyle(false), title: tr('editor.view.zoomIn'), children: "+" }), jsx("button", { onClick: () => {
504
- dispatch({ type: 'SET_ZOOM', zoom: 1 });
505
- dispatch({ type: 'SET_PAN', offset: { x: 0, y: 0 } });
506
- }, style: btnStyle(false), title: tr('editor.view.resetView'), children: jsx(NiceIcon, { name: "home", size: 14 }) }), jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsx("button", { onClick: () => dispatch({ type: 'TOGGLE_GRID' }), style: btnStyle(state.showGrid), title: `${tr('editor.view.toggleGrid')} (Ctrl+G)`, children: jsx(NiceIcon, { name: "grid", size: 14 }) }), jsx("button", { onClick: () => dispatch({ type: 'TOGGLE_SNAP' }), style: btnStyle(state.snapToGrid), title: tr('editor.view.toggleSnap'), children: jsx(NiceIcon, { name: "target", size: 14 }) }), jsx("div", { style: { flex: 1 } }), jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: [state.mode.toUpperCase(), " | ", state.tool, " | ", state.dirty ? 'Unsaved' : 'Saved'] })] }));
481
+ const { t: tr } = useTranslation("engine");
482
+ const { state, dispatch, createNewEntity, undoStack } = useEditor();
483
+ const [showAddMenu, setShowAddMenu] = useState(false);
484
+ const tools = ["select", "move", "rotate", "scale", "pan"];
485
+ const toolIcons = {
486
+ select: "cursor",
487
+ move: "move",
488
+ rotate: "refresh",
489
+ scale: "expand",
490
+ pan: "grip"
491
+ };
492
+ const btnStyle = (active) => ({
493
+ background: active ? "#585b70" : "transparent",
494
+ border: "none",
495
+ color: "#cdd6f4",
496
+ padding: "4px 8px",
497
+ borderRadius: 4,
498
+ cursor: "pointer",
499
+ fontSize: 14
500
+ });
501
+ return /* @__PURE__ */ jsxs(
502
+ "div",
503
+ {
504
+ style: {
505
+ display: "flex",
506
+ alignItems: "center",
507
+ gap: 4,
508
+ padding: "4px 8px",
509
+ borderBottom: "1px solid #45475a",
510
+ backgroundColor: "#181825"
511
+ },
512
+ children: [
513
+ tools.map((t) => /* @__PURE__ */ jsx(
514
+ "button",
515
+ {
516
+ onClick: () => dispatch({ type: "SET_TOOL", tool: t }),
517
+ style: btnStyle(state.tool === t),
518
+ title: `${tr(`editor.tools.${t}`)} (${t[0].toUpperCase()})`,
519
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: toolIcons[t], size: 14 })
520
+ },
521
+ t
522
+ )),
523
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "#45475a", margin: "0 4px" } }),
524
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
525
+ /* @__PURE__ */ jsx(
526
+ "button",
527
+ {
528
+ onClick: () => setShowAddMenu(!showAddMenu),
529
+ style: {
530
+ ...btnStyle(false),
531
+ fontSize: 12,
532
+ padding: "4px 10px",
533
+ background: "#2f9e44",
534
+ color: "#fff"
535
+ },
536
+ children: "+ Entity"
537
+ }
538
+ ),
539
+ showAddMenu && /* @__PURE__ */ jsx(
540
+ "div",
541
+ {
542
+ style: {
543
+ position: "absolute",
544
+ top: "100%",
545
+ left: 0,
546
+ zIndex: 100,
547
+ background: "#1e1e2e",
548
+ border: "1px solid #45475a",
549
+ borderRadius: 4,
550
+ padding: 4,
551
+ minWidth: 150,
552
+ marginTop: 2
553
+ },
554
+ children: ENTITY_PRESETS.map((p) => /* @__PURE__ */ jsxs(
555
+ "button",
556
+ {
557
+ onClick: () => {
558
+ createNewEntity(p.label, p.components);
559
+ setShowAddMenu(false);
560
+ },
561
+ style: {
562
+ display: "block",
563
+ width: "100%",
564
+ textAlign: "left",
565
+ background: "transparent",
566
+ border: "none",
567
+ color: "#cdd6f4",
568
+ padding: "4px 8px",
569
+ cursor: "pointer",
570
+ borderRadius: 2,
571
+ fontSize: 12
572
+ },
573
+ children: [
574
+ /* @__PURE__ */ jsx(NiceIcon, { name: p.icon, size: 12 }),
575
+ " ",
576
+ tr(`editor.entity.${p.key}`)
577
+ ]
578
+ },
579
+ p.label
580
+ ))
581
+ }
582
+ )
583
+ ] }),
584
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "#45475a", margin: "0 4px" } }),
585
+ /* @__PURE__ */ jsx(
586
+ "button",
587
+ {
588
+ onClick: () => dispatch({ type: "SET_MODE", mode: state.mode === "play" ? "edit" : "play" }),
589
+ style: btnStyle(state.mode === "play"),
590
+ title: tr("editor.view.playStop"),
591
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: state.mode === "play" ? "stop" : "play", size: 14 })
592
+ }
593
+ ),
594
+ /* @__PURE__ */ jsx(
595
+ "button",
596
+ {
597
+ onClick: () => dispatch({ type: "SET_MODE", mode: state.mode === "pause" ? "play" : "pause" }),
598
+ style: btnStyle(state.mode === "pause"),
599
+ title: tr("editor.actions.pause"),
600
+ disabled: state.mode === "edit",
601
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "pause", size: 14 })
602
+ }
603
+ ),
604
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "#45475a", margin: "0 4px" } }),
605
+ /* @__PURE__ */ jsx(
606
+ "button",
607
+ {
608
+ onClick: () => undoStack.undo(),
609
+ style: btnStyle(false),
610
+ title: `${tr("editor.actions.undo")} (Ctrl+Z)`,
611
+ disabled: !undoStack.canUndo,
612
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "undo", size: 14 })
613
+ }
614
+ ),
615
+ /* @__PURE__ */ jsx(
616
+ "button",
617
+ {
618
+ onClick: () => undoStack.redo(),
619
+ style: btnStyle(false),
620
+ title: `${tr("editor.actions.redo")} (Ctrl+Y)`,
621
+ disabled: !undoStack.canRedo,
622
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "redo", size: 14 })
623
+ }
624
+ ),
625
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "#45475a", margin: "0 4px" } }),
626
+ /* @__PURE__ */ jsx(
627
+ "button",
628
+ {
629
+ onClick: () => dispatch({ type: "SET_ZOOM", zoom: state.zoom * 0.8 }),
630
+ style: btnStyle(false),
631
+ title: tr("editor.view.zoomOut"),
632
+ children: "−"
633
+ }
634
+ ),
635
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, minWidth: 40, textAlign: "center" }, children: [
636
+ state.zoom * 100 | 0,
637
+ "%"
638
+ ] }),
639
+ /* @__PURE__ */ jsx(
640
+ "button",
641
+ {
642
+ onClick: () => dispatch({ type: "SET_ZOOM", zoom: state.zoom * 1.25 }),
643
+ style: btnStyle(false),
644
+ title: tr("editor.view.zoomIn"),
645
+ children: "+"
646
+ }
647
+ ),
648
+ /* @__PURE__ */ jsx(
649
+ "button",
650
+ {
651
+ onClick: () => {
652
+ dispatch({ type: "SET_ZOOM", zoom: 1 });
653
+ dispatch({ type: "SET_PAN", offset: { x: 0, y: 0 } });
654
+ },
655
+ style: btnStyle(false),
656
+ title: tr("editor.view.resetView"),
657
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "home", size: 14 })
658
+ }
659
+ ),
660
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "#45475a", margin: "0 4px" } }),
661
+ /* @__PURE__ */ jsx(
662
+ "button",
663
+ {
664
+ onClick: () => dispatch({ type: "TOGGLE_GRID" }),
665
+ style: btnStyle(state.showGrid),
666
+ title: `${tr("editor.view.toggleGrid")} (Ctrl+G)`,
667
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "grid", size: 14 })
668
+ }
669
+ ),
670
+ /* @__PURE__ */ jsx(
671
+ "button",
672
+ {
673
+ onClick: () => dispatch({ type: "TOGGLE_SNAP" }),
674
+ style: btnStyle(state.snapToGrid),
675
+ title: tr("editor.view.toggleSnap"),
676
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "target", size: 14 })
677
+ }
678
+ ),
679
+ /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
680
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: [
681
+ state.mode.toUpperCase(),
682
+ " | ",
683
+ state.tool,
684
+ " | ",
685
+ state.dirty ? "Unsaved" : "Saved"
686
+ ] })
687
+ ]
688
+ }
689
+ );
507
690
  };
508
691
  const EditorPanelContainer = ({ title, width, height, position, children }) => {
509
- const isHorizontal = position === 'bottom';
510
- return (jsxs("div", { style: {
511
- width: isHorizontal ? '100%' : width,
512
- height: isHorizontal ? height : undefined,
513
- flex: isHorizontal ? undefined : undefined,
514
- borderLeft: position === 'right' ? '1px solid #45475a' : undefined,
515
- borderRight: position === 'left' ? '1px solid #45475a' : undefined,
516
- display: 'flex',
517
- flexDirection: 'column',
518
- overflow: 'hidden',
519
- backgroundColor: '#1e1e2e',
520
- }, children: [jsx("div", { style: {
521
- padding: '4px 8px',
522
- fontSize: 11,
523
- fontWeight: 'bold',
524
- backgroundColor: '#181825',
525
- borderBottom: '1px solid #313244',
526
- textTransform: 'uppercase',
527
- letterSpacing: 1,
528
- }, children: title }), jsx("div", { style: { flex: 1, overflow: 'auto', padding: 4 }, children: children })] }));
692
+ const isHorizontal = position === "bottom";
693
+ return /* @__PURE__ */ jsxs(
694
+ "div",
695
+ {
696
+ style: {
697
+ width: isHorizontal ? "100%" : width,
698
+ height: isHorizontal ? height : void 0,
699
+ flex: isHorizontal ? void 0 : void 0,
700
+ borderLeft: position === "right" ? "1px solid #45475a" : void 0,
701
+ borderRight: position === "left" ? "1px solid #45475a" : void 0,
702
+ display: "flex",
703
+ flexDirection: "column",
704
+ overflow: "hidden",
705
+ backgroundColor: "#1e1e2e"
706
+ },
707
+ children: [
708
+ /* @__PURE__ */ jsx(
709
+ "div",
710
+ {
711
+ style: {
712
+ padding: "4px 8px",
713
+ fontSize: 11,
714
+ fontWeight: "bold",
715
+ backgroundColor: "#181825",
716
+ borderBottom: "1px solid #313244",
717
+ textTransform: "uppercase",
718
+ letterSpacing: 1
719
+ },
720
+ children: title
721
+ }
722
+ ),
723
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, overflow: "auto", padding: 4 }, children })
724
+ ]
725
+ }
726
+ );
529
727
  };
530
728
  const HierarchyPanel = () => {
531
- const { t: tr } = useTranslation('engine');
532
- const { world, state, dispatch, createNewEntity, refreshWorld, addLog } = useEditor();
533
- const entities = world.allEntities().filter((e) => e.parent == null);
534
- const getIcon = (id) => {
535
- if (world.hasComponent(id, 'CameraTarget')) {
536
- return 'camera';
537
- }
538
- if (world.hasComponent(id, 'Sprite')) {
539
- return 'image';
540
- }
541
- if (world.hasComponent(id, 'Text')) {
542
- return 'type';
543
- }
544
- if (world.hasComponent(id, 'Shape')) {
545
- return 'square';
546
- }
547
- if (world.hasComponent(id, 'ParticleEmitter')) {
548
- return 'star';
549
- }
550
- if (world.hasComponent(id, 'AudioSource')) {
551
- return 'volume';
729
+ const { t: tr } = useTranslation("engine");
730
+ const { world, state, dispatch, createNewEntity, refreshWorld, addLog } = useEditor();
731
+ const entities = world.allEntities().filter((e) => e.parent == null);
732
+ const getIcon = (id) => {
733
+ if (world.hasComponent(id, "CameraTarget")) {
734
+ return "camera";
735
+ }
736
+ if (world.hasComponent(id, "Sprite")) {
737
+ return "image";
738
+ }
739
+ if (world.hasComponent(id, "Text")) {
740
+ return "type";
741
+ }
742
+ if (world.hasComponent(id, "Shape")) {
743
+ return "square";
744
+ }
745
+ if (world.hasComponent(id, "ParticleEmitter")) {
746
+ return "star";
747
+ }
748
+ if (world.hasComponent(id, "AudioSource")) {
749
+ return "volume";
750
+ }
751
+ return "circle";
752
+ };
753
+ const renderEntity = (e, depth) => {
754
+ const isSelected = state.selectedEntities.includes(e.id);
755
+ const children = e.children.map((cid) => world.getEntity(cid)).filter(Boolean);
756
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
757
+ /* @__PURE__ */ jsxs(
758
+ "div",
759
+ {
760
+ role: "button",
761
+ tabIndex: 0,
762
+ onKeyDown: clickOnActivationKey,
763
+ onClick: () => dispatch({ type: "SELECT", entities: [e.id] }),
764
+ style: {
765
+ padding: "2px 6px",
766
+ paddingLeft: 6 + depth * 16,
767
+ cursor: "pointer",
768
+ backgroundColor: isSelected ? "#45475a" : "transparent",
769
+ borderRadius: 2,
770
+ display: "flex",
771
+ alignItems: "center",
772
+ gap: 4,
773
+ fontSize: 12
774
+ },
775
+ children: [
776
+ /* @__PURE__ */ jsx("span", { style: { width: 14, display: "inline-flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx(NiceIcon, { name: getIcon(e.id), size: 12 }) }),
777
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: e.name }),
778
+ /* @__PURE__ */ jsxs("span", { style: { opacity: 0.3, fontSize: 10 }, children: [
779
+ "#",
780
+ e.id
781
+ ] })
782
+ ]
552
783
  }
553
- return 'circle';
554
- };
555
- const renderEntity = (e, depth) => {
556
- const isSelected = state.selectedEntities.includes(e.id);
557
- const children = e.children.map((cid) => world.getEntity(cid)).filter(Boolean);
558
- return (jsxs(React.Fragment, { children: [jsxs("div", { role: "button", tabIndex: 0, onKeyDown: clickOnActivationKey, onClick: () => dispatch({ type: 'SELECT', entities: [e.id] }), style: {
559
- padding: '2px 6px',
560
- paddingLeft: 6 + depth * 16,
561
- cursor: 'pointer',
562
- backgroundColor: isSelected ? '#45475a' : 'transparent',
563
- borderRadius: 2,
564
- display: 'flex',
565
- alignItems: 'center',
566
- gap: 4,
567
- fontSize: 12,
568
- }, children: [jsx("span", { style: { width: 14, display: 'inline-flex', justifyContent: 'center' }, children: jsx(NiceIcon, { name: getIcon(e.id), size: 12 }) }), jsx("span", { style: { flex: 1 }, children: e.name }), jsxs("span", { style: { opacity: 0.3, fontSize: 10 }, children: ["#", e.id] })] }), children.map((c) => renderEntity(c, depth + 1))] }, e.id));
569
- };
570
- return (jsxs("div", { children: [jsxs("div", { style: {
571
- padding: '2px 6px',
572
- fontSize: 10,
573
- display: 'flex',
574
- gap: 4,
575
- borderBottom: '1px solid #313244',
576
- marginBottom: 4,
577
- }, children: [jsx("button", { onClick: () => createNewEntity('Entity'), style: {
578
- background: 'transparent',
579
- border: 'none',
580
- color: '#a6adc8',
581
- cursor: 'pointer',
582
- fontSize: 10,
583
- }, title: tr('editor.assets.addEntity'), children: "+" }), jsx("button", { onClick: () => {
584
- if (state.selectedEntities.length > 0) {
585
- const id = state.selectedEntities[0];
586
- const child = world.createEntity('Child');
587
- world.addComponent(child.id, 'Transform');
588
- world.setParent(child.id, id);
589
- dispatch({ type: 'SELECT', entities: [child.id] });
590
- dispatch({ type: 'MARK_DIRTY' });
591
- refreshWorld();
592
- addLog('info', `Created child entity (#${child.id})`);
593
- }
594
- }, style: {
595
- background: 'transparent',
596
- border: 'none',
597
- color: '#a6adc8',
598
- cursor: 'pointer',
599
- fontSize: 10,
600
- }, title: tr('editor.assets.addChild'), disabled: state.selectedEntities.length === 0, children: jsx(NiceIcon, { name: "plus", size: 12 }) }), jsx("span", { style: { flex: 1 } }), jsxs("span", { style: { color: '#6c7086' }, children: [world.entityCount, " entities"] })] }), entities.map((e) => renderEntity(e, 0))] }));
784
+ ),
785
+ children.map((c) => renderEntity(c, depth + 1))
786
+ ] }, e.id);
787
+ };
788
+ return /* @__PURE__ */ jsxs("div", { children: [
789
+ /* @__PURE__ */ jsxs(
790
+ "div",
791
+ {
792
+ style: {
793
+ padding: "2px 6px",
794
+ fontSize: 10,
795
+ display: "flex",
796
+ gap: 4,
797
+ borderBottom: "1px solid #313244",
798
+ marginBottom: 4
799
+ },
800
+ children: [
801
+ /* @__PURE__ */ jsx(
802
+ "button",
803
+ {
804
+ onClick: () => createNewEntity("Entity"),
805
+ style: {
806
+ background: "transparent",
807
+ border: "none",
808
+ color: "#a6adc8",
809
+ cursor: "pointer",
810
+ fontSize: 10
811
+ },
812
+ title: tr("editor.assets.addEntity"),
813
+ children: "+"
814
+ }
815
+ ),
816
+ /* @__PURE__ */ jsx(
817
+ "button",
818
+ {
819
+ onClick: () => {
820
+ if (state.selectedEntities.length > 0) {
821
+ const id = state.selectedEntities[0];
822
+ const child = world.createEntity("Child");
823
+ world.addComponent(child.id, "Transform");
824
+ world.setParent(child.id, id);
825
+ dispatch({ type: "SELECT", entities: [child.id] });
826
+ dispatch({ type: "MARK_DIRTY" });
827
+ refreshWorld();
828
+ addLog("info", `Created child entity (#${child.id})`);
829
+ }
830
+ },
831
+ style: {
832
+ background: "transparent",
833
+ border: "none",
834
+ color: "#a6adc8",
835
+ cursor: "pointer",
836
+ fontSize: 10
837
+ },
838
+ title: tr("editor.assets.addChild"),
839
+ disabled: state.selectedEntities.length === 0,
840
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "plus", size: 12 })
841
+ }
842
+ ),
843
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 } }),
844
+ /* @__PURE__ */ jsxs("span", { style: { color: "#6c7086" }, children: [
845
+ world.entityCount,
846
+ " entities"
847
+ ] })
848
+ ]
849
+ }
850
+ ),
851
+ entities.map((e) => renderEntity(e, 0))
852
+ ] });
601
853
  };
602
- /* ── Inspector sub-editors ─────────────────────────────────────── */
603
854
  const fieldStyle = {
604
- width: '100%',
605
- background: '#313244',
606
- color: '#cdd6f4',
607
- border: '1px solid #45475a',
608
- borderRadius: 3,
609
- padding: '2px 4px',
610
- fontSize: 11,
855
+ width: "100%",
856
+ background: "#313244",
857
+ color: "#cdd6f4",
858
+ border: "1px solid #45475a",
859
+ borderRadius: 3,
860
+ padding: "2px 4px",
861
+ fontSize: 11
611
862
  };
612
- const NumField = ({ label, value, onChange, step = 1 }) => (jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: label }), jsx("input", { type: "number", value: Math.round(value * 1000) / 1000, step: step, onChange: (e) => onChange(Number(e.target.value)), style: { ...fieldStyle, flex: 1 } })] }));
613
- const ColField = ({ label, color, onChange }) => (jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: label }), jsx("input", { type: "color", value: colorToHex(color), onChange: (e) => onChange(cssToColor(e.target.value)), style: { width: 28, height: 20, border: 'none', padding: 0, cursor: 'pointer' } }), jsx("span", { style: { fontSize: 9, color: '#6c7086' }, children: colorToHex(color) })] }));
614
- const TransformEditor = ({ entityId }) => {
615
- const { world, refreshWorld, dispatch } = useEditor();
616
- const t = world.getComponent(entityId, 'Transform');
617
- if (!t) {
618
- return null;
863
+ const NumField = ({ label, value, onChange, step = 1 }) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, marginBottom: 2 }, children: [
864
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, minWidth: 50, color: "#a6adc8" }, children: label }),
865
+ /* @__PURE__ */ jsx(
866
+ "input",
867
+ {
868
+ type: "number",
869
+ value: Math.round(value * 1e3) / 1e3,
870
+ step,
871
+ onChange: (e) => onChange(Number(e.target.value)),
872
+ style: { ...fieldStyle, flex: 1 }
619
873
  }
620
- const update = (fn) => {
621
- fn();
622
- dispatch({ type: 'MARK_DIRTY' });
623
- refreshWorld();
624
- };
625
- return (jsxs(Fragment, { children: [jsx(NumField, { label: "Pos X", value: t.position.x, onChange: (v) => update(() => {
626
- t.position.x = v;
627
- }) }), jsx(NumField, { label: "Pos Y", value: t.position.y, onChange: (v) => update(() => {
628
- t.position.y = v;
629
- }) }), jsx(NumField, { label: "Rotation", value: t.rotation * (180 / Math.PI), step: 5, onChange: (v) => update(() => {
630
- t.rotation = v * (Math.PI / 180);
631
- }) }), jsx(NumField, { label: "Scale X", value: t.scale.x, step: 0.1, onChange: (v) => update(() => {
632
- t.scale.x = v;
633
- }) }), jsx(NumField, { label: "Scale Y", value: t.scale.y, step: 0.1, onChange: (v) => update(() => {
634
- t.scale.y = v;
635
- }) })] }));
874
+ )
875
+ ] });
876
+ const ColField = ({ label, color, onChange }) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, marginBottom: 2 }, children: [
877
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, minWidth: 50, color: "#a6adc8" }, children: label }),
878
+ /* @__PURE__ */ jsx(
879
+ "input",
880
+ {
881
+ type: "color",
882
+ value: colorToHex(color),
883
+ onChange: (e) => onChange(cssToColor(e.target.value)),
884
+ style: { width: 28, height: 20, border: "none", padding: 0, cursor: "pointer" }
885
+ }
886
+ ),
887
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: "#6c7086" }, children: colorToHex(color) })
888
+ ] });
889
+ const TransformEditor = ({ entityId }) => {
890
+ const { world, refreshWorld, dispatch } = useEditor();
891
+ const t = world.getComponent(entityId, "Transform");
892
+ if (!t) {
893
+ return null;
894
+ }
895
+ const update = (fn) => {
896
+ fn();
897
+ dispatch({ type: "MARK_DIRTY" });
898
+ refreshWorld();
899
+ };
900
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
901
+ /* @__PURE__ */ jsx(
902
+ NumField,
903
+ {
904
+ label: "Pos X",
905
+ value: t.position.x,
906
+ onChange: (v) => update(() => {
907
+ t.position.x = v;
908
+ })
909
+ }
910
+ ),
911
+ /* @__PURE__ */ jsx(
912
+ NumField,
913
+ {
914
+ label: "Pos Y",
915
+ value: t.position.y,
916
+ onChange: (v) => update(() => {
917
+ t.position.y = v;
918
+ })
919
+ }
920
+ ),
921
+ /* @__PURE__ */ jsx(
922
+ NumField,
923
+ {
924
+ label: "Rotation",
925
+ value: t.rotation * (180 / Math.PI),
926
+ step: 5,
927
+ onChange: (v) => update(() => {
928
+ t.rotation = v * (Math.PI / 180);
929
+ })
930
+ }
931
+ ),
932
+ /* @__PURE__ */ jsx(
933
+ NumField,
934
+ {
935
+ label: "Scale X",
936
+ value: t.scale.x,
937
+ step: 0.1,
938
+ onChange: (v) => update(() => {
939
+ t.scale.x = v;
940
+ })
941
+ }
942
+ ),
943
+ /* @__PURE__ */ jsx(
944
+ NumField,
945
+ {
946
+ label: "Scale Y",
947
+ value: t.scale.y,
948
+ step: 0.1,
949
+ onChange: (v) => update(() => {
950
+ t.scale.y = v;
951
+ })
952
+ }
953
+ )
954
+ ] });
636
955
  };
637
956
  const ShapeEditor = ({ entityId }) => {
638
- const { t: tr } = useTranslation('engine');
639
- const { world, refreshWorld, dispatch } = useEditor();
640
- const s = world.getComponent(entityId, 'Shape');
641
- if (!s) {
642
- return null;
643
- }
644
- const update = (fn) => {
645
- fn();
646
- dispatch({ type: 'MARK_DIRTY' });
647
- refreshWorld();
648
- };
649
- return (jsxs(Fragment, { children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: tr('editor.fields.shape') }), jsxs("select", { value: s.shape, onChange: (e) => update(() => {
650
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mutates discriminated-union variant; consumer narrows by `s.shape`
651
- s.shape = e.target.value;
652
- }), style: { ...fieldStyle, flex: 1 }, children: [jsx("option", { value: "rect", children: tr('editor.shape.rect') }), jsx("option", { value: "circle", children: tr('editor.shape.circle') }), jsx("option", { value: "polygon", children: tr('editor.shape.polygon') }), jsx("option", { value: "line", children: tr('editor.shape.line') })] })] }), s.shape !== 'circle' && (jsx(NumField, { label: "Width", value: s.width, onChange: (v) => update(() => {
653
- s.width = v;
654
- }) })), s.shape !== 'circle' && (jsx(NumField, { label: "Height", value: s.height, onChange: (v) => update(() => {
655
- s.height = v;
656
- }) })), s.shape === 'circle' && (jsx(NumField, { label: "Radius", value: s.radius, onChange: (v) => update(() => {
657
- s.radius = v;
658
- }) })), jsx(ColField, { label: "Fill", color: s.fillColor, onChange: (c) => update(() => {
659
- Object.assign(s.fillColor, c);
660
- }) }), jsx(ColField, { label: "Stroke", color: s.strokeColor, onChange: (c) => update(() => {
661
- Object.assign(s.strokeColor, c);
662
- }) }), jsx(NumField, { label: "Stroke W", value: s.strokeWidth, onChange: (v) => update(() => {
663
- s.strokeWidth = v;
664
- }) })] }));
957
+ const { t: tr } = useTranslation("engine");
958
+ const { world, refreshWorld, dispatch } = useEditor();
959
+ const s = world.getComponent(entityId, "Shape");
960
+ if (!s) {
961
+ return null;
962
+ }
963
+ const update = (fn) => {
964
+ fn();
965
+ dispatch({ type: "MARK_DIRTY" });
966
+ refreshWorld();
967
+ };
968
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
969
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, marginBottom: 2 }, children: [
970
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, minWidth: 50, color: "#a6adc8" }, children: tr("editor.fields.shape") }),
971
+ /* @__PURE__ */ jsxs(
972
+ "select",
973
+ {
974
+ value: s.shape,
975
+ onChange: (e) => update(() => {
976
+ s.shape = e.target.value;
977
+ }),
978
+ style: { ...fieldStyle, flex: 1 },
979
+ children: [
980
+ /* @__PURE__ */ jsx("option", { value: "rect", children: tr("editor.shape.rect") }),
981
+ /* @__PURE__ */ jsx("option", { value: "circle", children: tr("editor.shape.circle") }),
982
+ /* @__PURE__ */ jsx("option", { value: "polygon", children: tr("editor.shape.polygon") }),
983
+ /* @__PURE__ */ jsx("option", { value: "line", children: tr("editor.shape.line") })
984
+ ]
985
+ }
986
+ )
987
+ ] }),
988
+ s.shape !== "circle" && /* @__PURE__ */ jsx(
989
+ NumField,
990
+ {
991
+ label: "Width",
992
+ value: s.width,
993
+ onChange: (v) => update(() => {
994
+ s.width = v;
995
+ })
996
+ }
997
+ ),
998
+ s.shape !== "circle" && /* @__PURE__ */ jsx(
999
+ NumField,
1000
+ {
1001
+ label: "Height",
1002
+ value: s.height,
1003
+ onChange: (v) => update(() => {
1004
+ s.height = v;
1005
+ })
1006
+ }
1007
+ ),
1008
+ s.shape === "circle" && /* @__PURE__ */ jsx(
1009
+ NumField,
1010
+ {
1011
+ label: "Radius",
1012
+ value: s.radius,
1013
+ onChange: (v) => update(() => {
1014
+ s.radius = v;
1015
+ })
1016
+ }
1017
+ ),
1018
+ /* @__PURE__ */ jsx(
1019
+ ColField,
1020
+ {
1021
+ label: "Fill",
1022
+ color: s.fillColor,
1023
+ onChange: (c) => update(() => {
1024
+ Object.assign(s.fillColor, c);
1025
+ })
1026
+ }
1027
+ ),
1028
+ /* @__PURE__ */ jsx(
1029
+ ColField,
1030
+ {
1031
+ label: "Stroke",
1032
+ color: s.strokeColor,
1033
+ onChange: (c) => update(() => {
1034
+ Object.assign(s.strokeColor, c);
1035
+ })
1036
+ }
1037
+ ),
1038
+ /* @__PURE__ */ jsx(
1039
+ NumField,
1040
+ {
1041
+ label: "Stroke W",
1042
+ value: s.strokeWidth,
1043
+ onChange: (v) => update(() => {
1044
+ s.strokeWidth = v;
1045
+ })
1046
+ }
1047
+ )
1048
+ ] });
665
1049
  };
666
1050
  const SpriteEditor = ({ entityId }) => {
667
- const { t: tr } = useTranslation('engine');
668
- const { world, refreshWorld, dispatch } = useEditor();
669
- const s = world.getComponent(entityId, 'Sprite');
670
- if (!s) {
671
- return null;
672
- }
673
- const update = (fn) => {
674
- fn();
675
- dispatch({ type: 'MARK_DIRTY' });
676
- refreshWorld();
677
- };
678
- return (jsxs(Fragment, { children: [jsx(NumField, { label: "Width", value: s.width, onChange: (v) => update(() => {
679
- s.width = v;
680
- }) }), jsx(NumField, { label: "Height", value: s.height, onChange: (v) => update(() => {
681
- s.height = v;
682
- }) }), jsx(ColField, { label: "Tint", color: s.tint, onChange: (c) => update(() => {
683
- Object.assign(s.tint, c);
684
- }) }), jsx(NumField, { label: "Opacity", value: s.opacity, step: 0.1, onChange: (v) => update(() => {
685
- s.opacity = Math.max(0, Math.min(1, v));
686
- }) }), jsx(NumField, { label: "Layer", value: s.layer, step: 1, onChange: (v) => update(() => {
687
- s.layer = v;
688
- }) }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: tr('editor.fields.visible') }), jsx("input", { type: "checkbox", checked: s.visible, onChange: (e) => update(() => {
689
- s.visible = e.target.checked;
690
- }) })] })] }));
1051
+ const { t: tr } = useTranslation("engine");
1052
+ const { world, refreshWorld, dispatch } = useEditor();
1053
+ const s = world.getComponent(entityId, "Sprite");
1054
+ if (!s) {
1055
+ return null;
1056
+ }
1057
+ const update = (fn) => {
1058
+ fn();
1059
+ dispatch({ type: "MARK_DIRTY" });
1060
+ refreshWorld();
1061
+ };
1062
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1063
+ /* @__PURE__ */ jsx(
1064
+ NumField,
1065
+ {
1066
+ label: "Width",
1067
+ value: s.width,
1068
+ onChange: (v) => update(() => {
1069
+ s.width = v;
1070
+ })
1071
+ }
1072
+ ),
1073
+ /* @__PURE__ */ jsx(
1074
+ NumField,
1075
+ {
1076
+ label: "Height",
1077
+ value: s.height,
1078
+ onChange: (v) => update(() => {
1079
+ s.height = v;
1080
+ })
1081
+ }
1082
+ ),
1083
+ /* @__PURE__ */ jsx(
1084
+ ColField,
1085
+ {
1086
+ label: "Tint",
1087
+ color: s.tint,
1088
+ onChange: (c) => update(() => {
1089
+ Object.assign(s.tint, c);
1090
+ })
1091
+ }
1092
+ ),
1093
+ /* @__PURE__ */ jsx(
1094
+ NumField,
1095
+ {
1096
+ label: "Opacity",
1097
+ value: s.opacity,
1098
+ step: 0.1,
1099
+ onChange: (v) => update(() => {
1100
+ s.opacity = Math.max(0, Math.min(1, v));
1101
+ })
1102
+ }
1103
+ ),
1104
+ /* @__PURE__ */ jsx(
1105
+ NumField,
1106
+ {
1107
+ label: "Layer",
1108
+ value: s.layer,
1109
+ step: 1,
1110
+ onChange: (v) => update(() => {
1111
+ s.layer = v;
1112
+ })
1113
+ }
1114
+ ),
1115
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, marginBottom: 2 }, children: [
1116
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, minWidth: 50, color: "#a6adc8" }, children: tr("editor.fields.visible") }),
1117
+ /* @__PURE__ */ jsx(
1118
+ "input",
1119
+ {
1120
+ type: "checkbox",
1121
+ checked: s.visible,
1122
+ onChange: (e) => update(() => {
1123
+ s.visible = e.target.checked;
1124
+ })
1125
+ }
1126
+ )
1127
+ ] })
1128
+ ] });
691
1129
  };
692
1130
  const TextEditor = ({ entityId }) => {
693
- const { t: tr } = useTranslation('engine');
694
- const { world, refreshWorld, dispatch } = useEditor();
695
- const t = world.getComponent(entityId, 'Text');
696
- if (!t) {
697
- return null;
698
- }
699
- const update = (fn) => {
700
- fn();
701
- dispatch({ type: 'MARK_DIRTY' });
702
- refreshWorld();
703
- };
704
- return (jsxs(Fragment, { children: [jsxs("div", { style: { marginBottom: 2 }, children: [jsx("span", { style: { fontSize: 10, color: '#a6adc8' }, children: tr('editor.fields.text') }), jsx("input", { type: "text", value: t.text, onChange: (e) => update(() => {
705
- t.text = e.target.value;
706
- }), style: { ...fieldStyle, width: '100%' } })] }), jsx(NumField, { label: "Font Size", value: t.fontSize, onChange: (v) => update(() => {
707
- t.fontSize = v;
708
- }) }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: tr('editor.fields.align') }), jsxs("select", { value: t.align, onChange: (e) => update(() => {
709
- t.align = e.target.value;
710
- }), style: { ...fieldStyle, flex: 1 }, children: [jsx("option", { value: "left", children: tr('editor.align.left') }), jsx("option", { value: "center", children: tr('editor.align.center') }), jsx("option", { value: "right", children: tr('editor.align.right') })] })] }), jsx(ColField, { label: "Color", color: t.color, onChange: (c) => update(() => {
711
- Object.assign(t.color, c);
712
- }) })] }));
1131
+ const { t: tr } = useTranslation("engine");
1132
+ const { world, refreshWorld, dispatch } = useEditor();
1133
+ const t = world.getComponent(entityId, "Text");
1134
+ if (!t) {
1135
+ return null;
1136
+ }
1137
+ const update = (fn) => {
1138
+ fn();
1139
+ dispatch({ type: "MARK_DIRTY" });
1140
+ refreshWorld();
1141
+ };
1142
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1143
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 2 }, children: [
1144
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "#a6adc8" }, children: tr("editor.fields.text") }),
1145
+ /* @__PURE__ */ jsx(
1146
+ "input",
1147
+ {
1148
+ type: "text",
1149
+ value: t.text,
1150
+ onChange: (e) => update(() => {
1151
+ t.text = e.target.value;
1152
+ }),
1153
+ style: { ...fieldStyle, width: "100%" }
1154
+ }
1155
+ )
1156
+ ] }),
1157
+ /* @__PURE__ */ jsx(
1158
+ NumField,
1159
+ {
1160
+ label: "Font Size",
1161
+ value: t.fontSize,
1162
+ onChange: (v) => update(() => {
1163
+ t.fontSize = v;
1164
+ })
1165
+ }
1166
+ ),
1167
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, marginBottom: 2 }, children: [
1168
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, minWidth: 50, color: "#a6adc8" }, children: tr("editor.fields.align") }),
1169
+ /* @__PURE__ */ jsxs(
1170
+ "select",
1171
+ {
1172
+ value: t.align,
1173
+ onChange: (e) => update(() => {
1174
+ t.align = e.target.value;
1175
+ }),
1176
+ style: { ...fieldStyle, flex: 1 },
1177
+ children: [
1178
+ /* @__PURE__ */ jsx("option", { value: "left", children: tr("editor.align.left") }),
1179
+ /* @__PURE__ */ jsx("option", { value: "center", children: tr("editor.align.center") }),
1180
+ /* @__PURE__ */ jsx("option", { value: "right", children: tr("editor.align.right") })
1181
+ ]
1182
+ }
1183
+ )
1184
+ ] }),
1185
+ /* @__PURE__ */ jsx(
1186
+ ColField,
1187
+ {
1188
+ label: "Color",
1189
+ color: t.color,
1190
+ onChange: (c) => update(() => {
1191
+ Object.assign(t.color, c);
1192
+ })
1193
+ }
1194
+ )
1195
+ ] });
713
1196
  };
714
1197
  const RigidBodyEditor = ({ entityId }) => {
715
- const { t: tr } = useTranslation('engine');
716
- const { world, refreshWorld, dispatch } = useEditor();
717
- const rb = world.getComponent(entityId, 'RigidBody2D');
718
- if (!rb) {
719
- return null;
720
- }
721
- const update = (fn) => {
722
- fn();
723
- dispatch({ type: 'MARK_DIRTY' });
724
- refreshWorld();
725
- };
726
- return (jsxs(Fragment, { children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: tr('editor.fields.body') }), jsxs("select", { value: rb.bodyType, onChange: (e) => update(() => {
727
- rb.bodyType = e.target.value;
728
- }), style: { ...fieldStyle, flex: 1 }, children: [jsx("option", { value: "dynamic", children: tr('editor.body.dynamic') }), jsx("option", { value: "static", children: tr('editor.body.static') }), jsx("option", { value: "kinematic", children: tr('editor.body.kinematic') })] })] }), jsx(NumField, { label: "Mass", value: rb.mass, step: 0.1, onChange: (v) => update(() => {
729
- rb.mass = v;
730
- }) }), jsx(NumField, { label: "Drag", value: rb.drag, step: 0.01, onChange: (v) => update(() => {
731
- rb.drag = v;
732
- }) }), jsx(NumField, { label: "Bounce", value: rb.restitution, step: 0.05, onChange: (v) => update(() => {
733
- rb.restitution = v;
734
- }) }), jsx(NumField, { label: "Friction", value: rb.friction, step: 0.05, onChange: (v) => update(() => {
735
- rb.friction = v;
736
- }) }), jsx(NumField, { label: "Gravity", value: rb.gravityScale, step: 0.1, onChange: (v) => update(() => {
737
- rb.gravityScale = v;
738
- }) })] }));
1198
+ const { t: tr } = useTranslation("engine");
1199
+ const { world, refreshWorld, dispatch } = useEditor();
1200
+ const rb = world.getComponent(entityId, "RigidBody2D");
1201
+ if (!rb) {
1202
+ return null;
1203
+ }
1204
+ const update = (fn) => {
1205
+ fn();
1206
+ dispatch({ type: "MARK_DIRTY" });
1207
+ refreshWorld();
1208
+ };
1209
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1210
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, marginBottom: 2 }, children: [
1211
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, minWidth: 50, color: "#a6adc8" }, children: tr("editor.fields.body") }),
1212
+ /* @__PURE__ */ jsxs(
1213
+ "select",
1214
+ {
1215
+ value: rb.bodyType,
1216
+ onChange: (e) => update(() => {
1217
+ rb.bodyType = e.target.value;
1218
+ }),
1219
+ style: { ...fieldStyle, flex: 1 },
1220
+ children: [
1221
+ /* @__PURE__ */ jsx("option", { value: "dynamic", children: tr("editor.body.dynamic") }),
1222
+ /* @__PURE__ */ jsx("option", { value: "static", children: tr("editor.body.static") }),
1223
+ /* @__PURE__ */ jsx("option", { value: "kinematic", children: tr("editor.body.kinematic") })
1224
+ ]
1225
+ }
1226
+ )
1227
+ ] }),
1228
+ /* @__PURE__ */ jsx(
1229
+ NumField,
1230
+ {
1231
+ label: "Mass",
1232
+ value: rb.mass,
1233
+ step: 0.1,
1234
+ onChange: (v) => update(() => {
1235
+ rb.mass = v;
1236
+ })
1237
+ }
1238
+ ),
1239
+ /* @__PURE__ */ jsx(
1240
+ NumField,
1241
+ {
1242
+ label: "Drag",
1243
+ value: rb.drag,
1244
+ step: 0.01,
1245
+ onChange: (v) => update(() => {
1246
+ rb.drag = v;
1247
+ })
1248
+ }
1249
+ ),
1250
+ /* @__PURE__ */ jsx(
1251
+ NumField,
1252
+ {
1253
+ label: "Bounce",
1254
+ value: rb.restitution,
1255
+ step: 0.05,
1256
+ onChange: (v) => update(() => {
1257
+ rb.restitution = v;
1258
+ })
1259
+ }
1260
+ ),
1261
+ /* @__PURE__ */ jsx(
1262
+ NumField,
1263
+ {
1264
+ label: "Friction",
1265
+ value: rb.friction,
1266
+ step: 0.05,
1267
+ onChange: (v) => update(() => {
1268
+ rb.friction = v;
1269
+ })
1270
+ }
1271
+ ),
1272
+ /* @__PURE__ */ jsx(
1273
+ NumField,
1274
+ {
1275
+ label: "Gravity",
1276
+ value: rb.gravityScale,
1277
+ step: 0.1,
1278
+ onChange: (v) => update(() => {
1279
+ rb.gravityScale = v;
1280
+ })
1281
+ }
1282
+ )
1283
+ ] });
739
1284
  };
740
1285
  const ADDABLE_COMPONENTS = [
741
- 'Transform',
742
- 'Sprite',
743
- 'Shape',
744
- 'Collider2D',
745
- 'RigidBody2D',
746
- 'Text',
747
- 'Animator',
748
- 'Script',
749
- 'AudioSource',
750
- 'ParticleEmitter',
751
- 'CameraTarget',
1286
+ "Transform",
1287
+ "Sprite",
1288
+ "Shape",
1289
+ "Collider2D",
1290
+ "RigidBody2D",
1291
+ "Text",
1292
+ "Animator",
1293
+ "Script",
1294
+ "AudioSource",
1295
+ "ParticleEmitter",
1296
+ "CameraTarget"
752
1297
  ];
753
1298
  const InspectorPanel = () => {
754
- const { t: tr } = useTranslation('engine');
755
- const { state, world, dispatch, refreshWorld, addLog } = useEditor();
756
- const [showAddComp, setShowAddComp] = useState(false);
757
- if (state.selectedEntities.length === 0) {
758
- return jsx("div", { style: { opacity: 0.5, padding: 8 }, children: tr('editor.messages.noSelection') });
759
- }
760
- const entityId = state.selectedEntities[0];
761
- const entity = world.getEntity(entityId);
762
- if (!entity) {
763
- return null;
764
- }
765
- const compTypes = ADDABLE_COMPONENTS;
766
- const presentComponents = compTypes.filter((type) => world.hasComponent(entityId, type));
767
- const componentEditors = {
768
- Transform: TransformEditor,
769
- Shape: ShapeEditor,
770
- Sprite: SpriteEditor,
771
- Text: TextEditor,
772
- RigidBody2D: RigidBodyEditor,
773
- };
774
- return (jsxs("div", { children: [jsxs("div", { style: { padding: '4px 0', marginBottom: 4, borderBottom: '1px solid #313244' }, children: [jsx("input", { type: "text", value: entity.name, onChange: (e) => {
775
- entity.name = e.target.value;
776
- dispatch({ type: 'MARK_DIRTY' });
777
- refreshWorld();
778
- }, style: { ...fieldStyle, fontWeight: 'bold', fontSize: 13 } }), jsxs("span", { style: { fontSize: 9, color: '#6c7086' }, children: ["ID: ", entityId] })] }), presentComponents.map((type) => {
779
- const Editor = componentEditors[type];
780
- return (jsxs("div", { style: { marginBottom: 8, borderTop: '1px solid #313244', paddingTop: 4 }, children: [jsxs("div", { style: {
781
- display: 'flex',
782
- alignItems: 'center',
783
- justifyContent: 'space-between',
784
- marginBottom: 4,
785
- }, children: [jsx("span", { style: {
786
- fontSize: 11,
787
- fontWeight: 'bold',
788
- textTransform: 'uppercase',
789
- color: '#89b4fa',
790
- }, children: type }), type !== 'Transform' && (jsx("button", { onClick: () => {
791
- world.removeComponent(entityId, type);
792
- dispatch({ type: 'MARK_DIRTY' });
793
- refreshWorld();
794
- addLog('info', `Removed ${type} from #${entityId}`);
795
- }, style: {
796
- background: 'transparent',
797
- border: 'none',
798
- color: '#f38ba8',
799
- cursor: 'pointer',
800
- fontSize: 10,
801
- }, title: tr('editor.assets.removeComponent', { type }), children: jsx(NiceIcon, { name: "close", size: 10 }) }))] }), Editor ? (jsx(Editor, { entityId: entityId })) : (jsx("pre", { style: { fontSize: 10, opacity: 0.6, whiteSpace: 'pre-wrap', margin: 0 }, children: JSON.stringify(world.getComponent(entityId, type), null, 2) }))] }, type));
802
- }), jsxs("div", { style: { marginTop: 8, position: 'relative' }, children: [jsx("button", { onClick: () => setShowAddComp(!showAddComp), style: {
803
- width: '100%',
804
- padding: '4px 8px',
805
- background: '#313244',
806
- border: '1px solid #45475a',
807
- color: '#cdd6f4',
808
- borderRadius: 4,
809
- cursor: 'pointer',
810
- fontSize: 11,
811
- }, children: "+ Add Component" }), showAddComp && (jsx("div", { style: {
812
- position: 'absolute',
813
- bottom: '100%',
814
- left: 0,
815
- right: 0,
816
- background: '#1e1e2e',
817
- border: '1px solid #45475a',
818
- borderRadius: 4,
819
- padding: 4,
820
- marginBottom: 2,
821
- maxHeight: 200,
822
- overflow: 'auto',
823
- zIndex: 10,
824
- }, children: compTypes
825
- .filter((t) => !world.hasComponent(entityId, t))
826
- .map((type) => (jsx("button", { onClick: () => {
827
- world.addComponent(entityId, type);
828
- dispatch({ type: 'MARK_DIRTY' });
829
- refreshWorld();
830
- addLog('info', `Added ${type} to #${entityId}`);
831
- setShowAddComp(false);
832
- }, style: {
833
- display: 'block',
834
- width: '100%',
835
- textAlign: 'left',
836
- background: 'transparent',
837
- border: 'none',
838
- color: '#cdd6f4',
839
- padding: '3px 6px',
840
- cursor: 'pointer',
841
- borderRadius: 2,
842
- fontSize: 11,
843
- }, children: type }, type))) }))] })] }));
1299
+ const { t: tr } = useTranslation("engine");
1300
+ const { state, world, dispatch, refreshWorld, addLog } = useEditor();
1301
+ const [showAddComp, setShowAddComp] = useState(false);
1302
+ if (state.selectedEntities.length === 0) {
1303
+ return /* @__PURE__ */ jsx("div", { style: { opacity: 0.5, padding: 8 }, children: tr("editor.messages.noSelection") });
1304
+ }
1305
+ const entityId = state.selectedEntities[0];
1306
+ const entity = world.getEntity(entityId);
1307
+ if (!entity) {
1308
+ return null;
1309
+ }
1310
+ const compTypes = ADDABLE_COMPONENTS;
1311
+ const presentComponents = compTypes.filter((type) => world.hasComponent(entityId, type));
1312
+ const componentEditors = {
1313
+ Transform: TransformEditor,
1314
+ Shape: ShapeEditor,
1315
+ Sprite: SpriteEditor,
1316
+ Text: TextEditor,
1317
+ RigidBody2D: RigidBodyEditor
1318
+ };
1319
+ return /* @__PURE__ */ jsxs("div", { children: [
1320
+ /* @__PURE__ */ jsxs("div", { style: { padding: "4px 0", marginBottom: 4, borderBottom: "1px solid #313244" }, children: [
1321
+ /* @__PURE__ */ jsx(
1322
+ "input",
1323
+ {
1324
+ type: "text",
1325
+ value: entity.name,
1326
+ onChange: (e) => {
1327
+ entity.name = e.target.value;
1328
+ dispatch({ type: "MARK_DIRTY" });
1329
+ refreshWorld();
1330
+ },
1331
+ style: { ...fieldStyle, fontWeight: "bold", fontSize: 13 }
1332
+ }
1333
+ ),
1334
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 9, color: "#6c7086" }, children: [
1335
+ "ID: ",
1336
+ entityId
1337
+ ] })
1338
+ ] }),
1339
+ presentComponents.map((type) => {
1340
+ const Editor = componentEditors[type];
1341
+ return /* @__PURE__ */ jsxs(
1342
+ "div",
1343
+ {
1344
+ style: { marginBottom: 8, borderTop: "1px solid #313244", paddingTop: 4 },
1345
+ children: [
1346
+ /* @__PURE__ */ jsxs(
1347
+ "div",
1348
+ {
1349
+ style: {
1350
+ display: "flex",
1351
+ alignItems: "center",
1352
+ justifyContent: "space-between",
1353
+ marginBottom: 4
1354
+ },
1355
+ children: [
1356
+ /* @__PURE__ */ jsx(
1357
+ "span",
1358
+ {
1359
+ style: {
1360
+ fontSize: 11,
1361
+ fontWeight: "bold",
1362
+ textTransform: "uppercase",
1363
+ color: "#89b4fa"
1364
+ },
1365
+ children: type
1366
+ }
1367
+ ),
1368
+ type !== "Transform" && /* @__PURE__ */ jsx(
1369
+ "button",
1370
+ {
1371
+ onClick: () => {
1372
+ world.removeComponent(entityId, type);
1373
+ dispatch({ type: "MARK_DIRTY" });
1374
+ refreshWorld();
1375
+ addLog("info", `Removed ${type} from #${entityId}`);
1376
+ },
1377
+ style: {
1378
+ background: "transparent",
1379
+ border: "none",
1380
+ color: "#f38ba8",
1381
+ cursor: "pointer",
1382
+ fontSize: 10
1383
+ },
1384
+ title: tr("editor.assets.removeComponent", { type }),
1385
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "close", size: 10 })
1386
+ }
1387
+ )
1388
+ ]
1389
+ }
1390
+ ),
1391
+ Editor ? /* @__PURE__ */ jsx(Editor, { entityId }) : /* @__PURE__ */ jsx("pre", { style: { fontSize: 10, opacity: 0.6, whiteSpace: "pre-wrap", margin: 0 }, children: JSON.stringify(world.getComponent(entityId, type), null, 2) })
1392
+ ]
1393
+ },
1394
+ type
1395
+ );
1396
+ }),
1397
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: 8, position: "relative" }, children: [
1398
+ /* @__PURE__ */ jsx(
1399
+ "button",
1400
+ {
1401
+ onClick: () => setShowAddComp(!showAddComp),
1402
+ style: {
1403
+ width: "100%",
1404
+ padding: "4px 8px",
1405
+ background: "#313244",
1406
+ border: "1px solid #45475a",
1407
+ color: "#cdd6f4",
1408
+ borderRadius: 4,
1409
+ cursor: "pointer",
1410
+ fontSize: 11
1411
+ },
1412
+ children: "+ Add Component"
1413
+ }
1414
+ ),
1415
+ showAddComp && /* @__PURE__ */ jsx(
1416
+ "div",
1417
+ {
1418
+ style: {
1419
+ position: "absolute",
1420
+ bottom: "100%",
1421
+ left: 0,
1422
+ right: 0,
1423
+ background: "#1e1e2e",
1424
+ border: "1px solid #45475a",
1425
+ borderRadius: 4,
1426
+ padding: 4,
1427
+ marginBottom: 2,
1428
+ maxHeight: 200,
1429
+ overflow: "auto",
1430
+ zIndex: 10
1431
+ },
1432
+ children: compTypes.filter((t) => !world.hasComponent(entityId, t)).map((type) => /* @__PURE__ */ jsx(
1433
+ "button",
1434
+ {
1435
+ onClick: () => {
1436
+ world.addComponent(entityId, type);
1437
+ dispatch({ type: "MARK_DIRTY" });
1438
+ refreshWorld();
1439
+ addLog("info", `Added ${type} to #${entityId}`);
1440
+ setShowAddComp(false);
1441
+ },
1442
+ style: {
1443
+ display: "block",
1444
+ width: "100%",
1445
+ textAlign: "left",
1446
+ background: "transparent",
1447
+ border: "none",
1448
+ color: "#cdd6f4",
1449
+ padding: "3px 6px",
1450
+ cursor: "pointer",
1451
+ borderRadius: 2,
1452
+ fontSize: 11
1453
+ },
1454
+ children: type
1455
+ },
1456
+ type
1457
+ ))
1458
+ }
1459
+ )
1460
+ ] })
1461
+ ] });
844
1462
  };
845
1463
  const AssetPanel = () => {
846
- const { t: tr } = useTranslation('engine');
847
- const [tab, setTab] = useState('all');
848
- const tabBtnStyle = (active) => ({
849
- padding: '2px 8px',
850
- border: 'none',
851
- borderRadius: '3px 3px 0 0',
852
- cursor: 'pointer',
853
- fontSize: 10,
854
- background: active ? '#313244' : 'transparent',
855
- color: active ? '#cdd6f4' : '#6c7086',
856
- });
857
- const assets = [
858
- { name: 'player.png', type: 'sprites', icon: 'image' },
859
- { name: 'enemy.png', type: 'sprites', icon: 'image' },
860
- { name: 'tileset.png', type: 'sprites', icon: 'image' },
861
- { name: 'background.jpg', type: 'sprites', icon: 'image' },
862
- { name: 'jump.wav', type: 'audio', icon: 'volume' },
863
- { name: 'music.mp3', type: 'audio', icon: 'music' },
864
- { name: 'coin.wav', type: 'audio', icon: 'volume' },
865
- { name: 'playerController.ts', type: 'scripts', icon: 'code' },
866
- { name: 'enemyAI.ts', type: 'scripts', icon: 'code' },
867
- ];
868
- const filtered = tab === 'all' ? assets : assets.filter((a) => a.type === tab);
869
- return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%' }, children: [jsxs("div", { style: { display: 'flex', gap: 2, padding: '2px 4px', borderBottom: '1px solid #313244' }, children: [['all', 'sprites', 'audio', 'scripts'].map((t) => (jsx("button", { onClick: () => setTab(t), style: tabBtnStyle(tab === t), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))), jsx("span", { style: { flex: 1 } }), jsx("button", { style: {
870
- background: 'transparent',
871
- border: 'none',
872
- color: '#a6adc8',
873
- cursor: 'pointer',
874
- fontSize: 10,
875
- }, title: tr('editor.assets.import'), children: "+ Import" })] }), jsx("div", { style: {
876
- flex: 1,
877
- overflow: 'auto',
878
- display: 'grid',
879
- gridTemplateColumns: 'repeat(auto-fill, 80px)',
880
- gap: 4,
881
- padding: 4,
882
- }, children: filtered.map((a) => (jsxs("div", { draggable: true, style: {
883
- display: 'flex',
884
- flexDirection: 'column',
885
- alignItems: 'center',
886
- gap: 2,
887
- padding: 4,
888
- background: '#313244',
889
- borderRadius: 4,
890
- cursor: 'grab',
891
- fontSize: 9,
892
- textAlign: 'center',
893
- }, children: [jsx(NiceIcon, { name: a.icon, size: 20 }), jsx("span", { style: { wordBreak: 'break-all' }, children: a.name })] }, a.name))) })] }));
1464
+ const { t: tr } = useTranslation("engine");
1465
+ const [tab, setTab] = useState("all");
1466
+ const tabBtnStyle = (active) => ({
1467
+ padding: "2px 8px",
1468
+ border: "none",
1469
+ borderRadius: "3px 3px 0 0",
1470
+ cursor: "pointer",
1471
+ fontSize: 10,
1472
+ background: active ? "#313244" : "transparent",
1473
+ color: active ? "#cdd6f4" : "#6c7086"
1474
+ });
1475
+ const assets = [
1476
+ { name: "player.png", type: "sprites", icon: "image" },
1477
+ { name: "enemy.png", type: "sprites", icon: "image" },
1478
+ { name: "tileset.png", type: "sprites", icon: "image" },
1479
+ { name: "background.jpg", type: "sprites", icon: "image" },
1480
+ { name: "jump.wav", type: "audio", icon: "volume" },
1481
+ { name: "music.mp3", type: "audio", icon: "music" },
1482
+ { name: "coin.wav", type: "audio", icon: "volume" },
1483
+ { name: "playerController.ts", type: "scripts", icon: "code" },
1484
+ { name: "enemyAI.ts", type: "scripts", icon: "code" }
1485
+ ];
1486
+ const filtered = tab === "all" ? assets : assets.filter((a) => a.type === tab);
1487
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%" }, children: [
1488
+ /* @__PURE__ */ jsxs(
1489
+ "div",
1490
+ {
1491
+ style: { display: "flex", gap: 2, padding: "2px 4px", borderBottom: "1px solid #313244" },
1492
+ children: [
1493
+ ["all", "sprites", "audio", "scripts"].map((t) => /* @__PURE__ */ jsx("button", { onClick: () => setTab(t), style: tabBtnStyle(tab === t), children: t.charAt(0).toUpperCase() + t.slice(1) }, t)),
1494
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 } }),
1495
+ /* @__PURE__ */ jsx(
1496
+ "button",
1497
+ {
1498
+ style: {
1499
+ background: "transparent",
1500
+ border: "none",
1501
+ color: "#a6adc8",
1502
+ cursor: "pointer",
1503
+ fontSize: 10
1504
+ },
1505
+ title: tr("editor.assets.import"),
1506
+ children: "+ Import"
1507
+ }
1508
+ )
1509
+ ]
1510
+ }
1511
+ ),
1512
+ /* @__PURE__ */ jsx(
1513
+ "div",
1514
+ {
1515
+ style: {
1516
+ flex: 1,
1517
+ overflow: "auto",
1518
+ display: "grid",
1519
+ gridTemplateColumns: "repeat(auto-fill, 80px)",
1520
+ gap: 4,
1521
+ padding: 4
1522
+ },
1523
+ children: filtered.map((a) => /* @__PURE__ */ jsxs(
1524
+ "div",
1525
+ {
1526
+ draggable: true,
1527
+ style: {
1528
+ display: "flex",
1529
+ flexDirection: "column",
1530
+ alignItems: "center",
1531
+ gap: 2,
1532
+ padding: 4,
1533
+ background: "#313244",
1534
+ borderRadius: 4,
1535
+ cursor: "grab",
1536
+ fontSize: 9,
1537
+ textAlign: "center"
1538
+ },
1539
+ children: [
1540
+ /* @__PURE__ */ jsx(NiceIcon, { name: a.icon, size: 20 }),
1541
+ /* @__PURE__ */ jsx("span", { style: { wordBreak: "break-all" }, children: a.name })
1542
+ ]
1543
+ },
1544
+ a.name
1545
+ ))
1546
+ }
1547
+ )
1548
+ ] });
894
1549
  };
895
1550
  const ConsolePanel = () => {
896
- const { t: tr } = useTranslation('engine');
897
- const { consoleLogs } = useEditor();
898
- const [filter, setFilter] = useState('all');
899
- const scrollRef = useRef(null);
900
- useEffect(() => {
901
- if (scrollRef.current) {
902
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
903
- }
904
- }, [consoleLogs.length]);
905
- const filtered = filter === 'all' ? consoleLogs : consoleLogs.filter((l) => l.level === filter);
906
- const levelColors = { info: '#89b4fa', warn: '#f9e2af', error: '#f38ba8' };
907
- const filterBtn = (f, label) => (jsx("button", { onClick: () => setFilter(f), style: {
908
- padding: '1px 6px',
909
- border: 'none',
910
- borderRadius: 2,
911
- cursor: 'pointer',
912
- fontSize: 9,
913
- background: filter === f ? '#313244' : 'transparent',
914
- color: filter === f ? '#cdd6f4' : '#6c7086',
915
- }, children: label }));
916
- return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', fontFamily: 'monospace' }, children: [jsxs("div", { style: { display: 'flex', gap: 2, padding: '2px 4px', borderBottom: '1px solid #313244' }, children: [filterBtn('all', 'All'), " ", filterBtn('info', 'Info'), " ", filterBtn('warn', 'Warn'), ' ', filterBtn('error', 'Error'), jsx("span", { style: { flex: 1 } }), jsxs("span", { style: { fontSize: 9, color: '#6c7086' }, children: [filtered.length, " entries"] })] }), jsx("div", { ref: scrollRef, style: { flex: 1, overflow: 'auto', fontSize: 10 }, children: filtered.length === 0 ? (jsx("div", { style: { opacity: 0.4, padding: 8 }, children: tr('editor.messages.consoleReady') })) : (filtered.map((log) => (jsxs("div", { style: {
917
- padding: '1px 4px',
918
- borderBottom: '1px solid #181825',
919
- display: 'flex',
920
- gap: 6,
921
- }, children: [jsx("span", { style: { color: '#6c7086', minWidth: 55 }, children: new Date(log.timestamp).toLocaleTimeString([], { hour12: false }) }), jsxs("span", { style: { color: levelColors[log.level], fontWeight: 'bold', minWidth: 35 }, children: ["[", log.level.toUpperCase(), "]"] }), jsx("span", { children: log.message })] }, log.id)))) })] }));
1551
+ const { t: tr } = useTranslation("engine");
1552
+ const { consoleLogs } = useEditor();
1553
+ const [filter, setFilter] = useState("all");
1554
+ const scrollRef = useRef(null);
1555
+ useEffect(() => {
1556
+ if (scrollRef.current) {
1557
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1558
+ }
1559
+ }, [consoleLogs.length]);
1560
+ const filtered = filter === "all" ? consoleLogs : consoleLogs.filter((l) => l.level === filter);
1561
+ const levelColors = { info: "#89b4fa", warn: "#f9e2af", error: "#f38ba8" };
1562
+ const filterBtn = (f, label) => /* @__PURE__ */ jsx(
1563
+ "button",
1564
+ {
1565
+ onClick: () => setFilter(f),
1566
+ style: {
1567
+ padding: "1px 6px",
1568
+ border: "none",
1569
+ borderRadius: 2,
1570
+ cursor: "pointer",
1571
+ fontSize: 9,
1572
+ background: filter === f ? "#313244" : "transparent",
1573
+ color: filter === f ? "#cdd6f4" : "#6c7086"
1574
+ },
1575
+ children: label
1576
+ }
1577
+ );
1578
+ return /* @__PURE__ */ jsxs(
1579
+ "div",
1580
+ {
1581
+ style: { display: "flex", flexDirection: "column", height: "100%", fontFamily: "monospace" },
1582
+ children: [
1583
+ /* @__PURE__ */ jsxs(
1584
+ "div",
1585
+ {
1586
+ style: { display: "flex", gap: 2, padding: "2px 4px", borderBottom: "1px solid #313244" },
1587
+ children: [
1588
+ filterBtn("all", "All"),
1589
+ " ",
1590
+ filterBtn("info", "Info"),
1591
+ " ",
1592
+ filterBtn("warn", "Warn"),
1593
+ " ",
1594
+ filterBtn("error", "Error"),
1595
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 } }),
1596
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 9, color: "#6c7086" }, children: [
1597
+ filtered.length,
1598
+ " entries"
1599
+ ] })
1600
+ ]
1601
+ }
1602
+ ),
1603
+ /* @__PURE__ */ jsx("div", { ref: scrollRef, style: { flex: 1, overflow: "auto", fontSize: 10 }, children: filtered.length === 0 ? /* @__PURE__ */ jsx("div", { style: { opacity: 0.4, padding: 8 }, children: tr("editor.messages.consoleReady") }) : filtered.map((log) => /* @__PURE__ */ jsxs(
1604
+ "div",
1605
+ {
1606
+ style: {
1607
+ padding: "1px 4px",
1608
+ borderBottom: "1px solid #181825",
1609
+ display: "flex",
1610
+ gap: 6
1611
+ },
1612
+ children: [
1613
+ /* @__PURE__ */ jsx("span", { style: { color: "#6c7086", minWidth: 55 }, children: new Date(log.timestamp).toLocaleTimeString([], { hour12: false }) }),
1614
+ /* @__PURE__ */ jsxs("span", { style: { color: levelColors[log.level], fontWeight: "bold", minWidth: 35 }, children: [
1615
+ "[",
1616
+ log.level.toUpperCase(),
1617
+ "]"
1618
+ ] }),
1619
+ /* @__PURE__ */ jsx("span", { children: log.message })
1620
+ ]
1621
+ },
1622
+ log.id
1623
+ )) })
1624
+ ]
1625
+ }
1626
+ );
922
1627
  };
923
1628
  const EditorViewport = () => {
924
- const canvasRef = useRef(null);
925
- const { state, dispatch, world, worldVersion, refreshWorld, undoStack } = useEditor();
926
- const dragging = useRef(false);
927
- const dragStart = useRef({ x: 0, y: 0 });
928
- const dragEntityStart = useRef({ x: 0, y: 0 });
929
- const panning = useRef(false);
930
- const panStart = useRef({ x: 0, y: 0, px: 0, py: 0 });
931
- // Draw loop
932
- useEffect(() => {
933
- const canvas = canvasRef.current;
934
- if (!canvas) {
935
- return;
936
- }
937
- const ctx = canvas.getContext('2d');
938
- if (!ctx) {
939
- return;
940
- }
941
- const { width, height } = canvas.getBoundingClientRect();
942
- canvas.width = width;
943
- canvas.height = height;
944
- ctx.fillStyle = '#11111b';
945
- ctx.fillRect(0, 0, width, height);
946
- ctx.save();
947
- ctx.translate(width / 2 + state.panOffset.x, height / 2 + state.panOffset.y);
948
- ctx.scale(state.zoom, state.zoom);
949
- // Grid
950
- if (state.showGrid) {
951
- const gs = state.gridSize;
952
- const halfW = width / 2 / state.zoom;
953
- const halfH = height / 2 / state.zoom;
954
- const offX = -state.panOffset.x / state.zoom;
955
- const offY = -state.panOffset.y / state.zoom;
956
- const startX = Math.floor((offX - halfW) / gs) * gs;
957
- const endX = Math.ceil((offX + halfW) / gs) * gs;
958
- const startY = Math.floor((offY - halfH) / gs) * gs;
959
- const endY = Math.ceil((offY + halfH) / gs) * gs;
960
- ctx.strokeStyle = 'rgba(69, 71, 90, 0.3)';
961
- ctx.lineWidth = 0.5 / state.zoom;
962
- for (let x = startX; x <= endX; x += gs) {
963
- ctx.beginPath();
964
- ctx.moveTo(x, startY);
965
- ctx.lineTo(x, endY);
966
- ctx.stroke();
967
- }
968
- for (let y = startY; y <= endY; y += gs) {
969
- ctx.beginPath();
970
- ctx.moveTo(startX, y);
971
- ctx.lineTo(endX, y);
972
- ctx.stroke();
1629
+ const canvasRef = useRef(null);
1630
+ const { state, dispatch, world, worldVersion, refreshWorld, undoStack } = useEditor();
1631
+ const dragging = useRef(false);
1632
+ const dragStart = useRef({ x: 0, y: 0 });
1633
+ const dragEntityStart = useRef({ x: 0, y: 0 });
1634
+ const panning = useRef(false);
1635
+ const panStart = useRef({ x: 0, y: 0, px: 0, py: 0 });
1636
+ useEffect(() => {
1637
+ const canvas = canvasRef.current;
1638
+ if (!canvas) {
1639
+ return;
1640
+ }
1641
+ const ctx = canvas.getContext("2d");
1642
+ if (!ctx) {
1643
+ return;
1644
+ }
1645
+ const { width, height } = canvas.getBoundingClientRect();
1646
+ canvas.width = width;
1647
+ canvas.height = height;
1648
+ ctx.fillStyle = "#11111b";
1649
+ ctx.fillRect(0, 0, width, height);
1650
+ ctx.save();
1651
+ ctx.translate(width / 2 + state.panOffset.x, height / 2 + state.panOffset.y);
1652
+ ctx.scale(state.zoom, state.zoom);
1653
+ if (state.showGrid) {
1654
+ const gs = state.gridSize;
1655
+ const halfW = width / 2 / state.zoom;
1656
+ const halfH = height / 2 / state.zoom;
1657
+ const offX = -state.panOffset.x / state.zoom;
1658
+ const offY = -state.panOffset.y / state.zoom;
1659
+ const startX = Math.floor((offX - halfW) / gs) * gs;
1660
+ const endX = Math.ceil((offX + halfW) / gs) * gs;
1661
+ const startY = Math.floor((offY - halfH) / gs) * gs;
1662
+ const endY = Math.ceil((offY + halfH) / gs) * gs;
1663
+ ctx.strokeStyle = "rgba(69, 71, 90, 0.3)";
1664
+ ctx.lineWidth = 0.5 / state.zoom;
1665
+ for (let x = startX; x <= endX; x += gs) {
1666
+ ctx.beginPath();
1667
+ ctx.moveTo(x, startY);
1668
+ ctx.lineTo(x, endY);
1669
+ ctx.stroke();
1670
+ }
1671
+ for (let y = startY; y <= endY; y += gs) {
1672
+ ctx.beginPath();
1673
+ ctx.moveTo(startX, y);
1674
+ ctx.lineTo(endX, y);
1675
+ ctx.stroke();
1676
+ }
1677
+ ctx.strokeStyle = "rgba(205, 214, 244, 0.15)";
1678
+ ctx.lineWidth = 1 / state.zoom;
1679
+ ctx.beginPath();
1680
+ ctx.moveTo(0, startY);
1681
+ ctx.lineTo(0, endY);
1682
+ ctx.stroke();
1683
+ ctx.beginPath();
1684
+ ctx.moveTo(startX, 0);
1685
+ ctx.lineTo(endX, 0);
1686
+ ctx.stroke();
1687
+ }
1688
+ const entities = world.allEntities();
1689
+ for (const entity of entities) {
1690
+ if (!entity.enabled) {
1691
+ continue;
1692
+ }
1693
+ const transform = world.getComponent(entity.id, "Transform");
1694
+ if (!transform) {
1695
+ continue;
1696
+ }
1697
+ ctx.save();
1698
+ ctx.translate(transform.position.x, transform.position.y);
1699
+ ctx.rotate(transform.rotation);
1700
+ ctx.scale(transform.scale.x, transform.scale.y);
1701
+ const isSelected = state.selectedEntities.includes(entity.id);
1702
+ const shape = world.getComponent(entity.id, "Shape");
1703
+ if (shape && shape.visible) {
1704
+ ctx.fillStyle = colorToCSS(shape.fillColor);
1705
+ ctx.strokeStyle = colorToCSS(shape.strokeColor);
1706
+ ctx.lineWidth = shape.strokeWidth;
1707
+ switch (shape.shape) {
1708
+ case "rect":
1709
+ ctx.fillRect(-shape.width / 2, -shape.height / 2, shape.width, shape.height);
1710
+ if (shape.strokeWidth > 0) {
1711
+ ctx.strokeRect(-shape.width / 2, -shape.height / 2, shape.width, shape.height);
973
1712
  }
974
- // Origin axes
975
- ctx.strokeStyle = 'rgba(205, 214, 244, 0.15)';
976
- ctx.lineWidth = 1 / state.zoom;
1713
+ break;
1714
+ case "circle":
977
1715
  ctx.beginPath();
978
- ctx.moveTo(0, startY);
979
- ctx.lineTo(0, endY);
980
- ctx.stroke();
1716
+ ctx.arc(0, 0, shape.radius, 0, Math.PI * 2);
1717
+ ctx.fill();
1718
+ if (shape.strokeWidth > 0) {
1719
+ ctx.stroke();
1720
+ }
1721
+ break;
1722
+ case "line":
981
1723
  ctx.beginPath();
982
- ctx.moveTo(startX, 0);
983
- ctx.lineTo(endX, 0);
1724
+ ctx.moveTo(shape.lineStart.x, shape.lineStart.y);
1725
+ ctx.lineTo(shape.lineEnd.x, shape.lineEnd.y);
984
1726
  ctx.stroke();
985
- }
986
- // Render entities
987
- const entities = world.allEntities();
988
- for (const entity of entities) {
989
- if (!entity.enabled) {
990
- continue;
991
- }
992
- const transform = world.getComponent(entity.id, 'Transform');
993
- if (!transform) {
994
- continue;
995
- }
996
- ctx.save();
997
- ctx.translate(transform.position.x, transform.position.y);
998
- ctx.rotate(transform.rotation);
999
- ctx.scale(transform.scale.x, transform.scale.y);
1000
- const isSelected = state.selectedEntities.includes(entity.id);
1001
- // Shape
1002
- const shape = world.getComponent(entity.id, 'Shape');
1003
- if (shape && shape.visible) {
1004
- ctx.fillStyle = colorToCSS(shape.fillColor);
1005
- ctx.strokeStyle = colorToCSS(shape.strokeColor);
1006
- ctx.lineWidth = shape.strokeWidth;
1007
- switch (shape.shape) {
1008
- case 'rect':
1009
- ctx.fillRect(-shape.width / 2, -shape.height / 2, shape.width, shape.height);
1010
- if (shape.strokeWidth > 0) {
1011
- ctx.strokeRect(-shape.width / 2, -shape.height / 2, shape.width, shape.height);
1012
- }
1013
- break;
1014
- case 'circle':
1015
- ctx.beginPath();
1016
- ctx.arc(0, 0, shape.radius, 0, Math.PI * 2);
1017
- ctx.fill();
1018
- if (shape.strokeWidth > 0) {
1019
- ctx.stroke();
1020
- }
1021
- break;
1022
- case 'line':
1023
- ctx.beginPath();
1024
- ctx.moveTo(shape.lineStart.x, shape.lineStart.y);
1025
- ctx.lineTo(shape.lineEnd.x, shape.lineEnd.y);
1026
- ctx.stroke();
1027
- break;
1028
- case 'polygon':
1029
- if (shape.points.length > 2) {
1030
- ctx.beginPath();
1031
- ctx.moveTo(shape.points[0].x, shape.points[0].y);
1032
- for (let i = 1; i < shape.points.length; i++) {
1033
- ctx.lineTo(shape.points[i].x, shape.points[i].y);
1034
- }
1035
- ctx.closePath();
1036
- ctx.fill();
1037
- if (shape.strokeWidth > 0) {
1038
- ctx.stroke();
1039
- }
1040
- }
1041
- break;
1042
- }
1043
- }
1044
- // Sprite
1045
- const sprite = world.getComponent(entity.id, 'Sprite');
1046
- if (sprite && sprite.visible) {
1047
- ctx.globalAlpha = sprite.opacity;
1048
- ctx.fillStyle = colorToCSS(sprite.tint);
1049
- const ax = -sprite.width * sprite.anchor.x;
1050
- const ay = -sprite.height * sprite.anchor.y;
1051
- ctx.fillRect(ax, ay, sprite.width, sprite.height);
1052
- // Sprite border
1053
- ctx.strokeStyle = 'rgba(255,255,255,0.2)';
1054
- ctx.lineWidth = 1 / state.zoom;
1055
- ctx.strokeRect(ax, ay, sprite.width, sprite.height);
1056
- // Sprite label
1057
- ctx.fillStyle = 'rgba(255,255,255,0.5)';
1058
- ctx.font = `${9 / state.zoom}px monospace`;
1059
- ctx.textAlign = 'center';
1060
- ctx.textBaseline = 'middle';
1061
- ctx.fillText(sprite.assetId || 'sprite', 0, 0);
1062
- ctx.globalAlpha = 1;
1063
- }
1064
- // Text
1065
- const text = world.getComponent(entity.id, 'Text');
1066
- if (text && text.visible && text.text) {
1067
- ctx.font = `${text.fontSize}px ${text.fontFamily}`;
1068
- ctx.fillStyle = colorToCSS(text.color);
1069
- ctx.textAlign = text.align;
1070
- ctx.textBaseline = text.baseline;
1071
- ctx.fillText(text.text, 0, 0);
1072
- }
1073
- // Particle emitter indicator
1074
- const particles = world.getComponent(entity.id, 'ParticleEmitter');
1075
- if (particles) {
1076
- ctx.strokeStyle = '#f9e2af';
1077
- ctx.lineWidth = 1 / state.zoom;
1078
- ctx.setLineDash([3 / state.zoom, 3 / state.zoom]);
1079
- ctx.beginPath();
1080
- ctx.arc(0, 0, 12, 0, Math.PI * 2);
1727
+ break;
1728
+ case "polygon":
1729
+ if (shape.points.length > 2) {
1730
+ ctx.beginPath();
1731
+ ctx.moveTo(shape.points[0].x, shape.points[0].y);
1732
+ for (let i = 1; i < shape.points.length; i++) {
1733
+ ctx.lineTo(shape.points[i].x, shape.points[i].y);
1734
+ }
1735
+ ctx.closePath();
1736
+ ctx.fill();
1737
+ if (shape.strokeWidth > 0) {
1081
1738
  ctx.stroke();
1082
- ctx.setLineDash([]);
1083
- ctx.fillStyle = '#f9e2af';
1084
- ctx.font = `${8 / state.zoom}px monospace`;
1085
- ctx.textAlign = 'center';
1086
- // Sparkle marker drawn as a cross (no glyph fonts on the canvas)
1087
- const spark = 4 / state.zoom;
1088
- ctx.fillRect(-spark / 4, -spark, spark / 2, spark * 2);
1089
- ctx.fillRect(-spark, -spark / 4, spark * 2, spark / 2);
1090
- }
1091
- // Camera indicator
1092
- const cam = world.getComponent(entity.id, 'CameraTarget');
1093
- if (cam) {
1094
- ctx.strokeStyle = '#a6e3a1';
1095
- ctx.lineWidth = 1.5 / state.zoom;
1096
- const hw = 60, hh = 34;
1097
- ctx.strokeRect(-hw, -hh, hw * 2, hh * 2);
1098
- ctx.font = `${8 / state.zoom}px monospace`;
1099
- ctx.fillStyle = '#a6e3a1';
1100
- ctx.textAlign = 'center';
1101
- ctx.fillText('Camera', 0, -hh - 4 / state.zoom);
1102
- }
1103
- // Selection highlight
1104
- if (isSelected) {
1105
- const b = getEntityBounds(entity.id, world);
1106
- ctx.strokeStyle = '#89b4fa';
1107
- ctx.lineWidth = 2 / state.zoom;
1108
- ctx.setLineDash([4 / state.zoom, 4 / state.zoom]);
1109
- ctx.strokeRect(b.x - 2, b.y - 2, b.w + 4, b.h + 4);
1110
- ctx.setLineDash([]);
1111
- // Corner handles
1112
- const hs = 4 / state.zoom;
1113
- ctx.fillStyle = '#89b4fa';
1114
- ctx.fillRect(b.x - hs, b.y - hs, hs * 2, hs * 2);
1115
- ctx.fillRect(b.x + b.w - hs, b.y - hs, hs * 2, hs * 2);
1116
- ctx.fillRect(b.x - hs, b.y + b.h - hs, hs * 2, hs * 2);
1117
- ctx.fillRect(b.x + b.w - hs, b.y + b.h - hs, hs * 2, hs * 2);
1118
- }
1119
- // Entity name label (only if no visible shape/sprite/text OR selected)
1120
- if (isSelected || (!shape?.visible && !sprite?.visible && !text?.visible)) {
1121
- ctx.font = `${10 / state.zoom}px monospace`;
1122
- ctx.fillStyle = isSelected ? '#89b4fa' : 'rgba(205, 214, 244, 0.4)';
1123
- ctx.textAlign = 'center';
1124
- ctx.textBaseline = 'bottom';
1125
- const b = getEntityBounds(entity.id, world);
1126
- ctx.fillText(entity.name, 0, b.y - 4 / state.zoom);
1127
- // Draw small dot for invisible/empty entities
1128
- if (!shape?.visible && !sprite?.visible && !text?.visible) {
1129
- ctx.fillStyle = isSelected ? '#89b4fa' : 'rgba(205, 214, 244, 0.3)';
1130
- ctx.beginPath();
1131
- ctx.arc(0, 0, 3 / state.zoom, 0, Math.PI * 2);
1132
- ctx.fill();
1133
- }
1739
+ }
1134
1740
  }
1135
- ctx.restore();
1136
- }
1137
- ctx.restore();
1138
- // HUD
1139
- ctx.fillStyle = '#6c7086';
1140
- ctx.font = '10px monospace';
1141
- ctx.textAlign = 'left';
1142
- ctx.textBaseline = 'top';
1143
- ctx.fillText(`Entities: ${world.entityCount} | Grid: ${state.gridSize}px`, 8, 8);
1144
- }, [state, worldVersion, world]);
1145
- // Mouse handlers
1146
- const screenToWorld = useCallback((e) => {
1147
- const canvas = canvasRef.current;
1148
- const rect = canvas.getBoundingClientRect();
1149
- const sx = e.clientX - rect.left;
1150
- const sy = e.clientY - rect.top;
1151
- return {
1152
- x: (sx - canvas.width / 2 - state.panOffset.x) / state.zoom,
1153
- y: (sy - canvas.height / 2 - state.panOffset.y) / state.zoom,
1154
- };
1155
- }, [state.zoom, state.panOffset]);
1156
- const handlePointerDown = useCallback((e) => {
1157
- // Middle click or pan tool = pan
1158
- if (e.button === 1 || (e.button === 0 && state.tool === 'pan')) {
1159
- panning.current = true;
1160
- panStart.current = {
1161
- x: e.clientX,
1162
- y: e.clientY,
1163
- px: state.panOffset.x,
1164
- py: state.panOffset.y,
1165
- };
1166
- e.target.setPointerCapture(e.pointerId);
1167
- return;
1741
+ break;
1168
1742
  }
1169
- if (e.button !== 0) {
1170
- return;
1171
- }
1172
- const { x: wx, y: wy } = screenToWorld(e);
1173
- // Hit test (reverse order for top-most)
1174
- const entities = world.allEntities();
1175
- let hitId = null;
1176
- for (let i = entities.length - 1; i >= 0; i--) {
1177
- const ent = entities[i];
1178
- if (!ent.enabled) {
1179
- continue;
1180
- }
1181
- const bounds = getEntityWorldBounds(ent.id, world);
1182
- if (bounds &&
1183
- wx >= bounds.x &&
1184
- wx <= bounds.x + bounds.w &&
1185
- wy >= bounds.y &&
1186
- wy <= bounds.y + bounds.h) {
1187
- hitId = ent.id;
1188
- break;
1189
- }
1743
+ }
1744
+ const sprite = world.getComponent(entity.id, "Sprite");
1745
+ if (sprite && sprite.visible) {
1746
+ ctx.globalAlpha = sprite.opacity;
1747
+ ctx.fillStyle = colorToCSS(sprite.tint);
1748
+ const ax = -sprite.width * sprite.anchor.x;
1749
+ const ay = -sprite.height * sprite.anchor.y;
1750
+ ctx.fillRect(ax, ay, sprite.width, sprite.height);
1751
+ ctx.strokeStyle = "rgba(255,255,255,0.2)";
1752
+ ctx.lineWidth = 1 / state.zoom;
1753
+ ctx.strokeRect(ax, ay, sprite.width, sprite.height);
1754
+ ctx.fillStyle = "rgba(255,255,255,0.5)";
1755
+ ctx.font = `${9 / state.zoom}px monospace`;
1756
+ ctx.textAlign = "center";
1757
+ ctx.textBaseline = "middle";
1758
+ ctx.fillText(sprite.assetId || "sprite", 0, 0);
1759
+ ctx.globalAlpha = 1;
1760
+ }
1761
+ const text = world.getComponent(entity.id, "Text");
1762
+ if (text && text.visible && text.text) {
1763
+ ctx.font = `${text.fontSize}px ${text.fontFamily}`;
1764
+ ctx.fillStyle = colorToCSS(text.color);
1765
+ ctx.textAlign = text.align;
1766
+ ctx.textBaseline = text.baseline;
1767
+ ctx.fillText(text.text, 0, 0);
1768
+ }
1769
+ const particles = world.getComponent(entity.id, "ParticleEmitter");
1770
+ if (particles) {
1771
+ ctx.strokeStyle = "#f9e2af";
1772
+ ctx.lineWidth = 1 / state.zoom;
1773
+ ctx.setLineDash([3 / state.zoom, 3 / state.zoom]);
1774
+ ctx.beginPath();
1775
+ ctx.arc(0, 0, 12, 0, Math.PI * 2);
1776
+ ctx.stroke();
1777
+ ctx.setLineDash([]);
1778
+ ctx.fillStyle = "#f9e2af";
1779
+ ctx.font = `${8 / state.zoom}px monospace`;
1780
+ ctx.textAlign = "center";
1781
+ const spark = 4 / state.zoom;
1782
+ ctx.fillRect(-spark / 4, -spark, spark / 2, spark * 2);
1783
+ ctx.fillRect(-spark, -spark / 4, spark * 2, spark / 2);
1784
+ }
1785
+ const cam = world.getComponent(entity.id, "CameraTarget");
1786
+ if (cam) {
1787
+ ctx.strokeStyle = "#a6e3a1";
1788
+ ctx.lineWidth = 1.5 / state.zoom;
1789
+ const hw = 60, hh = 34;
1790
+ ctx.strokeRect(-hw, -hh, hw * 2, hh * 2);
1791
+ ctx.font = `${8 / state.zoom}px monospace`;
1792
+ ctx.fillStyle = "#a6e3a1";
1793
+ ctx.textAlign = "center";
1794
+ ctx.fillText("Camera", 0, -hh - 4 / state.zoom);
1795
+ }
1796
+ if (isSelected) {
1797
+ const b = getEntityBounds(entity.id, world);
1798
+ ctx.strokeStyle = "#89b4fa";
1799
+ ctx.lineWidth = 2 / state.zoom;
1800
+ ctx.setLineDash([4 / state.zoom, 4 / state.zoom]);
1801
+ ctx.strokeRect(b.x - 2, b.y - 2, b.w + 4, b.h + 4);
1802
+ ctx.setLineDash([]);
1803
+ const hs = 4 / state.zoom;
1804
+ ctx.fillStyle = "#89b4fa";
1805
+ ctx.fillRect(b.x - hs, b.y - hs, hs * 2, hs * 2);
1806
+ ctx.fillRect(b.x + b.w - hs, b.y - hs, hs * 2, hs * 2);
1807
+ ctx.fillRect(b.x - hs, b.y + b.h - hs, hs * 2, hs * 2);
1808
+ ctx.fillRect(b.x + b.w - hs, b.y + b.h - hs, hs * 2, hs * 2);
1809
+ }
1810
+ if (isSelected || !shape?.visible && !sprite?.visible && !text?.visible) {
1811
+ ctx.font = `${10 / state.zoom}px monospace`;
1812
+ ctx.fillStyle = isSelected ? "#89b4fa" : "rgba(205, 214, 244, 0.4)";
1813
+ ctx.textAlign = "center";
1814
+ ctx.textBaseline = "bottom";
1815
+ const b = getEntityBounds(entity.id, world);
1816
+ ctx.fillText(entity.name, 0, b.y - 4 / state.zoom);
1817
+ if (!shape?.visible && !sprite?.visible && !text?.visible) {
1818
+ ctx.fillStyle = isSelected ? "#89b4fa" : "rgba(205, 214, 244, 0.3)";
1819
+ ctx.beginPath();
1820
+ ctx.arc(0, 0, 3 / state.zoom, 0, Math.PI * 2);
1821
+ ctx.fill();
1190
1822
  }
1191
- if (hitId !== null) {
1192
- dispatch({ type: 'SELECT', entities: [hitId] });
1193
- if (state.tool === 'select' || state.tool === 'move') {
1194
- dragging.current = true;
1195
- dragStart.current = { x: wx, y: wy };
1196
- const t = world.getComponent(hitId, 'Transform');
1197
- if (t) {
1198
- dragEntityStart.current = { x: t.position.x, y: t.position.y };
1199
- }
1200
- }
1823
+ }
1824
+ ctx.restore();
1825
+ }
1826
+ ctx.restore();
1827
+ ctx.fillStyle = "#6c7086";
1828
+ ctx.font = "10px monospace";
1829
+ ctx.textAlign = "left";
1830
+ ctx.textBaseline = "top";
1831
+ ctx.fillText(`Entities: ${world.entityCount} | Grid: ${state.gridSize}px`, 8, 8);
1832
+ }, [state, worldVersion, world]);
1833
+ const screenToWorld = useCallback(
1834
+ (e) => {
1835
+ const canvas = canvasRef.current;
1836
+ const rect = canvas.getBoundingClientRect();
1837
+ const sx = e.clientX - rect.left;
1838
+ const sy = e.clientY - rect.top;
1839
+ return {
1840
+ x: (sx - canvas.width / 2 - state.panOffset.x) / state.zoom,
1841
+ y: (sy - canvas.height / 2 - state.panOffset.y) / state.zoom
1842
+ };
1843
+ },
1844
+ [state.zoom, state.panOffset]
1845
+ );
1846
+ const handlePointerDown = useCallback(
1847
+ (e) => {
1848
+ if (e.button === 1 || e.button === 0 && state.tool === "pan") {
1849
+ panning.current = true;
1850
+ panStart.current = {
1851
+ x: e.clientX,
1852
+ y: e.clientY,
1853
+ px: state.panOffset.x,
1854
+ py: state.panOffset.y
1855
+ };
1856
+ e.target.setPointerCapture(e.pointerId);
1857
+ return;
1858
+ }
1859
+ if (e.button !== 0) {
1860
+ return;
1861
+ }
1862
+ const { x: wx, y: wy } = screenToWorld(e);
1863
+ const entities = world.allEntities();
1864
+ let hitId = null;
1865
+ for (let i = entities.length - 1; i >= 0; i--) {
1866
+ const ent = entities[i];
1867
+ if (!ent.enabled) {
1868
+ continue;
1201
1869
  }
1202
- else {
1203
- dispatch({ type: 'DESELECT_ALL' });
1870
+ const bounds = getEntityWorldBounds(ent.id, world);
1871
+ if (bounds && wx >= bounds.x && wx <= bounds.x + bounds.w && wy >= bounds.y && wy <= bounds.y + bounds.h) {
1872
+ hitId = ent.id;
1873
+ break;
1204
1874
  }
1205
- e.target.setPointerCapture(e.pointerId);
1206
- }, [state.tool, state.panOffset, screenToWorld, world, dispatch]);
1207
- const handlePointerMove = useCallback((e) => {
1208
- if (panning.current) {
1209
- const dx = e.clientX - panStart.current.x;
1210
- const dy = e.clientY - panStart.current.y;
1211
- dispatch({
1212
- type: 'SET_PAN',
1213
- offset: { x: panStart.current.px + dx, y: panStart.current.py + dy },
1214
- });
1215
- return;
1875
+ }
1876
+ if (hitId !== null) {
1877
+ dispatch({ type: "SELECT", entities: [hitId] });
1878
+ if (state.tool === "select" || state.tool === "move") {
1879
+ dragging.current = true;
1880
+ dragStart.current = { x: wx, y: wy };
1881
+ const t = world.getComponent(hitId, "Transform");
1882
+ if (t) {
1883
+ dragEntityStart.current = { x: t.position.x, y: t.position.y };
1884
+ }
1216
1885
  }
1217
- if (dragging.current && state.selectedEntities.length > 0) {
1218
- const { x: wx, y: wy } = screenToWorld(e);
1219
- const dx = wx - dragStart.current.x;
1220
- const dy = wy - dragStart.current.y;
1221
- const entityId = state.selectedEntities[0];
1222
- const t = world.getComponent(entityId, 'Transform');
1223
- if (t) {
1224
- let nx = dragEntityStart.current.x + dx;
1225
- let ny = dragEntityStart.current.y + dy;
1226
- if (state.snapToGrid) {
1227
- nx = Math.round(nx / state.gridSize) * state.gridSize;
1228
- ny = Math.round(ny / state.gridSize) * state.gridSize;
1229
- }
1230
- t.position.x = nx;
1231
- t.position.y = ny;
1232
- dispatch({ type: 'MARK_DIRTY' });
1233
- refreshWorld();
1234
- }
1886
+ } else {
1887
+ dispatch({ type: "DESELECT_ALL" });
1888
+ }
1889
+ e.target.setPointerCapture(e.pointerId);
1890
+ },
1891
+ [state.tool, state.panOffset, screenToWorld, world, dispatch]
1892
+ );
1893
+ const handlePointerMove = useCallback(
1894
+ (e) => {
1895
+ if (panning.current) {
1896
+ const dx = e.clientX - panStart.current.x;
1897
+ const dy = e.clientY - panStart.current.y;
1898
+ dispatch({
1899
+ type: "SET_PAN",
1900
+ offset: { x: panStart.current.px + dx, y: panStart.current.py + dy }
1901
+ });
1902
+ return;
1903
+ }
1904
+ if (dragging.current && state.selectedEntities.length > 0) {
1905
+ const { x: wx, y: wy } = screenToWorld(e);
1906
+ const dx = wx - dragStart.current.x;
1907
+ const dy = wy - dragStart.current.y;
1908
+ const entityId = state.selectedEntities[0];
1909
+ const t = world.getComponent(entityId, "Transform");
1910
+ if (t) {
1911
+ let nx = dragEntityStart.current.x + dx;
1912
+ let ny = dragEntityStart.current.y + dy;
1913
+ if (state.snapToGrid) {
1914
+ nx = Math.round(nx / state.gridSize) * state.gridSize;
1915
+ ny = Math.round(ny / state.gridSize) * state.gridSize;
1916
+ }
1917
+ t.position.x = nx;
1918
+ t.position.y = ny;
1919
+ dispatch({ type: "MARK_DIRTY" });
1920
+ refreshWorld();
1235
1921
  }
1236
- }, [
1237
- state.selectedEntities,
1238
- state.snapToGrid,
1239
- state.gridSize,
1240
- screenToWorld,
1241
- world,
1242
- dispatch,
1243
- refreshWorld,
1244
- ]);
1245
- const handlePointerUp = useCallback(() => {
1246
- if (dragging.current && state.selectedEntities.length > 0) {
1247
- const entityId = state.selectedEntities[0];
1248
- const t = world.getComponent(entityId, 'Transform');
1249
- if (t) {
1250
- const finalPos = { x: t.position.x, y: t.position.y };
1251
- const startPos = { ...dragEntityStart.current };
1252
- if (finalPos.x !== startPos.x || finalPos.y !== startPos.y) {
1253
- undoStack.push({
1254
- description: 'Move entity',
1255
- undo: () => {
1256
- t.position.x = startPos.x;
1257
- t.position.y = startPos.y;
1258
- refreshWorld();
1259
- },
1260
- redo: () => {
1261
- t.position.x = finalPos.x;
1262
- t.position.y = finalPos.y;
1263
- refreshWorld();
1264
- },
1265
- });
1266
- }
1922
+ }
1923
+ },
1924
+ [
1925
+ state.selectedEntities,
1926
+ state.snapToGrid,
1927
+ state.gridSize,
1928
+ screenToWorld,
1929
+ world,
1930
+ dispatch,
1931
+ refreshWorld
1932
+ ]
1933
+ );
1934
+ const handlePointerUp = useCallback(() => {
1935
+ if (dragging.current && state.selectedEntities.length > 0) {
1936
+ const entityId = state.selectedEntities[0];
1937
+ const t = world.getComponent(entityId, "Transform");
1938
+ if (t) {
1939
+ const finalPos = { x: t.position.x, y: t.position.y };
1940
+ const startPos = { ...dragEntityStart.current };
1941
+ if (finalPos.x !== startPos.x || finalPos.y !== startPos.y) {
1942
+ undoStack.push({
1943
+ description: "Move entity",
1944
+ undo: () => {
1945
+ t.position.x = startPos.x;
1946
+ t.position.y = startPos.y;
1947
+ refreshWorld();
1948
+ },
1949
+ redo: () => {
1950
+ t.position.x = finalPos.x;
1951
+ t.position.y = finalPos.y;
1952
+ refreshWorld();
1267
1953
  }
1954
+ });
1268
1955
  }
1269
- dragging.current = false;
1270
- panning.current = false;
1271
- }, [state.selectedEntities, world, undoStack, refreshWorld]);
1272
- const handleWheel = useCallback((e) => {
1273
- e.preventDefault();
1274
- const factor = e.deltaY > 0 ? 0.9 : 1.1;
1275
- dispatch({ type: 'SET_ZOOM', zoom: state.zoom * factor });
1276
- }, [state.zoom, dispatch]);
1277
- return (jsx("canvas", { ref: canvasRef, style: {
1278
- width: '100%',
1279
- height: '100%',
1280
- display: 'block',
1281
- cursor: getCursorForTool(state.tool),
1282
- }, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onWheel: handleWheel, onContextMenu: (e) => e.preventDefault() }));
1956
+ }
1957
+ }
1958
+ dragging.current = false;
1959
+ panning.current = false;
1960
+ }, [state.selectedEntities, world, undoStack, refreshWorld]);
1961
+ const handleWheel = useCallback(
1962
+ (e) => {
1963
+ e.preventDefault();
1964
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
1965
+ dispatch({ type: "SET_ZOOM", zoom: state.zoom * factor });
1966
+ },
1967
+ [state.zoom, dispatch]
1968
+ );
1969
+ return /* @__PURE__ */ jsx(
1970
+ "canvas",
1971
+ {
1972
+ ref: canvasRef,
1973
+ style: {
1974
+ width: "100%",
1975
+ height: "100%",
1976
+ display: "block",
1977
+ cursor: getCursorForTool(state.tool)
1978
+ },
1979
+ onPointerDown: handlePointerDown,
1980
+ onPointerMove: handlePointerMove,
1981
+ onPointerUp: handlePointerUp,
1982
+ onWheel: handleWheel,
1983
+ onContextMenu: (e) => e.preventDefault()
1984
+ }
1985
+ );
1986
+ };
1987
+ export {
1988
+ NiceGameEditor,
1989
+ UndoStack,
1990
+ deserializeProject,
1991
+ serializeProject,
1992
+ useEditor
1283
1993
  };
1284
-
1285
- export { NiceGameEditor, UndoStack, deserializeProject, serializeProject, useEditor };