@myvillage/cli 2.6.1 → 2.7.0
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/package.json +1 -1
- package/src/utils/templates.js +178 -33
package/package.json
CHANGED
package/src/utils/templates.js
CHANGED
|
@@ -235,7 +235,7 @@ function generate2DGameIndexHtml(name, orientation) {
|
|
|
235
235
|
<html lang="en">
|
|
236
236
|
<head>
|
|
237
237
|
<meta charset="UTF-8" />
|
|
238
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
238
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
239
239
|
<!-- orientation: ${orientation} -->
|
|
240
240
|
<title>${name}</title>
|
|
241
241
|
<link rel="stylesheet" href="/src/styles.css" />
|
|
@@ -435,10 +435,19 @@ html, body {
|
|
|
435
435
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
/* The background runs edge to edge on a phone (behind the status bar and home
|
|
439
|
+
indicator); content stays inside the safe area. The extra top space keeps the
|
|
440
|
+
top-left corner clear for the host app's Exit button. The muni app injects
|
|
441
|
+
--mv-safe-* because Android web views don't report env() insets. */
|
|
438
442
|
body {
|
|
443
|
+
--safe-top: max(env(safe-area-inset-top, 0px), var(--mv-safe-top, 0px));
|
|
444
|
+
--safe-bottom: max(env(safe-area-inset-bottom, 0px), var(--mv-safe-bottom, 0px));
|
|
445
|
+
--safe-left: max(env(safe-area-inset-left, 0px), var(--mv-safe-left, 0px));
|
|
446
|
+
--safe-right: max(env(safe-area-inset-right, 0px), var(--mv-safe-right, 0px));
|
|
439
447
|
background: linear-gradient(160deg, var(--deep-green), var(--dark));
|
|
440
448
|
color: var(--cream);
|
|
441
449
|
display: flex;
|
|
450
|
+
padding: calc(var(--safe-top) + 44px) var(--safe-right) var(--safe-bottom) var(--safe-left);
|
|
442
451
|
-webkit-font-smoothing: antialiased;
|
|
443
452
|
}
|
|
444
453
|
|
|
@@ -851,10 +860,19 @@ html, body {
|
|
|
851
860
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
852
861
|
}
|
|
853
862
|
|
|
863
|
+
/* The background runs edge to edge on a phone (behind the status bar and home
|
|
864
|
+
indicator); content stays inside the safe area. The extra top space keeps the
|
|
865
|
+
top-left corner clear for the host app's Exit button. The muni app injects
|
|
866
|
+
--mv-safe-* because Android web views don't report env() insets. */
|
|
854
867
|
body {
|
|
868
|
+
--safe-top: max(env(safe-area-inset-top, 0px), var(--mv-safe-top, 0px));
|
|
869
|
+
--safe-bottom: max(env(safe-area-inset-bottom, 0px), var(--mv-safe-bottom, 0px));
|
|
870
|
+
--safe-left: max(env(safe-area-inset-left, 0px), var(--mv-safe-left, 0px));
|
|
871
|
+
--safe-right: max(env(safe-area-inset-right, 0px), var(--mv-safe-right, 0px));
|
|
855
872
|
background: linear-gradient(160deg, var(--deep-green), var(--dark));
|
|
856
873
|
color: var(--cream);
|
|
857
874
|
display: flex;
|
|
875
|
+
padding: calc(var(--safe-top) + 44px) var(--safe-right) var(--safe-bottom) var(--safe-left);
|
|
858
876
|
-webkit-font-smoothing: antialiased;
|
|
859
877
|
}
|
|
860
878
|
|
|
@@ -1085,11 +1103,11 @@ function generate3DGameAgents(name, type, orientation) {
|
|
|
1085
1103
|
? '9:16 (e.g. 1080x1920) for this portrait game'
|
|
1086
1104
|
: '16:9 (e.g. 1920x1080) for this landscape game';
|
|
1087
1105
|
const logicLine = isExploration
|
|
1088
|
-
? 'Holds the gameplay — player movement (via `input.getMovementVector()`), collectible spawning + collision, the follow
|
|
1106
|
+
? 'Holds the gameplay — player movement (via `input.getMovementVector()`), collectible spawning + collision, the drag-to-look follow camera, score, and win condition. It calls `input.enableJoystick()` and `input.enableLookControls()` in `start()` so phones get a move stick plus drag-to-look.'
|
|
1089
1107
|
: 'A blank starter — `update(delta)` is empty with hints. Build your mechanics here; the 3D scene, input, audio, UI, and game loop are already wired.';
|
|
1090
1108
|
const mobileLine = isExploration
|
|
1091
|
-
? 'This game already calls `this.game.input.enableJoystick()` in `GameLogic.start()
|
|
1092
|
-
: 'If
|
|
1109
|
+
? 'This game already calls `this.game.input.enableJoystick()` and `this.game.input.enableLookControls()` in `GameLogic.start()`: left thumb moves (WASD on desktop), dragging anywhere else turns the camera (mouse drag on desktop). Movement is relative to where the camera faces. Keep both whenever a character moves through the 3D world.'
|
|
1110
|
+
: 'If a character moves through the 3D world, give it BOTH controls in `GameLogic.start()`: `this.game.input.enableJoystick()` (on-screen move stick on touch devices, WASD on desktop; read with `getMovementVector()`) and `this.game.input.enableLookControls()` (drag to turn the camera; read with `getLookDelta()` each frame). Make movement relative to the camera\'s facing.';
|
|
1093
1111
|
const uiExtra = isExploration
|
|
1094
1112
|
? ' This game also adds `showMinimap(pos, items)` and `showInventory(items)`.'
|
|
1095
1113
|
: '';
|
|
@@ -1125,7 +1143,7 @@ function generate3DGameAgents(name, type, orientation) {
|
|
|
1125
1143
|
'## Engine API (reach it as `this.game` inside GameLogic)',
|
|
1126
1144
|
'',
|
|
1127
1145
|
'- `this.game.scene` — `.scene` (THREE.Scene), `.camera` (THREE.PerspectiveCamera), `.sampleObject`, `.update(delta)`.',
|
|
1128
|
-
'- `this.game.input` — `getMovementVector()` (returns `{ x, z }` from WASD/arrows or the joystick), `isKeyDown(code)`, `isKeyJustPressed(code)`, `enableJoystick()`, `
|
|
1146
|
+
'- `this.game.input` — `getMovementVector()` (returns `{ x, z }` from WASD/arrows or the joystick), `getLookDelta()` (drag since last frame, `{ x, y }` px), `isKeyDown(code)`, `isKeyJustPressed(code)`, `enableJoystick()`, `enableLookControls()`, `mouse`, `touches`. The loop calls `clearFrame()` for you.',
|
|
1129
1147
|
'- `this.game.audio` — `playSFX(name)`, `playMusic(name)`, `stopMusic()`, `setMusicVolume(v)`, `setSFXVolume(v)`.',
|
|
1130
1148
|
'- `this.game.ui` — `showMenu()`, `showHUD()`, `showPauseMenu()`, `showGameOver(result)`, `clearOverlay()`, `createElement(tag, styles)`.' + uiExtra,
|
|
1131
1149
|
'- `this.game` — `startGame()`, `pauseGame()`, `resumeGame()`, `endGame(result)`, `state`.',
|
|
@@ -1134,6 +1152,12 @@ function generate3DGameAgents(name, type, orientation) {
|
|
|
1134
1152
|
'',
|
|
1135
1153
|
mobileLine,
|
|
1136
1154
|
'',
|
|
1155
|
+
'## Screen layout (phones)',
|
|
1156
|
+
'',
|
|
1157
|
+
'- The game fills the whole screen edge to edge, including behind the status bar and home indicator. Never letterbox it or add bars; on a wide desktop window `index.html` frames it automatically.',
|
|
1158
|
+
'- Keep HUD inside the safe area with the CSS variables in `index.html`: top row at `var(--hud-top)`, and `var(--safe-bottom)` / `var(--safe-left)` / `var(--safe-right)` from the other edges.',
|
|
1159
|
+
'- Leave the **top-left corner** (`var(--exit-clear)` wide, below `var(--safe-top)`) empty: the host app draws its Exit button there. Put Pause and other top buttons on the right.',
|
|
1160
|
+
'',
|
|
1137
1161
|
'## The MyVillage game contract — DO NOT BREAK',
|
|
1138
1162
|
'',
|
|
1139
1163
|
'- `MyVillageGame.ready()` — called after assets load in `src/main.js`. Leave it.',
|
|
@@ -1839,31 +1863,49 @@ cover art with the submission. An admin sets the game's MVT reward at review tim
|
|
|
1839
1863
|
|
|
1840
1864
|
function generateIndexHtml(name, orientation = 'landscape') {
|
|
1841
1865
|
const isPortrait = orientation === 'portrait';
|
|
1842
|
-
// Portrait games
|
|
1843
|
-
//
|
|
1844
|
-
//
|
|
1866
|
+
// Portrait games fill the whole phone screen edge to edge (behind the status
|
|
1867
|
+
// bar and home indicator — the muni app draws a soft fade + Exit over the top
|
|
1868
|
+
// corner). Only on a wide screen (desktop web) is the game framed in a 9:16
|
|
1869
|
+
// stage so it doesn't stretch. Landscape always fills the viewport.
|
|
1845
1870
|
const containerStyle = isPortrait
|
|
1846
1871
|
? `#game-container {
|
|
1847
1872
|
position: relative;
|
|
1848
|
-
width:
|
|
1849
|
-
height:
|
|
1850
|
-
|
|
1873
|
+
width: 100vw;
|
|
1874
|
+
height: 100vh;
|
|
1875
|
+
height: 100dvh;
|
|
1851
1876
|
overflow: hidden;
|
|
1877
|
+
}
|
|
1878
|
+
@media (min-aspect-ratio: 3/4) {
|
|
1879
|
+
#game-container { width: min(100vw, calc(100dvh * 9 / 16)); margin: 0 auto; }
|
|
1852
1880
|
}`
|
|
1853
1881
|
: `#game-container { width: 100%; height: 100%; position: relative; overflow: hidden; }`;
|
|
1854
1882
|
return `<!DOCTYPE html>
|
|
1855
1883
|
<html lang="en">
|
|
1856
1884
|
<head>
|
|
1857
1885
|
<meta charset="UTF-8" />
|
|
1858
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
1886
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
1859
1887
|
<!-- orientation: ${orientation} -->
|
|
1860
1888
|
<title>${name}</title>
|
|
1861
1889
|
<style>
|
|
1890
|
+
/* Screen-edge spacing. On a phone the game runs under the status bar and
|
|
1891
|
+
home indicator; keep HUD inside these. The muni app also injects
|
|
1892
|
+
--mv-safe-* (Android web views don't report env() insets).
|
|
1893
|
+
--hud-top: first row of HUD buttons. The TOP-LEFT corner (--exit-clear
|
|
1894
|
+
wide, below --safe-top) belongs to the app's Exit button — keep it empty. */
|
|
1895
|
+
:root {
|
|
1896
|
+
--safe-top: max(env(safe-area-inset-top, 0px), var(--mv-safe-top, 0px));
|
|
1897
|
+
--safe-bottom: max(env(safe-area-inset-bottom, 0px), var(--mv-safe-bottom, 0px));
|
|
1898
|
+
--safe-left: max(env(safe-area-inset-left, 0px), var(--mv-safe-left, 0px));
|
|
1899
|
+
--safe-right: max(env(safe-area-inset-right, 0px), var(--mv-safe-right, 0px));
|
|
1900
|
+
--hud-top: calc(var(--safe-top) + 12px);
|
|
1901
|
+
--exit-clear: 64px;
|
|
1902
|
+
}
|
|
1862
1903
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1863
1904
|
html, body { width: 100%; height: 100%; overflow: hidden; }
|
|
1864
1905
|
body { background: ${BRAND.darkBrown}; display: flex; align-items: center; justify-content: center; }
|
|
1865
1906
|
${containerStyle}
|
|
1866
|
-
|
|
1907
|
+
/* touch-action: none — drags on the game steer the camera, never scroll the page. */
|
|
1908
|
+
canvas { display: block; touch-action: none; }
|
|
1867
1909
|
|
|
1868
1910
|
/* Loading screen */
|
|
1869
1911
|
#loading-screen {
|
|
@@ -2040,6 +2082,8 @@ class Game {
|
|
|
2040
2082
|
this.gameLogic.update(delta);
|
|
2041
2083
|
this.scene.update(delta);
|
|
2042
2084
|
}
|
|
2085
|
+
// One-shot input (just-pressed keys, look drag) lasts exactly one frame.
|
|
2086
|
+
this.input.clearFrame();
|
|
2043
2087
|
|
|
2044
2088
|
this.renderer.render(this.scene.scene, this.scene.camera);
|
|
2045
2089
|
}
|
|
@@ -2247,6 +2291,11 @@ export class InputManager {
|
|
|
2247
2291
|
// movement-based game calls enableJoystick(), and on desktop.
|
|
2248
2292
|
this.joystick = null;
|
|
2249
2293
|
|
|
2294
|
+
// Look-around drag, in pixels since the last frame. Stays inactive until a
|
|
2295
|
+
// 3D game calls enableLookControls(); read it with getLookDelta().
|
|
2296
|
+
this.lookEnabled = false;
|
|
2297
|
+
this.lookDelta = { x: 0, y: 0 };
|
|
2298
|
+
|
|
2250
2299
|
this.setupEventListeners();
|
|
2251
2300
|
}
|
|
2252
2301
|
|
|
@@ -2327,8 +2376,8 @@ export class InputManager {
|
|
|
2327
2376
|
const base = document.createElement('div');
|
|
2328
2377
|
Object.assign(base.style, {
|
|
2329
2378
|
position: 'absolute',
|
|
2330
|
-
left: 'calc(24px +
|
|
2331
|
-
bottom: 'calc(24px +
|
|
2379
|
+
left: 'calc(24px + var(--safe-left, 0px))',
|
|
2380
|
+
bottom: 'calc(24px + var(--safe-bottom, 0px))',
|
|
2332
2381
|
width: '120px',
|
|
2333
2382
|
height: '120px',
|
|
2334
2383
|
borderRadius: '50%',
|
|
@@ -2414,6 +2463,66 @@ export class InputManager {
|
|
|
2414
2463
|
window.addEventListener('resize', recenter);
|
|
2415
2464
|
}
|
|
2416
2465
|
|
|
2466
|
+
// Drag anywhere on the game (outside the joystick) to look around — one
|
|
2467
|
+
// finger on a phone, or click-and-drag with a mouse. Pair it with
|
|
2468
|
+
// enableJoystick() in any game where a character moves through a 3D world:
|
|
2469
|
+
// left thumb moves, right thumb looks. Read the drag each frame with
|
|
2470
|
+
// getLookDelta() and turn the camera with it. No-op if already enabled.
|
|
2471
|
+
enableLookControls() {
|
|
2472
|
+
if (this.lookEnabled) return;
|
|
2473
|
+
this.lookEnabled = true;
|
|
2474
|
+
|
|
2475
|
+
let lookId = null;
|
|
2476
|
+
let last = { x: 0, y: 0 };
|
|
2477
|
+
|
|
2478
|
+
this.canvas.addEventListener('pointerdown', (e) => {
|
|
2479
|
+
if (lookId !== null) return;
|
|
2480
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
2481
|
+
lookId = e.pointerId;
|
|
2482
|
+
last = { x: e.clientX, y: e.clientY };
|
|
2483
|
+
this.canvas.setPointerCapture(lookId);
|
|
2484
|
+
hint.style.opacity = '0';
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
this.canvas.addEventListener('pointermove', (e) => {
|
|
2488
|
+
if (e.pointerId !== lookId) return;
|
|
2489
|
+
this.lookDelta.x += e.clientX - last.x;
|
|
2490
|
+
this.lookDelta.y += e.clientY - last.y;
|
|
2491
|
+
last = { x: e.clientX, y: e.clientY };
|
|
2492
|
+
});
|
|
2493
|
+
|
|
2494
|
+
const release = (e) => {
|
|
2495
|
+
if (e.pointerId === lookId) lookId = null;
|
|
2496
|
+
};
|
|
2497
|
+
this.canvas.addEventListener('pointerup', release);
|
|
2498
|
+
this.canvas.addEventListener('pointercancel', release);
|
|
2499
|
+
|
|
2500
|
+
// A small hint on the right, gone after the first drag.
|
|
2501
|
+
const hint = document.createElement('div');
|
|
2502
|
+
hint.textContent = this.isTouchDevice ? 'Drag to look around' : 'Drag to look · WASD to move';
|
|
2503
|
+
Object.assign(hint.style, {
|
|
2504
|
+
position: 'absolute',
|
|
2505
|
+
right: 'calc(20px + var(--safe-right, 0px))',
|
|
2506
|
+
bottom: 'calc(64px + var(--safe-bottom, 0px))',
|
|
2507
|
+
padding: '6px 12px',
|
|
2508
|
+
borderRadius: '14px',
|
|
2509
|
+
background: 'rgba(0, 0, 0, 0.35)',
|
|
2510
|
+
color: 'rgba(255, 255, 255, 0.9)',
|
|
2511
|
+
font: '13px system-ui, sans-serif',
|
|
2512
|
+
pointerEvents: 'none',
|
|
2513
|
+
transition: 'opacity 0.6s',
|
|
2514
|
+
zIndex: '50',
|
|
2515
|
+
});
|
|
2516
|
+
(this.canvas.parentElement || document.body).appendChild(hint);
|
|
2517
|
+
setTimeout(() => { hint.style.opacity = '0'; }, 6000);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// Look-around drag since the last frame, in pixels ({ x: right, y: down }).
|
|
2521
|
+
// Multiply by a small sensitivity (e.g. 0.005 rad/px) to turn the camera.
|
|
2522
|
+
getLookDelta() {
|
|
2523
|
+
return { x: this.lookDelta.x, y: this.lookDelta.y };
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2417
2526
|
// Check if a key is currently held down
|
|
2418
2527
|
isKeyDown(code) {
|
|
2419
2528
|
return !!this.keys[code];
|
|
@@ -2454,6 +2563,8 @@ export class InputManager {
|
|
|
2454
2563
|
// Call at the end of each frame to clear one-shot inputs
|
|
2455
2564
|
clearFrame() {
|
|
2456
2565
|
this.keysJustPressed = {};
|
|
2566
|
+
this.lookDelta.x = 0;
|
|
2567
|
+
this.lookDelta.y = 0;
|
|
2457
2568
|
}
|
|
2458
2569
|
}
|
|
2459
2570
|
`;
|
|
@@ -2558,9 +2669,10 @@ function generateUI(type) {
|
|
|
2558
2669
|
// Minimap rendering in top-right corner
|
|
2559
2670
|
let minimap = this.overlay.querySelector('#minimap');
|
|
2560
2671
|
if (!minimap) {
|
|
2672
|
+
// Top-right, under the Pause button (the top-left corner is the app's Exit).
|
|
2561
2673
|
minimap = this.createElement('canvas', {
|
|
2562
|
-
position: 'absolute', top: '
|
|
2563
|
-
width: '
|
|
2674
|
+
position: 'absolute', top: 'calc(var(--hud-top) + 48px)', right: 'calc(12px + var(--safe-right))',
|
|
2675
|
+
width: '120px', height: '120px',
|
|
2564
2676
|
background: 'rgba(48,32,23,0.8)', borderRadius: '8px',
|
|
2565
2677
|
border: '2px solid #B07C00',
|
|
2566
2678
|
});
|
|
@@ -2571,7 +2683,7 @@ function generateUI(type) {
|
|
|
2571
2683
|
}
|
|
2572
2684
|
|
|
2573
2685
|
const ctx = minimap.getContext('2d');
|
|
2574
|
-
ctx.clearRect(0, 0, 150, 150);
|
|
2686
|
+
ctx.clearRect(0, 0, 150, 150); // drawing space stays 150; CSS scales it
|
|
2575
2687
|
|
|
2576
2688
|
// Draw player
|
|
2577
2689
|
ctx.fillStyle = '#FFD700';
|
|
@@ -2591,19 +2703,23 @@ function generateUI(type) {
|
|
|
2591
2703
|
showInventory(items) {
|
|
2592
2704
|
let inv = this.overlay.querySelector('#inventory');
|
|
2593
2705
|
if (!inv) {
|
|
2706
|
+
// Bottom-right, clear of the joystick (bottom-left) and the home indicator.
|
|
2707
|
+
// pointerEvents none so drags over it still turn the camera.
|
|
2594
2708
|
inv = this.createElement('div', {
|
|
2595
|
-
position: 'absolute', bottom: '
|
|
2596
|
-
display: 'flex',
|
|
2597
|
-
|
|
2709
|
+
position: 'absolute', bottom: 'calc(12px + var(--safe-bottom))', right: 'calc(12px + var(--safe-right))',
|
|
2710
|
+
maxWidth: '55%', display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end',
|
|
2711
|
+
gap: '6px', background: 'rgba(48,32,23,0.85)',
|
|
2712
|
+
padding: '8px', borderRadius: '12px', pointerEvents: 'none',
|
|
2598
2713
|
});
|
|
2599
2714
|
inv.id = 'inventory';
|
|
2600
2715
|
this.overlay.appendChild(inv);
|
|
2601
2716
|
}
|
|
2602
2717
|
inv.innerHTML = '';
|
|
2718
|
+
inv.style.display = items.length ? 'flex' : 'none';
|
|
2603
2719
|
|
|
2604
2720
|
items.forEach((item) => {
|
|
2605
2721
|
const slot = this.createElement('div', {
|
|
2606
|
-
width: '
|
|
2722
|
+
width: '40px', height: '40px', background: '#043922',
|
|
2607
2723
|
borderRadius: '8px', border: '2px solid #228B22',
|
|
2608
2724
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
2609
2725
|
color: '#FFD700', fontSize: '12px', textAlign: 'center',
|
|
@@ -2668,8 +2784,10 @@ export class UI {
|
|
|
2668
2784
|
showHUD() {
|
|
2669
2785
|
this.clearOverlay();
|
|
2670
2786
|
|
|
2787
|
+
// Top-right, inside the safe area. Keep the top-left corner empty — the
|
|
2788
|
+
// host app draws its Exit button there.
|
|
2671
2789
|
const pauseBtn = this.createElement('button', {
|
|
2672
|
-
position: 'absolute', top: '
|
|
2790
|
+
position: 'absolute', top: 'var(--hud-top)', right: 'calc(12px + var(--safe-right))',
|
|
2673
2791
|
padding: '8px 16px', background: 'rgba(48,32,23,0.7)',
|
|
2674
2792
|
color: '#E4DCCB', border: '1px solid #B07C00',
|
|
2675
2793
|
borderRadius: '8px', cursor: 'pointer', fontSize: '14px',
|
|
@@ -2798,6 +2916,13 @@ export class GameLogic {
|
|
|
2798
2916
|
this.player = null;
|
|
2799
2917
|
this.collectibles = [];
|
|
2800
2918
|
this.collectedItems = [];
|
|
2919
|
+
|
|
2920
|
+
// Follow camera: orbits the player. Drag to change yaw (turn) and pitch
|
|
2921
|
+
// (tilt); movement is relative to where the camera faces.
|
|
2922
|
+
this.cameraYaw = 0;
|
|
2923
|
+
this.cameraPitch = 0.55; // radians above the horizon
|
|
2924
|
+
this.cameraDistance = 12;
|
|
2925
|
+
this.lookSensitivity = 0.005; // radians per dragged pixel
|
|
2801
2926
|
}
|
|
2802
2927
|
|
|
2803
2928
|
start() {
|
|
@@ -2806,8 +2931,10 @@ export class GameLogic {
|
|
|
2806
2931
|
this.setupPlayer();
|
|
2807
2932
|
this.spawnCollectibles();
|
|
2808
2933
|
|
|
2809
|
-
//
|
|
2934
|
+
// Phones: left thumb moves (joystick), drag anywhere else to look around.
|
|
2935
|
+
// Desktop: WASD / arrows to move, mouse drag to look.
|
|
2810
2936
|
this.game.input.enableJoystick();
|
|
2937
|
+
this.game.input.enableLookControls();
|
|
2811
2938
|
}
|
|
2812
2939
|
|
|
2813
2940
|
reset() {
|
|
@@ -2828,8 +2955,19 @@ export class GameLogic {
|
|
|
2828
2955
|
this.player.castShadow = true;
|
|
2829
2956
|
this.game.scene.scene.add(this.player);
|
|
2830
2957
|
|
|
2831
|
-
|
|
2832
|
-
|
|
2958
|
+
this.updateCamera();
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
updateCamera() {
|
|
2962
|
+
const cam = this.game.scene.camera;
|
|
2963
|
+
const target = this.player.position;
|
|
2964
|
+
const flat = Math.cos(this.cameraPitch) * this.cameraDistance;
|
|
2965
|
+
cam.position.set(
|
|
2966
|
+
target.x + Math.sin(this.cameraYaw) * flat,
|
|
2967
|
+
target.y + Math.sin(this.cameraPitch) * this.cameraDistance,
|
|
2968
|
+
target.z + Math.cos(this.cameraYaw) * flat
|
|
2969
|
+
);
|
|
2970
|
+
cam.lookAt(target);
|
|
2833
2971
|
}
|
|
2834
2972
|
|
|
2835
2973
|
spawnCollectibles() {
|
|
@@ -2861,19 +2999,26 @@ export class GameLogic {
|
|
|
2861
2999
|
update(delta) {
|
|
2862
3000
|
if (!this.player) return;
|
|
2863
3001
|
|
|
2864
|
-
//
|
|
3002
|
+
// Look around: drag turns (x) and tilts (y) the camera.
|
|
3003
|
+
const look = this.game.input.getLookDelta();
|
|
3004
|
+
this.cameraYaw -= look.x * this.lookSensitivity;
|
|
3005
|
+
this.cameraPitch = Math.max(0.15, Math.min(1.3, this.cameraPitch + look.y * this.lookSensitivity));
|
|
3006
|
+
|
|
3007
|
+
// Move relative to the camera: stick up / W walks away from the camera.
|
|
2865
3008
|
const movement = this.game.input.getMovementVector();
|
|
2866
|
-
|
|
2867
|
-
|
|
3009
|
+
const sin = Math.sin(this.cameraYaw);
|
|
3010
|
+
const cos = Math.cos(this.cameraYaw);
|
|
3011
|
+
const dx = movement.x * cos + movement.z * sin;
|
|
3012
|
+
const dz = -movement.x * sin + movement.z * cos;
|
|
3013
|
+
this.player.position.x += dx * this.moveSpeed * delta;
|
|
3014
|
+
this.player.position.z += dz * this.moveSpeed * delta;
|
|
3015
|
+
if (dx !== 0 || dz !== 0) this.player.rotation.y = Math.atan2(dx, dz);
|
|
2868
3016
|
|
|
2869
3017
|
// Keep player within bounds
|
|
2870
3018
|
this.player.position.x = Math.max(-24, Math.min(24, this.player.position.x));
|
|
2871
3019
|
this.player.position.z = Math.max(-24, Math.min(24, this.player.position.z));
|
|
2872
3020
|
|
|
2873
|
-
|
|
2874
|
-
this.game.scene.camera.position.x = this.player.position.x;
|
|
2875
|
-
this.game.scene.camera.position.z = this.player.position.z + 12;
|
|
2876
|
-
this.game.scene.camera.lookAt(this.player.position);
|
|
3021
|
+
this.updateCamera();
|
|
2877
3022
|
|
|
2878
3023
|
// Check collisions with collectibles
|
|
2879
3024
|
this.collectibles.forEach((item) => {
|