完成稿件管理逻辑; 新增博客编辑器; 新增博客; 新增请求测试小工具; 修改sweetheart样式; 打包用户头像组件; 新增导航栏隐藏功能; 新增3D打枪小游戏;
395 lines
15 KiB
HTML
395 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>3D第一人称射击游戏</title>
|
||
<style>
|
||
body { margin: 0; overflow: hidden; }
|
||
#crosshair {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 10px;
|
||
height: 10px;
|
||
background-color: red;
|
||
border-radius: 50%;
|
||
}
|
||
.ui { font-family: Arial, sans-serif; color: white; font-size: 20px; position: absolute; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 准星 -->
|
||
<div id="crosshair"></div>
|
||
<!-- 受伤闪烁效果 -->
|
||
<div id="damageOverlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 0, 0, 0); pointer-events: none;"></div>
|
||
<!-- 游戏结束弹窗 -->
|
||
<div id="gameOverModal" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 20px; border-radius: 10px; text-align: center; display: none;">
|
||
<h2>游戏结束</h2>
|
||
<p id="finalScore"></p>
|
||
<button id="restartButton" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">重新开始</button>
|
||
</div>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
|
||
<script>
|
||
let isGamePaused = false;
|
||
// 初始化场景
|
||
const scene = new THREE.Scene();
|
||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||
camera.position.set(0, 1.6, 0); // 玩家高度1.6米
|
||
|
||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
document.body.appendChild(renderer.domElement);
|
||
|
||
// 添加光照
|
||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||
scene.add(ambientLight);
|
||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
|
||
directionalLight.position.set(1, 1, 1);
|
||
scene.add(directionalLight);
|
||
|
||
// 视角控制(重新设计)
|
||
let yaw = 0; // 水平旋转角度(左右)
|
||
let pitch = 0; // 垂直旋转角度(上下)
|
||
const sensitivity = 0.002; // 鼠标灵敏度
|
||
document.addEventListener('click', () => {
|
||
renderer.domElement.requestPointerLock();
|
||
});
|
||
document.addEventListener('mousemove', (event) => {
|
||
if (document.pointerLockElement === renderer.domElement) {
|
||
const movementX = event.movementX || 0;
|
||
const movementY = event.movementY || 0;
|
||
|
||
// 水平旋转(向左移动鼠标镜头向左)
|
||
yaw -= movementX * sensitivity;
|
||
|
||
// 垂直旋转(向上移动鼠标镜头向上)
|
||
pitch -= movementY * sensitivity;
|
||
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch)); // 限制垂直角度
|
||
|
||
// 更新相机旋转
|
||
camera.rotation.order = 'YXZ'; // 先Y(水平)后X(垂直)
|
||
camera.rotation.set(pitch, yaw, 0);
|
||
}
|
||
});
|
||
|
||
|
||
// WASD移动控制
|
||
const keys = {};
|
||
document.addEventListener('keydown', (event) => keys[event.key.toLowerCase()] = true);
|
||
document.addEventListener('keyup', (event) => keys[event.key.toLowerCase()] = false);
|
||
|
||
// 子弹管理
|
||
let bullets = 10;
|
||
const maxBullets = 10;
|
||
const bulletDisplay = document.createElement('div');
|
||
bulletDisplay.className = 'ui';
|
||
bulletDisplay.style.bottom = '10px';
|
||
bulletDisplay.style.right = '10px';
|
||
bulletDisplay.innerText = `子弹: ${bullets}`;
|
||
document.body.appendChild(bulletDisplay);
|
||
|
||
// 生命值
|
||
let health = 100;
|
||
const healthDisplay = document.createElement('div');
|
||
healthDisplay.className = 'ui';
|
||
healthDisplay.style.bottom = '10px';
|
||
healthDisplay.style.left = '10px';
|
||
healthDisplay.innerText = `生命值: ${health}`;
|
||
document.body.appendChild(healthDisplay);
|
||
|
||
function deductHealth(amount) {
|
||
if (isGamePaused) return; // 如果已暂停,不再扣血
|
||
health -= amount;
|
||
healthDisplay.innerText = `生命值: ${health}`;
|
||
|
||
// 触发红色闪烁效果
|
||
const damageOverlay = document.getElementById('damageOverlay');
|
||
gsap.fromTo(
|
||
damageOverlay,
|
||
{ backgroundColor: 'rgba(255, 0, 0, 0.5)' },
|
||
{
|
||
backgroundColor: 'rgba(255, 0, 0, 0)',
|
||
duration: 0.5,
|
||
ease: 'power1.out'
|
||
}
|
||
);
|
||
|
||
if (health <= 0) {
|
||
endGame();
|
||
}
|
||
}
|
||
|
||
// 枪械展示
|
||
const gunTexture = new THREE.TextureLoader().load('./image/gun.png'); // 替换为实际枪械图片URL
|
||
const gunMaterial = new THREE.MeshBasicMaterial({ map: gunTexture, transparent: true });
|
||
const gunGeometry = new THREE.PlaneGeometry(1, 1);
|
||
const gun = new THREE.Mesh(gunGeometry, gunMaterial);
|
||
gun.position.set(0.5, -0.5, -1);
|
||
camera.add(gun);
|
||
scene.add(camera);
|
||
|
||
// 开枪函数
|
||
function shoot() {
|
||
if (bullets > 0) {
|
||
bullets--;
|
||
bulletDisplay.innerText = `子弹: ${bullets}`;
|
||
gsap.to(gun.position, { z: -1.2, duration: 0.05, yoyo: true, repeat: 1 });
|
||
|
||
const raycaster = new THREE.Raycaster();
|
||
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
|
||
const intersects = raycaster.intersectObjects(enemies.map(e => e.sprite));
|
||
|
||
if (intersects.length > 0) {
|
||
const enemy = intersects[0].object;
|
||
const hitDirection = new THREE.Vector3()
|
||
.subVectors(enemy.position, camera.position)
|
||
.normalize();
|
||
|
||
// 添加击飞效果
|
||
gsap.to(enemy.position, {
|
||
x: enemy.position.x + hitDirection.x * 5, // 沿射击方向飞5个单位
|
||
y: enemy.position.y + 2, // 向上飞2个单位
|
||
z: enemy.position.z + hitDirection.z * 5,
|
||
duration: 0.3, // 飞行时间0.3秒
|
||
ease: "power2.out", // 减速效果
|
||
onComplete: () => {
|
||
scene.remove(enemy); // 动画完成后移除敌人
|
||
enemies = enemies.filter(e => e.sprite !== enemy);
|
||
}
|
||
});
|
||
|
||
// 同时淡出敌人
|
||
gsap.to(enemy.material, {
|
||
opacity: 0,
|
||
duration: 0.3,
|
||
onComplete: () => {
|
||
enemy.material.opacity = 1; // 重置透明度以备复用
|
||
}
|
||
});
|
||
|
||
score += 20;
|
||
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
|
||
}
|
||
}
|
||
}
|
||
document.addEventListener('click', shoot);
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'r' && bullets < maxBullets) {
|
||
bullets = maxBullets;
|
||
bulletDisplay.innerText = `子弹: ${bullets}`;
|
||
}
|
||
});
|
||
|
||
// 地形生成
|
||
const noise = new SimplexNoise();
|
||
const terrainSize = 100;
|
||
const terrainDetail = 0.1;
|
||
const terrainHeight = 1; // 平缓地形
|
||
const geometry = new THREE.PlaneGeometry(terrainSize, terrainSize, 100, 100);
|
||
geometry.rotateX(-Math.PI / 2);
|
||
const vertices = geometry.attributes.position.array;
|
||
for (let i = 0; i < vertices.length; i += 3) {
|
||
const x = vertices[i];
|
||
const z = vertices[i + 2];
|
||
const y = noise.noise2D(x * terrainDetail, z * terrainDetail) * terrainHeight;
|
||
vertices[i + 1] = y;
|
||
}
|
||
geometry.computeVertexNormals();
|
||
const terrainMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
|
||
const terrain = new THREE.Mesh(geometry, terrainMaterial);
|
||
scene.add(terrain);
|
||
|
||
// 射线投射器
|
||
const raycaster = new THREE.Raycaster();
|
||
raycaster.ray.direction.set(0, -1, 0);
|
||
|
||
// 敌人生成(高度贴合地表)
|
||
let enemies = [];
|
||
class Enemy {
|
||
constructor() {
|
||
const enemyTextures = [
|
||
'./image/1.png',
|
||
'./image/2.png',
|
||
];
|
||
const texture = new THREE.TextureLoader().load(enemyTextures[Math.floor(Math.random() * enemyTextures.length)]);
|
||
const material = new THREE.SpriteMaterial({ map: texture });
|
||
this.sprite = new THREE.Sprite(material);
|
||
this.sprite.scale.set(2, 2, 1);
|
||
|
||
// 随机选择地图边缘(0: 上, 1: 下, 2: 左, 3: 右)
|
||
const edge = Math.floor(Math.random() * 4);
|
||
const halfSize = terrainSize / 2; // terrainSize = 100, halfSize = 50
|
||
|
||
switch (edge) {
|
||
case 0: // 上边缘 (z = 50)
|
||
this.sprite.position.set(
|
||
Math.random() * terrainSize - halfSize, // x: [-50, 50]
|
||
10, // y: 10
|
||
halfSize // z: 50
|
||
);
|
||
break;
|
||
case 1: // 下边缘 (z = -50)
|
||
this.sprite.position.set(
|
||
Math.random() * terrainSize - halfSize, // x: [-50, 50]
|
||
10, // y: 10
|
||
-halfSize // z: -50
|
||
);
|
||
break;
|
||
case 2: // 左边缘 (x = -50)
|
||
this.sprite.position.set(
|
||
-halfSize, // x: -50
|
||
10, // y: 10
|
||
Math.random() * terrainSize - halfSize // z: [-50, 50]
|
||
);
|
||
break;
|
||
case 3: // 右边缘 (x = 50)
|
||
this.sprite.position.set(
|
||
halfSize, // x: 50
|
||
10, // y: 10
|
||
Math.random() * terrainSize - halfSize // z: [-50, 50]
|
||
);
|
||
break;
|
||
}
|
||
|
||
scene.add(this.sprite);
|
||
this.sprite.material.opacity = 0;
|
||
gsap.to(this.sprite.material, { opacity: 1, duration: 1 });
|
||
}
|
||
|
||
update() {
|
||
this.sprite.lookAt(camera.position);
|
||
const direction = new THREE.Vector3().subVectors(camera.position, this.sprite.position).normalize();
|
||
this.sprite.position.add(direction.multiplyScalar(0.05));
|
||
|
||
// 贴合地表高度
|
||
raycaster.ray.origin.copy(this.sprite.position);
|
||
raycaster.ray.origin.y = 10; // 从上方检测
|
||
const intersects = raycaster.intersectObject(terrain);
|
||
if (intersects.length > 0) {
|
||
this.sprite.position.y = intersects[0].point.y + 1; // 高度为地表+1
|
||
}
|
||
|
||
if (this.sprite.position.distanceTo(camera.position) < 1) {
|
||
deductHealth(15);
|
||
scene.remove(this.sprite);
|
||
enemies = enemies.filter(e => e !== this);
|
||
}
|
||
}
|
||
}
|
||
|
||
setInterval(() => {
|
||
if (enemies.length < 10) {
|
||
const newEnemy = new Enemy();
|
||
enemies.push(newEnemy);
|
||
}
|
||
}, 2000); // 每2秒生成一个敌人
|
||
|
||
// 计时与得分
|
||
let timeLeft = 150;
|
||
let score = 0;
|
||
const timerDisplay = document.createElement('div');
|
||
timerDisplay.className = 'ui';
|
||
timerDisplay.style.top = '10px';
|
||
timerDisplay.style.left = '10px';
|
||
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
|
||
document.body.appendChild(timerDisplay);
|
||
|
||
setInterval(() => {
|
||
if (isGamePaused) return; // 如果暂停,不更新计时器
|
||
timeLeft--;
|
||
score++;
|
||
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
|
||
if (timeLeft <= 0) {
|
||
endGame();
|
||
}
|
||
}, 1000);
|
||
// 新增游戏结束函数
|
||
function endGame() {
|
||
isGamePaused = true;
|
||
document.exitPointerLock();
|
||
|
||
// 显示弹窗
|
||
const modal = document.getElementById('gameOverModal');
|
||
const finalScore = document.getElementById('finalScore');
|
||
finalScore.innerText = `最终得分: ${score}`;
|
||
modal.style.display = 'block';
|
||
}
|
||
// 新增重新开始函数
|
||
function restartGame() {
|
||
// 重置游戏状态
|
||
isGamePaused = false;
|
||
health = 100;
|
||
bullets = maxBullets;
|
||
timeLeft = 150;
|
||
score = 0;
|
||
enemies.forEach(enemy => scene.remove(enemy.sprite)); // 移除所有敌人
|
||
enemies = [];
|
||
|
||
// 更新UI
|
||
healthDisplay.innerText = `生命值: ${health}`;
|
||
bulletDisplay.innerText = `子弹: ${bullets}`;
|
||
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
|
||
|
||
// 隐藏弹窗
|
||
document.getElementById('gameOverModal').style.display = 'none';
|
||
|
||
// 重新启动动画循环
|
||
animate();
|
||
}
|
||
|
||
// 为重新开始按钮添加事件监听
|
||
document.getElementById('restartButton').addEventListener('click', restartGame);
|
||
|
||
// 天空贴图
|
||
const skyboxLoader = new THREE.CubeTextureLoader();
|
||
const skyboxTexture = skyboxLoader.load([
|
||
'https://i.imgur.com/px.jpg', 'https://i.imgur.com/nx.jpg',
|
||
'https://i.imgur.com/py.jpg', 'https://i.imgur.com/ny.jpg',
|
||
'https://i.imgur.com/pz.jpg', 'https://i.imgur.com/nz.jpg'
|
||
]); // 替换为实际天空贴图URL
|
||
scene.background = skyboxTexture;
|
||
|
||
// 动画循环
|
||
function animate() {
|
||
if (isGamePaused) return; // 如果暂停则跳出循环
|
||
requestAnimationFrame(animate);
|
||
|
||
// WASD移动
|
||
const speed = 0.1;
|
||
const frontVector = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
||
const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
|
||
if (keys['w']) camera.position.add(frontVector.multiplyScalar(speed));
|
||
if (keys['s']) camera.position.add(frontVector.multiplyScalar(-speed));
|
||
if (keys['a']) camera.position.add(rightVector.multiplyScalar(-speed));
|
||
if (keys['d']) camera.position.add(rightVector.multiplyScalar(speed));
|
||
|
||
// 贴合地形高度
|
||
raycaster.ray.origin.copy(camera.position);
|
||
raycaster.ray.origin.y += 1.6;
|
||
const intersects = raycaster.intersectObject(terrain);
|
||
if (intersects.length > 0) {
|
||
camera.position.y = intersects[0].point.y + 1.6;
|
||
}
|
||
|
||
// 更新敌人(仅在游戏未暂停时)
|
||
if (!isGamePaused) {
|
||
enemies.forEach(enemy => enemy.update());
|
||
}
|
||
|
||
renderer.render(scene, camera);
|
||
}
|
||
animate();
|
||
|
||
// 自适应窗口大小
|
||
window.addEventListener('resize', () => {
|
||
camera.aspect = window.innerWidth / window.innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |