!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:
qingss0 committed 2026-07-23 12:34:57 +08:00
commit 72cb84c935
26 files changed
+7487 -121

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[Server]
address = 0.0.0.0
port = 9201
port = 443
[Proxy]
default = http://127.0.0.1:9200
+3 -2
View File
@@ -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
View File
@@ -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'
+6 -2
View File
@@ -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>
-71
View File
@@ -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;
}
}
+274
View File
@@ -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); }
}
+14
View File
@@ -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
View File
@@ -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>
+21
View File
@@ -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.
+345
View File
@@ -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>
+813
View File
@@ -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>
+707
View File
@@ -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>
+846
View File
@@ -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
+739
View File
@@ -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>
+556
View File
@@ -0,0 +1,556 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, 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>
+479
View File
@@ -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>
+562
View File
@@ -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>
+448
View File
@@ -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>
+459
View File
@@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, 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>
+28 -7
View File
@@ -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)
+3 -2
View File
@@ -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}
+6 -9
View File
@@ -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>
+6 -10
View File
@@ -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>