!5 merge dev into main
重构前端、增加备案号 Created-by: qingss0 Commit-by: qing;shufeng2012 Merged-by: qingss0 Description: 重构前端 refix fix dnfs ui 修改前端 修改标题为树菜单相关 ... See merge request: qingss0/micro_servers!5
This commit is contained in:
26 files changed
+7487
-121
No files matched your search
+1
-1
@@ -1,3 +1,3 @@
|
||||
bash /opt/micro_servers/static/_run.sh
|
||||
bash /opt/micro_servers/dnfs/_run.sh
|
||||
bash /opt/micro_servers/static/_run.sh
|
||||
bash /opt/micro_servers/gateway/_run.sh
|
||||
+4
-4
@@ -186,10 +186,10 @@ def update_note():
|
||||
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def view_page():
|
||||
"""返回应用主页"""
|
||||
return render_template('index.html')
|
||||
#@app.route('/')
|
||||
#def view_page():
|
||||
# """返回应用主页"""
|
||||
# return render_template('index.html')
|
||||
|
||||
ADDR='127.0.0.1'
|
||||
PORT=8006
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[Server]
|
||||
address = 0.0.0.0
|
||||
port = 9201
|
||||
port = 443
|
||||
|
||||
[Proxy]
|
||||
default = http://127.0.0.1:9200
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
#source ./venv/bin/activate
|
||||
exec ./fsys.bin --config config.ini 1>/dev/null
|
||||
source ./venv/bin/activate
|
||||
#exec ./fsys.bin --config config.ini 1>/dev/null
|
||||
exec python3 ./fsys.py --config config.ini 1>/dev/null
|
||||
+17
-1
@@ -8,12 +8,20 @@ import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
import sys
|
||||
|
||||
# 基准目录:编译后取可执行文件所在目录,开发时取脚本所在目录
|
||||
# 解决 Nuitka onefile 解压到 /tmp 后相对路径失效的问题
|
||||
if getattr(sys, 'frozen', False):
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(sys.executable))
|
||||
else:
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
file_handler = RotatingFileHandler('app.log', maxBytes=10485760, backupCount=10)
|
||||
file_handler = RotatingFileHandler(os.path.join(BASE_DIR, 'app.log'), maxBytes=10485760, backupCount=10)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s'
|
||||
))
|
||||
@@ -29,6 +37,8 @@ CORS(app)
|
||||
|
||||
# ==================== 配置文件加载 ====================
|
||||
CONFIG_FILE = args.config
|
||||
if not os.path.isabs(CONFIG_FILE):
|
||||
CONFIG_FILE = os.path.join(BASE_DIR, CONFIG_FILE)
|
||||
|
||||
file_config = {
|
||||
'server': {'host': '0.0.0.0', 'port': '5000'},
|
||||
@@ -46,6 +56,8 @@ if os.path.exists(CONFIG_FILE):
|
||||
file_config[section][key] = value
|
||||
|
||||
HTML_ROOT_DIR = file_config['directory']['root']
|
||||
if HTML_ROOT_DIR and not os.path.isabs(HTML_ROOT_DIR):
|
||||
HTML_ROOT_DIR = os.path.join(BASE_DIR, HTML_ROOT_DIR)
|
||||
FILESYSTEM_ENABLED = bool(HTML_ROOT_DIR and HTML_ROOT_DIR.strip())
|
||||
|
||||
if FILESYSTEM_ENABLED:
|
||||
@@ -59,6 +71,10 @@ if FILESYSTEM_ENABLED:
|
||||
SSL_ENABLED = file_config['ssl'].get('enabled', 'false').lower() == 'true'
|
||||
SSL_CERT_FILE = file_config['ssl'].get('cert_file', 'cert.pem')
|
||||
SSL_KEY_FILE = file_config['ssl'].get('key_file', 'key.pem')
|
||||
if not os.path.isabs(SSL_CERT_FILE):
|
||||
SSL_CERT_FILE = os.path.join(BASE_DIR, SSL_CERT_FILE)
|
||||
if not os.path.isabs(SSL_KEY_FILE):
|
||||
SSL_KEY_FILE = os.path.join(BASE_DIR, SSL_KEY_FILE)
|
||||
|
||||
# 视图文件名:根路径与目录访问时固定返回此静态文件,由前端 JS 调用 /api/filelist 拉取数据
|
||||
VIEW_FILE = '__view.html'
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
|
||||
<title>文件浏览</title>
|
||||
<title>树菜单 · 文件浏览</title>
|
||||
<link rel="stylesheet" href="css/view.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<h1>📁 文件浏览</h1>
|
||||
<h1>📁 树菜单 · 文件浏览</h1>
|
||||
<nav class="header-actions">
|
||||
<a href="/" title="根目录">🏠</a>
|
||||
<a href="#" id="btnRefresh" title="刷新">🔄</a>
|
||||
@@ -43,6 +43,10 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
|
||||
</footer>
|
||||
|
||||
<script src="js/view.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,71 +0,0 @@
|
||||
code {
|
||||
color: red;
|
||||
background-color: rgb(254, 235, 238);
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
pre > code {
|
||||
display: block;
|
||||
color: white;
|
||||
background-color: rgb(96, 96, 96);
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
blockquote {
|
||||
margin-right: 0px;
|
||||
background-color: #cfcfcf;
|
||||
}
|
||||
|
||||
/* by qing */
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fffbf0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto,sans-serif;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #eedeb0;
|
||||
border: 1px solid #000000;
|
||||
border-radius: 1.125rem;
|
||||
padding: 1.5rem 1.25rem;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04), 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
/*max-width: 480px;*/
|
||||
width: 90%;
|
||||
color: #4d3e2b;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #c7b38a;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
/* 移动端微调 */
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.2rem, 1rem;
|
||||
border-radius: 1rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 极小屏 (宽度 < 360px) */
|
||||
@media (max-width: 360px) {
|
||||
.card {
|
||||
padding: 1rem 0.8rem;
|
||||
border-radius: 0.9rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/* ===== Design Tokens ===== */
|
||||
:root {
|
||||
--bg: #fdf6ee;
|
||||
--fg: #4a3728;
|
||||
--fg-secondary: #8c7260;
|
||||
--card: #ffffff;
|
||||
--card-hover: #fff7ef;
|
||||
--border: #f0e0cc;
|
||||
--border-light: #f7ecdd;
|
||||
--muted: #b89b7a;
|
||||
--accent: #e8853a;
|
||||
--accent-dark: #cf6b1f;
|
||||
--accent-soft: rgba(232, 133, 58, 0.10);
|
||||
--accent-glow: rgba(232, 133, 58, 0.25);
|
||||
--error: #d94f3d;
|
||||
--error-bg: #fef0ee;
|
||||
--success: #5a9e6f;
|
||||
--warning: #d4922a;
|
||||
--shadow-sm: 0 1px 3px rgba(74, 55, 40, 0.06);
|
||||
--shadow-md: 0 4px 12px rgba(74, 55, 40, 0.08);
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 20px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #1e1712;
|
||||
--fg: #f0e4d8;
|
||||
--fg-secondary: #a08c78;
|
||||
--card: #2a2119;
|
||||
--card-hover: #342a20;
|
||||
--border: #3e3228;
|
||||
--border-light: #342a20;
|
||||
--muted: #8c7a66;
|
||||
--accent: #e8934a;
|
||||
--accent-dark: #f0a060;
|
||||
--accent-soft: rgba(232, 147, 74, 0.12);
|
||||
--accent-glow: rgba(232, 147, 74, 0.20);
|
||||
--error: #e8685a;
|
||||
--error-bg: #3a2220;
|
||||
--success: #6fb87f;
|
||||
--warning: #e0a84a;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Base ===== */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Roboto, Arial, sans-serif;
|
||||
padding: 2rem 1rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
body > .card,
|
||||
body > .box { margin: auto 0; }
|
||||
|
||||
/* ===== Layout ===== */
|
||||
.box { width: 90%; max-width: 720px; }
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.75rem 1.5rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--fg);
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
overflow-wrap: break-word;
|
||||
width: 90%;
|
||||
max-width: 440px;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: var(--border);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.box > .card { width: 100%; max-width: none; }
|
||||
|
||||
/* ===== Typography ===== */
|
||||
h1 { font-size: 1.4rem; font-weight: 700; }
|
||||
h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 1.25rem; }
|
||||
h3 { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
|
||||
/* ===== Form Elements ===== */
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--fg-secondary);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
form > div > label:first-child { margin-top: 0; }
|
||||
|
||||
input:not([type="checkbox"]):not([type="submit"]):not([type="reset"]):not([type="button"]),
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
font-family: inherit;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
input:not([type="checkbox"]):not([type="submit"]):not([type="reset"]):not([type="button"]):focus,
|
||||
textarea:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
input::placeholder, textarea::placeholder { color: var(--muted); }
|
||||
textarea { min-height: 80px; resize: vertical; }
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
button, input[type="submit"], input[type="reset"], input[type="button"] {
|
||||
padding: 0.55rem 1.3rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s, transform 0.1s;
|
||||
line-height: 1.4;
|
||||
}
|
||||
button:hover, input[type="submit"]:hover,
|
||||
input[type="reset"]:hover, input[type="button"]:hover { opacity: 0.85; }
|
||||
button:active, input[type="submit"]:active,
|
||||
input[type="reset"]:active, input[type="button"]:active { transform: scale(0.96); }
|
||||
|
||||
input[type="submit"] { background: var(--accent); color: #fff; }
|
||||
input[type="reset"] { background: transparent; color: var(--muted); border: 1.5px solid var(--border); }
|
||||
button[type="button"], input[type="button"] { background: var(--accent-soft); color: var(--accent-dark); }
|
||||
|
||||
#emlSub { display: inline-block; margin-top: 0.4rem; }
|
||||
#emlSub input[type="submit"] {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent-dark);
|
||||
font-size: 0.82rem;
|
||||
padding: 0.45rem 1rem;
|
||||
}
|
||||
|
||||
/* ===== Form Layout ===== */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
.form-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.form-check input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); }
|
||||
.form-check label {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
|
||||
/* ===== Code Box (register) ===== */
|
||||
.code-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.code-box input[type="text"] { width: auto; flex: 1; min-width: 100px; display: inline-block; }
|
||||
.code-box input[type="button"] { white-space: nowrap; }
|
||||
|
||||
/* ===== Validation Tips ===== */
|
||||
.tip-red { color: var(--error); font-size: 0.78rem; }
|
||||
.tip-yellow { color: var(--warning); font-size: 0.78rem; }
|
||||
.tip-green { color: var(--success); font-size: 0.78rem; }
|
||||
|
||||
/* ===== Article (dnfs) ===== */
|
||||
.article-header { text-align: center; margin-bottom: 1.25rem; }
|
||||
.article-header h1 { margin-bottom: 0.5rem; }
|
||||
.article-meta { font-size: 0.82rem; color: var(--muted); margin-bottom: 0.3rem; }
|
||||
.article-stats {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.article-stats span { font-weight: 600; color: var(--fg-secondary); }
|
||||
#article-content { font-size: 0.95rem; line-height: 1.7; margin-bottom: 1.25rem; }
|
||||
|
||||
/* ===== Recommend List (dnfs) ===== */
|
||||
.recommend-list .card { margin-bottom: 0.75rem; width: 100%; max-width: none; }
|
||||
.recommend-list center {
|
||||
cursor: pointer;
|
||||
padding: 0.4rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.recommend-list center:hover { background: var(--accent-soft); }
|
||||
.recommend-list center:hover h3 { color: var(--accent); }
|
||||
.recommend-list .card h3 { transition: color 0.15s; }
|
||||
.recommend-list .card p { font-size: 0.9rem; line-height: 1.6; color: var(--fg-secondary); }
|
||||
|
||||
/* ===== Node Form (dnfs) ===== */
|
||||
.node-form { margin-top: 1rem; }
|
||||
.node-form .form-actions { margin-top: 0.75rem; }
|
||||
|
||||
/* ===== Markdown Content ===== */
|
||||
code {
|
||||
color: var(--error);
|
||||
background: var(--error-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
pre > code {
|
||||
display: block;
|
||||
color: #f0e4d8;
|
||||
background: #3d3229;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow-x: auto;
|
||||
}
|
||||
blockquote {
|
||||
margin-right: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--accent-soft);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
padding-top: 1.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.footer a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.footer a:hover { color: var(--accent); }
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 480px) {
|
||||
body { padding: 1rem 0.8rem; }
|
||||
.card { padding: 1.25rem 1rem; border-radius: 16px; width: 95%; }
|
||||
.box { width: 95%; }
|
||||
h2 { font-size: 1.1rem; }
|
||||
.form-actions button, .form-actions input { flex: 1; padding: 0.5rem 0.6rem; font-size: 0.82rem; }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.card { padding: 1rem 0.8rem; border-radius: var(--radius-sm); }
|
||||
}
|
||||
@@ -331,6 +331,20 @@ body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ===== 页脚 ===== */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 24px 20px 32px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.footer a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.footer a:hover { color: var(--accent); }
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
@media (max-width: 480px) {
|
||||
.header { padding: 0 14px; height: 50px; }
|
||||
|
||||
+15
-11
@@ -4,20 +4,20 @@
|
||||
<meta charset="utf-8">
|
||||
<!-- 移动端适配核心标签 -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>DNFS · 树状笔记</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<title>树状笔记</title>
|
||||
<link rel="stylesheet" href="css/theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<center>
|
||||
<center class="article-header">
|
||||
<!-- 文章主标题 -->
|
||||
<h1><div id="article-title">title</div></h1>
|
||||
|
||||
<!-- 文章详情 -->
|
||||
<div>
|
||||
<div class="article-meta">
|
||||
发布时间:<span id="publish-time">2026-1-1</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="article-stats">
|
||||
浏览:<span id="visit-count">999</span>
|
||||
点赞:<span id="like-count">999</span>
|
||||
分享:<span id="share-count">999</span>
|
||||
@@ -25,18 +25,22 @@
|
||||
</center>
|
||||
<div id="article-content">hello</div>
|
||||
<div class="recommend-list" id="recommend-list"></div>
|
||||
<br>
|
||||
<div class="card">
|
||||
<div class="card node-form">
|
||||
<h3>节点操作</h3>
|
||||
<label>新增节点</label><input id="input-note-title"></input><br>
|
||||
<label>节点内容</label><textarea id="input-note-content"></textarea><br>
|
||||
<button id="input-note-submit">提交</button>
|
||||
<button id="input-note-clear">重置</button>
|
||||
<label>新增节点</label><input id="input-note-title">
|
||||
<label>节点内容</label><textarea id="input-note-content"></textarea>
|
||||
<div class="form-actions">
|
||||
<button id="input-note-submit">提交</button>
|
||||
<button id="input-note-clear">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.0/markdown-it.min.js"></script>
|
||||
<script src="js/validation.js"></script>
|
||||
<script src="js/dnfs.js"></script>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 卿松
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,345 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>记忆翻牌游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Arial Rounded MT Bold', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.game-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
margin: 0 auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 120px;
|
||||
perspective: 1000px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
transition: transform 0.6s;
|
||||
transform-style: preserve-3d;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card.flipped .card-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card-front, .card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card-front {
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-back {
|
||||
background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%);
|
||||
color: white;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card.matched .card-back {
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.game-board {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.game-board {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>记忆翻牌游戏</h1>
|
||||
<p>找出所有匹配的卡片对,考验你的记忆力!</p>
|
||||
|
||||
<div class="game-info">
|
||||
<div class="info-item">时间: <span id="timer">0</span>秒</div>
|
||||
<div class="info-item">移动次数: <span id="moves">0</span></div>
|
||||
<div class="info-item">匹配: <span id="matches">0</span>/8</div>
|
||||
</div>
|
||||
|
||||
<div class="game-board" id="gameBoard"></div>
|
||||
|
||||
<div class="message" id="message"></div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="startBtn">开始游戏</button>
|
||||
<button id="resetBtn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const gameBoard = document.getElementById('gameBoard');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const timerEl = document.getElementById('timer');
|
||||
const movesEl = document.getElementById('moves');
|
||||
const matchesEl = document.getElementById('matches');
|
||||
const messageEl = document.getElementById('message');
|
||||
|
||||
let cards = [];
|
||||
let flippedCards = [];
|
||||
let matchedPairs = 0;
|
||||
let moves = 0;
|
||||
let timer = 0;
|
||||
let timerInterval;
|
||||
let gameStarted = false;
|
||||
|
||||
// 卡片符号
|
||||
const cardSymbols = ['🍎', '🍌', '🍒', '🍇', '🍊', '🍓', '🍑', '🥭'];
|
||||
|
||||
// 初始化游戏
|
||||
function initGame() {
|
||||
// 创建卡片数组(每对两张)
|
||||
cards = [...cardSymbols, ...cardSymbols];
|
||||
|
||||
// 洗牌算法
|
||||
for (let i = cards.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[cards[i], cards[j]] = [cards[j], cards[i]];
|
||||
}
|
||||
|
||||
// 清空游戏板
|
||||
gameBoard.innerHTML = '';
|
||||
|
||||
// 创建卡片元素
|
||||
cards.forEach((symbol, index) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.dataset.index = index;
|
||||
card.dataset.symbol = symbol;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-inner">
|
||||
<div class="card-front">?</div>
|
||||
<div class="card-back">${symbol}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.addEventListener('click', flipCard);
|
||||
gameBoard.appendChild(card);
|
||||
});
|
||||
|
||||
// 重置游戏状态
|
||||
flippedCards = [];
|
||||
matchedPairs = 0;
|
||||
moves = 0;
|
||||
timer = 0;
|
||||
gameStarted = false;
|
||||
|
||||
// 更新显示
|
||||
movesEl.textContent = moves;
|
||||
matchesEl.textContent = matchedPairs;
|
||||
timerEl.textContent = timer;
|
||||
messageEl.textContent = '点击"开始游戏"按钮开始';
|
||||
}
|
||||
|
||||
// 翻转卡片
|
||||
function flipCard() {
|
||||
if (!gameStarted) return;
|
||||
|
||||
const card = this;
|
||||
|
||||
// 如果卡片已经匹配或已经翻转,则忽略点击
|
||||
if (card.classList.contains('matched') || card.classList.contains('flipped')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 翻转卡片
|
||||
card.classList.add('flipped');
|
||||
flippedCards.push(card);
|
||||
|
||||
// 如果翻转了两张卡片,检查是否匹配
|
||||
if (flippedCards.length === 2) {
|
||||
moves++;
|
||||
movesEl.textContent = moves;
|
||||
|
||||
const card1 = flippedCards[0];
|
||||
const card2 = flippedCards[1];
|
||||
|
||||
if (card1.dataset.symbol === card2.dataset.symbol) {
|
||||
// 匹配成功
|
||||
card1.classList.add('matched');
|
||||
card2.classList.add('matched');
|
||||
matchedPairs++;
|
||||
matchesEl.textContent = matchedPairs;
|
||||
|
||||
flippedCards = [];
|
||||
|
||||
// 检查游戏是否结束
|
||||
if (matchedPairs === cardSymbols.length) {
|
||||
endGame();
|
||||
}
|
||||
} else {
|
||||
// 不匹配,翻转回去
|
||||
setTimeout(() => {
|
||||
card1.classList.remove('flipped');
|
||||
card2.classList.remove('flipped');
|
||||
flippedCards = [];
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
function startGame() {
|
||||
if (gameStarted) return;
|
||||
|
||||
gameStarted = true;
|
||||
messageEl.textContent = '游戏进行中...';
|
||||
|
||||
// 启动计时器
|
||||
timerInterval = setInterval(() => {
|
||||
timer++;
|
||||
timerEl.textContent = timer;
|
||||
}, 1000);
|
||||
|
||||
// 禁用开始按钮
|
||||
startBtn.disabled = true;
|
||||
}
|
||||
|
||||
// 结束游戏
|
||||
function endGame() {
|
||||
clearInterval(timerInterval);
|
||||
messageEl.textContent = `恭喜!你用了${moves}步和${timer}秒完成了游戏!`;
|
||||
startBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 重置游戏
|
||||
function resetGame() {
|
||||
clearInterval(timerInterval);
|
||||
initGame();
|
||||
startBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
startBtn.addEventListener('click', startGame);
|
||||
resetBtn.addEventListener('click', resetGame);
|
||||
|
||||
// 初始化游戏
|
||||
initGame();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,813 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>太空入侵者</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff;
|
||||
letter-spacing: 3px;
|
||||
background: linear-gradient(to right, #00ffff, #ff00ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 20px;
|
||||
color: #a0e7ff;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
border: 3px solid #00ffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px #00ffff, inset 0 0 20px rgba(0, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
background: #000;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.game-ui {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #00ffff;
|
||||
}
|
||||
|
||||
.ui-item {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px #00ffff;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #00ffff, #ff00ff);
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 0 15px rgba(0, 255, 255, 0.7);
|
||||
transition: all 0.3s ease;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 0 20px rgba(0, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #555;
|
||||
color: #888;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 25px;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #00ffff;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
color: #00ffff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 8px 0;
|
||||
color: #a0e7ff;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
border: 2px solid #ff00ff;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 30px #ff00ff;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
font-size: 2.2rem;
|
||||
color: #ff00ff;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 0 0 10px #ff00ff;
|
||||
}
|
||||
|
||||
.game-over p {
|
||||
font-size: 1.2rem;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stars {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
animation: twinkle 5s infinite;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.game-ui {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.ui-item {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.game-over p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stars" id="stars"></div>
|
||||
|
||||
<div class="container">
|
||||
<h1>太空入侵者</h1>
|
||||
<p class="subtitle">保卫地球,击退外星入侵者!</p>
|
||||
|
||||
<div class="game-ui">
|
||||
<div class="ui-item">分数: <span id="score">0</span></div>
|
||||
<div class="ui-item">生命: <span id="lives">3</span></div>
|
||||
<div class="ui-item">等级: <span id="level">1</span></div>
|
||||
</div>
|
||||
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="800" height="500"></canvas>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>游戏结束</h2>
|
||||
<p>你的最终分数: <span id="finalScore">0</span></p>
|
||||
<p id="gameMessage">地球需要你,再试一次!</p>
|
||||
<button id="restartBtn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="startBtn">开始游戏</button>
|
||||
<button id="pauseBtn">暂停游戏</button>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>游戏说明</h3>
|
||||
<p>← → 键: 移动飞船</p>
|
||||
<p>空格键: 发射激光</p>
|
||||
<p>躲避敌人的攻击,击落所有外星飞船!</p>
|
||||
<p>每关敌人数量和速度会增加,坚持越久得分越高!</p>
|
||||
<p>每得1000分升一级,敌人速度会加快!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 创建星空背景
|
||||
createStars();
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const pauseBtn = document.getElementById('pauseBtn');
|
||||
const restartBtn = document.getElementById('restartBtn');
|
||||
const scoreEl = document.getElementById('score');
|
||||
const livesEl = document.getElementById('lives');
|
||||
const levelEl = document.getElementById('level');
|
||||
const gameOverEl = document.getElementById('gameOver');
|
||||
const finalScoreEl = document.getElementById('finalScore');
|
||||
const gameMessageEl = document.getElementById('gameMessage');
|
||||
|
||||
// 游戏状态
|
||||
let game = {
|
||||
running: false,
|
||||
paused: false,
|
||||
score: 0,
|
||||
lives: 3,
|
||||
level: 1,
|
||||
enemies: [],
|
||||
bullets: [],
|
||||
enemyBullets: [],
|
||||
particles: [],
|
||||
lastEnemySpawn: 0,
|
||||
lastEnemyShoot: 0
|
||||
};
|
||||
|
||||
// 玩家飞船
|
||||
const player = {
|
||||
x: canvas.width / 2 - 25,
|
||||
y: canvas.height - 70,
|
||||
width: 50,
|
||||
height: 60,
|
||||
speed: 8,
|
||||
color: '#00ffff',
|
||||
lastShot: 0,
|
||||
shotDelay: 300
|
||||
};
|
||||
|
||||
// 键盘状态
|
||||
const keys = {};
|
||||
|
||||
// 初始化游戏
|
||||
function initGame() {
|
||||
game = {
|
||||
running: true,
|
||||
paused: false,
|
||||
score: 0,
|
||||
lives: 3,
|
||||
level: 1,
|
||||
enemies: [],
|
||||
bullets: [],
|
||||
enemyBullets: [],
|
||||
particles: [],
|
||||
lastEnemySpawn: 0,
|
||||
lastEnemyShoot: 0
|
||||
};
|
||||
|
||||
player.x = canvas.width / 2 - 25;
|
||||
player.y = canvas.height - 70;
|
||||
|
||||
updateUI();
|
||||
gameOverEl.style.display = 'none';
|
||||
pauseBtn.textContent = '暂停游戏';
|
||||
|
||||
// 开始游戏循环
|
||||
if (!game.paused) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建星空背景
|
||||
function createStars() {
|
||||
const starsContainer = document.getElementById('stars');
|
||||
const starCount = 150;
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const star = document.createElement('div');
|
||||
star.className = 'star';
|
||||
|
||||
const size = Math.random() * 3;
|
||||
star.style.width = `${size}px`;
|
||||
star.style.height = `${size}px`;
|
||||
|
||||
star.style.left = `${Math.random() * 100}%`;
|
||||
star.style.top = `${Math.random() * 100}%`;
|
||||
|
||||
star.style.animationDelay = `${Math.random() * 5}s`;
|
||||
|
||||
starsContainer.appendChild(star);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制玩家飞船
|
||||
function drawPlayer() {
|
||||
ctx.fillStyle = player.color;
|
||||
|
||||
// 飞船主体
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(player.x + player.width / 2, player.y);
|
||||
ctx.lineTo(player.x + player.width, player.y + player.height);
|
||||
ctx.lineTo(player.x, player.y + player.height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 飞船引擎
|
||||
ctx.fillStyle = '#ff9900';
|
||||
ctx.fillRect(player.x + player.width / 2 - 10, player.y + player.height, 20, 10);
|
||||
|
||||
// 飞船细节
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(player.x + player.width / 2 - 5, player.y + 10, 10, 15);
|
||||
}
|
||||
|
||||
// 生成敌人
|
||||
function spawnEnemy() {
|
||||
const now = Date.now();
|
||||
if (now - game.lastEnemySpawn > 1000 - game.level * 50) {
|
||||
const enemy = {
|
||||
x: Math.random() * (canvas.width - 40),
|
||||
y: -40,
|
||||
width: 40,
|
||||
height: 40,
|
||||
speed: 1 + game.level * 0.2,
|
||||
color: `hsl(${Math.random() * 360}, 100%, 60%)`,
|
||||
health: 1
|
||||
};
|
||||
|
||||
game.enemies.push(enemy);
|
||||
game.lastEnemySpawn = now;
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制敌人
|
||||
function drawEnemies() {
|
||||
game.enemies.forEach(enemy => {
|
||||
ctx.fillStyle = enemy.color;
|
||||
|
||||
// 敌人飞船主体
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(enemy.x + enemy.width / 2, enemy.y + enemy.height);
|
||||
ctx.lineTo(enemy.x + enemy.width, enemy.y);
|
||||
ctx.lineTo(enemy.x, enemy.y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 敌人飞船细节
|
||||
ctx.fillStyle = '#ff0000';
|
||||
ctx.fillRect(enemy.x + enemy.width / 2 - 5, enemy.y + 10, 10, 10);
|
||||
});
|
||||
}
|
||||
|
||||
// 绘制子弹
|
||||
function drawBullets() {
|
||||
// 玩家子弹
|
||||
ctx.fillStyle = '#00ffff';
|
||||
game.bullets.forEach(bullet => {
|
||||
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
|
||||
});
|
||||
|
||||
// 敌人子弹
|
||||
ctx.fillStyle = '#ff0000';
|
||||
game.enemyBullets.forEach(bullet => {
|
||||
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
|
||||
});
|
||||
}
|
||||
|
||||
// 绘制粒子效果
|
||||
function drawParticles() {
|
||||
game.particles.forEach(particle => {
|
||||
ctx.fillStyle = particle.color;
|
||||
ctx.globalAlpha = particle.alpha;
|
||||
ctx.fillRect(particle.x, particle.y, particle.size, particle.size);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// 玩家射击
|
||||
function playerShoot() {
|
||||
const now = Date.now();
|
||||
if (now - player.lastShot > player.shotDelay) {
|
||||
const bullet = {
|
||||
x: player.x + player.width / 2 - 2,
|
||||
y: player.y,
|
||||
width: 4,
|
||||
height: 15,
|
||||
speed: 10,
|
||||
color: '#00ffff'
|
||||
};
|
||||
|
||||
game.bullets.push(bullet);
|
||||
player.lastShot = now;
|
||||
|
||||
// 射击音效
|
||||
playLaserSound();
|
||||
}
|
||||
}
|
||||
|
||||
// 敌人射击
|
||||
function enemyShoot() {
|
||||
const now = Date.now();
|
||||
if (now - game.lastEnemyShoot > 2000 - game.level * 100 && game.enemies.length > 0) {
|
||||
const shooter = game.enemies[Math.floor(Math.random() * game.enemies.length)];
|
||||
|
||||
const bullet = {
|
||||
x: shooter.x + shooter.width / 2 - 2,
|
||||
y: shooter.y + shooter.height,
|
||||
width: 4,
|
||||
height: 15,
|
||||
speed: 5,
|
||||
color: '#ff0000'
|
||||
};
|
||||
|
||||
game.enemyBullets.push(bullet);
|
||||
game.lastEnemyShoot = now;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建爆炸粒子
|
||||
function createExplosion(x, y, color) {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const particle = {
|
||||
x: x,
|
||||
y: y,
|
||||
size: Math.random() * 5 + 2,
|
||||
speedX: (Math.random() - 0.5) * 5,
|
||||
speedY: (Math.random() - 0.5) * 5,
|
||||
color: color,
|
||||
alpha: 1,
|
||||
decay: 0.02
|
||||
};
|
||||
|
||||
game.particles.push(particle);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新游戏对象位置
|
||||
function updatePositions() {
|
||||
// 更新玩家子弹
|
||||
game.bullets = game.bullets.filter(bullet => {
|
||||
bullet.y -= bullet.speed;
|
||||
return bullet.y > 0;
|
||||
});
|
||||
|
||||
// 更新敌人子弹
|
||||
game.enemyBullets = game.enemyBullets.filter(bullet => {
|
||||
bullet.y += bullet.speed;
|
||||
return bullet.y < canvas.height;
|
||||
});
|
||||
|
||||
// 更新敌人
|
||||
game.enemies = game.enemies.filter(enemy => {
|
||||
enemy.y += enemy.speed;
|
||||
|
||||
// 敌人超出屏幕
|
||||
if (enemy.y > canvas.height) {
|
||||
game.lives--;
|
||||
updateUI();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 更新粒子
|
||||
game.particles = game.particles.filter(particle => {
|
||||
particle.x += particle.speedX;
|
||||
particle.y += particle.speedY;
|
||||
particle.alpha -= particle.decay;
|
||||
return particle.alpha > 0;
|
||||
});
|
||||
}
|
||||
|
||||
// 检测碰撞
|
||||
function checkCollisions() {
|
||||
// 玩家子弹与敌人碰撞
|
||||
game.bullets.forEach((bullet, bulletIndex) => {
|
||||
game.enemies.forEach((enemy, enemyIndex) => {
|
||||
if (
|
||||
bullet.x < enemy.x + enemy.width &&
|
||||
bullet.x + bullet.width > enemy.x &&
|
||||
bullet.y < enemy.y + enemy.height &&
|
||||
bullet.y + bullet.height > enemy.y
|
||||
) {
|
||||
// 击中敌人
|
||||
game.bullets.splice(bulletIndex, 1);
|
||||
game.enemies.splice(enemyIndex, 1);
|
||||
game.score += 100;
|
||||
createExplosion(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2, enemy.color);
|
||||
updateUI();
|
||||
|
||||
// 爆炸音效
|
||||
playExplosionSound();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 敌人子弹与玩家碰撞
|
||||
game.enemyBullets.forEach((bullet, bulletIndex) => {
|
||||
if (
|
||||
bullet.x < player.x + player.width &&
|
||||
bullet.x + bullet.width > player.x &&
|
||||
bullet.y < player.y + player.height &&
|
||||
bullet.y + bullet.height > player.y
|
||||
) {
|
||||
// 玩家被击中
|
||||
game.enemyBullets.splice(bulletIndex, 1);
|
||||
game.lives--;
|
||||
createExplosion(player.x + player.width / 2, player.y + player.height / 2, player.color);
|
||||
updateUI();
|
||||
|
||||
if (game.lives <= 0) {
|
||||
endGame(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 敌人与玩家碰撞
|
||||
game.enemies.forEach((enemy, enemyIndex) => {
|
||||
if (
|
||||
enemy.x < player.x + player.width &&
|
||||
enemy.x + enemy.width > player.x &&
|
||||
enemy.y < player.y + player.height &&
|
||||
enemy.y + enemy.height > player.y
|
||||
) {
|
||||
// 玩家与敌人相撞
|
||||
game.enemies.splice(enemyIndex, 1);
|
||||
game.lives--;
|
||||
createExplosion(player.x + player.width / 2, player.y + player.height / 2, player.color);
|
||||
updateUI();
|
||||
|
||||
if (game.lives <= 0) {
|
||||
endGame(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新等级
|
||||
function updateLevel() {
|
||||
if (game.score >= game.level * 1000) {
|
||||
game.level++;
|
||||
levelEl.textContent = game.level;
|
||||
|
||||
// 升级音效
|
||||
playLevelUpSound();
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏结束
|
||||
function endGame(win) {
|
||||
game.running = false;
|
||||
finalScoreEl.textContent = game.score;
|
||||
|
||||
if (win) {
|
||||
gameMessageEl.textContent = "恭喜!你成功保卫了地球!";
|
||||
} else {
|
||||
gameMessageEl.textContent = "地球需要你,再试一次!";
|
||||
}
|
||||
|
||||
gameOverEl.style.display = 'block';
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
function updateUI() {
|
||||
scoreEl.textContent = game.score;
|
||||
livesEl.textContent = game.lives;
|
||||
levelEl.textContent = game.level;
|
||||
}
|
||||
|
||||
// 游戏主循环
|
||||
function gameLoop() {
|
||||
if (!game.running || game.paused) return;
|
||||
|
||||
// 清空画布
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 生成敌人
|
||||
spawnEnemy();
|
||||
|
||||
// 敌人射击
|
||||
enemyShoot();
|
||||
|
||||
// 更新位置
|
||||
updatePositions();
|
||||
|
||||
// 检测碰撞
|
||||
checkCollisions();
|
||||
|
||||
// 更新等级
|
||||
updateLevel();
|
||||
|
||||
// 绘制游戏对象
|
||||
drawEnemies();
|
||||
drawBullets();
|
||||
drawParticles();
|
||||
drawPlayer();
|
||||
|
||||
// 处理玩家移动
|
||||
if (keys['ArrowLeft'] && player.x > 0) {
|
||||
player.x -= player.speed;
|
||||
}
|
||||
if (keys['ArrowRight'] && player.x < canvas.width - player.width) {
|
||||
player.x += player.speed;
|
||||
}
|
||||
if (keys[' ']) {
|
||||
playerShoot();
|
||||
}
|
||||
|
||||
// 继续游戏循环
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// 音效函数
|
||||
function playLaserSound() {
|
||||
// 在实际应用中,这里会播放音效文件
|
||||
// 为了简化,我们只创建音频上下文并生成音效
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'square';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.1);
|
||||
} catch (e) {
|
||||
// 音频上下文不支持时的降级处理
|
||||
console.log("音效播放失败");
|
||||
}
|
||||
}
|
||||
|
||||
function playExplosionSound() {
|
||||
// 类似上面的实现,但使用不同的音调
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 100;
|
||||
oscillator.type = 'sawtooth';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.5);
|
||||
} catch (e) {
|
||||
console.log("音效播放失败");
|
||||
}
|
||||
}
|
||||
|
||||
function playLevelUpSound() {
|
||||
// 类似上面的实现
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.setValueAtTime(300, audioContext.currentTime);
|
||||
oscillator.frequency.exponentialRampToValueAtTime(600, audioContext.currentTime + 0.2);
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.3);
|
||||
} catch (e) {
|
||||
console.log("音效播放失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
window.addEventListener('keydown', (e) => {
|
||||
keys[e.key] = true;
|
||||
|
||||
// 防止空格键滚动页面
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
keys[e.key] = false;
|
||||
});
|
||||
|
||||
startBtn.addEventListener('click', () => {
|
||||
if (!game.running) {
|
||||
initGame();
|
||||
startBtn.disabled = true;
|
||||
setTimeout(() => {
|
||||
startBtn.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
if (game.running) {
|
||||
game.paused = !game.paused;
|
||||
pauseBtn.textContent = game.paused ? '继续游戏' : '暂停游戏';
|
||||
|
||||
if (!game.paused) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
restartBtn.addEventListener('click', () => {
|
||||
initGame();
|
||||
});
|
||||
|
||||
// 初始UI更新
|
||||
updateUI();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,707 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>贪吃蛇大作战</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(to right, #00ff00, #ffff00);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 15px;
|
||||
color: #a0ffa0;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
margin: 15px auto;
|
||||
border: 3px solid #00ff00;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px #00ff00, inset 0 0 20px rgba(0, 255, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
background: #000;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.game-ui {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px 0;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #00ff00;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ui-item {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px #00ff00;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #00ff00, #ffff00);
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 0 15px rgba(0, 255, 0, 0.7);
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 0 20px rgba(0, 255, 0, 0.9);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #555;
|
||||
color: #888;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #00ff00;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
color: #00ff00;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 8px 0;
|
||||
color: #a0ffa0;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
border: 2px solid #ff0000;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 30px #ff0000;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
width: 85%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
font-size: 1.8rem;
|
||||
color: #ff0000;
|
||||
margin-bottom: 12px;
|
||||
text-shadow: 0 0 10px #ff0000;
|
||||
}
|
||||
|
||||
.game-over p {
|
||||
font-size: 1.1rem;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.mobile-controls {
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.mobile-controls button {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
padding: 0;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.mobile-controls .up {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mobile-controls .left {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mobile-controls .right {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mobile-controls .down {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.game-ui {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.mobile-controls {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.ui-item {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.game-over p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 0.9rem;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>贪吃蛇大作战</h1>
|
||||
<p class="subtitle">控制小蛇吃食物,变得越来越长!</p>
|
||||
|
||||
<div class="game-ui">
|
||||
<div class="ui-item">分数: <span id="score">0</span></div>
|
||||
<div class="ui-item">长度: <span id="length">1</span></div>
|
||||
<div class="ui-item">速度: <span id="speed">1</span></div>
|
||||
</div>
|
||||
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="600" height="400"></canvas>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>游戏结束</h2>
|
||||
<p>你的最终分数: <span id="finalScore">0</span></p>
|
||||
<p>蛇的长度: <span id="finalLength">0</span></p>
|
||||
<p id="gameMessage">再试一次,创造新纪录!</p>
|
||||
<button id="restartBtn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-controls">
|
||||
<button class="up">↑</button>
|
||||
<button class="left">←</button>
|
||||
<button class="right">→</button>
|
||||
<button class="down">↓</button>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="startBtn">开始游戏</button>
|
||||
<button id="pauseBtn">暂停游戏</button>
|
||||
<button id="speedUpBtn">加速</button>
|
||||
<button id="speedDownBtn">减速</button>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>游戏说明</h3>
|
||||
<p>使用方向键或屏幕按钮控制蛇的移动方向</p>
|
||||
<p>吃到食物可以增加蛇的长度和得分</p>
|
||||
<p>撞到墙壁或自己的身体会导致游戏结束</p>
|
||||
<p>每得100分速度会增加一级</p>
|
||||
<p>看看你能让蛇变得多长!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const pauseBtn = document.getElementById('pauseBtn');
|
||||
const restartBtn = document.getElementById('restartBtn');
|
||||
const speedUpBtn = document.getElementById('speedUpBtn');
|
||||
const speedDownBtn = document.getElementById('speedDownBtn');
|
||||
const scoreEl = document.getElementById('score');
|
||||
const lengthEl = document.getElementById('length');
|
||||
const speedEl = document.getElementById('speed');
|
||||
const gameOverEl = document.getElementById('gameOver');
|
||||
const finalScoreEl = document.getElementById('finalScore');
|
||||
const finalLengthEl = document.getElementById('finalLength');
|
||||
const gameMessageEl = document.getElementById('gameMessage');
|
||||
|
||||
// 移动控制按钮
|
||||
const upBtn = document.querySelector('.mobile-controls .up');
|
||||
const leftBtn = document.querySelector('.mobile-controls .left');
|
||||
const rightBtn = document.querySelector('.mobile-controls .right');
|
||||
const downBtn = document.querySelector('.mobile-controls .down');
|
||||
|
||||
// 游戏配置
|
||||
const gridSize = 20;
|
||||
const gridWidth = canvas.width / gridSize;
|
||||
const gridHeight = canvas.height / gridSize;
|
||||
|
||||
// 游戏状态
|
||||
let game = {
|
||||
running: false,
|
||||
paused: false,
|
||||
score: 0,
|
||||
speed: 1,
|
||||
baseSpeed: 8,
|
||||
gameLoop: null
|
||||
};
|
||||
|
||||
// 蛇的状态
|
||||
let snake = {
|
||||
body: [],
|
||||
direction: 'right',
|
||||
nextDirection: 'right',
|
||||
color: '#00ff00',
|
||||
headColor: '#ffff00'
|
||||
};
|
||||
|
||||
// 食物状态
|
||||
let food = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
color: '#ff0000'
|
||||
};
|
||||
|
||||
// 初始化游戏
|
||||
function initGame() {
|
||||
// 初始化蛇
|
||||
snake.body = [
|
||||
{x: Math.floor(gridWidth / 2), y: Math.floor(gridHeight / 2)}
|
||||
];
|
||||
snake.direction = 'right';
|
||||
snake.nextDirection = 'right';
|
||||
|
||||
// 生成第一个食物
|
||||
generateFood();
|
||||
|
||||
// 重置游戏状态
|
||||
game.running = true;
|
||||
game.paused = false;
|
||||
game.score = 0;
|
||||
game.speed = 1;
|
||||
|
||||
updateUI();
|
||||
gameOverEl.style.display = 'none';
|
||||
pauseBtn.textContent = '暂停游戏';
|
||||
|
||||
// 开始游戏循环
|
||||
if (game.gameLoop) clearInterval(game.gameLoop);
|
||||
game.gameLoop = setInterval(updateGame, 1000 / (game.baseSpeed * game.speed));
|
||||
|
||||
// 初始绘制
|
||||
drawGame();
|
||||
}
|
||||
|
||||
// 生成食物
|
||||
function generateFood() {
|
||||
let newFoodPosition;
|
||||
let foodOnSnake;
|
||||
|
||||
do {
|
||||
foodOnSnake = false;
|
||||
newFoodPosition = {
|
||||
x: Math.floor(Math.random() * gridWidth),
|
||||
y: Math.floor(Math.random() * gridHeight)
|
||||
};
|
||||
|
||||
// 检查食物是否生成在蛇身上
|
||||
for (let segment of snake.body) {
|
||||
if (segment.x === newFoodPosition.x && segment.y === newFoodPosition.y) {
|
||||
foodOnSnake = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (foodOnSnake);
|
||||
|
||||
food.x = newFoodPosition.x;
|
||||
food.y = newFoodPosition.y;
|
||||
}
|
||||
|
||||
// 更新游戏状态
|
||||
function updateGame() {
|
||||
if (!game.running || game.paused) return;
|
||||
|
||||
// 更新蛇的方向
|
||||
snake.direction = snake.nextDirection;
|
||||
|
||||
// 计算新的头部位置
|
||||
const head = {...snake.body[0]};
|
||||
|
||||
switch (snake.direction) {
|
||||
case 'up':
|
||||
head.y -= 1;
|
||||
break;
|
||||
case 'down':
|
||||
head.y += 1;
|
||||
break;
|
||||
case 'left':
|
||||
head.x -= 1;
|
||||
break;
|
||||
case 'right':
|
||||
head.x += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查游戏结束条件
|
||||
if (checkCollision(head)) {
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加新的头部
|
||||
snake.body.unshift(head);
|
||||
|
||||
// 检查是否吃到食物
|
||||
if (head.x === food.x && head.y === food.y) {
|
||||
// 吃到食物,增加分数
|
||||
game.score += 10;
|
||||
|
||||
// 每100分增加速度
|
||||
if (game.score % 100 === 0) {
|
||||
game.speed = Math.min(game.speed + 0.5, 5);
|
||||
clearInterval(game.gameLoop);
|
||||
game.gameLoop = setInterval(updateGame, 1000 / (game.baseSpeed * game.speed));
|
||||
}
|
||||
|
||||
// 生成新食物
|
||||
generateFood();
|
||||
} else {
|
||||
// 没吃到食物,移除尾部
|
||||
snake.body.pop();
|
||||
}
|
||||
|
||||
// 绘制游戏
|
||||
drawGame();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// 检查碰撞
|
||||
function checkCollision(head) {
|
||||
// 检查墙壁碰撞
|
||||
if (head.x < 0 || head.x >= gridWidth || head.y < 0 || head.y >= gridHeight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查自身碰撞
|
||||
for (let i = 1; i < snake.body.length; i++) {
|
||||
if (head.x === snake.body[i].x && head.y === snake.body[i].y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 绘制游戏
|
||||
function drawGame() {
|
||||
// 清空画布
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制网格
|
||||
ctx.strokeStyle = '#222';
|
||||
ctx.lineWidth = 0.5;
|
||||
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();
|
||||
}
|
||||
|
||||
// 绘制食物
|
||||
ctx.fillStyle = food.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
food.x * gridSize + gridSize / 2,
|
||||
food.y * gridSize + gridSize / 2,
|
||||
gridSize / 2 - 1,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
// 绘制蛇
|
||||
snake.body.forEach((segment, index) => {
|
||||
if (index === 0) {
|
||||
// 蛇头
|
||||
ctx.fillStyle = snake.headColor;
|
||||
} else {
|
||||
// 蛇身
|
||||
ctx.fillStyle = snake.color;
|
||||
}
|
||||
|
||||
ctx.fillRect(
|
||||
segment.x * gridSize + 1,
|
||||
segment.y * gridSize + 1,
|
||||
gridSize - 2,
|
||||
gridSize - 2
|
||||
);
|
||||
|
||||
// 绘制蛇身圆角
|
||||
ctx.fillStyle = index === 0 ? snake.headColor : snake.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
segment.x * gridSize + gridSize / 2,
|
||||
segment.y * gridSize + gridSize / 2,
|
||||
gridSize / 2 - 1,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
function updateUI() {
|
||||
scoreEl.textContent = game.score;
|
||||
lengthEl.textContent = snake.body.length;
|
||||
speedEl.textContent = game.speed.toFixed(1);
|
||||
}
|
||||
|
||||
// 游戏结束
|
||||
function endGame() {
|
||||
game.running = false;
|
||||
clearInterval(game.gameLoop);
|
||||
|
||||
finalScoreEl.textContent = game.score;
|
||||
finalLengthEl.textContent = snake.body.length;
|
||||
|
||||
// 根据分数给出不同消息
|
||||
if (game.score >= 200) {
|
||||
gameMessageEl.textContent = "太厉害了!你是贪吃蛇大师!";
|
||||
} else if (game.score >= 100) {
|
||||
gameMessageEl.textContent = "很棒!继续努力!";
|
||||
} else {
|
||||
gameMessageEl.textContent = "再试一次,创造新纪录!";
|
||||
}
|
||||
|
||||
gameOverEl.style.display = 'block';
|
||||
}
|
||||
|
||||
// 改变方向
|
||||
function changeDirection(newDirection) {
|
||||
// 防止直接反向移动
|
||||
if (
|
||||
(newDirection === 'up' && snake.direction !== 'down') ||
|
||||
(newDirection === 'down' && snake.direction !== 'up') ||
|
||||
(newDirection === 'left' && snake.direction !== 'right') ||
|
||||
(newDirection === 'right' && snake.direction !== 'left')
|
||||
) {
|
||||
snake.nextDirection = newDirection;
|
||||
}
|
||||
}
|
||||
|
||||
// 改变速度
|
||||
function changeSpeed(change) {
|
||||
if (!game.running) return;
|
||||
|
||||
game.speed = Math.max(0.5, Math.min(game.speed + change, 5));
|
||||
speedEl.textContent = game.speed.toFixed(1);
|
||||
|
||||
clearInterval(game.gameLoop);
|
||||
game.gameLoop = setInterval(updateGame, 1000 / (game.baseSpeed * game.speed));
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (!game.running) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
changeDirection('up');
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
changeDirection('down');
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
changeDirection('left');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
changeDirection('right');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 移动控制按钮事件
|
||||
upBtn.addEventListener('click', () => changeDirection('up'));
|
||||
downBtn.addEventListener('click', () => changeDirection('down'));
|
||||
leftBtn.addEventListener('click', () => changeDirection('left'));
|
||||
rightBtn.addEventListener('click', () => changeDirection('right'));
|
||||
|
||||
// 控制按钮事件
|
||||
startBtn.addEventListener('click', () => {
|
||||
if (!game.running) {
|
||||
initGame();
|
||||
startBtn.disabled = true;
|
||||
setTimeout(() => {
|
||||
startBtn.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
if (game.running) {
|
||||
game.paused = !game.paused;
|
||||
pauseBtn.textContent = game.paused ? '继续游戏' : '暂停游戏';
|
||||
}
|
||||
});
|
||||
|
||||
restartBtn.addEventListener('click', () => {
|
||||
initGame();
|
||||
});
|
||||
|
||||
speedUpBtn.addEventListener('click', () => changeSpeed(0.5));
|
||||
speedDownBtn.addEventListener('click', () => changeSpeed(-0.5));
|
||||
|
||||
// 初始绘制
|
||||
drawGame();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,846 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>2048数字拼图</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 0 10px #ff9900, 0 0 20px #ff9900;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(to right, #ff9900, #ffff00);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 15px;
|
||||
color: #ffcc80;
|
||||
}
|
||||
|
||||
.game-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 15px 0;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ff9900;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px #ff9900;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
margin: 15px auto;
|
||||
border: 3px solid #ff9900;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px #ff9900, inset 0 0 20px rgba(255, 153, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 450px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background: rgba(187, 173, 160, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
background: rgba(238, 228, 218, 0.35);
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
color: #776e65;
|
||||
}
|
||||
|
||||
.tile {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tile-2 { background: #eee4da; color: #776e65; }
|
||||
.tile-4 { background: #ede0c8; color: #776e65; }
|
||||
.tile-8 { background: #f2b179; color: white; }
|
||||
.tile-16 { background: #f59563; color: white; }
|
||||
.tile-32 { background: #f67c5f; color: white; }
|
||||
.tile-64 { background: #f65e3b; color: white; }
|
||||
.tile-128 { background: #edcf72; color: white; font-size: 1.5rem; }
|
||||
.tile-256 { background: #edcc61; color: white; font-size: 1.5rem; }
|
||||
.tile-512 { background: #edc850; color: white; font-size: 1.5rem; }
|
||||
.tile-1024 { background: #edc53f; color: white; font-size: 1.2rem; }
|
||||
.tile-2048 { background: #edc22e; color: white; font-size: 1.2rem; }
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #ff9900, #ffff00);
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 0 15px rgba(255, 153, 0, 0.7);
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 0 20px rgba(255, 153, 0, 0.9);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #555;
|
||||
color: #888;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ff9900;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
color: #ff9900;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 8px 0;
|
||||
color: #ffcc80;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
border: 2px solid #ff0000;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 30px #ff0000;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
width: 85%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
font-size: 1.8rem;
|
||||
color: #ff0000;
|
||||
margin-bottom: 12px;
|
||||
text-shadow: 0 0 10px #ff0000;
|
||||
}
|
||||
|
||||
.game-over p {
|
||||
font-size: 1.1rem;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.win-message {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
border: 2px solid #00ff00;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 30px #00ff00;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
width: 85%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.win-message h2 {
|
||||
font-size: 1.8rem;
|
||||
color: #00ff00;
|
||||
margin-bottom: 12px;
|
||||
text-shadow: 0 0 10px #00ff00;
|
||||
}
|
||||
|
||||
.win-message p {
|
||||
font-size: 1.1rem;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.mobile-controls {
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.mobile-controls button {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
padding: 0;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.mobile-controls .up {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mobile-controls .left {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mobile-controls .right {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mobile-controls .down {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.game-info {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.mobile-controls {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.tile-128, .tile-256, .tile-512 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tile-1024, .tile-2048 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
margin: 10px auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 0.9rem;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
|
||||
.grid-cell, .tile {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>2048数字拼图</h1>
|
||||
<p class="subtitle">合并相同数字,挑战2048!</p>
|
||||
|
||||
<div class="game-info">
|
||||
<div class="info-item">分数: <span id="score">0</span></div>
|
||||
<div class="info-item">步数: <span id="moves">0</span></div>
|
||||
<div class="info-item">最高分: <span id="best">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="game-container">
|
||||
<div class="grid-container" id="grid">
|
||||
<!-- 网格将由JavaScript生成 -->
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>游戏结束</h2>
|
||||
<p>你的最终分数: <span id="finalScore">0</span></p>
|
||||
<p id="gameMessage">再试一次,创造新纪录!</p>
|
||||
<button id="restartBtn">重新开始</button>
|
||||
</div>
|
||||
|
||||
<div class="win-message" id="winMessage">
|
||||
<h2>恭喜你赢了!</h2>
|
||||
<p>你成功合成了2048!</p>
|
||||
<p>分数: <span id="winScore">0</span></p>
|
||||
<button id="continueBtn">继续游戏</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-controls">
|
||||
<button class="up">↑</button>
|
||||
<button class="left">←</button>
|
||||
<button class="right">→</button>
|
||||
<button class="down">↓</button>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="startBtn">开始游戏</button>
|
||||
<button id="undoBtn">撤销</button>
|
||||
<button id="hintBtn">提示</button>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>游戏说明</h3>
|
||||
<p>使用方向键或屏幕按钮移动方块</p>
|
||||
<p>相同数字的方块相撞时会合并成为它们的和</p>
|
||||
<p>每次移动后会在空白处随机生成一个2或4的方块</p>
|
||||
<p>当棋盘被填满且无法移动时游戏结束</p>
|
||||
<p>成功合成2048即可获胜,但可以继续挑战更高分!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const gridContainer = document.getElementById('grid');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const undoBtn = document.getElementById('undoBtn');
|
||||
const hintBtn = document.getElementById('hintBtn');
|
||||
const restartBtn = document.getElementById('restartBtn');
|
||||
const continueBtn = document.getElementById('continueBtn');
|
||||
const scoreEl = document.getElementById('score');
|
||||
const movesEl = document.getElementById('moves');
|
||||
const bestEl = document.getElementById('best');
|
||||
const gameOverEl = document.getElementById('gameOver');
|
||||
const winMessageEl = document.getElementById('winMessage');
|
||||
const finalScoreEl = document.getElementById('finalScore');
|
||||
const winScoreEl = document.getElementById('winScore');
|
||||
const gameMessageEl = document.getElementById('gameMessage');
|
||||
|
||||
// 移动控制按钮
|
||||
const upBtn = document.querySelector('.mobile-controls .up');
|
||||
const leftBtn = document.querySelector('.mobile-controls .left');
|
||||
const rightBtn = document.querySelector('.mobile-controls .right');
|
||||
const downBtn = document.querySelector('.mobile-controls .down');
|
||||
|
||||
// 游戏状态
|
||||
let game = {
|
||||
grid: [],
|
||||
score: 0,
|
||||
moves: 0,
|
||||
best: localStorage.getItem('2048-best') || 0,
|
||||
gameOver: false,
|
||||
won: false,
|
||||
history: []
|
||||
};
|
||||
|
||||
// 初始化游戏
|
||||
function initGame() {
|
||||
// 创建4x4网格
|
||||
game.grid = Array(4).fill().map(() => Array(4).fill(0));
|
||||
game.score = 0;
|
||||
game.moves = 0;
|
||||
game.gameOver = false;
|
||||
game.won = false;
|
||||
game.history = [];
|
||||
|
||||
// 生成网格HTML
|
||||
gridContainer.innerHTML = '';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'grid-cell';
|
||||
gridContainer.appendChild(cell);
|
||||
}
|
||||
|
||||
// 添加两个初始方块
|
||||
addRandomTile();
|
||||
addRandomTile();
|
||||
|
||||
updateUI();
|
||||
gameOverEl.style.display = 'none';
|
||||
winMessageEl.style.display = 'none';
|
||||
|
||||
// 保存初始状态
|
||||
saveState();
|
||||
|
||||
// 渲染方块
|
||||
renderTiles();
|
||||
}
|
||||
|
||||
// 添加随机方块
|
||||
function addRandomTile() {
|
||||
const emptyCells = [];
|
||||
|
||||
// 找到所有空格子
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
if (game.grid[i][j] === 0) {
|
||||
emptyCells.push({x: i, y: j});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (emptyCells.length > 0) {
|
||||
// 随机选择一个空格子
|
||||
const cell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
|
||||
// 90%概率生成2,10%概率生成4
|
||||
game.grid[cell.x][cell.y] = Math.random() < 0.9 ? 2 : 4;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存游戏状态
|
||||
function saveState() {
|
||||
game.history.push({
|
||||
grid: JSON.parse(JSON.stringify(game.grid)),
|
||||
score: game.score,
|
||||
moves: game.moves
|
||||
});
|
||||
|
||||
// 只保留最近10步的历史
|
||||
if (game.history.length > 10) {
|
||||
game.history.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销上一步
|
||||
function undoMove() {
|
||||
if (game.history.length > 1) {
|
||||
game.history.pop(); // 移除当前状态
|
||||
const prevState = game.history[game.history.length - 1];
|
||||
game.grid = JSON.parse(JSON.stringify(prevState.grid));
|
||||
game.score = prevState.score;
|
||||
game.moves = prevState.moves;
|
||||
game.gameOver = false;
|
||||
game.won = false;
|
||||
|
||||
updateUI();
|
||||
renderTiles();
|
||||
}
|
||||
}
|
||||
|
||||
// 移动方块
|
||||
function move(direction) {
|
||||
if (game.gameOver) return;
|
||||
|
||||
const prevGrid = JSON.parse(JSON.stringify(game.grid));
|
||||
let moved = false;
|
||||
|
||||
// 根据方向处理移动
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
moved = moveUp();
|
||||
break;
|
||||
case 'down':
|
||||
moved = moveDown();
|
||||
break;
|
||||
case 'left':
|
||||
moved = moveLeft();
|
||||
break;
|
||||
case 'right':
|
||||
moved = moveRight();
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果有移动,更新游戏状态
|
||||
if (moved) {
|
||||
game.moves++;
|
||||
addRandomTile();
|
||||
saveState();
|
||||
updateUI();
|
||||
renderTiles();
|
||||
|
||||
// 检查游戏是否结束
|
||||
if (isGameOver()) {
|
||||
game.gameOver = true;
|
||||
finalScoreEl.textContent = game.score;
|
||||
gameOverEl.style.display = 'block';
|
||||
}
|
||||
|
||||
// 检查是否获胜
|
||||
if (!game.won && hasWon()) {
|
||||
game.won = true;
|
||||
winScoreEl.textContent = game.score;
|
||||
winMessageEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 向上移动
|
||||
function moveUp() {
|
||||
let moved = false;
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
for (let i = 1; i < 4; i++) {
|
||||
if (game.grid[i][j] !== 0) {
|
||||
let row = i;
|
||||
while (row > 0 && game.grid[row - 1][j] === 0) {
|
||||
game.grid[row - 1][j] = game.grid[row][j];
|
||||
game.grid[row][j] = 0;
|
||||
row--;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (row > 0 && game.grid[row - 1][j] === game.grid[row][j]) {
|
||||
game.grid[row - 1][j] *= 2;
|
||||
game.score += game.grid[row - 1][j];
|
||||
game.grid[row][j] = 0;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
|
||||
// 向下移动
|
||||
function moveDown() {
|
||||
let moved = false;
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
for (let i = 2; i >= 0; i--) {
|
||||
if (game.grid[i][j] !== 0) {
|
||||
let row = i;
|
||||
while (row < 3 && game.grid[row + 1][j] === 0) {
|
||||
game.grid[row + 1][j] = game.grid[row][j];
|
||||
game.grid[row][j] = 0;
|
||||
row++;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (row < 3 && game.grid[row + 1][j] === game.grid[row][j]) {
|
||||
game.grid[row + 1][j] *= 2;
|
||||
game.score += game.grid[row + 1][j];
|
||||
game.grid[row][j] = 0;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
|
||||
// 向左移动
|
||||
function moveLeft() {
|
||||
let moved = false;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 1; j < 4; j++) {
|
||||
if (game.grid[i][j] !== 0) {
|
||||
let col = j;
|
||||
while (col > 0 && game.grid[i][col - 1] === 0) {
|
||||
game.grid[i][col - 1] = game.grid[i][col];
|
||||
game.grid[i][col] = 0;
|
||||
col--;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (col > 0 && game.grid[i][col - 1] === game.grid[i][col]) {
|
||||
game.grid[i][col - 1] *= 2;
|
||||
game.score += game.grid[i][col - 1];
|
||||
game.grid[i][col] = 0;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
|
||||
// 向右移动
|
||||
function moveRight() {
|
||||
let moved = false;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 2; j >= 0; j--) {
|
||||
if (game.grid[i][j] !== 0) {
|
||||
let col = j;
|
||||
while (col < 3 && game.grid[i][col + 1] === 0) {
|
||||
game.grid[i][col + 1] = game.grid[i][col];
|
||||
game.grid[i][col] = 0;
|
||||
col++;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (col < 3 && game.grid[i][col + 1] === game.grid[i][col]) {
|
||||
game.grid[i][col + 1] *= 2;
|
||||
game.score += game.grid[i][col + 1];
|
||||
game.grid[i][col] = 0;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
|
||||
// 检查游戏是否结束
|
||||
function isGameOver() {
|
||||
// 检查是否有空格子
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
if (game.grid[i][j] === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有可以合并的相邻格子
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const value = game.grid[i][j];
|
||||
if (
|
||||
(i < 3 && game.grid[i + 1][j] === value) ||
|
||||
(j < 3 && game.grid[i][j + 1] === value)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否获胜
|
||||
function hasWon() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
if (game.grid[i][j] === 2048) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 渲染方块
|
||||
function renderTiles() {
|
||||
// 移除所有现有的方块
|
||||
const existingTiles = document.querySelectorAll('.tile');
|
||||
existingTiles.forEach(tile => tile.remove());
|
||||
|
||||
// 获取网格容器的实际尺寸
|
||||
const containerWidth = gridContainer.offsetWidth;
|
||||
const containerHeight = gridContainer.offsetHeight;
|
||||
|
||||
// 计算每个格子的实际尺寸
|
||||
const cellWidth = (containerWidth - 50) / 4; // 减去内边距和间隙
|
||||
const cellHeight = (containerHeight - 50) / 4;
|
||||
|
||||
// 添加新的方块
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const value = game.grid[i][j];
|
||||
if (value !== 0) {
|
||||
const tile = document.createElement('div');
|
||||
tile.className = `tile tile-${value}`;
|
||||
tile.textContent = value;
|
||||
|
||||
// 使用像素精确定位
|
||||
const left = 10 + j * (cellWidth + 10);
|
||||
const top = 10 + i * (cellHeight + 10);
|
||||
|
||||
tile.style.left = `${left}px`;
|
||||
tile.style.top = `${top}px`;
|
||||
tile.style.width = `${cellWidth}px`;
|
||||
tile.style.height = `${cellHeight}px`;
|
||||
|
||||
gridContainer.appendChild(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
function updateUI() {
|
||||
scoreEl.textContent = game.score;
|
||||
movesEl.textContent = game.moves;
|
||||
|
||||
// 更新最高分
|
||||
if (game.score > game.best) {
|
||||
game.best = game.score;
|
||||
localStorage.setItem('2048-best', game.best);
|
||||
}
|
||||
bestEl.textContent = game.best;
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (game.gameOver && !game.won) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
move('up');
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
move('down');
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
move('left');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
move('right');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 移动控制按钮事件
|
||||
upBtn.addEventListener('click', () => move('up'));
|
||||
downBtn.addEventListener('click', () => move('down'));
|
||||
leftBtn.addEventListener('click', () => move('left'));
|
||||
rightBtn.addEventListener('click', () => move('right'));
|
||||
|
||||
// 控制按钮事件
|
||||
startBtn.addEventListener('click', () => {
|
||||
initGame();
|
||||
});
|
||||
|
||||
undoBtn.addEventListener('click', () => {
|
||||
undoMove();
|
||||
});
|
||||
|
||||
hintBtn.addEventListener('click', () => {
|
||||
alert('提示:尽量保持最大数字在角落,按顺序合并小数字!');
|
||||
});
|
||||
|
||||
restartBtn.addEventListener('click', () => {
|
||||
initGame();
|
||||
});
|
||||
|
||||
continueBtn.addEventListener('click', () => {
|
||||
winMessageEl.style.display = 'none';
|
||||
});
|
||||
|
||||
// 窗口大小改变时重新渲染
|
||||
window.addEventListener('resize', () => {
|
||||
if (game.grid && game.grid.length > 0) {
|
||||
renderTiles();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化游戏
|
||||
initGame();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,739 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>末日生存 · 二维射击游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(135deg, #0a1f1a 0%, #1a2f1f 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', 'Arial', sans-serif;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
.game-container {
|
||||
background: #2d3a2c;
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.6), inset 0 0 15px rgba(0,0,0,0.4);
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 25px rgba(0, 180, 0, 0.3);
|
||||
background: #1e2b1c;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 800px;
|
||||
aspect-ratio: 800 / 600;
|
||||
cursor: crosshair;
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #e0f0d8;
|
||||
font-weight: bold;
|
||||
background: #1e2b1c;
|
||||
border-radius: 40px;
|
||||
padding: 12px 25px;
|
||||
box-shadow: inset 0 2px 8px rgba(0,0,0,0.5);
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
.health-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.health-fill {
|
||||
width: 180px;
|
||||
height: 14px;
|
||||
background: #3a2e2e;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 2px 5px rgba(0,0,0,0.7);
|
||||
}
|
||||
.fill-level {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #e74c3c, #f1c40f);
|
||||
border-radius: 20px;
|
||||
transition: width 0.15s ease;
|
||||
}
|
||||
.score-board {
|
||||
background: #0f1a0f;
|
||||
padding: 6px 20px;
|
||||
border-radius: 30px;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 1px;
|
||||
color: #d4ed5e;
|
||||
box-shadow: 0 0 12px #5a8f4a;
|
||||
}
|
||||
button {
|
||||
background: #d4b34c;
|
||||
border: none;
|
||||
color: #1e2b1c;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
padding: 10px 25px;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 1px;
|
||||
background: linear-gradient(180deg, #e9d584 0%, #b38b40 100%);
|
||||
box-shadow: 0 5px 0 #6b4e1d, 0 8px 15px rgba(0,0,0,0.4);
|
||||
transition: all 0.08s ease;
|
||||
border: 1px solid #f3e3a7;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(3px);
|
||||
box-shadow: 0 2px 0 #4b3a1a, 0 5px 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: #b9d0a0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
.info-panel {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="800" height="600" aria-label="二维射击游戏地图"></canvas>
|
||||
<div class="info-panel">
|
||||
<div class="health-bar">
|
||||
<span style="color:#f1c40f;">❤️ 生命</span>
|
||||
<div class="health-fill">
|
||||
<div id="healthFillLevel" class="fill-level" style="width: 100%;"></div>
|
||||
</div>
|
||||
<span id="healthText" style="min-width: 50px;">100</span>
|
||||
</div>
|
||||
<div class="score-board">
|
||||
🎯 <span id="scoreDisplay">0</span>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<span>🖱️ 鼠标瞄准 | 左键射击</span>
|
||||
<button id="restartButton">↺ 重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color:#8aa87d; margin-top: 8px; font-size: 0.9rem;">
|
||||
⌨️ WASD / 方向键 移动人物
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// UI元素
|
||||
const healthFill = document.getElementById('healthFillLevel');
|
||||
const healthText = document.getElementById('healthText');
|
||||
const scoreSpan = document.getElementById('scoreDisplay');
|
||||
|
||||
// 固定尺寸
|
||||
const W = 800, H = 600;
|
||||
|
||||
// ----- 游戏状态 -----
|
||||
let player = {
|
||||
x: W / 2,
|
||||
y: H / 2,
|
||||
radius: 18,
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
speed: 4.8,
|
||||
};
|
||||
|
||||
let monsters = [];
|
||||
let bullets = [];
|
||||
let score = 0;
|
||||
|
||||
// 射击冷却 (防止按住左键瞬间连射太夸张)
|
||||
const SHOOT_COOLDOWN_MAX = 18; // 帧数 (约60fps => 0.3秒)
|
||||
let shootCooldown = 0;
|
||||
|
||||
// 怪物生成控制
|
||||
let monsterSpawnTimer = 0;
|
||||
const SPAWN_DELAY = 45; // 大约每0.75秒生成一只 (60fps)
|
||||
const MAX_MONSTERS = 14;
|
||||
|
||||
// 控制标志
|
||||
let keys = {
|
||||
w: false, up: false,
|
||||
s: false, down: false,
|
||||
a: false, left: false,
|
||||
d: false, right: false
|
||||
};
|
||||
|
||||
// 鼠标坐标 (用于瞄准方向)
|
||||
let mouseX = player.x + 30;
|
||||
let mouseY = player.y;
|
||||
let mouseInside = true;
|
||||
|
||||
// 游戏循环控制
|
||||
let gameRunning = true;
|
||||
let animationFrameId = null;
|
||||
|
||||
// ----- 辅助函数:随机范围 -----
|
||||
const rand = (min, max) => Math.random() * (max - min) + min;
|
||||
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
// ----- 重置游戏 -----
|
||||
function restartGame() {
|
||||
player = {
|
||||
x: W / 2,
|
||||
y: H / 2,
|
||||
radius: 18,
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
speed: 4.8,
|
||||
};
|
||||
monsters = [];
|
||||
bullets = [];
|
||||
score = 0;
|
||||
shootCooldown = 0;
|
||||
monsterSpawnTimer = 0;
|
||||
gameRunning = true;
|
||||
|
||||
// 重置鼠标位置到玩家附近
|
||||
mouseX = player.x + 20;
|
||||
mouseY = player.y;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// ----- 生成怪物 (在屏幕边缘附近) -----
|
||||
function spawnMonster() {
|
||||
if (monsters.length >= MAX_MONSTERS) return;
|
||||
|
||||
// 随机选择边缘:0=上, 1=下, 2=左, 3=右
|
||||
const edge = randInt(0, 3);
|
||||
let spawnX, spawnY;
|
||||
const padding = 25; // 略微在屏幕外生成,自然走入
|
||||
|
||||
switch (edge) {
|
||||
case 0: // 上
|
||||
spawnX = rand(padding, W - padding);
|
||||
spawnY = -padding;
|
||||
break;
|
||||
case 1: // 下
|
||||
spawnX = rand(padding, W - padding);
|
||||
spawnY = H + padding;
|
||||
break;
|
||||
case 2: // 左
|
||||
spawnX = -padding;
|
||||
spawnY = rand(padding, H - padding);
|
||||
break;
|
||||
case 3: // 右
|
||||
spawnX = W + padding;
|
||||
spawnY = rand(padding, H - padding);
|
||||
break;
|
||||
default:
|
||||
spawnX = -padding;
|
||||
spawnY = rand(padding, H - padding);
|
||||
}
|
||||
|
||||
// 怪物类型:基础速度 + 大小变化
|
||||
const type = Math.random();
|
||||
let radius, speed, health, color;
|
||||
if (type < 0.6) {
|
||||
// 普通僵尸
|
||||
radius = 16 + rand(0, 6);
|
||||
speed = 1.1 + rand(0, 0.9);
|
||||
health = 1;
|
||||
color = '#6b8c5c';
|
||||
} else if (type < 0.88) {
|
||||
// 快速怪物
|
||||
radius = 13 + rand(0, 5);
|
||||
speed = 2.0 + rand(0, 1.3);
|
||||
health = 1;
|
||||
color = '#b5876a';
|
||||
} else {
|
||||
// 大型怪物 (需要两枪)
|
||||
radius = 22 + rand(0, 7);
|
||||
speed = 0.7 + rand(0, 0.7);
|
||||
health = 2;
|
||||
color = '#8b3a3a';
|
||||
}
|
||||
|
||||
monsters.push({
|
||||
x: spawnX,
|
||||
y: spawnY,
|
||||
radius: radius,
|
||||
speed: speed,
|
||||
health: health,
|
||||
maxHealth: health,
|
||||
color: color,
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 玩家移动 (基于键盘) -----
|
||||
function movePlayer() {
|
||||
if (!gameRunning) return;
|
||||
let dx = 0, dy = 0;
|
||||
|
||||
if (keys.w || keys.up) dy -= 1;
|
||||
if (keys.s || keys.down) dy += 1;
|
||||
if (keys.a || keys.left) dx -= 1;
|
||||
if (keys.d || keys.right) dx += 1;
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
dx = (dx / len) * player.speed;
|
||||
dy = (dy / len) * player.speed;
|
||||
|
||||
let newX = player.x + dx;
|
||||
let newY = player.y + dy;
|
||||
|
||||
// 边界限制 (考虑玩家半径)
|
||||
const minBound = player.radius;
|
||||
const maxBoundX = W - player.radius;
|
||||
const maxBoundY = H - player.radius;
|
||||
|
||||
newX = Math.max(minBound, Math.min(maxBoundX, newX));
|
||||
newY = Math.max(minBound, Math.min(maxBoundY, newY));
|
||||
|
||||
player.x = newX;
|
||||
player.y = newY;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 怪物移动 (朝向玩家) -----
|
||||
function moveMonsters() {
|
||||
for (let m of monsters) {
|
||||
const dx = player.x - m.x;
|
||||
const dy = player.y - m.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < 0.5) continue; // 已重叠
|
||||
|
||||
const moveX = (dx / dist) * m.speed;
|
||||
const moveY = (dy / dist) * m.speed;
|
||||
m.x += moveX;
|
||||
m.y += moveY;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 玩家射击 (生成子弹) -----
|
||||
function shootBullet(targetX, targetY) {
|
||||
if (!gameRunning) return;
|
||||
// 计算方向向量 (从玩家指向鼠标)
|
||||
const dx = targetX - player.x;
|
||||
const dy = targetY - player.y;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len < 0.001) return; // 避免零向量
|
||||
|
||||
const normX = dx / len;
|
||||
const normY = dy / len;
|
||||
|
||||
bullets.push({
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
vx: normX * 7.5, // 子弹速度
|
||||
vy: normY * 7.5,
|
||||
radius: 4.5,
|
||||
active: true,
|
||||
damage: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 更新子弹位置与碰撞-----
|
||||
function updateBullets() {
|
||||
for (let i = bullets.length - 1; i >= 0; i--) {
|
||||
const b = bullets[i];
|
||||
b.x += b.vx;
|
||||
b.y += b.vy;
|
||||
|
||||
// 边界消除 (超出画布太远)
|
||||
if (b.x < -20 || b.x > W + 20 || b.y < -20 || b.y > H + 20) {
|
||||
bullets.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let bulletHit = false;
|
||||
|
||||
// 检测子弹与怪物碰撞
|
||||
for (let j = monsters.length - 1; j >= 0; j--) {
|
||||
const m = monsters[j];
|
||||
const d = Math.sqrt((b.x - m.x) ** 2 + (b.y - m.y) ** 2);
|
||||
if (d < m.radius + b.radius) {
|
||||
// 击中怪物
|
||||
m.health -= b.damage;
|
||||
bulletHit = true;
|
||||
|
||||
if (m.health <= 0) {
|
||||
// 击杀怪物,增加分数
|
||||
score += (m.maxHealth === 2 ? 15 : 10);
|
||||
monsters.splice(j, 1);
|
||||
}
|
||||
break; // 一颗子弹只击中一个怪物
|
||||
}
|
||||
}
|
||||
|
||||
if (bulletHit) {
|
||||
bullets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 怪物攻击玩家 (碰撞检测) -----
|
||||
function checkMonsterCollision() {
|
||||
if (!gameRunning) return;
|
||||
for (let i = monsters.length - 1; i >= 0; i--) {
|
||||
const m = monsters[i];
|
||||
const dist = Math.sqrt((m.x - player.x) ** 2 + (m.y - player.y) ** 2);
|
||||
if (dist < player.radius + m.radius) {
|
||||
// 怪物对玩家造成伤害 (每次碰撞扣血,并推开怪物防止连续帧多次伤害)
|
||||
const damageAmount = 8 + (m.maxHealth === 2 ? 4 : 0); // 大型怪物伤害更高
|
||||
player.health -= damageAmount;
|
||||
|
||||
// 推开怪物:沿连线方向移动怪物到刚好不重叠
|
||||
const angle = Math.atan2(m.y - player.y, m.x - player.x);
|
||||
const overlap = player.radius + m.radius - dist;
|
||||
if (overlap > 0) {
|
||||
m.x += Math.cos(angle) * overlap;
|
||||
m.y += Math.sin(angle) * overlap;
|
||||
}
|
||||
|
||||
// 检查玩家死亡
|
||||
if (player.health <= 0) {
|
||||
player.health = 0;
|
||||
gameRunning = false;
|
||||
updateUI();
|
||||
break; // 停止检测其他怪物
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 再次确保玩家生命值不超限
|
||||
if (player.health <= 0) {
|
||||
player.health = 0;
|
||||
gameRunning = false;
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// ----- 更新UI元素-----
|
||||
function updateUI() {
|
||||
let healthPercent = Math.max(0, player.health) / player.maxHealth * 100;
|
||||
healthFill.style.width = `${healthPercent}%`;
|
||||
healthText.textContent = `${Math.max(0, player.health)}`;
|
||||
scoreSpan.textContent = score;
|
||||
}
|
||||
|
||||
// ----- 游戏主循环更新-----
|
||||
function update() {
|
||||
if (gameRunning) {
|
||||
// 减少射击冷却
|
||||
if (shootCooldown > 0) shootCooldown--;
|
||||
|
||||
// 移动玩家
|
||||
movePlayer();
|
||||
|
||||
// 怪物移动
|
||||
moveMonsters();
|
||||
|
||||
// 子弹更新与碰撞
|
||||
updateBullets();
|
||||
|
||||
// 怪物攻击玩家
|
||||
checkMonsterCollision();
|
||||
|
||||
// 怪物生成逻辑
|
||||
if (gameRunning) {
|
||||
monsterSpawnTimer--;
|
||||
if (monsterSpawnTimer <= 0 && monsters.length < MAX_MONSTERS) {
|
||||
spawnMonster();
|
||||
monsterSpawnTimer = SPAWN_DELAY + randInt(-8, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查玩家死亡 (可能由碰撞导致)
|
||||
if (player.health <= 0) {
|
||||
gameRunning = false;
|
||||
player.health = 0;
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 绘制图形 (所有视觉元素) -----
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
// 绘制网格/地面纹理
|
||||
ctx.strokeStyle = '#3a4a2e';
|
||||
ctx.lineWidth = 0.6;
|
||||
for (let i = 0; i < W; i += 40) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i, 0);
|
||||
ctx.lineTo(i, H);
|
||||
ctx.strokeStyle = '#3a5535';
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let j = 0; j < H; j += 40) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, j);
|
||||
ctx.lineTo(W, j);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ----- 绘制怪物 -----
|
||||
for (let m of monsters) {
|
||||
// 怪物身体
|
||||
ctx.beginPath();
|
||||
ctx.arc(m.x, m.y, m.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = m.color;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#1a1a1a';
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.stroke();
|
||||
|
||||
// 怪物眼睛 (白色+瞳孔朝向玩家)
|
||||
const eyeOffset = m.radius * 0.5;
|
||||
const angleToPlayer = Math.atan2(player.y - m.y, player.x - m.x);
|
||||
const leftEyeX = m.x + Math.cos(angleToPlayer - 0.5) * eyeOffset;
|
||||
const leftEyeY = m.y + Math.sin(angleToPlayer - 0.5) * eyeOffset;
|
||||
const rightEyeX = m.x + Math.cos(angleToPlayer + 0.5) * eyeOffset;
|
||||
const rightEyeY = m.y + Math.sin(angleToPlayer + 0.5) * eyeOffset;
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.arc(leftEyeX, leftEyeY, m.radius * 0.27, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(rightEyeX, rightEyeY, m.radius * 0.27, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.beginPath();
|
||||
ctx.arc(leftEyeX + Math.cos(angleToPlayer) * 1.8, leftEyeY + Math.sin(angleToPlayer) * 1.8, m.radius * 0.12, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(rightEyeX + Math.cos(angleToPlayer) * 1.8, rightEyeY + Math.sin(angleToPlayer) * 1.8, m.radius * 0.12, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
// 如果生命值>1 显示护甲标记
|
||||
if (m.maxHealth === 2 && m.health === 2) {
|
||||
ctx.fillStyle = '#f0d050';
|
||||
ctx.beginPath();
|
||||
ctx.arc(m.x, m.y - m.radius - 4, 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
} else if (m.maxHealth === 2 && m.health === 1) {
|
||||
ctx.fillStyle = '#b5773a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(m.x, m.y - m.radius - 4, 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 绘制子弹 -----
|
||||
for (let b of bullets) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(b.x, b.y, b.radius, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = '#f1e05a';
|
||||
ctx.shadowColor = '#ffbb00';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fill();
|
||||
ctx.shadowColor = 'transparent';
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = '#b38b20';
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ----- 绘制玩家 (英雄) -----
|
||||
ctx.save();
|
||||
ctx.translate(player.x, player.y);
|
||||
// 玩家面向鼠标方向
|
||||
const aimAngle = Math.atan2(mouseY - player.y, mouseX - player.x);
|
||||
ctx.rotate(aimAngle);
|
||||
|
||||
// 身体
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, player.radius, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = '#3b7bb7';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#102840';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.stroke();
|
||||
|
||||
// 护甲/装备线条
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, player.radius - 3, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#c0daf5';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// 武器/枪口
|
||||
ctx.fillStyle = '#4a4a5a';
|
||||
ctx.fillRect(8, -3.5, 15, 7);
|
||||
ctx.fillStyle = '#2b2b35';
|
||||
ctx.fillRect(19, -2.8, 7, 5.6);
|
||||
|
||||
// 眼睛
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.arc(5, -7, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(5, 7, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0a14';
|
||||
ctx.beginPath();
|
||||
ctx.arc(7, -7, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(7, 7, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// 玩家血条 (头顶)
|
||||
if (gameRunning) {
|
||||
const healthPercent = player.health / player.maxHealth;
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(player.x - 22, player.y - player.radius - 13, 44, 8);
|
||||
ctx.fillStyle = healthPercent > 0.6 ? '#6f9' : (healthPercent > 0.25 ? '#f4c542' : '#e03a3a');
|
||||
ctx.fillRect(player.x - 22, player.y - player.radius - 13, 44 * healthPercent, 8);
|
||||
}
|
||||
|
||||
// 游戏结束画面
|
||||
if (!gameRunning) {
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillStyle = '#0f1a0f';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = '#e0d7b0';
|
||||
ctx.font = 'bold 38px "Segoe UI", Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('💀 你倒下了', W/2, H/2-20);
|
||||
ctx.font = '18px "Segoe UI"';
|
||||
ctx.fillText('点击"重新开始"继续战斗', W/2, H/2+50);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 动画循环 -----
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
animationFrameId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ----- 事件监听 -----
|
||||
function bindEvents() {
|
||||
// 键盘按下
|
||||
window.addEventListener('keydown', (e) => {
|
||||
const key = e.key.toLowerCase();
|
||||
// 防止页面滚动 / 默认行为
|
||||
if (key === 'w' || key === 'a' || key === 's' || key === 'd' ||
|
||||
key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (key === 'w') keys.w = true;
|
||||
if (key === 'a') keys.a = true;
|
||||
if (key === 's') keys.s = true;
|
||||
if (key === 'd') keys.d = true;
|
||||
if (key === 'arrowup') keys.up = true;
|
||||
if (key === 'arrowdown') keys.down = true;
|
||||
if (key === 'arrowleft') keys.left = true;
|
||||
if (key === 'arrowright') keys.right = true;
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
const key = e.key.toLowerCase();
|
||||
if (key === 'w' || key === 'a' || key === 's' || key === 'd' ||
|
||||
key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (key === 'w') keys.w = false;
|
||||
if (key === 'a') keys.a = false;
|
||||
if (key === 's') keys.s = false;
|
||||
if (key === 'd') keys.d = false;
|
||||
if (key === 'arrowup') keys.up = false;
|
||||
if (key === 'arrowdown') keys.down = false;
|
||||
if (key === 'arrowleft') keys.left = false;
|
||||
if (key === 'arrowright') keys.right = false;
|
||||
});
|
||||
|
||||
// 鼠标移动 (追踪瞄准位置)
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = W / rect.width; // canvas 物理像素比例
|
||||
const scaleY = H / rect.height;
|
||||
const relativeX = (e.clientX - rect.left) * scaleX;
|
||||
const relativeY = (e.clientY - rect.top) * scaleY;
|
||||
mouseX = Math.min(W, Math.max(0, relativeX));
|
||||
mouseY = Math.min(H, Math.max(0, relativeY));
|
||||
mouseInside = true;
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
// 离开画布时瞄准点设为玩家右侧默认方向
|
||||
mouseX = player.x + 30;
|
||||
mouseY = player.y;
|
||||
mouseInside = false;
|
||||
});
|
||||
|
||||
// 射击:鼠标按下 (左键)
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
if (e.button === 0) { // 左键
|
||||
if (gameRunning && shootCooldown <= 0) {
|
||||
shootBullet(mouseX, mouseY);
|
||||
shootCooldown = SHOOT_COOLDOWN_MAX;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 防止右键菜单弹出
|
||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
|
||||
// 重新开始按钮
|
||||
document.getElementById('restartButton').addEventListener('click', () => {
|
||||
restartGame();
|
||||
});
|
||||
|
||||
// 窗口失去焦点时重置按键状态 (防止卡方向)
|
||||
window.addEventListener('blur', () => {
|
||||
keys.w = keys.a = keys.s = keys.d = false;
|
||||
keys.up = keys.down = keys.left = keys.right = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 启动一切
|
||||
bindEvents();
|
||||
restartGame(); // 初始状态
|
||||
gameLoop();
|
||||
|
||||
// 清理动画 (非必须,但良好实践)
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
|
||||
<title>天空冲刺 · 手机躲避游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(180deg, #0b1a3a 0%, #1b3b5c 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.game-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: radial-gradient(circle at 50% 30%, #35637c, #0e2a3f);
|
||||
border-radius: 48px;
|
||||
box-shadow: 0 25px 50px rgba(0,0,0,0.6), inset 0 0 20px rgba(255,255,255,0.2);
|
||||
padding: 16px 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 32px;
|
||||
background: radial-gradient(ellipse at 50% 20%, #7fc9ff 0%, #3a6d9c 70%);
|
||||
box-shadow: inset 0 -8px 20px rgba(0,20,40,0.7), 0 12px 25px rgba(0,0,0,0.5);
|
||||
touch-action: none; /* 禁止默认手势,优化触摸响应 */
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
padding: 0 10px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,0.6);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.score-board {
|
||||
background: rgba(10, 25, 45, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 50px;
|
||||
padding: 8px 22px;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,0.5);
|
||||
border: 1px solid rgba(255,255,255,0.25);
|
||||
}
|
||||
.restart-btn {
|
||||
background: #ffb84d;
|
||||
border: none;
|
||||
color: #0a1f32;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
padding: 10px 25px;
|
||||
border-radius: 40px;
|
||||
background: linear-gradient(180deg, #ffd966 0%, #f0a500 100%);
|
||||
box-shadow: 0 8px 0 #b76e00, 0 12px 22px rgba(0,0,0,0.4);
|
||||
transition: all 0.08s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(255,255,200,0.7);
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.restart-btn:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 4px 0 #b76e00, 0 8px 14px rgba(0,0,0,0.5);
|
||||
background: linear-gradient(180deg, #f5c45e 0%, #d18d00 100%);
|
||||
}
|
||||
.hint {
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 2px 5px black;
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
.game-container {
|
||||
padding: 12px 8px 16px;
|
||||
}
|
||||
.info-panel {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="360" height="600" aria-label="天空冲刺游戏画面"></canvas>
|
||||
<div class="info-panel">
|
||||
<div class="score-board" id="scoreDisplay">0</div>
|
||||
<button class="restart-btn" id="restartButton">↺ 起飞</button>
|
||||
</div>
|
||||
<div class="hint">👆 手指滑动或拖动飞机 | 躲避石块</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreSpan = document.getElementById('scoreDisplay');
|
||||
|
||||
// 固定尺寸,适配高清显示
|
||||
const CW = 360;
|
||||
const CH = 600;
|
||||
canvas.width = CW;
|
||||
canvas.height = CH;
|
||||
|
||||
// ---------- 游戏状态 ----------
|
||||
let gameActive = true;
|
||||
let score = 0;
|
||||
|
||||
// 玩家飞机属性
|
||||
const PLAYER_WIDTH = 38;
|
||||
const PLAYER_HEIGHT = 44;
|
||||
let playerX = CW / 2 - PLAYER_WIDTH/2;
|
||||
const playerY = CH - 90; // 垂直位置固定(玩家只水平移动)
|
||||
|
||||
// 障碍物数组
|
||||
let obstacles = [];
|
||||
|
||||
// 障碍物设置
|
||||
const OBSTACLE_WIDTH = 36;
|
||||
const OBSTACLE_HEIGHT = 48;
|
||||
const OBSTACLE_SPEED = 4.2; // 每帧下落速度
|
||||
const SPAWN_FRAME_INTERVAL = 38; // 大约每38帧生成一个
|
||||
let frameCounter = 0;
|
||||
|
||||
// 移动控制 (触摸/鼠标)
|
||||
let targetPlayerX = playerX; // 用于平滑跟随或直接设置
|
||||
|
||||
// 触摸状态
|
||||
let activeTouchId = null; // 追踪当前触摸点
|
||||
|
||||
// ---------- 辅助函数:限制玩家位置 ----------
|
||||
function clampPlayerX(value) {
|
||||
return Math.max(5, Math.min(CW - PLAYER_WIDTH - 5, value));
|
||||
}
|
||||
|
||||
// ---------- 生成新障碍物 ----------
|
||||
function spawnObstacle() {
|
||||
// 随机x位置,确保不超出边界
|
||||
const maxX = CW - OBSTACLE_WIDTH;
|
||||
const x = Math.random() * maxX;
|
||||
obstacles.push({
|
||||
x: x,
|
||||
y: -OBSTACLE_HEIGHT - Math.random() * 20, // 从顶外出现
|
||||
width: OBSTACLE_WIDTH,
|
||||
height: OBSTACLE_HEIGHT,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 重置游戏 ----------
|
||||
function restartGame() {
|
||||
gameActive = true;
|
||||
score = 0;
|
||||
obstacles = [];
|
||||
frameCounter = 0;
|
||||
playerX = CW / 2 - PLAYER_WIDTH/2;
|
||||
targetPlayerX = playerX;
|
||||
activeTouchId = null;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
// ---------- 游戏结束 ----------
|
||||
function endGame() {
|
||||
if (!gameActive) return; // 防止重复触发
|
||||
gameActive = false;
|
||||
// 可以添加震动反馈(如果支持)
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(30);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 更新分数显示 ----------
|
||||
function updateScoreDisplay() {
|
||||
scoreSpan.textContent = score;
|
||||
}
|
||||
|
||||
// ---------- 碰撞检测 (矩形重叠) ----------
|
||||
function checkCollision(playerRect, obs) {
|
||||
return (playerRect.x < obs.x + obs.width &&
|
||||
playerRect.x + playerRect.width > obs.x &&
|
||||
playerRect.y < obs.y + obs.height &&
|
||||
playerRect.y + playerRect.height > obs.y);
|
||||
}
|
||||
|
||||
// ---------- 更新逻辑 ----------
|
||||
function update() {
|
||||
if (!gameActive) return;
|
||||
|
||||
// 1. 玩家位置平滑移动到目标位置 (使操作更跟手)
|
||||
const DX = targetPlayerX - playerX;
|
||||
// 快速跟随,但保留手感
|
||||
if (Math.abs(DX) > 0.3) {
|
||||
playerX += DX * 0.35;
|
||||
// 避免微小抖动
|
||||
if (Math.abs(DX) < 0.5) {
|
||||
playerX = targetPlayerX;
|
||||
}
|
||||
} else {
|
||||
playerX = targetPlayerX;
|
||||
}
|
||||
playerX = clampPlayerX(playerX);
|
||||
// 同步目标位置以防超出边界后不一致
|
||||
targetPlayerX = clampPlayerX(targetPlayerX);
|
||||
|
||||
// 2. 移动障碍物 & 碰撞检测 & 移除屏幕外
|
||||
const playerRect = {
|
||||
x: playerX,
|
||||
y: playerY,
|
||||
width: PLAYER_WIDTH,
|
||||
height: PLAYER_HEIGHT,
|
||||
};
|
||||
|
||||
for (let i = obstacles.length - 1; i >= 0; i--) {
|
||||
const obs = obstacles[i];
|
||||
obs.y += OBSTACLE_SPEED;
|
||||
|
||||
// 碰撞检测 (仅当游戏活跃时)
|
||||
if (gameActive && checkCollision(playerRect, obs)) {
|
||||
endGame();
|
||||
return; // 立即停止更新,避免继续处理
|
||||
}
|
||||
|
||||
// 移除超出底部的障碍物,并增加分数
|
||||
if (obs.y > CH + 50) {
|
||||
obstacles.splice(i, 1);
|
||||
if (gameActive) {
|
||||
score += 1;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 生成新障碍物 (游戏活跃时才生成)
|
||||
if (gameActive) {
|
||||
frameCounter++;
|
||||
if (frameCounter >= SPAWN_FRAME_INTERVAL) {
|
||||
spawnObstacle();
|
||||
frameCounter = 0;
|
||||
// 动态略微调整生成间隔,增加随机性
|
||||
// 保持在30~45之间变化
|
||||
// 但为了稳定,使用固定基准加小波动
|
||||
const variation = Math.floor(Math.random() * 12) - 4; // -4~7
|
||||
frameCounter = -variation; // 技巧:让下次间隔变化
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 绘制图形 ----------
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, CW, CH);
|
||||
|
||||
// ------- 绘制装饰云朵 (背景) -------
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
|
||||
drawCloud(40, 120, 30);
|
||||
drawCloud(280, 200, 35);
|
||||
drawCloud(70, 320, 28);
|
||||
drawCloud(300, 430, 32);
|
||||
drawCloud(150, 70, 38);
|
||||
|
||||
// ------- 绘制障碍物 (石块/陨石) -------
|
||||
for (let obs of obstacles) {
|
||||
// 石块渐变
|
||||
const gradient = ctx.createLinearGradient(obs.x, obs.y, obs.x + 10, obs.y + obs.height);
|
||||
gradient.addColorStop(0, '#5d4a3a');
|
||||
gradient.addColorStop(0.5, '#4a3728');
|
||||
gradient.addColorStop(1, '#2f231a');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.beginPath();
|
||||
// 不规则石块效果
|
||||
const w = obs.width;
|
||||
const h = obs.height;
|
||||
const x = obs.x;
|
||||
const y = obs.y;
|
||||
ctx.moveTo(x + 5, y);
|
||||
ctx.lineTo(x + w - 6, y + 4);
|
||||
ctx.lineTo(x + w, y + h/2);
|
||||
ctx.lineTo(x + w - 5, y + h - 5);
|
||||
ctx.lineTo(x + w/2, y + h);
|
||||
ctx.lineTo(x + 3, y + h - 7);
|
||||
ctx.lineTo(x, y + h/3);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 高光线
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.fillStyle = '#7d6b5c';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 8, y + 5);
|
||||
ctx.lineTo(x + w - 12, y + 8);
|
||||
ctx.lineTo(x + w - 9, y + h/2 - 2);
|
||||
ctx.lineTo(x + 6, y + h/2 - 3);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 重置阴影
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowColor = 'transparent';
|
||||
|
||||
// 裂纹
|
||||
ctx.strokeStyle = '#1a120a';
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 10, y + 12);
|
||||
ctx.lineTo(x + 18, y + 25);
|
||||
ctx.lineTo(x + 13, y + 34);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ------- 绘制玩家飞机 -------
|
||||
ctx.shadowColor = '#00000080';
|
||||
ctx.shadowBlur = 12;
|
||||
const px = playerX;
|
||||
const py = playerY;
|
||||
|
||||
// 机身主色
|
||||
const planeGrad = ctx.createLinearGradient(px, py, px + PLAYER_WIDTH*0.8, py + PLAYER_HEIGHT);
|
||||
planeGrad.addColorStop(0, '#ffd966');
|
||||
planeGrad.addColorStop(0.6, '#f0a500');
|
||||
planeGrad.addColorStop(1, '#c47a00');
|
||||
ctx.fillStyle = planeGrad;
|
||||
|
||||
// 绘制飞机主体 (喷气式风格)
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + PLAYER_WIDTH/2, py - 4); // 机头
|
||||
ctx.lineTo(px + PLAYER_WIDTH-8, py + PLAYER_HEIGHT-12); // 右尾翼
|
||||
ctx.lineTo(px + PLAYER_WIDTH-4, py + PLAYER_HEIGHT-6);
|
||||
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT-10); // 尾部中心
|
||||
ctx.lineTo(px + 4, py + PLAYER_HEIGHT-6);
|
||||
ctx.lineTo(px + 8, py + PLAYER_HEIGHT-12); // 左尾翼
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 机翼
|
||||
ctx.fillStyle = '#e59400';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + PLAYER_WIDTH/2, py + 12);
|
||||
ctx.lineTo(px - 3, py + 28);
|
||||
ctx.lineTo(px + 10, py + 32);
|
||||
ctx.lineTo(px + PLAYER_WIDTH/2, py + 24);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + PLAYER_WIDTH/2, py + 12);
|
||||
ctx.lineTo(px + PLAYER_WIDTH+3, py + 28);
|
||||
ctx.lineTo(px + PLAYER_WIDTH-10, py + 32);
|
||||
ctx.lineTo(px + PLAYER_WIDTH/2, py + 24);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 驾驶舱
|
||||
ctx.fillStyle = '#aee2ff';
|
||||
ctx.shadowColor = '#ffffff80';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(px + PLAYER_WIDTH/2, py + 10, 7, 9, 0, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(px + PLAYER_WIDTH/2-2, py + 8, 2.5, 3.5, 0, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 引擎火焰
|
||||
ctx.shadowBlur = 14;
|
||||
ctx.fillStyle = '#ff7b24';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + PLAYER_WIDTH/2-5, py + PLAYER_HEIGHT-8);
|
||||
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT+6);
|
||||
ctx.lineTo(px + PLAYER_WIDTH/2+5, py + PLAYER_HEIGHT-8);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffdd55';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + PLAYER_WIDTH/2-3, py + PLAYER_HEIGHT-6);
|
||||
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT);
|
||||
ctx.lineTo(px + PLAYER_WIDTH/2+3, py + PLAYER_HEIGHT-6);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 重置阴影
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowColor = 'transparent';
|
||||
|
||||
// ------- 游戏结束画面 -------
|
||||
if (!gameActive) {
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillStyle = '#0a1424';
|
||||
ctx.fillRect(0, 0, CW, CH);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 28px "Segoe UI", system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'black';
|
||||
ctx.shadowBlur = 16;
|
||||
ctx.fillText('💥 坠毁', CW/2, CH/2-20);
|
||||
ctx.font = '18px system-ui';
|
||||
ctx.fillText('点击"起飞"继续', CW/2, CH/2+45);
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowColor = 'transparent';
|
||||
}
|
||||
}
|
||||
|
||||
function drawCloud(x, y, size) {
|
||||
ctx.fillStyle = 'rgba(255, 255, 250, 0.25)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, size*0.6, 0, Math.PI*2);
|
||||
ctx.arc(x+size*0.5, y-size*0.2, size*0.7, 0, Math.PI*2);
|
||||
ctx.arc(x-size*0.4, y-size*0.1, size*0.55, 0, Math.PI*2);
|
||||
ctx.arc(x+size*0.2, y+size*0.2, size*0.5, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ---------- 游戏循环 ----------
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ---------- 事件处理 (触摸/鼠标适配) ----------
|
||||
function handleStart(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return; // 游戏结束时不处理移动
|
||||
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
// 计算相对于canvas内部的x坐标 (按比例缩放)
|
||||
const scaleX = CW / canvasRect.width; // canvas物理像素与CSS宽度比例
|
||||
const relativeX = (clientX - canvasRect.left) * scaleX;
|
||||
|
||||
// 设定目标位置:飞机中心对准触摸点
|
||||
let newTarget = relativeX - PLAYER_WIDTH/2;
|
||||
targetPlayerX = clampPlayerX(newTarget);
|
||||
playerX = targetPlayerX; // 立即吸附,提升响应
|
||||
|
||||
// 记录触摸点id用于移动跟踪
|
||||
if (e.touches) {
|
||||
activeTouchId = e.touches[0].identifier;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMove(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
|
||||
// 对于触摸事件,需要找到匹配的触摸点
|
||||
if (e.touches) {
|
||||
if (activeTouchId === null) return;
|
||||
let currentTouch = null;
|
||||
for (let i = 0; i < e.touches.length; i++) {
|
||||
if (e.touches[i].identifier === activeTouchId) {
|
||||
currentTouch = e.touches[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!currentTouch) return;
|
||||
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / canvasRect.width;
|
||||
const relativeX = (currentTouch.clientX - canvasRect.left) * scaleX;
|
||||
let newTarget = relativeX - PLAYER_WIDTH/2;
|
||||
targetPlayerX = clampPlayerX(newTarget);
|
||||
} else {
|
||||
// 鼠标移动 (用于桌面调试)
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / canvasRect.width;
|
||||
const relativeX = (e.clientX - canvasRect.left) * scaleX;
|
||||
let newTarget = relativeX - PLAYER_WIDTH/2;
|
||||
targetPlayerX = clampPlayerX(newTarget);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnd(e) {
|
||||
e.preventDefault();
|
||||
// 触摸结束,清除活动触摸点
|
||||
if (e.touches) {
|
||||
// 如果剩余触摸点为空,则重置activeTouchId
|
||||
if (e.touches.length === 0) {
|
||||
activeTouchId = null;
|
||||
} else {
|
||||
// 仍有其他手指,尝试切换到第一个
|
||||
activeTouchId = e.touches[0].identifier;
|
||||
}
|
||||
} else {
|
||||
// 鼠标松开不做特殊处理
|
||||
activeTouchId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
function bindEvents() {
|
||||
// 触摸事件
|
||||
canvas.addEventListener('touchstart', handleStart, { passive: false });
|
||||
canvas.addEventListener('touchmove', handleMove, { passive: false });
|
||||
canvas.addEventListener('touchend', handleEnd);
|
||||
canvas.addEventListener('touchcancel', handleEnd);
|
||||
|
||||
// 鼠标事件 (便于开发调试和部分设备)
|
||||
canvas.addEventListener('mousedown', handleStart);
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
// 只有按下鼠标左键时才处理移动
|
||||
if (e.buttons === 1 && gameActive) {
|
||||
e.preventDefault();
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / canvasRect.width;
|
||||
const relativeX = (e.clientX - canvasRect.left) * scaleX;
|
||||
let newTarget = relativeX - PLAYER_WIDTH/2;
|
||||
targetPlayerX = clampPlayerX(newTarget);
|
||||
}
|
||||
});
|
||||
window.addEventListener('mouseup', (e) => {
|
||||
// 不做特殊操作
|
||||
});
|
||||
|
||||
// 重启按钮
|
||||
document.getElementById('restartButton').addEventListener('click', (e) => {
|
||||
restartGame();
|
||||
});
|
||||
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
restartGame();
|
||||
});
|
||||
}
|
||||
|
||||
// 启动一切
|
||||
bindEvents();
|
||||
restartGame(); // 初始状态
|
||||
gameLoop();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,479 @@
|
||||
<!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, viewport-fit=cover, shrink-to-fit=yes">
|
||||
<title>深渊矿工 · 收集宝石游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(180deg, #1a0e08 0%, #3b2a1a 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.game-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: radial-gradient(circle at 50% 40%, #5e3e1a, #2d1c0b);
|
||||
border-radius: 48px;
|
||||
box-shadow: 0 25px 45px rgba(0,0,0,0.7), inset 0 0 20px rgba(255,215,0,0.2);
|
||||
padding: 16px 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid #7a5c3a;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 32px;
|
||||
background: radial-gradient(ellipse at 50% 10%, #4a3620 0%, #1e1308 85%);
|
||||
box-shadow: inset 0 -12px 25px rgba(0,0,0,0.8), 0 10px 20px black;
|
||||
touch-action: none;
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
padding: 0 10px;
|
||||
color: #ffd78b;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 6px black;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.score-board {
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 50px;
|
||||
padding: 8px 22px;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 6px 14px rgba(0,0,0,0.7);
|
||||
border: 1px solid #c9a23b;
|
||||
color: #ffd966;
|
||||
}
|
||||
.restart-btn {
|
||||
background: #e6b422;
|
||||
border: none;
|
||||
color: #2b1a04;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
padding: 10px 24px;
|
||||
border-radius: 40px;
|
||||
background: linear-gradient(180deg, #f9e076 0%, #ca9620 100%);
|
||||
box-shadow: 0 7px 0 #7a5200, 0 10px 20px black;
|
||||
transition: all 0.08s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ffe28c;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.restart-btn:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 3px 0 #7a5200, 0 7px 12px black;
|
||||
background: linear-gradient(180deg, #ebc85c 0%, #ad7d18 100%);
|
||||
}
|
||||
.hint {
|
||||
color: #dbbc6c;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="360" height="600" aria-label="深渊矿工游戏画面"></canvas>
|
||||
<div class="info-panel">
|
||||
<div class="score-board" id="scoreDisplay">💎 0</div>
|
||||
<button class="restart-btn" id="restartButton">⛏️ 新矿洞</button>
|
||||
</div>
|
||||
<div class="hint">👆 左右滑动矿车 | 接宝石 ✦ 躲岩石</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreSpan = document.getElementById('scoreDisplay');
|
||||
|
||||
const CW = 360;
|
||||
const CH = 600;
|
||||
canvas.width = CW;
|
||||
canvas.height = CH;
|
||||
|
||||
// ----- 游戏状态 -----
|
||||
let gameActive = true;
|
||||
let score = 0;
|
||||
|
||||
// 矿车属性
|
||||
const CART_WIDTH = 56;
|
||||
const CART_HEIGHT = 40;
|
||||
let cartX = CW / 2 - CART_WIDTH/2;
|
||||
const cartY = CH - 85; // 固定垂直位置
|
||||
|
||||
// 下落物品数组
|
||||
let items = [];
|
||||
|
||||
// 物品类型
|
||||
const TYPE_GEM = 'gem'; // 宝石 (+1分)
|
||||
const TYPE_ROCK = 'rock'; // 岩石 (碰撞结束)
|
||||
|
||||
const ITEM_SIZE = 34; // 统一尺寸
|
||||
const FALL_SPEED = 3.8;
|
||||
const SPAWN_RATE = 32; // 帧间隔
|
||||
let frameCount = 0;
|
||||
|
||||
// 触摸控制
|
||||
let targetCartX = cartX;
|
||||
let activeTouchId = null;
|
||||
|
||||
// ----- 辅助函数 -----
|
||||
function clampCart(value) {
|
||||
return Math.max(4, Math.min(CW - CART_WIDTH - 4, value));
|
||||
}
|
||||
|
||||
// 生成物品 (宝石或岩石)
|
||||
function spawnItem() {
|
||||
const maxX = CW - ITEM_SIZE;
|
||||
const x = Math.random() * maxX;
|
||||
// 70%概率宝石,30%岩石
|
||||
const type = Math.random() < 0.7 ? TYPE_GEM : TYPE_ROCK;
|
||||
items.push({
|
||||
x: x,
|
||||
y: -ITEM_SIZE - Math.random() * 15,
|
||||
size: ITEM_SIZE,
|
||||
type: type,
|
||||
});
|
||||
}
|
||||
|
||||
// 重置游戏
|
||||
function restartGame() {
|
||||
gameActive = true;
|
||||
score = 0;
|
||||
items = [];
|
||||
frameCount = 0;
|
||||
cartX = CW / 2 - CART_WIDTH/2;
|
||||
targetCartX = cartX;
|
||||
activeTouchId = null;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
if (!gameActive) return;
|
||||
gameActive = false;
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(40);
|
||||
}
|
||||
}
|
||||
|
||||
function updateScoreDisplay() {
|
||||
scoreSpan.textContent = `💎 ${score}`;
|
||||
}
|
||||
|
||||
// 碰撞检测
|
||||
function rectCollide(r1, r2) {
|
||||
return !(r2.x > r1.x + r1.w || r2.x + r2.w < r1.x ||
|
||||
r2.y > r1.y + r1.h || r2.y + r2.h < r1.y);
|
||||
}
|
||||
|
||||
// ----- 更新逻辑 -----
|
||||
function update() {
|
||||
if (!gameActive) return;
|
||||
|
||||
// 矿车平滑移动
|
||||
const diff = targetCartX - cartX;
|
||||
if (Math.abs(diff) > 0.3) {
|
||||
cartX += diff * 0.3;
|
||||
if (Math.abs(diff) < 0.6) cartX = targetCartX;
|
||||
} else {
|
||||
cartX = targetCartX;
|
||||
}
|
||||
cartX = clampCart(cartX);
|
||||
targetCartX = clampCart(targetCartX);
|
||||
|
||||
// 矿车碰撞矩形
|
||||
const cartRect = {
|
||||
x: cartX,
|
||||
y: cartY,
|
||||
w: CART_WIDTH,
|
||||
h: CART_HEIGHT,
|
||||
};
|
||||
|
||||
// 处理物品下落
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const item = items[i];
|
||||
item.y += FALL_SPEED;
|
||||
|
||||
const itemRect = {
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
w: item.size,
|
||||
h: item.size,
|
||||
};
|
||||
|
||||
// 碰撞检测
|
||||
if (gameActive && rectCollide(cartRect, itemRect)) {
|
||||
if (item.type === TYPE_GEM) {
|
||||
// 收集宝石
|
||||
score++;
|
||||
updateScoreDisplay();
|
||||
items.splice(i, 1);
|
||||
continue;
|
||||
} else if (item.type === TYPE_ROCK) {
|
||||
// 撞到岩石,游戏结束
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除超出屏幕的物品
|
||||
if (item.y > CH + 50) {
|
||||
items.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新物品
|
||||
if (gameActive) {
|
||||
frameCount++;
|
||||
if (frameCount >= SPAWN_RATE) {
|
||||
spawnItem();
|
||||
frameCount = 0;
|
||||
// 动态微调
|
||||
const variation = Math.floor(Math.random() * 10) - 3;
|
||||
frameCount = -variation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 绘制图形 -----
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, CW, CH);
|
||||
|
||||
// 绘制岩壁纹理
|
||||
ctx.fillStyle = '#2f2416';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
ctx.fillRect(0, i * 85 + (Date.now()*0.01 % 85), 8, 25);
|
||||
ctx.fillRect(CW-8, i * 85 + 30 + (Date.now()*0.01 % 70), 8, 25);
|
||||
}
|
||||
|
||||
// 绘制物品
|
||||
for (let item of items) {
|
||||
const { x, y, size, type } = item;
|
||||
ctx.save();
|
||||
ctx.shadowColor = '#00000080';
|
||||
ctx.shadowBlur = 8;
|
||||
|
||||
if (type === TYPE_GEM) {
|
||||
// 绘制宝石 (多面体)
|
||||
const grad = ctx.createLinearGradient(x, y, x+size, y+size);
|
||||
grad.addColorStop(0, '#b5f2ff');
|
||||
grad.addColorStop(0.4, '#3cc7e0');
|
||||
grad.addColorStop(1, '#0f5b6e');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + size/2, y);
|
||||
ctx.lineTo(x + size, y + size*0.6);
|
||||
ctx.lineTo(x + size*0.7, y + size);
|
||||
ctx.lineTo(x + size*0.3, y + size);
|
||||
ctx.lineTo(x, y + size*0.6);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffffcc';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + size/2, y+2);
|
||||
ctx.lineTo(x + size*0.8, y + size*0.55);
|
||||
ctx.lineTo(x + size/2, y + size*0.7);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else {
|
||||
// 绘制岩石
|
||||
const grad = ctx.createLinearGradient(x, y, x+6, y+size);
|
||||
grad.addColorStop(0, '#7d6b5a');
|
||||
grad.addColorStop(0.7, '#3e3325');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x+5, y);
|
||||
ctx.lineTo(x+size-6, y+3);
|
||||
ctx.lineTo(x+size, y+size/2);
|
||||
ctx.lineTo(x+size-4, y+size-5);
|
||||
ctx.lineTo(x+size/2, y+size);
|
||||
ctx.lineTo(x+3, y+size-6);
|
||||
ctx.lineTo(x, y+size/3);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#5c5040';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x+size/2-2, y+size/2-2, 4, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// 绘制矿车
|
||||
ctx.save();
|
||||
ctx.shadowColor = '#000000';
|
||||
ctx.shadowBlur = 14;
|
||||
const cx = cartX;
|
||||
const cy = cartY;
|
||||
|
||||
// 矿车车身
|
||||
const cartGrad = ctx.createLinearGradient(cx, cy, cx, cy+CART_HEIGHT);
|
||||
cartGrad.addColorStop(0, '#b3823e');
|
||||
cartGrad.addColorStop(0.7, '#613d1c');
|
||||
ctx.fillStyle = cartGrad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx+8, cy);
|
||||
ctx.lineTo(cx+CART_WIDTH-8, cy);
|
||||
ctx.lineTo(cx+CART_WIDTH-2, cy+CART_HEIGHT-8);
|
||||
ctx.lineTo(cx+CART_WIDTH-2, cy+CART_HEIGHT-2);
|
||||
ctx.lineTo(cx+2, cy+CART_HEIGHT-2);
|
||||
ctx.lineTo(cx+2, cy+CART_HEIGHT-8);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 车轮
|
||||
ctx.fillStyle = '#2b1a0a';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx+10, cy+CART_HEIGHT-6, 8, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx+CART_WIDTH-10, cy+CART_HEIGHT-6, 8, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#5a5a5a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx+10, cy+CART_HEIGHT-6, 4, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx+CART_WIDTH-10, cy+CART_HEIGHT-6, 4, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 矿石点缀
|
||||
ctx.fillStyle = '#f9e076';
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx+15, cy+12, 5, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffb703';
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx+35, cy+18, 5, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// 游戏结束画面
|
||||
if (!gameActive) {
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillStyle = '#0f0803';
|
||||
ctx.fillRect(0, 0, CW, CH);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = '#f5d78c';
|
||||
ctx.font = 'bold 28px "Segoe UI", system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'black';
|
||||
ctx.shadowBlur = 18;
|
||||
ctx.fillText('⛔ 矿洞塌方', CW/2, CH/2-20);
|
||||
ctx.font = '18px system-ui';
|
||||
ctx.fillText('点击"新矿洞"继续', CW/2, CH/2+45);
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowColor = 'transparent';
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 游戏循环 -----
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ----- 事件处理 -----
|
||||
function handleStart(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const relX = (clientX - rect.left) * scaleX;
|
||||
targetCartX = clampCart(relX - CART_WIDTH/2);
|
||||
cartX = targetCartX;
|
||||
if (e.touches) activeTouchId = e.touches[0].identifier;
|
||||
}
|
||||
|
||||
function handleMove(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
if (e.touches) {
|
||||
if (activeTouchId === null) return;
|
||||
let touch = null;
|
||||
for (let i=0; i<e.touches.length; i++) {
|
||||
if (e.touches[i].identifier === activeTouchId) { touch = e.touches[i]; break; }
|
||||
}
|
||||
if (!touch) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const relX = (touch.clientX - rect.left) * scaleX;
|
||||
targetCartX = clampCart(relX - CART_WIDTH/2);
|
||||
} else {
|
||||
if (e.buttons === 1) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const relX = (e.clientX - rect.left) * scaleX;
|
||||
targetCartX = clampCart(relX - CART_WIDTH/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnd(e) {
|
||||
e.preventDefault();
|
||||
if (e.touches && e.touches.length === 0) {
|
||||
activeTouchId = null;
|
||||
} else if (e.touches) {
|
||||
activeTouchId = e.touches[0].identifier;
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
canvas.addEventListener('touchstart', handleStart, { passive: false });
|
||||
canvas.addEventListener('touchmove', handleMove, { passive: false });
|
||||
canvas.addEventListener('touchend', handleEnd);
|
||||
canvas.addEventListener('touchcancel', handleEnd);
|
||||
canvas.addEventListener('mousedown', handleStart);
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (e.buttons === 1 && gameActive) {
|
||||
e.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const relX = (e.clientX - rect.left) * scaleX;
|
||||
targetCartX = clampCart(relX - CART_WIDTH/2);
|
||||
}
|
||||
});
|
||||
document.getElementById('restartButton').addEventListener('click', restartGame);
|
||||
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
restartGame();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
restartGame();
|
||||
gameLoop();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,562 @@
|
||||
<!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, viewport-fit=cover, shrink-to-fit=yes">
|
||||
<title>泡泡射手 · 消除游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(180deg, #1a2f4a 0%, #3c5f7c 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.game-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: radial-gradient(circle at 50% 30%, #5b8cbf, #1e3b4f);
|
||||
border-radius: 48px;
|
||||
box-shadow: 0 25px 50px rgba(0,0,0,0.6), inset 0 0 25px rgba(255,255,255,0.2);
|
||||
padding: 16px 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 32px;
|
||||
background: radial-gradient(ellipse at 50% 15%, #a3d0ff 0%, #32638a 80%);
|
||||
box-shadow: inset 0 -10px 25px rgba(0,20,40,0.7), 0 12px 25px rgba(0,0,0,0.5);
|
||||
touch-action: none;
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
padding: 0 10px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,0.6);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.score-board {
|
||||
background: rgba(20, 40, 60, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 50px;
|
||||
padding: 8px 22px;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,0.5);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
}
|
||||
.restart-btn {
|
||||
background: #ff7b9c;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
padding: 10px 25px;
|
||||
border-radius: 40px;
|
||||
background: linear-gradient(180deg, #ffb3c6 0%, #e85d7a 100%);
|
||||
box-shadow: 0 8px 0 #a8324e, 0 12px 22px rgba(0,0,0,0.4);
|
||||
transition: all 0.08s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ffd1dc;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.restart-btn:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 4px 0 #a8324e, 0 8px 14px rgba(0,0,0,0.5);
|
||||
background: linear-gradient(180deg, #f595aa 0%, #c94a62 100%);
|
||||
}
|
||||
.hint {
|
||||
color: rgba(255,255,255,0.85);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="360" height="600" aria-label="泡泡射手游戏画面"></canvas>
|
||||
<div class="info-panel">
|
||||
<div class="score-board" id="scoreDisplay">🫧 0</div>
|
||||
<button class="restart-btn" id="restartButton">🔁 新一局</button>
|
||||
</div>
|
||||
<div class="hint">👆 点击屏幕发射泡泡 | 消除三个同色</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreSpan = document.getElementById('scoreDisplay');
|
||||
|
||||
const CW = 360;
|
||||
const CH = 600;
|
||||
canvas.width = CW;
|
||||
canvas.height = CH;
|
||||
|
||||
// ----- 游戏配置 -----
|
||||
const ROWS = 7; // 泡泡网格行数
|
||||
const COLS = 8; // 列数
|
||||
const BUBBLE_RADIUS = 18; // 泡泡半径
|
||||
const CELL_SIZE = BUBBLE_RADIUS * 2 + 2; // 间距
|
||||
const GRID_OFFSET_X = (CW - COLS * CELL_SIZE) / 2 + BUBBLE_RADIUS;
|
||||
const GRID_OFFSET_Y = 65;
|
||||
|
||||
// 发射器位置
|
||||
const SHOOTER_Y = CH - 50;
|
||||
let shooterX = CW / 2;
|
||||
|
||||
// 颜色池
|
||||
const COLORS = ['#FF6B6B', '#4ECDC4', '#FFE66D', '#A37EF0', '#FF8A5C'];
|
||||
|
||||
// 游戏状态
|
||||
let gameActive = true;
|
||||
let score = 0;
|
||||
let grid = []; // 二维数组存储泡泡颜色(null为空)
|
||||
let currentBubbleColor = null;
|
||||
let nextBubbleColor = null;
|
||||
let projectile = null; // 飞行中的泡泡 { x, y, vx, vy, color }
|
||||
|
||||
// 触摸追踪
|
||||
let aiming = false;
|
||||
let aimStartX = 0;
|
||||
let aimStartY = 0;
|
||||
|
||||
// ----- 初始化网格 -----
|
||||
function createGrid() {
|
||||
grid = [];
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
grid[r] = [];
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
// 偏移行 (交错效果)
|
||||
const colOffset = (r % 2 === 1) ? 0 : 0;
|
||||
// 只填充前几行
|
||||
if (r < 4) {
|
||||
const colorIndex = Math.floor(Math.random() * COLORS.length);
|
||||
grid[r][c] = COLORS[colorIndex];
|
||||
} else {
|
||||
grid[r][c] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 随机颜色
|
||||
function randomColor() {
|
||||
return COLORS[Math.floor(Math.random() * COLORS.length)];
|
||||
}
|
||||
|
||||
// 重置游戏
|
||||
function restartGame() {
|
||||
gameActive = true;
|
||||
score = 0;
|
||||
createGrid();
|
||||
currentBubbleColor = randomColor();
|
||||
nextBubbleColor = randomColor();
|
||||
projectile = null;
|
||||
shooterX = CW / 2;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
if (!gameActive) return;
|
||||
gameActive = false;
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(25);
|
||||
}
|
||||
}
|
||||
|
||||
function updateScoreDisplay() {
|
||||
scoreSpan.textContent = `🫧 ${score}`;
|
||||
}
|
||||
|
||||
// 根据行列计算泡泡中心坐标
|
||||
function getBubblePos(row, col) {
|
||||
const x = GRID_OFFSET_X + col * CELL_SIZE + (row % 2 === 1 ? CELL_SIZE/2 : 0);
|
||||
const y = GRID_OFFSET_Y + row * CELL_SIZE * 0.88;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
// 查找相邻同色泡泡 (用于消除)
|
||||
function findConnected(row, col, color, visited = new Set()) {
|
||||
const key = `${row},${col}`;
|
||||
if (row < 0 || row >= ROWS || col < 0 || col >= COLS || visited.has(key)) return [];
|
||||
if (grid[row][col] !== color) return [];
|
||||
|
||||
visited.add(key);
|
||||
const cluster = [{ row, col }];
|
||||
|
||||
// 六个方向 (考虑交错行)
|
||||
const directionsEven = [
|
||||
[-1, -1], [-1, 0], [0, -1], [0, 1], [1, -1], [1, 0]
|
||||
];
|
||||
const directionsOdd = [
|
||||
[-1, 0], [-1, 1], [0, -1], [0, 1], [1, 0], [1, 1]
|
||||
];
|
||||
const dirs = row % 2 === 0 ? directionsEven : directionsOdd;
|
||||
|
||||
for (let [dr, dc] of dirs) {
|
||||
const nr = row + dr;
|
||||
const nc = col + dc;
|
||||
cluster.push(...findConnected(nr, nc, color, visited));
|
||||
}
|
||||
return cluster;
|
||||
}
|
||||
|
||||
// 移除泡泡并更新分数
|
||||
function removeCluster(cluster) {
|
||||
if (cluster.length < 3) return false;
|
||||
for (let { row, col } of cluster) {
|
||||
grid[row][col] = null;
|
||||
}
|
||||
score += cluster.length * 10;
|
||||
updateScoreDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理悬浮泡泡掉落 (没有连接到顶部的泡泡全部掉落)
|
||||
function dropFloatingBubbles() {
|
||||
const visited = new Set();
|
||||
// 从顶行开始标记所有连接的泡泡
|
||||
function markConnected(row, col) {
|
||||
const key = `${row},${col}`;
|
||||
if (row < 0 || row >= ROWS || col < 0 || col >= COLS || visited.has(key)) return;
|
||||
if (grid[row][col] === null) return;
|
||||
visited.add(key);
|
||||
const dirsEven = [[-1,-1],[-1,0],[0,-1],[0,1],[1,-1],[1,0]];
|
||||
const dirsOdd = [[-1,0],[-1,1],[0,-1],[0,1],[1,0],[1,1]];
|
||||
const dirs = row % 2 === 0 ? dirsEven : dirsOdd;
|
||||
for (let [dr, dc] of dirs) {
|
||||
markConnected(row + dr, col + dc);
|
||||
}
|
||||
}
|
||||
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (grid[0][c] !== null) markConnected(0, c);
|
||||
}
|
||||
|
||||
let dropped = 0;
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (grid[r][c] !== null && !visited.has(`${r},${c}`)) {
|
||||
grid[r][c] = null;
|
||||
dropped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dropped > 0) {
|
||||
score += dropped * 15;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查游戏是否结束 (泡泡到达底部警戒线)
|
||||
function checkGameOver() {
|
||||
for (let r = ROWS - 1; r >= 0; r--) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (grid[r][c] !== null) {
|
||||
const pos = getBubblePos(r, c);
|
||||
if (pos.y + BUBBLE_RADIUS > SHOOTER_Y - 15) {
|
||||
endGame();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 发射泡泡
|
||||
function shootBubble(targetX, targetY) {
|
||||
if (!gameActive || projectile) return;
|
||||
if (!currentBubbleColor) return;
|
||||
|
||||
const dx = targetX - shooterX;
|
||||
const dy = targetY - SHOOTER_Y;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
if (length < 5) return; // 太近不发射
|
||||
|
||||
const speed = 9.5;
|
||||
const vx = (dx / length) * speed;
|
||||
const vy = (dy / length) * speed;
|
||||
|
||||
projectile = {
|
||||
x: shooterX,
|
||||
y: SHOOTER_Y,
|
||||
vx: vx,
|
||||
vy: vy,
|
||||
color: currentBubbleColor,
|
||||
radius: BUBBLE_RADIUS
|
||||
};
|
||||
|
||||
currentBubbleColor = nextBubbleColor;
|
||||
nextBubbleColor = randomColor();
|
||||
}
|
||||
|
||||
// 更新弹丸位置与碰撞
|
||||
function updateProjectile() {
|
||||
if (!projectile || !gameActive) return;
|
||||
|
||||
projectile.x += projectile.vx;
|
||||
projectile.y += projectile.vy;
|
||||
|
||||
// 边界反弹
|
||||
if (projectile.x - BUBBLE_RADIUS < 0) {
|
||||
projectile.x = BUBBLE_RADIUS;
|
||||
projectile.vx *= -0.9;
|
||||
}
|
||||
if (projectile.x + BUBBLE_RADIUS > CW) {
|
||||
projectile.x = CW - BUBBLE_RADIUS;
|
||||
projectile.vx *= -0.9;
|
||||
}
|
||||
|
||||
// 顶部碰撞
|
||||
if (projectile.y - BUBBLE_RADIUS < GRID_OFFSET_Y - BUBBLE_RADIUS) {
|
||||
placeProjectile();
|
||||
return;
|
||||
}
|
||||
|
||||
// 与网格泡泡碰撞检测
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (grid[r][c] === null) continue;
|
||||
const pos = getBubblePos(r, c);
|
||||
const dist = Math.hypot(projectile.x - pos.x, projectile.y - pos.y);
|
||||
if (dist < BUBBLE_RADIUS * 1.9) {
|
||||
placeProjectile();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 掉出底部 (失败情况,但不直接结束,只是移除弹丸)
|
||||
if (projectile.y > CH + 50) {
|
||||
projectile = null;
|
||||
}
|
||||
}
|
||||
|
||||
function placeProjectile() {
|
||||
if (!projectile) return;
|
||||
|
||||
// 找到最近的网格位置
|
||||
let bestRow = -1, bestCol = -1, bestDist = Infinity;
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (grid[r][c] !== null) continue;
|
||||
const pos = getBubblePos(r, c);
|
||||
const dist = Math.hypot(projectile.x - pos.x, projectile.y - pos.y);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestRow = r;
|
||||
bestCol = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestRow !== -1 && bestCol !== -1 && bestDist < CELL_SIZE * 1.2) {
|
||||
grid[bestRow][bestCol] = projectile.color;
|
||||
|
||||
// 检查消除
|
||||
const cluster = findConnected(bestRow, bestCol, projectile.color);
|
||||
if (cluster.length >= 3) {
|
||||
removeCluster(cluster);
|
||||
dropFloatingBubbles();
|
||||
}
|
||||
|
||||
checkGameOver();
|
||||
}
|
||||
|
||||
projectile = null;
|
||||
}
|
||||
|
||||
// ----- 绘制 -----
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, CW, CH);
|
||||
|
||||
// 绘制网格泡泡
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (grid[r][c] === null) continue;
|
||||
const { x, y } = getBubblePos(r, c);
|
||||
drawBubble(x, y, grid[r][c]);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制飞行弹丸
|
||||
if (projectile) {
|
||||
drawBubble(projectile.x, projectile.y, projectile.color);
|
||||
}
|
||||
|
||||
// 绘制发射器
|
||||
ctx.save();
|
||||
ctx.shadowColor = '#00000060';
|
||||
ctx.shadowBlur = 12;
|
||||
// 发射台
|
||||
ctx.fillStyle = '#445566';
|
||||
ctx.beginPath();
|
||||
ctx.arc(shooterX, SHOOTER_Y, 14, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#2c3e50';
|
||||
ctx.beginPath();
|
||||
ctx.arc(shooterX, SHOOTER_Y, 10, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 当前泡泡预览
|
||||
if (currentBubbleColor) {
|
||||
drawBubble(shooterX, SHOOTER_Y - 2, currentBubbleColor);
|
||||
}
|
||||
|
||||
// 下一个泡泡预览
|
||||
if (nextBubbleColor) {
|
||||
ctx.fillStyle = '#ffffff30';
|
||||
ctx.beginPath();
|
||||
ctx.arc(shooterX + 38, SHOOTER_Y - 8, 14, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
drawBubble(shooterX + 38, SHOOTER_Y - 8, nextBubbleColor, 12);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// 引导线
|
||||
if (aiming && gameActive && !projectile) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#ffffff80';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([6, 8]);
|
||||
ctx.moveTo(shooterX, SHOOTER_Y);
|
||||
ctx.lineTo(aimStartX, aimStartY);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// 游戏结束
|
||||
if (!gameActive) {
|
||||
ctx.globalAlpha = 0.75;
|
||||
ctx.fillStyle = '#0a1a2a';
|
||||
ctx.fillRect(0, 0, CW, CH);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 28px "Segoe UI"';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('🫧 结束', CW/2, CH/2-20);
|
||||
}
|
||||
}
|
||||
|
||||
function drawBubble(x, y, color, radius = BUBBLE_RADIUS) {
|
||||
ctx.save();
|
||||
ctx.shadowColor = '#00000040';
|
||||
ctx.shadowBlur = 8;
|
||||
const grad = ctx.createRadialGradient(x-4, y-4, radius*0.1, x, y, radius);
|
||||
grad.addColorStop(0, '#ffffff');
|
||||
grad.addColorStop(0.4, color);
|
||||
grad.addColorStop(1, '#00000040');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff90';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x-3, y-4, radius*0.3, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ----- 游戏循环 -----
|
||||
function gameLoop() {
|
||||
if (gameActive) {
|
||||
updateProjectile();
|
||||
}
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ----- 事件处理 -----
|
||||
function handleStart(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const scaleY = CH / rect.height;
|
||||
const canvasX = (clientX - rect.left) * scaleX;
|
||||
const canvasY = (clientY - rect.top) * scaleY;
|
||||
|
||||
// 如果点击靠近发射器区域,开始瞄准
|
||||
if (Math.abs(canvasY - SHOOTER_Y) < 60 && !projectile) {
|
||||
aiming = true;
|
||||
aimStartX = canvasX;
|
||||
aimStartY = canvasY;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMove(e) {
|
||||
e.preventDefault();
|
||||
if (!aiming || !gameActive) return;
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const scaleY = CH / rect.height;
|
||||
aimStartX = (clientX - rect.left) * scaleX;
|
||||
aimStartY = (clientY - rect.top) * scaleY;
|
||||
}
|
||||
|
||||
function handleEnd(e) {
|
||||
e.preventDefault();
|
||||
if (aiming && gameActive && !projectile) {
|
||||
shootBubble(aimStartX, aimStartY);
|
||||
}
|
||||
aiming = false;
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
canvas.addEventListener('touchstart', handleStart, { passive: false });
|
||||
canvas.addEventListener('touchmove', handleMove, { passive: false });
|
||||
canvas.addEventListener('touchend', handleEnd);
|
||||
canvas.addEventListener('touchcancel', handleEnd);
|
||||
canvas.addEventListener('mousedown', handleStart);
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (aiming && gameActive) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const scaleY = CH / rect.height;
|
||||
aimStartX = (e.clientX - rect.left) * scaleX;
|
||||
aimStartY = (e.clientY - rect.top) * scaleY;
|
||||
}
|
||||
});
|
||||
window.addEventListener('mouseup', handleEnd);
|
||||
document.getElementById('restartButton').addEventListener('click', restartGame);
|
||||
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
restartGame();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
restartGame();
|
||||
gameLoop();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,448 @@
|
||||
<!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, viewport-fit=cover, shrink-to-fit=yes">
|
||||
<title>节奏光剑 · 反应游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(180deg, #0a0a1a 0%, #1a1a3a 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.game-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: radial-gradient(circle at 50% 40%, #2a2a5a, #0d0d2b);
|
||||
border-radius: 48px;
|
||||
box-shadow: 0 25px 50px rgba(0,0,0,0.8), inset 0 0 30px rgba(100,100,255,0.2);
|
||||
padding: 16px 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid #4a4a8a;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 32px;
|
||||
background: radial-gradient(ellipse at 50% 0%, #1e1e4a 0%, #08081a 80%);
|
||||
box-shadow: inset 0 -10px 30px rgba(0,0,0,0.9), 0 12px 30px black;
|
||||
touch-action: none;
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
padding: 0 10px;
|
||||
color: #d0d0ff;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 10px #000066;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.score-board {
|
||||
background: rgba(10, 10, 40, 0.8);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 50px;
|
||||
padding: 8px 22px;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.7);
|
||||
border: 1px solid #6a6aff;
|
||||
color: #ccccff;
|
||||
}
|
||||
.restart-btn {
|
||||
background: #7b4aff;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
padding: 10px 25px;
|
||||
border-radius: 40px;
|
||||
background: linear-gradient(180deg, #9f7aff 0%, #5a2ae0 100%);
|
||||
box-shadow: 0 8px 0 #361a8c, 0 12px 24px rgba(0,0,0,0.6);
|
||||
transition: all 0.08s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #b89fff;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.restart-btn:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 4px 0 #361a8c, 0 8px 16px black;
|
||||
background: linear-gradient(180deg, #8a65f0 0%, #421db0 100%);
|
||||
}
|
||||
.hint {
|
||||
color: #aaaadd;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="360" height="600" aria-label="节奏光剑游戏画面"></canvas>
|
||||
<div class="info-panel">
|
||||
<div class="score-board" id="scoreDisplay">⚡ 0</div>
|
||||
<button class="restart-btn" id="restartButton">🔮 重启</button>
|
||||
</div>
|
||||
<div class="hint">👇 点击下方轨道区域 | 击中光点</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreSpan = document.getElementById('scoreDisplay');
|
||||
|
||||
const CW = 360;
|
||||
const CH = 600;
|
||||
canvas.width = CW;
|
||||
canvas.height = CH;
|
||||
|
||||
// ----- 轨道设置 -----
|
||||
const NUM_TRACKS = 4;
|
||||
const TRACK_WIDTH = CW / NUM_TRACKS;
|
||||
const HIT_ZONE_Y = CH - 75; // 判定线Y坐标
|
||||
const HIT_TOLERANCE = 42; // 判定范围
|
||||
|
||||
// 游戏状态
|
||||
let gameActive = true;
|
||||
let score = 0;
|
||||
let combo = 0;
|
||||
let maxCombo = 0;
|
||||
|
||||
// 音符数组
|
||||
let notes = [];
|
||||
|
||||
// 粒子效果
|
||||
let particles = [];
|
||||
|
||||
// 节拍控制
|
||||
const NOTE_SPEED = 5.2;
|
||||
const SPAWN_INTERVAL = 28; // 帧间隔
|
||||
let frameCounter = 0;
|
||||
|
||||
// 轨道闪烁效果
|
||||
let trackFlash = Array(NUM_TRACKS).fill(0);
|
||||
|
||||
// ----- 辅助函数 -----
|
||||
function randomTrack() {
|
||||
return Math.floor(Math.random() * NUM_TRACKS);
|
||||
}
|
||||
|
||||
function spawnNote() {
|
||||
const track = randomTrack();
|
||||
const x = track * TRACK_WIDTH + TRACK_WIDTH/2;
|
||||
notes.push({
|
||||
x: x,
|
||||
y: -30,
|
||||
track: track,
|
||||
radius: 14,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
gameActive = true;
|
||||
score = 0;
|
||||
combo = 0;
|
||||
maxCombo = 0;
|
||||
notes = [];
|
||||
particles = [];
|
||||
frameCounter = 0;
|
||||
trackFlash = Array(NUM_TRACKS).fill(0);
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
if (!gameActive) return;
|
||||
gameActive = false;
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(50);
|
||||
}
|
||||
}
|
||||
|
||||
function updateScoreDisplay() {
|
||||
scoreSpan.textContent = `⚡ ${score}`;
|
||||
}
|
||||
|
||||
// 添加粒子
|
||||
function spawnParticles(x, y, color) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 2 + Math.random() * 5;
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
life: 1.0,
|
||||
decay: 0.02 + Math.random() * 0.04,
|
||||
color: color,
|
||||
size: 2 + Math.random() * 4,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理点击轨道
|
||||
function hitTrack(trackIndex) {
|
||||
if (!gameActive) return;
|
||||
|
||||
let hitSuccess = false;
|
||||
let bestNoteIndex = -1;
|
||||
let bestDistance = HIT_TOLERANCE;
|
||||
|
||||
// 寻找最近的音符
|
||||
for (let i = notes.length - 1; i >= 0; i--) {
|
||||
const note = notes[i];
|
||||
if (!note.active || note.track !== trackIndex) continue;
|
||||
const distToZone = Math.abs(note.y - HIT_ZONE_Y);
|
||||
if (distToZone < bestDistance) {
|
||||
bestDistance = distToZone;
|
||||
bestNoteIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestNoteIndex !== -1) {
|
||||
const note = notes[bestNoteIndex];
|
||||
notes.splice(bestNoteIndex, 1);
|
||||
combo++;
|
||||
if (combo > maxCombo) maxCombo = combo;
|
||||
|
||||
// 根据准确度加分
|
||||
const accuracy = 1 - (bestDistance / HIT_TOLERANCE);
|
||||
const points = Math.floor(10 + accuracy * 30);
|
||||
score += points;
|
||||
|
||||
spawnParticles(note.x, note.y, '#ffff88');
|
||||
trackFlash[trackIndex] = 1.0;
|
||||
hitSuccess = true;
|
||||
}
|
||||
|
||||
if (!hitSuccess) {
|
||||
// 失误:重置连击
|
||||
combo = 0;
|
||||
trackFlash[trackIndex] = 0.4;
|
||||
spawnParticles(
|
||||
trackIndex * TRACK_WIDTH + TRACK_WIDTH/2,
|
||||
HIT_ZONE_Y,
|
||||
'#ff5555'
|
||||
);
|
||||
}
|
||||
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
// ----- 更新逻辑 -----
|
||||
function update() {
|
||||
if (!gameActive) return;
|
||||
|
||||
// 更新音符位置
|
||||
for (let i = notes.length - 1; i >= 0; i--) {
|
||||
const note = notes[i];
|
||||
note.y += NOTE_SPEED;
|
||||
|
||||
// 超出屏幕底部移除,并断连击
|
||||
if (note.y > CH + 50) {
|
||||
notes.splice(i, 1);
|
||||
combo = 0;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新粒子
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life -= p.decay;
|
||||
if (p.life <= 0) particles.splice(i, 1);
|
||||
}
|
||||
|
||||
// 更新轨道闪烁
|
||||
for (let t = 0; t < NUM_TRACKS; t++) {
|
||||
if (trackFlash[t] > 0) trackFlash[t] -= 0.05;
|
||||
}
|
||||
|
||||
// 生成音符
|
||||
if (gameActive) {
|
||||
frameCounter++;
|
||||
if (frameCounter >= SPAWN_INTERVAL) {
|
||||
spawnNote();
|
||||
frameCounter = 0;
|
||||
const variation = Math.floor(Math.random() * 8) - 2;
|
||||
frameCounter = -variation;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查游戏结束条件 (可选:漏掉太多音符?这里不使用,纯无限)
|
||||
}
|
||||
|
||||
// ----- 绘制图形 -----
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, CW, CH);
|
||||
|
||||
// 绘制轨道线
|
||||
for (let t = 0; t < NUM_TRACKS; t++) {
|
||||
const trackX = t * TRACK_WIDTH;
|
||||
ctx.fillStyle = trackFlash[t] > 0.1
|
||||
? `rgba(180, 180, 255, ${0.15 + trackFlash[t]*0.4})`
|
||||
: 'rgba(40, 40, 80, 0.4)';
|
||||
ctx.fillRect(trackX + 4, 0, TRACK_WIDTH - 8, CH);
|
||||
|
||||
ctx.strokeStyle = '#3a3a7a';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([12, 20]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(trackX + TRACK_WIDTH/2, 0);
|
||||
ctx.lineTo(trackX + TRACK_WIDTH/2, CH);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// 判定线
|
||||
ctx.strokeStyle = '#ffdd44';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.shadowColor = '#ffaa00';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, HIT_ZONE_Y);
|
||||
ctx.lineTo(CW, HIT_ZONE_Y);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// 绘制音符
|
||||
for (let note of notes) {
|
||||
if (!note.active) continue;
|
||||
ctx.save();
|
||||
ctx.shadowColor = '#ffffff';
|
||||
ctx.shadowBlur = 16;
|
||||
|
||||
const gradient = ctx.createRadialGradient(note.x-2, note.y-2, 2, note.x, note.y, note.radius);
|
||||
gradient.addColorStop(0, '#ffffff');
|
||||
gradient.addColorStop(0.5, '#aaccff');
|
||||
gradient.addColorStop(1, '#3355aa');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(note.x, note.y, note.radius, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 内核高光
|
||||
ctx.fillStyle = '#ffffffdd';
|
||||
ctx.beginPath();
|
||||
ctx.arc(note.x-2, note.y-3, note.radius*0.4, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// 绘制粒子
|
||||
for (let p of particles) {
|
||||
ctx.globalAlpha = p.life;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// 连击显示
|
||||
if (combo > 2) {
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 22px "Segoe UI"';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = '#ffaa00';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.fillText(`${combo} COMBO`, CW/2, 65);
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// 游戏结束
|
||||
if (!gameActive) {
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillStyle = '#05051a';
|
||||
ctx.fillRect(0, 0, CW, CH);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = '#d0d0ff';
|
||||
ctx.font = 'bold 28px "Segoe UI"';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('🌙 结束', CW/2, CH/2-25);
|
||||
ctx.font = '16px system-ui';
|
||||
ctx.fillText(`最高连击: ${maxCombo}`, CW/2, CH/2+30);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 游戏循环 -----
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ----- 事件处理 -----
|
||||
function handleCanvasTouch(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
|
||||
// 支持多点触摸处理多个轨道
|
||||
if (e.touches) {
|
||||
for (let i = 0; i < e.touches.length; i++) {
|
||||
const touch = e.touches[i];
|
||||
const canvasX = (touch.clientX - rect.left) * scaleX;
|
||||
const trackIndex = Math.floor(canvasX / TRACK_WIDTH);
|
||||
if (trackIndex >= 0 && trackIndex < NUM_TRACKS) {
|
||||
hitTrack(trackIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const canvasX = (e.clientX - rect.left) * scaleX;
|
||||
const trackIndex = Math.floor(canvasX / TRACK_WIDTH);
|
||||
if (trackIndex >= 0 && trackIndex < NUM_TRACKS) {
|
||||
hitTrack(trackIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
canvas.addEventListener('touchstart', handleCanvasTouch, { passive: false });
|
||||
canvas.addEventListener('mousedown', handleMouseDown);
|
||||
|
||||
document.getElementById('restartButton').addEventListener('click', restartGame);
|
||||
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
restartGame();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
restartGame();
|
||||
gameLoop();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
|
||||
<title>像素守卫 · 射击游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(180deg, #1a3a1a 0%, #0d260d 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.game-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: radial-gradient(circle at 50% 30%, #3a6b3a, #1a331a);
|
||||
border-radius: 48px;
|
||||
box-shadow: 0 25px 50px rgba(0,0,0,0.7), inset 0 0 25px rgba(150,255,150,0.15);
|
||||
padding: 16px 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid #5a8a5a;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 32px;
|
||||
background: radial-gradient(ellipse at 50% 20%, #4a7a4a 0%, #1a2e1a 85%);
|
||||
box-shadow: inset 0 -8px 25px rgba(0,0,0,0.8), 0 12px 28px black;
|
||||
touch-action: none;
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
padding: 0 10px;
|
||||
color: #c0ffc0;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 8px black;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.score-board {
|
||||
background: rgba(0,20,0,0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 50px;
|
||||
padding: 8px 22px;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,0.6);
|
||||
border: 1px solid #6aaa6a;
|
||||
}
|
||||
.restart-btn {
|
||||
background: #4caf50;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
padding: 10px 25px;
|
||||
border-radius: 40px;
|
||||
background: linear-gradient(180deg, #6fbf6f 0%, #2e7d32 100%);
|
||||
box-shadow: 0 8px 0 #1b5e20, 0 12px 22px rgba(0,0,0,0.5);
|
||||
transition: all 0.08s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #a5d6a7;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.restart-btn:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 4px 0 #1b5e20, 0 8px 14px rgba(0,0,0,0.6);
|
||||
background: linear-gradient(180deg, #5aa55a 0%, #1f5a22 100%);
|
||||
}
|
||||
.hint {
|
||||
color: #a0d0a0;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="360" height="600" aria-label="像素守卫射击游戏"></canvas>
|
||||
<div class="info-panel">
|
||||
<div class="score-board" id="scoreDisplay">🗡️ 0</div>
|
||||
<button class="restart-btn" id="restartButton">🏰 守城</button>
|
||||
</div>
|
||||
<div class="hint">👈 滑动瞄准 | 点击射击敌人</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreSpan = document.getElementById('scoreDisplay');
|
||||
|
||||
const CW = 360;
|
||||
const CH = 600;
|
||||
canvas.width = CW;
|
||||
canvas.height = CH;
|
||||
|
||||
// ----- 炮台设置 -----
|
||||
const TOWER_X = CW / 2;
|
||||
const TOWER_Y = CH - 55;
|
||||
let aimAngle = -Math.PI / 2; // 默认向上
|
||||
|
||||
// 游戏状态
|
||||
let gameActive = true;
|
||||
let score = 0;
|
||||
let health = 100;
|
||||
const MAX_HEALTH = 100;
|
||||
|
||||
// 敌人数组
|
||||
let enemies = [];
|
||||
|
||||
// 子弹数组
|
||||
let bullets = [];
|
||||
|
||||
// 粒子效果
|
||||
let particles = [];
|
||||
|
||||
// 生成控制
|
||||
const ENEMY_SPAWN_INTERVAL = 40;
|
||||
let frameCounter = 0;
|
||||
let difficultyTimer = 0;
|
||||
|
||||
// ----- 辅助函数 -----
|
||||
function spawnEnemy() {
|
||||
const side = Math.random() < 0.5 ? 'left' : 'right';
|
||||
const x = side === 'left' ? -20 : CW + 20;
|
||||
const y = 40 + Math.random() * 250;
|
||||
const speedX = side === 'left' ? 1.2 + Math.random() * 1.5 : -(1.2 + Math.random() * 1.5);
|
||||
const speedY = (Math.random() - 0.5) * 1.2;
|
||||
|
||||
enemies.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: speedX,
|
||||
vy: speedY,
|
||||
radius: 14,
|
||||
health: 1,
|
||||
color: '#ff5555',
|
||||
});
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
gameActive = true;
|
||||
score = 0;
|
||||
health = MAX_HEALTH;
|
||||
enemies = [];
|
||||
bullets = [];
|
||||
particles = [];
|
||||
frameCounter = 0;
|
||||
difficultyTimer = 0;
|
||||
aimAngle = -Math.PI / 2;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
if (!gameActive) return;
|
||||
gameActive = false;
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(60);
|
||||
}
|
||||
}
|
||||
|
||||
function updateScoreDisplay() {
|
||||
scoreSpan.textContent = `🗡️ ${score} ❤️ ${health}`;
|
||||
}
|
||||
|
||||
function damagePlayer(amount) {
|
||||
health -= amount;
|
||||
if (health <= 0) {
|
||||
health = 0;
|
||||
endGame();
|
||||
}
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
// 发射子弹
|
||||
function shootBullet() {
|
||||
if (!gameActive) return;
|
||||
const bulletSpeed = 8;
|
||||
const vx = Math.cos(aimAngle) * bulletSpeed;
|
||||
const vy = Math.sin(aimAngle) * bulletSpeed;
|
||||
bullets.push({
|
||||
x: TOWER_X,
|
||||
y: TOWER_Y,
|
||||
vx: vx,
|
||||
vy: vy,
|
||||
radius: 5,
|
||||
life: 80,
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 更新逻辑 -----
|
||||
function update() {
|
||||
if (!gameActive) return;
|
||||
|
||||
// 更新子弹
|
||||
for (let i = bullets.length - 1; i >= 0; i--) {
|
||||
const b = bullets[i];
|
||||
b.x += b.vx;
|
||||
b.y += b.vy;
|
||||
b.life--;
|
||||
|
||||
if (b.life <= 0 || b.x < -30 || b.x > CW + 30 || b.y < -30 || b.y > CH + 30) {
|
||||
bullets.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 子弹与敌人碰撞
|
||||
let bulletRemoved = false;
|
||||
for (let j = enemies.length - 1; j >= 0; j--) {
|
||||
const enemy = enemies[j];
|
||||
const dist = Math.hypot(b.x - enemy.x, b.y - enemy.y);
|
||||
if (dist < enemy.radius + b.radius) {
|
||||
// 消灭敌人
|
||||
enemies.splice(j, 1);
|
||||
bulletRemoved = true;
|
||||
score += 20;
|
||||
updateScoreDisplay();
|
||||
spawnParticles(enemy.x, enemy.y, '#ffaa00', 8);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bulletRemoved) {
|
||||
bullets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新敌人
|
||||
for (let i = enemies.length - 1; i >= 0; i--) {
|
||||
const enemy = enemies[i];
|
||||
enemy.x += enemy.vx;
|
||||
enemy.y += enemy.vy;
|
||||
|
||||
// 敌人与炮台碰撞
|
||||
const distToTower = Math.hypot(enemy.x - TOWER_X, enemy.y - TOWER_Y);
|
||||
if (distToTower < 35) {
|
||||
enemies.splice(i, 1);
|
||||
damagePlayer(12);
|
||||
spawnParticles(TOWER_X, TOWER_Y, '#ff3333', 12);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 移除离开屏幕的敌人
|
||||
if (enemy.x < -60 || enemy.x > CW + 60 || enemy.y < -60 || enemy.y > CH + 60) {
|
||||
enemies.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新粒子
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life -= 0.03;
|
||||
if (p.life <= 0) particles.splice(i, 1);
|
||||
}
|
||||
|
||||
// 生成敌人 (随难度增加)
|
||||
if (gameActive) {
|
||||
frameCounter++;
|
||||
difficultyTimer++;
|
||||
const adjustedInterval = Math.max(18, ENEMY_SPAWN_INTERVAL - Math.floor(difficultyTimer / 300));
|
||||
if (frameCounter >= adjustedInterval) {
|
||||
spawnEnemy();
|
||||
frameCounter = 0;
|
||||
if (Math.random() < 0.3) spawnEnemy(); // 偶尔双倍
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function spawnParticles(x, y, color, count) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 2 + Math.random() * 5;
|
||||
particles.push({
|
||||
x: x, y: y,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
life: 1.0,
|
||||
color: color,
|
||||
size: 3 + Math.random() * 4,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 绘制图形 -----
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, CW, CH);
|
||||
|
||||
// 绘制地面网格
|
||||
ctx.strokeStyle = '#2a4a2a';
|
||||
ctx.lineWidth = 0.8;
|
||||
for (let i = 0; i < CW; i += 30) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i, 0);
|
||||
ctx.lineTo(i, CH);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 绘制敌人
|
||||
for (let enemy of enemies) {
|
||||
ctx.save();
|
||||
ctx.shadowColor = '#ff0000aa';
|
||||
ctx.shadowBlur = 14;
|
||||
ctx.fillStyle = enemy.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(enemy.x-3, enemy.y-4, 4, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// 绘制子弹
|
||||
for (let b of bullets) {
|
||||
ctx.fillStyle = '#ffff66';
|
||||
ctx.shadowColor = '#ffaa00';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.beginPath();
|
||||
ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// 绘制粒子
|
||||
for (let p of particles) {
|
||||
ctx.globalAlpha = p.life;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// 绘制炮台
|
||||
ctx.save();
|
||||
ctx.translate(TOWER_X, TOWER_Y);
|
||||
// 基座
|
||||
ctx.fillStyle = '#5a5a7a';
|
||||
ctx.shadowColor = '#000000';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 18, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#7a7aaa';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 12, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 炮管
|
||||
ctx.rotate(aimAngle);
|
||||
ctx.fillStyle = '#ccccdd';
|
||||
ctx.fillRect(12, -5, 24, 10);
|
||||
ctx.fillStyle = '#aaaacc';
|
||||
ctx.fillRect(30, -3, 10, 6);
|
||||
ctx.restore();
|
||||
|
||||
// 生命条
|
||||
ctx.fillStyle = '#331111';
|
||||
ctx.fillRect(15, CH-18, CW-30, 14);
|
||||
const healthPercent = health / MAX_HEALTH;
|
||||
const healthColor = healthPercent > 0.5 ? '#44cc44' : '#ff6644';
|
||||
ctx.fillStyle = healthColor;
|
||||
ctx.fillRect(15, CH-18, (CW-30) * healthPercent, 14);
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(15, CH-18, CW-30, 14);
|
||||
|
||||
// 游戏结束
|
||||
if (!gameActive) {
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillStyle = '#0a0f0a';
|
||||
ctx.fillRect(0, 0, CW, CH);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = '#ffcccc';
|
||||
ctx.font = 'bold 28px "Segoe UI"';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('💀 失守', CW/2, CH/2-20);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 游戏循环 -----
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ----- 事件处理 -----
|
||||
function handleAim(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = CW / rect.width;
|
||||
const scaleY = CH / rect.height;
|
||||
const canvasX = (clientX - rect.left) * scaleX;
|
||||
const canvasY = (clientY - rect.top) * scaleY;
|
||||
|
||||
aimAngle = Math.atan2(canvasY - TOWER_Y, canvasX - TOWER_X);
|
||||
}
|
||||
|
||||
function handleShoot(e) {
|
||||
e.preventDefault();
|
||||
if (!gameActive) return;
|
||||
shootBullet();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
canvas.addEventListener('touchmove', handleAim, { passive: false });
|
||||
canvas.addEventListener('touchstart', (e) => {
|
||||
handleAim(e);
|
||||
handleShoot(e);
|
||||
});
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (e.buttons === 1) handleAim(e);
|
||||
});
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
handleAim(e);
|
||||
handleShoot(e);
|
||||
});
|
||||
|
||||
document.getElementById('restartButton').addEventListener('click', restartGame);
|
||||
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
restartGame();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
restartGame();
|
||||
gameLoop();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,8 @@
|
||||
|
||||
/* by qing.
|
||||
*
|
||||
* 用于获取一个节点下的所有子节点信息,0为根节点。
|
||||
*/
|
||||
async function getNode(token, id) {
|
||||
try {
|
||||
obj = { "pid": String(id) }
|
||||
@@ -43,6 +48,10 @@ async function getNode(token, id) {
|
||||
}
|
||||
}
|
||||
|
||||
/* by qing
|
||||
*
|
||||
* 用于向dnfs添加节点
|
||||
*/
|
||||
async function addNode(token, pid, title, content) {
|
||||
try {
|
||||
obj = {
|
||||
@@ -92,6 +101,7 @@ async function addNode(token, pid, title, content) {
|
||||
}
|
||||
}
|
||||
|
||||
/* by shufeng2012 */
|
||||
function renderMarkdown(element, content) {
|
||||
/*
|
||||
Markdown渲染
|
||||
@@ -99,7 +109,9 @@ function renderMarkdown(element, content) {
|
||||
const md = window.markdownit();
|
||||
element.innerHTML = md.render(content);
|
||||
}
|
||||
const stack = []; /* 这个是笔记节点的栈 */
|
||||
|
||||
/* ---- by qing ---- */
|
||||
const stack = []; /* 用于存储当前节点的路径,也就是经过的每一个节点的id, 否则就回不去上一层*/
|
||||
var article_title = document.getElementById('article-title');
|
||||
var article_content = document.getElementById('article-content');
|
||||
var input_note_title = document.getElementById('input-note-title');
|
||||
@@ -142,20 +154,29 @@ function load_notes(main_node) {
|
||||
bak.appendChild(bak1)
|
||||
recommend_list.appendChild(bak)
|
||||
}
|
||||
for (i in main_node) {
|
||||
for (let i in main_node) {
|
||||
item = main_node[i]
|
||||
var card = document.createElement('div');
|
||||
card.className = "card";
|
||||
var ttl = document.createElement('center');
|
||||
ttl.addEventListener('click', function(event) {
|
||||
card.addEventListener('click', function(event) {
|
||||
var id = Number(item.id)
|
||||
stack.push(id)
|
||||
getNode(token, id).then(load_notes)
|
||||
});
|
||||
var ttl = document.createElement('center');
|
||||
var ttl1 =document.createElement('h3');
|
||||
ttl1.textContent = item.title
|
||||
ttl.appendChild(ttl1)
|
||||
card.appendChild(ttl)
|
||||
ttl1.textContent = item.title;
|
||||
ttl.appendChild(ttl1);
|
||||
card.appendChild(ttl);
|
||||
var vit_div = document.createElement('p');
|
||||
var lke_div = document.createElement('p');
|
||||
var srd_div = document.createElement('p');
|
||||
vit_div.textContent = "观看数:" + item.visit_count;
|
||||
lke_div.textContent = "点赞数:" + item.like_count;
|
||||
srd_div.textContent = "分享数:" + item.share_count;
|
||||
ttl.appendChild(vit_div);
|
||||
ttl.appendChild(lke_div);
|
||||
ttl.appendChild(srd_div);
|
||||
var ctt = document.createElement('p');
|
||||
renderMarkdown(ctt, item.content);
|
||||
card.appendChild(ctt)
|
||||
|
||||
@@ -54,6 +54,7 @@ function esc(str) {
|
||||
function navigate(path, type) {
|
||||
if (type === "dir") window.location.href = "/" + path;
|
||||
else if (type === "video" || type === "audio") window.location.href = "/video/" + path;
|
||||
else if (/\.html?$/i.test(path)) window.open("/" + path, "_blank");
|
||||
else window.location.href = "/" + path;
|
||||
}
|
||||
function playMedia(path) { window.location.href = "/video/" + path; }
|
||||
@@ -108,9 +109,9 @@ function render() {
|
||||
? `<button class="btn-play" onclick="event.stopPropagation(); playMedia('${path}')">▶</button>`
|
||||
: '';
|
||||
return `
|
||||
<div class="item">
|
||||
<div class="item" onclick="navigate('${path}', '${type}')">
|
||||
<span class="icon">${getIcon(item)}</span>
|
||||
<div class="info" onclick="navigate('${path}', '${type}')">
|
||||
<div class="info">
|
||||
<div class="name">${name}</div>
|
||||
<div class="meta">
|
||||
${sizeOrDir}
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
<!-- 移动端适配核心标签 -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>登录</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
.tip-red {color:red;}
|
||||
.tip-yellow {color: orange;}
|
||||
.tip-green {color: greenyellow;}
|
||||
</style>
|
||||
<link rel="stylesheet" href="css/theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
@@ -20,7 +15,6 @@
|
||||
<label>用户名/推荐邮箱:</label>
|
||||
<input type="text" id="loginUsername" name="username" placeholder="请输入用户名">
|
||||
<span id="msgTip" ></span>
|
||||
<br>
|
||||
<label>密码:</label>
|
||||
<input type="password" id="loginPassword" name="password" placeholder="请输入密码">
|
||||
<span id="pwdTip"></span>
|
||||
@@ -33,18 +27,21 @@
|
||||
</span>
|
||||
<span id="emlTip"></span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-actions">
|
||||
<button type="button" id="registerBtn">注册</button>
|
||||
<input type="submit" id="loginBtn" value="登录">
|
||||
<input type="reset" id="resetUser" value="重置">
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="agreeBox" name="agree" value="access">
|
||||
<label id="agreement">同意该系统相关用户使用条例</label>
|
||||
<span id="agreeTip"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
|
||||
</footer>
|
||||
<script src="js/validation.js"></script>
|
||||
<script src="js/login.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -3,13 +3,8 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
.tip-red {color:red;}
|
||||
.tip-yellow {color: orange;}
|
||||
.tip-green {color: greenyellow;}
|
||||
</style>
|
||||
<title>注册</title>
|
||||
<link rel="stylesheet" href="css/theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
@@ -19,7 +14,6 @@
|
||||
<label>用户名/推荐邮箱:</label>
|
||||
<input type="text" id="regusername" placeholder="请输入用户名">
|
||||
<span id="msgTip" ></span>
|
||||
<br>
|
||||
<label>邮箱号:</label>
|
||||
<input type="email" id="regemail" placeholder="请输入邮箱号">
|
||||
<div class="code-box">验证码:
|
||||
@@ -28,20 +22,22 @@
|
||||
<span id="codeTip"></span>
|
||||
</div>
|
||||
<span id="emlTip"></span>
|
||||
<br>
|
||||
<label>密码:</label>
|
||||
<input type="password" id="regpsw" placeholder="密码至少6位">
|
||||
<label>验证密码:</label>
|
||||
<input type="password" id="repwd" placeholder="请再次输入密码">
|
||||
<span id="pwdTip"></span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-actions">
|
||||
<input id="formsub" type="submit" value="提交">
|
||||
<input id="resetUser" type="reset" value="重置">
|
||||
<input type="button" value="返回登录" onclick="location.href='login.html'">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
|
||||
</footer>
|
||||
<script src="js/validation.js"></script>
|
||||
<script src="js/register.js?v=6"></script>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user