nova64 0.2.4 → 0.2.6
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.
- package/README.md +25 -8
- package/bin/nova64.js +165 -0
- package/dist/assets/console-CY_kygm3.js +14 -0
- package/dist/assets/console-CY_kygm3.js.map +1 -0
- package/dist/assets/main-l0sNRNKZ.js.map +1 -0
- package/dist/assets/sky/studio/nx.png +0 -0
- package/dist/assets/sky/studio/ny.png +0 -0
- package/dist/assets/sky/studio/nz.png +0 -0
- package/dist/assets/sky/studio/px.png +0 -0
- package/dist/assets/sky/studio/py.png +0 -0
- package/dist/assets/sky/studio/pz.png +0 -0
- package/dist/assets/vanilla-Dcuy32gi.js +2 -0
- package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
- package/dist/console.html +899 -0
- package/dist/docs/BENCHMARK.md +77 -0
- package/dist/docs/CHEATSHEET.md +255 -0
- package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
- package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
- package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
- package/dist/docs/FREE_GLB_ASSETS.md +330 -0
- package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
- package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
- package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
- package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
- package/dist/docs/MIGRATION_GUIDE.md +553 -0
- package/dist/docs/NOVA64_3D_API.md +356 -0
- package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
- package/dist/docs/NOVA64_UI_API.md +503 -0
- package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
- package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
- package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
- package/dist/docs/api-3d.html +750 -0
- package/dist/docs/api-effects.html +385 -0
- package/dist/docs/api-improvements.md +121 -0
- package/dist/docs/api-skybox.html +407 -0
- package/dist/docs/api-sprites.html +321 -0
- package/dist/docs/api-voxel.html +337 -0
- package/dist/docs/api.html +543 -0
- package/dist/docs/assets.html +306 -0
- package/dist/docs/audio.html +340 -0
- package/dist/docs/blogs.html +286 -0
- package/dist/docs/collision.html +316 -0
- package/dist/docs/console.html +247 -0
- package/dist/docs/editor.html +297 -0
- package/dist/docs/font.html +247 -0
- package/dist/docs/framebuffer.html +247 -0
- package/dist/docs/fullscreen-button.html +297 -0
- package/dist/docs/gpu-systems.html +247 -0
- package/dist/docs/index.html +580 -0
- package/dist/docs/input.html +491 -0
- package/dist/docs/physics.html +311 -0
- package/dist/docs/screens.html +311 -0
- package/dist/docs/storage.html +311 -0
- package/dist/docs/textinput.html +332 -0
- package/dist/docs/ui.html +488 -0
- package/dist/examples/3d-advanced/code.js +695 -0
- package/dist/examples/adventure-comic-3d/code.js +342 -0
- package/dist/examples/audio-lab/code.js +150 -0
- package/dist/examples/boids-flocking/code.js +270 -0
- package/dist/examples/crystal-cathedral-3d/code.js +706 -0
- package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
- package/dist/examples/demoscene/README.md +192 -0
- package/dist/examples/demoscene/code.js +1081 -0
- package/dist/examples/demoscene/meta.json +21 -0
- package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
- package/dist/examples/f-zero-nova-3d/code.js +865 -0
- package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
- package/dist/examples/fps-demo-3d/code.js +744 -0
- package/dist/examples/game-of-life-3d/code.js +338 -0
- package/dist/examples/generative-art/code.js +632 -0
- package/dist/examples/hello-3d/code.js +325 -0
- package/dist/examples/hello-skybox/code.js +183 -0
- package/dist/examples/hello-world/code.js +19 -0
- package/dist/examples/input-showcase/code.js +109 -0
- package/dist/examples/instancing-demo/code.js +315 -0
- package/dist/examples/minecraft-demo/code.js +387 -0
- package/dist/examples/model-viewer-3d/code.js +114 -0
- package/dist/examples/mystical-realm-3d/code.js +1203 -0
- package/dist/examples/nature-explorer-3d/code.js +1318 -0
- package/dist/examples/particles-demo/code.js +522 -0
- package/dist/examples/pbr-showcase/code.js +140 -0
- package/dist/examples/physics-demo-3d/code.js +948 -0
- package/dist/examples/screen-demo/code.js +267 -0
- package/dist/examples/shooter-demo-3d/code.js +1286 -0
- package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
- package/dist/examples/space-combat-3d/README.md +135 -0
- package/dist/examples/space-combat-3d/code.js +1332 -0
- package/dist/examples/space-harrier-3d/code.js +923 -0
- package/dist/examples/star-fox-nova-3d/code.js +1116 -0
- package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
- package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
- package/dist/examples/storage-quest/code.js +209 -0
- package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
- package/dist/examples/strider-demo-3d/cache-test.html +132 -0
- package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
- package/dist/examples/strider-demo-3d/code-old.js +1537 -0
- package/dist/examples/strider-demo-3d/code.js +1462 -0
- package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
- package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
- package/dist/examples/super-plumber-64/README.md +128 -0
- package/dist/examples/super-plumber-64/code.js +1185 -0
- package/dist/examples/super-plumber-64/index.html +88 -0
- package/dist/examples/test-2d-overlay/code.js +32 -0
- package/dist/examples/test-font/code.js +51 -0
- package/dist/examples/test-minimal/code.js +21 -0
- package/dist/examples/ui-demo/code.js +306 -0
- package/dist/examples/wing-commander-space/README.md +180 -0
- package/dist/examples/wing-commander-space/code.js +1285 -0
- package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
- package/dist/examples/wizardry-3d/code.js +3928 -0
- package/dist/index.html +666 -0
- package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/dist/os9-shell/index.html +23 -0
- package/dist/os9-shell/nova-icon.svg +12 -0
- package/index.html +6 -1
- package/package.json +37 -32
- package/public/assets/sky/studio/nx.png +0 -0
- package/public/assets/sky/studio/ny.png +0 -0
- package/public/assets/sky/studio/nz.png +0 -0
- package/public/assets/sky/studio/px.png +0 -0
- package/public/assets/sky/studio/py.png +0 -0
- package/public/assets/sky/studio/pz.png +0 -0
- package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/public/os9-shell/index.html +10 -1
- package/runtime/api-2d.js +301 -21
- package/runtime/api-3d/pbr.js +45 -1
- package/runtime/api-3d.js +1 -0
- package/runtime/api-effects.js +90 -3
- package/runtime/api-gameutils.js +476 -0
- package/runtime/api-generative.js +610 -0
- package/runtime/api-skybox.js +54 -0
- package/runtime/api-voxel.js +139 -28
- package/runtime/gpu-threejs.js +13 -9
- package/runtime/ui.js +2 -2
- package/src/main.js +24 -1
- package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
- 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
|
+
}
|