This commit is contained in:
HuangHai
2026-01-25 18:17:37 +08:00
parent 19803f96a8
commit 1f6a48004b
10 changed files with 2113 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,87 @@
# coding=utf-8
import os
import sys
import time
import cv2
import uiautomator2 as u2
# 添加项目根目录到 sys.path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_root not in sys.path:
sys.path.append(project_root)
from WeiXin.WxUtil import find_all_template_matches
def run_cv_debug():
# 1. 拍照 (获取当前设备屏幕)
print("📸 正在连接设备并截取屏幕...")
try:
d = u2.connect()
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
image_path = os.path.join(screenshot_dir, "t6_live_shot.jpg")
output_path = os.path.join(screenshot_dir, "T6_debug_view.jpg")
d.screenshot(image_path)
print(f"✅ 截图已保存: {image_path}")
except Exception as e:
print(f"❌ 拍照失败: {e}")
return
print(f"🔍 正在分析实时图片...")
# 模板路径
audio_template = r"d:\dsWork\aiData\WeiXin\Templates\audio.jpg"
red_point_template = r"d:\dsWork\aiData\WeiXin\Templates\red_point.jpg"
if not os.path.exists(audio_template) or not os.path.exists(red_point_template):
print("错误: 模板文件不存在")
return
# 2. 识别逻辑
audio_matches = find_all_template_matches(image_path, audio_template, threshold=0.8)
red_points = find_all_template_matches(image_path, red_point_template, threshold=0.8)
print(f"发现语音图标数量: {len(audio_matches)}")
print(f"发现红点数量: {len(red_points)}")
# 3. 读取图片并绘制
img = cv2.imread(image_path)
if img is None:
print("错误: 无法读取图片")
return
for ax, ay in audio_matches:
# 排除顶部标题栏和底部输入区 (假设 300-1800 为有效区)
if ay < 300 or ay > 1800:
continue
sender = "对方" if ax < 500 else ""
is_unread = False
# 1. 绘制语音图标绿框 (根据模板大小,假设 60x60)
cv2.rectangle(img, (int(ax-30), int(ay-30)), (int(ax+30), int(ay+30)), (0, 255, 0), 3)
# 2. 绘制模拟点击红点 (用户要求用红点标出模拟点击位置)
cv2.circle(img, (int(ax), int(ay)), 15, (0, 0, 255), -1)
# 3. 检查并标记未读红点 (如果存在)
for rx, ry in red_points:
if abs(ry - ay) < 50 and rx > ax:
is_unread = True
# 绘制未读红点
cv2.circle(img, (int(rx), int(ry)), 12, (0, 0, 255), -1)
break
print(f"标注语音消息: ({ax}, {ay}), 发送者: {sender}, 未读: {is_unread}")
# 保存结果
cv2.imwrite(output_path, img)
print(f"✅ 调试图片已保存至: {output_path}")
if __name__ == "__main__":
run_cv_debug()

View File

