@nice2dev/game-engine 0.1.0 → 1.0.2

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 (112) hide show
  1. package/CHANGELOG.md +193 -1
  2. package/dist/cjs/audio/AudioBridge.js +454 -0
  3. package/dist/cjs/audio/AudioBridge.js.map +1 -0
  4. package/dist/cjs/devtools/GameplayAnalytics.js +651 -0
  5. package/dist/cjs/devtools/GameplayAnalytics.js.map +1 -0
  6. package/dist/cjs/dialogue/DialogueSystem.js +1023 -0
  7. package/dist/cjs/dialogue/DialogueSystem.js.map +1 -0
  8. package/dist/cjs/editor/NiceGameEditor.js +569 -71
  9. package/dist/cjs/editor/NiceGameEditor.js.map +1 -1
  10. package/dist/cjs/engine/SaveSystemV2.js +494 -0
  11. package/dist/cjs/engine/SaveSystemV2.js.map +1 -0
  12. package/dist/cjs/i18n/useTranslation.js +11 -11
  13. package/dist/cjs/index.js +90 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/input/GamepadNavigation.js +21 -21
  16. package/dist/cjs/input/useGamepads.js +6 -6
  17. package/dist/cjs/integration/IconSprite.js +281 -0
  18. package/dist/cjs/integration/IconSprite.js.map +1 -0
  19. package/dist/cjs/inventory/InventorySystem.js +930 -0
  20. package/dist/cjs/inventory/InventorySystem.js.map +1 -0
  21. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AbortController.js.map +1 -1
  22. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js.map +1 -1
  23. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js.map +1 -1
  24. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js.map +1 -1
  25. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Errors.js.map +1 -1
  26. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js.map +1 -1
  27. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js.map +1 -1
  28. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js.map +1 -1
  29. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpClient.js.map +1 -1
  30. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js.map +1 -1
  31. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnection.js.map +1 -1
  32. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js.map +1 -1
  33. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js.map +1 -1
  34. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ILogger.js.map +1 -1
  35. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ITransport.js.map +1 -1
  36. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js.map +1 -1
  37. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Loggers.js.map +1 -1
  38. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js.map +1 -1
  39. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js.map +1 -1
  40. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js.map +1 -1
  41. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Subject.js.map +1 -1
  42. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js.map +1 -1
  43. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Utils.js.map +1 -1
  44. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js.map +1 -1
  45. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js.map +1 -1
  46. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/pkg-version.js.map +1 -1
  47. package/dist/cjs/quest/QuestSystem.js +924 -0
  48. package/dist/cjs/quest/QuestSystem.js.map +1 -0
  49. package/dist/cjs/rendering/WebGPURenderPipeline.js +658 -0
  50. package/dist/cjs/rendering/WebGPURenderPipeline.js.map +1 -0
  51. package/dist/cjs/xr/ARVR.js.map +1 -1
  52. package/dist/esm/audio/AudioBridge.js +446 -0
  53. package/dist/esm/audio/AudioBridge.js.map +1 -0
  54. package/dist/esm/devtools/GameplayAnalytics.js +639 -0
  55. package/dist/esm/devtools/GameplayAnalytics.js.map +1 -0
  56. package/dist/esm/dialogue/DialogueSystem.js +1008 -0
  57. package/dist/esm/dialogue/DialogueSystem.js.map +1 -0
  58. package/dist/esm/editor/NiceGameEditor.js +556 -58
  59. package/dist/esm/editor/NiceGameEditor.js.map +1 -1
  60. package/dist/esm/engine/SaveSystemV2.js +487 -0
  61. package/dist/esm/engine/SaveSystemV2.js.map +1 -0
  62. package/dist/esm/index.js +11 -3
  63. package/dist/esm/index.js.map +1 -1
  64. package/dist/esm/integration/IconSprite.js +266 -0
  65. package/dist/esm/integration/IconSprite.js.map +1 -0
  66. package/dist/esm/inventory/InventorySystem.js +924 -0
  67. package/dist/esm/inventory/InventorySystem.js.map +1 -0
  68. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AbortController.js.map +1 -1
  69. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js.map +1 -1
  70. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js.map +1 -1
  71. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js.map +1 -1
  72. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Errors.js.map +1 -1
  73. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js.map +1 -1
  74. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js.map +1 -1
  75. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js.map +1 -1
  76. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpClient.js.map +1 -1
  77. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js.map +1 -1
  78. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnection.js.map +1 -1
  79. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js.map +1 -1
  80. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js.map +1 -1
  81. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ILogger.js.map +1 -1
  82. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ITransport.js.map +1 -1
  83. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js.map +1 -1
  84. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Loggers.js.map +1 -1
  85. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js.map +1 -1
  86. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js.map +1 -1
  87. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js.map +1 -1
  88. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Subject.js.map +1 -1
  89. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js.map +1 -1
  90. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Utils.js.map +1 -1
  91. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js.map +1 -1
  92. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js.map +1 -1
  93. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/pkg-version.js.map +1 -1
  94. package/dist/esm/quest/QuestSystem.js +916 -0
  95. package/dist/esm/quest/QuestSystem.js.map +1 -0
  96. package/dist/esm/rendering/WebGPURenderPipeline.js +642 -0
  97. package/dist/esm/rendering/WebGPURenderPipeline.js.map +1 -0
  98. package/dist/esm/xr/ARVR.js.map +1 -1
  99. package/dist/types/__tests__/setup.d.ts +1 -1
  100. package/dist/types/audio/AudioBridge.d.ts +199 -0
  101. package/dist/types/devtools/GameplayAnalytics.d.ts +279 -0
  102. package/dist/types/dialogue/DialogueSystem.d.ts +326 -0
  103. package/dist/types/dialogue/index.d.ts +2 -0
  104. package/dist/types/editor/NiceGameEditor.d.ts +12 -1
  105. package/dist/types/engine/SaveSystemV2.d.ts +155 -0
  106. package/dist/types/index.d.ts +19 -3
  107. package/dist/types/integration/IconSprite.d.ts +196 -0
  108. package/dist/types/inventory/InventorySystem.d.ts +336 -0
  109. package/dist/types/performance/WebGPUCompute.d.ts +0 -10
  110. package/dist/types/quest/QuestSystem.d.ts +287 -0
  111. package/dist/types/rendering/WebGPURenderPipeline.d.ts +255 -0
  112. package/package.json +7 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
- var react = require('react');
4
+ var React = require('react');
5
5
  var EventBus = require('../core/EventBus.js');
6
6
  var World = require('../ecs/World.js');
7
7
  var SceneManager = require('../scene/SceneManager.js');
@@ -113,15 +113,15 @@ function editorReducer(state, action) {
113
113
  return state;
114
114
  }
115
115
  }
116
- const EditorContext = react.createContext(null);
116
+ const EditorContext = React.createContext(null);
117
117
  function useEditor() {
118
- const ctx = react.useContext(EditorContext);
118
+ const ctx = React.useContext(EditorContext);
119
119
  if (!ctx)
120
120
  throw new Error('useEditor must be used within NiceGameEditor');
121
121
  return ctx;
122
122
  }
