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,1203 @@
|
|
|
1
|
+
// MYSTICAL REALM 3D - Creative Nintendo 64/PlayStation style fantasy world
|
|
2
|
+
// Showcases advanced 3D features: dynamic lighting, particle systems, procedural generation
|
|
3
|
+
|
|
4
|
+
// Game state management
|
|
5
|
+
let gameState = 'start'; // 'start', 'playing', 'paused', 'gameover'
|
|
6
|
+
let startScreenTime = 0;
|
|
7
|
+
let uiButtons = [];
|
|
8
|
+
let score = 0;
|
|
9
|
+
let crystalsCollected = 0;
|
|
10
|
+
let creaturesKept = 0;
|
|
11
|
+
let playTime = 0;
|
|
12
|
+
let magicBolts = []; // { mesh, x, y, z, vx, vy, vz, life }
|
|
13
|
+
|
|
14
|
+
let world = {
|
|
15
|
+
terrain: [],
|
|
16
|
+
crystals: [],
|
|
17
|
+
particles: [],
|
|
18
|
+
creatures: [],
|
|
19
|
+
weather: { type: 'clear', intensity: 0 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let player = {
|
|
23
|
+
x: 0,
|
|
24
|
+
y: 5,
|
|
25
|
+
z: 0,
|
|
26
|
+
rotation: 0,
|
|
27
|
+
speed: 8,
|
|
28
|
+
jumpVelocity: 0,
|
|
29
|
+
onGround: false,
|
|
30
|
+
health: 100,
|
|
31
|
+
maxHealth: 100,
|
|
32
|
+
magicCooldown: 0,
|
|
33
|
+
magicCharges: 3,
|
|
34
|
+
maxMagicCharges: 3,
|
|
35
|
+
magicRechargeTimer: 0,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let camera = {
|
|
39
|
+
offset: { x: 0, y: 12, z: 20 },
|
|
40
|
+
target: { x: 0, y: 0, z: 0 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let shake;
|
|
44
|
+
let magicCD;
|
|
45
|
+
|
|
46
|
+
let time = 0;
|
|
47
|
+
let dayNightCycle = 0;
|
|
48
|
+
let lightningTimer = 0;
|
|
49
|
+
|
|
50
|
+
// GPU instancing for trees (new feature)
|
|
51
|
+
let treeTrunkInstanceId = null;
|
|
52
|
+
let treeCrownInstanceId = null;
|
|
53
|
+
let treeMeta = []; // [{x, z, swayPhase}]
|
|
54
|
+
|
|
55
|
+
export async function init() {
|
|
56
|
+
cls();
|
|
57
|
+
|
|
58
|
+
console.log('🏰 Initializing Mystical Realm 3D...');
|
|
59
|
+
|
|
60
|
+
shake = createShake({ decay: 5 });
|
|
61
|
+
magicCD = createCooldown(0.35);
|
|
62
|
+
|
|
63
|
+
// Setup camera
|
|
64
|
+
setCameraPosition(camera.offset.x, camera.offset.y, camera.offset.z);
|
|
65
|
+
setCameraTarget(0, 0, 0);
|
|
66
|
+
setCameraFOV(65);
|
|
67
|
+
|
|
68
|
+
// Enable all retro effects for maximum N64/PSX nostalgia
|
|
69
|
+
enablePixelation(1);
|
|
70
|
+
enableDithering(true);
|
|
71
|
+
enableBloom(0.7, 0.5, 0.5); // Soft magical glow
|
|
72
|
+
enableFXAA(); // Smooth edges
|
|
73
|
+
enableVignette(1.2, 0.9); // Cinematic border
|
|
74
|
+
|
|
75
|
+
// Generate the mystical world
|
|
76
|
+
await generateTerrain();
|
|
77
|
+
await spawnCrystals();
|
|
78
|
+
await createCreatures();
|
|
79
|
+
|
|
80
|
+
// Set initial lighting
|
|
81
|
+
updateLighting();
|
|
82
|
+
|
|
83
|
+
// Initialize start screen
|
|
84
|
+
initStartScreen();
|
|
85
|
+
|
|
86
|
+
console.log('✨ Mystical Realm 3D loaded! Explore the fantasy world!');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function initStartScreen() {
|
|
90
|
+
uiButtons = [];
|
|
91
|
+
|
|
92
|
+
// START button
|
|
93
|
+
uiButtons.push(
|
|
94
|
+
createButton(
|
|
95
|
+
centerX(220),
|
|
96
|
+
150,
|
|
97
|
+
220,
|
|
98
|
+
55,
|
|
99
|
+
'▶ BEGIN QUEST',
|
|
100
|
+
() => {
|
|
101
|
+
console.log('🎯 BEGIN QUEST CLICKED! Changing gameState to playing...');
|
|
102
|
+
gameState = 'playing';
|
|
103
|
+
playTime = 0;
|
|
104
|
+
score = 0;
|
|
105
|
+
crystalsCollected = 0;
|
|
106
|
+
console.log('✅ gameState is now:', gameState);
|
|
107
|
+
console.log('🏰 Quest begun!');
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
normalColor: rgba8(100, 50, 200, 255),
|
|
111
|
+
hoverColor: rgba8(130, 70, 230, 255),
|
|
112
|
+
pressedColor: rgba8(70, 30, 160, 255),
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// CONTROLS button
|
|
118
|
+
uiButtons.push(
|
|
119
|
+
createButton(
|
|
120
|
+
centerX(220),
|
|
121
|
+
290,
|
|
122
|
+
220,
|
|
123
|
+
45,
|
|
124
|
+
'? CONTROLS',
|
|
125
|
+
() => {
|
|
126
|
+
console.log('🎮 WASD/Arrows = Move, SPACE = Jump, Collect crystals!');
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
normalColor: uiColors.primary,
|
|
130
|
+
hoverColor: rgba8(50, 150, 255, 255),
|
|
131
|
+
pressedColor: rgba8(20, 100, 200, 255),
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function generateTerrain() {
|
|
138
|
+
console.log('🌍 Generating mystical terrain...');
|
|
139
|
+
|
|
140
|
+
// Create main ground plane with texture-like pattern
|
|
141
|
+
const mainGround = createPlane(100, 100, 0x2a4a2a, [0, 0, 0]);
|
|
142
|
+
setRotation(mainGround, -Math.PI / 2, 0, 0);
|
|
143
|
+
world.terrain.push({ mesh: mainGround, type: 'ground' });
|
|
144
|
+
|
|
145
|
+
// Generate procedural hills and mountains
|
|
146
|
+
for (let i = 0; i < 20; i++) {
|
|
147
|
+
const x = (Math.random() - 0.5) * 80;
|
|
148
|
+
const z = (Math.random() - 0.5) * 80;
|
|
149
|
+
const height = 3 + Math.random() * 8;
|
|
150
|
+
const width = 4 + Math.random() * 6;
|
|
151
|
+
|
|
152
|
+
const hill = createCube(width, height, width, 0x3a5a3a, [x, height / 2, z]);
|
|
153
|
+
world.terrain.push({
|
|
154
|
+
mesh: hill,
|
|
155
|
+
type: 'hill',
|
|
156
|
+
originalY: height / 2,
|
|
157
|
+
bobPhase: Math.random() * Math.PI * 2,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Create mystical stone circles
|
|
162
|
+
for (let circle = 0; circle < 3; circle++) {
|
|
163
|
+
const centerX = (Math.random() - 0.5) * 60;
|
|
164
|
+
const centerZ = (Math.random() - 0.5) * 60;
|
|
165
|
+
const radius = 8 + Math.random() * 4;
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < 8; i++) {
|
|
168
|
+
const angle = (i / 8) * Math.PI * 2;
|
|
169
|
+
const x = centerX + Math.cos(angle) * radius;
|
|
170
|
+
const z = centerZ + Math.sin(angle) * radius;
|
|
171
|
+
const height = 6 + Math.random() * 4;
|
|
172
|
+
|
|
173
|
+
const stone = createAdvancedCube(
|
|
174
|
+
1.5,
|
|
175
|
+
{
|
|
176
|
+
color: 0x555555,
|
|
177
|
+
emissive: 0x111144,
|
|
178
|
+
emissiveIntensity: 0.4,
|
|
179
|
+
metallic: true,
|
|
180
|
+
animated: true,
|
|
181
|
+
},
|
|
182
|
+
[x, height / 2, z]
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
world.terrain.push({
|
|
186
|
+
mesh: stone,
|
|
187
|
+
type: 'monolith',
|
|
188
|
+
originalY: height / 2,
|
|
189
|
+
glowPhase: Math.random() * Math.PI * 2,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Ancient trees — GPU instanced (30 draw calls → 2)
|
|
195
|
+
treeTrunkInstanceId = createInstancedMesh('cube', 15, 0x4a3a2a, { size: 1 });
|
|
196
|
+
treeCrownInstanceId = createInstancedMesh('sphere', 15, 0x2a5a2a, { size: 4, segments: 8 });
|
|
197
|
+
treeMeta = [];
|
|
198
|
+
for (let i = 0; i < 15; i++) {
|
|
199
|
+
const x = (Math.random() - 0.5) * 90;
|
|
200
|
+
const z = (Math.random() - 0.5) * 90;
|
|
201
|
+
treeMeta.push({ x, z, swayPhase: Math.random() * Math.PI * 2 });
|
|
202
|
+
setInstanceTransform(treeTrunkInstanceId, i, x, 4, z, 0, 0, 0, 1, 8, 1);
|
|
203
|
+
setInstanceTransform(treeCrownInstanceId, i, x, 10, z);
|
|
204
|
+
}
|
|
205
|
+
finalizeInstances(treeTrunkInstanceId);
|
|
206
|
+
finalizeInstances(treeCrownInstanceId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function spawnCrystals() {
|
|
210
|
+
console.log('💎 Spawning mystical crystals...');
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < 25; i++) {
|
|
213
|
+
const x = (Math.random() - 0.5) * 70;
|
|
214
|
+
const z = (Math.random() - 0.5) * 70;
|
|
215
|
+
const y = 2;
|
|
216
|
+
|
|
217
|
+
// Create stunning holographic crystals
|
|
218
|
+
const colors = [0xff4444, 0x44ff44, 0x4444ff, 0xffff44, 0xff44ff, 0x44ffff];
|
|
219
|
+
const emissiveColors = [0x331111, 0x113311, 0x111133, 0x333311, 0x331133, 0x113333];
|
|
220
|
+
const colorIndex = Math.floor(Math.random() * colors.length);
|
|
221
|
+
|
|
222
|
+
const crystal = createAdvancedSphere(
|
|
223
|
+
0.8,
|
|
224
|
+
{
|
|
225
|
+
color: colors[colorIndex],
|
|
226
|
+
emissive: emissiveColors[colorIndex],
|
|
227
|
+
emissiveIntensity: 0.7,
|
|
228
|
+
holographic: true,
|
|
229
|
+
animated: true,
|
|
230
|
+
metallic: Math.random() > 0.5,
|
|
231
|
+
transparent: true,
|
|
232
|
+
opacity: 0.9,
|
|
233
|
+
},
|
|
234
|
+
[x, y, z],
|
|
235
|
+
12
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
world.crystals.push({
|
|
239
|
+
mesh: crystal,
|
|
240
|
+
x,
|
|
241
|
+
y,
|
|
242
|
+
z,
|
|
243
|
+
color: colors[colorIndex],
|
|
244
|
+
rotationSpeed: 1 + Math.random() * 2,
|
|
245
|
+
bobPhase: Math.random() * Math.PI * 2,
|
|
246
|
+
glowIntensity: Math.random(),
|
|
247
|
+
collected: false,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function createCreatures() {
|
|
253
|
+
console.log('🦋 Creating mystical creatures...');
|
|
254
|
+
|
|
255
|
+
// Flying magical orbs — passive, high altitude
|
|
256
|
+
for (let i = 0; i < 8; i++) {
|
|
257
|
+
const orb = createAdvancedSphere(
|
|
258
|
+
0.5,
|
|
259
|
+
{
|
|
260
|
+
color: 0xffff88,
|
|
261
|
+
emissive: 0x888844,
|
|
262
|
+
emissiveIntensity: 1.0,
|
|
263
|
+
holographic: true,
|
|
264
|
+
animated: true,
|
|
265
|
+
transparent: true,
|
|
266
|
+
opacity: 0.8,
|
|
267
|
+
},
|
|
268
|
+
[0, 15, 0],
|
|
269
|
+
16
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
world.creatures.push({
|
|
273
|
+
mesh: orb,
|
|
274
|
+
type: 'orb',
|
|
275
|
+
x: (Math.random() - 0.5) * 60,
|
|
276
|
+
y: 15 + Math.random() * 10,
|
|
277
|
+
z: (Math.random() - 0.5) * 60,
|
|
278
|
+
vx: (Math.random() - 0.5) * 2,
|
|
279
|
+
vy: (Math.random() - 0.5) * 1,
|
|
280
|
+
vz: (Math.random() - 0.5) * 2,
|
|
281
|
+
glowPhase: Math.random() * Math.PI * 2,
|
|
282
|
+
trail: [],
|
|
283
|
+
stunTimer: 0,
|
|
284
|
+
caught: false,
|
|
285
|
+
aggressive: false,
|
|
286
|
+
atk: 0,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Shadow wisps — ground-level, aggressive, chase the player
|
|
291
|
+
for (let i = 0; i < 6; i++) {
|
|
292
|
+
const wisp = createAdvancedSphere(
|
|
293
|
+
0.6,
|
|
294
|
+
{
|
|
295
|
+
color: 0x9933cc,
|
|
296
|
+
emissive: 0x440066,
|
|
297
|
+
emissiveIntensity: 0.9,
|
|
298
|
+
holographic: true,
|
|
299
|
+
animated: true,
|
|
300
|
+
transparent: true,
|
|
301
|
+
opacity: 0.85,
|
|
302
|
+
},
|
|
303
|
+
[0, 3, 0],
|
|
304
|
+
12
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
world.creatures.push({
|
|
308
|
+
mesh: wisp,
|
|
309
|
+
type: 'wisp',
|
|
310
|
+
x: (Math.random() - 0.5) * 70,
|
|
311
|
+
y: 3,
|
|
312
|
+
z: (Math.random() - 0.5) * 70,
|
|
313
|
+
vx: 0,
|
|
314
|
+
vy: 0,
|
|
315
|
+
vz: 0,
|
|
316
|
+
glowPhase: Math.random() * Math.PI * 2,
|
|
317
|
+
trail: [],
|
|
318
|
+
stunTimer: 0,
|
|
319
|
+
caught: false,
|
|
320
|
+
aggressive: true,
|
|
321
|
+
atk: 8,
|
|
322
|
+
aggroRange: 18,
|
|
323
|
+
speed: 5 + Math.random() * 3,
|
|
324
|
+
attackCD: 0,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Fire sprites — fast, aggressive, appear near monoliths
|
|
329
|
+
for (let i = 0; i < 4; i++) {
|
|
330
|
+
const sprite = createAdvancedSphere(
|
|
331
|
+
0.4,
|
|
332
|
+
{
|
|
333
|
+
color: 0xff4400,
|
|
334
|
+
emissive: 0x882200,
|
|
335
|
+
emissiveIntensity: 1.0,
|
|
336
|
+
holographic: true,
|
|
337
|
+
animated: true,
|
|
338
|
+
transparent: true,
|
|
339
|
+
opacity: 0.9,
|
|
340
|
+
},
|
|
341
|
+
[0, 4, 0],
|
|
342
|
+
10
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
world.creatures.push({
|
|
346
|
+
mesh: sprite,
|
|
347
|
+
type: 'fire',
|
|
348
|
+
x: (Math.random() - 0.5) * 50,
|
|
349
|
+
y: 4,
|
|
350
|
+
z: (Math.random() - 0.5) * 50,
|
|
351
|
+
vx: 0,
|
|
352
|
+
vy: 0,
|
|
353
|
+
vz: 0,
|
|
354
|
+
glowPhase: Math.random() * Math.PI * 2,
|
|
355
|
+
trail: [],
|
|
356
|
+
stunTimer: 0,
|
|
357
|
+
caught: false,
|
|
358
|
+
aggressive: true,
|
|
359
|
+
atk: 12,
|
|
360
|
+
aggroRange: 14,
|
|
361
|
+
speed: 8 + Math.random() * 3,
|
|
362
|
+
attackCD: 0,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function update(dt) {
|
|
368
|
+
// Handle start screen
|
|
369
|
+
if (gameState === 'start') {
|
|
370
|
+
startScreenTime += dt;
|
|
371
|
+
updateAllButtons();
|
|
372
|
+
// Still update world animations in background
|
|
373
|
+
time += dt;
|
|
374
|
+
dayNightCycle += dt * 0.1;
|
|
375
|
+
updateTerrain(dt);
|
|
376
|
+
updateCrystals(dt);
|
|
377
|
+
updateLighting();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle game over
|
|
382
|
+
if (gameState === 'gameover') {
|
|
383
|
+
updateAllButtons();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Handle win
|
|
388
|
+
if (gameState === 'win') {
|
|
389
|
+
time += dt;
|
|
390
|
+
updateAllButtons();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Playing state
|
|
395
|
+
time += dt;
|
|
396
|
+
dayNightCycle += dt * 0.1;
|
|
397
|
+
playTime += dt;
|
|
398
|
+
|
|
399
|
+
// Update player movement
|
|
400
|
+
updatePlayer(dt);
|
|
401
|
+
|
|
402
|
+
// Update camera
|
|
403
|
+
updateCamera(dt);
|
|
404
|
+
|
|
405
|
+
// Update world elements
|
|
406
|
+
updateTerrain(dt);
|
|
407
|
+
updateCrystals(dt);
|
|
408
|
+
updateCreatures(dt);
|
|
409
|
+
updateWeather(dt);
|
|
410
|
+
|
|
411
|
+
// Update lighting based on time of day
|
|
412
|
+
updateLighting();
|
|
413
|
+
|
|
414
|
+
// Particle system
|
|
415
|
+
updateParticles(dt);
|
|
416
|
+
|
|
417
|
+
// Check for crystal collection
|
|
418
|
+
checkCrystalCollection();
|
|
419
|
+
|
|
420
|
+
// Magic bolts and creature catching
|
|
421
|
+
updateMagicBolts(dt);
|
|
422
|
+
checkCreatureCollection();
|
|
423
|
+
checkWinCondition();
|
|
424
|
+
|
|
425
|
+
// Check game over
|
|
426
|
+
if (player.health <= 0 && gameState === 'playing') {
|
|
427
|
+
gameState = 'gameover';
|
|
428
|
+
initGameOverScreen();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function updatePlayer(dt) {
|
|
433
|
+
// Simple physics
|
|
434
|
+
player.jumpVelocity -= 25 * dt; // gravity
|
|
435
|
+
player.y += player.jumpVelocity * dt;
|
|
436
|
+
|
|
437
|
+
// Ground collision
|
|
438
|
+
if (player.y <= 2) {
|
|
439
|
+
player.y = 2;
|
|
440
|
+
player.jumpVelocity = 0;
|
|
441
|
+
player.onGround = true;
|
|
442
|
+
} else {
|
|
443
|
+
player.onGround = false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Input handling
|
|
447
|
+
let inputX = 0,
|
|
448
|
+
inputZ = 0;
|
|
449
|
+
|
|
450
|
+
if (key('KeyW') || key('ArrowUp')) inputZ = -1;
|
|
451
|
+
if (key('KeyS') || key('ArrowDown')) inputZ = 1;
|
|
452
|
+
if (key('KeyA') || key('ArrowLeft')) inputX = -1;
|
|
453
|
+
if (key('KeyD') || key('ArrowRight')) inputX = 1;
|
|
454
|
+
|
|
455
|
+
if (key('Space') && player.onGround) {
|
|
456
|
+
player.jumpVelocity = 12;
|
|
457
|
+
player.onGround = false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Movement
|
|
461
|
+
if (inputX !== 0 || inputZ !== 0) {
|
|
462
|
+
const moveSpeed = player.speed * dt;
|
|
463
|
+
player.x += inputX * moveSpeed;
|
|
464
|
+
player.z += inputZ * moveSpeed;
|
|
465
|
+
|
|
466
|
+
// Rotation based on movement
|
|
467
|
+
if (inputX !== 0 || inputZ !== 0) {
|
|
468
|
+
player.rotation = Math.atan2(inputX, inputZ);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Magic recharge
|
|
473
|
+
updateCooldown(magicCD, dt);
|
|
474
|
+
player.magicRechargeTimer -= dt;
|
|
475
|
+
if (player.magicRechargeTimer <= 0 && player.magicCharges < player.maxMagicCharges) {
|
|
476
|
+
player.magicCharges++;
|
|
477
|
+
player.magicRechargeTimer = 3;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Cast magic bolt
|
|
481
|
+
if (keyp('KeyE') && player.magicCharges > 0 && useCooldown(magicCD)) {
|
|
482
|
+
fireMagicBolt();
|
|
483
|
+
sfx('jump');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Keep player in bounds
|
|
487
|
+
player.x = Math.max(-45, Math.min(45, player.x));
|
|
488
|
+
player.z = Math.max(-45, Math.min(45, player.z));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function fireMagicBolt() {
|
|
492
|
+
const dx = -Math.sin(player.rotation);
|
|
493
|
+
const dz = -Math.cos(player.rotation);
|
|
494
|
+
const speed = 28;
|
|
495
|
+
magicBolts.push({
|
|
496
|
+
mesh: createSphere(0.35, 0xff44ff, [player.x + dx, player.y + 1.5, player.z + dz]),
|
|
497
|
+
x: player.x + dx,
|
|
498
|
+
y: player.y + 1.5,
|
|
499
|
+
z: player.z + dz,
|
|
500
|
+
vx: dx * speed,
|
|
501
|
+
vy: 1.5,
|
|
502
|
+
vz: dz * speed,
|
|
503
|
+
life: 2.5,
|
|
504
|
+
});
|
|
505
|
+
player.magicCharges--;
|
|
506
|
+
triggerShake(shake, 0.8);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function updateMagicBolts(dt) {
|
|
510
|
+
for (let i = magicBolts.length - 1; i >= 0; i--) {
|
|
511
|
+
const bolt = magicBolts[i];
|
|
512
|
+
bolt.life -= dt;
|
|
513
|
+
if (bolt.life <= 0) {
|
|
514
|
+
removeMesh(bolt.mesh);
|
|
515
|
+
magicBolts.splice(i, 1);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
bolt.x += bolt.vx * dt;
|
|
519
|
+
bolt.y += bolt.vy * dt;
|
|
520
|
+
bolt.z += bolt.vz * dt;
|
|
521
|
+
bolt.vy -= 4 * dt; // gentle arc
|
|
522
|
+
setPosition(bolt.mesh, bolt.x, bolt.y, bolt.z);
|
|
523
|
+
|
|
524
|
+
// Check hit on creatures
|
|
525
|
+
world.creatures.forEach(creature => {
|
|
526
|
+
if (creature.caught || creature.stunTimer > 0) return;
|
|
527
|
+
const dx = bolt.x - creature.x;
|
|
528
|
+
const dy = bolt.y - creature.y;
|
|
529
|
+
const dz = bolt.z - creature.z;
|
|
530
|
+
if (Math.sqrt(dx * dx + dy * dy + dz * dz) < 2.5) {
|
|
531
|
+
creature.stunTimer = 5;
|
|
532
|
+
creature.vx = 0;
|
|
533
|
+
creature.vy = 0;
|
|
534
|
+
creature.vz = 0;
|
|
535
|
+
removeMesh(bolt.mesh);
|
|
536
|
+
magicBolts.splice(i, 1);
|
|
537
|
+
score += 10;
|
|
538
|
+
triggerShake(shake, 1.5);
|
|
539
|
+
sfx('explosion');
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function checkCreatureCollection() {
|
|
546
|
+
world.creatures.forEach(creature => {
|
|
547
|
+
if (creature.caught || creature.stunTimer <= 0) return;
|
|
548
|
+
const dx = player.x - creature.x;
|
|
549
|
+
const dy = player.y - creature.y;
|
|
550
|
+
const dz = player.z - creature.z;
|
|
551
|
+
if (Math.sqrt(dx * dx + dy * dy + dz * dz) < 3) {
|
|
552
|
+
creature.caught = true;
|
|
553
|
+
setMeshVisible(creature.mesh, false);
|
|
554
|
+
creaturesKept++;
|
|
555
|
+
score += 50;
|
|
556
|
+
triggerShake(shake, 2);
|
|
557
|
+
sfx('coin');
|
|
558
|
+
// Particle burst
|
|
559
|
+
for (let i = 0; i < 8; i++) spawnParticle(creature.x, creature.y, creature.z, 0xff88ff);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function checkWinCondition() {
|
|
565
|
+
if (gameState !== 'playing') return;
|
|
566
|
+
const allCrystals = world.crystals.every(c => c.collected);
|
|
567
|
+
const allCreatures = world.creatures.every(c => c.caught);
|
|
568
|
+
if (allCrystals && allCreatures) {
|
|
569
|
+
gameState = 'win';
|
|
570
|
+
initWinScreen();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function initWinScreen() {
|
|
575
|
+
uiButtons = [];
|
|
576
|
+
uiButtons.push(
|
|
577
|
+
createButton(
|
|
578
|
+
centerX(200),
|
|
579
|
+
340,
|
|
580
|
+
200,
|
|
581
|
+
50,
|
|
582
|
+
'↻ PLAY AGAIN',
|
|
583
|
+
() => {
|
|
584
|
+
resetGame();
|
|
585
|
+
gameState = 'playing';
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
normalColor: uiColors.success,
|
|
589
|
+
hoverColor: rgba8(60, 220, 120, 255),
|
|
590
|
+
pressedColor: rgba8(30, 160, 80, 255),
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
uiButtons.push(
|
|
595
|
+
createButton(
|
|
596
|
+
centerX(200),
|
|
597
|
+
405,
|
|
598
|
+
200,
|
|
599
|
+
45,
|
|
600
|
+
'← MAIN MENU',
|
|
601
|
+
() => {
|
|
602
|
+
resetGame();
|
|
603
|
+
gameState = 'start';
|
|
604
|
+
initStartScreen();
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
normalColor: uiColors.primary,
|
|
608
|
+
hoverColor: rgba8(50, 150, 255, 255),
|
|
609
|
+
pressedColor: rgba8(20, 100, 200, 255),
|
|
610
|
+
}
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function drawWinScreen() {
|
|
616
|
+
drawGradientRect(0, 0, 640, 480, rgba8(10, 30, 10, 220), rgba8(20, 80, 20, 240), true);
|
|
617
|
+
const glow = (Math.sin(time * 3) + 1) * 0.5;
|
|
618
|
+
setFont('huge');
|
|
619
|
+
setTextAlign('center');
|
|
620
|
+
drawTextShadow('QUEST COMPLETE!', 320, 60, rgba8(255, 215, 0, 255), rgba8(0, 0, 0, 255), 5, 1);
|
|
621
|
+
setFont('large');
|
|
622
|
+
drawText('The realm is saved!', 320, 130, rgba8(Math.floor(100 + 155 * glow), 255, 100, 255), 1);
|
|
623
|
+
const panel = createPanel(centerX(420), 170, 420, 140, {
|
|
624
|
+
bgColor: rgba8(20, 40, 20, 220),
|
|
625
|
+
borderColor: rgba8(100, 200, 100, 255),
|
|
626
|
+
borderWidth: 3,
|
|
627
|
+
shadow: true,
|
|
628
|
+
title: 'FINAL SCORE',
|
|
629
|
+
titleBgColor: rgba8(60, 160, 60, 255),
|
|
630
|
+
});
|
|
631
|
+
drawPanel(panel);
|
|
632
|
+
setFont('normal');
|
|
633
|
+
setTextAlign('center');
|
|
634
|
+
const minutes = Math.floor(playTime / 60);
|
|
635
|
+
const seconds = Math.floor(playTime % 60);
|
|
636
|
+
drawText(`Score: ${score}`, 320, 215, rgba8(255, 215, 0, 255), 1);
|
|
637
|
+
drawText(
|
|
638
|
+
`Crystals: ${crystalsCollected} / ${world.crystals.length}`,
|
|
639
|
+
320,
|
|
640
|
+
240,
|
|
641
|
+
uiColors.light,
|
|
642
|
+
1
|
|
643
|
+
);
|
|
644
|
+
drawText(
|
|
645
|
+
`Creatures caught: ${creaturesKept} / ${world.creatures.length}`,
|
|
646
|
+
320,
|
|
647
|
+
265,
|
|
648
|
+
uiColors.light,
|
|
649
|
+
1
|
|
650
|
+
);
|
|
651
|
+
drawText(`Time: ${minutes}m ${seconds}s`, 320, 290, uiColors.secondary, 1);
|
|
652
|
+
drawAllButtons();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function updateCamera(dt) {
|
|
656
|
+
// Smooth camera follow
|
|
657
|
+
const targetX = player.x + camera.offset.x;
|
|
658
|
+
const targetY = player.y + camera.offset.y;
|
|
659
|
+
const targetZ = player.z + camera.offset.z;
|
|
660
|
+
|
|
661
|
+
// Add camera shake for dramatic effect
|
|
662
|
+
updateShake(shake, dt);
|
|
663
|
+
const [shakeX, shakeY] = getShakeOffset(shake);
|
|
664
|
+
|
|
665
|
+
setCameraPosition(targetX + shakeX, targetY + shakeY, targetZ);
|
|
666
|
+
setCameraTarget(player.x, player.y + 2, player.z);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function updateTerrain(dt) {
|
|
670
|
+
world.terrain.forEach(element => {
|
|
671
|
+
if (element.type === 'hill') {
|
|
672
|
+
// Gentle bobbing motion
|
|
673
|
+
element.bobPhase += dt;
|
|
674
|
+
const bobY = Math.sin(element.bobPhase * 0.5) * 0.2;
|
|
675
|
+
setPosition(
|
|
676
|
+
element.mesh,
|
|
677
|
+
getPosition(element.mesh)[0],
|
|
678
|
+
element.originalY + bobY,
|
|
679
|
+
getPosition(element.mesh)[2]
|
|
680
|
+
);
|
|
681
|
+
} else if (element.type === 'monolith') {
|
|
682
|
+
// Mysterious glowing
|
|
683
|
+
element.glowPhase += dt * 3;
|
|
684
|
+
const glow = (Math.sin(element.glowPhase) + 1) * 0.5;
|
|
685
|
+
// Color intensity varies
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Animate instanced tree crowns (sway in wind)
|
|
690
|
+
if (treeCrownInstanceId !== null && treeMeta.length > 0) {
|
|
691
|
+
for (let i = 0; i < treeMeta.length; i++) {
|
|
692
|
+
const t = treeMeta[i];
|
|
693
|
+
t.swayPhase += dt * 2;
|
|
694
|
+
const swayX = Math.sin(t.swayPhase) * 0.3;
|
|
695
|
+
setInstanceTransform(treeCrownInstanceId, i, t.x + swayX, 10, t.z);
|
|
696
|
+
}
|
|
697
|
+
finalizeInstances(treeCrownInstanceId);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function updateCrystals(dt) {
|
|
702
|
+
world.crystals.forEach(crystal => {
|
|
703
|
+
if (crystal.collected) return;
|
|
704
|
+
|
|
705
|
+
// Rotation
|
|
706
|
+
rotateMesh(crystal.mesh, 0, dt * crystal.rotationSpeed, 0);
|
|
707
|
+
|
|
708
|
+
// Bobbing motion
|
|
709
|
+
crystal.bobPhase += dt * 2;
|
|
710
|
+
const bobY = Math.sin(crystal.bobPhase) * 0.5;
|
|
711
|
+
setPosition(crystal.mesh, crystal.x, crystal.y + bobY, crystal.z);
|
|
712
|
+
|
|
713
|
+
// Glowing effect
|
|
714
|
+
crystal.glowIntensity += dt * 2;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function updateCreatures(dt) {
|
|
719
|
+
world.creatures.forEach(creature => {
|
|
720
|
+
if (creature.caught) return;
|
|
721
|
+
|
|
722
|
+
if (creature.stunTimer > 0) {
|
|
723
|
+
// Stunned — blink and drift toward ground
|
|
724
|
+
creature.stunTimer -= dt;
|
|
725
|
+
creature.glowPhase += dt * 20;
|
|
726
|
+
setMeshVisible(creature.mesh, Math.sin(creature.glowPhase * 5) > 0);
|
|
727
|
+
const groundY = creature.type === 'orb' ? 4 : 2;
|
|
728
|
+
creature.y = Math.max(groundY, creature.y - 4 * dt);
|
|
729
|
+
setPosition(creature.mesh, creature.x, creature.y, creature.z);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Normal visible state
|
|
734
|
+
setMeshVisible(creature.mesh, true);
|
|
735
|
+
creature.glowPhase += dt * 5;
|
|
736
|
+
|
|
737
|
+
// Aggressive creatures chase and attack player
|
|
738
|
+
if (creature.aggressive && creature.attackCD > 0) {
|
|
739
|
+
creature.attackCD -= dt;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (creature.type === 'orb') {
|
|
743
|
+
// Flying movement — passive
|
|
744
|
+
creature.x += creature.vx * dt;
|
|
745
|
+
creature.y += creature.vy * dt;
|
|
746
|
+
creature.z += creature.vz * dt;
|
|
747
|
+
|
|
748
|
+
if (Math.abs(creature.x) > 40) creature.vx *= -0.8;
|
|
749
|
+
if (creature.y < 10 || creature.y > 25) creature.vy *= -0.8;
|
|
750
|
+
if (Math.abs(creature.z) > 40) creature.vz *= -0.8;
|
|
751
|
+
|
|
752
|
+
if (Math.random() < 0.01) {
|
|
753
|
+
creature.vx += (Math.random() - 0.5) * 2;
|
|
754
|
+
creature.vy += (Math.random() - 0.5) * 1;
|
|
755
|
+
creature.vz += (Math.random() - 0.5) * 2;
|
|
756
|
+
}
|
|
757
|
+
} else if (creature.type === 'wisp' || creature.type === 'fire') {
|
|
758
|
+
// Aggressive ground creatures — chase player when in range
|
|
759
|
+
const dx = player.x - creature.x;
|
|
760
|
+
const dz = player.z - creature.z;
|
|
761
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
762
|
+
|
|
763
|
+
if (dist < creature.aggroRange && dist > 1.5) {
|
|
764
|
+
const spd = creature.speed * dt;
|
|
765
|
+
creature.x += (dx / dist) * spd;
|
|
766
|
+
creature.z += (dz / dist) * spd;
|
|
767
|
+
} else if (dist >= creature.aggroRange) {
|
|
768
|
+
// Idle wander
|
|
769
|
+
if (Math.random() < 0.02) {
|
|
770
|
+
creature.vx = (Math.random() - 0.5) * 3;
|
|
771
|
+
creature.vz = (Math.random() - 0.5) * 3;
|
|
772
|
+
}
|
|
773
|
+
creature.x += creature.vx * dt;
|
|
774
|
+
creature.z += creature.vz * dt;
|
|
775
|
+
creature.vx *= 0.98;
|
|
776
|
+
creature.vz *= 0.98;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Hover bob
|
|
780
|
+
const baseY = creature.type === 'wisp' ? 3 : 4;
|
|
781
|
+
creature.y = baseY + Math.sin(creature.glowPhase) * 0.5;
|
|
782
|
+
|
|
783
|
+
// Deal damage on contact
|
|
784
|
+
if (dist < 2.5 && creature.attackCD <= 0) {
|
|
785
|
+
player.health -= creature.atk;
|
|
786
|
+
creature.attackCD = 1.2;
|
|
787
|
+
triggerShake(shake, 2);
|
|
788
|
+
sfx('explosion');
|
|
789
|
+
for (let i = 0; i < 4; i++) spawnParticle(player.x, player.y + 1, player.z, 0xff2222);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Keep in bounds
|
|
793
|
+
creature.x = Math.max(-44, Math.min(44, creature.x));
|
|
794
|
+
creature.z = Math.max(-44, Math.min(44, creature.z));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
setPosition(creature.mesh, creature.x, creature.y, creature.z);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function updateWeather(dt) {
|
|
802
|
+
// Weather system
|
|
803
|
+
if (Math.random() < 0.001) {
|
|
804
|
+
// Random weather change
|
|
805
|
+
const weathers = ['clear', 'rain', 'storm', 'mystical'];
|
|
806
|
+
world.weather.type = weathers[Math.floor(Math.random() * weathers.length)];
|
|
807
|
+
world.weather.intensity = Math.random();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Lightning effects during storms
|
|
811
|
+
if (world.weather.type === 'storm') {
|
|
812
|
+
lightningTimer += dt;
|
|
813
|
+
if (lightningTimer > 2 + Math.random() * 3) {
|
|
814
|
+
// Lightning flash
|
|
815
|
+
setLightColor(0xffffff);
|
|
816
|
+
triggerShake(shake, 3);
|
|
817
|
+
lightningTimer = 0;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function updateLighting() {
|
|
823
|
+
// Day/night cycle
|
|
824
|
+
const dayPhase = (Math.sin(dayNightCycle) + 1) * 0.5;
|
|
825
|
+
|
|
826
|
+
// Sunrise/sunset colors
|
|
827
|
+
let lightColor = 0xffffff;
|
|
828
|
+
let ambientColor = 0x404040;
|
|
829
|
+
let fogColor = 0x202040;
|
|
830
|
+
|
|
831
|
+
if (dayPhase < 0.3) {
|
|
832
|
+
// Night
|
|
833
|
+
lightColor = 0x4444aa;
|
|
834
|
+
ambientColor = 0x202040;
|
|
835
|
+
fogColor = 0x101030;
|
|
836
|
+
} else if (dayPhase < 0.5) {
|
|
837
|
+
// Dawn
|
|
838
|
+
lightColor = 0xffaa44;
|
|
839
|
+
ambientColor = 0x404030;
|
|
840
|
+
fogColor = 0x403020;
|
|
841
|
+
} else if (dayPhase < 0.8) {
|
|
842
|
+
// Day
|
|
843
|
+
lightColor = 0xffffdd;
|
|
844
|
+
ambientColor = 0x606060;
|
|
845
|
+
fogColor = 0x808080;
|
|
846
|
+
} else {
|
|
847
|
+
// Dusk
|
|
848
|
+
lightColor = 0xff6644;
|
|
849
|
+
ambientColor = 0x404020;
|
|
850
|
+
fogColor = 0x402010;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Apply lighting
|
|
854
|
+
setLightDirection(-0.5, -1, -0.3);
|
|
855
|
+
setLightColor(lightColor);
|
|
856
|
+
setAmbientLight(ambientColor);
|
|
857
|
+
setFog(fogColor, 30, 80);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function updateParticles(dt) {
|
|
861
|
+
// Simple particle system for mystical effects
|
|
862
|
+
if (Math.random() < 0.1) {
|
|
863
|
+
// Add sparkle particles around crystals
|
|
864
|
+
world.crystals.forEach(crystal => {
|
|
865
|
+
if (!crystal.collected && Math.random() < 0.05) {
|
|
866
|
+
spawnParticle(crystal.x, crystal.y + 2, crystal.z, crystal.color);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function spawnParticle(x, y, z, color) {
|
|
873
|
+
const particle = createSphere(0.1, color, [x, y, z]);
|
|
874
|
+
world.particles.push({
|
|
875
|
+
mesh: particle,
|
|
876
|
+
x,
|
|
877
|
+
y,
|
|
878
|
+
z,
|
|
879
|
+
vx: (Math.random() - 0.5) * 4,
|
|
880
|
+
vy: Math.random() * 3 + 2,
|
|
881
|
+
vz: (Math.random() - 0.5) * 4,
|
|
882
|
+
life: 2,
|
|
883
|
+
maxLife: 2,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function checkCrystalCollection() {
|
|
888
|
+
world.crystals.forEach(crystal => {
|
|
889
|
+
if (crystal.collected) return;
|
|
890
|
+
|
|
891
|
+
const dx = player.x - crystal.x;
|
|
892
|
+
const dy = player.y - crystal.y;
|
|
893
|
+
const dz = player.z - crystal.z;
|
|
894
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
895
|
+
|
|
896
|
+
if (distance < 3) {
|
|
897
|
+
crystal.collected = true;
|
|
898
|
+
crystalsCollected++;
|
|
899
|
+
score += 25;
|
|
900
|
+
setPosition(crystal.mesh, -1000, -1000, -1000);
|
|
901
|
+
sfx('coin');
|
|
902
|
+
|
|
903
|
+
// Particle burst effect
|
|
904
|
+
for (let i = 0; i < 10; i++) {
|
|
905
|
+
spawnParticle(
|
|
906
|
+
crystal.x + (Math.random() - 0.5) * 2,
|
|
907
|
+
crystal.y + Math.random() * 2,
|
|
908
|
+
crystal.z + (Math.random() - 0.5) * 2,
|
|
909
|
+
crystal.color
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
triggerShake(shake, 1);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export function draw() {
|
|
919
|
+
// Handle start screen
|
|
920
|
+
if (gameState === 'start') {
|
|
921
|
+
drawStartScreen();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Handle game over
|
|
926
|
+
if (gameState === 'gameover') {
|
|
927
|
+
drawGameOverScreen();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Handle win
|
|
932
|
+
if (gameState === 'win') {
|
|
933
|
+
drawWinScreen();
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Playing state - Atmospheric UI
|
|
938
|
+
const dayPhase = (Math.sin(dayNightCycle) + 1) * 0.5;
|
|
939
|
+
let timeOfDay = 'Day';
|
|
940
|
+
if (dayPhase < 0.3) timeOfDay = 'Night';
|
|
941
|
+
else if (dayPhase < 0.5) timeOfDay = 'Dawn';
|
|
942
|
+
else if (dayPhase > 0.8) timeOfDay = 'Dusk';
|
|
943
|
+
|
|
944
|
+
// Title and info
|
|
945
|
+
print('🏰 MYSTICAL REALM 3D', 8, 8, rgba8(255, 215, 0, 255));
|
|
946
|
+
print('Nintendo 64 / PlayStation Fantasy World', 8, 24, rgba8(200, 150, 255, 255));
|
|
947
|
+
|
|
948
|
+
// Game stats
|
|
949
|
+
const collectedCrystals = world.crystals.filter(c => c.collected).length;
|
|
950
|
+
print(`Time: ${timeOfDay} | Weather: ${world.weather.type}`, 8, 50, rgba8(150, 200, 255, 255));
|
|
951
|
+
print(
|
|
952
|
+
`Crystals: ${collectedCrystals}/${world.crystals.length}`,
|
|
953
|
+
8,
|
|
954
|
+
66,
|
|
955
|
+
rgba8(255, 200, 100, 255)
|
|
956
|
+
);
|
|
957
|
+
const caughtCount = world.creatures.filter(c => c.caught).length;
|
|
958
|
+
print(`Creatures: ${caughtCount}/${world.creatures.length}`, 8, 82, rgba8(255, 150, 255, 255));
|
|
959
|
+
print(
|
|
960
|
+
`Position: ${player.x.toFixed(1)}, ${player.y.toFixed(1)}, ${player.z.toFixed(1)}`,
|
|
961
|
+
8,
|
|
962
|
+
98,
|
|
963
|
+
rgba8(100, 255, 150, 255)
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
// 3D stats
|
|
967
|
+
const stats = get3DStats();
|
|
968
|
+
if (stats && stats.render) {
|
|
969
|
+
print(
|
|
970
|
+
`3D Objects: ${world.terrain.length + world.crystals.length + world.creatures.length}`,
|
|
971
|
+
8,
|
|
972
|
+
114,
|
|
973
|
+
rgba8(150, 150, 255, 255)
|
|
974
|
+
);
|
|
975
|
+
print(
|
|
976
|
+
`GPU: ${stats.renderer || 'Three.js'} | Effects: Bloom+Dither+Pixel`,
|
|
977
|
+
8,
|
|
978
|
+
130,
|
|
979
|
+
rgba8(150, 150, 255, 255)
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Magic charge indicator
|
|
984
|
+
print('Magic:', 8, 148, rgba8(255, 100, 255, 255));
|
|
985
|
+
for (let i = 0; i < player.maxMagicCharges; i++) {
|
|
986
|
+
const filled = i < player.magicCharges;
|
|
987
|
+
print(
|
|
988
|
+
filled ? '✦' : '✧',
|
|
989
|
+
60 + i * 18,
|
|
990
|
+
148,
|
|
991
|
+
filled ? rgba8(255, 100, 255, 255) : rgba8(100, 50, 100, 180)
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Controls
|
|
996
|
+
print('WASD: Move | Space: Jump | E: Magic Bolt', 8, 300, rgba8(200, 200, 200, 180));
|
|
997
|
+
print('Stun creatures with magic, walk into them to catch!', 8, 316, rgba8(255, 255, 100, 200));
|
|
998
|
+
print('Collect all crystals + catch all creatures to win!', 8, 332, rgba8(100, 255, 100, 180));
|
|
999
|
+
|
|
1000
|
+
// Weather indicator
|
|
1001
|
+
if (world.weather.type === 'storm') {
|
|
1002
|
+
print('⚡ STORM APPROACHING ⚡', 200, 8, rgba8(255, 255, 0, 255));
|
|
1003
|
+
} else if (world.weather.type === 'mystical') {
|
|
1004
|
+
print('✨ Mystical energies swirl... ✨', 200, 8, rgba8(255, 100, 255, 255));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Health bar
|
|
1008
|
+
const healthPct = player.health / player.maxHealth;
|
|
1009
|
+
const hpColor =
|
|
1010
|
+
healthPct > 0.5
|
|
1011
|
+
? rgba8(50, 200, 50, 255)
|
|
1012
|
+
: healthPct > 0.25
|
|
1013
|
+
? rgba8(220, 180, 30, 255)
|
|
1014
|
+
: rgba8(200, 40, 40, 255);
|
|
1015
|
+
drawProgressBar(16, 168, 160, 10, healthPct, hpColor, rgba8(20, 10, 30, 220));
|
|
1016
|
+
print(`HP ${player.health}/${player.maxHealth}`, 16, 158, rgba8(255, 200, 255, 220));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function drawStartScreen() {
|
|
1020
|
+
// Mystical gradient background
|
|
1021
|
+
drawGradientRect(0, 0, 640, 360, rgba8(20, 10, 40, 220), rgba8(50, 20, 80, 240), true);
|
|
1022
|
+
|
|
1023
|
+
// Animated title with magical glow
|
|
1024
|
+
const glow = Math.sin(startScreenTime * 2) * 0.3 + 0.7;
|
|
1025
|
+
const glowColor = rgba8(
|
|
1026
|
+
Math.floor(200 * glow),
|
|
1027
|
+
Math.floor(100 * glow),
|
|
1028
|
+
Math.floor(255 * glow),
|
|
1029
|
+
255
|
|
1030
|
+
);
|
|
1031
|
+
|
|
1032
|
+
setFont('huge');
|
|
1033
|
+
setTextAlign('center');
|
|
1034
|
+
const bounce = Math.sin(startScreenTime * 2) * 12;
|
|
1035
|
+
drawTextShadow('MYSTICAL', 320, 50 + bounce, glowColor, rgba8(0, 0, 0, 255), 5, 1);
|
|
1036
|
+
drawTextShadow('REALM', 320, 100 + bounce, rgba8(255, 215, 0, 255), rgba8(0, 0, 0, 255), 5, 1);
|
|
1037
|
+
|
|
1038
|
+
// Subtitle with pulse
|
|
1039
|
+
setFont('large');
|
|
1040
|
+
const pulse = Math.sin(startScreenTime * 3) * 0.2 + 0.8;
|
|
1041
|
+
drawTextOutline(
|
|
1042
|
+
'3D Fantasy Adventure',
|
|
1043
|
+
320,
|
|
1044
|
+
150,
|
|
1045
|
+
rgba8(150, 100, 255, Math.floor(pulse * 255)),
|
|
1046
|
+
rgba8(0, 0, 0, 255),
|
|
1047
|
+
1
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
// Info panel
|
|
1051
|
+
const panel = createPanel(centerX(420), 340, 420, 200, {
|
|
1052
|
+
bgColor: rgba8(20, 10, 40, 200),
|
|
1053
|
+
borderColor: rgba8(100, 50, 200, 255),
|
|
1054
|
+
borderWidth: 3,
|
|
1055
|
+
shadow: true,
|
|
1056
|
+
gradient: true,
|
|
1057
|
+
gradientColor: rgba8(40, 20, 60, 200),
|
|
1058
|
+
});
|
|
1059
|
+
drawPanel(panel);
|
|
1060
|
+
|
|
1061
|
+
// Quest description
|
|
1062
|
+
setFont('normal');
|
|
1063
|
+
setTextAlign('center');
|
|
1064
|
+
drawText('QUEST BRIEFING', 320, 185, rgba8(255, 215, 0, 255), 1);
|
|
1065
|
+
|
|
1066
|
+
setFont('small');
|
|
1067
|
+
drawText('A mystical realm awaits exploration', 320, 210, uiColors.light, 1);
|
|
1068
|
+
drawText('Collect magical crystals scattered across the land', 320, 225, uiColors.light, 1);
|
|
1069
|
+
drawText('Navigate through day, night, and mystical storms', 320, 240, uiColors.light, 1);
|
|
1070
|
+
|
|
1071
|
+
setFont('tiny');
|
|
1072
|
+
drawText('WASD = Move | Space = Jump | E = Magic Bolt', 320, 270, uiColors.secondary, 1);
|
|
1073
|
+
drawText(
|
|
1074
|
+
'Stun creatures • Catch them • Collect all crystals to WIN!',
|
|
1075
|
+
320,
|
|
1076
|
+
284,
|
|
1077
|
+
rgba8(255, 100, 255, 180),
|
|
1078
|
+
1
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
// Draw buttons
|
|
1082
|
+
drawAllButtons();
|
|
1083
|
+
|
|
1084
|
+
// Pulsing start prompt
|
|
1085
|
+
const alpha = Math.floor((Math.sin(startScreenTime * 5) * 0.5 + 0.5) * 255);
|
|
1086
|
+
setFont('normal');
|
|
1087
|
+
drawText('▶ PRESS BEGIN QUEST TO START ◀', 320, 305, rgba8(200, 100, 255, alpha), 1);
|
|
1088
|
+
|
|
1089
|
+
// Mystical particles hint
|
|
1090
|
+
setFont('tiny');
|
|
1091
|
+
drawText('Nintendo 64 / PlayStation Style Graphics', 320, 340, rgba8(150, 150, 200, 150), 1);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function drawGameOverScreen() {
|
|
1095
|
+
// Dark mystical overlay
|
|
1096
|
+
rect(0, 0, 640, 360, rgba8(10, 5, 20, 220), true);
|
|
1097
|
+
|
|
1098
|
+
// Game over title
|
|
1099
|
+
setFont('huge');
|
|
1100
|
+
setTextAlign('center');
|
|
1101
|
+
const flash = Math.floor(time * 2) % 2 === 0;
|
|
1102
|
+
const color = flash ? rgba8(200, 100, 255, 255) : rgba8(150, 50, 200, 255);
|
|
1103
|
+
drawTextShadow('QUEST ENDED', 320, 80, color, rgba8(0, 0, 0, 255), 5, 1);
|
|
1104
|
+
|
|
1105
|
+
// Stats panel
|
|
1106
|
+
const statsPanel = createPanel(centerX(420), centerY(220), 420, 220, {
|
|
1107
|
+
bgColor: rgba8(20, 10, 40, 220),
|
|
1108
|
+
borderColor: rgba8(100, 50, 200, 255),
|
|
1109
|
+
borderWidth: 3,
|
|
1110
|
+
shadow: true,
|
|
1111
|
+
title: 'FINAL STATISTICS',
|
|
1112
|
+
titleBgColor: rgba8(100, 50, 200, 255),
|
|
1113
|
+
});
|
|
1114
|
+
drawPanel(statsPanel);
|
|
1115
|
+
|
|
1116
|
+
// Stats
|
|
1117
|
+
setFont('large');
|
|
1118
|
+
setTextAlign('center');
|
|
1119
|
+
drawText(`Crystals Collected: ${crystalsCollected}`, 320, 200, rgba8(255, 215, 0, 255), 1);
|
|
1120
|
+
|
|
1121
|
+
setFont('normal');
|
|
1122
|
+
const minutes = Math.floor(playTime / 60);
|
|
1123
|
+
const seconds = Math.floor(playTime % 60);
|
|
1124
|
+
drawText(`Time Played: ${minutes}m ${seconds}s`, 320, 235, uiColors.secondary, 1);
|
|
1125
|
+
drawText(`Score: ${score}`, 320, 260, uiColors.success, 1);
|
|
1126
|
+
|
|
1127
|
+
// Draw buttons
|
|
1128
|
+
drawAllButtons();
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function initGameOverScreen() {
|
|
1132
|
+
uiButtons = [];
|
|
1133
|
+
|
|
1134
|
+
// Try again button
|
|
1135
|
+
uiButtons.push(
|
|
1136
|
+
createButton(
|
|
1137
|
+
centerX(200),
|
|
1138
|
+
310,
|
|
1139
|
+
200,
|
|
1140
|
+
50,
|
|
1141
|
+
'↻ TRY AGAIN',
|
|
1142
|
+
() => {
|
|
1143
|
+
resetGame();
|
|
1144
|
+
gameState = 'playing';
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
normalColor: uiColors.success,
|
|
1148
|
+
hoverColor: rgba8(60, 220, 120, 255),
|
|
1149
|
+
pressedColor: rgba8(30, 160, 80, 255),
|
|
1150
|
+
}
|
|
1151
|
+
)
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
// Main menu button
|
|
1155
|
+
uiButtons.push(
|
|
1156
|
+
createButton(
|
|
1157
|
+
centerX(200),
|
|
1158
|
+
375,
|
|
1159
|
+
200,
|
|
1160
|
+
45,
|
|
1161
|
+
'← MAIN MENU',
|
|
1162
|
+
() => {
|
|
1163
|
+
resetGame();
|
|
1164
|
+
gameState = 'start';
|
|
1165
|
+
initStartScreen();
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
normalColor: uiColors.primary,
|
|
1169
|
+
hoverColor: rgba8(50, 150, 255, 255),
|
|
1170
|
+
pressedColor: rgba8(20, 100, 200, 255),
|
|
1171
|
+
}
|
|
1172
|
+
)
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function resetGame() {
|
|
1177
|
+
player.health = player.maxHealth;
|
|
1178
|
+
player.x = 0;
|
|
1179
|
+
player.y = 5;
|
|
1180
|
+
player.z = 0;
|
|
1181
|
+
player.jumpVelocity = 0;
|
|
1182
|
+
player.magicCharges = player.maxMagicCharges;
|
|
1183
|
+
magicCD.remaining = 0;
|
|
1184
|
+
player.magicRechargeTimer = 0;
|
|
1185
|
+
playTime = 0;
|
|
1186
|
+
score = 0;
|
|
1187
|
+
crystalsCollected = 0;
|
|
1188
|
+
creaturesKept = 0;
|
|
1189
|
+
|
|
1190
|
+
// Reset crystals
|
|
1191
|
+
world.crystals.forEach(c => (c.collected = false));
|
|
1192
|
+
|
|
1193
|
+
// Reset creatures
|
|
1194
|
+
world.creatures.forEach(c => {
|
|
1195
|
+
c.stunTimer = 0;
|
|
1196
|
+
c.caught = false;
|
|
1197
|
+
setMeshVisible(c.mesh, true);
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// Remove stray magic bolts
|
|
1201
|
+
magicBolts.forEach(b => removeMesh(b.mesh));
|
|
1202
|
+
magicBolts = [];
|
|
1203
|
+
}
|