nova64 0.2.5 → 0.2.7

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 (185) hide show
  1. package/README.md +25 -8
  2. package/bin/nova64.js +165 -0
  3. package/dist/assets/console-CY_kygm3.js +14 -0
  4. package/dist/assets/console-CY_kygm3.js.map +1 -0
  5. package/dist/assets/main-l0sNRNKZ.js.map +1 -0
  6. package/dist/assets/sky/studio/nx.png +0 -0
  7. package/dist/assets/sky/studio/ny.png +0 -0
  8. package/dist/assets/sky/studio/nz.png +0 -0
  9. package/dist/assets/sky/studio/px.png +0 -0
  10. package/dist/assets/sky/studio/py.png +0 -0
  11. package/dist/assets/sky/studio/pz.png +0 -0
  12. package/dist/assets/vanilla-Dcuy32gi.js +2 -0
  13. package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
  14. package/dist/console.html +899 -0
  15. package/dist/docs/BENCHMARK.md +77 -0
  16. package/dist/docs/CHEATSHEET.md +255 -0
  17. package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
  18. package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
  19. package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
  20. package/dist/docs/FREE_GLB_ASSETS.md +330 -0
  21. package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
  22. package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
  23. package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
  24. package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
  25. package/dist/docs/MIGRATION_GUIDE.md +553 -0
  26. package/dist/docs/NOVA64_3D_API.md +356 -0
  27. package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
  28. package/dist/docs/NOVA64_UI_API.md +503 -0
  29. package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
  30. package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
  31. package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
  32. package/dist/docs/api-3d.html +750 -0
  33. package/dist/docs/api-effects.html +385 -0
  34. package/dist/docs/api-improvements.md +121 -0
  35. package/dist/docs/api-skybox.html +407 -0
  36. package/dist/docs/api-sprites.html +321 -0
  37. package/dist/docs/api-voxel.html +337 -0
  38. package/dist/docs/api.html +543 -0
  39. package/dist/docs/assets.html +306 -0
  40. package/dist/docs/audio.html +340 -0
  41. package/dist/docs/blogs.html +286 -0
  42. package/dist/docs/collision.html +316 -0
  43. package/dist/docs/console.html +247 -0
  44. package/dist/docs/editor.html +297 -0
  45. package/dist/docs/font.html +247 -0
  46. package/dist/docs/framebuffer.html +247 -0
  47. package/dist/docs/fullscreen-button.html +297 -0
  48. package/dist/docs/gpu-systems.html +247 -0
  49. package/dist/docs/index.html +580 -0
  50. package/dist/docs/input.html +491 -0
  51. package/dist/docs/physics.html +311 -0
  52. package/dist/docs/screens.html +311 -0
  53. package/dist/docs/storage.html +311 -0
  54. package/dist/docs/textinput.html +332 -0
  55. package/dist/docs/ui.html +488 -0
  56. package/dist/examples/3d-advanced/code.js +695 -0
  57. package/dist/examples/adventure-comic-3d/code.js +342 -0
  58. package/dist/examples/audio-lab/code.js +150 -0
  59. package/dist/examples/boids-flocking/code.js +270 -0
  60. package/dist/examples/crystal-cathedral-3d/code.js +706 -0
  61. package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
  62. package/dist/examples/demoscene/README.md +192 -0
  63. package/dist/examples/demoscene/code.js +1081 -0
  64. package/dist/examples/demoscene/meta.json +21 -0
  65. package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
  66. package/dist/examples/f-zero-nova-3d/code.js +865 -0
  67. package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
  68. package/dist/examples/fps-demo-3d/code.js +744 -0
  69. package/dist/examples/game-of-life-3d/code.js +338 -0
  70. package/dist/examples/generative-art/code.js +632 -0
  71. package/dist/examples/hello-3d/code.js +325 -0
  72. package/dist/examples/hello-skybox/code.js +183 -0
  73. package/dist/examples/hello-world/code.js +19 -0
  74. package/dist/examples/input-showcase/code.js +109 -0
  75. package/dist/examples/instancing-demo/code.js +315 -0
  76. package/dist/examples/minecraft-demo/code.js +387 -0
  77. package/dist/examples/model-viewer-3d/code.js +114 -0
  78. package/dist/examples/mystical-realm-3d/code.js +1203 -0
  79. package/dist/examples/nature-explorer-3d/code.js +1318 -0
  80. package/dist/examples/particles-demo/code.js +522 -0
  81. package/dist/examples/pbr-showcase/code.js +140 -0
  82. package/dist/examples/physics-demo-3d/code.js +948 -0
  83. package/dist/examples/screen-demo/code.js +267 -0
  84. package/dist/examples/shooter-demo-3d/code.js +1286 -0
  85. package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
  86. package/dist/examples/space-combat-3d/README.md +135 -0
  87. package/dist/examples/space-combat-3d/code.js +1332 -0
  88. package/dist/examples/space-harrier-3d/code.js +923 -0
  89. package/dist/examples/star-fox-nova-3d/code.js +1116 -0
  90. package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
  91. package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
  92. package/dist/examples/storage-quest/code.js +209 -0
  93. package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
  94. package/dist/examples/strider-demo-3d/cache-test.html +132 -0
  95. package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
  96. package/dist/examples/strider-demo-3d/code-old.js +1537 -0
  97. package/dist/examples/strider-demo-3d/code.js +1462 -0
  98. package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
  99. package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
  100. package/dist/examples/super-plumber-64/README.md +128 -0
  101. package/dist/examples/super-plumber-64/code.js +1185 -0
  102. package/dist/examples/super-plumber-64/index.html +88 -0
  103. package/dist/examples/test-2d-overlay/code.js +32 -0
  104. package/dist/examples/test-font/code.js +51 -0
  105. package/dist/examples/test-minimal/code.js +21 -0
  106. package/dist/examples/ui-demo/code.js +306 -0
  107. package/dist/examples/wing-commander-space/README.md +180 -0
  108. package/dist/examples/wing-commander-space/code.js +1285 -0
  109. package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
  110. package/dist/examples/wizardry-3d/code.js +3928 -0
  111. package/dist/index.html +666 -0
  112. package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
  113. package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
  114. package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  115. package/dist/os9-shell/index.html +23 -0
  116. package/dist/os9-shell/nova-icon.svg +12 -0
  117. package/dist/runtime/api-2d.js +1158 -0
  118. package/dist/runtime/api-3d/camera.js +73 -0
  119. package/dist/runtime/api-3d/instancing.js +180 -0
  120. package/dist/runtime/api-3d/lights.js +51 -0
  121. package/dist/runtime/api-3d/materials.js +47 -0
  122. package/dist/runtime/api-3d/models.js +84 -0
  123. package/dist/runtime/api-3d/particles.js +296 -0
  124. package/dist/runtime/api-3d/pbr.js +113 -0
  125. package/dist/runtime/api-3d/primitives.js +304 -0
  126. package/dist/runtime/api-3d/scene.js +169 -0
  127. package/dist/runtime/api-3d/transforms.js +161 -0
  128. package/dist/runtime/api-3d.js +166 -0
  129. package/dist/runtime/api-effects.js +840 -0
  130. package/dist/runtime/api-gameutils.js +476 -0
  131. package/dist/runtime/api-generative.js +610 -0
  132. package/dist/runtime/api-presets.js +85 -0
  133. package/dist/runtime/api-skybox.js +232 -0
  134. package/dist/runtime/api-sprites.js +100 -0
  135. package/dist/runtime/api-voxel.js +712 -0
  136. package/dist/runtime/api.js +201 -0
  137. package/dist/runtime/assets.js +27 -0
  138. package/dist/runtime/audio.js +114 -0
  139. package/dist/runtime/collision.js +47 -0
  140. package/dist/runtime/console.js +101 -0
  141. package/dist/runtime/editor.js +233 -0
  142. package/dist/runtime/font.js +233 -0
  143. package/dist/runtime/framebuffer.js +28 -0
  144. package/dist/runtime/fullscreen-button.js +185 -0
  145. package/dist/runtime/gpu-canvas2d.js +47 -0
  146. package/dist/runtime/gpu-threejs.js +643 -0
  147. package/dist/runtime/gpu-webgl2.js +310 -0
  148. package/dist/runtime/index.d.ts +682 -0
  149. package/dist/runtime/index.js +22 -0
  150. package/dist/runtime/input.js +225 -0
  151. package/dist/runtime/logger.js +60 -0
  152. package/dist/runtime/physics.js +101 -0
  153. package/dist/runtime/screens.js +213 -0
  154. package/dist/runtime/storage.js +38 -0
  155. package/dist/runtime/store.js +151 -0
  156. package/dist/runtime/textinput.js +68 -0
  157. package/dist/runtime/ui/buttons.js +124 -0
  158. package/dist/runtime/ui/panels.js +105 -0
  159. package/dist/runtime/ui/text.js +86 -0
  160. package/dist/runtime/ui/widgets.js +141 -0
  161. package/dist/runtime/ui.js +111 -0
  162. package/index.html +6 -1
  163. package/package.json +9 -2
  164. package/public/assets/sky/studio/nx.png +0 -0
  165. package/public/assets/sky/studio/ny.png +0 -0
  166. package/public/assets/sky/studio/nz.png +0 -0
  167. package/public/assets/sky/studio/px.png +0 -0
  168. package/public/assets/sky/studio/py.png +0 -0
  169. package/public/assets/sky/studio/pz.png +0 -0
  170. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  171. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  172. package/public/os9-shell/index.html +10 -1
  173. package/runtime/api-2d.js +301 -21
  174. package/runtime/api-3d/pbr.js +45 -1
  175. package/runtime/api-3d.js +1 -0
  176. package/runtime/api-effects.js +90 -3
  177. package/runtime/api-gameutils.js +476 -0
  178. package/runtime/api-generative.js +610 -0
  179. package/runtime/api-skybox.js +54 -0
  180. package/runtime/api-voxel.js +139 -28
  181. package/runtime/gpu-threejs.js +13 -9
  182. package/runtime/ui.js +2 -2
  183. package/src/main.js +20 -0
  184. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  185. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -0,0 +1,1318 @@