@@ -15,7 +15,7 @@ if project_root not in sys.path:
from Util import Win32Patch
from WeiXin.WxUtil import perform_input_action, clean_screenshots_dir, is_in_chat_interface, find_template_match, find_all_template_matches
from WeiXin.WxUtil import perform_input_action, clean_screenshots_dir, find_template_match, find_all_template_matches
from Util.LlmUtil import get_llm_response
from Util.EasyOcrKit import EasyOcrKit
@@ -255,14 +255,8 @@ class ChatBot:
while True:
try:
# 0.5 检查是否在聊天界面
if not is_in_chat_interface(self.d):
logger.warning("📵 当前不在聊天界面,跳过扫描...")
await asyncio.sleep(CHECK_INTERVAL)
continue
logger.info("🔍 正在扫描当前界面内容...")
# 1. 截图
tmp_shot = os.path.join(self.screenshot_dir, "t6_monitor_temp.jpg")
logger.info(f"📸 正在截取屏幕... ({datetime.now().strftime('%H:%M:%S')})")
@@ -340,10 +334,10 @@ class ChatBot:
click_x, click_y = ax, ay
# 绘制视觉反馈
# 语音图标用绿框
# 1. 语音图标用绿框
cv2.rectangle(debug_img, (int(ax-30), int(ay-30)), (int(ax+30), int(ay+30)), (0, 255, 0), 3)
# 点击位置用红十字
cv2.drawMarker(debug_img, (int(click_x), int(click_y)), (0, 0, 255), cv2.MARKER_CROSS, 35, 3)
# 2. 点击位置用红点 (用户偏好)
cv2.circle(debug_img, (int(click_x), int(click_y)), 15, (0, 0, 255), -1)
v_msg = {
"type": "voice",

View File

@@ -0,0 +1,571 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🍲 地狱火锅 V3 - 谁是那块肉? 🍲</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&display=swap');
:root {
--soup-red: #d63031;
--soup-dark: #8b0000;
--pot-gold: #fdcb6e;
--steam-white: rgba(255, 255, 255, 0.3);
}
body {
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #fff;
font-family: 'ZCOOL KuaiLe', cursive, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
user-select: none;
}
h1 {
font-size: 3rem;
color: var(--pot-gold);
text-shadow: 3px 3px 0px #000, 0 0 20px #ff4757;
margin-bottom: 10px;
z-index: 100;
}
.subtitle {
font-size: 1.2rem;
color: #fab1a0;
margin-bottom: 20px;
z-index: 100;
}
/* 火锅容器 */
.pot-container {
position: relative;
width: 600px;
height: 600px;
background: radial-gradient(circle, var(--soup-red) 0%, var(--soup-dark) 70%, #000 100%);
border: 20px solid #57606f;
border-radius: 50%;
box-shadow: inset 0 0 50px rgba(0,0,0,0.8), 0 0 30px rgba(214, 48, 49, 0.5);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* 锅中间的隔断(鸳鸯锅效果) */
.pot-divider {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 8px solid #57606f;
pointer-events: none;
z-index: 5;
}
.pot-inner-rim {
position: absolute;
width: 90%;
height: 90%;
border: 2px solid rgba(255,255,255,0.1);
border-radius: 50%;
pointer-events: none;
}
/* 沸腾气泡 */
.bubble {
position: absolute;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
pointer-events: none;
animation: boil-bubble 2s infinite ease-in;
}
@keyframes boil-bubble {
0% { transform: translateY(0) scale(0); opacity: 0.5; }
50% { opacity: 0.8; }
100% { transform: translateY(-100px) scale(1.5); opacity: 0; }
}
/* 蒸汽效果 */
.steam {
position: absolute;
width: 200px;
height: 200px;
background: radial-gradient(circle, var(--steam-white) 0%, transparent 70%);
border-radius: 50%;
filter: blur(20px);
pointer-events: none;
z-index: 20;
animation: steam-move 4s infinite linear;
}
@keyframes steam-move {
0% { transform: translate(-50%, -50%) scale(1); opacity: 0; }
50% { opacity: 0.5; }
100% { transform: translate(50%, -150%) scale(2); opacity: 0; }
}
/* 食材(学生头像) */
.ingredient {
position: absolute;
width: 80px;
height: 80px;
border-radius: 50%;
border: 4px solid var(--pot-gold);
background-color: #fff;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
transition: transform 0.3s, opacity 0.5s, filter 0.5s;
cursor: pointer;
z-index: 10;
}
.ingredient img {
width: 100%;
height: 100%;
object-fit: cover;
}
.ingredient .label {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: rgba(0,0,0,0.7);
color: #fff;
font-size: 0.7rem;
text-align: center;
padding: 2px 0;
}
/* 选中状态 */
.ingredient.active {
transform: scale(1.2);
border-color: #fff;
box-shadow: 0 0 20px #fff;
z-index: 15;
}
/* 被捞走状态 */
.ingredient.removed {
transform: translateY(-200px) scale(0);
opacity: 0;
}
/* 控制按钮 */
#actionBtn {
margin-top: 30px;
padding: 15px 50px;
font-size: 1.8rem;
font-family: 'ZCOOL KuaiLe', cursive;
background: linear-gradient(to bottom, #ff7675, #d63031);
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 10px 0 #8b0000, 0 15px 25px rgba(0,0,0,0.5);
transition: all 0.1s;
z-index: 100;
}
#actionBtn:active {
transform: translateY(5px);
box-shadow: 0 5px 0 #8b0000, 0 10px 15px rgba(0,0,0,0.5);
}
#actionBtn:disabled {
background: #636e72;
box-shadow: 0 5px 0 #2d3436;
cursor: not-allowed;
}
/* 结果展示层 */
#resultOverlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.9);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.result-card {
background: #fff;
padding: 30px;
border-radius: 20px;
border: 10px solid var(--pot-gold);
display: flex;
flex-direction: column;
align-items: center;
transform: scale(0);
animation: popIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.5s forwards;
}
@keyframes popIn {
to { transform: scale(1); }
}
.result-image {
width: 250px;
height: 250px;
border-radius: 50%;
border: 8px solid var(--soup-red);
object-fit: cover;
margin-bottom: 20px;
}
.result-name {
font-size: 3rem;
color: var(--soup-red);
margin-bottom: 10px;
}
.result-title {
font-size: 1.5rem;
color: #636e72;
margin-bottom: 20px;
}
#closeBtn {
padding: 10px 30px;
background: var(--soup-red);
color: #fff;
border: none;
border-radius: 10px;
font-size: 1.2rem;
cursor: pointer;
font-family: 'ZCOOL KuaiLe', cursive;
}
/* 吐槽弹幕 */
.danmu {
position: absolute;
white-space: nowrap;
font-size: 1.2rem;
color: #fab1a0;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
pointer-events: none;
z-index: 50;
animation: danmu-move 5s linear forwards;
}
@keyframes danmu-move {
from { left: 100%; }
to { left: -100%; }
}
/* 筷子 */
.chopsticks {
position: absolute;
width: 10px;
height: 400px;
background: #747d8c;
border-radius: 5px;
z-index: 60;
pointer-events: none;
transform-origin: top center;
display: none;
}
.chopstick-left { transform: rotate(5deg); margin-left: -15px; }
.chopstick-right { transform: rotate(-5deg); margin-left: 15px; }
</style>
</head>
<body>
<h1>🍲 地狱火锅 V3 🍲</h1>
<div class="subtitle">—— 今天的你,是什么口味的食材? ——</div>
<div class="pot-container" id="pot">
<div class="pot-divider"></div>
<div class="pot-inner-rim"></div>
<!-- 气泡和食材将在这里生成 -->
</div>
<div class="chopsticks" id="chopstickLeft"></div>
<div class="chopsticks" id="chopstickRight"></div>
<button id="actionBtn" onclick="startCooking()">开始开涮!</button>
<div id="resultOverlay">
<div class="result-card">
<img src="" class="result-image" id="resultImg">
<div class="result-name" id="resultName">???</div>
<div class="result-title" id="resultTitle">美味食材</div>
<button id="closeBtn" onclick="resetPot()">再涮一锅</button>
</div>
</div>
<script>
const totalStudents = 14;
const imagePath = './Result/';
const pot = document.getElementById('pot');
const ingredients = [];
const ingredientNames = [
"雪花肥牛", "极品毛肚", "脆皮肠", "手打虾滑", "鲜嫩脑花",
"爽口裙带菜", "魔芋丝", "功夫土豆片", "大刀腰片", "鸭血",
"千层肚", "贡菜", "水晶粉", "虎皮凤爪"
];
const funnyComments = [
"火开大点!", "别抢我的肉!", "这个同学一看就很嫩", "哎呀烫手!",
"锅底不够辣啊", "谁把香菜放进来了?", "救命,我要被煮熟了",
"捞我!捞我!", "还没熟呢,再等等", "这是天选之肉"
];
// 音效系统
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(freq, type, duration, vol = 0.1) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function playBoilSound() {
// 低频噪音模拟沸腾
const bufferSize = audioCtx.sampleRate * 2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 100;
const gain = audioCtx.createGain();
gain.gain.value = 0.05;
noise.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
noise.start();
return { noise, gain };
}
// 初始化食材
function init() {
for (let i = 1; i <= totalStudents; i++) {
const div = document.createElement('div');
div.className = 'ingredient';
div.id = `student-${i}`;
// 随机位置
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 200;
const x = 300 + Math.cos(angle) * radius - 40;
const y = 300 + Math.sin(angle) * radius - 40;
div.style.left = x + 'px';
div.style.top = y + 'px';
const img = document.createElement('img');
img.src = `${imagePath}${i}.png`;
img.onerror = () => img.src = 'https://via.placeholder.com/80?text=Meat';
const label = document.createElement('div');
label.className = 'label';
label.innerText = ingredientNames[i-1];
div.appendChild(img);
div.appendChild(label);
pot.appendChild(div);
ingredients.push({
el: div,
x: x,
y: y,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
angle: angle,
radius: radius,
removed: false
});
}
// 生成背景气泡
for (let i = 0; i < 20; i++) {
createBubble();
}
animate();
startDanmu();
}
function createBubble() {
const b = document.createElement('div');
b.className = 'bubble';
const size = Math.random() * 20 + 5;
b.style.width = size + 'px';
b.style.height = size + 'px';
b.style.left = Math.random() * 100 + '%';
b.style.top = Math.random() * 100 + '%';
b.style.animationDelay = Math.random() * 2 + 's';
pot.appendChild(b);
}
function createSteam() {
const s = document.createElement('div');
s.className = 'steam';
s.style.left = Math.random() * 100 + '%';
s.style.top = Math.random() * 100 + '%';
pot.appendChild(s);
setTimeout(() => s.remove(), 4000);
}
function startDanmu() {
setInterval(() => {
if (Math.random() > 0.3) return;
const d = document.createElement('div');
d.className = 'danmu';
d.innerText = funnyComments[Math.floor(Math.random() * funnyComments.length)];
d.style.top = (Math.random() * 60 + 20) + '%';
document.body.appendChild(d);
setTimeout(() => d.remove(), 5000);
}, 2000);
}
let isCooking = false;
let speedMultiplier = 1;
function animate() {
ingredients.forEach(item => {
if (item.removed) return;
item.x += item.vx * speedMultiplier;
item.y += item.vy * speedMultiplier;
// 边界碰撞(锅是圆的)
const dx = item.x + 40 - 300;
const dy = item.y + 40 - 300;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist > 250) {
const nx = dx / dist;
const ny = dy / dist;
// 反射向量
const dot = item.vx * nx + item.vy * ny;
item.vx = (item.vx - 2 * dot * nx) * 0.9;
item.vy = (item.vy - 2 * dot * ny) * 0.9;
// 修正位置防止卡住
item.x = 300 + nx * 249 - 40;
item.y = 300 + ny * 249 - 40;
}
item.el.style.left = item.x + 'px';
item.el.style.top = item.y + 'px';
});
requestAnimationFrame(animate);
}
async function startCooking() {
if (isCooking) return;
isCooking = true;
document.getElementById('actionBtn').disabled = true;
document.getElementById('actionBtn').innerText = "火热开涮中...";
if (audioCtx.state === 'suspended') audioCtx.resume();
const boilSound = playBoilSound();
// 加速阶段
for (let i = 0; i < 20; i++) {
speedMultiplier += 0.2;
await new Promise(r => setTimeout(r, 50));
if (i % 5 === 0) playSound(200 + i * 10, 'sine', 0.1);
}
// 陆续淘汰阶段
const activeIngredients = ingredients.filter(i => !i.removed);
const totalSteps = activeIngredients.length - 1;
for (let i = 0; i < totalSteps; i++) {
const currentActive = ingredients.filter(i => !i.removed);
const randomIndex = Math.floor(Math.random() * currentActive.length);
const toRemove = currentActive[randomIndex];
toRemove.removed = true;
toRemove.el.classList.add('removed');
playSound(150, 'triangle', 0.3, 0.2);
createSteam();
// 间隔时间逐渐拉长
await new Promise(r => setTimeout(r, 300 + i * 100));
}
// 最后的胜者
const winner = ingredients.find(i => !i.removed);
showResult(winner);
// 停止声音
boilSound.gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1);
setTimeout(() => boilSound.noise.stop(), 1000);
}
function showResult(winner) {
const overlay = document.getElementById('resultOverlay');
const img = document.getElementById('resultImg');
const name = document.getElementById('resultName');
const title = document.getElementById('resultTitle');
const studentIdx = winner.el.id.split('-')[1];
img.src = `${imagePath}${studentIdx}.png`;
name.innerText = `天选之子: ${ingredientNames[studentIdx-1]}`;
const titles = ["真香!", "肉质鲜美!", "有点辣手", "极品食材", "嚼劲十足", "入口即化"];
title.innerText = titles[Math.floor(Math.random() * titles.length)];
overlay.style.display = 'flex';
playSound(523.25, 'square', 0.5, 0.2); // C5
setTimeout(() => playSound(659.25, 'square', 0.5, 0.2), 150); // E5
setTimeout(() => playSound(783.99, 'square', 1.0, 0.2), 300); // G5
}
function resetPot() {
document.getElementById('resultOverlay').style.display = 'none';
document.getElementById('actionBtn').disabled = false;
document.getElementById('actionBtn').innerText = "开始开涮!";
isCooking = false;
speedMultiplier = 1;
ingredients.forEach(item => {
item.removed = false;
item.el.classList.remove('removed');
// 随机重置位置
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 200;
item.x = 300 + Math.cos(angle) * radius - 40;
item.y = 300 + Math.sin(angle) * radius - 40;
item.vx = (Math.random() - 0.5) * 2;
item.vy = (Math.random() - 0.5) * 2;
});
}
window.onload = init;
</script>
</body>
</html>