123
123
  function useKeyboardShortcuts(shortcuts) {
124
- react.useEffect(() => {
124
+ React.useEffect(() => {
125
125
  const handler = (e) => {
126
126
  for (const s of shortcuts) {
127
127
  if (e.key.toLowerCase() === s.key.toLowerCase() &&
@@ -147,17 +147,35 @@ function deserializeProject(json) {
147
147
  }
148
148
  const NiceGameEditor = ({ project, onSave, className, style, }) => {
149
149
  var _a, _b, _c, _d;
150
- const [state, dispatch] = react.useReducer(editorReducer, DEFAULT_STATE);
151
- const worldRef = react.useRef(new World.World());
152
- const undoRef = react.useRef(new UndoStack());
153
- const sceneManagerRef = react.useRef(new SceneManager.SceneManager());
154
- const eventsRef = react.useRef(new EventBus.EventBus());
150
+ const [state, dispatch] = React.useReducer(editorReducer, DEFAULT_STATE);
151
+ const worldRef = React.useRef(new World.World());
152
+ const undoRef = React.useRef(new UndoStack());
153
+ const sceneManagerRef = React.useRef(new SceneManager.SceneManager());
154
+ const eventsRef = React.useRef(new EventBus.EventBus());
155
155
  const world = worldRef.current;
156
156
  const undoStack = undoRef.current;
157
157
  const sceneManager = sceneManagerRef.current;
158
158
  const events = eventsRef.current;
159
+ const [worldVersion, setWorldVersion] = React.useState(0);
160
+ const [consoleLogs, setConsoleLogs] = React.useState([]);
161
+ const logIdRef = React.useRef(0);
162
+ const refreshWorld = React.useCallback(() => setWorldVersion(v => v + 1), []);
163
+ const addLog = React.useCallback((level, message) => {
164
+ setConsoleLogs(prev => [...prev.slice(-199), { id: logIdRef.current++, level, message, timestamp: Date.now() }]);
165
+ }, []);
166
+ const createNewEntity = React.useCallback((name, components = ['Transform']) => {
167
+ const entity = world.createEntity(name);
168
+ for (const comp of components) {
169
+ world.addComponent(entity.id, comp);
170
+ }
171
+ dispatch({ type: 'SELECT', entities: [entity.id] });
172
+ dispatch({ type: 'MARK_DIRTY' });
173
+ refreshWorld();
174
+ addLog('info', `Created entity "${name}" (#${entity.id})`);
175
+ return entity.id;
176
+ }, [world, refreshWorld, addLog]);
159
177
  // Keyboard shortcuts
160
- const shortcuts = react.useMemo(() => [
178
+ const shortcuts = React.useMemo(() => [
161
179
  { key: 'z', ctrl: true, action: () => undoStack.undo(), description: 'Undo' },
162
180
  { key: 'z', ctrl: true, shift: true, action: () => undoStack.redo(), description: 'Redo' },
163
181
  { key: 'y', ctrl: true, action: () => undoStack.redo(), description: 'Redo' },
@@ -171,7 +189,7 @@ const NiceGameEditor = ({ project, onSave, className, style, }) => {
171
189
  { key: 'Escape', action: () => dispatch({ type: 'DESELECT_ALL' }), description: 'Deselect all' },
172
190
  ], []);
173
191
  useKeyboardShortcuts(shortcuts);
174
- const handleSave = react.useCallback(() => {
192
+ const handleSave = React.useCallback(() => {
175
193
  var _a;
176
194
  if (!onSave)
177
195
  return;
@@ -195,7 +213,7 @@ const NiceGameEditor = ({ project, onSave, className, style, }) => {
195
213
  onSave(proj);
196
214
  dispatch({ type: 'MARK_CLEAN' });
197
215
  }, [world, sceneManager, state.currentSceneId, project, onSave]);
198
- const deleteSelected = react.useCallback(() => {
216
+ const deleteSelected = React.useCallback(() => {
199
217
  const toDelete = [...state.selectedEntities];
200
218
  if (toDelete.length === 0)
201
219
  return;
@@ -209,6 +227,8 @@ const NiceGameEditor = ({ project, onSave, className, style, }) => {
209
227
  }
210
228
  dispatch({ type: 'DESELECT_ALL' });
211
229
  dispatch({ type: 'MARK_DIRTY' });
230
+ refreshWorld();
231
+ addLog('info', `Deleted ${toDelete.length} entity(ies)`);
212
232
  undoStack.push({
213
233
  description: `Delete ${toDelete.length} entity(ies)`,
214
234
  undo: () => {
@@ -221,14 +241,19 @@ const NiceGameEditor = ({ project, onSave, className, style, }) => {
221
241
  },
222
242
  });
223
243
  }, [state.selectedEntities, world, undoStack]);
224
- const ctxValue = react.useMemo(() => ({
244
+ const ctxValue = React.useMemo(() => ({
225
245
  state,
226
246
  dispatch,
227
247
  world,
228
248
  undoStack,
229
249
  sceneManager,
230
250
  events,
231
- }), [state, world, undoStack, sceneManager, events]);
251
+ worldVersion,
252
+ refreshWorld,
253
+ createNewEntity,
254
+ consoleLogs,
255
+ addLog,
256
+ }), [state, world, undoStack, sceneManager, events, worldVersion, refreshWorld, createNewEntity, consoleLogs, addLog]);
232
257
  return (jsxRuntime.jsx(EditorContext.Provider, { value: ctxValue, children: jsxRuntime.jsxs("div", { className: `nice-game-editor ${className !== null && className !== void 0 ? className : ''}`, style: {
233
258
  display: 'flex',
234
259
  flexDirection: 'column',
@@ -242,20 +267,86 @@ const NiceGameEditor = ({ project, onSave, className, style, }) => {
242
267
  ...style,
243
268
  }, children: [jsxRuntime.jsx(EditorToolbar, {}), jsxRuntime.jsxs("div", { style: { display: 'flex', flex: 1, overflow: 'hidden' }, children: [((_a = state.panels.find(p => p.id === 'hierarchy')) === null || _a === void 0 ? void 0 : _a.visible) && (jsxRuntime.jsx(EditorPanelContainer, { title: "Hierarchy", width: 250, position: "left", children: jsxRuntime.jsx(HierarchyPanel, {}) })), jsxRuntime.jsx("div", { style: { flex: 1, position: 'relative', overflow: 'hidden' }, children: jsxRuntime.jsx(EditorViewport, {}) }), ((_b = state.panels.find(p => p.id === 'inspector')) === null || _b === void 0 ? void 0 : _b.visible) && (jsxRuntime.jsx(EditorPanelContainer, { title: "Inspector", width: 300, position: "right", children: jsxRuntime.jsx(InspectorPanel, {}) }))] }), jsxRuntime.jsxs("div", { style: { display: 'flex', borderTop: '1px solid #45475a' }, children: [((_c = state.panels.find(p => p.id === 'assets')) === null || _c === void 0 ? void 0 : _c.visible) && (jsxRuntime.jsx(EditorPanelContainer, { title: "Assets", height: 180, position: "bottom", children: jsxRuntime.jsx(AssetPanel, {}) })), ((_d = state.panels.find(p => p.id === 'console')) === null || _d === void 0 ? void 0 : _d.visible) && (jsxRuntime.jsx(EditorPanelContainer, { title: "Console", height: 180, position: "bottom", children: jsxRuntime.jsx(ConsolePanel, {}) }))] })] }) }));
244
269
  };
245
- /* ── Sub-components (stubs for now, will be expanded) ─────────── */
270
+ /* ── Helpers ───────────────────────────────────────────────────── */
271
+ function getEntityBounds(entityId, world) {
272
+ const shape = world.getComponent(entityId, 'Shape');
273
+ if (shape) {
274
+ if (shape.shape === 'circle')
275
+ return { x: -shape.radius, y: -shape.radius, w: shape.radius * 2, h: shape.radius * 2 };
276
+ return { x: -shape.width / 2, y: -shape.height / 2, w: shape.width, h: shape.height };
277
+ }
278
+ const sprite = world.getComponent(entityId, 'Sprite');
279
+ if (sprite) {
280
+ return { x: -sprite.width * sprite.anchor.x, y: -sprite.height * sprite.anchor.y, w: sprite.width, h: sprite.height };
281
+ }
282
+ const text = world.getComponent(entityId, 'Text');
283
+ if (text)
284
+ return { x: -30, y: -text.fontSize / 2, w: 60, h: text.fontSize };
285
+ return { x: -8, y: -8, w: 16, h: 16 };
286
+ }
287
+ function getEntityWorldBounds(entityId, world) {
288
+ const transform = world.getComponent(entityId, 'Transform');
289
+ if (!transform)
290
+ return null;
291
+ const b = getEntityBounds(entityId, world);
292
+ return { x: transform.position.x + b.x, y: transform.position.y + b.y, w: b.w, h: b.h };
293
+ }
294
+ function getCursorForTool(tool) {
295
+ switch (tool) {
296
+ case 'move': return 'move';
297
+ case 'rotate': return 'crosshair';
298
+ case 'scale': return 'nwse-resize';
299
+ case 'pan': return 'grab';
300
+ default: return 'default';
301
+ }
302
+ }
303
+ function colorToCSS(c) {
304
+ return `rgba(${Math.round(c.r * 255)},${Math.round(c.g * 255)},${Math.round(c.b * 255)},${c.a})`;
305
+ }
306
+ function cssToColor(hex) {
307
+ const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
308
+ if (!m)
309
+ return { r: 1, g: 1, b: 1, a: 1 };
310
+ return { r: parseInt(m[1], 16) / 255, g: parseInt(m[2], 16) / 255, b: parseInt(m[3], 16) / 255, a: 1 };
311
+ }
312
+ function colorToHex(c) {
313
+ const r = Math.round(c.r * 255).toString(16).padStart(2, '0');
314
+ const g = Math.round(c.g * 255).toString(16).padStart(2, '0');
315
+ const b = Math.round(c.b * 255).toString(16).padStart(2, '0');
316
+ return `#${r}${g}${b}`;
317
+ }
318
+ const ENTITY_PRESETS = [
319
+ { label: 'Empty', icon: '○', components: ['Transform'] },
320
+ { label: 'Sprite', icon: '🖼', components: ['Transform', 'Sprite'] },
321
+ { label: 'Shape', icon: '■', components: ['Transform', 'Shape'] },
322
+ { label: 'Text', icon: 'T', components: ['Transform', 'Text'] },
323
+ { label: 'Physics Body', icon: '⚡', components: ['Transform', 'Shape', 'Collider2D', 'RigidBody2D'] },
324
+ { label: 'Camera', icon: '📷', components: ['Transform', 'CameraTarget'] },
325
+ ];
326
+ /* ── Sub-components ───────────────────────────────────────────── */
246
327
  const EditorToolbar = () => {
247
- const { state, dispatch } = useEditor();
328
+ const { state, dispatch, createNewEntity, undoStack } = useEditor();
329
+ const [showAddMenu, setShowAddMenu] = React.useState(false);
248
330
  const tools = ['select', 'move', 'rotate', 'scale', 'pan'];
249
331
  const toolIcons = { select: '⊕', move: '✥', rotate: '↻', scale: '⇲', pan: '✋' };
332
+ const btnStyle = (active) => ({
333
+ background: active ? '#585b70' : 'transparent',
334
+ border: 'none', color: '#cdd6f4', padding: '4px 8px',
335
+ borderRadius: 4, cursor: 'pointer', fontSize: 14,
336
+ });
250
337
  return (jsxRuntime.jsxs("div", { style: {
251
338
  display: 'flex', alignItems: 'center', gap: 4,
252
339
  padding: '4px 8px', borderBottom: '1px solid #45475a',
253
340
  backgroundColor: '#181825',
254
- }, children: [tools.map(t => (jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'SET_TOOL', tool: t }), style: {
255
- background: state.tool === t ? '#585b70' : 'transparent',
256
- border: 'none', color: '#cdd6f4', padding: '4px 8px',
257
- borderRadius: 4, cursor: 'pointer', fontSize: 14,
258
- }, title: t, children: toolIcons[t] }, t))), jsxRuntime.jsx("div", { style: { flex: 1 } }), jsxRuntime.jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: [state.mode.toUpperCase(), " | ", state.tool, " | Zoom: ", (state.zoom * 100) | 0, "%", state.dirty ? ' •' : ''] })] }));
341
+ }, children: [tools.map(t => (jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'SET_TOOL', tool: t }), style: btnStyle(state.tool === t), title: `${t} (${t[0].toUpperCase()})`, children: toolIcons[t] }, t))), jsxRuntime.jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsxRuntime.jsxs("div", { style: { position: 'relative' }, children: [jsxRuntime.jsx("button", { onClick: () => setShowAddMenu(!showAddMenu), style: { ...btnStyle(false), fontSize: 12, padding: '4px 10px', background: '#2f9e44', color: '#fff' }, children: "+ Entity" }), showAddMenu && (jsxRuntime.jsx("div", { style: {
342
+ position: 'absolute', top: '100%', left: 0, zIndex: 100,
343
+ background: '#1e1e2e', border: '1px solid #45475a', borderRadius: 4,
344
+ padding: 4, minWidth: 150, marginTop: 2,
345
+ }, children: ENTITY_PRESETS.map(p => (jsxRuntime.jsxs("button", { onClick: () => { createNewEntity(p.label, p.components); setShowAddMenu(false); }, style: {
346
+ display: 'block', width: '100%', textAlign: 'left',
347
+ background: 'transparent', border: 'none', color: '#cdd6f4',
348
+ padding: '4px 8px', cursor: 'pointer', borderRadius: 2, fontSize: 12,
349
+ }, children: [p.icon, " ", p.label] }, p.label))) }))] }), jsxRuntime.jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'SET_MODE', mode: state.mode === 'play' ? 'edit' : 'play' }), style: btnStyle(state.mode === 'play'), title: "Play/Stop", children: state.mode === 'play' ? '⏹' : '▶' }), jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'SET_MODE', mode: state.mode === 'pause' ? 'play' : 'pause' }), style: btnStyle(state.mode === 'pause'), title: "Pause", disabled: state.mode === 'edit', children: "\u23F8" }), jsxRuntime.jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsxRuntime.jsx("button", { onClick: () => undoStack.undo(), style: btnStyle(false), title: "Undo (Ctrl+Z)", disabled: !undoStack.canUndo, children: "\u21A9" }), jsxRuntime.jsx("button", { onClick: () => undoStack.redo(), style: btnStyle(false), title: "Redo (Ctrl+Y)", disabled: !undoStack.canRedo, children: "\u21AA" }), jsxRuntime.jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'SET_ZOOM', zoom: state.zoom * 0.8 }), style: btnStyle(false), title: "Zoom Out", children: "\u2212" }), jsxRuntime.jsxs("span", { style: { fontSize: 11, minWidth: 40, textAlign: 'center' }, children: [(state.zoom * 100) | 0, "%"] }), jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'SET_ZOOM', zoom: state.zoom * 1.25 }), style: btnStyle(false), title: "Zoom In", children: "+" }), jsxRuntime.jsx("button", { onClick: () => { dispatch({ type: 'SET_ZOOM', zoom: 1 }); dispatch({ type: 'SET_PAN', offset: { x: 0, y: 0 } }); }, style: btnStyle(false), title: "Reset View", children: "\u2302" }), jsxRuntime.jsx("div", { style: { width: 1, height: 20, background: '#45475a', margin: '0 4px' } }), jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'TOGGLE_GRID' }), style: btnStyle(state.showGrid), title: "Toggle Grid (Ctrl+G)", children: "\u25A6" }), jsxRuntime.jsx("button", { onClick: () => dispatch({ type: 'TOGGLE_SNAP' }), style: btnStyle(state.snapToGrid), title: "Toggle Snap", children: "\u229E" }), jsxRuntime.jsx("div", { style: { flex: 1 } }), jsxRuntime.jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: [state.mode.toUpperCase(), " | ", state.tool, " | ", state.dirty ? '● Unsaved' : '✓ Saved'] })] }));
259
350
  };
