@loonylabs/create-game 0.1.0

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 (166) hide show
  1. package/bin/create-game.js +213 -0
  2. package/package.json +18 -0
  3. package/template/.claude/skills/aigdtk-create-game-stories/SKILL.md +177 -0
  4. package/template/.claude/skills/aigdtk-create-game-stories/story-template.md +85 -0
  5. package/template/.claude/skills/aigdtk-implement-game-stories/SKILL.md +129 -0
  6. package/template/.claude/skills/aigdtk-new-game/SKILL.md +126 -0
  7. package/template/.claude/skills/aigdtk-shared/ascii-grammar.md +133 -0
  8. package/template/.claude/skills/aigdtk-shared/enemies.md +112 -0
  9. package/template/.claude/skills/aigdtk-shared/framework.md +93 -0
  10. package/template/.claude/skills/aigdtk-shared/visuals.md +125 -0
  11. package/template/apps/client/index.html +14 -0
  12. package/template/apps/client/package.json +31 -0
  13. package/template/apps/client/public/assets/audio/enemy_killed.wav +0 -0
  14. package/template/apps/client/src/components/App.svelte +290 -0
  15. package/template/apps/client/src/components/CraftingPanel.svelte +253 -0
  16. package/template/apps/client/src/components/DevPanel.svelte +180 -0
  17. package/template/apps/client/src/components/DungeonClearOverlay.svelte +53 -0
  18. package/template/apps/client/src/components/EquipmentPanel.svelte +191 -0
  19. package/template/apps/client/src/components/HealthBar.svelte +50 -0
  20. package/template/apps/client/src/components/Hub/BackToHubButton.svelte +37 -0
  21. package/template/apps/client/src/components/Hub/ExperienceCard.svelte +115 -0
  22. package/template/apps/client/src/components/Hub/Hub.svelte +88 -0
  23. package/template/apps/client/src/components/Inventory.svelte +174 -0
  24. package/template/apps/client/src/components/Runner/RunnerDeathScreen.svelte +182 -0
  25. package/template/apps/client/src/components/Runner/RunnerHUD.svelte +157 -0
  26. package/template/apps/client/src/components/Shooter/DamageNumbers.svelte +96 -0
  27. package/template/apps/client/src/components/Shooter/GameOverScreen.svelte +109 -0
  28. package/template/apps/client/src/components/Shooter/ShooterHUD.svelte +95 -0
  29. package/template/apps/client/src/components/SkillBar.svelte +146 -0
  30. package/template/apps/client/src/components/ToastSystem.svelte +158 -0
  31. package/template/apps/client/src/game.ts +918 -0
  32. package/template/apps/client/src/input.ts +92 -0
  33. package/template/apps/client/src/lib/audio/CombatDetector.test.ts +59 -0
  34. package/template/apps/client/src/lib/audio/CombatDetector.ts +53 -0
  35. package/template/apps/client/src/lib/audio/MusicManager.ts +137 -0
  36. package/template/apps/client/src/lib/audio/SoundManager.ts +59 -0
  37. package/template/apps/client/src/lib/audio/index.ts +9 -0
  38. package/template/apps/client/src/main.ts +32 -0
  39. package/template/apps/client/src/renderer/basecamp.ts +126 -0
  40. package/template/apps/client/src/renderer/dungeon.ts +250 -0
  41. package/template/apps/client/src/renderer/dungeonPortal.ts +73 -0
  42. package/template/apps/client/src/renderer/dungeonZone.ts +301 -0
  43. package/template/apps/client/src/renderer/entities.ts +197 -0
  44. package/template/apps/client/src/renderer/runnerTrack.ts +221 -0
  45. package/template/apps/client/src/renderer/shaders/SkyShader.ts +190 -0
  46. package/template/apps/client/src/renderer/shaders/TerrainShaderMaterial.ts +133 -0
  47. package/template/apps/client/src/renderer/shaders/floor.frag.glsl.ts +17 -0
  48. package/template/apps/client/src/renderer/shaders/shaderConfig.ts +18 -0
  49. package/template/apps/client/src/renderer/shaders/spawn.frag.glsl.ts +19 -0
  50. package/template/apps/client/src/renderer/shaders/terrain.frag.glsl.ts +314 -0
  51. package/template/apps/client/src/renderer/shaders/terrain.vert.glsl.ts +16 -0
  52. package/template/apps/client/src/renderer/shaders/wall.frag.glsl.ts +20 -0
  53. package/template/apps/client/src/renderer/shooterArena.ts +102 -0
  54. package/template/apps/client/src/renderer/voxelChunkStreamer.ts +79 -0
  55. package/template/apps/client/src/renderer/voxelMesh.ts +86 -0
  56. package/template/apps/client/src/renderer/voxelTerrain.ts +74 -0
  57. package/template/apps/client/src/socket.ts +268 -0
  58. package/template/apps/client/src/store.ts +74 -0
  59. package/template/apps/client/src/style.css +60 -0
  60. package/template/apps/client/tsconfig.json +11 -0
  61. package/template/apps/client/vite.config.ts +10 -0
  62. package/template/apps/client/vitest.config.ts +8 -0
  63. package/template/apps/experiences/diablo/index.ts +94 -0
  64. package/template/apps/experiences/diablo/systems/dungeonClearSystem.ts +60 -0
  65. package/template/apps/experiences/diablo/systems/enemyAISystem.ts +11 -0
  66. package/template/apps/experiences/diablo/systems/entitySyncSystem.ts +80 -0
  67. package/template/apps/experiences/diablo/systems/itemPickupSystem.ts +11 -0
  68. package/template/apps/experiences/diablo/systems/movementSystem.ts +13 -0
  69. package/template/apps/experiences/diablo/systems/physicsSystem.ts +92 -0
  70. package/template/apps/experiences/diablo/systems/transitionSystem.ts +105 -0
  71. package/template/apps/experiences/runner/data/runner-config.ts +54 -0
  72. package/template/apps/experiences/runner/index.ts +143 -0
  73. package/template/apps/experiences/runner/systems/collectibleSystem.ts +157 -0
  74. package/template/apps/experiences/runner/systems/deathSystem.ts +42 -0
  75. package/template/apps/experiences/runner/systems/entitySyncSystem.ts +59 -0
  76. package/template/apps/experiences/runner/systems/obstacleSystem.ts +91 -0
  77. package/template/apps/experiences/runner/systems/runnerPhysicsSystem.ts +82 -0
  78. package/template/apps/experiences/runner/systems/trackStreamSystem.ts +19 -0
  79. package/template/apps/experiences/runner/track/laneSystem.ts +53 -0
  80. package/template/apps/experiences/runner/track/segmentTypes.ts +141 -0
  81. package/template/apps/experiences/runner/track/trackGenerator.ts +292 -0
  82. package/template/apps/experiences/shooter/ai/aiStateMachine.ts +394 -0
  83. package/template/apps/experiences/shooter/ai/lineOfSight.ts +32 -0
  84. package/template/apps/experiences/shooter/arena/arenaGenerator.ts +101 -0
  85. package/template/apps/experiences/shooter/arena/arenaTypes.ts +49 -0
  86. package/template/apps/experiences/shooter/arena/hitscan.ts +101 -0
  87. package/template/apps/experiences/shooter/data/enemy-types.ts +108 -0
  88. package/template/apps/experiences/shooter/data/wave-definitions.ts +40 -0
  89. package/template/apps/experiences/shooter/data/weapon-config.ts +80 -0
  90. package/template/apps/experiences/shooter/index.ts +127 -0
  91. package/template/apps/experiences/shooter/systems/enemyAISystem.ts +113 -0
  92. package/template/apps/experiences/shooter/systems/entitySyncSystem.ts +68 -0
  93. package/template/apps/experiences/shooter/systems/shooterPhysicsSystem.ts +89 -0
  94. package/template/apps/experiences/shooter/systems/waveSpawnerSystem.ts +87 -0
  95. package/template/apps/experiences/shooter/systems/weaponSystem.ts +157 -0
  96. package/template/apps/game-data/src/areas/area-manifest.json +18 -0
  97. package/template/apps/game-data/src/assets/migration.test.ts +291 -0
  98. package/template/apps/game-data/src/audio/music-config.json +21 -0
  99. package/template/apps/game-data/src/audio/sound-config.json +11 -0
  100. package/template/apps/game-data/src/combat/action-types.ts +2 -0
  101. package/template/apps/game-data/src/combat/enemy-def.ts +12 -0
  102. package/template/apps/game-data/src/combat/hitboxes.ts +23 -0
  103. package/template/apps/game-data/src/dungeon/cell-types.ts +20 -0
  104. package/template/apps/game-data/src/dungeon/cell-visuals.ts +13 -0
  105. package/template/apps/game-data/src/dungeon/door-directions.ts +2 -0
  106. package/template/apps/game-data/src/enemies/enemy-defs.json +32 -0
  107. package/template/apps/game-data/src/equipment/slots.json +5 -0
  108. package/template/apps/game-data/src/events/event-defs.ts +20 -0
  109. package/template/apps/game-data/src/events/event-types.ts +10 -0
  110. package/template/apps/game-data/src/events/toast-config.json +49 -0
  111. package/template/apps/game-data/src/items/item-pool.json +13 -0
  112. package/template/apps/game-data/src/loot/item-pool.ts +14 -0
  113. package/template/apps/game-data/src/loot/rarities.ts +2 -0
  114. package/template/apps/game-data/src/physics/dungeon-physics-config.ts +12 -0
  115. package/template/apps/game-data/src/physics/jump-config.ts +17 -0
  116. package/template/apps/game-data/src/recipes/recipe-book.json +68 -0
  117. package/template/apps/game-data/src/rooms/room_basecamp.json +16 -0
  118. package/template/apps/game-data/src/rooms/room_corridor_ew.json +9 -0
  119. package/template/apps/game-data/src/rooms/room_corridor_ns.json +11 -0
  120. package/template/apps/game-data/src/rooms/room_crossroads.json +11 -0
  121. package/template/apps/game-data/src/rooms/room_dead_end.json +10 -0
  122. package/template/apps/game-data/src/rooms/room_staircase.json +12 -0
  123. package/template/apps/game-data/src/rooms/room_start.json +11 -0
  124. package/template/apps/game-data/src/skills/skill-book.json +20 -0
  125. package/template/apps/game-data/src/voxel/biome-terrain.ts +76 -0
  126. package/template/apps/game-data/src/voxel/materials.ts +45 -0
  127. package/template/apps/game-data/src/voxel/sandbox-terrain-config.ts +19 -0
  128. package/template/apps/game-data/src/world/area-config.ts +33 -0
  129. package/template/apps/game-data/src/world/biome-def.ts +15 -0
  130. package/template/apps/game-data/src/world/biomes.json +57 -0
  131. package/template/apps/game-data/src/world/movement.ts +2 -0
  132. package/template/apps/game-data/src/world/overworld-layout.test.ts +93 -0
  133. package/template/apps/game-data/src/world/overworld-layout.ts +127 -0
  134. package/template/apps/server/data/game.db +0 -0
  135. package/template/apps/server/package.json +30 -0
  136. package/template/apps/server/src/areaManager.ts +346 -0
  137. package/template/apps/server/src/db/client.ts +45 -0
  138. package/template/apps/server/src/db/schema.ts +40 -0
  139. package/template/apps/server/src/gameLoop.ts +267 -0
  140. package/template/apps/server/src/gameState.ts +3 -0
  141. package/template/apps/server/src/handlers/actionEvent.ts +55 -0
  142. package/template/apps/server/src/handlers/craftHandler.ts +59 -0
  143. package/template/apps/server/src/handlers/equipHandler.ts +73 -0
  144. package/template/apps/server/src/handlers/raycastHandler.ts +97 -0
  145. package/template/apps/server/src/handlers/skillHandler.ts +87 -0
  146. package/template/apps/server/src/handlers/terraformHandler.ts +74 -0
  147. package/template/apps/server/src/index.ts +597 -0
  148. package/template/apps/server/src/persistence.ts +135 -0
  149. package/template/apps/server/src/rooms.ts +20 -0
  150. package/template/apps/server/src/systems/dungeonPhysics.test.ts +32 -0
  151. package/template/apps/server/src/systems/dungeonPhysics.ts +16 -0
  152. package/template/apps/server/src/systems/enemyAI.ts +129 -0
  153. package/template/apps/server/src/systems/itemPickup.ts +31 -0
  154. package/template/apps/server/src/tests/areaManager.test.ts +77 -0
  155. package/template/apps/server/src/tests/diablo-experience.test.ts +60 -0
  156. package/template/apps/server/src/tests/runner-experience.test.ts +273 -0
  157. package/template/apps/server/src/tests/runner-powerups-scoring.test.ts +221 -0
  158. package/template/apps/server/src/tests/server.integration.test.ts +92 -0
  159. package/template/apps/server/src/tests/shooter-enemy-ai.test.ts +328 -0
  160. package/template/apps/server/src/tests/shooter-experience.test.ts +281 -0
  161. package/template/apps/server/src/tests/voxelChunkCache.test.ts +29 -0
  162. package/template/apps/server/src/tests/voxelSandbox.test.ts +133 -0
  163. package/template/apps/server/src/voxelChunkCache.ts +31 -0
  164. package/template/apps/server/src/voxelPlayerState.ts +23 -0
  165. package/template/apps/server/tsconfig.json +17 -0
  166. package/template/apps/server/vitest.config.ts +8 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @file TerrainShaderMaterial.ts
