猫武士斗蛐蛐 - 日神 VS 冬青叶
by TurboKoala83590 lines23.9 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猫武士斗蛐蛐 - 日神 VS 冬青叶</title>
<style>
* { box-sizing: border-box; user-select: none; }
body {
margin: 0; padding: 0; width: 100vw; height: 100vh;
background: #111; color: #eee; font-family: sans-serif;
overflow: hidden; display: flex; justify-content: center; align-items: center;
}
.screen {
width: 100%; height: 100%; position: absolute;
display: flex; flex-direction: column; justify-content: center; align-items: center;
transition: opacity 0.3s ease;
}
.hidden { opacity: 0; pointer-events: none; z-index: -1; }
/* 开始页面 */
#screen-start { background: #1a1a1a; }
.start-btn {
padding: 15px 50px; font-size: 24px; font-weight: bold;
color: #fff; background: #e74c3c; border: none; border-radius: 8px;
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4); cursor: pointer;
transition: transform 0.1s;
}
.start-btn:active { transform: scale(0.95); }
/* 选人页面 */
#screen-select h2 { margin-bottom: 20px; color: #f1c40f; font-size: 20px; }
.hero-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; width: 80%; max-width: 300px; }
.hero-card {
background: #333; border: 2px solid #555; border-radius: 10px;
padding: 15px; text-align: center; cursor: pointer; transition: all 0.2s;
}
.hero-card.selected { border-color: #f1c40f; background: #444; box-shadow: 0 0 15px #f1c40f; }
.hero-card .emoji { font-size: 40px; display: block; margin-bottom: 8px; }
.hero-card .name { font-size: 16px; font-weight: bold; }
/* 决斗场 */
#screen-arena { justify-content: flex-start; padding-top: 50px; }
.arena-bg {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background-size: 25px 25px;
background-image:
linear-gradient(to right, #222 1px, transparent 1px),
linear-gradient(to bottom, #222 1px, transparent 1px);
z-index: -1;
}
.arena-box {
width: 320px; height: 480px;
border: 4px solid #e74c3c; background: rgba(20, 20, 20, 0.8);
border-radius: 5px; position: relative; overflow: hidden;
}
.title-arena { position: absolute; top: 15px; color: #aaa; font-size: 14px; }
/* 战斗单位 */
.cat-fighter {
position: absolute; font-size: 40px; width: 50px; height: 50px;
display: flex; justify-content: center; align-items: center;
will-change: left, top; z-index: 2;
}
/* 伤害飘字与状态特效 */
.damage-text {
position: absolute; color: #ff4d4d; font-weight: bold; font-size: 20px;
pointer-events: none; animation: floatUp 0.8s ease-out forwards; z-index: 10;
}
@keyframes floatUp {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(-40px); opacity: 0; }
}
.buff-icon {
position: absolute; font-size: 14px; top: -10px; right: -10px;
animation: pop 0.3s ease-out; z-index: 5;
}
@keyframes pop { 0% { transform: scale(0); } 80% { transform: scale(1.2); } 100% { transform: scale(1); } }
.dialogue-bubble {
position: absolute; background: #ffd700; color: #000;
padding: 4px 8px; border-radius: 10px; font-size: 12px; font-weight: bold;
white-space: nowrap; top: -40px; left: 50%; transform: translateX(-50%);
animation: bubbleFade 1.5s ease-out forwards; z-index: 20; pointer-events: none;
box-shadow: 0 2px 10px rgba(255, 215, 0, 0.5);
}
@keyframes bubbleFade {
0% { opacity: 0; transform: translateX(-50%) translateY(10px); }
20% { opacity: 1; transform: translateX(-50%) translateY(0); }
80% { opacity: 1; }
100% { opacity: 0; transform: translateX(-50%) translateY(-20px); }
}
/* 冬青叶的特殊视觉 */
.hole-entry, .hole-exit {
position: absolute; width: 40px; height: 40px; border-radius: 50%;
display: flex; justify-content: center; align-items: center;
font-size: 24px; color: #fff; z-index: 1;
}
.hole-entry { border: 2px dashed #000; background: rgba(0,0,0,0.5); }
.hole-exit { border: 2px dashed #666; background: rgba(50,50,50,0.5); }
</style>
</head>
<body>
<div id="screen-start" class="screen">
<button class="start-btn" onclick="goToSelect()">开始游戏</button>
</div>
<div id="screen-select" class="screen hidden">
<h2>请选择两位武士</h2>
<div class="hero-grid" id="heroGrid"></div>
</div>
<div id="screen-arena" class="screen hidden">
<div class="arena-bg"></div>
<div class="title-arena">⚔️ 物理决斗场 - 机制实装版</div>
<div class="arena-box" id="arenaBox"></div>
</div>
<script>
// ================= 1. 英雄数据定义 =================
const ALL_HEROES = [
{
id: 1, name: "日神", emoji: "😏", color: "#ffd700",
hp: 900, maxHp: 900, speed: 130, range: 45, attack: 67,
type: "mage"
},
{
id: 2, name: "冬青叶", emoji: "🐈⬛", color: "#2e8b57",
hp: 1500, maxHp: 1500, speed: 150, range: 45, attack: 45,
type: "assassin"
}
];
let selectedHeroes = [];
// ================= 2. 场景切换逻辑 =================
function switchScreen(fromId, toId) {
document.getElementById(fromId).classList.add('hidden');
document.getElementById(toId).classList.remove('hidden');
}
function goToSelect() {
switchScreen('screen-start', 'screen-select');
renderHeroList();
}
function renderHeroList() {
const grid = document.getElementById('heroGrid');
grid.innerHTML = '';
selectedHeroes = [];
ALL_HEROES.forEach(hero => {
const card = document.createElement('div');
card.className = 'hero-card';
card.innerHTML = `<span class="emoji">${hero.emoji}</span><span class="name">${hero.name}</span>`;
card.onclick = () => selectHero(hero, card);
grid.appendChild(card);
});
}
function selectHero(hero, cardElement) {
const index = selectedHeroes.findIndex(h => h.id === hero.id);
if (index !== -1) {
selectedHeroes.splice(index, 1);
cardElement.classList.remove('selected');
} else {
if (selectedHeroes.length < 2) {
selectedHeroes.push(hero);
cardElement.classList.add('selected');
}
}
if (selectedHeroes.length === 2) {
setTimeout(() => startArena(selectedHeroes[0], selectedHeroes[1]), 500);
}
}
// ================= 3. 游戏主循环与状态系统 =================
let gameLoopId = null;
let fighters = []; // 场上选手
let lastTime = 0;
// 格子换算:1格 ≈ 30像素
const GRID_SIZE = 30;
function startArena(hero1, hero2) {
switchScreen('screen-select', 'screen-arena');
const box = document.getElementById('arenaBox');
box.innerHTML = '';
// 初始化实例数据
fighters = [
createFighterInstance(hero1, 30, 30, 1.5, 1.2),
createFighterInstance(hero2, 200, 350, -1.5, -1.2)
];
fighters.forEach(f => {
const el = document.createElement('div');
el.className = 'cat-fighter';
el.id = 'fighter-' + f.uid;
el.innerText = f.emoji;
el.style.color = f.color;
el.style.left = f.x + 'px';
el.style.top = f.y + 'px';
box.appendChild(el);
f.el = el;
});
lastTime = performance.now();
if (gameLoopId) cancelAnimationFrame(gameLoopId);
gameLoopId = requestAnimationFrame(gameLoop);
}
function createFighterInstance(data, x, y, vx, vy) {
return {
uid: data.id,
name: data.name, emoji: data.emoji, color: data.color,
maxHp: data.maxHp, hp: data.hp,
speed: data.speed, baseSpeed: data.speed,
range: data.range, baseAttack: data.attack,
x: x, y: y, vx: vx, vy: vy,
// 状态控制
isInvincible: false,
state: "normal", // normal, digging, striking, retreating
stateTimer: 0,
attackCd: 0,
skillCd: 6, // 日神技能冷却/冬青叶刷洞冷却
// Buff系统
selfDoubtTimer: 0, // 自我怀疑
confusionTimer: 0, // 困惑
// 冬青叶专属
tunnelEntry: null,
tunnelExit: null
};
}
// ================= 4. 核心逻辑更新 =================
function gameLoop(currentTime) {
const dt = (currentTime - lastTime) / 1000; // 转秒
lastTime = currentTime;
updatePhysics(dt);
updateCombatAndSkills(dt);
gameLoopId = requestAnimationFrame(gameLoop);
}
function updatePhysics(dt) {
const box = document.getElementById('arenaBox');
const maxWidth = box.clientWidth;
const maxHeight = box.clientHeight;
const catSize = 50;
fighters.forEach(f => {
// 根据状态决定移动
if (f.state === "digging" || f.state === "retreating") {
// 无敌状态,停止物理移动,由技能逻辑控制
return;
}
// 计算最终速度(受困惑Buff影响)
let currentSpeed = f.baseSpeed;
if (f.confusionTimer > 0) {
currentSpeed *= 0.5; // 移速减半
}
// 根据速度改变位置
f.x += f.vx * (currentSpeed / f.baseSpeed) * 60 * dt;
f.y += f.vy * (currentSpeed / f.baseSpeed) * 60 * dt;
// 边界碰撞检测(红框物理)
if (f.x <= 0) { f.x = 0; f.vx = Math.abs(f.vx); }
if (f.x >= maxWidth - catSize) { f.x = maxWidth - catSize; f.vx = -Math.abs(f.vx); }
if (f.y <= 0) { f.y = 0; f.vy = Math.abs(f.vy); }
if (f.y >= maxHeight - catSize) { f.y = maxHeight - catSize; f.vy = -Math.abs(f.vy); }
// 更新DOM位置
if (f.el) {
f.el.style.left = f.x + 'px';
f.el.style.top = f.y + 'px';
}
});
}
// ================= 5. 战斗与技能系统 =================
function updateCombatAndSkills(dt) {
const box = document.getElementById('arenaBox');
const f1 = fighters[0];
const f2 = fighters[1];
if (!f1 || !f2) return;
// 递减 Buff 计时器
[f1, f2].forEach(f => {
if (f.selfDoubtTimer > 0) f.selfDoubtTimer -= dt;
if (f.confusionTimer > 0) f.confusionTimer -= dt;
if (f.attackCd > 0) f.attackCd -= dt;
if (f.skillCd > 0) f.skillCd -= dt;
});
// 物理距离计算(像素转格)
const dx = (f2.x - f1.x);
const dy = (f2.y - f1.y);
const dist = Math.hypot(dx, dy);
const gridDist = dist / GRID_SIZE;
// ---------------- 日神 AI (f1) ----------------
if (f1.state === "normal") {
// 机制2:逻辑闭环(隔空挂Buff,每帧判定,10%概率,代码里每0.5秒判定一次)
if (Math.random() < 0.02 && f2.confusionTimer <= 0) { // 概率稍作调整避免过于频繁
f2.confusionTimer = 30; // 30秒困惑
showBuffIcon(f2, "❓");
showLog(`【日神】隔空触发逻辑闭环!${f2.name} 陷入困惑!`);
}
// 机制1:灵魂嘴炮(技能冷却好了就发射)
if (f1.skillCd <= 0) {
f1.skillCd = 6 + Math.random() * 2; // 6-8秒冷却
fireProjectile(f1, f2, box);
}
// 普攻(1.5格近战)
if (gridDist <= 1.5 && f1.attackCd <= 0) {
f1.attackCd = 1.0; // 日神攻速1秒
performAttack(f1, f2, f1.baseAttack);
} else if (gridDist > 1.5) {
// 追击
f1.x += (dx / dist) * f1.speed * dt * 2; // 临时加速追击
f1.y += (dy / dist) * f1.speed * dt * 2;
}
}
// ---------------- 冬青叶 AI (f2) ----------------
if (f2.state === "normal") {
// 机制1:地道奇袭(冷却好了就触发)
if (f2.skillCd <= 0) {
startTunnelStrike(f2, f1, box);
f2.skillCd = 8; // 8秒冷却
}
// 常规追击(冷却期间)
else {
if (gridDist <= 1.5 && f2.attackCd <= 0) {
f2.attackCd = 1.2; // 冬青叶攻速1.2秒
performAttack(f2, f1, f2.baseAttack);
} else if (gridDist > 1.5) {
// 绕后迂回走位(模拟一点切线方向)
let angle = Math.atan2(dy, dx) + Math.PI / 4; // 偏转45度
f2.x += Math.cos(angle) * f2.speed * dt * 1.5;
f2.y += Math.sin(angle) * f2.speed * dt * 1.5;
}
}
}
// 统一处理冬青叶的状态机
if (f2.state !== "normal") {
updateTunnelStateMachine(f2, f1, box, dt);
}
}
// ---------------- 核心机制函数 ----------------
// 伤害计算(包含Debuff乘法叠加)
function calculateDamage(attacker, baseDmg) {
let dmg = baseDmg;
if (attacker.selfDoubtTimer > 0) dmg *= 0.8; // 自我怀疑 -20%
if (attacker.confusionTimer > 0) dmg *= 0.6; // 困惑 -40%
return Math.round(dmg);
}
// 执行普通攻击
function performAttack(attacker, defender, baseDmg) {
const finalDmg = calculateDamage(attacker, baseDmg);
defender.hp = Math.max(0, defender.hp - finalDmg);
// 受击反馈
showDamage(defender.el, finalDmg);
checkDeath(defender);
}
// 日神:灵魂嘴炮(发射金色弹幕)
function fireProjectile(attacker, defender, box) {
// 弹出气泡台词
const quotes = [
"这样倒不像是明确的选择。",
"哦,你那样做可真的是愚蠢透了,我有个更明智的办法。"
];
const quote = quotes[Math.floor(Math.random() * quotes.length)];
showDialogueBubble(attacker.el, quote);
// 生成弹幕视觉
const proj = document.createElement('div');
proj.innerText = "📜";
proj.style.position = "absolute";
proj.style.fontSize = "20px";
proj.style.left = (attacker.x + 15) + 'px';
proj.style.top = (attacker.y + 15) + 'px';
proj.style.transition = "all 0.3s linear"; // 快速飞向敌人
proj.style.zIndex = "5";
box.appendChild(proj);
// 下一帧移动到敌人位置
requestAnimationFrame(() => {
proj.style.left = (defender.x + 15) + 'px';
proj.style.top = (defender.y + 15) + 'px';
});
// 命中判定(简化为延迟结算)
setTimeout(() => {
if (!proj.parentNode) return;
proj.remove();
// 命中伤害
defender.hp = Math.max(0, defender.hp - 11);
showDamage(defender.el, 11);
// 挂上"自我怀疑" Debuff
defender.selfDoubtTimer = 20; // 20秒
showBuffIcon(defender.el, "🤔");
showLog(`【日神】灵魂嘴炮命中!造成11点伤害,${defender.name} 被施加【自我怀疑】`);
checkDeath(defender);
}, 350);
}
// 冬青叶:地道奇袭三阶段状态机
function startTunnelStrike(attacker, defender, box) {
// 阶段1:刷新入口(在敌方附近 3-5格)
const angle = Math.random() * Math.PI * 2;
const distance = (3 + Math.random() * 2) * GRID_SIZE; // 3-5格
const entryX = defender.x + Math.cos(angle) * distance;
const entryY = defender.y + Math.sin(angle) * distance;
// 边界限制
const entryXClamped = Math.max(20, Math.min(box.clientWidth - 60, entryX));
const entryYClamped = Math.max(20, Math.min(box.clientHeight - 60, entryY));
const hole = document.createElement('div');
hole.className = 'hole-entry';
hole.innerText = '⬇️';
hole.style.left = entryXClamped + 'px';
hole.style.top = entryYClamped + 'px';
box.appendChild(hole);
attacker.tunnelEntry = hole;
attacker.entryX = entryXClamped;
attacker.entryY = entryYClamped;
// 切换状态:开始钻地
attacker.state = "digging";
attacker.stateTimer = 0.3; // 0.3秒无敌
attacker.isInvincible = true;
attacker.el.style.opacity = "0.4"; // 半透明
// 记录位置回去
attacker.vx = 0; attacker.vy = 0;
showLog(`【冬青叶】释放地道奇袭!钻入地道...`);
}
function updateTunnelStateMachine(f, defender, box, dt) {
if (f.stateTimer > 0) {
f.stateTimer -= dt;
return;
}
if (f.state === "digging") {
// 阶段2:钻出(刷新出口,在敌人身旁 2-3格)
if (f.tunnelEntry) f.tunnelEntry.remove(); // 移除入口
const angle = Math.random() * Math.PI * 2;
const distance = (2 + Math.random() * 1) * GRID_SIZE; // 2-3格
let exitX = defender.x + Math.cos(angle) * distance;
let exitY = defender.y + Math.sin(angle) * distance;
// 边界限制
exitX = Math.max(0, Math.min(box.clientWidth - 50, exitX));
exitY = Math.max(0, Math.min(box.clientHeight - 50, exitY));
const exitHole = document.createElement('div');
exitHole.className = 'hole-exit';
exitHole.innerText = '⬆️';
exitHole.style.left = exitX + 'px';
exitHole.style.top = exitY + 'px';
box.appendChild(exitHole);
f.tunnelExit = exitHole;
// 移动本体到出口
f.x = exitX; f.y = exitY;
f.el.style.left = f.x + 'px';
f.el.style.top = f.y + 'px';
f.el.style.opacity = "1";
f.state = "striking";
f.stateTimer = 0.2; // 0.2秒打完三连击
showLog(`【冬青叶】从出口钻出!发动三连击!`);
}
else if (f.state === "striking") {
// 阶段3:三连击(0.2秒内打完,总伤害390)
// 判定敌人是否在1.5格范围内
const dx = defender.x - f.x;
const dy = defender.y - f.y;
const dist = Math.hypot(dx, dy) / GRID_SIZE;
if (dist <= 1.5) {
for (let i = 0; i < 3; i++) {
setTimeout(() => {
if (defender.hp <= 0) return;
const finalDmg = calculateDamage(f, 130);
defender.hp = Math.max(0, defender.hp - finalDmg);
showDamage(defender.el, finalDmg);
checkDeath(defender);
}, i * 60); // 每隔60毫秒打一下
}
showLog(`【冬青叶】三连击命中!造成巨额伤害!`);
} else {
showLog(`【冬青叶】三连击落空!敌人已位移。`);
}
f.state = "retreating";
f.stateTimer = 0.3; // 0.3秒撤回无敌
f.isInvincible = true;
f.el.style.opacity = "0.4"; // 再次半透明
}
else if (f.state === "retreating") {
// 阶段4:撤回
if (f.tunnelExit) f.tunnelExit.remove(); // 移除出口
f.x = f.entryX; f.y = f.entryY;
f.el.style.left = f.x + 'px';
f.el.style.top = f.y + 'px';
// 结束,恢复正常
f.state = "normal";
f.isInvincible = false;
f.el.style.opacity = "1";
f.vx = (Math.random() > 0.5 ? 1 : -1) * 1.5;
f.vy = (Math.random() > 0.5 ? 1 : -1) * 1.2;
showLog(`【冬青叶】安全撤回。`);
}
}
// ---------------- 视觉与辅助函数 ----------------
function showDamage(el, dmg) {
const dmgEl = document.createElement('div');
dmgEl.className = 'damage-text';
dmgEl.innerText = `-${dmg}`;
dmgEl.style.left = `${Math.random() * 50 + 10}%`;
dmgEl.style.top = `20%`;
el.appendChild(dmgEl);
setTimeout(() => dmgEl.remove(), 800);
}
function showBuffIcon(el, icon) {
const iconEl = document.createElement('div');
iconEl.className = 'buff-icon';
iconEl.innerText = icon;
el.appendChild(iconEl);
// 持续一段时间后消失(为了视觉不堆积,设置比Buff时间短一点)
setTimeout(() => iconEl.remove(), 3000);
}
function showDialogueBubble(el, text) {
const bubble = document.createElement('div');
bubble.className = 'dialogue-bubble';
bubble.innerText = text;
el.appendChild(bubble);
setTimeout(() => bubble.remove(), 1500);
}
function showLog(text) {
console.log(text); // 暂时输出在控制台,下一版可以做个UI日志栏
}
let isGameOver = false;
function checkDeath(fighter) {
if (fighter.hp <= 0 && !isGameOver) {
isGameOver = true;
fighter.hp = 0;
cancelAnimationFrame(gameLoopId);
setTimeout(() => {
alert(`💀 ${fighter.name} 倒下了!战斗结束!`);
// 简单刷新回选人界面(你可以按需改回开始页面)
location.reload();
}, 500);
}
}
</script>
</body>
</html>Game Source: 猫武士斗蛐蛐 - 日神 VS 冬青叶
Creator: TurboKoala83
Libraries: none
Complexity: complex (590 lines, 23.9 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: vs-turbokoala83" to link back to the original. Then publish at arcadelab.ai/publish.