钢铁前线 - KARDS规则版
by HyperShark771389 lines63.4 KB
<!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>钢铁前线 - KARDS规则版</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
body { background: #0a0a0f; font-family: 'Microsoft YaHei', sans-serif; color: #e0e8f0; min-height: 100vh; overflow-x: hidden; user-select: none; }
.screen { display: none; }
.screen.active { display: block; }
/* ============ 主菜单 ============ */
#menuScreen { padding: 20px 16px; min-height: 100vh; }
.title { text-align: center; font-size: 40px; letter-spacing: 8px; background: linear-gradient(90deg, #ffb000, #ff3d3d, #ffb000); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 4px; filter: drop-shadow(0 0 30px rgba(255, 100, 0, 0.4)); font-weight: 900; }
.subtitle { text-align: center; color: #708090; font-size: 13px; letter-spacing: 5px; margin-bottom: 20px; }
.faction-config { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: 1000px; margin: 0 auto; }
@media (max-width: 760px) { .faction-config { grid-template-columns: 1fr; } .title { font-size: 26px; } }
.side-panel { background: rgba(20, 25, 35, 0.7); border-radius: 14px; padding: 16px; border: 2px solid rgba(74, 155, 223, 0.3); }
.side-panel.enemy { border-color: rgba(255, 61, 113, 0.3); }
.side-title { font-size: 17px; font-weight: bold; letter-spacing: 2px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid rgba(74, 155, 223, 0.2); }
.side-title.ally { color: #4a9bdf; }
.side-title.enemy { color: #ff3d71; border-bottom-color: rgba(255, 61, 113, 0.2); }
.faction-slot { background: rgba(30, 40, 55, 0.6); border-radius: 10px; padding: 10px; margin-bottom: 8px; border: 1px solid rgba(74, 155, 223, 0.2); display: flex; gap: 10px; align-items: center; }
.faction-slot.filled { border-color: rgba(0, 255, 160, 0.5); background: rgba(0, 255, 160, 0.05); }
.faction-index { width: 26px; height: 26px; border-radius: 50%; background: rgba(74, 155, 223, 0.3); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 13px; color: #4a9bdf; flex-shrink: 0; }
.side-panel.enemy .faction-index { background: rgba(255, 61, 113, 0.3); color: #ff3d71; }
.faction-inputs { flex: 1; display: flex; flex-direction: column; gap: 5px; }
.faction-inputs input[type="text"] { background: rgba(0, 0, 0, 0.4); border: 1.5px solid rgba(74, 155, 223, 0.4); color: #e0e8f0; padding: 5px 10px; border-radius: 6px; font-family: inherit; font-size: 13px; outline: none; width: 100%; }
.faction-inputs input[type="text"]:focus { border-color: #4a9bdf; box-shadow: 0 0 12px rgba(74, 155, 223, 0.5); }
.side-panel.enemy .faction-inputs input[type="text"]:focus { border-color: #ff3d71; box-shadow: 0 0 12px rgba(255, 61, 113, 0.5); }
.faction-inputs input.empty { border-color: rgba(255, 200, 0, 0.6); animation: pulseInput 1.5s infinite; }
@keyframes pulseInput { 0%, 100% { box-shadow: 0 0 6px rgba(255, 200, 0, 0.3); } 50% { box-shadow: 0 0 16px rgba(255, 200, 0, 0.7); } }
.file-btn { position: relative; overflow: hidden; display: inline-block; padding: 4px 12px; background: rgba(74, 155, 223, 0.15); border: 1px dashed rgba(74, 155, 223, 0.4); border-radius: 6px; font-size: 11px; color: #7a95b0; cursor: pointer; text-align: center; }
.side-panel.enemy .file-btn { background: rgba(255, 61, 113, 0.15); border-color: rgba(255, 61, 113, 0.4); color: #b07a8a; }
.file-btn.has-img { border-style: solid; color: #00ffa0; border-color: rgba(0, 255, 160, 0.5); }
.file-btn input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.faction-preview { width: 40px; height: 40px; border-radius: 6px; background: #1a1e2a; border: 2px solid rgba(74, 155, 223, 0.3); background-size: cover; background-position: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: #3a4555; font-size: 18px; }
.faction-preview.ally { background: linear-gradient(135deg, #1a3a5f, #2a5a9f); color: #66b8ff; }
.faction-preview.enemy { background: linear-gradient(135deg, #5f1a3a, #9f2a5a); color: #ff8fb0; }
.faction-preview.filled { border-color: #00ffa0; }
.hint { text-align: center; margin: 14px 0; color: #ffcc00; font-size: 13px; }
.hint.ready { color: #00ffa0; }
.start-btn { display: block; margin: 14px auto 0; padding: 14px 50px; font-size: 17px; font-weight: bold; letter-spacing: 4px; color: #001018; background: linear-gradient(90deg, #00e5ff, #00ffa0); border: none; border-radius: 10px; cursor: pointer; font-family: inherit; }
.start-btn:hover:not(:disabled) { transform: scale(1.05); box-shadow: 0 0 40px rgba(0, 229, 255, 0.7); }
.start-btn:disabled { background: rgba(60, 70, 80, 0.6); color: #4a5a60; cursor: not-allowed; }
/* ============ 战场 ============ */
#gameScreen { min-height: 100vh; display: flex; flex-direction: column; padding: 4px; background: radial-gradient(ellipse at center, #1a1f2f 0%, #080a10 100%); position: relative; }
.hq-bar { display: flex; justify-content: space-between; align-items: center; padding: 4px 12px; border-radius: 8px; background: rgba(20, 25, 35, 0.85); border: 2px solid rgba(255, 61, 113, 0.3); margin-bottom: 3px; }
.hq-bar.player { border-color: rgba(74, 155, 223, 0.3); }
.hq-name { font-size: 12px; font-weight: bold; letter-spacing: 1px; }
.hq-hp { display: flex; align-items: center; gap: 6px; cursor: pointer; }
.hq-hp:hover .hp-value { transform: scale(1.15); }
.hp-icon { font-size: 16px; }
.hp-value { font-size: 18px; font-weight: bold; color: #ff3d71; transition: transform 0.15s; }
.hq-bar.player .hp-value { color: #4a9bdf; }
.kredit-info { display: flex; align-items: center; gap: 5px; font-size: 12px; }
.kredit-icon { color: #ffb000; font-size: 15px; }
.battlefield { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 3px; min-height: 0; }
.row-wrap { position: relative; }
.row-label { position: absolute; top: 50%; left: 3px; transform: translateY(-50%); font-size: 9px; color: #4a5a70; letter-spacing: 2px; writing-mode: vertical-rl; z-index: 5; }
.row { border-radius: 8px; padding: 4px; display: flex; gap: 5px; align-items: center; justify-content: center; min-height: 88px; flex-wrap: nowrap; overflow-x: auto; transition: all 0.2s; }
.row.back { background: rgba(15, 20, 30, 0.5); border: 1.5px dashed rgba(74, 155, 223, 0.15); min-height: 78px; }
.row.front {
background: linear-gradient(180deg, rgba(255, 61, 113, 0.04), rgba(255, 176, 0, 0.08), rgba(74, 155, 223, 0.04));
border: 2.5px solid rgba(255, 176, 0, 0.5);
box-shadow: inset 0 0 30px rgba(255, 176, 0, 0.1);
position: relative;
min-height: 96px;
}
.row.front::before {
content: '⚔ 公 用 前 线 ⚔';
position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
background: #0a0a0f; color: #ffb000; font-size: 10px; padding: 0 10px;
letter-spacing: 3px; font-weight: bold;
}
.row.front.controlled-player { border-color: #4a9bdf; box-shadow: inset 0 0 30px rgba(74, 155, 223, 0.2); }
.row.front.controlled-player::before { content: '⚔ 我方控制前线 ⚔'; color: #4a9bdf; }
.row.front.controlled-enemy { border-color: #ff3d71; box-shadow: inset 0 0 30px rgba(255, 61, 113, 0.2); }
.row.front.controlled-enemy::before { content: '⚔ 敌方控制前线 ⚔'; color: #ff3d71; }
.row.drop-hover { background: rgba(0, 255, 160, 0.15); border-color: #00ffa0; }
.row.drop-invalid { background: rgba(255, 61, 113, 0.15); border-color: #ff3d71; }
/* 单位卡 */
.unit {
width: 68px; height: 92px; border-radius: 7px;
position: relative;
background-size: cover; background-position: center;
background-color: #1a1e2a;
border: 2px solid rgba(74, 155, 223, 0.7);
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
overflow: hidden;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
flex-shrink: 0;
}
.unit::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 40%, rgba(0, 0, 0, 0.85) 100%); }
.unit.enemy-unit { border-color: rgba(255, 61, 113, 0.8); }
.unit.enemy-unit::before { content: ''; position: absolute; inset: -2px; border-radius: 7px; pointer-events: none; box-shadow: inset 0 0 12px rgba(255, 61, 113, 0.5); }
.unit:hover { transform: translateY(-3px); }
.unit.selected { border-color: #00ffa0; box-shadow: 0 0 25px rgba(0, 255, 160, 0.9), 0 0 40px rgba(0, 255, 160, 0.5); transform: translateY(-6px) scale(1.05); z-index: 10; }
.unit.can-attack::after { content: ''; position: absolute; top: -4px; right: -4px; width: 12px; height: 12px; border-radius: 50%; background: #ffb000; box-shadow: 0 0 12px #ffb000; animation: pulseDot 1s infinite; z-index: 2; }
.unit.can-move::before { content: '↗'; position: absolute; top: -4px; right: -4px; width: 14px; height: 14px; border-radius: 50%; background: #00ffa0; box-shadow: 0 0 12px #00ffa0; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #000; font-weight: bold; z-index: 3; animation: pulseDot 1s infinite; }
@keyframes pulseDot { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.3); } }
.unit.attacking { animation: attackShake 0.3s; }
@keyframes attackShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
.unit.hit { animation: hitShake 0.3s; }
@keyframes hitShake { 0%, 100% { transform: translateX(0); filter: brightness(1); } 50% { transform: translateX(3px); filter: brightness(2.5); } }
.unit-name { position: absolute; bottom: 22px; left: 0; right: 0; text-align: center; font-size: 10px; font-weight: bold; color: #fff; text-shadow: 0 1px 3px #000; z-index: 1; padding: 0 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.unit-faction { position: absolute; bottom: 10px; left: 0; right: 0; text-align: center; font-size: 8px; color: #a0c8ff; z-index: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.unit-stats { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; padding: 1px 5px; font-size: 12px; font-weight: bold; z-index: 1; }
.unit-atk { color: #ffb000; text-shadow: 0 0 6px #000; }
.unit-def { color: #4a9bdf; text-shadow: 0 0 6px #000; }
.unit-cost { position: absolute; top: 3px; left: 3px; width: 19px; height: 19px; border-radius: 50%; background: rgba(0, 0, 0, 0.85); border: 1.5px solid #ffb000; color: #ffb000; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; z-index: 2; }
.unit-owner-tag { position: absolute; top: 3px; right: 3px; font-size: 8px; padding: 1px 4px; border-radius: 3px; z-index: 2; font-weight: bold; }
.unit-owner-tag.ally { background: rgba(74, 155, 223, 0.9); color: #fff; }
.unit-owner-tag.enemy { background: rgba(255, 61, 113, 0.9); color: #fff; }
.empty-slot { width: 68px; height: 92px; border-radius: 7px; border: 1.5px dashed rgba(74, 155, 223, 0.15); display: flex; align-items: center; justify-content: center; color: rgba(74, 155, 223, 0.25); font-size: 18px; flex-shrink: 0; }
/* 手牌区 */
.hand-area { display: flex; flex-direction: column; gap: 3px; }
.hand { display: flex; gap: 6px; justify-content: center; flex-wrap: nowrap; overflow-x: auto; min-height: 100px; align-items: flex-end; padding: 5px; background: rgba(15, 20, 30, 0.6); border-radius: 10px; border: 1.5px solid rgba(74, 155, 223, 0.2); }
.card {
width: 68px; height: 100px; border-radius: 7px;
background-size: cover; background-position: center;
background-color: #1a1e2a;
border: 2px solid rgba(74, 155, 223, 0.6);
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
position: relative; overflow: hidden;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
flex-shrink: 0;
}
.card::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 35%, rgba(0, 0, 0, 0.85) 100%); }
.card.skill-card { border-color: rgba(155, 89, 255, 0.8); background: linear-gradient(135deg, rgba(155, 89, 255, 0.3), rgba(0, 200, 255, 0.2)); }
.card.skill-card::after { background: linear-gradient(180deg, transparent 30%, rgba(30, 0, 60, 0.9) 100%); }
.card.skill-card::before { content: '✦ 技能'; position: absolute; top: 3px; right: 3px; font-size: 8px; color: #d4b3ff; background: rgba(155, 89, 255, 0.4); padding: 1px 5px; border-radius: 3px; z-index: 3; font-weight: bold; }
.card:hover:not(.unaffordable):not(.dragging) { transform: translateY(-10px) scale(1.05); box-shadow: 0 12px 25px rgba(0, 229, 255, 0.5); border-color: #00e5ff; }
.card.skill-card:hover:not(.unaffordable) { box-shadow: 0 12px 25px rgba(155, 89, 255, 0.7); border-color: #d4b3ff; }
.card.unaffordable { opacity: 0.4; cursor: not-allowed; filter: grayscale(60%); }
.card.dragging { opacity: 0.3; }
.card-name { position: absolute; bottom: 24px; left: 0; right: 0; text-align: center; font-size: 10px; font-weight: bold; color: #fff; text-shadow: 0 1px 3px #000; z-index: 1; padding: 0 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-faction { position: absolute; bottom: 11px; left: 0; right: 0; text-align: center; font-size: 8px; color: #a0c8ff; z-index: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-stats { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; padding: 1px 5px; font-size: 12px; font-weight: bold; z-index: 1; }
.card-desc { position: absolute; bottom: 8px; left: 0; right: 0; text-align: center; font-size: 8px; color: #d4b3ff; z-index: 1; padding: 0 3px; line-height: 1.2; }
.card-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -70%); font-size: 26px; z-index: 1; filter: drop-shadow(0 0 8px rgba(155, 89, 255, 0.8)); }
.move-cost-badge { position: absolute; bottom: 52px; left: 50%; transform: translateX(-50%); font-size: 8px; color: #00ffa0; background: rgba(0, 0, 0, 0.7); padding: 1px 5px; border-radius: 3px; z-index: 2; border: 1px solid rgba(0, 255, 160, 0.5); }
.unit .move-cost-badge { bottom: 42px; }
/* 拖拽幽灵 */
.ghost-card {
position: fixed; width: 68px; height: 100px; border-radius: 7px;
background-size: cover; background-position: center;
background-color: #1a1e2a;
border: 2px solid #00e5ff;
box-shadow: 0 0 30px rgba(0, 229, 255, 0.8), 0 12px 40px rgba(0, 0, 0, 0.7);
pointer-events: none; z-index: 1000;
transform: translate(-50%, -50%);
opacity: 0.92; overflow: hidden;
}
.ghost-card::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 35%, rgba(0, 0, 0, 0.85) 100%); }
.ghost-card.invalid { border-color: #ff3d71; box-shadow: 0 0 30px rgba(255, 61, 113, 0.8), 0 12px 40px rgba(0, 0, 0, 0.7); }
.ghost-card .card-name { position: absolute; bottom: 24px; left: 0; right: 0; text-align: center; font-size: 10px; font-weight: bold; color: #fff; text-shadow: 0 1px 3px #000; z-index: 1; }
.ghost-card .card-faction { position: absolute; bottom: 11px; left: 0; right: 0; text-align: center; font-size: 8px; color: #a0c8ff; z-index: 1; }
.ghost-card .card-stats { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; padding: 1px 5px; font-size: 12px; font-weight: bold; z-index: 1; }
.ghost-card .unit-cost { position: absolute; top: 3px; left: 3px; width: 19px; height: 19px; border-radius: 50%; background: rgba(0, 0, 0, 0.85); border: 1.5px solid #ffb000; color: #ffb000; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; z-index: 2; }
/* 攻击箭头 */
#arrowLayer { position: fixed; inset: 0; pointer-events: none; z-index: 500; }
/* 回合信息 */
.turn-bar { display: flex; justify-content: space-between; align-items: center; padding: 5px 12px; background: rgba(20, 25, 35, 0.85); border-radius: 8px; border: 2px solid rgba(74, 155, 223, 0.3); }
.turn-indicator { font-size: 13px; font-weight: bold; letter-spacing: 2px; }
.turn-indicator.player { color: #4a9bdf; }
.turn-indicator.enemy { color: #ff3d71; }
.round-info { font-size: 12px; color: #a0b0c0; }
.end-turn-btn { padding: 7px 20px; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #001018; background: linear-gradient(90deg, #ffb000, #ff8000); border: none; border-radius: 8px; cursor: pointer; font-family: inherit; box-shadow: 0 0 20px rgba(255, 176, 0, 0.4); }
.end-turn-btn:hover:not(:disabled) { transform: scale(1.05); }
.end-turn-btn:disabled { background: rgba(60, 70, 80, 0.6); color: #4a5a60; cursor: not-allowed; box-shadow: none; }
/* 提示 */
.toast { position: fixed; top: 70px; left: 50%; transform: translateX(-50%); padding: 9px 22px; background: rgba(0, 0, 0, 0.9); border: 2px solid #ffb000; border-radius: 8px; color: #ffb000; font-size: 13px; font-weight: bold; z-index: 2000; animation: toastIn 0.3s, toastOut 0.3s 1.2s forwards; pointer-events: none; }
@keyframes toastIn { from { opacity: 0; transform: translate(-50%, -20px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes toastOut { to { opacity: 0; transform: translate(-50%, -20px); } }
.float-text { position: fixed; font-size: 20px; font-weight: bold; pointer-events: none; z-index: 800; text-shadow: 0 0 8px currentColor; animation: floatUp 1s ease-out forwards; }
@keyframes floatUp { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-70px); opacity: 0; } }
.spark { position: fixed; pointer-events: none; z-index: 700; border-radius: 50%; }
.screen-shake { animation: shakeScreen 0.35s; }
@keyframes shakeScreen { 0%, 100% { transform: translate(0, 0); } 20% { transform: translate(-4px, 3px); } 40% { transform: translate(4px, -3px); } 60% { transform: translate(-3px, -2px); } 80% { transform: translate(3px, 2px); } }
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.9); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1500; animation: fadeIn 0.5s; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.overlay-title { font-size: 56px; font-weight: 900; letter-spacing: 8px; margin-bottom: 20px; }
.overlay-title.win { background: linear-gradient(90deg, #00ffa0, #00e5ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; filter: drop-shadow(0 0 30px rgba(0, 255, 160, 0.6)); }
.overlay-title.lose { background: linear-gradient(90deg, #ff3d71, #ff6b6b); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; filter: drop-shadow(0 0 30px rgba(255, 61, 113, 0.6)); }
.overlay-sub { color: #a0b0c0; font-size: 15px; margin-bottom: 30px; letter-spacing: 2px; }
.overlay-btn { padding: 13px 40px; font-size: 15px; font-weight: bold; letter-spacing: 3px; color: #001018; background: linear-gradient(90deg, #00e5ff, #00ffa0); border: none; border-radius: 10px; cursor: pointer; font-family: inherit; }
.overlay-btn:hover { transform: scale(1.05); }
.rule-badge { position: fixed; bottom: 4px; left: 4px; background: rgba(0, 0, 0, 0.75); border: 1px solid rgba(255, 176, 0, 0.4); border-radius: 6px; padding: 5px 9px; font-size: 9px; color: #ffb000; z-index: 100; line-height: 1.5; max-width: 280px; }
.rule-badge b { color: #00e5ff; }
.skill-hint { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 16px 32px; background: rgba(155, 89, 255, 0.95); border: 3px solid #d4b3ff; border-radius: 12px; color: #fff; font-size: 16px; font-weight: bold; z-index: 2000; box-shadow: 0 0 40px rgba(155, 89, 255, 0.8); pointer-events: none; animation: skillPop 0.8s forwards; }
@keyframes skillPop { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } 30% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.3); } }
/* 底部操作栏 */
.action-bar { position: fixed; bottom: 130px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; z-index: 600; pointer-events: none; }
.action-bar > * { pointer-events: auto; }
.action-btn { padding: 11px 24px; border-radius: 25px; font-family: inherit; font-size: 13px; font-weight: bold; letter-spacing: 1.5px; cursor: pointer; border: none; }
.move-btn { background: linear-gradient(90deg, #00ffa0, #00e5ff); color: #001018; box-shadow: 0 0 30px rgba(0, 255, 160, 0.6); animation: pulseBtn 1.5s infinite; }
.attack-btn { background: linear-gradient(90deg, #ffb000, #ff8000); color: #001018; box-shadow: 0 0 30px rgba(255, 176, 0, 0.6); animation: pulseBtn 1.5s infinite; }
@keyframes pulseBtn { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
.cancel-btn { background: rgba(60, 70, 80, 0.9); color: #c0c8d0; }
.move-btn:disabled, .attack-btn:disabled { background: rgba(60, 70, 80, 0.6); color: #4a5a60; cursor: not-allowed; box-shadow: none; animation: none; }
/* 选中单位信息 */
.unit-info { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); opacity: 0; padding: 12px 24px; background: rgba(0, 0, 0, 0.9); border: 2px solid #00ffa0; border-radius: 10px; z-index: 900; pointer-events: none; transition: all 0.2s; text-align: center; }
.unit-info.show { transform: translate(-50%, -50%) scale(1); opacity: 1; }
</style>
</head>
<body>
<!-- ==================== 主菜单 ==================== -->
<div class="screen active" id="menuScreen">
<h1 class="title">钢铁前线</h1>
<div class="subtitle">STEEL FRONTLINE · KARDS 规则</div>
<div class="faction-config">
<div class="side-panel ally">
<div class="side-title ally">🔵 我方阵营(3 个)</div>
<div id="allyFactions"></div>
</div>
<div class="side-panel enemy">
<div class="side-title enemy">🔴 敌方阵营(3 个)</div>
<div id="enemyFactions"></div>
</div>
</div>
<div class="hint" id="hint">⚠️ 请填完 6 个阵营名称(图片可选)</div>
<button class="start-btn" id="startBtn" disabled onclick="startGame()">开 始 对 战</button>
</div>
<!-- ==================== 战场 ==================== -->
<div class="screen" id="gameScreen">
<div class="hq-bar">
<div class="hq-name" style="color:#ff3d71;" id="enemyName">敌军指挥部</div>
<div class="hq-hp" onclick="attackHQ('enemy')">
<span class="hp-icon">❤️</span>
<span class="hp-value" id="enemyHQ">30</span>
</div>
<div class="kredit-info">
<span class="kredit-icon">◆</span>
<span id="enemyKredit">0</span>
<span style="color:#708090; font-size:11px;">手牌 <span id="enemyHandCount">0</span></span>
</div>
</div>
<div class="battlefield">
<div class="row-wrap">
<div class="row-label">敌后</div>
<div class="row back enemy-row" id="enemyBackRow"></div>
</div>
<div class="row-wrap">
<div class="row front" id="frontLineRow"></div>
</div>
<div class="turn-bar">
<div class="turn-indicator player" id="turnIndicator">🔵 我方回合</div>
<div class="round-info" id="roundInfo">第 1 回合</div>
<button class="end-turn-btn" id="endTurnBtn" onclick="endTurn()">结 束 回 合</button>
</div>
<div class="row-wrap">
<div class="row-label">我后</div>
<div class="row back" id="playerBackRow"></div>
</div>
</div>
<div class="hq-bar player">
<div class="hq-name" style="color:#4a9bdf;" id="playerName">我军指挥部</div>
<div class="hq-hp" onclick="attackHQ('player')">
<span class="hp-icon">❤️</span>
<span class="hp-value" id="playerHQ">30</span>
</div>
<div class="kredit-info">
<span class="kredit-icon">◆</span>
<span id="playerKredit">0</span>
</div>
</div>
<div class="hand-area">
<div class="hand" id="playerHand"></div>
</div>
</div>
<svg id="arrowLayer"></svg>
<div class="rule-badge">
<b>KARDS 规则:</b><br>
<b>①</b> 卡牌只能先部署到我方后方<br>
<b>②</b> 花行动费才能从后方 → 前线<br>
<b>③</b> 前线只能被一方控制<br>
<b>④</b> 后方单位可攻击敌方前线<br>
<b>⑤</b> 只有前线单位能攻击敌方 HQ
</div>
<script>
/* ============================================================
钢铁前线 - KARDS 规则严格版(修复版)
============================================================ */
const allyFactions = [{ name:'', image:null }, { name:'', image:null }, { name:'', image:null }];
const enemyFactions = [{ name:'', image:null }, { name:'', image:null }, { name:'', image:null }];
const CARD_TEMPLATES = [
{ name: '步兵班', cost: 1, attack: 1, defense: 2, moveCost: 1 },
{ name: '精锐步兵', cost: 2, attack: 2, defense: 3, moveCost: 1 },
{ name: '装甲车', cost: 3, attack: 3, defense: 3, moveCost: 2 },
{ name: '中型坦克', cost: 4, attack: 4, defense: 5, moveCost: 2 },
{ name: '重型坦克', cost: 5, attack: 6, defense: 6, moveCost: 3 },
];
const SKILL_CARDS = [
{ name: '空袭', cost: 3, type: 'skill', skill: 'airstrike', icon: '✈️', desc: '前线敌方单位 -3 血' },
{ name: '增援', cost: 2, type: 'skill', skill: 'reinforce', icon: '📦', desc: '立即抽 2 张牌' },
{ name: '战地医院', cost: 3, type: 'skill', skill: 'heal', icon: '🏥', desc: '恢复 HQ 5 血' },
{ name: '冲锋号', cost: 2, type: 'skill', skill: 'charge', icon: '📯', desc: '我方单位立刻可攻击' },
{ name: '铁血', cost: 4, type: 'skill', skill: 'buff', icon: '⚔️', desc: '我方单位 +1攻 +1防' },
{ name: '烟幕', cost: 3, type: 'skill', skill: 'smoke', icon: '💨', desc: '敌方单位本回合无法攻击' },
];
const MAX_FRONT = 6;
const MAX_BACK = 5;
let game = null;
let dragging = null;
let movingUnit = null;
function createGame() {
return {
turn: 'player',
round: 1,
playerHQ: 30,
enemyHQ: 30,
playerKredit: 1,
enemyKredit: 1,
maxKredit: 10,
playerHand: [],
enemyHand: [],
frontLine: [],
frontOwner: null,
playerBack: [],
enemyBack: [],
playerDeck: buildDeck('player'),
enemyDeck: buildDeck('enemy'),
selectedUnit: null,
busy: false,
};
}
function buildDeck(side) {
const factions = side === 'player' ? allyFactions : enemyFactions;
const deck = [];
for (let round = 0; round < 2; round++) {
for (let f = 0; f < 3; f++) {
CARD_TEMPLATES.forEach(tpl => {
deck.push({
factionIndex: f,
factionName: factions[f].name,
factionImage: factions[f].image,
name: tpl.name,
cost: tpl.cost,
attack: tpl.attack,
defense: tpl.defense,
moveCost: tpl.moveCost,
type: 'unit',
});
});
}
}
SKILL_CARDS.forEach(sk => {
for (let i = 0; i < 2; i++) {
deck.push({
factionIndex: -1,
factionName: '战术',
factionImage: null,
name: sk.name,
cost: sk.cost,
type: 'skill',
skill: sk.skill,
icon: sk.icon,
desc: sk.desc,
});
}
});
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}
/* ============ 主菜单 ============ */
function buildFactionConfig() {
const a = document.getElementById('allyFactions');
const e = document.getElementById('enemyFactions');
a.innerHTML = ''; e.innerHTML = '';
[1, 2, 3].forEach((num, i) => {
a.appendChild(createFactionSlot('ally', i, num));
e.appendChild(createFactionSlot('enemy', i, num));
});
}
function createFactionSlot(side, index, num) {
const faction = side === 'ally' ? allyFactions[index] : enemyFactions[index];
const div = document.createElement('div');
div.className = 'faction-slot';
div.id = `slot-${side}-${index}`;
div.innerHTML = `
<div class="faction-index">${num}</div>
<div class="faction-inputs">
<input type="text" placeholder="阵营名 ${num}" maxlength="8" class="empty">
<label class="file-btn">📷 上传图片(可选)<input type="file" accept="image/*"></label>
</div>
<div class="faction-preview ${side}">?</div>
`;
const input = div.querySelector('input[type="text"]');
const fileBtn = div.querySelector('.file-btn');
const file = div.querySelector('input[type="file"]');
const preview = div.querySelector('.faction-preview');
input.addEventListener('input', () => {
faction.name = input.value.trim();
input.classList.toggle('empty', !faction.name);
updateSlotState(side, index);
checkReady();
});
file.addEventListener('change', evt => {
const f = evt.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.onload = ev => {
faction.image = ev.target.result;
preview.style.backgroundImage = `url(${faction.image})`;
preview.textContent = '';
preview.classList.add('filled');
fileBtn.classList.add('has-img');
fileBtn.childNodes[0].textContent = '✓ 已上传';
updateSlotState(side, index);
checkReady();
};
reader.readAsDataURL(f);
});
return div;
}
function updateSlotState(side, index) {
const faction = side === 'ally' ? allyFactions[index] : enemyFactions[index];
const slot = document.getElementById(`slot-${side}-${index}`);
if (faction.name) slot.classList.add('filled');
else slot.classList.remove('filled');
}
function checkReady() {
const ok = [...allyFactions, ...enemyFactions].every(f => f.name);
const hint = document.getElementById('hint');
const btn = document.getElementById('startBtn');
if (ok) { hint.textContent = '✅ 阵营名已就绪,可以开始(未上传图片的用默认色)'; hint.classList.add('ready'); btn.disabled = false; }
else {
const c = [...allyFactions, ...enemyFactions].filter(f => f.name).length;
hint.textContent = `⚠️ 请填完 6 个阵营名称(${c}/6),图片可选`;
hint.classList.remove('ready');
btn.disabled = true;
}
}
/* ============ 开始游戏 ============ */
function startGame() {
document.getElementById('menuScreen').classList.remove('active');
document.getElementById('gameScreen').classList.add('active');
game = createGame();
for (let i = 0; i < 3; i++) drawCard('player');
for (let i = 0; i < 3; i++) drawCard('enemy');
render();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('touchend', onTouchEnd);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('click', onClickOutside);
}
function drawCard(side) {
const g = game;
if (side === 'player') {
if (g.playerDeck.length > 0 && g.playerHand.length < 7) g.playerHand.push(g.playerDeck.pop());
} else {
if (g.enemyDeck.length > 0 && g.enemyHand.length < 7) g.enemyHand.push(g.enemyDeck.pop());
}
}
/* ============ 渲染 ============ */
function render() {
if (!game) return;
const g = game;
document.getElementById('playerHQ').textContent = g.playerHQ;
document.getElementById('enemyHQ').textContent = g.enemyHQ;
document.getElementById('playerKredit').textContent = g.playerKredit;
document.getElementById('enemyKredit').textContent = g.enemyKredit;
document.getElementById('enemyHandCount').textContent = g.enemyHand.length;
document.getElementById('roundInfo').textContent = `第 ${g.round} 回合`;
const ind = document.getElementById('turnIndicator');
if (g.turn === 'player') { ind.textContent = '🔵 我方回合'; ind.className = 'turn-indicator player'; }
else { ind.textContent = '🔴 敌方回合'; ind.className = 'turn-indicator enemy'; }
document.getElementById('endTurnBtn').disabled = g.turn !== 'player' || g.busy;
renderRow('enemyBackRow', g.enemyBack, 'back');
renderFrontLine();
renderRow('playerBackRow', g.playerBack, 'back');
renderHand();
document.getElementById('playerName').textContent = allyFactions[0].name + ' 指挥部';
document.getElementById('enemyName').textContent = enemyFactions[0].name + ' 指挥部';
updateActionBar();
if (g.selectedUnit) drawAttackArrows(); else clearArrows();
}
/* 底部操作栏 */
function updateActionBar() {
const existing = document.getElementById('actionBar');
if (existing) existing.remove();
if (!game || game.turn !== 'player' || game.busy) return;
const sel = game.selectedUnit;
if (!sel || sel.owner !== 'player') return;
const bar = document.createElement('div');
bar.className = 'action-bar';
bar.id = 'actionBar';
// 移动按钮(仅后方单位)
if (sel.lane === 'back') {
const canMove = game.playerKredit >= sel.moveCost
&& game.frontOwner !== 'enemy'
&& !game.frontLine.some(u => u.owner === 'enemy')
&& game.frontLine.length < MAX_FRONT;
const moveBtn = document.createElement('button');
moveBtn.className = 'move-btn action-btn';
moveBtn.textContent = canMove ? `↗ 移动到前线(消耗 ${sel.moveCost})` : `↗ 移动到前线(条件不足)`;
moveBtn.disabled = !canMove;
moveBtn.onclick = (e) => { e.stopPropagation(); executeMove(); };
bar.appendChild(moveBtn);
}
const cancelBtn = document.createElement('button');
cancelBtn.className = 'cancel-btn action-btn';
cancelBtn.textContent = '取消';
cancelBtn.onclick = (e) => { e.stopPropagation(); game.selectedUnit = null; movingUnit = null; clearArrows(); render(); };
bar.appendChild(cancelBtn);
document.body.appendChild(bar);
}
function renderRow(rowId, units, lane) {
const row = document.getElementById(rowId);
row.innerHTML = '';
units.forEach(u => row.appendChild(buildUnitEl(u, lane)));
for (let i = units.length; i < MAX_BACK; i++) {
const el = document.createElement('div');
el.className = 'empty-slot';
el.textContent = '·';
row.appendChild(el);
}
}
function renderFrontLine() {
const row = document.getElementById('frontLineRow');
row.innerHTML = '';
row.classList.remove('controlled-player', 'controlled-enemy');
if (game.frontOwner === 'player') row.classList.add('controlled-player');
if (game.frontOwner === 'enemy') row.classList.add('controlled-enemy');
game.frontLine.forEach(u => row.appendChild(buildUnitEl(u, 'front')));
for (let i = game.frontLine.length; i < MAX_FRONT; i++) {
const el = document.createElement('div');
el.className = 'empty-slot';
el.textContent = '·';
row.appendChild(el);
}
}
function buildUnitEl(u, lane) {
const el = document.createElement('div');
const side = u.owner;
el.className = 'unit' + (side === 'enemy' ? ' enemy-unit' : '');
if (side === 'player' && game.selectedUnit === u) el.classList.add('selected');
if (side === 'player' && u.canAttack && game.turn === 'player') el.classList.add('can-attack');
if (side === 'player' && u.canMove && lane === 'back' && game.turn === 'player') el.classList.add('can-move');
if (u.image) el.style.backgroundImage = `url(${u.image})`;
let moveCostHtml = '';
if (side === 'player' && lane === 'back' && u.type === 'unit') {
moveCostHtml = `<div class="move-cost-badge">↗ ${u.moveCost}</div>`;
}
el.innerHTML = `
<div class="unit-cost">${u.cost}</div>
<div class="unit-owner-tag ${side === 'player' ? 'ally' : 'enemy'}">${side === 'player' ? '我' : '敌'}</div>
${moveCostHtml}
<div class="unit-name">${u.name}</div>
<div class="unit-faction">${u.factionName}</div>
<div class="unit-stats">
<span class="unit-atk">⚔${u.attack}</span>
<span class="unit-def">🛡${u.currentDefense}</span>
</div>
`;
el.addEventListener('click', e => {
e.stopPropagation();
onUnitClick(side, u, lane);
});
return el;
}
function renderHand() {
const hand = document.getElementById('playerHand');
hand.innerHTML = '';
game.playerHand.forEach((card, idx) => {
const canAfford = game.playerKredit >= card.cost;
const isSkill = card.type === 'skill';
const el = document.createElement('div');
el.className = 'card' + (isSkill ? ' skill-card' : '') + (canAfford ? '' : ' unaffordable');
el.dataset.handIndex = idx;
if (!isSkill && card.factionImage) el.style.backgroundImage = `url(${card.factionImage})`;
if (isSkill) {
el.innerHTML = `
<div class="unit-cost">${card.cost}</div>
<div class="card-icon">${card.icon}</div>
<div class="card-name">${card.name}</div>
<div class="card-desc">${card.desc}</div>
`;
el.addEventListener('click', () => useSkill(idx));
} else {
el.innerHTML = `
<div class="unit-cost">${card.cost}</div>
<div class="move-cost-badge">↗ ${card.moveCost}</div>
<div class="card-name">${card.name}</div>
<div class="card-faction">${card.factionName}</div>
<div class="card-stats">
<span class="unit-atk">⚔${card.attack}</span>
<span class="unit-def">🛡${card.defense}</span>
</div>
`;
el.addEventListener('mousedown', e => startDrag(e, idx));
el.addEventListener('touchstart', e => { e.preventDefault(); startDrag(e, idx); }, { passive: false });
}
hand.appendChild(el);
});
}
/* ============ 拖拽 ============ */
function startDrag(e, handIndex) {
if (!game || game.turn !== 'player' || game.busy) return;
const card = game.playerHand[handIndex];
if (!card || card.type === 'skill') return;
if (game.playerKredit < card.cost) { toast('圣水不足!'); return; }
if (game.playerBack.length >= MAX_BACK) { toast('我方后方已满!'); return; }
dragging = { card, handIndex, ghostEl: null };
const ghost = document.createElement('div');
ghost.className = 'ghost-card';
if (card.factionImage) ghost.style.backgroundImage = `url(${card.factionImage})`;
ghost.innerHTML = `
<div class="unit-cost">${card.cost}</div>
<div class="card-name">${card.name}</div>
<div class="card-faction">${card.factionName}</div>
<div class="card-stats">
<span class="unit-atk">⚔${card.attack}</span>
<span class="unit-def">🛡${card.defense}</span>
</div>
`;
document.body.appendChild(ghost);
dragging.ghostEl = ghost;
moveGhost(e);
renderHand();
const el = document.querySelector(`.card[data-hand-index="${handIndex}"]`);
if (el) el.classList.add('dragging');
}
function moveGhost(e) {
if (!dragging || !dragging.ghostEl) return;
const x = e.clientX !== undefined ? e.clientX : e.touches[0].clientX;
const y = e.clientY !== undefined ? e.clientY : e.touches[0].clientY;
dragging.ghostEl.style.left = x + 'px';
dragging.ghostEl.style.top = y + 'px';
const target = detectDropTarget(x, y);
dragging.ghostEl.classList.toggle('invalid', !target);
document.querySelectorAll('.row').forEach(r => r.classList.remove('drop-hover'));
if (target) target.row.classList.add('drop-hover');
}
function detectDropTarget(x, y) {
const els = document.elementsFromPoint(x, y);
for (const el of els) {
if (el.id === 'playerBackRow') {
if (game.playerBack.length < MAX_BACK) return { row: el, lane: 'back' };
return null;
}
if (el.id === 'frontLineRow' || el.id === 'enemyBackRow') return null;
}
return null;
}
function onMouseMove(e) { if (dragging) moveGhost(e); }
function onTouchMove(e) { if (dragging) { e.preventDefault(); moveGhost(e); } }
function onMouseUp(e) { if (dragging) finishDrag(e.clientX, e.clientY); }
function onTouchEnd(e) { if (dragging) { const t = e.changedTouches[0]; finishDrag(t.clientX, t.clientY); } }
function finishDrag(x, y) {
if (!dragging) return;
const d = dragging;
const target = detectDropTarget(x, y);
if (target) {
const card = d.card;
if (game.playerKredit >= card.cost) {
game.playerKredit -= card.cost;
game.playerHand.splice(d.handIndex, 1);
const newUnit = {
...card,
owner: 'player',
currentDefense: card.defense,
canAttack: false,
canMove: true,
image: card.factionImage,
lane: 'back',
};
game.playerBack.push(newUnit);
showFloatText('+ ' + card.name, '#00e5ff', target.row);
}
}
if (d.ghostEl) d.ghostEl.remove();
document.querySelectorAll('.row').forEach(r => r.classList.remove('drop-hover'));
dragging = null;
render();
}
function onKeyDown(e) {
if (e.code === 'Escape') {
if (dragging) { if (dragging.ghostEl) dragging.ghostEl.remove(); dragging = null; render(); }
if (movingUnit) movingUnit = null;
if (game && game.selectedUnit) { game.selectedUnit = null; clearArrows(); render(); }
}
}
function onClickOutside(e) {
if (!game) return;
if (e.target.closest('.unit')) return;
if (e.target.closest('.row')) return;
if (e.target.closest('.hq-bar')) return;
if (e.target.closest('.hand')) return;
if (e.target.closest('.action-bar')) return;
if (game.selectedUnit) { game.selectedUnit = null; clearArrows(); }
if (movingUnit) movingUnit = null;
render();
}
/* ============ 移动 ============ */
function executeMove() {
if (!movingUnit) return;
const u = movingUnit;
if (game.playerKredit < u.moveCost) { toast(`需要 ${u.moveCost} 圣水才能移动到前线`); return; }
if (game.frontOwner === 'enemy') { toast('敌方控制前线,无法进入!'); return; }
if (game.frontLine.some(x => x.owner === 'enemy')) { toast('前线有敌军,无法进入!'); return; }
if (game.frontLine.length >= MAX_FRONT) { toast('前线已满!'); return; }
game.playerKredit -= u.moveCost;
const idx = game.playerBack.indexOf(u);
if (idx >= 0) game.playerBack.splice(idx, 1);
u.lane = 'front';
u.canMove = false;
u.canAttack = false;
game.frontLine.push(u);
game.frontOwner = 'player';
movingUnit = null;
game.selectedUnit = null;
render();
}
/* ============ 单位点击(核心修复) ============ */
function onUnitClick(side, unit, lane) {
if (!game || game.turn !== 'player' || game.busy) return;
// 我方单位
if (side === 'player') {
if (game.selectedUnit === unit) {
// 再次点击 = 取消选中
game.selectedUnit = null;
movingUnit = null;
clearArrows();
render();
return;
}
// 选中该单位(无论在前线还是后方)
game.selectedUnit = unit;
// 后方单位同时进入移动模式
if (lane === 'back') {
movingUnit = unit;
} else {
movingUnit = null;
}
// 显示攻击箭头
drawAttackArrows();
render();
return;
}
// 敌方单位
if (side === 'enemy') {
if (!game.selectedUnit) {
toast('请先选中我方单位');
return;
}
const attacker = game.selectedUnit;
if (!attacker.canAttack) {
toast('该单位本回合已行动');
return;
}
if (!canAttackTarget(attacker, unit)) {
if (attacker.lane === 'back') toast('后方单位只能攻击敌方前线单位!');
else toast('无法攻击该目标');
return;
}
performAttack(attacker, unit);
}
}
function canAttackTarget(attacker, target) {
if (attacker.owner !== 'player') return false;
if (target.owner !== 'enemy') return false;
if (attacker.lane === 'front') return true;
// 后方:只能打敌方前线
return target.lane === 'front';
}
function canAttackHQ(attacker) {
if (!attacker) return false;
if (attacker.owner !== 'player') return false;
if (attacker.lane !== 'front') return false;
return !game.frontLine.some(u => u.owner === 'enemy');
}
/* ============ 攻击箭头 ============ */
function drawAttackArrows() {
clearArrows();
const svg = document.getElementById('arrowLayer');
const attacker = game.selectedUnit;
if (!attacker) return;
const attackerEl = findUnitEl(attacker);
if (!attackerEl) return;
const aRect = attackerEl.getBoundingClientRect();
const ax = aRect.left + aRect.width / 2;
const ay = aRect.top + aRect.height / 2;
const targets = [];
game.frontLine.forEach(u => {
if (u.owner === 'enemy' && canAttackTarget(attacker, u)) {
const el = findUnitEl(u);
if (el) { const r = el.getBoundingClientRect(); targets.push({ x: r.left + r.width / 2, y: r.top + r.height / 2, type: 'unit' }); }
}
});
game.enemyBack.forEach(u => {
if (canAttackTarget(attacker, u)) {
const el = findUnitEl(u);
if (el) { const r = el.getBoundingClientRect(); targets.push({ x: r.left + r.width / 2, y: r.top + r.height / 2, type: 'unit' }); }
}
});
if (canAttackHQ(attacker)) {
const hqEl = document.getElementById('enemyHQ');
const r = hqEl.getBoundingClientRect();
targets.push({ x: r.left + r.width / 2, y: r.top + r.height / 2, type: 'hq' });
}
targets.forEach((t, i) => drawArrowSVG(svg, ax, ay, t.x, t.y, t.type === 'hq' ? '#ff3d71' : '#ffb000', i));
}
function drawArrowSVG(svg, x1, y1, x2, y2, color, delay) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const shorten = 28;
const sx = x1 + Math.cos(angle) * shorten;
const sy = y1 + Math.sin(angle) * shorten;
const ex = x2 - Math.cos(angle) * shorten;
const ey = y2 - Math.sin(angle) * shorten;
const midX = (sx + ex) / 2, midY = (sy + ey) / 2;
const perpX = -Math.sin(angle), perpY = Math.cos(angle);
const curve = Math.min(60, len * 0.2);
const ctrlX = midX + perpX * curve;
const ctrlY = midY + perpY * curve;
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', `M ${sx} ${sy} Q ${ctrlX} ${ctrlY} ${ex} ${ey}`);
path.setAttribute('stroke', color);
path.setAttribute('stroke-width', '4');
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
path.style.filter = `drop-shadow(0 0 8px ${color})`;
path.style.animation = `dashIn 0.4s ${delay * 0.05}s both`;
path.setAttribute('stroke-dasharray', '12 6');
svg.appendChild(path);
const headLen = 14, headAngle = 0.5;
const hx1 = ex - Math.cos(angle - headAngle) * headLen;
const hy1 = ey - Math.sin(angle - headAngle) * headLen;
const hx2 = ex - Math.cos(angle + headAngle) * headLen;
const hy2 = ey - Math.sin(angle + headAngle) * headLen;
const head = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
head.setAttribute('points', `${ex},${ey} ${hx1},${hy1} ${hx2},${hy2}`);
head.setAttribute('fill', color);
head.style.filter = `drop-shadow(0 0 8px ${color})`;
svg.appendChild(head);
const ring = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
ring.setAttribute('cx', x2);
ring.setAttribute('cy', y2);
ring.setAttribute('r', '16');
ring.setAttribute('stroke', color);
ring.setAttribute('stroke-width', '3');
ring.setAttribute('fill', 'none');
ring.style.animation = 'ringPulse 1s infinite';
ring.style.filter = `drop-shadow(0 0 10px ${color})`;
svg.appendChild(ring);
}
function clearArrows() { document.getElementById('arrowLayer').innerHTML = ''; }
function findUnitEl(unit) {
const rows = [
{ id: 'frontLineRow', arr: game.frontLine },
{ id: 'playerBackRow', arr: game.playerBack },
{ id: 'enemyBackRow', arr: game.enemyBack },
];
for (const r of rows) {
const idx = r.arr.indexOf(unit);
if (idx >= 0) {
const row = document.getElementById(r.id);
const children = row.querySelectorAll('.unit');
if (children[idx]) return children[idx];
}
}
return null;
}
/* ============ 攻击 HQ ============ */
function attackHQ(targetSide) {
if (!game || game.turn !== 'player' || game.busy) return;
if (targetSide !== 'enemy') return;
const attacker = game.selectedUnit;
if (!attacker) { toast('请先选中一个前线单位'); return; }
if (!attacker.canAttack) { toast('该单位本回合已行动'); return; }
if (!canAttackHQ(attacker)) {
if (attacker.lane !== 'front') toast('后方单位无法攻击 HQ!');
else toast('前线还有敌军,请先击败它们!');
return;
}
const dmg = attacker.attack;
game.enemyHQ -= dmg;
attacker.canAttack = false;
showFloatText(`-${dmg}`, '#ff3d71', document.getElementById('enemyHQ'));
spawnSparks(window.innerWidth / 2, 60, '#ff3d71', 20);
shakeScreen();
game.selectedUnit = null;
movingUnit = null;
clearArrows();
render();
checkWin();
}
/* ============ 攻击执行 ============ */
function performAttack(attacker, defender) {
const g = game;
g.busy = true;
const atkEl = findUnitEl(attacker);
const defEl = findUnitEl(defender);
if (atkEl) atkEl.classList.add('attacking');
if (defEl) defEl.classList.add('hit');
if (atkEl && defEl) {
const ar = atkEl.getBoundingClientRect(), dr = defEl.getBoundingClientRect();
drawStrikeLine(ar.left + ar.width / 2, ar.top + ar.height / 2, dr.left + dr.width / 2, dr.top + dr.height / 2);
}
defender.currentDefense -= attacker.attack;
attacker.currentDefense -= defender.attack;
if (defEl) { showFloatText(`-${attacker.attack}`, '#ff3d71', defEl); const r = defEl.getBoundingClientRect(); spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ffb000', 14); }
if (atkEl) showFloatText(`-${defender.attack}`, '#ffb000', atkEl);
shakeScreen();
attacker.canAttack = false;
g.selectedUnit = null;
movingUnit = null;
clearArrows();
setTimeout(() => {
game.frontLine = game.frontLine.filter(u => u.currentDefense > 0);
game.playerBack = game.playerBack.filter(u => u.currentDefense > 0);
game.enemyBack = game.enemyBack.filter(u => u.currentDefense > 0);
updateFrontOwner();
g.busy = false;
render();
checkWin();
}, 400);
}
function updateFrontOwner() {
const allyInFront = game.frontLine.filter(u => u.owner === 'player');
const enemyInFront = game.frontLine.filter(u => u.owner === 'enemy');
if (enemyInFront.length > 0) game.frontOwner = 'enemy';
else if (allyInFront.length > 0) game.frontOwner = 'player';
else game.frontOwner = null;
}
function drawStrikeLine(x1, y1, x2, y2) {
const svg = document.getElementById('arrowLayer');
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('stroke', '#fff');
line.setAttribute('stroke-width', '6');
line.setAttribute('stroke-linecap', 'round');
line.style.filter = 'drop-shadow(0 0 12px #fff) drop-shadow(0 0 20px #ffb000)';
svg.appendChild(line);
setTimeout(() => line.remove(), 300);
}
function spawnSparks(x, y, color, count) {
for (let i = 0; i < count; i++) {
const spark = document.createElement('div');
spark.className = 'spark';
const size = 3 + Math.random() * 5;
spark.style.width = size + 'px';
spark.style.height = size + 'px';
spark.style.background = color;
spark.style.left = x + 'px';
spark.style.top = y + 'px';
spark.style.boxShadow = `0 0 12px ${color}`;
document.body.appendChild(spark);
const angle = Math.random() * Math.PI * 2;
const dist = 40 + Math.random() * 80;
const tx = Math.cos(angle) * dist, ty = Math.sin(angle) * dist;
spark.animate([
{ transform: 'translate(-50%, -50%) scale(1)', opacity: 1 },
{ transform: `translate(calc(-50% + ${tx}px), calc(-50% + ${ty}px)) scale(0)`, opacity: 0 },
], { duration: 600, easing: 'cubic-bezier(0.15, 0.7, 0.4, 1)' });
setTimeout(() => spark.remove(), 600);
}
}
function shakeScreen() {
const el = document.getElementById('gameScreen');
el.classList.add('screen-shake');
setTimeout(() => el.classList.remove('screen-shake'), 350);
}
/* ============ 技能卡 ============ */
function useSkill(handIndex) {
if (!game || game.turn !== 'player' || game.busy) return;
const card = game.playerHand[handIndex];
if (!card || card.type !== 'skill') return;
if (game.playerKredit < card.cost) { toast('圣水不足!'); return; }
game.playerKredit -= card.cost;
game.playerHand.splice(handIndex, 1);
const hint = document.createElement('div');
hint.className = 'skill-hint';
hint.textContent = `${card.icon} ${card.name}!`;
document.body.appendChild(hint);
setTimeout(() => hint.remove(), 800);
switch (card.skill) {
case 'airstrike': {
game.frontLine.forEach(u => {
if (u.owner === 'enemy') {
u.currentDefense -= 3;
const el = findUnitEl(u);
if (el) {
el.classList.add('hit');
const r = el.getBoundingClientRect();
spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ff3d71', 12);
showFloatText('-3', '#ff3d71', el);
}
}
});
shakeScreen();
setTimeout(() => {
game.frontLine = game.frontLine.filter(u => u.currentDefense > 0);
updateFrontOwner();
render();
}, 500);
break;
}
case 'reinforce': {
for (let i = 0; i < 2; i++) drawCard('player');
toast('抽了 2 张牌');
render();
break;
}
case 'heal': {
game.playerHQ = Math.min(30, game.playerHQ + 5);
showFloatText('+5', '#00ffa0', document.getElementById('playerHQ'));
render();
break;
}
case 'charge': {
game.frontLine.forEach(u => { if (u.owner === 'player') u.canAttack = true; });
toast('冲锋号!前线单位可立即攻击');
render();
break;
}
case 'buff': {
game.frontLine.forEach(u => { if (u.owner === 'player') { u.attack += 1; u.currentDefense += 1; u.defense += 1; } });
game.playerBack.forEach(u => { u.attack += 1; u.currentDefense += 1; u.defense += 1; });
toast('铁血!全员 +1/+1');
render();
break;
}
case 'smoke': {
game.frontLine.forEach(u => { if (u.owner === 'enemy') u.canAttack = false; });
game.enemyBack.forEach(u => u.canAttack = false);
toast('烟幕!敌军无法攻击');
render();
break;
}
}
}
/* ============ 结束回合 ============ */
function endTurn() {
if (!game || game.turn !== 'player' || game.busy) return;
if (dragging) return;
game.selectedUnit = null;
movingUnit = null;
clearArrows();
game.turn = 'enemy';
game.round++;
render();
setTimeout(() => enemyTurnStart(), 500);
}
function enemyTurnStart() {
if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
game.enemyKredit = Math.min(game.maxKredit, game.enemyKredit + 1);
drawCard('enemy');
game.frontLine.forEach(u => { if (u.owner === 'enemy') u.canAttack = true; });
game.enemyBack.forEach(u => { u.canAttack = true; u.canMove = true; });
render();
setTimeout(() => enemyPlayCards(), 500);
}
function enemyPlayCards() {
if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
const playable = game.enemyHand
.map((c, i) => ({ c, i }))
.filter(x => x.c.cost <= game.enemyKredit)
.sort((a, b) => b.c.cost - a.c.cost);
if (playable.length > 0 && Math.random() < 0.9) {
const pick = playable[0];
const card = pick.c;
if (card.type === 'skill') {
game.enemyKredit -= card.cost;
game.enemyHand.splice(pick.i, 1);
if (card.skill === 'airstrike') {
game.frontLine.forEach(u => { if (u.owner === 'player') u.currentDefense -= 3; });
setTimeout(() => { game.frontLine = game.frontLine.filter(u => u.currentDefense > 0); updateFrontOwner(); render(); }, 500);
} else if (card.skill === 'heal') {
game.enemyHQ = Math.min(30, game.enemyHQ + 5);
} else if (card.skill === 'buff') {
game.frontLine.forEach(u => { if (u.owner === 'enemy') { u.attack += 1; u.currentDefense += 1; } });
game.enemyBack.forEach(u => { u.attack += 1; u.currentDefense += 1; });
} else if (card.skill === 'charge') {
game.frontLine.forEach(u => { if (u.owner === 'enemy') u.canAttack = true; });
game.enemyBack.forEach(u => u.canAttack = true);
} else if (card.skill === 'smoke') {
game.frontLine.forEach(u => { if (u.owner === 'player') u.canAttack = false; });
game.playerBack.forEach(u => u.canAttack = false);
} else if (card.skill === 'reinforce') {
for (let i = 0; i < 2; i++) drawCard('enemy');
}
render();
setTimeout(() => enemyPlayCards(), 500);
return;
}
if (game.enemyBack.length >= MAX_BACK) {
setTimeout(() => enemyMovePhase(), 300);
return;
}
game.enemyKredit -= card.cost;
game.enemyHand.splice(pick.i, 1);
const newUnit = { ...card, owner: 'enemy', currentDefense: card.defense, canAttack: false, canMove: true, image: card.factionImage, lane: 'back' };
game.enemyBack.push(newUnit);
showFloatText('敌军部署:' + card.name, '#ff3d71', document.getElementById('enemyBackRow'));
render();
setTimeout(() => enemyPlayCards(), 400);
return;
}
setTimeout(() => enemyMovePhase(), 300);
}
function enemyMovePhase() {
if (game.playerHQ <= 0 || game.enemyHQ <= 0) { setTimeout(() => enemyAttackPhase(), 300); return; }
if (game.frontOwner === 'player') { setTimeout(() => enemyAttackPhase(), 300); return; }
if (game.frontLine.some(u => u.owner === 'player')) { setTimeout(() => enemyAttackPhase(), 300); return; }
if (game.frontLine.length >= MAX_FRONT) { setTimeout(() => enemyAttackPhase(), 300); return; }
const movable = game.enemyBack.filter(u => u.canMove && u.moveCost <= game.enemyKredit);
if (movable.length === 0) { setTimeout(() => enemyAttackPhase(), 300); return; }
movable.sort((a, b) => b.attack - a.attack);
const u = movable[0];
game.enemyKredit -= u.moveCost;
const idx = game.enemyBack.indexOf(u);
if (idx >= 0) game.enemyBack.splice(idx, 1);
u.lane = 'front';
u.canMove = false;
u.canAttack = false;
game.frontLine.push(u);
game.frontOwner = 'enemy';
const el = findUnitEl(u);
if (el) {
const r = el.getBoundingClientRect();
spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ff3d71', 10);
}
render();
setTimeout(() => enemyMovePhase(), 600);
}
function enemyAttackPhase() {
if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
const allAttackers = [
...game.frontLine.filter(u => u.owner === 'enemy' && u.canAttack).map(u => ({ u, lane: 'front' })),
...game.enemyBack.filter(u => u.canAttack).map(u => ({ u, lane: 'back' })),
];
if (allAttackers.length === 0) { setTimeout(endEnemyTurn, 300); return; }
const pick = allAttackers[0];
const attacker = pick.u;
attacker.canAttack = false;
let targetInfo = null;
if (pick.lane === 'front') {
const myFront = game.frontLine.filter(u => u.owner === 'player');
if (myFront.length > 0) {
let best = myFront.find(u => u.currentDefense <= attacker.attack) || myFront[0];
targetInfo = { unit: best };
} else if (game.playerBack.length > 0 && Math.random() < 0.6) {
targetInfo = { unit: game.playerBack[0] };
} else {
targetInfo = { hq: true };
}
} else {
const myFront = game.frontLine.filter(u => u.owner === 'player');
if (myFront.length > 0) {
let best = myFront.find(u => u.currentDefense <= attacker.attack) || myFront[0];
targetInfo = { unit: best };
}
}
if (targetInfo && targetInfo.hq) {
game.playerHQ -= attacker.attack;
showFloatText(`-${attacker.attack}`, '#ffb000', document.getElementById('playerHQ'));
spawnSparks(window.innerWidth / 2, window.innerHeight - 80, '#ffb000', 15);
shakeScreen();
render();
checkWin();
setTimeout(() => enemyAttackPhase(), 400);
return;
}
if (targetInfo && targetInfo.unit) {
const def = targetInfo.unit;
const atkEl = findUnitEl(attacker);
const defEl = findUnitEl(def);
if (atkEl) atkEl.classList.add('attacking');
if (defEl) defEl.classList.add('hit');
if (atkEl && defEl) {
const ar = atkEl.getBoundingClientRect(), dr = defEl.getBoundingClientRect();
drawStrikeLine(ar.left + ar.width / 2, ar.top + ar.height / 2, dr.left + dr.width / 2, dr.top + dr.height / 2);
}
def.currentDefense -= attacker.attack;
attacker.currentDefense -= def.attack;
if (defEl) { showFloatText(`-${attacker.attack}`, '#ffb000', defEl); const r = defEl.getBoundingClientRect(); spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ff3d71', 12); }
if (atkEl) showFloatText(`-${def.attack}`, '#ff3d71', atkEl);
shakeScreen();
render();
setTimeout(() => {
game.frontLine = game.frontLine.filter(u => u.currentDefense > 0);
game.playerBack = game.playerBack.filter(u => u.currentDefense > 0);
game.enemyBack = game.enemyBack.filter(u => u.currentDefense > 0);
updateFrontOwner();
render();
checkWin();
setTimeout(() => enemyAttackPhase(), 400);
}, 400);
return;
}
setTimeout(() => enemyAttackPhase(), 200);
}
function endEnemyTurn() {
if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
game.turn = 'player';
game.playerKredit = Math.min(game.maxKredit, game.playerKredit + 1);
drawCard('player');
game.frontLine.forEach(u => { if (u.owner === 'player') u.canAttack = true; });
game.playerBack.forEach(u => { u.canAttack = true; u.canMove = true; });
render();
}
/* ============ 胜负 ============ */
function checkWin() {
if (game.playerHQ <= 0) { showOverlay('lose'); return true; }
if (game.enemyHQ <= 0) { showOverlay('win'); return true; }
return false;
}
function showOverlay(type) {
game.busy = true;
const overlay = document.createElement('div');
overlay.className = 'overlay';
const win = type === 'win';
overlay.innerHTML = `
<div class="overlay-title ${type}">${win ? '胜 利' : '失 败'}</div>
<div class="overlay-sub">${win ? allyFactions[0].name + ' 军团突破了敌军防线' : enemyFactions[0].name + ' 军团攻陷了我军指挥部'}</div>
<button class="overlay-btn" onclick="location.reload()">再 战 一 局</button>
`;
document.body.appendChild(overlay);
}
/* ============ 提示 ============ */
function toast(msg) {
const t = document.createElement('div');
t.className = 'toast';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 1600);
}
function showFloatText(text, color, targetEl) {
if (!targetEl) return;
const r = targetEl.getBoundingClientRect();
const span = document.createElement('div');
span.className = 'float-text';
span.textContent = text;
span.style.color = color;
span.style.left = (r.left + r.width / 2 + (Math.random() - 0.5) * 30) + 'px';
span.style.top = (r.top + r.height / 2 - 20) + 'px';
document.body.appendChild(span);
setTimeout(() => span.remove(), 1000);
}
/* ============ 动画样式 ============ */
const style = document.createElement('style');
style.textContent = `
@keyframes dashIn { from { opacity: 0; stroke-dashoffset: 40; } to { opacity: 1; stroke-dashoffset: 0; } }
@keyframes ringPulse { 0%, 100% { r: 16; opacity: 0.8; } 50% { r: 22; opacity: 0.4; } }
`;
document.head.appendChild(style);
buildFactionConfig();
</script>
</body>
</html>Game Source: 钢铁前线 - KARDS规则版
Creator: HyperShark77
Libraries: none
Complexity: complex (1389 lines, 63.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-hypershark77-muv62wcc" to link back to the original. Then publish at arcadelab.ai/publish.