260
351
  const EditorPanelContainer = ({ title, width, height, position, children }) => {
261
352
  const isHorizontal = position === 'bottom';
@@ -274,16 +365,99 @@ const EditorPanelContainer = ({ title, width, height, position, children }) => {
274
365
  }, children: title }), jsxRuntime.jsx("div", { style: { flex: 1, overflow: 'auto', padding: 4 }, children: children })] }));
275
366
  };
276
367
  const HierarchyPanel = () => {
277
- const { world, state, dispatch } = useEditor();
368
+ const { world, state, dispatch, createNewEntity, refreshWorld, addLog } = useEditor();
278
369
  const entities = world.allEntities().filter(e => e.parent == null);
279
- return (jsxRuntime.jsx("div", { children: entities.map(e => (jsxRuntime.jsxs("div", { onClick: () => dispatch({ type: 'SELECT', entities: [e.id] }), style: {
280
- padding: '2px 6px', cursor: 'pointer',
281
- backgroundColor: state.selectedEntities.includes(e.id) ? '#45475a' : 'transparent',
282
- borderRadius: 2,
283
- }, children: [e.name, " ", jsxRuntime.jsxs("span", { style: { opacity: 0.4 }, children: ["#", e.id] })] }, e.id))) }));
370
+ const getIcon = (id) => {
371
+ if (world.hasComponent(id, 'CameraTarget'))
372
+ return '📷';
373
+ if (world.hasComponent(id, 'Sprite'))
374
+ return '🖼';
375
+ if (world.hasComponent(id, 'Text'))
376
+ return 'T';
377
+ if (world.hasComponent(id, 'Shape'))
378
+ return '■';
379
+ if (world.hasComponent(id, 'ParticleEmitter'))
380
+ return '✨';
381
+ if (world.hasComponent(id, 'AudioSource'))
382
+ return '🔊';
383
+ return '○';
384
+ };
385
+ const renderEntity = (e, depth) => {
386
+ const isSelected = state.selectedEntities.includes(e.id);
387
+ const children = e.children.map(cid => world.getEntity(cid)).filter(Boolean);
388
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsxs("div", { onClick: () => dispatch({ type: 'SELECT', entities: [e.id] }), style: {
389
+ padding: '2px 6px', paddingLeft: 6 + depth * 16, cursor: 'pointer',
390
+ backgroundColor: isSelected ? '#45475a' : 'transparent',
391
+ borderRadius: 2, display: 'flex', alignItems: 'center', gap: 4, fontSize: 12,
392
+ }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, width: 14, textAlign: 'center' }, children: getIcon(e.id) }), jsxRuntime.jsx("span", { style: { flex: 1 }, children: e.name }), jsxRuntime.jsxs("span", { style: { opacity: 0.3, fontSize: 10 }, children: ["#", e.id] })] }), children.map(c => renderEntity(c, depth + 1))] }, e.id));
393
+ };
394
+ return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { style: { padding: '2px 6px', fontSize: 10, display: 'flex', gap: 4, borderBottom: '1px solid #313244', marginBottom: 4 }, children: [jsxRuntime.jsx("button", { onClick: () => createNewEntity('Entity'), style: { background: 'transparent', border: 'none', color: '#a6adc8', cursor: 'pointer', fontSize: 10 }, title: "Add Entity", children: "+" }), jsxRuntime.jsx("button", { onClick: () => {
395
+ if (state.selectedEntities.length > 0) {
396
+ const id = state.selectedEntities[0];
397
+ const child = world.createEntity('Child');
398
+ world.addComponent(child.id, 'Transform');
399
+ world.setParent(child.id, id);
400
+ dispatch({ type: 'SELECT', entities: [child.id] });
401
+ dispatch({ type: 'MARK_DIRTY' });
402
+ refreshWorld();
403
+ addLog('info', `Created child entity (#${child.id})`);
404
+ }
405
+ }, style: { background: 'transparent', border: 'none', color: '#a6adc8', cursor: 'pointer', fontSize: 10 }, title: "Add Child", disabled: state.selectedEntities.length === 0, children: "\u21B3+" }), jsxRuntime.jsx("span", { style: { flex: 1 } }), jsxRuntime.jsxs("span", { style: { color: '#6c7086' }, children: [world.entityCount, " entities"] })] }), entities.map(e => renderEntity(e, 0))] }));
406
+ };
407
+ /* ── Inspector sub-editors ─────────────────────────────────────── */
408
+ const fieldStyle = {
409
+ width: '100%', background: '#313244', color: '#cdd6f4', border: '1px solid #45475a',
410
+ borderRadius: 3, padding: '2px 4px', fontSize: 11,
284
411
  };
