gopeak 2.3.6 → 2.3.8

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.
@@ -1,7 +1,7 @@
1
- [plugin]
2
-
3
- name="Godot MCP Runtime"
4
- description="Runtime communication addon for Godot MCP server. Enables real-time scene inspection, property modification, and method calling from AI assistants."
5
- author="godot-mcp"
6
- version="1.0.0"
7
- script="godot_mcp_runtime.gd"
1
+ [plugin]
2
+
3
+ name="Godot MCP Runtime"
4
+ description="Runtime communication addon for Godot MCP server. Enables real-time scene inspection, property modification, and method calling from AI assistants."
5
+ author="godot-mcp"
6
+ version="1.0.0"
7
+ script="godot_mcp_runtime.gd"
@@ -42,9 +42,10 @@ export async function showNotification() {
42
42
  }
43
43
  // --- Star prompt (one-time) ---
44
44
  if (!hasStarPrompted) {
45
- await askYesNo(' \u2b50 Star GoPeak on GitHub? (y/n): ');
46
- // Star regardless of answer
47
- await handleStar();
45
+ const wantsStar = await askYesNo(' \u2b50 Star GoPeak on GitHub? (y/n): ');
46
+ if (wantsStar) {
47
+ await handleStar();
48
+ }
48
49
  writeFileSync(STAR_PROMPTED_FILE, new Date().toISOString());
49
50
  console.log('');
50
51
  }
package/build/cli.js CHANGED
@@ -65,24 +65,24 @@ async function main() {
65
65
  }