View File

@@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎰 地狱扭蛋机 V4 - 非酋还是欧皇? 🎰</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&display=swap');
:root {
--machine-red: #ff4757;
--machine-dark: #c0392b;
--gold: #f1c40f;
--glass: rgba(255, 255, 255, 0.2);
}
body {
margin: 0;
padding: 0;
background-color: #2f3542;
color: #fff;
font-family: 'ZCOOL KuaiLe', cursive, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
h1 {
font-size: 2.5rem;
color: var(--gold);
text-shadow: 2px 2px 0px #000;
margin-bottom: 10px;
}
/* 扭蛋机外壳 */
.gacha-machine {
position: relative;
width: 400px;
height: 600px;
background: var(--machine-red);
border: 10px solid #2f3542;
border-radius: 50px 50px 20px 20px;
box-shadow: 0 20px 0 var(--machine-dark), 0 30px 50px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
align-items: center;
}
/* 透明视窗 */
.glass-dome {
position: relative;
width: 340px;
height: 300px;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.4) 0%, transparent 60%),
rgba(255,255,255,0.1);
border: 8px solid #333;
border-radius: 40px;
margin-top: 30px;
overflow: hidden;
box-shadow: inset 0 0 30px rgba(0,0,0,0.5);
}
/* 扭蛋球 */
.ball {
position: absolute;
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.3);
box-shadow: inset -5px -5px 10px rgba(0,0,0,0.3), 0 5px 10px rgba(0,0,0,0.2);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(2px);
}
.ball img {
width: 90%;
height: 90%;
border-radius: 50%;
object-fit: cover;
border: 1px solid rgba(255,255,255,0.5);
}
/* 扭蛋机下半部 */
.controls {
position: relative;
width: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
padding: 20px 0;
}
/* 旋转摇杆 */
.handle-container {
position: relative;
width: 120px;
height: 120px;
background: #dfe6e9;
border-radius: 50%;
border: 8px solid #636e72;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.5s;
}
.handle-bar {
width: 80%;
height: 20px;
background: #636e72;
border-radius: 10px;
position: relative;
}
.handle-bar::before, .handle-bar::after {
content: '';
position: absolute;
width: 30px;
height: 30px;
background: #2d3436;
border-radius: 50%;
top: -5px;
}
.handle-bar::before { left: -10px; }
.handle-bar::after { right: -10px; }
.handle-container.spinning {
animation: spin-handle 0.5s linear infinite;
}
@keyframes spin-handle {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 出蛋口 */
.exit-chute {
width: 100px;
height: 60px;
background: #2d3436;
border-radius: 10px 10px 0 0;
position: relative;
box-shadow: inset 0 10px 10px rgba(0,0,0,0.8);
}
.exit-chute::after {
content: 'PUSH';
position: absolute;
bottom: 5px;
width: 100%;
text-align: center;
font-size: 0.8rem;
color: #636e72;
}
/* 结果弹窗 */
#resultOverlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.95);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.card-container {
perspective: 1000px;
width: 300px;
height: 450px;
}
.card {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.8s;
animation: card-reveal 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
@keyframes card-reveal {
0% { transform: scale(0) rotateY(0deg); }
100% { transform: scale(1) rotateY(720deg); }
}
.card-front {
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f1c40f 0%, #f39c12 100%);
border: 10px solid #fff;
border-radius: 20px;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
box-sizing: border-box;
box-shadow: 0 0 50px rgba(241, 196, 15, 0.5);
}
.rarity-badge {
position: absolute;
top: 10px;
right: 10px;
background: #ff4757;
color: white;
padding: 5px 15px;
border-radius: 20px;
font-size: 1.5rem;
font-weight: bold;
transform: rotate(15deg);
box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
animation: flash 0.5s infinite alternate;
}
@keyframes flash {
from { opacity: 0.8; transform: rotate(15deg) scale(1); }
to { opacity: 1; transform: rotate(15deg) scale(1.1); }
}
.student-photo {
width: 200px;
height: 200px;
border-radius: 15px;
border: 5px solid #fff;
object-fit: cover;
margin-top: 40px;
}
.student-name {
font-size: 2.5rem;
color: #2d3436;
margin-top: 20px;
}
.student-desc {
font-size: 1rem;
color: #636e72;
text-align: center;
margin-top: 10px;
}
#closeBtn {
margin-top: 30px;
padding: 10px 40px;
background: var(--gold);
color: #2d3436;
border: none;
border-radius: 10px;
font-size: 1.5rem;
cursor: pointer;
font-family: 'ZCOOL KuaiLe', cursive;
}
/* 提示文字 */
.hint {
margin-top: 20px;
color: #fab1a0;
font-size: 1.2rem;
animation: bounce 1s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
</style>
</head>
<body>
<h1>🎰 地狱扭蛋机 V4 🎰</h1>
<div class="gacha-machine">
<div class="glass-dome" id="glassDome">
<!-- 扭蛋球将由JS生成 -->
</div>
<div class="controls">
<div class="handle-container" id="handle" onclick="drawGacha()">
<div class="handle-bar"></div>
</div>
<div class="exit-chute"></div>
</div>
</div>
<div class="hint">点击摇杆,抽取今日“欧皇”!</div>
<div id="resultOverlay">
<div class="card-container">
<div class="card">
<div class="card-front">
<div class="rarity-badge" id="rarity">SSR</div>
<img src="" class="student-photo" id="resultImg">
<div class="student-name" id="resultName">???</div>
<div class="student-desc" id="resultDesc">这是一个极其稀有的灵魂</div>
</div>
</div>
</div>
<button id="closeBtn" onclick="hideResult()">收下并返回</button>
</div>
<script>
const totalStudents = 14;
const imagePath = './Result/';
const ballColors = ['#ff7675', '#74b9ff', '#55efc4', '#ffeaa7', '#a29bfe', '#fd79a8'];
const balls = [];
const glassDome = document.getElementById('glassDome');
const rarities = [
{ name: 'N', color: '#bdc3c7', desc: '平平无奇的小可爱' },
{ name: 'R', color: '#3498db', desc: '有点意思的灵魂' },
{ name: 'SR', color: '#9b59b6', desc: '闪闪发光的候选人' },
{ name: 'SSR', color: '#f1c40f', desc: '传说中的天选之子' },
{ name: 'UR', color: '#e74c3c', desc: '绝无仅有的地狱之皇' }
];
// 音效系统
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(freq, type, duration, vol = 0.1) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function init() {
for (let i = 0; i < 20; i++) {
const ball = document.createElement('div');
ball.className = 'ball';
// 添加学生头像
const img = document.createElement('img');
const studentId = (i % totalStudents) + 1;
img.src = `${imagePath}${studentId}.png`;
img.onerror = () => img.src = 'https://via.placeholder.com/60?text=?';
ball.appendChild(img);
const x = Math.random() * 280;
const y = Math.random() * 240;
ball.style.left = x + 'px';
ball.style.top = y + 'px';
// 为扭蛋壳添加随机透明色
const color = ballColors[Math.floor(Math.random() * ballColors.length)];
ball.style.backgroundColor = color + '66'; // 增加透明度
ball.style.borderColor = color;
glassDome.appendChild(ball);
balls.push({
el: ball,
x: x,
y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6
});
}
animate();
}
let isSpinning = false;
let shakeIntensity = 0;
function animate() {
balls.forEach(ball => {
ball.x += ball.vx * (1 + shakeIntensity);
ball.y += ball.vy * (1 + shakeIntensity);
if (ball.x < 0 || ball.x > 280) ball.vx *= -1;
if (ball.y < 0 || ball.y > 240) ball.vy *= -1;
ball.el.style.left = ball.x + 'px';
ball.el.style.top = ball.y + 'px';
});
requestAnimationFrame(animate);
}
async function drawGacha() {
if (isSpinning) return;
isSpinning = true;
if (audioCtx.state === 'suspended') audioCtx.resume();
const handle = document.getElementById('handle');
handle.classList.add('spinning');
// 震动效果
shakeIntensity = 5;
for (let i = 0; i < 10; i++) {
playSound(100 + i * 50, 'square', 0.1, 0.05);
await new Promise(r => setTimeout(r, 100));
}
shakeIntensity = 0;
handle.classList.remove('spinning');
// 抽取结果
const studentIdx = Math.floor(Math.random() * totalStudents) + 1;
const rarityIdx = Math.random() < 0.1 ? 4 : (Math.random() < 0.2 ? 3 : (Math.random() < 0.3 ? 2 : (Math.random() < 0.4 ? 1 : 0)));
showResult(studentIdx, rarityIdx);
}
function showResult(idx, rarityIdx) {
const overlay = document.getElementById('resultOverlay');
const img = document.getElementById('resultImg');
const name = document.getElementById('resultName');
const rarity = document.getElementById('rarity');
const desc = document.getElementById('resultDesc');
const rarityData = rarities[rarityIdx];
img.src = `${imagePath}${idx}.png`;
img.onerror = () => img.src = 'https://via.placeholder.com/200?text=Student';
name.innerText = `同学 ${idx}`; // 这里可以根据需要替换成真实姓名列表
rarity.innerText = rarityData.name;
rarity.style.backgroundColor = rarityData.color;
desc.innerText = rarityData.desc;
overlay.style.display = 'flex';
// 胜利音效
if (rarityIdx >= 3) {
// SSR/UR 华丽音效
playSound(523, 'sawtooth', 0.5, 0.1);
setTimeout(() => playSound(659, 'sawtooth', 0.5, 0.1), 100);
setTimeout(() => playSound(783, 'sawtooth', 0.8, 0.1), 200);
} else {
playSound(440, 'sine', 0.5, 0.1);
}
}
function hideResult() {
document.getElementById('resultOverlay').style.display = 'none';
isSpinning = false;
}
window.onload = init;
</script>
</body>
</html>

View File

@@ -0,0 +1,435 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔥 地狱炙烤 V8 - 谁是最后那块肉? 🔥</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&display=swap');
:root {
--fire-orange: #ff9f43;
--fire-red: #ee5253;
--charcoal: #2d3436;
--danger-bg: #1a1a1a;
}
body {
margin: 0;
padding: 0;
background-color: var(--danger-bg);
color: #fff;
font-family: 'ZCOOL KuaiLe', cursive, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
user-select: none;
}
h1 {
font-size: 3rem;
color: var(--fire-orange);
text-shadow: 0 0 20px var(--fire-red);
margin-bottom: 10px;
z-index: 100;
}
/* 心理压力:温度计 */
.temp-gauge {
width: 600px;
height: 30px;
background: #000;
border: 3px solid #555;
border-radius: 15px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
z-index: 100;
}
#tempFill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #ff9f43, #ee5253, #ff0000);
transition: width 0.5s linear;
}
.temp-label {
position: absolute;
width: 100%;
text-align: center;
line-height: 30px;
font-size: 1.2rem;
font-weight: bold;
color: #fff;
text-shadow: 1px 1px 2px #000;
}
/* 烤炉区域 */
.grill-area {
position: relative;
width: 800px;
height: 500px;
background: radial-gradient(circle at center, #3d3d3d 0%, #1a1a1a 100%);
border: 15px solid #444;
border-radius: 20px;
display: flex;
flex-wrap: wrap;
align-content: center;
justify-content: center;
gap: 20px;
padding: 20px;
box-shadow: inset 0 0 100px rgba(255, 69, 0, 0.2);
}
/* 铁丝网效果 */
.grill-area::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image:
linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px),
linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
}
/* 学生头像(肉块) */
.student-meat {
position: relative;
width: 120px;
height: 120px;
border-radius: 15px;
border: 4px solid #555;
background: #fff;
overflow: hidden;
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 10;
}
.student-meat img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 危险状态:颤抖 */
.student-meat.danger {
animation: shake 0.1s infinite;
border-color: var(--fire-red);
box-shadow: 0 0 20px var(--fire-red);
}
@keyframes shake {
0% { transform: translate(1px, 1px) rotate(0deg); }
20% { transform: translate(-1px, -2px) rotate(-1deg); }
40% { transform: translate(-3px, 0px) rotate(1deg); }
60% { transform: translate(3px, 2px) rotate(0deg); }
80% { transform: translate(1px, -1px) rotate(1deg); }
100% { transform: translate(-1px, 2px) rotate(-1deg); }
}
/* 淘汰状态:被烤焦 */
.student-meat.charred {
filter: grayscale(1) brightness(0.2) blur(2px);
transform: scale(0.8);
opacity: 0.5;
border-color: #000;
}
/* 幸存状态:飞走 */
.student-meat.saved {
transform: translateY(-500px) rotate(360deg) scale(0);
opacity: 0;
}
/* 火焰特效层 */
.fire-layer {
position: absolute;
bottom: -50px;
left: 0;
width: 100%;
height: 200px;
background: url('https://www.transparenttextures.com/patterns/carbon-fibre.png'); /* 占位背景 */
pointer-events: none;
z-index: 5;
opacity: 0;
transition: opacity 2s;
}
/* 最终结果弹窗 */
#resultOverlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.95);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.roasted-card {
position: relative;
background: #000;
padding: 40px;
border-radius: 30px;
border: 10px solid var(--fire-orange);
text-align: center;
box-shadow: 0 0 100px var(--fire-red);
animation: emerge 1s forwards;
}
@keyframes emerge {
from { transform: scale(0) rotate(-20deg); }
to { transform: scale(1) rotate(0deg); }
}
.roasted-img {
width: 300px;
height: 300px;
border-radius: 20px;
border: 5px solid #fff;
object-fit: cover;
}
.roasted-title {
font-size: 4rem;
color: var(--fire-red);
margin: 20px 0;
text-shadow: 0 0 10px #fff;
}
/* 控制按钮 */
#startBtn {
margin-top: 30px;
padding: 15px 60px;
font-size: 2rem;
background: linear-gradient(to bottom, #ff9f43, #ee5253);
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 10px 0 #8b0000;
transition: all 0.1s;
z-index: 100;
}
#startBtn:active {
transform: translateY(5px);
box-shadow: 0 5px 0 #8b0000;
}
#startBtn:disabled {
background: #555;
box-shadow: 0 5px 0 #222;
cursor: not-allowed;
}
/* 吐槽气泡 */
.bubble {
position: absolute;
background: white;
color: black;
padding: 5px 10px;
border-radius: 10px;
font-size: 0.9rem;
top: -40px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
pointer-events: none;
animation: bounceIn 0.3s forwards;
}
@keyframes bounceIn {
from { transform: translateX(-50%) scale(0); }
to { transform: translateX(-50%) scale(1); }
}
</style>
</head>
<body>
<h1>🔥 地狱炙烤 V8 🔥</h1>
<div class="temp-gauge">
<div id="tempFill"></div>
<div class="temp-label">当前温度: <span id="tempVal">25</span>°C</div>
</div>
<div class="grill-area" id="grill">
<!-- 学生肉块将由JS生成 -->
</div>
<button id="startBtn" onclick="startRoasting()">开始加火!</button>
<div id="resultOverlay">
<div class="roasted-card">
<div style="font-size: 2rem; color: #ff9f43;">恭喜!这块肉烤得恰到好处!</div>
<img src="" class="roasted-img" id="resultImg">
<div class="roasted-title" id="resultName">???</div>
<button id="startBtn" onclick="location.reload()" style="background: #27ae60; box-shadow: 0 10px 0 #1e8449;">再烤一锅</button>
</div>
</div>
<script>
const totalStudents = 14;
const imagePath = './Result/';
const grill = document.getElementById('grill');
const students = [];
const scaryQuotes = ["好烫!好烫!", "救命啊!", "我不想被烤!", "火太大了!", "轮到谁了?", "鸭梨山大...", "感觉要熟了"];
// 音效系统
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(freq, type, duration, vol = 0.1) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
// 心跳/节奏音
let heartbeatInterval;
function startHeartbeat(bpm) {
clearInterval(heartbeatInterval);
heartbeatInterval = setInterval(() => {
playSound(150, 'sine', 0.1, 0.2);
setTimeout(() => playSound(100, 'sine', 0.1, 0.2), 150);
}, (60 / bpm) * 1000);
}
function init() {
for (let i = 1; i <= totalStudents; i++) {
const meat = document.createElement('div');
meat.className = 'student-meat';
meat.id = `meat-${i}`;
const img = document.createElement('img');
img.src = `${imagePath}${i}.png`;
img.onerror = () => img.src = 'https://via.placeholder.com/120?text=Meat';
meat.appendChild(img);
grill.appendChild(meat);
students.push({
el: meat,
id: i,
status: 'raw' // raw, charred, saved, roasted
});
}
}
let temperature = 25;
let isRoasting = false;
async function startRoasting() {
if (isRoasting) return;
isRoasting = true;
document.getElementById('startBtn').disabled = true;
if (audioCtx.state === 'suspended') audioCtx.resume();
// 1. 心理压力启动:心跳声和温度上升
let bpm = 60;
startHeartbeat(bpm);
// 温度上升动画
const tempFill = document.getElementById('tempFill');
const tempVal = document.getElementById('tempVal');
// 陆续排除过程
const aliveList = [...students];
while (aliveList.length > 1) {
// 提高温度和心跳频率
temperature += Math.floor(Math.random() * 50) + 20;
bpm += 20;
startHeartbeat(bpm);
tempFill.style.width = (temperature / 1000 * 100) + '%';
tempVal.innerText = temperature;
// 让所有人颤抖
aliveList.forEach(s => s.el.classList.add('danger'));
// 随机吐槽
if (Math.random() > 0.5) {
const randomS = aliveList[Math.floor(Math.random() * aliveList.length)];
showBubble(randomS.el, scaryQuotes[Math.floor(Math.random() * scaryQuotes.length)]);
}
// 心理压力:随机停顿时间(越来越长)
const waitTime = 1000 + (14 - aliveList.length) * 300;
await new Promise(r => setTimeout(r, waitTime));
// 排除一个:要么被救走,要么被烤焦(视觉上只是排除)
const removeIdx = Math.floor(Math.random() * aliveList.length);
const removed = aliveList.splice(removeIdx, 1)[0];
if (Math.random() > 0.5) {
removed.el.classList.remove('danger');
removed.el.classList.add('saved');
playSound(440, 'sine', 0.5, 0.1); // 逃生成功音
} else {
removed.el.classList.remove('danger');
removed.el.classList.add('charred');
playSound(200, 'sawtooth', 0.5, 0.1); // 被淘汰音
}
}
// 2. 最终时刻:只剩一人
const winner = aliveList[0];
temperature = 999;
tempFill.style.width = '100%';
tempVal.innerText = temperature;
// 最后的挣扎
winner.el.style.transform = 'scale(1.5)';
showBubble(winner.el, "不!!!!!!");
startHeartbeat(240); // 极速心跳
await new Promise(r => setTimeout(r, 2000));
// 终极火焰
clearInterval(heartbeatInterval);
playSound(100, 'noise', 2, 0.3); // 喷火声
showResult(winner.id);
}
function showBubble(parent, text) {
const b = document.createElement('div');
b.className = 'bubble';
b.innerText = text;
parent.appendChild(b);
setTimeout(() => b.remove(), 1000);
}
function showResult(id) {
const overlay = document.getElementById('resultOverlay');
const img = document.getElementById('resultImg');
const name = document.getElementById('resultName');
img.src = `${imagePath}${id}.png`;
name.innerText = `玩家 ${id} 熟透了!`;
overlay.style.display = 'flex';
// 最终音效
playSound(523, 'sawtooth', 0.5);
setTimeout(() => playSound(392, 'sawtooth', 0.5), 200);
setTimeout(() => playSound(523, 'sawtooth', 1.0), 400);
}
window.onload = init;
</script>
</body>
</html>

