Upload ag1.html with huggingface_hub
Browse files
ag1.html
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-Hant">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>天使帝國 · 瓦爾哈拉防衛戰 | 第一關</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root{
|
| 9 |
+
--gold:#f5d76e; --gold-d:#b8860b; --bg:#0d1023; --panel:#1a2040;
|
| 10 |
+
--blue:#4da6ff; --red:#ff6b6b; --green:#7bed6f;
|
| 11 |
+
}
|
| 12 |
+
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
| 13 |
+
body{
|
| 14 |
+
margin:0;background:radial-gradient(1200px 600px at 50% -10%,#2a3570 0%,#0d1023 60%) fixed,var(--bg);
|
| 15 |
+
color:#eef;font-family:"Noto Sans TC","PingFang TC","Microsoft JhengHei",sans-serif;
|
| 16 |
+
display:flex;flex-direction:column;align-items:center;min-height:100vh;padding:10px;
|
| 17 |
+
}
|
| 18 |
+
h1{margin:8px 0 2px;font-size:22px;letter-spacing:4px;color:var(--gold);text-shadow:0 0 12px #b8860b88,2px 2px 0 #000}
|
| 19 |
+
.sub{font-size:12px;opacity:.8;letter-spacing:2px;margin-bottom:8px}
|
| 20 |
+
#wrap{width:100%;max-width:860px}
|
| 21 |
+
#story{
|
| 22 |
+
background:linear-gradient(180deg,#232a55,#141936);border:2px solid var(--gold-d);
|
| 23 |
+
border-radius:12px;padding:14px 16px;margin-bottom:10px;box-shadow:0 4px 24px #000a;
|
| 24 |
+
}
|
| 25 |
+
#story h2{margin:0 0 6px;font-size:16px;color:var(--gold)}
|
| 26 |
+
#story p{font-size:14px;line-height:1.7;margin:6px 0;min-height:86px}
|
| 27 |
+
#storyBtnRow{display:flex;gap:8px;justify-content:flex-end;margin-top:6px}
|
| 28 |
+
button{
|
| 29 |
+
background:linear-gradient(180deg,#f5d76e,#c9982a);border:1px solid #7a5c00;color:#2a2000;
|
| 30 |
+
font-weight:700;border-radius:8px;padding:8px 16px;font-size:14px;cursor:pointer;
|
| 31 |
+
}
|
| 32 |
+
button.ghost{background:#2a3270;color:#fff;border-color:#4a55aa}
|
| 33 |
+
button:disabled{opacity:.4;cursor:not-allowed}
|
| 34 |
+
button:active{transform:translateY(1px)}
|
| 35 |
+
#hud{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
|
| 36 |
+
.chip{background:#141936cc;border:1px solid #4a55aa;border-radius:20px;padding:4px 12px;font-size:13px}
|
| 37 |
+
.chip b{color:var(--gold)}
|
| 38 |
+
#main{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
|
| 39 |
+
#boardBox{background:#0a0d20;border:2px solid var(--gold-d);border-radius:12px;padding:8px;box-shadow:0 4px 24px #000}
|
| 40 |
+
#board{
|
| 41 |
+
display:grid;grid-template-columns:repeat(10,1fr);gap:2px;
|
| 42 |
+
width:min(92vw,560px);aspect-ratio:10/8;user-select:none;
|
| 43 |
+
}
|
| 44 |
+
.cell{
|
| 45 |
+
position:relative;border-radius:6px;display:flex;align-items:center;justify-content:center;
|
| 46 |
+
font-size:clamp(18px,4.5vw,30px);cursor:pointer;transition:transform .08s;
|
| 47 |
+
border:1px solid #0006;
|
| 48 |
+
}
|
| 49 |
+
.cell.plain{background:linear-gradient(180deg,#5da24d,#3d7a32)}
|
| 50 |
+
.cell.forest{background:linear-gradient(180deg,#2e7d46,#1a4d2a)}
|
| 51 |
+
.cell.mountain{background:linear-gradient(180deg,#8a8f9a,#54575f)}
|
| 52 |
+
.cell.castle{background:linear-gradient(180deg,#c9a86a,#8a6d35)}
|
| 53 |
+
.cell.road{background:linear-gradient(180deg,#a99a6b,#7a6e45)}
|
| 54 |
+
.cell.moveable::after{content:"";position:absolute;inset:18%;background:#4da6ffaa;border-radius:6px;border:2px solid #fff}
|
| 55 |
+
.cell.attackable::after{content:"";position:absolute;inset:12%;background:#ff3b3baa;border-radius:6px;border:2px dashed #fff;animation:pulse .8s infinite alternate}
|
| 56 |
+
.cell.healable::after{content:"";position:absolute;inset:12%;background:#43d17caa;border-radius:6px;border:2px dashed #fff}
|
| 57 |
+
.cell.selected{outline:3px solid var(--gold);z-index:2}
|
| 58 |
+
.cell .u{filter:drop-shadow(1px 2px 1px #000);line-height:1;z-index:1}
|
| 59 |
+
.cell .hpbar{position:absolute;left:2px;right:2px;bottom:2px;height:5px;background:#000a;border-radius:3px;overflow:hidden;z-index:2}
|
| 60 |
+
.cell .hpbar i{display:block;height:100%;background:linear-gradient(90deg,#43d17c,#b6ff5e)}
|
| 61 |
+
.cell .hpbar.low i{background:linear-gradient(90deg,#ff3b3b,#ff9a3b)}
|
| 62 |
+
.cell .side-dot{position:absolute;top:2px;left:3px;width:8px;height:8px;border-radius:50%;z-index:2}
|
| 63 |
+
.cell .terrain-tag{position:absolute;top:1px;right:2px;font-size:9px;opacity:.85;z-index:0}
|
| 64 |
+
.cell.acted .u{filter:grayscale(.8) brightness(.6)}
|
| 65 |
+
@keyframes pulse{from{opacity:.6}to{opacity:1}}
|
| 66 |
+
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
|
| 67 |
+
.shake{animation:shake .3s}
|
| 68 |
+
@keyframes floatDmg{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-28px)}}
|
| 69 |
+
.dmgFloat{position:absolute;top:2px;left:50%;transform:translateX(-50%);font-weight:900;font-size:18px;color:#ffeb3b;text-shadow:1px 1px 0 #000,0 0 8px #f00;animation:floatDmg 1s forwards;z-index:9;pointer-events:none}
|
| 70 |
+
.dmgFloat.heal{color:#7bff9e}
|
| 71 |
+
#side{background:#141936;border:2px solid #4a55aa;border-radius:12px;padding:10px;width:min(92vw,260px)}
|
| 72 |
+
#side h3{margin:2px 0 8px;font-size:14px;color:var(--gold);letter-spacing:2px}
|
| 73 |
+
#unitCard{background:#0f1430;border:1px solid #3a4488;border-radius:8px;padding:8px;font-size:13px;line-height:1.6;min-height:150px}
|
| 74 |
+
#unitCard .big{font-size:28px}
|
| 75 |
+
.stat{display:flex;justify-content:space-between}
|
| 76 |
+
.bar{height:8px;background:#000;border-radius:4px;overflow:hidden;margin:3px 0 6px}
|
| 77 |
+
.bar i{display:block;height:100%;background:linear-gradient(90deg,#ff5f6d,#ffc371)}
|
| 78 |
+
.bar.exp i{background:linear-gradient(90deg,#4da6ff,#b174ff)}
|
| 79 |
+
#menu{margin-top:8px;display:flex;gap:6px;flex-wrap:wrap}
|
| 80 |
+
#log{background:#0a0d20;border:1px solid #3a4488;border-radius:8px;height:150px;overflow-y:auto;font-size:12.5px;line-height:1.6;padding:8px;margin-top:8px}
|
| 81 |
+
#log div{border-bottom:1px dotted #ffffff18;padding:2px 0}
|
| 82 |
+
#controls{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
|
| 83 |
+
#overlay{
|
| 84 |
+
position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;
|
| 85 |
+
z-index:50;padding:16px;
|
| 86 |
+
}
|
| 87 |
+
#overlay .card{background:linear-gradient(180deg,#232a55,#0f1430);border:2px solid var(--gold);border-radius:16px;padding:24px;max-width:440px;text-align:center;box-shadow:0 0 40px #b8860b66}
|
| 88 |
+
#overlay h2{color:var(--gold);letter-spacing:4px}
|
| 89 |
+
#overlay p{line-height:1.8;font-size:14px}
|
| 90 |
+
.hidden{display:none!important}
|
| 91 |
+
.legend{font-size:11px;opacity:.85;line-height:1.8;margin-top:8px}
|
| 92 |
+
kbd{background:#000;border:1px solid #666;border-radius:4px;padding:0 5px;font-size:11px}
|
| 93 |
+
</style>
|
| 94 |
+
</head>
|
| 95 |
+
<body>
|
| 96 |
+
<div id="wrap">
|
| 97 |
+
<h1>⚔️ 天使帝國 · 網頁版 ⚔️</h1>
|
| 98 |
+
<div class="sub">第一關 —— 瓦爾哈拉防衛戰 | 向1993年大宇經典致敬的同人重製</div>
|
| 99 |
+
|
| 100 |
+
<div id="story">
|
| 101 |
+
<h2>📜 序章</h2>
|
| 102 |
+
<p id="storyText"></p>
|
| 103 |
+
<div id="storyBtnRow">
|
| 104 |
+
<button class="ghost" id="skipStory">跳過劇情 ⏩</button>
|
| 105 |
+
<button id="nextStory">繼續 ▸</button>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<div id="hud">
|
| 110 |
+
<div class="chip">回合 <b id="turn">1</b></div>
|
| 111 |
+
<div class="chip">階段 <b id="phase">我軍行動</b></div>
|
| 112 |
+
<div class="chip">敵軍殘存 <b id="enemyCount">5</b></div>
|
| 113 |
+
<div class="chip">我軍 <b id="allyCount">4</b></div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div id="main">
|
| 117 |
+
<div id="boardBox">
|
| 118 |
+
<div id="board"></div>
|
| 119 |
+
<div id="controls">
|
| 120 |
+
<button id="endTurn">🔔 結束回合</button>
|
| 121 |
+
<button class="ghost" id="restart">↺ 重新開始</button>
|
| 122 |
+
<button class="ghost" id="howto">❓ 玩法</button>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
<div id="side">
|
| 126 |
+
<h3>▌ 部隊資訊</h3>
|
| 127 |
+
<div id="unitCard">點選我軍單位開始行動。<br>藍色格可移動,紅色格可攻擊。</div>
|
| 128 |
+
<div id="menu"></div>
|
| 129 |
+
<div id="log"></div>
|
| 130 |
+
<div class="legend">
|
| 131 |
+
地形:🌿草原 �🌲森林(迴避+防禦) ⛰️山(不可進入) 🏰城砦(防禦++) 🛣️道路<br>
|
| 132 |
+
操作:點藍格移動 → 選攻擊/治療/待機 → 點紅格目標<br>
|
| 133 |
+
僧侶可治療綠格友軍。主角 <b>愛琳</b> 陣亡即失敗。
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<div id="overlay" class="hidden"><div class="card"><h2 id="ovTitle"></h2><p id="ovText"></p><button id="ovBtn">再戰一關</button></div></div>
|
| 140 |
+
|
| 141 |
+
<script>
|
| 142 |
+
// ============ 音效 (WebAudio 合成, 無需外部檔) ============
|
| 143 |
+
let AC=null;
|
| 144 |
+
function beep(f,d=0.12,type='square',v=0.12){try{AC=AC||new (window.AudioContext||window.webkitAudioContext)();const o=AC.createOscillator(),g=AC.createGain();o.type=type;o.frequency.value=f;g.gain.value=v;o.connect(g);g.connect(AC.destination);o.start();g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+d);o.stop(AC.currentTime+d);}catch(e){}}
|
| 145 |
+
const sMove=()=>{beep(440,.08);setTimeout(()=>beep(660,.08),70)};
|
| 146 |
+
const sHit=()=>{beep(160,.18,'sawtooth',.18);setTimeout(()=>beep(90,.22,'sawtooth',.18),60)};
|
| 147 |
+
const sMiss=()=>beep(800,.15,'sine',.1);
|
| 148 |
+
const sHeal=()=>{[523,659,784].forEach((f,i)=>setTimeout(()=>beep(f,.15,'sine'),i*90))};
|
| 149 |
+
const sWin=()=>{[523,659,784,1046,1318].forEach((f,i)=>setTimeout(()=>beep(f,.25),i*140))};
|
| 150 |
+
const sLose=()=>{[400,350,300,200].forEach((f,i)=>setTimeout(()=>beep(f,.3,'sawtooth'),i*180))};
|
| 151 |
+
|
| 152 |
+
// ============ 地圖 10x8 ============
|
| 153 |
+
// .:草原 F:森林 M:山 C:城砦 R:道路
|
| 154 |
+
const MAP=[
|
| 155 |
+
"....FF....",
|
| 156 |
+
"..FFFF..C.",
|
| 157 |
+
".FF..RRR..",
|
| 158 |
+
"..F.RRR.F.",
|
| 159 |
+
".M.RRC.RM.",
|
| 160 |
+
"..F.RRR.F.",
|
| 161 |
+
".FF..RR...",
|
| 162 |
+
"..FFFF....",
|
| 163 |
+
];
|
| 164 |
+
const W=10,H=8;
|
| 165 |
+
function terrainAt(x,y){return MAP[y][x];}
|
| 166 |
+
function terrainName(t){return {'.':'🌿草原',F:'🌲森林',M:'⛰️高山',C:'🏰城砦',R:'🛣️道路'}[t]}
|
| 167 |
+
function moveCost(t){return t==='M'?99 : t==='F'?2 : 1}
|
| 168 |
+
function defBonus(t){return t==='C'?0.55 : t==='F'?0.75 : 1}
|
| 169 |
+
function evaBonus(t){return t==='C'?25 : t==='F'?20 : 0}
|
| 170 |
+
|
| 171 |
+
// ============ 部隊 ============
|
| 172 |
+
function baseUnits(){return [
|
| 173 |
+
{id:'erin', side:'P', name:'愛琳', title:'女王近衛劍士', icon:'👸', x:3,y:6, maxhp:34, hp:34, atk:13, def:8, mov:4, rng:[1,1], exp:0, lv:1, hero:true},
|
| 174 |
+
{id:'leah', side:'P', name:'莉亞', title:'精靈弓箭手', icon:'🧝', x:4,y:6, maxhp:26, hp:26, atk:11, def:4, mov:4, rng:[1,2]},
|
| 175 |
+
{id:'mia', side:'P', name:'米雅', title:'火焰魔法師', icon:'🧙', x:5,y:6, maxhp:22, hp:22, atk:15, def:3, mov:3, rng:[1,2], magic:true},
|
| 176 |
+
{id:'jenny',side:'P', name:'珍妮', title:'見習僧侶', icon:'👼', x:6,y:6, maxhp:24, hp:24, atk:7, def:5, mov:3, rng:[1,2], healer:true},
|
| 177 |
+
{id:'sk1', side:'E', name:'骷髏戰士', title:'不死前鋒', icon:'💀', x:2,y:1, maxhp:22, hp:22, atk:9, def:5, mov:3, rng:[1,1], expV:15},
|
| 178 |
+
{id:'sk2', side:'E', name:'骷髏戰士', title:'不死前鋒', icon:'💀', x:7,y:1, maxhp:22, hp:22, atk:9, def:5, mov:3, rng:[1,1], expV:15},
|
| 179 |
+
{id:'gob', side:'E', name:'哥���林射手', title:'荒野斥候', icon:'👺', x:4,y:1, maxhp:18, hp:18, atk:9, def:3, mov:4, rng:[1,2], expV:14},
|
| 180 |
+
{id:'sho', side:'E', name:'暗黑斥候', title:'影之牙', icon:'👹', x:5,y:2, maxhp:20, hp:20, atk:10, def:4, mov:4, rng:[1,1], expV:16},
|
| 181 |
+
{id:'boss',side:'E', name:'屍龍術士 莫戈', title:'第一關首領', icon:'🧛', x:4,y:0, maxhp:34, hp:34, atk:13, def:6, mov:3, rng:[1,2], magic:true, expV:30, boss:true},
|
| 182 |
+
];}
|
| 183 |
+
|
| 184 |
+
let units, turn, phase, selected, moveTiles, atkTiles, healTiles, pendingMove, gameOver, busy;
|
| 185 |
+
const board=document.getElementById('board'), card=document.getElementById('unitCard'),
|
| 186 |
+
menu=document.getElementById('menu'), logEl=document.getElementById('log');
|
| 187 |
+
|
| 188 |
+
function log(m){const d=document.createElement('div');d.innerHTML=m;logEl.prepend(d);}
|
| 189 |
+
function unitAt(x,y){return units.find(u=>u.hp>0&&u.x===x&&u.y===y)}
|
| 190 |
+
function alive(side){return units.filter(u=>u.side===side&&u.hp>0)}
|
| 191 |
+
|
| 192 |
+
function init(){
|
| 193 |
+
units=baseUnits(); turn=1; phase='P'; selected=null; moveTiles=[]; atkTiles=[]; healTiles=[]; pendingMove=null; gameOver=false; busy=false;
|
| 194 |
+
units.forEach(u=>{u.moved=false;u.acted=false;u.lv=u.lv||1;u.exp=u.exp||0;});
|
| 195 |
+
document.getElementById('turn').textContent=turn;
|
| 196 |
+
document.getElementById('phase').textContent='我軍行動';
|
| 197 |
+
logEl.innerHTML='';
|
| 198 |
+
log('⚔️ 第一關開始:消滅所有敵軍!保護 <b>愛琳</b>!');
|
| 199 |
+
log('僧侶珍妮 👼 可以治療鄰近友軍。');
|
| 200 |
+
render();
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
function render(){
|
| 204 |
+
document.getElementById('enemyCount').textContent=alive('E').length;
|
| 205 |
+
document.getElementById('allyCount').textContent=alive('P').length;
|
| 206 |
+
board.innerHTML='';
|
| 207 |
+
for(let y=0;y<H;y++)for(let x=0;x<W;x++){
|
| 208 |
+
const t=terrainAt(x,y);
|
| 209 |
+
const c=document.createElement('div');
|
| 210 |
+
c.className='cell '+({'.':'plain',F:'forest',M:'mountain',C:'castle',R:'road'}[t]);
|
| 211 |
+
c.dataset.x=x;c.dataset.y=y;
|
| 212 |
+
const tag=document.createElement('span');tag.className='terrain-tag';
|
| 213 |
+
tag.textContent=t==='F'?'🌲':t==='M'?'⛰️':t==='C'?'🏰':'';
|
| 214 |
+
c.appendChild(tag);
|
| 215 |
+
if(moveTiles.some(m=>m.x===x&&m.y===y))c.classList.add('moveable');
|
| 216 |
+
if(atkTiles.some(m=>m.x===x&&m.y===y))c.classList.add('attackable');
|
| 217 |
+
if(healTiles.some(m=>m.x===x&&m.y===y))c.classList.add('healable');
|
| 218 |
+
const u=unitAt(x,y);
|
| 219 |
+
if(u){
|
| 220 |
+
const s=document.createElement('span');s.className='u';s.textContent=u.icon;c.appendChild(s);
|
| 221 |
+
const d=document.createElement('span');d.className='side-dot';
|
| 222 |
+
d.style.background=u.side==='P'?'#4da6ff':'#ff4d4d';c.appendChild(d);
|
| 223 |
+
const hb=document.createElement('div');hb.className='hpbar'+(u.hp/u.maxhp<0.35?' low':'');
|
| 224 |
+
const i=document.createElement('i');i.style.width=Math.max(0,u.hp/u.maxhp*100)+'%';hb.appendChild(i);c.appendChild(hb);
|
| 225 |
+
if(u.acted&&u.side==='P')c.classList.add('acted');
|
| 226 |
+
if(selected===u)c.classList.add('selected');
|
| 227 |
+
}
|
| 228 |
+
c.onclick=()=>onCell(x,y);
|
| 229 |
+
c.onmouseenter=()=>{const uu=unitAt(x,y); if(uu) showCard(uu);};
|
| 230 |
+
board.appendChild(c);
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
function showCard(u){
|
| 235 |
+
card.innerHTML=`<span class="big">${u.icon}</span> <b>${u.name}</b> <span style="opacity:.7">Lv${u.lv} · ${u.title||''}</span>
|
| 236 |
+
<span style="float:right;color:${u.side==='P'?'#4da6ff':'#ff6b6b'}">${u.side==='P'?'【我軍】':'【敵軍】'}</span>
|
| 237 |
+
<div class="stat"><span>HP ${Math.max(0,u.hp)}/${u.maxhp}</span><span>EXP ${u.exp||0}/30</span></div>
|
| 238 |
+
<div class="bar"><i style="width:${u.hp/u.maxhp*100}%"></i></div>
|
| 239 |
+
<div class="stat"><span>⚔️攻擊 ${u.atk}</span><span>🛡️防禦 ${u.def}</span></div>
|
| 240 |
+
<div class="stat"><span>👟移動 ${u.mov}</span><span>🏹射程 ${u.rng[0]}-${u.rng[1]}</span></div>
|
| 241 |
+
<div style="opacity:.75">地形:${terrainName(terrainAt(u.x,u.y))}</div>`;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// ---- 移動範圍 BFS ----
|
| 245 |
+
function calcMove(u){
|
| 246 |
+
const dist=Array.from({length:H},()=>Array(W).fill(99));
|
| 247 |
+
dist[u.y][u.x]=0;const q=[[u.x,u.y]];
|
| 248 |
+
while(q.length){const [x,y]=q.shift();
|
| 249 |
+
for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){
|
| 250 |
+
const nx=x+dx,ny=y+dy;
|
| 251 |
+
if(nx<0||ny<0||nx>=W||ny>=H)continue;
|
| 252 |
+
const c=moveCost(terrainAt(nx,ny));if(c>=99)continue;
|
| 253 |
+
const occ=unitAt(nx,ny);if(occ&&occ!==u)continue;
|
| 254 |
+
if(dist[ny][nx]>dist[y][x]+c){dist[ny][nx]=dist[y][x]+c;q.push([nx,ny]);}
|
| 255 |
+
}}
|
| 256 |
+
const out=[];for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(!(x===u.x&&y===u.y)&&dist[y][x]<=u.mov)out.push({x,y});
|
| 257 |
+
return out;
|
| 258 |
+
}
|
| 259 |
+
function tilesInRange(x,y,r0,r1,side,targetSide){
|
| 260 |
+
const out=[];
|
| 261 |
+
for(let yy=0;yy<H;yy++)for(let xx=0;xx<W;xx++){
|
| 262 |
+
const d=Math.abs(xx-x)+Math.abs(yy-y);
|
| 263 |
+
if(d<r0||d>r1)continue;
|
| 264 |
+
const t=unitAt(xx,yy);
|
| 265 |
+
if(targetSide==='enemy'&&t&&t.side!==side&&t.hp>0)out.push({x:xx,y:yy});
|
| 266 |
+
if(targetSide==='ally'&&t&&t.side===side&&t.hp>0&&t.hp<t.maxhp&&!(xx===x&&yy===y))out.push({x:xx,y:yy});
|
| 267 |
+
}
|
| 268 |
+
return out;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
// ---- 點擊 ----
|
| 272 |
+
function onCell(x,y){
|
| 273 |
+
if(gameOver||busy||phase!=='P')return;
|
| 274 |
+
const u=unitAt(x,y);
|
| 275 |
+
// 1. 有待命中攻擊目標?
|
| 276 |
+
if(selected&&pendingMove==='attack'){
|
| 277 |
+
if(atkTiles.some(t=>t.x===x&&t.y===y)){doAttack(selected,u);return;}
|
| 278 |
+
}
|
| 279 |
+
if(selected&&pendingMove==='heal'){
|
| 280 |
+
if(healTiles.some(t=>t.x===x&&t.y===y)){doHeal(selected,u);return;}
|
| 281 |
+
else {pendingMove=null;showActions(selected);render();return;}
|
| 282 |
+
}
|
| 283 |
+
// 2. 點移動格?
|
| 284 |
+
if(selected&&!selected.moved&&moveTiles.some(t=>t.x===x&&t.y===y)){
|
| 285 |
+
selected.x=x;selected.y=y;selected.moved=true;sMove();
|
| 286 |
+
log(`${selected.icon} ${selected.name} 移動至 (${x},${y})`);
|
| 287 |
+
moveTiles=[];pendingMove=null;showActions(selected);render();showCard(selected);return;
|
| 288 |
+
}
|
| 289 |
+
// 3. 選取我軍?
|
| 290 |
+
if(u&&u.side==='P'&&!u.acted){
|
| 291 |
+
selected=u;pendingMove=null;
|
| 292 |
+
moveTiles=u.moved?[]:calcMove(u);
|
| 293 |
+
atkTiles=[];healTiles=[];
|
| 294 |
+
showCard(u);renderMenuHint(u);render();
|
| 295 |
+
return;
|
| 296 |
+
}
|
| 297 |
+
if(u&&u.side==='P'&&u.acted){showCard(u);menu.innerHTML='<span style="font-size:12px;opacity:.7">該單位已行動完畢。</span>';return;}
|
| 298 |
+
if(u)showCard(u);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
function renderMenuHint(u){
|
| 302 |
+
menu.innerHTML=`<span style="font-size:12px;opacity:.8">已選擇 ${u.icon}${u.name}:點藍色格移動,或直接選行動。</span>`;
|
| 303 |
+
const m=document.createElement('div');m.style.cssText='display:flex;gap:6px;margin-top:6px;flex-wrap:wrap';
|
| 304 |
+
const bA=document.createElement('button');bA.textContent='⚔️ 攻擊';bA.style.padding='6px 10px';
|
| 305 |
+
bA.onclick=()=>showActions(u,'attack');
|
| 306 |
+
const bW=document.createElement('button');bW.textContent='待機 ⏳';bW.className='ghost';bW.style.padding='6px 10px';
|
| 307 |
+
bW.onclick=()=>{finishUnit(u);log(`${u.icon} ${u.name} 待機。`)};
|
| 308 |
+
m.appendChild(bA);
|
| 309 |
+
if(u.healer){const bH=document.createElement('button');bH.textContent='💚 治療';bH.style.padding='6px 10px';bH.onclick=()=>showActions(u,'heal');m.appendChild(bH);}
|
| 310 |
+
m.appendChild(bW);menu.appendChild(m);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
function showActions(u,force){
|
| 314 |
+
atkTiles=tilesInRange(u.x,u.y,u.rng[0],u.rng[1],u.side,'enemy');
|
| 315 |
+
healTiles=u.healer?tilesInRange(u.x,u.y,1,2,u.side,'ally'):[];
|
| 316 |
+
menu.innerHTML='';
|
| 317 |
+
const mk=(label,fn,cls='')=>{const b=document.createElement('button');b.textContent=label;b.style.padding='6px 10px';if(cls)b.className=cls;b.onclick=fn;menu.appendChild(b);};
|
| 318 |
+
if(force==='attack'||(!force&&!u.healer)){
|
| 319 |
+
if(atkTiles.length){pendingMove='attack';mk(`⚔️ 攻擊 (${atkTiles.length}個目標)`,()=>{render();});}
|
| 320 |
+
else{pendingMove=null;mk('⚔️ 無可攻擊目標',()=>{},{disabled:1});}
|
| 321 |
+
}
|
| 322 |
+
if(u.healer&&(force==='heal'||!force)){
|
| 323 |
+
if(force==='heal'){pendingMove='heal';if(!healTiles.length)log('附近沒有需要治療的友軍。');}
|
| 324 |
+
}
|
| 325 |
+
if(!force){
|
| 326 |
+
if(u.healer&&healTiles.length)mk(`💚 治療 (${healTiles.length})`,()=>showActions(u,'heal'));
|
| 327 |
+
mk('待機 ⏳',()=>{finishUnit(u);log(`${u.icon} ${u.name} 待機。`)},'ghost');
|
| 328 |
+
} else if(force==='attack'){
|
| 329 |
+
mk('取消',()=>{pendingMove=null;atkTiles=[];showActions(u)},'ghost');
|
| 330 |
+
} else if(force==='heal'){
|
| 331 |
+
mk('取消',()=>{pendingMove=null;healTiles=[];showActions(u)},'ghost');
|
| 332 |
+
}
|
| 333 |
+
if(force==='attack'&&!atkTiles.length){pendingMove=null;}
|
| 334 |
+
render();
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
function finishUnit(u){u.acted=true;u.moved=true;selected=null;moveTiles=[];atkTiles=[];healTiles=[];menu.innerHTML='';checkAutoEnd();render();}
|
| 338 |
+
|
| 339 |
+
// ---- 戰鬥 ----
|
| 340 |
+
function calcDamage(att,def){
|
| 341 |
+
const t=terrainAt(def.x,def.y);
|
| 342 |
+
let base=att.magic? att.atk - def.def*0.35 : att.atk - def.def*0.6;
|
| 343 |
+
base*=defBonus(t);
|
| 344 |
+
base+=Math.floor(Math.random()*5)-2;
|
| 345 |
+
return Math.max(1,Math.round(base));
|
| 346 |
+
}
|
| 347 |
+
function hitChance(att,def){
|
| 348 |
+
return Math.max(55, 92 - evaBonus(terrainAt(def.x,def.y)) + (att.lv-def.lv)*3);
|
| 349 |
+
}
|
| 350 |
+
function floatText(x,y,txt,heal=false){
|
| 351 |
+
const idx=y*W+x;
|
| 352 |
+
const cell=board.children[idx];if(!cell)return;
|
| 353 |
+
const d=document.createElement('div');d.className='dmgFloat'+(heal?' heal':'');d.textContent=txt;
|
| 354 |
+
cell.appendChild(d);setTimeout(()=>d.remove(),1000);
|
| 355 |
+
}
|
| 356 |
+
function gainExp(u,n){
|
| 357 |
+
if(u.side!=='P'||u.hp<=0)return;
|
| 358 |
+
u.exp=(u.exp||0)+n;
|
| 359 |
+
if(u.exp>=30){u.exp-=30;u.lv++;u.maxhp+=4;u.hp=Math.min(u.maxhp,u.hp+4);u.atk+=2;u.def+=1;
|
| 360 |
+
log(`✨ ${u.icon} ${u.name} 升級!Lv${u.lv}(攻擊+2 防禦+1 HP+4)`);beep(880,.2,'sine');}
|
| 361 |
+
}
|
| 362 |
+
function doAttack(att,def){
|
| 363 |
+
if(!def||def.hp<=0)return;
|
| 364 |
+
busy=true;
|
| 365 |
+
const ch=hitChance(att,def);
|
| 366 |
+
const roll=Math.random()*100;
|
| 367 |
+
const idx=def.y*W+def.x;
|
| 368 |
+
if(roll<ch){
|
| 369 |
+
const dmg=calcDamage(att,def);
|
| 370 |
+
def.hp-=dmg;sHit();
|
| 371 |
+
if(board.children[idx]){board.children[idx].classList.add('shake');setTimeout(()=>board.children[idx]&&board.children[idx].classList.remove('shake'),320);}
|
| 372 |
+
floatText(def.x,def.y,'-'+dmg);
|
| 373 |
+
log(`${att.icon}${att.name} → ${def.icon}${def.name}:造成 <b>${dmg}</b> 傷害!${att.magic?'🔥':''}`);
|
| 374 |
+
gainExp(att,def.hp<=0? (def.expV||15):8);
|
| 375 |
+
if(def.hp<=0){def.hp=0;log(`☠️ ${def.icon} ${def.name} 被擊破!`);beep(220,.3,'sawtooth');}
|
| 376 |
+
else{
|
| 377 |
+
// 反擊
|
| 378 |
+
const d=Math.abs(att.x-def.x)+Math.abs(att.y-def.y);
|
| 379 |
+
if(d>=def.rng[0]&&d<=def.rng[1]){
|
| 380 |
+
setTimeout(()=>{
|
| 381 |
+
const ch2=hitChance(def,att);
|
| 382 |
+
if(Math.random()*100<ch2){
|
| 383 |
+
const dmg2=calcDamage(def,att);
|
| 384 |
+
att.hp-=dmg2;sHit();floatText(att.x,att.y,'-'+dmg2);
|
| 385 |
+
log(`${def.icon}${def.name} 反擊!${att.icon}${att.name} 受到 <b>${dmg2}</b> 傷害`);
|
| 386 |
+
if(att.hp<=0){att.hp=0;log(`☠️ ${att.icon} ${att.name} 倒下了…`);}
|
| 387 |
+
} else {sMiss();floatText(att.x,att.y,'MISS');log(`${def.icon}${def.name} 反擊…落空!`);}
|
| 388 |
+
afterCombat(att);
|
| 389 |
+
},650);
|
| 390 |
+
render();return;
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
} else {sMiss();floatText(def.x,def.y,'MISS');log(`${att.icon}${att.name} 的攻擊被 ${def.icon}${def.name} 閃開了!`);gainExp(att,2);}
|
| 394 |
+
setTimeout(()=>afterCombat(att),450);
|
| 395 |
+
render();
|
| 396 |
+
}
|
| 397 |
+
function afterCombat(att){
|
| 398 |
+
busy=false;
|
| 399 |
+
att.acted=true;att.moved=true;selected=null;moveTiles=[];atkTiles=[];healTiles=[];menu.innerHTML='';
|
| 400 |
+
render();checkEnd();if(!gameOver)checkAutoEnd();
|
| 401 |
+
}
|
| 402 |
+
function doHeal(healer,target){
|
| 403 |
+
const amt=9+Math.floor(Math.random()*4)+healer.lv;
|
| 404 |
+
target.hp=Math.min(target.maxhp,target.hp+amt);
|
| 405 |
+
sHeal();floatText(target.x,target.y,'+'+amt,true);
|
| 406 |
+
log(`${healer.icon}${healer.name} 治療 ${target.icon}${target.name},恢復 <b style="color:#7bff9e">${amt}</b> HP`);
|
| 407 |
+
gainExp(healer,6);
|
| 408 |
+
busy=false;healer.acted=true;healer.moved=true;selected=null;moveTiles=[];atkTiles=[];healTiles=[];menu.innerHTML='';
|
| 409 |
+
render();checkAutoEnd();
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// ---- 回合 ----
|
| 413 |
+
function checkAutoEnd(){
|
| 414 |
+
if(gameOver)return;
|
| 415 |
+
if(alive('P').every(u=>u.acted)){setTimeout(()=>{if(!gameOver&&phase==='P')endTurn();},600);}
|
| 416 |
+
}
|
| 417 |
+
document.getElementById('endTurn').onclick=()=>{if(phase==='P'&&!busy&&!gameOver)endTurn();};
|
| 418 |
+
function endTurn(){
|
| 419 |
+
selected=null;moveTiles=[];atkTiles=[];healTiles=[];menu.innerHTML='';
|
| 420 |
+
phase='E';document.getElementById('phase').textContent='敵軍行動';render();
|
| 421 |
+
log(`―― 第 ${turn} 回合・敵軍行動 ――`);
|
| 422 |
+
setTimeout(enemyTurn,700);
|
| 423 |
+
}
|
| 424 |
+
function enemyTurn(){
|
| 425 |
+
const foes=alive('E');
|
| 426 |
+
let i=0;
|
| 427 |
+
busy=true;
|
| 428 |
+
function step(){
|
| 429 |
+
if(gameOver){busy=false;return;}
|
| 430 |
+
if(i>=foes.length){
|
| 431 |
+
busy=false;turn++;document.getElementById('turn').textContent=turn;
|
| 432 |
+
phase='P';document.getElementById('phase').textContent='我軍行動';
|
| 433 |
+
units.forEach(u=>{if(u.hp>0&&u.side==='P'){u.moved=false;u.acted=false;}});
|
| 434 |
+
log(`―― 第 ${turn} 回合・我軍行動 ――`);
|
| 435 |
+
render();return;
|
| 436 |
+
}
|
| 437 |
+
const e=foes[i++];
|
| 438 |
+
if(e.hp<=0){step();return;}
|
| 439 |
+
showCard(e);
|
| 440 |
+
// 找最近的玩家
|
| 441 |
+
const ps=alive('P');
|
| 442 |
+
if(!ps.length){step();return;}
|
| 443 |
+
let best=null,bd=1e9;
|
| 444 |
+
ps.forEach(p=>{const d=Math.abs(p.x-e.x)+Math.abs(p.y-e.y);if(d<bd){bd=d;best=p;}});
|
| 445 |
+
// 若已在射程內直接打
|
| 446 |
+
let inR=tilesInRange(e.x,e.y,e.rng[0],e.rng[1],e.side,'enemy');
|
| 447 |
+
if(!inR.length){
|
| 448 |
+
// 靠近:BFS 找可走且離目標最近的格
|
| 449 |
+
const opts=calcMove(e);
|
| 450 |
+
let bx=e.x,by=e.y,bbd=bd;
|
| 451 |
+
opts.forEach(o=>{const d=Math.abs(o.x-best.x)+Math.abs(o.y-best.y);if(d<bbd){bbd=d;bx=o.x;by=o.y;}});
|
| 452 |
+
e.x=bx;e.y=by;render();
|
| 453 |
+
inR=tilesInRange(e.x,e.y,e.rng[0],e.rng[1],e.side,'enemy');
|
| 454 |
+
}
|
| 455 |
+
render();
|
| 456 |
+
setTimeout(()=>{
|
| 457 |
+
if(inR.length){
|
| 458 |
+
// 打最弱的
|
| 459 |
+
inR.sort((a,b)=>{const ua=unitAt(a.x,a.y),ub=unitAt(b.x,b.y);return ua.hp-ub.hp;});
|
| 460 |
+
const t=unitAt(inR[0].x,inR[0].y);
|
| 461 |
+
// 簡化版敵方攻擊(不遞迴 busy)
|
| 462 |
+
const ch=hitChance(e,t);
|
| 463 |
+
if(Math.random()*100<ch){
|
| 464 |
+
const dmg=calcDamage(e,t);t.hp-=dmg;sHit();floatText(t.x,t.y,'-'+dmg);
|
| 465 |
+
const ec=e.y*W+e.x, tc=t.y*W+t.x;
|
| 466 |
+
log(`${e.icon}${e.name} → ${t.icon}${t.name}:<b>${dmg}</b> 傷害!`);
|
| 467 |
+
if(t.hp<=0){t.hp=0;log(`☠️ ${t.icon} ${t.name} 倒下了…`);}
|
| 468 |
+
} else {sMiss();floatText(t.x,t.y,'MISS');log(`${e.icon}${e.name} 攻擊 ${t.icon}${t.name}…落空!`);}
|
| 469 |
+
render();
|
| 470 |
+
if(checkEnd()){busy=false;return;}
|
| 471 |
+
}
|
| 472 |
+
setTimeout(step,650);
|
| 473 |
+
},550);
|
| 474 |
+
}
|
| 475 |
+
step();
|
| 476 |
+
}
|
| 477 |
+
function checkEnd(){
|
| 478 |
+
const erin=units.find(u=>u.id==='erin');
|
| 479 |
+
if(erin.hp<=0){gameOver=true;showOverlay(false,'愛琳倒下了…瓦爾哈拉的光芒熄滅了。');return true;}
|
| 480 |
+
if(!alive('P').length){gameOver=true;showOverlay(false,'我軍全滅…黑暗吞噬了瓦爾哈拉。');return true;}
|
| 481 |
+
if(!alive('E').length){gameOver=true;showOverlay(true,`第 ${turn} 回合,我軍赢得了瓦爾哈拉防衛戰!<br>愛琳: 「這只是開始…帝國的命運就掌握在我們手中!」<br><small>― 第一關・完 ―</small>`);return true;}
|
| 482 |
+
return false;
|
| 483 |
+
}
|
| 484 |
+
function showOverlay(win,text){
|
| 485 |
+
win?sWin():sLose();
|
| 486 |
+
document.getElementById('ovTitle').textContent=win?'🏆 勝利!':'💀 戰敗…';
|
| 487 |
+
document.getElementById('ovText').innerHTML=text;
|
| 488 |
+
document.getElementById('overlay').classList.remove('hidden');
|
| 489 |
+
}
|
| 490 |
+
document.getElementById('ovBtn').onclick=()=>{document.getElementById('overlay').classList.add('hidden');init();};
|
| 491 |
+
document.getElementById('restart').onclick=()=>{if(confirm('重新開始第一關?'))init();};
|
| 492 |
+
document.getElementById('howto').onclick=()=>alert('【玩法】\n1. 點我軍(藍點)→ 藍格移動\n2. 移動後選 攻擊/治療/待機\n3. 紅格是可攻擊的敵人,綠格是可治療的友軍\n4. 全部行動完自動換敵方回合\n5. 消滅包含首領在內全部5名敵軍即勝利\n6. 愛琳陣亡或我軍全滅即失敗\n\n森林/城砦有防禦加成,魔法師無視部分防禦。');
|
| 493 |
+
|
| 494 |
+
// ============ 劇情 ============
|
| 495 |
+
const STORY=[
|
| 496 |
+
"聖曆312年,瓦爾哈拉帝國——一個由女戰士守護的光之國度。",
|
| 497 |
+
"北方死靈大軍越過冰封隘口,屍龍術士「莫戈」率領骷髏與哥布林先鋒,兵臨城下。",
|
| 498 |
+
"女王近衛・愛琳:「姊妹們!城牆之後就是我們的家園,一步也不能退!」",
|
| 499 |
+
"弓箭手莉亞、魔法師米雅、見習僧侶珍妮已集結完畢——第一關:瓦爾哈拉防衛戰,開戰!",
|
| 500 |
+
];
|
| 501 |
+
let si=0;
|
| 502 |
+
function showStory(){
|
| 503 |
+
document.getElementById('storyText').textContent=STORY[si];
|
| 504 |
+
document.getElementById('nextStory').textContent=(si===STORY.length-1?'開戰!⚔️':'繼續 ▸');
|
| 505 |
+
}
|
| 506 |
+
document.getElementById('nextStory').onclick=()=>{
|
| 507 |
+
beep(660,.08);
|
| 508 |
+
if(si<STORY.length-1){si++;showStory();}
|
| 509 |
+
else{document.getElementById('story').style.display='none';log('📜 劇情結束,戰鬥開始!');}
|
| 510 |
+
};
|
| 511 |
+
document.getElementById('skipStory').onclick=()=>{document.getElementById('story').style.display='none';};
|
| 512 |
+
showStory();
|
| 513 |
+
init();
|
| 514 |
+
</script>
|
| 515 |
+
</body>
|
| 516 |
+
</html>
|