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,1537 @@
|
|
|
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
|
+
// VERSION: v006-NUCLEAR-CACHE-BUST-${Date.now()}
|
|
5
|
+
|
|
6
|
+
// 🚨 CACHE DETECTION: If you see logs repeating in console, browser cache is stuck!
|
|
7
|
+
const CACHE_BUSTER_V006 = 'FRESH_CODE_LOADED_' + Date.now();
|
|
8
|
+
console.log('🚀🚀🚀 CACHE BUSTER V006 LOADED:', CACHE_BUSTER_V006);
|
|
9
|
+
|
|
10
|
+
// Helper function for 3D vectors
|
|
11
|
+
function vec3(x, y, z) {
|
|
12
|
+
return { x: x, y: y, z: z };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Game state
|
|
16
|
+
let gameTime = 0;
|
|
17
|
+
let gameState = 'start'; // 'start', 'playing', 'gameOver'
|
|
18
|
+
let startScreenTime = 0;
|
|
19
|
+
let uiButtons = [];
|
|
20
|
+
let score = 0;
|
|
21
|
+
let level = 1;
|
|
22
|
+
let combo = 0;
|
|
23
|
+
let comboTimer = 0;
|
|
24
|
+
|
|
25
|
+
// 🔥 DEFINE BUTTON CALLBACK AT MODULE LEVEL TO AVOID SCOPE ISSUES
|
|
26
|
+
const startGameCallback = () => {
|
|
27
|
+
console.log('🎯🎯🎯 START BUTTON CLICKED V006! FRESH CODE! 🎯🎯🎯');
|
|
28
|
+
console.log('📊 BEFORE: gameState =', gameState);
|
|
29
|
+
gameState = 'playing';
|
|
30
|
+
console.log('📊 AFTER: gameState =', gameState);
|
|
31
|
+
console.log('✅✅✅ STATE CHANGED TO PLAYING! ✅✅✅');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 3D Game objects
|
|
35
|
+
let playerKnight = null;
|
|
36
|
+
let platforms = [];
|
|
37
|
+
let enemies = [];
|
|
38
|
+
let coins = [];
|
|
39
|
+
let particles = [];
|
|
40
|
+
let environment = [];
|
|
41
|
+
|
|
42
|
+
// Player state - NINJA POWERS!
|
|
43
|
+
let player = {
|
|
44
|
+
x: 0,
|
|
45
|
+
y: 0,
|
|
46
|
+
z: 0,
|
|
47
|
+
vx: 0,
|
|
48
|
+
vy: 0,
|
|
49
|
+
vz: 0,
|
|
50
|
+
onGround: false,
|
|
51
|
+
health: 100,
|
|
52
|
+
energy: 100,
|
|
53
|
+
coins: 0,
|
|
54
|
+
shuriken: 20,
|
|
55
|
+
facingRight: true,
|
|
56
|
+
jumpPower: 10,
|
|
57
|
+
speed: 8,
|
|
58
|
+
doubleJump: true,
|
|
59
|
+
attackCooldown: 0,
|
|
60
|
+
// Ninja abilities
|
|
61
|
+
wallRunning: false,
|
|
62
|
+
wallRunTime: 0,
|
|
63
|
+
maxWallRunTime: 1.5,
|
|
64
|
+
dashCooldown: 0,
|
|
65
|
+
dashDuration: 0,
|
|
66
|
+
crouching: false,
|
|
67
|
+
stealth: false,
|
|
68
|
+
grapplePoints: [],
|
|
69
|
+
grappling: false,
|
|
70
|
+
grappleTarget: null,
|
|
71
|
+
airDashAvailable: true,
|
|
72
|
+
slideDuration: 0,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Camera state
|
|
76
|
+
let camera = {
|
|
77
|
+
x: 0,
|
|
78
|
+
y: 8,
|
|
79
|
+
z: 15,
|
|
80
|
+
targetX: 0,
|
|
81
|
+
targetY: 5,
|
|
82
|
+
targetZ: 0,
|
|
83
|
+
smoothing: 0.1,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function init() {
|
|
87
|
+
console.log('��� KNIGHT PLATFORMER V006 INIT START 🚀🚀🚀');
|
|
88
|
+
console.log('📦 gameState BEFORE init:', gameState);
|
|
89
|
+
console.log('�️ Clearing arrays. Current particles:', particles.length);
|
|
90
|
+
|
|
91
|
+
// Clear all arrays to prevent mesh errors
|
|
92
|
+
enemies = [];
|
|
93
|
+
coins = [];
|
|
94
|
+
particles = [];
|
|
95
|
+
platforms = [];
|
|
96
|
+
environment = [];
|
|
97
|
+
playerKnight = null;
|
|
98
|
+
|
|
99
|
+
// Reset player state completely
|
|
100
|
+
// Start ON the starting platform - PERFECT ONBOARDING!
|
|
101
|
+
player = {
|
|
102
|
+
pos: vec3(-10, 2, 0), // Start on LEFT side of ground platform at comfortable height
|
|
103
|
+
vel: vec3(0, 0, 0),
|
|
104
|
+
yaw: 0, // Face RIGHT to see the level ahead
|
|
105
|
+
grounded: true, // Start grounded so player doesn't fall!
|
|
106
|
+
attacking: false,
|
|
107
|
+
attackTime: 0,
|
|
108
|
+
health: 100,
|
|
109
|
+
maxHealth: 100,
|
|
110
|
+
stamina: 100,
|
|
111
|
+
maxStamina: 100,
|
|
112
|
+
isDashing: false,
|
|
113
|
+
dashTime: 0,
|
|
114
|
+
wallRunning: false,
|
|
115
|
+
wallRunTime: 0,
|
|
116
|
+
wallSide: 0,
|
|
117
|
+
doubleJumpAvailable: true,
|
|
118
|
+
grappling: false,
|
|
119
|
+
grappleTarget: null,
|
|
120
|
+
sliding: false,
|
|
121
|
+
slideTime: 0,
|
|
122
|
+
shurikenCount: 10,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
score = 0;
|
|
126
|
+
combo = 0;
|
|
127
|
+
comboTimer = 0;
|
|
128
|
+
gameTime = 0;
|
|
129
|
+
level = 1;
|
|
130
|
+
// 🔥 DON'T RESET gameState HERE - button callback may have already changed it!
|
|
131
|
+
// gameState = 'start' // ← REMOVED - was resetting state after button click!
|
|
132
|
+
startScreenTime = 0;
|
|
133
|
+
|
|
134
|
+
// Create start button - callback is defined at module level
|
|
135
|
+
uiButtons = [];
|
|
136
|
+
|
|
137
|
+
uiButtons.push(
|
|
138
|
+
createButton(210, 310, 220, 50, '▶ START GAME', startGameCallback, {
|
|
139
|
+
normalColor: rgba8(150, 80, 255, 255),
|
|
140
|
+
hoverColor: rgba8(180, 110, 255, 255),
|
|
141
|
+
pressedColor: rgba8(120, 60, 220, 255),
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Force canvas focus for keyboard events
|
|
146
|
+
console.log('🎮 Focusing canvas for input...');
|
|
147
|
+
const canvas = document.querySelector('canvas');
|
|
148
|
+
if (canvas) {
|
|
149
|
+
canvas.focus();
|
|
150
|
+
canvas.tabIndex = 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create game objects and store references
|
|
154
|
+
playerKnight = createNinjaPlayer();
|
|
155
|
+
createNinjaWorld();
|
|
156
|
+
createPlatforms();
|
|
157
|
+
createGrapplePoints();
|
|
158
|
+
spawnEnemies();
|
|
159
|
+
spawnCoins();
|
|
160
|
+
|
|
161
|
+
// 📷 OPTIMAL SIDE-SCROLLER CAMERA (like Strider/Shinobi)
|
|
162
|
+
// Camera positioned for perfect gameplay view!
|
|
163
|
+
camera.x = player.pos.x;
|
|
164
|
+
camera.y = 6; // Eye-level height for immersive side-scrolling
|
|
165
|
+
camera.z = 18; // Closer for better visibility and control
|
|
166
|
+
camera.targetX = player.pos.x;
|
|
167
|
+
camera.targetY = 5; // Look slightly ahead
|
|
168
|
+
camera.targetZ = 0; // Straight ahead on the action plane
|
|
169
|
+
|
|
170
|
+
// Set initial camera position
|
|
171
|
+
setCameraPosition(camera.x, camera.y, camera.z);
|
|
172
|
+
setCameraTarget(camera.targetX, camera.targetY, camera.targetZ);
|
|
173
|
+
|
|
174
|
+
// 💡 MAXIMUM BRIGHTNESS - Crystal clear visibility!
|
|
175
|
+
setAmbientLight(0xffffff); // FULL WHITE ambient - no shadows!
|
|
176
|
+
setLightColor(0xffffff); // FULL WHITE directional light
|
|
177
|
+
setLightDirection(0.5, -0.5, 0.8); // From front and above for maximum visibility
|
|
178
|
+
|
|
179
|
+
console.log('✅✅✅ KNIGHT PLATFORMER V006 INIT COMPLETE ✅✅✅');
|
|
180
|
+
console.log('📦 gameState AFTER init:', gameState);
|
|
181
|
+
console.log('📷 Camera initialized at:', camera.x, camera.y, camera.z);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function initStartScreen() {
|
|
185
|
+
uiButtons = [];
|
|
186
|
+
console.log('🥷 Start screen initialized');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function update() {
|
|
190
|
+
const dt = 1 / 60;
|
|
191
|
+
|
|
192
|
+
console.log(
|
|
193
|
+
'🔄 UPDATE called - gameState:',
|
|
194
|
+
gameState,
|
|
195
|
+
'playerKnight:',
|
|
196
|
+
playerKnight ? 'exists' : 'null'
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Safety check: Don't update if game objects aren't initialized yet
|
|
200
|
+
if (playerKnight === null) {
|
|
201
|
+
// Game not initialized yet, skip this frame
|
|
202
|
+
console.log('⚠️ UPDATE: playerKnight is null, returning early');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle start screen
|
|
207
|
+
if (gameState === 'start') {
|
|
208
|
+
startScreenTime += dt;
|
|
209
|
+
|
|
210
|
+
// DEBUG: Log keyboard state
|
|
211
|
+
const enterDown = isKeyDown('Enter');
|
|
212
|
+
const spaceDown = isKeyDown('Space');
|
|
213
|
+
if (enterDown || spaceDown) {
|
|
214
|
+
console.log('⌨️ Keyboard detected! Enter:', enterDown, 'Space:', spaceDown);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Update UI buttons (may change gameState via callback)
|
|
218
|
+
updateAllButtons();
|
|
219
|
+
|
|
220
|
+
// ✨ CRITICAL: Re-check gameState IMMEDIATELY after updateAllButtons()
|
|
221
|
+
// The button callback executes SYNCHRONOUSLY and may have changed gameState
|
|
222
|
+
if (gameState !== 'start') {
|
|
223
|
+
console.log('🎮🎮🎮 Button changed gameState to:', gameState, '- starting game!');
|
|
224
|
+
// DON'T return - fall through to playing state below
|
|
225
|
+
} else {
|
|
226
|
+
// Still on start screen - check for keyboard/gamepad input
|
|
227
|
+
// KEYBOARD FALLBACK: Press ENTER or SPACE to start
|
|
228
|
+
// Use isKeyDown (held) instead of isKeyPressed (transition) for more reliable detection
|
|
229
|
+
if (isKeyDown('Enter') || isKeyDown(' ') || isKeyDown('Space') || btnp(0) || btnp(1)) {
|
|
230
|
+
console.log('🥷🥷🥷 Starting Shadow Ninja 3D via keyboard or button!');
|
|
231
|
+
gameState = 'playing';
|
|
232
|
+
console.log('🎮🎮🎮 Game started! State:', gameState);
|
|
233
|
+
// Fall through to playing state
|
|
234
|
+
} else {
|
|
235
|
+
// Check for any OTHER button press to start
|
|
236
|
+
for (let i = 2; i < 10; i++) {
|
|
237
|
+
if (btnp(i)) {
|
|
238
|
+
console.log(`🥷 Starting Shadow Ninja 3D via button ${i}!`);
|
|
239
|
+
gameState = 'playing';
|
|
240
|
+
console.log('🎮 Game started! State:', gameState);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ✨ CRITICAL FIX: Only return if STILL on start screen after ALL checks (including UI button callback)
|
|
246
|
+
if (gameState === 'start') {
|
|
247
|
+
console.log('📺 UPDATE: Start screen done, returning');
|
|
248
|
+
return;
|
|
249
|
+
} else {
|
|
250
|
+
console.log('🚀 gameState changed to:', gameState, '- continuing to game!');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If we reach here and still on start screen somehow, return
|
|
257
|
+
if (gameState === 'start') {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Playing state - main game loop
|
|
262
|
+
gameTime += dt;
|
|
263
|
+
|
|
264
|
+
// Update input
|
|
265
|
+
updateInput(dt);
|
|
266
|
+
|
|
267
|
+
// Update player
|
|
268
|
+
updatePlayer(dt);
|
|
269
|
+
|
|
270
|
+
// Update enemies
|
|
271
|
+
updateEnemies(dt);
|
|
272
|
+
|
|
273
|
+
// Update coins
|
|
274
|
+
updateCoins(dt);
|
|
275
|
+
|
|
276
|
+
// Update particles
|
|
277
|
+
updateParticles(dt);
|
|
278
|
+
|
|
279
|
+
// Update camera to follow player (CRITICAL for 2.5D side-scroller view)
|
|
280
|
+
updateCamera(dt);
|
|
281
|
+
|
|
282
|
+
// Update combo timer
|
|
283
|
+
if (comboTimer > 0) {
|
|
284
|
+
comboTimer -= dt;
|
|
285
|
+
if (comboTimer <= 0) {
|
|
286
|
+
combo = 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for player death
|
|
291
|
+
if (player.health <= 0) {
|
|
292
|
+
gameState = 'gameover';
|
|
293
|
+
console.log('💀 Game Over! Final Score:', score);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function draw() {
|
|
298
|
+
if (gameState === 'start') {
|
|
299
|
+
drawStartScreen();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 3D scene is automatically rendered by GPU backend
|
|
304
|
+
// Draw UI overlay using 2D API
|
|
305
|
+
drawUI();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function drawStartScreen() {
|
|
309
|
+
// Dark background
|
|
310
|
+
rect(0, 0, 640, 360, rgba8(10, 5, 30, 255), true);
|
|
311
|
+
|
|
312
|
+
// Title with glow effect
|
|
313
|
+
print('SHADOW NINJA 3D', 180, 70, rgba8(180, 80, 255, 255));
|
|
314
|
+
|
|
315
|
+
// Subtitle
|
|
316
|
+
const pulse = Math.sin(startScreenTime * 3) * 0.25 + 0.75;
|
|
317
|
+
print(
|
|
318
|
+
'Strider-Style 3.5D Ninja Platformer',
|
|
319
|
+
160,
|
|
320
|
+
100,
|
|
321
|
+
rgba8(200, 150, 255, Math.floor(pulse * 255))
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// START PROMPT - Make it obvious
|
|
325
|
+
const promptPulse = Math.sin(startScreenTime * 5) * 0.5 + 0.5;
|
|
326
|
+
print(
|
|
327
|
+
'PRESS ENTER OR SPACE TO START',
|
|
328
|
+
190,
|
|
329
|
+
120,
|
|
330
|
+
rgba8(255, 255, 100, Math.floor(promptPulse * 255))
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Controls panel
|
|
334
|
+
rect(100, 140, 440, 160, rgba8(20, 10, 40, 220), true);
|
|
335
|
+
rect(100, 140, 440, 160, rgba8(150, 80, 255, 255), false);
|
|
336
|
+
|
|
337
|
+
// Controls title
|
|
338
|
+
print('NINJA ABILITIES:', 220, 155, rgba8(255, 200, 255, 255));
|
|
339
|
+
|
|
340
|
+
// Controls - two columns
|
|
341
|
+
print('ARROWS = Move', 120, 180, rgba8(200, 200, 255, 255));
|
|
342
|
+
print('DOWN = Slide', 120, 200, rgba8(200, 200, 255, 255));
|
|
343
|
+
print('UP = Jump/Double Jump', 120, 220, rgba8(200, 200, 255, 255));
|
|
344
|
+
print('Z = Attack', 120, 240, rgba8(200, 200, 255, 255));
|
|
345
|
+
print('X = Dash/Air Dash', 120, 260, rgba8(200, 200, 255, 255));
|
|
346
|
+
|
|
347
|
+
print('C = Throw Shuriken', 340, 180, rgba8(200, 200, 255, 255));
|
|
348
|
+
print('G = Grappling Hook', 340, 200, rgba8(200, 200, 255, 255));
|
|
349
|
+
print('Wall Running Active', 340, 220, rgba8(150, 255, 150, 255));
|
|
350
|
+
print('Combo System', 340, 240, rgba8(255, 255, 150, 255));
|
|
351
|
+
print('Energy Management', 340, 260, rgba8(150, 200, 255, 255));
|
|
352
|
+
|
|
353
|
+
// Draw start button
|
|
354
|
+
drawAllButtons();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function createNinjaPlayer() {
|
|
358
|
+
// 🥷 BRIGHT NINJA - Highly visible and COOL!
|
|
359
|
+
|
|
360
|
+
// Main body - BRIGHT BLUE ninja suit
|
|
361
|
+
const body = createCube(1, 0x4466ff, [0, 0, 0]); // Bright blue suit
|
|
362
|
+
setScale(body, 1.2, 2, 0.9); // Tall and athletic
|
|
363
|
+
|
|
364
|
+
// Chest armor - BRIGHT CYAN GLOWING plate
|
|
365
|
+
const chest = createCube(0.8, 0x44ffff, [0, 0.4, 0.46]); // Bright cyan chest plate
|
|
366
|
+
setScale(chest, 1.1, 1.2, 0.1);
|
|
367
|
+
|
|
368
|
+
// Head - BRIGHT GRAY helmet
|
|
369
|
+
const head = createCube(0.7, 0x888888, [0, 1.5, 0]); // Gray ninja helmet
|
|
370
|
+
setScale(head, 0.9, 0.9, 0.9);
|
|
371
|
+
|
|
372
|
+
// GLOWING YELLOW VISOR - highly visible eyes
|
|
373
|
+
const visor = createCube(0.8, 0xffff44, [0, 1.5, 0.36]); // Yellow glowing visor
|
|
374
|
+
setScale(visor, 0.85, 0.3, 0.05);
|
|
375
|
+
|
|
376
|
+
// BRIGHT RED HEADBAND - flowing behind
|
|
377
|
+
const headband = createCube(1.5, 0xff4444, [0, 1.7, -0.4]); // Bright red ribbon
|
|
378
|
+
setScale(headband, 0.15, 0.1, 1.2);
|
|
379
|
+
|
|
380
|
+
// BRIGHT PURPLE ENERGY KATANA - glowing blade
|
|
381
|
+
const katana = createCube(0.08, 0xdd88ff, [-0.5, 0.9, -0.4]); // Bright purple katana
|
|
382
|
+
setScale(katana, 0.08, 2, 0.08);
|
|
383
|
+
setRotation(katana, 0, 0, -0.4); // Angled on back
|
|
384
|
+
|
|
385
|
+
// Katana handle - BRIGHT SILVER
|
|
386
|
+
const katanaHandle = createCube(0.12, 0xdddddd, [-0.5, -0.3, -0.4]);
|
|
387
|
+
setScale(katanaHandle, 0.12, 0.4, 0.12);
|
|
388
|
+
setRotation(katanaHandle, 0, 0, -0.4);
|
|
389
|
+
|
|
390
|
+
// BRIGHT CYAN SCARF - flowing behind
|
|
391
|
+
const scarf = createCube(0.4, 0x44ddff, [0, 1, -0.6]); // Bright cyan trail
|
|
392
|
+
setScale(scarf, 0.7, 1.2, 0.15);
|
|
393
|
+
|
|
394
|
+
// Arms - BRIGHT BLUE with energy
|
|
395
|
+
const leftArm = createCube(0.3, 0x4466ff, [-0.7, 0.2, 0]);
|
|
396
|
+
setScale(leftArm, 0.3, 1.3, 0.3);
|
|
397
|
+
|
|
398
|
+
const rightArm = createCube(0.3, 0x4466ff, [0.7, 0.2, 0]);
|
|
399
|
+
setScale(rightArm, 0.3, 1.3, 0.3);
|
|
400
|
+
|
|
401
|
+
// BRIGHT arm guards - YELLOW ENERGY
|
|
402
|
+
const leftGuard = createCube(0.32, 0xffff44, [-0.7, -0.3, 0]);
|
|
403
|
+
setScale(leftGuard, 0.35, 0.25, 0.35);
|
|
404
|
+
|
|
405
|
+
const rightGuard = createCube(0.32, 0xffff44, [0.7, -0.3, 0]);
|
|
406
|
+
setScale(rightGuard, 0.35, 0.25, 0.35);
|
|
407
|
+
|
|
408
|
+
// Legs - BRIGHT BLUE pants
|
|
409
|
+
const leftLeg = createCube(0.35, 0x4466ff, [-0.4, -1.3, 0]);
|
|
410
|
+
setScale(leftLeg, 0.35, 1.4, 0.35);
|
|
411
|
+
|
|
412
|
+
const rightLeg = createCube(0.35, 0x4466ff, [0.4, -1.3, 0]);
|
|
413
|
+
setScale(rightLeg, 0.35, 1.4, 0.35);
|
|
414
|
+
|
|
415
|
+
// Knee guards - BRIGHT YELLOW
|
|
416
|
+
const leftKnee = createCube(0.36, 0xffff44, [-0.4, -1, 0]);
|
|
417
|
+
setScale(leftKnee, 0.38, 0.2, 0.38);
|
|
418
|
+
|
|
419
|
+
const rightKnee = createCube(0.36, 0xffff44, [0.4, -1, 0]);
|
|
420
|
+
setScale(rightKnee, 0.38, 0.2, 0.38);
|
|
421
|
+
|
|
422
|
+
// NO AURA - it blocks visibility!
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
body,
|
|
426
|
+
chest,
|
|
427
|
+
head,
|
|
428
|
+
visor,
|
|
429
|
+
headband,
|
|
430
|
+
katana,
|
|
431
|
+
katanaHandle,
|
|
432
|
+
scarf,
|
|
433
|
+
leftArm,
|
|
434
|
+
rightArm,
|
|
435
|
+
leftGuard,
|
|
436
|
+
rightGuard,
|
|
437
|
+
leftLeg,
|
|
438
|
+
rightLeg,
|
|
439
|
+
leftKnee,
|
|
440
|
+
rightKnee,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function createNinjaWorld() {
|
|
445
|
+
// 🌈 BRIGHT COLORFUL WORLD - Maximum visibility!
|
|
446
|
+
|
|
447
|
+
// BRIGHT BLUE ground - easy to see!
|
|
448
|
+
const ground = createPlane(400, 400, 0x6699ff, [25, -3, 0]);
|
|
449
|
+
setRotation(ground, -Math.PI / 2, 0, 0);
|
|
450
|
+
|
|
451
|
+
// BRIGHT YELLOW grid lines on ground for depth perception
|
|
452
|
+
for (let i = -10; i <= 20; i++) {
|
|
453
|
+
const gridLine = createCube(200, 0.15, 0xffee44, [i * 5, -2.8, 0]);
|
|
454
|
+
setScale(gridLine, 1, 1, 1);
|
|
455
|
+
setRotation(gridLine, -Math.PI / 2, 0, 0);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// COLORFUL BUILDINGS - Bright and visible!
|
|
459
|
+
const buildingColors = [
|
|
460
|
+
0xff6688, // Bright pink
|
|
461
|
+
0x66ff88, // Bright green
|
|
462
|
+
0x6688ff, // Bright blue
|
|
463
|
+
0xffaa66, // Bright orange
|
|
464
|
+
0xaa66ff, // Bright purple
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
// Background buildings - BRIGHT colors!
|
|
468
|
+
for (let i = 0; i < 12; i++) {
|
|
469
|
+
const color = buildingColors[i % buildingColors.length];
|
|
470
|
+
const x = (i - 6) * 15;
|
|
471
|
+
const height = 20 + Math.random() * 15;
|
|
472
|
+
|
|
473
|
+
// Main building - BRIGHT!
|
|
474
|
+
const building = createCube(8, height, color, [x, height / 2 - 3, -40]);
|
|
475
|
+
setScale(building, 1, 1, 1);
|
|
476
|
+
|
|
477
|
+
// WHITE accent strips for contrast
|
|
478
|
+
for (let j = 0; j < 3; j++) {
|
|
479
|
+
const strip = createCube(8.3, 1, 0xffffff, [x, (height / 4) * (j + 1) - 3, -39.7]);
|
|
480
|
+
setScale(strip, 1, 1, 1);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// BRIGHT FLOATING PLATFORMS in background
|
|
485
|
+
const bgPlatforms = [
|
|
486
|
+
{ x: -15, y: 8, color: 0xff88ff },
|
|
487
|
+
{ x: 5, y: 10, color: 0x88ffff },
|
|
488
|
+
{ x: 25, y: 9, color: 0xffff88 },
|
|
489
|
+
{ x: 40, y: 11, color: 0x88ff88 },
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
bgPlatforms.forEach(plat => {
|
|
493
|
+
const platform = createCube(6, 1, plat.color, [plat.x, plat.y, -25]);
|
|
494
|
+
setScale(platform, 1, 1, 1);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// BRIGHT DECORATIVE SPHERES
|
|
498
|
+
for (let i = 0; i < 15; i++) {
|
|
499
|
+
const colors = [0xff8888, 0x88ff88, 0x8888ff, 0xffff88, 0xff88ff, 0x88ffff];
|
|
500
|
+
const orbColor = colors[i % colors.length];
|
|
501
|
+
createSphere(0.8, orbColor, [-20 + i * 5, 4 + Math.sin(i) * 3, -15 - Math.cos(i) * 5]);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// BRIGHT YELLOW SUN/LIGHT SOURCE in sky
|
|
505
|
+
createSphere(10, 0xffffaa, [0, 30, -70]);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function createPlatforms() {
|
|
509
|
+
platforms = [];
|
|
510
|
+
|
|
511
|
+
// 🏗️ MAIN GROUND - Long continuous platform for easy side-scrolling!
|
|
512
|
+
const mainGround = {
|
|
513
|
+
mesh: createCube(100, 1, 0x6644ff, [-10, 0, 0]), // BRIGHT PURPLE main platform!
|
|
514
|
+
x: -10,
|
|
515
|
+
y: 0,
|
|
516
|
+
z: 0,
|
|
517
|
+
width: 100,
|
|
518
|
+
height: 1,
|
|
519
|
+
depth: 8,
|
|
520
|
+
type: 'ground',
|
|
521
|
+
};
|
|
522
|
+
setScale(mainGround.mesh, 1, 1, 1);
|
|
523
|
+
platforms.push(mainGround);
|
|
524
|
+
|
|
525
|
+
// Add BRIGHT YELLOW glowing edge to main platform
|
|
526
|
+
const mainEdge = createCube(100, 0.2, 0xffff00, [-10, 1.1, 0]);
|
|
527
|
+
setScale(mainEdge, 1, 1, 1);
|
|
528
|
+
|
|
529
|
+
// EASY ACCESSIBLE PLATFORMS - Progressive difficulty on the same Z plane (Z=0)!
|
|
530
|
+
const easyPlatforms = [
|
|
531
|
+
// Tutorial section - easy jumps
|
|
532
|
+
{ x: -5, y: 3, w: 6, color: 0x88aa66 },
|
|
533
|
+
{ x: 2, y: 4.5, w: 5, color: 0x6688aa },
|
|
534
|
+
{ x: 8, y: 6, w: 5, color: 0xaa6688 },
|
|
535
|
+
{ x: 14, y: 7.5, w: 4.5, color: 0x66aa88 },
|
|
536
|
+
|
|
537
|
+
// Mid-section - gaining height
|
|
538
|
+
{ x: 20, y: 9, w: 5, color: 0xaa8866 },
|
|
539
|
+
{ x: 26, y: 10.5, w: 4, color: 0x88aa66 },
|
|
540
|
+
{ x: 31, y: 12, w: 4, color: 0x6688aa },
|
|
541
|
+
|
|
542
|
+
// Upper section - skill jumps
|
|
543
|
+
{ x: 37, y: 13, w: 4.5, color: 0xaa6688 },
|
|
544
|
+
{ x: 43, y: 14, w: 4, color: 0x66aa88 },
|
|
545
|
+
{ x: 48, y: 15, w: 5, color: 0xaa8866 },
|
|
546
|
+
];
|
|
547
|
+
|
|
548
|
+
easyPlatforms.forEach((data, i) => {
|
|
549
|
+
const platform = {
|
|
550
|
+
mesh: createCube(data.w, 0.8, data.color, [data.x, data.y, 0]), // All at Z=0!
|
|
551
|
+
x: data.x,
|
|
552
|
+
y: data.y,
|
|
553
|
+
z: 0,
|
|
554
|
+
width: data.w,
|
|
555
|
+
height: 0.8,
|
|
556
|
+
depth: 4,
|
|
557
|
+
type: 'floating',
|
|
558
|
+
id: i,
|
|
559
|
+
};
|
|
560
|
+
setScale(platform.mesh, 1, 1, 1);
|
|
561
|
+
platforms.push(platform);
|
|
562
|
+
|
|
563
|
+
// Add BRIGHT WHITE edge to each platform
|
|
564
|
+
const edgeLight = createCube(data.w + 0.1, 0.15, 0xffffff, [data.x, data.y + 0.48, 0]);
|
|
565
|
+
setScale(edgeLight, 1, 1, 1);
|
|
566
|
+
|
|
567
|
+
// Add BRIGHT YELLOW underglow
|
|
568
|
+
const underglow = createCube(data.w - 0.2, 0.12, 0xffee44, [data.x, data.y - 0.42, 0]);
|
|
569
|
+
setScale(underglow, 1, 1, 1);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// COLLECTIBLE PLATFORMS - coins trail
|
|
573
|
+
const coinPlatforms = [
|
|
574
|
+
{ x: 5, y: 2, w: 3 },
|
|
575
|
+
{ x: 11, y: 3.5, w: 2.5 },
|
|
576
|
+
{ x: 17, y: 5, w: 3 },
|
|
577
|
+
{ x: 23, y: 6.5, w: 2.5 },
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
coinPlatforms.forEach((data, i) => {
|
|
581
|
+
const platform = {
|
|
582
|
+
mesh: createCube(data.w, 0.5, 0xffaa44, [data.x, data.y, 0]), // BRIGHT ORANGE platforms!
|
|
583
|
+
x: data.x,
|
|
584
|
+
y: data.y,
|
|
585
|
+
z: 0,
|
|
586
|
+
width: data.w,
|
|
587
|
+
height: 0.5,
|
|
588
|
+
depth: 3,
|
|
589
|
+
type: 'coin',
|
|
590
|
+
id: i + 100,
|
|
591
|
+
};
|
|
592
|
+
setScale(platform.mesh, 1, 1, 1);
|
|
593
|
+
platforms.push(platform);
|
|
594
|
+
|
|
595
|
+
// BRIGHT YELLOW glow for coin platforms
|
|
596
|
+
const glow = createCube(data.w + 0.2, 0.12, 0xffff44, [data.x, data.y + 0.32, 0]);
|
|
597
|
+
setScale(glow, 1, 1, 1);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function createGrapplePoints() {
|
|
602
|
+
player.grapplePoints = [];
|
|
603
|
+
|
|
604
|
+
// Add grapple points in strategic locations
|
|
605
|
+
const grappleData = [
|
|
606
|
+
{ x: 18, y: 12, z: 5 },
|
|
607
|
+
{ x: 35, y: 15, z: 8 },
|
|
608
|
+
{ x: 20, y: 22, z: -8 },
|
|
609
|
+
{ x: -5, y: 24, z: -18 },
|
|
610
|
+
{ x: -30, y: 18, z: -10 },
|
|
611
|
+
{ x: -40, y: 12, z: 2 },
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
grappleData.forEach(data => {
|
|
615
|
+
const point = {
|
|
616
|
+
mesh: createSphere(0.4, 0x00ffff, [data.x, data.y, data.z]),
|
|
617
|
+
x: data.x,
|
|
618
|
+
y: data.y,
|
|
619
|
+
z: data.z,
|
|
620
|
+
active: true,
|
|
621
|
+
};
|
|
622
|
+
player.grapplePoints.push(point);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function spawnEnemies() {
|
|
627
|
+
enemies = [];
|
|
628
|
+
|
|
629
|
+
// Ground patrol enemies - ON THE SAME Z PLANE as player!
|
|
630
|
+
const enemyPositions = [
|
|
631
|
+
{ x: 10, y: 1.5 },
|
|
632
|
+
{ x: 25, y: 1.5 },
|
|
633
|
+
{ x: 35, y: 1.5 },
|
|
634
|
+
{ x: 50, y: 1.5 },
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
enemyPositions.forEach((pos, i) => {
|
|
638
|
+
const enemy = {
|
|
639
|
+
mesh: createCube(1, 0xff3355, [pos.x, pos.y, 0]), // Red cyber-enemies at Z=0!
|
|
640
|
+
x: pos.x,
|
|
641
|
+
y: pos.y,
|
|
642
|
+
z: 0,
|
|
643
|
+
vx: -2, // Move left
|
|
644
|
+
health: 3,
|
|
645
|
+
type: 'patrol',
|
|
646
|
+
patrolRange: 8,
|
|
647
|
+
originalX: pos.x,
|
|
648
|
+
attackCooldown: 0,
|
|
649
|
+
};
|
|
650
|
+
setScale(enemy.mesh, 1, 1.5, 1);
|
|
651
|
+
enemies.push(enemy);
|
|
652
|
+
|
|
653
|
+
// Red glowing eyes
|
|
654
|
+
const leftEye = createSphere(0.15, 0xff0000, [pos.x - 0.3, pos.y + 0.5, 0.51]);
|
|
655
|
+
const rightEye = createSphere(0.15, 0xff0000, [pos.x + 0.3, pos.y + 0.5, 0.51]);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Flying drones - hovering enemies on same Z plane
|
|
659
|
+
const dronePositions = [
|
|
660
|
+
{ x: 18, y: 8 },
|
|
661
|
+
{ x: 33, y: 10 },
|
|
662
|
+
{ x: 46, y: 12 },
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
dronePositions.forEach((pos, i) => {
|
|
666
|
+
const enemy = {
|
|
667
|
+
mesh: createSphere(0.6, 0xff8844, [pos.x, pos.y, 0]), // Orange drones at Z=0!
|
|
668
|
+
x: pos.x,
|
|
669
|
+
y: pos.y,
|
|
670
|
+
z: 0,
|
|
671
|
+
vx: 0,
|
|
672
|
+
vy: 0,
|
|
673
|
+
vz: 0,
|
|
674
|
+
health: 2,
|
|
675
|
+
type: 'flyer',
|
|
676
|
+
orbitCenter: { x: pos.x, y: pos.y, z: 0 },
|
|
677
|
+
orbitRadius: 3,
|
|
678
|
+
orbitAngle: i * 2,
|
|
679
|
+
attackCooldown: 0,
|
|
680
|
+
};
|
|
681
|
+
enemies.push(enemy);
|
|
682
|
+
|
|
683
|
+
// Cyan propeller effect
|
|
684
|
+
const propeller = createCube(1.2, 0.1, 0x00ffff, [pos.x, pos.y + 0.7, 0]);
|
|
685
|
+
setScale(propeller, 1, 1, 1);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function spawnCoins() {
|
|
690
|
+
coins = [];
|
|
691
|
+
|
|
692
|
+
// Place coins on platforms
|
|
693
|
+
platforms.forEach((platform, i) => {
|
|
694
|
+
if (i > 8) {
|
|
695
|
+
// Skip ground platforms
|
|
696
|
+
const coin = {
|
|
697
|
+
mesh: createSphere(0.3, 0xffdd00, [platform.x, platform.y + 1.5, platform.z]),
|
|
698
|
+
x: platform.x,
|
|
699
|
+
y: platform.y + 1.5,
|
|
700
|
+
z: platform.z,
|
|
701
|
+
collected: false,
|
|
702
|
+
rotationY: 0,
|
|
703
|
+
bobOffset: Math.random() * Math.PI * 2,
|
|
704
|
+
};
|
|
705
|
+
coins.push(coin);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Bonus coins in hard to reach places
|
|
710
|
+
for (let i = 0; i < 8; i++) {
|
|
711
|
+
const coin = {
|
|
712
|
+
mesh: createSphere(0.25, 0xffaa00, [
|
|
713
|
+
(Math.random() - 0.5) * 60,
|
|
714
|
+
5 + Math.random() * 10,
|
|
715
|
+
(Math.random() - 0.5) * 30,
|
|
716
|
+
]),
|
|
717
|
+
x: 0,
|
|
718
|
+
y: 0,
|
|
719
|
+
z: 0,
|
|
720
|
+
collected: false,
|
|
721
|
+
rotationY: 0,
|
|
722
|
+
bobOffset: Math.random() * Math.PI * 2,
|
|
723
|
+
};
|
|
724
|
+
const pos = getPosition(coin.mesh);
|
|
725
|
+
coin.x = pos[0];
|
|
726
|
+
coin.y = pos[1];
|
|
727
|
+
coin.z = pos[2];
|
|
728
|
+
coins.push(coin);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function updateInput(dt) {
|
|
733
|
+
const moveSpeed = 15; // Base movement speed
|
|
734
|
+
const dashMultiplier = player.isDashing && player.dashTime > 0 ? 2.5 : 1.0;
|
|
735
|
+
|
|
736
|
+
// Update timers
|
|
737
|
+
if (player.attackTime > 0) player.attackTime -= dt;
|
|
738
|
+
if (player.dashTime > 0) player.dashTime -= dt;
|
|
739
|
+
if (player.slideTime > 0) player.slideTime -= dt;
|
|
740
|
+
|
|
741
|
+
// Sliding
|
|
742
|
+
if (player.sliding && player.slideTime > 0) {
|
|
743
|
+
const slideDir = Math.cos(player.yaw);
|
|
744
|
+
player.vel.x = slideDir * moveSpeed * 1.5;
|
|
745
|
+
return; // Override other controls during slide
|
|
746
|
+
} else {
|
|
747
|
+
player.sliding = false;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Grappling hook movement
|
|
751
|
+
if (player.grappling && player.grappleTarget) {
|
|
752
|
+
const dx = player.grappleTarget.x - player.pos.x;
|
|
753
|
+
const dy = player.grappleTarget.y - player.pos.y;
|
|
754
|
+
const dz = player.grappleTarget.z - player.pos.z;
|
|
755
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
756
|
+
|
|
757
|
+
if (dist > 1.5) {
|
|
758
|
+
const pullSpeed = 20;
|
|
759
|
+
player.vel.x = (dx / dist) * pullSpeed;
|
|
760
|
+
player.vel.y = (dy / dist) * pullSpeed;
|
|
761
|
+
player.vel.z = (dz / dist) * pullSpeed;
|
|
762
|
+
} else {
|
|
763
|
+
player.grappling = false;
|
|
764
|
+
player.grappleTarget = null;
|
|
765
|
+
}
|
|
766
|
+
return; // Override other controls during grapple
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// 🎮 SIDE-SCROLLER MOVEMENT - Simple and responsive!
|
|
770
|
+
// Debug - log key states
|
|
771
|
+
const leftPressed = isKeyDown('ArrowLeft');
|
|
772
|
+
const rightPressed = isKeyDown('ArrowRight');
|
|
773
|
+
const spacePressed = isKeyDown('Space');
|
|
774
|
+
|
|
775
|
+
if (leftPressed || rightPressed || spacePressed) {
|
|
776
|
+
console.log('⌨️ KEYS:', 'Left:', leftPressed, 'Right:', rightPressed, 'Space:', spacePressed);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Horizontal movement - LEFT/RIGHT arrows
|
|
780
|
+
if (leftPressed) {
|
|
781
|
+
player.vel.x = -moveSpeed * dashMultiplier;
|
|
782
|
+
player.yaw = Math.PI; // Face left
|
|
783
|
+
console.log('⬅️ MOVING LEFT at speed', player.vel.x);
|
|
784
|
+
} else if (rightPressed) {
|
|
785
|
+
player.vel.x = moveSpeed * dashMultiplier;
|
|
786
|
+
player.yaw = 0; // Face right
|
|
787
|
+
console.log('➡️ MOVING RIGHT at speed', player.vel.x);
|
|
788
|
+
} else {
|
|
789
|
+
player.vel.x *= 0.8; // Friction - stop quickly
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Z-axis movement locked for true side-scroller feel
|
|
793
|
+
// Player stays on single plane like Strider
|
|
794
|
+
player.vel.z *= 0.9; // Dampen any z-movement
|
|
795
|
+
|
|
796
|
+
// Down arrow = crouch/slide
|
|
797
|
+
if (isKeyDown('ArrowDown') && player.grounded && !player.sliding) {
|
|
798
|
+
player.sliding = true;
|
|
799
|
+
player.slideTime = 0.5;
|
|
800
|
+
player.stamina -= 5;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Jump - Space or Z
|
|
804
|
+
if ((isKeyPressed('Space') || isKeyPressed('KeyZ')) && player.grounded) {
|
|
805
|
+
player.vel.y = 18; // Jump power
|
|
806
|
+
player.grounded = false;
|
|
807
|
+
player.doubleJumpAvailable = true;
|
|
808
|
+
} else if (
|
|
809
|
+
(isKeyPressed('Space') || isKeyPressed('KeyZ')) &&
|
|
810
|
+
player.doubleJumpAvailable &&
|
|
811
|
+
player.stamina >= 15
|
|
812
|
+
) {
|
|
813
|
+
// Double jump
|
|
814
|
+
player.vel.y = 16;
|
|
815
|
+
player.doubleJumpAvailable = false;
|
|
816
|
+
player.stamina -= 15;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Dash - X key
|
|
820
|
+
if (isKeyPressed('KeyX') && player.stamina >= 20) {
|
|
821
|
+
player.isDashing = true;
|
|
822
|
+
player.dashTime = 0.3;
|
|
823
|
+
player.stamina -= 20;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Attack - Z key
|
|
827
|
+
if (isKeyPressed('KeyZ') && player.attackTime <= 0) {
|
|
828
|
+
performAttack();
|
|
829
|
+
player.attackTime = 0.4;
|
|
830
|
+
player.attacking = true;
|
|
831
|
+
} else if (player.attackTime <= 0) {
|
|
832
|
+
player.attacking = false;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Shuriken - C key
|
|
836
|
+
if (isKeyPressed('KeyC') && player.shurikenCount > 0 && player.stamina >= 5) {
|
|
837
|
+
throwShuriken();
|
|
838
|
+
player.shurikenCount--;
|
|
839
|
+
player.stamina -= 5;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Grapple - G key
|
|
843
|
+
if (isKeyPressed('KeyG')) {
|
|
844
|
+
const nearestGrapple = findNearestGrapplePoint();
|
|
845
|
+
if (nearestGrapple && !player.grappling) {
|
|
846
|
+
player.grappling = true;
|
|
847
|
+
player.grappleTarget = nearestGrapple;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function updatePlayer(dt) {
|
|
853
|
+
// Update combo timer
|
|
854
|
+
if (comboTimer > 0) {
|
|
855
|
+
comboTimer -= dt;
|
|
856
|
+
if (comboTimer <= 0) {
|
|
857
|
+
combo = 0;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Check for wall running
|
|
862
|
+
player.wallRunning = false;
|
|
863
|
+
if (!player.grounded && Math.abs(player.vel.x) > 5) {
|
|
864
|
+
// Check if near any wall structure
|
|
865
|
+
for (let i = 0; i < 8; i++) {
|
|
866
|
+
const wallX = (i - 4) * 15;
|
|
867
|
+
const wallZ = i % 2 === 0 ? -20 : -30;
|
|
868
|
+
|
|
869
|
+
const distToWall = Math.sqrt((player.pos.x - wallX) ** 2 + (player.pos.z - wallZ) ** 2);
|
|
870
|
+
if (distToWall < 3) {
|
|
871
|
+
player.wallRunning = true;
|
|
872
|
+
player.wallRunTime += dt;
|
|
873
|
+
|
|
874
|
+
// Wall running slows vertical fall and provides slight upward boost
|
|
875
|
+
player.vel.y = Math.max(player.vel.y, -5);
|
|
876
|
+
if (player.wallRunTime < 10) {
|
|
877
|
+
// max wall run time
|
|
878
|
+
player.vel.y += 3 * dt; // Slight upward boost
|
|
879
|
+
}
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Reset wall run time if not wall running
|
|
886
|
+
if (!player.wallRunning) {
|
|
887
|
+
player.wallRunTime = 0;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Apply gravity (reduced during wall run)
|
|
891
|
+
const gravityMultiplier = player.wallRunning ? 0.3 : 1.0;
|
|
892
|
+
player.vel.y -= 25 * gravityMultiplier * dt;
|
|
893
|
+
|
|
894
|
+
// Update position
|
|
895
|
+
player.pos.x += player.vel.x * dt;
|
|
896
|
+
player.pos.y += player.vel.y * dt;
|
|
897
|
+
player.pos.z += player.vel.z * dt;
|
|
898
|
+
|
|
899
|
+
// Platform collision detection
|
|
900
|
+
player.grounded = false;
|
|
901
|
+
|
|
902
|
+
for (const platform of platforms) {
|
|
903
|
+
// Simple AABB collision
|
|
904
|
+
if (
|
|
905
|
+
player.pos.x > platform.x - platform.width / 2 &&
|
|
906
|
+
player.pos.x < platform.x + platform.width / 2 &&
|
|
907
|
+
player.pos.z > platform.z - platform.depth / 2 &&
|
|
908
|
+
player.pos.z < platform.z + platform.depth / 2
|
|
909
|
+
) {
|
|
910
|
+
if (
|
|
911
|
+
player.pos.y <= platform.y + platform.height &&
|
|
912
|
+
player.pos.y + player.vel.y * dt > platform.y + platform.height
|
|
913
|
+
) {
|
|
914
|
+
player.pos.y = platform.y + platform.height;
|
|
915
|
+
player.vel.y = 0;
|
|
916
|
+
player.grounded = true;
|
|
917
|
+
player.doubleJumpAvailable = true;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// World boundaries
|
|
923
|
+
if (player.pos.x < -50) player.pos.x = -50;
|
|
924
|
+
if (player.pos.x > 50) player.pos.x = 50;
|
|
925
|
+
if (player.pos.z < -30) player.pos.z = -30;
|
|
926
|
+
if (player.pos.z > 30) player.pos.z = 30;
|
|
927
|
+
|
|
928
|
+
// Fall reset
|
|
929
|
+
if (player.pos.y < -10) {
|
|
930
|
+
player.pos.x = 0;
|
|
931
|
+
player.pos.y = 2;
|
|
932
|
+
player.pos.z = 0;
|
|
933
|
+
player.vel.x = 0;
|
|
934
|
+
player.vel.y = 0;
|
|
935
|
+
player.vel.z = 0;
|
|
936
|
+
player.health -= 20;
|
|
937
|
+
combo = 0;
|
|
938
|
+
comboTimer = 0;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Update ninja meshes
|
|
942
|
+
updatePlayerMeshes();
|
|
943
|
+
|
|
944
|
+
// Regenerate stamina
|
|
945
|
+
if (player.stamina < player.maxStamina) {
|
|
946
|
+
player.stamina += 25 * dt;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function updatePlayerMeshes() {
|
|
951
|
+
// Safety check
|
|
952
|
+
if (!playerKnight || !playerKnight.body) {
|
|
953
|
+
console.log('⚠️ Player meshes missing!');
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Calculate facing direction for rotation
|
|
958
|
+
const facingDir = player.yaw;
|
|
959
|
+
|
|
960
|
+
// BODY - main torso (NO AURA - keeps ninja visible!)
|
|
961
|
+
if (playerKnight.body) {
|
|
962
|
+
setPosition(playerKnight.body, player.pos.x, player.pos.y, player.pos.z);
|
|
963
|
+
setRotation(playerKnight.body, 0, facingDir, 0);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// CHEST ARMOR - cyan plate
|
|
967
|
+
if (playerKnight.chest) {
|
|
968
|
+
const chestZ = player.pos.z + Math.sin(facingDir) * 0.46;
|
|
969
|
+
setPosition(playerKnight.chest, player.pos.x, player.pos.y + 0.4, chestZ);
|
|
970
|
+
setRotation(playerKnight.chest, 0, facingDir, 0);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// HEAD - helmet
|
|
974
|
+
if (playerKnight.head) {
|
|
975
|
+
setPosition(playerKnight.head, player.pos.x, player.pos.y + 1.5, player.pos.z);
|
|
976
|
+
setRotation(playerKnight.head, 0, facingDir, 0);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// VISOR - cyan glowing eyes
|
|
980
|
+
if (playerKnight.visor) {
|
|
981
|
+
const visorZ = player.pos.z + Math.sin(facingDir) * 0.36;
|
|
982
|
+
setPosition(playerKnight.visor, player.pos.x, player.pos.y + 1.5, visorZ);
|
|
983
|
+
setRotation(playerKnight.visor, 0, facingDir, 0);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// HEADBAND - red energy ribbon flowing behind
|
|
987
|
+
if (playerKnight.headband) {
|
|
988
|
+
const headbandZ = player.pos.z - Math.sin(facingDir) * 0.4;
|
|
989
|
+
const flowOffset = Math.sin(gameTime * 5) * 0.1;
|
|
990
|
+
setPosition(playerKnight.headband, player.pos.x, player.pos.y + 1.7 + flowOffset, headbandZ);
|
|
991
|
+
setRotation(playerKnight.headband, 0, facingDir, Math.sin(gameTime * 3) * 0.1);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// KATANA - purple energy blade on back
|
|
995
|
+
if (playerKnight.katana) {
|
|
996
|
+
const katanaX = player.pos.x - Math.cos(facingDir) * 0.5;
|
|
997
|
+
const katanaZ = player.pos.z - Math.sin(facingDir) * 0.4;
|
|
998
|
+
setPosition(playerKnight.katana, katanaX, player.pos.y + 0.9, katanaZ);
|
|
999
|
+
setRotation(playerKnight.katana, 0, facingDir, -0.4);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// KATANA HANDLE
|
|
1003
|
+
if (playerKnight.katanaHandle) {
|
|
1004
|
+
const handleX = player.pos.x - Math.cos(facingDir) * 0.5;
|
|
1005
|
+
const handleZ = player.pos.z - Math.sin(facingDir) * 0.4;
|
|
1006
|
+
setPosition(playerKnight.katanaHandle, handleX, player.pos.y - 0.3, handleZ);
|
|
1007
|
+
setRotation(playerKnight.katanaHandle, 0, facingDir, -0.4);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// SCARF - flowing cyan energy trail
|
|
1011
|
+
if (playerKnight.scarf) {
|
|
1012
|
+
const scarfX = player.pos.x - Math.cos(facingDir) * 0.5;
|
|
1013
|
+
const scarfZ = player.pos.z - Math.sin(facingDir) * 0.6;
|
|
1014
|
+
const flowY = Math.sin(gameTime * 4) * 0.15;
|
|
1015
|
+
setPosition(playerKnight.scarf, scarfX, player.pos.y + 1 + flowY, scarfZ);
|
|
1016
|
+
setRotation(playerKnight.scarf, 0, facingDir, Math.sin(gameTime * 2) * 0.15);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ARMS - with running animation
|
|
1020
|
+
const armSwing = Math.abs(player.vel.x) > 1 ? Math.sin(gameTime * 12) * 0.4 : 0;
|
|
1021
|
+
if (playerKnight.leftArm) {
|
|
1022
|
+
setPosition(playerKnight.leftArm, player.pos.x - 0.7, player.pos.y + 0.2, player.pos.z);
|
|
1023
|
+
setRotation(playerKnight.leftArm, armSwing, facingDir, 0);
|
|
1024
|
+
}
|
|
1025
|
+
if (playerKnight.rightArm) {
|
|
1026
|
+
setPosition(playerKnight.rightArm, player.pos.x + 0.7, player.pos.y + 0.2, player.pos.z);
|
|
1027
|
+
setRotation(playerKnight.rightArm, -armSwing, facingDir, 0);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// GLOWING ARM GUARDS
|
|
1031
|
+
if (playerKnight.leftGuard) {
|
|
1032
|
+
setPosition(playerKnight.leftGuard, player.pos.x - 0.7, player.pos.y - 0.3, player.pos.z);
|
|
1033
|
+
}
|
|
1034
|
+
if (playerKnight.rightGuard) {
|
|
1035
|
+
setPosition(playerKnight.rightGuard, player.pos.x + 0.7, player.pos.y - 0.3, player.pos.z);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// LEGS - with running animation
|
|
1039
|
+
const legSwing = Math.abs(player.vel.x) > 1 ? Math.sin(gameTime * 14) * 0.3 : 0;
|
|
1040
|
+
if (playerKnight.leftLeg) {
|
|
1041
|
+
setPosition(playerKnight.leftLeg, player.pos.x - 0.4, player.pos.y - 1.3, player.pos.z);
|
|
1042
|
+
setRotation(playerKnight.leftLeg, -legSwing, facingDir, 0);
|
|
1043
|
+
}
|
|
1044
|
+
if (playerKnight.rightLeg) {
|
|
1045
|
+
setPosition(playerKnight.rightLeg, player.pos.x + 0.4, player.pos.y - 1.3, player.pos.z);
|
|
1046
|
+
setRotation(playerKnight.rightLeg, legSwing, facingDir, 0);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// KNEE GUARDS - cyan glow
|
|
1050
|
+
if (playerKnight.leftKnee) {
|
|
1051
|
+
setPosition(playerKnight.leftKnee, player.pos.x - 0.4, player.pos.y - 1, player.pos.z);
|
|
1052
|
+
}
|
|
1053
|
+
if (playerKnight.rightKnee) {
|
|
1054
|
+
setPosition(playerKnight.rightKnee, player.pos.x + 0.4, player.pos.y - 1, player.pos.z);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function performAttack() {
|
|
1059
|
+
// Create katana slash effect - purple arc
|
|
1060
|
+
const slashAngle = player.yaw; // Use yaw for direction
|
|
1061
|
+
const slashDir = Math.cos(player.yaw);
|
|
1062
|
+
const slashX = player.pos.x + slashDir * 1.5;
|
|
1063
|
+
|
|
1064
|
+
// Multiple slash particles for arc effect
|
|
1065
|
+
for (let i = 0; i < 8; i++) {
|
|
1066
|
+
const angle = slashAngle + (i - 4) * 0.3;
|
|
1067
|
+
const offset = 1.5;
|
|
1068
|
+
particles.push({
|
|
1069
|
+
mesh: createSphere(0.15, 0xaa44ff, [
|
|
1070
|
+
slashX + Math.cos(angle) * offset,
|
|
1071
|
+
player.pos.y + 0.5 + Math.sin(i * 0.5) * 0.5,
|
|
1072
|
+
player.pos.z,
|
|
1073
|
+
]),
|
|
1074
|
+
vx: Math.cos(angle) * 3,
|
|
1075
|
+
vy: Math.sin(i * 0.5) * 2,
|
|
1076
|
+
vz: 0,
|
|
1077
|
+
life: 0.3,
|
|
1078
|
+
maxLife: 0.3,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Check for enemy hits
|
|
1083
|
+
enemies.forEach(enemy => {
|
|
1084
|
+
const dx = enemy.x - player.pos.x;
|
|
1085
|
+
const dy = enemy.y - player.pos.y;
|
|
1086
|
+
const dz = enemy.z - player.pos.z;
|
|
1087
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1088
|
+
|
|
1089
|
+
if (distance < 3) {
|
|
1090
|
+
// Combo system - more damage with higher combo
|
|
1091
|
+
const damage = 2 + Math.floor(combo / 5);
|
|
1092
|
+
enemy.health -= damage;
|
|
1093
|
+
|
|
1094
|
+
// Award combo
|
|
1095
|
+
combo++;
|
|
1096
|
+
comboTimer = 2.0;
|
|
1097
|
+
|
|
1098
|
+
createHitParticles(enemy.x, enemy.y, enemy.z);
|
|
1099
|
+
|
|
1100
|
+
if (enemy.health <= 0) {
|
|
1101
|
+
destroyMesh(enemy.mesh);
|
|
1102
|
+
enemy.dead = true;
|
|
1103
|
+
score += 100 * (1 + Math.floor(combo / 10)); // Bonus score for combos
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function updateEnemies(dt) {
|
|
1110
|
+
enemies.forEach(enemy => {
|
|
1111
|
+
if (enemy.dead) return;
|
|
1112
|
+
|
|
1113
|
+
enemy.attackCooldown -= dt;
|
|
1114
|
+
|
|
1115
|
+
switch (enemy.type) {
|
|
1116
|
+
case 'patrol':
|
|
1117
|
+
// Patrol back and forth
|
|
1118
|
+
enemy.x += enemy.vx * dt;
|
|
1119
|
+
|
|
1120
|
+
if (Math.abs(enemy.x - enemy.originalX) > enemy.patrolRange) {
|
|
1121
|
+
enemy.vx *= -1;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
|
|
1125
|
+
|
|
1126
|
+
// Attack player if close
|
|
1127
|
+
{
|
|
1128
|
+
const distToPlayer = Math.sqrt(
|
|
1129
|
+
Math.pow(enemy.x - player.x, 2) + Math.pow(enemy.z - player.z, 2)
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
if (distToPlayer < 4 && enemy.attackCooldown <= 0) {
|
|
1133
|
+
// Simple attack - damage player
|
|
1134
|
+
if (distToPlayer < 2) {
|
|
1135
|
+
player.health -= 10;
|
|
1136
|
+
enemy.attackCooldown = 2;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
break;
|
|
1141
|
+
|
|
1142
|
+
case 'flyer':
|
|
1143
|
+
// Orbit around center point
|
|
1144
|
+
enemy.orbitAngle += dt * 2;
|
|
1145
|
+
enemy.x = enemy.orbitCenter.x + Math.cos(enemy.orbitAngle) * enemy.orbitRadius;
|
|
1146
|
+
enemy.z = enemy.orbitCenter.z + Math.sin(enemy.orbitAngle) * enemy.orbitRadius;
|
|
1147
|
+
enemy.y = enemy.orbitCenter.y + Math.sin(enemy.orbitAngle * 2) * 2;
|
|
1148
|
+
|
|
1149
|
+
setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
|
|
1150
|
+
rotateMesh(enemy.mesh, 0, dt * 3, 0);
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
// Remove dead enemies
|
|
1156
|
+
enemies = enemies.filter(enemy => !enemy.dead);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function updateCoins(dt) {
|
|
1160
|
+
coins.forEach(coin => {
|
|
1161
|
+
if (coin.collected) return;
|
|
1162
|
+
|
|
1163
|
+
// Animate coins
|
|
1164
|
+
coin.rotationY += dt * 4;
|
|
1165
|
+
coin.bobOffset += dt * 3;
|
|
1166
|
+
const newY = coin.y + Math.sin(coin.bobOffset) * 0.3;
|
|
1167
|
+
|
|
1168
|
+
setPosition(coin.mesh, coin.x, newY, coin.z);
|
|
1169
|
+
setRotation(coin.mesh, 0, coin.rotationY, 0);
|
|
1170
|
+
|
|
1171
|
+
// Check collection
|
|
1172
|
+
const distance = Math.sqrt(
|
|
1173
|
+
Math.pow(coin.x - player.x, 2) + Math.pow(newY - player.y, 2) + Math.pow(coin.z - player.z, 2)
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
if (distance < 1.5) {
|
|
1177
|
+
coin.collected = true;
|
|
1178
|
+
destroyMesh(coin.mesh);
|
|
1179
|
+
player.coins += 10;
|
|
1180
|
+
score += 50;
|
|
1181
|
+
createCoinParticles(coin.x, coin.y, coin.z);
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function updateParticles(dt) {
|
|
1187
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
1188
|
+
const particle = particles[i];
|
|
1189
|
+
if (!particle || !particle.mesh) {
|
|
1190
|
+
particles.splice(i, 1);
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
particle.life -= dt;
|
|
1195
|
+
|
|
1196
|
+
const pos = getPosition(particle.mesh);
|
|
1197
|
+
if (!pos) {
|
|
1198
|
+
// Mesh doesn't exist anymore, remove particle
|
|
1199
|
+
particles.splice(i, 1);
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
pos[0] += particle.vx * dt;
|
|
1204
|
+
pos[1] += particle.vy * dt;
|
|
1205
|
+
pos[2] += particle.vz * dt;
|
|
1206
|
+
|
|
1207
|
+
// Shuriken don't have gravity and spin
|
|
1208
|
+
if (particle.isShuriken) {
|
|
1209
|
+
particle.rotation += dt * 20;
|
|
1210
|
+
setRotation(particle.mesh, 0, 0, particle.rotation);
|
|
1211
|
+
} else {
|
|
1212
|
+
particle.vy -= 10 * dt; // Gravity for normal particles
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
setPosition(particle.mesh, pos[0], pos[1], pos[2]);
|
|
1216
|
+
|
|
1217
|
+
if (!particle.isShuriken) {
|
|
1218
|
+
const scale = particle.life / particle.maxLife;
|
|
1219
|
+
setScale(particle.mesh, scale, scale, scale);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (particle.life <= 0) {
|
|
1223
|
+
destroyMesh(particle.mesh);
|
|
1224
|
+
particles.splice(i, 1);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function updateCamera(_dt) {
|
|
1230
|
+
// 📷 PERFECT SIDE-SCROLLER CAMERA (like Strider, Shinobi, Ninja Gaiden)
|
|
1231
|
+
// Camera follows player smoothly - optimal for gameplay!
|
|
1232
|
+
|
|
1233
|
+
camera.targetX = player.pos.x + 3; // Look slightly ahead
|
|
1234
|
+
camera.targetY = 5; // Center on action
|
|
1235
|
+
camera.targetZ = 0; // LOCKED - straight ahead view
|
|
1236
|
+
|
|
1237
|
+
// Smooth horizontal following only
|
|
1238
|
+
camera.x += (player.pos.x - camera.x) * 0.1;
|
|
1239
|
+
camera.y = 6; // Perfect eye-level view!
|
|
1240
|
+
camera.z = 18; // Close enough to see details, far enough to see platforms!
|
|
1241
|
+
|
|
1242
|
+
setCameraPosition(camera.x, camera.y, camera.z);
|
|
1243
|
+
setCameraTarget(camera.targetX, camera.targetY, camera.targetZ);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function checkCollisions(dt) {
|
|
1247
|
+
// Already handled in updatePlayer and updateEnemies
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function updateGameLogic(dt) {
|
|
1251
|
+
// Check for level completion
|
|
1252
|
+
const remainingCoins = coins.filter(coin => !coin.collected).length;
|
|
1253
|
+
if (remainingCoins === 0) {
|
|
1254
|
+
level++;
|
|
1255
|
+
score += 1000;
|
|
1256
|
+
// Could spawn new level here
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Game over check
|
|
1260
|
+
if (player.health <= 0) {
|
|
1261
|
+
gameState = 'gameOver';
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function createJumpParticles() {
|
|
1266
|
+
for (let i = 0; i < 5; i++) {
|
|
1267
|
+
const particle = {
|
|
1268
|
+
mesh: createSphere(0.1, 0x88ccff, [player.x, player.y - 0.5, player.z]),
|
|
1269
|
+
vx: (Math.random() - 0.5) * 4,
|
|
1270
|
+
vy: Math.random() * 3,
|
|
1271
|
+
vz: (Math.random() - 0.5) * 4,
|
|
1272
|
+
life: 1,
|
|
1273
|
+
maxLife: 1,
|
|
1274
|
+
};
|
|
1275
|
+
particles.push(particle);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function createDoubleJumpParticles() {
|
|
1280
|
+
for (let i = 0; i < 8; i++) {
|
|
1281
|
+
const particle = {
|
|
1282
|
+
mesh: createSphere(0.15, 0xffff44, [player.x, player.y, player.z]),
|
|
1283
|
+
vx: (Math.random() - 0.5) * 6,
|
|
1284
|
+
vy: (Math.random() - 0.5) * 6,
|
|
1285
|
+
vz: (Math.random() - 0.5) * 6,
|
|
1286
|
+
life: 1.5,
|
|
1287
|
+
maxLife: 1.5,
|
|
1288
|
+
};
|
|
1289
|
+
particles.push(particle);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function createHitParticles(x, y, z) {
|
|
1294
|
+
for (let i = 0; i < 10; i++) {
|
|
1295
|
+
const particle = {
|
|
1296
|
+
mesh: createSphere(0.08, 0xff4444, [x, y, z]),
|
|
1297
|
+
vx: (Math.random() - 0.5) * 8,
|
|
1298
|
+
vy: Math.random() * 6,
|
|
1299
|
+
vz: (Math.random() - 0.5) * 8,
|
|
1300
|
+
life: 0.8,
|
|
1301
|
+
maxLife: 0.8,
|
|
1302
|
+
};
|
|
1303
|
+
particles.push(particle);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function createCoinParticles(x, y, z) {
|
|
1308
|
+
for (let i = 0; i < 6; i++) {
|
|
1309
|
+
const particle = {
|
|
1310
|
+
mesh: createSphere(0.05, 0xffdd00, [x, y, z]),
|
|
1311
|
+
vx: (Math.random() - 0.5) * 5,
|
|
1312
|
+
vy: Math.random() * 4 + 2,
|
|
1313
|
+
vz: (Math.random() - 0.5) * 5,
|
|
1314
|
+
life: 1.2,
|
|
1315
|
+
maxLife: 1.2,
|
|
1316
|
+
};
|
|
1317
|
+
particles.push(particle);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function createDashParticles() {
|
|
1322
|
+
for (let i = 0; i < 10; i++) {
|
|
1323
|
+
const particle = {
|
|
1324
|
+
mesh: createSphere(0.12, 0xaa44ff, [player.x, player.y + 0.5, player.z]),
|
|
1325
|
+
vx: (Math.random() - 0.5) * 3 - (player.facingRight ? 5 : -5),
|
|
1326
|
+
vy: (Math.random() - 0.5) * 2,
|
|
1327
|
+
vz: (Math.random() - 0.5) * 3,
|
|
1328
|
+
life: 0.5,
|
|
1329
|
+
maxLife: 0.5,
|
|
1330
|
+
};
|
|
1331
|
+
particles.push(particle);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function createAirDashParticles() {
|
|
1336
|
+
for (let i = 0; i < 15; i++) {
|
|
1337
|
+
const particle = {
|
|
1338
|
+
mesh: createSphere(0.15, 0x00ffff, [player.x, player.y, player.z]),
|
|
1339
|
+
vx: (Math.random() - 0.5) * 8,
|
|
1340
|
+
vy: (Math.random() - 0.5) * 8,
|
|
1341
|
+
vz: (Math.random() - 0.5) * 8,
|
|
1342
|
+
life: 0.8,
|
|
1343
|
+
maxLife: 0.8,
|
|
1344
|
+
};
|
|
1345
|
+
particles.push(particle);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function createSlideParticles() {
|
|
1350
|
+
for (let i = 0; i < 8; i++) {
|
|
1351
|
+
const particle = {
|
|
1352
|
+
mesh: createSphere(0.08, 0x8844aa, [player.x, player.y - 0.3, player.z]),
|
|
1353
|
+
vx: (Math.random() - 0.5) * 4 - (player.facingRight ? 3 : -3),
|
|
1354
|
+
vy: Math.random() * 2,
|
|
1355
|
+
vz: (Math.random() - 0.5) * 4,
|
|
1356
|
+
life: 0.6,
|
|
1357
|
+
maxLife: 0.6,
|
|
1358
|
+
};
|
|
1359
|
+
particles.push(particle);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function createGrappleEffect() {
|
|
1364
|
+
for (let i = 0; i < 12; i++) {
|
|
1365
|
+
const t = i / 12;
|
|
1366
|
+
const x = player.x + (player.grappleTarget.x - player.x) * t;
|
|
1367
|
+
const y = player.y + (player.grappleTarget.y - player.y) * t;
|
|
1368
|
+
const z = player.z + (player.grappleTarget.z - player.z) * t;
|
|
1369
|
+
|
|
1370
|
+
const particle = {
|
|
1371
|
+
mesh: createSphere(0.1, 0x00ffff, [x, y, z]),
|
|
1372
|
+
vx: 0,
|
|
1373
|
+
vy: 0,
|
|
1374
|
+
vz: 0,
|
|
1375
|
+
life: 0.4,
|
|
1376
|
+
maxLife: 0.4,
|
|
1377
|
+
};
|
|
1378
|
+
particles.push(particle);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function throwShuriken() {
|
|
1383
|
+
const shuriken = {
|
|
1384
|
+
mesh: createCube(0.3, 0xcccccc, [
|
|
1385
|
+
player.x + (player.facingRight ? 1 : -1),
|
|
1386
|
+
player.y + 0.5,
|
|
1387
|
+
player.z,
|
|
1388
|
+
]),
|
|
1389
|
+
vx: (player.facingRight ? 1 : -1) * 25,
|
|
1390
|
+
vy: 0,
|
|
1391
|
+
vz: 0,
|
|
1392
|
+
life: 2,
|
|
1393
|
+
maxLife: 2,
|
|
1394
|
+
rotation: 0,
|
|
1395
|
+
isShuriken: true,
|
|
1396
|
+
};
|
|
1397
|
+
setScale(shuriken.mesh, 0.8, 0.1, 0.8);
|
|
1398
|
+
particles.push(shuriken);
|
|
1399
|
+
|
|
1400
|
+
// Check for enemy hits
|
|
1401
|
+
setTimeout(() => {
|
|
1402
|
+
enemies.forEach(enemy => {
|
|
1403
|
+
const dx = enemy.x - (player.x + (player.facingRight ? 5 : -5));
|
|
1404
|
+
const dy = enemy.y - player.y;
|
|
1405
|
+
const dz = enemy.z - player.z;
|
|
1406
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1407
|
+
|
|
1408
|
+
if (distance < 3 && !enemy.dead) {
|
|
1409
|
+
enemy.health -= 1;
|
|
1410
|
+
createHitParticles(enemy.x, enemy.y, enemy.z);
|
|
1411
|
+
combo++;
|
|
1412
|
+
comboTimer = 2;
|
|
1413
|
+
|
|
1414
|
+
if (enemy.health <= 0) {
|
|
1415
|
+
destroyMesh(enemy.mesh);
|
|
1416
|
+
enemy.dead = true;
|
|
1417
|
+
score += 150;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
}, 100);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function findNearestGrapplePoint() {
|
|
1425
|
+
let nearest = null;
|
|
1426
|
+
let minDist = 20; // Max grapple range
|
|
1427
|
+
|
|
1428
|
+
player.grapplePoints.forEach(point => {
|
|
1429
|
+
if (!point.active) return;
|
|
1430
|
+
|
|
1431
|
+
const dx = point.x - player.x;
|
|
1432
|
+
const dy = point.y - player.y;
|
|
1433
|
+
const dz = point.z - player.z;
|
|
1434
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1435
|
+
|
|
1436
|
+
if (dist < minDist) {
|
|
1437
|
+
minDist = dist;
|
|
1438
|
+
nearest = point;
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
return nearest;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function drawUI() {
|
|
1446
|
+
// Ninja HUD Background - dark with purple/cyan accents
|
|
1447
|
+
rect(16, 16, 420, 100, rgba8(10, 10, 26, 200), true);
|
|
1448
|
+
rect(16, 16, 420, 100, rgba8(136, 51, 170, 180), false);
|
|
1449
|
+
rect(17, 17, 418, 98, rgba8(0, 255, 255, 80), false);
|
|
1450
|
+
|
|
1451
|
+
// Score and Level
|
|
1452
|
+
print(`SCORE: ${score.toString().padStart(8, '0')}`, 24, 24, rgba8(0, 255, 255, 255));
|
|
1453
|
+
print(`LEVEL: ${level}`, 24, 40, rgba8(170, 68, 255, 255));
|
|
1454
|
+
print(`COINS: ${player.coins}`, 24, 56, rgba8(255, 215, 0, 255));
|
|
1455
|
+
|
|
1456
|
+
// Shuriken count
|
|
1457
|
+
print(`SHURIKEN: ${player.shuriken}`, 24, 72, rgba8(200, 200, 200, 255));
|
|
1458
|
+
|
|
1459
|
+
// Combo meter
|
|
1460
|
+
if (combo > 0) {
|
|
1461
|
+
print(`COMBO x${combo}`, 24, 88, rgba8(255, 100, 255, 255));
|
|
1462
|
+
const comboBarWidth = Math.floor((comboTimer / 2.0) * 80);
|
|
1463
|
+
rect(100, 90, 80, 6, rgba8(50, 20, 60, 255), true);
|
|
1464
|
+
rect(100, 90, comboBarWidth, 6, rgba8(255, 100, 255, 255), true);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Health bar - red with dark background
|
|
1468
|
+
print('HEALTH:', 220, 24, rgba8(255, 255, 255, 255));
|
|
1469
|
+
rect(285, 22, 120, 10, rgba8(50, 0, 0, 255), true);
|
|
1470
|
+
rect(285, 22, Math.floor((player.health / 100) * 120), 10, rgba8(255, 0, 80, 255), true);
|
|
1471
|
+
rect(285, 22, 120, 10, rgba8(255, 0, 80, 100), false);
|
|
1472
|
+
|
|
1473
|
+
// Energy bar - cyan with dark background
|
|
1474
|
+
print('ENERGY:', 220, 42, rgba8(255, 255, 255, 255));
|
|
1475
|
+
rect(285, 40, 120, 10, rgba8(0, 20, 40, 255), true);
|
|
1476
|
+
rect(285, 40, Math.floor((player.energy / 100) * 120), 10, rgba8(0, 255, 255, 255), true);
|
|
1477
|
+
rect(285, 40, 120, 10, rgba8(0, 255, 255, 100), false);
|
|
1478
|
+
|
|
1479
|
+
// Ability indicators
|
|
1480
|
+
print('ABILITIES:', 220, 60, rgba8(200, 200, 200, 255));
|
|
1481
|
+
|
|
1482
|
+
// Dash indicator
|
|
1483
|
+
const dashReady = player.dashCooldown <= 0 && player.energy >= 20;
|
|
1484
|
+
rect(285, 58, 24, 8, dashReady ? rgba8(170, 68, 255, 255) : rgba8(50, 20, 60, 255), true);
|
|
1485
|
+
print('DSH', 288, 60, rgba8(255, 255, 255, 255));
|
|
1486
|
+
|
|
1487
|
+
// Air Dash indicator
|
|
1488
|
+
const airDashReady = player.airDashAvailable && player.energy >= 25;
|
|
1489
|
+
rect(312, 58, 24, 8, airDashReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
|
|
1490
|
+
print('AIR', 315, 60, rgba8(255, 255, 255, 255));
|
|
1491
|
+
|
|
1492
|
+
// Grapple indicator
|
|
1493
|
+
const grappleReady = findNearestGrapplePoint() !== null;
|
|
1494
|
+
rect(339, 58, 24, 8, grappleReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
|
|
1495
|
+
print('GRP', 342, 60, rgba8(255, 255, 255, 255));
|
|
1496
|
+
|
|
1497
|
+
// Shuriken indicator
|
|
1498
|
+
const shurikenReady = player.shuriken > 0 && player.energy >= 5;
|
|
1499
|
+
rect(366, 58, 24, 8, shurikenReady ? rgba8(200, 200, 200, 255) : rgba8(40, 40, 40, 255), true);
|
|
1500
|
+
print('SHR', 369, 60, rgba8(255, 255, 255, 255));
|
|
1501
|
+
|
|
1502
|
+
// 3D Stats - smaller and in corner
|
|
1503
|
+
const stats = get3DStats();
|
|
1504
|
+
if (stats) {
|
|
1505
|
+
print(`${stats.meshes || 0}m`, 580, 24, rgba8(100, 100, 100, 200));
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Position info - debug mode
|
|
1509
|
+
// print(`POS: ${player.x.toFixed(1)}, ${player.y.toFixed(1)}, ${player.z.toFixed(1)}`, 220, 90, rgba8(100, 100, 100, 150));
|
|
1510
|
+
|
|
1511
|
+
// Controls hint
|
|
1512
|
+
print('X=DASH C=SHURIKEN G=GRAPPLE Z=ATTACK', 16, 340, rgba8(136, 51, 170, 200));
|
|
1513
|
+
|
|
1514
|
+
if (gameState === 'gameOver') {
|
|
1515
|
+
rect(0, 0, 640, 360, rgba8(0, 0, 0, 200), true);
|
|
1516
|
+
print('GAME OVER', 260, 150, rgba8(255, 50, 50, 255));
|
|
1517
|
+
print(`FINAL SCORE: ${score}`, 230, 180, rgba8(255, 255, 0, 255));
|
|
1518
|
+
print(`COINS COLLECTED: ${player.coins}`, 220, 200, rgba8(255, 215, 0, 255));
|
|
1519
|
+
print('PRESS R TO RESTART', 220, 240, rgba8(255, 255, 255, 255));
|
|
1520
|
+
|
|
1521
|
+
if (btnp(17)) {
|
|
1522
|
+
// R key
|
|
1523
|
+
// Reset game
|
|
1524
|
+
score = 0;
|
|
1525
|
+
level = 1;
|
|
1526
|
+
player.health = 100;
|
|
1527
|
+
player.energy = 100;
|
|
1528
|
+
player.coins = 0;
|
|
1529
|
+
player.x = 0;
|
|
1530
|
+
player.y = 2;
|
|
1531
|
+
player.z = 0;
|
|
1532
|
+
gameState = 'playing';
|
|
1533
|
+
clearScene();
|
|
1534
|
+
init();
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|