412
+ const NumField = ({ label, value, onChange, step = 1 }) => (jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: label }), jsxRuntime.jsx("input", { type: "number", value: Math.round(value * 1000) / 1000, step: step, onChange: e => onChange(Number(e.target.value)), style: { ...fieldStyle, flex: 1 } })] }));
413
+ const ColField = ({ label, color, onChange }) => (jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: label }), jsxRuntime.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' } }), jsxRuntime.jsx("span", { style: { fontSize: 9, color: '#6c7086' }, children: colorToHex(color) })] }));
414
+ const TransformEditor = ({ entityId }) => {
415
+ const { world, refreshWorld, dispatch } = useEditor();
416
+ const t = world.getComponent(entityId, 'Transform');
417
+ if (!t)
418
+ return null;
419
+ const update = (fn) => { fn(); dispatch({ type: 'MARK_DIRTY' }); refreshWorld(); };
420
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(NumField, { label: "Pos X", value: t.position.x, onChange: v => update(() => { t.position.x = v; }) }), jsxRuntime.jsx(NumField, { label: "Pos Y", value: t.position.y, onChange: v => update(() => { t.position.y = v; }) }), jsxRuntime.jsx(NumField, { label: "Rotation", value: t.rotation * (180 / Math.PI), step: 5, onChange: v => update(() => { t.rotation = v * (Math.PI / 180); }) }), jsxRuntime.jsx(NumField, { label: "Scale X", value: t.scale.x, step: 0.1, onChange: v => update(() => { t.scale.x = v; }) }), jsxRuntime.jsx(NumField, { label: "Scale Y", value: t.scale.y, step: 0.1, onChange: v => update(() => { t.scale.y = v; }) })] }));
421
+ };
422
+ const ShapeEditor = ({ entityId }) => {
423
+ const { world, refreshWorld, dispatch } = useEditor();
424
+ const s = world.getComponent(entityId, 'Shape');
425
+ if (!s)
426
+ return null;
427
+ const update = (fn) => { fn(); dispatch({ type: 'MARK_DIRTY' }); refreshWorld(); };
428
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: "Shape" }), jsxRuntime.jsxs("select", { value: s.shape, onChange: e => update(() => { s.shape = e.target.value; }), style: { ...fieldStyle, flex: 1 }, children: [jsxRuntime.jsx("option", { value: "rect", children: "Rectangle" }), jsxRuntime.jsx("option", { value: "circle", children: "Circle" }), jsxRuntime.jsx("option", { value: "polygon", children: "Polygon" }), jsxRuntime.jsx("option", { value: "line", children: "Line" })] })] }), s.shape !== 'circle' && jsxRuntime.jsx(NumField, { label: "Width", value: s.width, onChange: v => update(() => { s.width = v; }) }), s.shape !== 'circle' && jsxRuntime.jsx(NumField, { label: "Height", value: s.height, onChange: v => update(() => { s.height = v; }) }), s.shape === 'circle' && jsxRuntime.jsx(NumField, { label: "Radius", value: s.radius, onChange: v => update(() => { s.radius = v; }) }), jsxRuntime.jsx(ColField, { label: "Fill", color: s.fillColor, onChange: c => update(() => { Object.assign(s.fillColor, c); }) }), jsxRuntime.jsx(ColField, { label: "Stroke", color: s.strokeColor, onChange: c => update(() => { Object.assign(s.strokeColor, c); }) }), jsxRuntime.jsx(NumField, { label: "Stroke W", value: s.strokeWidth, onChange: v => update(() => { s.strokeWidth = v; }) })] }));
429
+ };
430
+ const SpriteEditor = ({ entityId }) => {
431
+ const { world, refreshWorld, dispatch } = useEditor();
432
+ const s = world.getComponent(entityId, 'Sprite');
433
+ if (!s)
434
+ return null;
435
+ const update = (fn) => { fn(); dispatch({ type: 'MARK_DIRTY' }); refreshWorld(); };
436
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(NumField, { label: "Width", value: s.width, onChange: v => update(() => { s.width = v; }) }), jsxRuntime.jsx(NumField, { label: "Height", value: s.height, onChange: v => update(() => { s.height = v; }) }), jsxRuntime.jsx(ColField, { label: "Tint", color: s.tint, onChange: c => update(() => { Object.assign(s.tint, c); }) }), jsxRuntime.jsx(NumField, { label: "Opacity", value: s.opacity, step: 0.1, onChange: v => update(() => { s.opacity = Math.max(0, Math.min(1, v)); }) }), jsxRuntime.jsx(NumField, { label: "Layer", value: s.layer, step: 1, onChange: v => update(() => { s.layer = v; }) }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: "Visible" }), jsxRuntime.jsx("input", { type: "checkbox", checked: s.visible, onChange: e => update(() => { s.visible = e.target.checked; }) })] })] }));
437
+ };
438
+ const TextEditor = ({ entityId }) => {
439
+ const { world, refreshWorld, dispatch } = useEditor();
440
+ const t = world.getComponent(entityId, 'Text');
441
+ if (!t)
442
+ return null;
443
+ const update = (fn) => { fn(); dispatch({ type: 'MARK_DIRTY' }); refreshWorld(); };
444
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { style: { marginBottom: 2 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, color: '#a6adc8' }, children: "Text" }), jsxRuntime.jsx("input", { type: "text", value: t.text, onChange: e => update(() => { t.text = e.target.value; }), style: { ...fieldStyle, width: '100%' } })] }), jsxRuntime.jsx(NumField, { label: "Font Size", value: t.fontSize, onChange: v => update(() => { t.fontSize = v; }) }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: "Align" }), jsxRuntime.jsxs("select", { value: t.align, onChange: e => update(() => { t.align = e.target.value; }), style: { ...fieldStyle, flex: 1 }, children: [jsxRuntime.jsx("option", { value: "left", children: "Left" }), jsxRuntime.jsx("option", { value: "center", children: "Center" }), jsxRuntime.jsx("option", { value: "right", children: "Right" })] })] }), jsxRuntime.jsx(ColField, { label: "Color", color: t.color, onChange: c => update(() => { Object.assign(t.color, c); }) })] }));
445
+ };
446
+ const RigidBodyEditor = ({ entityId }) => {
447
+ const { world, refreshWorld, dispatch } = useEditor();
448
+ const rb = world.getComponent(entityId, 'RigidBody2D');
449
+ if (!rb)
450
+ return null;
451
+ const update = (fn) => { fn(); dispatch({ type: 'MARK_DIRTY' }); refreshWorld(); };
452
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, minWidth: 50, color: '#a6adc8' }, children: "Body" }), jsxRuntime.jsxs("select", { value: rb.bodyType, onChange: e => update(() => { rb.bodyType = e.target.value; }), style: { ...fieldStyle, flex: 1 }, children: [jsxRuntime.jsx("option", { value: "dynamic", children: "Dynamic" }), jsxRuntime.jsx("option", { value: "static", children: "Static" }), jsxRuntime.jsx("option", { value: "kinematic", children: "Kinematic" })] })] }), jsxRuntime.jsx(NumField, { label: "Mass", value: rb.mass, step: 0.1, onChange: v => update(() => { rb.mass = v; }) }), jsxRuntime.jsx(NumField, { label: "Drag", value: rb.drag, step: 0.01, onChange: v => update(() => { rb.drag = v; }) }), jsxRuntime.jsx(NumField, { label: "Bounce", value: rb.restitution, step: 0.05, onChange: v => update(() => { rb.restitution = v; }) }), jsxRuntime.jsx(NumField, { label: "Friction", value: rb.friction, step: 0.05, onChange: v => update(() => { rb.friction = v; }) }), jsxRuntime.jsx(NumField, { label: "Gravity", value: rb.gravityScale, step: 0.1, onChange: v => update(() => { rb.gravityScale = v; }) })] }));
453
+ };
454
+ const ADDABLE_COMPONENTS = [
455
+ 'Transform', 'Sprite', 'Shape', 'Collider2D', 'RigidBody2D',
456
+ 'Text', 'Animator', 'Script', 'AudioSource', 'ParticleEmitter', 'CameraTarget',
457
+ ];
285
458
  const InspectorPanel = () => {
286
- const { state, world } = useEditor();
459
+ const { state, world, dispatch, refreshWorld, addLog } = useEditor();
460
+ const [showAddComp, setShowAddComp] = React.useState(false);
287
461
  if (state.selectedEntities.length === 0) {
288
462
  return jsxRuntime.jsx("div", { style: { opacity: 0.5, padding: 8 }, children: "No selection" });
289
463
  }
@@ -291,66 +465,390 @@ const InspectorPanel = () => {
291
465
  const entity = world.getEntity(entityId);
292
466
  if (!entity)
293
467
  return null;
294
- const compTypes = [
295
- 'Transform', 'Sprite', 'Shape', 'Collider2D', 'RigidBody2D',
296
- 'Text', 'Animator', 'Script', 'AudioSource', 'ParticleEmitter', 'CameraTarget',
297
- ];
298
- return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { style: { fontWeight: 'bold', padding: '4px 0' }, children: entity.name }), compTypes.map(type => {
299
- const comp = world.getComponent(entityId, type);
300
- if (!comp)
301
- return null;
302
- return (jsxRuntime.jsxs("div", { style: { marginTop: 8, borderTop: '1px solid #313244', paddingTop: 4 }, children: [jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 'bold', textTransform: 'uppercase' }, children: type }), jsxRuntime.jsx("pre", { style: { fontSize: 10, opacity: 0.7, whiteSpace: 'pre-wrap', margin: 0 }, children: JSON.stringify(comp, null, 2) })] }, type));
303
- })] }));
468
+ const compTypes = ADDABLE_COMPONENTS;
469
+ const presentComponents = compTypes.filter(type => world.hasComponent(entityId, type));
470
+ const componentEditors = {
471
+ Transform: TransformEditor,
472
+ Shape: ShapeEditor,
473
+ Sprite: SpriteEditor,
474
+ Text: TextEditor,
475
+ RigidBody2D: RigidBodyEditor,
476
+ };
477
+ return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { style: { padding: '4px 0', marginBottom: 4, borderBottom: '1px solid #313244' }, children: [jsxRuntime.jsx("input", { type: "text", value: entity.name, onChange: e => { entity.name = e.target.value; dispatch({ type: 'MARK_DIRTY' }); refreshWorld(); }, style: { ...fieldStyle, fontWeight: 'bold', fontSize: 13 } }), jsxRuntime.jsxs("span", { style: { fontSize: 9, color: '#6c7086' }, children: ["ID: ", entityId] })] }), presentComponents.map(type => {
478
+ const Editor = componentEditors[type];
479
+ return (jsxRuntime.jsxs("div", { style: { marginBottom: 8, borderTop: '1px solid #313244', paddingTop: 4 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 11, fontWeight: 'bold', textTransform: 'uppercase', color: '#89b4fa' }, children: type }), type !== 'Transform' && (jsxRuntime.jsx("button", { onClick: () => {
480
+ world.removeComponent(entityId, type);
481
+ dispatch({ type: 'MARK_DIRTY' });
482
+ refreshWorld();
483
+ addLog('info', `Removed ${type} from #${entityId}`);
484
+ }, style: { background: 'transparent', border: 'none', color: '#f38ba8', cursor: 'pointer', fontSize: 10 }, title: `Remove ${type}`, children: "\u2715" }))] }), Editor ? jsxRuntime.jsx(Editor, { entityId: entityId }) : (jsxRuntime.jsx("pre", { style: { fontSize: 10, opacity: 0.6, whiteSpace: 'pre-wrap', margin: 0 }, children: JSON.stringify(world.getComponent(entityId, type), null, 2) }))] }, type));
485
+ }), jsxRuntime.jsxs("div", { style: { marginTop: 8, position: 'relative' }, children: [jsxRuntime.jsx("button", { onClick: () => setShowAddComp(!showAddComp), style: { width: '100%', padding: '4px 8px', background: '#313244', border: '1px solid #45475a', color: '#cdd6f4', borderRadius: 4, cursor: 'pointer', fontSize: 11 }, children: "+ Add Component" }), showAddComp && (jsxRuntime.jsx("div", { style: {
486
+ position: 'absolute', bottom: '100%', left: 0, right: 0,
487
+ background: '#1e1e2e', border: '1px solid #45475a', borderRadius: 4,
488
+ padding: 4, marginBottom: 2, maxHeight: 200, overflow: 'auto', zIndex: 10,
489
+ }, children: compTypes.filter(t => !world.hasComponent(entityId, t)).map(type => (jsxRuntime.jsx("button", { onClick: () => {
490
+ world.addComponent(entityId, type);
491
+ dispatch({ type: 'MARK_DIRTY' });
492
+ refreshWorld();
493
+ addLog('info', `Added ${type} to #${entityId}`);
494
+ setShowAddComp(false);
495
+ }, style: {
496
+ display: 'block', width: '100%', textAlign: 'left',
497
+ background: 'transparent', border: 'none', color: '#cdd6f4',
498
+ padding: '3px 6px', cursor: 'pointer', borderRadius: 2, fontSize: 11,
499
+ }, children: type }, type))) }))] })] }));
304
500
  };
