dylanebert commited on
Commit
dd99b77
·
1 Parent(s): 9434357

improve agent

Browse files
bun.lock CHANGED
@@ -14,7 +14,7 @@
14
  "marked": "^16.2.1",
15
  "monaco-editor": "^0.50.0",
16
  "svelte-splitpanes": "^8.0.5",
17
- "vibegame": "^0.1.3",
18
  "zod": "^4.1.8",
19
  },
20
  "devDependencies": {
@@ -780,7 +780,7 @@
780
 
781
  "uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
782
 
783
- "vibegame": ["[email protected].3", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.18.2", "gsap": "^3.13.0", "zod": "^4.1.5" }, "peerDependencies": { "bitecs": ">=0.3.40", "three": ">=0.170.0" } }, "sha512-pUUK8yjHhb9UCgaBKZerKovn3rR+RD8AKoF8iFoYRIOhtDBXjGV+hLzJ2Z9QLpymCCjEuo+vDTdfatagARQ1wg=="],
784
 
785
  "vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="],
786
 
 
14
  "marked": "^16.2.1",
15
  "monaco-editor": "^0.50.0",
16
  "svelte-splitpanes": "^8.0.5",
17
+ "vibegame": "^0.1.4",
18
  "zod": "^4.1.8",
19
  },
