nova64 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/dist/runtime/api-2d.js +1158 -0
- package/dist/runtime/api-3d/camera.js +73 -0
- package/dist/runtime/api-3d/instancing.js +180 -0
- package/dist/runtime/api-3d/lights.js +51 -0
- package/dist/runtime/api-3d/materials.js +47 -0
- package/dist/runtime/api-3d/models.js +84 -0
- package/dist/runtime/api-3d/particles.js +296 -0
- package/dist/runtime/api-3d/pbr.js +113 -0
- package/dist/runtime/api-3d/primitives.js +304 -0
- package/dist/runtime/api-3d/scene.js +169 -0
- package/dist/runtime/api-3d/transforms.js +161 -0
- package/dist/runtime/api-3d.js +166 -0
- package/dist/runtime/api-effects.js +840 -0
- package/dist/runtime/api-gameutils.js +476 -0
- package/dist/runtime/api-generative.js +610 -0
- package/dist/runtime/api-presets.js +85 -0
- package/dist/runtime/api-skybox.js +232 -0
- package/dist/runtime/api-sprites.js +100 -0
- package/dist/runtime/api-voxel.js +712 -0
- package/dist/runtime/api.js +201 -0
- package/dist/runtime/assets.js +27 -0
- package/dist/runtime/audio.js +114 -0
- package/dist/runtime/collision.js +47 -0
- package/dist/runtime/console.js +101 -0
- package/dist/runtime/editor.js +233 -0
- package/dist/runtime/font.js +233 -0
- package/dist/runtime/framebuffer.js +28 -0
- package/dist/runtime/fullscreen-button.js +185 -0
- package/dist/runtime/gpu-canvas2d.js +47 -0
- package/dist/runtime/gpu-threejs.js +643 -0
- package/dist/runtime/gpu-webgl2.js +310 -0
- package/dist/runtime/index.d.ts +682 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/input.js +225 -0
- package/dist/runtime/logger.js +60 -0
- package/dist/runtime/physics.js +101 -0
- package/dist/runtime/screens.js +213 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/textinput.js +68 -0
- package/dist/runtime/ui/buttons.js +124 -0
- package/dist/runtime/ui/panels.js +105 -0
- package/dist/runtime/ui/text.js +86 -0
- package/dist/runtime/ui/widgets.js +141 -0
- package/dist/runtime/ui.js +111 -0
- package/index.html +6 -1
- package/package.json +9 -2
- 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 +20 -0
- package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
- package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
// SHADOW NINJA 3D - True 3.5D Ninja Platformer
|
|
2
|
+
// Nintendo 64 / PlayStation style 3D ninja action with parkour and stealth
|
|
3
|
+
// Inspired by Strider, Shinobi, and Ninja Gaiden
|
|
4
|
+
|
|
5
|
+
// Game state
|
|
6
|
+
let gameTime = 0;
|
|
7
|
+
let gameState = 'start'; // 'start', 'playing', 'gameOver'
|
|
8
|
+
let startScreenTime = 0;
|
|
9
|
+
let uiButtons = [];
|
|
10
|
+
let score = 0;
|
|
11
|
+
let level = 1;
|
|
12
|
+
let combo = 0;
|
|
13
|
+
let comboTimer = 0;
|
|
14
|
+
|
|
15
|
+
// 3D Game objects
|
|
16
|
+
let playerKnight = null;
|
|
17
|
+
let platforms = [];
|
|
18
|
+
let enemies = [];
|
|
19
|
+
let coins = [];
|
|
20
|
+
let particles = [];
|
|
21
|
+
let environment = [];
|
|
22
|
+
|
|
23
|
+
// Player state - NINJA POWERS!
|
|
24
|
+
let player = {
|
|
25
|
+
x: 0, y: 0, z: 0,
|
|
26
|
+
vx: 0, vy: 0, vz: 0,
|
|
27
|
+
onGround: false,
|
|
28
|
+
health: 100,
|
|
29
|
+
energy: 100,
|
|
30
|
+
coins: 0,
|
|
31
|
+
shuriken: 20,
|
|
32
|
+
facingRight: true,
|
|
33
|
+
jumpPower: 10,
|
|
34
|
+
speed: 8,
|
|
35
|
+
doubleJump: true,
|
|
36
|
+
attackCooldown: 0,
|
|
37
|
+
// Ninja abilities
|
|
38
|
+
wallRunning: false,
|
|
39
|
+
wallRunTime: 0,
|
|
40
|
+
maxWallRunTime: 1.5,
|
|
41
|
+
dashCooldown: 0,
|
|
42
|
+
dashDuration: 0,
|
|
43
|
+
crouching: false,
|
|
44
|
+
stealth: false,
|
|
45
|
+
grapplePoints: [],
|
|
46
|
+
grappling: false,
|
|
47
|
+
grappleTarget: null,
|
|
48
|
+
airDashAvailable: true,
|
|
49
|
+
slideDuration: 0
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Camera state
|
|
53
|
+
let camera = {
|
|
54
|
+
x: 0, y: 8, z: 15,
|
|
55
|
+
targetX: 0, targetY: 5, targetZ: 0,
|
|
56
|
+
smoothing: 0.1
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function init() {
|
|
60
|
+
// Clear all arrays to prevent mesh errors
|
|
61
|
+
enemies = []
|
|
62
|
+
coins = []
|
|
63
|
+
particles = []
|
|
64
|
+
platforms = []
|
|
65
|
+
environment = []
|
|
66
|
+
playerKnight = null
|
|
67
|
+
|
|
68
|
+
// Reset player state completely
|
|
69
|
+
player = {
|
|
70
|
+
pos: vec3(0, 5, 0),
|
|
71
|
+
vel: vec3(0, 0, 0),
|
|
72
|
+
yaw: 0,
|
|
73
|
+
grounded: false,
|
|
74
|
+
attacking: false,
|
|
75
|
+
attackTime: 0,
|
|
76
|
+
health: 100,
|
|
77
|
+
maxHealth: 100,
|
|
78
|
+
stamina: 100,
|
|
79
|
+
maxStamina: 100,
|
|
80
|
+
isDashing: false,
|
|
81
|
+
dashTime: 0,
|
|
82
|
+
wallRunning: false,
|
|
83
|
+
wallRunTime: 0,
|
|
84
|
+
wallSide: 0,
|
|
85
|
+
doubleJumpAvailable: true,
|
|
86
|
+
grappling: false,
|
|
87
|
+
grappleTarget: null,
|
|
88
|
+
sliding: false,
|
|
89
|
+
slideTime: 0,
|
|
90
|
+
shurikenCount: 10
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
score = 0
|
|
94
|
+
combo = 0
|
|
95
|
+
comboTime = 0
|
|
96
|
+
gameTime = 0
|
|
97
|
+
level = 1
|
|
98
|
+
gameState = 'start'
|
|
99
|
+
startScreenTime = 0
|
|
100
|
+
|
|
101
|
+
// Force canvas focus for keyboard events
|
|
102
|
+
console.log('🎮 Focusing canvas for input...')
|
|
103
|
+
const canvas = document.querySelector('canvas')
|
|
104
|
+
if (canvas) {
|
|
105
|
+
canvas.focus()
|
|
106
|
+
canvas.tabIndex = 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
createNinjaPlayer()
|
|
110
|
+
createNinjaWorld()
|
|
111
|
+
createPlatforms()
|
|
112
|
+
createGrapplePoints()
|
|
113
|
+
spawnEnemies()
|
|
114
|
+
spawnCoins()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function initStartScreen() {
|
|
118
|
+
uiButtons = [];
|
|
119
|
+
console.log('🥷 Start screen initialized');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function update() {
|
|
123
|
+
const dt = 1/60;
|
|
124
|
+
|
|
125
|
+
// Handle start screen
|
|
126
|
+
if (gameState === 'start') {
|
|
127
|
+
startScreenTime += dt;
|
|
128
|
+
|
|
129
|
+
// KEYBOARD FALLBACK: Press ENTER or SPACE to start
|
|
130
|
+
if (isKeyPressed('Enter') || isKeyPressed(' ') || isKeyPressed('Space')) {
|
|
131
|
+
console.log('🥷 Starting Shadow Ninja 3D via keyboard!');
|
|
132
|
+
gameState = 'playing';
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for any button press to start
|
|
137
|
+
for (let i = 0; i < 10; i++) {
|
|
138
|
+
if (btnp(i)) {
|
|
139
|
+
console.log(`🥷 Starting Shadow Ninja 3D via button ${i}!`);
|
|
140
|
+
gameState = 'playing';
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Playing state - main game loop
|
|
149
|
+
gameTime += dt;
|
|
150
|
+
|
|
151
|
+
// Update input
|
|
152
|
+
updateInput(dt);
|
|
153
|
+
|
|
154
|
+
// Update player
|
|
155
|
+
updatePlayer(dt);
|
|
156
|
+
|
|
157
|
+
// Update enemies
|
|
158
|
+
updateEnemies(dt);
|
|
159
|
+
|
|
160
|
+
// Update coins
|
|
161
|
+
updateCoins(dt);
|
|
162
|
+
|
|
163
|
+
// Update particles
|
|
164
|
+
updateParticles(dt);
|
|
165
|
+
|
|
166
|
+
// Update combo timer
|
|
167
|
+
if (comboTime > 0) {
|
|
168
|
+
comboTime -= dt;
|
|
169
|
+
if (comboTime <= 0) {
|
|
170
|
+
combo = 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for player death
|
|
175
|
+
if (player.health <= 0) {
|
|
176
|
+
gameState = 'gameover';
|
|
177
|
+
console.log('💀 Game Over! Final Score:', score);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function draw() {
|
|
182
|
+
if (gameState === 'start') {
|
|
183
|
+
|
|
184
|
+
export function draw() {
|
|
185
|
+
if (gameState === 'start') {
|
|
186
|
+
drawStartScreen();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 3D scene is automatically rendered by GPU backend
|
|
191
|
+
// Draw UI overlay using 2D API
|
|
192
|
+
drawUI();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function drawStartScreen() {
|
|
196
|
+
// Dark background
|
|
197
|
+
rect(0, 0, 640, 360, rgba8(10, 5, 30, 255), true);
|
|
198
|
+
|
|
199
|
+
// Title with glow effect
|
|
200
|
+
print('SHADOW NINJA 3D', 180, 70, rgba8(180, 80, 255, 255));
|
|
201
|
+
|
|
202
|
+
// Subtitle
|
|
203
|
+
const pulse = Math.sin(startScreenTime * 3) * 0.25 + 0.75;
|
|
204
|
+
print('Strider-Style 3.5D Ninja Platformer', 160, 100, rgba8(200, 150, 255, Math.floor(pulse * 255)));
|
|
205
|
+
|
|
206
|
+
// Controls panel
|
|
207
|
+
rect(100, 140, 440, 160, rgba8(20, 10, 40, 220), true);
|
|
208
|
+
rect(100, 140, 440, 160, rgba8(150, 80, 255, 255), false);
|
|
209
|
+
|
|
210
|
+
// Controls title
|
|
211
|
+
print('NINJA ABILITIES:', 220, 155, rgba8(255, 200, 255, 255));
|
|
212
|
+
|
|
213
|
+
// Controls - two columns
|
|
214
|
+
print('ARROWS = Move', 120, 180, rgba8(200, 200, 255, 255));
|
|
215
|
+
print('DOWN = Slide', 120, 200, rgba8(200, 200, 255, 255));
|
|
216
|
+
print('UP = Jump/Double Jump', 120, 220, rgba8(200, 200, 255, 255));
|
|
217
|
+
print('Z = Attack', 120, 240, rgba8(200, 200, 255, 255));
|
|
218
|
+
print('X = Dash/Air Dash', 120, 260, rgba8(200, 200, 255, 255));
|
|
219
|
+
|
|
220
|
+
print('C = Throw Shuriken', 340, 180, rgba8(200, 200, 255, 255));
|
|
221
|
+
print('G = Grappling Hook', 340, 200, rgba8(200, 200, 255, 255));
|
|
222
|
+
print('Wall Running Active', 340, 220, rgba8(150, 255, 150, 255));
|
|
223
|
+
print('Combo System', 340, 240, rgba8(255, 255, 150, 255));
|
|
224
|
+
print('Energy Management', 340, 260, rgba8(150, 200, 255, 255));
|
|
225
|
+
|
|
226
|
+
// Pulsing prompt - larger and centered
|
|
227
|
+
const alpha = Math.floor((Math.sin(startScreenTime * 5) * 0.5 + 0.5) * 255);
|
|
228
|
+
print('CLICK SCREEN OR PRESS ANY KEY', 180, 320, rgba8(255, 180, 255, alpha));
|
|
229
|
+
|
|
230
|
+
// Debug helper - show if keys are being detected
|
|
231
|
+
if (startScreenTime > 1) {
|
|
232
|
+
print('(Click on game screen to focus)', 190, 335, rgba8(150, 150, 150, 200));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function createNinjaPlayer() {
|
|
237
|
+
// Create ninja body - sleek and dark
|
|
238
|
+
const body = createCube(0.6, 0x2a2a3a, [0, 0, 0]);
|
|
239
|
+
setScale(body, 0.7, 1.1, 0.5);
|
|
240
|
+
|
|
241
|
+
// Create ninja mask/head - mysterious
|
|
242
|
+
const head = createCube(0.5, 0x1a1a2a, [0, 0.75, 0]);
|
|
243
|
+
setScale(head, 0.6, 0.5, 0.5);
|
|
244
|
+
|
|
245
|
+
// Create eyes - glowing
|
|
246
|
+
const eyes = createCube(0.1, 0x00ffff, [0, 0.8, 0.25]);
|
|
247
|
+
setScale(eyes, 0.4, 0.1, 0.1);
|
|
248
|
+
|
|
249
|
+
// Create katana - sleek blade
|
|
250
|
+
const katana = createCube(0.1, 0xccccff, [0.7, 0.5, 0]);
|
|
251
|
+
setScale(katana, 0.1, 1.3, 0.1);
|
|
252
|
+
|
|
253
|
+
// Create scarf - flowing
|
|
254
|
+
const scarf = createCube(0.3, 0x8833aa, [0, 0.3, -0.5]);
|
|
255
|
+
setScale(scarf, 0.8, 0.6, 0.3);
|
|
256
|
+
|
|
257
|
+
return { body, head, eyes, katana, scarf };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function createNinjaWorld() {
|
|
261
|
+
// Create dark ground - rooftop aesthetic
|
|
262
|
+
const ground = createPlane(150, 150, 0x1a1a2a, [0, -1, 0]);
|
|
263
|
+
setRotation(ground, -Math.PI/2, 0, 0);
|
|
264
|
+
|
|
265
|
+
// Create cyberpunk city buildings in background
|
|
266
|
+
for (let i = 0; i < 12; i++) {
|
|
267
|
+
const building = createCube(6 + Math.random() * 4, 15 + Math.random() * 20, 0x2a2a4a, [
|
|
268
|
+
(i - 6) * 18 + (Math.random() - 0.5) * 8,
|
|
269
|
+
8,
|
|
270
|
+
-50 - Math.random() * 30
|
|
271
|
+
]);
|
|
272
|
+
setScale(building, 1, 1, 1);
|
|
273
|
+
|
|
274
|
+
// Add neon accents on buildings
|
|
275
|
+
if (Math.random() > 0.5) {
|
|
276
|
+
const neon = createCube(0.3, 1, 0xff00ff, [
|
|
277
|
+
(i - 6) * 18 + (Math.random() - 0.5) * 8,
|
|
278
|
+
12 + Math.random() * 10,
|
|
279
|
+
-50 - Math.random() * 30
|
|
280
|
+
]);
|
|
281
|
+
setScale(neon, 4, 0.2, 0.1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create vertical wall structures - for wall running
|
|
286
|
+
for (let i = 0; i < 8; i++) {
|
|
287
|
+
const wall = createCube(0.5, 8, 0x3a3a5a, [
|
|
288
|
+
(i - 4) * 15,
|
|
289
|
+
4,
|
|
290
|
+
-20 + (Math.random() - 0.5) * 10
|
|
291
|
+
]);
|
|
292
|
+
setScale(wall, 4, 1, 0.5);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Create neon light posts
|
|
296
|
+
for (let i = 0; i < 10; i++) {
|
|
297
|
+
const post = createCube(0.2, 4, 0x4a4a6a, [
|
|
298
|
+
(Math.random() - 0.5) * 100,
|
|
299
|
+
2,
|
|
300
|
+
(Math.random() - 0.5) * 80
|
|
301
|
+
]);
|
|
302
|
+
setScale(post, 1, 1, 1);
|
|
303
|
+
|
|
304
|
+
// Neon light on top
|
|
305
|
+
const light = createSphere(0.3, 0x00ffff, [
|
|
306
|
+
(Math.random() - 0.5) * 100,
|
|
307
|
+
4.5,
|
|
308
|
+
(Math.random() - 0.5) * 80
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function createPlatforms() {
|
|
314
|
+
platforms = [];
|
|
315
|
+
|
|
316
|
+
// Ground level platforms - rooftop tiles
|
|
317
|
+
for (let i = -6; i <= 6; i++) {
|
|
318
|
+
const platform = {
|
|
319
|
+
mesh: createCube(4, 0.4, 0x3a3a4a, [i * 7, -0.2, 0]),
|
|
320
|
+
x: i * 7, y: -0.2, z: 0,
|
|
321
|
+
width: 4, height: 0.4, depth: 5,
|
|
322
|
+
type: 'ground'
|
|
323
|
+
};
|
|
324
|
+
setScale(platform.mesh, 1, 1, 5);
|
|
325
|
+
platforms.push(platform);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Floating platforms - ninja parkour course with varied heights
|
|
329
|
+
const platformData = [
|
|
330
|
+
{ x: 12, y: 3, z: 8, color: 0x4a4a6a },
|
|
331
|
+
{ x: 22, y: 6, z: 12, color: 0x5a3a7a },
|
|
332
|
+
{ x: 32, y: 9, z: 10, color: 0x4a4a6a },
|
|
333
|
+
{ x: 42, y: 12, z: 5, color: 0x5a3a7a },
|
|
334
|
+
{ x: 28, y: 15, z: -5, color: 0x4a4a6a },
|
|
335
|
+
{ x: 15, y: 18, z: -12, color: 0x5a3a7a },
|
|
336
|
+
{ x: 0, y: 20, z: -15, color: 0x4a4a6a },
|
|
337
|
+
{ x: -15, y: 17, z: -18, color: 0x5a3a7a },
|
|
338
|
+
{ x: -28, y: 13, z: -12, color: 0x4a4a6a },
|
|
339
|
+
{ x: -35, y: 9, z: -5, color: 0x5a3a7a },
|
|
340
|
+
{ x: -42, y: 6, z: 5, color: 0x4a4a6a },
|
|
341
|
+
{ x: -30, y: 4, z: 10, color: 0x5a3a7a }
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
platformData.forEach((data, i) => {
|
|
345
|
+
const platform = {
|
|
346
|
+
mesh: createCube(3.5, 0.6, data.color, [data.x, data.y, data.z]),
|
|
347
|
+
x: data.x, y: data.y, z: data.z,
|
|
348
|
+
width: 3.5, height: 0.6, depth: 3.5,
|
|
349
|
+
type: 'floating',
|
|
350
|
+
id: i
|
|
351
|
+
};
|
|
352
|
+
setScale(platform.mesh, 1, 1, 1);
|
|
353
|
+
platforms.push(platform);
|
|
354
|
+
|
|
355
|
+
// Add neon edge lights to floating platforms
|
|
356
|
+
const edgeLight = createCube(3.6, 0.1, 0xff00ff, [data.x, data.y - 0.3, data.z]);
|
|
357
|
+
setScale(edgeLight, 1, 1, 1);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function createGrapplePoints() {
|
|
362
|
+
player.grapplePoints = [];
|
|
363
|
+
|
|
364
|
+
// Add grapple points in strategic locations
|
|
365
|
+
const grappleData = [
|
|
366
|
+
{ x: 18, y: 12, z: 5 },
|
|
367
|
+
{ x: 35, y: 15, z: 8 },
|
|
368
|
+
{ x: 20, y: 22, z: -8 },
|
|
369
|
+
{ x: -5, y: 24, z: -18 },
|
|
370
|
+
{ x: -30, y: 18, z: -10 },
|
|
371
|
+
{ x: -40, y: 12, z: 2 }
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
grappleData.forEach(data => {
|
|
375
|
+
const point = {
|
|
376
|
+
mesh: createSphere(0.4, 0x00ffff, [data.x, data.y, data.z]),
|
|
377
|
+
x: data.x, y: data.y, z: data.z,
|
|
378
|
+
active: true
|
|
379
|
+
};
|
|
380
|
+
player.grapplePoints.push(point);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function spawnEnemies() {
|
|
385
|
+
enemies = [];
|
|
386
|
+
|
|
387
|
+
// Ground patrol enemies
|
|
388
|
+
for (let i = 0; i < 5; i++) {
|
|
389
|
+
const enemy = {
|
|
390
|
+
mesh: createCube(0.6, 0xff4444, [(i - 2) * 15, 0.5, 8]),
|
|
391
|
+
x: (i - 2) * 15, y: 0.5, z: 8,
|
|
392
|
+
vx: (Math.random() > 0.5 ? 1 : -1) * 2,
|
|
393
|
+
health: 3,
|
|
394
|
+
type: 'patrol',
|
|
395
|
+
patrolRange: 10,
|
|
396
|
+
originalX: (i - 2) * 15,
|
|
397
|
+
attackCooldown: 0
|
|
398
|
+
};
|
|
399
|
+
setScale(enemy.mesh, 0.8, 0.8, 0.8);
|
|
400
|
+
enemies.push(enemy);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Flying enemies
|
|
404
|
+
for (let i = 0; i < 3; i++) {
|
|
405
|
+
const enemy = {
|
|
406
|
+
mesh: createSphere(0.4, 0xff8844, [i * 20 - 20, 8 + i * 2, 0]),
|
|
407
|
+
x: i * 20 - 20, y: 8 + i * 2, z: 0,
|
|
408
|
+
vx: Math.sin(i) * 3, vy: 0, vz: Math.cos(i) * 2,
|
|
409
|
+
health: 2,
|
|
410
|
+
type: 'flyer',
|
|
411
|
+
orbitCenter: { x: i * 20 - 20, y: 8 + i * 2, z: 0 },
|
|
412
|
+
orbitRadius: 5,
|
|
413
|
+
orbitAngle: i * 2,
|
|
414
|
+
attackCooldown: 0
|
|
415
|
+
};
|
|
416
|
+
enemies.push(enemy);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function spawnCoins() {
|
|
421
|
+
coins = [];
|
|
422
|
+
|
|
423
|
+
// Place coins on platforms
|
|
424
|
+
platforms.forEach((platform, i) => {
|
|
425
|
+
if (i > 8) { // Skip ground platforms
|
|
426
|
+
const coin = {
|
|
427
|
+
mesh: createSphere(0.3, 0xffdd00, [platform.x, platform.y + 1.5, platform.z]),
|
|
428
|
+
x: platform.x, y: platform.y + 1.5, z: platform.z,
|
|
429
|
+
collected: false,
|
|
430
|
+
rotationY: 0,
|
|
431
|
+
bobOffset: Math.random() * Math.PI * 2
|
|
432
|
+
};
|
|
433
|
+
coins.push(coin);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Bonus coins in hard to reach places
|
|
438
|
+
for (let i = 0; i < 8; i++) {
|
|
439
|
+
const coin = {
|
|
440
|
+
mesh: createSphere(0.25, 0xffaa00, [
|
|
441
|
+
(Math.random() - 0.5) * 60,
|
|
442
|
+
5 + Math.random() * 10,
|
|
443
|
+
(Math.random() - 0.5) * 30
|
|
444
|
+
]),
|
|
445
|
+
x: 0, y: 0, z: 0,
|
|
446
|
+
collected: false,
|
|
447
|
+
rotationY: 0,
|
|
448
|
+
bobOffset: Math.random() * Math.PI * 2
|
|
449
|
+
};
|
|
450
|
+
const pos = getPosition(coin.mesh);
|
|
451
|
+
coin.x = pos[0]; coin.y = pos[1]; coin.z = pos[2];
|
|
452
|
+
coins.push(coin);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function updateInput(dt) {
|
|
457
|
+
const moveSpeed = player.dashDuration > 0 ? player.speed * 2.5 : player.speed;
|
|
458
|
+
|
|
459
|
+
// Update cooldowns
|
|
460
|
+
player.attackCooldown -= dt;
|
|
461
|
+
player.dashCooldown -= dt;
|
|
462
|
+
player.dashDuration -= dt;
|
|
463
|
+
player.slideDuration -= dt;
|
|
464
|
+
|
|
465
|
+
// Sliding
|
|
466
|
+
if (player.slideDuration > 0) {
|
|
467
|
+
player.vx = (player.facingRight ? 1 : -1) * moveSpeed * 1.5;
|
|
468
|
+
player.crouching = true;
|
|
469
|
+
return; // Override other controls during slide
|
|
470
|
+
} else {
|
|
471
|
+
player.crouching = false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Grappling hook movement
|
|
475
|
+
if (player.grappling && player.grappleTarget) {
|
|
476
|
+
const dx = player.grappleTarget.x - player.x;
|
|
477
|
+
const dy = player.grappleTarget.y - player.y;
|
|
478
|
+
const dz = player.grappleTarget.z - player.z;
|
|
479
|
+
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
|
|
480
|
+
|
|
481
|
+
if (dist > 1.5) {
|
|
482
|
+
const pullSpeed = 20;
|
|
483
|
+
player.vx = (dx / dist) * pullSpeed;
|
|
484
|
+
player.vy = (dy / dist) * pullSpeed;
|
|
485
|
+
player.vz = (dz / dist) * pullSpeed;
|
|
486
|
+
} else {
|
|
487
|
+
player.grappling = false;
|
|
488
|
+
player.grappleTarget = null;
|
|
489
|
+
}
|
|
490
|
+
return; // Override other controls during grapple
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Horizontal movement
|
|
494
|
+
if (btn(0)) { // Left
|
|
495
|
+
player.vx = -moveSpeed;
|
|
496
|
+
player.facingRight = false;
|
|
497
|
+
} else if (btn(1)) { // Right
|
|
498
|
+
player.vx = moveSpeed;
|
|
499
|
+
player.facingRight = true;
|
|
500
|
+
} else {
|
|
501
|
+
player.vx *= 0.85; // Friction
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Forward/backward movement
|
|
505
|
+
if (btn(3) && player.onGround && !player.crouching) { // Down + on ground = slide
|
|
506
|
+
player.slideDuration = 0.5;
|
|
507
|
+
player.energy -= 5;
|
|
508
|
+
createSlideParticles();
|
|
509
|
+
} else if (btn(3)) { // Down in air
|
|
510
|
+
player.vz = moveSpeed * 0.6;
|
|
511
|
+
} else {
|
|
512
|
+
player.vz *= 0.85;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Jump and double jump
|
|
516
|
+
if (btnp(2) && player.onGround) { // Up - jump when on ground
|
|
517
|
+
player.vy = player.jumpPower;
|
|
518
|
+
player.onGround = false;
|
|
519
|
+
player.doubleJump = true;
|
|
520
|
+
player.airDashAvailable = true;
|
|
521
|
+
createJumpParticles();
|
|
522
|
+
} else if (btnp(2) && player.doubleJump && player.energy >= 15) { // Double jump
|
|
523
|
+
player.vy = player.jumpPower * 0.9;
|
|
524
|
+
player.doubleJump = false;
|
|
525
|
+
player.energy -= 15;
|
|
526
|
+
createDoubleJumpParticles();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Dash (X key) - ground dash or air dash
|
|
530
|
+
if (btnp(5) && player.dashCooldown <= 0 && player.energy >= 20) {
|
|
531
|
+
if (player.onGround) {
|
|
532
|
+
// Ground dash
|
|
533
|
+
player.dashDuration = 0.3;
|
|
534
|
+
player.dashCooldown = 0.8;
|
|
535
|
+
player.energy -= 20;
|
|
536
|
+
createDashParticles();
|
|
537
|
+
} else if (player.airDashAvailable) {
|
|
538
|
+
// Air dash
|
|
539
|
+
player.vx = (player.facingRight ? 1 : -1) * player.speed * 3;
|
|
540
|
+
player.vy = 0;
|
|
541
|
+
player.airDashAvailable = false;
|
|
542
|
+
player.dashCooldown = 1.0;
|
|
543
|
+
player.energy -= 25;
|
|
544
|
+
createAirDashParticles();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Shuriken throw (C key)
|
|
549
|
+
if (btnp(6) && player.shuriken > 0 && player.energy >= 5) {
|
|
550
|
+
throwShuriken();
|
|
551
|
+
player.shuriken--;
|
|
552
|
+
player.energy -= 5;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Grapple hook (G key or Space)
|
|
556
|
+
if (btnp(7) || btnp(4)) {
|
|
557
|
+
const nearestGrapple = findNearestGrapplePoint();
|
|
558
|
+
if (nearestGrapple && !player.grappling) {
|
|
559
|
+
player.grappling = true;
|
|
560
|
+
player.grappleTarget = nearestGrapple;
|
|
561
|
+
createGrappleEffect();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Melee attack (Z key)
|
|
566
|
+
if (btn(4) && player.attackCooldown <= 0) {
|
|
567
|
+
performAttack();
|
|
568
|
+
player.attackCooldown = 0.4;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function updatePlayer(dt) {
|
|
573
|
+
// Update combo timer
|
|
574
|
+
if (comboTimer > 0) {
|
|
575
|
+
comboTimer -= dt;
|
|
576
|
+
if (comboTimer <= 0) {
|
|
577
|
+
combo = 0;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Check for wall running
|
|
582
|
+
player.wallRunning = false;
|
|
583
|
+
if (!player.onGround && Math.abs(player.vx) > 5) {
|
|
584
|
+
// Check if near any wall structure
|
|
585
|
+
for (let i = 0; i < 8; i++) {
|
|
586
|
+
const wallX = (i - 4) * 15;
|
|
587
|
+
const wallZ = i % 2 === 0 ? -20 : -30;
|
|
588
|
+
|
|
589
|
+
const distToWall = Math.sqrt((player.x - wallX) ** 2 + (player.z - wallZ) ** 2);
|
|
590
|
+
if (distToWall < 3) {
|
|
591
|
+
player.wallRunning = true;
|
|
592
|
+
player.wallRunTime += dt;
|
|
593
|
+
|
|
594
|
+
// Wall running slows vertical fall and provides slight upward boost
|
|
595
|
+
player.vy = Math.max(player.vy, -5);
|
|
596
|
+
if (player.wallRunTime < player.maxWallRunTime) {
|
|
597
|
+
player.vy += 3 * dt; // Slight upward boost
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Reset wall run time if not wall running
|
|
605
|
+
if (!player.wallRunning) {
|
|
606
|
+
player.wallRunTime = 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Apply gravity (reduced during wall run)
|
|
610
|
+
const gravityMultiplier = player.wallRunning ? 0.3 : 1.0;
|
|
611
|
+
player.vy -= 25 * gravityMultiplier * dt;
|
|
612
|
+
|
|
613
|
+
// Update position
|
|
614
|
+
player.x += player.vx * dt;
|
|
615
|
+
player.y += player.vy * dt;
|
|
616
|
+
player.z += player.vz * dt;
|
|
617
|
+
|
|
618
|
+
// Platform collision detection
|
|
619
|
+
player.onGround = false;
|
|
620
|
+
|
|
621
|
+
for (const platform of platforms) {
|
|
622
|
+
// Simple AABB collision - adjust for crouching
|
|
623
|
+
const hitboxHeight = player.crouching ? 0.8 : 1.0;
|
|
624
|
+
|
|
625
|
+
if (player.x > platform.x - platform.width/2 &&
|
|
626
|
+
player.x < platform.x + platform.width/2 &&
|
|
627
|
+
player.z > platform.z - platform.depth/2 &&
|
|
628
|
+
player.z < platform.z + platform.depth/2) {
|
|
629
|
+
|
|
630
|
+
if (player.y <= platform.y + platform.height &&
|
|
631
|
+
player.y + player.vy * dt > platform.y + platform.height) {
|
|
632
|
+
player.y = platform.y + platform.height;
|
|
633
|
+
player.vy = 0;
|
|
634
|
+
player.onGround = true;
|
|
635
|
+
player.doubleJump = true;
|
|
636
|
+
player.airDashAvailable = true; // Reset air dash on landing
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// World boundaries
|
|
642
|
+
if (player.x < -50) player.x = -50;
|
|
643
|
+
if (player.x > 50) player.x = 50;
|
|
644
|
+
if (player.z < -30) player.z = -30;
|
|
645
|
+
if (player.z > 30) player.z = 30;
|
|
646
|
+
|
|
647
|
+
// Fall reset
|
|
648
|
+
if (player.y < -10) {
|
|
649
|
+
player.x = 0;
|
|
650
|
+
player.y = 2;
|
|
651
|
+
player.z = 0;
|
|
652
|
+
player.vx = 0;
|
|
653
|
+
player.vy = 0;
|
|
654
|
+
player.vz = 0;
|
|
655
|
+
player.health -= 20;
|
|
656
|
+
combo = 0;
|
|
657
|
+
comboTimer = 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Update ninja meshes
|
|
661
|
+
updatePlayerMeshes();
|
|
662
|
+
|
|
663
|
+
// Regenerate energy
|
|
664
|
+
if (player.energy < 100) {
|
|
665
|
+
player.energy += 25 * dt;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function updatePlayerMeshes() {
|
|
670
|
+
// Safety check - make sure player meshes exist
|
|
671
|
+
if (!playerKnight || !playerKnight.body || !playerKnight.head ||
|
|
672
|
+
!playerKnight.eyes || !playerKnight.katana || !playerKnight.scarf) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const bodyY = player.crouching ? player.y - 0.2 : player.y;
|
|
677
|
+
|
|
678
|
+
setPosition(playerKnight.body, player.x, bodyY, player.z);
|
|
679
|
+
setPosition(playerKnight.head, player.x, bodyY + 0.75, player.z);
|
|
680
|
+
setPosition(playerKnight.eyes, player.x, bodyY + 0.8, player.z + (player.facingRight ? 0.25 : -0.25));
|
|
681
|
+
|
|
682
|
+
// Katana position based on facing direction
|
|
683
|
+
const katanaX = player.facingRight ? player.x + 0.7 : player.x - 0.7;
|
|
684
|
+
const katanaRotY = player.facingRight ? -0.3 : Math.PI + 0.3;
|
|
685
|
+
setPosition(playerKnight.katana, katanaX, bodyY + 0.5, player.z);
|
|
686
|
+
setRotation(playerKnight.katana, 0, katanaRotY, 0);
|
|
687
|
+
|
|
688
|
+
// Animate based on movement
|
|
689
|
+
const speed = Math.sqrt(player.vx * player.vx + player.vz * player.vz);
|
|
690
|
+
const walkCycle = Math.sin(gameTime * (speed + 1) * 2) * 0.08;
|
|
691
|
+
const facing = player.facingRight ? 0 : Math.PI;
|
|
692
|
+
setRotation(playerKnight.body, walkCycle, facing, 0);
|
|
693
|
+
|
|
694
|
+
// Scarf physics - flowing behind ninja
|
|
695
|
+
const scarfSwing = Math.sin(gameTime * 5) * 0.25;
|
|
696
|
+
const scarfZ = player.z + (player.facingRight ? -0.5 : 0.5);
|
|
697
|
+
setPosition(playerKnight.scarf, player.x, bodyY + 0.3, scarfZ);
|
|
698
|
+
setRotation(playerKnight.scarf, scarfSwing, facing, 0);
|
|
699
|
+
|
|
700
|
+
// Dash effect - tilt body
|
|
701
|
+
if (player.dashDuration > 0) {
|
|
702
|
+
const tilt = player.facingRight ? -0.4 : 0.4;
|
|
703
|
+
setRotation(playerKnight.body, tilt, facing, 0);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function performAttack() {
|
|
708
|
+
// Create katana slash effect - purple arc
|
|
709
|
+
const slashAngle = player.facingRight ? 0 : Math.PI;
|
|
710
|
+
const slashX = player.x + (player.facingRight ? 1.5 : -1.5);
|
|
711
|
+
|
|
712
|
+
// Multiple slash particles for arc effect
|
|
713
|
+
for (let i = 0; i < 8; i++) {
|
|
714
|
+
const angle = slashAngle + (i - 4) * 0.3;
|
|
715
|
+
const offset = 1.5;
|
|
716
|
+
particles.push({
|
|
717
|
+
mesh: createSphere(0.15, 0xaa44ff, [
|
|
718
|
+
slashX + Math.cos(angle) * offset,
|
|
719
|
+
player.y + 0.5 + Math.sin(i * 0.5) * 0.5,
|
|
720
|
+
player.z
|
|
721
|
+
]),
|
|
722
|
+
vx: Math.cos(angle) * 3,
|
|
723
|
+
vy: Math.sin(i * 0.5) * 2,
|
|
724
|
+
vz: 0,
|
|
725
|
+
life: 0.3,
|
|
726
|
+
maxLife: 0.3
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Check for enemy hits
|
|
731
|
+
enemies.forEach(enemy => {
|
|
732
|
+
const dx = enemy.x - player.x;
|
|
733
|
+
const dy = enemy.y - player.y;
|
|
734
|
+
const dz = enemy.z - player.z;
|
|
735
|
+
const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
|
|
736
|
+
|
|
737
|
+
if (distance < 3) {
|
|
738
|
+
// Combo system - more damage with higher combo
|
|
739
|
+
const damage = 2 + Math.floor(combo / 5);
|
|
740
|
+
enemy.health -= damage;
|
|
741
|
+
|
|
742
|
+
// Award combo
|
|
743
|
+
combo++;
|
|
744
|
+
comboTimer = 2.0;
|
|
745
|
+
|
|
746
|
+
createHitParticles(enemy.x, enemy.y, enemy.z);
|
|
747
|
+
|
|
748
|
+
if (enemy.health <= 0) {
|
|
749
|
+
destroyMesh(enemy.mesh);
|
|
750
|
+
enemy.dead = true;
|
|
751
|
+
score += 100 * (1 + Math.floor(combo / 10)); // Bonus score for combos
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function updateEnemies(dt) {
|
|
758
|
+
enemies.forEach(enemy => {
|
|
759
|
+
if (enemy.dead) return;
|
|
760
|
+
|
|
761
|
+
enemy.attackCooldown -= dt;
|
|
762
|
+
|
|
763
|
+
switch (enemy.type) {
|
|
764
|
+
case 'patrol':
|
|
765
|
+
// Patrol back and forth
|
|
766
|
+
enemy.x += enemy.vx * dt;
|
|
767
|
+
|
|
768
|
+
if (Math.abs(enemy.x - enemy.originalX) > enemy.patrolRange) {
|
|
769
|
+
enemy.vx *= -1;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
|
|
773
|
+
|
|
774
|
+
// Attack player if close
|
|
775
|
+
const distToPlayer = Math.sqrt(
|
|
776
|
+
Math.pow(enemy.x - player.x, 2) +
|
|
777
|
+
Math.pow(enemy.z - player.z, 2)
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
if (distToPlayer < 4 && enemy.attackCooldown <= 0) {
|
|
781
|
+
// Simple attack - damage player
|
|
782
|
+
if (distToPlayer < 2) {
|
|
783
|
+
player.health -= 10;
|
|
784
|
+
enemy.attackCooldown = 2;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
|
|
789
|
+
case 'flyer':
|
|
790
|
+
// Orbit around center point
|
|
791
|
+
enemy.orbitAngle += dt * 2;
|
|
792
|
+
enemy.x = enemy.orbitCenter.x + Math.cos(enemy.orbitAngle) * enemy.orbitRadius;
|
|
793
|
+
enemy.z = enemy.orbitCenter.z + Math.sin(enemy.orbitAngle) * enemy.orbitRadius;
|
|
794
|
+
enemy.y = enemy.orbitCenter.y + Math.sin(enemy.orbitAngle * 2) * 2;
|
|
795
|
+
|
|
796
|
+
setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
|
|
797
|
+
rotateMesh(enemy.mesh, 0, dt * 3, 0);
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Remove dead enemies
|
|
803
|
+
enemies = enemies.filter(enemy => !enemy.dead);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function updateCoins(dt) {
|
|
807
|
+
coins.forEach(coin => {
|
|
808
|
+
if (coin.collected) return;
|
|
809
|
+
|
|
810
|
+
// Animate coins
|
|
811
|
+
coin.rotationY += dt * 4;
|
|
812
|
+
coin.bobOffset += dt * 3;
|
|
813
|
+
const newY = coin.y + Math.sin(coin.bobOffset) * 0.3;
|
|
814
|
+
|
|
815
|
+
setPosition(coin.mesh, coin.x, newY, coin.z);
|
|
816
|
+
setRotation(coin.mesh, 0, coin.rotationY, 0);
|
|
817
|
+
|
|
818
|
+
// Check collection
|
|
819
|
+
const distance = Math.sqrt(
|
|
820
|
+
Math.pow(coin.x - player.x, 2) +
|
|
821
|
+
Math.pow(newY - player.y, 2) +
|
|
822
|
+
Math.pow(coin.z - player.z, 2)
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (distance < 1.5) {
|
|
826
|
+
coin.collected = true;
|
|
827
|
+
destroyMesh(coin.mesh);
|
|
828
|
+
player.coins += 10;
|
|
829
|
+
score += 50;
|
|
830
|
+
createCoinParticles(coin.x, coin.y, coin.z);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function updateParticles(dt) {
|
|
836
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
837
|
+
const particle = particles[i];
|
|
838
|
+
particle.life -= dt;
|
|
839
|
+
|
|
840
|
+
const pos = getPosition(particle.mesh);
|
|
841
|
+
pos[0] += particle.vx * dt;
|
|
842
|
+
pos[1] += particle.vy * dt;
|
|
843
|
+
pos[2] += particle.vz * dt;
|
|
844
|
+
|
|
845
|
+
// Shuriken don't have gravity and spin
|
|
846
|
+
if (particle.isShuriken) {
|
|
847
|
+
particle.rotation += dt * 20;
|
|
848
|
+
setRotation(particle.mesh, 0, 0, particle.rotation);
|
|
849
|
+
} else {
|
|
850
|
+
particle.vy -= 10 * dt; // Gravity for normal particles
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
setPosition(particle.mesh, pos[0], pos[1], pos[2]);
|
|
854
|
+
|
|
855
|
+
if (!particle.isShuriken) {
|
|
856
|
+
const scale = particle.life / particle.maxLife;
|
|
857
|
+
setScale(particle.mesh, scale, scale, scale);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (particle.life <= 0) {
|
|
861
|
+
destroyMesh(particle.mesh);
|
|
862
|
+
particles.splice(i, 1);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function updateCamera(dt) {
|
|
868
|
+
// Smooth camera following
|
|
869
|
+
camera.targetX = player.x;
|
|
870
|
+
camera.targetY = player.y + 5;
|
|
871
|
+
camera.targetZ = player.z;
|
|
872
|
+
|
|
873
|
+
camera.x += (player.x - camera.x) * camera.smoothing;
|
|
874
|
+
camera.y += (player.y + 8 - camera.y) * camera.smoothing;
|
|
875
|
+
camera.z += (player.z + 15 - camera.z) * camera.smoothing;
|
|
876
|
+
|
|
877
|
+
setCameraPosition(camera.x, camera.y, camera.z);
|
|
878
|
+
setCameraTarget(camera.targetX, camera.targetY, camera.targetZ);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function checkCollisions(dt) {
|
|
882
|
+
// Already handled in updatePlayer and updateEnemies
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function updateGameLogic(dt) {
|
|
886
|
+
// Check for level completion
|
|
887
|
+
const remainingCoins = coins.filter(coin => !coin.collected).length;
|
|
888
|
+
if (remainingCoins === 0) {
|
|
889
|
+
level++;
|
|
890
|
+
score += 1000;
|
|
891
|
+
// Could spawn new level here
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Game over check
|
|
895
|
+
if (player.health <= 0) {
|
|
896
|
+
gameState = 'gameOver';
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function createJumpParticles() {
|
|
901
|
+
for (let i = 0; i < 5; i++) {
|
|
902
|
+
const particle = {
|
|
903
|
+
mesh: createSphere(0.1, 0x88ccff, [player.x, player.y - 0.5, player.z]),
|
|
904
|
+
vx: (Math.random() - 0.5) * 4,
|
|
905
|
+
vy: Math.random() * 3,
|
|
906
|
+
vz: (Math.random() - 0.5) * 4,
|
|
907
|
+
life: 1,
|
|
908
|
+
maxLife: 1
|
|
909
|
+
};
|
|
910
|
+
particles.push(particle);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function createDoubleJumpParticles() {
|
|
915
|
+
for (let i = 0; i < 8; i++) {
|
|
916
|
+
const particle = {
|
|
917
|
+
mesh: createSphere(0.15, 0xffff44, [player.x, player.y, player.z]),
|
|
918
|
+
vx: (Math.random() - 0.5) * 6,
|
|
919
|
+
vy: (Math.random() - 0.5) * 6,
|
|
920
|
+
vz: (Math.random() - 0.5) * 6,
|
|
921
|
+
life: 1.5,
|
|
922
|
+
maxLife: 1.5
|
|
923
|
+
};
|
|
924
|
+
particles.push(particle);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function createHitParticles(x, y, z) {
|
|
929
|
+
for (let i = 0; i < 10; i++) {
|
|
930
|
+
const particle = {
|
|
931
|
+
mesh: createSphere(0.08, 0xff4444, [x, y, z]),
|
|
932
|
+
vx: (Math.random() - 0.5) * 8,
|
|
933
|
+
vy: Math.random() * 6,
|
|
934
|
+
vz: (Math.random() - 0.5) * 8,
|
|
935
|
+
life: 0.8,
|
|
936
|
+
maxLife: 0.8
|
|
937
|
+
};
|
|
938
|
+
particles.push(particle);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function createCoinParticles(x, y, z) {
|
|
943
|
+
for (let i = 0; i < 6; i++) {
|
|
944
|
+
const particle = {
|
|
945
|
+
mesh: createSphere(0.05, 0xffdd00, [x, y, z]),
|
|
946
|
+
vx: (Math.random() - 0.5) * 5,
|
|
947
|
+
vy: Math.random() * 4 + 2,
|
|
948
|
+
vz: (Math.random() - 0.5) * 5,
|
|
949
|
+
life: 1.2,
|
|
950
|
+
maxLife: 1.2
|
|
951
|
+
};
|
|
952
|
+
particles.push(particle);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function createDashParticles() {
|
|
957
|
+
for (let i = 0; i < 10; i++) {
|
|
958
|
+
const particle = {
|
|
959
|
+
mesh: createSphere(0.12, 0xaa44ff, [player.x, player.y + 0.5, player.z]),
|
|
960
|
+
vx: (Math.random() - 0.5) * 3 - (player.facingRight ? 5 : -5),
|
|
961
|
+
vy: (Math.random() - 0.5) * 2,
|
|
962
|
+
vz: (Math.random() - 0.5) * 3,
|
|
963
|
+
life: 0.5,
|
|
964
|
+
maxLife: 0.5
|
|
965
|
+
};
|
|
966
|
+
particles.push(particle);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function createAirDashParticles() {
|
|
971
|
+
for (let i = 0; i < 15; i++) {
|
|
972
|
+
const particle = {
|
|
973
|
+
mesh: createSphere(0.15, 0x00ffff, [player.x, player.y, player.z]),
|
|
974
|
+
vx: (Math.random() - 0.5) * 8,
|
|
975
|
+
vy: (Math.random() - 0.5) * 8,
|
|
976
|
+
vz: (Math.random() - 0.5) * 8,
|
|
977
|
+
life: 0.8,
|
|
978
|
+
maxLife: 0.8
|
|
979
|
+
};
|
|
980
|
+
particles.push(particle);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function createSlideParticles() {
|
|
985
|
+
for (let i = 0; i < 8; i++) {
|
|
986
|
+
const particle = {
|
|
987
|
+
mesh: createSphere(0.08, 0x8844aa, [player.x, player.y - 0.3, player.z]),
|
|
988
|
+
vx: (Math.random() - 0.5) * 4 - (player.facingRight ? 3 : -3),
|
|
989
|
+
vy: Math.random() * 2,
|
|
990
|
+
vz: (Math.random() - 0.5) * 4,
|
|
991
|
+
life: 0.6,
|
|
992
|
+
maxLife: 0.6
|
|
993
|
+
};
|
|
994
|
+
particles.push(particle);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function createGrappleEffect() {
|
|
999
|
+
for (let i = 0; i < 12; i++) {
|
|
1000
|
+
const t = i / 12;
|
|
1001
|
+
const x = player.x + (player.grappleTarget.x - player.x) * t;
|
|
1002
|
+
const y = player.y + (player.grappleTarget.y - player.y) * t;
|
|
1003
|
+
const z = player.z + (player.grappleTarget.z - player.z) * t;
|
|
1004
|
+
|
|
1005
|
+
const particle = {
|
|
1006
|
+
mesh: createSphere(0.1, 0x00ffff, [x, y, z]),
|
|
1007
|
+
vx: 0,
|
|
1008
|
+
vy: 0,
|
|
1009
|
+
vz: 0,
|
|
1010
|
+
life: 0.4,
|
|
1011
|
+
maxLife: 0.4
|
|
1012
|
+
};
|
|
1013
|
+
particles.push(particle);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function throwShuriken() {
|
|
1018
|
+
const shuriken = {
|
|
1019
|
+
mesh: createCube(0.3, 0xcccccc, [
|
|
1020
|
+
player.x + (player.facingRight ? 1 : -1),
|
|
1021
|
+
player.y + 0.5,
|
|
1022
|
+
player.z
|
|
1023
|
+
]),
|
|
1024
|
+
vx: (player.facingRight ? 1 : -1) * 25,
|
|
1025
|
+
vy: 0,
|
|
1026
|
+
vz: 0,
|
|
1027
|
+
life: 2,
|
|
1028
|
+
maxLife: 2,
|
|
1029
|
+
rotation: 0,
|
|
1030
|
+
isShuriken: true
|
|
1031
|
+
};
|
|
1032
|
+
setScale(shuriken.mesh, 0.8, 0.1, 0.8);
|
|
1033
|
+
particles.push(shuriken);
|
|
1034
|
+
|
|
1035
|
+
// Check for enemy hits
|
|
1036
|
+
setTimeout(() => {
|
|
1037
|
+
enemies.forEach(enemy => {
|
|
1038
|
+
const dx = enemy.x - (player.x + (player.facingRight ? 5 : -5));
|
|
1039
|
+
const dy = enemy.y - player.y;
|
|
1040
|
+
const dz = enemy.z - player.z;
|
|
1041
|
+
const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
|
|
1042
|
+
|
|
1043
|
+
if (distance < 3 && !enemy.dead) {
|
|
1044
|
+
enemy.health -= 1;
|
|
1045
|
+
createHitParticles(enemy.x, enemy.y, enemy.z);
|
|
1046
|
+
combo++;
|
|
1047
|
+
comboTimer = 2;
|
|
1048
|
+
|
|
1049
|
+
if (enemy.health <= 0) {
|
|
1050
|
+
destroyMesh(enemy.mesh);
|
|
1051
|
+
enemy.dead = true;
|
|
1052
|
+
score += 150;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}, 100);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function findNearestGrapplePoint() {
|
|
1060
|
+
let nearest = null;
|
|
1061
|
+
let minDist = 20; // Max grapple range
|
|
1062
|
+
|
|
1063
|
+
player.grapplePoints.forEach(point => {
|
|
1064
|
+
if (!point.active) return;
|
|
1065
|
+
|
|
1066
|
+
const dx = point.x - player.x;
|
|
1067
|
+
const dy = point.y - player.y;
|
|
1068
|
+
const dz = point.z - player.z;
|
|
1069
|
+
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
|
|
1070
|
+
|
|
1071
|
+
if (dist < minDist) {
|
|
1072
|
+
minDist = dist;
|
|
1073
|
+
nearest = point;
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
return nearest;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function drawUI() {
|
|
1081
|
+
// Ninja HUD Background - dark with purple/cyan accents
|
|
1082
|
+
rect(16, 16, 420, 100, rgba8(10, 10, 26, 200), true);
|
|
1083
|
+
rect(16, 16, 420, 100, rgba8(136, 51, 170, 180), false);
|
|
1084
|
+
rect(17, 17, 418, 98, rgba8(0, 255, 255, 80), false);
|
|
1085
|
+
|
|
1086
|
+
// Score and Level
|
|
1087
|
+
print(`SCORE: ${score.toString().padStart(8, '0')}`, 24, 24, rgba8(0, 255, 255, 255));
|
|
1088
|
+
print(`LEVEL: ${level}`, 24, 40, rgba8(170, 68, 255, 255));
|
|
1089
|
+
print(`COINS: ${player.coins}`, 24, 56, rgba8(255, 215, 0, 255));
|
|
1090
|
+
|
|
1091
|
+
// Shuriken count
|
|
1092
|
+
print(`SHURIKEN: ${player.shuriken}`, 24, 72, rgba8(200, 200, 200, 255));
|
|
1093
|
+
|
|
1094
|
+
// Combo meter
|
|
1095
|
+
if (combo > 0) {
|
|
1096
|
+
print(`COMBO x${combo}`, 24, 88, rgba8(255, 100, 255, 255));
|
|
1097
|
+
const comboBarWidth = Math.floor((comboTimer / 2.0) * 80);
|
|
1098
|
+
rect(100, 90, 80, 6, rgba8(50, 20, 60, 255), true);
|
|
1099
|
+
rect(100, 90, comboBarWidth, 6, rgba8(255, 100, 255, 255), true);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Health bar - red with dark background
|
|
1103
|
+
print('HEALTH:', 220, 24, rgba8(255, 255, 255, 255));
|
|
1104
|
+
rect(285, 22, 120, 10, rgba8(50, 0, 0, 255), true);
|
|
1105
|
+
rect(285, 22, Math.floor((player.health / 100) * 120), 10, rgba8(255, 0, 80, 255), true);
|
|
1106
|
+
rect(285, 22, 120, 10, rgba8(255, 0, 80, 100), false);
|
|
1107
|
+
|
|
1108
|
+
// Energy bar - cyan with dark background
|
|
1109
|
+
print('ENERGY:', 220, 42, rgba8(255, 255, 255, 255));
|
|
1110
|
+
rect(285, 40, 120, 10, rgba8(0, 20, 40, 255), true);
|
|
1111
|
+
rect(285, 40, Math.floor((player.energy / 100) * 120), 10, rgba8(0, 255, 255, 255), true);
|
|
1112
|
+
rect(285, 40, 120, 10, rgba8(0, 255, 255, 100), false);
|
|
1113
|
+
|
|
1114
|
+
// Ability indicators
|
|
1115
|
+
print('ABILITIES:', 220, 60, rgba8(200, 200, 200, 255));
|
|
1116
|
+
|
|
1117
|
+
// Dash indicator
|
|
1118
|
+
const dashReady = player.dashCooldown <= 0 && player.energy >= 20;
|
|
1119
|
+
rect(285, 58, 24, 8, dashReady ? rgba8(170, 68, 255, 255) : rgba8(50, 20, 60, 255), true);
|
|
1120
|
+
print('DSH', 288, 60, rgba8(255, 255, 255, 255));
|
|
1121
|
+
|
|
1122
|
+
// Air Dash indicator
|
|
1123
|
+
const airDashReady = player.airDashAvailable && player.energy >= 25;
|
|
1124
|
+
rect(312, 58, 24, 8, airDashReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
|
|
1125
|
+
print('AIR', 315, 60, rgba8(255, 255, 255, 255));
|
|
1126
|
+
|
|
1127
|
+
// Grapple indicator
|
|
1128
|
+
const grappleReady = findNearestGrapplePoint() !== null;
|
|
1129
|
+
rect(339, 58, 24, 8, grappleReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
|
|
1130
|
+
print('GRP', 342, 60, rgba8(255, 255, 255, 255));
|
|
1131
|
+
|
|
1132
|
+
// Shuriken indicator
|
|
1133
|
+
const shurikenReady = player.shuriken > 0 && player.energy >= 5;
|
|
1134
|
+
rect(366, 58, 24, 8, shurikenReady ? rgba8(200, 200, 200, 255) : rgba8(40, 40, 40, 255), true);
|
|
1135
|
+
print('SHR', 369, 60, rgba8(255, 255, 255, 255));
|
|
1136
|
+
|
|
1137
|
+
// 3D Stats - smaller and in corner
|
|
1138
|
+
const stats = get3DStats();
|
|
1139
|
+
if (stats) {
|
|
1140
|
+
print(`${stats.meshes || 0}m`, 580, 24, rgba8(100, 100, 100, 200));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Position info - debug mode
|
|
1144
|
+
// print(`POS: ${player.x.toFixed(1)}, ${player.y.toFixed(1)}, ${player.z.toFixed(1)}`, 220, 90, rgba8(100, 100, 100, 150));
|
|
1145
|
+
|
|
1146
|
+
// Controls hint
|
|
1147
|
+
print('X=DASH C=SHURIKEN G=GRAPPLE Z=ATTACK', 16, 340, rgba8(136, 51, 170, 200));
|
|
1148
|
+
|
|
1149
|
+
if (gameState === 'gameOver') {
|
|
1150
|
+
rect(0, 0, 640, 360, rgba8(0, 0, 0, 200), true);
|
|
1151
|
+
print('GAME OVER', 260, 150, rgba8(255, 50, 50, 255));
|
|
1152
|
+
print(`FINAL SCORE: ${score}`, 230, 180, rgba8(255, 255, 0, 255));
|
|
1153
|
+
print(`COINS COLLECTED: ${player.coins}`, 220, 200, rgba8(255, 215, 0, 255));
|
|
1154
|
+
print('PRESS R TO RESTART', 220, 240, rgba8(255, 255, 255, 255));
|
|
1155
|
+
|
|
1156
|
+
if (btnp(17)) { // R key
|
|
1157
|
+
// Reset game
|
|
1158
|
+
score = 0;
|
|
1159
|
+
level = 1;
|
|
1160
|
+
player.health = 100;
|
|
1161
|
+
player.energy = 100;
|
|
1162
|
+
player.coins = 0;
|
|
1163
|
+
player.x = 0; player.y = 2; player.z = 0;
|
|
1164
|
+
gameState = 'playing';
|
|
1165
|
+
clearScene();
|
|
1166
|
+
init();
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|