Faker走位练习器
·牛客 "网页小游戏 AI coding" 活动。
·游戏链接:http://www.silencer76.com/nowcoderToFaker/
核心玩法
·使用(左键)点击控制(右键会触发浏览器手势)角色移动,(A键)发射攻击摧毁弹道,(F键)使用闪现,冷却5秒。
·弹道有多种类型,不同难度出现概率数量均不同,随着时间进行,弹幕会增多
游戏视图
制作过程
·链接:https://www.bilibili.com/video/BV17Q6gBfE6m/
游戏代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Faker走位练习器 - 英雄联盟躲技能训练</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Arial', sans-serif;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
body {
background: linear-gradient(135deg, #0a0a2a, #1a1a3a);
color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow-x: hidden;
}
/* 游戏主界面 */
.game-interface {
display: flex;
width: 100%;
max-width: 1400px;
flex-direction: column;
animation: fadeIn 0.5s ease;
}
/* 顶部控制栏 */
.top-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 20px;
flex-wrap: wrap;
}
.game-title {
flex: 1;
}
.game-title h1 {
color: #ffcc00;
font-size: 2.8rem;
text-shadow: 0 0 10px rgba(255, 204, 0, 0.5);
background: linear-gradient(to right, #ffcc00, #ff9900);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
-moz-text-fill-color: transparent;
}
/* 紧凑游戏控制按钮 */
.compact-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.compact-control-btn {
padding: 8px 16px;
font-size: 1rem;
background: #2a2a5a;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(255, 204, 0, 0.3);
font-weight: bold;
white-space: nowrap;
}
.compact-control-btn:hover {
background: #3a3a7a;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.compact-start-btn {
background: linear-gradient(45deg, #4CAF50, #2E7D32);
}
.compact-pause-btn {
background: linear-gradient(45deg, #2196F3, #0D47A1);
}
.compact-reset-btn {
background: linear-gradient(45deg, #f44336, #b71c1c);
}
.compact-attack-btn {
background: linear-gradient(45deg, #9C27B0, #673AB7);
}
.compact-flash-btn {
background: linear-gradient(45deg, #00BCD4, #0097A7);
}
.display-mode-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.display-btn {
padding: 10px 20px;
background: rgba(20, 20, 40, 0.8);
color: white;
border: 1px solid rgba(255, 204, 0, 0.3);
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
}
.display-btn:hover {
background: rgba(30, 30, 60, 0.9);
transform: translateY(-2px);
}
.main-container {
display: flex;
width: 100%;
gap: 20px;
margin-bottom: 20px;
}
/* 左侧面板优化 - 删除了游戏控制栏 */
.left-panel {
flex: 0 0 220px;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
padding: 20px;
border: 1px solid rgba(255, 204, 0, 0.3);
display: flex;
flex-direction: column;
gap: 20px;
}
.game-container {
flex: 1;
position: relative;
min-height: 600px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.7);
border: 2px solid rgba(255, 204, 0, 0.3);
cursor: crosshair;
-webkit-user-drag: none;
touch-action: none;
background: #0d0d1a;
}
#gameCanvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
}
/* 顶部游戏状态信息 */
.top-game-info {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
padding: 0 10px;
flex-wrap: wrap;
}
.current-difficulty {
font-size: 1.4rem;
font-weight: bold;
padding: 8px 20px;
border-radius: 8px;
text-align: center;
min-width: 180px;
border: 2px solid;
}
.normal-difficulty {
color: #4CAF50;
border-color: #4CAF50;
background: rgba(76, 175, 80, 0.1);
}
.hard-difficulty {
color: #FF9800;
border-color: #FF9800;
background: rgba(255, 152, 0, 0.1);
}
.faker-difficulty {
color: #FF0000;
border-color: #FF0000;
background: rgba(255, 0, 0, 0.1);
animation: pulse 1.5s infinite;
}
/* 技能类型面板 - 可折叠 */
.skill-list-container {
position: relative;
}
.skill-toggle-btn {
padding: 10px 20px;
background: rgba(20, 20, 40, 0.8);
color: white;
border: 1px solid rgba(255, 204, 0, 0.3);
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
font-size: 1.1rem;
}
.skill-toggle-btn:hover {
background: rgba(30, 30, 60, 0.9);
transform: translateY(-2px);
}
.skill-list-panel {
position: absolute;
top: 100%;
left: 0;
background: rgba(0, 0, 0, 0.95);
padding: 20px;
border-radius: 10px;
border: 1px solid rgba(255, 204, 0, 0.3);
z-index: 1000;
min-width: 300px;
display: none;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
}
.skill-list-panel.show {
display: block;
}
.skill-list-panel h4 {
color: #ffcc00;
margin-bottom: 15px;
font-size: 1.4rem;
text-align: center;
}
.skill-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.skill-item {
display: flex;
align-items: center;
font-size: 1.1rem;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
transition: all 0.3s;
}
.skill-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(5px);
}
.skill-color {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 15px;
flex-shrink: 0;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.skill-name {
color: #fff;
font-weight: bold;
flex: 1;
}
.skill-desc {
color: #aaa;
font-size: 0.9rem;
margin-left: 10px;
font-style: italic;
}
/* 全屏样式优化 */
body.fullscreen {
padding: 15px;
background: #000;
}
body.fullscreen .game-interface {
max-width: 100%;
height: calc(100vh - 30px);
padding: 0;
}
body.fullscreen .top-controls {
margin-bottom: 15px;
}
body.fullscreen .main-container {
height: calc(100vh - 180px);
margin-bottom: 0;
}
body.fullscreen .game-container {
min-height: auto;
height: 100%;
}
body.fullscreen .left-panel {
flex: 0 0 200px;
padding: 15px;
gap: 15px;
}
body.fullscreen .compact-controls {
gap: 8px;
}
body.fullscreen .compact-control-btn {
padding: 6px 12px;
font-size: 0.9rem;
}
/* 控制面板样式优化 */
.control-section {
background: rgba(20, 20, 40, 0.8);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(255, 204, 0, 0.2);
}
.control-section h3 {
color: #ffcc00;
margin-bottom: 12px;
font-size: 1.2rem;
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 15px;
}
.stat-item {
text-align: center;
background: rgba(0, 0, 0, 0.3);
padding: 12px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-value {
font-size: 1.6rem;
font-weight: bold;
color: #ffcc00;
margin-bottom: 4px;
text-shadow: 0 0 5px rgba(255, 204, 0, 0.5);
}
.stat-label {
font-size: 0.95rem;
color: #aaa;
}
.difficulty-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.difficulty-btn {
padding: 12px;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
font-size: 1.1rem;
text-align: center;
}
.difficulty-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.difficulty-btn.active {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.normal-btn {
background: #4CAF50;
color: white;
}
.normal-btn.active {
background: #2E7D32;
border: 2px solid #81C784;
}
.hard-btn {
background: #FF9800;
color: white;
}
.hard-btn.active {
background: #EF6C00;
border: 2px solid #FFB74D;
}
.faker-btn {
background: linear-gradient(45deg, #FF0000, #FF9900);
color: white;
}
.faker-btn.active {
background: linear-gradient(45deg, #CC0000, #CC6600);
border: 2px solid #FF6666;
}
@keyframes pulse {
0% { box-shadow: 0 0 5px rgba(255, 0, 0, 0.5); }
50% { box-shadow: 0 0 20px rgba(255, 0, 0, 0.8); }
100% { box-shadow: 0 0 5px rgba(255, 0, 0, 0.5); }
}
.combo-display {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 5rem;
font-weight: bold;
color: #ffcc00;
text-shadow: 0 0 10px rgba(255, 204, 0, 0.7);
opacity: 0;
pointer-events: none;
z-index: 50;
transition: opacity 0.3s;
}
.click-indicator {
position: absolute;
width: 40px;
height: 40px;
border: 2px solid #4CAF50;
border-radius: 50%;
pointer-events: none;
z-index: 20;
opacity: 0;
transform: translate(-50%, -50%);
animation: clickEffect 0.5s ease-out;
}
@keyframes clickEffect {
0% {
transform: translate(-50%, -50%) scale(0.5);
opacity: 0.8;
}
100% {
transform: translate(-50%, -50%) scale(1.5);
opacity: 0;
}
}
.flash-effect {
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.7);
pointer-events: none;
z-index: 30;
opacity: 0;
transition: opacity 0.2s;
}
.game-over {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
backdrop-filter: blur(5px);
}
.game-over h2 {
font-size: 3.5rem;
color: #ffcc00;
margin-bottom: 20px;
text-align: center;
}
.final-score {
font-size: 2.5rem;
margin-bottom: 30px;
color: white;
text-align: center;
}
/* 鼠标瞄准指示器样式 */
.aim-indicator {
position: absolute;
pointer-events: none;
z-index: 5;
opacity: 0;
transition: opacity 0.2s;
}
.aim-line {
position: absolute;
background: rgba(255, 235, 59, 0.3);
height: 2px;
transform-origin: 0 0;
pointer-events: none;
z-index: 5;
}
.aim-circle {
position: absolute;
width: 25px;
height: 25px;
border: 2px solid #FFEB3B;
border-radius: 50%;
pointer-events: none;
z-index: 5;
}
.instructions {
background: rgba(0, 0, 0, 0.6);
padding: 15px;
border-radius: 10px;
width: 100%;
max-width: 1400px;
border: 1px solid rgba(255, 204, 0, 0.2);
margin-top: 0;
}
.instructions h3 {
color: #ffcc00;
margin-bottom: 10px;
font-size: 1.4rem;
}
.instructions ul {
list-style-position: inside;
margin-left: 10px;
}
.instructions li {
margin-bottom: 8px;
color: #ccc;
font-size: 1rem;
line-height: 1.5;
}
.control-hint {
color: #ffcc00;
font-size: 1rem;
margin-top: 8px;
text-align: center;
font-weight: bold;
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* 响应式调整 */
@media (max-width: 1100px) {
.main-container {
flex-direction: column;
}
.left-panel {
flex: none;
width: 100%;
}
.game-container {
min-height: 500px;
}
.top-game-info {
justify-content: center;
margin-top: 10px;
}
.top-controls {
flex-direction: column;
align-items: stretch;
}
}
@media (max-width: 768px) {
.display-mode-controls {
justify-content: center;
}
.stat-value {
font-size: 1.4rem;
}
.skill-list-panel {
min-width: 250px;
padding: 15px;
}
.skill-item {
font-size: 1rem;
}
.game-title h1 {
font-size: 2.2rem;
}
.current-difficulty {
font-size: 1.2rem;
min-width: 150px;
}
}
@media (max-width: 600px) {
body {
padding: 10px;
}
body.fullscreen {
padding: 10px;
}
.game-container {
min-height: 400px;
}
.left-panel {
padding: 12px;
}
.control-section {
padding: 12px;
}
.compact-control-btn {
padding: 6px 10px;
font-size: 0.85rem;
}
.skill-toggle-btn {
padding: 8px 15px;
font-size: 1rem;
}
}
@media (max-width: 480px) {
.game-title h1 {
font-size: 1.8rem;
}
.stat-value {
font-size: 1.2rem;
}
.stat-label {
font-size: 0.8rem;
}
.difficulty-btn {
padding: 10px;
font-size: 1rem;
}
.current-difficulty {
font-size: 1rem;
min-width: 120px;
padding: 6px 12px;
}
.skill-list-panel {
min-width: 200px;
padding: 12px;
}
}
</style>
</head>
<body>
<!-- 游戏主界面 -->
<div class="game-interface" id="gameInterface">
<!-- 顶部控制栏 -->
<div class="top-controls">
<div class="game-title">
<h1>FAKER 走位练习器</h1>
</div>
<!-- 紧凑游戏控制按钮 -->
<div class="compact-controls">
<button class="compact-control-btn compact-start-btn" id="compactStartBtn">开始</button>
<button class="compact-control-btn compact-pause-btn" id="compactPauseBtn">暂停</button>
<button class="compact-control-btn compact-reset-btn" id="compactResetBtn">重置</button>
<button class="compact-control-btn compact-attack-btn" id="compactAttackBtn">攻击(A)</button>
<button class="compact-control-btn compact-flash-btn" id="compactFlashBtn">闪现(F)</button>
</div>
<div class="display-mode-controls">
<button class="display-btn" id="toggleFullscreenBtn">全屏模式</button>
<button class="display-btn" id="instructionsBtn">游戏说明</button>
</div>
</div>
<!-- 顶部游戏信息 -->
<div class="top-game-info">
<div class="current-difficulty normal-difficulty" id="currentDifficulty">普通难度</div>
<!-- 技能类型可折叠面板 -->
<div class="skill-list-container">
<button class="skill-toggle-btn" id="skillToggleBtn">技能类型</button>
<div class="skill-list-panel" id="skillListPanel">
<h4>技能类型说明</h4>
<div class="skill-items">
<div class="skill-item">
<div class="skill-color" style="background-color: #4CAF50;"></div>
<div class="skill-name">直线技能</div>
<div class="skill-desc">慢速</div>
</div>
<div class="skill-item">
<div class="skill-color" style="background-color: #FF9800;"></div>
<div class="skill-name">快速技能</div>
<div class="skill-desc">高速</div>
</div>
<div class="skill-item">
<div class="skill-color" style="background-color: #FF0000;"></div>
<div class="skill-name">预判技能</div>
<div class="skill-desc">预测走位</div>
</div>
<div class="skill-item">
<div class="skill-color" style="background-color: #9C27B0;"></div>
<div class="skill-name">范围技能</div>
<div class="skill-desc">AOE/范围</div>
</div>
<div class="skill-item">
<div class="skill-color" style="background-color: #00BCD4;"></div>
<div class="skill-name">追踪技能</div>
<div class="skill-desc">自动追踪</div>
</div>
<div class="skill-item">
<div class="skill-color" style="background-color: #FFEB3B;"></div>
<div class="skill-name">玩家攻击</div>
<div class="skill-desc">摧毁技能</div>
</div>
</div>
</div>
</div>
</div>
<div class="main-container">
<!-- 左侧控制面板 -->
<div class="left-panel">
<div class="control-section">
<h3>游戏状态</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" id="score">0</div>
<div class="stat-label">得分</div>
</div>
<div class="stat-item">
<div class="stat-value" id="combo">0</div>
<div class="stat-label">连躲次数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="lives">3</div>
<div class="stat-label">生命值</div>
</div>
<div class="stat-item">
<div class="stat-value" id="time">0</div>
<div class="stat-label">时间(秒)</div>
</div>
<div class="stat-item">
<div class="stat-value" id="attackCooldown">0</div>
<div class="stat-label">攻击冷却</div>
</div>
<div class="stat-item">
<div class="stat-value" id="flashCooldown">0</div>
<div class="stat-label">闪现冷却</div>
</div>
</div>
</div>
<div class="control-section">
<h3>难度选择</h3>
<div class="difficulty-section">
<button class="difficulty-btn normal-btn active" id="normalBtn">普通模式</button>
<button class="difficulty-btn hard-btn" id="hardBtn">困难模式</button>
<button class="difficulty-btn faker-btn" id="fakerBtn">FAKER! 模式</button>
</div>
</div>
</div>
<!-- 游戏区域 -->
<div class="game-container">
<canvas id="gameCanvas"></canvas>
<div class="flash-effect" id="flashEffect"></div>
<!-- 鼠标瞄准指示器 -->
<div class="aim-indicator" id="aimIndicator">
<div class="aim-line" id="aimLine"></div>
<div class="aim-circle" id="aimCircle"></div>
</div>
<div class="combo-display" id="comboDisplay">+5 Combo!</div>
<div class="game-over" id="gameOverScreen">
<h2>游戏结束!</h2>
<div class="final-score">最终得分: <span id="finalScore">0</span></div>
<div class="game-controls">
<button class="control-btn start-btn" id="restartBtn">重新开始</button>
<button class="control-btn" id="menuBtn">返回菜单</button>
</div>
</div>
</div>
</div>
<div class="instructions" id="instructionsPanel" style="display: none;">
<h3>游戏说明:</h3>
<ul>
<li><strong>目标:</strong>控制你的英雄(俯视图样式)躲避所有技能弹道,可以发射攻击摧毁技能</li>
<li><strong>移动控制:</strong>在游戏区域使用鼠标左键点击地面,英雄会向点击位置移动并改变朝向</li>
<li><strong>攻击控制:</strong>鼠标悬停显示瞄准线,按A键或点击"攻击(A)"按钮,向鼠标方向发射能量弹</li>
<li><strong>闪现技能:</strong>按F键或点击"闪现(F)"按钮,瞬间向鼠标方向闪现一段距离</li>
<li><strong>冷却系统:</strong>
<ul>
<li>攻击冷却:每次攻击后0.5秒冷却(无限使用)</li>
<li>闪现冷却:每次使用后5秒冷却</li>
</ul>
</li>
<li><strong>难度说明:</strong>
<ul>
<li><strong>普通:</strong>少量慢速技能,没有追踪和范围技能</li>
<li><strong>困难:</strong>增加技能数量和速度,加入预判和少量追踪技能</li>
<li><strong>FAKER!:</strong>高密度技能,大量追踪技能和范围技能</li>
</ul>
</li>
<li><strong>得分规则:</strong>成功躲避技能+1分,摧毁技能+2分,连续操作有额外加分,被击中-1生命</li>
<li><strong>技巧:</strong>用鼠标瞄准追踪技能,合理使用攻击和闪现,注意冷却时间!</li>
</ul>
<div class="control-hint">提示:鼠标左键移动,鼠标瞄准,A键攻击,F键闪现!</div>
</div>
</div>
<script>
// 游戏模式管理
let displayMode = 'fullscreen';
let canvasBaseWidth = 1920;
let canvasBaseHeight = 1080;
// 技能类型面板状态
let skillPanelVisible = false;
// 全屏功能
document.getElementById('toggleFullscreenBtn').addEventListener('click', toggleFullscreen);
// 游戏说明按钮
document.getElementById('instructionsBtn').addEventListener('click', () => {
const instructions = document.getElementById('instructionsPanel');
instructions.style.display = instructions.style.display === 'none' ? 'block' : 'none';
});
// 技能类型面板切换
document.getElementById('skillToggleBtn').addEventListener('click', toggleSkillPanel);
// 紧凑控制按钮事件
document.getElementById('compactStartBtn').addEventListener('click', startGame);
document.getElementById('compactPauseBtn').addEventListener('click', togglePause);
document.getElementById('compactResetBtn').addEventListener('click', resetGame);
document.getElementById('compactAttackBtn').addEventListener('click', playerAttack);
document.getElementById('compactFlashBtn').addEventListener('click', useFlash);
// 游戏结束按钮
document.getElementById('restartBtn').addEventListener('click', restartGame);
document.getElementById('menuBtn').addEventListener('click', returnToMenu);
// 点击其他地方关闭技能面板
document.addEventListener('click', (e) => {
const skillPanel = document.getElementById('skillListPanel');
const skillToggleBtn = document.getElementById('skillToggleBtn');
if (skillPanelVisible &&
!skillPanel.contains(e.target) &&
!skillToggleBtn.contains(e.target)) {
hideSkillPanel();
}
});
function toggleSkillPanel() {
if (skillPanelVisible) {
hideSkillPanel();
} else {
showSkillPanel();
}
}
function showSkillPanel() {
if (gameRunning) {
// 游戏进行中不显示,防止遮挡
return;
}
const skillPanel = document.getElementById('skillListPanel');
skillPanel.classList.add('show');
skillPanelVisible = true;
}
function hideSkillPanel() {
const skillPanel = document.getElementById('skillListPanel');
skillPanel.classList.remove('show');
skillPanelVisible = false;
}
// 游戏进行时自动关闭技能面板
function startGame() {
if (gameRunning) return;
hideSkillPanel(); // 游戏开始时关闭技能面板
gameRunning = true;
gamePaused = false;
score = 0;
combo = 0;
maxCombo = 0;
lives = 3;
gameTime = 0;
skills = [];
playerAttacks = [];
playerPositions = [];
attackCooldown = 0;
flashCooldown = 0;
const config = difficultyConfig[difficulty];
currentSkillInterval = config.skillInterval;
skillSpawnRate = config.baseSpawnRate;
playerPosition.x = canvas.width / 2;
playerPosition.y = canvas.height / 2;
playerTargetPosition.x = canvas.width / 2;
playerTargetPosition.y = canvas.height / 2;
playerAttackDirection = { x: 1, y: 0 };
document.getElementById('gameOverScreen').style.display = 'none';
updateUI();
updateAimIndicator();
document.getElementById('compactPauseBtn').textContent = '暂停';
gameLoop();
}
function toggleFullscreen() {
if (displayMode === 'window') {
displayMode = 'fullscreen';
enterFullscreen();
} else {
displayMode = 'fullscreen';
if (!document.fullscreenElement) {
enterFullscreen();
} else {
exitFullscreen();
}
}
}
function enterFullscreen() {
document.body.classList.add('fullscreen');
canvasBaseWidth = 1920;
canvasBaseHeight = 1080;
resizeCanvas();
const element = document.documentElement;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestfullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
function exitFullscreen() {
document.body.classList.remove('fullscreen');
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
function handleFullscreenChange() {
const isFullscreen = document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement;
if (!isFullscreen) {
document.body.classList.remove('fullscreen');
}
}
function resizeCanvas() {
const canvas = document.getElementById('gameCanvas');
const container = canvas.parentElement;
canvas.width = canvasBaseWidth;
canvas.height = canvasBaseHeight;
const scaleX = container.clientWidth / canvasBaseWidth;
const scaleY = container.clientHeight / canvasBaseHeight;
const scale = Math.min(scaleX, scaleY);
canvas.style.width = (canvasBaseWidth * scale) + 'px';
canvas.style.height = (canvasBaseHeight * scale) + 'px';
canvas.style.margin = 'auto';
canvas.style.display = 'block';
if (playerTargetPosition) {
playerTargetPosition.x = canvas.width / 2;
playerTargetPosition.y = canvas.height / 2;
}
if (playerPosition) {
playerPosition.x = canvas.width / 2;
playerPosition.y = canvas.height / 2;
}
}
window.addEventListener('load', () => {
initGame();
setTimeout(() => {
enterFullscreen();
}, 500);
});
// =============== 游戏核心代码 ===============
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const gameContainer = document.querySelector('.game-container');
let mouseX = 0;
let mouseY = 0;
let isMouseOverCanvas = false;
let gameRunning = false;
let gamePaused = false;
let difficulty = 'normal';
let score = 0;
let combo = 0;
let maxCombo = 0;
let lives = 3;
let gameTime = 0;
let lastSkillTime = 0;
let baseSkillInterval = 2000;
let currentSkillInterval = baseSkillInterval;
let skillSpawnRate = 1;
let playerPosition = { x: 0, y: 0 };
let playerTargetPosition = { x: 0, y: 0 };
let playerAttackDirection = { x: 1, y: 0 };
let playerWidth = 60;
let playerHeight = 90;
let playerSpeed = 3.5;
let playerVelocity = { x: 0, y: 0 };
let playerPositions = [];
let playerAttacks = [];
let attackCooldown = 0;
let attackCooldownTime = 0.5;
let flashCooldown = 0;
let flashCooldownTime = 5;
let flashDistance = 150;
let skills = [];
// 弹道管理常量
const PROJECTILE = {
// 玩家攻击
PLAYER_ATTACK: {
LIFETIME: 1000, // 1秒
SCREEN_MARGIN: 50, // 离开屏幕50像素后删除
SPEED: 10
},
// 技能
SKILL: {
STRAIGHT: { LIFETIME: 5000, SCREEN_MARGIN: 100 },
FAST_STRAIGHT: { LIFETIME: 5000, SCREEN_MARGIN: 100 },
PREDICTION: { LIFETIME: 5000, SCREEN_MARGIN: 100 },
RANGE: { LIFETIME: 3000, SCREEN_MARGIN: 200 },
HOMING: { LIFETIME: 5000, SCREEN_MARGIN: 100 }
}
};
const difficultyConfig = {
normal: {
skillSpeed: { min: 2.5, max: 3.5 },
skillInterval: 2000,
baseSpawnRate: 1,
maxSpawnRate: 3,
skillTypes: ['straight'],
preditionLevel: 0,
maxSkills: 8,
fastSkillChance: 0.1,
homingSkillChance: 0,
rangeSkillChance: 0,
},
hard: {
skillSpeed: { min: 3.0, max: 4.5 },
skillInterval: 1500,
baseSpawnRate: 1,
maxSpawnRate: 4,
skillTypes: ['straight', 'fastStraight', 'range'],
preditionLevel: 0.3,
maxSkills: 12,
fastSkillChance: 0.2,
homingSkillChance: 0.1,
rangeSkillChance: 0.15,
},
faker: {
skillSpeed: { min: 3.5, max: 5.5 },
skillInterval: 1000,
baseSpawnRate: 2,
maxSpawnRate: 5,
skillTypes: ['straight', 'fastStraight', 'prediction', 'range', 'homing'],
preditionLevel: 0.5,
maxSkills: 15,
fastSkillChance: 0.3,
homingSkillChance: 0.2,
rangeSkillChance: 0.25,
}
};
const skillConfig = {
straight: {
color: '#4CAF50',
radius: 15,
speed: 3.0,
lifetime: PROJECTILE.SKILL.STRAIGHT.LIFETIME
},
fastStraight: {
color: '#FF9800',
radius: 12,
speed: 5.0,
lifetime: PROJECTILE.SKILL.FAST_STRAIGHT.LIFETIME
},
prediction: {
color: '#FF0000',
radius: 18,
speed: 4.0,
lifetime: PROJECTILE.SKILL.PREDICTION.LIFETIME
},
range: {
color: '#9C27B0',
radius: 100,
speed: 0,
lifetime: PROJECTILE.SKILL.RANGE.LIFETIME,
shrinkSpeed: 0.8,
targetRadius: 15
},
homing: {
color: '#00BCD4',
radius: 14,
speed: 3.5,
lifetime: PROJECTILE.SKILL.HOMING.LIFETIME
}
};
// 初始化游戏
function initGame() {
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
gameContainer.addEventListener('mousemove', function(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
mouseX = (e.clientX - rect.left) * scaleX;
mouseY = (e.clientY - rect.top) * scaleY;
updateAimIndicator();
});
gameContainer.addEventListener('mouseenter', function() {
isMouseOverCanvas = true;
updateAimIndicator();
});
gameContainer.addEventListener('mouseleave', function() {
isMouseOverCanvas = false;
updateAimIndicator();
});
canvas.addEventListener('mousedown', function(e) {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
if (!gameRunning || gamePaused) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
playerTargetPosition.x = x;
playerTargetPosition.y = y;
const dx = x - playerPosition.x;
const dy = y - playerPosition.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
playerAttackDirection.x = dx / distance;
playerAttackDirection.y = dy / distance;
}
addClickIndicator(x, y);
return false;
});
document.addEventListener('keydown', function(e) {
if (!gameRunning || gamePaused) return;
if (e.key === 'a' || e.key === 'A') {
e.preventDefault();
playerAttack();
} else if (e.key === 'f' || e.key === 'F') {
e.preventDefault();
useFlash();
} else if (e.key === 'Escape') {
if (displayMode === 'fullscreen') {
exitFullscreen();
}
}
});
canvas.addEventListener('contextmenu', function(e) {
e.preventDefault();
e.stopPropagation();
return false;
});
document.addEventListener('dragstart', function(e) {
e.preventDefault();
return false;
});
const difficultyButtons = document.querySelectorAll('.difficulty-btn');
difficultyButtons.forEach(btn => {
btn.addEventListener('click', function() {
const level = this.id.replace('Btn', '');
setDifficulty(level);
difficultyButtons.forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
draw();
}
function updateAimIndicator() {
const aimIndicator = document.getElementById('aimIndicator');
const aimLine = document.getElementById('aimLine');
const aimCircle = document.getElementById('aimCircle');
if (isMouseOverCanvas && gameRunning && !gamePaused && attackCooldown <= 0) {
aimIndicator.style.opacity = '1';
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const playerScreenX = playerPosition.x / scaleX;
const playerScreenY = playerPosition.y / scaleY;
const mouseScreenX = mouseX / scaleX;
const mouseScreenY = mouseY / scaleY;
const dx = mouseScreenX - playerScreenX;
const dy = mouseScreenY - playerScreenY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
aimLine.style.left = playerScreenX + 'px';
aimLine.style.top = playerScreenY + 'px';
aimLine.style.width = Math.min(distance, 300) + 'px';
aimLine.style.transform = `rotate(${angle}rad)`;
aimCircle.style.left = (mouseScreenX - 12.5) + 'px';
aimCircle.style.top = (mouseScreenY - 12.5) + 'px';
} else {
aimIndicator.style.opacity = '0';
}
}
function addClickIndicator(x, y) {
const indicator = document.createElement('div');
indicator.className = 'click-indicator';
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
indicator.style.left = (x / scaleX) + 'px';
indicator.style.top = (y / scaleY) + 'px';
gameContainer.appendChild(indicator);
setTimeout(() => {
if (indicator.parentNode) {
indicator.parentNode.removeChild(indicator);
}
}, 500);
}
function playerAttack() {
if (attackCooldown > 0) return;
attackCooldown = attackCooldownTime;
const dx = mouseX - playerPosition.x;
const dy = mouseY - playerPosition.y;
const distance = Math.sqrt(dx * dx + dy * dy);
let directionX = 1;
let directionY = 0;
if (distance > 10) {
directionX = dx / distance;
directionY = dy / distance;
}
playerAttackDirection.x = directionX;
playerAttackDirection.y = directionY;
const attack = {
x: playerPosition.x + directionX * 30,
y: playerPosition.y + directionY * 30,
radius: 12,
color: '#FFEB3B',
speed: PROJECTILE.PLAYER_ATTACK.SPEED,
directionX: directionX,
directionY: directionY,
lifetime: PROJECTILE.PLAYER_ATTACK.LIFETIME,
age: 0,
damage: 1
};
playerAttacks.push(attack);
updateUI();
updateAimIndicator();
}
function useFlash() {
if (flashCooldown > 0) return;
flashCooldown = flashCooldownTime;
const dx = mouseX - playerPosition.x;
const dy = mouseY - playerPosition.y;
const distance = Math.sqrt(dx * dx + dy * dy);
let directionX = 0;
let directionY = 0;
if (distance > 10) {
directionX = dx / distance;
directionY = dy / distance;
}
const flashX = playerPosition.x + directionX * flashDistance;
const flashY = playerPosition.y + directionY * flashDistance;
showFlashEffect();
playerPosition.x = flashX;
playerPosition.y = flashY;
playerTargetPosition.x = flashX;
playerTargetPosition.y = flashY;
playerAttackDirection.x = directionX;
playerAttackDirection.y = directionY;
playerPosition.x = Math.max(playerWidth/2, Math.min(canvas.width - playerWidth/2, playerPosition.x));
playerPosition.y = Math.max(playerHeight/2, Math.min(canvas.height - playerHeight/2, playerPosition.y));
playerTargetPosition.x = playerPosition.x;
playerTargetPosition.y = playerPosition.y;
playerPositions = [];
updateUI();
}
function showFlashEffect() {
const flashEffect = document.getElementById('flashEffect');
flashEffect.style.opacity = '1';
setTimeout(() => {
flashEffect.style.opacity = '0';
}, 200);
}
// =============== 弹道删除逻辑优化 ===============
// 检查玩家攻击是否应该被删除
function shouldRemovePlayerAttack(attack) {
// 1. 超过生命周期
if (attack.age > attack.lifetime) return true;
// 2. 完全离开屏幕(使用定义的边界)
const margin = PROJECTILE.PLAYER_ATTACK.SCREEN_MARGIN;
if (attack.x < -margin || attack.x > canvas.width + margin ||
attack.y < -margin || attack.y > canvas.height + margin) {
return true;
}
return false;
}
// 检查技能是否应该被删除
function shouldRemoveSkill(skill) {
// 1. 超过生命周期
if (skill.age > skill.lifetime) return true;
// 2. 范围技能收缩完成
if (skill.type === 'range' && skill.currentRadius <= skill.targetRadius) return true;
// 3. 完全离开屏幕(使用技能类型特定的边界)
const margin = PROJECTILE.SKILL[skill.type.toUpperCase()]?.SCREEN_MARGIN || 100;
if (skill.x < -margin || skill.x > canvas.width + margin ||
skill.y < -margin || skill.y > canvas.height + margin) {
return true;
}
return false;
}
// 更新玩家攻击 - 使用新的删除逻辑
function updatePlayerAttacks(deltaTime) {
for (let i = playerAttacks.length - 1; i >= 0; i--) {
const attack = playerAttacks[i];
attack.age += deltaTime;
// 使用统一的删除检查
if (shouldRemovePlayerAttack(attack)) {
playerAttacks.splice(i, 1);
continue;
}
attack.x += attack.directionX * attack.speed;
attack.y += attack.directionY * attack.speed;
}
}
// 更新技能位置 - 使用新的删除逻辑
function updateSkills(deltaTime) {
for (let i = skills.length - 1; i >= 0; i--) {
const skill = skills[i];
skill.age += deltaTime;
// 使用统一的删除检查
if (shouldRemoveSkill(skill)) {
// 如果是正常消失(非碰撞),算作成功躲避
if (skill.health > 0) {
onSkillDodged();
}
skills.splice(i, 1);
continue;
}
if (skill.type === 'range') {
// 范围技能:逐渐缩小并虚化
if (skill.currentRadius > skill.targetRadius) {
skill.currentRadius -= skill.shrinkSpeed;
skill.fadeProgress = (skill.radius - skill.currentRadius) / (skill.radius - skill.targetRadius);
if (skill.currentRadius <= skill.targetRadius * 3 && !skill.highlight) {
skill.highlight = true;
skill.highlightTime = 500;
}
if (skill.highlight) {
skill.highlightTime -= deltaTime;
if (skill.highlightTime <= 0) {
skill.highlight = false;
}
}
}
} else {
// 移动技能
if (skill.homing) {
const dx = playerPosition.x - skill.x;
const dy = playerPosition.y - skill.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
skill.directionX = dx / distance;
skill.directionY = dy / distance;
}
}
skill.x += skill.directionX * skill.speed;
skill.y += skill.directionY * skill.speed;
}
}
}
// =============== 其他游戏逻辑 ===============
function updatePlayer(deltaTime) {
if (!gameRunning) return;
const dx = playerTargetPosition.x - playerPosition.x;
const dy = playerTargetPosition.y - playerPosition.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 1) {
const moveDistance = playerSpeed * (deltaTime / 16);
if (distance <= moveDistance) {
playerPosition.x = playerTargetPosition.x;
playerPosition.y = playerTargetPosition.y;
} else {
const ratio = moveDistance / distance;
playerPosition.x += dx * ratio;
playerPosition.y += dy * ratio;
}
playerPosition.x = Math.max(playerWidth/2, Math.min(canvas.width - playerWidth/2, playerPosition.x));
playerPosition.y = Math.max(playerHeight/2, Math.min(canvas.height - playerHeight/2, playerPosition.y));
playerTargetPosition.x = Math.max(playerWidth/2, Math.min(canvas.width - playerWidth/2, playerTargetPosition.x));
playerTargetPosition.y = Math.max(playerHeight/2, Math.min(canvas.height - playerHeight/2, playerTargetPosition.y));
if (dx !== 0 || dy !== 0) {
const moveDistance = Math.sqrt(dx * dx + dy * dy);
if (moveDistance > 0) {
playerAttackDirection.x = dx / moveDistance;
playerAttackDirection.y = dy / moveDistance;
}
}
playerPositions.push({x: playerPosition.x, y: playerPosition.y});
if (playerPositions.length > 10) {
playerPositions.shift();
}
if (playerPositions.length >= 2) {
const lastPos = playerPositions[playerPositions.length - 2];
playerVelocity.x = playerPosition.x - lastPos.x;
playerVelocity.y = playerPosition.y - lastPos.y;
}
}
if (attackCooldown > 0) {
attackCooldown -= deltaTime / 1000;
if (attackCooldown < 0) attackCooldown = 0;
}
if (flashCooldown > 0) {
flashCooldown -= deltaTime / 1000;
if (flashCooldown < 0) flashCooldown = 0;
}
if (isMouseOverCanvas) {
updateAimIndicator();
}
}
function setDifficulty(level) {
difficulty = level;
updateDifficultyDisplay();
const config = difficultyConfig[difficulty];
currentSkillInterval = config.skillInterval;
skillSpawnRate = config.baseSpawnRate;
baseSkillInterval = config.skillInterval;
if (gameRunning) {
resetGame();
startGame();
}
}
function updateDifficultyDisplay() {
const difficultyElement = document.getElementById('currentDifficulty');
const difficultyText =
difficulty === 'normal' ? '普通难度' :
difficulty === 'hard' ? '困难难度' : 'FAKER! 模式';
difficultyElement.textContent = difficultyText;
difficultyElement.className = 'current-difficulty';
if (difficulty === 'normal') difficultyElement.classList.add('normal-difficulty');
else if (difficulty === 'hard') difficultyElement.classList.add('hard-difficulty');
else difficultyElement.classList.add('faker-difficulty');
}
function gameLoop(timestamp) {
if (!gameRunning) return;
const deltaTime = timestamp - lastTime || 0;
lastTime = timestamp;
if (!gamePaused) {
gameTime += deltaTime / 1000;
updatePlayer(deltaTime);
updateSkillSpawnRate();
generateSkills();
updateSkills(deltaTime);
updatePlayerAttacks(deltaTime);
checkCollisions();
updateUI();
if (lives <= 0) {
endGame();
return;
}
}
draw();
requestAnimationFrame(gameLoop);
}
function updateSkillSpawnRate() {
const config = difficultyConfig[difficulty];
const timeFactor = Math.min(gameTime / 60, 1);
skillSpawnRate = config.baseSpawnRate + (config.maxSpawnRate - config.baseSpawnRate) * timeFactor;
currentSkillInterval = baseSkillInterval * (1 - timeFactor * 0.5);
}
function generateSkills() {
if (!gameRunning || gamePaused) return;
const currentTime = Date.now();
const config = difficultyConfig[difficulty];
if (currentTime - lastSkillTime > currentSkillInterval && skills.length < config.maxSkills) {
lastSkillTime = currentTime;
const spawnCount = Math.floor(skillSpawnRate) + (Math.random() < (skillSpawnRate % 1) ? 1 : 0);
for (let i = 0; i < spawnCount; i++) {
const skillTypes = config.skillTypes;
let skillType = skillTypes[Math.floor(Math.random() * skillTypes.length)];
if (Math.random() < config.fastSkillChance && skillTypes.includes('fastStraight')) {
skillType = 'fastStraight';
}
if (skillType === 'homing' && Math.random() > config.homingSkillChance) {
const nonHomingTypes = skillTypes.filter(type => type !== 'homing');
skillType = nonHomingTypes[Math.floor(Math.random() * nonHomingTypes.length)];
}
if (skillType === 'range' && Math.random() > config.rangeSkillChance) {
const nonRangeTypes = skillTypes.filter(type => type !== 'range');
skillType = nonRangeTypes[Math.floor(Math.random() * nonRangeTypes.length)];
}
const skill = createSkill(skillType);
skills.push(skill);
}
}
}
function createSkill(skillType) {
const config = difficultyConfig[difficulty];
const skillSpec = skillConfig[skillType];
let startX, startY, targetX, targetY;
if (skillType === 'range') {
const angle = Math.random() * Math.PI * 2;
const distance = 300 + Math.random() * 400;
startX = playerPosition.x + Math.cos(angle) * distance;
startY = playerPosition.y + Math.sin(angle) * distance;
targetX = playerPosition.x;
targetY = playerPosition.y;
startX = Math.max(100, Math.min(canvas.width - 100, startX));
startY = Math.max(100, Math.min(canvas.height - 100, startY));
} else {
const edge = Math.floor(Math.random() * 4);
switch(edge) {
case 0:
startX = Math.random() * canvas.width;
startY = -50;
break;
case 1:
startX = canvas.width + 50;
startY = Math.random() * canvas.height;
break;
case 2:
startX = Math.random() * canvas.width;
startY = canvas.height + 50;
break;
case 3:
startX = -50;
startY = Math.random() * canvas.height;
break;
}
targetX = playerPosition.x;
targetY = playerPosition.y;
}
const speed = skillSpec.speed * (0.8 + Math.random() * 0.4);
if (skillType === 'prediction' && playerPositions.length > 0) {
const predictionAmount = config.preditionLevel;
targetX = playerPosition.x + playerVelocity.x * 20 * predictionAmount;
targetY = playerPosition.y + playerVelocity.y * 20 * predictionAmount;
targetX = Math.max(50, Math.min(canvas.width - 50, targetX));
targetY = Math.max(50, Math.min(canvas.height - 50, targetY));
} else if (skillType === 'homing') {
targetX = playerPosition.x;
targetY = playerPosition.y;
}
const dx = targetX - startX;
const dy = targetY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const directionX = dx / distance;
const directionY = dy / distance;
const rangeSkillProps = skillType === 'range' ? {
currentRadius: skillSpec.radius,
targetRadius: skillSpec.targetRadius,
shrinkSpeed: skillSpec.shrinkSpeed,
fadeProgress: 0,
highlight: false,
highlightTime: 0
} : {};
return {
type: skillType,
x: startX,
y: startY,
radius: skillSpec.radius,
color: skillSpec.color,
speed: speed,
directionX: skillType === 'range' ? 0 : directionX,
directionY: skillType === 'range' ? 0 : directionY,
lifetime: skillSpec.lifetime,
age: 0,
targetX: targetX,
targetY: targetY,
homing: skillType === 'homing',
health: 1,
...rangeSkillProps
};
}
function checkCollisions() {
for (let i = playerAttacks.length - 1; i >= 0; i--) {
const attack = playerAttacks[i];
for (let j = skills.length - 1; j >= 0; j--) {
const skill = skills[j];
const dx = attack.x - skill.x;
const dy = attack.y - skill.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < attack.radius + (skill.type === 'range' ? skill.currentRadius : skill.radius)) {
playerAttacks.splice(i, 1);
skill.health -= attack.damage;
if (skill.health <= 0) {
skills.splice(j, 1);
onSkillDestroyed();
}
break;
}
}
}
for (let i = skills.length - 1; i >= 0; i--) {
const skill = skills[i];
const dx = playerPosition.x - skill.x;
const dy = playerPosition.y - skill.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const playerCollisionRadius = Math.min(playerWidth, playerHeight) / 2 * 0.7;
const skillRadius = skill.type === 'range' ? skill.currentRadius : skill.radius;
if (distance < playerCollisionRadius + skillRadius) {
skills.splice(i, 1);
onPlayerHit();
return;
}
}
}
function onPlayerHit() {
lives--;
combo = 0;
canvas.style.backgroundColor = '#ff0000';
setTimeout(() => {
canvas.style.backgroundColor = '#0d0d1a';
}, 100);
updateUI();
}
function onSkillDodged() {
score++;
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
if (combo >= 5 && combo % 5 === 0) {
const bonus = Math.floor(combo / 5) * 5;
score += bonus;
const comboDisplay = document.getElementById('comboDisplay');
comboDisplay.textContent = `+${bonus} Combo!`;
comboDisplay.style.opacity = '1';
setTimeout(() => {
comboDisplay.style.opacity = '0';
}, 1000);
}
updateUI();
}
function onSkillDestroyed() {
score += 2;
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
if (combo >= 5 && combo % 5 === 0) {
const bonus = Math.floor(combo / 5) * 5;
score += bonus;
const comboDisplay = document.getElementById('comboDisplay');
comboDisplay.textContent = `+${bonus} Destroy!`;
comboDisplay.style.opacity = '1';
setTimeout(() => {
comboDisplay.style.opacity = '0';
}, 1000);
}
updateUI();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
for (const skill of skills) {
drawSkill(skill);
}
for (const attack of playerAttacks) {
drawPlayerAttack(attack);
}
drawPlayerTopView();
if (difficulty === 'faker' && gameRunning && !gamePaused) {
drawPlayerTrail();
}
if (playerPosition.x !== playerTargetPosition.x || playerPosition.y !== playerTargetPosition.y) {
drawTargetIndicator();
}
}
function drawGrid() {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
const baseGridSize = 60;
const scale = canvas.width / canvasBaseWidth;
const gridSize = baseGridSize * scale;
for (let x = 0; x <= canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y <= canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
}
function drawSkill(skill) {
ctx.save();
const scale = canvas.width / canvasBaseWidth;
if (skill.type === 'range') {
const currentRadius = skill.currentRadius * scale;
const targetRadius = skill.targetRadius * scale;
const alpha = 0.7 * (1 - skill.fadeProgress);
ctx.globalAlpha = alpha;
ctx.strokeStyle = skill.color;
ctx.lineWidth = 4 * scale;
ctx.beginPath();
ctx.arc(skill.x, skill.y, currentRadius, 0, Math.PI * 2);
ctx.stroke();
if (skill.fadeProgress > 0) {
ctx.globalAlpha = alpha * 0.3;
ctx.fillStyle = skill.color;
ctx.beginPath();
ctx.arc(skill.x, skill.y, targetRadius, 0, Math.PI * 2);
ctx.fill();
for (let r = targetRadius; r < currentRadius; r += 10 * scale) {
const progress = (r - targetRadius) / (currentRadius - targetRadius);
ctx.globalAlpha = alpha * (1 - progress) * 0.5;
ctx.strokeStyle = skill.color;
ctx.lineWidth = 2 * scale;
ctx.beginPath();
ctx.arc(skill.x, skill.y, r, 0, Math.PI * 2);
ctx.stroke();
}
}
if (skill.highlight) {
ctx.globalAlpha = 0.5;
ctx.fillStyle = skill.color;
ctx.beginPath();
ctx.arc(skill.x, skill.y, currentRadius * 1.2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.8;
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 3 * scale;
ctx.beginPath();
ctx.arc(skill.x, skill.y, currentRadius * 1.1, 0, Math.PI * 2);
ctx.stroke();
}
} else {
const adjustedRadius = skill.radius * scale;
ctx.fillStyle = skill.color;
ctx.beginPath();
ctx.arc(skill.x, skill.y, adjustedRadius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowColor = skill.color;
ctx.shadowBlur = 15 * scale;
ctx.beginPath();
ctx.arc(skill.x, skill.y, adjustedRadius * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = skill.color;
ctx.lineWidth = 3 * scale;
ctx.globalAlpha = 0.6;
ctx.beginPath();
ctx.moveTo(skill.x, skill.y);
const trailLength = 30 * scale;
ctx.lineTo(
skill.x - skill.directionX * trailLength,
skill.y - skill.directionY * trailLength
);
ctx.stroke();
ctx.globalAlpha = 1;
}
ctx.restore();
}
function drawPlayerAttack(attack) {
ctx.save();
const scale = canvas.width / canvasBaseWidth;
const radius = attack.radius * scale;
ctx.fillStyle = attack.color;
ctx.shadowColor = attack.color;
ctx.shadowBlur = 20 * scale;
ctx.beginPath();
ctx.arc(attack.x, attack.y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 1.5 * scale;
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.arc(attack.x, attack.y, radius * 1.5, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = '#FF9800';
ctx.lineWidth = 3 * scale;
ctx.globalAlpha = 0.5;
ctx.beginPath();
ctx.moveTo(attack.x, attack.y);
ctx.lineTo(
attack.x - attack.directionX * 22.5 * scale,
attack.y - attack.directionY * 22.5 * scale
);
ctx.stroke();
ctx.restore();
}
function drawPlayerTopView() {
ctx.save();
const scale = canvas.width / canvasBaseWidth;
const width = playerWidth * scale;
const height = playerHeight * scale;
ctx.translate(playerPosition.x, playerPosition.y);
const angle = Math.atan2(playerAttackDirection.y, playerAttackDirection.x);
ctx.rotate(angle);
ctx.fillStyle = '#1E88E5';
ctx.beginPath();
ctx.arc(0, height/6, height/2.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1565C0';
ctx.beginPath();
ctx.ellipse(0, -height/8, width/2.5, height/4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFCC99';
ctx.beginPath();
ctx.arc(0, 0, width/4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(0, -width/8, width/4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#333333';
ctx.beginPath();
ctx.arc(-width/10, -width/20, width/20, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(width/10, -width/20, width/20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1E88E5';
ctx.fillRect(width/3, -height/6, width/3, height/8);
ctx.fillRect(-width/2, -height/6, width/3, height/8);
if (attackCooldown <= 0 && isMouseOverCanvas) {
ctx.fillStyle = '#4FC3F7';
ctx.shadowColor = '#4FC3F7';
ctx.shadowBlur = 15 * scale;
ctx.beginPath();
ctx.arc(width/2, -height/12, width/6, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
ctx.restore();
}
function drawTargetIndicator() {
ctx.save();
const scale = canvas.width / canvasBaseWidth;
ctx.strokeStyle = '#4CAF50';
ctx.lineWidth = 3 * scale;
ctx.setLineDash([7.5 * scale, 7.5 * scale]);
ctx.beginPath();
ctx.moveTo(playerPosition.x, playerPosition.y);
ctx.lineTo(playerTargetPosition.x, playerTargetPosition.y);
ctx.stroke();
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.arc(playerTargetPosition.x, playerTargetPosition.y, 7.5 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.setLineDash([]);
ctx.restore();
}
function drawPlayerTrail() {
if (playerPositions.length < 2) return;
ctx.save();
ctx.strokeStyle = 'rgba(30, 136, 229, 0.3)';
ctx.lineWidth = 3 * (canvas.width / canvasBaseWidth);
ctx.beginPath();
ctx.moveTo(playerPositions[0].x, playerPositions[0].y);
for (let i = 1; i < playerPositions.length; i++) {
ctx.lineTo(playerPositions[i].x, playerPositions[i].y);
}
ctx.stroke();
ctx.restore();
}
function updateUI() {
document.getElementById('score').textContent = score;
document.getElementById('combo').textContent = combo;
document.getElementById('lives').textContent = lives;
document.getElementById('time').textContent = Math.floor(gameTime);
document.getElementById('attackCooldown').textContent = attackCooldown > 0 ? attackCooldown.toFixed(1) : '0';
document.getElementById('flashCooldown').textContent = flashCooldown > 0 ? flashCooldown.toFixed(1) : '0';
const compactAttackBtn = document.getElementById('compactAttackBtn');
if (attackCooldown > 0 || !gameRunning || gamePaused) {
compactAttackBtn.disabled = true;
compactAttackBtn.style.opacity = '0.5';
} else {
compactAttackBtn.disabled = false;
compactAttackBtn.style.opacity = '1';
}
const compactFlashBtn = document.getElementById('compactFlashBtn');
if (flashCooldown > 0 || !gameRunning || gamePaused) {
compactFlashBtn.disabled = true;
compactFlashBtn.style.opacity = '0.5';
} else {
compactFlashBtn.disabled = false;
compactFlashBtn.style.opacity = '1';
}
}
function togglePause() {
if (!gameRunning) return;
gamePaused = !gamePaused;
const compactPauseText = gamePaused ? '继续' : '暂停';
document.getElementById('compactPauseBtn').textContent = compactPauseText;
if (gamePaused) {
document.getElementById('aimIndicator').style.opacity = '0';
} else {
updateAimIndicator();
}
}
function resetGame() {
gameRunning = false;
gamePaused = false;
score = 0;
combo = 0;
maxCombo = 0;
lives = 3;
gameTime = 0;
skills = [];
playerAttacks = [];
attackCooldown = 0;
flashCooldown = 0;
updateUI();
document.getElementById('compactPauseBtn').textContent = '暂停';
document.getElementById('aimIndicator').style.opacity = '0';
draw();
}
function restartGame() {
resetGame();
startGame();
}
function returnToMenu() {
resetGame();
document.getElementById('gameOverScreen').style.display = 'none';
}
function endGame() {
gameRunning = false;
document.getElementById('finalScore').textContent = score;
document.getElementById('gameOverScreen').style.display = 'flex';
document.getElementById('aimIndicator').style.opacity = '0';
}
let lastTime = 0;
</script>
</body>
</html>

