<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>卡通生态拖动游戏</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="m-0 p-0 overflow-hidden bg-[#e6f7ff] select-none">
<div id="game-container" class="w-screen h-screen relative"></div>
<script>
const elementsConfig = [
{ type: 'grass', name: '青草', count: 5, size: 40 },
{ type: 'apple', name: '苹果', count: 5, size: 36 },
{ type: 'rabbit', name: '兔子', count: 3, size: 48 },
{ type: 'cow', name: '牛', count: 1, size: 60 },
{ type: 'fox', name: '狐狸', count: 1, size: 56 },
{ type: 'human', name: '人', count: 1, size: 52 }
];
const foodChain = {
grass: ['rabbit', 'cow'],
apple: ['cow', 'human'],
rabbit: ['fox', 'human'],
cow: ['human'],
fox: [],
human: []
};
const emojis = {
grass: '🌿',
apple: '🍎',
rabbit: '🐇',
cow: '🐄',
fox: '🦊',
human: '👨'
};
const elements = [];
let activeDrag = null; // 当前正在拖拽的元素
function initGame() {
const container = document.getElementById('game-container');
let id = 0;
elementsConfig.forEach(config => {
for (let i = 0; i < config.count; i++) {
const element = createElement(config.type, id++, config.size);
elements.push(element);
container.appendChild(element.element);
}
});
enableDragging();
startCollisionDetection();
}
function createElement(type, id, size) {
const element = document.createElement('div');
let left, top, attempts = 0;
const spacing = 20;
while (attempts < 100) {
left = Math.random() * (window.innerWidth - size);
top = Math.random() * (window.innerHeight - size);
let overlap = false;
for (const item of elements) {
const itemLeft = parseFloat(item.element.style.left);
const itemTop = parseFloat(item.element.style.top);
const dx = Math.abs(left - itemLeft);
const dy = Math.abs(top - itemTop);
if (dx < size + spacing && dy < size + spacing) {
overlap = true;
break;
}
}
if (!overlap) break;
attempts++;
}
Object.assign(element.style, {
position: 'absolute',
left: `${left}px`,
top: `${top}px`,
width: `${size}px`,
height: `${size}px`,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: `${size * 0.8}px`,
cursor: 'move',
transition: 'transform 0.3s, opacity 0.3s',
userSelect: 'none',
WebkitUserSelect: 'none',
zIndex: 10,
touchAction: 'none'
});
element.textContent = emojis[type];
element.dataset.type = type;
element.dataset.id = id;
return { element, type, id, size, active: true };
}
function enableDragging() {
// ---- 统一在 document 上处理移动和抬起,避免闭包泄漏 ----
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
document.addEventListener('touchmove', onDragMove, { passive: false });
document.addEventListener('touchend', onDragEnd);
document.addEventListener('touchcancel', onDragEnd);
elements.forEach(item => {
const el = item.element;
el.addEventListener('mousedown', e => startDrag(e, item));
el.addEventListener('touchstart', e => startDrag(e, item), { passive: false });
});
}
function getXY(e) {
// 同时兼容鼠标和触摸
if (e.touches && e.touches.length > 0) {
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
return { x: e.clientX, y: e.clientY };
}
function startDrag(e, item) {
if (!item.active) return;
e.preventDefault(); // 阻止默认滚动 / 文本选中
const { x, y } = getXY(e);
const rect = item.element.getBoundingClientRect();
activeDrag = {
item,
offsetX: x - rect.left,
offsetY: y - rect.top
};
item.element.style.zIndex = 20;
// 拖动时去掉 transition,避免延迟
item.element.style.transition = 'opacity 0.3s';
}
function onDragMove(e) {
if (!activeDrag) return;
e.preventDefault();
const { x, y } = getXY(e);
const { item, offsetX, offsetY } = activeDrag;
const maxX = window.innerWidth - item.size;
const maxY = window.innerHeight - item.size;
item.element.style.left = `${Math.max(0, Math.min(x - offsetX, maxX))}px`;
item.element.style.top = `${Math.max(0, Math.min(y - offsetY, maxY))}px`;
}
function onDragEnd() {
if (!activeDrag) return;
activeDrag.item.element.style.zIndex = 10;
activeDrag.item.element.style.transition = 'transform 0.3s, opacity 0.3s';
activeDrag = null;
}
// ---- 碰撞检测 ----
function startCollisionDetection() {
setInterval(checkAllCollisions, 50);
}
function checkAllCollisions() {
for (let i = 0; i < elements.length; i++) {
const a = elements[i];
if (!a.active) continue;
for (let j = i + 1; j < elements.length; j++) {
const b = elements[j];
if (!b.active) continue;
if (isColliding(a, b)) {
handleCorrectCollision(a, b);
}
}
}
}
function isColliding(a, b) {
const r1 = a.element.getBoundingClientRect();
const r2 = b.element.getBoundingClientRect();
return !(r1.right < r2.left || r1.left > r2.right || r1.bottom < r2.top || r1.top > r2.bottom);
}
function handleCorrectCollision(a, b) {
if (foodChain[a.type]?.includes(b.type)) {
vanish(a);
} else if (foodChain[b.type]?.includes(a.type)) {
vanish(b);
}
}
function vanish(item) {
if (!item.active) return;
item.active = false;
item.element.style.transform = 'scale(0)';
item.element.style.opacity = '0';
setTimeout(() => item.element.remove(), 300);
}
window.addEventListener('load', initGame);
</script>
</body>
</html>
index.html
md
README.md
index.html