1
+ // NATURE EXPLORER 3D — Immersive Low-Poly Wilderness with Wildlife & Discovery
2
+ // Explore a vast procedural landscape, discover wildlife, collect specimens,
3
+ // photograph creatures, and uncover all points of interest across biomes
4
+
5
+ // ── World Constants ──
6
+ const WORLD_SIZE = 120;
7
+ const TREE_COUNT = 50;
8
+ const ROCK_COUNT = 30;
9
+ const FLOWER_PATCH_COUNT = 25;
10
+ const MUSHROOM_COUNT = 15;
11
+ const CRYSTAL_COUNT = 10;
12
+ const POI_COUNT = 8;
13
+
14
+ // ── Biome definitions ──
15
+ const BIOMES = [
16
+ { name: 'Meadow', ground: 0x5a9c4f, tree: 0x228b22, fog: 0x88ccaa, sky: [0x99ccee, 0x4488bb] },
17
+ {
18
+ name: 'Pine Forest',
19
+ ground: 0x3d7a3d,
20
+ tree: 0x1a5c1a,
21
+ fog: 0x668877,
22
+ sky: [0x7799aa, 0x335566],
23
+ },
24
+ {
25
+ name: 'Cherry Grove',
26
+ ground: 0x6aac5f,
27
+ tree: 0xff88aa,
28
+ fog: 0xddaacc,
29
+ sky: [0xffccdd, 0xaa6688],
30
+ },
31
+ {
32
+ name: 'Autumn Wood',
33
+ ground: 0x8a7a4f,
34
+ tree: 0xcc6622,
35
+ fog: 0xaa9966,
36
+ sky: [0xddaa77, 0x885533],
37
+ },
38
+ ];
39
+
40
+ // ── Wildlife templates ──
41
+ const WILDLIFE = [
42
+ { name: 'Deer', shape: 'tall', color: 0xaa7744, size: 1.2, speed: 4, flee: 12, rare: false },
43
+ { name: 'Rabbit', shape: 'small', color: 0xccaa88, size: 0.4, speed: 6, flee: 8, rare: false },
44
+ { name: 'Fox', shape: 'medium', color: 0xdd6622, size: 0.7, speed: 5, flee: 10, rare: false },
45
+ { name: 'Bear', shape: 'large', color: 0x554433, size: 1.8, speed: 2, flee: 0, rare: false },
46
+ { name: 'Owl', shape: 'bird', color: 0x887766, size: 0.5, speed: 3, flee: 15, rare: false },
47
+ { name: 'Blue Jay', shape: 'bird', color: 0x3366cc, size: 0.3, speed: 7, flee: 12, rare: false },
48
+ { name: 'White Stag', shape: 'tall', color: 0xeeeeff, size: 1.4, speed: 8, flee: 20, rare: true },
49
+ {
50
+ name: 'Golden Eagle',
51
+ shape: 'bird',
52
+ color: 0xddaa33,
53
+ size: 0.8,
54
+ speed: 10,
55
+ flee: 25,
56
+ rare: true,
57
+ },
58
+ {
59
+ name: 'Crystal Butterfly',
60
+ shape: 'tiny',
61
+ color: 0xaaddff,
62
+ size: 0.2,
63
+ speed: 2,
64
+ flee: 6,
65
+ rare: true,
66
+ },
67
+ ];
68
+
69
+ // ── Collectible types ──
70
+ const COLLECTIBLES = [
71
+ { name: 'Red Mushroom', color: 0xcc2222, shape: 'mushroom', points: 10 },
72
+ { name: 'Blue Mushroom', color: 0x2244cc, shape: 'mushroom', points: 15 },
73
+ { name: 'Golden Mushroom', color: 0xddaa22, shape: 'mushroom', points: 30 },
74
+ { name: 'Amethyst Crystal', color: 0x9944cc, shape: 'crystal', points: 25 },
75
+ { name: 'Emerald Crystal', color: 0x22cc44, shape: 'crystal', points: 25 },
76
+ { name: 'Ruby Crystal', color: 0xcc2244, shape: 'crystal', points: 40 },
77
+ { name: 'Wildflower', color: 0xff88aa, shape: 'flower', points: 5 },
78
+ { name: 'Sunflower', color: 0xffcc22, shape: 'flower', points: 5 },
79
+ { name: 'Feather', color: 0xeeeedd, shape: 'feather', points: 20 },
80
+ ];
81
+
82
+ // ── Points of Interest ──
83
+ const POI_TYPES = [
84
+ { name: 'Ancient Ruins', desc: 'Crumbling stone pillars from a forgotten age', color: 0x888877 },
85
+ { name: 'Fairy Ring', desc: 'A circle of glowing mushrooms', color: 0x88ffaa },
86
+ { name: 'Old Campsite', desc: 'Remains of a campfire, still warm', color: 0xcc6633 },
87
+ { name: 'Waterfall', desc: 'Crystal-clear water cascading over mossy rocks', color: 0x44aadd },
88
+ { name: 'Hollow Tree', desc: 'A massive ancient tree with a carved entrance', color: 0x6b4226 },
89
+ { name: 'Stone Circle', desc: 'Mysterious standing stones hum with energy', color: 0x99aaaa },
90
+ { name: 'Flower Meadow', desc: 'A carpet of wildflowers stretches before you', color: 0xff99bb },
91
+ { name: 'Crystal Cave', desc: 'Glittering crystals line a shallow cave', color: 0xaabbff },
92
+ ];
93
+
94
+ // ── State ──
95
+ let playerPos = { x: 0, y: 1, z: 0 };
96
+ let playerAngle = 0;
97
+ let time = 0;
98
+ let gameState = 'loading';
99
+ let loadingProgress = 0;
100
+ let loadingText = 'Generating world...';
101
+
102
+ let trees = [];
103
+ let rocks = [];
104
+ let butterflies = [];
105
+ let clouds = [];
106
+ let flowers = [];
107
+ let animals = [];
108
+ let collectibles = [];
109
+ let pointsOfInterest = [];
110
+ let particleSystems = [];
111
+ let campfireLights = [];
112
+
113
+ let sunAngle = 0;
114
+ let dayNightCycle = 0;
115
+ let weatherState = 'clear'; // clear, cloudy, rain
116
+ let weatherTimer = 0;
117
+ let rainParticles = null;
118
+ let windStrength = 0;
119
+
120
+ // Discovery / Journal
121
+ let journal = { creatures: new Set(), pois: new Set(), collectibles: new Set() };
122
+ let score = 0;
123
+ let totalCollectibles = 0;
124
+ let photoMode = false;
125
+ let photoZoom = 1;
126
+ let photoFlash = 0;
127
+ let photos = []; // { name, time }
128
+ let notifications = [];
129
+ let floatingTexts = null;
130
+ let minimap = null;
131
+
132
+ // Camera smoothing
133
+ let camPos = { x: 0, y: 6, z: 12 };
134
+ let camTarget = { x: 0, y: 1, z: 0 };
135
+
136
+ // GLB model URLs (Khronos glTF samples — all CC0/CC-BY 4.0)
137
+ const MODEL_URLS = {
138
+ fox: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Fox/glTF-Binary/Fox.glb',
139
+ duck: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb',
140
+ };
141
+ let models = {};
142
+
143
+ // Seeded random for consistent world gen
144
+ let seed = 12345;
145
+ function seededRandom() {
146
+ seed = (seed * 16807 + 0) % 2147483647;
147
+ return (seed - 1) / 2147483646;
148
+ }
149
+
150
+ function dist2D(ax, az, bx, bz) {
151
+ const dx = ax - bx,
152
+ dz = az - bz;
153
+ return Math.sqrt(dx * dx + dz * dz);
154
+ }
155
+
156
+ function getBiome(x, z) {
157
+ // Simple noise-based biome selection
158
+ const n = Math.sin(x * 0.03) * Math.cos(z * 0.04) + Math.sin(x * 0.07 + z * 0.05);
159
+ if (n > 0.5) return BIOMES[2]; // Cherry Grove
160
+ if (n > 0) return BIOMES[0]; // Meadow
161
+ if (n > -0.5) return BIOMES[3]; // Autumn Wood
162
+ return BIOMES[1]; // Pine Forest
163
+ }
164
+
165
+ // ── Create animal 3D mesh ──
166
+ function createAnimalMesh(template, x, z) {
167
+ const s = template.size;
168
+ const c = template.color;
169
+ let mesh;
170
+ switch (template.shape) {
171
+ case 'tall': {
172
+ // Deer / Stag
173
+ const body = createCapsule(s * 0.4, s * 0.8, c, [x, s * 0.8, z]);
174
+ setScale(body, 1, 1, 1.5);
175
+ const head = createSphere(s * 0.25, c, [x, s * 1.2, z - s * 0.6]);
176
+ // Legs
177
+ createCylinder(s * 0.08, s * 0.6, c - 0x111111, [x - s * 0.2, s * 0.3, z - s * 0.3]);
178
+ createCylinder(s * 0.08, s * 0.6, c - 0x111111, [x + s * 0.2, s * 0.3, z - s * 0.3]);
179
+ createCylinder(s * 0.08, s * 0.6, c - 0x111111, [x - s * 0.2, s * 0.3, z + s * 0.3]);
180
+ createCylinder(s * 0.08, s * 0.6, c - 0x111111, [x + s * 0.2, s * 0.3, z + s * 0.3]);
181
+ mesh = body;
182
+ break;
183
+ }
184
+ case 'small': {
185
+ // Rabbit
186
+ mesh = createSphere(s * 0.5, c, [x, s * 0.4, z]);
187
+ setScale(mesh, 1, 0.8, 1.2);
188
+ createSphere(s * 0.2, c, [x, s * 0.7, z - s * 0.2]);
189
+ // Ears
190
+ createCylinder(s * 0.06, s * 0.4, 0xffccaa, [x - s * 0.08, s * 0.9, z - s * 0.2]);
191
+ createCylinder(s * 0.06, s * 0.4, 0xffccaa, [x + s * 0.08, s * 0.9, z - s * 0.2]);
192
+ break;
193
+ }
194
+ case 'medium': {
195
+ // Fox
196
+ mesh = createCapsule(s * 0.3, s * 0.5, c, [x, s * 0.5, z]);
197
+ setScale(mesh, 1, 0.9, 1.4);
198
+ const fHead = createSphere(s * 0.22, c, [x, s * 0.7, z - s * 0.4]);
199
+ createCone(s * 0.12, s * 0.2, 0xffffff, [x, s * 0.65, z - s * 0.6]); // Nose
200
+ // Tail
201
+ createCapsule(s * 0.1, s * 0.4, 0xffffff, [x, s * 0.5, z + s * 0.5]);
202
+ break;
203
+ }
204
+ case 'large': {
205
+ // Bear
206
+ mesh = createCapsule(s * 0.5, s * 0.7, c, [x, s * 0.7, z]);
207
+ setScale(mesh, 1.2, 1, 1.3);
208
+ createSphere(s * 0.35, c, [x, s * 1.1, z - s * 0.4]);
209
+ // Ears
210
+ createSphere(s * 0.1, c, [x - s * 0.2, s * 1.35, z - s * 0.35]);
211
+ createSphere(s * 0.1, c, [x + s * 0.2, s * 1.35, z - s * 0.35]);
212
+ break;
213
+ }
214
+ case 'bird': {
215
+ // Flying bird
216
+ mesh = createSphere(s * 0.4, c, [x, 5 + Math.random() * 8, z]);
217
+ setScale(mesh, 1, 0.6, 1.5);
218
+ // Wings
219
+ createPlane(s * 1.5, s * 0.4, c + 0x222222, [x, 5 + Math.random() * 8, z]);
220
+ break;
221
+ }
222
+ case 'tiny': {
223
+ // Crystal butterfly
224
+ mesh = createSphere(s * 0.3, c, [x, 2, z], 6, { material: 'holographic' });
225
+ break;
226
+ }
227
+ default:
228
+ mesh = createSphere(s * 0.5, c, [x, s, z]);
229
+ }
230
+ return mesh;
231
+ }
232
+
233
+ // ── Create collectible mesh ──
234
+ function createCollectibleMesh(type, x, z) {
235
+ let mesh;
236
+ switch (type.shape) {
237
+ case 'mushroom': {
238
+ const stem = createCylinder(0.1, 0.3, 0xeeddcc, [x, 0.15, z]);
239
+ mesh = createSphere(0.2, type.color, [x, 0.35, z]);
240
+ setScale(mesh, 1, 0.6, 1);
241
+ break;
242
+ }
243
+ case 'crystal': {
244
+ mesh = createCone(0.15, 0.5, type.color, [x, 0.25, z], { material: 'holographic' });
245
+ break;
246
+ }
247
+ case 'flower': {
248
+ createCylinder(0.04, 0.3, 0x44aa22, [x, 0.15, z]);
249
+ mesh = createSphere(0.12, type.color, [x, 0.35, z]);
250
+ break;
251
+ }
252
+ case 'feather': {
253
+ mesh = createPlane(0.3, 0.1, type.color, [x, 0.5, z]);
254
+ setRotation(mesh, 0.3, 0, 0.5);
255
+ break;
256
+ }
257
+ default:
258
+ mesh = createSphere(0.2, type.color, [x, 0.3, z]);
259
+ }
260
+ return mesh;
261
+ }
262
+
263
+ // ── Create POI structure ──
264
+ function createPOIMesh(type, x, z) {
265
+ const c = type.color;
266
+ switch (type.name) {
267
+ case 'Ancient Ruins': {
268
+ for (let i = 0; i < 5; i++) {
269
+ const a = (i / 5) * Math.PI * 2;
270
+ const px = x + Math.cos(a) * 3;
271
+ const pz = z + Math.sin(a) * 3;
272
+ const h = 2 + Math.random() * 3;
273
+ const pillar = createCylinder(0.4, h, c, [px, h / 2, pz]);
274
+ if (Math.random() > 0.5) setScale(pillar, 1, 0.6, 1); // Broken
275
+ }
276
+ createPlane(6, 6, 0x777766, [x, 0.05, z]);
277
+ break;
278
+ }
279
+ case 'Fairy Ring': {
280
+ for (let i = 0; i < 12; i++) {
281
+ const a = (i / 12) * Math.PI * 2;
282
+ const px = x + Math.cos(a) * 2.5;
283
+ const pz = z + Math.sin(a) * 2.5;
284
+ createCylinder(0.08, 0.2, 0xeeddcc, [px, 0.1, pz]);
285
+ createSphere(0.12, 0x88ffaa, [px, 0.25, pz], 4, { material: 'emissive' });
286
+ }
287
+ const glow = createParticleSystem(60, {
288
+ size: 0.08,
289
+ color: 0x88ffaa,
290
+ emissive: 0x44ff66,
291
+ emissiveIntensity: 3,
292
+ gravity: 0.5,
293
+ drag: 0.9,
294
+ emitterX: x,
295
+ emitterY: 0.5,
296
+ emitterZ: z,
297
+ emitRate: 8,
298
+ minLife: 1,
299
+ maxLife: 3,
300
+ minSpeed: 0.3,
301
+ maxSpeed: 1,
302
+ spread: Math.PI,
303
+ minSize: 0.03,
304
+ maxSize: 0.1,
305
+ endColor: 0x004400,
306
+ });
307
+ particleSystems.push(glow);
308
+ break;
309
+ }
310
+ case 'Old Campsite': {
311
+ // Fire pit
312
+ for (let i = 0; i < 8; i++) {
313
+ const a = (i / 8) * Math.PI * 2;
314
+ createCube(0.3, 0x555555, [x + Math.cos(a) * 1, 0.15, z + Math.sin(a) * 1]);
315
+ }
316
+ // Campfire particles
317
+ const fire = createParticleSystem(80, {
318
+ size: 0.12,
319
+ color: 0xff6622,
320
+ emissive: 0xff4400,
321
+ emissiveIntensity: 3,
322
+ gravity: 2,
323
+ drag: 0.95,
324
+ emitterX: x,
325
+ emitterY: 0.3,
326
+ emitterZ: z,
327
+ emitRate: 15,
328
+ minLife: 0.3,
329
+ maxLife: 1,
330
+ minSpeed: 1,
331
+ maxSpeed: 3,
332
+ spread: 0.4,
333
+ minSize: 0.05,
334
+ maxSize: 0.15,
335
+ endColor: 0x331100,
336
+ });
337
+ particleSystems.push(fire);
338
+ const light = createPointLight(0xff6622, 2, 12, [x, 2, z]);
339
+ campfireLights.push({ light, x, z, base: 2 });
340
+ // Log seats
341
+ createCylinder(0.2, 1.5, 0x6b4226, [x + 2, 0.2, z]);
342
+ setRotation(createCylinder(0.2, 1.5, 0x6b4226, [x - 1.5, 0.2, z + 1.5]), 0, 0.8, 0);
343
+ break;
344
+ }
345
+ case 'Waterfall': {
346
+ // Rock wall
347
+ createCube(4, 0x666655, [x, 3, z - 1]);
348
+ setScale(createCube(3, 0x555544, [x, 5, z - 0.5]), 0.8, 1, 0.5);
349
+ // Water particles
350
+ const waterfall = createParticleSystem(120, {
351
+ size: 0.1,
352
+ color: 0x66bbee,
353
+ emissive: 0x2288bb,
354
+ emissiveIntensity: 1,
355
+ gravity: -8,
356
+ drag: 0.98,
357
+ emitterX: x,
358
+ emitterY: 5.5,
359
+ emitterZ: z,
360
+ emitRate: 25,
361
+ minLife: 0.5,
362
+ maxLife: 1.2,
363
+ minSpeed: 0.5,
364
+ maxSpeed: 2,
365
+ spread: 0.3,
366
+ minSize: 0.04,
367
+ maxSize: 0.12,
368
+ endColor: 0x224466,
369
+ });
370
+ particleSystems.push(waterfall);
371
+ // Pool
372
+ const pool = createCylinder(3, 0.1, 0x2266aa, [x, 0.05, z + 2]);
373
+ setScale(pool, 1.5, 1, 1);
374
+ break;
375
+ }
376
+ case 'Hollow Tree': {
377
+ const trunk = createCylinder(2, 8, 0x6b4226, [x, 4, z]);
378
+ createSphere(5, 0x2e8b57, [x, 9, z]);
379
+ // Hollow entrance
380
+ createCube(1.2, 0x332211, [x, 1, z + 2], { material: 'standard' });
381
+ break;
382
+ }
383
+ case 'Stone Circle': {
384
+ for (let i = 0; i < 7; i++) {
385
+ const a = (i / 7) * Math.PI * 2;
386
+ const px = x + Math.cos(a) * 4;
387
+ const pz = z + Math.sin(a) * 4;
388
+ const stone = createCube(1, 0x889999, [px, 1.5, pz]);
389
+ setScale(stone, 0.5, 1 + Math.random(), 0.4);
390
+ }
391
+ // Central energy
392
+ const energy = createParticleSystem(40, {
393
+ size: 0.06,
394
+ color: 0xaaddff,
395
+ emissive: 0x6699ff,
396
+ emissiveIntensity: 4,
397
+ gravity: 1,
398
+ drag: 0.9,
399
+ emitterX: x,
400
+ emitterY: 1,
401
+ emitterZ: z,
402
+ emitRate: 6,
403
+ minLife: 1.5,
404
+ maxLife: 3,
405
+ minSpeed: 0.5,
406
+ maxSpeed: 1.5,
407
+ spread: Math.PI * 2,
408
+ minSize: 0.02,
409
+ maxSize: 0.08,
410
+ endColor: 0x001133,
411
+ });
412
+ particleSystems.push(energy);
413
+ break;
414
+ }
415
+ case 'Flower Meadow': {
416
+ for (let i = 0; i < 40; i++) {
417
+ const fx = x + (Math.random() - 0.5) * 8;
418
+ const fz = z + (Math.random() - 0.5) * 8;
419
+ const fc = [0xff6699, 0xffaa33, 0xff44aa, 0xaa44ff, 0xffff44][
420
+ Math.floor(Math.random() * 5)
421
+ ];
422
+ createCylinder(0.03, 0.2 + Math.random() * 0.2, 0x44aa22, [fx, 0.1, fz]);
423
+ createSphere(0.08 + Math.random() * 0.06, fc, [fx, 0.3, fz]);
424
+ }
425
+ break;
426
+ }
427
+ case 'Crystal Cave': {
428
+ // Rock overhang
429
+ createSphere(4, 0x555544, [x, 0, z]);
430
+ setScale(createSphere(4, 0x555544, [x, 0, z]), 1.5, 0.6, 1.5);
431
+ // Crystals inside
432
+ for (let i = 0; i < 8; i++) {
433
+ const a = (i / 8) * Math.PI * 2;
434
+ const cx = x + Math.cos(a) * 2;
435
+ const cz = z + Math.sin(a) * 2;
436
+ const cc = [0x9944cc, 0x4499ff, 0x22ccaa][i % 3];
437
+ createCone(0.15, 0.5 + Math.random() * 0.5, cc, [cx, 0.3, cz], { material: 'holographic' });
438
+ }
439
+ const crystGlow = createParticleSystem(30, {
440
+ size: 0.05,
441
+ color: 0xaabbff,
442
+ emissive: 0x6677ff,
443
+ emissiveIntensity: 3,
444
+ gravity: 0.3,
445
+ drag: 0.85,
446
+ emitterX: x,
447
+ emitterY: 0.5,
448
+ emitterZ: z,
449
+ emitRate: 4,
450
+ minLife: 2,
451
+ maxLife: 4,
452
+ minSpeed: 0.2,
453
+ maxSpeed: 0.6,
454
+ spread: Math.PI,
455
+ minSize: 0.02,
456
+ maxSize: 0.06,
457
+ endColor: 0x000033,
458
+ });
459
+ particleSystems.push(crystGlow);
460
+ break;
461
+ }
462
+ }
463
+ }
464
+
465
+ function generateWorld() {
466
+ seed = 42;
467
+
468
+ // Ground — layered for depth
469
+ createPlane(WORLD_SIZE * 2.5, WORLD_SIZE * 2.5, 0x4a8c3f, [0, 0, 0]);
470
+ // Dirt ring around world edge
471
+ createPlane(WORLD_SIZE * 4, WORLD_SIZE * 4, 0x8a7a5a, [0, -0.02, 0]);
472
+ // Water plane
473
+ const water = createPlane(WORLD_SIZE * 5, WORLD_SIZE * 5, 0x2266aa, [0, -0.8, 0]);
474
+
475
+ // Hills with biome-colored grass
476
+ for (let i = 0; i < 20; i++) {
477
+ const x = (seededRandom() - 0.5) * WORLD_SIZE * 2;
478
+ const z = (seededRandom() - 0.5) * WORLD_SIZE * 2;
479
+ const biome = getBiome(x, z);
480
+ const height = 2 + seededRandom() * 10;
481
+ const radius = 5 + seededRandom() * 12;
482
+ const hill = createSphere(radius, biome.ground, [x, -radius + height, z]);
483
+ setScale(hill, 1, 0.35, 1);
484
+ }
485
+
486
+ // Trees — biome-dependent styles
487
+ for (let i = 0; i < TREE_COUNT; i++) {
488
+ const x = (seededRandom() - 0.5) * WORLD_SIZE * 1.8;
489
+ const z = (seededRandom() - 0.5) * WORLD_SIZE * 1.8;
490
+ const biome = getBiome(x, z);
491
+ const height = 3 + seededRandom() * 5;
492
+ const trunkColor = 0x8b5a2b + Math.floor(seededRandom() * 0x151515);
493
+
494
+ const trunk = createCylinder(0.25 + seededRandom() * 0.15, height, trunkColor, [
495
+ x,
496
+ height / 2,
497
+ z,
498
+ ]);
499
+
500
+ if (biome.name === 'Pine Forest') {
501
+ // Cone-shaped pine
502
+ for (let layer = 0; layer < 3; layer++) {
503
+ const ly = height * 0.5 + layer * 1.2;
504
+ const lr = 2 - layer * 0.5;
505
+ createCone(lr, 2, biome.tree, [x, ly, z]);
506
+ }
507
+ } else {
508
+ // Round canopy
509
+ const canopySize = 1.5 + seededRandom() * 2.5;
510
+ const canopy = createSphere(canopySize, biome.tree, [x, height + canopySize * 0.4, z]);
511
+ if (biome.name === 'Cherry Grove' && seededRandom() > 0.5) {
512
+ // Cherry blossoms particle
513
+ const blossom = createParticleSystem(30, {
514
+ size: 0.06,
515
+ color: 0xff88aa,
516
+ emissive: 0xff6688,
517
+ emissiveIntensity: 0.5,
518
+ gravity: -0.5,
519
+ drag: 0.92,
520
+ emitterX: x,
521
+ emitterY: height + canopySize,
522
+ emitterZ: z,
523
+ emitRate: 3,
524
+ minLife: 2,
525
+ maxLife: 5,
526
+ minSpeed: 0.3,
527
+ maxSpeed: 1,
528
+ spread: Math.PI,
529
+ minSize: 0.03,
530
+ maxSize: 0.08,
531
+ endColor: 0x994466,
532
+ });
533
+ particleSystems.push(blossom);
534
+ }
535
+ }
536
+
537
+ trees.push({ x, z, height, trunk });
538
+ }
539
+
540
+ // Rocks
541
+ for (let i = 0; i < ROCK_COUNT; i++) {
542
+ const x = (seededRandom() - 0.5) * WORLD_SIZE * 1.8;
543
+ const z = (seededRandom() - 0.5) * WORLD_SIZE * 1.8;
544
+ const size = 0.5 + seededRandom() * 2;
545
+ const gray = 0x666666 + Math.floor(seededRandom() * 0x333333);
546
+ const rock = createCube(size, gray, [x, size / 2, z]);
547
+ setScale(rock, 1 + seededRandom() * 0.5, 0.5 + seededRandom() * 0.8, 1 + seededRandom() * 0.5);
548
+ rocks.push({ x, z, mesh: rock });
549
+ }
550
+
551
+ // Flower patches (decorative, not collectible)
552
+ for (let i = 0; i < FLOWER_PATCH_COUNT; i++) {
553
+ const cx = (seededRandom() - 0.5) * WORLD_SIZE * 1.5;
554
+ const cz = (seededRandom() - 0.5) * WORLD_SIZE * 1.5;
555
+ const patchColor = [0xff6699, 0xffaa33, 0xff44aa, 0xaa44ff, 0xffff44, 0xff8844][
556
+ Math.floor(seededRandom() * 6)
557
+ ];
558
+ for (let j = 0; j < 8; j++) {
559
+ const fx = cx + (seededRandom() - 0.5) * 3;
560
+ const fz = cz + (seededRandom() - 0.5) * 3;
561
+ createCylinder(0.03, 0.15 + seededRandom() * 0.15, 0x44aa22, [fx, 0.08, fz]);
562
+ const fm = createSphere(0.07, patchColor, [fx, 0.22, fz]);
563
+ flowers.push({ x: fx, z: fz, mesh: fm, phase: seededRandom() * Math.PI * 2 });
564
+ }
565
+ }
566
+
567
+ // Butterflies
568
+ for (let i = 0; i < 20; i++) {
569
+ const x = (seededRandom() - 0.5) * 60;
570
+ const z = (seededRandom() - 0.5) * 60;
571
+ const colors = [0xff44aa, 0xffaa00, 0x44aaff, 0xaaff44, 0xff88ff, 0xffdd44];
572
+ const mesh = createSphere(0.12, colors[i % colors.length], [x, 2, z]);
573
+ butterflies.push({
574
+ x,
575
+ z,
576
+ y: 2,
577
+ vx: (seededRandom() - 0.5) * 2,
578
+ vz: (seededRandom() - 0.5) * 2,
579
+ mesh,
580
+ phase: seededRandom() * Math.PI * 2,
581
+ });
582
+ }
583
+
584
+ // Clouds — more varied
585
+ for (let i = 0; i < 14; i++) {
586
+ const x = (seededRandom() - 0.5) * 300;
587
+ const z = (seededRandom() - 0.5) * 300;
588
+ const y = 25 + seededRandom() * 20;
589
+ const mesh = createSphere(3 + seededRandom() * 5, 0xffffff, [x, y, z]);
590
+ setScale(mesh, 2 + seededRandom() * 2, 0.4 + seededRandom() * 0.3, 1 + seededRandom());
591
+ setMeshOpacity(mesh, 0.7 + seededRandom() * 0.3);
592
+ clouds.push({ mesh, x, z, y, speed: 0.3 + seededRandom() * 1.2 });
593
+ }
594
+
595
+ // Spawn wildlife
596
+ seed = 777;
597
+ for (let i = 0; i < 20; i++) {
598
+ const template = WILDLIFE[Math.floor(seededRandom() * WILDLIFE.length)];
599
+ if (template.rare && seededRandom() > 0.25) continue; // Rare animals 25% chance
600
+ const x = (seededRandom() - 0.5) * WORLD_SIZE * 1.5;
601
+ const z = (seededRandom() - 0.5) * WORLD_SIZE * 1.5;
602
+ const mesh = createAnimalMesh(template, x, z);
603
+ const homeX = x,
604
+ homeZ = z;
605
+ animals.push({
606
+ ...template,
607
+ x,
608
+ z,
609
+ homeX,
610
+ homeZ,
611
+ mesh,
612
+ state: 'idle',
613
+ stateTimer: 2 + seededRandom() * 5,
614
+ angle: seededRandom() * Math.PI * 2,
615
+ wanderX: x,
616
+ wanderZ: z,
617
+ discovered: false,
618
+ photographed: false,
619
+ bobPhase: seededRandom() * Math.PI * 2,
620
+ });
621
+ }
622
+
623
+ // Spawn collectibles
624
+ seed = 999;
625
+ for (let i = 0; i < MUSHROOM_COUNT + CRYSTAL_COUNT; i++) {
626
+ const type = COLLECTIBLES[Math.floor(seededRandom() * COLLECTIBLES.length)];
627
+ const x = (seededRandom() - 0.5) * WORLD_SIZE * 1.5;
628
+ const z = (seededRandom() - 0.5) * WORLD_SIZE * 1.5;
629
+ const mesh = createCollectibleMesh(type, x, z);
630
+ collectibles.push({
631
+ ...type,
632
+ x,
633
+ z,
634
+ mesh,
635
+ collected: false,
636
+ bobPhase: seededRandom() * Math.PI * 2,
637
+ });
638
+ totalCollectibles++;
639
+ }
640
+
641
+ // Spawn Points of Interest
642
+ seed = 555;
643
+ const usedPOIs = new Set();
644
+ for (let i = 0; i < POI_COUNT; i++) {
645
+ let typeIdx;
646
+ do {
647
+ typeIdx = Math.floor(seededRandom() * POI_TYPES.length);
648
+ } while (usedPOIs.has(typeIdx) && usedPOIs.size < POI_TYPES.length);
649
+ usedPOIs.add(typeIdx);
650
+ const type = POI_TYPES[typeIdx];
651
+ const x = (seededRandom() - 0.5) * WORLD_SIZE * 1.2;
652
+ const z = (seededRandom() - 0.5) * WORLD_SIZE * 1.2;
653
+ createPOIMesh(type, x, z);
654
+ pointsOfInterest.push({ ...type, x, z, discovered: false });
655
+ }
656
+ }
657
+
658
+ async function loadModels() {
659
+ try {
660
+ loadingText = 'Loading Fox model...';
661
+ loadingProgress = 0.6;
662
+ models.fox = await loadModel(MODEL_URLS.fox, [5, 0, -5], 0.04);
663
+ loadingProgress = 0.75;
664
+
665
+ loadingText = 'Loading Duck model...';
666
+ models.duck = await loadModel(MODEL_URLS.duck, [-10, -0.3, 5], 1.5);
667
+ loadingProgress = 0.85;
668
+
669
+ // Ducks in pond
670
+ for (let i = 0; i < 3; i++) {
671
+ const angle = (i / 3) * Math.PI * 2;
672
+ await loadModel(
673
+ MODEL_URLS.duck,
674
+ [-10 + Math.cos(angle) * 3, -0.3, 5 + Math.sin(angle) * 3],
675
+ 1.0
676
+ );
677
+ }
678
+ loadingProgress = 0.95;
679
+ loadingText = 'World ready!';
680
+ } catch (e) {
681
+ console.warn('Model loading failed:', e);
682
+ loadingText = 'Models unavailable — geometry world!';
683
+ loadingProgress = 0.95;
684
+ }
685
+ }
686
+
687
+ function addNotification(text, color) {
688
+ notifications.push({ text, color: color || rgba8(200, 255, 200), timer: 4 });
689
+ }
690
+
691
+ export async function init() {
692
+ clearScene();
693
+ trees = [];
694
+ rocks = [];
695
+ butterflies = [];
696
+ clouds = [];
697
+ flowers = [];
698
+ animals = [];
699
+ collectibles = [];
700
+ pointsOfInterest = [];
701
+ particleSystems = [];
702
+ campfireLights = [];
703
+ playerPos = { x: 0, y: 1, z: 0 };
704
+ playerAngle = 0;
705
+ time = 0;
706
+ score = 0;
707
+ totalCollectibles = 0;
708
+ photoMode = false;
709
+ photoZoom = 1;
710
+ photoFlash = 0;
711
+ photos = [];
712
+ notifications = [];
713
+ journal = { creatures: new Set(), pois: new Set(), collectibles: new Set() };
714
+ gameState = 'loading';
715
+ loadingProgress = 0;
716
+ weatherState = 'clear';
717
+ weatherTimer = 30 + Math.random() * 60;
718
+ windStrength = 0;
719
+ models = {};
720
+
721
+ // Atmosphere
722
+ setAmbientLight(0xffeedd, 0.5);
723
+ setLightDirection(0.5, -1, 0.3);
724
+ setFog(0x88bbdd, 50, 150);
725
+ enableBloom(0.35, 0.3, 0.5);
726
+ enableVignette(0.8, 0.88);
727
+
728
+ if (typeof createGradientSkybox === 'function') {
729
+ createGradientSkybox(0x88ccee, 0x3366aa);
730
+ }
731
+
732
+ setCameraFOV(65);
733
+
734
+ loadingText = 'Generating world...';
735
+ loadingProgress = 0.1;
736
+
737
+ // Delay a frame so loading screen shows
738
+ await new Promise(r => setTimeout(r, 50));
739
+ generateWorld();
740
+ loadingProgress = 0.5;
741
+ loadingText = 'Loading models...';
742
+
743
+ floatingTexts = createFloatingTextSystem();
744
+
745
+ // Minimap
746
+ minimap = createMinimap({
747
+ x: 640 - 95,
748
+ y: 10,
749
+ width: 85,
750
+ height: 85,
751
+ shape: 'circle',
752
+ bgColor: rgba8(10, 20, 10, 180),
753
+ worldW: WORLD_SIZE * 2,
754
+ worldH: WORLD_SIZE * 2,
755
+ player: { x: 0, y: 0, color: rgba8(255, 255, 100), blink: true },
756
+ gridLines: 4,
757
+ gridColor: rgba8(40, 80, 40, 60),
758
+ });
759
+
760
+ await loadModels();
761
+ loadingProgress = 1;
762
+ gameState = 'exploring';
763
+ }
764
+
765
+ // ── UPDATE ──
766
+ export function update(dt) {
767
+ time += dt;
768
+
769
+ if (gameState === 'loading') return;
770
+
771
+ // Day/night cycle
772
+ dayNightCycle += dt * 0.025;
773
+ sunAngle = dayNightCycle;
774
+ const daylight = Math.max(0.12, Math.cos(sunAngle) * 0.5 + 0.5);
775
+ const nightTint = Math.max(0, 1 - daylight * 2);
776
+
777
+ setAmbientLight(0xffeedd, daylight * 0.5 + 0.05);
778
+ setLightDirection(Math.cos(sunAngle), -Math.abs(Math.cos(sunAngle)) - 0.3, 0.3);
779
+
780
+ // Sky color shift with time of day
781
+ if (typeof createGradientSkybox === 'function') {
782
+ if (daylight > 0.6) {
783
+ createGradientSkybox(0x88ccee, 0x3366aa);
784
+ } else if (daylight > 0.3) {
785
+ createGradientSkybox(0xdd8844, 0x663322);
786
+ setFog(0xcc8855, 40, 130);
787
+ } else {
788
+ createGradientSkybox(0x112244, 0x000811);
789
+ setFog(0x112233, 30, 100);
790
+ }
791
+ }
792
+ if (daylight > 0.6) setFog(0x88bbdd, 50, 150);
793
+
794
+ // Weather system
795
+ weatherTimer -= dt;
796
+ if (weatherTimer <= 0) {
797
+ const r = Math.random();
798
+ if (weatherState === 'clear') {
799
+ weatherState = r > 0.5 ? 'cloudy' : 'rain';
800
+ weatherTimer = 20 + Math.random() * 40;
801
+ if (weatherState === 'rain') {
802
+ addNotification('Rain begins to fall...', rgba8(100, 180, 255));
803
+ rainParticles = createParticleSystem(200, {
804
+ size: 0.05,
805
+ color: 0x88bbee,
806
+ emissive: 0x4488bb,
807
+ emissiveIntensity: 0.5,
808
+ gravity: -15,
809
+ drag: 1,
810
+ emitterX: playerPos.x,
811
+ emitterY: 20,
812
+ emitterZ: playerPos.z,
813
+ emitRate: 50,
814
+ minLife: 0.8,
815
+ maxLife: 1.5,
816
+ minSpeed: 8,
817
+ maxSpeed: 15,
818
+ spread: 1.5,
819
+ minSize: 0.02,
820
+ maxSize: 0.06,
821
+ endColor: 0x224466,
822
+ });
823
+ particleSystems.push(rainParticles);
824
+ }
825
+ } else {
826
+ if (weatherState === 'rain' && rainParticles) {
827
+ removeParticleSystem(rainParticles);
828
+ rainParticles = null;
829
+ addNotification('The rain stops.', rgba8(200, 240, 200));
830
+ }
831
+ weatherState = 'clear';
832
+ weatherTimer = 30 + Math.random() * 60;
833
+ }
834
+ }
835
+ // Move rain to follow player
836
+ if (rainParticles) {
837
+ setParticleEmitter(rainParticles, { x: playerPos.x, z: playerPos.z });
838
+ }
839
+
840
+ // Wind
841
+ windStrength = Math.sin(time * 0.2) * 0.5 + Math.sin(time * 0.07) * 0.3;
842
+
843
+ // ── Player movement ──
844
+ if (!photoMode) {
845
+ const moveSpeed = key('ShiftLeft') || key('ShiftRight') ? 14 : 8;
846
+ const turnSpeed = 2.5;
847
+
848
+ if (key('ArrowLeft') || key('KeyA')) playerAngle += turnSpeed * dt;
849
+ if (key('ArrowRight') || key('KeyD')) playerAngle -= turnSpeed * dt;
850
+
851
+ if (key('ArrowUp') || key('KeyW')) {
852
+ playerPos.x -= Math.sin(playerAngle) * moveSpeed * dt;
853
+ playerPos.z -= Math.cos(playerAngle) * moveSpeed * dt;
854
+ }
855
+ if (key('ArrowDown') || key('KeyS')) {
856
+ playerPos.x += Math.sin(playerAngle) * moveSpeed * dt * 0.5;
857
+ playerPos.z += Math.cos(playerAngle) * moveSpeed * dt * 0.5;
858
+ }
859
+ }
860
+
861
+ // Photo mode toggle
862
+ if (keyp('KeyP') || keyp('KeyC')) {
863
+ photoMode = !photoMode;
864
+ photoZoom = 1;
865
+ if (photoMode) addNotification('PHOTO MODE — Space to capture!', rgba8(255, 255, 150));
866
+ else addNotification('Photo mode off', rgba8(180, 180, 180));
867
+ }
868
+ if (photoMode) {
869
+ if (key('KeyQ')) photoZoom = Math.max(0.5, photoZoom - dt * 2);
870
+ if (key('KeyE')) photoZoom = Math.min(3, photoZoom + dt * 2);
871
+ setCameraFOV(65 / photoZoom);
872
+ // Capture photo
873
+ if (keyp('Space')) {
874
+ photoFlash = 1;
875
+ sfx('coin');
876
+ // Check what's in view (nearest animal)
877
+ let nearest = null,
878
+ nearDist = 30;
879
+ for (const a of animals) {
880
+ const d = dist2D(
881
+ a.x,
882
+ a.z,
883
+ playerPos.x - Math.sin(playerAngle) * 10,
884
+ playerPos.z - Math.cos(playerAngle) * 10
885
+ );
886
+ if (d < nearDist) {
887
+ nearest = a;
888
+ nearDist = d;
889
+ }
890
+ }
891
+ if (nearest && !nearest.photographed) {
892
+ nearest.photographed = true;
893
+ photos.push({ name: nearest.name, time: time });
894
+ score += nearest.rare ? 100 : 30;
895
+ addNotification(
896
+ `Photographed: ${nearest.name}! +${nearest.rare ? 100 : 30}`,
897
+ rgba8(255, 220, 100)
898
+ );
899
+ floatingTexts.spawn(`${nearest.name}!`, 320, 200, {
900
+ color: rgba8(255, 220, 100),
901
+ duration: 1.5,
902
+ riseSpeed: 40,
903
+ });
904
+ } else if (nearest && nearest.photographed) {
905
+ addNotification(`Already photographed ${nearest.name}`, rgba8(150, 150, 150));
906
+ } else {
907
+ addNotification('No wildlife in frame', rgba8(180, 120, 120));
908
+ }
909
+ }
910
+ } else {
911
+ setCameraFOV(65);
912
+ }
913
+
914
+ // ── Camera ── (smooth follow)
915
+ const camDist = photoMode ? 6 : 12;
916
+ const camHeight = photoMode ? 3 : 6;
917
+ const goalX = playerPos.x + Math.sin(playerAngle) * camDist;
918
+ const goalZ = playerPos.z + Math.cos(playerAngle) * camDist;
919
+ const goalY = playerPos.y + camHeight;
920
+ camPos.x += (goalX - camPos.x) * 4 * dt;
921
+ camPos.y += (goalY - camPos.y) * 4 * dt;
922
+ camPos.z += (goalZ - camPos.z) * 4 * dt;
923
+ camTarget.x += (playerPos.x - camTarget.x) * 6 * dt;
924
+ camTarget.y += (playerPos.y + 1 - camTarget.y) * 6 * dt;
925
+ camTarget.z += (playerPos.z - camTarget.z) * 6 * dt;
926
+ setCameraPosition(camPos.x, camPos.y, camPos.z);
927
+ setCameraTarget(camTarget.x, camTarget.y, camTarget.z);
928
+
929
+ // ── Collect nearby items ──
930
+ for (const c of collectibles) {
931
+ if (c.collected) continue;
932
+ if (dist2D(playerPos.x, playerPos.z, c.x, c.z) < 2) {
933
+ c.collected = true;
934
+ removeMesh(c.mesh);
935
+ score += c.points;
936
+ sfx('coin');
937
+ if (!journal.collectibles.has(c.name)) {
938
+ journal.collectibles.add(c.name);
939
+ addNotification(`NEW: ${c.name} discovered! +${c.points}`, rgba8(255, 220, 100));
940
+ } else {
941
+ addNotification(`${c.name} +${c.points}`, rgba8(200, 220, 200));
942
+ }
943
+ floatingTexts.spawn(`+${c.points}`, 320, 250, {
944
+ color: rgba8(255, 220, 100),
945
+ duration: 1,
946
+ riseSpeed: 50,
947
+ });
948
+ }
949
+ // Bob animation
950
+ if (!c.collected) {
951
+ c.bobPhase += dt * 2;
952
+ setPosition(c.mesh, c.x, 0.3 + Math.sin(c.bobPhase) * 0.1, c.z);
953
+ }
954
+ }
955
+
956
+ // ── Discover POIs ──
957
+ for (const poi of pointsOfInterest) {
958
+ if (poi.discovered) continue;
959
+ if (dist2D(playerPos.x, playerPos.z, poi.x, poi.z) < 6) {
960
+ poi.discovered = true;
961
+ journal.pois.add(poi.name);
962
+ score += 50;
963
+ sfx('explosion');
964
+ addNotification(`Discovered: ${poi.name}!`, rgba8(200, 255, 255));
965
+ addNotification(poi.desc, rgba8(150, 200, 200));
966
+ floatingTexts.spawn(poi.name, 320, 180, {
967
+ color: rgba8(200, 255, 255),
968
+ duration: 2.5,
969
+ riseSpeed: 25,
970
+ });
971
+ }
972
+ }
973
+
974
+ // ── Wildlife AI ──
975
+ for (const a of animals) {
976
+ const distToPlayer = dist2D(playerPos.x, playerPos.z, a.x, a.z);
977
+
978
+ // Discovery
979
+ if (!a.discovered && distToPlayer < 15) {
980
+ a.discovered = true;
981
+ if (!journal.creatures.has(a.name)) {
982
+ journal.creatures.add(a.name);
983
+ score += a.rare ? 50 : 15;
984
+ addNotification(
985
+ `Spotted: ${a.name}${a.rare ? ' (RARE!)' : ''}`,
986
+ a.rare ? rgba8(255, 200, 100) : rgba8(180, 255, 180)
987
+ );
988
+ floatingTexts.spawn(a.name, 320, 220, {
989
+ color: a.rare ? rgba8(255, 200, 100) : rgba8(180, 255, 180),
990
+ duration: 1.5,
991
+ riseSpeed: 30,
992
+ });
993
+ }
994
+ }
995
+
996
+ a.stateTimer -= dt;
997
+ switch (a.state) {
998
+ case 'idle':
999
+ if (a.flee > 0 && distToPlayer < a.flee) {
1000
+ a.state = 'flee';
1001
+ a.stateTimer = 2 + Math.random() * 2;
1002
+ break;
1003
+ }
1004
+ if (a.stateTimer <= 0) {
1005
+ a.state = 'wander';
1006
+ a.wanderX = a.homeX + (Math.random() - 0.5) * 20;
1007
+ a.wanderZ = a.homeZ + (Math.random() - 0.5) * 20;
1008
+ a.stateTimer = 3 + Math.random() * 4;
1009
+ }
1010
+ break;
1011
+ case 'wander': {
1012
+ const dx = a.wanderX - a.x,
1013
+ dz = a.wanderZ - a.z;
1014
+ const d = Math.sqrt(dx * dx + dz * dz);
1015
+ if (d > 0.5) {
1016
+ a.angle = Math.atan2(dx, dz);
1017
+ a.x += (dx / d) * a.speed * 0.5 * dt;
1018
+ a.z += (dz / d) * a.speed * 0.5 * dt;
1019
+ }
1020
+ if (a.flee > 0 && distToPlayer < a.flee) {
1021
+ a.state = 'flee';
1022
+ a.stateTimer = 2 + Math.random() * 2;
1023
+ break;
1024
+ }
1025
+ if (a.stateTimer <= 0 || d < 0.5) {
1026
+ a.state = 'idle';
1027
+ a.stateTimer = 2 + Math.random() * 5;
1028
+ }
1029
+ break;
1030
+ }
1031
+ case 'flee': {
1032
+ const fleeAngle = Math.atan2(a.x - playerPos.x, a.z - playerPos.z);
1033
+ a.angle = fleeAngle;
1034
+ a.x += Math.sin(fleeAngle) * a.speed * dt;
1035
+ a.z += Math.cos(fleeAngle) * a.speed * dt;
1036
+ if (distToPlayer > a.flee * 2 || a.stateTimer <= 0) {
1037
+ a.state = 'idle';
1038
+ a.stateTimer = 3 + Math.random() * 3;
1039
+ }
1040
+ break;
1041
+ }
1042
+ }
1043
+
1044
+ // Position mesh
1045
+ const bobY = a.shape === 'bird' ? 5 + Math.sin(a.bobPhase + time * 2) * 2 : 0;
1046
+ a.bobPhase += dt;
1047
+ setPosition(a.mesh, a.x, bobY, a.z);
1048
+ setRotation(a.mesh, 0, a.angle, 0);
1049
+ }
1050
+
1051
+ // ── Animate butterflies ──
1052
+ for (const b of butterflies) {
1053
+ b.phase += dt * 3;
1054
+ b.x += b.vx * dt + windStrength * dt * 0.5;
1055
+ b.z += b.vz * dt;
1056
+ b.y = 1.5 + Math.sin(b.phase) * 1.5 + Math.sin(b.phase * 2.3) * 0.5;
1057
+ b.vx += (Math.random() - 0.5) * dt * 3;
1058
+ b.vz += (Math.random() - 0.5) * dt * 3;
1059
+ b.vx *= 0.99;
1060
+ b.vz *= 0.99;
1061
+ setPosition(b.mesh, b.x, b.y, b.z);
1062
+ }
1063
+
1064
+ // ── Animate clouds ──
1065
+ for (const c of clouds) {
1066
+ c.x += (c.speed + windStrength * 0.3) * dt;
1067
+ if (c.x > 180) c.x = -180;
1068
+ setPosition(c.mesh, c.x, c.y, c.z);
1069
+ }
1070
+
1071
+ // ── Flower sway ──
1072
+ for (const f of flowers) {
1073
+ f.phase += dt * 2;
1074
+ setPosition(f.mesh, f.x + Math.sin(f.phase + windStrength) * 0.03, 0.22, f.z);
1075
+ }
1076
+
1077
+ // ── Campfire light flicker ──
1078
+ for (const cf of campfireLights) {
1079
+ const flicker = cf.base + Math.sin(time * 8 + cf.x) * 0.5 + Math.sin(time * 13) * 0.3;
1080
+ setPointLightColor(cf.light, 0xff6622, flicker, 12);
1081
+ }
1082
+
1083
+ // ── GLB model animations ──
1084
+ if (models.fox) {
1085
+ const foxAngle = time * 0.5;
1086
+ const foxX = playerPos.x + Math.cos(foxAngle) * 8;
1087
+ const foxZ = playerPos.z + Math.sin(foxAngle) * 8;
1088
+ setPosition(models.fox, foxX, 0, foxZ);
1089
+ setRotation(models.fox, 0, -foxAngle + Math.PI / 2, 0);
1090
+ }
1091
+ if (typeof updateAnimations === 'function') updateAnimations(dt);
1092
+
1093
+ // Update particles
1094
+ updateParticles(dt);
1095
+
1096
+ // Update floating texts
1097
+ if (floatingTexts) floatingTexts.update(dt);
1098
+
1099
+ // Photo flash decay
1100
+ if (photoFlash > 0) photoFlash -= dt * 4;
1101
+
1102
+ // Update notifications
1103
+ for (let i = notifications.length - 1; i >= 0; i--) {
1104
+ notifications[i].timer -= dt;
1105
+ if (notifications[i].timer <= 0) notifications.splice(i, 1);
1106
+ }
1107
+
1108
+ // Update minimap player position
1109
+ if (minimap) {
1110
+ minimap.player.x = playerPos.x + WORLD_SIZE;
1111
+ minimap.player.y = playerPos.z + WORLD_SIZE;
1112
+
1113
+ // Entity dots
1114
+ const ents = [];
1115
+ for (const a of animals) {
1116
+ if (!a.discovered) continue;
1117
+ ents.push({
1118
+ x: a.x + WORLD_SIZE,
1119
+ y: a.z + WORLD_SIZE,
1120
+ color: a.rare ? rgba8(255, 200, 80) : rgba8(100, 255, 100),
1121
+ size: a.rare ? 3 : 2,
1122
+ });
1123
+ }
1124
+ for (const poi of pointsOfInterest) {
1125
+ ents.push({
1126
+ x: poi.x + WORLD_SIZE,
1127
+ y: poi.z + WORLD_SIZE,
1128
+ color: poi.discovered ? rgba8(100, 200, 255) : rgba8(80, 80, 80),
1129
+ size: 3,
1130
+ });
1131
+ }
1132
+ minimap.entities = ents;
1133
+ }
1134
+ }
1135
+
1136
+ // ── DRAW ──
1137
+ export function draw() {
1138
+ // ── Loading screen ──
1139
+ if (gameState === 'loading') {
1140
+ rectfill(0, 0, 640, 360, rgba8(15, 30, 20));
1141
+ drawGlowText('NATURE EXPLORER', 200, 60, rgba8(100, 220, 150), rgba8(50, 150, 80), 2);
1142
+ printCentered(
1143
+ 'Discover Wildlife * Collect Specimens * Photograph Creatures',
1144
+ 320,
1145
+ 110,
1146
+ rgba8(120, 180, 140)
1147
+ );
1148
+
1149
+ // Loading bar
1150
+ drawProgressBar(
1151
+ 170,
1152
+ 170,
1153
+ 300,
1154
+ 14,
1155
+ loadingProgress,
1156
+ rgba8(80, 200, 100),
1157
+ rgba8(30, 50, 30),
1158
+ rgba8(100, 160, 100)
1159
+ );
1160
+ printCentered(loadingText, 320, 195, rgba8(160, 220, 160));
1161
+
1162
+ printCentered(
1163
+ 'WASD — Move | Shift — Run | P — Photo Mode',
1164
+ 320,
1165
+ 250,
1166
+ rgba8(100, 150, 120)
1167
+ );
1168
+ printCentered('Explore, discover and collect everything!', 320, 270, rgba8(80, 120, 100));
1169
+
1170
+ // Draw decorative leaves
1171
+ for (let i = 0; i < 6; i++) {
1172
+ const lx = 80 + i * 100 + Math.sin(time * 1.5 + i) * 10;
1173
+ const ly = 310 + Math.sin(time * 2 + i * 1.3) * 8;
1174
+ print('*', lx, ly, rgba8(80, 180, 100, 150));
1175
+ }
1176
+ return;
1177
+ }
1178
+
1179
+ // ── Photo flash overlay ──
1180
+ if (photoFlash > 0) {
1181
+ const a = Math.min(255, Math.floor(photoFlash * 255));
1182
+ rectfill(0, 0, 640, 360, rgba8(255, 255, 255, a));
1183
+ }
1184
+
1185
+ // ── Floating texts (3D spawned ones show as 2D) ──
1186
+ if (floatingTexts) {
1187
+ const texts = floatingTexts.getTexts();
1188
+ for (const t of texts) {
1189
+ const alpha = Math.min(255, Math.floor((t.remaining / t.duration) * 255));
1190
+ printCentered(t.text, Math.floor(t.x), Math.floor(t.y), t.color);
1191
+ }
1192
+ }
1193
+
1194
+ // ── Photo mode viewfinder ──
1195
+ if (photoMode) {
1196
+ // Viewfinder frame
1197
+ rect(80, 40, 480, 280, rgba8(255, 255, 255, 100));
1198
+ rect(81, 41, 478, 278, rgba8(0, 0, 0, 80));
1199
+ // Crosshairs
1200
+ line(320, 40, 320, 320, rgba8(255, 255, 255, 60));
1201
+ line(80, 180, 560, 180, rgba8(255, 255, 255, 60));
1202
+ // Corner brackets
1203
+ const bl = 20;
1204
+ line(80, 40, 80 + bl, 40, rgba8(255, 255, 255, 200));
1205
+ line(80, 40, 80, 40 + bl, rgba8(255, 255, 255, 200));
1206
+ line(560, 40, 560 - bl, 40, rgba8(255, 255, 255, 200));
1207
+ line(560, 40, 560, 40 + bl, rgba8(255, 255, 255, 200));
1208
+ line(80, 320, 80 + bl, 320, rgba8(255, 255, 255, 200));
1209
+ line(80, 320, 80, 320 - bl, rgba8(255, 255, 255, 200));
1210
+ line(560, 320, 560 - bl, 320, rgba8(255, 255, 255, 200));
1211
+ line(560, 320, 560, 320 - bl, rgba8(255, 255, 255, 200));
1212
+
1213
+ // Zoom indicator
1214
+ rectfill(90, 300, 100, 10, rgba8(0, 0, 0, 120));
1215
+ rectfill(90, 300, Math.floor(100 * ((photoZoom - 0.5) / 2.5)), 10, rgba8(255, 200, 80));
1216
+ print(`${photoZoom.toFixed(1)}x`, 195, 298, rgba8(255, 255, 255, 200));
1217
+
1218
+ print('SPACE: Capture Q/E: Zoom P: Exit', 90, 330, rgba8(200, 200, 200, 180));
1219
+
1220
+ // Photos taken count
1221
+ print(`Photos: ${photos.length}`, 460, 330, rgba8(255, 220, 100));
1222
+
1223
+ return; // Don't draw normal HUD in photo mode
1224
+ }
1225
+
1226
+ // ── HUD Panel ──
1227
+ const dayPct = Math.cos(sunAngle) * 0.5 + 0.5;
1228
+ const timeLabel =
1229
+ dayPct > 0.7 ? 'DAY' : dayPct > 0.4 ? 'AFTERNOON' : dayPct > 0.2 ? 'DUSK' : 'NIGHT';
1230
+ const weatherLabel =
1231
+ weatherState === 'rain' ? ' (Rain)' : weatherState === 'cloudy' ? ' (Cloudy)' : '';
1232
+
1233
+ // Top-left info panel
1234
+ drawPixelBorder(8, 8, 195, 52, rgba8(80, 120, 80), rgba8(30, 50, 30));
1235
+ rectfill(10, 10, 191, 48, rgba8(10, 25, 15, 200));
1236
+ print(`NATURE EXPLORER`, 16, 15, rgba8(100, 220, 150));
1237
+ print(`${timeLabel}${weatherLabel}`, 16, 27, rgba8(150, 200, 150));
1238
+ print(`Score: ${score}`, 16, 39, rgba8(255, 220, 100));
1239
+ print(`Photos: ${photos.length}`, 110, 39, rgba8(200, 200, 255));
1240
+
1241
+ // Journal summary — bottom left
1242
+ const jCreatures = journal.creatures.size;
1243
+ const jPois = journal.pois.size;
1244
+ const jCollect = journal.collectibles.size;
1245
+ const totalCreatures = WILDLIFE.length;
1246
+ const totalPois = POI_COUNT;
1247
+ const totalCollTypes = COLLECTIBLES.length;
1248
+
1249
+ drawPixelBorder(8, 295, 160, 55, rgba8(80, 120, 80), rgba8(30, 50, 30));
1250
+ rectfill(10, 297, 156, 51, rgba8(10, 25, 15, 200));
1251
+ print('JOURNAL', 16, 302, rgba8(180, 220, 180));
1252
+ print(`Creatures: ${jCreatures}/${totalCreatures}`, 16, 314, rgba8(150, 255, 150));
1253
+ print(`Places: ${jPois}/${totalPois}`, 16, 326, rgba8(150, 200, 255));
1254
+ print(`Items: ${jCollect}/${totalCollTypes}`, 16, 338, rgba8(255, 220, 150));
1255
+
1256
+ // ── Compass ── (top center)
1257
+ const cx = 320,
1258
+ cy = 20;
1259
+ rectfill(cx - 30, cy - 8, 60, 16, rgba8(0, 0, 0, 120));
1260
+ const compassAngle = playerAngle;
1261
+ const dirs = ['N', 'E', 'S', 'W'];
1262
+ for (let i = 0; i < 4; i++) {
1263
+ const a = (i * Math.PI) / 2 - compassAngle;
1264
+ const dx = Math.sin(a) * 24;
1265
+ if (Math.abs(dx) < 28) {
1266
+ const col = i === 0 ? rgba8(255, 100, 100) : rgba8(180, 180, 180);
1267
+ print(dirs[i], cx + dx - 3, cy - 4, col);
1268
+ }
1269
+ }
1270
+
1271
+ // ── Nearby wildlife indicator ──
1272
+ let nearbyAnimal = null;
1273
+ let nearDist = 20;
1274
+ for (const a of animals) {
1275
+ const d = dist2D(playerPos.x, playerPos.z, a.x, a.z);
1276
+ if (d < nearDist) {
1277
+ nearbyAnimal = a;
1278
+ nearDist = d;
1279
+ }
1280
+ }
1281
+ if (nearbyAnimal && nearDist < 15) {
1282
+ const alpha = Math.floor(Math.max(0, 1 - nearDist / 15) * 200);
1283
+ const nameCol = nearbyAnimal.rare ? rgba8(255, 200, 80, alpha) : rgba8(200, 255, 200, alpha);
1284
+ printCentered(nearbyAnimal.name + (nearbyAnimal.rare ? ' (RARE)' : ''), 320, 70, nameCol);
1285
+ if (nearDist < 8) {
1286
+ printCentered(
1287
+ nearbyAnimal.photographed ? 'Already photographed' : 'P to enter Photo Mode',
1288
+ 320,
1289
+ 82,
1290
+ rgba8(180, 180, 180, alpha)
1291
+ );
1292
+ }
1293
+ }
1294
+
1295
+ // ── Notifications ──
1296
+ for (let i = 0; i < notifications.length && i < 5; i++) {
1297
+ const n = notifications[i];
1298
+ const alpha = Math.min(255, Math.floor(n.timer * 200));
1299
+ const ny = 100 + i * 14;
1300
+ printCentered(n.text, 320, ny, n.color);
1301
+ }
1302
+
1303
+ // ── Minimap ──
1304
+ if (minimap) drawMinimap(minimap, time);
1305
+
1306
+ // ── Sprint indicator ──
1307
+ if (key('ShiftLeft') || key('ShiftRight')) {
1308
+ print('SPRINT', 300, 345, rgba8(255, 200, 100, 200));
1309
+ }
1310
+
1311
+ // ── Completion check ──
1312
+ const totalDisc = jCreatures + jPois + jCollect;
1313
+ const totalPossible = totalCreatures + totalPois + totalCollTypes;
1314
+ if (totalDisc >= totalPossible) {
1315
+ drawGlowText('100% COMPLETE!', 220, 160, rgba8(255, 220, 100), rgba8(200, 150, 50), 2);
1316
+ printCentered('You discovered everything!', 320, 195, rgba8(255, 255, 200));
1317
+ }
1318
+ }