🎮ArcadeLab

摸鱼反应堆

by CrystalPenguin76
463 lines14.3 KB
▶ Play
<!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>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
body{background:#000;overflow:hidden;height:100vh;width:100vw;font-family:system-ui,sans-serif;}
canvas{display:block;width:100vw;height:100vh;}
.popup{
    position:fixed;inset:0;background:rgba(0,0,0,0.85);
    display:flex;align-items:center;justify-content:center;
    z-index:99;
}
.box{
    background:#222;color:#fff;padding:32px 24px;border-radius:16px;
    width:min(88vw,360px);text-align:center;
}
.box h2{margin-bottom:16px;color:#0cf;}
.box p{line-height:1.6;margin:8px 0;color:#ccc;}
.btn{
    margin-top:20px;padding:12px 24px;border:none;border-radius:8px;
    background:#0cf;color:#000;font-size:16px;font-weight:bold;
    width:100%;
}
.btn:active{opacity:0.7}
.hidden{display:none !important;}
.red-border{box-shadow:inset 0 0 0 8px #f33;animation:redFlash 0.3s infinite alternate;}
@keyframes redFlash{from{box-shadow:inset 0 0 0 6px #f33}to{box-shadow:inset 0 0 0 12px #f00}}
</style>
</head>
<body>
<canvas id="game"></canvas>

<!-- 开局引导弹窗 -->
<div id="guidePopup" class="popup">
    <div class="box">
        <h2>摸鱼反应堆</h2>
        <p>👔老板脸正对中间反应堆时千万别点!会被抓扣分</p>
        <p>老板脸转向左边/右边墙壁时,放心点击攒能量</p>
        <p>⚠️紧急会议全屏变红,抓紧连点刷分</p>
        <button class="btn" id="startBtn">点击开始游戏</button>
    </div>
</div>

<!-- 结算弹窗 -->
<div id="endPopup" class="popup hidden">
    <div class="box">
        <h2>游戏结束!</h2>
        <p>本次能量:<span id="finalEnergy">0</span></p>
        <p>被抓包次数:<span id="catchCount">0</span></p>
        <p>紧急会议触发:<span id="meetingCount">0</span></p>
        <p>历史最高:<span id="highScore">0</span></p>
        <button class="btn" id="restartBtn">再来一局</button>
        <button class="btn" id="shareBtn" style="margin-top:10px;background:#444;color:#fff;">复制分享文案</button>
    </div>
</div>

<script>
// 画布初始化
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
function resizeCanvas(){
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize',resizeCanvas);

// 音频工具
let audioCtx = null;
function initAudio(){if(!audioCtx)audioCtx = new (window.AudioContext || window.webkitAudioContext)();}
function playTone(freq,dur,type='sine'){
    if(!audioCtx)return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);gain.connect(audioCtx.destination);
    osc.type = type;osc.frequency.value = freq;
    gain.gain.setValueAtTime(0.2,audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur);
    osc.start();osc.stop(audioCtx.currentTime+dur);
}
const audio = {
    success:()=>playTone(880,0.15,'sine'),
    catch:()=>playTone(120,0.3,'triangle'),
    meeting:()=>playTone(1200,0.06,'square')
}

// 游戏全局状态
const game = {
    running:false,timeLeft:60,energy:0,maxEnergy:100,
    catchTimes:0,meetingTimes:0,highScore:parseInt(localStorage.getItem('fishHigh'))||0,
    reactor:{x:0,y:0,baseR:70,pulse:0,pulseSpeed:0.06,meetingShake:0},
    boss:{x:0,y:60,w:80,speed:0.6,dir:1,//1右 -1左
        lookDir:'front',//left/right/front
        lookTransition:0.3,lookTimer:0
    },
    emergency:{active:false,duration:4000,timer:0,nextTimer:0,nextDelay:0},
    floatTexts:[],
    flashRed:0
};

// 弹窗DOM
const guidePopup = document.getElementById('guidePopup');
const endPopup = document.getElementById('endPopup');
const startBtn = document.getElementById('startBtn');
const restartBtn = document.getElementById('restartBtn');
const shareBtn = document.getElementById('shareBtn');
const finalEnergyEl = document.getElementById('finalEnergy');
const catchCountEl = document.getElementById('catchCount');
const meetingCountEl = document.getElementById('meetingCount');
const highScoreEl = document.getElementById('highScore');

// 重置游戏数据
function resetGame(){
    game.running = true;
    game.timeLeft = 60;
    game.energy = 0;
    game.catchTimes = 0;
    game.meetingTimes = 0;
    game.floatTexts = [];
    game.flashRed = 0;
    game.emergency.active = false;
    game.reactor.x = canvas.width/2;
    game.reactor.y = canvas.height/2;
    game.reactor.pulse = 0;
    game.boss.x = game.boss.w;
    game.boss.dir = 1;
    game.boss.lookDir = 'front';
    game.boss.lookTimer = 0;
    // 随机8-12秒触发第一次紧急会议
    game.emergency.nextDelay = 8000 + Math.random()*4000;
    game.emergency.nextTimer = 0;
}

// 开始游戏
startBtn.addEventListener('click',()=>{
    initAudio();
    guidePopup.classList.add('hidden');
    resetGame();
    requestAnimationFrame(gameLoop);
});
restartBtn.addEventListener('click',()=>{
    endPopup.classList.add('hidden');
    resetGame();
    requestAnimationFrame(gameLoop);
});
shareBtn.addEventListener('click',async ()=>{
    const text = `我在摸鱼反应堆里攒了${game.energy}点能量,你敢挑战吗?`;
    await navigator.clipboard.writeText(text);
    alert('分享文案已复制!');
});

// 【重写优化版老板绘制:朝向极其明显,带视线射线+头部旋转】
function drawBoss(){
    const b = game.boss;
    ctx.save();
    ctx.translate(b.x,b.y);
    const size = b.w;
    // 根据朝向旋转头部,区分度拉满
    let rotateAngle = 0;
    let rayEndX = 0, rayEndY = -size * 4;
    if(b.lookDir === "left"){
        rotateAngle = -Math.PI / 6;
        rayEndX = -canvas.width * 0.6;
    }else if(b.lookDir === "right"){
        rotateAngle = Math.PI / 6;
        rayEndX = canvas.width * 0.6;
    }else{
        // front正视,视线直指反应堆中心
        rayEndX = canvas.width/2 - b.x;
        rayEndY = canvas.height/2 - b.y;
    }

    // 正视老板增加红色警告外发光
    if(b.lookDir === "front"){
        ctx.shadowColor = "#ff2222";
        ctx.shadowBlur = 20;
    }

    // 身体西装
    ctx.fillStyle='#223';
    ctx.fillRect(-size/2,-size/2,size,size*0.8);

    // 头部容器旋转
    ctx.save();
    ctx.rotate(rotateAngle);
    // 脑袋底色
    ctx.fillStyle='#ffe0cc';
    ctx.beginPath();ctx.arc(0,-size*0.35,size*0.22,0,Math.PI*2);ctx.fill();
    // 领带
    ctx.fillStyle='#d22';
    ctx.beginPath();
    ctx.moveTo(0,-size*0.1);
    ctx.lineTo(-size*0.08,size*0.3);
    ctx.lineTo(size*0.08,size*0.3);
    ctx.closePath();ctx.fill();
    // 眼睛大幅偏移,一眼分清朝向
    ctx.fillStyle='#000';
    const eyeY = -size*0.38;
    const eyeBaseOffset = size*0.08;
    if(b.lookDir === 'left'){
        // 看向左边,眼球全部左移
        ctx.beginPath();
        ctx.arc(-eyeBaseOffset - 8, eyeY, 4, 0, Math.PI*2);
        ctx.arc(eyeBaseOffset - 12, eyeY, 4, 0, Math.PI*2);
        ctx.fill();
    }else if(b.lookDir === 'right'){
        // 看向右边,眼球全部右移
        ctx.beginPath();
        ctx.arc(-eyeBaseOffset + 12, eyeY, 4, 0, Math.PI*2);
        ctx.arc(eyeBaseOffset + 8, eyeY, 4, 0, Math.PI*2);
        ctx.fill();
    }else{
        // front正视,双眼居中对准玩家
        ctx.beginPath();
        ctx.arc(-eyeBaseOffset, eyeY, 4, 0, Math.PI*2);
        ctx.arc(eyeBaseOffset, eyeY, 4, 0, Math.PI*2);
        ctx.fill();
    }
    ctx.restore();

    // 绘制视线指示射线(最关键!直观看到看哪里)
    ctx.strokeStyle = "rgba(0,0,0,0.7)";
    ctx.lineWidth = 3;
    ctx.setLineDash([8,6]);
    ctx.beginPath();
    ctx.moveTo(0, -size*0.35);
    ctx.lineTo(rayEndX, rayEndY);
    ctx.stroke();
    ctx.setLineDash([]);

    ctx.shadowBlur = 0;
    ctx.restore();
}

// 绘制反应堆
function drawReactor(){
    const r = game.reactor;
    let pulseScale = 1 + Math.sin(r.pulse)*0.12;
    if(game.emergency.active){
        pulseScale = 1 + Math.sin(r.pulse*3)*0.25;
    }
    const radius = r.baseR * pulseScale;
    const shakeX = game.emergency.active ? (Math.random()-0.5)*r.meetingShake : 0;
    const shakeY = game.emergency.active ? (Math.random()-0.5)*r.meetingShake : 0;
    const cx = r.x + shakeX;
    const cy = r.y + shakeY;

    // 径向渐变
    const grad = ctx.createRadialGradient(cx,cy,0,cx,cy,radius);
    if(game.emergency.active){
        grad.addColorStop(0,'#ffdd00');
        grad.addColorStop(1,'#ff4400');
    }else{
        grad.addColorStop(0,'#00f0ff');
        grad.addColorStop(1,'#002266');
    }
    ctx.beginPath();
    ctx.arc(cx,cy,radius,0,Math.PI*2);
    ctx.fillStyle = grad;
    ctx.fill();

    // 发光外描边
    ctx.shadowColor = game.emergency.active ? '#ff7700' : '#0cf';
    ctx.shadowBlur = 30;
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.shadowBlur = 0;

    // 能量文字
    let energyColor;
    if(game.energy < 30) energyColor = '#f33';
    else if(game.energy < 70) energyColor = '#fc0';
    else energyColor = '#3f3';
    ctx.fillStyle = energyColor;
    ctx.font = 'bold 32px system-ui';
    ctx.textAlign = 'center';
    ctx.fillText(game.energy + '/' + game.maxEnergy, cx, cy - radius - 16);
}

// 绘制浮动文字
function drawFloatText(){
    game.floatTexts.forEach((t,i)=>{
        ctx.fillStyle = t.color;
        ctx.font = 'bold 28px system-ui';
        ctx.textAlign = 'center';
        ctx.globalAlpha = t.alpha;
        ctx.fillText(t.text,t.x,t.y);
        ctx.globalAlpha = 1;
        t.y -= 1.2;
        t.alpha -= 0.018;
        if(t.alpha <= 0) game.floatTexts.splice(i,1);
    })
}

// 绘制UI顶部文字
function drawUI(){
    // 剩余时间 左上
    ctx.fillStyle='#fff';
    ctx.font='bold 22px system-ui';
    ctx.textAlign='left';
    ctx.fillText(`剩余:${Math.ceil(game.timeLeft)}s`,20,36);
    // 当前能量 右上
    ctx.textAlign='right';
    ctx.fillText(`能量:${game.energy}/${game.maxEnergy}`,canvas.width-20,36);
}

// 绘制背景
function drawBg(){
    ctx.fillStyle='#1a1a1a';
    ctx.fillRect(0,0,canvas.width,canvas.height);
    // 磨砂噪点简化
    ctx.fillStyle='rgba(255,255,255,0.02)';
    for(let i=0;i<120;i++){
        const x = Math.random()*canvas.width;
        const y = Math.random()*canvas.height;
        ctx.fillRect(x,y,2,2);
    }
    // 红色警告闪烁遮罩
    if(game.flashRed>0){
        ctx.fillStyle=`rgba(255,40,40,${game.flashRed*0.35})`;
        ctx.fillRect(0,0,canvas.width,canvas.height);
        game.flashRed -= 0.04;
    }
    // 紧急会议红色边框
    if(game.emergency.active){
        ctx.strokeStyle='#f22';
        ctx.lineWidth=10;
        ctx.strokeRect(4,4,canvas.width-8,canvas.height-8);
    }
}

// 点击处理(判定逻辑完全不变,仅视觉升级)
function handleTap(touchX,touchY){
    if(!game.running)return;
    const r = game.reactor;
    const dist = Math.hypot(touchX - r.x, touchY - r.y);
    const hitRadius = r.baseR + 50;
    if(dist > hitRadius) return; // 没点到反应堆区域

    if(game.emergency.active){
        // 紧急会议加分
        game.energy = Math.min(game.energy+5,game.maxEnergy);
        game.floatTexts.push({
            text:'+5',x:r.x,y:r.y-40,color:'#ff0',alpha:1
        });
        audio.meeting();
        return;
    }

    const bossLook = game.boss.lookDir;
    // 判定核心:只有front才是看向你
    if(bossLook === 'front'){
        // 被老板抓包
        game.energy = Math.max(game.energy-2,0);
        game.catchTimes++;
        game.flashRed = 1;
        game.floatTexts.push({
            text:'-2',x:r.x,y:r.y-40,color:'#f33',alpha:1
        });
        audio.catch();
    }else{
        // 左/右朝向墙壁,摸鱼成功
        game.energy = Math.min(game.energy+2,game.maxEnergy);
        game.floatTexts.push({
            text:'+2',x:r.x,y:r.y-40,color:'#3f3',alpha:1
        });
        audio.success();
    }
}

// 触摸绑定
canvas.addEventListener('touchstart',e=>{
    e.preventDefault();
    const t = e.touches[0];
    handleTap(t.clientX,t.clientY);
})
canvas.addEventListener('mousedown',e=>handleTap(e.clientX,e.clientY));

// 游戏主循环
let lastTime = 0;
function gameLoop(ts){
    const dt = (ts - lastTime)/1000;
    lastTime = ts;
    if(!game.running)return;

    // 倒计时
    game.timeLeft -= dt;
    if(game.timeLeft <= 0){
        game.running = false;
        // 更新最高分
        if(game.energy > game.highScore){
            game.highScore = game.energy;
            localStorage.setItem('fishHigh',game.highScore);
        }
        // 结算面板赋值
        finalEnergyEl.textContent = game.energy;
        catchCountEl.textContent = game.catchTimes;
        meetingCountEl.textContent = game.meetingTimes;
        highScoreEl.textContent = game.highScore;
        endPopup.classList.remove('hidden');
        return;
    }

    // 反应堆脉动
    game.reactor.pulse += game.reactor.pulseSpeed;

    // 老板移动
    const b = game.boss;
    b.x += b.speed * b.dir;
    const boundRight = canvas.width - b.w/2;
    const boundLeft = b.w/2;
    // 碰到边界转向
    if(b.x >= boundRight){
        b.dir = -1;
        b.lookDir = 'right';
    }else if(b.x <= boundLeft){
        b.dir = 1;
        b.lookDir = 'left';
    }else{
        // 随机切换正面看向反应堆
        b.lookTimer += dt;
        if(b.lookTimer > 2.2 + Math.random()*1.8){
            b.lookDir = 'front';
            setTimeout(()=>{
                if(b.dir>0)b.lookDir='left';
                else b.lookDir='right';
            },b.lookTransition*1000);
            b.lookTimer = 0;
        }
    }

    // 紧急会议计时器
    game.emergency.nextTimer += dt*1000;
    if(!game.emergency.active && game.emergency.nextTimer >= game.emergency.nextDelay){
        game.emergency.active = true;
        game.emergency.timer = game.emergency.duration;
        game.meetingTimes++;
    }
    if(game.emergency.active){
        game.emergency.timer -= dt*1000;
        if(game.emergency.timer <= 0){
            game.emergency.active = false;
            game.emergency.nextTimer = 0;
            game.emergency.nextDelay = 8000 + Math.random()*4000;
        }
    }

    // 渲染层
    drawBg();
    drawReactor();
    if(!game.emergency.active) drawBoss();
    drawFloatText();
    drawUI();

    requestAnimationFrame(gameLoop);
}
</script>
</body>
</html>

Game Source: 摸鱼反应堆

Creator: CrystalPenguin76

Libraries: none

Complexity: complex (463 lines, 14.3 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-crystalpenguin76-mr1hqhgh" to link back to the original. Then publish at arcadelab.ai/publish.