View File

@@ -0,0 +1,556 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧸 地狱抓娃娃机 V6 - 抓到谁就是谁! 🧸</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&display=swap');
:root {
--machine-pink: #fd79a8;
--machine-purple: #a29bfe;
--neon-blue: #00d2ff;
--neon-pink: #ff00ff;
}
body {
margin: 0;
padding: 0;
background-color: #2d3436;
color: #fff;
font-family: 'ZCOOL KuaiLe', cursive, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
h1 {
font-size: 2.5rem;
color: #fff;
text-shadow: 0 0 10px var(--neon-blue), 0 0 20px var(--neon-pink);
margin-bottom: 20px;
}
/* 娃娃机主体 */
.claw-machine {
position: relative;
width: 500px;
height: 650px;
background: #34495e;
border: 15px solid #2c3e50;
border-radius: 20px;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 玻璃展示区 */
.display-area {
position: relative;
width: 100%;
height: 450px;
background: linear-gradient(to bottom, #1e272e, #2f3542);
overflow: hidden;
border-bottom: 10px solid #2c3e50;
}
/* 天花板轨道 */
.rail {
position: absolute;
top: 10px;
left: 50px;
right: 50px;
height: 10px;
background: #7f8c8d;
border-radius: 5px;
}
/* 爪子组件 */
.claw-assembly {
position: absolute;
top: 10px;
left: 50px;
width: 60px;
transition: left 0.5s ease;
z-index: 100;
}
/* 激光射线 */
.laser-beam {
position: absolute;
top: 80px;
left: 50%;
width: 4px;
height: 0;
background: linear-gradient(to bottom, #ff0000, transparent);
transform: translateX(-50%);
z-index: 90;
display: none;
box-shadow: 0 0 10px #ff0000;
}
.laser-beam.firing {
display: block;
height: 400px;
animation: laserFlash 0.1s infinite;
}
@keyframes laserFlash {
0% { opacity: 0.8; width: 4px; }
50% { opacity: 1; width: 8px; }
100% { opacity: 0.8; width: 4px; }
}
.claw-string {
position: absolute;
top: 0;
left: 50%;
width: 4px;
height: 50px;
background: #bdc3c7;
transform: translateX(-50%);
transition: height 1s ease;
}
.claw-head {
position: absolute;
top: 50px;
left: 50%;
width: 40px;
height: 30px;
background: #ecf0f1;
transform: translateX(-50%);
border-radius: 10px 10px 0 0;
transition: top 1s ease;
}
.claw-arm {
position: absolute;
width: 10px;
height: 30px;
background: #ecf0f1;
top: 20px;
transition: transform 0.3s;
}
.arm-left { left: 0; transform-origin: top right; transform: rotate(30deg); }
.arm-right { right: 0; transform-origin: top left; transform: rotate(-30deg); }
.claw-assembly.grabbing .arm-left { transform: rotate(0deg); }
.claw-assembly.grabbing .arm-right { transform: rotate(0deg); }
/* 娃娃 (学生) */
.doll {
position: absolute;
width: 70px;
height: 70px;
border-radius: 15px;
border: 4px solid var(--neon-blue);
background: #fff;
overflow: hidden;
transition: transform 0.1s;
}
.doll img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 娃娃消失动画 */
.doll.eliminated {
animation: explode 0.5s forwards;
pointer-events: none;
}
@keyframes explode {
0% { transform: scale(1) rotate(0deg); filter: brightness(1); }
50% { transform: scale(1.5) rotate(20deg); filter: brightness(5) saturate(2); opacity: 1; }
100% { transform: scale(0) rotate(-45deg); opacity: 0; }
}
/* 瞄准红点 */
.target-dot {
position: absolute;
width: 10px;
height: 10px;
background: #ff0000;
border-radius: 50%;
box-shadow: 0 0 10px #ff0000;
display: none;
z-index: 80;
animation: blink 0.2s infinite;
}
@keyframes blink {
0% { opacity: 0; }
100% { opacity: 1; }
}
/* 操作面板 */
.control-panel {
flex-grow: 1;
background: var(--machine-pink);
display: flex;
align-items: center;
justify-content: space-around;
padding: 20px;
}
.joystick-base {
width: 80px;
height: 80px;
background: #c0392b;
border-radius: 50%;
position: relative;
box-shadow: inset 0 5px 10px rgba(0,0,0,0.5);
}
.joystick-stick {
width: 20px;
height: 60px;
background: #7f8c8d;
position: absolute;
bottom: 40px;
left: 30px;
border-radius: 10px;
transform-origin: bottom center;
transition: transform 0.2s;
}
.joystick-ball {
width: 40px;
height: 40px;
background: #e74c3c;
border-radius: 50%;
position: absolute;
top: -30px;
left: -10px;
box-shadow: 0 5px 0 #c0392b;
}
.btn-grab {
width: 100px;
height: 100px;
background: #f1c40f;
border: none;
border-radius: 50%;
color: #2d3436;
font-family: 'ZCOOL KuaiLe', cursive;
font-size: 1.5rem;
cursor: pointer;
box-shadow: 0 8px 0 #d4ac0d, 0 10px 20px rgba(0,0,0,0.3);
transition: all 0.1s;
}
.btn-grab:active {
transform: translateY(4px);
box-shadow: 0 4px 0 #d4ac0d;
}
.btn-grab:disabled {
background: #bdc3c7;
box-shadow: 0 4px 0 #95a5a6;
cursor: not-allowed;
}
/* 结果展示 */
#resultOverlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.9);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.prize-card {
background: #fff;
padding: 30px;
border-radius: 20px;
border: 10px solid var(--neon-blue);
text-align: center;
animation: bounceIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes bounceIn {
0% { transform: scale(0); }
100% { transform: scale(1); }
}
.prize-img {
width: 250px;
height: 250px;
border-radius: 15px;
object-fit: cover;
margin-bottom: 20px;
}
.prize-name {
font-size: 3rem;
color: #2d3436;
margin-bottom: 20px;
}
#closeBtn {
padding: 10px 40px;
background: var(--neon-blue);
color: #fff;
border: none;
border-radius: 10px;
font-size: 1.5rem;
cursor: pointer;
font-family: 'ZCOOL KuaiLe', cursive;
}
/* 装饰霓虹灯 */
.neon-light {
position: absolute;
width: 100%;
height: 5px;
background: var(--neon-blue);
box-shadow: 0 0 10px var(--neon-blue);
top: 0;
}
</style>
</head>
<body>
<h1>🧸 地狱抓娃娃机 V6 🧸</h1>
<div class="claw-machine">
<div class="neon-light"></div>
<div class="display-area" id="displayArea">
<div class="rail"></div>
<div class="target-dot" id="targetDot"></div>
<!-- 爪子 -->
<div class="claw-assembly" id="claw">
<div class="laser-beam" id="laserBeam"></div>
<div class="claw-string" id="clawString"></div>
<div class="claw-head" id="clawHead">
<div class="claw-arm arm-left"></div>
<div class="claw-arm arm-right"></div>
</div>
</div>
<!-- 娃娃将由JS生成 -->
</div>
<div class="control-panel">
<div class="joystick-base">
<div class="joystick-stick" id="joystick">
<div class="joystick-ball"></div>
</div>
</div>
<button class="btn-grab" id="grabBtn" onclick="startGrabbing()">抓取!</button>
</div>
</div>
<div id="resultOverlay">
<div class="prize-card">
<div style="font-size: 1.5rem; color: var(--neon-blue); margin-bottom: 10px;">恭喜抓到幸运儿!</div>
<img src="" class="prize-img" id="resultImg">
<div class="prize-name" id="resultName">???</div>
<button id="closeBtn" onclick="resetMachine()">放回去重抓</button>
</div>
</div>
<script>
const totalStudents = 14;
const imagePath = './Result/';
const displayArea = document.getElementById('displayArea');
const dolls = [];
let isGrabbing = false;
// 音效系统
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(freq, type, duration, vol = 0.1) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function init() {
for (let i = 1; i <= totalStudents; i++) {
const doll = document.createElement('div');
doll.className = 'doll';
doll.id = `doll-${i}`;
// 底部堆叠
const x = 50 + Math.random() * 330;
const y = 350 + (Math.random() - 0.5) * 30;
doll.style.left = x + 'px';
doll.style.top = y + 'px';
doll.style.transform = `rotate(${(Math.random()-0.5)*30}deg)`;
const img = document.createElement('img');
img.src = `${imagePath}${i}.png`;
img.onerror = () => img.src = 'https://via.placeholder.com/70?text=Doll';
doll.appendChild(img);
displayArea.appendChild(doll);
dolls.push({
el: doll,
id: i,
x: x,
y: y
});
}
}
async function startGrabbing() {
if (isGrabbing) return;
isGrabbing = true;
document.getElementById('grabBtn').disabled = true;
if (audioCtx.state === 'suspended') audioCtx.resume();
const claw = document.getElementById('claw');
const clawString = document.getElementById('clawString');
const clawHead = document.getElementById('clawHead');
const joystick = document.getElementById('joystick');
const laser = document.getElementById('laserBeam');
const targetDot = document.getElementById('targetDot');
// 1. 左右扫描 (制造恐慌)
const scanTimes = 3;
for(let i=0; i<scanTimes; i++) {
const scanX = 50 + Math.random() * 330;
claw.style.left = scanX + 'px';
joystick.style.transform = scanX > 200 ? 'rotate(20deg)' : 'rotate(-20deg)';
playSound(200 + i*100, 'sine', 0.3, 0.05);
// 扫描过程中随机开火
if (Math.random() > 0.4) {
await new Promise(r => setTimeout(r, 400));
await fireLaser(scanX);
}
await new Promise(r => setTimeout(r, 600));
}
// 2. 最终锁定目标
const aliveDolls = dolls.filter(d => !d.eliminated);
if (aliveDolls.length === 0) {
alert("所有人都被消灭了... 地狱之门重新开启!");
location.reload();
return;
}
const targetDoll = aliveDolls[Math.floor(Math.random() * aliveDolls.length)];
const targetX = targetDoll.x;
claw.style.left = targetX + 'px';
joystick.style.transform = targetX > 200 ? 'rotate(20deg)' : 'rotate(-20deg)';
await new Promise(r => setTimeout(r, 800));
joystick.style.transform = 'rotate(0deg)';
// 3. 下降
clawString.style.height = '350px';
clawHead.style.top = '350px';
playSound(300, 'sine', 1, 0.05);
await new Promise(r => setTimeout(r, 1000));
// 4. 抓取动作
claw.classList.add('grabbing');
playSound(150, 'square', 0.2, 0.1);
await new Promise(r => setTimeout(r, 300));
// 5. 上升
targetDoll.el.style.transition = 'top 1s ease, left 1s ease';
targetDoll.el.style.zIndex = '150';
targetDoll.el.style.top = '50px';
targetDoll.el.style.left = (targetX - 5) + 'px';
clawString.style.height = '50px';
clawHead.style.top = '50px';
await new Promise(r => setTimeout(r, 1000));
// 6. 展示结果
showResult(targetDoll.id);
}
async function fireLaser(x) {
const laser = document.getElementById('laserBeam');
const targetDot = document.getElementById('targetDot');
// 显示红点
targetDot.style.left = (x + 25) + 'px';
targetDot.style.top = '380px';
targetDot.style.display = 'block';
playSound(800, 'sine', 0.1, 0.2);
await new Promise(r => setTimeout(r, 300));
// 发射激光
laser.classList.add('firing');
playSound(100, 'sawtooth', 0.3, 0.3);
// 判定是否击中
const hitDoll = dolls.find(d => !d.eliminated && Math.abs(d.x - x) < 40);
if (hitDoll) {
hitDoll.eliminated = true;
hitDoll.el.classList.add('eliminated');
playSound(50, 'noise', 0.5, 0.5);
}
await new Promise(r => setTimeout(r, 200));
laser.classList.remove('firing');
targetDot.style.display = 'none';
}
function showResult(id) {
const overlay = document.getElementById('resultOverlay');
const img = document.getElementById('resultImg');
const name = document.getElementById('resultName');
img.src = `${imagePath}${id}.png`;
name.innerText = `玩家 ${id}`;
overlay.style.display = 'flex';
playSound(523, 'sine', 0.3);
setTimeout(() => playSound(659, 'sine', 0.3), 150);
setTimeout(() => playSound(783, 'sine', 0.5), 300);
}
function resetMachine() {
document.getElementById('resultOverlay').style.display = 'none';
document.getElementById('grabBtn').disabled = false;
isGrabbing = false;
const claw = document.getElementById('claw');
claw.classList.remove('grabbing');
// 重置娃娃位置
dolls.forEach(d => {
d.el.style.transition = 'none';
d.el.style.zIndex = '10';
d.el.classList.remove('eliminated');
d.eliminated = false;
const x = 50 + Math.random() * 330;
const y = 350 + (Math.random() - 0.5) * 30;
d.x = x; d.y = y;
d.el.style.left = x + 'px';
d.el.style.top = y + 'px';
d.el.style.transform = `rotate(${(Math.random()-0.5)*30}deg) scale(1)`;
d.el.style.opacity = '1';
});
}
window.onload = init;
</script>
</body>
</html>