66
66
  function printHelp() {
67
67
  const version = getLocalVersion();
68
- console.log(`
69
- GoPeak v${version} — AI-Powered Godot Development via MCP
70
-
71
- Usage:
72
- gopeak Start MCP server (default)
73
- gopeak setup Install shell hooks for update notifications
74
- gopeak check Check for GoPeak updates
75
- gopeak check --bg Background check (used by shell hooks)
76
- gopeak check --quiet Print only if update available
77
- gopeak star Star GoPeak on GitHub
78
- gopeak uninstall Remove shell hooks
79
- gopeak version Show current version
80
- gopeak help Show this help
81
-
82
- Shell hooks wrap these commands with update notifications:
83
- claude, codex, gemini, opencode, omc, omx
84
-
85
- More info: https://github.com/HaD0Yun/Gopeak-godot-mcp
68
+ console.log(`
69
+ GoPeak v${version} — AI-Powered Godot Development via MCP
70
+
71
+ Usage:
72
+ gopeak Start MCP server (default)
73
+ gopeak setup Install shell hooks for update notifications
74
+ gopeak check Check for GoPeak updates
75
+ gopeak check --bg Background check (used by shell hooks)
76
+ gopeak check --quiet Print only if update available
77
+ gopeak star Star GoPeak on GitHub
78
+ gopeak uninstall Remove shell hooks
79
+ gopeak version Show current version
80
+ gopeak help Show this help
81
+
82
+ Shell hooks wrap these commands with update notifications:
83
+ claude, codex, gemini, opencode, omc, omx
84
+
85
+ More info: https://github.com/HaD0Yun/Gopeak-godot-mcp
86
86
  `.trim());
87
87
  }
88
88
  main().catch((err) => {
@@ -482,17 +482,17 @@ export class GodotBridge extends EventEmitter {
482
482
  this.emit(eventName, payload);
483
483
  }
484
484
  getDefaultVisualizerHtml() {
485
- return `<!doctype html>
486
- <html lang="en">
487
- <head>
488
- <meta charset="utf-8" />
489
- <meta name="viewport" content="width=device-width, initial-scale=1" />
490
- <title>Godot MCP Visualizer</title>
491
- </head>
492
- <body>
493
- <h1>Godot MCP Visualizer</h1>
494
- <p>Run the map_project tool to load visualization data.</p>
495
- </body>
485
+ return `<!doctype html>
486
+ <html lang="en">
487
+ <head>
488
+ <meta charset="utf-8" />
489
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
490
+ <title>Godot MCP Visualizer</title>
491
+ </head>
492
+ <body>
493
+ <h1>Godot MCP Visualizer</h1>
494
+ <p>Run the map_project tool to load visualization data.</p>
495
+ </body>
496
496
  </html>`;
497
497
  }
498
498
  rejectAllPending(error) {
package/build/index.js CHANGED
@@ -475,10 +475,20 @@ class GodotServer {
475
475
  const params = (args && typeof args === 'object') ? args : {};
476
476
  const RUNTIME_PORT = 7777;
477
477
  const RUNTIME_HOST = '127.0.0.1';
478
- const TIMEOUT_MS = 10000;
478
+ const timeoutOverride = Number.parseInt(process.env.GOPEAK_RUNTIME_TIMEOUT_MS || '', 10);
479
+ const TIMEOUT_MS = Number.isInteger(timeoutOverride) && timeoutOverride > 0 ? timeoutOverride : 10000;
480
+ const expectsScreenshot = command === 'capture_screenshot' || command === 'capture_viewport';
481
+ const screenshotDir = expectsScreenshot ? mkdtempSync(join(tmpdir(), 'gopeak-runtime-screenshot-')) : null;
482
+ const screenshotPath = screenshotDir ? join(screenshotDir, 'capture.png') : null;
483
+ const runtimeParams = screenshotPath ? { ...params, output_path: screenshotPath } : params;
484
+ const cleanupScreenshotDir = () => {
485
+ if (screenshotDir) {
486
+ rmSync(screenshotDir, { recursive: true, force: true });
487
+ }
488
+ };
479
489
  return new Promise((resolve) => {
480
490
  const socket = createTcpConnection({ port: RUNTIME_PORT, host: RUNTIME_HOST }, () => {
481
- const payload = JSON.stringify({ command, params, id: Date.now() });
491
+ const payload = JSON.stringify({ command, params: runtimeParams, id: Date.now() });
482
492
  socket.write(payload + '\n');
483
493
  });
484
494
  let responseBuffer = Buffer.alloc(0);
@@ -489,6 +499,7 @@ class GodotServer {
489
499
  }
490
500
  resolved = true;
491
501
  socket.destroy();
502
+ cleanupScreenshotDir();
492
503
  resolve({
493
504
  content: [{ type: 'text', text: `Runtime command '${command}' timed out after ${TIMEOUT_MS}ms. Ensure the Godot game is running with the MCP runtime addon enabled.` }],
494
505
  });
@@ -500,7 +511,36 @@ class GodotServer {
500
511
  resolved = true;
501
512
  clearTimeout(timer);
502
513
  socket.destroy();
514
+ if (parsed.type === 'screenshot_file' && parsed.path) {
515
+ const returnedPath = String(parsed.path);
516
+ if (!screenshotPath || normalize(returnedPath) !== normalize(screenshotPath)) {
517
+ cleanupScreenshotDir();
518
+ resolve({
519
+ content: [{ type: 'text', text: `Rejected screenshot file path outside the GoPeak-managed capture path: '${returnedPath}'` }],
520
+ });
521
+ return;
522
+ }
523
+ try {
524
+ const imageData = readFileSync(screenshotPath).toString('base64');
525
+ cleanupScreenshotDir();
526
+ resolve({
527
+ content: [
528
+ { type: 'text', text: `Screenshot captured: ${parsed.width}x${parsed.height} ${parsed.format}` },
529
+ { type: 'image', data: imageData, mimeType: 'image/png' },
530
+ ],
531
+ });
532
+ }
533
+ catch (error) {
534
+ cleanupScreenshotDir();
535
+ const message = error instanceof Error ? error.message : String(error);
536
+ resolve({
537
+ content: [{ type: 'text', text: `Failed to read screenshot file '${screenshotPath}': ${message}` }],
538
+ });
539
+ }
540
+ return;
541
+ }
503
542
  if (parsed.type === 'screenshot' && parsed.data) {
543
+ cleanupScreenshotDir();
504
544
  resolve({
505
545
  content: [
506
546
  { type: 'text', text: `Screenshot captured: ${parsed.width}x${parsed.height} ${parsed.format}` },
@@ -509,6 +549,7 @@ class GodotServer {
509
549
  });
510
550
  return;
511
551
  }
552
+ cleanupScreenshotDir();
512
553
  resolve({
513
554
  content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
514
555
  });
@@ -551,7 +592,8 @@ class GodotServer {
551
592
  newlineIndex = responseBuffer.indexOf(0x0a);
552
593
  }
553
594
  if (parsedMessages.length > 0) {
554
- const candidate = parsedMessages.find((message) => message?.type === 'screenshot' && message?.data)
595
+ const candidate = parsedMessages.find((message) => message?.type === 'screenshot_file' && message?.path)
596
+ ?? parsedMessages.find((message) => message?.type === 'screenshot' && message?.data)
555
597
  ?? parsedMessages.find((message) => message?.type === 'pong')
556
598
  ?? parsedMessages.find((message) => message?.type && message.type !== 'welcome')
557
599
  ?? null;
@@ -567,6 +609,7 @@ class GodotServer {
567
609
  clearTimeout(timer);
568
610
  const responseData = responseBuffer.toString('utf8').trim();
569
611
  resolved = true;
612
+ cleanupScreenshotDir();
570
613
  try {
571
614
  const parsed = JSON.parse(responseData);
572
615
  resolve({
@@ -585,6 +628,7 @@ class GodotServer {
585
628
  }
586
629
  resolved = true;
587
630
  clearTimeout(timer);
631
+ cleanupScreenshotDir();
588
632
  resolve({
589
633
  content: [{ type: 'text', text: `Failed to connect to Godot runtime addon at ${RUNTIME_HOST}:${RUNTIME_PORT}: ${error.message}. Ensure the game is running with the MCP runtime autoload enabled.` }],
590
634
  });
@@ -1077,7 +1121,7 @@ class GodotServer {
1077
1121
  this.logDebug(`Command: ${cmd}`);
1078
1122
  try {
1079
1123
  const { stdout, stderr } = await execAsync(cmd);
1080
- return { stdout, stderr };
1124
+ return { stdout, stderr: this.sanitizeGodotStderr(stderr) };
1081
1125
  }
1082
1126
  finally {
1083
1127
  rmSync(paramsDir, { recursive: true, force: true });
@@ -1089,7 +1133,7 @@ class GodotServer {
1089
1133
  const execError = error;
1090
1134
  return {
1091
1135
  stdout: execError.stdout,
1092
- stderr: execError.stderr,
1136
+ stderr: this.sanitizeGodotStderr(execError.stderr),
1093
1137
  };
1094
1138
  }
1095
1139
  throw error;
@@ -1589,7 +1633,7 @@ class GodotServer {
1589
1633
  this.logDebug('Killing existing Godot process before starting a new one');
1590
1634
  this.activeProcess.process.kill();
1591
1635
  }
1592
- const cmdArgs = ['-d', '--path', args.projectPath];
1636
+ const cmdArgs = ['--headless', '-d', '--path', args.projectPath];
1593
1637
  if (args.scene && this.validatePath(args.scene)) {
1594
1638
  this.logDebug(`Adding scene parameter: ${args.scene}`);
1595
1639
  cmdArgs.push(args.scene);
@@ -2287,6 +2331,27 @@ class GodotServer {
2287
2331
  }
2288
2332
  return null;
2289
2333
  }
2334
+ sanitizeGodotStderr(stderr) {
2335
+ if (!stderr) {
2336
+ return stderr;
2337
+ }
2338
+ const ignoredPatterns = [
2339
+ /WARNING: ObjectDB instances leaked at exit/i,
2340
+ /at:\s+cleanup\s+\(core\/object\/object\.cpp:/i,
2341
+ /ERROR:\s+\d+\s+resources still in use at exit/i,
2342
+ /at:\s+clear\s+\(core\/io\/resource\.cpp:/i,
2343
+ ];
2344
+ const filteredLines = stderr
2345
+ .split(/\r?\n/)
2346
+ .filter((line) => {
2347
+ const trimmed = line.trim();
2348
+ if (!trimmed) {
2349
+ return false;
2350
+ }
2351
+ return !ignoredPatterns.some((pattern) => pattern.test(trimmed));
2352
+ });
2353
+ return filteredLines.join('\n').trim();
2354
+ }
2290
2355
  /**
2291
2356
  * Capture/update current intent snapshot
2292
2357
  */
@@ -4646,6 +4711,80 @@ class GodotServer {
4646
4711
  // ============================================
4647
4712
  // Project Search Handlers
4648
4713
  // ============================================
4714
+ searchProjectNatively(projectPath, query, fileTypes, useRegex, caseSensitive, maxResults) {
4715
+ const normalizedExtensions = new Set(fileTypes.map((ext) => ext.replace(/^\./, '').toLowerCase()).filter(Boolean));
4716
+ const result = {
4717
+ query,
4718
+ results: [],
4719
+ summary: {
4720
+ files_searched: 0,
4721
+ files_with_matches: 0,
4722
+ total_matches: 0,
4723
+ truncated: false,
4724
+ },
4725
+ };
4726
+ const regex = useRegex ? new RegExp(query, caseSensitive ? '' : 'i') : null;
4727
+ const queryToCheck = caseSensitive ? query : query.toLowerCase();
4728
+ const visit = (dirPath) => {
4729
+ if (result.summary.total_matches >= maxResults) {
4730
+ result.summary.truncated = true;
4731
+ return;
4732
+ }
4733
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
4734
+ if (result.summary.total_matches >= maxResults) {
4735
+ result.summary.truncated = true;
4736
+ return;
4737
+ }
4738
+ if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.godot') {
4739
+ continue;
4740
+ }
4741
+ const entryPath = join(dirPath, entry.name);
4742
+ if (entry.isDirectory()) {
4743
+ visit(entryPath);
4744
+ continue;
4745
+ }
4746
+ if (!entry.isFile()) {
4747
+ continue;
4748
+ }
4749
+ const extension = entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() || '' : '';
4750
+ if (!normalizedExtensions.has(extension)) {
4751
+ continue;
4752
+ }
4753
+ result.summary.files_searched += 1;
4754
+ const content = readFileSync(entryPath, 'utf8');
4755
+ const lines = content.split('\n');
4756
+ const matches = [];
4757
+ for (let index = 0; index < lines.length; index += 1) {
4758
+ if (result.summary.total_matches >= maxResults) {
4759
+ result.summary.truncated = true;
4760
+ break;
4761
+ }
4762
+ const line = lines[index];
4763
+ const match = regex
4764
+ ? regex.exec(line)?.[0]
4765
+ : ((caseSensitive ? line : line.toLowerCase()).includes(queryToCheck) ? query : '');
4766
+ if (match) {
4767
+ matches.push({
4768
+ line: index + 1,
4769
+ content: line.trim(),
4770
+ match,
4771
+ });
4772
+ result.summary.total_matches += 1;
4773
+ }
4774
+ }
4775
+ if (matches.length > 0) {
4776
+ const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, '/');
4777
+ result.results.push({
4778
+ file: `res://${relativePath}`,
4779
+ matches,
4780
+ });
4781
+ result.summary.files_with_matches += 1;
4782
+ }
4783
+ }
4784
+ };
4785
+ visit(projectPath);
4786
+ return result;
4787
+ }
4649
4788
  /**
4650
4789
  * Handle the search_project tool
4651
4790
  */
@@ -4669,12 +4808,9 @@ class GodotServer {
4669
4808
  caseSensitive: args.caseSensitive || false,
4670
4809
  maxResults: args.maxResults || 100,
4671
4810
  };
4672
- const { stdout, stderr } = await this.executeOperation('search_project', params, args.projectPath);
4673
- if (stderr && stderr.includes('ERROR')) {
4674
- return this.createErrorResponse(`Failed to search project: ${stderr}`, ['Check if the query/regex pattern is valid']);
4675
- }
4811
+ const result = this.searchProjectNatively(args.projectPath, params.query, params.fileTypes, params.regex, params.caseSensitive, params.maxResults);
4676
4812
  return {
4677
- content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
4813
+ content: [{ type: 'text', text: JSON.stringify(result) }],
4678
4814
  };
4679
4815
  }
4680
4816
  catch (error) {
@@ -5102,146 +5238,146 @@ class GodotServer {
5102
5238
  const shaderTemplates = {
5103
5239
  medieval: {
5104
5240
  description: 'Warm stone/aged material look',
5105
- code: `shader_type spatial;
5106
- render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
5107
-
5108
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5109
- uniform float roughness : hint_range(0.0, 1.0) = 0.85;
5110
- uniform float age_factor : hint_range(0.0, 1.0) = 0.3;
5111
- uniform vec3 tint_color : source_color = vec3(0.9, 0.85, 0.75);
5112
-
5113
- void fragment() {
5114
- vec4 albedo = texture(albedo_texture, UV);
5115
- vec3 aged = mix(albedo.rgb, albedo.rgb * tint_color, age_factor);
5116
- ALBEDO = aged;
5117
- ROUGHNESS = roughness;
5118
- SPECULAR = 0.2;
5119
- METALLIC = 0.0;
5241
+ code: `shader_type spatial;
5242
+ render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
5243
+
5244
+ uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5245
+ uniform float roughness : hint_range(0.0, 1.0) = 0.85;
5246
+ uniform float age_factor : hint_range(0.0, 1.0) = 0.3;
5247
+ uniform vec3 tint_color : source_color = vec3(0.9, 0.85, 0.75);
5248
+
5249
+ void fragment() {
5250
+ vec4 albedo = texture(albedo_texture, UV);
5251
+ vec3 aged = mix(albedo.rgb, albedo.rgb * tint_color, age_factor);
5252
+ ALBEDO = aged;
5253
+ ROUGHNESS = roughness;
5254
+ SPECULAR = 0.2;
5255
+ METALLIC = 0.0;
5120
5256
  }`,
5121
5257
  },
5122
5258
  cyberpunk: {
5123
5259
  description: 'Neon glow with pulsing effect',
5124
- code: `shader_type spatial;
5125
- render_mode blend_mix, depth_draw_opaque, cull_back;
5126
-
5127
- uniform vec3 neon_color : source_color = vec3(1.0, 0.0, 0.8);
5128
- uniform float pulse_speed : hint_range(0.0, 10.0) = 2.0;
5129
- uniform float intensity : hint_range(0.0, 5.0) = 2.5;
5130
- uniform float base_brightness : hint_range(0.0, 1.0) = 0.3;
5131
-
5132
- void fragment() {
5133
- float pulse = sin(TIME * pulse_speed) * 0.5 + 0.5;
5134
- vec3 emissive = neon_color * intensity * (0.5 + pulse * 0.5);
5135
-
5136
- ALBEDO = neon_color * base_brightness;
5137
- EMISSION = emissive;
5138
- METALLIC = 0.8;
5139
- ROUGHNESS = 0.2;
5260
+ code: `shader_type spatial;
5261
+ render_mode blend_mix, depth_draw_opaque, cull_back;
5262
+
5263
+ uniform vec3 neon_color : source_color = vec3(1.0, 0.0, 0.8);
5264
+ uniform float pulse_speed : hint_range(0.0, 10.0) = 2.0;
5265
+ uniform float intensity : hint_range(0.0, 5.0) = 2.5;
5266
+ uniform float base_brightness : hint_range(0.0, 1.0) = 0.3;
5267
+
5268
+ void fragment() {
5269
+ float pulse = sin(TIME * pulse_speed) * 0.5 + 0.5;
5270
+ vec3 emissive = neon_color * intensity * (0.5 + pulse * 0.5);
5271
+
5272
+ ALBEDO = neon_color * base_brightness;
5273
+ EMISSION = emissive;
5274
+ METALLIC = 0.8;
5275
+ ROUGHNESS = 0.2;
5140
5276
  }`,
5141
5277
  },
5142
5278
  nature: {
5143
5279
  description: 'Wind sway for foliage',
5144
- code: `shader_type spatial;
5145
- render_mode blend_mix, depth_draw_opaque, cull_disabled;
5146
-
5147
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5148
- uniform float wind_strength : hint_range(0.0, 2.0) = 0.3;
5149
- uniform float wind_speed : hint_range(0.0, 5.0) = 1.5;
5150
- uniform float wind_scale : hint_range(0.1, 10.0) = 1.0;
5151
-
5152
- void vertex() {
5153
- float wind = sin(TIME * wind_speed + VERTEX.x * wind_scale + VERTEX.z * wind_scale * 0.7);
5154
- float height_factor = UV.y;
5155
- VERTEX.x += wind * wind_strength * height_factor;
5156
- VERTEX.z += wind * wind_strength * height_factor * 0.5;
5157
- }
5158
-
5159
- void fragment() {
5160
- vec4 albedo = texture(albedo_texture, UV);
5161
- ALBEDO = albedo.rgb;
5162
- ALPHA = albedo.a;
5163
- ALPHA_SCISSOR_THRESHOLD = 0.5;
5164
- ROUGHNESS = 0.8;
5280
+ code: `shader_type spatial;
5281
+ render_mode blend_mix, depth_draw_opaque, cull_disabled;
5282
+
5283
+ uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5284
+ uniform float wind_strength : hint_range(0.0, 2.0) = 0.3;
5285
+ uniform float wind_speed : hint_range(0.0, 5.0) = 1.5;
5286
+ uniform float wind_scale : hint_range(0.1, 10.0) = 1.0;
5287
+
5288
+ void vertex() {
5289
+ float wind = sin(TIME * wind_speed + VERTEX.x * wind_scale + VERTEX.z * wind_scale * 0.7);
5290
+ float height_factor = UV.y;
5291
+ VERTEX.x += wind * wind_strength * height_factor;
5292
+ VERTEX.z += wind * wind_strength * height_factor * 0.5;
5293
+ }
5294
+
5295
+ void fragment() {
5296
+ vec4 albedo = texture(albedo_texture, UV);
5297
+ ALBEDO = albedo.rgb;
5298
+ ALPHA = albedo.a;
5299
+ ALPHA_SCISSOR_THRESHOLD = 0.5;
5300
+ ROUGHNESS = 0.8;
5165
5301
  }`,
5166
5302
  },
5167
5303
  scifi: {
5168
5304
  description: 'Clean metallic with LED accents',
5169
- code: `shader_type spatial;
5170
- render_mode blend_mix, depth_draw_opaque, cull_back;
5171
-
5172
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5173
- uniform vec3 led_color : source_color = vec3(0.0, 0.8, 1.0);
5174
- uniform float led_intensity : hint_range(0.0, 3.0) = 1.5;
5175
- uniform float metallic_value : hint_range(0.0, 1.0) = 0.9;
5176
-
5177
- void fragment() {
5178
- vec4 albedo = texture(albedo_texture, UV);
5179
- float led_mask = step(0.9, albedo.r) * step(0.9, albedo.g) * step(0.9, albedo.b);
5180
-
5181
- ALBEDO = mix(albedo.rgb, albedo.rgb * 0.3, led_mask);
5182
- EMISSION = led_color * led_intensity * led_mask;
5183
- METALLIC = metallic_value;
5184
- ROUGHNESS = mix(0.3, 0.1, led_mask);
5305
+ code: `shader_type spatial;
5306
+ render_mode blend_mix, depth_draw_opaque, cull_back;
5307
+
5308
+ uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5309
+ uniform vec3 led_color : source_color = vec3(0.0, 0.8, 1.0);
5310
+ uniform float led_intensity : hint_range(0.0, 3.0) = 1.5;
5311
+ uniform float metallic_value : hint_range(0.0, 1.0) = 0.9;
5312
+
5313
+ void fragment() {
5314
+ vec4 albedo = texture(albedo_texture, UV);
5315
+ float led_mask = step(0.9, albedo.r) * step(0.9, albedo.g) * step(0.9, albedo.b);
5316
+
5317
+ ALBEDO = mix(albedo.rgb, albedo.rgb * 0.3, led_mask);
5318
+ EMISSION = led_color * led_intensity * led_mask;
5319
+ METALLIC = metallic_value;
5320
+ ROUGHNESS = mix(0.3, 0.1, led_mask);
5185
5321
  }`,
5186
5322
  },
5187
5323
  horror: {
5188
5324
  description: 'Dark with subtle pulsing shadows',
5189
- code: `shader_type spatial;
5190
- render_mode blend_mix, depth_draw_opaque, cull_back;
5191
-
5192
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5193
- uniform float darkness : hint_range(0.0, 1.0) = 0.6;
5194
- uniform float pulse_speed : hint_range(0.0, 5.0) = 0.5;
5195
- uniform vec3 shadow_tint : source_color = vec3(0.1, 0.0, 0.15);
5196
-
5197
- void fragment() {
5198
- float pulse = sin(TIME * pulse_speed) * 0.1 + 0.9;
5199
- vec4 albedo = texture(albedo_texture, UV);
5200
- vec3 darkened = mix(albedo.rgb, shadow_tint, darkness);
5201
-
5202
- ALBEDO = darkened * pulse;
5203
- ROUGHNESS = 0.9;
5204
- METALLIC = 0.0;
5325
+ code: `shader_type spatial;
5326
+ render_mode blend_mix, depth_draw_opaque, cull_back;
5327
+
5328
+ uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5329
+ uniform float darkness : hint_range(0.0, 1.0) = 0.6;
5330
+ uniform float pulse_speed : hint_range(0.0, 5.0) = 0.5;
5331
+ uniform vec3 shadow_tint : source_color = vec3(0.1, 0.0, 0.15);
5332
+
5333
+ void fragment() {
5334
+ float pulse = sin(TIME * pulse_speed) * 0.1 + 0.9;
5335
+ vec4 albedo = texture(albedo_texture, UV);
5336
+ vec3 darkened = mix(albedo.rgb, shadow_tint, darkness);
5337
+
5338
+ ALBEDO = darkened * pulse;
5339
+ ROUGHNESS = 0.9;
5340
+ METALLIC = 0.0;
5205
5341
  }`,
5206
5342
  },
5207
5343
  cartoon: {
5208
5344
  description: 'Cel-shaded toon look',
5209
- code: `shader_type spatial;
5210
- render_mode blend_mix, depth_draw_opaque, cull_back;
5211
-
5212
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5213
- uniform vec3 outline_color : source_color = vec3(0.0, 0.0, 0.0);
5214
- uniform float shade_levels : hint_range(2.0, 8.0) = 3.0;
5215
-
5216
- void fragment() {
5217
- vec4 albedo = texture(albedo_texture, UV);
5218
- ALBEDO = albedo.rgb;
5219
- ROUGHNESS = 1.0;
5220
- SPECULAR = 0.0;
5221
- }
5222
-
5223
- void light() {
5224
- float NdotL = dot(NORMAL, LIGHT);
5225
- float stepped = floor(NdotL * shade_levels) / shade_levels;
5226
- DIFFUSE_LIGHT += stepped * ATTENUATION * LIGHT_COLOR;
5345
+ code: `shader_type spatial;
5346
+ render_mode blend_mix, depth_draw_opaque, cull_back;
5347
+
5348
+ uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5349
+ uniform vec3 outline_color : source_color = vec3(0.0, 0.0, 0.0);
5350
+ uniform float shade_levels : hint_range(2.0, 8.0) = 3.0;
5351
+
5352
+ void fragment() {
5353
+ vec4 albedo = texture(albedo_texture, UV);
5354
+ ALBEDO = albedo.rgb;
5355
+ ROUGHNESS = 1.0;
5356
+ SPECULAR = 0.0;
5357
+ }
5358
+
5359
+ void light() {
5360
+ float NdotL = dot(NORMAL, LIGHT);
5361
+ float stepped = floor(NdotL * shade_levels) / shade_levels;
5362
+ DIFFUSE_LIGHT += stepped * ATTENUATION * LIGHT_COLOR;
5227
5363
  }`,
5228
5364
  },
5229
5365
  };
5230
5366
  const effectTemplates = {
5231
- glow: `
5232
- uniform float glow_power : hint_range(0.0, 5.0) = 1.5;
5367
+ glow: `
5368
+ uniform float glow_power : hint_range(0.0, 5.0) = 1.5;
5233
5369
  // Add to fragment(): EMISSION += ALBEDO * glow_power;`,
5234
- hologram: `
5235
- uniform float scan_speed : hint_range(0.0, 10.0) = 2.0;
5236
- uniform float scan_lines : hint_range(10.0, 100.0) = 50.0;
5237
- // Add to fragment():
5238
- // float scan = sin(UV.y * scan_lines + TIME * scan_speed) * 0.5 + 0.5;
5370
+ hologram: `
5371
+ uniform float scan_speed : hint_range(0.0, 10.0) = 2.0;
5372
+ uniform float scan_lines : hint_range(10.0, 100.0) = 50.0;
5373
+ // Add to fragment():
5374
+ // float scan = sin(UV.y * scan_lines + TIME * scan_speed) * 0.5 + 0.5;
5239
5375
  // ALPHA = 0.7 * scan;`,
5240
- dissolve: `
5241
- uniform sampler2D noise_texture : filter_linear;
5242
- uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
5243
- // Add to fragment():
5244
- // float noise = texture(noise_texture, UV).r;
5376
+ dissolve: `
5377
+ uniform sampler2D noise_texture : filter_linear;
5378
+ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
5379
+ // Add to fragment():
5380
+ // float noise = texture(noise_texture, UV).r;
5245
5381
  // if (noise < dissolve_amount) discard;`,
5246
5382
  };
5247
5383
  const theme = args.theme;