@aiyiran/myclaw 1.1.83 → 1.1.85
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/a/1.html +842 -0
- package/a/2.html +1531 -0
- package/assets/myclaw-artifacts.js +2 -2
- package/index.js +53 -0
- package/package.json +1 -1
- package/skills/yiran-course-template-pipeline/template-index.json +109 -1
- package/skills/yiran-course-template-pipeline/template-index.md +36 -0
- package//346/210/221/347/232/204/345/217/254/345/224/244/345/233/276/351/211/264/357/274/232/346/215/242/344/270/273/351/242/230/346/215/242/345/215/241/346/261/240/__demo__.html +1531 -0
- package//346/210/221/347/232/204/345/217/254/345/224/244/345/233/276/351/211/264/357/274/232/346/215/242/344/270/273/351/242/230/346/215/242/345/215/241/346/261/240/__student-view__.html +77 -0
- package//346/210/221/347/232/204/345/217/254/345/224/244/345/233/276/351/211/264/357/274/232/346/215/242/344/270/273/351/242/230/346/215/242/345/215/241/346/261/240/__student__.json +60 -0
- package//346/210/221/347/232/204/345/217/254/345/224/244/345/233/276/351/211/264/357/274/232/346/215/242/344/270/273/351/242/230/346/215/242/345/215/241/346/261/240/__teacher-view__.html +52 -0
- package//346/210/221/347/232/204/345/217/254/345/224/244/345/233/276/351/211/264/357/274/232/346/215/242/344/270/273/351/242/230/346/215/242/345/215/241/346/261/240/__teacher__.json +45 -0
- package//346/210/221/347/232/204/345/217/254/345/224/244/345/233/276/351/211/264/357/274/232/346/215/242/344/270/273/351/242/230/346/215/242/345/215/241/346/261/240/index.html +150 -0
- package//351/200/203/345/207/272/350/277/267/345/256/253/357/274/232/346/210/221/346/235/245/345/256/232/350/247/204/345/210/231/__demo__.html +842 -0
- package//351/200/203/345/207/272/350/277/267/345/256/253/357/274/232/346/210/221/346/235/245/345/256/232/350/247/204/345/210/231/__student-view__.html +77 -0
- package//351/200/203/345/207/272/350/277/267/345/256/253/357/274/232/346/210/221/346/235/245/345/256/232/350/247/204/345/210/231/__student__.json +60 -0
- package//351/200/203/345/207/272/350/277/267/345/256/253/357/274/232/346/210/221/346/235/245/345/256/232/350/247/204/345/210/231/__teacher-view__.html +52 -0
- package//351/200/203/345/207/272/350/277/267/345/256/253/357/274/232/346/210/221/346/235/245/345/256/232/350/247/204/345/210/231/__teacher__.json +45 -0
- package//351/200/203/345/207/272/350/277/267/345/256/253/357/274/232/346/210/221/346/235/245/345/256/232/350/247/204/345/210/231/index.html +150 -0
package/a/1.html
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>3D 迷宫</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
canvas {
|
|
19
|
+
display: block;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#info {
|
|
23
|
+
position: absolute;
|
|
24
|
+
bottom: 20px;
|
|
25
|
+
left: 50%;
|
|
26
|
+
transform: translateX(-50%);
|
|
27
|
+
color: white;
|
|
28
|
+
font-family: sans-serif;
|
|
29
|
+
font-size: 14px;
|
|
30
|
+
text-align: center;
|
|
31
|
+
background: rgba(0, 0, 0, 0.5);
|
|
32
|
+
padding: 10px 20px;
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#control-panel {
|
|
37
|
+
position: absolute;
|
|
38
|
+
top: 20px;
|
|
39
|
+
left: 20px;
|
|
40
|
+
background: rgba(0, 0, 0, 0.7);
|
|
41
|
+
border-radius: 12px;
|
|
42
|
+
padding: 15px;
|
|
43
|
+
color: white;
|
|
44
|
+
font-family: sans-serif;
|
|
45
|
+
min-width: 160px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#control-panel h3 {
|
|
49
|
+
margin-bottom: 12px;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
color: #aaa;
|
|
52
|
+
text-align: center;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.view-btn {
|
|
56
|
+
display: block;
|
|
57
|
+
width: 100%;
|
|
58
|
+
padding: 10px 15px;
|
|
59
|
+
margin-bottom: 8px;
|
|
60
|
+
border: 2px solid transparent;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
background: rgba(255, 255, 255, 0.1);
|
|
63
|
+
color: white;
|
|
64
|
+
font-size: 14px;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
transition: all 0.2s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.view-btn:hover {
|
|
70
|
+
background: rgba(255, 255, 255, 0.2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.view-btn.active {
|
|
74
|
+
border-color: #4a90d9;
|
|
75
|
+
background: rgba(74, 144, 217, 0.3);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.view-icon {
|
|
79
|
+
margin-right: 8px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#win-message {
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: 50%;
|
|
85
|
+
left: 50%;
|
|
86
|
+
transform: translate(-50%, -50%);
|
|
87
|
+
background: rgba(0, 0, 0, 0.9);
|
|
88
|
+
border: 3px solid #ffd43b;
|
|
89
|
+
border-radius: 20px;
|
|
90
|
+
padding: 40px 60px;
|
|
91
|
+
color: white;
|
|
92
|
+
font-family: sans-serif;
|
|
93
|
+
text-align: center;
|
|
94
|
+
display: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#win-message h2 {
|
|
98
|
+
color: #ffd43b;
|
|
99
|
+
font-size: 32px;
|
|
100
|
+
margin-bottom: 15px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#win-message p {
|
|
104
|
+
font-size: 16px;
|
|
105
|
+
color: #aaa;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#restart-btn {
|
|
109
|
+
margin-top: 20px;
|
|
110
|
+
padding: 12px 30px;
|
|
111
|
+
font-size: 16px;
|
|
112
|
+
background: #51cf66;
|
|
113
|
+
border: none;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
color: white;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#restart-btn:hover {
|
|
120
|
+
background: #40a854;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#lives-display {
|
|
124
|
+
position: absolute;
|
|
125
|
+
top: 20px;
|
|
126
|
+
right: 20px;
|
|
127
|
+
font-size: 32px;
|
|
128
|
+
background: rgba(0, 0, 0, 0.7);
|
|
129
|
+
padding: 10px 20px;
|
|
130
|
+
border-radius: 12px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#game-over-message {
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 50%;
|
|
136
|
+
left: 50%;
|
|
137
|
+
transform: translate(-50%, -50%);
|
|
138
|
+
background: rgba(20, 0, 0, 0.95);
|
|
139
|
+
border: 3px solid #8B0000;
|
|
140
|
+
border-radius: 20px;
|
|
141
|
+
padding: 40px 60px;
|
|
142
|
+
color: white;
|
|
143
|
+
font-family: 'Courier New', monospace;
|
|
144
|
+
text-align: center;
|
|
145
|
+
display: none;
|
|
146
|
+
box-shadow: 0 0 50px rgba(139, 0, 0, 0.8), inset 0 0 30px rgba(0, 0, 0, 0.5);
|
|
147
|
+
animation: horrorPulse 1s infinite;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@keyframes horrorPulse {
|
|
151
|
+
|
|
152
|
+
0%,
|
|
153
|
+
100% {
|
|
154
|
+
box-shadow: 0 0 50px rgba(139, 0, 0, 0.8), inset 0 0 30px rgba(0, 0, 0, 0.5);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
50% {
|
|
158
|
+
box-shadow: 0 0 80px rgba(139, 0, 0, 1), inset 0 0 50px rgba(0, 0, 0, 0.8);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#game-over-message h2 {
|
|
163
|
+
color: #ff0000;
|
|
164
|
+
font-size: 42px;
|
|
165
|
+
margin-bottom: 15px;
|
|
166
|
+
text-shadow: 0 0 20px #ff0000, 0 0 40px #ff0000;
|
|
167
|
+
animation: textFlicker 0.5s infinite;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@keyframes textFlicker {
|
|
171
|
+
|
|
172
|
+
0%,
|
|
173
|
+
100% {
|
|
174
|
+
opacity: 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
50% {
|
|
178
|
+
opacity: 0.8;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#game-over-message p {
|
|
183
|
+
font-size: 18px;
|
|
184
|
+
color: #ccc;
|
|
185
|
+
text-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#restart-btn-gameover {
|
|
189
|
+
margin-top: 20px;
|
|
190
|
+
padding: 12px 30px;
|
|
191
|
+
font-size: 16px;
|
|
192
|
+
background: #ff4444;
|
|
193
|
+
border: none;
|
|
194
|
+
border-radius: 8px;
|
|
195
|
+
color: white;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#restart-btn-gameover:hover {
|
|
200
|
+
background: #cc0000;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* 危险特效遮罩 */
|
|
204
|
+
#danger-overlay {
|
|
205
|
+
position: fixed;
|
|
206
|
+
top: 0;
|
|
207
|
+
left: 0;
|
|
208
|
+
width: 100%;
|
|
209
|
+
height: 100%;
|
|
210
|
+
pointer-events: none;
|
|
211
|
+
z-index: 1000;
|
|
212
|
+
opacity: 0;
|
|
213
|
+
transition: opacity 0.1s;
|
|
214
|
+
}
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
|
|
218
|
+
<body>
|
|
219
|
+
<!-- 全屏特效遮罩 -->
|
|
220
|
+
<div id="danger-overlay"></div>
|
|
221
|
+
|
|
222
|
+
<div id="control-panel">
|
|
223
|
+
<h3>📷 视角切换</h3>
|
|
224
|
+
<button class="view-btn active" id="btn-free">
|
|
225
|
+
<span class="view-icon">🎥</span>自由视角
|
|
226
|
+
</button>
|
|
227
|
+
<button class="view-btn" id="btn-immersive">
|
|
228
|
+
<span class="view-icon">👁️</span>身临其境
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div id="info">
|
|
233
|
+
🏁 找到黄色旗帜走出迷宫! 小心僵尸!<br>
|
|
234
|
+
<b>WASD</b> = 移动 | <b>方向键</b> = 控制视角
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div id="lives-display">
|
|
238
|
+
❤️❤️❤️
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div id="game-over-message">
|
|
242
|
+
<h2>💀 游戏结束!</h2>
|
|
243
|
+
<p>你被僵尸抓住了...</p>
|
|
244
|
+
<button id="restart-btn-gameover">再来一局</button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div id="win-message">
|
|
248
|
+
<h2>🎉 恭喜通关!</h2>
|
|
249
|
+
<p>你成功走出了迷宫!</p>
|
|
250
|
+
<button id="restart-btn">再来一局</button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<script type="importmap">
|
|
254
|
+
{
|
|
255
|
+
"imports": {
|
|
256
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
|
257
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
</script>
|
|
261
|
+
<script type="module">
|
|
262
|
+
import * as THREE from 'three';
|
|
263
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
264
|
+
|
|
265
|
+
// ============ 迷宫配置 ============
|
|
266
|
+
const MAZE_SIZE = 11; // 迷宫格子数(奇数)
|
|
267
|
+
const CELL_SIZE = 4; // 每格大小
|
|
268
|
+
const WALL_HEIGHT = 3; // 墙高度
|
|
269
|
+
const PLAYER_RADIUS = 0.5; // 玩家半径
|
|
270
|
+
const MOVE_SPEED = 0.15; // 移动速度
|
|
271
|
+
|
|
272
|
+
// ============ 全局变量 ============
|
|
273
|
+
let scene, camera, renderer, orbitControls;
|
|
274
|
+
let playerSphere, endFlag;
|
|
275
|
+
let zombie;
|
|
276
|
+
let walls = [];
|
|
277
|
+
let mazeData = [];
|
|
278
|
+
let viewMode = 'free';
|
|
279
|
+
let gameWon = false;
|
|
280
|
+
let gameOver = false;
|
|
281
|
+
// 生命系统
|
|
282
|
+
let playerLives = 3;
|
|
283
|
+
let isInvincible = false; // 无敌时间
|
|
284
|
+
let invincibilityTimer = 0;
|
|
285
|
+
// WASD 控制移动,方向键控制视角
|
|
286
|
+
const keys = {
|
|
287
|
+
KeyW: false, KeyS: false, KeyA: false, KeyD: false,
|
|
288
|
+
ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// ============ 初始化 ============
|
|
292
|
+
function init() {
|
|
293
|
+
// 创建场景
|
|
294
|
+
scene = new THREE.Scene();
|
|
295
|
+
scene.background = new THREE.Color(0x1a1a2e);
|
|
296
|
+
|
|
297
|
+
// 创建相机
|
|
298
|
+
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
299
|
+
camera.position.set(0, 15, 15);
|
|
300
|
+
|
|
301
|
+
// 创建渲染器
|
|
302
|
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
303
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
304
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
305
|
+
document.body.appendChild(renderer.domElement);
|
|
306
|
+
|
|
307
|
+
// 轨道控制器
|
|
308
|
+
orbitControls = new OrbitControls(camera, renderer.domElement);
|
|
309
|
+
orbitControls.enableDamping = true;
|
|
310
|
+
orbitControls.maxPolarAngle = Math.PI / 2 + 0.1;
|
|
311
|
+
|
|
312
|
+
// 生成迷宫
|
|
313
|
+
generateMaze();
|
|
314
|
+
|
|
315
|
+
// 创建地面
|
|
316
|
+
createGround();
|
|
317
|
+
|
|
318
|
+
// 创建墙壁
|
|
319
|
+
createWalls();
|
|
320
|
+
|
|
321
|
+
// 创建玩家
|
|
322
|
+
createPlayer();
|
|
323
|
+
|
|
324
|
+
// 创建终点
|
|
325
|
+
createEndFlag();
|
|
326
|
+
|
|
327
|
+
// 灯光
|
|
328
|
+
createLights();
|
|
329
|
+
|
|
330
|
+
// 创建僵尸
|
|
331
|
+
createZombie();
|
|
332
|
+
|
|
333
|
+
// 事件监听
|
|
334
|
+
setupEventListeners();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============ 迷宫生成(递归回溯算法) ============
|
|
338
|
+
function generateMaze() {
|
|
339
|
+
// 初始化迷宫数据(全为墙)
|
|
340
|
+
mazeData = [];
|
|
341
|
+
for (let z = 0; z < MAZE_SIZE; z++) {
|
|
342
|
+
mazeData[z] = [];
|
|
343
|
+
for (let x = 0; x < MAZE_SIZE; x++) {
|
|
344
|
+
mazeData[z][x] = 1; // 1 = 墙
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 递归回溯生成迷宫
|
|
349
|
+
function carve(x, z) {
|
|
350
|
+
mazeData[z][x] = 0; // 0 = 路
|
|
351
|
+
|
|
352
|
+
// 随机方向
|
|
353
|
+
const directions = [
|
|
354
|
+
[0, -2], [0, 2], [-2, 0], [2, 0]
|
|
355
|
+
].sort(() => Math.random() - 0.5);
|
|
356
|
+
|
|
357
|
+
for (const [dx, dz] of directions) {
|
|
358
|
+
const nx = x + dx;
|
|
359
|
+
const nz = z + dz;
|
|
360
|
+
|
|
361
|
+
if (nx > 0 && nx < MAZE_SIZE - 1 && nz > 0 && nz < MAZE_SIZE - 1 && mazeData[nz][nx] === 1) {
|
|
362
|
+
// 打通中间的墙
|
|
363
|
+
mazeData[z + dz / 2][x + dx / 2] = 0;
|
|
364
|
+
carve(nx, nz);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 从角落开始生成
|
|
370
|
+
carve(1, 1);
|
|
371
|
+
|
|
372
|
+
// 确保起点和终点是路
|
|
373
|
+
mazeData[1][1] = 0;
|
|
374
|
+
mazeData[MAZE_SIZE - 2][MAZE_SIZE - 2] = 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============ 创建地面 ============
|
|
378
|
+
function createGround() {
|
|
379
|
+
const groundSize = MAZE_SIZE * CELL_SIZE + 10;
|
|
380
|
+
const groundGeometry = new THREE.PlaneGeometry(groundSize, groundSize);
|
|
381
|
+
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
382
|
+
color: 0x1a3a1a, // 深绿色地板
|
|
383
|
+
side: THREE.DoubleSide
|
|
384
|
+
});
|
|
385
|
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
386
|
+
ground.rotation.x = -Math.PI / 2;
|
|
387
|
+
ground.position.y = -0.5;
|
|
388
|
+
scene.add(ground);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============ 创建墙壁 ============
|
|
392
|
+
function createWalls() {
|
|
393
|
+
const wallMaterial = new THREE.MeshStandardMaterial({
|
|
394
|
+
color: 0x8B4513, // 棕色墙壁(像砖墙)
|
|
395
|
+
metalness: 0.2,
|
|
396
|
+
roughness: 0.8
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
for (let z = 0; z < MAZE_SIZE; z++) {
|
|
400
|
+
for (let x = 0; x < MAZE_SIZE; x++) {
|
|
401
|
+
if (mazeData[z][x] === 1) {
|
|
402
|
+
const wallGeometry = new THREE.BoxGeometry(CELL_SIZE, WALL_HEIGHT, CELL_SIZE);
|
|
403
|
+
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
|
|
404
|
+
wall.position.set(
|
|
405
|
+
x * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2,
|
|
406
|
+
WALL_HEIGHT / 2 - 0.5,
|
|
407
|
+
z * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2
|
|
408
|
+
);
|
|
409
|
+
scene.add(wall);
|
|
410
|
+
walls.push(wall);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ============ 创建玩家 ============
|
|
417
|
+
function createPlayer() {
|
|
418
|
+
const geometry = new THREE.SphereGeometry(PLAYER_RADIUS, 32, 32);
|
|
419
|
+
const material = new THREE.MeshStandardMaterial({
|
|
420
|
+
color: 0x4a90d9,
|
|
421
|
+
metalness: 0.3,
|
|
422
|
+
roughness: 0.4
|
|
423
|
+
});
|
|
424
|
+
playerSphere = new THREE.Mesh(geometry, material);
|
|
425
|
+
|
|
426
|
+
// 起点位置
|
|
427
|
+
playerSphere.position.set(
|
|
428
|
+
1 * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2,
|
|
429
|
+
PLAYER_RADIUS - 0.5,
|
|
430
|
+
1 * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
scene.add(playerSphere);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ============ 创建终点旗帜 ============
|
|
437
|
+
function createEndFlag() {
|
|
438
|
+
// 旗杆
|
|
439
|
+
const poleGeometry = new THREE.CylinderGeometry(0.1, 0.1, 2, 16);
|
|
440
|
+
const poleMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
|
|
441
|
+
const pole = new THREE.Mesh(poleGeometry, poleMaterial);
|
|
442
|
+
|
|
443
|
+
// 红旗(黄色)
|
|
444
|
+
const flagGeometry = new THREE.PlaneGeometry(1.5, 1);
|
|
445
|
+
const flagMaterial = new THREE.MeshStandardMaterial({
|
|
446
|
+
color: 0xffdd00,
|
|
447
|
+
side: THREE.DoubleSide
|
|
448
|
+
});
|
|
449
|
+
const flag = new THREE.Mesh(flagGeometry, flagMaterial);
|
|
450
|
+
flag.position.set(0.75, 0.5, 0);
|
|
451
|
+
flag.rotation.y = Math.PI / 4;
|
|
452
|
+
|
|
453
|
+
endFlag = new THREE.Group();
|
|
454
|
+
endFlag.add(pole);
|
|
455
|
+
endFlag.add(flag);
|
|
456
|
+
|
|
457
|
+
// 终点位置
|
|
458
|
+
const endX = (MAZE_SIZE - 2) * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2;
|
|
459
|
+
const endZ = (MAZE_SIZE - 2) * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2;
|
|
460
|
+
endFlag.position.set(endX, 0, endZ);
|
|
461
|
+
|
|
462
|
+
scene.add(endFlag);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ============ 创建僵尸 ============
|
|
466
|
+
function createZombie() {
|
|
467
|
+
// 身体(红色方块)
|
|
468
|
+
const bodyGeometry = new THREE.BoxGeometry(0.8, 1.5, 0.6);
|
|
469
|
+
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
470
|
+
color: 0x8B0000,
|
|
471
|
+
metalness: 0.1,
|
|
472
|
+
roughness: 0.9
|
|
473
|
+
});
|
|
474
|
+
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
475
|
+
|
|
476
|
+
// 头
|
|
477
|
+
const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
|
478
|
+
const headMaterial = new THREE.MeshStandardMaterial({
|
|
479
|
+
color: 0xB22222,
|
|
480
|
+
metalness: 0.1,
|
|
481
|
+
roughness: 0.9
|
|
482
|
+
});
|
|
483
|
+
const head = new THREE.Mesh(headGeometry, headMaterial);
|
|
484
|
+
head.position.y = 1;
|
|
485
|
+
|
|
486
|
+
// 眼睛(黄色发光)
|
|
487
|
+
const eyeGeometry = new THREE.SphereGeometry(0.08, 16, 16);
|
|
488
|
+
const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 });
|
|
489
|
+
const leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
|
|
490
|
+
leftEye.position.set(-0.12, 1.05, 0.25);
|
|
491
|
+
const rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
|
|
492
|
+
rightEye.position.set(0.12, 1.05, 0.25);
|
|
493
|
+
|
|
494
|
+
zombie = new THREE.Group();
|
|
495
|
+
zombie.add(body);
|
|
496
|
+
zombie.add(head);
|
|
497
|
+
zombie.add(leftEye);
|
|
498
|
+
zombie.add(rightEye);
|
|
499
|
+
|
|
500
|
+
// 找到有效的随机位置(路上,不是墙)
|
|
501
|
+
let validPositions = [];
|
|
502
|
+
for (let z = 0; z < MAZE_SIZE; z++) {
|
|
503
|
+
for (let x = 0; x < MAZE_SIZE; x++) {
|
|
504
|
+
if (mazeData[z][x] === 0) {
|
|
505
|
+
// 计算世界坐标
|
|
506
|
+
const worldX = x * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2;
|
|
507
|
+
const worldZ = z * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2;
|
|
508
|
+
|
|
509
|
+
// 排除起点附近(距离起点大于5格)
|
|
510
|
+
const distToPlayer = Math.sqrt(
|
|
511
|
+
Math.pow(worldX - playerSphere.position.x, 2) +
|
|
512
|
+
Math.pow(worldZ - playerSphere.position.z, 2)
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (distToPlayer > CELL_SIZE * 4) {
|
|
516
|
+
validPositions.push({ x: worldX, z: worldZ });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 随机选一个位置
|
|
523
|
+
const pos = validPositions[Math.floor(Math.random() * validPositions.length)];
|
|
524
|
+
zombie.position.set(pos.x, 0.25, pos.z);
|
|
525
|
+
|
|
526
|
+
scene.add(zombie);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ============ 创建灯光 ============
|
|
530
|
+
function createLights() {
|
|
531
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
532
|
+
scene.add(ambientLight);
|
|
533
|
+
|
|
534
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
535
|
+
directionalLight.position.set(10, 20, 10);
|
|
536
|
+
scene.add(directionalLight);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ============ 碰撞检测 ============
|
|
540
|
+
const raycaster = new THREE.Raycaster();
|
|
541
|
+
|
|
542
|
+
function canMoveTo(position, direction, distance) {
|
|
543
|
+
raycaster.set(position, direction);
|
|
544
|
+
raycaster.far = distance + PLAYER_RADIUS;
|
|
545
|
+
const intersects = raycaster.intersectObjects(walls);
|
|
546
|
+
|
|
547
|
+
if (intersects.length > 0) {
|
|
548
|
+
return Math.max(0, intersects[0].distance - PLAYER_RADIUS * 0.3);
|
|
549
|
+
}
|
|
550
|
+
return distance;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ============ 僵尸穿墙碰撞检测 ============
|
|
554
|
+
// 僵尸可以穿墙,但穿过墙时速度会变慢
|
|
555
|
+
const ZOMBIE_RADIUS = 0.5;
|
|
556
|
+
const ZOMBIE_SPEED_NORMAL = MOVE_SPEED * 0.5; // 正常速度
|
|
557
|
+
const ZOMBIE_SPEED_WALL = MOVE_SPEED * 0.15; // 穿墙时速度(很慢)
|
|
558
|
+
|
|
559
|
+
function isZombieInWall(position) {
|
|
560
|
+
// 检测位置是否在墙里面
|
|
561
|
+
raycaster.set(position, new THREE.Vector3(0, 0, 1));
|
|
562
|
+
raycaster.far = 10;
|
|
563
|
+
const intersects = raycaster.intersectObjects(walls);
|
|
564
|
+
|
|
565
|
+
// 如果前方很近就有墙,说明在墙里面
|
|
566
|
+
if (intersects.length > 0 && intersects[0].distance < CELL_SIZE * 0.8) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function canZombieMove(position, direction, distance) {
|
|
573
|
+
// 僵尸可以穿墙,所以直接返回可以移动
|
|
574
|
+
// 但返回的距离会考虑是否在墙里
|
|
575
|
+
const inWall = isZombieInWall(position);
|
|
576
|
+
const speed = inWall ? ZOMBIE_SPEED_WALL : ZOMBIE_SPEED_NORMAL;
|
|
577
|
+
const actualDistance = Math.min(distance, speed);
|
|
578
|
+
return { canMove: true, distance: actualDistance, inWall: inWall };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ============ 身临其境模式鼠标控制 ============
|
|
582
|
+
let isDragging = false;
|
|
583
|
+
let previousMousePosition = { x: 0, y: 0 };
|
|
584
|
+
let cameraRotationY = Math.PI; // 默认朝向 -Z 方向
|
|
585
|
+
let cameraRotationX = 0;
|
|
586
|
+
|
|
587
|
+
// ============ 更新生命显示 ============
|
|
588
|
+
function updateLivesDisplay() {
|
|
589
|
+
const hearts = '❤️'.repeat(playerLives) + '🖤'.repeat(3 - playerLives);
|
|
590
|
+
document.getElementById('lives-display').textContent = hearts;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ============ 事件监听 ============
|
|
594
|
+
function setupEventListeners() {
|
|
595
|
+
window.addEventListener('keydown', (e) => {
|
|
596
|
+
if (keys.hasOwnProperty(e.code)) keys[e.code] = true;
|
|
597
|
+
});
|
|
598
|
+
window.addEventListener('keyup', (e) => {
|
|
599
|
+
if (keys.hasOwnProperty(e.code)) keys[e.code] = false;
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// 鼠标点击(身临其境模式切换用)
|
|
603
|
+
renderer.domElement.addEventListener('click', () => {
|
|
604
|
+
// 聚焦以便接收键盘事件
|
|
605
|
+
renderer.domElement.focus();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// 视角切换
|
|
609
|
+
document.getElementById('btn-free').addEventListener('click', () => {
|
|
610
|
+
viewMode = 'free';
|
|
611
|
+
document.getElementById('btn-free').classList.add('active');
|
|
612
|
+
document.getElementById('btn-immersive').classList.remove('active');
|
|
613
|
+
orbitControls.enabled = true;
|
|
614
|
+
camera.position.set(0, 15, 15);
|
|
615
|
+
camera.lookAt(0, 0, 0);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
document.getElementById('btn-immersive').addEventListener('click', () => {
|
|
619
|
+
viewMode = 'immersive';
|
|
620
|
+
document.getElementById('btn-immersive').classList.add('active');
|
|
621
|
+
document.getElementById('btn-free').classList.remove('active');
|
|
622
|
+
orbitControls.enabled = false;
|
|
623
|
+
|
|
624
|
+
// 立即将相机放到玩家位置
|
|
625
|
+
camera.position.copy(playerSphere.position);
|
|
626
|
+
camera.position.y = 0.8;
|
|
627
|
+
|
|
628
|
+
// 重置旋转角度,让相机朝向迷宫中心方向
|
|
629
|
+
cameraRotationY = Math.PI; // 朝向 -Z 方向(迷宫深处)
|
|
630
|
+
cameraRotationX = 0;
|
|
631
|
+
camera.rotation.set(0, 0, 0);
|
|
632
|
+
camera.rotateY(cameraRotationY);
|
|
633
|
+
camera.rotateX(cameraRotationX);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// 重新开始
|
|
637
|
+
document.getElementById('restart-btn').addEventListener('click', () => {
|
|
638
|
+
location.reload();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// 游戏结束重新开始
|
|
642
|
+
document.getElementById('restart-btn-gameover').addEventListener('click', () => {
|
|
643
|
+
location.reload();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// 窗口大小变化
|
|
647
|
+
window.addEventListener('resize', () => {
|
|
648
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
649
|
+
camera.updateProjectionMatrix();
|
|
650
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ============ 动画循环 ============
|
|
655
|
+
function animate() {
|
|
656
|
+
requestAnimationFrame(animate);
|
|
657
|
+
|
|
658
|
+
if (!gameWon && !gameOver) {
|
|
659
|
+
// 方向键控制视角(身临其境模式)
|
|
660
|
+
if (viewMode === 'immersive') {
|
|
661
|
+
if (keys.ArrowLeft) cameraRotationY += 0.06;
|
|
662
|
+
if (keys.ArrowRight) cameraRotationY -= 0.06;
|
|
663
|
+
if (keys.ArrowUp) cameraRotationX = Math.max(-Math.PI / 3, cameraRotationX - 0.04);
|
|
664
|
+
if (keys.ArrowDown) cameraRotationX = Math.min(Math.PI / 3, cameraRotationX + 0.04);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let isMoving = false;
|
|
668
|
+
|
|
669
|
+
// 移动方向(WASD 控制)
|
|
670
|
+
// 先更新身临其境视角相机(这样移动才能用正确的朝向)
|
|
671
|
+
if (viewMode === 'immersive') {
|
|
672
|
+
camera.position.copy(playerSphere.position);
|
|
673
|
+
camera.position.y = 0.8;
|
|
674
|
+
camera.rotation.set(0, 0, 0);
|
|
675
|
+
camera.rotateY(cameraRotationY);
|
|
676
|
+
camera.rotateX(cameraRotationX);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let moveDir = new THREE.Vector3();
|
|
680
|
+
|
|
681
|
+
if (keys.KeyW) moveDir.z -= 1;
|
|
682
|
+
if (keys.KeyS) moveDir.z += 1;
|
|
683
|
+
if (keys.KeyA) moveDir.x -= 1;
|
|
684
|
+
if (keys.KeyD) moveDir.x += 1;
|
|
685
|
+
|
|
686
|
+
if (moveDir.length() > 0) {
|
|
687
|
+
isMoving = true;
|
|
688
|
+
moveDir.normalize();
|
|
689
|
+
|
|
690
|
+
// 根据视角方向计算移动方向
|
|
691
|
+
moveDir.applyQuaternion(camera.quaternion);
|
|
692
|
+
moveDir.y = 0;
|
|
693
|
+
moveDir.normalize();
|
|
694
|
+
|
|
695
|
+
const currentPos = playerSphere.position.clone();
|
|
696
|
+
currentPos.y = 0;
|
|
697
|
+
const maxDist = canMoveTo(currentPos, moveDir, MOVE_SPEED);
|
|
698
|
+
|
|
699
|
+
if (maxDist > 0) {
|
|
700
|
+
playerSphere.position.add(moveDir.multiplyScalar(maxDist));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 浮动效果
|
|
705
|
+
if (!isMoving) {
|
|
706
|
+
playerSphere.position.y = PLAYER_RADIUS - 0.5 + Math.sin(Date.now() * 0.002) * 0.1;
|
|
707
|
+
} else {
|
|
708
|
+
playerSphere.position.y = PLAYER_RADIUS - 0.5;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// 旗帜飘动
|
|
712
|
+
if (endFlag) {
|
|
713
|
+
endFlag.children[1].rotation.y = Math.sin(Date.now() * 0.003) * 0.3 + Math.PI / 4;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 僵尸追击玩家
|
|
717
|
+
if (zombie && !gameWon) {
|
|
718
|
+
const toPlayer = new THREE.Vector3();
|
|
719
|
+
toPlayer.subVectors(playerSphere.position, zombie.position);
|
|
720
|
+
toPlayer.y = 0;
|
|
721
|
+
const distToPlayer = toPlayer.length();
|
|
722
|
+
|
|
723
|
+
if (distToPlayer > 1) {
|
|
724
|
+
toPlayer.normalize();
|
|
725
|
+
|
|
726
|
+
// 僵尸可以直接穿墙,但穿墙时速度会变慢
|
|
727
|
+
const zombiePos = zombie.position.clone();
|
|
728
|
+
zombiePos.y = 0;
|
|
729
|
+
const moveResult = canZombieMove(zombiePos, toPlayer, MOVE_SPEED * 0.5);
|
|
730
|
+
|
|
731
|
+
// 穿墙时颜色变深表示正在穿墙
|
|
732
|
+
const bodyMesh = zombie.children[0];
|
|
733
|
+
if (moveResult.inWall) {
|
|
734
|
+
bodyMesh.material.color.setHex(0x4a0000); // 深红色,表示穿墙中
|
|
735
|
+
} else {
|
|
736
|
+
bodyMesh.material.color.setHex(0x8B0000); // 正常红色
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (moveResult.canMove) {
|
|
740
|
+
zombie.position.add(toPlayer.clone().multiplyScalar(moveResult.distance));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// 让僵尸朝向玩家
|
|
744
|
+
zombie.lookAt(playerSphere.position.x, zombie.position.y, playerSphere.position.z);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 僵尸上下浮动
|
|
748
|
+
zombie.position.y = 0.25 + Math.sin(Date.now() * 0.003) * 0.05;
|
|
749
|
+
|
|
750
|
+
// ===== 恐怖特效 =====
|
|
751
|
+
// 计算危险程度(基于距离)
|
|
752
|
+
const dangerDistance = 8; // 开始显示警告的距离
|
|
753
|
+
const dangerOpacity = Math.max(0, 1 - distToPlayer / dangerDistance);
|
|
754
|
+
|
|
755
|
+
// 检测僵尸是否攻击到玩家
|
|
756
|
+
const attackDist = distToPlayer < 1.2; // 攻击距离
|
|
757
|
+
if (attackDist && !isInvincible && !gameOver) {
|
|
758
|
+
playerLives--;
|
|
759
|
+
updateLivesDisplay();
|
|
760
|
+
|
|
761
|
+
if (playerLives <= 0) {
|
|
762
|
+
gameOver = true;
|
|
763
|
+
// 游戏结束 - 全屏血红特效
|
|
764
|
+
document.getElementById('danger-overlay').style.background = 'radial-gradient(circle, rgba(139,0,0,0.9) 0%, rgba(50,0,0,0.95) 100%)';
|
|
765
|
+
document.getElementById('danger-overlay').style.opacity = '1';
|
|
766
|
+
document.getElementById('game-over-message').style.display = 'block';
|
|
767
|
+
} else {
|
|
768
|
+
// 攻击特效 - 闪红
|
|
769
|
+
document.getElementById('danger-overlay').style.background = 'radial-gradient(circle, rgba(255,0,0,0.8) 0%, rgba(139,0,0,0.9) 100%)';
|
|
770
|
+
document.getElementById('danger-overlay').style.opacity = '1';
|
|
771
|
+
setTimeout(() => {
|
|
772
|
+
document.getElementById('danger-overlay').style.opacity = (dangerOpacity * 0.5).toString();
|
|
773
|
+
document.getElementById('danger-overlay').style.background = 'radial-gradient(circle, rgba(255,200,0,0.3) 0%, rgba(255,100,0,0) 100%)';
|
|
774
|
+
}, 200);
|
|
775
|
+
|
|
776
|
+
// 给玩家1秒无敌时间
|
|
777
|
+
isInvincible = true;
|
|
778
|
+
invincibilityTimer = 60; // 约1秒(60帧)
|
|
779
|
+
// 闪烁玩家
|
|
780
|
+
playerSphere.material.color.setHex(0xff6666);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 僵尸靠近时显示黄色警告(只有在攻击范围内且不是无敌时间)
|
|
785
|
+
if (!isInvincible && !gameOver) {
|
|
786
|
+
if (distToPlayer < dangerDistance && distToPlayer >= 1.2) {
|
|
787
|
+
// 黄色危险光晕
|
|
788
|
+
const yellowOpacity = dangerOpacity * 0.5;
|
|
789
|
+
document.getElementById('danger-overlay').style.background = `radial-gradient(circle, rgba(255,200,0,${yellowOpacity * 0.3}) 0%, rgba(255,100,0,${yellowOpacity * 0.2}) 50%, transparent 70%)`;
|
|
790
|
+
document.getElementById('danger-overlay').style.opacity = '1';
|
|
791
|
+
} else if (distToPlayer >= dangerDistance) {
|
|
792
|
+
// 远离后渐隐
|
|
793
|
+
document.getElementById('danger-overlay').style.opacity = '0';
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// 无敌时间倒计时
|
|
798
|
+
if (isInvincible) {
|
|
799
|
+
invincibilityTimer--;
|
|
800
|
+
// 闪烁效果
|
|
801
|
+
playerSphere.visible = Math.floor(invincibilityTimer / 5) % 2 === 0;
|
|
802
|
+
if (invincibilityTimer <= 0) {
|
|
803
|
+
isInvincible = false;
|
|
804
|
+
playerSphere.visible = true;
|
|
805
|
+
playerSphere.material.color.setHex(0x4a90d9);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// 检测胜利
|
|
811
|
+
checkWin();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (viewMode === 'free') {
|
|
815
|
+
orbitControls.update();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
renderer.render(scene, camera);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ============ 检测胜利 ============
|
|
822
|
+
function checkWin() {
|
|
823
|
+
const endX = (MAZE_SIZE - 2) * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2;
|
|
824
|
+
const endZ = (MAZE_SIZE - 2) * CELL_SIZE - (MAZE_SIZE * CELL_SIZE) / 2 + CELL_SIZE / 2;
|
|
825
|
+
|
|
826
|
+
const dx = playerSphere.position.x - endX;
|
|
827
|
+
const dz = playerSphere.position.z - endZ;
|
|
828
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
829
|
+
|
|
830
|
+
if (dist < 1.5) {
|
|
831
|
+
gameWon = true;
|
|
832
|
+
document.getElementById('win-message').style.display = 'block';
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// ============ 启动 ============
|
|
837
|
+
init();
|
|
838
|
+
animate();
|
|
839
|
+
</script>
|
|
840
|
+
</body>
|
|
841
|
+
|
|
842
|
+
</html>
|