305
501
  const AssetPanel = () => {
306
- return jsxRuntime.jsx("div", { style: { opacity: 0.5, padding: 8 }, children: "Asset browser \u2014 drop files here" });
502
+ const [tab, setTab] = React.useState('all');
503
+ const tabBtnStyle = (active) => ({
504
+ padding: '2px 8px', border: 'none', borderRadius: '3px 3px 0 0', cursor: 'pointer', fontSize: 10,
505
+ background: active ? '#313244' : 'transparent', color: active ? '#cdd6f4' : '#6c7086',
506
+ });
507
+ const assets = [
508
+ { name: 'player.png', type: 'sprites', icon: '🖼' },
509
+ { name: 'enemy.png', type: 'sprites', icon: '🖼' },
510
+ { name: 'tileset.png', type: 'sprites', icon: '🖼' },
511
+ { name: 'background.jpg', type: 'sprites', icon: '🖼' },
512
+ { name: 'jump.wav', type: 'audio', icon: '🔊' },
513
+ { name: 'music.mp3', type: 'audio', icon: '🎵' },
514
+ { name: 'coin.wav', type: 'audio', icon: '🔊' },
515
+ { name: 'playerController.ts', type: 'scripts', icon: '📜' },
516
+ { name: 'enemyAI.ts', type: 'scripts', icon: '📜' },
517
+ ];
518
+ const filtered = tab === 'all' ? assets : assets.filter(a => a.type === tab);
519
+ return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%' }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 2, padding: '2px 4px', borderBottom: '1px solid #313244' }, children: [['all', 'sprites', 'audio', 'scripts'].map(t => (jsxRuntime.jsx("button", { onClick: () => setTab(t), style: tabBtnStyle(tab === t), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))), jsxRuntime.jsx("span", { style: { flex: 1 } }), jsxRuntime.jsx("button", { style: { background: 'transparent', border: 'none', color: '#a6adc8', cursor: 'pointer', fontSize: 10 }, title: "Import Asset", children: "+ Import" })] }), jsxRuntime.jsx("div", { style: { flex: 1, overflow: 'auto', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, 80px)', gap: 4, padding: 4 }, children: filtered.map(a => (jsxRuntime.jsxs("div", { draggable: true, style: {
520
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
521
+ padding: 4, background: '#313244', borderRadius: 4, cursor: 'grab',
522
+ fontSize: 9, textAlign: 'center',
523
+ }, children: [jsxRuntime.jsx("span", { style: { fontSize: 20 }, children: a.icon }), jsxRuntime.jsx("span", { style: { wordBreak: 'break-all' }, children: a.name })] }, a.name))) })] }));
307
524
  };