20
  "devDependencies": {
 
780
 
781
  "uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
782
 
783
+ "vibegame": ["[email protected].4", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.18.2", "gsap": "^3.13.0", "zod": "^4.1.5" }, "peerDependencies": { "bitecs": ">=0.3.40", "three": ">=0.170.0" } }, "sha512-U8iZzedz/egganPKym2Kjc7ZG1YJpnZ2CuB1q+e+1H3tOPrH5rBrwUKK+5jfBtgdUkMVT451GIcXioF+YBsKHg=="],
784
 
785
  "vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="],
786
 
llms.txt CHANGED
@@ -101,9 +101,10 @@ const Health = GAME.defineComponent({
101
  });
102
 
103
  // System = Logic only
 
104
  const DamageSystem: GAME.System = {
105
  update: (state) => {
106
- const entities = GAME.defineQuery([Health])(state.world);
107
  for (const entity of entities) {
108
  Health.current[entity] -= 1 * state.time.delta;
109
  if (Health.current[entity] <= 0) {
 
101
  });
102
 
103
  // System = Logic only
104
+ const healthQuery = GAME.defineQuery([Health]);
105
  const DamageSystem: GAME.System = {
106
  update: (state) => {
107
+ const entities = healthQuery(state.world);
108
  for (const entity of entities) {
109
  Health.current[entity] -= 1 * state.time.delta;
110
  if (Health.current[entity] <= 0) {
package.json CHANGED
@@ -46,7 +46,7 @@
46
  "marked": "^16.2.1",
47
  "monaco-editor": "^0.50.0",
48
  "svelte-splitpanes": "^8.0.5",
49
- "vibegame": "^0.1.3",
50
  "zod": "^4.1.8"
51
  }
52
  }
 
46
  "marked": "^16.2.1",
47
  "monaco-editor": "^0.50.0",
48
  "svelte-splitpanes": "^8.0.5",
49
+ "vibegame": "^0.1.4",
50
  "zod": "^4.1.8"
51
  }
52
  }
src/App.svelte CHANGED
@@ -1,20 +1,16 @@
1
  <script lang="ts">
2
  import { onMount, onDestroy } from 'svelte';
3
- import { consoleCapture } from './lib/services/console-capture';
4
- import { consoleForwarder } from './lib/services/console-forward';
5
  import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
6
  import { loadingStore } from './lib/stores/loading';
7
  import AppHeader from './lib/components/layout/AppHeader.svelte';
8
  import SplitView from './lib/components/layout/SplitView.svelte';
9
  import LoadingScreen from './lib/components/layout/LoadingScreen.svelte';
10
-
11
  let unregisterShortcuts: () => void;
12
-
13
  onMount(() => {
14
  loadingStore.startLoading();
15
 
16
- consoleCapture.setup();
17
- consoleForwarder.start();
18
  unregisterShortcuts = registerShortcuts(shortcuts);
19
 
20
  setTimeout(() => {
@@ -29,8 +25,6 @@
29
  });
30
 
31
  onDestroy(() => {
32
- consoleCapture.teardown();
33
- consoleForwarder.stop();
34
  if (unregisterShortcuts) unregisterShortcuts();
35
  });
36
  </script>
 
1
  <script lang="ts">
2
  import { onMount, onDestroy } from 'svelte';
 
 
3
  import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
4
  import { loadingStore } from './lib/stores/loading';
5
  import AppHeader from './lib/components/layout/AppHeader.svelte';
6
  import SplitView from './lib/components/layout/SplitView.svelte';
7
  import LoadingScreen from './lib/components/layout/LoadingScreen.svelte';
8
+
9
  let unregisterShortcuts: () => void;
10
+
11
  onMount(() => {
12
  loadingStore.startLoading();
13
 
 
 
14
  unregisterShortcuts = registerShortcuts(shortcuts);
15
 
16
  setTimeout(() => {
 
25
  });
26
 
27
  onDestroy(() => {
 
 
28
  if (unregisterShortcuts) unregisterShortcuts();
29
  });
30
  </script>
src/lib/components/chat/StreamingText.svelte CHANGED
@@ -16,7 +16,6 @@
16
  let lastProcessedLength = 0;
17
 
18
  $: if (streaming && content) {
19
- // Only process truly new content
20
  if (content.length > lastProcessedLength) {
21
  const newChars = content.slice(lastProcessedLength);
22
  if (newChars) {
@@ -39,12 +38,10 @@
39
  isProcessing = true;
40
 
41
  while (buffer.length > 0) {
42
- // Process multiple characters at once for better performance
43
  const chunkSize = Math.min(3, buffer.length);
44
  const chunk = buffer.splice(0, chunkSize).join('');
45
  displayedContent += chunk;
46
 
47
- // Only delay if there are more characters to process
48
  if (buffer.length > 0) {
49
  await new Promise(resolve => setTimeout(resolve, 1000 / speed));
50
  }
@@ -89,10 +86,15 @@
89
  onMount(() => {
90
  if (streaming) {
91
  showCursor();
92
- // Reset tracking when component mounts with streaming
93
  lastProcessedLength = 0;
94
  displayedContent = "";
95
 
 
 
 
 
 
 
96
  gsap.fromTo(containerElement,
97
  { opacity: 0, y: 5 },
98
  { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
@@ -141,4 +143,4 @@
141
  animation: none;
142
  vertical-align: baseline;
143
  }
144
- </style>
 
16
  let lastProcessedLength = 0;
17
 
18
  $: if (streaming && content) {
 
19
  if (content.length > lastProcessedLength) {
20
  const newChars = content.slice(lastProcessedLength);
21
  if (newChars) {
 
38
  isProcessing = true;
39
 
40
  while (buffer.length > 0) {
 
41
  const chunkSize = Math.min(3, buffer.length);
42
  const chunk = buffer.splice(0, chunkSize).join('');
43
  displayedContent += chunk;
44
 
 
45
  if (buffer.length > 0) {
46
  await new Promise(resolve => setTimeout(resolve, 1000 / speed));
47
  }
 
86
  onMount(() => {
87
  if (streaming) {
88
  showCursor();
 
89
  lastProcessedLength = 0;
90
  displayedContent = "";
91
 
92
+ if (content) {
93
+ buffer.push(...content.split(''));
94
+ lastProcessedLength = content.length;
95
+ processBuffer();
96
+ }
97
+
98
  gsap.fromTo(containerElement,
99
  { opacity: 0, y: 5 },
100
  { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
 
143
  animation: none;
144
  vertical-align: baseline;
145
  }
146
+ </style>
src/lib/components/game/GameCanvas.svelte CHANGED
@@ -10,24 +10,33 @@
10
  let reloadTimer: any;
11
  let previousContent = '';
12
  let isInitialized = false;
13
-
 
14
  $: if ($editorStore.content !== previousContent && $gameStore.isAutoRunning && isInitialized) {
15
  previousContent = $editorStore.content;
16
  clearTimeout(reloadTimer);
17
- reloadTimer = setTimeout(async () => {
18
- if (!$gameStore.isStarting) {
19
- const worldContent = HTMLParser.extractGameContent($editorStore.content);
20
- await gameEngine.start(worldContent);
21
- }
22
- }, 800);
 
 
 
 
 
 
 
 
23
  }
24
-
25
  onMount(async () => {
26
  previousContent = $editorStore.content;
27
  setTimeout(async () => {
28
  if (!$gameStore.instance && $gameStore.isAutoRunning) {
29
- const worldContent = HTMLParser.extractGameContent($editorStore.content);
30
- await gameEngine.start(worldContent);
31
  }
32
  isInitialized = true;
33
  }, 400);
 
10
  let reloadTimer: any;
11
  let previousContent = '';
12
  let isInitialized = false;
13
+ let lastRestartTime = 0;
14
+
15
  $: if ($editorStore.content !== previousContent && $gameStore.isAutoRunning && isInitialized) {
16
  previousContent = $editorStore.content;
17
  clearTimeout(reloadTimer);
18
+
19
+ const now = Date.now();
20
+ if (now - lastRestartTime >= 2000) {
21
+ reloadTimer = setTimeout(async () => {
22
+ const currentTime = Date.now();
23
+ if (currentTime - lastRestartTime < 2000 || $gameStore.isStarting) {
24
+ return;
25
+ }
26
+ lastRestartTime = currentTime;
27
+
28
+ const { world, scripts } = HTMLParser.extractGameContent($editorStore.content);
29
+ await gameEngine.start(world, scripts);
30
+ }, 1500);
31
+ }
32
  }
33
+
34
  onMount(async () => {
35
  previousContent = $editorStore.content;
36
  setTimeout(async () => {
37
  if (!$gameStore.instance && $gameStore.isAutoRunning) {
38
+ const { world, scripts } = HTMLParser.extractGameContent($editorStore.content);
39
+ await gameEngine.start(world, scripts);
40
  }
41
  isInitialized = true;
42
  }, 400);
src/lib/components/layout/AppHeader.svelte CHANGED
@@ -9,8 +9,8 @@
9
  import gsap from 'gsap';
10
 
11
  async function restartGame() {
12
- const worldContent = HTMLParser.extractGameContent($editorStore.content);
13
- await gameEngine.start(worldContent);
14
  }
15
 
16
  function handleViewModeChange(mode: 'code' | 'preview') {
 
9
  import gsap from 'gsap';
10
 
11
  async function restartGame() {
12
+ const { world, scripts } = HTMLParser.extractGameContent($editorStore.content);
13
+ await gameEngine.start(world, scripts);
14
  }
15
 
16
  function handleViewModeChange(mode: 'code' | 'preview') {
src/lib/server/langgraph-agent.ts CHANGED
@@ -72,24 +72,23 @@ export class LangGraphAgent {
72
  let currentSegmentContent = "";
73
  let buffer = "";
74
  const messageId = config?.metadata?.messageId;
75
- const toolRegex = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
 
76
 
77
  for await (const token of this.streamModelResponse(messages)) {
78
  fullResponse += token;
79
  config?.writer?.({ type: "token", content: token });
80
  buffer += token;
81
 
82
- // Process buffer to separate text from tool calls
83
  let processedUpTo = 0;
84
  let match;
85
- toolRegex.lastIndex = 0; // Reset regex state
86
 
87
  while ((match = toolRegex.exec(buffer)) !== null) {
88
- // Send any text before the tool call
89
  const textBefore = buffer.substring(processedUpTo, match.index);
90
 
91
- if (textBefore.trim()) {
92
- if (!currentSegmentId && this.ws) {
93
  currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
94
  currentSegmentContent = "";
95
  this.ws.send(
@@ -105,25 +104,20 @@ export class LangGraphAgent {
105
  );
106
  }
107
 
108
- if (currentSegmentId) {
109
- currentSegmentContent += textBefore;
110
- if (this.ws) {
111
- this.ws.send(
112
- JSON.stringify({
113
- type: "segment_token",
114
- payload: {
115
- segmentId: currentSegmentId,
116
- token: textBefore,
117
- messageId,
118
- },
119
- timestamp: Date.now(),
120
- }),
121
- );
122
- }
123
- }
124
  }
125
 
126
- // End current text segment before tool
127
  if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
128
  this.ws.send(
129
  JSON.stringify({
@@ -143,11 +137,9 @@ export class LangGraphAgent {
143
  processedUpTo = match.index + match[0].length;
144
  }
145
 
146
- // Keep unprocessed text in buffer or flush if no potential tool start
147
  if (processedUpTo > 0) {
148
  buffer = buffer.substring(processedUpTo);
149
- } else if (buffer.length > 100 && !buffer.includes("[TOOL:")) {
150
- // Flush buffer if it's getting large and has no tool pattern
151
  if (!currentSegmentId && buffer.trim() && this.ws) {
152
  currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
153
  currentSegmentContent = "";
@@ -184,11 +176,7 @@ export class LangGraphAgent {
184
  }
185
  }
186
 
187
- // Flush remaining buffer after stream ends
188
- if (
189
- buffer.trim() &&
190
- !buffer.match(/\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/)
191
- ) {
192
  if (!currentSegmentId && this.ws) {
193
  currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
194
  currentSegmentContent = "";
@@ -256,6 +244,24 @@ export class LangGraphAgent {
256
  };
257
  }
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  return {
260
  messages: [new AIMessage(fullResponse)],
261
  hasToolCalls: false,
@@ -292,102 +298,44 @@ export class LangGraphAgent {
292
  }
293
 
294
  private buildSystemPrompt(): string {
295
- return `You are an expert VibeGame developer assistant. VibeGame is a declarative 3D game engine using XML-like syntax.
 
 
 
 
 
 
296
 
297
- VIBEGAME DOCUMENTATION:
298
  ${this.documentation}
299
 
 
 
300
  AVAILABLE TOOLS:
 
 
301
 
302
- 1. search_editor - Search for text or patterns to locate code elements
303
- Use FIRST when: Looking for specific elements, functions, or components
304
- Parameters:
305
- - query (string, required): Text or regex pattern to search for
306
- - mode (string, optional): "text" or "regex" (default: "text")
307
- - contextLines (number, optional): Context lines before/after match (0-5, default: 2)
308
- Example: [TOOL: search_editor {"query": "dynamic-part", "mode": "text"}]
309
-
310
- 2. read_editor - Read the entire code in the editor
311
- Use when: Need complete file overview or search returned no results
312
- No parameters needed
313
- Example: [TOOL: read_editor]
314
-
315
- 3. read_editor_lines - Read specific lines from the editor
316
- Use AFTER search_editor: To read detailed context around found elements
317
- Parameters:
318
- - startLine (number, required): Starting line number (1-indexed)
319
- - endLine (number, optional): Ending line number (inclusive)
320
- Example: [TOOL: read_editor_lines {"startLine": 5, "endLine": 10}]
321
-
322
- 4. edit_editor - Replace specific text in the editor
323
- Use when: Making targeted changes to existing code
324
- Parameters:
325
- - oldText (string, required): Exact text to find and replace
326
- - newText (string, required): Replacement text
327
- Example: [TOOL: edit_editor {"oldText": "color=\"#ff4500\"", "newText": "color=\"#00ff00\""}]
328
-
329
- 5. write_editor - Replace entire editor content
330
- Use when: Creating new file or complete rewrite
331
- Parameters:
332
- - content (string, required): Complete new content
333
- Example: [TOOL: write_editor {"content": "<world>...</world>"}]
334
-
335
- 6. observe_console - Read recent console messages
336
- Use when: Checking for errors or game state after changes
337
- No parameters needed
338
- Example: [TOOL: observe_console]
339
-
340
- EDITOR EXPLORATION WORKFLOW:
341
- 1. For understanding code structure:
342
- - Use search_editor to locate specific elements (classes, functions, components)
343
- - Use read_editor_lines with found line numbers for detailed context
344
-
345
- 2. For making targeted changes:
346
- - First search_editor to find exact location
347
- - Then read_editor_lines to understand surrounding context (if needed)
348
- - Finally edit_editor with precise text replacement
349
-
350
- 3. For broad understanding:
351
- - Use read_editor to see complete file structure
352
- - Then search_editor to navigate to specific sections
353
-
354
- EXAMPLE WORKFLOW - "Change the ball color to blue":
355
- 1. [TOOL: search_editor {"query": "ball", "mode": "text"}]
356
- 2. [TOOL: search_editor {"query": "dynamic-part", "mode": "text"}]
357
- 3. [TOOL: read_editor_lines {"startLine": 12, "endLine": 12}]
358
- 4. [TOOL: edit_editor {"oldText": "color=\"#ff4500\"", "newText": "color=\"#0000ff\""}]
359
-
360
- IMPORTANT NOTES:
361
- - When search_editor returns no matches, try alternative search terms or use read_editor
362
- - Always continue after receiving tool results - don't stop until task is complete
363
- - After making changes, check observe_console for any errors
364
-
365
- Be concise, accurate, and focus on practical solutions.`;
366
- }
367
 
368
- private formatMessages(
369
- messages: BaseMessage[],
370
- systemPrompt: string,
371
- ): Array<{ role: string; content: string }> {
372
- const formatted = [
373
- { role: "system", content: systemPrompt },
374
- ...messages.map((msg) => {
375
- let role = "assistant";
376
- if (msg instanceof HumanMessage) {
377
- role = "user";
378
- } else if (msg instanceof ToolMessage) {
379
- const content = `Tool result for ${msg.name}: ${msg.content}`;
380
- return { role: "assistant", content };
381
- }
382
 
383
- const content =
384
- typeof msg.content === "string"
385
- ? msg.content
386
- : JSON.stringify(msg.content);
387
- return { role, content };
388
- }),
389
- ];
390
- return formatted;
 
 
 
 
 
 
 
391
  }
392
 
393
  private async *streamModelResponse(
@@ -421,7 +369,7 @@ Be concise, accurate, and focus on practical solutions.`;
421
  response: string,
422
  ): Array<{ name: string; args: Record<string, unknown> }> {
423
  const toolCalls = [];
424
- const toolRegex = /\[TOOL:\s*(\w+)(?:\s+({.*?}))?\]/gs;
425
  let match;
426
 
427
  while ((match = toolRegex.exec(response)) !== null) {
@@ -448,6 +396,66 @@ Be concise, accurate, and focus on practical solutions.`;
448
  return toolCalls;
449
  }
450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  private async executeToolsWithSegments(
452
  toolCalls: Array<{ name: string; args: Record<string, unknown> }>,
453
  messageId?: string,
 
72
  let currentSegmentContent = "";
73
  let buffer = "";
74
  const messageId = config?.metadata?.messageId;
75
+
76
+ const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/g;
77
 
78
  for await (const token of this.streamModelResponse(messages)) {
79
  fullResponse += token;
80
  config?.writer?.({ type: "token", content: token });
81
  buffer += token;
82
 
 
83
  let processedUpTo = 0;
84
  let match;
85
+ toolRegex.lastIndex = 0;
86
 
87
  while ((match = toolRegex.exec(buffer)) !== null) {
 
88
  const textBefore = buffer.substring(processedUpTo, match.index);
89
 
90
+ if (textBefore.trim() && this.ws) {
91
+ if (!currentSegmentId) {
92
  currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
93
  currentSegmentContent = "";
94
  this.ws.send(
 
104
  );
105
  }
106
 
107
+ currentSegmentContent += textBefore;
108
+ this.ws.send(
109
+ JSON.stringify({
110
+ type: "segment_token",
111
+ payload: {
112
+ segmentId: currentSegmentId,
113
+ token: textBefore,
114
+ messageId,
115
+ },
116
+ timestamp: Date.now(),
117
+ }),
118
+ );
 
 
 
 
119
  }
120
 
 
121
  if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
122
  this.ws.send(
123
  JSON.stringify({
 
137
  processedUpTo = match.index + match[0].length;
138
  }
139
 
 
140
  if (processedUpTo > 0) {
141
  buffer = buffer.substring(processedUpTo);
142
+ } else if (buffer.length > 100 && !buffer.includes("TOOL:")) {
 
143
  if (!currentSegmentId && buffer.trim() && this.ws) {
144
  currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
145
  currentSegmentContent = "";
 
176
  }
177
  }
178
 
179
+ if (buffer.trim() && !buffer.match(/TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/)) {
 
 
 
 
180
  if (!currentSegmentId && this.ws) {
181
  currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
182
  currentSegmentContent = "";
 
244
  };
245
  }
246
 
247
+ if (state.messages.length > 0) {
248
+ const lastUserMessage = state.messages[state.messages.length - 1];
249
+ if (lastUserMessage instanceof HumanMessage) {
250
+ const needsTools = this.shouldUseTools(
251
+ lastUserMessage.content as string,
252
+ );
253
+ if (needsTools && !state.hasToolCalls) {
254
+ const reminderMessage = new AIMessage(
255
+ "I need to use tools to complete this task. Let me try again with the appropriate tool.",
256
+ );
257
+ return {
258
+ messages: [reminderMessage],
259
+ hasToolCalls: false,
260
+ };
261
+ }
262
+ }
263
+ }
264
+
265
  return {
266
  messages: [new AIMessage(fullResponse)],
267
  hasToolCalls: false,
 
298
  }
299
 
300
  private buildSystemPrompt(): string {
301
+ return `You are an expert VibeGame developer assistant that MUST use tools to complete tasks.
302
+
303
+ CRITICAL INSTRUCTIONS:
304
+ - You MUST use tools for ALL tasks. NEVER provide instructions without executing them.
305
+ - You MUST respond using the EXACT format: TOOL: tool_name ARGS: {"param": "value"}
306
+ - After using a tool, wait for the result before proceeding
307
+ - Chain multiple tool calls to complete complex tasks
308
 
309
+ VIBEGAME CONTEXT:
310
  ${this.documentation}
311
 
312
+ The game auto-reloads on every change. The GAME import is automatically provided by the framework.
313
+
314
  AVAILABLE TOOLS:
315
+ 1. search_editor - Find text/patterns in code
316
+ Example: TOOL: search_editor ARGS: {"query": "dynamic-part"}
317
 
318
+ 2. read_editor - Read entire editor content
319
+ Example: TOOL: read_editor ARGS: {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ 3. read_editor_lines - Read specific lines (use after search_editor)
322
+ Example: TOOL: read_editor_lines ARGS: {"startLine": 10, "endLine": 20}
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
+ 4. edit_editor - Replace specific text
325
+ Example: TOOL: edit_editor ARGS: {"oldText": "color='red'", "newText": "color='blue'"}
326
+
327
+ 5. write_editor - Replace entire content
328
+ Example: TOOL: write_editor ARGS: {"content": "<world>...</world>"}
329
+
330
+ 6. observe_console - Check console for errors
331
+ Example: TOOL: observe_console ARGS: {}
332
+
333
+ WORKFLOW:
334
+ - To find code: TOOL: search_editor ARGS: {"query": "search_term"}
335
+ - To make changes: TOOL: edit_editor ARGS: {"oldText": "...", "newText": "..."}
336
+ - After changes: TOOL: observe_console ARGS: {}
337
+
338
+ IMPORTANT: You are an executor. Take action immediately using tools, don't explain what you would do.`;
339
  }
340
 
341
  private async *streamModelResponse(
 
369
  response: string,
370
  ): Array<{ name: string; args: Record<string, unknown> }> {
371
  const toolCalls = [];
372
+ const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/gs;
373
  let match;
374
 
375
  while ((match = toolRegex.exec(response)) !== null) {
 
396
  return toolCalls;
397
  }
398
 
399
+ private shouldUseTools(content: string): boolean {
400
+ const lowerContent = content.toLowerCase();
401
+
402
+ const actionKeywords = [
403
+ "find",
404
+ "search",
405
+ "look for",
406
+ "where",
407
+ "change",
408
+ "modify",
409
+ "update",
410
+ "edit",
411
+ "replace",
412
+ "show",
413
+ "display",
414
+ "read",
415
+ "what",
416
+ "check",
417
+ "create",
418
+ "write",
419
+ "add",
420
+ "implement",
421
+ "fix",
422
+ "debug",
423
+ "error",
424
+ "console",
425
+ ];
426
+
427
+ return actionKeywords.some((keyword) => lowerContent.includes(keyword));
428
+ }
429
+
430
+ private formatMessages(
431
+ messages: BaseMessage[],
432
+ systemPrompt: string,
433
+ ): Array<{ role: string; content: string }> {
434
+ const formatted = [
435
+ { role: "system", content: systemPrompt },
436
+ ...messages.map((msg) => {
437
+ let role = "assistant";
438
+ if (msg instanceof HumanMessage) {
439
+ role = "user";
440
+ } else if (msg instanceof ToolMessage) {
441
+ const content = `Tool result for ${msg.name}: ${
442
+ typeof msg.content === "string"
443
+ ? msg.content
444
+ : JSON.stringify(msg.content)
445
+ }`;
446
+ return { role: "assistant", content };
447
+ }
448
+
449
+ const content =
450
+ typeof msg.content === "string"
451
+ ? msg.content
452
+ : JSON.stringify(msg.content);
453
+ return { role, content };
454
+ }),
455
+ ];
456
+ return formatted;
457
+ }
458
+
459
  private async executeToolsWithSegments(
460
  toolCalls: Array<{ name: string; args: Record<string, unknown> }>,
461
  messageId?: string,
src/lib/server/tools.ts CHANGED
@@ -245,7 +245,7 @@ export const searchEditorTool = new DynamicStructuredTool({
245
  try {
246
  const regex = new RegExp(input.query);
247
  isMatch = regex.test(lines[i]);
248
- } catch (e) {
249
  return `Error: Invalid regex pattern "${input.query}"`;
250
  }
251
  }
 
245
  try {
246
  const regex = new RegExp(input.query);
247
  isMatch = regex.test(lines[i]);
248
+ } catch {
249
  return `Error: Invalid regex pattern "${input.query}"`;
250
  }
251
  }
src/lib/services/console-forward.ts DELETED
@@ -1,65 +0,0 @@
1
- import { consoleStore, type ConsoleMessage } from "../stores/console";
2
- import { agentService } from "./agent";
3
-
4
- export class ConsoleForwarder {
5
- private static instance: ConsoleForwarder | null = null;
6
- private unsubscribe: (() => void) | null = null;
7
- private lastForwardedId: string | null = null;
8
-
9
- private constructor() {}
10
-
11
- static getInstance(): ConsoleForwarder {
12
- if (!ConsoleForwarder.instance) {
13
- ConsoleForwarder.instance = new ConsoleForwarder();
14
- }
15
- return ConsoleForwarder.instance;
16
- }
17
-
18
- start(): void {
19
- if (this.unsubscribe) return;
20
-
21
- this.unsubscribe = consoleStore.subscribe((state) => {
22
- if (state.messages.length === 0) return;
23
-
24
- const messagesToForward = this.lastForwardedId
25
- ? state.messages.filter(
26
- (msg) =>
27
- msg.timestamp >
28
- (state.messages.find((m) => m.id === this.lastForwardedId)
29
- ?.timestamp || 0),
30
- )
31
- : state.messages;
32
-
33
- if (messagesToForward.length > 0) {
34
- this.forwardMessages(messagesToForward);
35
- this.lastForwardedId =
36
- messagesToForward[messagesToForward.length - 1].id;
37
- }
38
- });
39
- }
40
-
41
- private forwardMessages(messages: ConsoleMessage[]): void {
42
- messages.forEach((message) => {
43
- agentService.sendRawMessage({
44
- type: "console_sync",
45
- payload: {
46
- id: message.id,
47
- type: message.type,
48
- message: message.message,
49
- timestamp: message.timestamp,
50
- },
51
- timestamp: Date.now(),
52
- });
53
- });
54
- }
55
-
56
- stop(): void {
57
- if (this.unsubscribe) {
58
- this.unsubscribe();
59
- this.unsubscribe = null;
60
- }
61
- this.lastForwardedId = null;
62
- }
63
- }
64
-
65
- export const consoleForwarder = ConsoleForwarder.getInstance();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/services/{console-capture.ts → console-sync.ts} RENAMED
@@ -1,50 +1,35 @@
1
  import { consoleStore } from "../stores/console";
 
2
 
3
  type ConsoleMethod = "log" | "warn" | "error" | "info";
4
- type ConsoleMethodFunc = (...args: unknown[]) => void;
5
- type OriginalConsole = Record<ConsoleMethod, ConsoleMethodFunc>;
6
 
7
- export class ConsoleCapture {
8
- private static instance: ConsoleCapture | null = null;
9
- private originalConsole: OriginalConsole = {} as OriginalConsole;
10
- private isCapturing = false;
11
-
12
- private constructor() {}
13
-
14
- static getInstance(): ConsoleCapture {
15
- if (!ConsoleCapture.instance) {
16
- ConsoleCapture.instance = new ConsoleCapture();
17
- }
18
- return ConsoleCapture.instance;
19
- }
20
 
21
  setup(): void {
22
- if (this.isCapturing) return;
23
 
24
  this.originalConsole = {
25
- log: console.log,
26
- warn: console.warn,
27
- error: console.error,
28
- info: console.info,
29
  };
30
 
31
- const methods: Array<"log" | "warn" | "error" | "info"> = [
32
- "log",
33
- "warn",
34
- "error",
35
- "info",
36
- ];
37
-
38
- methods.forEach((method) => {
39
  console[method] = (...args: unknown[]) => {
40
- this.originalConsole[method]?.apply(console, args);
41
 
42
  const firstArg = args[0];
43
  if (typeof firstArg === "string") {
44
  if (
45
  firstArg.includes("[vite]") ||
46
  firstArg.includes("[VibeGame] Console forwarding") ||
47
- firstArg.includes("[DEBUG]")
 
48
  ) {
49
  return;
50
  }
@@ -65,26 +50,42 @@ export class ConsoleCapture {
65
  })
66
  .join(" ");
67
 
68
- consoleStore.addMessage(method, message);
 
 
 
 
 
 
 
 
 
 
 
 
69
  };
70
- });
71
 
72
- this.isCapturing = true;
 
 
 
73
  }
74
 
75
  teardown(): void {
76
- if (!this.isCapturing) return;
77
-
78
- if (this.originalConsole.log) {
79
- console.log = this.originalConsole.log;
80
- console.warn = this.originalConsole.warn;
81
- console.error = this.originalConsole.error;
82
- console.info = this.originalConsole.info;
83
- }
84
-
85
- this.originalConsole = {} as OriginalConsole;
86
- this.isCapturing = false;
 
87
  }
88
  }
89
 
90
- export const consoleCapture = ConsoleCapture.getInstance();
 
1
  import { consoleStore } from "../stores/console";
2
+ import { agentService } from "./agent";
3
 
4
  type ConsoleMethod = "log" | "warn" | "error" | "info";
 
 
5
 
6
+ export class ConsoleSyncService {
7
+ private isSetup = false;
8
+ private originalConsole: Record<ConsoleMethod, (...args: unknown[]) => void> =
9
+ {} as Record<ConsoleMethod, (...args: unknown[]) => void>;
 
 
 
 
 
 
 
 
 
10
 
11
  setup(): void {
12
+ if (this.isSetup) return;
13
 
14
  this.originalConsole = {
15
+ log: console.log.bind(console),
16
+ warn: console.warn.bind(console),
17
+ error: console.error.bind(console),
18
+ info: console.info.bind(console),
19
  };
20
 
21
+ const interceptConsole = (method: ConsoleMethod) => {
22
+ const original = this.originalConsole[method];
 
 
 
 
 
 
23
  console[method] = (...args: unknown[]) => {
24
+ original(...args);
25
 
26
  const firstArg = args[0];
27
  if (typeof firstArg === "string") {
28
  if (
29
  firstArg.includes("[vite]") ||
30
  firstArg.includes("[VibeGame] Console forwarding") ||
31
+ firstArg.includes("[DEBUG]") ||
32
+ firstArg.includes("hot updated:")
33
  ) {
34
  return;
35
  }
 
50
  })
51
  .join(" ");
52
 
53
+ const messageId = `${Date.now()}-${Math.random()}`;
54
+ consoleStore.addMessage(method === "log" ? "info" : method, message);
55
+
56
+ agentService.sendRawMessage({
57
+ type: "console_sync",
58
+ payload: {
59
+ id: messageId,
60
+ type: method === "log" ? "info" : method,
61
+ message: message,
62
+ timestamp: Date.now(),
63
+ },
64
+ timestamp: Date.now(),
65
+ });
66
  };
67
+ };
68
 
69
+ (["log", "warn", "error", "info"] as ConsoleMethod[]).forEach(
70
+ interceptConsole,
71
+ );
72
+ this.isSetup = true;
73
  }
74
 
75
  teardown(): void {
76
+ if (!this.isSetup) return;
77
+
78
+ console.log = this.originalConsole.log;
79
+ console.warn = this.originalConsole.warn;
80
+ console.error = this.originalConsole.error;
81
+ console.info = this.originalConsole.info;
82
+
83
+ this.originalConsole = {} as Record<
84
+ ConsoleMethod,
85
+ (...args: unknown[]) => void
86
+ >;
87
+ this.isSetup = false;
88
  }
89
  }
90
 
91
+ export const consoleSyncService = new ConsoleSyncService();
src/lib/services/context.md CHANGED
@@ -18,8 +18,7 @@ services/
18
  ├── websocket.ts # WebSocket connection management
19
  ├── message-handler.ts # WebSocket message processing
20
  ├── game-engine.ts # Game lifecycle management
21
- ├── console-capture.ts # Console interception
22
- ├── console-forward.ts # WebSocket console forwarding
23
  └── html-parser.ts # Game HTML parsing
24
  ```
25
 
@@ -38,12 +37,12 @@ services/
38
  - `agentService.disconnect()` - Disconnect from server
39
  - `agentService.sendMessage()` - Send chat message
40
  - `agentService.sendRawMessage()` - Send raw WebSocket message
41
- - `gameEngine.start()` - Start game with world content
42
- - `gameEngine.stop()` - Clean up game instance
43
- - `consoleCapture.setup()` - Begin console interception
44
- - `consoleForwarder.start()` - Start forwarding console messages
45
- - `consoleForwarder.stop()` - Stop console forwarding
46
- - `HTMLParser.extractGameContent()` - Parse world from HTML
47
 
48
  ## Dependencies
49
 
 
18
  ├── websocket.ts # WebSocket connection management
19
  ├── message-handler.ts # WebSocket message processing
20
  ├── game-engine.ts # Game lifecycle management
21
+ ├── console-sync.ts # VibeGame console synchronization
 
22
  └── html-parser.ts # Game HTML parsing
23
  ```
24
 
 
37
  - `agentService.disconnect()` - Disconnect from server
38
  - `agentService.sendMessage()` - Send chat message
39
  - `agentService.sendRawMessage()` - Send raw WebSocket message
40
+ - `gameEngine.start(worldContent, scripts)` - Start game with parsed world and scripts
41
+ - `gameEngine.stop()` - Destroy game instance and clean up
42
+ - `gameEngine.isRunning()` - Check if game is active
43
+ - `consoleSyncService.setup()` - Intercept console methods
44
+ - `consoleSyncService.teardown()` - Restore original console
45
+ - `HTMLParser.extractGameContent(html)` - Parse world and scripts from HTML
46
 
47
  ## Dependencies
48
 
src/lib/services/game-engine.ts CHANGED
@@ -1,4 +1,5 @@
1
  import * as GAME from "vibegame";
 
2
  import { gameStore } from "../stores/game";
3
  import { consoleStore } from "../stores/console";
4
  import { uiStore } from "../stores/ui";
@@ -18,11 +19,15 @@ export class GameEngine {
18
  return GameEngine.instance;
19
  }
20
 
21
- async start(worldContent: string): Promise<void> {
 
 
 
 
 
 
22
  gameStore.setStarting(true);
23
  consoleStore.addMessage("info", "🎮 Starting game...");
24
-
25
- this.stop();
26
  uiStore.setError(null);
27
 
28
  try {
@@ -33,6 +38,69 @@ export class GameEngine {
33
 
34
  container.innerHTML = worldContent;
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  this.gameInstance = await GAME.run();
37
  gameStore.setInstance(this.gameInstance);
38
  consoleStore.addMessage("info", "✅ Game started!");
@@ -41,6 +109,7 @@ export class GameEngine {
41
  uiStore.setError(errorMsg);
42
  consoleStore.addMessage("error", `❌ Error: ${errorMsg}`);
43
  gameStore.setInstance(null);
 
44
  } finally {
45
  gameStore.setStarting(false);
46
  }
@@ -49,13 +118,11 @@ export class GameEngine {
49
  stop(): void {
50
  if (this.gameInstance) {
51
  try {
52
- if (typeof this.gameInstance.destroy === "function") {
53
- this.gameInstance.destroy();
54
- } else if (typeof this.gameInstance.stop === "function") {
55
- this.gameInstance.stop();
56
- }
57
  } catch (error) {
58
- console.error("Error stopping game:", error);
 
59
  }
60
  this.gameInstance = null;
61
  gameStore.setInstance(null);
@@ -67,6 +134,8 @@ export class GameEngine {
67
  container.removeChild(container.firstChild);
68
  }
69
  }
 
 
70
  }
71
 
72
  isRunning(): boolean {
 
1
  import * as GAME from "vibegame";
2
+ import type { System, Plugin, Component, BuilderOptions } from "vibegame";
3
  import { gameStore } from "../stores/game";
4
  import { consoleStore } from "../stores/console";
5
  import { uiStore } from "../stores/ui";
 
19
  return GameEngine.instance;
20
  }
21
 
22
+ async start(worldContent: string, scripts: string[] = []): Promise<void> {
23
+ if (this.gameInstance) {
24
+ consoleStore.addMessage("info", "Stopping previous game instance...");
25
+ this.stop();
26
+ await new Promise((resolve) => setTimeout(resolve, 100));
27
+ }
28
+
29
  gameStore.setStarting(true);
30
  consoleStore.addMessage("info", "🎮 Starting game...");
 
 
31
  uiStore.setError(null);
32
 
33
  try {
 
38
 
39
  container.innerHTML = worldContent;
40
 
41
+ GAME.resetBuilder();
42
+
43
+ const gameProxy = {
44
+ withSystem: (system: System) => {
45
+ GAME.withSystem(system);
46
+ return gameProxy;
47
+ },
48
+ withPlugin: (plugin: Plugin) => {
49
+ GAME.withPlugin(plugin);
50
+ return gameProxy;
51
+ },
52
+ withComponent: (name: string, component: Component) => {
53
+ GAME.withComponent(name, component);
54
+ return gameProxy;
55
+ },
56
+ configure: (options: BuilderOptions) => {
57
+ GAME.configure(options);
58
+ return gameProxy;
59
+ },
60
+ withoutDefaultPlugins: () => {
61
+ GAME.withoutDefaultPlugins();
62
+ return gameProxy;
63
+ },
64
+ run: () => {
65
+ console.warn(
66
+ "GAME.run() is not available in user scripts - the framework handles game lifecycle",
67
+ );
68
+ return Promise.resolve({
69
+ stop: () => {},
70
+ destroy: () => {},
71
+ step: () => {},
72
+ getState: () => null,
73
+ });
74
+ },
75
+ defineComponent: GAME.defineComponent,
76
+ defineQuery: GAME.defineQuery,
77
+ Types: GAME.Types,
78
+ };
79
+
80
+ (window as unknown as { GAME: typeof gameProxy }).GAME = gameProxy;
81
+
82
+ let scriptExecutionFailed = false;
83
+ for (const script of scripts) {
84
+ try {
85
+ const cleanedScript = script.replace(/GAME\.run\(\)/g, "");
86
+ eval(cleanedScript);
87
+ } catch (scriptError) {
88
+ scriptExecutionFailed = true;
89
+ const errorMsg =
90
+ scriptError instanceof Error
91
+ ? scriptError.message
92
+ : String(scriptError);
93
+ console.error("Error executing user script:", errorMsg);
94
+ consoleStore.addMessage("error", `Script error: ${errorMsg}`);
95
+ }
96
+ }
97
+
98
+ (window as unknown as { GAME: typeof gameProxy | null }).GAME = null;
99
+
100
+ if (scriptExecutionFailed) {
101
+ throw new Error("Script execution failed - game not started");
102
+ }
103
+
104
  this.gameInstance = await GAME.run();
105
  gameStore.setInstance(this.gameInstance);
106
  consoleStore.addMessage("info", "✅ Game started!");
 
109
  uiStore.setError(errorMsg);
110
  consoleStore.addMessage("error", `❌ Error: ${errorMsg}`);
111
  gameStore.setInstance(null);
112
+ this.gameInstance = null;
113
  } finally {
114
  gameStore.setStarting(false);
115
  }
 
118
  stop(): void {
119
  if (this.gameInstance) {
120
  try {
121
+ this.gameInstance.destroy();
122
+ consoleStore.addMessage("info", "Game instance destroyed");
 
 
 
123
  } catch (error) {
124
+ console.error("Error destroying game:", error);
125
+ consoleStore.addMessage("error", `Error destroying game: ${error}`);
126
  }
127
  this.gameInstance = null;
128
  gameStore.setInstance(null);
 
134
  container.removeChild(container.firstChild);
135
  }
136
  }
137
+
138
+ GAME.resetBuilder();
139
  }
140
 
141
  isRunning(): boolean {
src/lib/services/html-parser.ts CHANGED
@@ -1,7 +1,27 @@
 
 
 
 
 
1
  export class HTMLParser {
2
- static extractGameContent(html: string): string {
3
  const worldMatch = html.match(/<world[^>]*>[\s\S]*?<\/world>/);
4
- return worldMatch ? worldMatch[0] : '<world canvas="#game-canvas"></world>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  }
6
 
7
  static validateGameHTML(html: string): { valid: boolean; error?: string } {
 
1
+ export interface GameContent {
2
+ world: string;
3
+ scripts: string[];
4
+ }
5
+
6
  export class HTMLParser {
7
+ static extractGameContent(html: string): GameContent {
8
  const worldMatch = html.match(/<world[^>]*>[\s\S]*?<\/world>/);
9
+ const world = worldMatch
10
+ ? worldMatch[0]
11
+ : '<world canvas="#game-canvas"></world>';
12
+
13
+ const scripts: string[] = [];
14
+ const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
15
+ let match;
16
+
17
+ while ((match = scriptRegex.exec(html)) !== null) {
18
+ const scriptContent = match[1].trim();
19
+ if (scriptContent) {
20
+ scripts.push(scriptContent);
21
+ }
22
+ }
23
+
24
+ return { world, scripts };
25
  }
26
 
27
  static validateGameHTML(html: string): { valid: boolean; error?: string } {
src/lib/stores/editor.ts CHANGED
@@ -6,8 +6,7 @@ export interface EditorState {
6
  theme: string;
7
  }
8
 
9
- const DEFAULT_CONTENT =
10
- `<canvas id="game-canvas"></canvas>
11
 
12
  <world canvas="#game-canvas" sky="#87ceeb">
13
  <player pos="0 0 0"></player>
@@ -17,13 +16,41 @@ const DEFAULT_CONTENT =
17
 
18
  <!-- Ball -->
19
  <dynamic-part pos="-2 4 -3" shape="sphere" size="1" color="#ff4500"></dynamic-part>
 
 
 
20
  </world>
21
 
22
- <script type="module">
23
- import * as GAME from 'vibegame';
24
-
25
- GAME.run();
26
- <` + `/script>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  function createEditorStore() {
29
  const { subscribe, set, update } = writable<EditorState>({
 
6
  theme: string;
7
  }
8
 
9
+ const DEFAULT_CONTENT = `<canvas id="game-canvas"></canvas>
 
10
 
11
  <world canvas="#game-canvas" sky="#87ceeb">
12
  <player pos="0 0 0"></player>
 
16
 
17
  <!-- Ball -->
18
  <dynamic-part pos="-2 4 -3" shape="sphere" size="1" color="#ff4500"></dynamic-part>
19
+
20
+ <!-- Custom component -->
21
+ <entity my-component="value: 0"></entity>
22
  </world>
23
 
24
+ <script>
25
+ // GAME is automatically provided by the framework
26
+ console.log("Game script loaded!");
27
+
28
+ const MyComponent = GAME.defineComponent({
29
+ value: GAME.Types.i32,
30
+ });
31
+
32
+ const myQuery = GAME.defineQuery([MyComponent]);
33
+
34
+ const MySystem = {
35
+ update: (state) => {
36
+ const entities = myQuery(state.world);
37
+ for (const entity of entities) {
38
+ MyComponent.value[entity]++;
39
+ if (MyComponent.value[entity] === 100) {
40
+ console.log("Entity value is 100");
41
+ }
42
+ }
43
+ }
44
+ };
45
+
46
+ const MyPlugin = {
47
+ components: { MyComponent },
48
+ systems: [MySystem],
49
+ };
50
+
51
+ GAME.withPlugin(MyPlugin);
52
+ // .run() is handled by the framework
53
+ </script>`;
54
 
55
  function createEditorStore() {
56
  const { subscribe, set, update } = writable<EditorState>({
src/main.ts CHANGED
@@ -1,8 +1,11 @@
1
  import "./app.css";
2
  import App from "./App.svelte";
 
3
 
4
  const app = new App({
5
  target: document.getElementById("app")!,
6
  });
7
 
 
 
8
  export default app;
 
1
  import "./app.css";
2
  import App from "./App.svelte";
3
+ import { consoleSyncService } from "./lib/services/console-sync";
4
 
5
  const app = new App({
6
  target: document.getElementById("app")!,
7
  });
8
 
9
+ consoleSyncService.setup();
10
+
11
  export default app;