gopeak 2.3.7 → 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
  });
@@ -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);
@@ -4667,6 +4711,80 @@ class GodotServer {
4667
4711
  // ============================================
4668
4712
  // Project Search Handlers
4669
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
+ }
4670
4788
  /**
4671
4789
  * Handle the search_project tool
4672
4790
  */
@@ -4690,12 +4808,9 @@ class GodotServer {
4690
4808
  caseSensitive: args.caseSensitive || false,
4691
4809
  maxResults: args.maxResults || 100,
4692
4810
  };
4693
- const { stdout, stderr } = await this.executeOperation('search_project', params, args.projectPath);
4694
- if (stderr && stderr.includes('ERROR')) {
4695
- return this.createErrorResponse(`Failed to search project: ${stderr}`, ['Check if the query/regex pattern is valid']);
4696
- }
4811
+ const result = this.searchProjectNatively(args.projectPath, params.query, params.fileTypes, params.regex, params.caseSensitive, params.maxResults);
4697
4812
  return {
4698
- content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
4813
+ content: [{ type: 'text', text: JSON.stringify(result) }],
4699
4814
  };
4700
4815
  }
4701
4816
  catch (error) {
@@ -5123,146 +5238,146 @@ class GodotServer {
5123
5238
  const shaderTemplates = {
5124
5239
  medieval: {
5125
5240
  description: 'Warm stone/aged material look',
5126
- code: `shader_type spatial;
5127
- render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
5128
-
5129
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5130
- uniform float roughness : hint_range(0.0, 1.0) = 0.85;
5131
- uniform float age_factor : hint_range(0.0, 1.0) = 0.3;
5132
- uniform vec3 tint_color : source_color = vec3(0.9, 0.85, 0.75);
5133
-
5134
- void fragment() {
5135
- vec4 albedo = texture(albedo_texture, UV);
5136
- vec3 aged = mix(albedo.rgb, albedo.rgb * tint_color, age_factor);
5137
- ALBEDO = aged;
5138
- ROUGHNESS = roughness;
5139
- SPECULAR = 0.2;
5140
- 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;
5141
5256
  }`,
5142
5257
  },
5143
5258
  cyberpunk: {
5144
5259
  description: 'Neon glow with pulsing effect',
5145
- code: `shader_type spatial;
5146
- render_mode blend_mix, depth_draw_opaque, cull_back;
5147
-
5148
- uniform vec3 neon_color : source_color = vec3(1.0, 0.0, 0.8);
5149
- uniform float pulse_speed : hint_range(0.0, 10.0) = 2.0;
5150
- uniform float intensity : hint_range(0.0, 5.0) = 2.5;
5151
- uniform float base_brightness : hint_range(0.0, 1.0) = 0.3;
5152
-
5153
- void fragment() {
5154
- float pulse = sin(TIME * pulse_speed) * 0.5 + 0.5;
5155
- vec3 emissive = neon_color * intensity * (0.5 + pulse * 0.5);
5156
-
5157
- ALBEDO = neon_color * base_brightness;
5158
- EMISSION = emissive;
5159
- METALLIC = 0.8;
5160
- 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;
5161
5276
  }`,
5162
5277
  },
5163
5278
  nature: {
5164
5279
  description: 'Wind sway for foliage',
5165
- code: `shader_type spatial;
5166
- render_mode blend_mix, depth_draw_opaque, cull_disabled;
5167
-
5168
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5169
- uniform float wind_strength : hint_range(0.0, 2.0) = 0.3;
5170
- uniform float wind_speed : hint_range(0.0, 5.0) = 1.5;
5171
- uniform float wind_scale : hint_range(0.1, 10.0) = 1.0;
5172
-
5173
- void vertex() {
5174
- float wind = sin(TIME * wind_speed + VERTEX.x * wind_scale + VERTEX.z * wind_scale * 0.7);
5175
- float height_factor = UV.y;
5176
- VERTEX.x += wind * wind_strength * height_factor;
5177
- VERTEX.z += wind * wind_strength * height_factor * 0.5;
5178
- }
5179
-
5180
- void fragment() {
5181
- vec4 albedo = texture(albedo_texture, UV);
5182
- ALBEDO = albedo.rgb;
5183
- ALPHA = albedo.a;
5184
- ALPHA_SCISSOR_THRESHOLD = 0.5;
5185
- 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;
5186
5301
  }`,
5187
5302
  },
5188
5303
  scifi: {
5189
5304
  description: 'Clean metallic with LED accents',
5190
- code: `shader_type spatial;
5191
- render_mode blend_mix, depth_draw_opaque, cull_back;
5192
-
5193
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5194
- uniform vec3 led_color : source_color = vec3(0.0, 0.8, 1.0);
5195
- uniform float led_intensity : hint_range(0.0, 3.0) = 1.5;
5196
- uniform float metallic_value : hint_range(0.0, 1.0) = 0.9;
5197
-
5198
- void fragment() {
5199
- vec4 albedo = texture(albedo_texture, UV);
5200
- float led_mask = step(0.9, albedo.r) * step(0.9, albedo.g) * step(0.9, albedo.b);
5201
-
5202
- ALBEDO = mix(albedo.rgb, albedo.rgb * 0.3, led_mask);
5203
- EMISSION = led_color * led_intensity * led_mask;
5204
- METALLIC = metallic_value;
5205
- 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);
5206
5321
  }`,
5207
5322
  },
5208
5323
  horror: {
5209
5324
  description: 'Dark with subtle pulsing shadows',
5210
- code: `shader_type spatial;
5211
- render_mode blend_mix, depth_draw_opaque, cull_back;
5212
-
5213
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5214
- uniform float darkness : hint_range(0.0, 1.0) = 0.6;
5215
- uniform float pulse_speed : hint_range(0.0, 5.0) = 0.5;
5216
- uniform vec3 shadow_tint : source_color = vec3(0.1, 0.0, 0.15);
5217
-
5218
- void fragment() {
5219
- float pulse = sin(TIME * pulse_speed) * 0.1 + 0.9;
5220
- vec4 albedo = texture(albedo_texture, UV);
5221
- vec3 darkened = mix(albedo.rgb, shadow_tint, darkness);
5222
-
5223
- ALBEDO = darkened * pulse;
5224
- ROUGHNESS = 0.9;
5225
- 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;
5226
5341
  }`,
5227
5342
  },
5228
5343
  cartoon: {
5229
5344
  description: 'Cel-shaded toon look',
5230
- code: `shader_type spatial;
5231
- render_mode blend_mix, depth_draw_opaque, cull_back;
5232
-
5233
- uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
5234
- uniform vec3 outline_color : source_color = vec3(0.0, 0.0, 0.0);
5235
- uniform float shade_levels : hint_range(2.0, 8.0) = 3.0;
5236
-
5237
- void fragment() {
5238
- vec4 albedo = texture(albedo_texture, UV);
5239
- ALBEDO = albedo.rgb;
5240
- ROUGHNESS = 1.0;
5241
- SPECULAR = 0.0;
5242
- }
5243
-
5244
- void light() {
5245
- float NdotL = dot(NORMAL, LIGHT);
5246
- float stepped = floor(NdotL * shade_levels) / shade_levels;
5247
- 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;
5248
5363
  }`,
5249
5364
  },
5250
5365
  };
5251
5366
  const effectTemplates = {
5252
- glow: `
5253
- 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;
5254
5369
  // Add to fragment(): EMISSION += ALBEDO * glow_power;`,
5255
- hologram: `
5256
- uniform float scan_speed : hint_range(0.0, 10.0) = 2.0;
5257
- uniform float scan_lines : hint_range(10.0, 100.0) = 50.0;
5258
- // Add to fragment():
5259
- // 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;
5260
5375
  // ALPHA = 0.7 * scan;`,
5261
- dissolve: `
5262
- uniform sampler2D noise_texture : filter_linear;
5263
- uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
5264
- // Add to fragment():
5265
- // 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;
5266
5381
  // if (noise < dissolve_amount) discard;`,
5267
5382
  };
5268
5383
  const theme = args.theme;