308
525
  const ConsolePanel = () => {
309
- return jsxRuntime.jsx("div", { style: { opacity: 0.5, padding: 8, fontFamily: 'monospace', fontSize: 11 }, children: "Console ready." });
526
+ const { consoleLogs } = useEditor();
527
+ const [filter, setFilter] = React.useState('all');
528
+ const scrollRef = React.useRef(null);
529
+ React.useEffect(() => {
530
+ if (scrollRef.current)
531
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
532
+ }, [consoleLogs.length]);
533
+ const filtered = filter === 'all' ? consoleLogs : consoleLogs.filter(l => l.level === filter);
534
+ const levelColors = { info: '#89b4fa', warn: '#f9e2af', error: '#f38ba8' };
535
+ const filterBtn = (f, label) => (jsxRuntime.jsx("button", { onClick: () => setFilter(f), style: { padding: '1px 6px', border: 'none', borderRadius: 2, cursor: 'pointer', fontSize: 9,
536
+ background: filter === f ? '#313244' : 'transparent', color: filter === f ? '#cdd6f4' : '#6c7086' }, children: label }));
537
+ return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', fontFamily: 'monospace' }, children: [jsxRuntime.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'), jsxRuntime.jsx("span", { style: { flex: 1 } }), jsxRuntime.jsxs("span", { style: { fontSize: 9, color: '#6c7086' }, children: [filtered.length, " entries"] })] }), jsxRuntime.jsx("div", { ref: scrollRef, style: { flex: 1, overflow: 'auto', fontSize: 10 }, children: filtered.length === 0 ? (jsxRuntime.jsx("div", { style: { opacity: 0.4, padding: 8 }, children: "Console ready." })) : filtered.map(log => (jsxRuntime.jsxs("div", { style: { padding: '1px 4px', borderBottom: '1px solid #181825', display: 'flex', gap: 6 }, children: [jsxRuntime.jsx("span", { style: { color: '#6c7086', minWidth: 55 }, children: new Date(log.timestamp).toLocaleTimeString([], { hour12: false }) }), jsxRuntime.jsxs("span", { style: { color: levelColors[log.level], fontWeight: 'bold', minWidth: 35 }, children: ["[", log.level.toUpperCase(), "]"] }), jsxRuntime.jsx("span", { children: log.message })] }, log.id))) })] }));
310
538
  };