3
+ * Custom ShaderMaterial for voxel terrain with procedural noise,
4
+ * material blending, height tinting, normal perturbation, and LOD.
5
+ */
6
+
7
+ import * as THREE from 'three';
8
+ import { TERRAIN_VERT_SHADER } from './terrain.vert.glsl.js';
9
+ import { TERRAIN_FRAG_SHADER } from './terrain.frag.glsl.js';
10
+
11
+ export interface VoxelMaterialVisual {
12
+ color: [number, number, number]; // RGB 0-1
13
+ noiseScale: number;
14
+ noiseStrength: number;
15
+ noiseType?: 'fbm' | 'cellular' | 'ridged';
16
+ fbmOctaves?: number;
17
+ fbmLacunarity?: number;
18
+ fbmGain?: number;
19
+ heightTintLow?: [number, number, number];
20
+ heightTintHigh?: [number, number, number];
21
+ bumpStrength?: number;
22
+ }
23
+
24
+ const QUALITY_PRESETS = [
25
+ { lodNear: 16, lodMid: 32 }, // Low
26
+ { lodNear: 32, lodMid: 64 }, // Medium
27
+ { lodNear: 48, lodMid: 96 }, // High
28
+ { lodNear: 80, lodMid: 160 }, // Ultra
29
+ ];
30
+
31
+ /**
32
+ * Procedural terrain material using custom GLSL shaders.
33
+ * Supports up to 8 material types with per-material noise,
34
+ * smooth blending, height tinting, normal perturbation, and LOD.
35
+ */
36
+ export class TerrainShaderMaterial extends THREE.ShaderMaterial {
37
+ constructor(
38
+ materialVisuals: Record<number, VoxelMaterialVisual>,
39
+ options?: {
40
+ slopeThreshold?: number;
41
+ fogColor?: THREE.Color;
42
+ fogDensity?: number;
43
+ blendNoiseScale?: number;
44
+ blendNoiseStrength?: number;
45
+ heightTintRange?: number;
46
+ qualityLevel?: number;
47
+ },
48
+ ) {
49
+ // Pack per-material uniform arrays (8 materials max)
50
+ const colorArray: THREE.Color[] = [];
51
+ const noiseTypes: number[] = [];
52
+ const noiseScales: number[] = [];
53
+ const noiseStrengths: number[] = [];
54
+ const fbmOctaves: number[] = [];
55
+ const fbmLacunarities: number[] = [];
56
+ const fbmGains: number[] = [];
57
+ const heightTintLows: THREE.Color[] = [];
58
+ const heightTintHighs: THREE.Color[] = [];
59
+ const bumpStrengths: number[] = [];
60
+
61
+ const typeMap: Record<string, number> = { fbm: 0, cellular: 1, ridged: 2 };
62
+ const zeroTint = [0, 0, 0] as [number, number, number];
63
+
64
+ for (let i = 0; i < 8; i++) {
65
+ const v = materialVisuals[i];
66
+ if (v) {
67
+ colorArray.push(new THREE.Color(v.color[0], v.color[1], v.color[2]));
68
+ noiseTypes.push(typeMap[v.noiseType ?? 'fbm'] ?? 0);
69
+ noiseScales.push(v.noiseScale);
70
+ noiseStrengths.push(v.noiseStrength);
71
+ fbmOctaves.push(v.fbmOctaves ?? 3);
72
+ fbmLacunarities.push(v.fbmLacunarity ?? 2.0);
73
+ fbmGains.push(v.fbmGain ?? 0.5);
74
+ const lo = v.heightTintLow ?? zeroTint;
75
+ const hi = v.heightTintHigh ?? zeroTint;
76
+ heightTintLows.push(new THREE.Color(lo[0], lo[1], lo[2]));
77
+ heightTintHighs.push(new THREE.Color(hi[0], hi[1], hi[2]));
78
+ bumpStrengths.push(v.bumpStrength ?? 0.0);
79
+ } else {
80
+ colorArray.push(new THREE.Color(1, 0, 1));
81
+ noiseTypes.push(0);
82
+ noiseScales.push(0.5);
83
+ noiseStrengths.push(0.15);
84
+ fbmOctaves.push(3);
85
+ fbmLacunarities.push(2.0);
86
+ fbmGains.push(0.5);
87
+ heightTintLows.push(new THREE.Color(0, 0, 0));
88
+ heightTintHighs.push(new THREE.Color(0, 0, 0));
89
+ bumpStrengths.push(0.0);
90
+ }
91
+ }
92
+
93
+ const quality = options?.qualityLevel ?? 2;
94
+ const preset = QUALITY_PRESETS[quality] ?? QUALITY_PRESETS[2];
95
+
96
+ super({
97
+ vertexShader: TERRAIN_VERT_SHADER,
98
+ fragmentShader: TERRAIN_FRAG_SHADER,
99
+ uniforms: {
100
+ uMaterialColors: { value: colorArray },
101
+ uNoiseTypes: { value: noiseTypes },
102
+ uNoiseScales: { value: noiseScales },
103
+ uNoiseStrengths: { value: noiseStrengths },
104
+ uFbmOctaves: { value: fbmOctaves },
105
+ uFbmLacunarity: { value: fbmLacunarities },
106
+ uFbmGain: { value: fbmGains },
107
+ uHeightTintLow: { value: heightTintLows },
108
+ uHeightTintHigh: { value: heightTintHighs },
109
+ uHeightTintRange: { value: options?.heightTintRange ?? 64.0 },
110
+ uBlendNoiseScale: { value: options?.blendNoiseScale ?? 2.0 },
111
+ uBlendNoiseStrength: { value: options?.blendNoiseStrength ?? 0.6 },
112
+ uBumpStrengths: { value: bumpStrengths },
113
+ uQualityLevel: { value: quality },
114
+ uLodDistances: { value: new THREE.Vector2(preset.lodNear, preset.lodMid) },
115
+ uSlopeThreshold: { value: options?.slopeThreshold ?? 0.5 },
116
+ uTime: { value: 0 },
117
+ uFogColor: { value: options?.fogColor ?? new THREE.Color(0.7, 0.8, 0.9) },
118
+ uFogDensity: { value: options?.fogDensity ?? 0.015 },
119
+ },
120
+ fog: false,
121
+ });
122
+ }
123
+
124
+ /** Update time and quality-dependent uniforms each frame. */
125
+ tick(time: number, qualityLevel?: number): void {
126
+ this.uniforms['uTime'].value = time;
127
+ if (qualityLevel !== undefined) {
128
+ this.uniforms['uQualityLevel'].value = qualityLevel;
129
+ const preset = QUALITY_PRESETS[qualityLevel] ?? QUALITY_PRESETS[2];
130
+ (this.uniforms['uLodDistances'].value as THREE.Vector2).set(preset.lodNear, preset.lodMid);
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,17 @@
1
+ import { NOISE_GLSL } from '@loonylabs/gamedev-client';
2
+
3
+ /** Fragment shader for floor/door cells. Fine stone-slab noise pattern. */
4
+ export const FLOOR_FRAG_SHADER = /* glsl */`
5
+ uniform vec3 u_baseColor;
6
+ uniform float u_noiseScale;
7
+ varying vec2 v_uv;
8
+
9
+ ${NOISE_GLSL}
10
+
11
+ void main() {
12
+ float n = valueNoise(v_uv * u_noiseScale);
13
+ float n2 = valueNoise(v_uv * u_noiseScale * 3.0) * 0.3;
14
+ vec3 color = u_baseColor * (0.7 + n * 0.3 + n2);
15
+ gl_FragColor = vec4(color, 1.0);
16
+ }
17
+ `;
@@ -0,0 +1,18 @@
1
+ import * as THREE from 'three';
2
+ import type { ShaderConfig } from '@loonylabs/gamedev-client';
3
+ import { FLOOR_FRAG_SHADER } from './floor.frag.glsl.js';
4
+ import { WALL_FRAG_SHADER } from './wall.frag.glsl.js';
5
+ import { SPAWN_FRAG_SHADER } from './spawn.frag.glsl.js';
6
+
7
+ /** Game-specific shader config with optional glow color. */
8
+ export interface DungeonShaderEntry extends ShaderConfig {
9
+ glowColor?: THREE.Color;
10
+ }
11
+
12
+ /** Game-specific shader configurations keyed by cell type. */
13
+ export const DUNGEON_SHADERS: Record<string, DungeonShaderEntry> = {
14
+ floor: { fragmentShader: FLOOR_FRAG_SHADER, noiseScale: 6.0 },
15
+ wall: { fragmentShader: WALL_FRAG_SHADER, noiseScale: 3.0 },
16
+ spawn: { fragmentShader: SPAWN_FRAG_SHADER, noiseScale: 4.0, glowColor: new THREE.Color(0xffcc44) },
17
+ door: { fragmentShader: FLOOR_FRAG_SHADER, noiseScale: 5.0 },
18
+ };
@@ -0,0 +1,19 @@
1
+ import { NOISE_GLSL } from '@loonylabs/gamedev-client';
2
+
3
+ /** Fragment shader for spawn cell. Pulsing emissive gold glow over stone base. */
4
+ export const SPAWN_FRAG_SHADER = /* glsl */`
5
+ uniform vec3 u_baseColor;
6
+ uniform vec3 u_glowColor;
7
+ uniform float u_noiseScale;
8
+ uniform float u_time;
9
+ varying vec2 v_uv;
10
+
11
+ ${NOISE_GLSL}
12
+
13
+ void main() {
14
+ float n = valueNoise(v_uv * u_noiseScale);
15
+ float pulse = sin(u_time * 2.0) * 0.5 + 0.5;
16
+ vec3 color = mix(u_baseColor * (0.7 + n * 0.3), u_glowColor, pulse * 0.4 + n * 0.1);
17
+ gl_FragColor = vec4(color, 1.0);
18
+ }
19
+ `;
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Terrain fragment shader — procedural material rendering.
3
+ *
4
+ * Features:
5
+ * - Per-material noise type: FBM, Cellular (Voronoi), or Ridged
6
+ * - Smooth material blending with noise-perturbed transitions
7
+ * - Height-based color tinting per material
8
+ * - Normal perturbation for surface bumps (per-material bumpStrength)
9
+ * - Distance-based LOD (fewer octaves + skip perturbation at distance)
10
+ * - Quality level control (Low/Medium/High/Ultra)
11
+ * - Slope-based rock blending
12
+ */
13
+ export const TERRAIN_FRAG_SHADER = /* glsl */ `
14
+ precision highp float;
15
+
16
+ uniform vec3 uMaterialColors[8];
17
+ uniform float uNoiseTypes[8]; // 0.0=fbm, 1.0=cellular, 2.0=ridged
18
+ uniform float uNoiseScales[8];
19
+ uniform float uNoiseStrengths[8];
20
+ uniform float uFbmOctaves[8];
21
+ uniform float uFbmLacunarity[8];
22
+ uniform float uFbmGain[8];
23
+ uniform vec3 uHeightTintLow[8];
24
+ uniform vec3 uHeightTintHigh[8];
25
+ uniform float uHeightTintRange;
26
+ uniform float uBlendNoiseScale;
27
+ uniform float uBlendNoiseStrength;
28
+ uniform float uBumpStrengths[8];
29
+ uniform float uQualityLevel; // 0=low, 1=medium, 2=high, 3=ultra
30
+ uniform vec2 uLodDistances; // x=near, y=mid
31
+ uniform float uSlopeThreshold;
32
+ uniform float uTime;
33
+ uniform vec3 uFogColor;
34
+ uniform float uFogDensity;
35
+
36
+ varying vec3 vWorldPosition;
37
+ varying vec3 vNormal;
38
+ varying float vMaterialIndex;
39
+
40
+ // ---- Simplex-like noise (hash-based, fast) ----
41
+
42
+ vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
43
+ vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
44
+ vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
45
+ vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
46
+
47
+ float snoise(vec3 v) {
48
+ const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
49
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
50
+
51
+ vec3 i = floor(v + dot(v, C.yyy));
52
+ vec3 x0 = v - i + dot(i, C.xxx);
53
+
54
+ vec3 g = step(x0.yzx, x0.xyz);
55
+ vec3 l = 1.0 - g;
56
+ vec3 i1 = min(g.xyz, l.zxy);
57
+ vec3 i2 = max(g.xyz, l.zxy);
58
+
59
+ vec3 x1 = x0 - i1 + C.xxx;
60
+ vec3 x2 = x0 - i2 + C.yyy;
61
+ vec3 x3 = x0 - D.yyy;
62
+
63
+ i = mod289(i);
64
+ vec4 p = permute(permute(permute(
65
+ i.z + vec4(0.0, i1.z, i2.z, 1.0))
66
+ + i.y + vec4(0.0, i1.y, i2.y, 1.0))
67
+ + i.x + vec4(0.0, i1.x, i2.x, 1.0));
68
+
69
+ float n_ = 0.142857142857;
70
+ vec3 ns = n_ * D.wyz - D.xzx;
71
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
72
+ vec4 x_ = floor(j * ns.z);
73
+ vec4 y_ = floor(j - 7.0 * x_);
74
+ vec4 x = x_ * ns.x + ns.yyyy;
75
+ vec4 y = y_ * ns.x + ns.yyyy;
76
+ vec4 h = 1.0 - abs(x) - abs(y);
77
+ vec4 b0 = vec4(x.xy, y.xy);
78
+ vec4 b1 = vec4(x.zw, y.zw);
79
+ vec4 s0 = floor(b0) * 2.0 + 1.0;
80
+ vec4 s1 = floor(b1) * 2.0 + 1.0;
81
+ vec4 sh = -step(h, vec4(0.0));
82
+ vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
83
+ vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
84
+ vec3 p0 = vec3(a0.xy, h.x);
85
+ vec3 p1 = vec3(a0.zw, h.y);
86
+ vec3 p2 = vec3(a1.xy, h.z);
87
+ vec3 p3 = vec3(a1.zw, h.w);
88
+
89
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
90
+ p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
91
+
92
+ vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
93
+ m = m * m;
94
+ return 42.0 * dot(m * m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
95
+ }
96
+
97
+ // ---- Hash function for cellular noise ----
98
+
99
+ vec3 hash33(vec3 p) {
100
+ p = vec3(
101
+ dot(p, vec3(127.1, 311.7, 74.7)),
102
+ dot(p, vec3(269.5, 183.3, 246.1)),
103
+ dot(p, vec3(113.5, 271.9, 124.6))
104
+ );
105
+ return fract(sin(p) * 43758.5453123);
106
+ }
107
+
108
+ // ---- FBM — fractal Brownian motion ----
109
+
110
+ float fbm(vec3 p, float scale, int octaves, float lacunarity, float gain) {
111
+ float value = 0.0;
112
+ float amplitude = 1.0;
113
+ float frequency = scale;
114
+ float maxAmp = 0.0;
115
+ for (int i = 0; i < 6; i++) {
116
+ if (i >= octaves) break;
117
+ value += amplitude * snoise(p * frequency);
118
+ maxAmp += amplitude;
119
+ frequency *= lacunarity;
120
+ amplitude *= gain;
121
+ }
122
+ return value / maxAmp;
123
+ }
124
+
125
+ // ---- Cellular / Voronoi noise ----
126
+
127
+ float cellular(vec3 p, float scale) {
128
+ vec3 sp = p * scale;
129
+ vec3 i = floor(sp);
130
+ vec3 f = fract(sp);
131
+ float minDist = 1.0;
132
+ for (int x = -1; x <= 1; x++) {
133
+ for (int y = -1; y <= 1; y++) {
134
+ for (int z = -1; z <= 1; z++) {
135
+ vec3 neighbor = vec3(float(x), float(y), float(z));
136
+ vec3 point = hash33(i + neighbor);
137
+ vec3 diff = neighbor + point - f;
138
+ float d = dot(diff, diff);
139
+ minDist = min(minDist, d);
140
+ }
141
+ }
142
+ }
143
+ return sqrt(minDist) * 2.0 - 1.0;
144
+ }
145
+
146
+ // ---- Ridged noise ----
147
+
148
+ float ridged(vec3 p, float scale, int octaves, float lacunarity, float gain) {
149
+ float value = 0.0;
150
+ float amplitude = 1.0;
151
+ float frequency = scale;
152
+ float maxAmp = 0.0;
153
+ for (int i = 0; i < 6; i++) {
154
+ if (i >= octaves) break;
155
+ float n = 1.0 - abs(snoise(p * frequency));
156
+ value += amplitude * n * n;
157
+ maxAmp += amplitude;
158
+ frequency *= lacunarity;
159
+ amplitude *= gain;
160
+ }
161
+ return value / maxAmp * 2.0 - 1.0;
162
+ }
163
+
164
+ // ---- LOD helpers ----
165
+
166
+ int getMaxOctaves() {
167
+ int q = int(uQualityLevel + 0.5);
168
+ if (q == 0) return 2;
169
+ if (q == 1) return 3;
170
+ if (q == 2) return 4;
171
+ return 6;
172
+ }
173
+
174
+ int getEffectiveOctaves(int baseOctaves, float dist) {
175
+ int maxOct = getMaxOctaves();
176
+ // Far: minimal noise
177
+ if (dist > uLodDistances.y) return 1;
178
+ // Mid: reduced
179
+ if (dist > uLodDistances.x) return min(baseOctaves, max(maxOct - 2, 1));
180
+ // Near: full
181
+ return min(baseOctaves, maxOct);
182
+ }
183
+
184
+ bool shouldPerturb(float dist) {
185
+ return uQualityLevel >= 1.5 && dist < uLodDistances.x;
186
+ }
187
+
188
+ bool shouldBlend(float dist) {
189
+ return uQualityLevel >= 0.5 && dist < uLodDistances.y;
190
+ }
191
+
192
+ // ---- Per-material noise dispatch (LOD-aware) ----
193
+
194
+ float getNoiseForMaterial(int matIdx, vec3 worldPos, float dist) {
195
+ float scale = uNoiseScales[matIdx];
196
+ int noiseType = int(uNoiseTypes[matIdx] + 0.5);
197
+ int baseOctaves = int(uFbmOctaves[matIdx] + 0.5);
198
+ int octaves = getEffectiveOctaves(baseOctaves, dist);
199
+ float lac = uFbmLacunarity[matIdx];
200
+ float gain = uFbmGain[matIdx];
201
+
202
+ float n;
203
+ if (noiseType == 1) {
204
+ // Cellular is expensive — skip at far distance
205
+ if (dist > uLodDistances.y) {
206
+ n = snoise(worldPos * scale);
207
+ } else {
208
+ n = cellular(worldPos, scale);
209
+ }
210
+ } else if (noiseType == 2) {
211
+ n = ridged(worldPos, scale, octaves, lac, gain);
212
+ } else {
213
+ n = fbm(worldPos, scale, octaves, lac, gain);
214
+ }
215
+ return n * uNoiseStrengths[matIdx];
216
+ }
217
+
218
+ // ---- Height tint ----
219
+
220
+ vec3 applyHeightTint(vec3 color, int matIdx, float worldY) {
221
+ float heightFactor = clamp(worldY / uHeightTintRange, 0.0, 1.0);
222
+ vec3 tint = mix(uHeightTintLow[matIdx], uHeightTintHigh[matIdx], heightFactor);
223
+ return color * (1.0 + tint);
224
+ }
225
+
226
+ // ---- Full material color ----
227
+
228
+ vec3 getMaterialColor(int matIdx, vec3 worldPos, float dist) {
229
+ vec3 base = uMaterialColors[matIdx];
230
+ float noise = getNoiseForMaterial(matIdx, worldPos, dist);
231
+ base *= 1.0 + noise;
232
+ base = applyHeightTint(base, matIdx, worldPos.y);
233
+ return base;
234
+ }
235
+
236
+ // ---- Material blending (LOD-aware) ----
237
+
238
+ vec3 blendMaterials(float rawMatIdx, vec3 worldPos, float dist) {
239
+ int matA = clamp(int(floor(rawMatIdx)), 0, 7);
240
+ int matB = clamp(matA + 1, 0, 7);
241
+ float blendFactor = fract(rawMatIdx);
242
+
243
+ // No fractional part or blending disabled — single material
244
+ if (blendFactor < 0.01 || !shouldBlend(dist)) {
245
+ return getMaterialColor(matA, worldPos, dist);
246
+ }
247
+
248
+ // Noise-perturb the blend boundary for organic, irregular edges
249
+ float blendNoise = snoise(worldPos * uBlendNoiseScale) * 0.5 + 0.5;
250
+ blendFactor = smoothstep(0.2, 0.8, blendFactor + (blendNoise - 0.5) * uBlendNoiseStrength);
251
+
252
+ vec3 colorA = getMaterialColor(matA, worldPos, dist);
253
+ vec3 colorB = getMaterialColor(matB, worldPos, dist);
254
+ return mix(colorA, colorB, blendFactor);
255
+ }
256
+
257
+ // ---- Normal perturbation ----
258
+
259
+ vec3 perturbNormal(vec3 normal, vec3 worldPos, int matIdx, float bumpStr) {
260
+ float eps = 0.1;
261
+ float scale = uNoiseScales[matIdx] * 2.0;
262
+
263
+ // Finite differences: sample noise at 6 offset positions
264
+ float nx = snoise((worldPos + vec3(eps, 0.0, 0.0)) * scale)
265
+ - snoise((worldPos - vec3(eps, 0.0, 0.0)) * scale);
266
+ float ny = snoise((worldPos + vec3(0.0, eps, 0.0)) * scale)
267
+ - snoise((worldPos - vec3(0.0, eps, 0.0)) * scale);
268
+ float nz = snoise((worldPos + vec3(0.0, 0.0, eps)) * scale)
269
+ - snoise((worldPos - vec3(0.0, 0.0, eps)) * scale);
270
+
271
+ vec3 grad = vec3(nx, ny, nz) * bumpStr;
272
+ return normalize(normal + grad);
273
+ }
274
+
275
+ void main() {
276
+ float dist = length(vWorldPosition - cameraPosition);
277
+
278
+ // 1. Blend between adjacent materials (LOD-aware)
279
+ vec3 baseColor = blendMaterials(vMaterialIndex, vWorldPosition, dist);
280
+
281
+ // 2. Slope-based rock override with noise-perturbed boundary
282
+ float slope = 1.0 - vNormal.y;
283
+ float slopeNoise = snoise(vWorldPosition * 1.5) * 0.15;
284
+ float rockBlend = smoothstep(uSlopeThreshold - 0.15, uSlopeThreshold + 0.25, slope + slopeNoise);
285
+ if (rockBlend > 0.0) {
286
+ vec3 rockColor = getMaterialColor(3, vWorldPosition, dist);
287
+ baseColor = mix(baseColor, rockColor, rockBlend);
288
+ }
289
+
290
+ // 3. Normal perturbation for lighting (near + high quality only)
291
+ vec3 lightNormal = vNormal;
292
+ if (shouldPerturb(dist)) {
293
+ int matIdx = clamp(int(floor(vMaterialIndex + 0.5)), 0, 7);
294
+ float bumpStr = uBumpStrengths[matIdx];
295
+ if (bumpStr > 0.0) {
296
+ // Fade perturbation smoothly toward LOD boundary
297
+ float perturbFade = 1.0 - smoothstep(uLodDistances.x * 0.7, uLodDistances.x, dist);
298
+ lightNormal = perturbNormal(vNormal, vWorldPosition, matIdx, bumpStr * perturbFade);
299
+ }
300
+ }
301
+
302
+ // 4. Lighting: directional sun + ambient (uses perturbed normal)
303
+ vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
304
+ float diffuse = max(dot(lightNormal, lightDir), 0.0);
305
+ vec3 ambient = baseColor * 0.4;
306
+ vec3 lit = ambient + baseColor * diffuse * 0.6;
307
+
308
+ // 5. Fog (exponential, matches scene fog)
309
+ float fogFactor = 1.0 - exp(-uFogDensity * gl_FragCoord.z / gl_FragCoord.w);
310
+ lit = mix(lit, uFogColor, clamp(fogFactor, 0.0, 1.0));
311
+
312
+ gl_FragColor = vec4(lit, 1.0);
313
+ }
314
+ `;
@@ -0,0 +1,16 @@
1
+ /** Terrain vertex shader — passes world position, normal, and material index to fragment. */
2
+ export const TERRAIN_VERT_SHADER = /* glsl */ `
3
+ attribute float materialIndex;
4
+
5
+ varying vec3 vWorldPosition;
6
+ varying vec3 vNormal;
7
+ varying float vMaterialIndex;
8
+
9
+ void main() {
10
+ vec4 worldPos = modelMatrix * vec4(position, 1.0);
11
+ vWorldPosition = worldPos.xyz;
12
+ vNormal = normalize(mat3(modelMatrix) * normal);
13
+ vMaterialIndex = materialIndex;
14
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
15
+ }
16
+ `;
@@ -0,0 +1,20 @@
1
+ import { NOISE_GLSL } from '@loonylabs/gamedev-client';
2
+
3
+ /** Fragment shader for wall cells. Coarser noise, brick-like dark palette. */
4
+ export const WALL_FRAG_SHADER = /* glsl */`
5
+ uniform vec3 u_baseColor;
6
+ uniform float u_noiseScale;
7
+ varying vec2 v_uv;
8
+
9
+ ${NOISE_GLSL}
10
+
11
+ void main() {
12
+ float n = valueNoise(v_uv * u_noiseScale);
13
+ float n2 = valueNoise(v_uv * u_noiseScale * 2.0) * 0.2;
14
+ // Subtle horizontal brick lines
15
+ float brick = smoothstep(0.45, 0.5, fract(v_uv.y * 0.5 + floor(v_uv.x) * 0.5));
16
+ float brightness = 0.6 + n * 0.25 + n2 - brick * 0.1;
17
+ vec3 color = u_baseColor * brightness;
18
+ gl_FragColor = vec4(color, 1.0);
19
+ }
20
+ `;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @file renderer/shooterArena.ts
3
+ * Builds Three.js meshes for the Shooter arena from server payload data.
4
+ */
5
+
6
+ import * as THREE from 'three';
7
+
8
+ interface Vec3 {
9
+ x: number;
10
+ y: number;
11
+ z: number;
12
+ }
13
+
14
+ interface AABB {
15
+ min: Vec3;
16
+ max: Vec3;
17
+ }
18
+
19
+ interface CoverBlock {
20
+ position: Vec3;
21
+ width: number;
22
+ depth: number;
23
+ height: number;
24
+ aabb: AABB;
25
+ }
26
+
27
+ export interface ArenaPayload {
28
+ arena: {
29
+ width: number;
30
+ depth: number;
31
+ walls: AABB[];
32
+ covers: CoverBlock[];
33
+ spawnPoints: Vec3[];
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Build the arena scene group from EXPERIENCE_CHANGE payload data.
39
+ * Returns a disposable THREE.Group containing floor, walls, covers, and lighting.
40
+ */
41
+ export function buildArenaScene(payload: ArenaPayload): THREE.Group {
42
+ const group = new THREE.Group();
43
+
44
+ // Floor
45
+ const floorGeo = new THREE.PlaneGeometry(payload.arena.width, payload.arena.depth);
46
+ const floorMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.9 });
47
+ const floor = new THREE.Mesh(floorGeo, floorMat);
48
+ floor.rotation.x = -Math.PI / 2;
49
+ group.add(floor);
50
+
51
+ // Walls
52
+ for (const wall of payload.arena.walls) {
53
+ const w = wall.max.x - wall.min.x;
54
+ const h = wall.max.y - wall.min.y;
55
+ const d = wall.max.z - wall.min.z;
56
+ const geo = new THREE.BoxGeometry(w, h, d);
57
+ const mat = new THREE.MeshStandardMaterial({ color: 0x555555 });
58
+ const mesh = new THREE.Mesh(geo, mat);
59
+ mesh.position.set(
60
+ (wall.min.x + wall.max.x) / 2,
61
+ (wall.min.y + wall.max.y) / 2,
62
+ (wall.min.z + wall.max.z) / 2,
63
+ );
64
+ group.add(mesh);
65
+ }
66
+
67
+ // Covers
68
+ for (const cover of payload.arena.covers) {
69
+ const geo = new THREE.BoxGeometry(cover.width, cover.height, cover.depth);
70
+ const mat = new THREE.MeshStandardMaterial({ color: 0x886644, roughness: 0.7 });
71
+ const mesh = new THREE.Mesh(geo, mat);
72
+ mesh.position.set(cover.position.x, cover.height / 2, cover.position.z);
73
+ group.add(mesh);
74
+ }
75
+
76
+ // Ambient light
77
+ const ambient = new THREE.AmbientLight(0xffffff, 0.5);
78
+ group.add(ambient);
79
+
80
+ // Directional light (sun-like)
81
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
82
+ dirLight.position.set(10, 20, 10);
83
+ group.add(dirLight);
84
+
85
+ return group;
86
+ }
87
+
88
+ /**
89
+ * Dispose all geometry and materials in an arena group.
90
+ */
91
+ export function disposeArenaScene(group: THREE.Group): void {
92
+ group.traverse((child) => {
93
+ if (child instanceof THREE.Mesh) {
94
+ child.geometry.dispose();
95
+ if (Array.isArray(child.material)) {
96
+ child.material.forEach(m => m.dispose());
97
+ } else {
98
+ child.material.dispose();
99
+ }
100
+ }
101
+ });
102
+ }