311
539
  const EditorViewport = () => {
312
- const canvasRef = react.useRef(null);
313
- const { state } = useEditor();
314
- react.useEffect(() => {
540
+ const canvasRef = React.useRef(null);
541
+ const { state, dispatch, world, worldVersion, refreshWorld, undoStack } = useEditor();
542
+ const dragging = React.useRef(false);
543
+ const dragStart = React.useRef({ x: 0, y: 0 });
544
+ const dragEntityStart = React.useRef({ x: 0, y: 0 });
545
+ const panning = React.useRef(false);
546
+ const panStart = React.useRef({ x: 0, y: 0, px: 0, py: 0 });
547
+ // Draw loop
548
+ React.useEffect(() => {
315
549
  const canvas = canvasRef.current;
316
550
  if (!canvas)
317
551
  return;
318
552
  const ctx = canvas.getContext('2d');
319
553
  if (!ctx)
320
554
  return;
321
- const draw = () => {
322
- const { width, height } = canvas.getBoundingClientRect();
323
- canvas.width = width;
324
- canvas.height = height;
325
- ctx.fillStyle = '#11111b';
326
- ctx.fillRect(0, 0, width, height);
327
- // Grid
328
- if (state.showGrid) {
329
- const gs = state.gridSize * state.zoom;
330
- ctx.strokeStyle = 'rgba(69, 71, 90, 0.3)';
331
- ctx.lineWidth = 0.5;
332
- const ox = state.panOffset.x % gs;
333
- const oy = state.panOffset.y % gs;
334
- for (let x = ox; x < width; x += gs) {
335
- ctx.beginPath();
336
- ctx.moveTo(x, 0);
337
- ctx.lineTo(x, height);
338
- ctx.stroke();
555
+ const { width, height } = canvas.getBoundingClientRect();
556
+ canvas.width = width;
557
+ canvas.height = height;
558
+ ctx.fillStyle = '#11111b';
559
+ ctx.fillRect(0, 0, width, height);
560
+ ctx.save();
561
+ ctx.translate(width / 2 + state.panOffset.x, height / 2 + state.panOffset.y);
562
+ ctx.scale(state.zoom, state.zoom);
563
+ // Grid
564
+ if (state.showGrid) {
565
+ const gs = state.gridSize;
566
+ const halfW = (width / 2) / state.zoom;
567
+ const halfH = (height / 2) / state.zoom;
568
+ const offX = -state.panOffset.x / state.zoom;
569
+ const offY = -state.panOffset.y / state.zoom;
570
+ const startX = Math.floor((offX - halfW) / gs) * gs;
571
+ const endX = Math.ceil((offX + halfW) / gs) * gs;
572
+ const startY = Math.floor((offY - halfH) / gs) * gs;
573
+ const endY = Math.ceil((offY + halfH) / gs) * gs;
574
+ ctx.strokeStyle = 'rgba(69, 71, 90, 0.3)';
575
+ ctx.lineWidth = 0.5 / state.zoom;
576
+ for (let x = startX; x <= endX; x += gs) {
577
+ ctx.beginPath();
578
+ ctx.moveTo(x, startY);
579
+ ctx.lineTo(x, endY);
580
+ ctx.stroke();
581
+ }
582
+ for (let y = startY; y <= endY; y += gs) {
583
+ ctx.beginPath();
584
+ ctx.moveTo(startX, y);
585
+ ctx.lineTo(endX, y);
586
+ ctx.stroke();
587
+ }
588
+ // Origin axes
589
+ ctx.strokeStyle = 'rgba(205, 214, 244, 0.15)';
590
+ ctx.lineWidth = 1 / state.zoom;
591
+ ctx.beginPath();
592
+ ctx.moveTo(0, startY);
593
+ ctx.lineTo(0, endY);
594
+ ctx.stroke();
595
+ ctx.beginPath();
596
+ ctx.moveTo(startX, 0);
597
+ ctx.lineTo(endX, 0);
598
+ ctx.stroke();
599
+ }
600
+ // Render entities
601
+ const entities = world.allEntities();
602
+ for (const entity of entities) {
603
+ if (!entity.enabled)
604
+ continue;
605
+ const transform = world.getComponent(entity.id, 'Transform');
606
+ if (!transform)
607
+ continue;
608
+ ctx.save();
609
+ ctx.translate(transform.position.x, transform.position.y);
610
+ ctx.rotate(transform.rotation);
611
+ ctx.scale(transform.scale.x, transform.scale.y);
612
+ const isSelected = state.selectedEntities.includes(entity.id);
613
+ // Shape
614
+ const shape = world.getComponent(entity.id, 'Shape');
615
+ if (shape && shape.visible) {
616
+ ctx.fillStyle = colorToCSS(shape.fillColor);
617
+ ctx.strokeStyle = colorToCSS(shape.strokeColor);
618
+ ctx.lineWidth = shape.strokeWidth;
619
+ switch (shape.shape) {
620
+ case 'rect':
621
+ ctx.fillRect(-shape.width / 2, -shape.height / 2, shape.width, shape.height);
622
+ if (shape.strokeWidth > 0)
623
+ ctx.strokeRect(-shape.width / 2, -shape.height / 2, shape.width, shape.height);
624
+ break;
625
+ case 'circle':
626
+ ctx.beginPath();
627
+ ctx.arc(0, 0, shape.radius, 0, Math.PI * 2);
628
+ ctx.fill();
629
+ if (shape.strokeWidth > 0)
630
+ ctx.stroke();
631
+ break;
632
+ case 'line':
633
+ ctx.beginPath();
634
+ ctx.moveTo(shape.lineStart.x, shape.lineStart.y);
635
+ ctx.lineTo(shape.lineEnd.x, shape.lineEnd.y);
636
+ ctx.stroke();
637
+ break;
638
+ case 'polygon':
639
+ if (shape.points.length > 2) {
640
+ ctx.beginPath();
641
+ ctx.moveTo(shape.points[0].x, shape.points[0].y);
642
+ for (let i = 1; i < shape.points.length; i++)
643
+ ctx.lineTo(shape.points[i].x, shape.points[i].y);
644
+ ctx.closePath();
645
+ ctx.fill();
646
+ if (shape.strokeWidth > 0)
647
+ ctx.stroke();
648
+ }
649
+ break;
339
650
  }
340
- for (let y = oy; y < height; y += gs) {
651
+ }
652
+ // Sprite
653
+ const sprite = world.getComponent(entity.id, 'Sprite');
654
+ if (sprite && sprite.visible) {
655
+ ctx.globalAlpha = sprite.opacity;
656
+ ctx.fillStyle = colorToCSS(sprite.tint);
657
+ const ax = -sprite.width * sprite.anchor.x;
658
+ const ay = -sprite.height * sprite.anchor.y;
659
+ ctx.fillRect(ax, ay, sprite.width, sprite.height);
660
+ // Sprite border
661
+ ctx.strokeStyle = 'rgba(255,255,255,0.2)';
662
+ ctx.lineWidth = 1 / state.zoom;
663
+ ctx.strokeRect(ax, ay, sprite.width, sprite.height);
664
+ // Sprite label
665
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
666
+ ctx.font = `${9 / state.zoom}px monospace`;
667
+ ctx.textAlign = 'center';
668
+ ctx.textBaseline = 'middle';
669
+ ctx.fillText(sprite.assetId || 'sprite', 0, 0);
670
+ ctx.globalAlpha = 1;
671
+ }
672
+ // Text
673
+ const text = world.getComponent(entity.id, 'Text');
674
+ if (text && text.visible && text.text) {
675
+ ctx.font = `${text.fontSize}px ${text.fontFamily}`;
676
+ ctx.fillStyle = colorToCSS(text.color);
677
+ ctx.textAlign = text.align;
678
+ ctx.textBaseline = text.baseline;
679
+ ctx.fillText(text.text, 0, 0);
680
+ }
681
+ // Particle emitter indicator
682
+ const particles = world.getComponent(entity.id, 'ParticleEmitter');
683
+ if (particles) {
684
+ ctx.strokeStyle = '#f9e2af';
685
+ ctx.lineWidth = 1 / state.zoom;
686
+ ctx.setLineDash([3 / state.zoom, 3 / state.zoom]);
687
+ ctx.beginPath();
688
+ ctx.arc(0, 0, 12, 0, Math.PI * 2);
689
+ ctx.stroke();
690
+ ctx.setLineDash([]);
691
+ ctx.fillStyle = '#f9e2af';
692
+ ctx.font = `${8 / state.zoom}px monospace`;
693
+ ctx.textAlign = 'center';
694
+ ctx.fillText('✨', 0, 3 / state.zoom);
695
+ }
696
+ // Camera indicator
697
+ const cam = world.getComponent(entity.id, 'CameraTarget');
698
+ if (cam) {
699
+ ctx.strokeStyle = '#a6e3a1';
700
+ ctx.lineWidth = 1.5 / state.zoom;
701
+ const hw = 60, hh = 34;
702
+ ctx.strokeRect(-hw, -hh, hw * 2, hh * 2);
703
+ ctx.font = `${8 / state.zoom}px monospace`;
704
+ ctx.fillStyle = '#a6e3a1';
705
+ ctx.textAlign = 'center';
706
+ ctx.fillText('📷 Camera', 0, -hh - 4 / state.zoom);
707
+ }
708
+ // Selection highlight
709
+ if (isSelected) {
710
+ const b = getEntityBounds(entity.id, world);
711
+ ctx.strokeStyle = '#89b4fa';
712
+ ctx.lineWidth = 2 / state.zoom;
713
+ ctx.setLineDash([4 / state.zoom, 4 / state.zoom]);
714
+ ctx.strokeRect(b.x - 2, b.y - 2, b.w + 4, b.h + 4);
715
+ ctx.setLineDash([]);
716
+ // Corner handles
717
+ const hs = 4 / state.zoom;
718
+ ctx.fillStyle = '#89b4fa';
719
+ ctx.fillRect(b.x - hs, b.y - hs, hs * 2, hs * 2);
720
+ ctx.fillRect(b.x + b.w - hs, b.y - hs, hs * 2, hs * 2);
721
+ ctx.fillRect(b.x - hs, b.y + b.h - hs, hs * 2, hs * 2);
722
+ ctx.fillRect(b.x + b.w - hs, b.y + b.h - hs, hs * 2, hs * 2);
723
+ }
724
+ // Entity name label (only if no visible shape/sprite/text OR selected)
725
+ if (isSelected || (!(shape === null || shape === void 0 ? void 0 : shape.visible) && !(sprite === null || sprite === void 0 ? void 0 : sprite.visible) && !(text === null || text === void 0 ? void 0 : text.visible))) {
726
+ ctx.font = `${10 / state.zoom}px monospace`;
727
+ ctx.fillStyle = isSelected ? '#89b4fa' : 'rgba(205, 214, 244, 0.4)';
728
+ ctx.textAlign = 'center';
729
+ ctx.textBaseline = 'bottom';
730
+ const b = getEntityBounds(entity.id, world);
731
+ ctx.fillText(entity.name, 0, b.y - 4 / state.zoom);
732
+ // Draw small dot for invisible/empty entities
733
+ if (!(shape === null || shape === void 0 ? void 0 : shape.visible) && !(sprite === null || sprite === void 0 ? void 0 : sprite.visible) && !(text === null || text === void 0 ? void 0 : text.visible)) {
734
+ ctx.fillStyle = isSelected ? '#89b4fa' : 'rgba(205, 214, 244, 0.3)';
341
735
  ctx.beginPath();
342
- ctx.moveTo(0, y);
343
- ctx.lineTo(width, y);
344
- ctx.stroke();
736
+ ctx.arc(0, 0, 3 / state.zoom, 0, Math.PI * 2);
737
+ ctx.fill();
345
738
  }
346
739
  }
347
- ctx.fillStyle = '#cdd6f4';
348
- ctx.font = '12px monospace';
349
- ctx.fillText(`Viewport — ${state.mode}`, 8, 16);
740
+ ctx.restore();
741
+ }
742
+ ctx.restore();
743
+ // HUD
744
+ ctx.fillStyle = '#6c7086';
745
+ ctx.font = '10px monospace';
746
+ ctx.textAlign = 'left';
747
+ ctx.textBaseline = 'top';
748
+ ctx.fillText(`Entities: ${world.entityCount} | Grid: ${state.gridSize}px`, 8, 8);
749
+ }, [state, worldVersion, world]);
750
+ // Mouse handlers
751
+ const screenToWorld = React.useCallback((e) => {
752
+ const canvas = canvasRef.current;
753
+ const rect = canvas.getBoundingClientRect();
754
+ const sx = e.clientX - rect.left;
755
+ const sy = e.clientY - rect.top;
756
+ return {
757
+ x: (sx - canvas.width / 2 - state.panOffset.x) / state.zoom,
758
+ y: (sy - canvas.height / 2 - state.panOffset.y) / state.zoom,
350
759
  };
351
- draw();
352
- }, [state]);
353
- return (jsxRuntime.jsx("canvas", { ref: canvasRef, style: { width: '100%', height: '100%', display: 'block' } }));
760
+ }, [state.zoom, state.panOffset]);
761
+ const handlePointerDown = React.useCallback((e) => {
762
+ // Middle click or pan tool = pan
763
+ if (e.button === 1 || (e.button === 0 && state.tool === 'pan')) {
764
+ panning.current = true;
765
+ panStart.current = { x: e.clientX, y: e.clientY, px: state.panOffset.x, py: state.panOffset.y };
766
+ e.target.setPointerCapture(e.pointerId);
767
+ return;
768
+ }
769
+ if (e.button !== 0)
770
+ return;
771
+ const { x: wx, y: wy } = screenToWorld(e);
772
+ // Hit test (reverse order for top-most)
773
+ const entities = world.allEntities();
774
+ let hitId = null;
775
+ for (let i = entities.length - 1; i >= 0; i--) {
776
+ const ent = entities[i];
777
+ if (!ent.enabled)
778
+ continue;
779
+ const bounds = getEntityWorldBounds(ent.id, world);
780
+ if (bounds && wx >= bounds.x && wx <= bounds.x + bounds.w && wy >= bounds.y && wy <= bounds.y + bounds.h) {
781
+ hitId = ent.id;
782
+ break;
783
+ }
784
+ }
785
+ if (hitId !== null) {
786
+ dispatch({ type: 'SELECT', entities: [hitId] });
787
+ if (state.tool === 'select' || state.tool === 'move') {
788
+ dragging.current = true;
789
+ dragStart.current = { x: wx, y: wy };
790
+ const t = world.getComponent(hitId, 'Transform');
791
+ if (t)
792
+ dragEntityStart.current = { x: t.position.x, y: t.position.y };
793
+ }
794
+ }
795
+ else {
796
+ dispatch({ type: 'DESELECT_ALL' });
797
+ }
798
+ e.target.setPointerCapture(e.pointerId);
799
+ }, [state.tool, state.panOffset, screenToWorld, world, dispatch]);
800
+ const handlePointerMove = React.useCallback((e) => {
801
+ if (panning.current) {
802
+ const dx = e.clientX - panStart.current.x;
803
+ const dy = e.clientY - panStart.current.y;
804
+ dispatch({ type: 'SET_PAN', offset: { x: panStart.current.px + dx, y: panStart.current.py + dy } });
805
+ return;
806
+ }
807
+ if (dragging.current && state.selectedEntities.length > 0) {
808
+ const { x: wx, y: wy } = screenToWorld(e);
809
+ const dx = wx - dragStart.current.x;
810
+ const dy = wy - dragStart.current.y;
811
+ const entityId = state.selectedEntities[0];
812
+ const t = world.getComponent(entityId, 'Transform');
813
+ if (t) {
814
+ let nx = dragEntityStart.current.x + dx;
815
+ let ny = dragEntityStart.current.y + dy;
816
+ if (state.snapToGrid) {
817
+ nx = Math.round(nx / state.gridSize) * state.gridSize;
818
+ ny = Math.round(ny / state.gridSize) * state.gridSize;
819
+ }
820
+ t.position.x = nx;
821
+ t.position.y = ny;
822
+ dispatch({ type: 'MARK_DIRTY' });
823
+ refreshWorld();
824
+ }
825
+ }
826
+ }, [state.selectedEntities, state.snapToGrid, state.gridSize, screenToWorld, world, dispatch, refreshWorld]);
827
+ const handlePointerUp = React.useCallback(() => {
828
+ if (dragging.current && state.selectedEntities.length > 0) {
829
+ const entityId = state.selectedEntities[0];
830
+ const t = world.getComponent(entityId, 'Transform');
831
+ if (t) {
832
+ const finalPos = { x: t.position.x, y: t.position.y };
833
+ const startPos = { ...dragEntityStart.current };
834
+ if (finalPos.x !== startPos.x || finalPos.y !== startPos.y) {
835
+ undoStack.push({
836
+ description: 'Move entity',
837
+ undo: () => { t.position.x = startPos.x; t.position.y = startPos.y; refreshWorld(); },
838
+ redo: () => { t.position.x = finalPos.x; t.position.y = finalPos.y; refreshWorld(); },
839
+ });
840
+ }
841
+ }
842
+ }
843
+ dragging.current = false;
844
+ panning.current = false;
845
+ }, [state.selectedEntities, world, undoStack, refreshWorld]);
846
+ const handleWheel = React.useCallback((e) => {
847
+ e.preventDefault();
848
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
849
+ dispatch({ type: 'SET_ZOOM', zoom: state.zoom * factor });
850
+ }, [state.zoom, dispatch]);
851
+ return (jsxRuntime.jsx("canvas", { ref: canvasRef, style: { width: '100%', height: '100%', display: 'block', cursor: getCursorForTool(state.tool) }, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onWheel: handleWheel, onContextMenu: e => e.preventDefault() }));
354
852
  };
355
853
 
356
854
  exports.NiceGameEditor = NiceGameEditor;